Offer Semuta choice after winner battle card batch

This commit is contained in:
Quintine committed 2026-09-30 09:05:45 +10:00
1 parent 34390bb78b
commit 593ca3beb4
10 files changed
+149 -63

No files matched your search

+42 -5
View File
@@ -495,15 +495,16 @@ void test('a completed clean Ornithopter flight offers the retired card before i
assert.throws(() => applyAction(claimed, 'r', { type: 'semutaCommit', event }));
});
void test('a clean winner-selected battle card can be claimed after the resolved battle', () => {
void test('clean winner-selected battle cards allow one committed Semuta choice after resolution', () => {
const { g, semuta, hajr } = fixture();
player(g, 'a').hand = [];
g.deck.push(hajr);
const weaponIndex = g.deck.findIndex(card => card.kind === 'projectile');
const [weapon] = g.deck.splice(weaponIndex, 1);
const [shield] = g.deck.splice(g.deck.findIndex(card => card.kind === 'shield'), 1);
const [defense] = g.deck.splice(g.deck.findIndex(card => card.kind === 'snooper'), 1);
assert.ok(weapon && defense);
player(g, 'a').hand.push(weapon);
assert.ok(weapon && shield && defense);
player(g, 'a').hand.push(weapon, shield);
player(g, 'e').hand.push(defense);
for (const id of ['a', 'e']) {
player(g, id).forces = { 'arrakeen:10': 5 };
@@ -528,13 +529,13 @@ void test('a clean winner-selected battle card can be claimed after the resolved
type: 'battlePreparationReady', event: battle.battle!.preLeader!.event,
});
battle = applyAction(battle, 'a', { type: 'battlePlan', dial: 2, support: 2,
leader: player(battle, 'a').leaders[0].id, weapon: weapon.id });
leader: player(battle, 'a').leaders[0].id, weapon: weapon.id, defense: shield.id });
battle = applyAction(battle, 'e', { type: 'battlePlan', dial: 0, support: 0,
leader: player(battle, 'e').leaders[0].id, defense: defense.id });
battle = applyAction(battle, 'a', { type: 'traitorCall', call: false });
battle = applyAction(battle, 'e', { type: 'traitorCall', call: false });
assert.equal(battle.decision?.kind, 'battleCards');
const pending = applyAction(battle, 'a', { type: 'decision', discard: [weapon.id] });
const pending = applyAction(reload(battle), 'a', { type: 'decision', discard: [weapon.id] });
assert.equal(pending.pendingTreacheryDiscard?.batch.cause, 'battle:winner');
assert.equal(pending.pendingTreacheryDiscard?.reaction?.stage, 'offer');
assert.equal(pending.discard.filter(card => card.id === weapon.id).length, 1);
@@ -562,4 +563,40 @@ void test('a clean winner-selected battle card can be claimed after the resolved
for (const p of claimed.players)
assert.deepEqual(viewGame(reload(claimed), p.id), viewGame(claimed, p.id));
assert.throws(() => applyAction(claimed, 'r', { type: 'semutaCommit', event }));
const multiple = applyAction(reload(battle), 'a', {
type: 'decision', discard: [weapon.id, shield.id],
});
assert.equal(multiple.pendingTreacheryDiscard?.batch.entries.length, 2);
assert.equal(multiple.pendingTreacheryDiscard?.reaction?.stage, 'offer');
assert.deepEqual(viewGame(multiple, 'e').semutaReaction?.candidates, []);
const multiEvent = multiple.pendingTreacheryDiscard!.batch.event;
const committed = applyAction(reload(multiple), 'r', { type: 'semutaCommit', event: multiEvent });
assert.equal(committed.pendingTreacheryDiscard?.reaction?.stage, 'select');
assert.deepEqual(viewGame(committed, 'r').semutaReaction?.candidates.map(card => card.id),
[weapon.id, shield.id]);
assert.deepEqual(viewGame(committed, 'a').semutaReaction?.candidates, []);
assert.equal(player(committed, 'r').hand.some(card => card.id === semuta.id), true);
assert.equal(committed.discard.filter(card => card.id === weapon.id || card.id === shield.id).length, 2);
const snapshot = JSON.stringify(committed);
assert.throws(() => applyAction(committed, 'e', {
type: 'semutaSelect', event: multiEvent, card: weapon.id,
}));
assert.equal(JSON.stringify(committed), snapshot);
assert.deepEqual(normalizeAutomaticGame(reload(committed)), reload(committed));
const selected = applyAction(reload(committed), 'r', botActions(viewGame(committed, 'r'))[0]!);
assert.equal(selected.pendingTreacheryDiscard, null);
assert.equal(player(selected, 'r').hand.filter(card => card.id === weapon.id).length, 1);
assert.equal(selected.discard.some(card => card.id === shield.id), true);
assert.equal(selected.discard.filter(card => card.id === semuta.id).length, 1);
assert.deepEqual(inventory(selected), original);
assert.deepEqual(settled(selected), settled(declined));
const alternate = applyAction(reload(committed), 'r', {
type: 'semutaSelect', event: multiEvent, card: shield.id,
});
assert.equal(alternate.pendingTreacheryDiscard, null);
assert.equal(player(alternate, 'r').hand.filter(card => card.id === shield.id).length, 1);
assert.equal(alternate.discard.filter(card => card.id === weapon.id).length, 1);
assert.equal(alternate.discard.filter(card => card.id === semuta.id).length, 1);
assert.deepEqual(inventory(alternate), original);
assert.deepEqual(settled(alternate), settled(declined));
});
+21 -7
View File
@@ -324,10 +324,12 @@ void test('authenticated clean Semuta producers claim once through restart and r
seat(battleStart, ids[0]).hand.push(battleSemuta);
const weapon = battleStart.deck.splice(
battleStart.deck.findIndex(card => card.kind === 'projectile'), 1)[0];
const shield = battleStart.deck.splice(
battleStart.deck.findIndex(card => card.kind === 'shield'), 1)[0];
const defense = battleStart.deck.splice(
battleStart.deck.findIndex(card => card.kind === 'snooper'), 1)[0];
assert.ok(weapon && defense);
seat(battleStart, ids[1]).hand.push(weapon);
assert.ok(weapon && shield && defense);
seat(battleStart, ids[1]).hand.push(weapon, shield);
seat(battleStart, ids[2]).hand.push(defense);
for (const id of [ids[1], ids[2]]) {
const p = seat(battleStart, id);
@@ -353,24 +355,36 @@ void test('authenticated clean Semuta producers claim once through restart and r
for (const index of [1, 2])
duel = await act(index, { type: 'battlePreparationReady', event: duel.battle!.preLeader!.event });
duel = await act(1, { type: 'battlePlan', dial: 2, support: 2,
leader: seat(duel, ids[1]).leaders[0].id, weapon: weapon.id });
leader: seat(duel, ids[1]).leaders[0].id,
weapon: weapon.id, defense: shield.id });
duel = await act(2, { type: 'battlePlan', dial: 0, support: 0,
leader: seat(duel, ids[2]).leaders[0].id, defense: defense.id });
duel = await act(1, { type: 'traitorCall', call: false });
duel = await act(2, { type: 'traitorCall', call: false });
assert.equal(duel.decision?.kind, 'battleCards');
const chosen = await act(1, { type: 'decision', discard: [weapon.id] });
const chosen = await act(1, { type: 'decision', discard: [weapon.id, shield.id] });
assert.equal(chosen.pendingTreacheryDiscard?.batch.cause, 'battle:winner');
assert.equal(chosen.pendingTreacheryDiscard?.reaction?.stage, 'offer');
await f.restart().continueRoomAutomatic(code, clock);
assert.equal((await f.restart().readRoom(code)).version, chosen.version);
assert.equal((await f.restart().readSeatView(code, auths[0])).semutaReaction?.canCommit, true);
const battleClaim = await act(0, { type: 'semutaCommit',
const committed = await act(0, { type: 'semutaCommit',
event: chosen.pendingTreacheryDiscard!.batch.event });
assert.equal(committed.pendingTreacheryDiscard?.reaction?.stage, 'select');
await f.restart().continueRoomAutomatic(code, clock);
assert.equal((await f.restart().readRoom(code)).version, committed.version);
const ownerSelection = await f.restart().readSeatView(code, auths[0]);
const rivalSelection = await f.restart().readSeatView(code, auths[2]);
assert.deepEqual(ownerSelection.semutaReaction?.candidates.map(card => card.id),
[weapon.id, shield.id]);
assert.deepEqual(rivalSelection.semutaReaction?.candidates, []);
const battleClaim = await act(0, { type: 'semutaSelect',
event: chosen.pendingTreacheryDiscard!.batch.event, card: shield.id });
assert.equal(battleClaim.pendingTreacheryDiscard, null);
assert.equal(seat(battleClaim, ids[0]).hand.filter(card => card.id === weapon.id).length, 1);
assert.equal(seat(battleClaim, ids[0]).hand.filter(card => card.id === shield.id).length, 1);
assert.equal(battleClaim.discard.filter(card => card.id === SEMUTA_DRUG_ID).length, 1);
assert.equal(battleClaim.discard.some(card => card.id === weapon.id), false);
assert.equal(battleClaim.discard.filter(card => card.id === weapon.id).length, 1);
assert.equal(battleClaim.discard.some(card => card.id === shield.id), false);
assert.deepEqual(f.sqlite.prepare('SELECT * FROM seats').all(), originalSeats);
for (const auth of auths)
assert.deepEqual(await f.restart().readSeatView(code, auth), viewGame(battleClaim, auth.playerId));