Offer Semuta before optional winner battle cleanup
This commit is contained in:
1 parent
593ca3beb4
commit
e55d275014
9 files changed
+228
-46
No files matched your search
@@ -600,3 +600,69 @@ void test('clean winner-selected battle cards allow one committed Semuta choice
|
||||
assert.deepEqual(inventory(alternate), original);
|
||||
assert.deepEqual(settled(alternate), settled(declined));
|
||||
});
|
||||
|
||||
void test('a mandatory winner Hero discard can be claimed before optional battle cleanup', () => {
|
||||
const { g, semuta, hajr } = fixture();
|
||||
player(g, 'a').hand = [];
|
||||
g.deck.push(hajr);
|
||||
const [hero] = g.deck.splice(g.deck.findIndex(card => card.kind === 'hero'), 1);
|
||||
const [weapon] = g.deck.splice(g.deck.findIndex(card => card.kind === 'projectile'), 1);
|
||||
const [defense] = g.deck.splice(g.deck.findIndex(card => card.kind === 'snooper'), 1);
|
||||
assert.ok(hero && weapon && defense);
|
||||
player(g, 'a').hand.push(hero, weapon);
|
||||
player(g, 'e').hand.push(defense);
|
||||
for (const id of ['a', 'e']) {
|
||||
player(g, id).forces = { 'arrakeen:10': 5 };
|
||||
player(g, id).reserves = 15;
|
||||
for (const leader of player(g, id).leaders) leader.strength = 0;
|
||||
}
|
||||
Object.assign(g, { phase: 6, active: 'a', order: ['a', 'e', 'r'],
|
||||
response: null, decision: null, phaseOpening: null, ready: [] });
|
||||
const original = inventory(g);
|
||||
let battle = applyAction(g, 'a', { type: 'chooseBattle', territory: 'arrakeen', target: 'e' });
|
||||
for (let step = 0; step < 40; step++) {
|
||||
if (battle.response) {
|
||||
const p = battle.players.find(p => !battle.response!.passed.includes(p.id))!;
|
||||
battle = applyAction(battle, p.id, { type: 'passResponse' });
|
||||
} else if (battle.battle?.preparation)
|
||||
battle = applyAction(battle, battle.battle.preparation.owner, { type: 'declineBattlePower' });
|
||||
else break;
|
||||
}
|
||||
if (battle.battle?.preLeader && !battle.battle.preLeader.closed)
|
||||
for (const id of ['a', 'e'])
|
||||
battle = applyAction(battle, id, {
|
||||
type: 'battlePreparationReady', event: battle.battle!.preLeader!.event,
|
||||
});
|
||||
battle = applyAction(battle, 'a', { type: 'battlePlan', dial: 2, support: 2,
|
||||
leader: hero.id, weapon: weapon.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 });
|
||||
const pending = applyAction(battle, 'e', { type: 'traitorCall', call: false });
|
||||
assert.equal(pending.pendingTreacheryDiscard?.continuation.kind, 'winnerMandatoryDiscard');
|
||||
assert.equal(pending.pendingTreacheryDiscard?.reaction?.stage, 'offer');
|
||||
assert.deepEqual(pending.pendingTreacheryDiscard?.batch.entries.map(entry => entry.card.id), [hero.id]);
|
||||
assert.equal(pending.discard.filter(card => card.id === hero.id).length, 1);
|
||||
const event = pending.pendingTreacheryDiscard!.batch.event;
|
||||
assert.equal(viewGame(pending, 'r').semutaReaction?.canCommit, true);
|
||||
assert.deepEqual(normalizeAutomaticGame(reload(pending)), reload(pending));
|
||||
let declined = reload(pending);
|
||||
for (const id of ['e', 'a', 'r'])
|
||||
declined = applyAction(declined, id, { type: 'semutaPass', event });
|
||||
assert.equal(declined.pendingTreacheryDiscard, null);
|
||||
assert.equal(declined.decision?.kind, 'battleCards');
|
||||
assert.equal(declined.discard.filter(card => card.id === hero.id).length, 1);
|
||||
assert.deepEqual(inventory(declined), original);
|
||||
const claimed = applyAction(reload(pending), 'r', botActions(viewGame(pending, 'r'))[0]!);
|
||||
assert.equal(claimed.pendingTreacheryDiscard, null);
|
||||
assert.equal(claimed.decision?.kind, 'battleCards');
|
||||
assert.equal(player(claimed, 'a').hand.some(card => card.id === weapon.id), true);
|
||||
assert.equal(player(claimed, 'r').hand.filter(card => card.id === hero.id).length, 1);
|
||||
assert.equal(claimed.discard.filter(card => card.id === semuta.id).length, 1);
|
||||
assert.equal(claimed.discard.some(card => card.id === hero.id), false);
|
||||
assert.deepEqual(inventory(claimed), original);
|
||||
assert.deepEqual(claimed.lastBattle, declined.lastBattle);
|
||||
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 }));
|
||||
});
|
||||
@@ -388,5 +388,81 @@ void test('authenticated clean Semuta producers claim once through restart and r
|
||||
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));
|
||||
|
||||
const mandatoryStart = structuredClone(battleClaim);
|
||||
const heroSemuta = take(mandatoryStart, SEMUTA_DRUG_ID);
|
||||
mandatoryStart.deck.push(take(mandatoryStart, shield.id));
|
||||
seat(mandatoryStart, ids[0]).hand.push(heroSemuta);
|
||||
const hero = mandatoryStart.deck.splice(
|
||||
mandatoryStart.deck.findIndex(card => card.kind === 'hero'), 1)[0];
|
||||
const nextWeapon = mandatoryStart.deck.splice(
|
||||
mandatoryStart.deck.findIndex(card => card.kind === 'projectile'), 1)[0];
|
||||
const nextDefense = mandatoryStart.deck.splice(
|
||||
mandatoryStart.deck.findIndex(card => card.kind === 'snooper'), 1)[0];
|
||||
assert.ok(hero && nextWeapon && nextDefense);
|
||||
seat(mandatoryStart, ids[1]).hand.push(hero, nextWeapon);
|
||||
seat(mandatoryStart, ids[2]).hand.push(nextDefense);
|
||||
for (const id of [ids[1], ids[2]]) {
|
||||
const p = seat(mandatoryStart, id);
|
||||
p.reserves += Object.values(p.forces).reduce((sum, n) => sum + n, 0) - 5;
|
||||
p.forces = { 'arrakeen:10': 5 };
|
||||
for (const leader of p.leaders) leader.strength = 0;
|
||||
}
|
||||
Object.assign(mandatoryStart, { turn: 10, phase: 6, active: ids[1],
|
||||
order: [ids[1], ids[2], ids[0]], ready: [],
|
||||
decision: null, response: null, phaseOpening: null, stormPending: null,
|
||||
lastBattle: [], lastBattleContext: null });
|
||||
mandatoryStart.version = battleClaim.version + 1;
|
||||
assert.equal(f.sqlite.prepare('UPDATE rooms SET state=?,version=? WHERE code=? AND version=?')
|
||||
.run(JSON.stringify(mandatoryStart), mandatoryStart.version, code, battleClaim.version).changes, 1);
|
||||
let mandatoryBattle = await act(1, { type: 'chooseBattle', territory: 'arrakeen', target: ids[2] });
|
||||
for (let step = 0; step < 40; step++) {
|
||||
if (mandatoryBattle.response) {
|
||||
const p = mandatoryBattle.players.find(p => !mandatoryBattle.response!.passed.includes(p.id))!;
|
||||
mandatoryBattle = await act(ids.indexOf(p.id), { type: 'passResponse' });
|
||||
} else if (mandatoryBattle.battle?.preparation)
|
||||
mandatoryBattle = await act(ids.indexOf(mandatoryBattle.battle.preparation.owner),
|
||||
{ type: 'declineBattlePower' });
|
||||
else break;
|
||||
}
|
||||
if (mandatoryBattle.battle?.preLeader && !mandatoryBattle.battle.preLeader.closed)
|
||||
for (const index of [1, 2])
|
||||
mandatoryBattle = await act(index, {
|
||||
type: 'battlePreparationReady', event: mandatoryBattle.battle!.preLeader!.event,
|
||||
});
|
||||
mandatoryBattle = await act(1, { type: 'battlePlan', dial: 2, support: 2,
|
||||
leader: hero.id, weapon: nextWeapon.id });
|
||||
mandatoryBattle = await act(2, { type: 'battlePlan', dial: 0, support: 0,
|
||||
leader: seat(mandatoryBattle, ids[2]).leaders.find(leader => !leader.dead)!.id,
|
||||
defense: nextDefense.id });
|
||||
mandatoryBattle = await act(1, { type: 'traitorCall', call: false });
|
||||
const heroOffer = await act(2, { type: 'traitorCall', call: false });
|
||||
assert.equal(heroOffer.pendingTreacheryDiscard?.continuation.kind, 'winnerMandatoryDiscard');
|
||||
assert.equal(heroOffer.pendingTreacheryDiscard?.reaction?.stage, 'offer');
|
||||
assert.equal(heroOffer.discard.filter(card => card.id === hero.id).length, 1);
|
||||
await f.restart().continueRoomAutomatic(code, clock);
|
||||
assert.equal((await f.restart().readRoom(code)).version, heroOffer.version);
|
||||
assert.equal((await f.restart().readSeatView(code, auths[0])).semutaReaction?.canCommit, true);
|
||||
const heroClaim = await act(0, { type: 'semutaCommit',
|
||||
event: heroOffer.pendingTreacheryDiscard!.batch.event });
|
||||
assert.equal(heroClaim.pendingTreacheryDiscard, null);
|
||||
assert.equal(heroClaim.decision?.kind, 'battleCards');
|
||||
assert.equal(seat(heroClaim, ids[1]).hand.some(card => card.id === nextWeapon.id), true);
|
||||
assert.equal(seat(heroClaim, ids[0]).hand.filter(card => card.id === hero.id).length, 1);
|
||||
assert.equal(heroClaim.discard.filter(card => card.id === SEMUTA_DRUG_ID).length, 1);
|
||||
assert.equal(heroClaim.discard.some(card => card.id === hero.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(heroClaim, auth.playerId));
|
||||
let optional = await act(1, { type: 'decision', discard: [nextWeapon.id] });
|
||||
assert.equal(optional.pendingTreacheryDiscard?.batch.cause, 'battle:winner');
|
||||
assert.equal(optional.pendingTreacheryDiscard?.reaction?.stage, 'offer');
|
||||
const optionalEvent = optional.pendingTreacheryDiscard!.batch.event;
|
||||
for (const index of [2, 0, 1])
|
||||
optional = await act(index, { type: 'semutaPass', event: optionalEvent });
|
||||
assert.equal(optional.pendingTreacheryDiscard, null);
|
||||
assert.equal(optional.discard.filter(card => card.id === nextWeapon.id).length, 1);
|
||||
assert.equal(optional.discard.filter(card => card.id === SEMUTA_DRUG_ID).length, 1);
|
||||
assert.equal(seat(optional, ids[0]).hand.filter(card => card.id === hero.id).length, 1);
|
||||
} finally { f.sqlite.close(); }
|
||||
});
|
||||
Reference in new issue
Block a user