Allow retrying canceled Richese gifts
This commit is contained in:
1 parent
e5c7e338e8
commit
813a19bb1d
12 files changed
+156
-33
No files matched your search
@@ -287,7 +287,7 @@ void test('denied gift cancellation does not demand present custody, spare recip
|
||||
assert.equal(next.discard.filter((c) => c.id === f.gift.id).length, 1);
|
||||
assert.equal(player(next, 'h').hand.length, 9);
|
||||
});
|
||||
void test('current blocked gifts append once while a prior-phase blocked list is replaced', () => {
|
||||
void test('gift cancellation history remains unique on a repeated gift and prior-phase history is replaced', () => {
|
||||
const f = gift();
|
||||
f.g.richeseGiftBlocked = {
|
||||
turn: 2,
|
||||
@@ -299,6 +299,11 @@ void test('current blocked gifts append once while a prior-phase blocked list is
|
||||
(quote(f.g) as { blocked: unknown }).blocked,
|
||||
{ turn: 2, phase: 5, cards: ['richese-semuta-drug', f.gift.id] },
|
||||
);
|
||||
f.g.richeseGiftBlocked.cards.push(f.gift.id);
|
||||
const repeated = quote(f.g);
|
||||
assert.ok(repeated.kind === 'richeseGift');
|
||||
assert.deepEqual(repeated.blocked.cards, ['richese-semuta-drug', f.gift.id]);
|
||||
assert.deepEqual(repeated.resume, f.g.pendingRicheseGift!.resume);
|
||||
f.g.richeseGiftBlocked.phase = 4;
|
||||
const q = quote(f.g);
|
||||
assert.ok(q.kind === 'richeseGift');
|
||||
@@ -366,7 +371,10 @@ void test('canceled receipts and suspended control corruptions reject before car
|
||||
};
|
||||
},
|
||||
(g) => {
|
||||
g.richeseGiftBlocked = { turn: 2, phase: 5, cards: [source.gift.id] };
|
||||
g.richeseGiftBlocked = { turn: 2, phase: 5, cards: [source.gift.id, source.gift.id] };
|
||||
},
|
||||
(g) => {
|
||||
g.richeseGiftBlocked = { turn: 2, phase: 5, cards: ['not-a-richese-card'] };
|
||||
},
|
||||
];
|
||||
const random = t.mock.method(crypto, 'getRandomValues', () => {
|
||||
|
||||
@@ -90,7 +90,7 @@ void test('all profiles retain their card without public hand-space evidence or
|
||||
for (const difficulty of DIFFICULTIES) {
|
||||
for (const change of [
|
||||
(v: GameView) => {
|
||||
v.richeseGift!.blocked = 'Gift canceled this turn.';
|
||||
v.richeseGift!.blocked = 'Resolve the pending decision first.';
|
||||
},
|
||||
(v: GameView) => {
|
||||
v.richeseGift!.cards = [];
|
||||
@@ -144,7 +144,7 @@ void test('gift policy uses strict eligible identities and never inspects an all
|
||||
}
|
||||
});
|
||||
|
||||
void test('gift cancellation retains custody without an automatic retry, and Truthtrance and decisions take priority', () => {
|
||||
void test('gift cancellation retains custody and profiles retry legally while Truthtrance and decisions take priority', () => {
|
||||
for (const difficulty of DIFFICULTIES) {
|
||||
const g = fixture();
|
||||
const k = g.deck.find((c) => c.effect === 'karama')!;
|
||||
@@ -163,7 +163,14 @@ void test('gift cancellation retains custody without an automatic retry, and Tru
|
||||
true,
|
||||
);
|
||||
assert.equal(canceled.players[1].hand.length, 0);
|
||||
assert.equal(giftAction(projected(canceled, difficulty)), undefined);
|
||||
const retry = giftAction(projected(reload(canceled), difficulty));
|
||||
assert.ok(retry, difficulty);
|
||||
assert.equal(retry.card, 'richese-karama');
|
||||
const transferred = applyAction(reload(canceled), 'r', retry);
|
||||
assert.equal(transferred.response, null);
|
||||
assert.equal(transferred.players[0].hand.some((c) => c.id === retry.card), false);
|
||||
assert.deepEqual(transferred.players[1].hand.map((c) => c.id), [retry.card]);
|
||||
assert.equal(transferred.discard.some((c) => c.id === retry.card), false);
|
||||
const truth = projected(g, difficulty);
|
||||
truth.truthtrance = {
|
||||
stage: 'priority',
|
||||
|
||||
@@ -53,18 +53,54 @@ void test('pending exact gift is private to its two participants, never leaks ca
|
||||
assert.equal(next.log.some(entry => entry.text.includes(card.name) || entry.text.includes(card.id)), false);
|
||||
assert.deepEqual(next.players[1].hand, [card]);
|
||||
});
|
||||
void test('cancellation retains the exact card and parent decision; same-phase retry remains an explicit unresolved guard', () => {
|
||||
const g = fixture(); const card = take(g, 'richese-karama'), k = counter(g);
|
||||
void test('a canceled gift can be retried in the same phase, with a fresh counter window and original custody', () => {
|
||||
const g = fixture(); const card = take(g, 'richese-karama'), first = counter(g), second = counter(g);
|
||||
const parent = {kind: 'richeseDeclaration' as const, player: 'r'};
|
||||
g.decision = parent;
|
||||
const inventory = (state: Game) => [...state.deck, ...state.discard, ...state.richeseCache!, ...state.players.flatMap(p => p.hand)].map(c => c.id).sort();
|
||||
const original = inventory(g);
|
||||
const pending = gift(g, card.id);
|
||||
assert.equal(pending.decision, null);
|
||||
const next = applyAction(reload(pending), 'o', {type: 'card', mode: 'cancel', card: k.id});
|
||||
assert.deepEqual(next.decision, parent); assert.deepEqual(next.players[0].hand, [card]);
|
||||
assert.deepEqual(next.players[1].hand, []);
|
||||
assert.throws(() => gift(next, card.id), /awaits a ruling/);
|
||||
next.phase = 4; next.decision = null;
|
||||
assert.deepEqual(gift(next, card.id).players[1].hand, [card]);
|
||||
const canceled = applyAction(reload(pending), 'o', {type: 'card', mode: 'cancel', card: first.id});
|
||||
assert.deepEqual(canceled.decision, parent);
|
||||
assert.deepEqual(canceled.players[0].hand, [card]);
|
||||
assert.deepEqual(canceled.players[1].hand, []);
|
||||
assert.equal(canceled.discard.filter(c => c.id === first.id).length, 1);
|
||||
assert.ok(viewGame(canceled, 'r').richeseGift!.cards.some(c => c.id === card.id));
|
||||
const retry = gift(reload(canceled), card.id);
|
||||
assert.equal(retry.response?.kind, 'richeseGift');
|
||||
assert.notEqual(retry.pendingRicheseGift!.event, pending.pendingRicheseGift!.event);
|
||||
assert.deepEqual(retry.players[0].hand, [card]);
|
||||
assert.ok(retry.players[2].hand.some(c => c.id === second.id));
|
||||
assert.deepEqual(inventory(retry), original);
|
||||
const allowed = applyAction(reload(retry), 'o', {type: 'passResponse'});
|
||||
assert.deepEqual(allowed.decision, parent);
|
||||
assert.deepEqual(allowed.players[0].hand, []);
|
||||
assert.deepEqual(allowed.players[1].hand, [card]);
|
||||
assert.ok(allowed.players[2].hand.some(c => c.id === second.id));
|
||||
assert.deepEqual(inventory(allowed), original);
|
||||
assert.throws(() => gift(allowed, card.id));
|
||||
});
|
||||
void test('repeated same-phase cancellation costs each physical Karama once and allows the original gift afterward', () => {
|
||||
const g = fixture(); const card = take(g, 'richese-distrans'), first = counter(g), second = counter(g);
|
||||
const inventory = (state: Game) => [...state.deck, ...state.discard, ...state.richeseCache!, ...state.players.flatMap(p => p.hand)].map(c => c.id).sort();
|
||||
const original = inventory(g);
|
||||
let current = g;
|
||||
for (const k of [first, second]) {
|
||||
const pending = gift(reload(current), card.id);
|
||||
assert.equal(pending.response?.kind, 'richeseGift');
|
||||
current = applyAction(reload(pending), 'o', {type: 'card', mode: 'cancel', card: k.id});
|
||||
assert.equal(current.phase, g.phase);
|
||||
assert.deepEqual(current.players[0].hand, [card]);
|
||||
assert.deepEqual(current.players[1].hand, []);
|
||||
assert.equal(current.discard.filter(c => c.id === k.id).length, 1);
|
||||
assert.deepEqual(inventory(current), original);
|
||||
}
|
||||
const transferred = gift(reload(current), card.id);
|
||||
assert.deepEqual(transferred.players[0].hand, []);
|
||||
assert.deepEqual(transferred.players[1].hand, [card]);
|
||||
assert.equal(transferred.discard.filter(c => c.id === first.id || c.id === second.id).length, 2);
|
||||
assert.deepEqual(inventory(transferred), original);
|
||||
});
|
||||
void test('capacity changed during recovery aborts without consuming, discarding or replacing any hand card', () => {
|
||||
const g = fixture(); const card = take(g, 'richese-karama'); counter(g);
|
||||
|
||||
@@ -89,7 +89,21 @@ function settled(state: Game, f: Source, claimed: boolean) {
|
||||
assert.deepEqual(game.phaseOpening, f.originalParent.phaseOpening);
|
||||
assert.deepEqual(game.pendingKarama, f.originalParent.pendingKarama);
|
||||
assert.ok(player(game, f.observer).hand.some(card => card.id === f.parentCounter));
|
||||
reject(game, f.donor, f.giftAction);
|
||||
if (claimed) {
|
||||
// Claiming spent the selected Semuta, so it cannot be offered again.
|
||||
reject(game, f.donor, f.giftAction);
|
||||
} else {
|
||||
// The prevented card is still held and can be re-offered in this phase.
|
||||
// Fork the state so the original parent-continuation checks remain intact.
|
||||
const before = structuredClone(game);
|
||||
const offered = applyAction(game, f.donor, f.giftAction);
|
||||
assert.deepEqual(game, before);
|
||||
assert.equal(offered.response?.kind, 'richeseGift');
|
||||
assert.equal(offered.pendingRicheseGift?.intent.cardId, f.semuta);
|
||||
assert.ok(player(offered, f.donor).hand.some(card => card.id === f.semuta));
|
||||
assert.ok(player(offered, f.observer).hand.some(card => card.id === f.parentCounter));
|
||||
assert.deepEqual(inventory(offered), f.originalInventory);
|
||||
}
|
||||
return game;
|
||||
}
|
||||
|
||||
|
||||
@@ -81,7 +81,12 @@ function prevented(game: Game, f: SemutaRicheseGiftKaramaFixture, claimed: boole
|
||||
assert.deepEqual(done.response, f.originalParent.response); assert.deepEqual(done.decision, f.originalParent.decision);
|
||||
assert.deepEqual(done.phaseOpening, f.originalParent.phaseOpening);
|
||||
assert.deepEqual(done.pendingKarama, f.originalParent.pendingKarama);
|
||||
reject(done, f.donor, f.giftAction);
|
||||
assert.ok(viewGame(done, f.donor).richeseGift?.cards.some(card => card.id === f.gift));
|
||||
const preserved = structuredClone(done);
|
||||
const retry = applyAction(structuredClone(done), f.donor, f.giftAction);
|
||||
assert.equal(retry.response?.kind, 'richeseGift');
|
||||
assert.equal(retry.pendingRicheseGift?.intent.cardId, f.gift);
|
||||
assert.deepEqual(done, preserved);
|
||||
return done;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user