AdaptiveTactics / tests /test_rules.js
EphAsad's picture
Upload 61 files
076c3cb verified
/**
* 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 };