Connect Tleilaxu revival payments to Bureaucrat
This commit is contained in:
1 parent
3b118dd845
commit
e6bb9809f7
10 files changed
+252
-18
No files matched your search
@@ -27,11 +27,13 @@ export function takeBureaucratCard(g: Game, id: string, name = 'Karama') {
|
||||
export function bureaucratPaymentGame({
|
||||
advanced = false,
|
||||
choam = false,
|
||||
tleilaxu = false,
|
||||
phase = 5,
|
||||
advisor = false,
|
||||
}: {
|
||||
advanced?: boolean;
|
||||
choam?: boolean;
|
||||
tleilaxu?: boolean;
|
||||
phase?: number;
|
||||
advisor?: boolean;
|
||||
} = {}): Game {
|
||||
@@ -39,7 +41,7 @@ export function bureaucratPaymentGame({
|
||||
'BUROTEST',
|
||||
newPlayer('b', 'Bureaucrat', 'atreides'),
|
||||
advanced,
|
||||
choam ? ['choam'] : [],
|
||||
choam ? ['choam'] : tleilaxu ? ['ix'] : [],
|
||||
);
|
||||
for (const [id, name, faction] of [
|
||||
['p', 'Payer', 'harkonnen'],
|
||||
@@ -47,6 +49,7 @@ export function bureaucratPaymentGame({
|
||||
['g', 'Guild', 'guild'],
|
||||
...(choam ? [['r', 'Richese', 'richese']] : []),
|
||||
...(advisor ? [['bg', 'Bene Gesserit', 'beneGesserit']] : []),
|
||||
...(tleilaxu ? [['t', 'Tleilaxu', 'tleilaxu']] : []),
|
||||
] as [string, string, Parameters<typeof newPlayer>[2]][])
|
||||
joinGame(g, newPlayer(id, name, faction));
|
||||
for (const p of g.players) g = applyAction(g, p.id, { type: 'ready' });
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { createHash } from 'node:crypto';
|
||||
import test from 'node:test';
|
||||
import { applyAction, viewGame } from '../game/engine';
|
||||
import { newRevivalRules } from '../game/revival';
|
||||
import { unitStore } from './fixture-nexus-room-store';
|
||||
import { bureaucratPaymentGame, bureaucratPlayer as player, takeBureaucratCard } from './bureaucrat-payment-fixture';
|
||||
|
||||
const clock = { now: () => 82_000, sleep: async () => {} };
|
||||
const hash = (value: string) => createHash('sha256').update(value).digest('hex');
|
||||
|
||||
void test('saved Tleilaxu revival payment survives restart and racing choices settle one actual recipient share', async (t) => {
|
||||
const store = unitStore();
|
||||
t.after(() => store.sqlite.close());
|
||||
let game = bureaucratPaymentGame({ tleilaxu: true, phase: 4 });
|
||||
game.active = null;
|
||||
game.revivalRules = newRevivalRules();
|
||||
player(game, 'p').leaders[1].dead = true;
|
||||
takeBureaucratCard(game, 'e');
|
||||
game = applyAction(game, 'p', { type: 'requestLeaderRevival', leader: 'harkonnen-1' });
|
||||
game = applyAction(game, 't', { type: 'quoteLeaderRevival', target: 'p', amount: 7 });
|
||||
game = applyAction(game, 'p', { type: 'acceptLeaderRevival' });
|
||||
for (let step = 0; game.response && step < 30; step++) {
|
||||
const responder = game.players.find((seat) => !game.response!.passed.includes(seat.id));
|
||||
assert.ok(responder);
|
||||
game = applyAction(game, responder.id, { type: 'passResponse' });
|
||||
}
|
||||
assert.equal(game.decision?.kind, 'bureaucratPayment');
|
||||
assert.equal(game.bureaucratPayments?.pending?.source.amount, 7);
|
||||
game.version = 41;
|
||||
store.sqlite.prepare('INSERT INTO rooms(code,state,version,updated_at) VALUES(?,?,?,?)')
|
||||
.run(game.code, JSON.stringify(game), game.version, 80_000);
|
||||
const tokens = Object.fromEntries(game.players.map((seat) => [seat.id, hash(`revival-payment:${seat.id}`)]));
|
||||
for (const seat of game.players)
|
||||
store.sqlite.prepare('INSERT INTO seats(token_hash,room_code,player_id) VALUES(?,?,?)')
|
||||
.run(hash(tokens[seat.id]), game.code, seat.id);
|
||||
const auth = await store.rooms.authenticate(game.code, tokens.b);
|
||||
assert.deepEqual(await store.restart().readSeatView(game.code, auth), viewGame(game, 'b'));
|
||||
const outsider = await store.rooms.authenticate(game.code, tokens.g);
|
||||
assert.equal((await store.restart().readSeatView(game.code, outsider)).bureaucrat.pending, null);
|
||||
const event = game.decision.event!;
|
||||
const results = await Promise.allSettled([
|
||||
store.rooms.act(game.code, auth, 41, { type: 'decision', event, redirect: true }, clock),
|
||||
store.restart().act(game.code, auth, 41, { type: 'decision', event, redirect: false }, clock),
|
||||
]);
|
||||
assert.equal(results.filter((result) => result.status === 'fulfilled').length, 1);
|
||||
const after = await store.restart().readRoom(game.code);
|
||||
assert.equal(after.version, 42);
|
||||
assert.equal(player(after, 'p').spice, 23);
|
||||
assert.ok([35, 37].includes(player(after, 't').spice));
|
||||
assert.equal(after.bureaucratPayments?.used.length, player(after, 't').spice === 35 ? 1 : 0);
|
||||
assert.equal(after.bureaucratPayments?.pending, undefined);
|
||||
assert.equal(after.response, null);
|
||||
assert.equal(player(after, 'p').leaders[1].dead, false);
|
||||
assert.deepEqual(after.players.map((seat) => seat.hand), game.players.map((seat) => seat.hand));
|
||||
await assert.rejects(store.restart().act(game.code, auth, 41,
|
||||
{ type: 'decision', event, redirect: true }, clock), /table changed/);
|
||||
assert.deepEqual(await store.restart().readSeatView(game.code, outsider), viewGame(after, 'g'));
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { applyAction, viewGame, type Game } from '../game/engine';
|
||||
import { botActions } from '../game/bots';
|
||||
import { DIFFICULTIES } from '../game/bot-profiles';
|
||||
import {
|
||||
bureaucratPaymentGame,
|
||||
bureaucratPlayer as player,
|
||||
bureaucratReload as reload,
|
||||
takeBureaucratCard,
|
||||
} from './bureaucrat-payment-fixture';
|
||||
|
||||
function paidRevival(kind: 'leader' | 'forces', cancelIncome = false): Game {
|
||||
let game = bureaucratPaymentGame({ tleilaxu: true, phase: 4 });
|
||||
game.active = null;
|
||||
player(game, 'p').leaders[1].dead = true;
|
||||
player(game, 'p').leaderRevived = false;
|
||||
player(game, 'p').tanks = 3;
|
||||
player(game, 'p').reserves = 17;
|
||||
const karama = takeBureaucratCard(game, 'e');
|
||||
if (kind === 'leader') {
|
||||
game = applyAction(game, 'p', { type: 'requestLeaderRevival', leader: 'harkonnen-1' });
|
||||
game = applyAction(game, 't', { type: 'quoteLeaderRevival', target: 'p', amount: 7 });
|
||||
game = applyAction(game, 'p', { type: 'acceptLeaderRevival' });
|
||||
} else game = applyAction(game, 'p', { type: 'revive', amount: 3 });
|
||||
for (let step = 0; game.response && step < 30; step++) {
|
||||
if (cancelIncome && game.response.kind === 'revivalIncome') {
|
||||
game = applyAction(game, 'e', { type: 'card', card: karama.id, mode: 'cancel' });
|
||||
continue;
|
||||
}
|
||||
const responder = game.players.find((seat) => !game.response!.passed.includes(seat.id));
|
||||
assert.ok(responder);
|
||||
game = applyAction(game, responder.id, { type: 'passResponse' });
|
||||
}
|
||||
assert.equal(game.response, null);
|
||||
return game;
|
||||
}
|
||||
|
||||
void test('negotiated paid leader revival opens Bureaucrat after Tleilaxu income response, then redirects exactly two', () => {
|
||||
const before = paidRevival('leader');
|
||||
assert.equal(before.decision?.kind, 'bureaucratPayment');
|
||||
assert.equal(before.bureaucratPayments?.pending?.source.kind, 'revival');
|
||||
assert.equal(before.bureaucratPayments?.pending?.source.amount, 7);
|
||||
assert.equal(player(before, 'p').spice, 23);
|
||||
assert.equal(player(before, 't').spice, 30);
|
||||
assert.equal(player(before, 'p').leaders[1].dead, false);
|
||||
const event = before.decision!.event!;
|
||||
assert.throws(() => applyAction(reload(before), 'p', { type: 'decision', event, redirect: true }));
|
||||
const after = applyAction(reload(before), 'b', { type: 'decision', event, redirect: true });
|
||||
assert.equal(player(after, 'p').spice, 23);
|
||||
assert.equal(player(after, 't').spice, 35);
|
||||
assert.equal(after.bureaucratPayments?.used.length, 1);
|
||||
assert.equal(after.response, null);
|
||||
assert.equal(after.decision, null);
|
||||
const notice = after.log.findLast((entry) => entry.text.includes('used Bureaucrat'));
|
||||
assert.match(notice?.text ?? '', /qualifying revival payment/);
|
||||
assert.doesNotMatch(notice!.text, /7.spice/);
|
||||
assert.deepEqual(after.players.map((seat) => seat.hand), before.players.map((seat) => seat.hand));
|
||||
assert.throws(() => applyAction(reload(after), 'b', { type: 'decision', event, redirect: true }));
|
||||
});
|
||||
|
||||
void test('Karama-stopped native revival income goes to the bank without a Bureaucrat offer', () => {
|
||||
const after = paidRevival('leader', true);
|
||||
assert.equal(after.decision?.kind, undefined);
|
||||
assert.equal(after.bureaucratPayments?.pending, undefined);
|
||||
assert.equal(after.bureaucratPayments?.used.length ?? 0, 0);
|
||||
assert.equal(player(after, 'p').spice, 23);
|
||||
assert.equal(player(after, 't').spice, 30);
|
||||
assert.equal(after.discard.filter((card) => card.effect === 'karama').length, 1);
|
||||
});
|
||||
|
||||
void test('owner alone sees negotiated revival price in pending Bureaucrat choice; decline preserves the skill', () => {
|
||||
const before = paidRevival('leader');
|
||||
assert.equal(viewGame(before, 'g').bureaucrat.pending, null);
|
||||
assert.equal(viewGame(before, 'b').bureaucrat.pending?.amount, 7);
|
||||
const decision = before.decision;
|
||||
assert.ok(decision?.kind === 'bureaucratPayment');
|
||||
const after = applyAction(reload(before), 'b', {
|
||||
type: 'decision', event: decision.event, redirect: false,
|
||||
});
|
||||
assert.equal(player(after, 't').spice, 37);
|
||||
assert.equal(after.bureaucratPayments?.used.length ?? 0, 0);
|
||||
assert.equal(after.decision, null);
|
||||
});
|
||||
|
||||
void test('a small force-revival payment and separate free-revival reward cannot trigger Bureaucrat', () => {
|
||||
const game = paidRevival('forces');
|
||||
assert.equal(game.decision?.kind, undefined);
|
||||
assert.equal(game.bureaucratPayments?.pending, undefined);
|
||||
assert.equal(player(game, 'p').spice, 28);
|
||||
assert.equal(player(game, 't').spice, 33);
|
||||
});
|
||||
|
||||
for (const difficulty of DIFFICULTIES) void test(`${difficulty} resolves paid revival Bureaucrat through the projected legal choice`, () => {
|
||||
const game = paidRevival('leader');
|
||||
player(game, 'b').bot = difficulty;
|
||||
const actions = botActions(viewGame(game, 'b'));
|
||||
const decision = game.decision;
|
||||
assert.ok(decision?.kind === 'bureaucratPayment');
|
||||
assert.ok(actions.some((action) => action.type === 'decision' && action.event === decision.event));
|
||||
const after = applyAction(reload(game), 'b', actions[0]);
|
||||
assert.equal(after.bureaucratPayments?.pending, undefined);
|
||||
assert.equal(after.response, null);
|
||||
});
|
||||
Reference in new issue
Block a user