Support Advanced Sapho Charity suspension
This commit is contained in:
1 parent
fcd93ca252
commit
70ef142577
19 files changed
+694
-49
No files matched your search
@@ -85,6 +85,60 @@ void test('native clean Charity first and last render their scope and submit onl
|
||||
}
|
||||
});
|
||||
|
||||
void test('own started Charity pause displays the retained claim and submits its native action', () => {
|
||||
const source = native(true, true), game = source.game;
|
||||
// Controlled custody: transfer the one physical Sapho to the actual claimant.
|
||||
const donor = saphoCharityOrderPlayer(game, source.owner);
|
||||
const claimant = saphoCharityOrderPlayer(game, source.current);
|
||||
const index = donor.hand.findIndex(card => card.id === source.card);
|
||||
assert.ok(index >= 0);
|
||||
claimant.hand.push(donor.hand.splice(index, 1)[0]);
|
||||
const view = viewGame(game, source.current), option = optionFor(view, 'last');
|
||||
assert.equal(option.resumeOwn, true);
|
||||
const html = panel(view);
|
||||
assert.match(html, /Resume your charity last and discard Juice of Sapho/);
|
||||
assert.match(html, /Your completed claim and its payment remain unchanged/);
|
||||
assert.deepEqual(pressSapho(source.card, option), [{ type: 'card', card: source.card,
|
||||
scope: 'charity', event: option.event, mode: 'last' }]);
|
||||
assert.deepEqual(pressSapho(source.card, option, true), []);
|
||||
});
|
||||
|
||||
void test('actual v3 Charity queue enables the current player and blocks the paused player', () => {
|
||||
const source = native(true, true);
|
||||
let game = source.game;
|
||||
// The already-paid original Sapho changes hands without creating a card.
|
||||
const donor = saphoCharityOrderPlayer(game, source.owner);
|
||||
const claimant = saphoCharityOrderPlayer(game, source.current);
|
||||
const cardIndex = donor.hand.findIndex(card => card.id === source.card);
|
||||
assert.ok(cardIndex >= 0);
|
||||
claimant.hand.push(donor.hand.splice(cardIndex, 1)[0]);
|
||||
const pause = optionFor(viewGame(game, source.current), 'last');
|
||||
game = actSaphoCharityOrder(game, source.current, pressSapho(source.card, pause)[0]!);
|
||||
assert.ok(game.charityOrder && game.charityOrder.version === 3);
|
||||
const actor = game.charityOrder.remaining[0]; assert.notEqual(actor, source.current);
|
||||
const recipient = game.players.find(player => player.id !== actor)!.id;
|
||||
const purse = saphoCharityOrderPlayer(game, actor).spice;
|
||||
if (purse) game = actSaphoCharityOrder(game, actor, { type: 'bribe', target: recipient, amount: purse });
|
||||
const active = viewGame(game, actor), waiting = viewGame(game, source.current);
|
||||
assert.match(charity(active), /It is your Charity turn/);
|
||||
assert.match(charity(waiting), /Your turn comes later/);
|
||||
assert.deepEqual(pressCharity(waiting, 'charity-claim-submit'), { sent: [], disabled: true });
|
||||
assert.deepEqual(pressCharity(waiting, 'charity-turn-finish'), { sent: [], disabled: true });
|
||||
const claim = pressCharity(active, 'charity-claim-submit');
|
||||
assert.deepEqual(claim, { sent: [{ type: 'charity' }], disabled: false });
|
||||
game = actSaphoCharityOrder(game, actor, claim.sent[0]!);
|
||||
const finish = pressCharity(viewGame(game, actor), 'charity-turn-finish');
|
||||
assert.deepEqual(finish, { sent: [{ type: 'ready' }], disabled: false });
|
||||
game = actSaphoCharityOrder(game, actor, finish.sent[0]!);
|
||||
while (game.charityOrder?.remaining[0] !== source.current) {
|
||||
const current = game.charityOrder?.remaining[0]; assert.ok(current);
|
||||
game = actSaphoCharityOrder(game, current, { type: 'ready' });
|
||||
}
|
||||
const resumed = viewGame(game, source.current);
|
||||
assert.deepEqual(pressCharity(resumed, 'charity-claim-submit'), { sent: [], disabled: true });
|
||||
assert.deepEqual(pressCharity(resumed, 'charity-turn-finish'), { sent: [{ type: 'ready' }], disabled: false });
|
||||
});
|
||||
|
||||
void test('native claimed current actor stays first while Sapho first or last queues only its later tail', () => {
|
||||
for (const mode of ['first', 'last'] as const) {
|
||||
const source = native(true, true), game = viewGame(source.game, source.owner), option = optionFor(game, mode);
|
||||
|
||||
@@ -75,18 +75,22 @@ void test('an actual accepted current Charity claim stays ahead of both Sapho ta
|
||||
}
|
||||
});
|
||||
|
||||
void test('the real Sapho holder cannot split its own started Charity or choose its existing no-op position', () => {
|
||||
void test('the real Sapho holder can pause its started Charity but cannot choose a no-op position', () => {
|
||||
const f = fixture(); let game = advance(reload(f.game), state => clean(state) && state.charityOrder?.remaining[0] === f.owner);
|
||||
assert.equal(game.active, f.owner);
|
||||
assert.equal(viewGame(game, f.owner).saphoOptions.some(option => option.scope === 'charity' && option.mode === 'first'), false);
|
||||
reject(game, f.owner, { type: 'card', card: f.card, scope: 'charity', event: currentEvent(game), mode: 'first' });
|
||||
game = claim(game, f.owner).game;
|
||||
assert.equal(player(game, f.owner).charityTurn, game.turn);
|
||||
assert.deepEqual(viewGame(game, f.owner).saphoOptions.filter(option => option.scope === 'charity'), []);
|
||||
for (const mode of ['first', 'last'] as const)
|
||||
reject(game, f.owner, { type: 'card', card: f.card, scope: 'charity', event: currentEvent(game), mode });
|
||||
assert.ok(player(game, f.owner).hand.some(card => card.id === f.card));
|
||||
assert.equal(game.discard.some(card => card.id === f.card), false);
|
||||
reject(game, f.owner, { type: 'card', card: f.card, scope: 'charity', event: currentEvent(game), mode: 'first' });
|
||||
const option = viewGame(game, f.owner).saphoOptions.find(option => option.scope === 'charity' && option.mode === 'last');
|
||||
assert.ok(option && option.scope === 'charity' && option.resumeOwn);
|
||||
const before = player(game, f.owner).spice;
|
||||
game = act(game, f.owner, { type: 'card', card: f.card, scope: 'charity', event: option.event, mode: 'last' });
|
||||
assert.equal(game.charityOrder?.remaining.at(-1), f.owner);
|
||||
assert.equal(player(game, f.owner).spice, before);
|
||||
assert.equal(player(game, f.owner).charityTurn, game.turn);
|
||||
assert.equal(game.discard.filter(card => card.id === f.card).length, 1);
|
||||
});
|
||||
|
||||
void test('stale, foreign and finished Charity requests reject before cost while an accepted packet disposes Sapho once', () => {
|
||||
|
||||
@@ -64,7 +64,9 @@ void test('an accepted claim, including canceled BG Charity, locks the current a
|
||||
const claimed = context([], ['a']);
|
||||
rejects(claimed, frame, () => reorder(claimed, frame, { event: stale, player: 'd', mode: 'first' }));
|
||||
rejects(claimed, frame, () => use(claimed, frame, 'a', 'first'));
|
||||
rejects(claimed, frame, () => use(claimed, frame, 'a', 'last'));
|
||||
const paused = use(claimed, frame, 'a', 'last');
|
||||
assert.equal(paused.suspended, 'a');
|
||||
assert.deepEqual(paused.order.remaining, ['b', 'c', 'd', 'a']);
|
||||
const first = use(claimed, frame, 'd', 'first');
|
||||
assert.equal(first.afterCurrent, 'a');
|
||||
assert.deepEqual(first.order.remaining, ['a', 'd', 'b', 'c']);
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { applyAction, handLimit, viewGame, type Action, type Game } from '../game/engine';
|
||||
import { SEMUTA_DRUG_ID } from '../game/semuta-drug';
|
||||
import {
|
||||
actSaphoCharityOrder as act, advanceSaphoCharityOrder as advance,
|
||||
createSaphoCharitySemutaFixture as fixture, reactSaphoCharitySemuta as react,
|
||||
saphoCharityOrderClean as clean, saphoCharityOrderInventory as inventory,
|
||||
saphoCharityOrderPlayer as player,
|
||||
} from './fixture-sapho-charity-order';
|
||||
|
||||
function reject(game: Game, actor: string, action: Action) {
|
||||
const before = structuredClone(game);
|
||||
assert.throws(() => applyAction(game, actor, action));
|
||||
assert.deepEqual(game, before);
|
||||
}
|
||||
|
||||
/** CONTROLLED supplemental custody: the inherited setup conserves original
|
||||
* Sapho/Semuta cards rather than earning their purchase, then only the one
|
||||
* physical Sapho is transferred to the eventual native Charity claimant. If
|
||||
* that hand is full, one ordinary held card is exchanged to the donor so both
|
||||
* retain legal hand counts. The bribe, Charity claim and all later actions are
|
||||
* executed by the real engine. */
|
||||
function claimedSource() {
|
||||
const f = fixture();
|
||||
assert.equal(f.nativeAcquisition, false);
|
||||
assert.ok(f.staging.some(row => row.includes('CONTROLLED')));
|
||||
const originalQueue = f.game.charityOrder!.remaining;
|
||||
const actor = originalQueue.slice(0, -1).find(id => id !== f.holder && id !== f.owner);
|
||||
assert.ok(actor, 'A non-Semuta, non-donor claimant retains a later opportunity');
|
||||
let game = advance(structuredClone(f.game), state => clean(state) &&
|
||||
state.phase === 2 && state.charityOrder?.remaining[0] === actor);
|
||||
const donor = player(game, f.owner), claimant = player(game, actor);
|
||||
const index = donor.hand.findIndex(card => card.id === f.card);
|
||||
assert.ok(index >= 0);
|
||||
const [sapho] = donor.hand.splice(index, 1);
|
||||
if (claimant.hand.length >= handLimit(claimant)) {
|
||||
const swap = claimant.hand.findIndex(card => card.id !== SEMUTA_DRUG_ID);
|
||||
assert.ok(swap >= 0);
|
||||
donor.hand.push(...claimant.hand.splice(swap, 1));
|
||||
}
|
||||
claimant.hand.push(sapho);
|
||||
assert.ok(donor.hand.length <= handLimit(donor));
|
||||
assert.ok(claimant.hand.length <= handLimit(claimant));
|
||||
assert.deepEqual(inventory(game), f.originalInventory);
|
||||
const recipient = game.players.find(seat => seat.id !== actor)!.id;
|
||||
const paid = claimant.spice;
|
||||
assert.ok(paid > 0, 'The claimant makes an actual bribe before Charity');
|
||||
game = act(game, actor, { type: 'bribe', target: recipient, amount: paid });
|
||||
assert.equal(player(game, actor).spice, 0);
|
||||
game = act(game, actor, { type: 'charity' });
|
||||
game = advance(game, clean);
|
||||
assert.equal(player(game, actor).charityTurn, game.turn);
|
||||
const option = viewGame(game, actor).saphoOptions.find(row => row.scope === 'charity' && row.mode === 'last');
|
||||
assert.ok(option && option.scope === 'charity' && option.resumeOwn);
|
||||
return { f, game, actor, action: { type: 'card', card: f.card,
|
||||
scope: 'charity', event: option.event, mode: 'last' } satisfies Action };
|
||||
}
|
||||
|
||||
for (const claim of [false, true]) void test(
|
||||
`controlled own Charity Sapho suspension survives Semuta ${claim ? 'claim' : 'pass'} without repeating the claim`, () => {
|
||||
const { f, game: original, actor, action } = claimedSource();
|
||||
const before = structuredClone(player(original, actor));
|
||||
const paused = act(original, actor, action);
|
||||
assert.equal(paused.pendingTreacheryDiscard?.continuation?.kind, 'saphoCharityDiscard');
|
||||
assert.equal(paused.pendingTreacheryDiscard?.reaction?.stage, 'offer');
|
||||
assert.ok(paused.charityOrder?.version === 3);
|
||||
assert.deepEqual(paused.charityOrder.paused, [actor]);
|
||||
assert.equal(paused.charityOrder.remaining.at(-1), actor);
|
||||
assert.equal(player(paused, actor).spice, before.spice);
|
||||
assert.equal(player(paused, actor).charityTurn, before.charityTurn);
|
||||
assert.equal(viewGame(paused, f.holder).semutaReaction?.canCommit, true);
|
||||
reject(paused, actor, { type: 'ready' });
|
||||
const frame = structuredClone(paused.charityOrder), ready = [...paused.ready];
|
||||
let game = react(paused, f.holder, claim);
|
||||
game = advance(game, clean);
|
||||
assert.deepEqual(game.charityOrder, frame);
|
||||
assert.deepEqual(game.ready, ready);
|
||||
assert.equal(game.active, frame.remaining[0]);
|
||||
assert.equal(player(game, actor).spice, before.spice);
|
||||
assert.equal(player(game, actor).charityTurn, before.charityTurn);
|
||||
assert.deepEqual(inventory(game), f.originalInventory);
|
||||
assert.equal(player(game, f.holder).hand.some(card => card.id === f.card), claim);
|
||||
assert.equal(game.discard.some(card => card.id === f.card), !claim);
|
||||
if (claim) assert.equal(game.discard.filter(card => card.id === SEMUTA_DRUG_ID).length, 1);
|
||||
game = advance(game, state => state.phase === 2 && clean(state) &&
|
||||
state.charityOrder?.remaining[0] === actor);
|
||||
assert.ok(game.charityOrder?.version === 3 && game.charityOrder.started);
|
||||
assert.equal(player(game, actor).spice, before.spice);
|
||||
reject(game, actor, { type: 'charity' });
|
||||
game = act(game, actor, { type: 'ready' });
|
||||
assert.deepEqual(inventory(game), f.originalInventory);
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,114 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { applyAction, viewGame, type Action, type Game } from '../game/engine';
|
||||
import { botActions } from '../game/bots';
|
||||
import { DIFFICULTIES } from '../game/bot-profiles';
|
||||
import {
|
||||
actSaphoCharityOrder as act, advanceSaphoCharityOrder as advance,
|
||||
createSaphoCharityOrderFixture as fixture, claimSaphoCharityOrder as claim,
|
||||
finishSaphoCharityOrder as finish, saphoCharityOrderClean as clean,
|
||||
saphoCharityOrderInventory as inventory, saphoCharityOrderPlayer as player,
|
||||
} from './fixture-sapho-charity-order';
|
||||
|
||||
function reject(game: Game, actor: string, action: Action) {
|
||||
const before = structuredClone(game);
|
||||
assert.throws(() => applyAction(game, actor, action));
|
||||
assert.deepEqual(game, before);
|
||||
}
|
||||
|
||||
/** The original Advanced setup and paid cache purchase lead to an actual
|
||||
* bribe and Charity claim. Only the already purchased Sapho changes hands:
|
||||
* the conserved physical card is transferred from Emperor to current Richese. */
|
||||
function source() {
|
||||
const f = fixture({ started: true });
|
||||
const game = structuredClone(f.game);
|
||||
const donor = player(game, f.owner);
|
||||
const index = donor.hand.findIndex(card => card.id === f.card);
|
||||
assert.ok(index >= 0);
|
||||
player(game, f.current).hand.push(...donor.hand.splice(index, 1));
|
||||
assert.ok(f.claim?.steps.some(step => step.action.type === 'bribe'));
|
||||
assert.ok(f.claim?.steps.some(step => step.action.type === 'charity'));
|
||||
assert.equal(player(game, f.current).charityTurn, game.turn);
|
||||
assert.equal(player(game, f.current).spice, 2);
|
||||
assert.deepEqual(inventory(game), f.originalInventory);
|
||||
return { f, game };
|
||||
}
|
||||
|
||||
function ownLast(game: Game, actor: string, card: string): Action {
|
||||
const option = viewGame(game, actor).saphoOptions.find(row => row.scope === 'charity' && row.mode === 'last');
|
||||
assert.ok(option && option.scope === 'charity' && option.resumeOwn);
|
||||
return { type: 'card', card, scope: 'charity', mode: 'last', event: option.event };
|
||||
}
|
||||
|
||||
void test('Advanced own Charity suspension retains the earned claim and resumes once after the remaining players', () => {
|
||||
const { f, game: original } = source();
|
||||
const before = structuredClone(player(original, f.current));
|
||||
const expected = [...original.charityOrder!.remaining.slice(1), f.current];
|
||||
const declaration = ownLast(original, f.current, f.card);
|
||||
assert.deepEqual(Object.keys(declaration).sort(), ['card', 'event', 'mode', 'scope', 'type']);
|
||||
reject(original, f.current, { ...declaration, event: 'stale-charity-event' });
|
||||
reject(original, f.current, { ...declaration, mode: 'first' });
|
||||
let game = act(original, f.current, declaration);
|
||||
assert.equal(game.charityOrder?.version, 3);
|
||||
assert.deepEqual(game.charityOrder?.remaining, expected);
|
||||
assert.ok(game.charityOrder?.version === 3);
|
||||
assert.deepEqual(game.charityOrder.paused, [f.current]);
|
||||
assert.equal(game.charityOrder.started, false);
|
||||
assert.deepEqual(game.order, original.order);
|
||||
assert.equal(player(game, f.current).spice, before.spice);
|
||||
assert.equal(player(game, f.current).charityTurn, before.charityTurn);
|
||||
assert.equal(game.discard.filter(card => card.id === f.card).length, 1);
|
||||
assert.deepEqual(inventory(game), f.originalInventory);
|
||||
reject(game, f.current, declaration);
|
||||
game = advance(game, state => state.phase === 2 && clean(state) && state.charityOrder?.remaining[0] === f.current);
|
||||
assert.ok(game.charityOrder?.version === 3);
|
||||
assert.equal(game.charityOrder.started, true);
|
||||
assert.equal(player(game, f.current).spice, before.spice);
|
||||
assert.equal(player(game, f.current).charityTurn, game.turn);
|
||||
reject(game, f.current, { type: 'charity' });
|
||||
game = act(game, f.current, { type: 'ready' });
|
||||
game = finish(game);
|
||||
assert.equal(game.phase, 3);
|
||||
assert.deepEqual(game.order, original.order);
|
||||
assert.equal(game.discard.filter(card => card.id === f.card).length, 1);
|
||||
assert.deepEqual(inventory(game), f.originalInventory);
|
||||
});
|
||||
|
||||
void test('the actual Sapho purchaser can pause its own genuinely claimed Charity turn', () => {
|
||||
const f = fixture();
|
||||
let game = advance(structuredClone(f.game), state => state.phase === 2 && clean(state) &&
|
||||
state.charityOrder?.remaining[0] === f.owner);
|
||||
assert.ok(game.charityOrder && game.charityOrder.remaining.length > 1);
|
||||
assert.ok(player(game, f.owner).hand.some(card => card.id === f.card));
|
||||
const result = claim(game, f.owner);
|
||||
assert.ok(result.steps.some(step => step.action.type === 'bribe'));
|
||||
assert.ok(result.steps.some(step => step.action.type === 'charity'));
|
||||
game = result.game;
|
||||
const before = structuredClone(player(game, f.owner));
|
||||
game = act(game, f.owner, ownLast(game, f.owner, f.card));
|
||||
assert.equal(game.charityOrder?.remaining.at(-1), f.owner);
|
||||
assert.equal(player(game, f.owner).spice, before.spice);
|
||||
assert.equal(player(game, f.owner).charityTurn, game.turn);
|
||||
game = advance(game, state => state.phase === 2 && clean(state) && state.charityOrder?.remaining[0] === f.owner);
|
||||
reject(game, f.owner, { type: 'charity' });
|
||||
game = finish(act(game, f.owner, { type: 'ready' }));
|
||||
assert.equal(game.phase, 3);
|
||||
assert.deepEqual(inventory(game), f.originalInventory);
|
||||
});
|
||||
|
||||
void test('all four minimal bot profiles can use a projected Advanced own Charity suspension', () => {
|
||||
const { f, game } = source();
|
||||
for (const difficulty of DIFFICULTIES) {
|
||||
const view = viewGame(game, f.current);
|
||||
view.players.find(seat => seat.id === f.current)!.bot = difficulty;
|
||||
const proposed = botActions(view).find(row => row.type === 'card' && row.card === f.card && row.scope === 'charity');
|
||||
assert.ok(proposed, `${difficulty} proposes the physical owned Sapho`);
|
||||
assert.equal('afterCurrent' in proposed, false);
|
||||
assert.equal('resumeOwn' in proposed, false);
|
||||
const next = act(game, f.current, proposed);
|
||||
assert.equal(next.charityOrder?.remaining.at(-1), f.current);
|
||||
assert.equal(player(next, f.current).spice, player(game, f.current).spice);
|
||||
assert.equal(player(next, f.current).charityTurn, game.turn);
|
||||
assert.equal(next.discard.filter(card => card.id === f.card).length, 1);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
CharityOrderError, createCharityOrder, finishCharityOrder,
|
||||
reorderSaphoCharityOrder, saphoCharityOrderEvent, startCharityOrder,
|
||||
validateCharityOrder, type CharityOrder, type CharityOrderContext,
|
||||
} from '../game/charity-order';
|
||||
|
||||
const order = ['a', 'b', 'c', 'd'];
|
||||
function context(ready: string[] = [], claimed: string[] = []): CharityOrderContext {
|
||||
return { turn: 5, order, players: order.map(id => ({ id })), ready, claimed };
|
||||
}
|
||||
function use(ctx: CharityOrderContext, frame: CharityOrder, player: string, mode: 'first' | 'last') {
|
||||
return reorderSaphoCharityOrder(ctx, frame, { event: saphoCharityOrderEvent(ctx, frame), player, mode });
|
||||
}
|
||||
|
||||
void test('a started Charity claim can pause last and resume the same started opportunity', () => {
|
||||
const initial = context(), frame = createCharityOrder(initial), claimed = context([], ['a']);
|
||||
assert.equal(JSON.stringify(frame), '{"version":1,"turn":5,"order":["a","b","c","d"],"remaining":["a","b","c","d"]}');
|
||||
const result = use(claimed, frame, 'a', 'last');
|
||||
assert.equal(result.suspended, 'a'); assert.equal(result.afterCurrent, undefined);
|
||||
assert.ok(result.order.version === 3);
|
||||
assert.deepEqual(result.order.remaining, ['b', 'c', 'd', 'a']);
|
||||
assert.deepEqual(result.order.paused, ['a']); assert.equal(result.order.started, false);
|
||||
validateCharityOrder(claimed, result.order);
|
||||
let current = result.order as CharityOrder, ready: string[] = [];
|
||||
for (const actor of ['b', 'c', 'd']) {
|
||||
current = finishCharityOrder(context(ready, ['a']), current, actor);
|
||||
ready = [...ready, actor];
|
||||
}
|
||||
assert.ok(current.version === 3); assert.equal(current.started, true);
|
||||
assert.deepEqual(current.paused, ['a']);
|
||||
current = finishCharityOrder(context(ready, ['a']), current, 'a');
|
||||
assert.ok(current.version === 3); assert.deepEqual(current.paused, []);
|
||||
assert.deepEqual(current.remaining, []); assert.equal(current.started, false);
|
||||
validateCharityOrder(context([...ready, 'a'], ['a']), current);
|
||||
});
|
||||
|
||||
void test('a prior version-two Sapho order retains its history when the current claimant suspends', () => {
|
||||
const ctx = context(), original = createCharityOrder(ctx);
|
||||
const prior = use(ctx, original, 'd', 'first').order;
|
||||
assert.ok(prior.version === 2); const saved = JSON.stringify(prior);
|
||||
const started = startCharityOrder(ctx, prior, 'd');
|
||||
const claimed = context([], ['d']); validateCharityOrder(claimed, started);
|
||||
const result = use(claimed, started, 'd', 'last');
|
||||
assert.ok(result.order.version === 3); assert.equal(result.order.uses.length, 2);
|
||||
assert.deepEqual(result.order.uses[0], prior.uses[0]);
|
||||
assert.deepEqual(result.order.remaining, ['a', 'b', 'c', 'd']);
|
||||
assert.equal(JSON.stringify(prior), saved);
|
||||
validateCharityOrder(claimed, JSON.parse(JSON.stringify(result.order)) as CharityOrder);
|
||||
});
|
||||
|
||||
void test('a paused claimant moved first by another genuine use resumes with its claim still spent', () => {
|
||||
const claimed = context([], ['a']);
|
||||
const paused = use(claimed, createCharityOrder(context()), 'a', 'last').order;
|
||||
const resumed = use(claimed, paused, 'a', 'first').order;
|
||||
assert.ok(resumed.version === 3); assert.equal(resumed.remaining[0], 'a');
|
||||
assert.equal(resumed.started, true); assert.deepEqual(resumed.paused, ['a']);
|
||||
const complete = finishCharityOrder(claimed, resumed, 'a');
|
||||
assert.ok(complete.version === 3); assert.deepEqual(complete.paused, []);
|
||||
validateCharityOrder(context(['a'], ['a']), complete);
|
||||
});
|
||||
|
||||
void test('started first, stale requests, altered custody and impossible history reject without mutation', () => {
|
||||
const ctx = context([], ['a']), original = createCharityOrder(context());
|
||||
const old = saphoCharityOrderEvent(context(), original);
|
||||
const before = structuredClone({ ctx, original });
|
||||
assert.throws(() => use(ctx, original, 'a', 'first'), CharityOrderError);
|
||||
assert.throws(() => reorderSaphoCharityOrder(ctx, original, { event: old, player: 'a', mode: 'last' }), CharityOrderError);
|
||||
const paused = use(ctx, original, 'a', 'last').order; assert.ok(paused.version === 3);
|
||||
assert.throws(() => validateCharityOrder(context(), paused), CharityOrderError);
|
||||
assert.throws(() => validateCharityOrder(ctx, { ...paused, paused: [] }), CharityOrderError);
|
||||
assert.throws(() => validateCharityOrder(ctx, { ...paused, remaining: ['c', 'b', 'd', 'a'] }), CharityOrderError);
|
||||
assert.deepEqual({ ctx, original }, before);
|
||||
});
|
||||
@@ -54,6 +54,12 @@ function press(option: Option, busy = false, card = sapho!.id): Action[] {
|
||||
SaphoOrderButton({ card, option, busy, act(action) { actions.push(action); } }).props.onClick();
|
||||
return actions;
|
||||
}
|
||||
function pressFinish(game: GameView, busy = false) {
|
||||
const sent: Action[] = [];
|
||||
const button = FinishRevivalTurn({ game, busy, act(action) { sent.push(action); } });
|
||||
button.props.onClick();
|
||||
return { sent, disabled: button.props.disabled };
|
||||
}
|
||||
|
||||
void test('clean first and last Revival choices use their own labels and preserve completed-turn guidance', () => {
|
||||
for (const mode of ['first', 'last'] as const) {
|
||||
@@ -106,6 +112,38 @@ void test('own started Revival pause explains retained allowances and sends the
|
||||
assert.deepEqual(press(option, true), []);
|
||||
});
|
||||
|
||||
void test('actual v3 Revival queue enables the current player and blocks the paused player', () => {
|
||||
const source = createSaphoRevivalOrderFixture({ started: true });
|
||||
let game = structuredClone(source.game);
|
||||
// Transfer the one physical card to the real claimant; no card is created.
|
||||
const donor = game.players.find(player => player.id === source.owner)!;
|
||||
const claimant = game.players.find(player => player.id === source.current)!;
|
||||
const cardIndex = donor.hand.findIndex(card => card.id === source.card);
|
||||
assert.ok(cardIndex >= 0);
|
||||
claimant.hand.push(donor.hand.splice(cardIndex, 1)[0]);
|
||||
const pause = viewGame(game, source.current).saphoOptions.find(option =>
|
||||
option.scope === 'revival' && option.mode === 'last');
|
||||
assert.ok(pause && pause.scope === 'revival' && pause.resumeOwn);
|
||||
game = actSaphoRevivalOrder(game, source.current, press(pause, false, source.card)[0]!);
|
||||
assert.ok(game.revivalOrder && game.revivalOrder.version === 3);
|
||||
let actor = game.revivalOrder.remaining[0];
|
||||
assert.notEqual(actor, source.current);
|
||||
const waiting = viewGame(game, source.current);
|
||||
assert.deepEqual(pressFinish(waiting), { sent: [], disabled: true });
|
||||
while (actor !== source.current) {
|
||||
const active = viewGame(game, actor);
|
||||
assert.match(revival(active), /Current Revival actor:/);
|
||||
const finish = pressFinish(active);
|
||||
assert.deepEqual(finish, { sent: [{ type: 'ready' }], disabled: false });
|
||||
game = actSaphoRevivalOrder(game, actor, finish.sent[0]!);
|
||||
actor = game.revivalOrder?.remaining[0] ?? '';
|
||||
assert.ok(actor);
|
||||
}
|
||||
const resumed = viewGame(game, source.current);
|
||||
assert.deepEqual(pressFinish(resumed), { sent: [{ type: 'ready' }], disabled: false });
|
||||
assert.deepEqual(pressFinish(resumed, true), { sent: [], disabled: true });
|
||||
});
|
||||
|
||||
void test('Basic unstamped and completed controlled views offer no Revival order button even with stale projected options', () => {
|
||||
const basic = projected(); basic.advanced = false; basic.revivalOrder = null;
|
||||
const legacy = projected(); legacy.revivalOrder = null;
|
||||
|
||||
Reference in new issue
Block a user