Resolve definite Orgiz payers and prove native shared Collection
This commit is contained in:
1 parent
388c5fa8d5
commit
bc09ac3cfd
11 files changed
+310
-292
No files matched your search
@@ -513,26 +513,50 @@ void test('Advanced counts each territory once across positive partial sector co
|
||||
}
|
||||
});
|
||||
|
||||
void test('Advanced withholds a territory with distinct collected payers, including an own collection, but not independent theft', () => {
|
||||
void test('Advanced selects one actual foreign territory collector in storm order, skipping own collections', () => {
|
||||
for (const second of ['a', 'g']) {
|
||||
const game = fixture(true);
|
||||
reveal(game, ORGIZ_PROCESSING_STATION);
|
||||
game.players[0].forces = { [`${ORGIZ_PROCESSING_STATION}:0`]: 1 };
|
||||
game.players[1].forces = { 'hagga_basin:12': 1, 'red_chasm:7': 1 };
|
||||
game.players.find(player => player.id === second)!.forces['hagga_basin:13'] = 1;
|
||||
game.order = ['a', 'g', 'h'];
|
||||
game.spice = { 'hagga_basin:12': 3, 'hagga_basin:13': 2, 'red_chasm:7': 2 };
|
||||
const { ordinary, discovery } = composed(game);
|
||||
const payer = second === 'a' ? 'h' : 'g';
|
||||
assert.deepEqual(discovery.effects, [{
|
||||
kind: 'orgiz', player: 'a', from: payer,
|
||||
location: second === 'a' ? 'hagga_basin:12' : 'hagga_basin:13',
|
||||
amount: 1, source: 'player',
|
||||
}, {
|
||||
kind: 'orgiz', player: 'a', from: 'h', location: 'red_chasm:7',
|
||||
amount: 1, source: 'player',
|
||||
}]);
|
||||
assert.equal(balance(discovery, 'a'), ordinary.receipts[0].balance + 1);
|
||||
assert.equal(balance(discovery, 'h'), ordinary.receipts[1].balance - 1);
|
||||
assert.equal(balance(discovery, 'g'), ordinary.receipts[2].balance);
|
||||
assert.equal(balance(discovery, 'a'), ordinary.receipts[0].balance + 2);
|
||||
assert.equal(balance(discovery, 'h'), ordinary.receipts.find(r => r.player === 'h')!.balance - (payer === 'h' ? 2 : 1));
|
||||
assert.equal(balance(discovery, 'g'), ordinary.receipts.find(r => r.player === 'g')!.balance - (payer === 'g' ? 1 : 0));
|
||||
assert.equal(ordinary.spice['hagga_basin:13'], 0);
|
||||
}
|
||||
});
|
||||
|
||||
void test('Advanced falls through to the next actual collector when an earlier Orgiz owner exhausts the first purse', () => {
|
||||
const game = fixture(true, [['a', 'atreides'], ['h', 'harkonnen'], ['g', 'guild'], ['e', 'emperor']]);
|
||||
reveal(game, ORGIZ_PROCESSING_STATION);
|
||||
game.order = ['a', 'g', 'h', 'e'];
|
||||
game.players.find(p => p.id === 'a')!.forces = { [`${ORGIZ_PROCESSING_STATION}:0`]: 1 };
|
||||
game.players.find(p => p.id === 'g')!.forces = { [`${ORGIZ_PROCESSING_STATION}:0`]: 1 };
|
||||
game.players.find(p => p.id === 'h')!.forces = { 'hagga_basin:12': 1 };
|
||||
game.players.find(p => p.id === 'e')!.forces = { 'hagga_basin:13': 1 };
|
||||
game.spice = { 'hagga_basin:12': 1, 'hagga_basin:13': 2 };
|
||||
const { discovery } = composed(game);
|
||||
assert.deepEqual(discovery.effects, [
|
||||
{ kind: 'orgiz', player: 'a', from: 'h', location: 'hagga_basin:12', amount: 1, source: 'player' },
|
||||
{ kind: 'orgiz', player: 'g', from: 'e', location: 'hagga_basin:13', amount: 1, source: 'player' },
|
||||
]);
|
||||
assert.equal(balance(discovery, 'h'), 0);
|
||||
assert.equal(balance(discovery, 'e'), 1);
|
||||
});
|
||||
|
||||
void test('Advanced does not select a payer from an uncontested sector when another collected sector is contested', () => {
|
||||
const game = fixture(true);
|
||||
reveal(game, ORGIZ_PROCESSING_STATION);
|
||||
|
||||
@@ -1,68 +1,53 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { normalizeAutomaticGame, viewGame, type Game } from '../game/engine';
|
||||
import { ORGIZ_PROCESSING_STATION } from '../game/discovery-collection';
|
||||
import { prepareSharedOrgizCollection, settleSharedOrgiz } from './fixture-discovery-shared-orgiz';
|
||||
import {applyAction,type Game} from '../game/engine';
|
||||
import {ORGIZ_PROCESSING_STATION} from '../game/discovery-collection';
|
||||
import {prepareSharedOrgizCollection,settleSharedOrgiz} from './fixture-discovery-shared-orgiz';
|
||||
|
||||
const wallets = (game: Game) => Object.fromEntries(game.players.map(player => [player.id, player.spice]));
|
||||
const transfers = (game: Game) =>
|
||||
game.log.filter(entry => entry.automatic?.name === 'Orgiz Processing Station');
|
||||
const wallets=(game:Game)=>Object.fromEntries(game.players.map(p=>[p.id,p.spice]));
|
||||
const transfers=(game:Game)=>game.log.filter(entry=>entry.automatic?.name==='Orgiz Processing Station');
|
||||
|
||||
for (const scenario of [
|
||||
{ name: 'an equal split charges the first actual recipient', choice: { kind: 'equal' as const }, payer: 'e' },
|
||||
{ name: 'a full Ecaz share charges Ecaz', choice: { kind: 'propose' as const, ecazShare: 4 }, payer: 'e' },
|
||||
{ name: 'a zero Ecaz share charges the ally instead', choice: { kind: 'propose' as const, ecazShare: 0 }, payer: 'g' },
|
||||
]) {
|
||||
void test(`Advanced shared Collection: ${scenario.name}, never an old wallet or an unavailable share`, () => {
|
||||
const position = prepareSharedOrgizCollection();
|
||||
const lot = position.ordinary.shared[0];
|
||||
assert.equal(lot.territory, 'hagga_basin');
|
||||
assert.equal(lot.ecaz, position.ecaz);
|
||||
assert.equal(lot.ally, position.guild);
|
||||
const base = structuredClone(position.game);
|
||||
const before = wallets(base);
|
||||
const settled = settleSharedOrgiz(position, scenario.choice);
|
||||
assert.deepEqual(position.game, base, 'Settling must not mutate the prepared position.');
|
||||
const receipt = settled.receipts.at(-1)!;
|
||||
assert.equal(receipt.territory, lot.territory);
|
||||
assert.equal(receipt.ecazAmount + receipt.allyAmount, lot.amount);
|
||||
assert.ok(receipt[scenario.payer === 'e' ? 'ecazAmount' : 'allyAmount'] > 0);
|
||||
const expected = { ...before };
|
||||
expected[receipt.ecaz] += receipt.ecazAmount;
|
||||
expected[receipt.ally] += receipt.allyAmount;
|
||||
expected[position.fremen] += 1;
|
||||
expected[scenario.payer] -= 1;
|
||||
assert.deepEqual(wallets(settled.game), expected);
|
||||
// Total spice is conserved by the transfer itself, and the credited total
|
||||
// equals the real collected lot.
|
||||
assert.equal(Object.values(wallets(settled.game)).reduce((sum, amount) => sum + amount, 0),
|
||||
Object.values(before).reduce((sum, amount) => sum + amount, 0) + lot.amount);
|
||||
assert.equal(Object.values(settled.game.spice).reduce((sum, amount) => sum + amount, 0),
|
||||
Object.values(base.spice).reduce((sum, amount) => sum + amount, 0) - lot.amount);
|
||||
const owned = transfers(settled.game);
|
||||
assert.equal(owned.length, 1);
|
||||
assert.equal(owned[0].automatic?.faction, 'fremen');
|
||||
assert.match(owned[0].text, /after the original shared territory allocation/);
|
||||
// The occupied location keeps its real garrison and the fremen wallet grows.
|
||||
assert.equal(settled.game.players.find(player => player.id === position.fremen)!.forces[`${ORGIZ_PROCESSING_STATION}:0`], 1);
|
||||
assert.equal(wallets(settled.game)[position.fremen], before[position.fremen] + 1);
|
||||
const stable = JSON.parse(JSON.stringify(settled.game)) as Game;
|
||||
assert.deepEqual(normalizeAutomaticGame(JSON.parse(JSON.stringify(stable))), stable);
|
||||
assert.equal(viewGame(settled.game, position.ecaz).players
|
||||
.find(player => player.id === position.ecaz)!.spice, expected[position.ecaz]);
|
||||
assert.equal(viewGame(settled.game, position.ecaz).players
|
||||
.find(player => player.id === position.guild)!.spice, undefined,
|
||||
'Opposing wallets stay private.');
|
||||
for(const split of ['equal','all-ecaz','all-guild'] as const) {
|
||||
void test(`Advanced native shared Collection: ${split} respects the actual foreign recipient and sole Orgiz owner`,()=>{
|
||||
const position=prepareSharedOrgizCollection();
|
||||
const original=structuredClone(position.game);
|
||||
const before=wallets(original);
|
||||
const choice=split==='equal'?{kind:'equal' as const}:
|
||||
{kind:'propose' as const,ecazShare:split==='all-ecaz'?position.lot.amount:0};
|
||||
const settled=settleSharedOrgiz(position,choice);
|
||||
const receipt=settled.receipt;
|
||||
const expected={...before};
|
||||
expected[position.ecaz]+=receipt.ecazAmount;
|
||||
expected[position.guild]+=receipt.allyAmount;
|
||||
const tax=receipt.ecazAmount>0?1:0;
|
||||
expected[position.ecaz]-=tax;
|
||||
expected[position.owner]+=tax;
|
||||
assert.deepEqual(wallets(settled.game),expected);
|
||||
assert.equal(receipt.ecazAmount+receipt.allyAmount,position.lot.amount);
|
||||
assert.equal(transfers(settled.game).length,tax);
|
||||
assert.equal(settled.game.players.find(p=>p.id===position.owner)!
|
||||
.forces[`${ORGIZ_PROCESSING_STATION}:0`],2);
|
||||
assert.deepEqual(position.game,original);
|
||||
const committed=structuredClone(settled.game);
|
||||
const last=settled.steps.at(-1)!;
|
||||
assert.throws(()=>applyAction(settled.game,last.actor,last.action));
|
||||
assert.deepEqual(settled.game,committed,'A stale allocation cannot pay the same territory twice.');
|
||||
});
|
||||
}
|
||||
|
||||
void test('re-settling the same prepaid position is deterministic and never accumulates', () => {
|
||||
const position = prepareSharedOrgizCollection();
|
||||
const first = settleSharedOrgiz(position, { kind: 'equal' });
|
||||
const second = settleSharedOrgiz(position, { kind: 'equal' });
|
||||
assert.equal(first.steps.length, second.steps.length);
|
||||
assert.deepEqual(wallets(second.game), wallets(first.game));
|
||||
assert.deepEqual(transfers(second.game).length, transfers(first.game).length);
|
||||
assert.deepEqual(wallets(position.game), wallets(position.before));
|
||||
assert.deepEqual(normalizeAutomaticGame(structuredClone(second.game)), second.game);
|
||||
void test('Native shared Orgiz waits for ally acceptance and rejects the wrong actor without changing wallets',()=>{
|
||||
const position=prepareSharedOrgizCollection();
|
||||
const before=structuredClone(position.game);
|
||||
const event=before.ecazCollection!.event;
|
||||
const action={type:'decision' as const,event,allocation:{kind:'propose' as const,ecazShare:position.lot.amount}};
|
||||
assert.throws(()=>applyAction(position.game,position.guild,action));
|
||||
assert.deepEqual(position.game,before);
|
||||
const proposed=applyAction(position.game,position.ecaz,action);
|
||||
assert.equal(proposed.decision?.player,position.guild);
|
||||
assert.deepEqual(wallets(proposed),wallets(before));
|
||||
assert.equal(transfers(proposed).length,0);
|
||||
const accepted=applyAction(proposed,position.guild,
|
||||
{type:'decision',event,allocation:{kind:'accept'}});
|
||||
assert.equal(wallets(accepted)[position.ecaz],wallets(before)[position.ecaz]+position.lot.amount-1);
|
||||
assert.equal(wallets(accepted)[position.guild],wallets(before)[position.guild]+1);
|
||||
});
|
||||
@@ -16,36 +16,23 @@ export function createOccupiedDiscoveryCollectionFixture(
|
||||
location:typeof CISTERN|typeof ORGIZ_PROCESSING_STATION,
|
||||
advanced=false,
|
||||
) {
|
||||
const fixture=createDiscoveryOccupantsFixture({location,advanced});
|
||||
const fixture=createDiscoveryOccupantsFixture({location,advanced,adjacentBlow:location===ORGIZ_PROCESSING_STATION});
|
||||
const actions:TestingStationContestStep[]=[...fixture.actions];
|
||||
let game=fixture.game;
|
||||
let collector:string|null=null,deposit:string|null=null;
|
||||
if(location===ORGIZ_PROCESSING_STATION) {
|
||||
game=advanceTestingStationContest(game,g=>g.phase===5&&g.turn<=4&&g.active!==null&&
|
||||
(g.movementRemaining?.length??0)===1&&clean(g)&&
|
||||
Object.entries(g.spice).some(([key,amount])=>amount>0&&
|
||||
key!==`${ORGIZ_PROCESSING_STATION}:0`),actions);
|
||||
const shipActor=game.active;
|
||||
assert.ok(shipActor,'A real movement window must have an active seat.');
|
||||
const candidates=Object.entries(game.spice)
|
||||
.filter(([key,amount])=>amount>0&&key!==`${ORGIZ_PROCESSING_STATION}:0`)
|
||||
.sort((left,right)=>right[1]-left[1]);
|
||||
assert.ok(candidates.length,'A real blow must supply a positive desert deposit.');
|
||||
let shipped:Game|null=null,shippedDeposit:string|null=null;
|
||||
for(const [key] of candidates) {
|
||||
const {territory,sector}=splitLocation(key);
|
||||
const shipment:Action={type:'ship',territory,sector,amount:2,elite:0,allyPayment:0};
|
||||
try {
|
||||
const next=applyAction(game,shipActor,shipment);
|
||||
actions.push({actor:shipActor,action:shipment});
|
||||
shipped=advanceTestingStationContest(next,clean,actions);
|
||||
shippedDeposit=key;
|
||||
break;
|
||||
} catch { continue; }
|
||||
}
|
||||
assert.ok(shipped&&shippedDeposit,'A real paid shipment must reach one actual positive deposit.');
|
||||
collector=shipActor; deposit=shippedDeposit;
|
||||
game=shipped;
|
||||
game=advanceTestingStationContest(game,g=>g.turn===2&&g.phase===5&&
|
||||
g.active==='emperor'&&clean(g),actions);
|
||||
const candidate=Object.entries(game.spice).find(([key,amount])=>amount>=2&&
|
||||
splitLocation(key).sector!==game.storm&&game.players.every(p=>(p.forces[key]??0)===0));
|
||||
assert.ok(candidate,'An original printed blow must supply an empty non-storm deposit.');
|
||||
deposit=candidate[0];
|
||||
collector='emperor';
|
||||
const {territory,sector}=splitLocation(deposit);
|
||||
const shipment:Action={type:'ship',territory,sector,amount:2,elite:0,allyPayment:0};
|
||||
game=applyAction(game,collector,shipment);
|
||||
actions.push({actor:collector,action:shipment});
|
||||
game=advanceTestingStationContest(game,clean,actions);
|
||||
assert.equal(game.players.find(p=>p.id===collector)!.forces[deposit],2);
|
||||
}
|
||||
game=advanceTestingStationContest(game,g=>g.phase===5&&g.active!==null&&
|
||||
|
||||
@@ -1,103 +1,94 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import {applyAction,type Action,type Game} from '../game/engine';
|
||||
import {splitLocation,territory} from '../game/board';
|
||||
import {ORGIZ_PROCESSING_STATION} from '../game/discovery-collection';
|
||||
import {
|
||||
applyAction, createGame, initializeDiscoveryGameForAudit, joinGame, newPlayer,
|
||||
normalizeAutomaticGame, viewGame, type Action, type Game,
|
||||
} from '../game/engine';
|
||||
import { territory } from '../game/board';
|
||||
import { quoteSpiceCollection } from '../game/board-resolution-quote';
|
||||
import { ORGIZ_PROCESSING_STATION, quoteDiscoveryCollection } from '../game/discovery-collection';
|
||||
import { putDiscovery } from './fixture-discovery';
|
||||
import { nativeTypedClearBoard, nativeTypedPhase, nativeTypedPlace } from './fixture-discovery-native-typed';
|
||||
import { advanceTestingStationContest } from './fixture-testing-station-contest';
|
||||
createDiscoveryOccupantsFixture,advanceTestingStationContest,nextTestingStationContestStep,
|
||||
type TestingStationContestStep,
|
||||
} from './fixture-testing-station-contest';
|
||||
|
||||
export type SharedOrgizStep = { actor: string; action: Action };
|
||||
const clean=(game:Game)=>!game.decision&&!game.response&&!game.phaseOpening;
|
||||
|
||||
/** Fresh original Ecaz/Guild/Fremen Advanced Discovery game. Only conserved
|
||||
* unplayed Spice Card order, the printed supply token, reserve-counter positions
|
||||
* and board spice already produced by real blows are controlled; every phase,
|
||||
* reveal, payment, allocation, transfer and Collection is an original action. */
|
||||
/** Preserve original armies/cards/alliances. Only original unplayed Spice Card
|
||||
* order and the actual token lottery are staged by the shared native prefix. */
|
||||
export function prepareSharedOrgizCollection() {
|
||||
let game = createGame('SHAREDORGIZ', newPlayer('e', 'Ecaz', 'ecaz'), true, ['ecaz']);
|
||||
joinGame(game, newPlayer('g', 'Guild', 'guild'));
|
||||
joinGame(game, newPlayer('f', 'Fremen', 'fremen'));
|
||||
for (const player of game.players)
|
||||
if (!player.ready) game = applyAction(game, player.id, { type: 'ready' });
|
||||
game.discoveryEnabled = true;
|
||||
const initial = structuredClone(game);
|
||||
game = initializeDiscoveryGameForAudit(game);
|
||||
game = advanceTestingStationContest(game, state => state.status === 'playing');
|
||||
const ecaz = 'e', guild = 'g', fremen = 'f';
|
||||
for (const [position, name] of ['hagga_basin', 'cielago_south', 'red_chasm', 'funeral_plain'].entries()) {
|
||||
const index = game.spiceDeck.findIndex((card, i) => i >= position &&
|
||||
'territory' in card && card.territory === name && !card.discovery);
|
||||
assert.ok(index >= position);
|
||||
game.spiceDeck.splice(position, 0, game.spiceDeck.splice(index, 1)[0]);
|
||||
const prefix=createDiscoveryOccupantsFixture({location:ORGIZ_PROCESSING_STATION,advanced:true,secondAmount:2});
|
||||
const actions:TestingStationContestStep[]=[...prefix.actions];
|
||||
let game=advanceTestingStationContest(prefix.game,g=>g.turn===2&&g.phase===5&&g.active==='ecaz'&&clean(g),actions);
|
||||
const leave:Action={type:'move',from:`${ORGIZ_PROCESSING_STATION}:0`,
|
||||
territory:prefix.parent,sector:prefix.parentSector,amount:2,elite:0};
|
||||
actions.push({actor:'ecaz',action:leave});
|
||||
game=applyAction(game,'ecaz',leave);
|
||||
game=advanceTestingStationContest(game,g=>g.turn===2&&g.phase===8&&clean(g),actions);
|
||||
// Two unused printed blows with different sectors guarantee that at least
|
||||
// one new deposit is outside the next real Storm. Never edit a storm source.
|
||||
let firstSector:number|null=null;
|
||||
const blowKeys:string[]=[];
|
||||
for(let position=0;position<2;position++) {
|
||||
const index=game.spiceDeck.findIndex((card,i)=>i>=position&&'territory' in card&&
|
||||
!card.discovery&&territory(card.territory).type==='sand'&&card.sector!==firstSector);
|
||||
assert.ok(index>=position,'Original unused ordinary blows must supply the native Collection route.');
|
||||
const card=game.spiceDeck.splice(index,1)[0];
|
||||
game.spiceDeck.splice(position,0,card);
|
||||
if('territory' in card) {
|
||||
firstSector=card.sector;
|
||||
blowKeys.push(`${card.territory}:${card.sector}`);
|
||||
}
|
||||
}
|
||||
game = nativeTypedPhase(game, 5);
|
||||
nativeTypedClearBoard(game);
|
||||
const token = putDiscovery(game, ORGIZ_PROCESSING_STATION);
|
||||
const source = `${token.territory}:${token.sector}`;
|
||||
assert.notEqual(token.sector, game.storm);
|
||||
nativeTypedPlace(game, fremen, source, 2);
|
||||
game = nativeTypedPhase(game, 7);
|
||||
assert.ok(viewGame(game, fremen).discoveries!.canInspect.includes(token.id));
|
||||
game = applyAction(game, fremen, { type: 'discovery', token: token.id, reveal: false });
|
||||
assert.ok(viewGame(game, fremen).discoveries!.canReveal.includes(token.id));
|
||||
game = applyAction(game, fremen, { type: 'discovery', token: token.id, reveal: true });
|
||||
game = nativeTypedPhase(game, 8);
|
||||
game = nativeTypedPhase(game, 5, 2);
|
||||
nativeTypedClearBoard(game);
|
||||
// Ecaz and its Guild ally hold the same real desert territory, so the original
|
||||
// quote creates the shared lot whose allocation decides the actual recipient.
|
||||
const haggaKeys = territory('hagga_basin').sectors.filter(sector => sector !== game.storm)
|
||||
.slice(0, 2).map(sector => `hagga_basin:${sector}`);
|
||||
assert.equal(haggaKeys.length, 2);
|
||||
const untouchedKey = 'the_great_flat:15';
|
||||
nativeTypedPlace(game, fremen, `${ORGIZ_PROCESSING_STATION}:0`, 1);
|
||||
nativeTypedPlace(game, ecaz, haggaKeys[0], 1);
|
||||
nativeTypedPlace(game, guild, haggaKeys[1], 1);
|
||||
// Conserved precondition, as in the original Collection tests: the Ecaz/Guild
|
||||
// alliance exists before this Collection. The governed behavior under audit is
|
||||
// the real allocation-then-transfer sequence below, not the pact's own rules.
|
||||
game.players.find(player => player.id === ecaz)!.ally = guild;
|
||||
game.players.find(player => player.id === guild)!.ally = ecaz;
|
||||
const boardSpice = Object.values(game.spice).reduce((sum, amount) => sum + amount, 0);
|
||||
assert.ok(boardSpice >= 6);
|
||||
game.spice = { [haggaKeys[0]]: 4, [haggaKeys[1]]: 2, [untouchedKey]: boardSpice - 6 };
|
||||
const ordinary = quoteSpiceCollection(game);
|
||||
assert.equal(ordinary.shared.length, 1);
|
||||
assert.deepEqual({ ...ordinary.shared[0], amount: 0 },
|
||||
{ territory: 'hagga_basin', ecaz, ally: guild, amount: 0 });
|
||||
assert.ok(ordinary.shared[0].amount > 0, 'The real shared lot must hold collected desert spice.');
|
||||
assert.deepEqual(quoteDiscoveryCollection(game, ordinary).effects, []);
|
||||
const before = structuredClone(game);
|
||||
for (const player of game.players)
|
||||
Object.assign(player, { hand: [], traitors: [], traitorChoices: [], shipped: true, moved: 1 });
|
||||
Object.assign(game, { phase: 5, active: fremen, movementRemaining: [fremen], phaseOpening: null });
|
||||
return { initial, game, before, ordinary, boardSpice, token: token.id, source,
|
||||
haggaKeys, untouchedKey, ecaz, guild, fremen };
|
||||
game=advanceTestingStationContest(game,g=>g.turn===3&&g.phase===5&&clean(g),actions);
|
||||
const target=Object.entries(game.spice).find(([key,amount])=>blowKeys.includes(key)&&amount>=2&&
|
||||
splitLocation(key).sector!==game.storm&&game.players.every(p=>(p.forces[key]??0)===0));
|
||||
assert.ok(target,'An actual original blow must supply an empty non-storm shared collection deposit.');
|
||||
const destination=splitLocation(target[0]);
|
||||
const shipped=new Set<string>();
|
||||
let beforeCollection:Game|null=null;
|
||||
for(let step=0;step<500&&game.turn===3&&!(game.phase===7&&game.decision?.kind==='ecazSpice');step++) {
|
||||
if(game.phase===5&&clean(game)&&game.movementRemaining?.length===1)
|
||||
beforeCollection=game;
|
||||
let next:TestingStationContestStep|null;
|
||||
if(game.turn===3&&game.phase===5&&clean(game)&&game.active&&
|
||||
['ecaz','guild'].includes(game.active)&&!shipped.has(game.active)) {
|
||||
const actor=game.active;
|
||||
next={actor,action:{type:'ship',territory:destination.territory,sector:destination.sector,
|
||||
amount:1,elite:0,allyPayment:0}};
|
||||
shipped.add(actor);
|
||||
} else next=nextTestingStationContestStep(game);
|
||||
assert.ok(next,`Original shared Collection has no continuation: ${JSON.stringify({
|
||||
status:game.status,turn:game.turn,phase:game.phase,decision:game.decision,
|
||||
active:game.active,shipped:[...shipped],target:target[0],
|
||||
})}`);
|
||||
actions.push(next);
|
||||
game=applyAction(game,next.actor,next.action);
|
||||
}
|
||||
assert.equal(game.decision?.kind,'ecazSpice',JSON.stringify({
|
||||
turn:game.turn,phase:game.phase,shipped:[...shipped],target:target[0],
|
||||
spice:beforeCollection?.spice[target[0]],storm:beforeCollection?.storm,
|
||||
seats:beforeCollection?.players.map(p=>({id:p.id,ally:p.ally,forces:p.forces[target[0]]})),
|
||||
last:actions.slice(-12),
|
||||
}));
|
||||
assert.equal(game.players.find(p=>p.id==='ecaz')!.forces[`${ORGIZ_PROCESSING_STATION}:0`],undefined);
|
||||
assert.equal(game.players.find(p=>p.id==='guild')!.forces[`${ORGIZ_PROCESSING_STATION}:0`],2);
|
||||
const pending=game.ecazCollection!.allocation!;
|
||||
const lot=pending.lots[pending.index];
|
||||
assert.equal(lot.territory,destination.territory);
|
||||
assert.ok(lot.amount>=2);
|
||||
return {...prefix,game,actions,lot,key:target[0],ecaz:'ecaz',guild:'guild',owner:'guild'};
|
||||
}
|
||||
|
||||
/** Settle the real Collection, then answer each original Ecaz allocation prompt. */
|
||||
export function settleSharedOrgiz(
|
||||
position: ReturnType<typeof prepareSharedOrgizCollection>,
|
||||
choice: { kind: 'propose'; ecazShare: number } | { kind: 'equal' } = { kind: 'equal' },
|
||||
position:ReturnType<typeof prepareSharedOrgizCollection>,
|
||||
choice:{kind:'propose';ecazShare:number}|{kind:'equal'}={kind:'equal'},
|
||||
) {
|
||||
const steps: SharedOrgizStep[] = [];
|
||||
let game = applyAction(position.game, position.fremen, { type: 'endMovement' });
|
||||
steps.push({ actor: position.fremen, action: { type: 'endMovement' } });
|
||||
assert.equal(game.phase, 7);
|
||||
const event = game.ecazCollection?.event ?? null;
|
||||
assert.ok(event, 'The real Collection must own its allocation event.');
|
||||
while (game.decision?.kind === 'ecazSpice' && steps.length < 20) {
|
||||
const actor = game.decision.player;
|
||||
const allocation = actor === position.ecaz ? choice : { kind: 'accept' as const };
|
||||
const action = { type: 'decision', event: game.ecazCollection!.event, allocation } as Action;
|
||||
game = applyAction(game, game.decision.player, action);
|
||||
steps.push({ actor: position.ecaz, action });
|
||||
let game=position.game;
|
||||
const event=game.ecazCollection!.event;
|
||||
const steps:TestingStationContestStep[]=[];
|
||||
while(game.decision?.kind==='ecazSpice'&&steps.length<20) {
|
||||
const actor=game.decision.player;
|
||||
const allocation=actor===position.ecaz?choice:{kind:'accept' as const};
|
||||
const action:Action={type:'decision',event,allocation};
|
||||
steps.push({actor,action});
|
||||
game=applyAction(game,actor,action);
|
||||
}
|
||||
assert.equal(game.decision, null, 'The original shared allocation must settle without inventing another choice.');
|
||||
const stable = JSON.parse(JSON.stringify(game)) as Game;
|
||||
assert.deepEqual(normalizeAutomaticGame(JSON.parse(JSON.stringify(stable))), stable);
|
||||
return { game, steps, event, receipts: game.ecazCollection?.settled ?? [] };
|
||||
assert.equal(game.decision,null);
|
||||
return {game,steps,event,receipt:game.ecazCollection!.settled.at(-1)!};
|
||||
}
|
||||
Reference in new issue
Block a user