Spaces:
Running
Running
| /** | |
| * Adaptive Tactics - Combat Tests | |
| * Tests for combat execution, healing, death handling | |
| */ | |
| import { Unit, Weapon, MapData } from '../js/models.js'; | |
| import { executeCombat, executeHeal, CombatResult, generateCombatLog } from '../js/combat.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: 100, // 100% hit for deterministic tests | |
| minRange: 1, | |
| maxRange: 1, | |
| heal: 0, | |
| ...overrides | |
| }); | |
| } | |
| function createTestUnit(id, faction, weaponOverrides = {}) { | |
| const weapon = createTestWeapon(weaponOverrides); | |
| return new Unit({ | |
| id: id, | |
| name: `Test ${id}`, | |
| sprite: 'test.png', | |
| faction: faction, | |
| stats: { | |
| hp: 30, | |
| maxHp: 30, | |
| power: 10, | |
| skill: 15, | |
| speed: 10, | |
| def: 5, | |
| res: 5, | |
| mov: 5 | |
| }, | |
| weapons: [weapon.id], | |
| equippedWeapon: weapon.id | |
| }, { [weapon.id]: weapon }); | |
| } | |
| function createTestMap() { | |
| const mapData = new MapData({ | |
| id: 'test_map', | |
| name: 'Test Map', | |
| rows: 5, | |
| cols: 5, | |
| tiles: { | |
| blocked: [], | |
| bush: [], | |
| throne: [] | |
| }, | |
| player_spawn: {}, | |
| enemy_layout: {}, | |
| flex_nodes: {}, | |
| reinforcement_entries: {} | |
| }); | |
| mapData.initTiles(); | |
| return mapData; | |
| } | |
| const testGameRules = { | |
| double_speed_gap: 4 | |
| }; | |
| // Test: Basic Combat Exchange | |
| function testBasicCombat() { | |
| const map = createTestMap(); | |
| const attacker = createTestUnit('attacker', 'player', { hit: 100 }); | |
| const defender = createTestUnit('defender', 'enemy', { hit: 100 }); | |
| attacker.power = 12; | |
| attacker.setPosition(2, 2); | |
| defender.def = 5; | |
| defender.setPosition(2, 3); | |
| map.getTile(2, 2).placeUnit(attacker); | |
| map.getTile(2, 3).placeUnit(defender); | |
| const result = executeCombat(attacker, defender, map, testGameRules); | |
| // Damage: 12 + 5 - 5 = 12 | |
| assert(result.attacks.length >= 1, 'Should have at least 1 attack'); | |
| assert(result.attacks[0].damage === 12, `First attack should deal 12 damage, got ${result.attacks[0].damage}`); | |
| assert(defender.hp === 18, `Defender should have 18 HP remaining, got ${defender.hp}`); | |
| } | |
| // Test: Counter Attack | |
| function testCounterAttack() { | |
| const map = createTestMap(); | |
| const attacker = createTestUnit('attacker', 'player', { hit: 100 }); | |
| const defender = createTestUnit('defender', 'enemy', { hit: 100 }); | |
| attacker.power = 10; | |
| defender.power = 8; | |
| attacker.setPosition(2, 2); | |
| defender.setPosition(2, 3); | |
| map.getTile(2, 2).placeUnit(attacker); | |
| map.getTile(2, 3).placeUnit(defender); | |
| const result = executeCombat(attacker, defender, map, testGameRules); | |
| // Both should have acted | |
| const attackerAttacks = result.attacks.filter(a => a.attackerId === 'attacker'); | |
| const defenderCounters = result.attacks.filter(a => a.attackerId === 'defender' && a.isCounter); | |
| assert(attackerAttacks.length >= 1, 'Attacker should have attacked'); | |
| assert(defenderCounters.length >= 1, 'Defender should have countered'); | |
| } | |
| // Test: No Counter at Range 2 with Sword | |
| function testNoCounterAtRange2() { | |
| const map = createTestMap(); | |
| const attacker = createTestUnit('attacker', 'player', { | |
| id: 'spear', | |
| minRange: 1, | |
| maxRange: 2, | |
| hit: 100 | |
| }); | |
| const defender = createTestUnit('defender', 'enemy', { | |
| id: 'sword', | |
| minRange: 1, | |
| maxRange: 1, | |
| hit: 100 | |
| }); | |
| attacker.setPosition(2, 2); | |
| defender.setPosition(2, 4); // 2 tiles away | |
| map.getTile(2, 2).placeUnit(attacker); | |
| map.getTile(2, 4).placeUnit(defender); | |
| const result = executeCombat(attacker, defender, map, testGameRules); | |
| const defenderCounters = result.attacks.filter(a => a.attackerId === 'defender'); | |
| assert(defenderCounters.length === 0, 'Sword defender should not counter at range 2'); | |
| } | |
| // Test: Doubling | |
| function testDoubling() { | |
| const map = createTestMap(); | |
| const attacker = createTestUnit('attacker', 'player', { hit: 100 }); | |
| const defender = createTestUnit('defender', 'enemy', { hit: 100 }); | |
| attacker.speed = 15; | |
| defender.speed = 10; // 5 speed difference, should double | |
| attacker.setPosition(2, 2); | |
| defender.setPosition(2, 3); | |
| map.getTile(2, 2).placeUnit(attacker); | |
| map.getTile(2, 3).placeUnit(defender); | |
| const result = executeCombat(attacker, defender, map, testGameRules); | |
| const attackerAttacks = result.attacks.filter(a => a.attackerId === 'attacker'); | |
| assert(attackerAttacks.length === 2, `Attacker should attack twice (double), got ${attackerAttacks.length}`); | |
| } | |
| // Test: Death During Combat | |
| function testDeathDuringCombat() { | |
| const map = createTestMap(); | |
| const attacker = createTestUnit('attacker', 'player', { hit: 100, might: 30 }); | |
| const defender = createTestUnit('defender', 'enemy', { hit: 100 }); | |
| attacker.power = 30; // Will one-shot | |
| defender.hp = 20; | |
| defender.maxHp = 20; | |
| attacker.setPosition(2, 2); | |
| defender.setPosition(2, 3); | |
| map.getTile(2, 2).placeUnit(attacker); | |
| map.getTile(2, 3).placeUnit(defender); | |
| const result = executeCombat(attacker, defender, map, testGameRules); | |
| assert(result.defenderDied === true, 'Defender should be marked as died'); | |
| assert(defender.alive === false, 'Defender should be dead'); | |
| assert(defender.hp === 0, 'Defender HP should be 0'); | |
| } | |
| // Test: Defender Dies Before Counter | |
| function testDefenderDiesBeforeCounter() { | |
| const map = createTestMap(); | |
| const attacker = createTestUnit('attacker', 'player', { hit: 100 }); | |
| const defender = createTestUnit('defender', 'enemy', { hit: 100 }); | |
| attacker.power = 30; | |
| defender.hp = 10; | |
| defender.maxHp = 10; | |
| defender.power = 20; // Would deal big damage if alive | |
| attacker.setPosition(2, 2); | |
| defender.setPosition(2, 3); | |
| map.getTile(2, 2).placeUnit(attacker); | |
| map.getTile(2, 3).placeUnit(defender); | |
| const result = executeCombat(attacker, defender, map, testGameRules); | |
| const defenderCounters = result.attacks.filter(a => a.attackerId === 'defender'); | |
| assert(defenderCounters.length === 0, 'Dead defender should not counter'); | |
| assert(attacker.hp === 30, 'Attacker should be at full HP'); | |
| } | |
| // Test: Healing | |
| function testHealing() { | |
| const map = createTestMap(); | |
| const healWeapon = new Weapon({ | |
| id: 'staff', | |
| name: 'Heal Staff', | |
| kind: 'heal', | |
| might: 0, | |
| hit: 100, | |
| minRange: 1, | |
| maxRange: 1, | |
| heal: 6 | |
| }); | |
| const healer = new Unit({ | |
| id: 'healer', | |
| name: 'Healer', | |
| sprite: 'test.png', | |
| faction: 'player', | |
| stats: { hp: 20, maxHp: 20, power: 5, skill: 10, speed: 8, def: 3, res: 10, mov: 4 }, | |
| weapons: ['staff'], | |
| equippedWeapon: 'staff' | |
| }, { staff: healWeapon }); | |
| const target = createTestUnit('target', 'player'); | |
| target.hp = 15; | |
| target.maxHp = 30; | |
| healer.setPosition(2, 2); | |
| target.setPosition(2, 3); | |
| map.getTile(2, 2).placeUnit(healer); | |
| map.getTile(2, 3).placeUnit(target); | |
| const result = executeHeal(healer, target, map); | |
| assert(result.success === true, 'Heal should succeed'); | |
| assert(result.amount === 6, `Should heal for 6, got ${result.amount}`); | |
| assert(target.hp === 21, `Target should have 21 HP, got ${target.hp}`); | |
| } | |
| // Test: Overheal Capped at MaxHP | |
| function testOverhealCapped() { | |
| const map = createTestMap(); | |
| const healWeapon = new Weapon({ | |
| id: 'staff', | |
| name: 'Heal Staff', | |
| kind: 'heal', | |
| might: 0, | |
| hit: 100, | |
| minRange: 1, | |
| maxRange: 1, | |
| heal: 10 | |
| }); | |
| const healer = new Unit({ | |
| id: 'healer', | |
| name: 'Healer', | |
| sprite: 'test.png', | |
| faction: 'player', | |
| stats: { hp: 20, maxHp: 20, power: 5, skill: 10, speed: 8, def: 3, res: 10, mov: 4 }, | |
| weapons: ['staff'], | |
| equippedWeapon: 'staff' | |
| }, { staff: healWeapon }); | |
| const target = createTestUnit('target', 'player'); | |
| target.hp = 28; | |
| target.maxHp = 30; | |
| healer.setPosition(2, 2); | |
| target.setPosition(2, 3); | |
| map.getTile(2, 2).placeUnit(healer); | |
| map.getTile(2, 3).placeUnit(target); | |
| const result = executeHeal(healer, target, map); | |
| assert(result.success === true, 'Heal should succeed'); | |
| assert(result.amount === 2, `Should only heal 2 (to cap), got ${result.amount}`); | |
| assert(target.hp === 30, `Target should be at max HP 30, got ${target.hp}`); | |
| } | |
| // Test: Combat Log Generation | |
| function testCombatLogGeneration() { | |
| const result = new CombatResult(); | |
| result.addAttack( | |
| { id: 'a', name: 'Attacker' }, | |
| { id: 'd', name: 'Defender' }, | |
| true, | |
| 12, | |
| false | |
| ); | |
| result.addAttack( | |
| { id: 'd', name: 'Defender' }, | |
| { id: 'a', name: 'Attacker' }, | |
| false, | |
| 0, | |
| true | |
| ); | |
| const logs = generateCombatLog(result); | |
| assert(logs.length === 2, `Should have 2 log entries, got ${logs.length}`); | |
| assert(logs[0].text.includes('hit') && logs[0].text.includes('12'), 'First log should mention hit and damage'); | |
| assert(logs[1].text.includes('missed'), 'Second log should mention miss'); | |
| } | |
| // Run all tests | |
| function runAllTests() { | |
| console.log('=== Running Combat Tests ===\n'); | |
| try { | |
| testBasicCombat(); | |
| testCounterAttack(); | |
| testNoCounterAtRange2(); | |
| testDoubling(); | |
| testDeathDuringCombat(); | |
| testDefenderDiesBeforeCounter(); | |
| testHealing(); | |
| testOverhealCapped(); | |
| testCombatLogGeneration(); | |
| console.log('\n=== All Combat 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.runCombatTests = runAllTests; | |
| } | |
| export { runAllTests }; | |