Support multiple named Ghola commitments
Verify and publish checkpoint container / container (push) Canceled after 0s
Verify and publish checkpoint container / container (push) Canceled after 0s
This commit is contained in:
1 parent
6c1530382d
commit
bdd5aaf18c
8 files changed
+286
-26
No files matched your search
@@ -40,3 +40,27 @@ void test('the Ghola guide forwards only the current legal card and leader', ()
|
||||
game.ghola.available = true; game.truthtrance = { stage: 'priority', queue: [], passed: [], question: null } as typeof game.truthtrance;
|
||||
button.props.onClick(); assert.equal(sent.length, 1);
|
||||
});
|
||||
|
||||
void test('Ghola guide starts the current Box and rejects a stale quote', () => {
|
||||
const game = projected(), me = game.players.find(player => player.id === 'p')!;
|
||||
const box = { id: 'box', name: 'Nullentropy Box', kind: 'special' as const, effect: 'nullentropyBox' };
|
||||
me.hand = [box]; game.nullentropy = { card: box, blocked: null, search: null };
|
||||
game.gholaLeaderPromiseGuide = { action: { type: 'card', card: box.id } };
|
||||
const sent: Action[] = [], button = GholaLeaderPromiseGuideButton({ game, busy: false, send(action) { sent.push(action); } });
|
||||
assert.ok(button); assert.equal(button.props.disabled, false); button.props.onClick();
|
||||
assert.deepEqual(sent, [game.gholaLeaderPromiseGuide.action]);
|
||||
game.nullentropy.blocked = 'The search is no longer available.'; button.props.onClick();
|
||||
assert.equal(sent.length, 1);
|
||||
});
|
||||
|
||||
void test('Ghola guide selects only its current paid Box search card', () => {
|
||||
const game = projected(), box = { id: 'box', name: 'Nullentropy Box', kind: 'special' as const, effect: 'nullentropyBox' };
|
||||
const ghola = { id: 'recovered', name: 'Ghola', kind: 'special' as const, effect: 'ghola' };
|
||||
game.decision = { kind: 'nullentropy', player: 'p' };
|
||||
game.nullentropy = { card: box, blocked: null, search: { event: 'paid', cards: [ghola] } };
|
||||
game.gholaLeaderPromiseGuide = { action: { type: 'decision', event: 'paid', card: ghola.id } };
|
||||
const sent: Action[] = [], button = GholaLeaderPromiseGuideButton({ game, busy: false, send(action) { sent.push(action); } });
|
||||
assert.ok(button); assert.equal(button.props.disabled, false); button.props.onClick();
|
||||
assert.deepEqual(sent, [game.gholaLeaderPromiseGuide.action]);
|
||||
game.nullentropy.search!.cards = []; button.props.onClick(); assert.equal(sent.length, 1);
|
||||
});
|
||||
@@ -0,0 +1,112 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { viewGame, type Action, type Game } from '../game/engine';
|
||||
import { botActions } from '../game/bots';
|
||||
import { DIFFICULTIES } from '../game/bot-profiles';
|
||||
import { actTruthMovementGhola as act, advanceTruthMovementGhola as advance,
|
||||
createTruthMovementBoxGholaFixture as fixture, truthMovementGholaClean as clean,
|
||||
truthMovementGholaInventory as inventory, truthMovementGholaPlayer as player } from './fixture-truth-movement-ghola';
|
||||
|
||||
/** Original native Richese Box source, with controlled physical custody and
|
||||
* two staged dead Atreides leaders. Each card is moved, never copied; this is
|
||||
* a targeted composition fixture, not evidence of earned leader deaths. */
|
||||
function source(withBox: boolean) {
|
||||
const f = fixture(), game = structuredClone(f.game);
|
||||
const actor = player(game, f.target), asker = player(game, f.asker);
|
||||
const gholaIndex = game.discard.findIndex(card => card.id === f.ghola);
|
||||
assert.ok(gholaIndex >= 0);
|
||||
actor.hand.push(game.discard.splice(gholaIndex, 1)[0]);
|
||||
if (!withBox) {
|
||||
const index = actor.hand.findIndex(card => card.id === f.box); assert.ok(index >= 0);
|
||||
game.richeseCache!.push(actor.hand.splice(index, 1)[0]);
|
||||
}
|
||||
const otherTruth = game.deck.findIndex(card => card.effect === 'truthtrance');
|
||||
assert.ok(otherTruth >= 0, 'The second physical Truthtrance must remain in the undealt deck');
|
||||
asker.hand.push(game.deck.splice(otherTruth, 1)[0]);
|
||||
const truths = asker.hand.filter(card => card.effect === 'truthtrance');
|
||||
assert.equal(truths.length, 2);
|
||||
const leaders = actor.leaders.slice(0, 2);
|
||||
for (const leader of leaders) { leader.dead = true; leader.deaths = 1; delete leader.usedAt; }
|
||||
assert.deepEqual(inventory(game), f.originalCards);
|
||||
return { ...f, game, leaders: leaders.map(leader => leader.id), truths: truths.map(card => card.id) };
|
||||
}
|
||||
function declared(f: ReturnType<typeof source>) {
|
||||
let game = act(f.game, f.asker, { type:'card', card:f.truths[0], cards:[f.truths[1]] });
|
||||
for (let count = 0; game.truthtrance?.stage === 'priority' && count < game.players.length; count++) {
|
||||
const seat = game.players.find(p => !game.truthtrance!.passed.includes(p.id)); assert.ok(seat);
|
||||
game = act(game, seat.id, { type:'truthPass' });
|
||||
}
|
||||
assert.equal(game.truthtrance?.stage, 'ask');
|
||||
return game;
|
||||
}
|
||||
function settleDiscard(game: Game): Game {
|
||||
for (let count = 0; game.pendingTreacheryDiscard && count < 20; count++) {
|
||||
const pending = game.pendingTreacheryDiscard;
|
||||
if (pending.reaction?.stage === 'offer') {
|
||||
const passed = pending.reaction.passed;
|
||||
const actor = game.players.find(p => !passed.includes(p.id)); assert.ok(actor);
|
||||
game = act(game, actor.id, { type:'semutaPass', event:pending.batch.event });
|
||||
} else {
|
||||
assert.ok(!pending.reaction, 'A neutral pass does not select a card');
|
||||
game = act(game, game.host, { type:'advanceBots' });
|
||||
}
|
||||
}
|
||||
assert.equal(game.pendingTreacheryDiscard, null);
|
||||
return game;
|
||||
}
|
||||
function ask(game: Game, f: ReturnType<typeof source>, leader: string) {
|
||||
return act(game, f.asker, { type:'truthAsk', question: {kind:'gholaLeader', target:f.target, turn:game.turn, leader} });
|
||||
}
|
||||
function answer(game: Game, actor: string, value: 'yes' | 'no') {
|
||||
return act(game, actor, {type:'truthAnswer', answer:value});
|
||||
}
|
||||
function reject(game: Game, actor: string, action: Action) {
|
||||
const before = structuredClone(game); assert.throws(() => act(game, actor, action)); assert.deepEqual(game, before);
|
||||
}
|
||||
|
||||
void test('two distinct Ghola Yes answers require a real recovery route', () => {
|
||||
const f = source(false);
|
||||
let game = answer(ask(declared(f), f, f.leaders[0]), f.target, 'yes');
|
||||
game = ask(settleDiscard(game), f, f.leaders[1]);
|
||||
assert.ok(!viewGame(game, f.target).truthGholaLeaderAnswers?.includes('yes'));
|
||||
reject(game, f.target, {type:'truthAnswer', answer:'yes'});
|
||||
game = answer(game, f.target, 'no');
|
||||
assert.equal(game.gholaLeaderPromises!.filter(row => row.answer).length, 1);
|
||||
assert.deepEqual(inventory(game), f.originalCards);
|
||||
});
|
||||
|
||||
void test('two named Ghola Yes answers reuse one physical card through the paid Box', () => {
|
||||
const f = source(true);
|
||||
let game = answer(ask(declared(f), f, f.leaders[0]), f.target, 'yes');
|
||||
game = ask(settleDiscard(game), f, f.leaders[1]);
|
||||
assert.ok(viewGame(game, f.target).truthGholaLeaderAnswers?.includes('yes'));
|
||||
game = answer(game, f.target, 'yes');
|
||||
assert.equal(game.gholaLeaderPromises!.filter(row => row.answer && !row.fulfilled).length, 2);
|
||||
game = advance(game, clean);
|
||||
const firstGuide = viewGame(game, f.target).gholaLeaderPromiseGuide;
|
||||
assert.ok(firstGuide); assert.equal(firstGuide.action.type, 'card');
|
||||
for (const difficulty of DIFFICULTIES) {
|
||||
const view = viewGame(game, f.target);
|
||||
view.players.find(seat => seat.id === f.target)!.bot = difficulty;
|
||||
assert.deepEqual(botActions(view)[0], firstGuide.action);
|
||||
}
|
||||
game = act(game, f.target, firstGuide.action);
|
||||
game = advance(game, clean);
|
||||
assert.ok(game.discard.some(card => card.id === f.ghola));
|
||||
assert.equal(game.gholaLeaderPromises!.filter(row => row.fulfilled).length, 1);
|
||||
const recovery = viewGame(game, f.target).gholaLeaderPromiseGuide;
|
||||
assert.ok(recovery, 'The owner receives the next Box preparation');
|
||||
assert.equal(recovery.action.type, 'card'); assert.equal(recovery.action.card, f.box);
|
||||
game = act(game, f.target, recovery.action);
|
||||
if (game.decision?.kind === 'nullentropy')
|
||||
game = act(game, f.target, {type:'decision', event:viewGame(game, f.target).nullentropy!.search!.event, card:f.ghola});
|
||||
game = advance(game, clean);
|
||||
assert.ok(player(game, f.target).hand.some(card => card.id === f.ghola));
|
||||
const secondGuide = viewGame(game, f.target).gholaLeaderPromiseGuide;
|
||||
assert.ok(secondGuide); assert.equal(secondGuide.action.card, f.ghola);
|
||||
assert.notEqual(secondGuide.action.leader, firstGuide.action.leader);
|
||||
game = advance(act(game, f.target, secondGuide.action), clean);
|
||||
assert.ok(f.leaders.every(id => !player(game, f.target).leaders.find(leader => leader.id === id)!.dead));
|
||||
assert.equal(game.gholaLeaderPromises!.filter(row => row.answer && row.fulfilled).length, 2);
|
||||
assert.deepEqual(inventory(game), f.originalCards);
|
||||
});
|
||||
Reference in new issue
Block a user