React to a paid Box search in the Homeworld Semuta envelope
This commit is contained in:
1 parent
cb4c32fc87
commit
a9a8ae6b75
10 files changed
+157
-19
No files matched your search
@@ -30,6 +30,46 @@ export function createSemutaHomeworldOrdinaryFixture(): SemutaHomeworldOrdinaryF
|
||||
action: { type: 'card', card: hajr } };
|
||||
}
|
||||
|
||||
export type SemutaHomeworldBoxFixture = {
|
||||
game: Game; source: Game; holder: string; box: string; owner: string;
|
||||
action: Action; cost: number;
|
||||
};
|
||||
|
||||
/** Genuine original Homeworld occupation state; a real paid Nullentropy Box
|
||||
* search runs through the engine's own singleton completion. The searched card
|
||||
* and its candidate discard are conserved physical cards, not minted. */
|
||||
export function createSemutaHomeworldBoxFixture(): SemutaHomeworldBoxFixture {
|
||||
const income = createSemutaHomeworldGuildIncomeFixture({ advanced: false });
|
||||
const game = income.game;
|
||||
assert.equal(game.phase, 5);
|
||||
assert.equal(game.active, 'a');
|
||||
const holder = income.holder, owner = 'a';
|
||||
assert.equal(semutaEmperorPlayer(game, holder).hand.some(card => card.id === SEMUTA_DRUG_ID), true);
|
||||
if (!game.discard.some(card => card.effect !== 'nullentropyBox')) {
|
||||
const id = semutaEmperorCardId(game, card => card.effect !== 'nullentropyBox' &&
|
||||
card.kind !== 'special' && card.effect !== 'karama');
|
||||
moveSemutaEmperorCard(game, id, owner);
|
||||
game.discard.push(semutaEmperorPlayer(game, owner).hand.pop()!);
|
||||
}
|
||||
const box = moveSemutaEmperorCard(game,
|
||||
semutaEmperorCardId(game, card => card.effect === 'nullentropyBox'), owner);
|
||||
const atreides = semutaEmperorPlayer(game, owner);
|
||||
assert.ok(atreides.hand.some(card => card.id === box));
|
||||
const action: Action = { type: 'card', card: box };
|
||||
return { game, source: income.game, holder, box, owner, action, cost: 2 };
|
||||
}
|
||||
|
||||
export function reactSemutaHomeworldBox(
|
||||
state: Game, fixture: SemutaHomeworldBoxFixture, claim: boolean,
|
||||
): Game {
|
||||
const event = state.pendingTreacheryDiscard!.batch.event;
|
||||
if (claim) return applyAction(state, fixture.holder, { type: 'semutaCommit', event });
|
||||
let game = state;
|
||||
for (const player of state.players)
|
||||
game = applyAction(game, player.id, { type: 'semutaPass', event });
|
||||
return game;
|
||||
}
|
||||
|
||||
export function reactSemutaHomeworldOrdinary(
|
||||
state: Game, fixture: SemutaHomeworldOrdinaryFixture, claim: boolean,
|
||||
): Game {
|
||||
|
||||
@@ -5,7 +5,8 @@ import { botActions } from '../game/bots';
|
||||
import { SEMUTA_DRUG_ID } from '../game/semuta-drug';
|
||||
import { semutaEmperorPlayer } from './fixture-semuta-emperor-share';
|
||||
import {
|
||||
createSemutaHomeworldOrdinaryFixture, reactSemutaHomeworldOrdinary,
|
||||
createSemutaHomeworldBoxFixture, createSemutaHomeworldOrdinaryFixture,
|
||||
reactSemutaHomeworldBox, reactSemutaHomeworldOrdinary,
|
||||
semutaHomeworldOrdinaryState,
|
||||
} from './fixture-semuta-homeworld-ordinary';
|
||||
|
||||
@@ -50,6 +51,58 @@ for (const claim of [false, true]) void test(
|
||||
},
|
||||
);
|
||||
|
||||
for (const claim of [false, true]) void test(
|
||||
`Homeworld paid Nullentropy Box Semuta ${claim ? 'claims' : 'passes'} the used Box after its real search`, () => {
|
||||
const f = createSemutaHomeworldBoxFixture();
|
||||
const searched = applyAction(f.game, f.owner, f.action);
|
||||
const pending = searched.pendingTreacheryDiscard;
|
||||
assert.ok(pending, 'The completed paid Box search must offer its printed Semuta cost');
|
||||
assert.equal(pending.batch.cause, 'nullentropyBox');
|
||||
assert.deepEqual(pending.batch.entries.map(entry => ({
|
||||
id: entry.card.id, by: entry.discardedBy, face: entry.publicFace,
|
||||
})), [{ id: f.box, by: f.owner, face: true }]);
|
||||
assert.equal(searched.discard.at(-1)?.id, f.box, 'The used Box stays on top of the shuffled pile');
|
||||
assert.equal(semutaEmperorPlayer(searched, f.owner).spice,
|
||||
semutaEmperorPlayer(f.game, f.owner).spice - f.cost);
|
||||
for (const player of searched.players) viewGame(searched, player.id);
|
||||
const before = semutaHomeworldOrdinaryState(searched);
|
||||
const event = pending.batch.event;
|
||||
assert.throws(() => applyAction(searched, f.holder, { type: 'semutaSelect', event, card: f.box }));
|
||||
const done = reactSemutaHomeworldBox(searched, f, claim);
|
||||
assert.equal(done.pendingTreacheryDiscard, null);
|
||||
assert.equal(done.resolvedTreacheryDiscardSequence, pending.sequence);
|
||||
const holder = semutaEmperorPlayer(done, f.holder);
|
||||
assert.equal(holder.hand.some(card => card.id === f.box), claim);
|
||||
assert.equal(holder.hand.some(card => card.id === SEMUTA_DRUG_ID), !claim);
|
||||
assert.equal(done.discard.some(card => card.id === f.box), !claim);
|
||||
assert.deepEqual(semutaHomeworldOrdinaryState(done), before,
|
||||
'The two-spice payment, searched card and occupation authority are unchanged');
|
||||
assert.throws(() => applyAction(done, f.owner, f.action));
|
||||
const suffix = applyAction(done, f.owner, { type: 'endMovement' });
|
||||
assert.ok(!suffix.movementRemaining?.includes(f.owner));
|
||||
assert.equal(suffix.phase, 5);
|
||||
},
|
||||
);
|
||||
|
||||
for (const difficulty of ['Easy', 'Medium', 'Hard', 'Brutal'] as const) void test(
|
||||
`the ${difficulty} bot settles the Homeworld paid-Box Semuta cost`, () => {
|
||||
const f = createSemutaHomeworldBoxFixture();
|
||||
const searched = applyAction(f.game, f.owner, f.action);
|
||||
const view = viewGame(searched, f.holder);
|
||||
view.players.find(player => player.id === f.holder)!.bot = difficulty;
|
||||
const action = botActions(view)[0];
|
||||
assert.ok(action);
|
||||
const done = applyAction(searched, f.holder, action);
|
||||
assert.equal(done.pendingTreacheryDiscard, null);
|
||||
const holder = done.players.find(player => player.id === f.holder)!;
|
||||
const claimed = holder.hand.some(card => card.id === f.box);
|
||||
assert.equal(claimed || done.discard.some(card => card.id === f.box), true);
|
||||
assert.equal(holder.hand.some(card => card.id === SEMUTA_DRUG_ID), !claimed);
|
||||
assert.equal(done.players.find(player => player.id === f.owner)!.spice,
|
||||
f.game.players.find(player => player.id === f.owner)!.spice - f.cost);
|
||||
},
|
||||
);
|
||||
|
||||
void test('a full-hand Homeworld Richese exchanges Semuta for the used ordinary card', () => {
|
||||
const f = createSemutaHomeworldOrdinaryFixture();
|
||||
const holder = semutaEmperorPlayer(f.game, f.holder);
|
||||
|
||||
Reference in new issue
Block a user