Offer completed Homeworld CHOAM market disposal to Semuta
This commit is contained in:
1 parent
7004977e75
commit
b8a66a864b
10 files changed
+239
-2
No files matched your search
@@ -0,0 +1,76 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { applyAction, handLimit, viewGame, type Action, type Game } from '../game/engine';
|
||||
import { treacheryDeck } from '../game/cards';
|
||||
import { nativeShipmentSources } from '../game/homeworld-options';
|
||||
import { homeworldWorthlessSaleBlock } from '../game/homeworld-card-economy';
|
||||
import { moveSemutaEmperorCard, semutaEmperorPlayer } from './fixture-semuta-emperor-share';
|
||||
import { createSemutaHomeworldChoamSetup, advanceSemutaHomeworldChoam, holdSemutaHomeworldChoamCard } from './fixture-semuta-homeworld-choam';
|
||||
|
||||
export type HomeworldChoamSaleKind = 'worthless' | 'duplicate';
|
||||
/** Native setup, native closing market, conserved physical sale cards. A real
|
||||
* ten-force shipment lowers Tupile before the Worthless sale; nothing assigns
|
||||
* clocks, wallets, population, visitors or payment history. */
|
||||
export function createSemutaHomeworldChoamSaleFixture(kind: HomeworldChoamSaleKind, advanced = false, fullHolder = false) {
|
||||
const native = createSemutaHomeworldChoamSetup(advanced);
|
||||
let game = native.game;
|
||||
const owner = native.owner, holder = native.holder, actions = native.actions;
|
||||
const card = holdSemutaHomeworldChoamCard(game, owner, kind === 'worthless' ? 'Baliset' : 'Shield');
|
||||
let witness: string | undefined;
|
||||
if (kind === 'duplicate') {
|
||||
const other = treacheryDeck(game.expansions).find(c => c.name === 'Shield' && c.id !== card);
|
||||
assert.ok(other); witness = moveSemutaEmperorCard(game, other.id, owner);
|
||||
}
|
||||
holdSemutaHomeworldChoamCard(game, native.canceller, 'Karama');
|
||||
const action: Action = { type: 'decision', mode: 'sell', card, ...(witness ? { witness } : {}) };
|
||||
const setup = structuredClone(game);
|
||||
game = advanceSemutaHomeworldChoam(game, g => g.decision?.kind === 'choamMarket' && !g.response && !g.phaseOpening, actions);
|
||||
const highMarket = structuredClone(game);
|
||||
assert.equal(semutaEmperorPlayer(highMarket, owner).reserves, 20);
|
||||
let beforeShipment: Game | null = null, afterShipment: Game | null = null;
|
||||
if (kind === 'worthless') {
|
||||
assert.ok(homeworldWorthlessSaleBlock(highMarket, owner,
|
||||
semutaEmperorPlayer(highMarket, owner).hand.find(c => c.id === card)!));
|
||||
game = advanceSemutaHomeworldChoam(game,
|
||||
g => g.phase === 5 && g.active === owner && !g.phaseOpening && !g.response && !g.decision, actions);
|
||||
beforeShipment = structuredClone(game);
|
||||
const sources = nativeShipmentSources(viewGame(game, owner), 10, 0); assert.ok(sources);
|
||||
const ship: Action = { type: 'ship', territory: 'habbanya_ridge_sietch', sector: 17,
|
||||
amount: 10, elite: 0, homeworldSources: sources };
|
||||
actions.push({ actor: owner, action: ship }); game = applyAction(game, owner, ship);
|
||||
game = advanceSemutaHomeworldChoam(game, g => !g.phaseOpening && !g.response && !g.decision, actions);
|
||||
afterShipment = structuredClone(game);
|
||||
assert.equal(semutaEmperorPlayer(game, owner).reserves, 10);
|
||||
assert.equal(semutaEmperorPlayer(game, owner).forces['habbanya_ridge_sietch:17'], 10);
|
||||
const end: Action = { type: 'endMovement' };
|
||||
actions.push({ actor: owner, action: end }); game = applyAction(game, owner, end);
|
||||
game = advanceSemutaHomeworldChoam(game,
|
||||
g => g.phase === 5 && g.decision?.kind === 'choamMarket' && !g.response && !g.phaseOpening, actions);
|
||||
assert.equal(homeworldWorthlessSaleBlock(game, owner,
|
||||
semutaEmperorPlayer(game, owner).hand.find(c => c.id === card)!), null);
|
||||
}
|
||||
if (fullHolder) {
|
||||
const player = semutaEmperorPlayer(game, holder);
|
||||
while (player.hand.length < handLimit(player)) {
|
||||
assert.ok(game.deck[0]); moveSemutaEmperorCard(game, game.deck[0].id, holder);
|
||||
}
|
||||
}
|
||||
assert.equal(game.decision?.kind, 'choamMarket');
|
||||
assert.equal(game.decision!.player, owner);
|
||||
return { game, setup, highMarket, beforeShipment, afterShipment, owner, holder,
|
||||
canceller: native.canceller, card, witness, price: kind === 'worthless' ? 2 : 3, action, actions, kind };
|
||||
}
|
||||
|
||||
export function playSemutaHomeworldChoamSale(f: ReturnType<typeof createSemutaHomeworldChoamSaleFixture>): Game {
|
||||
let game = applyAction(f.game, f.owner, f.action);
|
||||
for (let n = 0; game.response && n < 16; n++) {
|
||||
assert.equal(game.response.kind, 'choamSale');
|
||||
const actor = game.players.find(p => {
|
||||
const controls = viewGame(game, p.id).responseControls;
|
||||
return controls && !controls.hasPassed && controls.cancelCards.length > 0;
|
||||
});
|
||||
assert.ok(actor);
|
||||
game = applyAction(game, actor.id, { type: 'passResponse' });
|
||||
}
|
||||
assert.ok(!game.response);
|
||||
return game;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { applyAction, viewGame } from '../game/engine';
|
||||
import { botActions } from '../game/bots';
|
||||
import { SEMUTA_DRUG_ID } from '../game/semuta-drug';
|
||||
import { createSemutaHomeworldChoamSaleFixture, playSemutaHomeworldChoamSale } from './fixture-semuta-homeworld-choam-sale';
|
||||
import { reactSemutaHomeworldChoam } from './fixture-semuta-homeworld-choam';
|
||||
import { semutaEmperorPlayer as player } from './fixture-semuta-emperor-share';
|
||||
import { semutaHomeworldChoamMovementStock as stock, semutaHomeworldChoamMovementResources as resources } from './fixture-semuta-homeworld-choam-movement';
|
||||
|
||||
for (const advanced of [false, true]) for (const kind of ['worthless', 'duplicate'] as const) {
|
||||
for (const claim of [false, true]) void test(`${advanced ? 'Advanced' : 'Basic'} Homeworld ${kind} market Semuta ${claim ? 'claim' : 'pass'} preserves its actual payout and original close`, () => {
|
||||
const f = createSemutaHomeworldChoamSaleFixture(kind, advanced, true);
|
||||
const pending = playSemutaHomeworldChoamSale(f);
|
||||
assert.ok(pending.pendingTreacheryDiscard);
|
||||
assert.equal(player(pending, f.owner).spice, player(f.game, f.owner).spice + f.price);
|
||||
assert.equal(player(pending, f.owner).hand.some(c => c.id === f.card), false);
|
||||
assert.equal(pending.discard.filter(c => c.id === f.card).length, 1);
|
||||
if (f.witness) assert.ok(player(pending, f.owner).hand.some(c => c.id === f.witness));
|
||||
if (kind === 'worthless') {
|
||||
assert.equal(player(f.afterShipment!, f.owner).spice, player(f.beforeShipment!, f.owner).spice - 10);
|
||||
assert.equal(player(pending, f.owner).reserves, 10);
|
||||
assert.equal(player(pending, f.owner).forces['habbanya_ridge_sietch:17'], 10);
|
||||
} else assert.equal(player(pending, f.owner).reserves, 20);
|
||||
const paid = resources(pending), event = pending.pendingTreacheryDiscard.batch.event;
|
||||
assert.throws(() => applyAction(pending, f.owner, { type: 'decision', done: true }));
|
||||
const done = reactSemutaHomeworldChoam(pending, f.holder, claim);
|
||||
assert.equal(player(done, f.holder).hand.some(c => c.id === f.card), claim);
|
||||
assert.equal(player(done, f.holder).hand.some(c => c.id === SEMUTA_DRUG_ID), !claim);
|
||||
assert.equal(player(done, f.holder).hand.length, player(f.game, f.holder).hand.length);
|
||||
assert.deepEqual(resources(done), paid, 'Recovery cannot repeat payout, refund shipment or move a counter');
|
||||
assert.deepEqual(stock(done), stock(f.game));
|
||||
if (f.witness) assert.ok(player(done, f.owner).hand.some(c => c.id === f.witness));
|
||||
assert.throws(() => applyAction(done, f.owner, f.action));
|
||||
assert.throws(() => applyAction(done, f.holder, { type: 'semutaCommit', event }));
|
||||
const next = applyAction(done, f.owner, { type: 'decision', done: true });
|
||||
assert.equal(next.phase, kind === 'worthless' ? 6 : 1);
|
||||
assert.equal(next.turn, f.game.turn);
|
||||
assert.equal(player(next, f.owner).spice, player(done, f.owner).spice);
|
||||
assert.equal(player(next, f.owner).reserves, player(done, f.owner).reserves);
|
||||
assert.equal(next.pendingTreacheryDiscard, null);
|
||||
});
|
||||
}
|
||||
|
||||
for (const advanced of [false, true]) void test(`${advanced ? 'Advanced' : 'Basic'} high Tupile still rejects Worthless sale before payment or disposal`, () => {
|
||||
const f = createSemutaHomeworldChoamSaleFixture('worthless', advanced);
|
||||
const before = structuredClone(f.highMarket);
|
||||
assert.throws(() => applyAction(f.highMarket, f.owner, f.action));
|
||||
assert.deepEqual(f.highMarket, before);
|
||||
});
|
||||
|
||||
for (const difficulty of ['Easy', 'Medium', 'Hard', 'Brutal'] as const) void test(`${difficulty} legally settles the native Homeworld market disposal`, () => {
|
||||
for (const advanced of [false, true]) for (const kind of ['worthless', 'duplicate'] as const) {
|
||||
const f = createSemutaHomeworldChoamSaleFixture(kind, advanced);
|
||||
let game = playSemutaHomeworldChoamSale(f);
|
||||
const paid = player(game, f.owner).spice;
|
||||
const view = viewGame(game, f.holder);
|
||||
view.players.find(p => p.id === f.holder)!.bot = difficulty;
|
||||
const action = botActions(view)[0]; assert.ok(action);
|
||||
game = applyAction(game, f.holder, action);
|
||||
const event = game.pendingTreacheryDiscard?.batch.event;
|
||||
for (const p of game.players) if (game.pendingTreacheryDiscard?.reaction?.stage === 'offer'
|
||||
&& !game.pendingTreacheryDiscard.reaction.passed.includes(p.id))
|
||||
game = applyAction(game, p.id, { type: 'semutaPass', event });
|
||||
assert.equal(game.pendingTreacheryDiscard, null);
|
||||
assert.equal(player(game, f.owner).spice, paid);
|
||||
const claimed = player(game, f.holder).hand.some(c => c.id === f.card);
|
||||
assert.equal(player(game, f.holder).hand.some(c => c.id === SEMUTA_DRUG_ID), !claimed);
|
||||
assert.deepEqual(stock(game), stock(f.game));
|
||||
const next = applyAction(game, f.owner, { type: 'decision', done: true });
|
||||
assert.equal(next.phase, kind === 'worthless' ? 6 : 1);
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user