import assert from 'node:assert/strict'; import test from 'node:test'; import { applyAction, viewGame, type Action, type Game } from '../game/engine'; import { botActions } from '../game/bots'; import type { Difficulty } from '../game/bot-profiles'; import { actNoFieldAlliedSeparation as act, createNoFieldAlliedSeparationFixture as fixture, noFieldAlliedSeparationInventory as inventory, noFieldAlliedSeparationPlayer as p, noFieldAlliedSeparationReload as reload, finishNoFieldAlliedSeparationMovement as finish, noFieldAlliedSeparationClean as clean, nextNoFieldAlliedSeparation as next } from './fixture-no-field-allied-separation'; function reject(game: Game, actor: string, action: Action) { const before = reload(game); assert.throws(() => applyAction(game, actor, action)); assert.deepEqual(game, before); } function assertDueLoss(f: ReturnType, game: Game) { const before = p(f.game, f.actor), after = p(game, f.actor), amount = Math.min(f.value, before.reserves); assert.equal(after.noField!.deployed, null); assert.notEqual(after.noFieldEvent, f.event); assert.deepEqual(after.noField!.tokens, f.marker!.tokens); assert.equal(after.noField!.lastShipped, f.token); assert.equal(after.reserves, before.reserves - amount); assert.equal(after.tanks, before.tanks + amount); assert.deepEqual(after.forces, {}); assert.equal(p(game, f.ally).forces['imperial_basin:11'], 2); assert.equal(p(game, f.ally).tanks, 0); assert.equal(p(game, f.ally).ally, f.actor); assert.equal(after.ally, f.ally); assert.equal((game.movementRemaining ?? []).includes(f.actor), false); assert.deepEqual(inventory(game), f.original); } void test('genuine protected Basin marker and army form a real next-worm alliance without premature revelation', () => { const f = fixture(); assert.equal(f.initial.status, 'lobby'); assert.equal(f.setup.status, 'setup'); assert.equal(f.sourceCounts.setup, f.setupActions.length); assert.equal(f.sourceCounts.source, f.sourceActions.length); assert.equal(f.sourceCounts.markerShipment, f.markerShipment.steps.length); assert.equal(f.sourceCounts.allyShipment, f.allyShipment.steps.length); assert.equal(f.sourceCounts.alliance, f.alliance.steps.length); assert.deepEqual(f.firstDials.steps.map(step => step.action.amount), [4, 5]); assert.equal(p(f.markerShipment.before, f.actor).spice - p(f.markerShipment.after, f.actor).spice, 2); assert.equal(p(f.allyShipment.before, f.ally).spice - p(f.allyShipment.after, f.ally).spice, 4); assert.deepEqual(f.alliance.steps.map(step => [step.actor, step.action.type, step.action.target]), [[f.actor, 'alliance', f.ally], [f.ally, 'alliance', f.actor]]); assert.equal(f.alliance.before.turn, 2); assert.equal(f.alliance.before.phase, 1); assert.equal(f.alliance.before.nexus, true); assert.equal(viewGame(f.alliance.after, f.actor).advancedAllySeparation, null); assert.deepEqual(p(f.game, f.actor).forces, {}); assert.equal(p(f.game, f.actor).noFieldEvent, f.event); assert.equal(f.game.active, f.actor); assert.deepEqual(viewGame(f.game, f.actor).advancedAllySeparation, { territories: ['Imperial Basin'], noField: true }); assert.equal(viewGame(f.game, f.ally).advancedAllySeparation, null); assert.equal(viewGame(f.game, f.actor).basicAllySeparation, null); assert.deepEqual(inventory(f.game), f.original); }); void test('Advanced due ends reveal zero three and five once and lose only the ending player materialized group', () => { for (const value of [0, 3, 5] as const) { const f = fixture({ value }); let game = act(reload(f.game), f.actor, f.action); assertDueLoss(f, game); const revealedEvent = p(game, f.actor).noFieldEvent; reject(game, f.actor, f.action); assert.equal(viewGame(game, f.actor).advancedAllySeparation, null); game = finish(game); assert.equal(game.phase, 7); assert.equal(p(game, f.actor).tanks, value); assert.equal(p(game, f.actor).noFieldEvent, revealedEvent); assert.equal(p(game, f.ally).forces['imperial_basin:11'], 2); assert.deepEqual(inventory(game), f.original); } }); void test('Basic actual new-alliance turn ends preserve the concealed marker and its ally formation grace', () => { const f = fixture({ advanced: false, value: 5 }); assert.ok(f.formationEnds); const before = f.formationMovement; assert.equal(before.turn, 2); assert.equal(before.active, f.ally); assert.equal(viewGame(before, f.ally).basicAllySeparation, null); assert.equal(viewGame(before, f.actor).basicAllySeparation, null); const game = finish(reload(before)); assert.equal(game.phase, 7); assert.deepEqual(p(game, f.actor).noField, p(before, f.actor).noField); assert.equal(p(game, f.actor).noFieldEvent, f.event); assert.equal(p(game, f.actor).tanks, 0); assert.equal(p(game, f.ally).forces['imperial_basin:11'], 2); assert.equal(p(game, f.ally).tanks, 0); assert.equal(f.formationEnds.after.phase, 7); assert.deepEqual(inventory(game), f.original); }); void test('Basic later-player deadline follows an actual earlier ally end and then destroys only the due marker group', () => { for (const value of [0, 3, 5] as const) { const f = fixture({ advanced: false, value }); assert.equal(f.beforeEarlierEnd.turn, 3); assert.equal(f.beforeEarlierEnd.active, f.ally); assert.equal(viewGame(f.beforeEarlierEnd, f.ally).basicAllySeparation, null); assert.ok(f.earlierEnds.some(step => step.actor === f.ally && step.action.type === 'endMovement')); assert.equal(p(f.game, f.actor).noFieldEvent, f.event); assert.equal(p(f.game, f.ally).forces['imperial_basin:11'], 2); assert.equal((f.game.movementRemaining ?? []).includes(f.ally), false); assert.deepEqual(viewGame(f.game, f.actor).basicAllySeparation, { territories: ['Imperial Basin'], noField: true }); assert.equal(viewGame(f.game, f.actor).advancedAllySeparation, null); const game = act(reload(f.game), f.actor, f.action); assertDueLoss(f, game); assert.equal(viewGame(game, f.actor).basicAllySeparation, null); } }); void test('native voluntary reveal remains an alternative before the same due physical separation without a second reveal', () => { const f = fixture({ value: 3 }); let game = act(reload(f.game), f.actor, { type: 'revealNoField', event: f.event, token: f.token }); const event = p(game, f.actor).noFieldEvent; assert.notEqual(event, f.event); assert.equal(p(game, f.actor).noField!.deployed, null); assert.equal(p(game, f.actor).forces['imperial_basin:9'], 3); assert.equal(p(game, f.actor).tanks, 0); assert.deepEqual(viewGame(game, f.actor).advancedAllySeparation, { territories: ['Imperial Basin'] }); game = act(game, f.actor, f.action); assertDueLoss(f, game); assert.equal(p(game, f.actor).noFieldEvent, event); assert.equal(p(game, f.actor).tanks, 3); }); void test('all four minimal profiles legally complete the native deadline by departure, revelation or ending', () => { const f = fixture({ value: 3 }); for (const difficulty of ['Easy', 'Medium', 'Hard', 'Brutal'] satisfies Difficulty[]) { let game = reload(f.game); for (let count = 0; game.phase === 5 && (game.movementRemaining ?? []).includes(f.actor) && count < 60; count++) { if (game.active === f.actor && clean(game)) { const view = viewGame(game, f.actor); view.players.find(player => player.id === f.actor)!.bot = difficulty; const choice = botActions(view)[0]; assert.ok(choice, `${difficulty} has a legal current continuation`); game = act(game, f.actor, choice); } else { const step = next(game); assert.ok(step); game = act(game, step.actor, step.action); } } assert.equal((game.movementRemaining ?? []).includes(f.actor), false, `${difficulty} completes instead of stalling on concealed custody`); assert.equal(p(game, f.ally).forces['imperial_basin:11'], 2); assert.equal(p(game, f.ally).tanks, 0); assert.deepEqual(inventory(game), f.original); game = finish(game); assert.ok(game.phase === 6 || game.phase === 7); assert.equal(p(game, f.ally).forces['imperial_basin:11'], 2); assert.deepEqual(inventory(game), f.original); } });