Duels record the rules revision they began under

CURRENT_RULES is stamped on the start line and the game state, and a
ledger replays under the revision it recorded (1 when it has none), so a
future fix that changes how a past turn resolves can keep the old path
for old duels. The reports desk skill now says when to bump it: only
when the deploy gate flags ledgers.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141G6xqLeNRYEtviLWSB5Up
This commit is contained in:
Eric Wagoner
2026-09-23 10:32:25 -04:00
co-authored by Claude Fable 5.1
parent 8eca078043
commit 7485d50b8d
8 changed files with 44 additions and 13 deletions
+1
View File
@@ -179,6 +179,7 @@ function loadSaved(): SavedDuel | null {
// Saves at this version may lack fields that came later; the engine expects them present.
saved.state.seats ??= Object.keys(saved.state.wizards);
saved.state.lessonsShown ??= [];
saved.state.rules ??= 1;
if (saved.state.lastTurn) {
saved.state.lastTurn.events ??= [];
saved.state.lastTurn.lessons ??= [];
+16 -1
View File
@@ -129,7 +129,21 @@ export interface Outcome {
reason: string;
}
/**
* The rules revision a duel resolves under. A duel keeps the revision it began
* with, so a ledger replays to what its players saw even after the resolver
* changes. Bump this only when a fix changes how an already-played turn would
* resolve, which the deploy gate (deploy/verify-ledgers.sh) shows by flagging
* ledgers; a fix that diverges from no ledger ships without a bump. Keep the
* old path behind `state.rules < N` and pin both with tests.
*
* 1 Bartle's text as first implemented (2026-09-22).
*/
export const CURRENT_RULES = 1;
export interface GameState {
/** The revision this duel resolves under; see CURRENT_RULES. */
rules: number;
turn: number;
/** Seats in table order; every wizard in play, whether still standing or not. */
seats: WizardId[];
@@ -235,9 +249,10 @@ function makeWizard(id: WizardId, name: string): WizardState {
};
}
export function createGame(names: Record<WizardId, string>, seed = Date.now()): GameState {
export function createGame(names: Record<WizardId, string>, seed = Date.now(), rules = CURRENT_RULES): GameState {
const seats = Object.keys(names);
return {
rules,
turn: 0,
seats,
wizards: Object.fromEntries(seats.map((id) => [id, makeWizard(id, names[id])])),
+8 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { SPECTATOR, createGame } from './state';
import { CURRENT_RULES, SPECTATOR, createGame } from './state';
import { viewFor } from './view';
/** Three wizards; B's first gestures were hidden from C alone. */
@@ -30,3 +30,10 @@ describe('the view a room sends', () => {
expect(view.log.map((l) => l.text)).toEqual(['Nothing else happened.']);
});
});
describe('the rules revision', () => {
it('is stamped on a new duel and kept from a ledger that recorded one', () => {
expect(createGame({ A: 'Aldric', B: 'Bryn' }).rules).toBe(CURRENT_RULES);
expect(createGame({ A: 'Aldric', B: 'Bryn' }, 1, 1).rules).toBe(1);
});
});