Clarify Sapho finite-bid timing and pending Nexus rulings

This commit is contained in:
Quintine committed 2026-09-29 20:10:13 +10:00
1 parent 6df505f003
commit 41fd6fe173
8 files changed
+96 -13

No files matched your search

+23 -1
View File
@@ -176,7 +176,29 @@ void test('Once Around last preserves prior bids and permits a final outbid afte
assert.deepEqual(g.order, ['a', 'e', 'g']);
});
void test('Once Around rejects late first, completed opportunity, no-op and stale event without card loss', () => {
void test('Once Around first after a prior bid moves only unacted bidders and preserves the committed bid', () => {
let g = auction('g');
const first = g.richeseAuction!.active!;
g = bid(g, first, 1);
const before = reload(g);
assert.ok(viewGame(g, 'g').saphoOptions.some((o) => o.scope === 'onceAround' && o.mode === 'first'));
g = applyAction(reload(g), 'g', action(g, 'first'));
assert.deepEqual(g.richeseAuction!.acted, [first]);
assert.equal(g.richeseAuction!.bid, 1);
assert.equal(g.richeseAuction!.bidder, first);
assert.deepEqual(g.richeseAuction!.tieOrder, before.richeseAuction!.tieOrder);
assert.equal(g.richeseAuction!.active, 'g');
assert.equal(g.discard.filter((c) => c.id === card).length, 1);
g = bid(reload(g), 'g', 2);
assert.equal(g.richeseAuction!.active, 'e');
assert.deepEqual(g.richeseAuction!.acted, [first, 'g']);
const sold = g.richeseAuction!.cardId;
g = bid(g, 'e', 3);
assert.ok(g.players[1].hand.some((c) => c.id === sold));
assert.equal(g.players[1].spice, 17);
});
void test('Once Around rejects no-op first, completed opportunity, no-op last and stale event without card loss', () => {
let g = auction();
g = bid(g, g.richeseAuction!.active!, 1);
reject(g, 'e', action(g, 'first'));
+39 -8
View File
@@ -96,7 +96,7 @@ const clock: Rooms.RoomsClock = { now: () => 10000, sleep: async () => {} };
const SAPHO = 'richese-juice-of-sapho';
type Scope = 'onceAround' | 'movement';
async function fixture(scope: Scope, advanced = false) {
async function fixture(scope: Scope, advanced = false, lateFirst = false) {
const store = unitStore();
const made = await store.rooms.createRoom(
'First player',
@@ -107,7 +107,7 @@ async function fixture(scope: Scope, advanced = false) {
const code = made.view.code;
const holder = await store.rooms.joinRoom(code, 'Sapho holder', 'emperor');
const guild = await store.rooms.joinRoom(code, 'Guild', 'guild');
const fourth = advanced
const fourth = advanced || lateFirst
? await store.rooms.joinRoom(code, 'Later player', 'harkonnen')
: null;
const seats = await Promise.all(
@@ -165,7 +165,9 @@ async function fixture(scope: Scope, advanced = false) {
'Roster',
'richese',
).leaders;
initial.order = [seats[2].playerId, seats[1].playerId, seats[0].playerId];
initial.order = lateFirst
? [seats[2].playerId, seats[3].playerId, seats[1].playerId, seats[0].playerId]
: [seats[2].playerId, seats[1].playerId, seats[0].playerId];
}
const save = (g: engine.Game) =>
store.sqlite
@@ -192,11 +194,9 @@ async function fixture(scope: Scope, advanced = false) {
direction: 'clockwise',
});
g = await f.restart().readRoom(code);
assert.deepEqual(g.richeseAuction!.order, [
seats[2].playerId,
seats[1].playerId,
seats[0].playerId,
]);
assert.deepEqual(g.richeseAuction!.order, lateFirst
? [seats[3].playerId, seats[2].playerId, seats[1].playerId, seats[0].playerId]
: [seats[2].playerId, seats[1].playerId, seats[0].playerId]);
} else if (advanced) {
const g = await f.restart().readRoom(code);
assert.equal(g.decision?.kind, 'guildTiming');
@@ -398,6 +398,37 @@ void test('Once Around last survives duplicate cardplay and restart without repl
}
});
void test('a prior persisted bid survives duplicate Sapho first and the remaining one-bid order', async () => {
const f = await fixture('onceAround', false, true);
try {
let g = await stable(f);
const event = g.richeseAuction!.event;
g = await act(f, 3, bid(g, 2));
const previous = structuredClone(g.richeseAuction!);
await privacy(f, 'onceAround', event, ['first', 'last']);
g = await duplicate(f, action('onceAround', event, 'first'));
assert.deepEqual(g.richeseAuction!.order, [
f.seats[3].playerId, f.seats[1].playerId,
f.seats[2].playerId, f.seats[0].playerId,
]);
assert.equal(g.richeseAuction!.active, f.seats[1].playerId);
for (const key of ['acted', 'bid', 'bidder', 'tieOrder', 'sealed'] as const)
assert.deepEqual(g.richeseAuction![key], previous[key]);
await privacy(f, 'onceAround', event, []);
g = await act(f, 1, bid(g, 3));
assert.equal(g.richeseAuction!.active, f.seats[2].playerId);
g = await act(f, 2, bid(g, null));
assert.equal(g.richeseAuction!.active, f.seats[0].playerId);
g = await act(f, 0, bid(g, null));
assert.equal(g.players[1].spice, 17);
assert.equal(g.discard.filter((card) => card.id === SAPHO).length, 1);
await rejectsWithoutWrite(f, 1, action('onceAround', event, 'first'));
} finally {
f.hooks.beforeWrite = undefined;
f.sqlite.close();
}
});
void test('Once Around first persists only within its lot and cannot be replayed after its first actual bid', async () => {
const f = await fixture('onceAround');
try {