Apply ruled Grumman return and Cunning placement
This commit is contained in:
1 parent
7994026a70
commit
2efa69d10f
24 files changed
+1246
-219
No files matched your search
@@ -0,0 +1,133 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { applyAction, viewGame, type Action, type Game } from '../game/engine';
|
||||
import { quoteGrummanCollection, quoteGrummanCollectionAction } from '../game/grumman-collection';
|
||||
import { grummanCollectionSignature, validateGrummanCollection } from '../game/grumman-collection-return';
|
||||
import { placeTerror, projectTerror, revealTerror, TERROR_KINDS } from '../game/moritani-terror';
|
||||
import { nextNativeE3HomeworldStep } from './fixture-homeworld-native-e3-modules';
|
||||
import { assertRuledGrummanInventory, createRuledGrummanFixture } from './fixture-ruled-grumman';
|
||||
|
||||
function reject(game: Game, actor: string, action: Action): void {
|
||||
const before = structuredClone(game);
|
||||
assert.throws(() => applyAction(game, actor, action));
|
||||
assert.deepEqual(game, before);
|
||||
}
|
||||
|
||||
void test('native Grumman removal returns the unrevealed face to rotated supply, credits four spice once and preserves Mentat use', () => {
|
||||
for (const advanced of [false, true]) {
|
||||
const f = createRuledGrummanFixture({ advanced });
|
||||
const original = structuredClone(f.game);
|
||||
const prior = f.game.moritaniTerror!;
|
||||
const face = prior.tokens.find(t => t.id === f.placed)!.kind;
|
||||
const owner = f.game.players.find(p => p.id === f.owner)!;
|
||||
const request: Action = { type: 'decision', event: f.game.grummanCollection!.event, mode: 'remove', token: f.placed };
|
||||
reject(f.game, 'guild', request);
|
||||
for (const token of [prior.tokens.find(t => t.status === 'available')!.id, 'missing'])
|
||||
reject(f.game, f.owner, { ...request, token });
|
||||
let game = applyAction(f.game, f.owner, request);
|
||||
assert.deepEqual(f.game, original);
|
||||
assert.equal(game.players.find(p => p.id === f.owner)!.spice, owner.spice + 4);
|
||||
assert.equal(game.grummanCollection!.stage, 'complete');
|
||||
assert.equal(game.grummanCollection!.outcome, 'remove');
|
||||
assert.equal(game.grummanCollection!.token, f.placed);
|
||||
assert.equal(game.grummanCollection!.territory, f.placedTerritory);
|
||||
assert.equal(game.moritaniTerror!.placementTurn, prior.placementTurn);
|
||||
assert.equal(game.moritaniTerror!.supplyEpoch, (prior.supplyEpoch ?? 0) + 1);
|
||||
assert.deepEqual(game.moritaniTerror!.tokens.map(t => t.kind).sort(), [...TERROR_KINDS].sort());
|
||||
assert.ok(game.moritaniTerror!.tokens.every(t => t.status === 'available' && t.location === null));
|
||||
assert.ok(game.moritaniTerror!.tokens.every(t => !prior.tokens.some(old => old.id === t.id)));
|
||||
assert.deepEqual(projectTerror(game.moritaniTerror!, false).tokens, []);
|
||||
assertRuledGrummanInventory(game);
|
||||
validateGrummanCollection(game);
|
||||
reject(game, f.owner, request);
|
||||
reject(game, f.owner, { ...request, mode: 'add', token: game.moritaniTerror!.tokens[0].id, territory: f.placedTerritory });
|
||||
for (let count = 0; game.decision?.kind !== 'moritaniPlacement' && count < 100; count++) {
|
||||
const step = nextNativeE3HomeworldStep(game); assert.ok(step);
|
||||
game = applyAction(game, step.actor, step.action);
|
||||
}
|
||||
assert.equal(game.decision?.kind, 'moritaniPlacement');
|
||||
const returned = game.moritaniTerror!.tokens.find(t => t.kind === face)!;
|
||||
assert.notEqual(returned.id, f.placed);
|
||||
game = applyAction(game, f.owner, { type: 'decision', token: returned.id, territory: 'carthag' });
|
||||
for (let count = 0; game.response && count < 20; count++) {
|
||||
const step = nextNativeE3HomeworldStep(game); assert.ok(step);
|
||||
game = applyAction(game, step.actor, step.action);
|
||||
}
|
||||
assert.equal(game.moritaniTerror!.placementTurn, 2);
|
||||
const redeployed = projectTerror(game.moritaniTerror!, false).tokens.find(t => t.id === returned.id)!;
|
||||
assert.deepEqual(redeployed, { id: returned.id, location: 'carthag', status: 'placed' });
|
||||
assertRuledGrummanInventory(game);
|
||||
assert.ok(!JSON.stringify(viewGame(game, 'guild').moritaniTerror).includes(f.placed));
|
||||
}
|
||||
});
|
||||
|
||||
void test('return quote remains available without ADD supply and preserves other placements and spent faces', () => {
|
||||
const f = createRuledGrummanFixture();
|
||||
const game = structuredClone(f.game);
|
||||
// Pure custody input: all faces are physically placed, so no addition exists.
|
||||
game.moritaniTerror = placeTerror(game.moritaniTerror!, game.moritaniTerror!.tokens.find(t => t.kind === 'sabotage')!.id, 'carthag', 2);
|
||||
for (const token of game.moritaniTerror.tokens)
|
||||
if (token.status === 'available') { token.status = 'placed'; token.location = 'carthag'; }
|
||||
const quote = quoteGrummanCollection(game, f.owner);
|
||||
assert.ok(quote.blocked);
|
||||
assert.equal(quote.removeBlocked, null);
|
||||
assert.equal(quote.removable.length, 6);
|
||||
quote.removable[0].location = 'not-a-location';
|
||||
assert.notEqual(game.moritaniTerror.tokens[0].location, 'not-a-location');
|
||||
const before = structuredClone(game);
|
||||
const result = quoteGrummanCollectionAction(game, f.owner, { mode: 'remove', token: f.placed }, () => 0);
|
||||
assert.equal(result.amount, 4);
|
||||
assert.equal(result.state.placementTurn, 2);
|
||||
assert.deepEqual(result.state.tokens.filter(t => t.status === 'placed'), before.moritaniTerror!.tokens.filter(t => t.id !== f.placed));
|
||||
assert.equal(result.state.tokens.filter(t => t.status === 'available').length, 1);
|
||||
assert.deepEqual(game, before);
|
||||
for (const kind of ['robbery', 'extortion'] as const) {
|
||||
const source = structuredClone(f.game);
|
||||
const selected = source.moritaniTerror!.tokens.find(t => t.kind === kind)!;
|
||||
if (selected.status === 'available') source.moritaniTerror = placeTerror(source.moritaniTerror!, selected.id, 'carthag', 2);
|
||||
source.moritaniTerror = revealTerror(source.moritaniTerror!, selected.id);
|
||||
const invalid = structuredClone(source);
|
||||
assert.throws(() => quoteGrummanCollectionAction(source, f.owner, { mode: 'remove', token: selected.id }, () => 0));
|
||||
assert.deepEqual(source, invalid);
|
||||
if (kind === 'extortion') {
|
||||
const returned = quoteGrummanCollectionAction(source, f.owner, { mode: 'remove', token: f.placed }, () => 0);
|
||||
assert.deepEqual(returned.state.tokens.find(t => t.id === selected.id), source.moritaniTerror.tokens.find(t => t.id === selected.id));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
void test('stale rotated IDs, inactive Grumman, low native population and foreign owners cannot quote a return', () => {
|
||||
const f = createRuledGrummanFixture();
|
||||
const action = { mode: 'remove', token: f.placed } as const;
|
||||
const result = quoteGrummanCollectionAction(f.game, f.owner, action, () => 0);
|
||||
const returned = { ...f.game, moritaniTerror: result.state };
|
||||
assert.throws(() => quoteGrummanCollectionAction(returned, f.owner, action, () => 0));
|
||||
for (const context of [
|
||||
{ ...f.game, homeworlds: undefined },
|
||||
{ ...f.game, players: f.game.players.map(p => p.id === f.owner ? { ...p, reserves: 7 } : p) },
|
||||
]) {
|
||||
const before = structuredClone(context);
|
||||
assert.ok(quoteGrummanCollection(context, f.owner).removeBlocked);
|
||||
assert.deepEqual(quoteGrummanCollection(context, f.owner).removable, []);
|
||||
assert.throws(() => quoteGrummanCollectionAction(context, f.owner, action, () => 0));
|
||||
assert.deepEqual(context, before);
|
||||
}
|
||||
assert.throws(() => quoteGrummanCollectionAction(f.game, 'guild', action, () => 0));
|
||||
assert.throws(() => quoteGrummanCollectionAction(f.game, f.owner, { ...action, token: 'missing' }, () => 0));
|
||||
});
|
||||
|
||||
void test('native removal historical frame binds its original ID and territory without requiring a live token', () => {
|
||||
const f = createRuledGrummanFixture();
|
||||
const game = applyAction(f.game, f.owner, { type: 'decision', event: f.game.grummanCollection!.event, mode: 'remove', token: f.placed });
|
||||
assert.ok(!game.moritaniTerror!.tokens.some(t => t.id === f.placed));
|
||||
validateGrummanCollection(game);
|
||||
for (const changes of [
|
||||
{ territory: 'homeworld:moritani' }, { territory: 'mobile_stronghold' }, { token: undefined },
|
||||
{ nexusEvent: 'unearned' }, { outcome: 'decline' as const },
|
||||
]) {
|
||||
const bad = structuredClone(game);
|
||||
Object.assign(bad.grummanCollection!, changes);
|
||||
bad.grummanCollection!.signature = grummanCollectionSignature(bad.grummanCollection!);
|
||||
assert.throws(() => validateGrummanCollection(bad));
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user