Files
Battle-for-Tismo/tests/orders_test.js
T

141 lines
5.8 KiB
JavaScript

import { TestCase } from "./framework/test_case.js";
import { smallState } from "./framework/helpers.js";
// Right-click attack and Ctrl-right-click scheduled movement, exercised on the
// authoritative GameState.
export class AttackOrderTest extends TestCase {
test_attack_is_rejected_without_a_target() {
const state = smallState();
const unit = state.units.find((u) => u.civ === 0);
const goal = neutralLine(state, 1)[0];
this.assertFalse(state.requestAttack([unit.id], goal), "an empty tile is not an attack");
this.assertFalse(state.requestAttack([99999], goal), "unknown units are rejected");
}
test_attack_marches_onto_the_enemy_and_joins_battle() {
const state = smallState();
const pair = emptyLandPair(state);
const attacker = state._spawnUnit(pair.a, 0, state.protoUnits[0]);
const defender = state._spawnUnit(pair.b, 1, state.protoUnits[0]);
this.assertTrue(state.requestAttack([attacker.id], pair.b), "the enemy stack is a target");
state.advanceMovement(1000);
this.assertEqual(attacker.coords, pair.b, "the attacker charges onto the enemy tile");
this.assertEqual(state._combatTarget(attacker), defender, "the two stacks are fighting");
}
test_attack_targets_an_enemy_city() {
const state = smallState();
const city = state.cities.find((c) => c.civ === 1);
const launch = emptyLandNeighbour(state, city.coords);
this.assertNotNull(launch, "found a staging tile by the enemy city");
const attacker = state._spawnUnit(launch, 0, state.protoUnits[0]);
this.assertTrue(state.requestAttack([attacker.id], city.coords), "the enemy city is a target");
}
test_attack_becomes_a_move_when_the_enemy_has_left() {
const state = smallState();
const pair = emptyLandPair(state);
const attacker = state._spawnUnit(pair.a, 0, state.protoUnits[0]);
const defender = state._spawnUnit(pair.b, 1, state.protoUnits[0]);
this.assertTrue(state.requestAttack([attacker.id], pair.b));
// The defender slips away before the attacker arrives.
state._destroyUnit(defender);
state.advanceMovement(1000);
this.assertEqual(attacker.coords, pair.b, "the order is simply a move to the tile");
this.assertNull(state._combatTarget(attacker), "there is no fight to join");
}
}
export class ScheduledOrderTest extends TestCase {
test_scheduled_move_visits_each_destination_in_turn() {
const state = smallState();
const unit = state.units.find((u) => u.civ === 0);
const [first, second] = neutralLine(state, 2);
this.assertTrue(state.requestScheduledMove([unit.id], [first, second]));
this.assertSize(unit.waypoints, 1, "the second destination stays queued");
this.assertEqual(unit.path[unit.path.length - 1], first, "the first leg heads to the first stop");
state.advanceMovement(1000);
this.assertEqual(unit.coords, second, "the unit carries on to the second stop");
this.assertEmpty(unit.waypoints);
}
test_scheduled_move_can_append_a_destination() {
const state = smallState();
const unit = state.units.find((u) => u.civ === 0);
const [first, second] = neutralLine(state, 2);
this.assertTrue(state.requestScheduledMove([unit.id], [first]));
this.assertTrue(state.requestScheduledMove([unit.id], [second], true));
this.assertSize(unit.waypoints, 1, "the appended stop joins the queue");
state.advanceMovement(1000);
this.assertEqual(unit.coords, second);
}
test_each_leg_is_routed_from_the_previous_destination() {
const state = smallState();
const unit = state.units.find((u) => u.civ === 0);
const [first, second] = neutralLine(state, 2);
state.requestScheduledMove([unit.id], [first, second]);
// Place the unit on the first stop with no partial step; the next tick must
// plan a fresh route from there rather than reuse the original one.
unit.coords = first;
unit.path = [first];
unit.pathIndex = 0;
unit.progressHours = 0;
state.advanceMovement(1000);
this.assertEqual(unit.coords, second, "a new path is computed from the stop");
}
test_replacing_a_schedule_drops_the_old_queue() {
const state = smallState();
const unit = state.units.find((u) => u.civ === 0);
const [first, second] = neutralLine(state, 2);
state.requestScheduledMove([unit.id], [first, second]);
this.assertTrue(state.requestScheduledMove([unit.id], [first]));
this.assertEmpty(unit.waypoints, "a plain scheduled move replaces the queue");
}
}
// `steps` empty land tiles in a line, returned without the start tile.
function neutralLine(state, steps) {
for (const start of state.landCells) {
if (state.unitAt(start) || state.cityAt(start)) continue;
const path = [start];
let current = start;
for (let i = 0; i < steps; i++) {
const next = state.topology.neighbours(current.x, current.y).find((n) =>
state._isLand(n) &&
!state.unitAt(n) &&
!state.cityAt(n) &&
!path.some((p) => p.x === n.x && p.y === n.y)
);
if (!next) break;
path.push(next);
current = next;
}
if (path.length === steps + 1) return path.slice(1);
}
throw new Error(`no empty line of ${steps} tiles`);
}
function emptyLandPair(state) {
for (const cell of state.landCells) {
if (state.unitAt(cell) || state.cityAt(cell)) continue;
for (const neighbour of state.topology.neighbours(cell.x, cell.y)) {
if (!state._isLand(neighbour)) continue;
if (state.unitAt(neighbour) || state.cityAt(neighbour)) continue;
return { a: { x: cell.x, y: cell.y }, b: { x: neighbour.x, y: neighbour.y } };
}
}
throw new Error("no empty land pair");
}
function emptyLandNeighbour(state, coords) {
for (const neighbour of state.topology.neighbours(coords.x, coords.y)) {
if (!state._isLand(neighbour)) continue;
if (state.unitAt(neighbour) || state.cityAt(neighbour)) continue;
return { x: neighbour.x, y: neighbour.y };
}
return null;
}