Offer Semuta after clean Residual Poison discard
This commit is contained in:
1 parent
ab5c78226a
commit
32d9ef4ec9
10 files changed
+235
-23
No files matched your search
@@ -11,6 +11,7 @@ import {
|
||||
} from '../game/engine';
|
||||
import { baseDeck, leaders } from '../game/cards';
|
||||
import { richeseCards } from '../game/richese-cards';
|
||||
import { SEMUTA_DRUG_ID } from '../game/semuta-drug';
|
||||
import { createDukeVidal, acquireDuke } from '../game/duke-vidal';
|
||||
|
||||
function fixture() {
|
||||
@@ -88,6 +89,52 @@ const inventory = (g: Game) =>
|
||||
.map((c) => c.id)
|
||||
.sort();
|
||||
|
||||
void test('clean Residual Poison death offers Semuta once before shared battle preparation', () => {
|
||||
let g = fixture();
|
||||
g.semutaPreview = true;
|
||||
hold(g, 'e', 'Semuta Drug');
|
||||
const original = inventory(g);
|
||||
g = poison(start(g));
|
||||
const event = g.pendingTreacheryDiscard!.batch.event;
|
||||
assert.equal(g.pendingTreacheryDiscard?.reaction?.stage, 'offer');
|
||||
assert.equal(g.players[1].leaders.filter(l => l.dead).length, 1);
|
||||
assert.equal(g.battle!.preLeader?.closed, false);
|
||||
assert.equal(viewGame(g, 'e').semutaReaction?.canCommit, true);
|
||||
assert.equal(viewGame(g, 'g').semutaReaction?.canCommit, false);
|
||||
assert.deepEqual(normalizeAutomaticGame(reload(g)), reload(g));
|
||||
const corrupt = reload(g);
|
||||
const victim = corrupt.players[1].leaders.find(l => l.dead)!;
|
||||
victim.dead = false;
|
||||
const before = reload(corrupt);
|
||||
assert.throws(() => send(corrupt, 'e', { type: 'semutaCommit', event }));
|
||||
assert.deepEqual(corrupt, before);
|
||||
let declined = reload(g);
|
||||
for (const id of ['r', 'g', 'e'])
|
||||
declined = send(declined, id, { type: 'semutaPass', event });
|
||||
assert.equal(declined.pendingTreacheryDiscard, null);
|
||||
assert.deepEqual(inventory(declined), original);
|
||||
const claimed = send(reload(g), 'e', { type: 'semutaCommit', event });
|
||||
assert.equal(claimed.pendingTreacheryDiscard, null);
|
||||
assert.equal(claimed.players[1].leaders.filter(l => l.dead).length, 1);
|
||||
assert.equal(claimed.players[2].hand.filter(c => c.id === 'richese-residual-poison').length, 1);
|
||||
assert.equal(claimed.discard.filter(c => c.id === SEMUTA_DRUG_ID).length, 1);
|
||||
assert.deepEqual(inventory(claimed), original);
|
||||
assert.deepEqual(claimed.battle!.preLeader?.ready, []);
|
||||
assert.equal(ready(claimed, 'r').battle!.preLeader?.ready.includes('r'), true);
|
||||
assert.throws(() => send(claimed, 'e', { type: 'semutaCommit', event }));
|
||||
});
|
||||
|
||||
void test('an already-ready opponent retains the automatic Residual Poison suffix', () => {
|
||||
let g = fixture();
|
||||
g.semutaPreview = true;
|
||||
hold(g, 'e', 'Semuta Drug');
|
||||
g = poison(ready(start(g), 'g'));
|
||||
assert.equal(g.pendingTreacheryDiscard ?? null, null);
|
||||
assert.deepEqual(g.battle?.preLeader?.ready, ['g']);
|
||||
assert.equal(g.players[1].leaders.filter(l => l.dead).length, 1);
|
||||
assert.equal(g.discard.filter(c => c.id === 'richese-residual-poison').length, 1);
|
||||
});
|
||||
|
||||
void test('a fast opponent cannot commit before both genuine preparation declarations, independent of card possession', () => {
|
||||
const withCard = start(fixture());
|
||||
const noCard = fixture();
|
||||
|
||||
@@ -10,6 +10,7 @@ import * as bots from '../game/bots';
|
||||
import * as seatAiDelegation from '../lib/seat-ai-delegation';
|
||||
import { baseDeck } from '../game/cards';
|
||||
import { richeseCards } from '../game/richese-cards';
|
||||
import { SEMUTA_DRUG_ID } from '../game/semuta-drug';
|
||||
import type * as Rooms from '../db/rooms';
|
||||
|
||||
/** Execute the production room module and SQL, with a hook immediately before its CAS. */
|
||||
@@ -94,7 +95,7 @@ function unitStore(runBots = bots.runBots) {
|
||||
|
||||
const clock: Rooms.RoomsClock = { now: () => 10000, sleep: async () => {} };
|
||||
|
||||
async function fixture() {
|
||||
async function fixture(semutaPreview = false) {
|
||||
const store = unitStore();
|
||||
const created = await store.rooms.createRoom('Holder', 'guild', false, []),
|
||||
code = created.view.code;
|
||||
@@ -145,6 +146,12 @@ async function fixture() {
|
||||
target: other.playerId,
|
||||
territory: 'arrakeen',
|
||||
});
|
||||
if (semutaPreview) {
|
||||
initial.semutaPreview = true;
|
||||
const semuta = initial.richeseCache!.findIndex(c => c.id === SEMUTA_DRUG_ID);
|
||||
assert.ok(semuta >= 0);
|
||||
initial.players[2].hand.push(initial.richeseCache!.splice(semuta, 1)[0]);
|
||||
}
|
||||
const save = (g: engine.Game) =>
|
||||
store.sqlite
|
||||
.prepare('UPDATE rooms SET state=?,version=? WHERE code=?')
|
||||
@@ -343,3 +350,35 @@ void test('two simultaneous ready declarations require refreshed retry; stale ev
|
||||
f.sqlite.close();
|
||||
}
|
||||
});
|
||||
|
||||
void test('authenticated Residual Poison death pauses for a neutral Semuta offer before shared preparation', async () => {
|
||||
const f = await fixture(true);
|
||||
try {
|
||||
const seats = f.sqlite.prepare('SELECT * FROM seats').all();
|
||||
const before = await f.restart().readRoom(f.code);
|
||||
await f.restart().act(f.code, f.owner, before.version, f.action, clock);
|
||||
let g = await f.restart().readRoom(f.code);
|
||||
assert.equal(g.pendingTreacheryDiscard?.reaction?.stage, 'offer');
|
||||
assert.equal(g.players[1].leaders.filter(l => l.dead).length, 1);
|
||||
assert.equal(g.battle?.preLeader?.closed, false);
|
||||
const event = g.pendingTreacheryDiscard!.batch.event;
|
||||
const offered = g.version;
|
||||
await f.restart().continueRoomAutomatic(f.code, clock);
|
||||
assert.equal((await f.restart().readRoom(f.code)).version, offered);
|
||||
assert.equal((await f.restart().readSeatView(f.code, f.observer)).semutaReaction?.canCommit, true);
|
||||
assert.equal((await f.restart().readSeatView(f.code, f.other)).semutaReaction?.canCommit, false);
|
||||
await f.restart().act(f.code, f.observer, g.version, { type: 'semutaCommit', event }, clock);
|
||||
g = await f.restart().readRoom(f.code);
|
||||
assert.equal(g.pendingTreacheryDiscard, null);
|
||||
assert.equal(g.players[1].leaders.filter(l => l.dead).length, 1);
|
||||
assert.equal(g.players[2].hand.filter(c => c.id === f.action.card).length, 1);
|
||||
assert.equal(g.discard.filter(c => c.id === SEMUTA_DRUG_ID).length, 1);
|
||||
assert.deepEqual(g.battle?.preLeader?.ready, []);
|
||||
assert.deepEqual(f.sqlite.prepare('SELECT * FROM seats').all(), seats);
|
||||
await f.restart().act(f.code, f.owner, g.version, f.ready, clock);
|
||||
const prepared = await f.restart().readRoom(f.code);
|
||||
assert.deepEqual(prepared.battle?.preLeader?.ready, [f.owner.playerId]);
|
||||
} finally {
|
||||
f.sqlite.close();
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user