Allow user-approved atomic full-hand Semuta exchange
This commit is contained in:
1 parent
98d556a0e0
commit
3285172eec
16 files changed
+168
-69
No files matched your search
@@ -26,7 +26,6 @@ function fixture(): SemutaContext {
|
||||
semutaId: SEMUTA_DRUG_ID,
|
||||
handLimit: 4,
|
||||
incomingReservedSlots: 0,
|
||||
capacityPolicy: 'freeSlot',
|
||||
reservedTargetIds: [],
|
||||
batch: {
|
||||
event: 'fresh-1',
|
||||
@@ -173,24 +172,22 @@ void test('canonical Semuta identity is required and every supplied physical zon
|
||||
rejected(c);
|
||||
}
|
||||
});
|
||||
void test('both explicit capacity policies account for incoming reservations without selecting a default ruling', () => {
|
||||
for (const capacityPolicy of ['exchange', 'freeSlot'] as const)
|
||||
for (let held = 1; held <= 5; held++)
|
||||
void test('atomic Semuta exchange preserves normal and Harkonnen capacity and incoming reservations', () => {
|
||||
for (const limit of [4, 8])
|
||||
for (let held = 1; held <= limit + 1; held++)
|
||||
for (let reserved = 0; reserved <= 3; reserved++) {
|
||||
const c = fixture();
|
||||
c.handLimit = limit;
|
||||
c.ownerHand = [semuta(), ...baseDeck().slice(10, 10 + held - 1)];
|
||||
c.capacityPolicy = capacityPolicy;
|
||||
c.incomingReservedSlots = reserved;
|
||||
const legal =
|
||||
held + reserved + (capacityPolicy === 'freeSlot' ? 1 : 0) <=
|
||||
c.handLimit;
|
||||
if (legal) {
|
||||
if (held + reserved <= limit) {
|
||||
const result = resolveSemutaDrug(c, target(c));
|
||||
assert.equal(result.ownerHand.length, held);
|
||||
assert.ok(result.ownerHand.some(card => card.id === target(c)));
|
||||
assert.ok(!result.ownerHand.some(card => card.id === SEMUTA_DRUG_ID));
|
||||
assert.equal(result.discard.filter(card => card.id === SEMUTA_DRUG_ID).length, 1);
|
||||
} else rejected(c);
|
||||
}
|
||||
for (const capacityPolicy of [undefined, null, 'automatic', ''])
|
||||
rejected({ ...fixture(), capacityPolicy } as SemutaContext);
|
||||
for (const n of [-1, 1.5, NaN, Infinity, Number.MAX_SAFE_INTEGER + 1]) {
|
||||
rejected({ ...fixture(), handLimit: n });
|
||||
rejected({ ...fixture(), incomingReservedSlots: n });
|
||||
|
||||
@@ -308,19 +308,46 @@ void test('neutral Semuta passes do not reveal hidden possession and resume a de
|
||||
assert.deepEqual(normalizeAutomaticGame(reload(declined)), reload(declined));
|
||||
});
|
||||
|
||||
void test('the provisional free-slot guard is private and never spends a full-hand Semuta', () => {
|
||||
void test('a full-hand Semuta exchanges one card without revealing private capacity', () => {
|
||||
const { g, semuta, hajr } = fixture();
|
||||
for (let n = 0; n < 3; n++) player(g, 'r').hand.push(g.deck.shift()!);
|
||||
const pending = applyAction(g, 'a', { type: 'card', card: hajr.id });
|
||||
const event = pending.pendingTreacheryDiscard!.batch.event;
|
||||
const richese = viewGame(pending, 'r').semutaReaction!;
|
||||
assert.equal(richese.canCommit, false);
|
||||
assert.match(richese.blocked ?? '', /free hand slot/);
|
||||
assert.equal(viewGame(pending, 'r').semutaReaction?.canCommit, true);
|
||||
assert.equal(viewGame(pending, 'r').semutaReaction?.blocked, null);
|
||||
assert.equal(viewGame(pending, 'a').semutaReaction?.blocked, null);
|
||||
const before = JSON.stringify(pending);
|
||||
assert.throws(() => applyAction(pending, 'r', { type: 'semutaCommit', event }));
|
||||
assert.equal(JSON.stringify(pending), before);
|
||||
assert.equal(player(pending, 'r').hand.some(card => card.id === semuta.id), true);
|
||||
const done = applyAction(reload(pending), 'r', { type: 'semutaCommit', event });
|
||||
assert.equal(player(done, 'r').hand.length, 4);
|
||||
assert.ok(player(done, 'r').hand.some(card => card.id === hajr.id));
|
||||
assert.ok(!player(done, 'r').hand.some(card => card.id === semuta.id));
|
||||
assert.equal(done.discard.filter(card => card.id === semuta.id).length, 1);
|
||||
assert.equal(done.pendingTreacheryDiscard, null);
|
||||
assert.deepEqual(inventory(done), inventory(pending));
|
||||
assert.equal(done.hajr.filter(id => id === 'a').length, 1);
|
||||
assert.throws(() => applyAction(done, 'r', { type: 'semutaCommit', event }));
|
||||
});
|
||||
|
||||
void test('Harkonnen exchanges Semuta at eight cards while an already overfull hand rejects immutably', () => {
|
||||
const { g, semuta, hajr } = fixture();
|
||||
player(g, 'r').hand = [];
|
||||
g.players[2] = newPlayer('e', 'Harkonnen', 'harkonnen');
|
||||
player(g, 'e').hand = [semuta, ...g.deck.splice(0, 7)];
|
||||
player(g, 'r').hand.push(g.deck.shift()!);
|
||||
const pending = applyAction(g, 'a', { type: 'card', card: hajr.id });
|
||||
const event = pending.pendingTreacheryDiscard!.batch.event;
|
||||
assert.equal(viewGame(pending, 'e').semutaReaction?.canCommit, true);
|
||||
const overfull = reload(pending);
|
||||
player(overfull, 'e').hand.push(player(overfull, 'r').hand.pop()!);
|
||||
assert.equal(viewGame(overfull, 'e').semutaReaction?.canCommit, false);
|
||||
assert.deepEqual(viewGame(overfull, 'a'), viewGame(pending, 'a'));
|
||||
const before = JSON.stringify(overfull);
|
||||
assert.throws(() => applyAction(overfull, 'e', { type: 'semutaCommit', event }));
|
||||
assert.equal(JSON.stringify(overfull), before);
|
||||
const done = applyAction(reload(pending), 'e', { type: 'semutaCommit', event });
|
||||
assert.equal(player(done, 'e').hand.length, 8);
|
||||
assert.ok(player(done, 'e').hand.some(card => card.id === hajr.id));
|
||||
assert.ok(!player(done, 'e').hand.some(card => card.id === semuta.id));
|
||||
assert.deepEqual(inventory(done), inventory(pending));
|
||||
});
|
||||
|
||||
void test('a saved neutral reaction cannot be deleted to auto-retire the fresh discard', () => {
|
||||
|
||||
@@ -70,9 +70,7 @@ void test('passed, blocked, busy and waiting seats hide or disable unavailable c
|
||||
const passed = offer({ passed: true, canCommit: true });
|
||||
assert.match(markup(passed), /You continued/);
|
||||
assert.doesNotMatch(markup(passed), /Commit Semuta Drug|>Continue</);
|
||||
const blocked = offer({ blocked: 'Your hand has no free slot.' });
|
||||
assert.match(markup(blocked), /Your hand has no free slot/);
|
||||
assert.doesNotMatch(markup(offer()), /Your hand has no free slot/);
|
||||
const blocked = offer({ blocked: 'Reserved for an unfinished transaction.' });
|
||||
assert.doesNotMatch(markup(blocked), /Commit Semuta Drug/);
|
||||
assert.match(markup(offer({ canCommit: true }), true), /disabled=""[^>]*>Commit Semuta Drug/);
|
||||
assert.match(markup(offer(), true), /disabled=""[^>]*>Continue/);
|
||||
|
||||
@@ -1045,7 +1045,7 @@ void test('authenticated proactive Baliset Semuta claim restores only the select
|
||||
} finally { f.sqlite.close(); }
|
||||
});
|
||||
|
||||
void test('authenticated ordinary Trip Semuta claim returns one force after saved offer', async () => {
|
||||
void test('authenticated full-hand Trip Semuta exchange returns one force after saved offer', async () => {
|
||||
const f = unitStore();
|
||||
try {
|
||||
const made = await f.rooms.createRoom('Semuta Trip', 'choam', false, ['choam']);
|
||||
@@ -1073,6 +1073,7 @@ void test('authenticated ordinary Trip Semuta claim returns one force after save
|
||||
const used = g.deck.splice(index, 1)[0];
|
||||
g.players[0].hand.push(used);
|
||||
g.players[1].hand.push(semuta);
|
||||
g.players[1].hand.push(...g.deck.splice(0, 3));
|
||||
g.players[1].forces = { 'arrakeen:10': 1 };
|
||||
g.players[1].reserves = 19;
|
||||
g.players[2].hand.push(karama);
|
||||
@@ -1111,6 +1112,7 @@ void test('authenticated ordinary Trip Semuta claim returns one force after save
|
||||
assert.equal(claimed.players[1].forces['arrakeen:10'], undefined);
|
||||
assert.equal(claimed.players[1].reserves, 20);
|
||||
assert.equal(claimed.players[1].hand.filter(card => card.id === used.id).length, 1);
|
||||
assert.equal(claimed.players[1].hand.length, 4);
|
||||
assert.equal(claimed.discard.filter(card => card.id === semuta.id).length, 1);
|
||||
await assert.rejects(f.restart().act(code, auths[1], current.version,
|
||||
{ type: 'semutaCommit', event }, clock));
|
||||
|
||||
Reference in new issue
Block a user