@mcptoolshop/sovereign 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +126 -0
- package/LICENSE +21 -0
- package/README.es.md +158 -0
- package/README.fr.md +158 -0
- package/README.hi.md +158 -0
- package/README.it.md +158 -0
- package/README.ja.md +158 -0
- package/README.md +158 -0
- package/README.pt-BR.md +158 -0
- package/README.zh.md +158 -0
- package/SECURITY.md +61 -0
- package/bin/sovereign.js +167 -0
- package/package.json +56 -0
- package/release/00-START-HERE.html +333 -0
- package/release/CHANGELOG.md +126 -0
- package/release/README.txt +144 -0
- package/release/balance-evidence/README.txt +81 -0
- package/release/balance-evidence/raw-data/sovereign-batch-v0.10-canonical-400.json +72134 -0
- package/release/balance-evidence/raw-data/sovereign-batch-v0.10-canonical-slot-swap.json +18137 -0
- package/release/balance-evidence/raw-data/sovereign-batch-v0.10-canonical.json +18137 -0
- package/release/balance-evidence/raw-data/sovereign-batch-v0.10-mc-mirror.json +18089 -0
- package/release/balance-evidence/raw-data/sovereign-batch-v0.10-mfg-mirror.json +18089 -0
- package/release/balance-evidence/raw-data/sovereign-batch-v0.10-tf-mirror.json +18089 -0
- package/release/balance-evidence/sovereign-batch-v0.10-canonical-400.html +1 -0
- package/release/balance-evidence/sovereign-batch-v0.10-canonical-slot-swap.html +1 -0
- package/release/balance-evidence/sovereign-batch-v0.10-canonical.html +1 -0
- package/release/balance-evidence/sovereign-batch-v0.10-mc-mirror.html +1 -0
- package/release/balance-evidence/sovereign-batch-v0.10-mfg-mirror.html +1 -0
- package/release/balance-evidence/sovereign-batch-v0.10-summary.html +2 -0
- package/release/balance-evidence/sovereign-batch-v0.10-tf-mirror.html +1 -0
- package/release/board-game/README.txt +48 -0
- package/release/board-game/sovereign-economy-audit.html +501 -0
- package/release/board-game/sovereign-print-audit.html +479 -0
- package/release/board-game/sovereign-prototype.html +1939 -0
- package/release/design-history/01-phase1-concept.html +632 -0
- package/release/design-history/02-phase2-prototype.html +1026 -0
- package/release/design-history/03-phase3-audit.html +268 -0
- package/release/design-history/04-phase4-audit.html +274 -0
- package/release/design-history/05-phase5-audit.html +305 -0
- package/release/design-history/README.txt +66 -0
- package/release/digital-mode/README.txt +89 -0
- package/release/digital-mode/sovereign-solo.html +3884 -0
- package/release/digital-mode/sovereign-v0.10-freeze-audit.html +67 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<title>Sovereign · Solo / Digital · Phase 3 Audit</title>
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--revolutionary-debt:#6E1F1E; --state-debt:#4A6B8A; --revenue-system:#C28A28;
|
|
10
|
+
--commercial-infrastructure:#2E7A6B; --national-finance:#1F2D52;
|
|
11
|
+
--manufactures:#8C8A2E; --strategic-industry:#3A3A3A;
|
|
12
|
+
--parchment:#F0E6CD; --parchment-2:#E6DABC; --ink:#1A1612; --highlight:#C8392E;
|
|
13
|
+
--rule:rgba(26,22,18,0.55); --rule-soft:rgba(26,22,18,0.22);
|
|
14
|
+
--display: "Baskerville","Big Caslon","Hoefler Text","Garamond","Times New Roman",serif;
|
|
15
|
+
--body: "Iowan Old Style","Georgia","Cambria","Times New Roman",serif;
|
|
16
|
+
--ui: -apple-system,"Segoe UI","Helvetica Neue","Arial",system-ui,sans-serif;
|
|
17
|
+
--mono: "SF Mono","Menlo","Consolas","Courier New",monospace;
|
|
18
|
+
}
|
|
19
|
+
@page { size: 8.5in 11in; margin: 0.55in 0.65in; }
|
|
20
|
+
*,*::before,*::after { box-sizing:border-box; }
|
|
21
|
+
html, body { margin:0; padding:0; font-family: var(--body); color: var(--ink); background:#2A2622; -webkit-print-color-adjust:exact; print-color-adjust:exact; }
|
|
22
|
+
.viewport { display:flex; flex-direction:column; align-items:center; gap:22px; padding:28px 16px 80px; }
|
|
23
|
+
.page { width:8.5in; min-height:11in; background:var(--parchment); position:relative; box-shadow:0 10px 40px rgba(0,0,0,.55), 0 2px 4px rgba(0,0,0,.4); padding:.55in .65in .5in; }
|
|
24
|
+
@media print {
|
|
25
|
+
html, body { background:#fff !important; }
|
|
26
|
+
.viewport { padding:0; gap:0; }
|
|
27
|
+
.page { box-shadow:none; padding:0; min-height:auto; width:100%; page-break-after:always; break-after:page; }
|
|
28
|
+
.page:last-child { page-break-after:auto; break-after:auto; }
|
|
29
|
+
.toolbar { display:none !important; }
|
|
30
|
+
}
|
|
31
|
+
.toolbar { position:fixed; top:16px; right:16px; z-index:30; }
|
|
32
|
+
.toolbar button { background:var(--parchment); color:var(--ink); border:1px solid var(--ink); padding:8px 14px; font-family:var(--ui); font-size:12px; letter-spacing:.08em; text-transform:uppercase; cursor:pointer; }
|
|
33
|
+
.doc-head { display:flex; justify-content:space-between; align-items:baseline; padding-bottom:6px; border-bottom:1px solid var(--ink); font-family:var(--ui); font-size:9.5px; letter-spacing:.22em; text-transform:uppercase; margin-bottom:16px; }
|
|
34
|
+
.doc-head .brand-mark { font-weight:700; letter-spacing:.32em; }
|
|
35
|
+
.doc-head .sigil { font-family:var(--display); font-style:italic; letter-spacing:0; text-transform:none; font-size:12px; }
|
|
36
|
+
h1.title { font-family:var(--display); font-weight:400; font-size:38px; line-height:1; margin:0 0 4px; letter-spacing:-.01em; }
|
|
37
|
+
.subtitle { font-family:var(--display); font-style:italic; font-size:14px; margin-bottom:12px; }
|
|
38
|
+
.lede { font-family:var(--display); font-style:italic; font-size:13px; line-height:1.45; max-width:64ch; margin:0 0 14px; }
|
|
39
|
+
h2 { font-family:var(--display); font-weight:400; font-size:20px; margin:16px 0 6px; border-bottom:1px solid var(--ink); padding-bottom:3px; }
|
|
40
|
+
h2 .ord { font-family:var(--mono); font-size:10.5px; letter-spacing:.12em; opacity:.65; margin-right:8px; }
|
|
41
|
+
h3 { font-family:var(--ui); font-size:10px; font-weight:700; letter-spacing:.18em; text-transform:uppercase; margin:10px 0 4px; }
|
|
42
|
+
p, li { font-family:var(--body); font-size:11px; line-height:1.5; margin:0 0 5px; }
|
|
43
|
+
ul, ol { padding-left:18px; margin:0 0 6px; }
|
|
44
|
+
strong { font-family:var(--display); font-weight:700; }
|
|
45
|
+
em { font-style:italic; }
|
|
46
|
+
.mono, code { font-family:var(--mono); font-size:10px; }
|
|
47
|
+
.formula { font-family:var(--mono); font-size:9.5px; background:rgba(26,22,18,0.04); padding:6px 9px; border-left:2px solid var(--ink); margin:4px 0 8px; line-height:1.5; white-space:pre-wrap; }
|
|
48
|
+
table { width:100%; border-collapse:collapse; margin:6px 0 10px; font-size:9.5px; }
|
|
49
|
+
th, td { text-align:left; vertical-align:top; padding:3px 6px; line-height:1.35; border-bottom:.5px solid var(--rule-soft); }
|
|
50
|
+
th { font-family:var(--ui); font-weight:700; letter-spacing:.12em; text-transform:uppercase; font-size:8.5px; background:var(--ink); color:var(--parchment); border-bottom:1px solid var(--ink); }
|
|
51
|
+
td.n { font-family:var(--mono); font-size:10px; text-align:right; }
|
|
52
|
+
td.v { font-family:var(--ui); font-size:9px; font-weight:700; letter-spacing:.12em; text-transform:uppercase; }
|
|
53
|
+
td.v.pass { color:var(--commercial-infrastructure); }
|
|
54
|
+
td.v.warn { color:#B07A1F; }
|
|
55
|
+
td.v.fail { color:var(--highlight); }
|
|
56
|
+
.tag-pass { background:rgba(46,122,107,0.15); padding:1px 6px; font-family:var(--ui); font-size:8.5px; letter-spacing:.12em; text-transform:uppercase; font-weight:700; color:var(--commercial-infrastructure); }
|
|
57
|
+
.tag-warn { background:rgba(176,122,31,0.15); padding:1px 6px; font-family:var(--ui); font-size:8.5px; letter-spacing:.12em; text-transform:uppercase; font-weight:700; color:#B07A1F; }
|
|
58
|
+
.tag-fail { background:rgba(200,57,46,0.15); padding:1px 6px; font-family:var(--ui); font-size:8.5px; letter-spacing:.12em; text-transform:uppercase; font-weight:700; color:var(--highlight); }
|
|
59
|
+
.verdict { padding:12px 14px; border:1.5px solid var(--ink); background:var(--parchment-2); margin:10px 0; position:relative; }
|
|
60
|
+
.verdict::before { content:""; position:absolute; inset:5px; border:.5px solid var(--rule-soft); pointer-events:none; }
|
|
61
|
+
.verdict .stamp { display:inline-block; font-family:var(--display); font-size:14px; letter-spacing:.12em; text-transform:uppercase; border:1.5px solid #B07A1F; color:#B07A1F; padding:4px 10px; margin-bottom:6px; background:var(--parchment); }
|
|
62
|
+
.verdict h3 { font-family:var(--display); font-weight:400; font-size:18px; text-transform:none; letter-spacing:0; margin:0 0 6px; }
|
|
63
|
+
.callout { border-left:2px solid var(--highlight); padding:5px 10px; background:rgba(200,57,46,0.04); margin:6px 0; font-size:10.5px; }
|
|
64
|
+
.callout.ok { border-left-color:var(--commercial-infrastructure); background:rgba(46,122,107,0.05); }
|
|
65
|
+
.callout strong { color:var(--highlight); }
|
|
66
|
+
.callout.ok strong { color:var(--commercial-infrastructure); }
|
|
67
|
+
.signature { margin-top:14px; padding-top:8px; border-top:1px solid var(--ink); display:flex; justify-content:space-between; font-family:var(--ui); font-size:8.5px; letter-spacing:.16em; text-transform:uppercase; }
|
|
68
|
+
.bug { padding:8px 12px; border:1px solid var(--ink); background:var(--parchment-2); margin:8px 0; }
|
|
69
|
+
.bug h4 { font-family:var(--display); font-weight:700; font-size:13px; margin:0 0 4px; }
|
|
70
|
+
.bug .where { font-family:var(--mono); font-size:9.5px; opacity:.75; }
|
|
71
|
+
.bug .repro { font-family:var(--body); font-size:10.5px; margin:4px 0; }
|
|
72
|
+
.bug .patch { font-family:var(--mono); font-size:9.5px; background:rgba(26,22,18,0.04); border-left:2px solid var(--ink); padding:5px 8px; margin-top:4px; white-space:pre-wrap; }
|
|
73
|
+
</style>
|
|
74
|
+
</head>
|
|
75
|
+
<body>
|
|
76
|
+
|
|
77
|
+
<div class="toolbar"><button onclick="window.print()">Print / Save PDF</button></div>
|
|
78
|
+
|
|
79
|
+
<div class="viewport">
|
|
80
|
+
|
|
81
|
+
<!-- ============================================================
|
|
82
|
+
PAGE 1 — VERDICT + CHECKLIST
|
|
83
|
+
============================================================ -->
|
|
84
|
+
<section class="page">
|
|
85
|
+
<header class="doc-head">
|
|
86
|
+
<span class="brand-mark">SOVEREIGN</span>
|
|
87
|
+
<span class="sigil">Solo / Digital · Phase 3 audit</span>
|
|
88
|
+
<span>1 / 4</span>
|
|
89
|
+
</header>
|
|
90
|
+
|
|
91
|
+
<h1 class="title">Phase 3 Audit</h1>
|
|
92
|
+
<div class="subtitle">Local state machine · FC-AU-002 · audit of sovereign-solo-phase3-state-machine.html</div>
|
|
93
|
+
|
|
94
|
+
<p class="lede">Twelve checks were verified against the source. The state machine is functional: a solo human can complete a 7-lap game, all 24 cards and 7 Acts resolve correctly, and Influence is computed from state. Three issues require fixes before freeze.</p>
|
|
95
|
+
|
|
96
|
+
<h2><span class="ord">§ 01</span>Verdict</h2>
|
|
97
|
+
<div class="verdict">
|
|
98
|
+
<div class="stamp">Pass with fixes</div>
|
|
99
|
+
<h3>Functional · ship-ready after three targeted patches.</h3>
|
|
100
|
+
<p>Two real bugs (phase clobber on lap-7 end, missing bankruptcy tracking) and one minor scoring overcount were found. The audit also notes three named actions from the spec that exist as functionality but are folded into other actions or helpers rather than dispatched as discrete types. That last item is a deviation, not a bug — the reducer is still exhaustive over the dispatched action set.</p>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<h2><span class="ord">§ 02</span>Checklist · 12 of 12</h2>
|
|
104
|
+
<table>
|
|
105
|
+
<thead><tr><th>#</th><th>Check</th><th>Verdict</th><th>Notes</th></tr></thead>
|
|
106
|
+
<tbody>
|
|
107
|
+
<tr><td class="n">01</td><td>All 18 named actions present and reachable</td><td class="v warn">Partial</td><td>16 reducer cases present, each writes a ledger entry. Spec's <code>OPEN_ACT_VOTE</code> · <code>RESOLVE_ACT</code> · <code>UPDATE_TRACK</code> are functional but folded into <code>BEGIN_LAP</code> / <code>CAST_VOTE</code> / inline <code>adjustTrack()</code> helper. No fall-through no-ops.</td></tr>
|
|
108
|
+
<tr><td class="n">02</td><td>No placeholder / demo pills in runtime UI</td><td class="v pass">Pass</td><td>grep on "demo", "placeholder", "TODO" returns zero runtime UI hits.</td></tr>
|
|
109
|
+
<tr><td class="n">03</td><td>Determinism · seed feeds dice and shuffles</td><td class="v pass">Pass</td><td>Mulberry32 at lines 219–227. <code>s.rng()</code> called in <code>ROLL_DICE</code>, <code>shuffleDeck()</code>, Default upgrade-loss. Two runs with same seed + same decisions produce identical ledgers.</td></tr>
|
|
110
|
+
<tr><td class="n">04</td><td>All 40 spaces resolve in <code>RESOLVE_LANDING</code></td><td class="v pass">Pass</td><td>22 properties (sys-*), 4 routes, 2 institutions, 2 taxes (with optional resistBump), 6 card spaces (3 shock + 3 debate), 4 corners (start / crisis / safe / send). Source lines 660–698.</td></tr>
|
|
111
|
+
<tr><td class="n">05</td><td>All 24 cards have <code>resolve(state)</code></td><td class="v pass">Pass</td><td>12 Market Shock cards (lines 380–451) + 12 Republic Debate cards (lines 453–500). Strict Construction Objection presents <code>choices[]</code> array → <code>card-choice</code> phase → <code>RESOLVE_CARD_CHOICE</code> action.</td></tr>
|
|
112
|
+
<tr><td class="n">06</td><td>All 7 Acts apply correctly</td><td class="v pass">Pass</td><td>Funding · Assumption · Bank Charter · Tariff · Coinage · Manufactures · Excise. Each sets flags on <code>s.flags</code> and adjusts tracks per v0.2 rules.</td></tr>
|
|
113
|
+
<tr><td class="n">07</td><td>v0.2 balance values preserved exactly</td><td class="v pass">Pass</td><td>Route ladder <code>[0,25,50,100,150]</code>; Continental Certs base 4; Soldier Pay base 6; Capacity thresholds ≥ 6 (payment) and ≥ 8 (Influence); Coinage Act +1 Credit + 1 Capacity.</td></tr>
|
|
114
|
+
<tr><td class="n">08</td><td>Default · Rebellion fire</td><td class="v pass">Pass</td><td><code>adjustTrack</code> sets <code>pendingDefault</code> when credit = 0; <code>pendingRebellion</code> when resistance = 12. <code>dispatch()</code> auto-fires both. Cash halved + random upgrade lost on Default; Revenue upgrades zeroed + Whiskey owner → Crisis on Rebellion.</td></tr>
|
|
115
|
+
<tr><td class="n">09</td><td>Endgame Influence computed from state</td><td class="v warn">Mostly</td><td>9 of 10 rule categories computed from state via <code>computeFinalInfluence()</code>. <strong>Capacity ≥ 8 bonus over-counts</strong>: scales by total Mfg/Strategic properties owned rather than once per owner per system. See § 04 issue 03.</td></tr>
|
|
116
|
+
<tr><td class="n">10</td><td>Solo human can complete 7 laps</td><td class="v warn">Mostly</td><td>Loop reaches END_GAME via lap-7 completion AND via Default/Rebellion firing the relevant triggers, but the <strong>endgame UI does not render on the lap-7 completion path</strong> because <code>MOVE_TOKEN</code> clobbers the <code>phase</code> set by the recursive <code>END_GAME</code> call. See § 04 issue 01.</td></tr>
|
|
117
|
+
<tr><td class="n">11</td><td>No scripted opponent logic introduced</td><td class="v pass">Pass</td><td>Cards referencing other players (Mint payouts, Cabinet Bargain, National Lottery) log "solo: no opponent" or treat the active player as the sole recipient. No phantom-player dispatches.</td></tr>
|
|
118
|
+
<tr><td class="n">12</td><td>No developer-aside comments in source</td><td class="v pass">Pass</td><td>Source comments are production-style. No "hmm…", "TODO:", "let me change…", or "placeholder shows…".</td></tr>
|
|
119
|
+
</tbody>
|
|
120
|
+
</table>
|
|
121
|
+
|
|
122
|
+
<div class="signature"><span>Sovereign · Phase 3 Audit · FC-AU-002</span><span>1 / 4</span></div>
|
|
123
|
+
</section>
|
|
124
|
+
|
|
125
|
+
<!-- ============================================================
|
|
126
|
+
PAGE 2 — BUG 1 + BUG 2
|
|
127
|
+
============================================================ -->
|
|
128
|
+
<section class="page">
|
|
129
|
+
<header class="doc-head"><span class="brand-mark">SOVEREIGN</span><span class="sigil">Bugs found · 01–02</span><span>2 / 4</span></header>
|
|
130
|
+
|
|
131
|
+
<h2><span class="ord">§ 03</span>Bugs Found · Required Fixes</h2>
|
|
132
|
+
|
|
133
|
+
<div class="bug">
|
|
134
|
+
<h4>BUG-01 · <span class="tag-fail">Critical</span> · Endgame UI never renders on lap-7 completion</h4>
|
|
135
|
+
<div class="where">Source: <code>MOVE_TOKEN</code> reducer case, lines 630–644.</div>
|
|
136
|
+
<div class="repro">When the player passes Treasury Opens on what would be their 8th time across the start line (completing lap 7), <code>PASS_TREASURY_OPENS</code> is dispatched recursively. That handler increments <code>s.lap</code> to 8, calls <code>END_GAME</code>, and <code>END_GAME</code> sets <code>s.phase = 'game-over'</code> and <code>s.status = 'gameOver'</code>. Control returns to <code>MOVE_TOKEN</code>, which then unconditionally executes <code>s.phase = 'resolve-landing'</code> — clobbering the <code>game-over</code> phase. <code>render()</code> checks <code>STATE.phase !== 'game-over'</code> in <code>renderEndgame()</code> and hides the endgame view. The player sees the regular grid with status flagged but no scoring panel.</div>
|
|
137
|
+
<div class="patch">--- MOVE_TOKEN case
|
|
138
|
+
+++ MOVE_TOKEN case
|
|
139
|
+
case 'MOVE_TOKEN': {
|
|
140
|
+
const from = s.player.position;
|
|
141
|
+
let to = (from + action.amount) % 40;
|
|
142
|
+
const passedStart = (from + action.amount) >= 40;
|
|
143
|
+
s.player.position = to;
|
|
144
|
+
s = logRow(s, { actor:'You', event:'MOVE', detail: '...', cls:'move' });
|
|
145
|
+
if (passedStart && to !== 0) {
|
|
146
|
+
s = reduce(s, { type:'PASS_TREASURY_OPENS' });
|
|
147
|
+
} else if (to === 0) {
|
|
148
|
+
s = reduce(s, { type:'PASS_TREASURY_OPENS', landed: true });
|
|
149
|
+
}
|
|
150
|
+
- s.phase = 'resolve-landing';
|
|
151
|
+
+ if (s.status !== 'gameOver') s.phase = 'resolve-landing';
|
|
152
|
+
return s;
|
|
153
|
+
}</div>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<div class="bug">
|
|
157
|
+
<h4>BUG-02 · <span class="tag-fail">High</span> · Bankruptcy never tracked, never ends the game</h4>
|
|
158
|
+
<div class="where">Source: <code>player.bankruptLaps</code> declared in <code>initialState()</code> at line 521 but never modified. No <code>BANKRUPT</code> action exists.</div>
|
|
159
|
+
<div class="repro">Spec acceptance criterion #10 lists three game-termination paths: lap-7 completion, Default firing, and <em>player bankruptcy (cash < 0 with no remaining recoverable value)</em>. The current state machine has no bankruptcy detection. The player's cash can go arbitrarily negative through tax spaces, card effects, and Crisis exit costs. <code>computeFinalInfluence()</code> reads <code>s.player.bankruptLaps</code> for the <strong>− 1 Influence per lap spent bankrupt</strong> deduction (rule line in <code>computeFinalInfluence</code>) but that field is always zero. Effect: a player can play through 7 laps with negative cash and never lose Influence for it; no early-termination path triggers.</div>
|
|
160
|
+
<div class="patch">// Add to adjustCash() helper, after s.player.cash += delta:
|
|
161
|
+
+ if (s.player.cash < 0 && !s.player.bankruptThisLap) {
|
|
162
|
+
+ s.player.bankruptThisLap = true;
|
|
163
|
+
+ s.player.bankruptLaps += 1;
|
|
164
|
+
+ s = logRow(s, { actor:'System', event:'BANKRUPT',
|
|
165
|
+
+ detail:'Cash below zero · lap ' + s.lap + ' counted as bankrupt', cls:'event' });
|
|
166
|
+
+ }
|
|
167
|
+
|
|
168
|
+
// Reset the per-lap flag in PASS_TREASURY_OPENS, after lap increment:
|
|
169
|
+
+ s.player.bankruptThisLap = false;
|
|
170
|
+
|
|
171
|
+
// Optionally: add END_GAME early-termination for sustained negative cash:
|
|
172
|
+
// (e.g. if cash < -500 at end of turn, fire END_GAME with status:'bankrupt')</div>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
<div class="signature"><span>Sovereign · Phase 3 Audit · FC-AU-002</span><span>2 / 4</span></div>
|
|
176
|
+
</section>
|
|
177
|
+
|
|
178
|
+
<!-- ============================================================
|
|
179
|
+
PAGE 3 — BUG 3 + DEVIATION
|
|
180
|
+
============================================================ -->
|
|
181
|
+
<section class="page">
|
|
182
|
+
<header class="doc-head"><span class="brand-mark">SOVEREIGN</span><span class="sigil">Bugs · 03 · Deviation</span><span>3 / 4</span></header>
|
|
183
|
+
|
|
184
|
+
<div class="bug">
|
|
185
|
+
<h4>BUG-03 · <span class="tag-warn">Minor</span> · Capacity ≥ 8 endgame bonus over-counts</h4>
|
|
186
|
+
<div class="where">Source: <code>computeFinalInfluence()</code>, lines ~858–862.</div>
|
|
187
|
+
<div class="repro">The v0.2 rule reads: <em>"If Industrial Capacity ≥ 8, each Manufactures and Strategic Industry owner gains + 2 Influence."</em> The intended reading is one owner gains +2 per system they hold (i.e. up to +4 if they own properties in both systems). The implementation computes <code>n = countOwnedSys('manufactures') + countOwnedSys('strategic-industry')</code> and adds <code>n × 2</code> — that scales by the number of <em>properties</em> in those systems, not owners. A player with all 3 Manufactures + all 2 Strategic properties scores +10 instead of +4.</div>
|
|
188
|
+
<div class="patch">--- computeFinalInfluence, Capacity ≥ 8 block
|
|
189
|
+
+++ computeFinalInfluence, Capacity ≥ 8 block
|
|
190
|
+
- if (s.tracks.capacity.value >= 8) {
|
|
191
|
+
- const n = countOwnedSys(s, 'manufactures') + countOwnedSys(s, 'strategic-industry');
|
|
192
|
+
- if (n > 0) add('Capacity ≥ 8 bonus', n + ' Mfg/Strategic owners × 2', n * 2);
|
|
193
|
+
- }
|
|
194
|
+
+ if (s.tracks.capacity.value >= 8) {
|
|
195
|
+
+ let bonus = 0;
|
|
196
|
+
+ if (countOwnedSys(s, 'manufactures') > 0) bonus += 2;
|
|
197
|
+
+ if (countOwnedSys(s, 'strategic-industry') > 0) bonus += 2;
|
|
198
|
+
+ if (bonus > 0) add('Capacity ≥ 8 bonus',
|
|
199
|
+
+ 'owns Mfg ' + (countOwnedSys(s, 'manufactures') > 0 ? '✓' : '✕') +
|
|
200
|
+
+ ' / Strategic ' + (countOwnedSys(s, 'strategic-industry') > 0 ? '✓' : '✕') +
|
|
201
|
+
+ ' · +2 per system held', bonus);
|
|
202
|
+
+ }</div>
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
<h2><span class="ord">§ 04</span>Deviation Notes · Not Required Fixes</h2>
|
|
206
|
+
|
|
207
|
+
<div class="bug">
|
|
208
|
+
<h4>DEV-01 · Spec named 18 actions; reducer has 16 cases</h4>
|
|
209
|
+
<div class="where">Source: <code>reduce()</code> switch statement, lines 589–824.</div>
|
|
210
|
+
<div class="repro">The spec lists these named actions as required: <code>BEGIN_LAP · ROLL_DICE · MOVE_TOKEN · PASS_TREASURY_OPENS · RESOLVE_LANDING · BUY_ASSET · DECLINE_ASSET · UPGRADE_ASSET · DRAW_CARD · RESOLVE_CARD · RESOLVE_CARD_CHOICE · CAST_VOTE · TRIGGER_DEFAULT · TRIGGER_REBELLION · END_TURN · END_GAME</code> — that is 16. The spec narrative also lists <code>OPEN_ACT_VOTE</code>, <code>RESOLVE_ACT</code>, and <code>UPDATE_TRACK</code> as separate actions in its action-set introduction but the canonical list (above) is 16. The reducer matches the canonical list exactly.</div>
|
|
211
|
+
<div class="repro" style="margin-top:5px"><strong>If the audit's stricter reading (18 actions including <code>OPEN_ACT_VOTE</code> / <code>RESOLVE_ACT</code> / <code>UPDATE_TRACK</code>) is correct, the patch is to split those three behaviors out as discrete dispatched cases:</strong></div>
|
|
212
|
+
<div class="patch">case 'OPEN_ACT_VOTE': // currently part of BEGIN_LAP
|
|
213
|
+
case 'RESOLVE_ACT': // currently part of CAST_VOTE's "yes" branch
|
|
214
|
+
case 'UPDATE_TRACK': // currently inlined as adjustTrack() helper
|
|
215
|
+
|
|
216
|
+
// Behavior is unchanged; splitting just gives each its own ledger pen.
|
|
217
|
+
// Verdict: optional cleanup, not a blocking bug. Functionality is exhaustive.</div>
|
|
218
|
+
</div>
|
|
219
|
+
|
|
220
|
+
<h3>Other Deviations (intentional · spec-acknowledged)</h3>
|
|
221
|
+
<ul>
|
|
222
|
+
<li><strong>Failed Acts do not return to bottom of sequence.</strong> Phase 3 simplification — the failed Act is skipped this lap and the next lap reveals its own Act. The board game's "rotate failed Act one position back" mechanic is deferred to Phase 4. Ledger logs the simplification.</li>
|
|
223
|
+
<li><strong>Coinage Act pays 0 TN in solo.</strong> Correct: spec says "from each <em>other</em> player" and solo has no other players. Credit + Capacity track movements still apply. Ledger notes this.</li>
|
|
224
|
+
<li><strong>Auctions are skipped.</strong> Declined assets remain unowned. Phase 4 will add the auction mechanic. <code>Speculation Fever</code> card logs the simplification.</li>
|
|
225
|
+
<li><strong>Trade-based cards default to active-player-only effects.</strong> <code>Cabinet Bargain</code> grants the active player 1 Influence and applies the Resistance −1 without a trade partner.</li>
|
|
226
|
+
</ul>
|
|
227
|
+
|
|
228
|
+
<div class="signature"><span>Sovereign · Phase 3 Audit · FC-AU-002</span><span>3 / 4</span></div>
|
|
229
|
+
</section>
|
|
230
|
+
|
|
231
|
+
<!-- ============================================================
|
|
232
|
+
PAGE 4 — FREEZE VERDICT
|
|
233
|
+
============================================================ -->
|
|
234
|
+
<section class="page">
|
|
235
|
+
<header class="doc-head"><span class="brand-mark">SOVEREIGN</span><span class="sigil">Freeze verdict</span><span>4 / 4</span></header>
|
|
236
|
+
|
|
237
|
+
<h2><span class="ord">§ 05</span>Freeze Verdict</h2>
|
|
238
|
+
|
|
239
|
+
<div class="verdict">
|
|
240
|
+
<div class="stamp">Pass with fixes</div>
|
|
241
|
+
<h3>Apply BUG-01, BUG-02, BUG-03 · then Phase 3 is frozen.</h3>
|
|
242
|
+
<p><strong>BUG-01</strong> is the only true blocker: a player who plays cleanly to lap-7 end does not see the Endgame Report. The fix is a one-line guard. <strong>BUG-02</strong> is a quiet correctness bug: bankruptcy never affects scoring, which violates the rules. Two lines to fix. <strong>BUG-03</strong> over-rewards Mfg-heavy strategies in the endgame bonus by ~ 6 Influence at the high end. Four-line fix.</p>
|
|
243
|
+
<p>Once these three patches are applied, all 12 acceptance criteria pass cleanly. <strong>DEV-01</strong> is a documentation / spec-interpretation question rather than a code bug; either the canonical 16-action list in the spec is correct (in which case the implementation already matches) or the narrative's 18-action reading is correct (in which case a cleanup pass splits three behaviors into discrete cases without changing behavior). Recommend treating as a follow-up question for the spec author, not a freeze blocker.</p>
|
|
244
|
+
</div>
|
|
245
|
+
|
|
246
|
+
<h2><span class="ord">§ 06</span>Patch Application Order</h2>
|
|
247
|
+
<ol>
|
|
248
|
+
<li><strong>BUG-01 first.</strong> Guard the <code>s.phase = 'resolve-landing'</code> assignment in <code>MOVE_TOKEN</code> with a <code>status !== 'gameOver'</code> check.</li>
|
|
249
|
+
<li><strong>BUG-02 second.</strong> Add bankruptcy detection inside <code>adjustCash()</code>; reset the per-lap flag in <code>PASS_TREASURY_OPENS</code>. Optional: add a hard game-end at sustained negative cash.</li>
|
|
250
|
+
<li><strong>BUG-03 third.</strong> Rewrite the Capacity ≥ 8 block in <code>computeFinalInfluence()</code> to grant + 2 per <em>system held</em>, not per property.</li>
|
|
251
|
+
<li><strong>Smoke test.</strong> Replay seed 2026 with all three patches; confirm the lap-7 endgame fires, the Influence breakdown matches the new Capacity formula, and a forced-bankruptcy scenario (decline all assets, take Federal Excise three times) shows a non-zero <code>bankruptLaps</code> deduction at scoring.</li>
|
|
252
|
+
<li><strong>Freeze.</strong> Tag the file as <code>sovereign-solo-phase3-state-machine.html v0.2</code>. Phase 4 (scripted opponents) is unblocked.</li>
|
|
253
|
+
</ol>
|
|
254
|
+
|
|
255
|
+
<h2><span class="ord">§ 07</span>What Phase 4 Inherits</h2>
|
|
256
|
+
<ul>
|
|
257
|
+
<li><strong>State model.</strong> The pseudo-schema from the concept doc is implemented and stable. Phase 4 adds a <code>players[]</code> array and converts the singular <code>state.player</code> into the human entry.</li>
|
|
258
|
+
<li><strong>Reducer pattern.</strong> All 16 cases are pure functions on state. Phase 4 adds an <code>OPPONENT_TURN</code> driver and per-profile decision functions.</li>
|
|
259
|
+
<li><strong>Cards' multi-player branches.</strong> The "solo: no opponent" notes mark exactly where Phase 4 must wire in real opponent payouts. Those notes are searchable strings, making the migration mechanical.</li>
|
|
260
|
+
<li><strong>Deterministic replay.</strong> The seed pill + ledger contract is the foundation for the Replay mode in Phase 5. Phase 4 must preserve it: opponent decisions must be deterministic functions of <code>(rng, state)</code>.</li>
|
|
261
|
+
</ul>
|
|
262
|
+
|
|
263
|
+
<div class="signature"><span>Sovereign · Phase 3 Audit · FC-AU-002</span><span>End · 4 / 4</span></div>
|
|
264
|
+
</section>
|
|
265
|
+
|
|
266
|
+
</div>
|
|
267
|
+
</body>
|
|
268
|
+
</html>
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<title>Sovereign · Solo / Digital · Phase 4 Audit</title>
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--revolutionary-debt:#6E1F1E; --state-debt:#4A6B8A; --revenue-system:#C28A28;
|
|
10
|
+
--commercial-infrastructure:#2E7A6B; --national-finance:#1F2D52;
|
|
11
|
+
--manufactures:#8C8A2E; --strategic-industry:#3A3A3A;
|
|
12
|
+
--parchment:#F0E6CD; --parchment-2:#E6DABC; --ink:#1A1612; --highlight:#C8392E;
|
|
13
|
+
--rule:rgba(26,22,18,0.55); --rule-soft:rgba(26,22,18,0.22);
|
|
14
|
+
--display:"Baskerville","Big Caslon","Hoefler Text","Garamond","Times New Roman",serif;
|
|
15
|
+
--body:"Iowan Old Style","Georgia","Cambria","Times New Roman",serif;
|
|
16
|
+
--ui:-apple-system,"Segoe UI","Helvetica Neue","Arial",system-ui,sans-serif;
|
|
17
|
+
--mono:"SF Mono","Menlo","Consolas","Courier New",monospace;
|
|
18
|
+
}
|
|
19
|
+
@page { size:8.5in 11in; margin:.55in .65in; }
|
|
20
|
+
*,*::before,*::after { box-sizing:border-box; }
|
|
21
|
+
html,body { margin:0; padding:0; font-family:var(--body); color:var(--ink); background:#2A2622; -webkit-print-color-adjust:exact; print-color-adjust:exact; }
|
|
22
|
+
.viewport { display:flex; flex-direction:column; align-items:center; gap:22px; padding:28px 16px 80px; }
|
|
23
|
+
.page { width:8.5in; min-height:11in; background:var(--parchment); position:relative; box-shadow:0 10px 40px rgba(0,0,0,.55),0 2px 4px rgba(0,0,0,.4); padding:.55in .65in .5in; }
|
|
24
|
+
@media print {
|
|
25
|
+
html,body { background:#fff !important; }
|
|
26
|
+
.viewport { padding:0; gap:0; }
|
|
27
|
+
.page { box-shadow:none; padding:0; min-height:auto; width:100%; page-break-after:always; break-after:page; }
|
|
28
|
+
.page:last-child { page-break-after:auto; break-after:auto; }
|
|
29
|
+
.toolbar { display:none !important; }
|
|
30
|
+
}
|
|
31
|
+
.toolbar { position:fixed; top:16px; right:16px; z-index:30; }
|
|
32
|
+
.toolbar button { background:var(--parchment); color:var(--ink); border:1px solid var(--ink); padding:8px 14px; font-family:var(--ui); font-size:12px; letter-spacing:.08em; text-transform:uppercase; cursor:pointer; }
|
|
33
|
+
.doc-head { display:flex; justify-content:space-between; align-items:baseline; padding-bottom:6px; border-bottom:1px solid var(--ink); font-family:var(--ui); font-size:9.5px; letter-spacing:.22em; text-transform:uppercase; margin-bottom:16px; }
|
|
34
|
+
.doc-head .brand-mark { font-weight:700; letter-spacing:.32em; }
|
|
35
|
+
.doc-head .sigil { font-family:var(--display); font-style:italic; letter-spacing:0; text-transform:none; font-size:12px; }
|
|
36
|
+
h1.title { font-family:var(--display); font-weight:400; font-size:38px; line-height:1; margin:0 0 4px; letter-spacing:-.01em; }
|
|
37
|
+
.subtitle { font-family:var(--display); font-style:italic; font-size:14px; margin-bottom:12px; }
|
|
38
|
+
.lede { font-family:var(--display); font-style:italic; font-size:13px; line-height:1.45; max-width:64ch; margin:0 0 14px; }
|
|
39
|
+
h2 { font-family:var(--display); font-weight:400; font-size:20px; margin:16px 0 6px; border-bottom:1px solid var(--ink); padding-bottom:3px; }
|
|
40
|
+
h2 .ord { font-family:var(--mono); font-size:10.5px; letter-spacing:.12em; opacity:.65; margin-right:8px; }
|
|
41
|
+
h3 { font-family:var(--ui); font-size:10px; font-weight:700; letter-spacing:.18em; text-transform:uppercase; margin:10px 0 4px; }
|
|
42
|
+
p,li { font-family:var(--body); font-size:11px; line-height:1.5; margin:0 0 5px; }
|
|
43
|
+
ul,ol { padding-left:18px; margin:0 0 6px; }
|
|
44
|
+
strong { font-family:var(--display); font-weight:700; }
|
|
45
|
+
em { font-style:italic; }
|
|
46
|
+
.mono,code { font-family:var(--mono); font-size:10px; }
|
|
47
|
+
table { width:100%; border-collapse:collapse; margin:6px 0 10px; font-size:9.5px; }
|
|
48
|
+
th,td { text-align:left; vertical-align:top; padding:3px 6px; line-height:1.35; border-bottom:.5px solid var(--rule-soft); }
|
|
49
|
+
th { font-family:var(--ui); font-weight:700; letter-spacing:.12em; text-transform:uppercase; font-size:8.5px; background:var(--ink); color:var(--parchment); border-bottom:1px solid var(--ink); }
|
|
50
|
+
td.n { font-family:var(--mono); font-size:10px; text-align:right; }
|
|
51
|
+
td.v { font-family:var(--ui); font-size:9px; font-weight:700; letter-spacing:.12em; text-transform:uppercase; }
|
|
52
|
+
td.v.pass { color:var(--commercial-infrastructure); }
|
|
53
|
+
td.v.warn { color:#B07A1F; }
|
|
54
|
+
td.v.fail { color:var(--highlight); }
|
|
55
|
+
.tag-pass { background:rgba(46,122,107,0.15); padding:1px 6px; font-family:var(--ui); font-size:8.5px; letter-spacing:.12em; text-transform:uppercase; font-weight:700; color:var(--commercial-infrastructure); }
|
|
56
|
+
.tag-warn { background:rgba(176,122,31,0.15); padding:1px 6px; font-family:var(--ui); font-size:8.5px; letter-spacing:.12em; text-transform:uppercase; font-weight:700; color:#B07A1F; }
|
|
57
|
+
.tag-fail { background:rgba(200,57,46,0.15); padding:1px 6px; font-family:var(--ui); font-size:8.5px; letter-spacing:.12em; text-transform:uppercase; font-weight:700; color:var(--highlight); }
|
|
58
|
+
.verdict { padding:12px 14px; border:1.5px solid var(--ink); background:var(--parchment-2); margin:10px 0; position:relative; }
|
|
59
|
+
.verdict::before { content:""; position:absolute; inset:5px; border:.5px solid var(--rule-soft); pointer-events:none; }
|
|
60
|
+
.verdict .stamp { display:inline-block; font-family:var(--display); font-size:14px; letter-spacing:.12em; text-transform:uppercase; border:1.5px solid #B07A1F; color:#B07A1F; padding:4px 10px; margin-bottom:6px; background:var(--parchment); }
|
|
61
|
+
.verdict h3 { font-family:var(--display); font-weight:400; font-size:18px; text-transform:none; letter-spacing:0; margin:0 0 6px; }
|
|
62
|
+
.bug { padding:8px 12px; border:1px solid var(--ink); background:var(--parchment-2); margin:8px 0; }
|
|
63
|
+
.bug h4 { font-family:var(--display); font-weight:700; font-size:13px; margin:0 0 4px; }
|
|
64
|
+
.bug .where { font-family:var(--mono); font-size:9.5px; opacity:.75; }
|
|
65
|
+
.bug .repro { font-family:var(--body); font-size:10.5px; margin:4px 0; }
|
|
66
|
+
.bug .patch { font-family:var(--mono); font-size:9.5px; background:rgba(26,22,18,0.04); border-left:2px solid var(--ink); padding:5px 8px; margin-top:4px; white-space:pre-wrap; }
|
|
67
|
+
.signature { margin-top:14px; padding-top:8px; border-top:1px solid var(--ink); display:flex; justify-content:space-between; font-family:var(--ui); font-size:8.5px; letter-spacing:.16em; text-transform:uppercase; }
|
|
68
|
+
</style>
|
|
69
|
+
</head>
|
|
70
|
+
<body>
|
|
71
|
+
|
|
72
|
+
<div class="toolbar"><button onclick="window.print()">Print / Save PDF</button></div>
|
|
73
|
+
|
|
74
|
+
<div class="viewport">
|
|
75
|
+
|
|
76
|
+
<section class="page">
|
|
77
|
+
<header class="doc-head">
|
|
78
|
+
<span class="brand-mark">SOVEREIGN</span>
|
|
79
|
+
<span class="sigil">Solo / Digital · Phase 4 audit</span>
|
|
80
|
+
<span>1 / 4</span>
|
|
81
|
+
</header>
|
|
82
|
+
|
|
83
|
+
<h1 class="title">Phase 4 Audit</h1>
|
|
84
|
+
<div class="subtitle">Scripted opponents · FC-AU-003 · audit of sovereign-solo-phase4-scripted-opponents.html</div>
|
|
85
|
+
|
|
86
|
+
<p class="lede">Twelve checks verified against the source. Phase 4 successfully migrates Phase 3's solo state machine to a 3-player table with two deterministic profiles, auctions, and multi-player rent. Two critical bugs, three high/medium bugs, and one cleanup found.</p>
|
|
87
|
+
|
|
88
|
+
<h2><span class="ord">§ 01</span>Verdict</h2>
|
|
89
|
+
<div class="verdict">
|
|
90
|
+
<div class="stamp">Pass with fixes</div>
|
|
91
|
+
<h3>Functional. Two critical patches block freeze; three additional fixes recommended before Phase 5.</h3>
|
|
92
|
+
<p>Card-driven movement (Yellow Fever) and card-driven auctions (Speculation Fever) are both broken by the same root cause: <code>finishLanding()</code> unconditionally ends the turn after a card resolves, clobbering any sub-phase the card set up (auction) or any deferred landing-resolution flag (movement). These are the freeze blockers. The remaining issues are: failed Acts don't actually re-queue (ledger claims they will), opponent <code>decideBuy / decideAuctionBid</code> reasons are dropped at the BUY/DECLINE_ASSET boundary, Crisis-exit ignores the doubles/skip options, and Strict Construction's opponent choice is hardcoded instead of profile-driven.</p>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<h2><span class="ord">§ 02</span>Checklist · 12 of 12</h2>
|
|
96
|
+
<table>
|
|
97
|
+
<thead><tr><th>#</th><th>Check</th><th>Verdict</th><th>Notes</th></tr></thead>
|
|
98
|
+
<tbody>
|
|
99
|
+
<tr><td class="n">01</td><td><code>players[]</code> migration · 3 entries · activePlayerIndex rotates · endgame scores all 3</td><td class="v pass">Pass</td><td>3 players in <code>initialState()</code>. No orphan <code>state.player</code> refs found. Lap advances only when <code>activePlayerIndex</code> returns to 0 (in END_TURN). Endgame iterates all players.</td></tr>
|
|
100
|
+
<tr><td class="n">02</td><td>Reducer action surface · 16 Phase 3 actions + AUCTION_BID</td><td class="v pass">Pass</td><td>All 17 cases present, exhaustive switch, every dispatch writes ledger via <code>logRow</code> or its helpers.</td></tr>
|
|
101
|
+
<tr><td class="n">03</td><td>Determinism · seed feeds all RNG; no Date.now / Math.random</td><td class="v pass">Pass</td><td>Mulberry32 unchanged. Opponent decisions are pure on (state, playerIndex). <code>setTimeout</code> only affects UI cadence, not state. Tie-breaking in auctions is by turn order, deterministic.</td></tr>
|
|
102
|
+
<tr><td class="n">04</td><td>Opponent profiles match spec</td><td class="v pass">Pass</td><td>Treasury/Finance: NF > State Debt > Rev Debt > Bank > Mint; 90 % auction cap; vote rules per spec; Bank force-fee gated on owning Bank + Charter pending. Merchant/Infrastructure: routes first; 100 % cap on routes, 85 % on Commerce/Improvements; Tariff force-fee gated on Resistance ≤ 4. Every decision logs a reason referencing visible state.</td></tr>
|
|
103
|
+
<tr><td class="n">05</td><td>Auctions · DECLINE → auction · ladder · payment · tie-break</td><td class="v warn">Partial</td><td>Auction flow works for normal DECLINE_ASSET. Bidding starts to decliner's left, decliner bids last. Highest bid pays bid (not list), unsold → unowned, ties broken by turn order via ">" not "≥". <strong>Bug:</strong> card-triggered auctions (Speculation Fever) are clobbered by finishLanding. See BUG-02.</td></tr>
|
|
104
|
+
<tr><td class="n">06</td><td>Multi-player rent · all formulas</td><td class="v pass">Pass</td><td><code>computeRent()</code> implements: full-set ×2, tiers ×5/15/30, Funding ×1.5 permanent, Assumption ×2 permanent, Tariff ×1.5 permanent, Capacity ≥ 6 +25 %, route ladder 25/50/100/150, Credit ≤ 2 routes-halve, Bank 4×/10× dice, Mint 0/10×/20× dice. Owner never pays self (checked in RESOLVE_LANDING). Rent transfer ledgered with payer/payee/amount.</td></tr>
|
|
105
|
+
<tr><td class="n">07</td><td>Round modifiers expire correctly</td><td class="v pass">Pass</td><td>All flag-based modifiers use <code>=== s.lap</code> comparison; once lap advances the check fails naturally. Shipping Disruption uses <code>turnIndex</code> + 3 (spans one round in 3-player). Excise / Cotton Gin / Treaty / Bank Run / Anti-Fed Pamphlet all checked correctly in <code>computeRent</code> or in their card resolves.</td></tr>
|
|
106
|
+
<tr><td class="n">08</td><td>Acts and voting · majority of 3 · force-fee</td><td class="v warn">Partial</td><td>CAST_VOTE collects votes from all 3 players; majority (2 or 3 YES) passes. Ties never occur with 3 binary votes (1-2 always fails by majority rule, no abstention path). Passed Acts apply once. Force-fee logic exists in profile <code>decideEarlyVoteFee()</code> but is <strong>never called</strong> in the reducer. See BUG-04. Failed Acts log "will re-queue next lap" but don't actually re-queue. See BUG-03.</td></tr>
|
|
107
|
+
<tr><td class="n">09</td><td>Track thresholds in multi-player context</td><td class="v pass">Pass</td><td>Default applies cash-halve + random upgrade loss to <em>all 3 players</em>. Rebellion zeroes Revenue tiers across all players, sends owner of space 14 to Crisis if any player owns it. Capacity ≥ 6 affects rent in <code>computeRent</code>. Capacity ≥ 8 affects endgame.</td></tr>
|
|
108
|
+
<tr><td class="n">10</td><td>Endgame scoring · all 3 players · bug-03 preserved</td><td class="v pass">Pass</td><td><code>scorePlayer()</code> applies v0.2 categories per player. Capacity ≥ 8 uses correct per-system formula (+2/+2, max +4). National Finance split uses actual NF-owner count. Bankruptcy penalty applied per player. Winner card highlights highest <code>finalScores[].total</code>.</td></tr>
|
|
109
|
+
<tr><td class="n">11</td><td>UI integrity · 3 tokens · active indicator · panels · no placeholders</td><td class="v pass">Pass</td><td>Three colored tokens render on board; active player shown via header pill, board-center active line, and outlined opponent card. Auction panel hidden except during auction phase. No "demo" or "placeholder" pills in runtime UI. No dev-aside comments in source.</td></tr>
|
|
110
|
+
<tr><td class="n">12</td><td>Phase boundary · no LLM, no backend, no 4-player</td><td class="v pass">Pass</td><td>Source is vanilla JS only. No network calls. No 4-player mode. Profiles list contains only Treasury/Finance and Merchant/Infrastructure.</td></tr>
|
|
111
|
+
</tbody>
|
|
112
|
+
</table>
|
|
113
|
+
|
|
114
|
+
<div class="signature"><span>Sovereign · Phase 4 Audit · FC-AU-003</span><span>1 / 4</span></div>
|
|
115
|
+
</section>
|
|
116
|
+
|
|
117
|
+
<section class="page">
|
|
118
|
+
<header class="doc-head"><span class="brand-mark">SOVEREIGN</span><span class="sigil">Critical bugs · 01–02</span><span>2 / 4</span></header>
|
|
119
|
+
|
|
120
|
+
<h2><span class="ord">§ 03</span>Bugs Found</h2>
|
|
121
|
+
|
|
122
|
+
<div class="bug">
|
|
123
|
+
<h4>BUG-01 · <span class="tag-fail">Critical</span> · Card-driven movement (Yellow Fever) skips landing resolution</h4>
|
|
124
|
+
<div class="where">Source: <code>finishLanding()</code> and Market Shock card #8 "Yellow Fever Outbreak". <code>RESOLVE_CARD</code> calls <code>finishLanding(s)</code> which unconditionally calls <code>END_TURN</code>. Yellow Fever sets <code>s.pendingResolveLanding = true</code> intending to trigger a follow-up RESOLVE_LANDING for the new position. But END_TURN advances <code>activePlayerIndex</code> first. The <code>queueMicrotask</code> check then dispatches RESOLVE_LANDING for the <em>next</em> player at whatever space they're standing on.</div>
|
|
125
|
+
<div class="repro">Land on a Market Shock space, draw Yellow Fever (card #8). Player moves to nearest Commercial Infrastructure. No rent is paid even when that space is owned by an opponent. Next player's turn begins and their turn's landing fires as if they had rolled and landed — but they haven't rolled yet. Ledger shows an out-of-order RESOLVE event.</div>
|
|
126
|
+
<div class="patch">// In finishLanding(s):
|
|
127
|
+
function finishLanding(s) {
|
|
128
|
+
if (s.pendingResolveLanding) {
|
|
129
|
+
s.pendingResolveLanding = false;
|
|
130
|
+
return reduce(s, { type:'RESOLVE_LANDING' });
|
|
131
|
+
}
|
|
132
|
+
s.phase = 'awaiting-roll';
|
|
133
|
+
return reduce(s, { type:'END_TURN' });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Remove the matching pendingResolveLanding check from dispatch()
|
|
137
|
+
// since finishLanding now handles it inline before END_TURN.</div>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<div class="bug">
|
|
141
|
+
<h4>BUG-02 · <span class="tag-fail">Critical</span> · Card-driven auction (Speculation Fever) gets clobbered</h4>
|
|
142
|
+
<div class="where">Source: Market Shock card #3 "Speculation Fever" sets <code>s.pendingAuction</code> and <code>s.phase = 'auction'</code>. Then <code>RESOLVE_CARD</code> calls <code>finishLanding(s)</code>, which sets <code>s.phase = 'awaiting-roll'</code> and dispatches END_TURN — destroying the auction's phase before any bids can be collected.</div>
|
|
143
|
+
<div class="repro">Land on Market Shock, draw Speculation Fever. Track movement (Resistance +1) is logged. Auction is supposed to open for an unowned Rev/State Debt property. Instead the turn ends immediately and the auction is silently dropped. <code>pendingAuction</code> remains set on state but is never rendered or resolved.</div>
|
|
144
|
+
<div class="patch">// In finishLanding(s), respect card-set sub-phases:
|
|
145
|
+
function finishLanding(s) {
|
|
146
|
+
if (s.phase === 'auction') return s; // auction is live, hold
|
|
147
|
+
if (s.phase === 'card-choice') return s; // sub-decision pending
|
|
148
|
+
if (s.pendingResolveLanding) {
|
|
149
|
+
s.pendingResolveLanding = false;
|
|
150
|
+
return reduce(s, { type:'RESOLVE_LANDING' });
|
|
151
|
+
}
|
|
152
|
+
s.phase = 'awaiting-roll';
|
|
153
|
+
return reduce(s, { type:'END_TURN' });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// And in processNextAuctionBid() resolution path, end the
|
|
157
|
+
// triggering player's turn explicitly via END_TURN — already done.</div>
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<div class="bug">
|
|
161
|
+
<h4>BUG-03 · <span class="tag-warn">High</span> · Failed Acts do not re-queue · ledger claims otherwise</h4>
|
|
162
|
+
<div class="where">Source: <code>CAST_VOTE</code> case logs "will re-queue next lap" when an Act fails, but the reducer doesn't modify <code>state.acts</code> beyond clearing <code>current</code>. On the next lap, <code>BEGIN_LAP</code> calls <code>ACTS.find(a => a.lap === s.lap)</code> which returns the canonical Act for that lap — the failed Act is gone.</div>
|
|
163
|
+
<div class="repro">Vote NO three times on the Funding Act in lap 1. Ledger says "Funding Act fails · will re-queue next lap." Lap 2 begins, Assumption Act is revealed for vote. Funding Act is never seen again. Per Phase 3 spec this was an accepted simplification — but the ledger claim is misleading.</div>
|
|
164
|
+
<div class="patch">// Option A (matches Phase 3 simplification): just fix the log
|
|
165
|
+
- detail: act.name + ' fails ' + ... + ' · will re-queue next lap'
|
|
166
|
+
+ detail: act.name + ' fails ' + ... + ' · skipped (Phase 4 simplification; re-queue is Phase 5)'
|
|
167
|
+
|
|
168
|
+
// Option B (proper re-queue): maintain state.acts.failed[] queue
|
|
169
|
+
// and merge into BEGIN_LAP's act-lookup. Larger change; recommend
|
|
170
|
+
// Option A for freeze.</div>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<div class="signature"><span>Sovereign · Phase 4 Audit · FC-AU-003</span><span>2 / 4</span></div>
|
|
174
|
+
</section>
|
|
175
|
+
|
|
176
|
+
<section class="page">
|
|
177
|
+
<header class="doc-head"><span class="brand-mark">SOVEREIGN</span><span class="sigil">Medium bugs · 04–06</span><span>3 / 4</span></header>
|
|
178
|
+
|
|
179
|
+
<div class="bug">
|
|
180
|
+
<h4>BUG-04 · <span class="tag-warn">High</span> · Opponent decision reasons lost at BUY_ASSET / DECLINE_ASSET</h4>
|
|
181
|
+
<div class="where">Source: <code>renderControls()</code> calls <code>dispatch({type:'BUY_ASSET', reason})</code> for opponents after computing <code>decideBuy()</code>. The BUY_ASSET reducer case reads <code>s.pendingLanding</code> but ignores <code>action.reason</code>. The opponent's profile reason ("aligned with profile; cash 820 ≥ cost+reserve 420") is dropped; the ledger logs only the generic "Buy Customs House" entry. Same story for DECLINE_ASSET. <code>decideEarlyVoteFee()</code> is similarly never called in the reducer.</div>
|
|
182
|
+
<div class="repro">Open the ledger after Hamilton (Treasury/Finance) buys an asset. The reason for the buy is not present. The human cannot read the rule that fired without inspecting the source. Spec requirement § 04 says "every decision logs a reason referencing visible state" — partially met for votes / bids, not met for buy / decline / upgrade for opponents.</div>
|
|
183
|
+
<div class="patch">// In BUY_ASSET:
|
|
184
|
+
- s = logRow(s, { actor: p.name, event:'OWN',
|
|
185
|
+
- detail:'Now owns ' + sp.name, cls:'cash' });
|
|
186
|
+
+ s = logRow(s, { actor: p.name, event:'OWN',
|
|
187
|
+
+ detail:'Now owns ' + sp.name +
|
|
188
|
+
+ (action.reason ? ' · ' + action.reason : ''),
|
|
189
|
+
+ cls:'cash' });
|
|
190
|
+
|
|
191
|
+
// In DECLINE_ASSET: same pattern.
|
|
192
|
+
// In UPGRADE_ASSET: append action.reason if provided (already mostly done).
|
|
193
|
+
// Also call profile.decideEarlyVoteFee(s, pIdx) in runOpponent()
|
|
194
|
+
// before CAST_VOTE — currently the function is defined but never invoked.</div>
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
<div class="bug">
|
|
198
|
+
<h4>BUG-05 · <span class="tag-warn">Medium</span> · Crisis exit ignores doubles and skip-turn options</h4>
|
|
199
|
+
<div class="where">Source: <code>ROLL_DICE</code> case handles Crisis by auto-paying 50 TN if <code>p.cash >= 50</code>, otherwise skipping. Rules give three options: (a) pay 50 TN, (b) roll doubles, (c) skip one full turn and exit on the following turn. Only (a) is implemented. The human is denied agency; opponents waste cash they could have saved by rolling doubles or skipping.</div>
|
|
200
|
+
<div class="repro">Land on Constitutional Crisis. Next turn, ROLL_DICE is dispatched. Cash is silently debited 50 TN before the dice are rolled. The human never sees a choice; opponents never roll for doubles.</div>
|
|
201
|
+
<div class="patch">// Split Crisis exit into a pre-roll sub-phase for the human:
|
|
202
|
+
// when activePlayer is human and inCrisis, set phase='crisis-choice'
|
|
203
|
+
// and render a 3-button UI (Pay 50 / Roll for doubles / Skip turn).
|
|
204
|
+
// For opponents, use a profile-driven decision: roll if cash < 100,
|
|
205
|
+
// pay if cash ≥ 200, skip otherwise. Each path is deterministic
|
|
206
|
+
// and writes its reason to the ledger.
|
|
207
|
+
|
|
208
|
+
// Minimal patch for freeze: implement the human UI choice;
|
|
209
|
+
// opponents continue with the pay-50 fallback (cheap to keep).</div>
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
<div class="bug">
|
|
213
|
+
<h4>BUG-06 · <span class="tag-warn">Low</span> · Strict Construction Objection · opponent choice hardcoded</h4>
|
|
214
|
+
<div class="where">Source: <code>DRAW_CARD</code> case, opponent-path. When an opponent draws a card with choices (only Strict Construction), code dispatches <code>RESOLVE_CARD_CHOICE</code> with <code>choiceIndex: 1</code> (always "pay 50 TN") via setTimeout. This is profile-blind: Merchant/Infrastructure might prefer Crisis (it preserves cash for the route grand-slam); Treasury/Finance might prefer paying.</div>
|
|
215
|
+
<div class="repro">Have an opponent land on a Republic Debate space and draw card #1. They always pay 50 TN. Profile asset priorities don't enter the decision.</div>
|
|
216
|
+
<div class="patch">// Add a decideCardChoice(profile, player, card, state) function
|
|
217
|
+
// to each profile. Treasury/Finance: prefer paying. Merchant/Infra:
|
|
218
|
+
// prefer Crisis when cash < 200, else pay. Wire it in DRAW_CARD
|
|
219
|
+
// opponent-path:
|
|
220
|
+
+ const profile = PROFILES[s.players[pIdx].profile];
|
|
221
|
+
+ const idx = profile.decideCardChoice
|
|
222
|
+
+ ? profile.decideCardChoice(s, pIdx, card)
|
|
223
|
+
+ : 1;
|
|
224
|
+
+ setTimeout(() => dispatch({type:'RESOLVE_CARD_CHOICE', choiceIndex: idx}), 250);</div>
|
|
225
|
+
</div>
|
|
226
|
+
|
|
227
|
+
<h2><span class="ord">§ 04</span>Non-Blocking Deviations</h2>
|
|
228
|
+
<ul>
|
|
229
|
+
<li><strong>Opponent auto-resolution timer.</strong> Opponent decisions fire via <code>setTimeout</code> in <code>renderControls()</code> at 250 ms. If <code>render()</code> is called repeatedly in fast succession (it isn't, but defensively), multiple timers could stack. Each fires with a guard (<code>if STATE.phase !== 'asset-decision' || STATE.activePlayerIndex === 0 return</code>) so behavior is correct but the pattern is fragile. Recommend moving the auto-resolve to a single queue inside <code>dispatch()</code> rather than render-bound.</li>
|
|
230
|
+
<li><strong>Failed-Act re-queue.</strong> Acknowledged Phase 3 simplification, but the ledger should be corrected (see BUG-03).</li>
|
|
231
|
+
<li><strong>End-of-game Influence on Default during lap 7.</strong> If Default fires after turn 21 (the human's last turn) but before END_TURN advances, the lap-7 increment may double-fire. Verified safe by inspection — <code>pendingDefault</code> is processed before END_TURN — but worth adding a regression test.</li>
|
|
232
|
+
</ul>
|
|
233
|
+
|
|
234
|
+
<div class="signature"><span>Sovereign · Phase 4 Audit · FC-AU-003</span><span>3 / 4</span></div>
|
|
235
|
+
</section>
|
|
236
|
+
|
|
237
|
+
<section class="page">
|
|
238
|
+
<header class="doc-head"><span class="brand-mark">SOVEREIGN</span><span class="sigil">Freeze verdict · patch order</span><span>4 / 4</span></header>
|
|
239
|
+
|
|
240
|
+
<h2><span class="ord">§ 05</span>Freeze Verdict</h2>
|
|
241
|
+
<div class="verdict">
|
|
242
|
+
<div class="stamp">Pass with fixes</div>
|
|
243
|
+
<h3>Apply BUG-01 and BUG-02 immediately. BUG-03 through BUG-06 before Phase 5 spec lock.</h3>
|
|
244
|
+
<p><strong>BUG-01</strong> and <strong>BUG-02</strong> are blockers: they break two specific cards in a way that produces incorrect game outcomes (missed rent, dropped auctions). The fix is two lines in <code>finishLanding()</code>. <strong>BUG-03</strong> is a misleading ledger string with no behavioral consequence — fix in either a five-character text edit or a Phase 5 proper-re-queue implementation. <strong>BUG-04</strong> degrades the legibility goal that scripted opponents promised (every decision should be inspectable). <strong>BUG-05</strong> reduces human agency in a way the rules explicitly allow. <strong>BUG-06</strong> is profile fidelity at the edges.</p>
|
|
245
|
+
<p>After BUG-01 and BUG-02 are patched, Phase 4 is functional and freeze-ready. The remaining four bugs do not prevent completing a 3-player game.</p>
|
|
246
|
+
</div>
|
|
247
|
+
|
|
248
|
+
<h2><span class="ord">§ 06</span>Patch Application Order</h2>
|
|
249
|
+
<ol>
|
|
250
|
+
<li><strong>BUG-01 first.</strong> Move the <code>pendingResolveLanding</code> handling inside <code>finishLanding()</code>; remove the duplicate check from <code>dispatch()</code>.</li>
|
|
251
|
+
<li><strong>BUG-02 second.</strong> Add the <code>phase === 'auction'</code> and <code>phase === 'card-choice'</code> guards at the top of <code>finishLanding()</code>.</li>
|
|
252
|
+
<li><strong>Smoke test.</strong> Replay seed 2026. Walk through one Speculation Fever card and one Yellow Fever card. Confirm: (a) Speculation Fever opens the auction panel, all three players bid, asset transfers correctly; (b) Yellow Fever moves the player and triggers RESOLVE_LANDING at the new position with rent paid to opponent if owed.</li>
|
|
253
|
+
<li><strong>BUG-03 fix.</strong> One-line ledger text correction.</li>
|
|
254
|
+
<li><strong>BUG-04 fix.</strong> Append <code>action.reason</code> to BUY/DECLINE/UPGRADE ledger entries; call <code>decideEarlyVoteFee()</code> in <code>runOpponent()</code>.</li>
|
|
255
|
+
<li><strong>BUG-05 fix.</strong> Add the human Crisis-choice UI; opponents can keep the pay-50 fallback.</li>
|
|
256
|
+
<li><strong>BUG-06 fix.</strong> Add <code>decideCardChoice()</code> hooks per profile.</li>
|
|
257
|
+
<li><strong>Freeze.</strong> Tag as <code>sovereign-solo-phase4-scripted-opponents.html v0.2</code>. Phase 5 (narration + replay UI + save/load) is unblocked.</li>
|
|
258
|
+
</ol>
|
|
259
|
+
|
|
260
|
+
<h2><span class="ord">§ 07</span>What Phase 5 Inherits</h2>
|
|
261
|
+
<ul>
|
|
262
|
+
<li><strong>Multi-player state shape.</strong> <code>players[]</code> array is stable; per-player <code>bankruptThisLap</code> / <code>keptHamilton</code> / <code>inCrisis</code> fields in place.</li>
|
|
263
|
+
<li><strong>Deterministic ledger.</strong> Seed pill + ledger array is the foundation for replay scrubbing.</li>
|
|
264
|
+
<li><strong>Opponent decision functions.</strong> Pure on (state, playerIndex); ready to be moved into a worker for batch simulation in the balance-telemetry mode.</li>
|
|
265
|
+
<li><strong>Auction state machine.</strong> Ready to extend with mortgages in Phase 5 (mortgaged properties transfer with auction).</li>
|
|
266
|
+
<li><strong>Failed-Act queue.</strong> If Phase 5 wants real re-queue semantics, <code>state.acts.failed[]</code> already exists in the schema; just needs reducer wiring.</li>
|
|
267
|
+
</ul>
|
|
268
|
+
|
|
269
|
+
<div class="signature"><span>Sovereign · Phase 4 Audit · FC-AU-003</span><span>End · 4 / 4</span></div>
|
|
270
|
+
</section>
|
|
271
|
+
|
|
272
|
+
</div>
|
|
273
|
+
</body>
|
|
274
|
+
</html>
|