Spaces:
Running
Running
| /** | |
| * 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 }; | |