React to ordinary used cards in the Homeworld Semuta envelope
This commit is contained in:
1 parent
d14f3d1717
commit
cb4c32fc87
10 files changed
+203
-6
No files matched your search
@@ -0,0 +1,56 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
applyAction, type Action, type Game,
|
||||
} from '../game/engine';
|
||||
import { SEMUTA_DRUG_ID } from '../game/semuta-drug';
|
||||
import { createSemutaHomeworldGuildIncomeFixture } from './fixture-semuta-homeworld-guild-income';
|
||||
import {
|
||||
moveSemutaEmperorCard, semutaEmperorCardId, semutaEmperorPlayer,
|
||||
} from './fixture-semuta-emperor-share';
|
||||
|
||||
export type SemutaHomeworldOrdinaryFixture = {
|
||||
game: Game; source: Game; holder: string; owner: string; semuta: string;
|
||||
hajr: string; action: Action;
|
||||
};
|
||||
|
||||
/** Genuine original Homeworld occupation state already at an active Movement
|
||||
* owner. Only conserved later physical cards isolate this ordinary play; the
|
||||
* occupation epoch, custody, wallets and clocks stay native. */
|
||||
export function createSemutaHomeworldOrdinaryFixture(): SemutaHomeworldOrdinaryFixture {
|
||||
const income = createSemutaHomeworldGuildIncomeFixture({ advanced: false });
|
||||
const game = income.game;
|
||||
assert.equal(game.phase, 5);
|
||||
assert.equal(game.active, 'a');
|
||||
const holder = income.holder, owner = income.canceller;
|
||||
assert.equal(semutaEmperorPlayer(game, holder).hand.some(card => card.id === SEMUTA_DRUG_ID), true);
|
||||
const hajr = moveSemutaEmperorCard(game,
|
||||
semutaEmperorCardId(game, card => card.effect === 'hajr'), 'a');
|
||||
assert.ok(semutaEmperorPlayer(game, 'a').hand.some(card => card.id === hajr));
|
||||
return { game, source: income.game, holder, owner, semuta: SEMUTA_DRUG_ID, hajr,
|
||||
action: { type: 'card', card: hajr } };
|
||||
}
|
||||
|
||||
export function reactSemutaHomeworldOrdinary(
|
||||
state: Game, fixture: SemutaHomeworldOrdinaryFixture, 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;
|
||||
}
|
||||
|
||||
/** Homeworld authority that an ordinary card play must not disturb. */
|
||||
export function semutaHomeworldOrdinaryState(game: Game) {
|
||||
return {
|
||||
homeworlds: game.homeworlds,
|
||||
history: game.homeworldOccupationHistory,
|
||||
percentages: game.homeworldOccupiedPercentageLedger,
|
||||
players: game.players.map(player => ({
|
||||
id: player.id, faction: player.faction, ally: player.ally, spice: player.spice,
|
||||
reserves: player.reserves, tanks: player.tanks, forces: player.forces,
|
||||
elites: player.elites, shipped: player.shipped, moved: player.moved,
|
||||
})),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
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 { semutaEmperorPlayer } from './fixture-semuta-emperor-share';
|
||||
import {
|
||||
createSemutaHomeworldOrdinaryFixture, reactSemutaHomeworldOrdinary,
|
||||
semutaHomeworldOrdinaryState,
|
||||
} from './fixture-semuta-homeworld-ordinary';
|
||||
|
||||
for (const claim of [false, true]) void test(
|
||||
`Homeworld ordinary special play Semuta ${claim ? 'claims' : 'passes'} the used card without disturbing occupation`, () => {
|
||||
const f = createSemutaHomeworldOrdinaryFixture();
|
||||
assert.equal(f.game.phase, 5);
|
||||
assert.equal(f.game.active, 'a');
|
||||
assert.ok(semutaEmperorPlayer(f.game, f.holder).hand.some(card => card.id === SEMUTA_DRUG_ID));
|
||||
const played = applyAction(f.game, 'a', f.action);
|
||||
const pending = played.pendingTreacheryDiscard;
|
||||
assert.ok(pending, 'The used ordinary special must offer its printed Semuta cost');
|
||||
assert.equal(pending.batch.cause, 'ordinary:hajr');
|
||||
assert.deepEqual(pending.batch.entries.map(entry => ({
|
||||
id: entry.card.id, by: entry.discardedBy, face: entry.publicFace,
|
||||
})), [{ id: f.hajr, by: 'a', face: true }]);
|
||||
assert.equal(pending.reaction?.stage, 'offer');
|
||||
assert.deepEqual(played.hajr, ['a'], 'The Hajr movement grant already committed');
|
||||
assert.equal(played.discard.filter(card => card.id === f.hajr).length, 1);
|
||||
for (const player of played.players) viewGame(played, player.id);
|
||||
const before = semutaHomeworldOrdinaryState(played);
|
||||
const event = pending.batch.event;
|
||||
assert.throws(() => applyAction(played, f.owner, { type: 'semutaCommit', event }));
|
||||
assert.throws(() => applyAction(played, f.holder, { type: 'semutaSelect', event, card: f.hajr }));
|
||||
const done = reactSemutaHomeworldOrdinary(played, 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.hajr), claim);
|
||||
assert.equal(holder.hand.some(card => card.id === SEMUTA_DRUG_ID), !claim);
|
||||
assert.equal(done.discard.filter(card => card.id === f.hajr).length, Number(!claim));
|
||||
assert.equal(done.discard.filter(card => card.id === SEMUTA_DRUG_ID).length, Number(claim));
|
||||
assert.deepEqual(semutaHomeworldOrdinaryState(done), before,
|
||||
'Claiming the used card cannot move wallets, custody or occupation authority');
|
||||
assert.deepEqual(done.hajr, ['a']);
|
||||
assert.throws(() => applyAction(done, 'a', f.action), 'The used card cannot be played again');
|
||||
assert.throws(() => applyAction(done, f.holder, { type: 'semutaCommit', event }));
|
||||
const suffix = applyAction(done, 'a', { type: 'endMovement' });
|
||||
assert.ok(!suffix.movementRemaining?.includes('a'));
|
||||
assert.equal(suffix.phase, 5);
|
||||
assert.deepEqual(semutaHomeworldOrdinaryState(suffix), before);
|
||||
},
|
||||
);
|
||||
|
||||
void test('a full-hand Homeworld Richese exchanges Semuta for the used ordinary card', () => {
|
||||
const f = createSemutaHomeworldOrdinaryFixture();
|
||||
const holder = semutaEmperorPlayer(f.game, f.holder);
|
||||
holder.hand.push(...f.game.deck.splice(0, 4 - holder.hand.length));
|
||||
assert.equal(holder.hand.length, 4);
|
||||
const played = applyAction(f.game, 'a', f.action);
|
||||
const offer = viewGame(played, f.holder).semutaReaction!;
|
||||
assert.equal(offer.canCommit, true);
|
||||
assert.deepEqual(offer.candidates, []);
|
||||
const done = applyAction(played, f.holder, { type: 'semutaCommit', event: played.pendingTreacheryDiscard!.batch.event });
|
||||
assert.equal(semutaEmperorPlayer(done, f.holder).hand.length, 4);
|
||||
assert.ok(semutaEmperorPlayer(done, f.holder).hand.some(card => card.id === f.hajr));
|
||||
});
|
||||
|
||||
for (const difficulty of ['Easy', 'Medium', 'Hard', 'Brutal'] as const) void test(
|
||||
`the ${difficulty} bot settles the Homeworld ordinary-card Semuta cost`, () => {
|
||||
const f = createSemutaHomeworldOrdinaryFixture();
|
||||
const played = applyAction(f.game, 'a', f.action);
|
||||
const view = viewGame(played, f.holder);
|
||||
view.players.find(player => player.id === f.holder)!.bot = difficulty;
|
||||
const action = botActions(view)[0];
|
||||
assert.ok(action);
|
||||
const done = applyAction(played, 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.hajr);
|
||||
assert.equal(claimed || done.discard.some(card => card.id === f.hajr), true,
|
||||
'A legal bot either acquires the used card or leaves it discarded');
|
||||
assert.equal(holder.hand.some(card => card.id === SEMUTA_DRUG_ID), !claimed);
|
||||
assert.equal(done.hajr.includes('a'), true);
|
||||
assert.equal(holder.spice, f.game.players.find(player => player.id === f.holder)!.spice);
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user