/** * Adaptive Tactics - Rules Tests * Tests for movement, attack range, hit/damage calculations */ import { Unit, Weapon, Tile, MapData } from '../js/models.js'; import { calculateMoveRange, calculateAttackRange, calculateHealRange, calculateDisplayedHit, calculateDisplayedAvoid, calculateHitChance, calculateDamage, canDouble, canCounter, rollHit } from '../js/rules.js'; // Test utilities function assert(condition, message) { if (!condition) { throw new Error(`FAILED: ${message}`); } console.log(`PASSED: ${message}`); } function createTestWeapon(overrides = {}) { return new Weapon({ id: 'test_sword', name: 'Test Sword', kind: 'physical', might: 5, hit: 90, minRange: 1, maxRange: 1, heal: 0, ...overrides }); } function createTestUnit(overrides = {}) { const weapons = { test_sword: createTestWeapon() }; return new Unit({ id: 'test_unit', name: 'Test Unit', sprite: 'test.png', faction: 'player', stats: { hp: 30, maxHp: 30, power: 10, skill: 10, speed: 10, def: 5, res: 5, mov: 5 }, weapons: ['test_sword'], equippedWeapon: 'test_sword', ...overrides }, weapons); } function createTestMap() { const mapData = new MapData({ id: 'test_map', name: 'Test Map', rows: 5, cols: 5, tiles: { blocked: [[3, 3]], bush: [[2, 2]], throne: [] }, player_spawn: {}, enemy_layout: {}, flex_nodes: {}, reinforcement_entries: {} }); mapData.initTiles(); return mapData; } // Test: Displayed Hit Calculation function testDisplayedHit() { const weapon = createTestWeapon({ hit: 80 }); const unit = createTestUnit(); unit.skill = 12; const displayedHit = calculateDisplayedHit(unit, weapon); // Formula: weapon_hit + skill × 2 = 80 + 12 × 2 = 104 assert(displayedHit === 104, `Displayed hit should be 104, got ${displayedHit}`); } // Test: Displayed Avoid Calculation function testDisplayedAvoid() { const unit = createTestUnit(); unit.speed = 15; const avoidNoTerrain = calculateDisplayedAvoid(unit, 0); // Formula: speed × 2 = 15 × 2 = 30 assert(avoidNoTerrain === 30, `Avoid without terrain should be 30, got ${avoidNoTerrain}`); const avoidWithBush = calculateDisplayedAvoid(unit, 10); // Formula: speed × 2 + terrain = 30 + 10 = 40 assert(avoidWithBush === 40, `Avoid with bush should be 40, got ${avoidWithBush}`); } // Test: Hit Chance Calculation function testHitChance() { const weapon = createTestWeapon({ hit: 90 }); const attacker = createTestUnit(); attacker.skill = 10; const defender = createTestUnit(); defender.speed = 12; const hitChance = calculateHitChance(attacker, defender, weapon, 0); // Attacker hit: 90 + 10 × 2 = 110 // Defender avoid: 12 × 2 = 24 // Hit chance: 110 - 24 = 86 assert(hitChance === 86, `Hit chance should be 86, got ${hitChance}`); } // Test: Damage Calculation function testDamageCalculation() { const weapon = createTestWeapon({ might: 6, kind: 'physical' }); const attacker = createTestUnit(); attacker.power = 14; const defender = createTestUnit(); defender.def = 8; const damage = calculateDamage(attacker, defender, weapon, 0); // Formula: power + might - def = 14 + 6 - 8 = 12 assert(damage === 12, `Physical damage should be 12, got ${damage}`); // Test magic damage const magicWeapon = createTestWeapon({ might: 4, kind: 'magic' }); defender.res = 5; const magicDamage = calculateDamage(attacker, defender, magicWeapon, 0); // Formula: power + might - res = 14 + 4 - 5 = 13 assert(magicDamage === 13, `Magic damage should be 13, got ${magicDamage}`); } // Test: Minimum Damage function testMinimumDamage() { const weapon = createTestWeapon({ might: 2 }); const attacker = createTestUnit(); attacker.power = 5; const defender = createTestUnit(); defender.def = 20; // Very high defense const damage = calculateDamage(attacker, defender, weapon, 0); // Formula: 5 + 2 - 20 = -13, but minimum is 0 assert(damage === 0, `Damage should be minimum 0, got ${damage}`); } // Test: Doubling function testDoubling() { const attacker = createTestUnit(); const defender = createTestUnit(); attacker.speed = 14; defender.speed = 10; assert(canDouble(attacker, defender, 4) === true, 'Should double with 4 speed advantage'); attacker.speed = 13; assert(canDouble(attacker, defender, 4) === false, 'Should not double with 3 speed advantage'); attacker.speed = 14; defender.speed = 11; assert(canDouble(attacker, defender, 4) === false, 'Should not double with only 3 speed gap'); } // Test: Counter Attack function testCounterAttack() { const attacker = createTestUnit(); const swordDefender = createTestUnit(); // Sword can counter at range 1 assert(canCounter(attacker, swordDefender, 1) === true, 'Sword should counter at range 1'); assert(canCounter(attacker, swordDefender, 2) === false, 'Sword should not counter at range 2'); // Spear can counter at range 1 and 2 const spearWeapon = createTestWeapon({ id: 'spear', minRange: 1, maxRange: 2 }); const spearDefender = new Unit({ id: 'spear_unit', name: 'Spear Unit', sprite: 'test.png', faction: 'enemy', stats: { hp: 20, power: 8, skill: 8, speed: 8, def: 4, res: 4, mov: 5 }, weapons: ['spear'], equippedWeapon: 'spear' }, { spear: spearWeapon }); assert(canCounter(attacker, spearDefender, 1) === true, 'Spear should counter at range 1'); assert(canCounter(attacker, spearDefender, 2) === true, 'Spear should counter at range 2'); } // Test: Bow Cannot Counter Melee function testBowCannotCounterMelee() { const attacker = createTestUnit(); const bowWeapon = createTestWeapon({ id: 'bow', minRange: 2, maxRange: 2 }); const bowDefender = new Unit({ id: 'bow_unit', name: 'Bow Unit', sprite: 'test.png', faction: 'enemy', stats: { hp: 20, power: 8, skill: 10, speed: 8, def: 3, res: 3, mov: 5 }, weapons: ['bow'], equippedWeapon: 'bow' }, { bow: bowWeapon }); assert(canCounter(attacker, bowDefender, 1) === false, 'Bow should not counter at range 1'); assert(canCounter(attacker, bowDefender, 2) === true, 'Bow should counter at range 2'); } // Test: 2RN Roll Distribution function test2RNDistribution() { // Run many rolls to verify 2RN system produces reasonable distribution let hits = 0; const trials = 10000; const hitChance = 75; for (let i = 0; i < trials; i++) { if (rollHit(hitChance)) hits++; } const actualRate = (hits / trials) * 100; // With 2RN, displayed 75% should hit more often (around 81%) // Allow 5% margin for statistical variance assert(actualRate > 70 && actualRate < 90, `2RN hit rate for 75% should be elevated, got ${actualRate.toFixed(1)}%`); } // Test: Terrain Defense Bonus function testTerrainDefenseBonus() { const weapon = createTestWeapon({ might: 8 }); const attacker = createTestUnit(); attacker.power = 12; const defender = createTestUnit(); defender.def = 6; // Without terrain const damageNoBush = calculateDamage(attacker, defender, weapon, 0); // 12 + 8 - 6 = 14 assert(damageNoBush === 14, `Damage without terrain should be 14, got ${damageNoBush}`); // With bush (+1 def) const damageWithBush = calculateDamage(attacker, defender, weapon, 1); // 12 + 8 - (6 + 1) = 13 assert(damageWithBush === 13, `Damage with bush should be 13, got ${damageWithBush}`); // With throne (+2 def) const damageWithThrone = calculateDamage(attacker, defender, weapon, 2); // 12 + 8 - (6 + 2) = 12 assert(damageWithThrone === 12, `Damage with throne should be 12, got ${damageWithThrone}`); } // Run all tests function runAllTests() { console.log('=== Running Rules Tests ===\n'); try { testDisplayedHit(); testDisplayedAvoid(); testHitChance(); testDamageCalculation(); testMinimumDamage(); testDoubling(); testCounterAttack(); testBowCannotCounterMelee(); test2RNDistribution(); testTerrainDefenseBonus(); console.log('\n=== All Rules Tests Passed ==='); } catch (e) { console.error('\n=== Test Failed ==='); console.error(e.message); throw e; } } // Export for use in browser or Node if (typeof window !== 'undefined') { window.runRulesTests = runAllTests; } export { runAllTests };