141 lines
5.8 KiB
JavaScript
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;
|
|
}
|