@mcptoolshop/sovereign 1.1.0 → 1.1.2
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 +86 -23
- package/README.es.md +12 -51
- package/README.fr.md +12 -51
- package/README.hi.md +12 -51
- package/README.it.md +12 -51
- package/README.ja.md +12 -51
- package/README.md +13 -52
- package/README.pt-BR.md +12 -51
- package/README.zh.md +12 -51
- package/package.json +1 -1
- package/release/CHANGELOG.md +89 -20
- package/release/digital-mode/README.txt +74 -86
- package/release/digital-mode/sovereign-solo.html +6459 -160
- package/release/balance-evidence/raw-data/sovereign-diagnosis-canonical-100-A.json +0 -1
- package/release/balance-evidence/raw-data/sovereign-diagnosis-canonical-100-B.json +0 -1
- package/release/balance-evidence/raw-data/sovereign-diagnosis-canonical-400.json +0 -1
- package/release/balance-evidence/raw-data/sovereign-diagnosis-mfg-mirror-100.json +0 -1
- package/release/balance-evidence/sovereign-failure-pressure-diagnosis.html +0 -494
- package/release/design-system/README.md +0 -30
- package/release/design-system/sovereign-screen-audit-v0.18.html +0 -183
- package/release/design-system/sovereign-visual-system-v0.18.html +0 -610
- package/release/digital-mode/sovereign-solo-v0.10-baseline.html +0 -3884
|
@@ -1,3884 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8" />
|
|
5
|
-
<title>Sovereign · Solo / Digital · Phase 6 Telemetry · Batch</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
|
-
--internal-improvements:#B85A28; --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
|
-
--p0: var(--state-debt);
|
|
19
|
-
--p1: var(--revolutionary-debt);
|
|
20
|
-
--p2: var(--commercial-infrastructure);
|
|
21
|
-
}
|
|
22
|
-
*,*::before,*::after { box-sizing:border-box; }
|
|
23
|
-
html, body { margin:0; padding:0; font-family:var(--body); color:var(--ink); background:#2A2622; min-height:100vh; }
|
|
24
|
-
button { font-family:var(--ui); font-size:11px; letter-spacing:.12em; text-transform:uppercase; cursor:pointer; padding:6px 12px; background:var(--parchment); color:var(--ink); border:1px solid var(--ink); }
|
|
25
|
-
button:hover:not([disabled]) { background:var(--ink); color:var(--parchment); }
|
|
26
|
-
button:focus { outline:2px solid var(--ink); outline-offset:2px; }
|
|
27
|
-
button.primary { background:var(--ink); color:var(--parchment); }
|
|
28
|
-
button.primary:hover:not([disabled]) { background:var(--national-finance); }
|
|
29
|
-
button[disabled] { opacity:.4; cursor:not-allowed; }
|
|
30
|
-
input[type="number"] { font-family:var(--mono); font-size:13px; padding:5px 8px; border:1px solid var(--ink); background:var(--parchment); width:90px; }
|
|
31
|
-
|
|
32
|
-
.app { display:grid; grid-template-rows:auto 1fr; min-height:100vh; max-width:1600px; margin:0 auto; background:var(--parchment); border-left:1px solid var(--ink); border-right:1px solid var(--ink); }
|
|
33
|
-
.topbar { background:var(--parchment); border-bottom:1px solid var(--ink); padding:12px 22px; display:grid; grid-template-columns:1fr auto; gap:14px; align-items:center; }
|
|
34
|
-
.brand .eyebrow { font-family:var(--ui); font-size:9px; letter-spacing:.32em; text-transform:uppercase; color:var(--national-finance); }
|
|
35
|
-
.brand .title { font-family:var(--display); font-size:22px; line-height:1; letter-spacing:-.01em; margin-top:2px; }
|
|
36
|
-
.brand .sub { font-family:var(--display); font-style:italic; font-size:11px; margin-top:2px; }
|
|
37
|
-
.controls { display:flex; gap:6px; align-items:center; flex-wrap:wrap; justify-content:flex-end; }
|
|
38
|
-
.seed-pill { font-family:var(--mono); font-size:10px; letter-spacing:.06em; padding:5px 10px; background:var(--ink); color:var(--parchment); }
|
|
39
|
-
.phase-pill { font-family:var(--ui); font-size:9px; letter-spacing:.18em; text-transform:uppercase; padding:5px 10px; background:var(--parchment-2); border:1px solid var(--ink); font-weight:700; }
|
|
40
|
-
.active-pill { font-family:var(--ui); font-size:9px; letter-spacing:.18em; text-transform:uppercase; padding:5px 10px; background:var(--p0); color:#fff; font-weight:700; }
|
|
41
|
-
.active-pill.opp1 { background:var(--p1); }
|
|
42
|
-
.active-pill.opp2 { background:var(--p2); }
|
|
43
|
-
|
|
44
|
-
.grid { display:grid; grid-template-columns:minmax(0,1fr) 440px; grid-template-rows:minmax(0,1fr) 200px; gap:12px; padding:12px 18px; min-height:0; }
|
|
45
|
-
.grid > * { min-width:0; min-height:0; }
|
|
46
|
-
.board-pane { grid-column:1; grid-row:1; display:flex; flex-direction:column; gap:8px; }
|
|
47
|
-
.panel-pane { grid-column:2; grid-row:1 / span 2; display:flex; flex-direction:column; gap:10px; overflow-y:auto; max-height:calc(100vh - 110px); }
|
|
48
|
-
.ledger-pane { grid-column:1; grid-row:2; }
|
|
49
|
-
|
|
50
|
-
.panel { background:var(--parchment); border:1.5px solid var(--ink); position:relative; padding:10px 12px; }
|
|
51
|
-
.panel::before { content:""; position:absolute; inset:4px; border:0.5px solid var(--rule-soft); pointer-events:none; }
|
|
52
|
-
.panel .panel-head { display:flex; justify-content:space-between; align-items:baseline; padding-bottom:5px; border-bottom:1px solid var(--ink); margin-bottom:7px; }
|
|
53
|
-
.panel .panel-head .name { font-family:var(--ui); font-size:9.5px; font-weight:700; letter-spacing:.26em; text-transform:uppercase; }
|
|
54
|
-
.panel .panel-head .surface-id { font-family:var(--mono); font-size:9px; letter-spacing:.18em; background:var(--ink); color:var(--parchment); padding:1px 6px; }
|
|
55
|
-
|
|
56
|
-
.board-view { position:relative; aspect-ratio:1; max-width:680px; margin:0 auto; width:100%; background:var(--parchment); border:2px solid var(--ink); }
|
|
57
|
-
.board-view::before { content:""; position:absolute; inset:6px; border:1px solid var(--ink); pointer-events:none; }
|
|
58
|
-
.board-grid { position:absolute; inset:14px; display:grid; grid-template-columns:1fr repeat(9,1fr) 1fr; grid-template-rows:1fr repeat(9,1fr) 1fr; gap:1px; background:var(--ink); }
|
|
59
|
-
.board-cell { background:var(--parchment); position:relative; padding:3px 4px; display:flex; flex-direction:column; justify-content:space-between; font-family:var(--ui); min-width:0; overflow:hidden; }
|
|
60
|
-
.board-cell .num { font-family:var(--mono); font-size:7.5px; opacity:.55; }
|
|
61
|
-
.board-cell .nm { font-family:var(--display); font-size:8.5px; line-height:1.05; text-wrap:balance; }
|
|
62
|
-
.board-cell .band { position:absolute; top:0; left:0; right:0; height:5px; }
|
|
63
|
-
.board-cell.empty { background:transparent; }
|
|
64
|
-
.board-cell.corner { background:var(--parchment-2); }
|
|
65
|
-
.board-cell.corner .nm { font-family:var(--display); font-size:9px; font-weight:700; }
|
|
66
|
-
.board-cell.tax .band { background:var(--highlight); }
|
|
67
|
-
.board-cell.card-debate .band { background:var(--ink); }
|
|
68
|
-
.board-cell.card-shock .band { background:var(--ink); background-image:linear-gradient(90deg,var(--highlight),var(--highlight) 50%,var(--ink) 50%); }
|
|
69
|
-
.board-cell.route .band { background:var(--ink); background-image:repeating-linear-gradient(90deg,transparent 0 4px,rgba(255,255,255,.5) 4px 5px); }
|
|
70
|
-
.board-cell.institution .band { background:var(--ink); background-image:radial-gradient(rgba(255,255,255,.7) 0.8px,transparent 1.2px); background-size:3px 3px; }
|
|
71
|
-
.board-cell.sys-revolutionary-debt .band { background:var(--revolutionary-debt); }
|
|
72
|
-
.board-cell.sys-state-debt .band { background:var(--state-debt); }
|
|
73
|
-
.board-cell.sys-revenue-system .band { background:var(--revenue-system); }
|
|
74
|
-
.board-cell.sys-commercial-infrastructure .band { background:var(--commercial-infrastructure); }
|
|
75
|
-
.board-cell.sys-national-finance .band { background:var(--national-finance); }
|
|
76
|
-
.board-cell.sys-internal-improvements .band { background:var(--internal-improvements); }
|
|
77
|
-
.board-cell.sys-manufactures .band { background:var(--manufactures); }
|
|
78
|
-
.board-cell.sys-strategic-industry .band { background:var(--strategic-industry); }
|
|
79
|
-
.board-cell .owner-dot { position:absolute; bottom:2px; right:2px; width:9px; height:9px; border-radius:50%; border:1px solid var(--ink); }
|
|
80
|
-
.board-cell.owned-p0 .owner-dot { background:var(--p0); }
|
|
81
|
-
.board-cell.owned-p1 .owner-dot { background:var(--p1); }
|
|
82
|
-
.board-cell.owned-p2 .owner-dot { background:var(--p2); }
|
|
83
|
-
.board-cell .tier { position:absolute; top:6px; right:2px; font-family:var(--mono); font-size:6.5px; font-weight:700; background:var(--parchment); padding:0 2px; border:0.5px solid var(--ink); }
|
|
84
|
-
.board-cell .tokens { position:absolute; top:6px; left:2px; display:flex; gap:1px; }
|
|
85
|
-
.board-cell .tokens .tok { width:9px; height:9px; border-radius:50%; border:1.5px solid var(--ink); }
|
|
86
|
-
.board-cell .tokens .tok.p0 { background:var(--p0); }
|
|
87
|
-
.board-cell .tokens .tok.p1 { background:var(--p1); }
|
|
88
|
-
.board-cell .tokens .tok.p2 { background:var(--p2); }
|
|
89
|
-
.board-cell.active-space { outline:3px solid var(--highlight); outline-offset:-3px; }
|
|
90
|
-
|
|
91
|
-
.board-center { grid-column:2 / span 9; grid-row:2 / span 9; background:var(--parchment); display:flex; flex-direction:column; align-items:center; justify-content:center; padding:10px; text-align:center; gap:5px; position:relative; }
|
|
92
|
-
.board-center::before, .board-center::after { content:""; position:absolute; left:10px; right:10px; height:1px; background:var(--ink); opacity:.4; }
|
|
93
|
-
.board-center::before { top:10px; } .board-center::after { bottom:10px; }
|
|
94
|
-
.bc-eyebrow { font-family:var(--ui); font-size:8px; letter-spacing:.32em; text-transform:uppercase; color:var(--national-finance); }
|
|
95
|
-
.bc-title { font-family:var(--display); font-size:22px; line-height:1; }
|
|
96
|
-
.bc-sub { font-family:var(--display); font-style:italic; font-size:10px; }
|
|
97
|
-
.bc-lap { font-family:var(--mono); font-size:10px; letter-spacing:.14em; background:var(--ink); color:var(--parchment); padding:3px 10px; margin-top:3px; }
|
|
98
|
-
.bc-tracks { display:flex; gap:8px; margin-top:5px; font-family:var(--ui); font-size:8.5px; }
|
|
99
|
-
.bc-tracks .t { padding:2px 7px; border:1px solid var(--ink); background:var(--parchment-2); }
|
|
100
|
-
.bc-tracks .t .v { font-family:var(--mono); font-size:10px; margin-left:3px; font-weight:700; }
|
|
101
|
-
.bc-tracks .t.credit { color:var(--national-finance); }
|
|
102
|
-
.bc-tracks .t.resist { color:var(--highlight); }
|
|
103
|
-
.bc-tracks .t.indust { color:var(--manufactures); }
|
|
104
|
-
.bc-acts { font-family:var(--ui); font-size:8px; letter-spacing:.16em; text-transform:uppercase; margin-top:4px; opacity:.75; }
|
|
105
|
-
.bc-active { font-family:var(--ui); font-size:9px; letter-spacing:.18em; text-transform:uppercase; margin-top:4px; font-weight:700; padding:2px 8px; }
|
|
106
|
-
|
|
107
|
-
.controls-bar { display:flex; gap:6px; flex-wrap:wrap; justify-content:center; padding:6px 0; align-items:center; }
|
|
108
|
-
|
|
109
|
-
.ledger { background:var(--parchment); border:1.5px solid var(--ink); padding:8px 12px; height:100%; overflow-y:auto; position:relative; font-family:var(--mono); font-size:10px; line-height:1.45; }
|
|
110
|
-
.ledger::before { content:""; position:absolute; inset:4px; border:0.5px solid var(--rule-soft); pointer-events:none; }
|
|
111
|
-
.ledger .head { display:flex; justify-content:space-between; align-items:center; font-family:var(--ui); font-size:9px; letter-spacing:.22em; text-transform:uppercase; border-bottom:1px solid var(--ink); padding-bottom:4px; margin-bottom:6px; font-weight:700; }
|
|
112
|
-
.ledger .head .surface-id { font-family:var(--mono); font-size:9px; background:var(--ink); color:var(--parchment); padding:1px 6px; letter-spacing:.18em; }
|
|
113
|
-
.ledger .row { display:grid; grid-template-columns:80px 130px 1fr; gap:8px; padding:1px 0; border-bottom:0.5px dashed var(--rule-soft); }
|
|
114
|
-
.ledger .row .stamp { opacity:.65; }
|
|
115
|
-
.ledger .row .actor { font-family:var(--ui); font-weight:700; font-size:9px; letter-spacing:.08em; text-transform:uppercase; }
|
|
116
|
-
.ledger .row.cash .actor { color:var(--national-finance); }
|
|
117
|
-
.ledger .row.track .actor { color:var(--manufactures); }
|
|
118
|
-
.ledger .row.vote .actor { color:var(--state-debt); }
|
|
119
|
-
.ledger .row.card .actor { color:var(--highlight); }
|
|
120
|
-
.ledger .row.move .actor { color:var(--commercial-infrastructure); }
|
|
121
|
-
.ledger .row.act .actor { color:var(--revolutionary-debt); }
|
|
122
|
-
.ledger .row.event .actor { color:var(--highlight); }
|
|
123
|
-
.ledger .row.auction .actor { color:var(--internal-improvements); }
|
|
124
|
-
.ledger .row.opp .actor { color:var(--ink); background:rgba(26,22,18,0.04); padding:0 4px; }
|
|
125
|
-
|
|
126
|
-
.treasury .cash-row { display:flex; justify-content:space-between; align-items:baseline; padding:4px 0; border-bottom:1px solid var(--ink); margin-bottom:4px; }
|
|
127
|
-
.treasury .cash { font-family:var(--display); font-size:22px; line-height:1; }
|
|
128
|
-
.treasury .lbl { font-family:var(--ui); font-size:9px; letter-spacing:.18em; text-transform:uppercase; }
|
|
129
|
-
.treasury .role-tag { display:inline-block; padding:1px 6px; background:var(--ink); color:var(--parchment); font-family:var(--ui); font-size:8.5px; letter-spacing:.14em; text-transform:uppercase; font-weight:700; }
|
|
130
|
-
.treasury .inf { font-family:var(--mono); font-size:9px; opacity:.85; margin-top:4px; text-align:right; }
|
|
131
|
-
.treasury .h-row { display:grid; grid-template-columns:auto 1fr auto auto; gap:6px; align-items:center; font-size:10px; padding:2px 4px; border-bottom:0.5px dashed var(--rule-soft); }
|
|
132
|
-
.treasury .sw { width:9px; height:9px; border:1px solid var(--ink); }
|
|
133
|
-
.treasury .tier-badge { font-family:var(--mono); font-size:8px; padding:1px 4px; background:var(--ink); color:var(--parchment); }
|
|
134
|
-
.treasury .tier-badge.muted { background:transparent; color:var(--ink); opacity:.5; border:0.5px solid var(--rule-soft); }
|
|
135
|
-
.treasury .up-btn { font-family:var(--ui); font-size:8px; padding:2px 6px; }
|
|
136
|
-
|
|
137
|
-
.opponents { display:flex; flex-direction:column; gap:8px; }
|
|
138
|
-
.opp-card { padding:8px 10px; background:var(--parchment-2); border:1px solid var(--ink); }
|
|
139
|
-
.opp-card .head { display:flex; justify-content:space-between; align-items:center; margin-bottom:4px; }
|
|
140
|
-
.opp-card .head .nm { font-family:var(--display); font-size:13px; }
|
|
141
|
-
.opp-card .head .pdot { width:12px; height:12px; border-radius:50%; border:1.5px solid var(--ink); }
|
|
142
|
-
.opp-card.p1 .head .pdot { background:var(--p1); }
|
|
143
|
-
.opp-card.p2 .head .pdot { background:var(--p2); }
|
|
144
|
-
.opp-card .pl { font-family:var(--ui); font-size:8.5px; letter-spacing:.18em; text-transform:uppercase; color:var(--national-finance); font-weight:700; }
|
|
145
|
-
.opp-card .cash { font-family:var(--display); font-size:14px; }
|
|
146
|
-
.opp-card .holdings { font-family:var(--body); font-size:10px; line-height:1.4; margin-top:4px; }
|
|
147
|
-
.opp-card .strategy { font-family:var(--display); font-style:italic; font-size:10px; margin-top:4px; padding-top:4px; border-top:0.5px dashed var(--rule-soft); }
|
|
148
|
-
.opp-card.active-opp { box-shadow:0 0 0 2px var(--highlight); }
|
|
149
|
-
|
|
150
|
-
.tracks-panel .t-row { display:grid; grid-template-columns:1fr auto; gap:8px; padding:6px 0; border-bottom:0.5px solid var(--rule-soft); }
|
|
151
|
-
.tracks-panel .lbl-t { font-family:var(--ui); font-size:9.5px; font-weight:700; letter-spacing:.14em; text-transform:uppercase; }
|
|
152
|
-
.tracks-panel .val { font-family:var(--display); font-size:18px; line-height:1; font-variant-numeric:tabular-nums; }
|
|
153
|
-
.tracks-panel .scale { grid-column:1 / span 2; display:grid; grid-template-columns:repeat(13,1fr); height:18px; border:1px solid var(--ink); margin-top:4px; }
|
|
154
|
-
.tracks-panel .tk { font-family:var(--mono); font-size:8.5px; display:flex; align-items:center; justify-content:center; border-right:0.5px solid var(--rule-soft); position:relative; }
|
|
155
|
-
.tracks-panel .tk:last-child { border-right:0; }
|
|
156
|
-
.tracks-panel .tk.major { background:rgba(26,22,18,0.05); }
|
|
157
|
-
.tracks-panel .tk.marker::after { content:""; position:absolute; inset:2px; border-radius:50%; border:2px solid var(--ink); }
|
|
158
|
-
.tracks-panel .t-row.credit .tk.marker::after { background:var(--national-finance); }
|
|
159
|
-
.tracks-panel .t-row.resist .tk.marker::after { background:var(--highlight); }
|
|
160
|
-
.tracks-panel .t-row.indust .tk.marker::after { background:var(--manufactures); }
|
|
161
|
-
.tracks-panel .reason { font-family:var(--mono); font-size:9px; opacity:.75; margin-top:3px; }
|
|
162
|
-
.tracks-panel .threshold-hit { font-family:var(--ui); font-size:8px; font-weight:700; letter-spacing:.14em; text-transform:uppercase; padding:2px 6px; background:var(--commercial-infrastructure); color:var(--parchment); margin-top:3px; display:inline-block; }
|
|
163
|
-
.tracks-panel .threshold-miss { font-family:var(--ui); font-size:8px; letter-spacing:.14em; text-transform:uppercase; padding:2px 6px; background:var(--parchment-2); border:0.5px solid var(--rule-soft); margin-top:3px; display:inline-block; opacity:.7; }
|
|
164
|
-
|
|
165
|
-
.acts-panel .current-act { padding:8px 10px; background:var(--ink); color:var(--parchment); }
|
|
166
|
-
.acts-panel .pretitle { font-family:var(--ui); font-size:8px; letter-spacing:.26em; text-transform:uppercase; }
|
|
167
|
-
.acts-panel .nm { font-family:var(--display); font-size:18px; line-height:1; margin-top:2px; }
|
|
168
|
-
.acts-panel .effect { font-family:var(--body); font-size:10px; line-height:1.4; margin-top:6px; }
|
|
169
|
-
.acts-panel .effect strong { color:var(--parchment); font-family:var(--display); font-weight:700; }
|
|
170
|
-
.acts-panel .votes { margin-top:8px; display:flex; flex-direction:column; gap:4px; }
|
|
171
|
-
.acts-panel .vote-row { display:grid; grid-template-columns:14px 1fr auto; gap:8px; align-items:center; padding:5px 8px; background:var(--parchment-2); border:1px solid var(--ink); font-size:10px; }
|
|
172
|
-
.acts-panel .vote-row.pending { background:var(--parchment); border-style:dashed; }
|
|
173
|
-
.acts-panel .vote-row .pdot { width:12px; height:12px; border-radius:50%; border:1.5px solid var(--ink); }
|
|
174
|
-
.acts-panel .vote-row.p0 .pdot { background:var(--p0); }
|
|
175
|
-
.acts-panel .vote-row.p1 .pdot { background:var(--p1); }
|
|
176
|
-
.acts-panel .vote-row.p2 .pdot { background:var(--p2); }
|
|
177
|
-
.acts-panel .vote-row .who { font-family:var(--ui); font-size:9px; letter-spacing:.12em; text-transform:uppercase; font-weight:700; }
|
|
178
|
-
.acts-panel .vote-row .reason { font-family:var(--body); font-size:10px; line-height:1.35; opacity:.85; display:block; }
|
|
179
|
-
.acts-panel .vote-row .ballot { font-family:var(--display); font-size:13px; font-weight:700; padding:2px 8px; border:1.5px solid var(--ink); min-width:40px; text-align:center; }
|
|
180
|
-
.acts-panel .vote-row .ballot.yes { background:var(--commercial-infrastructure); color:var(--parchment); }
|
|
181
|
-
.acts-panel .vote-row .ballot.no { background:var(--highlight); color:var(--parchment); }
|
|
182
|
-
.acts-panel .vote-row .ballot.pending { background:var(--parchment); }
|
|
183
|
-
.acts-panel .you-vote { display:flex; gap:6px; margin-top:6px; }
|
|
184
|
-
.acts-panel .tally { font-family:var(--mono); font-size:10px; letter-spacing:.1em; padding:4px 8px; margin-top:6px; text-align:center; }
|
|
185
|
-
.acts-panel .tally.passed { background:var(--commercial-infrastructure); color:var(--parchment); }
|
|
186
|
-
.acts-panel .tally.failed { background:var(--highlight); color:var(--parchment); }
|
|
187
|
-
.acts-panel .passed-tray { margin-top:8px; padding-top:6px; border-top:1px dashed var(--rule-soft); }
|
|
188
|
-
.acts-panel .passed-tray .lbl { font-family:var(--ui); font-size:8.5px; letter-spacing:.22em; text-transform:uppercase; font-weight:700; margin-bottom:4px; }
|
|
189
|
-
.acts-panel .tray-row { display:flex; gap:5px; flex-wrap:wrap; }
|
|
190
|
-
.acts-panel .slot { padding:3px 7px; background:var(--parchment); border:1px solid var(--ink); font-family:var(--display); font-size:10px; }
|
|
191
|
-
.acts-panel .slot .roman { font-weight:700; margin-right:3px; }
|
|
192
|
-
|
|
193
|
-
.inspector .card-art, .card-drawer .drawer-card, .auction .auction-card { padding:8px 10px; background:var(--parchment-2); border:1px solid var(--ink); margin-bottom:6px; }
|
|
194
|
-
.inspector .band, .card-drawer .band, .auction .band { padding:3px 6px; color:#fff; font-family:var(--ui); font-size:9px; letter-spacing:.18em; text-transform:uppercase; font-weight:700; display:flex; justify-content:space-between; }
|
|
195
|
-
.inspector .nm, .card-drawer .nm, .auction .nm { font-family:var(--display); font-size:16px; margin-top:5px; line-height:1.1; }
|
|
196
|
-
.inspector .subkind { font-family:var(--ui); font-size:8.5px; letter-spacing:.18em; text-transform:uppercase; opacity:.75; margin-top:2px; }
|
|
197
|
-
.inspector .flavor, .card-drawer .alert { font-family:var(--display); font-style:italic; font-size:10px; line-height:1.4; margin-top:6px; }
|
|
198
|
-
.inspector .pay-table { width:100%; border-collapse:collapse; margin-top:6px; }
|
|
199
|
-
.inspector .pay-table th, .inspector .pay-table td { font-family:var(--ui); font-size:9px; padding:3px 6px; border-bottom:0.5px solid var(--rule-soft); }
|
|
200
|
-
.inspector .pay-table th { background:var(--ink); color:var(--parchment); text-align:left; letter-spacing:.14em; text-transform:uppercase; }
|
|
201
|
-
.inspector .pay-table td.val { font-family:var(--display); font-size:11px; text-align:right; font-weight:700; font-variant-numeric:tabular-nums; }
|
|
202
|
-
.inspector .meta-row { font-family:var(--mono); font-size:9px; margin-top:4px; opacity:.8; }
|
|
203
|
-
.inspector .actions, .card-drawer .actions, .auction .actions { display:flex; gap:6px; margin-top:8px; flex-wrap:wrap; align-items:center; }
|
|
204
|
-
.card-drawer .effect { font-family:var(--body); font-size:11px; line-height:1.45; text-align:center; margin:8px 0; }
|
|
205
|
-
.card-drawer .effect strong { color:var(--national-finance); }
|
|
206
|
-
.card-drawer .outcomes { display:flex; gap:5px; justify-content:center; margin:6px 0; }
|
|
207
|
-
.card-drawer .chip { font-family:var(--ui); font-size:8.5px; font-weight:700; letter-spacing:.14em; text-transform:uppercase; padding:2px 6px; border:1px solid var(--ink); }
|
|
208
|
-
.card-drawer .chip.credit { background:var(--national-finance); color:#fff; }
|
|
209
|
-
.card-drawer .chip.resist { background:var(--highlight); color:#fff; }
|
|
210
|
-
.card-drawer .chip.indust { background:var(--manufactures); color:#fff; }
|
|
211
|
-
|
|
212
|
-
.auction .bid-list { margin:8px 0; display:flex; flex-direction:column; gap:3px; }
|
|
213
|
-
.auction .bid-row { display:grid; grid-template-columns:14px 80px 60px 1fr; gap:6px; align-items:center; font-size:10px; padding:3px 6px; background:var(--parchment); border:0.5px solid var(--rule-soft); }
|
|
214
|
-
.auction .bid-row .pdot { width:10px; height:10px; border-radius:50%; border:1px solid var(--ink); }
|
|
215
|
-
.auction .bid-row.p0 .pdot { background:var(--p0); }
|
|
216
|
-
.auction .bid-row.p1 .pdot { background:var(--p1); }
|
|
217
|
-
.auction .bid-row.p2 .pdot { background:var(--p2); }
|
|
218
|
-
.auction .bid-row .who { font-family:var(--ui); font-size:9px; font-weight:700; letter-spacing:.1em; text-transform:uppercase; }
|
|
219
|
-
.auction .bid-row .amt { font-family:var(--display); font-size:13px; font-weight:700; }
|
|
220
|
-
.auction .bid-row .reason { font-family:var(--body); font-size:9.5px; opacity:.8; line-height:1.3; }
|
|
221
|
-
.auction .high-bid { font-family:var(--mono); font-size:10px; padding:4px 8px; background:var(--parchment-2); border:1px solid var(--ink); margin-top:6px; }
|
|
222
|
-
|
|
223
|
-
.endgame { padding:18px 24px; }
|
|
224
|
-
.endgame h2 { font-family:var(--display); font-size:30px; line-height:1; margin:0 0 4px; }
|
|
225
|
-
.endgame .surface-id { font-family:var(--mono); font-size:11px; background:var(--ink); color:var(--parchment); padding:2px 8px; vertical-align:middle; margin-left:6px; letter-spacing:.18em; }
|
|
226
|
-
.endgame .sub { font-family:var(--display); font-style:italic; font-size:13px; margin-bottom:12px; }
|
|
227
|
-
.endgame .winner { padding:12px 16px; background:var(--commercial-infrastructure); color:var(--parchment); border:1.5px solid var(--ink); margin-bottom:14px; }
|
|
228
|
-
.endgame .winner .lbl { font-family:var(--ui); font-size:9px; letter-spacing:.26em; text-transform:uppercase; }
|
|
229
|
-
.endgame .winner .nm { font-family:var(--display); font-size:22px; line-height:1; margin-top:2px; }
|
|
230
|
-
.endgame .winner .score { font-family:var(--mono); font-size:14px; margin-top:4px; }
|
|
231
|
-
.endgame .results { display:grid; grid-template-columns:repeat(3,1fr); gap:10px; }
|
|
232
|
-
.endgame .pcol { padding:10px 12px; background:var(--parchment-2); border:1px solid var(--ink); }
|
|
233
|
-
.endgame .pcol h3 { font-family:var(--display); font-size:16px; margin:0 0 4px; }
|
|
234
|
-
.endgame .pcol .pl { font-family:var(--ui); font-size:8.5px; letter-spacing:.18em; text-transform:uppercase; color:var(--national-finance); }
|
|
235
|
-
.endgame .pcol table { width:100%; border-collapse:collapse; margin-top:6px; }
|
|
236
|
-
.endgame .pcol table td { font-size:9.5px; padding:2px 4px; border-bottom:0.5px solid var(--rule-soft); }
|
|
237
|
-
.endgame .pcol table td.n { font-family:var(--mono); text-align:right; font-weight:700; }
|
|
238
|
-
.endgame .pcol .total { font-family:var(--display); font-size:22px; margin-top:6px; text-align:right; font-weight:700; }
|
|
239
|
-
.endgame .actions { margin-top:14px; display:flex; gap:8px; }
|
|
240
|
-
|
|
241
|
-
.hidden { display:none !important; }
|
|
242
|
-
|
|
243
|
-
/* =====================================================================
|
|
244
|
-
PHASE 5 — narration drawer, resume pill, replay overlay, save UI
|
|
245
|
-
===================================================================== */
|
|
246
|
-
.resume-pill { position:fixed; top:14px; left:50%; transform:translateX(-50%); z-index:50;
|
|
247
|
-
background:var(--ink); color:var(--parchment); border:1px solid var(--parchment-2);
|
|
248
|
-
padding:10px 14px; display:flex; gap:10px; align-items:center; font-family:var(--ui);
|
|
249
|
-
font-size:10px; letter-spacing:.14em; text-transform:uppercase; box-shadow:0 4px 14px rgba(0,0,0,.35); }
|
|
250
|
-
.resume-pill .lbl { font-family:var(--display); font-style:italic; font-size:13px; letter-spacing:0; text-transform:none; }
|
|
251
|
-
.resume-pill button { font-size:9.5px; padding:5px 10px; }
|
|
252
|
-
|
|
253
|
-
.io-pill { font-family:var(--ui); font-size:9px; letter-spacing:.14em; text-transform:uppercase;
|
|
254
|
-
padding:4px 10px; margin-left:6px; border:1px solid var(--ink); background:var(--parchment-2); }
|
|
255
|
-
.io-pill.ok { background:var(--commercial-infrastructure); color:#fff; border-color:var(--ink); }
|
|
256
|
-
.io-pill.err { background:var(--highlight); color:#fff; border-color:var(--ink); }
|
|
257
|
-
|
|
258
|
-
.narration-panel { padding:10px 12px; }
|
|
259
|
-
.narration-panel .empty { font-family:var(--display); font-style:italic; font-size:11px; opacity:.55; }
|
|
260
|
-
.narration-entry { padding:9px 11px; border:1px solid var(--ink); background:var(--parchment-2); margin-bottom:7px; position:relative; }
|
|
261
|
-
.narration-entry .trig { font-family:var(--ui); font-size:8px; letter-spacing:.22em; text-transform:uppercase; color:var(--national-finance); font-weight:700; }
|
|
262
|
-
.narration-entry h4 { margin:2px 0 4px; font-family:var(--display); font-weight:400; font-style:italic; font-size:13.5px; line-height:1.15; }
|
|
263
|
-
.narration-entry .snip { font-family:var(--body); font-size:11px; line-height:1.5; }
|
|
264
|
-
.narration-entry .snip em { font-style:italic; }
|
|
265
|
-
.narration-entry .snip strong { font-family:var(--display); font-weight:700; font-style:normal; }
|
|
266
|
-
.narration-entry .meta { display:flex; justify-content:space-between; align-items:center; margin-top:5px; gap:6px; }
|
|
267
|
-
.narration-entry .stamp { font-family:var(--mono); font-size:8.5px; opacity:.6; letter-spacing:.06em; }
|
|
268
|
-
.narration-entry .expand-btn { font-family:var(--ui); font-size:8.5px; letter-spacing:.18em; text-transform:uppercase; padding:3px 8px; border:0.5px solid var(--ink); background:var(--parchment); cursor:pointer; }
|
|
269
|
-
.narration-entry .expand-btn:hover { background:var(--ink); color:var(--parchment); }
|
|
270
|
-
.narration-entry .long { display:none; margin-top:6px; padding-top:6px; border-top:0.5px dashed var(--rule-soft); font-family:var(--body); font-size:10.5px; line-height:1.55; }
|
|
271
|
-
.narration-entry.expanded .long { display:block; }
|
|
272
|
-
.narration-panel .controls-row { display:flex; gap:6px; align-items:center; margin-bottom:6px; }
|
|
273
|
-
.narration-panel .pref-toggle { font-family:var(--ui); font-size:9px; letter-spacing:.14em; text-transform:uppercase; cursor:pointer; padding:3px 8px; border:0.5px solid var(--ink); background:var(--parchment); }
|
|
274
|
-
.narration-panel .pref-toggle.on { background:var(--ink); color:var(--parchment); }
|
|
275
|
-
.narration-panel .count { font-family:var(--mono); font-size:9px; opacity:.7; margin-left:auto; }
|
|
276
|
-
|
|
277
|
-
.endgame-narration { margin-top:18px; padding:14px 18px; background:var(--parchment-2); border:1.5px solid var(--ink); position:relative; }
|
|
278
|
-
.endgame-narration::before { content:""; position:absolute; inset:4px; border:0.5px solid var(--rule-soft); pointer-events:none; }
|
|
279
|
-
.endgame-narration h3 { font-family:var(--display); font-weight:400; font-style:italic; font-size:18px; margin:0 0 6px; }
|
|
280
|
-
.endgame-narration p { font-family:var(--body); font-size:12px; line-height:1.6; margin:0 0 8px; max-width:780px; }
|
|
281
|
-
.endgame-narration p:last-child { margin-bottom:0; }
|
|
282
|
-
.endgame-narration .meta-line { font-family:var(--mono); font-size:9px; letter-spacing:.14em; text-transform:uppercase; opacity:.65; margin-top:8px; }
|
|
283
|
-
|
|
284
|
-
.replay-overlay { position:fixed; inset:0; z-index:80; background:rgba(26,22,18,0.84); display:flex; flex-direction:column; padding:18px 22px; overflow:auto; }
|
|
285
|
-
.replay-overlay .replay-frame { background:var(--parchment); border:1.5px solid var(--ink); flex:1; display:flex; flex-direction:column; min-height:0; max-width:1600px; margin:0 auto; width:100%; position:relative; }
|
|
286
|
-
.replay-overlay .replay-frame::before { content:""; position:absolute; inset:4px; border:0.5px solid var(--rule-soft); pointer-events:none; }
|
|
287
|
-
.replay-head { padding:10px 16px; border-bottom:1px solid var(--ink); display:grid; grid-template-columns:1fr auto; gap:12px; align-items:center; }
|
|
288
|
-
.replay-head .ttl { font-family:var(--display); font-size:18px; }
|
|
289
|
-
.replay-head .ttl .sub { font-family:var(--display); font-style:italic; font-size:11px; display:block; opacity:.75; }
|
|
290
|
-
.replay-head .actions { display:flex; gap:6px; align-items:center; }
|
|
291
|
-
.replay-body { padding:10px 16px; display:grid; grid-template-columns:minmax(0,1fr) 360px; gap:12px; flex:1; min-height:0; }
|
|
292
|
-
.replay-board-col { display:flex; flex-direction:column; gap:8px; min-width:0; }
|
|
293
|
-
.replay-board { aspect-ratio:1; max-width:540px; margin:0 auto; width:100%; background:var(--parchment); border:2px solid var(--ink); position:relative; }
|
|
294
|
-
.replay-board .board-grid { position:absolute; inset:10px; display:grid; grid-template-columns:1fr repeat(9,1fr) 1fr; grid-template-rows:1fr repeat(9,1fr) 1fr; gap:1px; background:var(--ink); }
|
|
295
|
-
.replay-board .board-center { font-size:80%; }
|
|
296
|
-
.replay-board .bc-title { font-size:18px; }
|
|
297
|
-
.replay-board .bc-sub { font-size:9px; }
|
|
298
|
-
.replay-board .bc-tracks .t { font-size:8px; }
|
|
299
|
-
.replay-board .nm { font-size:7.5px; }
|
|
300
|
-
.replay-meta { display:grid; grid-template-columns:repeat(3,1fr); gap:6px; padding:6px 0; }
|
|
301
|
-
.replay-meta .m { padding:6px 8px; background:var(--parchment-2); border:1px solid var(--ink); }
|
|
302
|
-
.replay-meta .m .lbl { font-family:var(--ui); font-size:8.5px; letter-spacing:.18em; text-transform:uppercase; opacity:.75; }
|
|
303
|
-
.replay-meta .m .v { font-family:var(--display); font-size:18px; line-height:1; margin-top:2px; }
|
|
304
|
-
.replay-meta .m .v.cash { font-family:var(--display); font-size:14px; }
|
|
305
|
-
.replay-side { display:flex; flex-direction:column; gap:8px; overflow-y:auto; min-height:0; }
|
|
306
|
-
.replay-side .ledger-mini { font-family:var(--mono); font-size:9.5px; line-height:1.4; padding:8px 10px; background:var(--parchment); border:1px solid var(--ink); height:100%; overflow-y:auto; }
|
|
307
|
-
.replay-side .ledger-mini .row { display:grid; grid-template-columns:60px 110px 1fr; gap:6px; padding:1px 0; border-bottom:0.5px dashed var(--rule-soft); }
|
|
308
|
-
.replay-side .ledger-mini .row .stamp { opacity:.6; }
|
|
309
|
-
.replay-side .ledger-mini .row .actor { font-family:var(--ui); font-weight:700; font-size:8.5px; letter-spacing:.08em; text-transform:uppercase; }
|
|
310
|
-
|
|
311
|
-
.scrubber { padding:10px 16px; border-top:1px solid var(--ink); display:flex; flex-direction:column; gap:8px; background:var(--parchment-2); }
|
|
312
|
-
.scrubber .row-1 { display:flex; gap:6px; align-items:center; }
|
|
313
|
-
.scrubber .ticks { flex:1; position:relative; height:36px; background:var(--parchment); border:1px solid var(--ink); cursor:pointer; }
|
|
314
|
-
.scrubber .tick { position:absolute; top:0; bottom:0; border-right:0.5px solid rgba(26,22,18,0.18); }
|
|
315
|
-
.scrubber .tick.lap-start { border-right:1px solid var(--ink); }
|
|
316
|
-
.scrubber .tick.lap-start::after { content:attr(data-lap); position:absolute; top:2px; left:3px; font-family:var(--ui); font-size:8px; letter-spacing:.14em; color:var(--national-finance); font-weight:700; }
|
|
317
|
-
.scrubber .marker { position:absolute; top:-3px; bottom:-3px; width:3px; background:var(--highlight); pointer-events:none; }
|
|
318
|
-
.scrubber .scrub-meta { font-family:var(--mono); font-size:10px; letter-spacing:.1em; padding:4px 10px; background:var(--ink); color:var(--parchment); min-width:120px; text-align:center; }
|
|
319
|
-
.scrubber .integrity { font-family:var(--ui); font-size:9px; letter-spacing:.14em; text-transform:uppercase; padding:3px 8px; }
|
|
320
|
-
.scrubber .integrity.ok { background:var(--commercial-infrastructure); color:#fff; }
|
|
321
|
-
.scrubber .integrity.err { background:var(--highlight); color:#fff; }
|
|
322
|
-
|
|
323
|
-
/* =====================================================================
|
|
324
|
-
PHASE 6 — balance report + batch simulation
|
|
325
|
-
===================================================================== */
|
|
326
|
-
.balance-report { margin-top:20px; padding:16px 20px; background:var(--parchment); border:1.5px solid var(--ink); position:relative; }
|
|
327
|
-
.balance-report::before { content:""; position:absolute; inset:5px; border:0.5px solid var(--rule-soft); pointer-events:none; }
|
|
328
|
-
.balance-report h3 { font-family:var(--display); font-weight:400; font-size:22px; line-height:1; margin:0 0 6px; }
|
|
329
|
-
.balance-report .sub { font-family:var(--display); font-style:italic; font-size:12px; margin-bottom:12px; }
|
|
330
|
-
.balance-report .surface-id { font-family:var(--mono); font-size:10px; background:var(--ink); color:var(--parchment); padding:2px 7px; vertical-align:middle; margin-left:6px; letter-spacing:.18em; }
|
|
331
|
-
.balance-report .br-actions { display:flex; gap:6px; margin:10px 0 14px; flex-wrap:wrap; }
|
|
332
|
-
.br-section { margin-top:14px; }
|
|
333
|
-
.br-section h4 { font-family:var(--ui); font-size:10px; font-weight:700; letter-spacing:.22em; text-transform:uppercase; margin:0 0 6px; padding-bottom:3px; border-bottom:1px solid var(--ink); }
|
|
334
|
-
.br-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:8px; }
|
|
335
|
-
.br-card { padding:9px 11px; background:var(--parchment-2); border:1px solid var(--ink); }
|
|
336
|
-
.br-card .who { font-family:var(--display); font-size:14px; line-height:1; }
|
|
337
|
-
.br-card .who .pdot { display:inline-block; width:10px; height:10px; border-radius:50%; border:1.5px solid var(--ink); margin-right:5px; vertical-align:middle; }
|
|
338
|
-
.br-card.p0 .pdot { background:var(--p0); } .br-card.p1 .pdot { background:var(--p1); } .br-card.p2 .pdot { background:var(--p2); }
|
|
339
|
-
.br-card .role { font-family:var(--ui); font-size:8.5px; letter-spacing:.18em; text-transform:uppercase; color:var(--national-finance); margin-top:2px; }
|
|
340
|
-
.br-table { width:100%; border-collapse:collapse; margin-top:6px; }
|
|
341
|
-
.br-table td { font-family:var(--ui); font-size:9.5px; padding:2px 4px; border-bottom:0.5px solid var(--rule-soft); }
|
|
342
|
-
.br-table td.lbl { width:60%; }
|
|
343
|
-
.br-table td.val { font-family:var(--mono); text-align:right; font-weight:700; font-variant-numeric:tabular-nums; }
|
|
344
|
-
.br-table td.val.pos { color:var(--commercial-infrastructure); }
|
|
345
|
-
.br-table td.val.neg { color:var(--highlight); }
|
|
346
|
-
.br-section table.full { width:100%; border-collapse:collapse; }
|
|
347
|
-
.br-section table.full th { background:var(--ink); color:var(--parchment); font-family:var(--ui); font-size:8.5px; letter-spacing:.14em; text-transform:uppercase; padding:4px 7px; text-align:left; }
|
|
348
|
-
.br-section table.full td { font-family:var(--ui); font-size:10px; padding:3px 7px; border-bottom:0.5px solid var(--rule-soft); }
|
|
349
|
-
.br-section table.full td.n { font-family:var(--mono); text-align:right; font-variant-numeric:tabular-nums; }
|
|
350
|
-
.br-section table.full td.pass { color:var(--commercial-infrastructure); font-weight:700; letter-spacing:.1em; text-transform:uppercase; font-size:9px; }
|
|
351
|
-
.br-section table.full td.fail { color:var(--highlight); font-weight:700; letter-spacing:.1em; text-transform:uppercase; font-size:9px; }
|
|
352
|
-
.br-section table.full td.pdot-cell { width:18px; }
|
|
353
|
-
.br-section table.full td.pdot-cell .pdot { width:10px; height:10px; border-radius:50%; border:1.5px solid var(--ink); display:inline-block; }
|
|
354
|
-
.br-section table.full tr.p0 .pdot { background:var(--p0); }
|
|
355
|
-
.br-section table.full tr.p1 .pdot { background:var(--p1); }
|
|
356
|
-
.br-section table.full tr.p2 .pdot { background:var(--p2); }
|
|
357
|
-
|
|
358
|
-
.tracks-svg { display:block; width:100%; height:140px; background:var(--parchment); border:1px solid var(--ink); }
|
|
359
|
-
.tracks-svg .axis { stroke:var(--ink); stroke-width:0.5; }
|
|
360
|
-
.tracks-svg .grid { stroke:var(--rule-soft); stroke-width:0.5; stroke-dasharray:2 2; }
|
|
361
|
-
.tracks-svg .credit-line { stroke:var(--national-finance); stroke-width:1.8; fill:none; }
|
|
362
|
-
.tracks-svg .resistance-line { stroke:var(--highlight); stroke-width:1.8; fill:none; }
|
|
363
|
-
.tracks-svg .capacity-line { stroke:var(--manufactures); stroke-width:1.8; fill:none; }
|
|
364
|
-
.tracks-svg .credit-dot { fill:var(--national-finance); }
|
|
365
|
-
.tracks-svg .resistance-dot { fill:var(--highlight); }
|
|
366
|
-
.tracks-svg .capacity-dot { fill:var(--manufactures); }
|
|
367
|
-
.tracks-svg .axis-label { font-family:var(--ui); font-size:8px; letter-spacing:.14em; text-transform:uppercase; fill:var(--ink); opacity:.7; }
|
|
368
|
-
.tracks-svg .threshold { stroke:var(--rule-soft); stroke-width:0.5; stroke-dasharray:1 2; }
|
|
369
|
-
.tracks-legend { display:flex; gap:14px; margin-top:6px; font-family:var(--ui); font-size:9px; letter-spacing:.1em; }
|
|
370
|
-
.tracks-legend .sw { display:inline-block; width:14px; height:2px; vertical-align:middle; margin-right:4px; }
|
|
371
|
-
.tracks-legend .sw.credit { background:var(--national-finance); }
|
|
372
|
-
.tracks-legend .sw.resistance { background:var(--highlight); }
|
|
373
|
-
.tracks-legend .sw.capacity { background:var(--manufactures); }
|
|
374
|
-
|
|
375
|
-
.batch-overlay { position:fixed; inset:0; z-index:80; background:rgba(26,22,18,0.85); display:flex; align-items:flex-start; justify-content:center; padding:30px 16px; overflow-y:auto; }
|
|
376
|
-
.batch-modal { background:var(--parchment); border:1.5px solid var(--ink); max-width:980px; width:100%; position:relative; padding:18px 22px; }
|
|
377
|
-
.batch-modal::before { content:""; position:absolute; inset:5px; border:0.5px solid var(--rule-soft); pointer-events:none; }
|
|
378
|
-
.batch-modal .bm-head { display:flex; justify-content:space-between; align-items:baseline; padding-bottom:6px; border-bottom:1px solid var(--ink); margin-bottom:12px; }
|
|
379
|
-
.batch-modal .bm-head .ttl { font-family:var(--display); font-size:22px; line-height:1; }
|
|
380
|
-
.batch-modal .bm-head .sub { font-family:var(--display); font-style:italic; font-size:11px; }
|
|
381
|
-
.batch-modal .bm-controls { display:grid; grid-template-columns:1fr 1fr; gap:14px; margin-bottom:14px; }
|
|
382
|
-
.batch-modal .ctrl-block { padding:10px 12px; background:var(--parchment-2); border:1px solid var(--ink); }
|
|
383
|
-
.batch-modal .ctrl-block .lbl { font-family:var(--ui); font-size:9px; letter-spacing:.22em; text-transform:uppercase; font-weight:700; margin-bottom:5px; display:block; }
|
|
384
|
-
.batch-modal .ctrl-row { display:flex; gap:8px; align-items:center; margin-bottom:6px; flex-wrap:wrap; }
|
|
385
|
-
.batch-modal .ctrl-row label { font-family:var(--ui); font-size:10px; cursor:pointer; padding:3px 9px; border:1px solid var(--ink); background:var(--parchment); }
|
|
386
|
-
.batch-modal .ctrl-row label.sel { background:var(--ink); color:var(--parchment); }
|
|
387
|
-
.batch-modal .ctrl-row label input { display:none; }
|
|
388
|
-
.batch-modal input[type="text"], .batch-modal input[type="number"] { font-family:var(--mono); font-size:11px; padding:5px 8px; border:1px solid var(--ink); background:var(--parchment); width:100%; box-sizing:border-box; }
|
|
389
|
-
.batch-modal select { font-family:var(--ui); font-size:10px; padding:4px 6px; border:1px solid var(--ink); background:var(--parchment); }
|
|
390
|
-
.batch-modal .slot-row { display:grid; grid-template-columns:60px 1fr; gap:6px; align-items:center; margin-bottom:4px; font-family:var(--ui); font-size:10px; }
|
|
391
|
-
.batch-modal .slot-row .slot-lbl { font-weight:700; letter-spacing:.12em; text-transform:uppercase; font-size:9px; }
|
|
392
|
-
.batch-modal .progress-row { padding:10px 14px; background:var(--ink); color:var(--parchment); font-family:var(--mono); font-size:11px; letter-spacing:.08em; margin:8px 0; min-height:34px; display:flex; align-items:center; justify-content:space-between; }
|
|
393
|
-
.batch-modal .progress-row .pct { font-family:var(--display); font-size:18px; }
|
|
394
|
-
.batch-modal .progress-bar { height:4px; background:rgba(0,0,0,0.2); position:relative; margin-top:8px; border:0.5px solid var(--rule-soft); }
|
|
395
|
-
.batch-modal .progress-bar > .fill { height:100%; background:var(--commercial-infrastructure); width:0; transition:width 80ms linear; }
|
|
396
|
-
.batch-modal .bm-actions { display:flex; gap:8px; }
|
|
397
|
-
.batch-modal .results { margin-top:14px; padding-top:10px; border-top:1px solid var(--ink); }
|
|
398
|
-
.batch-modal .results h4 { font-family:var(--ui); font-size:10px; font-weight:700; letter-spacing:.22em; text-transform:uppercase; margin:6px 0 5px; padding-bottom:3px; border-bottom:0.5px solid var(--rule-soft); }
|
|
399
|
-
.batch-modal .results .agg-grid { display:grid; grid-template-columns:repeat(2,1fr); gap:10px; }
|
|
400
|
-
.batch-modal .results .agg-card { padding:9px 11px; background:var(--parchment-2); border:1px solid var(--ink); }
|
|
401
|
-
.batch-modal .results table.full { width:100%; border-collapse:collapse; margin-top:4px; }
|
|
402
|
-
.batch-modal .results table.full th { background:var(--ink); color:var(--parchment); font-family:var(--ui); font-size:8.5px; letter-spacing:.14em; text-transform:uppercase; padding:3px 6px; text-align:left; }
|
|
403
|
-
.batch-modal .results table.full td { font-family:var(--ui); font-size:9.5px; padding:3px 6px; border-bottom:0.5px solid var(--rule-soft); }
|
|
404
|
-
.batch-modal .results table.full td.n { font-family:var(--mono); text-align:right; font-variant-numeric:tabular-nums; }
|
|
405
|
-
.batch-modal .winrate-bar { height:10px; background:var(--parchment); border:0.5px solid var(--rule-soft); position:relative; }
|
|
406
|
-
.batch-modal .winrate-bar .fill { height:100%; background:var(--commercial-infrastructure); }
|
|
407
|
-
.batch-modal .ex-actions { display:flex; gap:8px; margin-top:10px; flex-wrap:wrap; }
|
|
408
|
-
</style>
|
|
409
|
-
</head>
|
|
410
|
-
<body>
|
|
411
|
-
|
|
412
|
-
<div class="app">
|
|
413
|
-
<div id="resumePill" class="resume-pill hidden">
|
|
414
|
-
<span class="lbl" id="resumeLbl">Autosave found</span>
|
|
415
|
-
<button class="primary" id="resumeYes">Resume game</button>
|
|
416
|
-
<button id="resumeNo">Dismiss</button>
|
|
417
|
-
</div>
|
|
418
|
-
<input type="file" id="loadFile" accept="application/json,.json" class="hidden" />
|
|
419
|
-
<header class="topbar">
|
|
420
|
-
<div class="brand">
|
|
421
|
-
<div class="eyebrow">Sovereign · Solo / Digital · Phase 6.1 · v0.10 balance candidate</div>
|
|
422
|
-
<div class="title">Telemetry · Batch</div>
|
|
423
|
-
<div class="sub">v0.10 balance candidate — cash scoring softened to +1 IP per 400 TN.</div>
|
|
424
|
-
</div>
|
|
425
|
-
<div class="controls">
|
|
426
|
-
<span class="seed-pill" id="seedPill">seed: ----</span>
|
|
427
|
-
<span class="phase-pill" id="phasePill">phase: setup</span>
|
|
428
|
-
<span class="active-pill" id="activePill">Active: You</span>
|
|
429
|
-
<button id="btnSave" title="Download decision log + localStorage autosave">Save</button>
|
|
430
|
-
<button id="btnLoad" title="Import a saved JSON or load the autosave">Load</button>
|
|
431
|
-
<button id="btnReplay" class="hidden" title="Scrub through this finished game">Replay</button>
|
|
432
|
-
<button id="btnBatch" title="Run a scripted batch of 10 / 50 / 100 games">Batch</button>
|
|
433
|
-
<button id="btnNewSeed">New seed</button>
|
|
434
|
-
<button id="btnReset">Reset (same seed)</button>
|
|
435
|
-
</div>
|
|
436
|
-
</header>
|
|
437
|
-
|
|
438
|
-
<main class="grid" id="mainGrid">
|
|
439
|
-
<section class="board-pane">
|
|
440
|
-
<div class="panel" style="padding:6px 12px; border-bottom-width:0">
|
|
441
|
-
<div class="panel-head" style="margin-bottom:0; padding-bottom:0; border-bottom:0">
|
|
442
|
-
<span class="name">Board View</span><span class="surface-id">A</span>
|
|
443
|
-
</div>
|
|
444
|
-
</div>
|
|
445
|
-
<div class="board-view"><div class="board-grid" id="boardGrid"></div></div>
|
|
446
|
-
<div class="controls-bar" id="controlsBar"></div>
|
|
447
|
-
</section>
|
|
448
|
-
|
|
449
|
-
<aside class="panel-pane">
|
|
450
|
-
<div class="panel treasury" id="panelTreasury">
|
|
451
|
-
<div class="panel-head"><span class="name">Your Treasury</span><span class="surface-id">B</span></div>
|
|
452
|
-
<div class="cash-row">
|
|
453
|
-
<div>
|
|
454
|
-
<div class="lbl">Cash on hand</div>
|
|
455
|
-
<div class="cash" id="treasuryCash">$1,500</div>
|
|
456
|
-
</div>
|
|
457
|
-
<div style="text-align:right">
|
|
458
|
-
<span class="role-tag">Treasury Architect</span>
|
|
459
|
-
<div class="inf" id="treasuryInf">Influence est. 0</div>
|
|
460
|
-
</div>
|
|
461
|
-
</div>
|
|
462
|
-
<div id="treasuryHoldings"></div>
|
|
463
|
-
</div>
|
|
464
|
-
|
|
465
|
-
<div class="panel" id="panelOpponents">
|
|
466
|
-
<div class="panel-head"><span class="name">Scripted Opponents</span><span class="surface-id">B′</span></div>
|
|
467
|
-
<div class="opponents" id="opponentsBody"></div>
|
|
468
|
-
</div>
|
|
469
|
-
|
|
470
|
-
<div class="panel acts-panel" id="panelActs">
|
|
471
|
-
<div class="panel-head"><span class="name">Acts of Congress</span><span class="surface-id">E</span></div>
|
|
472
|
-
<div id="actsBody"></div>
|
|
473
|
-
</div>
|
|
474
|
-
|
|
475
|
-
<div class="panel narration-panel" id="panelNarration">
|
|
476
|
-
<div class="panel-head"><span class="name">Historical Narration</span><span class="surface-id">N</span></div>
|
|
477
|
-
<div class="controls-row">
|
|
478
|
-
<button class="pref-toggle on" id="narrPref" title="Toggle narration drawer">Drawer on</button>
|
|
479
|
-
<span class="count" id="narrCount">0 entries</span>
|
|
480
|
-
</div>
|
|
481
|
-
<div id="narrationBody"></div>
|
|
482
|
-
</div>
|
|
483
|
-
|
|
484
|
-
<div class="panel tracks-panel" id="panelTracks">
|
|
485
|
-
<div class="panel-head"><span class="name">Shared Tracks</span><span class="surface-id">F</span></div>
|
|
486
|
-
<div id="tracksBody"></div>
|
|
487
|
-
</div>
|
|
488
|
-
|
|
489
|
-
<div class="panel inspector hidden" id="panelInspector">
|
|
490
|
-
<div class="panel-head"><span class="name">Asset Inspector</span><span class="surface-id">C</span></div>
|
|
491
|
-
<div id="inspectorBody"></div>
|
|
492
|
-
</div>
|
|
493
|
-
|
|
494
|
-
<div class="panel card-drawer hidden" id="panelCardDrawer">
|
|
495
|
-
<div class="panel-head"><span class="name">Event Drawer</span><span class="surface-id">D</span></div>
|
|
496
|
-
<div id="cardDrawerBody"></div>
|
|
497
|
-
</div>
|
|
498
|
-
|
|
499
|
-
<div class="panel auction hidden" id="panelAuction">
|
|
500
|
-
<div class="panel-head"><span class="name">Auction</span><span class="surface-id">C′</span></div>
|
|
501
|
-
<div id="auctionBody"></div>
|
|
502
|
-
</div>
|
|
503
|
-
</aside>
|
|
504
|
-
|
|
505
|
-
<section class="ledger-pane">
|
|
506
|
-
<div class="ledger">
|
|
507
|
-
<div class="head"><span>Turn Log / Ledger</span><span class="surface-id">G</span><span id="ledgerCount">0 entries</span></div>
|
|
508
|
-
<div id="ledgerRows"></div>
|
|
509
|
-
</div>
|
|
510
|
-
</section>
|
|
511
|
-
</main>
|
|
512
|
-
|
|
513
|
-
<section class="endgame hidden" id="endgameView"></section>
|
|
514
|
-
</div>
|
|
515
|
-
|
|
516
|
-
<div class="batch-overlay hidden" id="batchOverlay">
|
|
517
|
-
<div class="batch-modal">
|
|
518
|
-
<div class="bm-head">
|
|
519
|
-
<div><div class="ttl">Batch Simulation</div><div class="sub">Scripted profiles only. Local. Deterministic per seed.</div></div>
|
|
520
|
-
<button id="batchClose">Close</button>
|
|
521
|
-
</div>
|
|
522
|
-
<div class="bm-controls">
|
|
523
|
-
<div class="ctrl-block">
|
|
524
|
-
<span class="lbl">Game count</span>
|
|
525
|
-
<div class="ctrl-row" id="batchCount">
|
|
526
|
-
<label class="sel"><input type="radio" name="batchN" value="10" checked />10</label>
|
|
527
|
-
<label><input type="radio" name="batchN" value="50" />50</label>
|
|
528
|
-
<label><input type="radio" name="batchN" value="100" />100</label>
|
|
529
|
-
</div>
|
|
530
|
-
<span class="lbl" style="margin-top:8px">Seed source</span>
|
|
531
|
-
<div class="ctrl-row">
|
|
532
|
-
<label class="sel"><input type="radio" name="seedMode" value="auto" checked />Auto-increment</label>
|
|
533
|
-
<label><input type="radio" name="seedMode" value="list" />Custom list</label>
|
|
534
|
-
</div>
|
|
535
|
-
<div id="seedAutoRow" class="ctrl-row">
|
|
536
|
-
<span style="font-family:var(--ui);font-size:9px;letter-spacing:.14em;text-transform:uppercase;opacity:.7">Start at</span>
|
|
537
|
-
<input type="number" id="seedStart" value="2026" style="width:120px" />
|
|
538
|
-
</div>
|
|
539
|
-
<div id="seedListRow" class="ctrl-row hidden">
|
|
540
|
-
<input type="text" id="seedList" placeholder="e.g. 1,2,3,99,100" />
|
|
541
|
-
</div>
|
|
542
|
-
</div>
|
|
543
|
-
<div class="ctrl-block">
|
|
544
|
-
<span class="lbl">Profile assignment</span>
|
|
545
|
-
<div class="slot-row"><span class="slot-lbl">Slot 0</span><select id="batchP0"></select></div>
|
|
546
|
-
<div class="slot-row"><span class="slot-lbl">Slot 1</span><select id="batchP1"></select></div>
|
|
547
|
-
<div class="slot-row"><span class="slot-lbl">Slot 2</span><select id="batchP2"></select></div>
|
|
548
|
-
<p style="font-family:var(--mono);font-size:9px;opacity:.7;margin:6px 0 0">Human slot is replaced by a scripted profile for the batch. Live game state untouched.</p>
|
|
549
|
-
</div>
|
|
550
|
-
</div>
|
|
551
|
-
<div class="bm-actions">
|
|
552
|
-
<button class="primary" id="batchRun">Run batch</button>
|
|
553
|
-
<button id="batchCancel" disabled>Cancel</button>
|
|
554
|
-
</div>
|
|
555
|
-
<div class="progress-row hidden" id="batchProgress">
|
|
556
|
-
<span id="batchProgressLabel">Idle.</span>
|
|
557
|
-
<span class="pct" id="batchProgressPct">0 %</span>
|
|
558
|
-
</div>
|
|
559
|
-
<div class="progress-bar hidden" id="batchProgressBar"><div class="fill" id="batchProgressFill"></div></div>
|
|
560
|
-
<div class="results hidden" id="batchResults"></div>
|
|
561
|
-
</div>
|
|
562
|
-
</div>
|
|
563
|
-
|
|
564
|
-
<div class="replay-overlay hidden" id="replayOverlay">
|
|
565
|
-
<div class="replay-frame">
|
|
566
|
-
<div class="replay-head">
|
|
567
|
-
<div class="ttl">Replay <span class="sub" id="replayTtlSub"></span></div>
|
|
568
|
-
<div class="actions">
|
|
569
|
-
<button id="replayStepBack" title="Step back one turn">← Step</button>
|
|
570
|
-
<button id="replayPlay" class="primary">▶ Play</button>
|
|
571
|
-
<button id="replayStepFwd" title="Step forward one turn">Step →</button>
|
|
572
|
-
<button id="replayClose">Close</button>
|
|
573
|
-
</div>
|
|
574
|
-
</div>
|
|
575
|
-
<div class="replay-body">
|
|
576
|
-
<div class="replay-board-col">
|
|
577
|
-
<div class="replay-board"><div class="board-grid" id="replayBoardGrid"></div></div>
|
|
578
|
-
<div class="replay-meta" id="replayMeta"></div>
|
|
579
|
-
</div>
|
|
580
|
-
<div class="replay-side">
|
|
581
|
-
<div class="ledger-mini" id="replayLedger"></div>
|
|
582
|
-
</div>
|
|
583
|
-
</div>
|
|
584
|
-
<div class="scrubber">
|
|
585
|
-
<div class="row-1">
|
|
586
|
-
<span class="scrub-meta" id="scrubMeta">Turn 0 / 0</span>
|
|
587
|
-
<div class="ticks" id="scrubberTicks"><div class="marker" id="scrubMarker" style="left:0"></div></div>
|
|
588
|
-
<span class="integrity ok" id="replayIntegrity">✓ integrity</span>
|
|
589
|
-
</div>
|
|
590
|
-
</div>
|
|
591
|
-
</div>
|
|
592
|
-
</div>
|
|
593
|
-
|
|
594
|
-
<script>
|
|
595
|
-
/* =====================================================================
|
|
596
|
-
PRNG — Mulberry32 (deterministic)
|
|
597
|
-
===================================================================== */
|
|
598
|
-
function makeRng(seed) {
|
|
599
|
-
let a = seed >>> 0;
|
|
600
|
-
return function() {
|
|
601
|
-
a = (a + 0x6D2B79F5) >>> 0;
|
|
602
|
-
let t = a;
|
|
603
|
-
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
604
|
-
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
605
|
-
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
606
|
-
};
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
/* =====================================================================
|
|
610
|
-
STATIC DATA — board, assets, acts, cards
|
|
611
|
-
===================================================================== */
|
|
612
|
-
const SPACES = [
|
|
613
|
-
{ num:0, name:'Treasury Opens', kind:'corner-start' },
|
|
614
|
-
{ num:1, name:'Continental Certificates', kind:'sys-revolutionary-debt' },
|
|
615
|
-
{ num:2, name:'Republic Debate', kind:'card-debate' },
|
|
616
|
-
{ num:3, name:'Soldier Pay Notes', kind:'sys-revolutionary-debt' },
|
|
617
|
-
{ num:4, name:'Federal Excise', kind:'tax', amount:200 },
|
|
618
|
-
{ num:5, name:'Northern Post Road', kind:'route' },
|
|
619
|
-
{ num:6, name:'Massachusetts Debt', kind:'sys-state-debt' },
|
|
620
|
-
{ num:7, name:'Market Shock', kind:'card-shock' },
|
|
621
|
-
{ num:8, name:'South Carolina Debt', kind:'sys-state-debt' },
|
|
622
|
-
{ num:9, name:'Virginia Debt', kind:'sys-state-debt' },
|
|
623
|
-
{ num:10, name:'Constitutional Crisis', kind:'corner-crisis' },
|
|
624
|
-
{ num:11, name:'Customs House', kind:'sys-revenue-system' },
|
|
625
|
-
{ num:12, name:'Bank of the United States', kind:'institution', instKind:'bank' },
|
|
626
|
-
{ num:13, name:'Import Duties', kind:'sys-revenue-system' },
|
|
627
|
-
{ num:14, name:'Whiskey Excise', kind:'sys-revenue-system' },
|
|
628
|
-
{ num:15, name:'Western Turnpike', kind:'route' },
|
|
629
|
-
{ num:16, name:'New York Harbor', kind:'sys-commercial-infrastructure' },
|
|
630
|
-
{ num:17, name:'Republic Debate', kind:'card-debate' },
|
|
631
|
-
{ num:18, name:'Philadelphia Exchange', kind:'sys-commercial-infrastructure' },
|
|
632
|
-
{ num:19, name:'Coastal Shipping', kind:'sys-commercial-infrastructure' },
|
|
633
|
-
{ num:20, name:'National Dividend', kind:'corner-safe' },
|
|
634
|
-
{ num:21, name:'Treasury Securities', kind:'sys-national-finance' },
|
|
635
|
-
{ num:22, name:'Market Shock', kind:'card-shock' },
|
|
636
|
-
{ num:23, name:'Bank Subscription', kind:'sys-national-finance' },
|
|
637
|
-
{ num:24, name:'Federal Deposits', kind:'sys-national-finance' },
|
|
638
|
-
{ num:25, name:'Potomac Canal', kind:'route' },
|
|
639
|
-
{ num:26, name:'Turnpike Charter', kind:'sys-internal-improvements' },
|
|
640
|
-
{ num:27, name:'Canal Company', kind:'sys-internal-improvements' },
|
|
641
|
-
{ num:28, name:'United States Mint', kind:'institution', instKind:'mint' },
|
|
642
|
-
{ num:29, name:'Postal Road Network', kind:'sys-internal-improvements' },
|
|
643
|
-
{ num:30, name:'Go to Crisis', kind:'corner-send' },
|
|
644
|
-
{ num:31, name:'Textile Works', kind:'sys-manufactures' },
|
|
645
|
-
{ num:32, name:'Iron Foundry', kind:'sys-manufactures' },
|
|
646
|
-
{ num:33, name:'Republic Debate', kind:'card-debate' },
|
|
647
|
-
{ num:34, name:'Glassworks', kind:'sys-manufactures' },
|
|
648
|
-
{ num:35, name:'Atlantic Port Chain', kind:'route' },
|
|
649
|
-
{ num:36, name:'Market Shock', kind:'card-shock' },
|
|
650
|
-
{ num:37, name:'Armory Works', kind:'sys-strategic-industry' },
|
|
651
|
-
{ num:38, name:'Speculation Scandal', kind:'tax', amount:100, resistBump:1 },
|
|
652
|
-
{ num:39, name:'Shipbuilding Yard', kind:'sys-strategic-industry' },
|
|
653
|
-
];
|
|
654
|
-
|
|
655
|
-
const ASSETS = {
|
|
656
|
-
1:{cost:60,base:4,up:30,mort:30,sys:'revolutionary-debt',subkind:'Federal Bond · Continental'},
|
|
657
|
-
3:{cost:60,base:6,up:30,mort:30,sys:'revolutionary-debt',subkind:'Federal Bond · Veterans'},
|
|
658
|
-
6:{cost:100,base:6,up:50,mort:50,sys:'state-debt',subkind:'State Bond · Bay State'},
|
|
659
|
-
8:{cost:100,base:6,up:50,mort:50,sys:'state-debt',subkind:'State Bond · Carolinas'},
|
|
660
|
-
9:{cost:120,base:8,up:50,mort:60,sys:'state-debt',subkind:'State Bond · Old Dominion'},
|
|
661
|
-
11:{cost:140,base:10,up:75,mort:70,sys:'revenue-system',subkind:'Federal Tariff · Customs'},
|
|
662
|
-
13:{cost:140,base:10,up:75,mort:70,sys:'revenue-system',subkind:'Federal Tariff · Imports'},
|
|
663
|
-
14:{cost:160,base:12,up:75,mort:80,sys:'revenue-system',subkind:'Federal Excise · Whiskey'},
|
|
664
|
-
16:{cost:180,base:14,up:100,mort:90,sys:'commercial-infrastructure',subkind:'Commercial · Port'},
|
|
665
|
-
18:{cost:180,base:14,up:100,mort:90,sys:'commercial-infrastructure',subkind:'Commercial · Exchange'},
|
|
666
|
-
19:{cost:200,base:16,up:100,mort:100,sys:'commercial-infrastructure',subkind:'Commercial · Cabotage'},
|
|
667
|
-
21:{cost:220,base:18,up:125,mort:110,sys:'national-finance',subkind:'Federal · Bonds'},
|
|
668
|
-
23:{cost:220,base:18,up:125,mort:110,sys:'national-finance',subkind:'Federal · Bank'},
|
|
669
|
-
24:{cost:240,base:20,up:125,mort:120,sys:'national-finance',subkind:'Federal · Deposits'},
|
|
670
|
-
26:{cost:260,base:22,up:150,mort:130,sys:'internal-improvements',subkind:'Infrastructure · Turnpike'},
|
|
671
|
-
27:{cost:260,base:22,up:150,mort:130,sys:'internal-improvements',subkind:'Infrastructure · Canal'},
|
|
672
|
-
29:{cost:280,base:24,up:150,mort:140,sys:'internal-improvements',subkind:'Infrastructure · Post Road'},
|
|
673
|
-
31:{cost:300,base:26,up:200,mort:150,sys:'manufactures',subkind:'Manufacturing · Textiles'},
|
|
674
|
-
32:{cost:300,base:26,up:200,mort:150,sys:'manufactures',subkind:'Manufacturing · Iron'},
|
|
675
|
-
34:{cost:320,base:28,up:200,mort:160,sys:'manufactures',subkind:'Manufacturing · Glass'},
|
|
676
|
-
37:{cost:350,base:35,up:200,mort:175,sys:'strategic-industry',subkind:'Strategic · Arms'},
|
|
677
|
-
39:{cost:400,base:50,up:200,mort:200,sys:'strategic-industry',subkind:'Strategic · Naval'},
|
|
678
|
-
5:{cost:200,kind:'route'}, 15:{cost:200,kind:'route'}, 25:{cost:200,kind:'route'}, 35:{cost:200,kind:'route'},
|
|
679
|
-
12:{cost:150,kind:'institution',instKind:'bank'}, 28:{cost:150,kind:'institution',instKind:'mint'},
|
|
680
|
-
};
|
|
681
|
-
|
|
682
|
-
const ROUTE_LADDER = [0, 25, 50, 100, 150];
|
|
683
|
-
const SYS_LABEL = {
|
|
684
|
-
'revolutionary-debt':'Revolutionary Debt','state-debt':'State Debt','revenue-system':'Revenue System',
|
|
685
|
-
'commercial-infrastructure':'Commercial Infrastructure','national-finance':'National Finance',
|
|
686
|
-
'internal-improvements':'Internal Improvements','manufactures':'Manufactures','strategic-industry':'Strategic Industry',
|
|
687
|
-
};
|
|
688
|
-
|
|
689
|
-
const ACTS = [
|
|
690
|
-
{ id:1, lap:1, roman:'I', name:'Funding Act',
|
|
691
|
-
summary:'All <strong>Revolutionary Debt</strong> payments +50 % permanently. Public Credit +2.',
|
|
692
|
-
apply(s){ s.flags.fundingPassed = true; return adjustTrack(s,'credit',2,'Funding Act passed'); } },
|
|
693
|
-
{ id:2, lap:2, roman:'II', name:'Assumption Act',
|
|
694
|
-
summary:'<strong>State Debt</strong> payments +100 % permanently. Public Credit +2. Public Resistance +1.',
|
|
695
|
-
apply(s){ s.flags.assumptionPassed = true; s = adjustTrack(s,'credit',2,'Assumption Act passed'); return adjustTrack(s,'resistance',1,'Assumption Act passed'); } },
|
|
696
|
-
{ id:3, lap:3, roman:'III', name:'Bank Charter',
|
|
697
|
-
summary:'<strong>Bank</strong> now 10 × dice (was 4×). Mint state enables 20× combo.',
|
|
698
|
-
apply(s){ s.flags.bankCharterPassed = true; return s; } },
|
|
699
|
-
{ id:4, lap:4, roman:'IV', name:'Tariff Schedule',
|
|
700
|
-
summary:'<strong>Revenue System</strong> payments +50 % permanently. Public Resistance +1.',
|
|
701
|
-
apply(s){ s.flags.tariffPassed = true; return adjustTrack(s,'resistance',1,'Tariff Schedule passed'); } },
|
|
702
|
-
{ id:5, lap:5, roman:'V', name:'Coinage Act',
|
|
703
|
-
summary:'<strong>Mint</strong> owner collects 50 TN from each other player. Public Credit +1. Industrial Capacity +1.',
|
|
704
|
-
apply(s){
|
|
705
|
-
const mintOwnerIdx = findOwnerIndex(s, 28);
|
|
706
|
-
if (mintOwnerIdx >= 0) {
|
|
707
|
-
for (let i = 0; i < s.players.length; i++) {
|
|
708
|
-
if (i !== mintOwnerIdx) {
|
|
709
|
-
s = adjustCash(s, -50, 'Coinage Act: ' + s.players[i].name + ' pays Mint owner', i);
|
|
710
|
-
s = adjustCash(s, 50, 'Coinage Act: receives from ' + s.players[i].name, mintOwnerIdx);
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
s = adjustTrack(s,'credit',1,'Coinage Act passed');
|
|
715
|
-
return adjustTrack(s,'capacity',1,'Coinage Act passed');
|
|
716
|
-
} },
|
|
717
|
-
{ id:6, lap:6, roman:'VI', name:'Report on Manufactures',
|
|
718
|
-
summary:'<strong>Manufactures</strong> & <strong>Strategic Industry</strong> upgrade costs halved one lap. Each Mfg/Strategic owner collects 50 TN per such property. Industrial Capacity +2.',
|
|
719
|
-
apply(s){
|
|
720
|
-
s.flags.manufacturesLap = s.lap;
|
|
721
|
-
for (let i = 0; i < s.players.length; i++) {
|
|
722
|
-
const n = countOwnedSys(s, i, 'manufactures') + countOwnedSys(s, i, 'strategic-industry');
|
|
723
|
-
if (n > 0) s = adjustCash(s, 50 * n, 'Report on Manufactures · capital event · ' + n + ' Mfg/Strategic', i);
|
|
724
|
-
}
|
|
725
|
-
return adjustTrack(s,'capacity',2,'Report on Manufactures passed');
|
|
726
|
-
} },
|
|
727
|
-
{ id:7, lap:7, roman:'VII', name:'Excise Enforcement',
|
|
728
|
-
summary:'<strong>Whiskey Excise</strong> pays 2× this round. Public Resistance +2.',
|
|
729
|
-
apply(s){ s.flags.exciseDoubleLap = s.lap; return adjustTrack(s,'resistance',2,'Excise Enforcement passed'); } },
|
|
730
|
-
];
|
|
731
|
-
|
|
732
|
-
/* Cards — most cards target the active player. Multi-player effects iterate. */
|
|
733
|
-
const MARKET_SHOCK_CARDS = [
|
|
734
|
-
{ id:1, name:'Panic in the Market', tags:['Conditional'],
|
|
735
|
-
effectText:'If <strong>Credit < 5</strong>, pay 50 TN. If <strong>Credit ≥ 8</strong>, collect 50 TN.',
|
|
736
|
-
resolve(s, idx){ const c=s.tracks.credit.value; if(c<5) return adjustCash(s,-50,'Panic in the Market: Credit < 5',idx); if(c>=8) return adjustCash(s,50,'Panic in the Market: Credit ≥ 8',idx); return logRow(s,{actor:'Card',event:'NO EFFECT',detail:'Panic in the Market: Credit 5–7, no effect',cls:'card'}); } },
|
|
737
|
-
{ id:2, name:'Foreign Demand Rises', tags:['System'],
|
|
738
|
-
effectText:'All <strong>Manufactures</strong> owners collect 40 TN.',
|
|
739
|
-
resolve(s){ for(let i=0;i<s.players.length;i++){ if(countOwnedSys(s,i,'manufactures')>0) s = adjustCash(s,40,'Foreign Demand Rises: owns Manufactures',i); } return s; } },
|
|
740
|
-
{ id:3, name:'Speculation Fever', tags:['Track'], chips:{resist:1},
|
|
741
|
-
effectText:'Choose an unowned Rev/State Debt property; auction it. Resistance +1.',
|
|
742
|
-
resolve(s, idx){ s = adjustTrack(s,'resistance',1,'Speculation Fever'); /* find an unowned Rev/State Debt property; if any, open auction */ const candidates = [1,3,6,8,9].filter(n => findOwnerIndex(s,n) < 0); if(candidates.length === 0) return logRow(s,{actor:'Card',event:'NO EFFECT',detail:'Speculation Fever: no unowned Rev/State Debt properties',cls:'card'}); /* pick deterministically: lowest spaceNum */ const target = candidates[0]; s.pendingAuction = startAuction(s, target, idx, 'Speculation Fever card'); s.phase = 'auction'; return logRow(s,{actor:'Card',event:'AUCTION',detail:'Speculation Fever: auctioning ' + SPACES[target].name,cls:'card'}); } },
|
|
743
|
-
{ id:4, name:'Shipping Disruption', tags:['Suspension'],
|
|
744
|
-
effectText:'Commerce / Route payments suspended until your next turn.',
|
|
745
|
-
resolve(s, idx){ s.flags.shippingDisruptedUntilTurn = s.turnIndex + 3; return logRow(s,{actor:'Card',event:'EFFECT',detail:'Shipping Disruption: Commerce/Route payments suspended',cls:'card'}); } },
|
|
746
|
-
{ id:5, name:'Gold and Silver Inflow', tags:['Owner','Track'], chips:{credit:1},
|
|
747
|
-
effectText:'Mint owner collects 50 TN from each player. Public Credit +1.',
|
|
748
|
-
resolve(s, idx){ s = adjustTrack(s,'credit',1,'Gold and Silver Inflow'); const mintIdx = findOwnerIndex(s, 28); if(mintIdx >= 0){ for(let i=0;i<s.players.length;i++){ if(i!==mintIdx){ s = adjustCash(s,-50,'Gold/Silver: pays Mint',i); s = adjustCash(s,50,'Gold/Silver: receives',mintIdx);} } } return s; } },
|
|
749
|
-
{ id:6, name:'Foreign Loan Secured', tags:['Payment','Track'], chips:{credit:1},
|
|
750
|
-
effectText:'You receive 100 TN. Public Credit +1.',
|
|
751
|
-
resolve(s, idx){ s = adjustCash(s,100,'Foreign Loan Secured',idx); return adjustTrack(s,'credit',1,'Foreign Loan Secured'); } },
|
|
752
|
-
{ id:7, name:'British Trade Embargo', tags:['System','Track'], chips:{indust:1},
|
|
753
|
-
effectText:'Revenue payments halved. Industrial Capacity +1.',
|
|
754
|
-
resolve(s){ s.flags.revenueHalvedLap = s.lap; return adjustTrack(s,'capacity',1,'British Trade Embargo'); } },
|
|
755
|
-
{ id:8, name:'Yellow Fever Outbreak', tags:['Movement'],
|
|
756
|
-
effectText:'Move to the nearest Commerce property.',
|
|
757
|
-
resolve(s, idx){ const targets=[16,18,19]; const cur=s.players[idx].position; let best=99,n=targets[0]; for(const t of targets){ let d=(t-cur+40)%40; if(d<best){best=d;n=t;} } s = logRow(s,{actor:'Card',event:'MOVE',detail:s.players[idx].name+': Yellow Fever moves '+cur+' → '+n,cls:'move'}); s.players[idx].position = n; s.pendingResolveLanding = true; return s; } },
|
|
758
|
-
{ id:9, name:'Successful Bond Auction', tags:['Multi-system'],
|
|
759
|
-
effectText:'Rev Debt and Nat\'l Finance owners collect 30 TN per property.',
|
|
760
|
-
resolve(s){ for(let i=0;i<s.players.length;i++){ const n = countOwnedSys(s,i,'revolutionary-debt') + countOwnedSys(s,i,'national-finance'); if(n>0) s = adjustCash(s, 30*n, 'Bond Auction: ' + n + ' × 30 TN', i); } return s; } },
|
|
761
|
-
{ id:10, name:'Bank Run', tags:['Conditional','Track'], chips:{indust:-1},
|
|
762
|
-
effectText:'If Charter passed, Nat\'l Finance owners lose 1 upgrade. Capacity -1.',
|
|
763
|
-
resolve(s){ s = adjustTrack(s,'capacity',-1,'Bank Run'); if(!s.flags.bankCharterPassed) return logRow(s,{actor:'Card',event:'NO EFFECT',detail:'Bank Run: Charter not passed, skipped',cls:'card'}); for(let i=0;i<s.players.length;i++){ const nfs = s.players[i].ownedAssets.filter(a => ASSETS[a.spaceNum]?.sys === 'national-finance' && a.tier > 0); if(nfs.length > 0){ const target = nfs.reduce((m,a)=> a.tier > m.tier ? a : m); target.tier -= 1; s = logRow(s,{actor:'Card',event:'EFFECT',detail:'Bank Run: '+s.players[i].name+' loses 1 upgrade on '+SPACES[target.spaceNum].name,cls:'card'}); } } return s; } },
|
|
764
|
-
{ id:11, name:'Cotton Gin Patented', tags:['System','Track'], chips:{indust:1},
|
|
765
|
-
effectText:'Textile Works payment doubled. Capacity +1.',
|
|
766
|
-
resolve(s){ s.flags.textileDoubleLap = s.lap; return adjustTrack(s,'capacity',1,'Cotton Gin Patented'); } },
|
|
767
|
-
{ id:12, name:'Treaty Renegotiation', tags:['Multi-system','Track'], chips:{credit:2},
|
|
768
|
-
effectText:'Rev Debt and State Debt payments doubled. Public Credit +2.',
|
|
769
|
-
resolve(s){ s.flags.debtDoubleLap = s.lap; return adjustTrack(s,'credit',2,'Treaty Renegotiation'); } },
|
|
770
|
-
];
|
|
771
|
-
|
|
772
|
-
const REPUBLIC_DEBATE_CARDS = [
|
|
773
|
-
{ id:1, name:'Strict Construction Objection', tags:['Choice'],
|
|
774
|
-
effectText:'Move to Crisis OR pay 50 TN.',
|
|
775
|
-
choices:[
|
|
776
|
-
{ label:'Move to Crisis', resolve(s, idx){ s.players[idx].position = 10; s.players[idx].inCrisis = true; return logRow(s,{actor:'Card',event:'CRISIS',detail:s.players[idx].name+': Strict Construction → Crisis',cls:'event'}); } },
|
|
777
|
-
{ label:'Pay 50 TN', resolve(s, idx){ return adjustCash(s,-50,'Strict Construction: lobby',idx); } },
|
|
778
|
-
] },
|
|
779
|
-
{ id:2, name:'Jeffersonian Opposition', tags:['Owner','Track'], chips:{resist:1},
|
|
780
|
-
effectText:'Bank owner pays 100 TN. Resistance +1.',
|
|
781
|
-
resolve(s){ s = adjustTrack(s,'resistance',1,'Jeffersonian Opposition'); const bIdx = findOwnerIndex(s, 12); if(bIdx >= 0) return adjustCash(s,-100,'Jeffersonian Opposition: Bank owner pays',bIdx); return logRow(s,{actor:'Card',event:'NO EFFECT',detail:'Jeffersonian Opposition: Bank unowned',cls:'card'}); } },
|
|
782
|
-
{ id:3, name:'Credit Restored', tags:['Multi-system','Track'], chips:{credit:1},
|
|
783
|
-
effectText:'Bond owners (Rev/State/Nat\'l Finance) collect 50 TN per property.',
|
|
784
|
-
resolve(s){ s = adjustTrack(s,'credit',1,'Credit Restored'); for(let i=0;i<s.players.length;i++){ const n = countOwnedSys(s,i,'revolutionary-debt') + countOwnedSys(s,i,'state-debt') + countOwnedSys(s,i,'national-finance'); if(n>0) s = adjustCash(s, 50*n, 'Credit Restored: ' + n + ' bonds × 50 TN', i); } return s; } },
|
|
785
|
-
{ id:4, name:'Whiskey Rebellion', tags:['Conditional','Crisis'],
|
|
786
|
-
effectText:'If Resistance ≥ 8, Whiskey Excise owner loses 1 upgrade and → Crisis.',
|
|
787
|
-
resolve(s){ if(s.tracks.resistance.value < 8) return logRow(s,{actor:'Card',event:'NO EFFECT',detail:'Whiskey Rebellion: Resistance < 8',cls:'card'}); const wIdx = findOwnerIndex(s, 14); if(wIdx < 0) return logRow(s,{actor:'Card',event:'NO EFFECT',detail:'Whiskey Rebellion: Whiskey unowned',cls:'card'}); const w = s.players[wIdx].ownedAssets.find(a => a.spaceNum === 14); if(w.tier > 0) w.tier -= 1; s.players[wIdx].position = 10; s.players[wIdx].inCrisis = true; return logRow(s,{actor:'Card',event:'CRISIS',detail:'Whiskey Rebellion: '+s.players[wIdx].name+' loses upgrade + → Crisis',cls:'event'}); } },
|
|
788
|
-
{ id:5, name:'Cabinet Bargain', tags:['Trade','Track'], chips:{resist:-1},
|
|
789
|
-
effectText:'Trade — active player gains 1 Influence. Resistance -1.',
|
|
790
|
-
resolve(s, idx){ s = adjustTrack(s,'resistance',-1,'Cabinet Bargain'); s.players[idx].influencePoints += 1; return logRow(s,{actor:'Card',event:'EFFECT',detail:'Cabinet Bargain: '+s.players[idx].name+' gains 1 Influence (trade unimplemented)',cls:'card'}); } },
|
|
791
|
-
{ id:6, name:'Newspaper Smear', tags:['Influence','Payment'],
|
|
792
|
-
effectText:'Pay 25 TN per Influence Point held.',
|
|
793
|
-
resolve(s, idx){ const cost = 25 * s.players[idx].influencePoints; if(cost === 0) return logRow(s,{actor:'Card',event:'NO EFFECT',detail:'Newspaper Smear: 0 Influence held',cls:'card'}); return adjustCash(s,-cost,'Newspaper Smear: 25 × '+s.players[idx].influencePoints+' Inf',idx); } },
|
|
794
|
-
{ id:7, name:'Federalist Victory', tags:['Conditional','Track'], chips:{credit:1},
|
|
795
|
-
effectText:'If you own Nat\'l Finance, collect 100 TN. Credit +1.',
|
|
796
|
-
resolve(s, idx){ s = adjustTrack(s,'credit',1,'Federalist Victory'); return countOwnedSys(s,idx,'national-finance') > 0 ? adjustCash(s,100,'Federalist Victory',idx) : logRow(s,{actor:'Card',event:'NO EFFECT',detail:'Federalist Victory: no NF held',cls:'card'}); } },
|
|
797
|
-
{ id:8, name:'Anti-Federalist Pamphlet', tags:['System','Track'], chips:{resist:1},
|
|
798
|
-
effectText:'All Revenue owners pay 30 TN per property. Resistance +1.',
|
|
799
|
-
resolve(s){ s = adjustTrack(s,'resistance',1,'Anti-Federalist Pamphlet'); for(let i=0;i<s.players.length;i++){ const n = countOwnedSys(s,i,'revenue-system'); if(n>0) s = adjustCash(s, -30*n, 'Anti-Fed Pamphlet: '+n+' × 30 TN', i); } return s; } },
|
|
800
|
-
{ id:9, name:'Funding Plan Advances', tags:['Vote'],
|
|
801
|
-
effectText:'Force next Act vote immediately. (Phase 4: noted only.)',
|
|
802
|
-
resolve(s){ return logRow(s,{actor:'Card',event:'NOTE',detail:'Funding Plan Advances: force-vote mechanic deferred',cls:'card'}); } },
|
|
803
|
-
{ id:10, name:'State Convention Calls', tags:['Option'],
|
|
804
|
-
effectText:'State Debt owners may sell one back at full price.',
|
|
805
|
-
resolve(s){ s.flags.stateConventionAvailable = true; return logRow(s,{actor:'Card',event:'EFFECT',detail:'State Convention Calls: option opened (UI deferred)',cls:'card'}); } },
|
|
806
|
-
{ id:11, name:'National Lottery', tags:['Payout'],
|
|
807
|
-
effectText:'All players collect 10 TN per property owned.',
|
|
808
|
-
resolve(s){ for(let i=0;i<s.players.length;i++){ const n = s.players[i].ownedAssets.length; if(n>0) s = adjustCash(s, 10*n, 'National Lottery: '+n+' × 10 TN', i); } return s; } },
|
|
809
|
-
{ id:12, name:'You Are Hamilton', tags:['Keepable','Influence'], chips:{credit:1},
|
|
810
|
-
effectText:'Collect 200 TN. +1 Influence. Keep card.',
|
|
811
|
-
resolve(s, idx){ s = adjustCash(s,200,'You Are Hamilton',idx); s = adjustTrack(s,'credit',1,'You Are Hamilton'); s.players[idx].influencePoints += 1; s.players[idx].keptHamilton = true; return logRow(s,{actor:'Card',event:'KEEP',detail:s.players[idx].name+' keeps You Are Hamilton',cls:'card'}); } },
|
|
812
|
-
];
|
|
813
|
-
|
|
814
|
-
/* =====================================================================
|
|
815
|
-
NARRATION LIBRARY — pure data, keyed by trigger
|
|
816
|
-
Default snippet: 40–60 words. Expand: 150–200 words.
|
|
817
|
-
No invented quotes. Specific dates and names where they ground the moment.
|
|
818
|
-
===================================================================== */
|
|
819
|
-
const NARRATION = {
|
|
820
|
-
'first-purchase · revolutionary-debt': {
|
|
821
|
-
h: 'Continental Certificates',
|
|
822
|
-
d: 'In 1789 Continental Certificates and soldier-pay notes traded at twenty cents on the dollar. Hamilton’s First Report on Public Credit, sent to Congress in January 1790, proposed redeeming the lot at par. Speculators rode west buying paper from veterans who had never been told. You hold what they were hoping to sell.',
|
|
823
|
-
x: 'The Continental Congress issued more than $200 million in paper currency and certificates between 1775 and 1781 to pay soldiers, contractors, and suppliers. Most of it depreciated almost immediately; the phrase “not worth a Continental” entered the language. By 1789 the open-market price ranged from fifteen to twenty-five cents on the dollar, varying by issue. Hamilton’s First Report on Public Credit treated this depreciation as a wound to be closed, not a debt to be discounted. Redeeming at par would restore federal credit, anchor the new bond market, and make future borrowing cheap. The political price was the windfall it handed to speculators who had bought paper from original holders — a transfer Madison tried to block with a “discrimination” clause that Congress rejected. Your purchase places you on the speculator’s side of that ledger. If the Funding Act passes, your bonds will pay fifty percent more for the rest of the game.',
|
|
824
|
-
},
|
|
825
|
-
'first-purchase · state-debt': {
|
|
826
|
-
h: 'State Debt',
|
|
827
|
-
d: 'Massachusetts, South Carolina, and Virginia each carried Revolutionary war debts ranging from four to nine million dollars. Some states had paid them down; others had not. Hamilton proposed federal assumption of the whole lot. Madison and Jefferson resisted until the dinner-table bargain of June 1790 traded assumption for the Potomac capital.',
|
|
828
|
-
x: 'The fiscal position of the thirteen states diverged sharply by 1789. Massachusetts and South Carolina, which had borne the heaviest war costs, still owed creditors several million each; Virginia had largely paid its share through wartime taxation and confiscation of loyalist estates. Hamilton’s second proposal under the First Report on Public Credit was federal assumption of remaining state debts — in total roughly $21.5 million. The political logic was to bind state-bond creditors to the federal government rather than to their states. Madison and Jefferson opposed it as a subsidy from prudent states to profligate ones. The bargain reached at Jefferson’s table in late June 1790, sealed with Robert Morris’s cooperation, traded passage of assumption for a permanent capital on the Potomac. Your purchase is a bet that Congress passes the Assumption Act. If it does, your bonds will double in value for the rest of the game and you will hold federal paper rather than state paper.',
|
|
829
|
-
},
|
|
830
|
-
'first-purchase · revenue-system': {
|
|
831
|
-
h: 'Revenue System',
|
|
832
|
-
d: 'A federal tariff schedule was the new government’s first major piece of legislation, signed in July 1789. The whiskey excise followed in 1791. Together they were the Treasury’s only reliable income before the bond market opened. Customs houses at New York, Philadelphia, and Boston handled most of it.',
|
|
833
|
-
x: 'The Constitution gave Congress an exclusive power to tax imports, and the Tariff Act of July 1789 used it immediately. Rates ran roughly five to fifteen percent on most goods, higher on luxury items, with discriminatory provisions favoring American shipping. Customs duties produced about ninety percent of federal revenue throughout the 1790s. The Whiskey Excise of 1791, the first internal federal tax, was designed to ride on top of the tariff and to bring frontier farmers — who paid no import duties — into the federal tax base. It produced perhaps a tenth of revenue and almost all of the political resistance. Your purchase places you inside that revenue stream. If a Tariff Schedule passes, customs receipts rise by half permanently. If Excise Enforcement passes, the Whiskey collector — you, perhaps — collects double for one round, at the cost of two points of Public Resistance and the political fallout that came with it on the frontier.',
|
|
834
|
-
},
|
|
835
|
-
'first-purchase · commercial-infrastructure': {
|
|
836
|
-
h: 'Commercial Infrastructure',
|
|
837
|
-
d: 'New York Harbor, the Philadelphia Exchange, and the coastal shipping fleet handled the federal customs revenue and most domestic commerce. The same merchants who underwrote bond auctions also owned the warehouses and the wharves. Commerce and credit were one circuit, not two.',
|
|
838
|
-
x: 'In 1790 the United States had perhaps half a million tons of shipping, most of it New England-built and merchant-owned. The Tariff Act of 1789 included tonnage duties that favored American hulls and excluded British shipping from the coasting trade outright. Philadelphia and New York competed for primacy: Philadelphia held the seat of government, the Bank of North America, and the Bank of the United States after 1791; New York had deeper water and a faster turnover. The merchant houses that financed cargo voyages also subscribed to bond auctions and bank stock issues; the same names appear on the books of all three markets. Your Commercial Infrastructure holdings convert the rest of the board’s activity into rent. If the Tariff Schedule passes, the customs revenue flowing through these wharves rises measurably and the duties paid to your owners increase with it. A Shipping Disruption card from the Market Shock deck can shut commercial-infrastructure payments entirely until your next turn.',
|
|
839
|
-
},
|
|
840
|
-
'first-purchase · national-finance': {
|
|
841
|
-
h: 'National Finance',
|
|
842
|
-
d: 'The Bank of the United States, chartered in February 1791, was the engine of the new bond market. It held federal deposits, issued banknotes redeemable in specie, and lent to merchants on commercial paper. Federal subscription was twenty percent; the rest sold to private investors in twenty-four hours.',
|
|
843
|
-
x: 'Hamilton’s Second Report on Public Credit, delivered to Congress in December 1790, proposed the Bank of the United States with a $10 million capitalization — twenty percent subscribed by the federal Treasury, the remaining eighty percent open to private subscription payable one-quarter in specie and three-quarters in federal bonds. The bond-payable clause was the key innovation: it gave a use to the new federal bonds that fixed their price floor and created the first national securities market. The Bank’s twenty-year charter was contested on constitutional grounds — Jefferson’s strict-construction opinion against it remains one of the founding documents of that doctrine — but Washington signed it after Hamilton’s rebuttal. Your National Finance holdings sit at the top of the federal credit pyramid. If Public Credit reaches eight or above by the end of the game, the National Finance owners split five additional Influence points among themselves at scoring, divided by the number of co-holders rounded down.',
|
|
844
|
-
},
|
|
845
|
-
'first-purchase · internal-improvements': {
|
|
846
|
-
h: 'Internal Improvements',
|
|
847
|
-
d: 'Turnpikes, canals, and post roads were chartered by states and built by private subscription. The Lancaster Turnpike of 1794 was the first; the Potomac Canal Company, chartered in 1785 with Washington as its president, was the most famous failure. The federal post road network grew under Pickering after 1792.',
|
|
848
|
-
x: 'The 1790s saw the first wave of American transportation construction — turnpikes, canals, and post roads — most of them chartered by individual states and built by joint-stock companies on private subscription rather than federal appropriation. The Philadelphia and Lancaster Turnpike, opened in 1794, charged tolls and paid dividends; it was widely imitated through Pennsylvania, Maryland, and New York. The Potomac Canal, chartered in 1785 with Washington as its first president, attempted to make the Potomac River navigable above the Great Falls and failed expensively after a decade of construction. The federal post road network, formalized under Postmaster General Timothy Pickering after 1792, knit the new states into a single news and credit network: market prices, vote tallies, and bills of exchange traveled at the speed of the rider and the speed of the rider improved every year. Your Internal Improvements holdings express that infrastructure as a revenue stream. They pair naturally with Commercial Infrastructure: roads feed wharves; wharves clear cargoes; cargoes pay tolls.',
|
|
849
|
-
},
|
|
850
|
-
'first-purchase · manufactures': {
|
|
851
|
-
h: 'Manufactures',
|
|
852
|
-
d: 'Hamilton’s Report on Manufactures, delivered to Congress in December 1791, argued that protective tariffs and federal subsidies should foster American textiles, iron, and glass. The Society for Establishing Useful Manufactures opened a mill in Paterson, New Jersey, in 1794. The Report itself never became a single statute.',
|
|
853
|
-
x: 'Hamilton’s Report on the Subject of Manufactures, his fourth major Treasury report, broke with the Smith-influenced free-trade orthodoxy that other framers had assumed. It argued that protective tariffs, bounties, and federal investment were necessary to overcome British industrial advantages — not as permanent policy, but as a temporary scaffold during the republic’s industrial infancy. Congress did not enact the Report as a single bill; instead, individual tariff schedules through the 1790s and 1800s incorporated pieces of its recommendations. The Society for Establishing Useful Manufactures, chartered by New Jersey in 1791 with Hamilton’s direct involvement, attempted to build an industrial town at the Passaic Falls; the mill opened in 1794 and immediately ran into management problems, labor shortages, and capital flight to the bond market. Your Manufactures holdings are a bet on this scaffold. If the Report on Manufactures passes, your upgrades cost half for one lap and Industrial Capacity climbs by two on the same turn.',
|
|
854
|
-
},
|
|
855
|
-
'first-purchase · strategic-industry': {
|
|
856
|
-
h: 'Strategic Industry',
|
|
857
|
-
d: 'The Springfield and Harpers Ferry armories, authorized by Congress in 1794 and 1798, were the first federal manufactories. Naval shipbuilding at Boston, New York, and Philadelphia followed under the Naval Act of 1794, which authorized the six original frigates including the Constitution.',
|
|
858
|
-
x: 'Strategic industry — arms manufacture and naval shipbuilding — entered federal hands faster than civilian industry did. The Springfield Armory, established 1794, and Harpers Ferry, authorized 1798, were the two federal small-arms factories; both were direct outgrowths of Washington’s argument that the republic could not depend on European arms in wartime. The Naval Act of 1794 authorized the six original frigates — including the United States, the Constellation, and the Constitution — partly in response to Barbary corsair attacks on American shipping and partly as a long-term assertion of federal naval capacity. These industries operated on direct federal appropriation rather than tariff protection and therefore had the most stable revenue of any sector through the 1790s. Your Strategic Industry holdings are the most expensive on the board and pay the highest base rent of any system. They are also where the Industrial Capacity bonus most clearly compounds with full-set and tier multipliers at endgame.',
|
|
859
|
-
},
|
|
860
|
-
|
|
861
|
-
'act-passed · funding': {
|
|
862
|
-
h: 'Funding Act',
|
|
863
|
-
d: 'August 1790. Congress redeems federal Revolutionary War debt at par. The bond market opens. Original holders — the soldiers and contractors of the 1780s — are not compensated; the speculators who bought their paper at twenty cents on the dollar collect at one hundred. Public Credit rises.',
|
|
864
|
-
x: 'The Funding Act of August 1790 implemented the first part of Hamilton’s First Report on Public Credit. Federal Revolutionary War debt — roughly $54 million in face value — was redeemed at par for new federal bonds at three different interest rates. Original holders were not compensated for the discount they had taken when they sold their paper years before; Madison’s amendment to compensate them was defeated. The new bonds traded immediately and at par on the Philadelphia and New York markets, and within months they were trading above par, indicating that the bond market judged the federal government a better credit risk than the states had been. The Funding Act was the moment the federal government acquired a credit reputation. In your game, every Revolutionary Debt property now pays fifty percent more rent permanently for every player who holds one, and Public Credit climbs by two points on passage.',
|
|
865
|
-
},
|
|
866
|
-
'act-passed · assumption': {
|
|
867
|
-
h: 'Assumption Act',
|
|
868
|
-
d: 'August 1790, in the same bill as Funding. The federal government takes over the remaining state war debts — roughly $21.5 million. Massachusetts and South Carolina creditors are now federal creditors. The deal that passed it moved the permanent capital to the Potomac, satisfying Virginia.',
|
|
869
|
-
x: 'Assumption was the second half of the First Report on Public Credit and the more politically difficult half. States that had paid down their war debts — chiefly Virginia and Maryland — saw assumption as a federal subsidy to states that had not. Madison fought it through three votes. The deal that finally passed it, struck at Jefferson’s rented house in New York in late June 1790, traded passage of assumption for the relocation of the permanent capital to a site on the Potomac — the future District of Columbia. Robert Morris helped move two Pennsylvania votes; Madison let two Virginia votes flip. The combined Funding and Assumption Act passed Congress in early August. Public Resistance ticks up: the assumption was widely read in the south as a transfer from agrarian states to commercial ones. Every State Debt property now pays double rent permanently; Credit climbs by two; Resistance climbs by one.',
|
|
870
|
-
},
|
|
871
|
-
'act-passed · bank-charter': {
|
|
872
|
-
h: 'Bank Charter',
|
|
873
|
-
d: 'February 1791. Washington signs the bill chartering the Bank of the United States after weighing Jefferson’s strict-construction opinion against it and Hamilton’s rebuttal. The Bank opens in Philadelphia in December 1791 with $10 million capital, twenty percent federally subscribed. The first stock issue sells out in two hours.',
|
|
874
|
-
x: 'The Bank Bill passed Congress in February 1791 and arrived on Washington’s desk. Jefferson, Madison, and Edmund Randolph submitted written opinions against it on strict-construction grounds: nothing in the Constitution explicitly authorized a federal corporation, and the Tenth Amendment reserved unenumerated powers to the states. Hamilton’s rebuttal, written in a week and running to thousands of words, advanced the doctrine of implied powers: any power necessary and proper to executing the enumerated powers was constitutionally available. Washington signed. The Bank opened in Philadelphia in December 1791; the initial public stock offering, payable one-quarter in specie and three-quarters in federal bonds, sold out within two hours of the books opening and drove a brief speculative panic when secondary-market prices spiked. In your game, the Bank now pays ten times the dice roll on landing — up from four times — and combines with the Mint for a twenty-times-dice combo whenever a player owns both institutions.',
|
|
875
|
-
},
|
|
876
|
-
'act-passed · tariff': {
|
|
877
|
-
h: 'Tariff Schedule',
|
|
878
|
-
d: 'The Tariff Act of 1789 was the first major piece of legislation under the new Constitution, signed in July. Subsequent revisions through the 1790s raised rates and added discriminatory tonnage duties favoring American shipping. Customs duties at the major ports supplied roughly ninety percent of federal revenue throughout the decade.',
|
|
879
|
-
x: 'Congress passed the first federal tariff in July 1789, six weeks into the First Congress, and the new revenue began flowing through the customs houses at New York, Philadelphia, Boston, Baltimore, and Charleston within months. Rates ran from five to fifteen percent ad valorem on most imported goods and higher on luxury items including spirits, tea, and cordage; tonnage duties charged foreign shipping at six cents per ton against the American six cents charged only once a year. The Tariff Acts of 1790, 1792, and 1794 progressively raised rates and added new schedules; by the late 1790s, customs duties produced roughly ninety percent of federal revenue. Politically the tariff was the least controversial of Hamilton’s revenue measures — northern manufacturers welcomed protection, and merchants passed the cost forward to consumers — but it shifted consumption patterns and produced predictable resistance from import-dependent agrarian regions. In your game, every Revenue System property now pays fifty percent more rent permanently, and Public Resistance climbs by one.',
|
|
880
|
-
},
|
|
881
|
-
'act-passed · coinage': {
|
|
882
|
-
h: 'Coinage Act',
|
|
883
|
-
d: 'April 1792. Congress establishes the United States Mint in Philadelphia and authorizes a bimetallic dollar at a silver-to-gold ratio of fifteen to one. The Mint begins striking silver dollars in 1794 and gold eagles in 1795. Domestic specie circulation slowly displaces foreign coin.',
|
|
884
|
-
x: 'The Coinage Act of April 1792 — also called the Mint Act — established the United States Mint as the second federal corporation after the Bank, with David Rittenhouse as its first director. The Act defined the dollar at 371.25 grains of pure silver and set a silver-to-gold ratio of fifteen to one, both numbers chosen to match the prevailing market ratio and the Spanish-milled dollar then dominant in domestic circulation. Production began with copper cents in 1793, silver coinage in 1794, and gold eagles in 1795. The Mint never produced enough specie to replace foreign coin in circulation during the 1790s — Spanish, Portuguese, and French coins remained legal tender until 1857 — but it gave the federal government a domestic minting capacity and a symbolic monetary anchor. In your game, the Mint owner collects fifty TN from each other player on passage; Credit climbs by one; Industrial Capacity climbs by one.',
|
|
885
|
-
},
|
|
886
|
-
'act-passed · manufactures-report': {
|
|
887
|
-
h: 'Report on Manufactures',
|
|
888
|
-
d: 'December 1791. Hamilton submits his Report on the Subject of Manufactures to Congress, arguing for federal bounties, protective tariffs, and direct investment in nascent industry. Under the v0.3 ruleset the Act passes as a direct capital event: every Manufactures and Strategic Industry owner receives a fifty taels subsidy per such property, both upgrade tracks halve cost for the full lap, and Industrial Capacity advances two points immediately.',
|
|
889
|
-
x: 'Hamilton submitted his Report on the Subject of Manufactures to Congress in December 1791 — the fourth and longest of his major Treasury reports. It broke with the free-trade orthodoxy of the Wealth of Nations on the grounds that the United States, as a late industrial entrant facing established British industry, required temporary protection and direct federal investment to develop a manufacturing base. Tench Coxe drafted much of the technical detail; Hamilton wrote the political argument. The Report listed seventeen industries to be developed and proposed bounties, premiums, and tariff protections for each. Congress did not pass it as a single statute. Instead, individual tariff schedules through the 1790s and 1800s absorbed portions of the recommendations — a process accelerated by the Embargo of 1807 and the War of 1812. In the v0.3 ruleset this Act is also a direct capital event: at the moment of passage every player collects fifty taels per Manufactures or Strategic Industry property already owned, both upgrade tracks halve cost for one full lap, and Industrial Capacity climbs by two points. Where the historical Report on the Subject of Manufactures recommended bounties and federal investment that Congress declined to enact as a single statute, the in-game Act delivers the bounty directly to the industrial owner on the same turn the legislation passes — a small subsidy on present holdings rather than the comprehensive tariff schedule Hamilton actually proposed, but enough to give the industrial line cash oxygen at the moment it needs the most.',
|
|
890
|
-
},
|
|
891
|
-
'act-passed · excise-enforcement': {
|
|
892
|
-
h: 'Excise Enforcement',
|
|
893
|
-
d: 'Through 1791–1794 federal excise collectors pressed the whiskey tax in western Pennsylvania. Resistance escalated from petitions to tarrings to armed assembly. In August 1794 Washington called out thirteen thousand militia to enforce the tax. The marchers met no organized opposition; the legal precedent stood.',
|
|
894
|
-
x: 'The Whiskey Excise of 1791 had produced organized resistance in the western counties of Pennsylvania, Virginia, and the Carolinas from the moment it took effect. Excise collectors were tarred and feathered; meetings condemned the tax; petitions for repeal collected hundreds of signatures. In the summer of 1794, federal officers in Allegheny County were attacked and the home of the regional inspector burned. Washington, after legal review, issued a proclamation invoking the Militia Acts of 1792 and called out thirteen thousand militia from Pennsylvania, New Jersey, Maryland, and Virginia. The army marched west under his nominal command, encountered no organized resistance, made twenty arrests, and dispersed. Two convictions for treason were obtained and both men pardoned. The principle that the federal government would enforce its tax laws was established. In your game, the Whiskey Excise pays double rent this round to its owner; Public Resistance climbs by two points and stays elevated.',
|
|
895
|
-
},
|
|
896
|
-
|
|
897
|
-
'track-threshold · credit-low': {
|
|
898
|
-
h: 'Credit Squeezed',
|
|
899
|
-
d: 'Public Credit has dropped to two or below. The bond market is jittery; specie tightens; merchant houses call in commercial paper outstanding. Route receipts halve until the reading recovers. Historically, the bond market collapses of March 1792 (Duer’s panic) and late 1797 began with similar credit contractions.',
|
|
900
|
-
x: 'A Credit reading of two or below indicates the federal government has lost the confidence of the bond market. Historically the comparable episodes are the panic of March 1792, triggered when William Duer’s speculation in federal bonds collapsed and pulled down a chain of secondary creditors in New York and Philadelphia, and the credit contraction of late 1797 that followed the XYZ Affair and the quasi-war scare with France. In both cases, commercial paper that had been rolled at three months suddenly required immediate redemption in specie; bills of exchange traded at discount; merchant houses called in receivables and cut new lending hard. The cargo trade slowed; tonnage duties at the customs houses fell; the Treasury’s own bond service costs rose because new debt could only be placed at a premium yield. In your game, all Route payments are now halved until Credit climbs back above two. The threshold is a warning, not yet a default: cash conservation matters more than acquisition this lap, and forcing an Act vote may not be worth the one-hundred-TN fee.',
|
|
901
|
-
},
|
|
902
|
-
'track-threshold · credit-default': {
|
|
903
|
-
h: 'Default',
|
|
904
|
-
d: 'Public Credit has reached zero. The federal bond market closes. All players lose half their cash and one upgrade each. Credit then resets to three. The closest historical analog is the federal credit collapse of late 1814, when bond auctions failed outright and Treasury notes traded at thirty cents on the dollar.',
|
|
905
|
-
x: 'A Credit reading of zero represents a federal credit default — a scenario the actual Treasury under Hamilton avoided but that came close in late 1814, when the federal Treasury was reduced to issuing tax-anticipation notes at roughly thirty cents on the dollar and bond auctions repeatedly failed to find subscribers. Madison’s administration negotiated emergency loans from the Bank of New York and the Girard Bank to keep the government solvent through the close of the War of 1812. In the game it is the punitive ceiling for the Credit track: every player loses half their cash on hand, every player loses one random upgrade, and Credit resets to three. The structural lesson is that federal credit is a shared resource, not a property of any one player. Even the holders of National Finance bonds and the Bank of the United States suffer when the issuer cannot service its own paper. Recovery is possible — the bond market reopens — but the upgrade losses are permanent and the lap cycle has been damaged.',
|
|
906
|
-
},
|
|
907
|
-
'track-threshold · resistance-rebellion-live': {
|
|
908
|
-
h: 'Whiskey Rebellion Live',
|
|
909
|
-
d: 'Public Resistance has reached eight. The frontier excise collectors are being tarred and feathered. The Whiskey Rebellion is live, in the sense that any further provocation triggers federal response. Historically, this is the summer of 1794: petitions exhausted, federal officers attacked, the militia not yet called.',
|
|
910
|
-
x: 'A Resistance reading of eight matches the political temperature in the western Pennsylvania counties through the summer of 1794. The Mingo Creek Association and the Parkinson’s Ferry assembly had organized; federal excise officers were unwelcome at every road junction; the inspector for Allegheny County had had his house burned and his salary collection refused at gunpoint. Washington had not yet issued the proclamation that would activate the militia; the legal and political question was whether organized resistance to a federal tax constituted insurrection within the meaning of the Militia Acts of 1792 and whether the federal government should march into Pennsylvania to enforce a revenue statute. In game terms, the Whiskey Rebellion card now activates: if drawn from the Republic Debate deck, the Whiskey Excise owner loses an upgrade and is sent directly to Crisis. Holding the Whiskey Excise becomes a liability rather than an asset. The Excise Enforcement Act, if it passes from this point forward, will compound the Resistance further toward the rebellion ceiling.',
|
|
911
|
-
},
|
|
912
|
-
'track-threshold · resistance-rebellion-fires': {
|
|
913
|
-
h: 'Rebellion',
|
|
914
|
-
d: 'Public Resistance has reached twelve. Open rebellion is in the field. Revenue upgrades are destroyed across all players; the Whiskey Excise owner is sent to Crisis; Resistance resets to six. The historical parallel is the brief military crisis of August 1794, before the militia march succeeded in restoring authority.',
|
|
915
|
-
x: 'A Resistance reading of twelve is the catastrophe scenario — organized armed resistance to federal taxation across multiple counties, on a scale the militia have not yet been called to suppress. The 1794 rebellion never actually escalated to this level: by the time Washington’s thirteen thousand militiamen reached the Monongahela in October, the assembled resistance had melted away and the army made twenty arrests against no field opposition. Game-mechanically this state is the punitive ceiling of the Resistance track, the mirror image of credit default. Every player’s Revenue System upgrades are destroyed — the federal tax collection apparatus has been physically dismantled by violence. The Whiskey Excise owner is sent to Crisis regardless of position. Resistance resets to six, the same level the track started elevated at, meaning recovery is possible but the political damage to revenue accumulation is permanent for this game and a second rebellion within the same run is materially possible.',
|
|
916
|
-
},
|
|
917
|
-
'track-threshold · capacity-payment-bonus': {
|
|
918
|
-
h: 'Industrial Capacity ≥ 6',
|
|
919
|
-
d: 'Industrial Capacity has reached six. The republic’s textile mills, iron foundries, and glassworks are producing at a threshold where Manufactures and Strategic Industry properties pay a permanent twenty-five percent capacity premium on every rent payment. Reaching eight raises that premium to fifty percent — replacing the lower bonus, not stacking on top of it — and also unlocks the endgame Influence bonus for industrial system owners.',
|
|
920
|
-
x: 'An Industrial Capacity reading of six represents the moment when American manufacturing displaces enough British and continental import volume to begin paying back its protective costs. Historically the analogous threshold was crossed in stages between roughly 1808 and 1815, accelerated by Jefferson’s Embargo Act and the broader trade interruptions of the War of 1812: the Slater mills in Rhode Island, the Boston Manufacturing Company at Waltham under Francis Cabot Lowell, the Springfield and Harpers Ferry armories under Roswell Lee and Eli Whitney all reached operating scale during the interruption. The Report on Manufactures laid the legislative groundwork two decades earlier and the Tariff of 1816 ratified it politically. In the v0.3 ruleset the payment bonus is a two-step ladder. At Capacity ≥ 6 every Manufactures and Strategic Industry rent is multiplied by one-and-a-quarter. At Capacity ≥ 8 that multiplier rises to one-and-a-half — replacing the lower bonus, not stacking on top of it — and the endgame Influence bonus of two points per industrial system owner unlocks at the same threshold. The ladder still composes with full-set multipliers and tier upgrades; a Tier III Naval Shipyard at Capacity 8 with the full Strategic Industry set pays significantly more rent than the same property paid under the pre-v0.3 single-tier bonus.',
|
|
921
|
-
},
|
|
922
|
-
'track-threshold · capacity-influence-bonus': {
|
|
923
|
-
h: 'Industrial Capacity ≥ 8',
|
|
924
|
-
d: 'Industrial Capacity has reached eight. Endgame scoring will award two additional Influence to anyone holding Manufactures, and two more for holding Strategic Industry. The young republic has built an industrial base that can no longer be unwound by ordinary tariff repeal in the next Congress.',
|
|
925
|
-
x: 'An Industrial Capacity of eight or above represents an irreversible industrial base — the scale at which a generation of mill investment, armory infrastructure, and skilled labor cannot be eliminated by mere policy reversal. In American history the comparable threshold was probably crossed around 1820: the protective tariff of that year, the Tariff of Abominations of 1828, and the political coalitions that defended both reflected an industrial constituency politically powerful enough to demand protection rather than one still being scaffolded into existence by Treasury argument. In the endgame scoring of this game, every player who holds at least one Manufactures property collects two additional Influence; every player who holds at least one Strategic Industry property collects two more. The bonus is per-player, not split among co-holders: industrial position pays double if you hold both sectors at once. A complete Strategic Industry set with this Capacity bonus active is decisive enough to swing the lap-7 scoring on its own.',
|
|
926
|
-
},
|
|
927
|
-
|
|
928
|
-
'endgame · winner-treasury-line': {
|
|
929
|
-
h: 'Treasury Architect Wins',
|
|
930
|
-
d: 'The Treasury / Finance line wins. Federal bonds, State Debt, the Bank — the Hamiltonian apex. The republic that emerges treats the bond market as the operative federal institution and the central bank as its hand on the credit lever. Jefferson’s objections become a minority position.',
|
|
931
|
-
x: '',
|
|
932
|
-
},
|
|
933
|
-
'endgame · winner-merchant-line': {
|
|
934
|
-
h: 'Merchant Capital Wins',
|
|
935
|
-
d: 'The Merchant / Infrastructure line wins. Routes, ports, exchanges, turnpikes — the visible republic. Federal credit matters, but so do the wharves and the post roads. The republic that emerges resembles the Whig program of the next generation more than the Hamiltonian apex.',
|
|
936
|
-
x: '',
|
|
937
|
-
},
|
|
938
|
-
'endgame · winner-human-line': {
|
|
939
|
-
h: 'Your Republic',
|
|
940
|
-
d: 'You won. The Influence Points accumulated through the lap-7 close credit your specific path: which sets you completed, which Acts you carried, what tracks you bent. The endgame report below scopes the full system to the choices you made, not to a generic Federalist program.',
|
|
941
|
-
x: '',
|
|
942
|
-
},
|
|
943
|
-
'endgame · republic-summary': {
|
|
944
|
-
h: 'Your Republic, after lap 7',
|
|
945
|
-
d: '',
|
|
946
|
-
x: '',
|
|
947
|
-
},
|
|
948
|
-
};
|
|
949
|
-
|
|
950
|
-
function wordCountOK(s, lo, hi) {
|
|
951
|
-
const n = s.trim().split(/\s+/).length;
|
|
952
|
-
return n >= lo && n <= hi;
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
function getNarration(triggerKey, state) {
|
|
956
|
-
const entry = NARRATION[triggerKey];
|
|
957
|
-
if (!entry) return null;
|
|
958
|
-
return { h: entry.h, d: entry.d, x: entry.x };
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
const PROFILES = {
|
|
963
|
-
'treasury-finance': {
|
|
964
|
-
label: 'Treasury / Finance',
|
|
965
|
-
strategy: 'Stacks federal credit. National Finance > State Debt > Rev Debt > Bank > Mint.',
|
|
966
|
-
decideCardChoice(s, pIdx, card) {
|
|
967
|
-
const p = s.players[pIdx];
|
|
968
|
-
if (card.id === 1 && p.cash >= 100) return { idx: 1, reason: 'cash ' + p.cash + ' ≥ 100, pay 50 TN over Crisis' };
|
|
969
|
-
return { idx: 0, reason: 'cash ' + p.cash + ' < 100, accept Crisis' };
|
|
970
|
-
},
|
|
971
|
-
assetPriority(sys, kind) {
|
|
972
|
-
const order = ['national-finance','state-debt','revolutionary-debt'];
|
|
973
|
-
const idx = order.indexOf(sys);
|
|
974
|
-
if (kind === 'institution') return 4;
|
|
975
|
-
if (idx >= 0) return idx;
|
|
976
|
-
return 99;
|
|
977
|
-
},
|
|
978
|
-
decideBuy(s, pIdx, spaceNum) {
|
|
979
|
-
const asset = ASSETS[spaceNum];
|
|
980
|
-
const sp = SPACES[spaceNum];
|
|
981
|
-
const p = s.players[pIdx];
|
|
982
|
-
const reserve = 200;
|
|
983
|
-
const aligned = (sp.kind === 'institution') || (asset.sys && ['national-finance','state-debt','revolutionary-debt'].includes(asset.sys));
|
|
984
|
-
if (!aligned) return { buy: false, reason: 'off-profile ('+(asset.sys||sp.kind)+')' };
|
|
985
|
-
if (p.cash - asset.cost < reserve) return { buy: false, reason: 'cash '+p.cash+' − cost '+asset.cost+' below reserve '+reserve };
|
|
986
|
-
return { buy: true, reason: 'aligned with profile ('+(asset.sys||sp.kind)+'); cash '+p.cash+' ≥ cost+reserve '+(asset.cost+reserve) };
|
|
987
|
-
},
|
|
988
|
-
decideAuctionBid(s, pIdx, spaceNum, highBid) {
|
|
989
|
-
const asset = ASSETS[spaceNum];
|
|
990
|
-
const sp = SPACES[spaceNum];
|
|
991
|
-
const p = s.players[pIdx];
|
|
992
|
-
const aligned = (sp.kind === 'institution') || (asset.sys && ['national-finance','state-debt','revolutionary-debt'].includes(asset.sys));
|
|
993
|
-
if (!aligned) return { bid: 0, reason: 'off-profile, no bid' };
|
|
994
|
-
const cap = Math.floor(asset.cost * 0.9);
|
|
995
|
-
const reserve = 200;
|
|
996
|
-
const myMax = Math.min(cap, p.cash - reserve);
|
|
997
|
-
const desired = Math.min(myMax, highBid + 10);
|
|
998
|
-
if (desired <= highBid || desired < 10) return { bid: 0, reason: 'cap '+cap+' / cash '+p.cash+' below current high '+highBid };
|
|
999
|
-
return { bid: desired, reason: 'aligned · bidding '+desired+' (cap '+cap+', high '+highBid+')' };
|
|
1000
|
-
},
|
|
1001
|
-
decideUpgrade(s, pIdx) {
|
|
1002
|
-
const p = s.players[pIdx];
|
|
1003
|
-
const reserve = 250;
|
|
1004
|
-
const candidates = p.ownedAssets.filter(a => {
|
|
1005
|
-
const asset = ASSETS[a.spaceNum]; if (!asset || !asset.up || a.tier >= 3) return false;
|
|
1006
|
-
return ownsFullSet(s, pIdx, asset.sys);
|
|
1007
|
-
}).sort((a, b) => ASSETS[b.spaceNum].base - ASSETS[a.spaceNum].base);
|
|
1008
|
-
for (const c of candidates) {
|
|
1009
|
-
let cost = ASSETS[c.spaceNum].up;
|
|
1010
|
-
if (ASSETS[c.spaceNum].sys === 'manufactures' && s.flags.manufacturesLap === s.lap) cost = Math.floor(cost / 2);
|
|
1011
|
-
if (p.cash - cost >= reserve) {
|
|
1012
|
-
return { spaceNum: c.spaceNum, cost, reason: 'highest-base set holding · '+SPACES[c.spaceNum].name+' Tier '+'I'.repeat(c.tier+1)+' · cost '+cost };
|
|
1013
|
-
}
|
|
1014
|
-
}
|
|
1015
|
-
return null;
|
|
1016
|
-
},
|
|
1017
|
-
decideVote(s, pIdx, actId) {
|
|
1018
|
-
const p = s.players[pIdx];
|
|
1019
|
-
if (actId === 1) return { vote: 'YES', reason: 'Funding Act lifts Rev Debt — profile aligned' };
|
|
1020
|
-
if (actId === 2) return { vote: 'YES', reason: 'Assumption Act lifts State Debt — profile aligned' };
|
|
1021
|
-
if (actId === 3) return { vote: 'YES', reason: 'Bank Charter — core Treasury/Finance trigger' };
|
|
1022
|
-
if (actId === 5) return { vote: 'YES', reason: 'Coinage Act — Credit + Capacity, profile aligned' };
|
|
1023
|
-
if (actId === 7) {
|
|
1024
|
-
const ownsRevenue = countOwnedSys(s, pIdx, 'revenue-system') > 0;
|
|
1025
|
-
return { vote: ownsRevenue ? 'NO' : 'YES', reason: ownsRevenue ? 'Excise Enforcement penalises owned Revenue assets' : 'no Revenue holdings — Excise harmless' };
|
|
1026
|
-
}
|
|
1027
|
-
if (actId === 4) {
|
|
1028
|
-
const ownsRevenue = countOwnedSys(s, pIdx, 'revenue-system') > 0;
|
|
1029
|
-
return { vote: ownsRevenue ? 'YES' : 'NO', reason: ownsRevenue ? 'Tariff lifts owned Revenue' : 'no Revenue holdings — Tariff offers nothing' };
|
|
1030
|
-
}
|
|
1031
|
-
if (actId === 6) {
|
|
1032
|
-
const ownsMfg = countOwnedSys(s, pIdx, 'manufactures') > 0;
|
|
1033
|
-
return { vote: ownsMfg ? 'YES' : 'NO', reason: ownsMfg ? 'Report on Mfg lifts owned Mfg' : 'no Mfg holdings' };
|
|
1034
|
-
}
|
|
1035
|
-
return { vote: 'NO', reason: 'unknown act · default no' };
|
|
1036
|
-
},
|
|
1037
|
-
decideEarlyVoteFee(s, pIdx) {
|
|
1038
|
-
const p = s.players[pIdx];
|
|
1039
|
-
if (s.acts.current && s.acts.current.actId === 3 && !s.flags.bankCharterPassed && ownsSpaceByIdx(s, pIdx, 12) && p.cash >= 300) {
|
|
1040
|
-
return { pay: true, reason: 'owns Bank · paying 100 TN to force Charter vote' };
|
|
1041
|
-
}
|
|
1042
|
-
return { pay: false, reason: 'no force-vote trigger' };
|
|
1043
|
-
},
|
|
1044
|
-
},
|
|
1045
|
-
'merchant-infrastructure': {
|
|
1046
|
-
label: 'Merchant / Infrastructure',
|
|
1047
|
-
strategy: 'Routes first — all four. Then Commerce, Improvements, Revenue.',
|
|
1048
|
-
decideCardChoice(s, pIdx, card) {
|
|
1049
|
-
const p = s.players[pIdx];
|
|
1050
|
-
if (card.id === 1 && p.cash >= 250) return { idx: 1, reason: 'cash ' + p.cash + ' ≥ 250, pay 50 TN' };
|
|
1051
|
-
return { idx: 0, reason: 'cash ' + p.cash + ' < 250, preserve route cash via Crisis' };
|
|
1052
|
-
},
|
|
1053
|
-
decideBuy(s, pIdx, spaceNum) {
|
|
1054
|
-
const asset = ASSETS[spaceNum];
|
|
1055
|
-
const sp = SPACES[spaceNum];
|
|
1056
|
-
const p = s.players[pIdx];
|
|
1057
|
-
const reserve = 150;
|
|
1058
|
-
if (sp.kind === 'route') {
|
|
1059
|
-
if (p.cash - asset.cost >= reserve) return { buy: true, reason: 'route grab — currently '+countOwnedRoutesByIdx(s,pIdx)+' routes · cash '+p.cash };
|
|
1060
|
-
return { buy: false, reason: 'route wanted but cash '+p.cash+' below cost+reserve' };
|
|
1061
|
-
}
|
|
1062
|
-
const aligned = asset.sys && ['commercial-infrastructure','internal-improvements','revenue-system'].includes(asset.sys);
|
|
1063
|
-
if (!aligned) return { buy: false, reason: 'off-profile ('+(asset.sys||sp.kind)+')' };
|
|
1064
|
-
if (p.cash - asset.cost < reserve) return { buy: false, reason: 'cash '+p.cash+' − cost '+asset.cost+' below reserve' };
|
|
1065
|
-
return { buy: true, reason: 'aligned ('+asset.sys+') · cash '+p.cash+' ≥ cost+reserve' };
|
|
1066
|
-
},
|
|
1067
|
-
decideAuctionBid(s, pIdx, spaceNum, highBid) {
|
|
1068
|
-
const asset = ASSETS[spaceNum];
|
|
1069
|
-
const sp = SPACES[spaceNum];
|
|
1070
|
-
const p = s.players[pIdx];
|
|
1071
|
-
const reserve = 150;
|
|
1072
|
-
let cap = 0;
|
|
1073
|
-
if (sp.kind === 'route') cap = asset.cost;
|
|
1074
|
-
else if (asset.sys && ['commercial-infrastructure','internal-improvements'].includes(asset.sys)) cap = Math.floor(asset.cost * 0.85);
|
|
1075
|
-
else if (asset.sys === 'revenue-system') cap = Math.floor(asset.cost * 0.7);
|
|
1076
|
-
else return { bid: 0, reason: 'off-profile, no bid' };
|
|
1077
|
-
const myMax = Math.min(cap, p.cash - reserve);
|
|
1078
|
-
const desired = Math.min(myMax, highBid + 10);
|
|
1079
|
-
if (desired <= highBid || desired < 10) return { bid: 0, reason: 'cap '+cap+' / cash '+p.cash+' below high '+highBid };
|
|
1080
|
-
return { bid: desired, reason: (sp.kind === 'route' ? 'route auction · willing to pay full' : 'aligned · capped at '+cap)+' · bid '+desired };
|
|
1081
|
-
},
|
|
1082
|
-
decideUpgrade(s, pIdx) {
|
|
1083
|
-
const p = s.players[pIdx];
|
|
1084
|
-
const reserve = 200;
|
|
1085
|
-
const order = ['commercial-infrastructure','internal-improvements','revenue-system'];
|
|
1086
|
-
for (const sys of order) {
|
|
1087
|
-
if (!ownsFullSet(s, pIdx, sys)) continue;
|
|
1088
|
-
const candidates = p.ownedAssets.filter(a => ASSETS[a.spaceNum]?.sys === sys && a.tier < 3 && ASSETS[a.spaceNum].up)
|
|
1089
|
-
.sort((a, b) => a.tier - b.tier);
|
|
1090
|
-
for (const c of candidates) {
|
|
1091
|
-
const cost = ASSETS[c.spaceNum].up;
|
|
1092
|
-
if (p.cash - cost >= reserve) {
|
|
1093
|
-
return { spaceNum: c.spaceNum, cost, reason: sys+' set held · Tier '+'I'.repeat(c.tier+1)+' on '+SPACES[c.spaceNum].name+' · cost '+cost };
|
|
1094
|
-
}
|
|
1095
|
-
}
|
|
1096
|
-
}
|
|
1097
|
-
return null;
|
|
1098
|
-
},
|
|
1099
|
-
decideVote(s, pIdx, actId) {
|
|
1100
|
-
const p = s.players[pIdx];
|
|
1101
|
-
if (actId === 4) return { vote: 'YES', reason: 'Tariff lifts Commerce-adjacent income' };
|
|
1102
|
-
if (actId === 3) return { vote: 'NO', reason: 'Bank Charter benefits Treasury/Finance, not Merchant' };
|
|
1103
|
-
if (actId === 2) return { vote: 'NO', reason: 'Assumption raises Resistance · profile averse' };
|
|
1104
|
-
if (actId === 7) return { vote: 'NO', reason: 'Excise Enforcement raises Resistance' };
|
|
1105
|
-
if (actId === 1) return { vote: 'NO', reason: 'Funding lifts Rev Debt — not held' };
|
|
1106
|
-
if (actId === 5) return { vote: 'NO', reason: 'Coinage rewards Mint owner — not held' };
|
|
1107
|
-
if (actId === 6) {
|
|
1108
|
-
const ownsMfg = countOwnedSys(s, pIdx, 'manufactures') > 0;
|
|
1109
|
-
return { vote: ownsMfg ? 'YES' : 'NO', reason: ownsMfg ? 'Report on Mfg lifts owned Mfg' : 'no Mfg holdings' };
|
|
1110
|
-
}
|
|
1111
|
-
return { vote: 'NO', reason: 'unknown act · default no' };
|
|
1112
|
-
},
|
|
1113
|
-
decideEarlyVoteFee(s, pIdx) {
|
|
1114
|
-
const p = s.players[pIdx];
|
|
1115
|
-
if (s.acts.current && s.acts.current.actId === 4 && !s.flags.tariffPassed && s.tracks.resistance.value <= 4 && p.cash >= 300) {
|
|
1116
|
-
return { pay: true, reason: 'forcing Tariff while Resistance ≤ 4' };
|
|
1117
|
-
}
|
|
1118
|
-
return { pay: false, reason: 'no force-vote trigger' };
|
|
1119
|
-
},
|
|
1120
|
-
},
|
|
1121
|
-
'manufacturer-industry': {
|
|
1122
|
-
label: 'Manufacturer / Industry',
|
|
1123
|
-
strategy: 'Mfg and Strategic Industry first. Pushes Capacity. Slow start, big late game.',
|
|
1124
|
-
decideCardChoice(s, pIdx, card) {
|
|
1125
|
-
const p = s.players[pIdx];
|
|
1126
|
-
if (card.id === 1 && p.cash >= 150) return { idx: 1, reason: 'cash ' + p.cash + ' ≥ 150, pay 50 TN over Crisis' };
|
|
1127
|
-
return { idx: 0, reason: 'cash ' + p.cash + ' < 150, accept Crisis to preserve industrial cash' };
|
|
1128
|
-
},
|
|
1129
|
-
decideBuy(s, pIdx, spaceNum) {
|
|
1130
|
-
const asset = ASSETS[spaceNum];
|
|
1131
|
-
const sp = SPACES[spaceNum];
|
|
1132
|
-
const p = s.players[pIdx];
|
|
1133
|
-
const reserve = 200;
|
|
1134
|
-
const isBank = sp.kind === 'institution' && sp.instKind === 'bank';
|
|
1135
|
-
const isMint = sp.kind === 'institution' && sp.instKind === 'mint';
|
|
1136
|
-
const isIndustrial = asset.sys === 'manufactures' || asset.sys === 'strategic-industry';
|
|
1137
|
-
const isImprovement = asset.sys === 'internal-improvements';
|
|
1138
|
-
const aligned = isIndustrial || isImprovement || isBank || isMint;
|
|
1139
|
-
if (!aligned) {
|
|
1140
|
-
return { buy: false, reason: 'off-profile after industrial priorities · ' + (asset.sys || sp.kind) };
|
|
1141
|
-
}
|
|
1142
|
-
if (p.cash - asset.cost < reserve) {
|
|
1143
|
-
return { buy: false, reason: 'cash reserve blocks buy · ' + p.cash + ' < ' + (asset.cost + reserve) };
|
|
1144
|
-
}
|
|
1145
|
-
if (isIndustrial) {
|
|
1146
|
-
/* v0.6 — set completion is a preference, NOT a gate.
|
|
1147
|
-
Surface which set this purchase advances when applicable. */
|
|
1148
|
-
if (asset.sys === 'manufactures' && countOwnedSys(s, pIdx, 'manufactures') === 2) {
|
|
1149
|
-
return { buy: true, reason: 'set-completion tie-break · Mfg' };
|
|
1150
|
-
}
|
|
1151
|
-
if (asset.sys === 'strategic-industry' && countOwnedSys(s, pIdx, 'strategic-industry') === 1) {
|
|
1152
|
-
return { buy: true, reason: 'set-completion tie-break · Strategic' };
|
|
1153
|
-
}
|
|
1154
|
-
return { buy: true, reason: 'industrial asset · Capacity path' };
|
|
1155
|
-
}
|
|
1156
|
-
/* Secondary preferences (Improvements / Bank / Mint) — buy when offered, no gating */
|
|
1157
|
-
return { buy: true, reason: 'no industrial pending · taking secondary ' + (isImprovement ? 'Improvements' : isBank ? 'Bank' : 'Mint') };
|
|
1158
|
-
},
|
|
1159
|
-
decideAuctionBid(s, pIdx, spaceNum, highBid) {
|
|
1160
|
-
const asset = ASSETS[spaceNum];
|
|
1161
|
-
const sp = SPACES[spaceNum];
|
|
1162
|
-
const p = s.players[pIdx];
|
|
1163
|
-
const reserve = 150;
|
|
1164
|
-
let cap = 0;
|
|
1165
|
-
if (asset.sys && ['manufactures','strategic-industry'].includes(asset.sys)) cap = Math.floor(asset.cost * 0.95);
|
|
1166
|
-
else if (asset.sys === 'internal-improvements') cap = Math.floor(asset.cost * 0.7);
|
|
1167
|
-
else return { bid: 0, reason: 'off-profile after industrial priorities · ' + (asset.sys || sp.kind) };
|
|
1168
|
-
const myMax = Math.min(cap, p.cash - reserve);
|
|
1169
|
-
const desired = Math.min(myMax, highBid + 10);
|
|
1170
|
-
if (desired <= highBid || desired < 10) return { bid: 0, reason: 'auction cap reached · ' + cap };
|
|
1171
|
-
return { bid: desired, reason: 'industrial asset · Capacity path · bid ' + desired + ' · cap ' + cap };
|
|
1172
|
-
},
|
|
1173
|
-
decideUpgrade(s, pIdx) {
|
|
1174
|
-
const p = s.players[pIdx];
|
|
1175
|
-
const reserve = 200;
|
|
1176
|
-
const order = ['manufactures','strategic-industry','internal-improvements'];
|
|
1177
|
-
for (const sys of order) {
|
|
1178
|
-
if (!ownsFullSet(s, pIdx, sys)) continue;
|
|
1179
|
-
const candidates = p.ownedAssets.filter(a => ASSETS[a.spaceNum]?.sys === sys && a.tier < 3 && ASSETS[a.spaceNum].up)
|
|
1180
|
-
.sort((a, b) => ASSETS[b.spaceNum].base - ASSETS[a.spaceNum].base);
|
|
1181
|
-
for (const c of candidates) {
|
|
1182
|
-
let cost = ASSETS[c.spaceNum].up;
|
|
1183
|
-
if (ASSETS[c.spaceNum].sys === 'manufactures' && s.flags.manufacturesLap === s.lap) cost = Math.floor(cost / 2);
|
|
1184
|
-
if (p.cash - cost >= reserve) {
|
|
1185
|
-
return { spaceNum: c.spaceNum, cost, reason: sys + ' set held · highest-base · ' + SPACES[c.spaceNum].name + ' Tier ' + 'I'.repeat(c.tier + 1) + ' · cost ' + cost };
|
|
1186
|
-
}
|
|
1187
|
-
}
|
|
1188
|
-
}
|
|
1189
|
-
return null;
|
|
1190
|
-
},
|
|
1191
|
-
decideVote(s, pIdx, actId) {
|
|
1192
|
-
if (actId === 6) return { vote: 'YES', reason: 'Report on Manufactures — core Manufacturer/Industry trigger' };
|
|
1193
|
-
if (actId === 1) return { vote: 'YES', reason: 'Funding Act — lifts owned Rev Debt; profile votes pro-credit' };
|
|
1194
|
-
if (actId === 2) {
|
|
1195
|
-
const ownsDebt = countOwnedSys(s, pIdx, 'revolutionary-debt') + countOwnedSys(s, pIdx, 'state-debt') > 0;
|
|
1196
|
-
return { vote: ownsDebt ? 'YES' : 'NO', reason: ownsDebt ? 'Assumption lifts owned debt' : 'no debt held — Assumption brings Resistance with no upside' };
|
|
1197
|
-
}
|
|
1198
|
-
if (actId === 4) return { vote: 'NO', reason: 'Tariff lifts Revenue — not held by Manufacturer profile' };
|
|
1199
|
-
if (actId === 3) {
|
|
1200
|
-
const ownsBank = ownsSpaceByIdx(s, pIdx, 12);
|
|
1201
|
-
return { vote: ownsBank ? 'YES' : 'NO', reason: ownsBank ? 'Bank Charter lifts owned Bank' : 'Bank not held — Charter favors Treasury/Finance' };
|
|
1202
|
-
}
|
|
1203
|
-
if (actId === 5) return { vote: 'NO', reason: 'Coinage rewards Mint owner — not aligned' };
|
|
1204
|
-
if (actId === 7) return { vote: 'NO', reason: 'Excise Enforcement raises Resistance — profile averse' };
|
|
1205
|
-
return { vote: 'NO', reason: 'unknown act · default no' };
|
|
1206
|
-
},
|
|
1207
|
-
decideEarlyVoteFee(s, pIdx) {
|
|
1208
|
-
const p = s.players[pIdx];
|
|
1209
|
-
const mfgPassed = s.acts.passed.some(a => a.actId === 6);
|
|
1210
|
-
if (s.acts.current && s.acts.current.actId === 6 && !mfgPassed && s.tracks.capacity.value < 4 && p.cash >= 300) {
|
|
1211
|
-
return { pay: true, reason: 'forcing Report on Manufactures while Capacity < 4' };
|
|
1212
|
-
}
|
|
1213
|
-
return { pay: false, reason: 'no force-vote trigger' };
|
|
1214
|
-
},
|
|
1215
|
-
},
|
|
1216
|
-
};
|
|
1217
|
-
|
|
1218
|
-
/* =====================================================================
|
|
1219
|
-
HELPERS
|
|
1220
|
-
===================================================================== */
|
|
1221
|
-
function adjustCash(s, delta, reason, playerIdx) {
|
|
1222
|
-
if (playerIdx == null) playerIdx = s.activePlayerIndex;
|
|
1223
|
-
const p = s.players[playerIdx];
|
|
1224
|
-
p.cash += delta;
|
|
1225
|
-
s = logRow(s, { actor: p.name, event:'CASH', detail: reason + ' · ' + (delta>=0?'+':'') + delta + ' TN · cash ' + p.cash, cls:'cash' });
|
|
1226
|
-
if (p.cash < 0 && !p.bankruptThisLap) {
|
|
1227
|
-
p.bankruptThisLap = true;
|
|
1228
|
-
p.bankruptLaps += 1;
|
|
1229
|
-
s = logRow(s, { actor:'System', event:'BANKRUPT', detail: p.name + ': cash below zero · lap ' + s.lap + ' counted as bankrupt (−1 Influence at scoring)', cls:'event' });
|
|
1230
|
-
}
|
|
1231
|
-
return s;
|
|
1232
|
-
}
|
|
1233
|
-
function adjustTrack(s, key, delta, reason) {
|
|
1234
|
-
const before = s.tracks[key].value;
|
|
1235
|
-
const next = Math.max(0, Math.min(12, before + delta));
|
|
1236
|
-
s.tracks[key].value = next;
|
|
1237
|
-
s.tracks[key].lastReason = reason;
|
|
1238
|
-
s = logRow(s, { actor:'Track', event:key.toUpperCase(), detail: reason + ' · ' + before + ' → ' + next + ' (' + (delta>=0?'+':'') + delta + ')', cls:'track' });
|
|
1239
|
-
if (key === 'credit' && before > 0 && next === 0) s.pendingDefault = true;
|
|
1240
|
-
if (key === 'resistance' && next === 12) s.pendingRebellion = true;
|
|
1241
|
-
return s;
|
|
1242
|
-
}
|
|
1243
|
-
function logRow(s, row) {
|
|
1244
|
-
s.ledger.push({ ...row, lap: s.lap, turn: s.turnIndex });
|
|
1245
|
-
return s;
|
|
1246
|
-
}
|
|
1247
|
-
function findOwnerIndex(s, spaceNum) {
|
|
1248
|
-
for (let i = 0; i < s.players.length; i++) if (s.players[i].ownedAssets.some(a => a.spaceNum === spaceNum)) return i;
|
|
1249
|
-
return -1;
|
|
1250
|
-
}
|
|
1251
|
-
function ownsSpaceByIdx(s, pIdx, num) { return s.players[pIdx].ownedAssets.some(a => a.spaceNum === num); }
|
|
1252
|
-
/* v0.5 — Industrial Capacity +1 when a Manufactures or Strategic Industry property is acquired from unowned state.
|
|
1253
|
-
Fires exactly once per industrial property (the trigger is the OWN event itself; we guard by checking the cap and by the fact that the property was unowned the moment before this is called).
|
|
1254
|
-
Respects the Capacity cap of 12. */
|
|
1255
|
-
const INDUSTRIAL_FIRST_PURCHASE_SPACES = new Set([31, 32, 34, 37, 39]);
|
|
1256
|
-
function bumpIndustrialCapacityOnFirstPurchase(s, spaceNum) {
|
|
1257
|
-
if (!INDUSTRIAL_FIRST_PURCHASE_SPACES.has(spaceNum)) return s;
|
|
1258
|
-
const spaceName = SPACES[spaceNum].name;
|
|
1259
|
-
if (s.tracks.capacity.value >= 12) {
|
|
1260
|
-
return logRow(s, { actor:'System', event:'CAPACITY', detail:'Industrial Capacity capped at 12 · ' + spaceName + ' enters production', cls:'event' });
|
|
1261
|
-
}
|
|
1262
|
-
return adjustTrack(s, 'capacity', 1, spaceName + ' enters production');
|
|
1263
|
-
}
|
|
1264
|
-
function countOwnedSys(s, pIdx, sys) { return s.players[pIdx].ownedAssets.filter(a => ASSETS[a.spaceNum]?.sys === sys).length; }
|
|
1265
|
-
function countOwnedRoutesByIdx(s, pIdx) { return s.players[pIdx].ownedAssets.filter(a => SPACES[a.spaceNum].kind === 'route').length; }
|
|
1266
|
-
function ownsFullSet(s, pIdx, sys) {
|
|
1267
|
-
const total = Object.entries(ASSETS).filter(([n,a]) => a.sys === sys).length;
|
|
1268
|
-
return countOwnedSys(s, pIdx, sys) === total;
|
|
1269
|
-
}
|
|
1270
|
-
function ownsInstitution(s, pIdx, instKind) {
|
|
1271
|
-
return s.players[pIdx].ownedAssets.some(a => SPACES[a.spaceNum].kind === 'institution' && SPACES[a.spaceNum].instKind === instKind);
|
|
1272
|
-
}
|
|
1273
|
-
function shuffleDeck(rng, deck) {
|
|
1274
|
-
const a = deck.slice();
|
|
1275
|
-
for (let i = a.length - 1; i > 0; i--) {
|
|
1276
|
-
const j = Math.floor(rng() * (i + 1));
|
|
1277
|
-
[a[i], a[j]] = [a[j], a[i]];
|
|
1278
|
-
}
|
|
1279
|
-
return a;
|
|
1280
|
-
}
|
|
1281
|
-
function startAuction(s, spaceNum, declinerIdx, reason) {
|
|
1282
|
-
/* Bidding order: starts to the left of the decliner (in 3-player, the next index). */
|
|
1283
|
-
const N = s.players.length;
|
|
1284
|
-
const order = [];
|
|
1285
|
-
for (let i = 1; i <= N; i++) order.push((declinerIdx + i) % N);
|
|
1286
|
-
/* Note: the decliner participates last (still has the right to bid). */
|
|
1287
|
-
return { spaceNum, declinerIdx, bidsRemaining: order, bids: [], highBid: 0, highBidder: -1, reason };
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
/* =====================================================================
|
|
1291
|
-
INITIAL STATE
|
|
1292
|
-
===================================================================== */
|
|
1293
|
-
function initialState(seed) {
|
|
1294
|
-
const rng = makeRng(seed);
|
|
1295
|
-
const players = [
|
|
1296
|
-
{ idx:0, name:'You', profile:'human', role:'Treasury Architect',
|
|
1297
|
-
cash:1500, position:0, ownedAssets:[], influencePoints:0, bankruptLaps:0, bankruptThisLap:false, keptHamilton:false, inCrisis:false },
|
|
1298
|
-
{ idx:1, name:'Hamilton', profile:'treasury-finance', role:'Treasury / Finance',
|
|
1299
|
-
cash:1500, position:0, ownedAssets:[], influencePoints:0, bankruptLaps:0, bankruptThisLap:false, keptHamilton:false, inCrisis:false },
|
|
1300
|
-
{ idx:2, name:'Morris', profile:'merchant-infrastructure', role:'Merchant / Infrastructure',
|
|
1301
|
-
cash:1500, position:0, ownedAssets:[], influencePoints:0, bankruptLaps:0, bankruptThisLap:false, keptHamilton:false, inCrisis:false },
|
|
1302
|
-
];
|
|
1303
|
-
return {
|
|
1304
|
-
rngSeed: seed, rng, lap: 1, turnIndex: 0, activePlayerIndex: 0,
|
|
1305
|
-
phase: 'lap-start', status: 'active',
|
|
1306
|
-
players,
|
|
1307
|
-
decks: {
|
|
1308
|
-
market: shuffleDeck(rng, MARKET_SHOCK_CARDS.map(c => c.id)),
|
|
1309
|
-
debate: shuffleDeck(rng, REPUBLIC_DEBATE_CARDS.map(c => c.id)),
|
|
1310
|
-
marketDiscard: [], debateDiscard: [],
|
|
1311
|
-
},
|
|
1312
|
-
acts: { passed: [], current: null },
|
|
1313
|
-
tracks: {
|
|
1314
|
-
credit: { value: 5, lastReason: 'Game start' },
|
|
1315
|
-
resistance: { value: 2, lastReason: 'Game start' },
|
|
1316
|
-
capacity: { value: 1, lastReason: 'Game start' },
|
|
1317
|
-
},
|
|
1318
|
-
flags: {},
|
|
1319
|
-
pendingCard: null, pendingLanding: null, pendingAuction: null, pendingResolveLanding: false,
|
|
1320
|
-
pendingDefault: false, pendingRebellion: false,
|
|
1321
|
-
ledger: [{ actor:'System', event:'INIT', detail:'3-player game · seed ' + seed + ' · You + Hamilton + Morris', cls:'event', lap:1, turn:0 }],
|
|
1322
|
-
};
|
|
1323
|
-
}
|
|
1324
|
-
|
|
1325
|
-
/* v0.8 — apply Starting Industrial Charter at setup if enabled and any Mfg profile present.
|
|
1326
|
-
Mutates state in place: gives lowest-indexed Mfg slot the Textile Works (space 31) at tier 0,
|
|
1327
|
-
bumps Capacity by 1, appends two setup ledger rows. Does NOT touch decisionLog. */
|
|
1328
|
-
let __CHARTER_ENABLED = true;
|
|
1329
|
-
function applyStartingCharter(s) {
|
|
1330
|
-
if (!__CHARTER_ENABLED) {
|
|
1331
|
-
s.flags.startingCharter = { granted:false, recipientSlot:null, asset:null };
|
|
1332
|
-
return s;
|
|
1333
|
-
}
|
|
1334
|
-
let recipient = -1;
|
|
1335
|
-
for (let i = 0; i < s.players.length; i++) {
|
|
1336
|
-
if (s.players[i].profile === 'manufacturer-industry') { recipient = i; break; }
|
|
1337
|
-
}
|
|
1338
|
-
if (recipient < 0) {
|
|
1339
|
-
s.flags.startingCharter = { granted:false, recipientSlot:null, asset:null };
|
|
1340
|
-
return s;
|
|
1341
|
-
}
|
|
1342
|
-
const TEXTILE_SPACE = 31;
|
|
1343
|
-
s.players[recipient].ownedAssets.push({ spaceNum: TEXTILE_SPACE, tier: 0 });
|
|
1344
|
-
s.tracks.capacity.value = Math.min(12, s.tracks.capacity.value + 1);
|
|
1345
|
-
s.tracks.capacity.lastReason = 'Industrial Charter at setup';
|
|
1346
|
-
const recipientName = s.players[recipient].name;
|
|
1347
|
-
s.ledger.push({ actor:'System', event:'OWN', detail:'Setup · ' + recipientName + ' receives Industrial Charter: Textile Works (0 TN)', cls:'event', lap:1, turn:0, actorIdx: recipient });
|
|
1348
|
-
s.ledger.push({ actor:'System', event:'CAPACITY', detail:'Industrial Capacity → ' + s.tracks.capacity.value + ' · Industrial Charter at setup', cls:'event', lap:1, turn:0, actorIdx: recipient });
|
|
1349
|
-
s.flags.startingCharter = { granted:true, recipientSlot:recipient, asset:'textile-works' };
|
|
1350
|
-
return s;
|
|
1351
|
-
}
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
/* =====================================================================
|
|
1356
|
-
REDUCER · 17 actions (16 from Phase 3 + AUCTION_BID)
|
|
1357
|
-
===================================================================== */
|
|
1358
|
-
let STATE;
|
|
1359
|
-
let DECISION_LOG = [];
|
|
1360
|
-
let TURN_SNAPSHOTS = [];
|
|
1361
|
-
let NARRATION_LOG = [];
|
|
1362
|
-
let NARRATION_ENABLED = true;
|
|
1363
|
-
const AUTOSAVE_KEY = 'sovereign.autosave';
|
|
1364
|
-
const SAVE_VERSION = 'v0.10';
|
|
1365
|
-
let REPLAY = null;
|
|
1366
|
-
|
|
1367
|
-
function dispatch(action) {
|
|
1368
|
-
const entry = {
|
|
1369
|
-
playerIdx: STATE.activePlayerIndex,
|
|
1370
|
-
action: action.type,
|
|
1371
|
-
params: Object.fromEntries(Object.entries(action).filter(([k]) => k !== 'type')),
|
|
1372
|
-
turn: STATE.turnIndex,
|
|
1373
|
-
lap: STATE.lap,
|
|
1374
|
-
};
|
|
1375
|
-
DECISION_LOG.push(entry);
|
|
1376
|
-
STATE = reduce(STATE, action);
|
|
1377
|
-
detectNarration();
|
|
1378
|
-
if (action.type === 'END_TURN' || action.type === 'END_GAME') captureSnapshot();
|
|
1379
|
-
if (action.type === 'END_TURN' || action.type === 'END_GAME') writeAutosave();
|
|
1380
|
-
render();
|
|
1381
|
-
if (STATE.pendingDefault) queueMicrotask(() => dispatch({ type:'TRIGGER_DEFAULT' }));
|
|
1382
|
-
else if (STATE.pendingRebellion) queueMicrotask(() => dispatch({ type:'TRIGGER_REBELLION' }));
|
|
1383
|
-
else if (STATE.phase === 'act-vote' && STATE.acts.current) {
|
|
1384
|
-
const nextOpp = findNextUnvotedOpponent();
|
|
1385
|
-
if (nextOpp >= 0) setTimeout(() => castOpponentActVote(nextOpp), 250);
|
|
1386
|
-
}
|
|
1387
|
-
else if (STATE.players[STATE.activePlayerIndex].profile !== 'human' && (STATE.phase === 'awaiting-roll' || STATE.phase === 'act-vote')) {
|
|
1388
|
-
setTimeout(() => runOpponent(), 250);
|
|
1389
|
-
}
|
|
1390
|
-
}
|
|
1391
|
-
|
|
1392
|
-
function findNextUnvotedOpponent() {
|
|
1393
|
-
if (!STATE.acts.current) return -1;
|
|
1394
|
-
if (STATE.acts.current.votes[0] == null && STATE.players[0].profile === 'human') return -1;
|
|
1395
|
-
const N = STATE.players.length;
|
|
1396
|
-
for (let off = 1; off < N; off++) {
|
|
1397
|
-
const idx = off % N;
|
|
1398
|
-
if (STATE.players[idx].profile !== 'human' && STATE.acts.current.votes[idx] == null) return idx;
|
|
1399
|
-
}
|
|
1400
|
-
return -1;
|
|
1401
|
-
}
|
|
1402
|
-
|
|
1403
|
-
function castOpponentActVote(pIdx) {
|
|
1404
|
-
if (!STATE || STATE.status === 'gameOver') return;
|
|
1405
|
-
if (STATE.phase !== 'act-vote' || !STATE.acts.current) return;
|
|
1406
|
-
if (STATE.acts.current.votes[pIdx] != null) return;
|
|
1407
|
-
const p = STATE.players[pIdx];
|
|
1408
|
-
if (p.profile === 'human') return;
|
|
1409
|
-
const profile = PROFILES[p.profile];
|
|
1410
|
-
const fee = profile.decideEarlyVoteFee(STATE, pIdx);
|
|
1411
|
-
if (fee.pay) {
|
|
1412
|
-
STATE = adjustCash(STATE, -100, 'Force-vote fee · ' + fee.reason, pIdx);
|
|
1413
|
-
STATE = logRow(STATE, { actor: p.name, event:'FORCE-FEE', detail:'Paid 100 TN to force Act vote · ' + fee.reason, cls:'vote' });
|
|
1414
|
-
render();
|
|
1415
|
-
}
|
|
1416
|
-
const decision = profile.decideVote(STATE, pIdx, STATE.acts.current.actId);
|
|
1417
|
-
dispatch({ type:'CAST_VOTE', playerIndex: pIdx, vote: decision.vote.toLowerCase(), reason: decision.reason });
|
|
1418
|
-
}
|
|
1419
|
-
|
|
1420
|
-
function runOpponent() {
|
|
1421
|
-
if (!STATE || STATE.status === 'gameOver') return;
|
|
1422
|
-
const pIdx = STATE.activePlayerIndex;
|
|
1423
|
-
const p = STATE.players[pIdx];
|
|
1424
|
-
if (p.profile === 'human') return;
|
|
1425
|
-
const profile = PROFILES[p.profile];
|
|
1426
|
-
|
|
1427
|
-
if (STATE.phase === 'act-vote' && STATE.acts.current && STATE.acts.current.votes[pIdx] == null) {
|
|
1428
|
-
/* Check force-fee first */
|
|
1429
|
-
const fee = profile.decideEarlyVoteFee(STATE, pIdx);
|
|
1430
|
-
if (fee.pay) {
|
|
1431
|
-
STATE = adjustCash(STATE, -100, 'Force-vote fee · ' + fee.reason, pIdx);
|
|
1432
|
-
STATE = logRow(STATE, { actor: p.name, event:'FORCE-FEE', detail:'Paid 100 TN to force Act vote · ' + fee.reason, cls:'vote' });
|
|
1433
|
-
render();
|
|
1434
|
-
}
|
|
1435
|
-
const decision = profile.decideVote(STATE, pIdx, STATE.acts.current.actId);
|
|
1436
|
-
dispatch({ type:'CAST_VOTE', playerIndex: pIdx, vote: decision.vote.toLowerCase(), reason: decision.reason });
|
|
1437
|
-
return;
|
|
1438
|
-
}
|
|
1439
|
-
|
|
1440
|
-
if (STATE.phase === 'awaiting-roll') {
|
|
1441
|
-
const up = profile.decideUpgrade(STATE, pIdx);
|
|
1442
|
-
if (up) { dispatch({ type:'UPGRADE_ASSET', playerIndex: pIdx, spaceNum: up.spaceNum, reason: up.reason }); return; }
|
|
1443
|
-
dispatch({ type:'ROLL_DICE' });
|
|
1444
|
-
}
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
function reduce(s, action) {
|
|
1448
|
-
switch (action.type) {
|
|
1449
|
-
|
|
1450
|
-
case 'BEGIN_LAP': {
|
|
1451
|
-
/* Reset per-lap flags for all players */
|
|
1452
|
-
s.players.forEach(p => p.bankruptThisLap = false);
|
|
1453
|
-
const act = ACTS.find(a => a.lap === s.lap);
|
|
1454
|
-
if (act) {
|
|
1455
|
-
s.acts.current = { actId: act.id, voting: true, votes: { 0:null, 1:null, 2:null } };
|
|
1456
|
-
s.phase = 'act-vote';
|
|
1457
|
-
s = logRow(s, { actor:'Acts', event:'REVEAL', detail:'Lap ' + s.lap + ' begins · ' + act.name + ' revealed for vote', cls:'act' });
|
|
1458
|
-
} else {
|
|
1459
|
-
s.phase = 'awaiting-roll';
|
|
1460
|
-
}
|
|
1461
|
-
return s;
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
|
-
case 'CAST_VOTE': {
|
|
1465
|
-
const pIdx = action.playerIndex;
|
|
1466
|
-
const yes = action.vote === 'yes';
|
|
1467
|
-
const p = s.players[pIdx];
|
|
1468
|
-
s.acts.current.votes[pIdx] = yes ? 'yes' : 'no';
|
|
1469
|
-
const reason = action.reason || (pIdx === 0 ? 'human vote' : 'opponent decision');
|
|
1470
|
-
s = logRow(s, { actor: p.name, event:'VOTE', detail: 'Vote ' + (yes ? 'YES' : 'NO') + ' · ' + reason, cls:'vote' });
|
|
1471
|
-
/* Check if all voted */
|
|
1472
|
-
const v = s.acts.current.votes;
|
|
1473
|
-
if (v[0] != null && v[1] != null && v[2] != null) {
|
|
1474
|
-
const yesCount = [v[0], v[1], v[2]].filter(x => x === 'yes').length;
|
|
1475
|
-
const act = ACTS.find(a => a.id === s.acts.current.actId);
|
|
1476
|
-
if (yesCount >= 2) {
|
|
1477
|
-
s.acts.passed.push({ actId: act.id, lap: s.lap });
|
|
1478
|
-
s = logRow(s, { actor:'Acts', event:'PASS', detail: act.name + ' passes ' + yesCount + '-' + (3 - yesCount), cls:'act' });
|
|
1479
|
-
s = act.apply(s);
|
|
1480
|
-
} else {
|
|
1481
|
-
s = logRow(s, { actor:'Acts', event:'FAIL', detail: act.name + ' fails ' + yesCount + '-' + (3 - yesCount) + ' · skipped for this game in Phase 4; re-queue behavior deferred', cls:'act' });
|
|
1482
|
-
s.flags.lastFailedActId = act.id;
|
|
1483
|
-
}
|
|
1484
|
-
s.acts.current = null;
|
|
1485
|
-
s.phase = 'awaiting-roll';
|
|
1486
|
-
}
|
|
1487
|
-
return s;
|
|
1488
|
-
}
|
|
1489
|
-
|
|
1490
|
-
case 'ROLL_DICE': {
|
|
1491
|
-
const pIdx = s.activePlayerIndex;
|
|
1492
|
-
const p = s.players[pIdx];
|
|
1493
|
-
if (p.inCrisis) {
|
|
1494
|
-
/* Human gets a choice sub-phase; opponents fall through to pay-50 fallback. */
|
|
1495
|
-
if (p.profile === 'human' && !action.crisisChoice) {
|
|
1496
|
-
s.phase = 'crisis-choice';
|
|
1497
|
-
return s;
|
|
1498
|
-
}
|
|
1499
|
-
if (action.crisisChoice === 'pay' || (p.profile !== 'human' && p.cash >= 50)) {
|
|
1500
|
-
s = adjustCash(s, -50, p.name + ': pay 50 TN to exit Crisis', pIdx);
|
|
1501
|
-
p.inCrisis = false;
|
|
1502
|
-
s = logRow(s, { actor: p.name, event:'EXIT CRISIS', detail:'Paid 50 TN', cls:'event' });
|
|
1503
|
-
} else if (action.crisisChoice === 'doubles') {
|
|
1504
|
-
const dd1 = Math.floor(s.rng() * 6) + 1;
|
|
1505
|
-
const dd2 = Math.floor(s.rng() * 6) + 1;
|
|
1506
|
-
s = logRow(s, { actor: p.name, event:'CRISIS ROLL', detail:'Doubles attempt 2d6 = ' + dd1 + ' + ' + dd2, cls:'event' });
|
|
1507
|
-
if (dd1 === dd2) {
|
|
1508
|
-
p.inCrisis = false;
|
|
1509
|
-
s.lastRoll = { d1: dd1, d2: dd2, total: dd1 + dd2 };
|
|
1510
|
-
s.turnIndex += 1;
|
|
1511
|
-
s = logRow(s, { actor: p.name, event:'EXIT CRISIS', detail:'Rolled doubles · exits Crisis and moves ' + (dd1 + dd2), cls:'event' });
|
|
1512
|
-
return reduce(s, { type:'MOVE_TOKEN', amount: dd1 + dd2 });
|
|
1513
|
-
} else {
|
|
1514
|
-
s = logRow(s, { actor: p.name, event:'STAY CRISIS', detail:'No doubles · turn ends', cls:'event' });
|
|
1515
|
-
return reduce(s, { type:'END_TURN' });
|
|
1516
|
-
}
|
|
1517
|
-
} else if (action.crisisChoice === 'skip') {
|
|
1518
|
-
p.crisisSkipBanked = true;
|
|
1519
|
-
s = logRow(s, { actor: p.name, event:'SKIP TURN', detail:'Skipping turn · Crisis clears next turn', cls:'event' });
|
|
1520
|
-
return reduce(s, { type:'END_TURN' });
|
|
1521
|
-
} else if (p.crisisSkipBanked) {
|
|
1522
|
-
p.inCrisis = false;
|
|
1523
|
-
p.crisisSkipBanked = false;
|
|
1524
|
-
s = logRow(s, { actor: p.name, event:'EXIT CRISIS', detail:'Skip-turn banked · free to move', cls:'event' });
|
|
1525
|
-
} else {
|
|
1526
|
-
s = logRow(s, { actor: p.name, event:'SKIP TURN', detail:'In Crisis without cash · skipping', cls:'event' });
|
|
1527
|
-
return reduce(s, { type:'END_TURN' });
|
|
1528
|
-
}
|
|
1529
|
-
}
|
|
1530
|
-
const d1 = Math.floor(s.rng() * 6) + 1;
|
|
1531
|
-
const d2 = Math.floor(s.rng() * 6) + 1;
|
|
1532
|
-
const total = d1 + d2;
|
|
1533
|
-
s.lastRoll = { d1, d2, total };
|
|
1534
|
-
s.turnIndex += 1;
|
|
1535
|
-
s = logRow(s, { actor: p.name, event:'ROLL', detail:'2d6 = ' + total + ' (' + d1 + '+' + d2 + ')', cls:'move' });
|
|
1536
|
-
return reduce(s, { type:'MOVE_TOKEN', amount: total });
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
|
-
case 'MOVE_TOKEN': {
|
|
1540
|
-
const pIdx = s.activePlayerIndex;
|
|
1541
|
-
const p = s.players[pIdx];
|
|
1542
|
-
const from = p.position;
|
|
1543
|
-
const to = (from + action.amount) % 40;
|
|
1544
|
-
const passedStart = (from + action.amount) >= 40;
|
|
1545
|
-
p.position = to;
|
|
1546
|
-
s = logRow(s, { actor: p.name, event:'MOVE', detail:'Move ' + action.amount + ' · ' + from + ' → ' + to + ' (' + SPACES[to].name + ')', cls:'move' });
|
|
1547
|
-
if (passedStart && to !== 0) s = reduce(s, { type:'PASS_TREASURY_OPENS' });
|
|
1548
|
-
else if (to === 0) s = reduce(s, { type:'PASS_TREASURY_OPENS', landed: true });
|
|
1549
|
-
if (s.status !== 'gameOver') return reduce(s, { type:'RESOLVE_LANDING' });
|
|
1550
|
-
return s;
|
|
1551
|
-
}
|
|
1552
|
-
|
|
1553
|
-
case 'PASS_TREASURY_OPENS': {
|
|
1554
|
-
const pIdx = s.activePlayerIndex;
|
|
1555
|
-
const amount = action.landed ? 400 : 200;
|
|
1556
|
-
s = adjustCash(s, amount, action.landed ? 'Treasury Opens: landed' : 'Treasury Opens: passed', pIdx);
|
|
1557
|
-
return s;
|
|
1558
|
-
}
|
|
1559
|
-
|
|
1560
|
-
case 'RESOLVE_LANDING': {
|
|
1561
|
-
const pIdx = s.activePlayerIndex;
|
|
1562
|
-
const p = s.players[pIdx];
|
|
1563
|
-
const space = SPACES[p.position];
|
|
1564
|
-
const k = space.kind;
|
|
1565
|
-
|
|
1566
|
-
if (k.startsWith('sys-') || k === 'route' || k === 'institution') {
|
|
1567
|
-
const ownerIdx = findOwnerIndex(s, space.num);
|
|
1568
|
-
if (ownerIdx < 0) {
|
|
1569
|
-
s.pendingLanding = { spaceNum: space.num, playerIndex: pIdx };
|
|
1570
|
-
s.phase = 'asset-decision';
|
|
1571
|
-
return s;
|
|
1572
|
-
} else if (ownerIdx === pIdx) {
|
|
1573
|
-
s = logRow(s, { actor:'Space', event:'OWN', detail: p.name + ' landed on own ' + space.name, cls:'move' });
|
|
1574
|
-
return finishLanding(s);
|
|
1575
|
-
} else {
|
|
1576
|
-
/* Pay rent */
|
|
1577
|
-
const rent = computeRent(s, space.num, ownerIdx);
|
|
1578
|
-
if (rent > 0) {
|
|
1579
|
-
s = adjustCash(s, -rent, 'Rent on ' + space.name + ' to ' + s.players[ownerIdx].name, pIdx);
|
|
1580
|
-
s = adjustCash(s, rent, 'Rent from ' + p.name + ' for ' + space.name, ownerIdx);
|
|
1581
|
-
} else {
|
|
1582
|
-
s = logRow(s, { actor:'Space', event:'NO RENT', detail: space.name + ': rent suspended or unowned-payment-condition', cls:'move' });
|
|
1583
|
-
}
|
|
1584
|
-
return finishLanding(s);
|
|
1585
|
-
}
|
|
1586
|
-
}
|
|
1587
|
-
if (k === 'tax') {
|
|
1588
|
-
s = adjustCash(s, -space.amount, space.name + ' tax', pIdx);
|
|
1589
|
-
if (space.resistBump) s = adjustTrack(s, 'resistance', space.resistBump, space.name);
|
|
1590
|
-
return finishLanding(s);
|
|
1591
|
-
}
|
|
1592
|
-
if (k === 'card-shock' || k === 'card-debate') {
|
|
1593
|
-
return reduce(s, { type:'DRAW_CARD', deck: k === 'card-shock' ? 'market' : 'debate' });
|
|
1594
|
-
}
|
|
1595
|
-
if (k === 'corner-crisis') {
|
|
1596
|
-
p.inCrisis = true;
|
|
1597
|
-
s = logRow(s, { actor:'Space', event:'CRISIS', detail: p.name + ' landed on Crisis', cls:'event' });
|
|
1598
|
-
return finishLanding(s);
|
|
1599
|
-
}
|
|
1600
|
-
if (k === 'corner-send') {
|
|
1601
|
-
p.position = 10;
|
|
1602
|
-
p.inCrisis = true;
|
|
1603
|
-
s = logRow(s, { actor:'Space', event:'SEND', detail: p.name + ' sent to Crisis', cls:'event' });
|
|
1604
|
-
return finishLanding(s);
|
|
1605
|
-
}
|
|
1606
|
-
s = logRow(s, { actor:'Space', event:'SAFE', detail: p.name + ' on ' + space.name + ' · no action', cls:'move' });
|
|
1607
|
-
return finishLanding(s);
|
|
1608
|
-
}
|
|
1609
|
-
|
|
1610
|
-
case 'BUY_ASSET': {
|
|
1611
|
-
const pIdx = s.pendingLanding.playerIndex;
|
|
1612
|
-
const sp = SPACES[s.pendingLanding.spaceNum];
|
|
1613
|
-
const asset = ASSETS[sp.num];
|
|
1614
|
-
const p = s.players[pIdx];
|
|
1615
|
-
if (p.cash < asset.cost) {
|
|
1616
|
-
s = logRow(s, { actor: p.name, event:'CANNOT BUY', detail:'Insufficient cash for ' + sp.name + ' · auctioning', cls:'cash' });
|
|
1617
|
-
s.pendingAuction = startAuction(s, sp.num, pIdx, 'insufficient cash');
|
|
1618
|
-
s.pendingLanding = null;
|
|
1619
|
-
s.phase = 'auction';
|
|
1620
|
-
return s;
|
|
1621
|
-
}
|
|
1622
|
-
s = adjustCash(s, -asset.cost, 'Buy ' + sp.name, pIdx);
|
|
1623
|
-
p.ownedAssets.push({ spaceNum: sp.num, tier: 0 });
|
|
1624
|
-
s = logRow(s, { actor: p.name, event:'OWN', detail:'Now owns ' + sp.name + (action.reason ? ' · ' + action.reason : ''), cls:'cash' });
|
|
1625
|
-
s = bumpIndustrialCapacityOnFirstPurchase(s, sp.num);
|
|
1626
|
-
s.pendingLanding = null;
|
|
1627
|
-
return finishLanding(s);
|
|
1628
|
-
}
|
|
1629
|
-
|
|
1630
|
-
case 'DECLINE_ASSET': {
|
|
1631
|
-
const pIdx = s.pendingLanding.playerIndex;
|
|
1632
|
-
const sp = SPACES[s.pendingLanding.spaceNum];
|
|
1633
|
-
s = logRow(s, { actor: s.players[pIdx].name, event:'DECLINE', detail:'Declined ' + sp.name + ' · auction opens' + (action.reason ? ' · ' + action.reason : ''), cls:'cash' });
|
|
1634
|
-
s.pendingAuction = startAuction(s, sp.num, pIdx, 'declined purchase');
|
|
1635
|
-
s.pendingLanding = null;
|
|
1636
|
-
s.phase = 'auction';
|
|
1637
|
-
return processNextAuctionBid(s);
|
|
1638
|
-
}
|
|
1639
|
-
|
|
1640
|
-
case 'AUCTION_BID': {
|
|
1641
|
-
const { playerIndex, amount } = action;
|
|
1642
|
-
const auction = s.pendingAuction;
|
|
1643
|
-
if (amount > auction.highBid) {
|
|
1644
|
-
auction.highBid = amount;
|
|
1645
|
-
auction.highBidder = playerIndex;
|
|
1646
|
-
}
|
|
1647
|
-
auction.bids.push({ playerIndex, amount, reason: action.reason || (playerIndex === 0 ? 'human bid' : 'opponent bid') });
|
|
1648
|
-
s = logRow(s, { actor: s.players[playerIndex].name, event:'BID', detail:'Bids ' + (amount === 0 ? 'PASS' : '$' + amount) + ' on ' + SPACES[auction.spaceNum].name + ' · ' + (action.reason || 'no reason logged'), cls:'auction' });
|
|
1649
|
-
auction.bidsRemaining.shift();
|
|
1650
|
-
return processNextAuctionBid(s);
|
|
1651
|
-
}
|
|
1652
|
-
|
|
1653
|
-
case 'UPGRADE_ASSET': {
|
|
1654
|
-
const pIdx = action.playerIndex != null ? action.playerIndex : s.activePlayerIndex;
|
|
1655
|
-
const p = s.players[pIdx];
|
|
1656
|
-
const a = p.ownedAssets.find(x => x.spaceNum === action.spaceNum);
|
|
1657
|
-
if (!a || a.tier >= 3) return s;
|
|
1658
|
-
const props = ASSETS[a.spaceNum];
|
|
1659
|
-
if (!props.up) return s;
|
|
1660
|
-
if (!ownsFullSet(s, pIdx, props.sys)) return s;
|
|
1661
|
-
let cost = props.up;
|
|
1662
|
-
if ((props.sys === 'manufactures' || props.sys === 'strategic-industry') && s.flags.manufacturesLap === s.lap) cost = Math.floor(cost / 2);
|
|
1663
|
-
if (p.cash < cost) return s;
|
|
1664
|
-
s = adjustCash(s, -cost, 'Upgrade ' + SPACES[a.spaceNum].name + ' → Tier ' + 'I'.repeat(a.tier + 1) + (action.reason ? ' · ' + action.reason : ''), pIdx);
|
|
1665
|
-
a.tier += 1;
|
|
1666
|
-
return logRow(s, { actor: p.name, event:'UPGRADE', detail: SPACES[a.spaceNum].name + ' now Tier ' + 'I'.repeat(a.tier), cls:'cash' });
|
|
1667
|
-
}
|
|
1668
|
-
|
|
1669
|
-
case 'DRAW_CARD': {
|
|
1670
|
-
const pIdx = s.activePlayerIndex;
|
|
1671
|
-
const deckKey = action.deck;
|
|
1672
|
-
const cards = deckKey === 'market' ? MARKET_SHOCK_CARDS : REPUBLIC_DEBATE_CARDS;
|
|
1673
|
-
const discardKey = deckKey + 'Discard';
|
|
1674
|
-
if (s.decks[deckKey].length === 0) {
|
|
1675
|
-
s.decks[deckKey] = shuffleDeck(s.rng, s.decks[discardKey]);
|
|
1676
|
-
s.decks[discardKey] = [];
|
|
1677
|
-
s = logRow(s, { actor:'Deck', event:'RESHUFFLE', detail: deckKey + ' deck exhausted · reshuffled', cls:'card' });
|
|
1678
|
-
}
|
|
1679
|
-
const cardId = s.decks[deckKey].shift();
|
|
1680
|
-
const card = cards.find(c => c.id === cardId);
|
|
1681
|
-
s = logRow(s, { actor:'Deck', event:'DRAW', detail: s.players[pIdx].name + ' drew ' + card.name, cls:'card' });
|
|
1682
|
-
s.pendingCard = { deck: deckKey, cardId, hasChoices: !!card.choices, playerIndex: pIdx };
|
|
1683
|
-
s.phase = card.choices ? 'card-choice' : 'card-resolve';
|
|
1684
|
-
if (pIdx !== 0) {
|
|
1685
|
-
if (card.choices) {
|
|
1686
|
-
const profile = PROFILES[s.players[pIdx].profile];
|
|
1687
|
-
const choice = profile.decideCardChoice ? profile.decideCardChoice(s, pIdx, card) : { idx: 1, reason:'default' };
|
|
1688
|
-
s = logRow(s, { actor: s.players[pIdx].name, event:'CARD CHOICE', detail:'Choosing "' + card.choices[choice.idx].label + '" · ' + choice.reason, cls:'card' });
|
|
1689
|
-
setTimeout(() => dispatch({ type:'RESOLVE_CARD_CHOICE', choiceIndex: choice.idx }), 250);
|
|
1690
|
-
} else {
|
|
1691
|
-
setTimeout(() => dispatch({ type:'RESOLVE_CARD' }), 250);
|
|
1692
|
-
}
|
|
1693
|
-
}
|
|
1694
|
-
return s;
|
|
1695
|
-
}
|
|
1696
|
-
|
|
1697
|
-
case 'RESOLVE_CARD': {
|
|
1698
|
-
const { deck, cardId, playerIndex } = s.pendingCard;
|
|
1699
|
-
const cards = deck === 'market' ? MARKET_SHOCK_CARDS : REPUBLIC_DEBATE_CARDS;
|
|
1700
|
-
const card = cards.find(c => c.id === cardId);
|
|
1701
|
-
s = card.resolve(s, playerIndex);
|
|
1702
|
-
if (!(card.id === 12 && deck === 'debate' && s.players[playerIndex].keptHamilton)) {
|
|
1703
|
-
s.decks[deck + 'Discard'].push(cardId);
|
|
1704
|
-
}
|
|
1705
|
-
s.pendingCard = null;
|
|
1706
|
-
return finishLanding(s);
|
|
1707
|
-
}
|
|
1708
|
-
|
|
1709
|
-
case 'RESOLVE_CARD_CHOICE': {
|
|
1710
|
-
const { deck, cardId, playerIndex } = s.pendingCard;
|
|
1711
|
-
const cards = deck === 'market' ? MARKET_SHOCK_CARDS : REPUBLIC_DEBATE_CARDS;
|
|
1712
|
-
const card = cards.find(c => c.id === cardId);
|
|
1713
|
-
s = card.choices[action.choiceIndex].resolve(s, playerIndex);
|
|
1714
|
-
s.decks[deck + 'Discard'].push(cardId);
|
|
1715
|
-
s.pendingCard = null;
|
|
1716
|
-
return finishLanding(s);
|
|
1717
|
-
}
|
|
1718
|
-
|
|
1719
|
-
case 'TRIGGER_DEFAULT': {
|
|
1720
|
-
s = logRow(s, { actor:'System', event:'DEFAULT', detail:'Public Credit = 0 · all players lose 50 % cash + 1 random upgrade', cls:'event' });
|
|
1721
|
-
for (let i = 0; i < s.players.length; i++) {
|
|
1722
|
-
const p = s.players[i];
|
|
1723
|
-
const before = p.cash;
|
|
1724
|
-
p.cash = Math.floor(p.cash / 2);
|
|
1725
|
-
s = logRow(s, { actor:'System', event:'DEFAULT', detail: p.name + ' loses ' + (before - p.cash) + ' TN (50 %)', cls:'event' });
|
|
1726
|
-
const upgraded = p.ownedAssets.filter(a => a.tier > 0);
|
|
1727
|
-
if (upgraded.length > 0) {
|
|
1728
|
-
const target = upgraded[Math.floor(s.rng() * upgraded.length)];
|
|
1729
|
-
target.tier -= 1;
|
|
1730
|
-
s = logRow(s, { actor:'System', event:'DEFAULT', detail: p.name + ': ' + SPACES[target.spaceNum].name + ' loses 1 upgrade', cls:'event' });
|
|
1731
|
-
}
|
|
1732
|
-
}
|
|
1733
|
-
s.tracks.credit.value = 3;
|
|
1734
|
-
s.tracks.credit.lastReason = 'Default reset';
|
|
1735
|
-
s.pendingDefault = false;
|
|
1736
|
-
return s;
|
|
1737
|
-
}
|
|
1738
|
-
|
|
1739
|
-
case 'TRIGGER_REBELLION': {
|
|
1740
|
-
s = logRow(s, { actor:'System', event:'REBELLION', detail:'Resistance = 12 · Revenue upgrades destroyed · Whiskey owner → Crisis · Resistance → 6', cls:'event' });
|
|
1741
|
-
for (let i = 0; i < s.players.length; i++) {
|
|
1742
|
-
s.players[i].ownedAssets.forEach(a => { if (ASSETS[a.spaceNum]?.sys === 'revenue-system') a.tier = 0; });
|
|
1743
|
-
}
|
|
1744
|
-
const wIdx = findOwnerIndex(s, 14);
|
|
1745
|
-
if (wIdx >= 0) {
|
|
1746
|
-
s.players[wIdx].position = 10;
|
|
1747
|
-
s.players[wIdx].inCrisis = true;
|
|
1748
|
-
}
|
|
1749
|
-
s.tracks.resistance.value = 6;
|
|
1750
|
-
s.tracks.resistance.lastReason = 'Rebellion reset';
|
|
1751
|
-
s.pendingRebellion = false;
|
|
1752
|
-
return s;
|
|
1753
|
-
}
|
|
1754
|
-
|
|
1755
|
-
case 'END_TURN': {
|
|
1756
|
-
s.activePlayerIndex = (s.activePlayerIndex + 1) % s.players.length;
|
|
1757
|
-
s.phase = 'awaiting-roll';
|
|
1758
|
-
if (s.activePlayerIndex === 0) {
|
|
1759
|
-
/* Full round done · advance lap */
|
|
1760
|
-
s.lap += 1;
|
|
1761
|
-
if (s.lap > 7) {
|
|
1762
|
-
s = logRow(s, { actor:'System', event:'GAME OVER', detail:'7 laps complete · scoring', cls:'event' });
|
|
1763
|
-
return reduce(s, { type:'END_GAME' });
|
|
1764
|
-
}
|
|
1765
|
-
s = logRow(s, { actor:'System', event:'LAP', detail:'Begin lap ' + s.lap, cls:'event' });
|
|
1766
|
-
return reduce(s, { type:'BEGIN_LAP' });
|
|
1767
|
-
}
|
|
1768
|
-
return s;
|
|
1769
|
-
}
|
|
1770
|
-
|
|
1771
|
-
case 'END_GAME': {
|
|
1772
|
-
s = computeFinalInfluence(s);
|
|
1773
|
-
s.phase = 'game-over';
|
|
1774
|
-
s.status = 'gameOver';
|
|
1775
|
-
return s;
|
|
1776
|
-
}
|
|
1777
|
-
|
|
1778
|
-
default:
|
|
1779
|
-
return s;
|
|
1780
|
-
}
|
|
1781
|
-
}
|
|
1782
|
-
|
|
1783
|
-
function finishLanding(s) {
|
|
1784
|
-
/* Preserve live sub-phases set by cards (auctions, choices). */
|
|
1785
|
-
if (s.phase === 'auction') return s;
|
|
1786
|
-
if (s.phase === 'card-choice' && s.pendingCard) return s;
|
|
1787
|
-
/* Card-driven movement: resolve landing at new position before ending turn. */
|
|
1788
|
-
if (s.pendingResolveLanding) {
|
|
1789
|
-
s.pendingResolveLanding = false;
|
|
1790
|
-
return reduce(s, { type:'RESOLVE_LANDING' });
|
|
1791
|
-
}
|
|
1792
|
-
s.phase = 'awaiting-roll';
|
|
1793
|
-
return reduce(s, { type:'END_TURN' });
|
|
1794
|
-
}
|
|
1795
|
-
|
|
1796
|
-
function processNextAuctionBid(s) {
|
|
1797
|
-
const a = s.pendingAuction;
|
|
1798
|
-
if (!a) return s;
|
|
1799
|
-
while (a.bidsRemaining.length > 0) {
|
|
1800
|
-
const pIdx = a.bidsRemaining[0];
|
|
1801
|
-
const p = s.players[pIdx];
|
|
1802
|
-
if (p.profile === 'human') {
|
|
1803
|
-
/* Wait for human UI to dispatch AUCTION_BID */
|
|
1804
|
-
s.phase = 'auction';
|
|
1805
|
-
return s;
|
|
1806
|
-
}
|
|
1807
|
-
/* Opponent bid via decision function */
|
|
1808
|
-
const profile = PROFILES[p.profile];
|
|
1809
|
-
const decision = profile.decideAuctionBid(s, pIdx, a.spaceNum, a.highBid);
|
|
1810
|
-
a.bidsRemaining.shift();
|
|
1811
|
-
s = logRow(s, { actor: p.name, event:'BID', detail:'Bids ' + (decision.bid === 0 ? 'PASS' : '$' + decision.bid) + ' on ' + SPACES[a.spaceNum].name + ' · ' + decision.reason, cls:'auction' });
|
|
1812
|
-
a.bids.push({ playerIndex: pIdx, amount: decision.bid, reason: decision.reason });
|
|
1813
|
-
if (decision.bid > a.highBid) {
|
|
1814
|
-
a.highBid = decision.bid;
|
|
1815
|
-
a.highBidder = pIdx;
|
|
1816
|
-
}
|
|
1817
|
-
}
|
|
1818
|
-
/* All bids in — resolve */
|
|
1819
|
-
if (a.highBid > 0 && a.highBidder >= 0) {
|
|
1820
|
-
const winner = s.players[a.highBidder];
|
|
1821
|
-
s = adjustCash(s, -a.highBid, 'Auction win: ' + SPACES[a.spaceNum].name, a.highBidder);
|
|
1822
|
-
winner.ownedAssets.push({ spaceNum: a.spaceNum, tier: 0 });
|
|
1823
|
-
s = logRow(s, { actor: winner.name, event:'AUCTION WIN', detail: 'Won ' + SPACES[a.spaceNum].name + ' for $' + a.highBid, cls:'auction' });
|
|
1824
|
-
s = bumpIndustrialCapacityOnFirstPurchase(s, a.spaceNum);
|
|
1825
|
-
} else {
|
|
1826
|
-
s = logRow(s, { actor:'Auction', event:'UNSOLD', detail: SPACES[a.spaceNum].name + ' remains unowned (no bids ≥ 10 TN)', cls:'auction' });
|
|
1827
|
-
}
|
|
1828
|
-
s.pendingAuction = null;
|
|
1829
|
-
s.phase = 'awaiting-roll';
|
|
1830
|
-
/* The decliner's turn ends */
|
|
1831
|
-
return reduce(s, { type:'END_TURN' });
|
|
1832
|
-
}
|
|
1833
|
-
|
|
1834
|
-
/* =====================================================================
|
|
1835
|
-
RENT COMPUTATION
|
|
1836
|
-
===================================================================== */
|
|
1837
|
-
function computeRent(s, spaceNum, ownerIdx) {
|
|
1838
|
-
const space = SPACES[spaceNum];
|
|
1839
|
-
const asset = ASSETS[spaceNum];
|
|
1840
|
-
const owner = s.players[ownerIdx];
|
|
1841
|
-
|
|
1842
|
-
if (space.kind === 'route') {
|
|
1843
|
-
if (s.flags.shippingDisruptedUntilTurn && s.turnIndex < s.flags.shippingDisruptedUntilTurn) return 0;
|
|
1844
|
-
const n = countOwnedRoutesByIdx(s, ownerIdx);
|
|
1845
|
-
let pay = ROUTE_LADDER[Math.min(n, 4)];
|
|
1846
|
-
if (s.tracks.credit.value <= 2) pay = Math.floor(pay / 2);
|
|
1847
|
-
return pay;
|
|
1848
|
-
}
|
|
1849
|
-
if (space.kind === 'institution') {
|
|
1850
|
-
const diceAvg = s.lastRoll ? s.lastRoll.total : 7;
|
|
1851
|
-
if (space.instKind === 'bank') {
|
|
1852
|
-
return (s.flags.bankCharterPassed ? 10 : 4) * diceAvg;
|
|
1853
|
-
}
|
|
1854
|
-
if (space.instKind === 'mint') {
|
|
1855
|
-
const hasBank = ownsInstitution(s, ownerIdx, 'bank');
|
|
1856
|
-
if (!hasBank) return 0;
|
|
1857
|
-
return (s.flags.bankCharterPassed ? 20 : 10) * diceAvg;
|
|
1858
|
-
}
|
|
1859
|
-
return 0;
|
|
1860
|
-
}
|
|
1861
|
-
/* Property */
|
|
1862
|
-
const ownedAsset = owner.ownedAssets.find(a => a.spaceNum === spaceNum);
|
|
1863
|
-
if (!ownedAsset) return 0;
|
|
1864
|
-
const tier = ownedAsset.tier;
|
|
1865
|
-
let multiplier = 2; // full set or base
|
|
1866
|
-
const fullSet = ownsFullSet(s, ownerIdx, asset.sys);
|
|
1867
|
-
if (tier === 0) multiplier = fullSet ? 2 : 1;
|
|
1868
|
-
else multiplier = [5, 15, 30][tier - 1];
|
|
1869
|
-
|
|
1870
|
-
let rent = asset.base * multiplier;
|
|
1871
|
-
/* Act-permanent multipliers */
|
|
1872
|
-
if (asset.sys === 'revolutionary-debt' && s.flags.fundingPassed) rent = Math.floor(rent * 1.5);
|
|
1873
|
-
if (asset.sys === 'state-debt' && s.flags.assumptionPassed) rent = rent * 2;
|
|
1874
|
-
if (asset.sys === 'revenue-system' && s.flags.tariffPassed) rent = Math.floor(rent * 1.5);
|
|
1875
|
-
/* Round-only modifiers */
|
|
1876
|
-
if (asset.sys === 'revenue-system' && s.flags.revenueHalvedLap === s.lap) rent = Math.floor(rent / 2);
|
|
1877
|
-
if (spaceNum === 14 && s.flags.exciseDoubleLap === s.lap) rent = rent * 2;
|
|
1878
|
-
if (spaceNum === 31 && s.flags.textileDoubleLap === s.lap) rent = rent * 2;
|
|
1879
|
-
if ((asset.sys === 'revolutionary-debt' || asset.sys === 'state-debt') && s.flags.debtDoubleLap === s.lap) rent = rent * 2;
|
|
1880
|
-
if (asset.sys === 'commercial-infrastructure' && s.flags.shippingDisruptedUntilTurn && s.turnIndex < s.flags.shippingDisruptedUntilTurn) rent = 0;
|
|
1881
|
-
/* Capacity payment ladder on Mfg/Strategic: ≥8 → +50 % · ≥6 → +25 % (replace, not stack) */
|
|
1882
|
-
if (asset.sys === 'manufactures' || asset.sys === 'strategic-industry') {
|
|
1883
|
-
if (s.tracks.capacity.value >= 8) rent = Math.floor(rent * 1.5);
|
|
1884
|
-
else if (s.tracks.capacity.value >= 6) rent = Math.floor(rent * 1.25);
|
|
1885
|
-
}
|
|
1886
|
-
return rent;
|
|
1887
|
-
}
|
|
1888
|
-
|
|
1889
|
-
/* =====================================================================
|
|
1890
|
-
INFLUENCE SCORING — per player
|
|
1891
|
-
===================================================================== */
|
|
1892
|
-
function computeFinalInfluence(s) {
|
|
1893
|
-
s.finalScores = s.players.map(p => scorePlayer(s, p.idx));
|
|
1894
|
-
s.players.forEach((p, i) => p.influencePoints = s.finalScores[i].total);
|
|
1895
|
-
s = logRow(s, { actor:'System', event:'SCORE', detail: 'Final Influence · ' + s.players.map((p, i) => p.name + ' ' + s.finalScores[i].total).join(' · '), cls:'event' });
|
|
1896
|
-
return s;
|
|
1897
|
-
}
|
|
1898
|
-
|
|
1899
|
-
function scorePlayer(s, pIdx) {
|
|
1900
|
-
const p = s.players[pIdx];
|
|
1901
|
-
const breakdown = []; let total = 0;
|
|
1902
|
-
const add = (label, detail, pts) => { total += pts; breakdown.push({ label, detail, pts }); };
|
|
1903
|
-
add('Cash held', Math.floor(p.cash / 400) + ' × 400 TN', Math.floor(p.cash / 400));
|
|
1904
|
-
const propsOwned = p.ownedAssets.filter(a => ASSETS[a.spaceNum]?.sys).length;
|
|
1905
|
-
add('Properties owned', propsOwned + ' properties', propsOwned);
|
|
1906
|
-
const upgradedCount = p.ownedAssets.filter(a => a.tier > 0).length;
|
|
1907
|
-
add('Upgraded properties', upgradedCount + ' upgraded', upgradedCount * 2);
|
|
1908
|
-
let setsCount = 0;
|
|
1909
|
-
Object.keys(SYS_LABEL).forEach(sys => { if (ownsFullSet(s, pIdx, sys)) setsCount++; });
|
|
1910
|
-
add('Complete sets', setsCount + ' complete', setsCount * 3);
|
|
1911
|
-
/* v0.4 — industrial set completion bonuses (stack on top of color-set count above) */
|
|
1912
|
-
if (ownsFullSet(s, pIdx, 'manufactures')) add('Industrial Mfg set', 'full Manufactures set', 3);
|
|
1913
|
-
if (ownsFullSet(s, pIdx, 'strategic-industry')) add('Industrial Strategic set', 'full Strategic set', 2);
|
|
1914
|
-
const routes = countOwnedRoutesByIdx(s, pIdx);
|
|
1915
|
-
add('Routes owned', routes + ' routes', routes);
|
|
1916
|
-
const insts = p.ownedAssets.filter(a => SPACES[a.spaceNum].kind === 'institution').length;
|
|
1917
|
-
add('Institutions owned', insts + ' institutions', insts * 2);
|
|
1918
|
-
if (s.tracks.credit.value >= 8 && countOwnedSys(s, pIdx, 'national-finance') > 0) {
|
|
1919
|
-
const bonus = s.tracks.credit.value === 12 ? 2 : 1;
|
|
1920
|
-
add(s.tracks.credit.value === 12 ? 'Credit = 12 end-game' : 'Credit ≥ 8 end-game',
|
|
1921
|
-
'NF owner · +' + bonus,
|
|
1922
|
-
bonus);
|
|
1923
|
-
}
|
|
1924
|
-
if (s.tracks.capacity.value >= 8) {
|
|
1925
|
-
const hasMfg = countOwnedSys(s, pIdx, 'manufactures') > 0;
|
|
1926
|
-
const hasStrat = countOwnedSys(s, pIdx, 'strategic-industry') > 0;
|
|
1927
|
-
let bonus = 0; if (hasMfg) bonus += 2; if (hasStrat) bonus += 2;
|
|
1928
|
-
if (bonus > 0) add('Capacity ≥ 8 bonus', 'owns Mfg ' + (hasMfg ? '✓' : '✕') + ' / Strategic ' + (hasStrat ? '✓' : '✕'), bonus);
|
|
1929
|
-
}
|
|
1930
|
-
/* v0.4 — Capacity ≥ 10 milestone (stacks on top of ≥ 8) */
|
|
1931
|
-
if (s.tracks.capacity.value >= 10) {
|
|
1932
|
-
const hasMfg = countOwnedSys(s, pIdx, 'manufactures') > 0;
|
|
1933
|
-
const hasStrat = countOwnedSys(s, pIdx, 'strategic-industry') > 0;
|
|
1934
|
-
let bonus = 0; if (hasMfg) bonus += 2; if (hasStrat) bonus += 2;
|
|
1935
|
-
if (bonus > 0) add('Capacity ≥ 10 milestone', 'owns Mfg ' + (hasMfg ? '✓' : '✕') + ' / Strategic ' + (hasStrat ? '✓' : '✕'), bonus);
|
|
1936
|
-
}
|
|
1937
|
-
if (p.keptHamilton) add('"You Are Hamilton" kept', 'card retained', 1);
|
|
1938
|
-
if (p.bankruptLaps > 0) add('Bankrupt laps', p.bankruptLaps + ' laps', -p.bankruptLaps);
|
|
1939
|
-
return { breakdown, total };
|
|
1940
|
-
}
|
|
1941
|
-
|
|
1942
|
-
/* =====================================================================
|
|
1943
|
-
RENDER
|
|
1944
|
-
===================================================================== */
|
|
1945
|
-
function el(tag, cls, html) { const e = document.createElement(tag); if (cls) e.className = cls; if (html !== undefined) e.innerHTML = html; return e; }
|
|
1946
|
-
function spaceCoord(num) {
|
|
1947
|
-
if (num === 0) return [10, 10];
|
|
1948
|
-
if (num >= 1 && num <= 9) return [10, 10 - num];
|
|
1949
|
-
if (num === 10) return [10, 0];
|
|
1950
|
-
if (num >= 11 && num <= 19) return [10 - (num - 10), 0];
|
|
1951
|
-
if (num === 20) return [0, 0];
|
|
1952
|
-
if (num >= 21 && num <= 29) return [0, num - 20];
|
|
1953
|
-
if (num === 30) return [0, 10];
|
|
1954
|
-
if (num >= 31 && num <= 39) return [num - 30, 10];
|
|
1955
|
-
}
|
|
1956
|
-
|
|
1957
|
-
function renderBoard() {
|
|
1958
|
-
const grid = document.getElementById('boardGrid');
|
|
1959
|
-
grid.innerHTML = '';
|
|
1960
|
-
for (let r = 0; r < 11; r++) for (let c = 0; c < 11; c++) {
|
|
1961
|
-
if (r === 0 || r === 10 || c === 0 || c === 10) {
|
|
1962
|
-
const space = SPACES.find(sp => { const [sr, sc] = spaceCoord(sp.num); return sr === r && sc === c; });
|
|
1963
|
-
if (!space) { const e = el('div', 'board-cell empty'); e.style.gridColumn = (c+1); e.style.gridRow = (r+1); grid.appendChild(e); continue; }
|
|
1964
|
-
const isCorner = space.kind.startsWith('corner');
|
|
1965
|
-
const cls = 'board-cell ' + (isCorner ? 'corner' : space.kind);
|
|
1966
|
-
const cell = el('div', cls);
|
|
1967
|
-
cell.dataset.num = space.num;
|
|
1968
|
-
cell.style.gridColumn = (c+1);
|
|
1969
|
-
cell.style.gridRow = (r+1);
|
|
1970
|
-
cell.innerHTML = `<div class="band"></div><div class="num">${String(space.num).padStart(2,'0')}</div><div class="nm">${space.name}</div><div class="tokens"></div>`;
|
|
1971
|
-
grid.appendChild(cell);
|
|
1972
|
-
}
|
|
1973
|
-
}
|
|
1974
|
-
const center = el('div', 'board-center');
|
|
1975
|
-
center.style.gridColumn = '2 / span 9';
|
|
1976
|
-
center.style.gridRow = '2 / span 9';
|
|
1977
|
-
center.innerHTML = `
|
|
1978
|
-
<div class="bc-eyebrow">Sovereign · Hamilton System</div>
|
|
1979
|
-
<div class="bc-title">Founding Credit</div>
|
|
1980
|
-
<div class="bc-sub">Fund the debt. Build the bank. Industrialize the republic.</div>
|
|
1981
|
-
<div class="bc-lap" id="bcLap">Lap 1 / 7</div>
|
|
1982
|
-
<div class="bc-tracks">
|
|
1983
|
-
<span class="t credit">Credit<span class="v" id="bcCredit">5</span></span>
|
|
1984
|
-
<span class="t resist">Resist<span class="v" id="bcResist">2</span></span>
|
|
1985
|
-
<span class="t indust">Indust<span class="v" id="bcIndust">1</span></span>
|
|
1986
|
-
</div>
|
|
1987
|
-
<div class="bc-acts" id="bcActs">Acts passed: —</div>
|
|
1988
|
-
<div class="bc-active" id="bcActive">Active: You</div>
|
|
1989
|
-
`;
|
|
1990
|
-
grid.appendChild(center);
|
|
1991
|
-
}
|
|
1992
|
-
|
|
1993
|
-
function applyOwnership() {
|
|
1994
|
-
document.querySelectorAll('.board-cell').forEach(cell => {
|
|
1995
|
-
cell.classList.remove('owned-p0','owned-p1','owned-p2','active-space');
|
|
1996
|
-
cell.querySelectorAll('.owner-dot, .tier').forEach(e => e.remove());
|
|
1997
|
-
cell.querySelectorAll('.tokens').forEach(e => e.innerHTML = '');
|
|
1998
|
-
});
|
|
1999
|
-
STATE.players.forEach((p, idx) => {
|
|
2000
|
-
p.ownedAssets.forEach(a => {
|
|
2001
|
-
const cell = document.querySelector('.board-cell[data-num="' + a.spaceNum + '"]');
|
|
2002
|
-
if (!cell) return;
|
|
2003
|
-
cell.classList.add('owned-p' + idx);
|
|
2004
|
-
cell.appendChild(el('div', 'owner-dot'));
|
|
2005
|
-
if (a.tier > 0) cell.appendChild(el('div', 'tier', 'I'.repeat(a.tier)));
|
|
2006
|
-
});
|
|
2007
|
-
});
|
|
2008
|
-
/* Tokens */
|
|
2009
|
-
STATE.players.forEach((p, idx) => {
|
|
2010
|
-
const cell = document.querySelector('.board-cell[data-num="' + p.position + '"]');
|
|
2011
|
-
if (!cell) return;
|
|
2012
|
-
const tw = cell.querySelector('.tokens');
|
|
2013
|
-
if (tw) { const t = el('div', 'tok p' + idx); tw.appendChild(t); }
|
|
2014
|
-
});
|
|
2015
|
-
/* Highlight active player's position */
|
|
2016
|
-
const cur = document.querySelector('.board-cell[data-num="' + STATE.players[STATE.activePlayerIndex].position + '"]');
|
|
2017
|
-
if (cur) cur.classList.add('active-space');
|
|
2018
|
-
document.getElementById('bcLap').textContent = 'Lap ' + Math.min(STATE.lap, 7) + ' / 7';
|
|
2019
|
-
document.getElementById('bcCredit').textContent = STATE.tracks.credit.value;
|
|
2020
|
-
document.getElementById('bcResist').textContent = STATE.tracks.resistance.value;
|
|
2021
|
-
document.getElementById('bcIndust').textContent = STATE.tracks.capacity.value;
|
|
2022
|
-
const passed = STATE.acts.passed.map(p => 'I'.repeat(ACTS.find(a=>a.id===p.actId).lap)).join(' · ');
|
|
2023
|
-
document.getElementById('bcActs').textContent = STATE.acts.passed.length === 0 ? 'Acts passed: —' : 'Acts passed: ' + passed;
|
|
2024
|
-
const aPlayer = STATE.players[STATE.activePlayerIndex];
|
|
2025
|
-
document.getElementById('bcActive').textContent = 'Active: ' + aPlayer.name + ' (' + (PROFILES[aPlayer.profile]?.label || aPlayer.role) + ')';
|
|
2026
|
-
}
|
|
2027
|
-
|
|
2028
|
-
function renderTreasury() {
|
|
2029
|
-
const p = STATE.players[0];
|
|
2030
|
-
document.getElementById('treasuryCash').textContent = '$' + p.cash.toLocaleString('en-US');
|
|
2031
|
-
document.getElementById('treasuryInf').textContent = 'Influence est. ' + scorePlayer(STATE, 0).total;
|
|
2032
|
-
const root = document.getElementById('treasuryHoldings');
|
|
2033
|
-
root.innerHTML = '';
|
|
2034
|
-
if (p.ownedAssets.length === 0) {
|
|
2035
|
-
root.innerHTML = '<p style="font-family:var(--display);font-style:italic;font-size:11px;opacity:.6;margin-top:6px">No assets owned yet.</p>';
|
|
2036
|
-
return;
|
|
2037
|
-
}
|
|
2038
|
-
p.ownedAssets.forEach(a => {
|
|
2039
|
-
const sp = SPACES[a.spaceNum];
|
|
2040
|
-
const asset = ASSETS[a.spaceNum];
|
|
2041
|
-
const tier = a.tier > 0 ? '<span class="tier-badge">T' + 'I'.repeat(a.tier) + '</span>' : '<span class="tier-badge muted">—</span>';
|
|
2042
|
-
const sw = sp.kind === 'route' ? '<span class="sw" style="background:var(--ink)"></span>'
|
|
2043
|
-
: sp.kind === 'institution' ? '<span class="sw" style="background:var(--ink)"></span>'
|
|
2044
|
-
: '<span class="sw" style="background:var(--' + asset.sys + ')"></span>';
|
|
2045
|
-
const row = el('div', 'h-row');
|
|
2046
|
-
row.innerHTML = sw + '<span>' + sp.name + '</span>' + tier;
|
|
2047
|
-
if (asset.up && ownsFullSet(STATE, 0, asset.sys) && a.tier < 3 && STATE.phase === 'awaiting-roll' && STATE.activePlayerIndex === 0) {
|
|
2048
|
-
const upBtn = el('button', 'up-btn', 'Upgrade $' + ((asset.sys === 'manufactures' || asset.sys === 'strategic-industry') && STATE.flags.manufacturesLap === STATE.lap ? Math.floor(asset.up/2) : asset.up));
|
|
2049
|
-
upBtn.onclick = () => dispatch({ type:'UPGRADE_ASSET', playerIndex: 0, spaceNum: a.spaceNum });
|
|
2050
|
-
row.appendChild(upBtn);
|
|
2051
|
-
}
|
|
2052
|
-
root.appendChild(row);
|
|
2053
|
-
});
|
|
2054
|
-
}
|
|
2055
|
-
|
|
2056
|
-
function renderOpponents() {
|
|
2057
|
-
const root = document.getElementById('opponentsBody');
|
|
2058
|
-
root.innerHTML = '';
|
|
2059
|
-
for (let i = 1; i < STATE.players.length; i++) {
|
|
2060
|
-
const p = STATE.players[i];
|
|
2061
|
-
const profile = PROFILES[p.profile];
|
|
2062
|
-
const active = STATE.activePlayerIndex === i;
|
|
2063
|
-
const card = el('div', 'opp-card p' + i + (active ? ' active-opp' : ''));
|
|
2064
|
-
const holdings = p.ownedAssets.map(a => SPACES[a.spaceNum].name + (a.tier > 0 ? ' (T'+'I'.repeat(a.tier)+')' : '')).join(', ') || '(none)';
|
|
2065
|
-
card.innerHTML = `
|
|
2066
|
-
<div class="head"><span class="nm">${p.name}</span><span class="pdot"></span></div>
|
|
2067
|
-
<div class="pl">${profile.label}</div>
|
|
2068
|
-
<div style="display:flex;justify-content:space-between;align-items:baseline;margin-top:3px"><span class="cash">$${p.cash.toLocaleString('en-US')}</span><span style="font-family:var(--mono);font-size:9px;opacity:.7">Inf est. ${scorePlayer(STATE, i).total}</span></div>
|
|
2069
|
-
<div class="holdings"><strong>Holds:</strong> ${holdings}</div>
|
|
2070
|
-
<div class="strategy">${profile.strategy}</div>`;
|
|
2071
|
-
root.appendChild(card);
|
|
2072
|
-
}
|
|
2073
|
-
}
|
|
2074
|
-
|
|
2075
|
-
function renderTracks() {
|
|
2076
|
-
const root = document.getElementById('tracksBody');
|
|
2077
|
-
root.innerHTML = '';
|
|
2078
|
-
const TRACKS = [
|
|
2079
|
-
{ key:'credit', label:'Public Credit', cls:'credit', thresholds:[{at:8,msg:'≥ 8 — NF owners split +5 Inf'}], danger:[{at:2,msg:'≤ 2 — routes pay half'},{at:0,msg:'= 0 — Default'}] },
|
|
2080
|
-
{ key:'resistance', label:'Public Resistance', cls:'resist', thresholds:[{at:8,msg:'≥ 8 — Whiskey Rebellion live'},{at:12,msg:'= 12 — Rebellion'}] },
|
|
2081
|
-
{ key:'capacity', label:'Industrial Capacity', cls:'indust', thresholds:[{at:6,msg:'≥ 6 — Mfg/Strategic +25 %'},{at:8,msg:'≥ 8 — +2 Inf endgame'}] },
|
|
2082
|
-
];
|
|
2083
|
-
TRACKS.forEach(t => {
|
|
2084
|
-
const v = STATE.tracks[t.key].value;
|
|
2085
|
-
const row = el('div', 't-row ' + t.cls);
|
|
2086
|
-
let html = '<div class="lbl-t">' + t.label + '</div><div class="val">' + v + '</div>';
|
|
2087
|
-
html += '<div class="scale">';
|
|
2088
|
-
for (let i = 0; i <= 12; i++) html += '<div class="tk' + (i%3===0?' major':'') + (i===v?' marker':'') + '">' + i + '</div>';
|
|
2089
|
-
html += '</div>';
|
|
2090
|
-
html += '<div class="reason">Reason: ' + STATE.tracks[t.key].lastReason + '</div>';
|
|
2091
|
-
t.thresholds.forEach(th => {
|
|
2092
|
-
const hit = v >= th.at;
|
|
2093
|
-
html += '<span class="' + (hit ? 'threshold-hit' : 'threshold-miss') + '">' + (hit ? '✓ ' : '○ ') + th.msg + '</span> ';
|
|
2094
|
-
});
|
|
2095
|
-
if (t.danger) t.danger.forEach(th => {
|
|
2096
|
-
const hit = v <= th.at;
|
|
2097
|
-
html += '<span class="' + (hit ? 'threshold-hit' : 'threshold-miss') + '" style="' + (hit ? 'background:var(--highlight)' : '') + '">' + (hit ? '⚠ ' : '○ ') + th.msg + '</span> ';
|
|
2098
|
-
});
|
|
2099
|
-
row.innerHTML = html;
|
|
2100
|
-
root.appendChild(row);
|
|
2101
|
-
});
|
|
2102
|
-
}
|
|
2103
|
-
|
|
2104
|
-
function renderActs() {
|
|
2105
|
-
const root = document.getElementById('actsBody');
|
|
2106
|
-
let html = '';
|
|
2107
|
-
if (STATE.acts.current) {
|
|
2108
|
-
const act = ACTS.find(a => a.id === STATE.acts.current.actId);
|
|
2109
|
-
html += `<div class="current-act"><div class="pretitle">Lap ${STATE.lap} · Federal Act</div><div class="nm">${act.name}</div><div class="effect">${act.summary}</div></div>`;
|
|
2110
|
-
html += '<div class="votes">';
|
|
2111
|
-
STATE.players.forEach((p, i) => {
|
|
2112
|
-
const v = STATE.acts.current.votes[i];
|
|
2113
|
-
const pending = v == null;
|
|
2114
|
-
let reason = '';
|
|
2115
|
-
if (!pending && p.profile !== 'human') {
|
|
2116
|
-
const decision = PROFILES[p.profile].decideVote(STATE, i, STATE.acts.current.actId);
|
|
2117
|
-
reason = decision.reason;
|
|
2118
|
-
} else if (!pending && p.profile === 'human') {
|
|
2119
|
-
reason = 'human ballot cast';
|
|
2120
|
-
} else if (pending && p.profile !== 'human') {
|
|
2121
|
-
reason = 'opponent vote pending · auto-decision';
|
|
2122
|
-
} else {
|
|
2123
|
-
reason = 'awaiting your ballot';
|
|
2124
|
-
}
|
|
2125
|
-
const ballot = v === 'yes' ? 'YES' : v === 'no' ? 'NO' : '?';
|
|
2126
|
-
const ballotCls = v === 'yes' ? 'yes' : v === 'no' ? 'no' : 'pending';
|
|
2127
|
-
html += `<div class="vote-row p${i} ${pending ? 'pending' : ''}">
|
|
2128
|
-
<span class="pdot"></span>
|
|
2129
|
-
<div><span class="who">${p.name}</span><span class="reason">${reason}</span></div>
|
|
2130
|
-
<span class="ballot ${ballotCls}">${ballot}</span>
|
|
2131
|
-
</div>`;
|
|
2132
|
-
});
|
|
2133
|
-
html += '</div>';
|
|
2134
|
-
if (STATE.acts.current.votes[0] == null && STATE.activePlayerIndex !== undefined) {
|
|
2135
|
-
html += '<div class="you-vote"><button class="primary" onclick="humanVote(\'yes\')">✓ Vote YES</button><button onclick="humanVote(\'no\')">✕ Vote NO</button></div>';
|
|
2136
|
-
}
|
|
2137
|
-
} else {
|
|
2138
|
-
html += '<p style="font-family:var(--display);font-style:italic;font-size:11px;opacity:.6">No Act currently in vote.</p>';
|
|
2139
|
-
}
|
|
2140
|
-
html += '<div class="passed-tray"><div class="lbl">Passed Acts — face-up tray</div><div class="tray-row">';
|
|
2141
|
-
if (STATE.acts.passed.length === 0) html += '<span style="font-family:var(--mono);font-size:9px;opacity:.5">(none yet)</span>';
|
|
2142
|
-
else STATE.acts.passed.forEach(p => { const a = ACTS.find(x => x.id === p.actId); html += `<div class="slot"><span class="roman">${a.roman}</span>${a.name}</div>`; });
|
|
2143
|
-
html += '</div></div>';
|
|
2144
|
-
root.innerHTML = html;
|
|
2145
|
-
}
|
|
2146
|
-
|
|
2147
|
-
function humanVote(v) { dispatch({ type:'CAST_VOTE', playerIndex: 0, vote: v, reason: 'human ballot' }); }
|
|
2148
|
-
|
|
2149
|
-
function renderInspector() {
|
|
2150
|
-
if (STATE.phase !== 'asset-decision' || !STATE.pendingLanding || STATE.pendingLanding.playerIndex !== 0) {
|
|
2151
|
-
document.getElementById('panelInspector').classList.add('hidden');
|
|
2152
|
-
return;
|
|
2153
|
-
}
|
|
2154
|
-
document.getElementById('panelInspector').classList.remove('hidden');
|
|
2155
|
-
const num = STATE.pendingLanding.spaceNum;
|
|
2156
|
-
const sp = SPACES[num];
|
|
2157
|
-
const asset = ASSETS[num];
|
|
2158
|
-
const root = document.getElementById('inspectorBody');
|
|
2159
|
-
const p = STATE.players[0];
|
|
2160
|
-
const buyDisabled = p.cash < asset.cost ? 'disabled' : '';
|
|
2161
|
-
|
|
2162
|
-
if (sp.kind === 'route') {
|
|
2163
|
-
root.innerHTML = `
|
|
2164
|
-
<div class="card-art"><div class="band" style="background:var(--ink);background-image:repeating-linear-gradient(90deg,transparent 0 4px,rgba(255,255,255,.5) 4px 5px)"><span>Route</span><span>${String(num).padStart(2,'0')}</span></div>
|
|
2165
|
-
<div class="nm">${sp.name}</div><div class="subkind">Infrastructure Corridor</div>
|
|
2166
|
-
<div class="flavor">Routes pay 25 / 50 / 100 / 150 TN by routes owned.</div></div>
|
|
2167
|
-
<p class="meta-row">Cost: $${asset.cost}</p>
|
|
2168
|
-
<div class="actions"><button class="primary" ${buyDisabled} onclick="dispatch({type:'BUY_ASSET'})">Buy $${asset.cost}</button><button onclick="dispatch({type:'DECLINE_ASSET'})">Decline → Auction</button></div>`;
|
|
2169
|
-
return;
|
|
2170
|
-
}
|
|
2171
|
-
if (sp.kind === 'institution') {
|
|
2172
|
-
const rules = sp.instKind === 'bank' ? 'Before Charter: 4 × dice. After: 10 × dice.' : 'Mint alone: no payment. Both: 10 × dice. Both + Charter: 20 × dice.';
|
|
2173
|
-
root.innerHTML = `
|
|
2174
|
-
<div class="card-art"><div class="band" style="background:var(--ink);background-image:radial-gradient(rgba(255,255,255,.5) 1px,transparent 1.5px);background-size:6px 6px"><span>Institution</span><span>${String(num).padStart(2,'0')}</span></div>
|
|
2175
|
-
<div class="nm">${sp.name}</div><div class="subkind">Federal Institution</div>
|
|
2176
|
-
<div class="flavor">${rules}</div></div>
|
|
2177
|
-
<p class="meta-row">Cost: $${asset.cost}</p>
|
|
2178
|
-
<div class="actions"><button class="primary" ${buyDisabled} onclick="dispatch({type:'BUY_ASSET'})">Buy $${asset.cost}</button><button onclick="dispatch({type:'DECLINE_ASSET'})">Decline → Auction</button></div>`;
|
|
2179
|
-
return;
|
|
2180
|
-
}
|
|
2181
|
-
const b = asset.base;
|
|
2182
|
-
root.innerHTML = `
|
|
2183
|
-
<div class="card-art"><div class="band" style="background:var(--${asset.sys})"><span>${SYS_LABEL[asset.sys]}</span><span>${String(num).padStart(2,'0')}</span></div>
|
|
2184
|
-
<div class="nm">${sp.name}</div><div class="subkind">${asset.subkind}</div></div>
|
|
2185
|
-
<table class="pay-table"><thead><tr><th>Tier</th><th>×</th><th>Pay</th></tr></thead><tbody>
|
|
2186
|
-
<tr><td>Base</td><td>—</td><td class="val">$${b}</td></tr>
|
|
2187
|
-
<tr><td>Full Set</td><td>×2</td><td class="val">$${b*2}</td></tr>
|
|
2188
|
-
<tr><td>Tier I</td><td>×5</td><td class="val">$${b*5}</td></tr>
|
|
2189
|
-
<tr><td>Tier II</td><td>×15</td><td class="val">$${b*15}</td></tr>
|
|
2190
|
-
<tr><td>Tier III</td><td>×30</td><td class="val">$${b*30}</td></tr>
|
|
2191
|
-
</tbody></table>
|
|
2192
|
-
<p class="meta-row">Cost: $${asset.cost} · Upgrade: $${asset.up} · Mortgage: $${asset.mort}</p>
|
|
2193
|
-
<div class="actions"><button class="primary" ${buyDisabled} onclick="dispatch({type:'BUY_ASSET'})">Buy $${asset.cost}</button><button onclick="dispatch({type:'DECLINE_ASSET'})">Decline → Auction</button></div>`;
|
|
2194
|
-
}
|
|
2195
|
-
|
|
2196
|
-
function renderCardDrawer() {
|
|
2197
|
-
if (STATE.phase !== 'card-resolve' && STATE.phase !== 'card-choice') {
|
|
2198
|
-
document.getElementById('panelCardDrawer').classList.add('hidden');
|
|
2199
|
-
return;
|
|
2200
|
-
}
|
|
2201
|
-
document.getElementById('panelCardDrawer').classList.remove('hidden');
|
|
2202
|
-
const { deck, cardId, playerIndex } = STATE.pendingCard;
|
|
2203
|
-
const cards = deck === 'market' ? MARKET_SHOCK_CARDS : REPUBLIC_DEBATE_CARDS;
|
|
2204
|
-
const card = cards.find(c => c.id === cardId);
|
|
2205
|
-
const root = document.getElementById('cardDrawerBody');
|
|
2206
|
-
const bandColor = deck === 'market' ? 'var(--highlight)' : 'var(--national-finance)';
|
|
2207
|
-
const deckLabel = deck === 'market' ? 'Market Shock' : 'Republic Debate';
|
|
2208
|
-
const drawnBy = STATE.players[playerIndex].name;
|
|
2209
|
-
let chips = '';
|
|
2210
|
-
if (card.chips) {
|
|
2211
|
-
if (card.chips.credit != null) chips += '<span class="chip credit">Credit ' + (card.chips.credit > 0 ? '+' : '') + card.chips.credit + '</span>';
|
|
2212
|
-
if (card.chips.resist != null) chips += '<span class="chip resist">Resist ' + (card.chips.resist > 0 ? '+' : '') + card.chips.resist + '</span>';
|
|
2213
|
-
if (card.chips.indust != null) chips += '<span class="chip indust">Indust ' + (card.chips.indust > 0 ? '+' : '') + card.chips.indust + '</span>';
|
|
2214
|
-
}
|
|
2215
|
-
const isHuman = playerIndex === 0;
|
|
2216
|
-
let actions = '';
|
|
2217
|
-
if (isHuman) {
|
|
2218
|
-
if (card.choices) actions = card.choices.map((c, i) => `<button class="primary" onclick="dispatch({type:'RESOLVE_CARD_CHOICE', choiceIndex: ${i}})">${c.label}</button>`).join('');
|
|
2219
|
-
else actions = `<button class="primary" onclick="dispatch({type:'RESOLVE_CARD'})">Resolve effect</button>`;
|
|
2220
|
-
} else {
|
|
2221
|
-
actions = `<em style="font-family:var(--display);font-size:11px;opacity:.7">Opponent resolving automatically…</em>`;
|
|
2222
|
-
}
|
|
2223
|
-
root.innerHTML = `
|
|
2224
|
-
<div class="drawer-card">
|
|
2225
|
-
<div class="band" style="background:${bandColor}"><span>${deckLabel}</span><span>№ ${String(card.id).padStart(2,'0')}</span></div>
|
|
2226
|
-
<div style="font-family:var(--ui);font-size:8.5px;letter-spacing:.18em;text-transform:uppercase;text-align:center;margin-top:4px;opacity:.7">Drawn by ${drawnBy}</div>
|
|
2227
|
-
<div class="nm">${card.name}</div>
|
|
2228
|
-
<div class="effect">${card.effectText}</div>
|
|
2229
|
-
<div class="outcomes">${chips}</div>
|
|
2230
|
-
<div class="actions">${actions}</div>
|
|
2231
|
-
</div>`;
|
|
2232
|
-
}
|
|
2233
|
-
|
|
2234
|
-
function renderAuction() {
|
|
2235
|
-
if (STATE.phase !== 'auction' || !STATE.pendingAuction) {
|
|
2236
|
-
document.getElementById('panelAuction').classList.add('hidden');
|
|
2237
|
-
return;
|
|
2238
|
-
}
|
|
2239
|
-
document.getElementById('panelAuction').classList.remove('hidden');
|
|
2240
|
-
const a = STATE.pendingAuction;
|
|
2241
|
-
const sp = SPACES[a.spaceNum];
|
|
2242
|
-
const asset = ASSETS[a.spaceNum];
|
|
2243
|
-
const root = document.getElementById('auctionBody');
|
|
2244
|
-
let bidRows = '';
|
|
2245
|
-
a.bids.forEach(b => {
|
|
2246
|
-
bidRows += `<div class="bid-row p${b.playerIndex}"><span class="pdot"></span><span class="who">${STATE.players[b.playerIndex].name}</span><span class="amt">${b.amount === 0 ? 'PASS' : '$' + b.amount}</span><span class="reason">${b.reason}</span></div>`;
|
|
2247
|
-
});
|
|
2248
|
-
const myTurn = a.bidsRemaining.length > 0 && a.bidsRemaining[0] === 0;
|
|
2249
|
-
const me = STATE.players[0];
|
|
2250
|
-
let humanForm = '';
|
|
2251
|
-
if (myTurn) {
|
|
2252
|
-
const minBid = a.highBid + 10;
|
|
2253
|
-
humanForm = `
|
|
2254
|
-
<div class="actions">
|
|
2255
|
-
<input type="number" id="humanBid" min="10" max="${me.cash}" step="10" placeholder="${minBid}" />
|
|
2256
|
-
<button class="primary" onclick="submitHumanBid()">Place bid</button>
|
|
2257
|
-
<button onclick="submitHumanPass()">Pass</button>
|
|
2258
|
-
</div>
|
|
2259
|
-
<p style="font-family:var(--mono);font-size:9px;opacity:.7;margin-top:4px">min next bid: $${minBid} · your cash: $${me.cash.toLocaleString('en-US')}</p>`;
|
|
2260
|
-
} else if (a.bidsRemaining.length > 0) {
|
|
2261
|
-
humanForm = `<em style="font-family:var(--display);font-size:11px;opacity:.7">Waiting on ${STATE.players[a.bidsRemaining[0]].name}…</em>`;
|
|
2262
|
-
}
|
|
2263
|
-
root.innerHTML = `
|
|
2264
|
-
<div class="auction-card">
|
|
2265
|
-
<div class="band" style="background:var(--internal-improvements)"><span>Auction</span><span>${String(a.spaceNum).padStart(2,'0')}</span></div>
|
|
2266
|
-
<div class="nm">${sp.name}</div>
|
|
2267
|
-
<div style="font-family:var(--display);font-style:italic;font-size:10px;margin-top:4px">List cost $${asset.cost} · auction triggered by ${STATE.players[a.declinerIdx].name}</div>
|
|
2268
|
-
</div>
|
|
2269
|
-
<div class="bid-list">${bidRows || '<em style="font-family:var(--display);font-size:11px;opacity:.7">No bids yet.</em>'}</div>
|
|
2270
|
-
<div class="high-bid">High bid: ${a.highBid > 0 ? '$' + a.highBid + ' (' + STATE.players[a.highBidder].name + ')' : '— no bids yet —'}</div>
|
|
2271
|
-
${humanForm}`;
|
|
2272
|
-
}
|
|
2273
|
-
|
|
2274
|
-
function submitHumanBid() {
|
|
2275
|
-
const input = document.getElementById('humanBid');
|
|
2276
|
-
const amount = parseInt(input.value, 10);
|
|
2277
|
-
if (isNaN(amount) || amount < 10 || amount > STATE.players[0].cash) { alert('Enter a bid between $10 and your cash on hand.'); return; }
|
|
2278
|
-
if (amount <= STATE.pendingAuction.highBid) { alert('Bid must exceed current high bid of $' + STATE.pendingAuction.highBid); return; }
|
|
2279
|
-
dispatch({ type:'AUCTION_BID', playerIndex: 0, amount, reason: 'human bid' });
|
|
2280
|
-
}
|
|
2281
|
-
function submitHumanPass() { dispatch({ type:'AUCTION_BID', playerIndex: 0, amount: 0, reason: 'human pass' }); }
|
|
2282
|
-
|
|
2283
|
-
function renderLedger() {
|
|
2284
|
-
const root = document.getElementById('ledgerRows');
|
|
2285
|
-
root.innerHTML = '';
|
|
2286
|
-
STATE.ledger.slice().reverse().forEach((row, i) => {
|
|
2287
|
-
const stamp = 'L' + row.lap + '·T' + row.turn + '·#' + (STATE.ledger.length - i);
|
|
2288
|
-
const r = el('div', 'row ' + (row.cls || ''));
|
|
2289
|
-
r.innerHTML = `<span class="stamp">${stamp}</span><span class="actor">${row.actor} · ${row.event}</span><span>${row.detail}</span>`;
|
|
2290
|
-
root.appendChild(r);
|
|
2291
|
-
});
|
|
2292
|
-
document.getElementById('ledgerCount').textContent = STATE.ledger.length + ' entries';
|
|
2293
|
-
}
|
|
2294
|
-
|
|
2295
|
-
function renderControls() {
|
|
2296
|
-
const bar = document.getElementById('controlsBar');
|
|
2297
|
-
bar.innerHTML = '';
|
|
2298
|
-
const isHumanTurn = STATE.activePlayerIndex === 0;
|
|
2299
|
-
if (STATE.phase === 'awaiting-roll' && isHumanTurn) {
|
|
2300
|
-
const roll = el('button', 'primary', '🎲 Roll dice');
|
|
2301
|
-
roll.onclick = () => dispatch({ type:'ROLL_DICE' });
|
|
2302
|
-
bar.appendChild(roll);
|
|
2303
|
-
} else if (STATE.phase === 'crisis-choice' && isHumanTurn) {
|
|
2304
|
-
const lbl = el('div', '', '<em style="font-family:var(--display);font-size:11px;margin-right:8px">In Crisis. Choose:</em>');
|
|
2305
|
-
bar.appendChild(lbl);
|
|
2306
|
-
const pay = el('button', 'primary', 'Pay 50 TN');
|
|
2307
|
-
pay.onclick = () => dispatch({ type:'ROLL_DICE', crisisChoice: 'pay' });
|
|
2308
|
-
bar.appendChild(pay);
|
|
2309
|
-
const dbl = el('button', '', 'Roll for doubles');
|
|
2310
|
-
dbl.onclick = () => dispatch({ type:'ROLL_DICE', crisisChoice: 'doubles' });
|
|
2311
|
-
bar.appendChild(dbl);
|
|
2312
|
-
const skp = el('button', '', 'Skip turn');
|
|
2313
|
-
skp.onclick = () => dispatch({ type:'ROLL_DICE', crisisChoice: 'skip' });
|
|
2314
|
-
bar.appendChild(skp);
|
|
2315
|
-
} else if (STATE.phase === 'awaiting-roll' && !isHumanTurn) {
|
|
2316
|
-
const hint = el('div', '', `<em style="font-family:var(--display);font-size:11px">${STATE.players[STATE.activePlayerIndex].name} is taking their turn…</em>`);
|
|
2317
|
-
bar.appendChild(hint);
|
|
2318
|
-
} else if (STATE.phase === 'asset-decision' && !isHumanTurn) {
|
|
2319
|
-
const hint = el('div', '', `<em style="font-family:var(--display);font-size:11px">${STATE.players[STATE.activePlayerIndex].name} is deciding buy / decline…</em>`);
|
|
2320
|
-
bar.appendChild(hint);
|
|
2321
|
-
/* Auto-resolve opponent decision */
|
|
2322
|
-
setTimeout(() => {
|
|
2323
|
-
if (STATE.phase !== 'asset-decision' || STATE.activePlayerIndex === 0) return;
|
|
2324
|
-
const pIdx = STATE.activePlayerIndex;
|
|
2325
|
-
const num = STATE.pendingLanding.spaceNum;
|
|
2326
|
-
const decision = PROFILES[STATE.players[pIdx].profile].decideBuy(STATE, pIdx, num);
|
|
2327
|
-
if (decision.buy) dispatch({ type:'BUY_ASSET', reason: decision.reason });
|
|
2328
|
-
else dispatch({ type:'DECLINE_ASSET', reason: decision.reason });
|
|
2329
|
-
}, 250);
|
|
2330
|
-
}
|
|
2331
|
-
document.getElementById('phasePill').textContent = 'phase: ' + STATE.phase;
|
|
2332
|
-
document.getElementById('seedPill').textContent = 'seed: ' + STATE.rngSeed;
|
|
2333
|
-
const ap = STATE.players[STATE.activePlayerIndex];
|
|
2334
|
-
const pill = document.getElementById('activePill');
|
|
2335
|
-
pill.textContent = 'Active: ' + ap.name;
|
|
2336
|
-
pill.className = 'active-pill' + (STATE.activePlayerIndex === 1 ? ' opp1' : STATE.activePlayerIndex === 2 ? ' opp2' : '');
|
|
2337
|
-
}
|
|
2338
|
-
|
|
2339
|
-
function renderEndgame() {
|
|
2340
|
-
if (STATE.phase !== 'game-over') {
|
|
2341
|
-
document.getElementById('endgameView').classList.add('hidden');
|
|
2342
|
-
document.getElementById('mainGrid').classList.remove('hidden');
|
|
2343
|
-
return;
|
|
2344
|
-
}
|
|
2345
|
-
document.getElementById('mainGrid').classList.add('hidden');
|
|
2346
|
-
const root = document.getElementById('endgameView');
|
|
2347
|
-
root.classList.remove('hidden');
|
|
2348
|
-
const winner = STATE.players.reduce((m, p, i) => STATE.finalScores[i].total > STATE.finalScores[m.idx].total ? { idx: i, total: STATE.finalScores[i].total } : m, { idx: 0, total: STATE.finalScores[0].total });
|
|
2349
|
-
let html = `
|
|
2350
|
-
<h2>Endgame Report <span class="surface-id">H</span></h2>
|
|
2351
|
-
<div class="sub">After lap 7. Three players. Influence breakdown computed from rules.</div>
|
|
2352
|
-
<div class="winner">
|
|
2353
|
-
<div class="lbl">Winner</div>
|
|
2354
|
-
<div class="nm">${STATE.players[winner.idx].name} · ${PROFILES[STATE.players[winner.idx].profile]?.label || STATE.players[winner.idx].role}</div>
|
|
2355
|
-
<div class="score">Influence ${winner.total} · cash $${STATE.players[winner.idx].cash.toLocaleString('en-US')} · ${STATE.players[winner.idx].ownedAssets.length} assets</div>
|
|
2356
|
-
</div>
|
|
2357
|
-
<div class="results">`;
|
|
2358
|
-
STATE.players.forEach((p, i) => {
|
|
2359
|
-
const score = STATE.finalScores[i];
|
|
2360
|
-
html += `<div class="pcol"><div class="pl">${PROFILES[p.profile]?.label || p.role}</div><h3>${p.name}</h3><table><tbody>`;
|
|
2361
|
-
score.breakdown.forEach(r => { html += `<tr><td>${r.label}</td><td class="n">${r.pts >= 0 ? '+' : ''}${r.pts}</td></tr>`; });
|
|
2362
|
-
html += `</tbody></table><div class="total">${score.total}</div></div>`;
|
|
2363
|
-
});
|
|
2364
|
-
html += `</div>
|
|
2365
|
-
<div class="actions"><button class="primary" onclick="startNewGame(STATE.rngSeed)">Replay same seed</button><button onclick="startNewGame(Math.floor(Math.random()*1e9))">New seed</button></div>`;
|
|
2366
|
-
root.innerHTML = html;
|
|
2367
|
-
}
|
|
2368
|
-
|
|
2369
|
-
function render() {
|
|
2370
|
-
applyOwnership();
|
|
2371
|
-
renderTreasury();
|
|
2372
|
-
renderOpponents();
|
|
2373
|
-
renderTracks();
|
|
2374
|
-
renderActs();
|
|
2375
|
-
renderInspector();
|
|
2376
|
-
renderCardDrawer();
|
|
2377
|
-
renderAuction();
|
|
2378
|
-
renderLedger();
|
|
2379
|
-
renderControls();
|
|
2380
|
-
renderEndgame();
|
|
2381
|
-
}
|
|
2382
|
-
|
|
2383
|
-
function startNewGame(seed) {
|
|
2384
|
-
STATE = initialState(seed);
|
|
2385
|
-
DECISION_LOG = [];
|
|
2386
|
-
TURN_SNAPSHOTS = [];
|
|
2387
|
-
NARRATION_LOG = [];
|
|
2388
|
-
hideResumePill();
|
|
2389
|
-
renderBoard();
|
|
2390
|
-
render();
|
|
2391
|
-
dispatch({ type:'BEGIN_LAP' });
|
|
2392
|
-
}
|
|
2393
|
-
|
|
2394
|
-
document.getElementById('btnNewSeed').addEventListener('click', () => startNewGame(Math.floor(Math.random() * 1e9)));
|
|
2395
|
-
document.getElementById('btnReset').addEventListener('click', () => startNewGame(STATE.rngSeed));
|
|
2396
|
-
document.addEventListener('keydown', (e) => {
|
|
2397
|
-
if (e.key === ' ' && STATE && STATE.phase === 'awaiting-roll' && STATE.activePlayerIndex === 0) { e.preventDefault(); dispatch({ type:'ROLL_DICE' }); }
|
|
2398
|
-
});
|
|
2399
|
-
|
|
2400
|
-
/* =====================================================================
|
|
2401
|
-
PHASE 5 — narration detection, render, save/load, replay
|
|
2402
|
-
No reducer changes. Reads the ledger after each dispatch.
|
|
2403
|
-
===================================================================== */
|
|
2404
|
-
function captureSnapshot() {
|
|
2405
|
-
TURN_SNAPSHOTS.push({
|
|
2406
|
-
decisionLogLen: DECISION_LOG.length,
|
|
2407
|
-
turn: STATE.turnIndex,
|
|
2408
|
-
lap: STATE.lap,
|
|
2409
|
-
ledgerLen: STATE.ledger.length,
|
|
2410
|
-
cashes: STATE.players.map(p => p.cash),
|
|
2411
|
-
positions: STATE.players.map(p => p.position),
|
|
2412
|
-
credit: STATE.tracks.credit.value,
|
|
2413
|
-
resistance: STATE.tracks.resistance.value,
|
|
2414
|
-
capacity: STATE.tracks.capacity.value,
|
|
2415
|
-
actsPassed: STATE.acts.passed.map(a => a.actId),
|
|
2416
|
-
});
|
|
2417
|
-
}
|
|
2418
|
-
|
|
2419
|
-
function snapshotHash(s) {
|
|
2420
|
-
return [
|
|
2421
|
-
s.turnIndex, s.lap,
|
|
2422
|
-
s.players.map(p => p.cash + ':' + p.position + ':' + p.ownedAssets.length + ':' + p.influencePoints).join('|'),
|
|
2423
|
-
s.tracks.credit.value + ',' + s.tracks.resistance.value + ',' + s.tracks.capacity.value,
|
|
2424
|
-
s.acts.passed.map(a => a.actId).join('-'),
|
|
2425
|
-
s.ledger.length,
|
|
2426
|
-
].join(' · ');
|
|
2427
|
-
}
|
|
2428
|
-
|
|
2429
|
-
/* ---- Narration detection ---- */
|
|
2430
|
-
const NARRATION_FIRED = new Set();
|
|
2431
|
-
const SYS_FIRST_OWNED = new Set();
|
|
2432
|
-
let LAST_TRACK = { credit: 5, resistance: 2, capacity: 1 };
|
|
2433
|
-
let LAST_ACTS_PASSED = 0;
|
|
2434
|
-
|
|
2435
|
-
function resetNarrationDetector() {
|
|
2436
|
-
NARRATION_FIRED.clear();
|
|
2437
|
-
SYS_FIRST_OWNED.clear();
|
|
2438
|
-
LAST_TRACK = { credit: 5, resistance: 2, capacity: 1 };
|
|
2439
|
-
LAST_ACTS_PASSED = 0;
|
|
2440
|
-
}
|
|
2441
|
-
|
|
2442
|
-
function fireNarration(key) {
|
|
2443
|
-
if (NARRATION_FIRED.has(key)) return;
|
|
2444
|
-
if (!NARRATION[key]) return;
|
|
2445
|
-
NARRATION_FIRED.add(key);
|
|
2446
|
-
NARRATION_LOG.push({ key, turn: STATE.turnIndex, lap: STATE.lap, ts: Date.now() });
|
|
2447
|
-
}
|
|
2448
|
-
|
|
2449
|
-
function detectNarration() {
|
|
2450
|
-
/* First-purchase per system */
|
|
2451
|
-
for (let i = 0; i < STATE.players.length; i++) {
|
|
2452
|
-
for (const a of STATE.players[i].ownedAssets) {
|
|
2453
|
-
const asset = ASSETS[a.spaceNum];
|
|
2454
|
-
if (asset && asset.sys && !SYS_FIRST_OWNED.has(asset.sys)) {
|
|
2455
|
-
SYS_FIRST_OWNED.add(asset.sys);
|
|
2456
|
-
fireNarration('first-purchase · ' + asset.sys);
|
|
2457
|
-
}
|
|
2458
|
-
}
|
|
2459
|
-
}
|
|
2460
|
-
/* Acts passed (compare count) */
|
|
2461
|
-
if (STATE.acts.passed.length > LAST_ACTS_PASSED) {
|
|
2462
|
-
for (let i = LAST_ACTS_PASSED; i < STATE.acts.passed.length; i++) {
|
|
2463
|
-
const id = STATE.acts.passed[i].actId;
|
|
2464
|
-
const keyMap = { 1:'funding', 2:'assumption', 3:'bank-charter', 4:'tariff', 5:'coinage', 6:'manufactures-report', 7:'excise-enforcement' };
|
|
2465
|
-
if (keyMap[id]) fireNarration('act-passed · ' + keyMap[id]);
|
|
2466
|
-
}
|
|
2467
|
-
LAST_ACTS_PASSED = STATE.acts.passed.length;
|
|
2468
|
-
}
|
|
2469
|
-
/* Track thresholds (cross-detection) */
|
|
2470
|
-
const c = STATE.tracks.credit.value;
|
|
2471
|
-
const r = STATE.tracks.resistance.value;
|
|
2472
|
-
const ic = STATE.tracks.capacity.value;
|
|
2473
|
-
if (c <= 2 && LAST_TRACK.credit > 2) fireNarration('track-threshold · credit-low');
|
|
2474
|
-
if (c === 0 && LAST_TRACK.credit > 0) fireNarration('track-threshold · credit-default');
|
|
2475
|
-
if (r >= 8 && LAST_TRACK.resistance < 8) fireNarration('track-threshold · resistance-rebellion-live');
|
|
2476
|
-
if (r === 12 && LAST_TRACK.resistance < 12) fireNarration('track-threshold · resistance-rebellion-fires');
|
|
2477
|
-
if (ic >= 6 && LAST_TRACK.capacity < 6) fireNarration('track-threshold · capacity-payment-bonus');
|
|
2478
|
-
if (ic >= 8 && LAST_TRACK.capacity < 8) fireNarration('track-threshold · capacity-influence-bonus');
|
|
2479
|
-
LAST_TRACK = { credit: c, resistance: r, capacity: ic };
|
|
2480
|
-
/* Endgame */
|
|
2481
|
-
if (STATE.status === 'gameOver' && !NARRATION_FIRED.has('endgame · republic-summary')) {
|
|
2482
|
-
const winner = STATE.players.reduce((m, p, i) => STATE.finalScores[i].total > STATE.finalScores[m.idx].total ? { idx: i, total: STATE.finalScores[i].total } : m, { idx: 0, total: STATE.finalScores[0].total });
|
|
2483
|
-
if (winner.idx === 0) fireNarration('endgame · winner-human-line');
|
|
2484
|
-
else if (STATE.players[winner.idx].profile === 'treasury-finance') fireNarration('endgame · winner-treasury-line');
|
|
2485
|
-
else if (STATE.players[winner.idx].profile === 'merchant-infrastructure') fireNarration('endgame · winner-merchant-line');
|
|
2486
|
-
fireNarration('endgame · republic-summary');
|
|
2487
|
-
}
|
|
2488
|
-
}
|
|
2489
|
-
|
|
2490
|
-
function renderNarrationPanel() {
|
|
2491
|
-
const root = document.getElementById('narrationBody');
|
|
2492
|
-
const countEl = document.getElementById('narrCount');
|
|
2493
|
-
if (NARRATION_LOG.length === 0) {
|
|
2494
|
-
root.innerHTML = '<p class="empty">No historical narration yet — snippets will surface as Acts pass, properties are first acquired, and track thresholds are crossed.</p>';
|
|
2495
|
-
countEl.textContent = '0 entries';
|
|
2496
|
-
return;
|
|
2497
|
-
}
|
|
2498
|
-
if (!NARRATION_ENABLED) {
|
|
2499
|
-
root.innerHTML = '<p class="empty">Drawer dismissed. ' + NARRATION_LOG.length + ' narration ' + (NARRATION_LOG.length === 1 ? 'entry' : 'entries') + ' logged. Click “Drawer on” above to re-open.</p>';
|
|
2500
|
-
countEl.textContent = NARRATION_LOG.length + ' entries';
|
|
2501
|
-
return;
|
|
2502
|
-
}
|
|
2503
|
-
countEl.textContent = NARRATION_LOG.length + ' entries';
|
|
2504
|
-
const recent = NARRATION_LOG.slice().reverse().filter(item => item.key !== 'endgame · republic-summary').slice(0, 8);
|
|
2505
|
-
root.innerHTML = recent.map(item => {
|
|
2506
|
-
const n = NARRATION[item.key];
|
|
2507
|
-
if (!n) return '';
|
|
2508
|
-
const stamp = 'L' + item.lap + ' · T' + item.turn;
|
|
2509
|
-
const hasExpand = n.x && n.x.length > 0;
|
|
2510
|
-
return '<div class="narration-entry" data-key="' + item.key + '">' +
|
|
2511
|
-
'<div class="trig">' + item.key + '</div>' +
|
|
2512
|
-
'<h4>' + n.h + '</h4>' +
|
|
2513
|
-
'<div class="snip">' + n.d + '</div>' +
|
|
2514
|
-
'<div class="meta"><span class="stamp">' + stamp + '</span>' +
|
|
2515
|
-
(hasExpand ? '<button class="expand-btn" onclick="toggleNarration(this)">Expand</button>' : '<span style="font-family:var(--mono);font-size:8.5px;opacity:.5">no expand</span>') +
|
|
2516
|
-
'</div>' +
|
|
2517
|
-
(hasExpand ? '<div class="long">' + n.x + '</div>' : '') +
|
|
2518
|
-
'</div>';
|
|
2519
|
-
}).join('');
|
|
2520
|
-
}
|
|
2521
|
-
|
|
2522
|
-
function toggleNarration(btn) {
|
|
2523
|
-
const entry = btn.closest('.narration-entry');
|
|
2524
|
-
const open = entry.classList.toggle('expanded');
|
|
2525
|
-
btn.textContent = open ? 'Collapse' : 'Expand';
|
|
2526
|
-
}
|
|
2527
|
-
|
|
2528
|
-
/* ---- Endgame republic summary (300–500 words, scoped to player) ---- */
|
|
2529
|
-
function buildRepublicSummary(state) {
|
|
2530
|
-
const p = state.players[0];
|
|
2531
|
-
const score = state.finalScores[0];
|
|
2532
|
-
const winnerIdx = state.players.reduce((mi, _, i) => state.finalScores[i].total > state.finalScores[mi].total ? i : mi, 0);
|
|
2533
|
-
const won = winnerIdx === 0;
|
|
2534
|
-
|
|
2535
|
-
const passedActs = state.acts.passed.map(a => ACTS.find(x => x.id === a.actId)).filter(Boolean);
|
|
2536
|
-
const failedAct = state.flags.lastFailedActId ? ACTS.find(x => x.id === state.flags.lastFailedActId) : null;
|
|
2537
|
-
|
|
2538
|
-
const completedSets = Object.keys(SYS_LABEL).filter(sys => ownsFullSet(state, 0, sys));
|
|
2539
|
-
const ownedSys = {};
|
|
2540
|
-
p.ownedAssets.forEach(a => { const sys = ASSETS[a.spaceNum]?.sys; if (sys) ownedSys[sys] = (ownedSys[sys] || 0) + 1; });
|
|
2541
|
-
const upgraded = p.ownedAssets.filter(a => a.tier > 0).length;
|
|
2542
|
-
const routes = countOwnedRoutesByIdx(state, 0);
|
|
2543
|
-
const hasBank = ownsInstitution(state, 0, 'bank');
|
|
2544
|
-
const hasMint = ownsInstitution(state, 0, 'mint');
|
|
2545
|
-
|
|
2546
|
-
const c = state.tracks.credit.value;
|
|
2547
|
-
const r = state.tracks.resistance.value;
|
|
2548
|
-
const ic = state.tracks.capacity.value;
|
|
2549
|
-
|
|
2550
|
-
const paras = [];
|
|
2551
|
-
/* Para 1 — verdict + headline numbers */
|
|
2552
|
-
let p1 = 'After lap 7 the republic’s books close. ';
|
|
2553
|
-
if (won) {
|
|
2554
|
-
p1 += 'You hold the largest Influence position, with ' + score.total + ' points against ' + state.finalScores[1].total + ' for Hamilton and ' + state.finalScores[2].total + ' for Morris. ';
|
|
2555
|
-
} else {
|
|
2556
|
-
p1 += 'The largest Influence position belongs to ' + state.players[winnerIdx].name + ' with ' + state.finalScores[winnerIdx].total + ' points. You finished with ' + score.total + '. ';
|
|
2557
|
-
}
|
|
2558
|
-
p1 += 'Cash on hand: $' + p.cash.toLocaleString('en-US') + '. Properties owned: ' + p.ownedAssets.length + '. Upgrades held: ' + upgraded + '. Bankrupt laps counted against you: ' + p.bankruptLaps + '.';
|
|
2559
|
-
paras.push(p1);
|
|
2560
|
-
|
|
2561
|
-
/* Para 2 — Acts narrative */
|
|
2562
|
-
let p2 = '';
|
|
2563
|
-
if (passedActs.length === 0) {
|
|
2564
|
-
p2 = 'No federal Act passed this game. The Constitution’s implied-powers doctrine was never tested at the Treasury level; the bond market opened on tariff revenue alone. Historically this resembles the counterfactual where Madison’s opposition to Hamilton’s program had succeeded in 1790–91.';
|
|
2565
|
-
} else {
|
|
2566
|
-
const names = passedActs.map(a => a.name).join(', ');
|
|
2567
|
-
p2 = 'Congress carried ' + passedActs.length + ' Act' + (passedActs.length === 1 ? '' : 's') + ': ' + names + '. ';
|
|
2568
|
-
if (passedActs.find(a => a.id === 1) && passedActs.find(a => a.id === 2)) {
|
|
2569
|
-
p2 += 'Funding and Assumption together — the August 1790 compromise — fixed both Revolutionary and State debt at par. ';
|
|
2570
|
-
} else if (passedActs.find(a => a.id === 1)) {
|
|
2571
|
-
p2 += 'Funding without Assumption: Revolutionary creditors were paid; state creditors were not. ';
|
|
2572
|
-
} else if (passedActs.find(a => a.id === 2)) {
|
|
2573
|
-
p2 += 'Assumption without Funding: an unusual sequence, with state debt federalized before Continental paper was redeemed. ';
|
|
2574
|
-
}
|
|
2575
|
-
if (passedActs.find(a => a.id === 3)) p2 += 'The Bank Charter passed, fixing the implied-powers reading of the Constitution for the next generation. ';
|
|
2576
|
-
if (passedActs.find(a => a.id === 5)) p2 += 'The Coinage Act gave the Mint owner a one-time fifty-TN tribute from each player and added a point to both Credit and Capacity. ';
|
|
2577
|
-
if (passedActs.find(a => a.id === 7)) p2 += 'Excise Enforcement collected the Whiskey doubling but added two points of Resistance. ';
|
|
2578
|
-
if (failedAct) p2 += 'The ' + failedAct.name + ' failed the floor vote.';
|
|
2579
|
-
}
|
|
2580
|
-
paras.push(p2);
|
|
2581
|
-
|
|
2582
|
-
/* Para 3 — player portfolio */
|
|
2583
|
-
let p3 = '';
|
|
2584
|
-
if (completedSets.length === 0) {
|
|
2585
|
-
p3 = 'You completed no full system. The portfolio is spread across ' + Object.keys(ownedSys).length + ' system' + (Object.keys(ownedSys).length === 1 ? '' : 's') + ' without consolidation: ' + (p.ownedAssets.length === 0 ? 'no properties' : p.ownedAssets.map(a => SPACES[a.spaceNum].name).join(', ')) + '. ';
|
|
2586
|
-
} else {
|
|
2587
|
-
p3 = 'You completed ' + completedSets.length + ' full system' + (completedSets.length === 1 ? '' : 's') + ': ' + completedSets.map(s => SYS_LABEL[s]).join(', ') + '. ';
|
|
2588
|
-
if (completedSets.includes('national-finance')) p3 += 'A National Finance set is the Hamiltonian apex. ';
|
|
2589
|
-
if (completedSets.includes('state-debt')) p3 += 'A State Debt set is the federalist payoff for Assumption. ';
|
|
2590
|
-
if (completedSets.includes('manufactures') || completedSets.includes('strategic-industry')) p3 += 'Your industrial position will pay forward into the next generation’s tariff politics. ';
|
|
2591
|
-
}
|
|
2592
|
-
if (routes >= 3) p3 += 'You held ' + routes + ' of four Routes, which is enough to dominate the ' + ROUTE_LADDER[Math.min(routes, 4)] + '-TN ladder. ';
|
|
2593
|
-
else if (routes > 0) p3 += 'Your ' + routes + ' Route holding paid the lower ' + ROUTE_LADDER[routes] + '-TN ladder. ';
|
|
2594
|
-
if (hasBank && hasMint) p3 += 'You held both the Bank and the Mint — the federal monetary combo, paying twenty times the dice after Charter. ';
|
|
2595
|
-
else if (hasBank) p3 += 'You held the Bank without the Mint. ';
|
|
2596
|
-
else if (hasMint) p3 += 'You held the Mint without the Bank — mute, until the Coinage Act fired. ';
|
|
2597
|
-
paras.push(p3);
|
|
2598
|
-
|
|
2599
|
-
/* Para 4 — tracks narrative */
|
|
2600
|
-
let p4 = 'Public Credit closed at ' + c + ', Public Resistance at ' + r + ', Industrial Capacity at ' + ic + '. ';
|
|
2601
|
-
if (c >= 8) p4 += 'A Credit reading at or above eight indicates the bond market trusts the federal government — the National Finance set splits the five-Influence endgame bonus. ';
|
|
2602
|
-
else if (c <= 2) p4 += 'A Credit reading at or below two indicates the bond market is jittery — routes were paying half their face. ';
|
|
2603
|
-
if (r >= 8) p4 += 'Resistance at or above eight means the Whiskey Rebellion was live the whole back half. ';
|
|
2604
|
-
if (ic >= 8) p4 += 'Capacity at or above eight pays the industrial endgame bonus: two Influence for Manufactures, two more for Strategic Industry. ';
|
|
2605
|
-
else if (ic >= 6) p4 += 'Capacity at six or above pays the twenty-five-percent industrial rent bonus, but did not reach the endgame Influence threshold. ';
|
|
2606
|
-
paras.push(p4);
|
|
2607
|
-
|
|
2608
|
-
/* Para 5 — verdict + history */
|
|
2609
|
-
let p5 = '';
|
|
2610
|
-
if (won) {
|
|
2611
|
-
p5 = 'The Hamilton System, in the form you built it, ran through to lap 7 with your Influence on top. The specific combination of sets, Acts, and tracks above is the republic you actually built — not a textbook Federalist program. Hamilton’s historical position depended on threading the same needle: enough credit to issue debt, enough revenue to service it, enough industry to make protection pay, and enough enforcement to keep resistance below rebellion. You threaded it.';
|
|
2612
|
-
} else {
|
|
2613
|
-
const wname = state.players[winnerIdx].name;
|
|
2614
|
-
const wprof = PROFILES[state.players[winnerIdx].profile]?.label || state.players[winnerIdx].role;
|
|
2615
|
-
p5 = wname + ' built a stronger ' + wprof + ' position than yours. The historical lesson is that the Hamiltonian apex is not the only winning configuration: the Merchant / Infrastructure line that runs through routes, ports, and exchanges is competitive whenever Credit stays high and Resistance stays low; the Treasury / Finance line is competitive whenever Funding, Assumption, and the Bank Charter all pass. Look at the breakdown above to see which sub-totals you ceded.';
|
|
2616
|
-
}
|
|
2617
|
-
paras.push(p5);
|
|
2618
|
-
|
|
2619
|
-
return paras.map(p => '<p>' + p + '</p>').join('');
|
|
2620
|
-
}
|
|
2621
|
-
|
|
2622
|
-
/* ---- Save / Load ---- */
|
|
2623
|
-
function writeAutosave() {
|
|
2624
|
-
if (REPLAY) return;
|
|
2625
|
-
try { localStorage.setItem(AUTOSAVE_KEY, JSON.stringify(buildSavePayload())); }
|
|
2626
|
-
catch (e) { /* quota exceeded, etc. */ }
|
|
2627
|
-
}
|
|
2628
|
-
|
|
2629
|
-
function buildSavePayload() {
|
|
2630
|
-
return {
|
|
2631
|
-
version: SAVE_VERSION,
|
|
2632
|
-
seed: STATE.rngSeed,
|
|
2633
|
-
decisionLog: DECISION_LOG.slice(),
|
|
2634
|
-
finalState: STATE.status === 'gameOver' ? {
|
|
2635
|
-
ledgerLen: STATE.ledger.length,
|
|
2636
|
-
hash: snapshotHash(STATE),
|
|
2637
|
-
finalScores: STATE.finalScores,
|
|
2638
|
-
} : null,
|
|
2639
|
-
};
|
|
2640
|
-
}
|
|
2641
|
-
|
|
2642
|
-
function hasAutosave() { try { return !!localStorage.getItem(AUTOSAVE_KEY); } catch (e) { return false; } }
|
|
2643
|
-
|
|
2644
|
-
function showResumePill() {
|
|
2645
|
-
if (!hasAutosave()) return;
|
|
2646
|
-
let parsed; try { parsed = JSON.parse(localStorage.getItem(AUTOSAVE_KEY)); } catch (e) { return; }
|
|
2647
|
-
if (!parsed || parsed.version !== SAVE_VERSION) return;
|
|
2648
|
-
const last = parsed.decisionLog[parsed.decisionLog.length - 1];
|
|
2649
|
-
const lapStr = last ? ' · lap ' + last.lap + ' turn ' + last.turn : '';
|
|
2650
|
-
document.getElementById('resumeLbl').textContent = 'Autosave · seed ' + parsed.seed + lapStr + ' · ' + parsed.decisionLog.length + ' actions';
|
|
2651
|
-
document.getElementById('resumePill').classList.remove('hidden');
|
|
2652
|
-
}
|
|
2653
|
-
function hideResumePill() { document.getElementById('resumePill').classList.add('hidden'); }
|
|
2654
|
-
|
|
2655
|
-
function loadFromPayload(payload) {
|
|
2656
|
-
if (!payload || payload.version !== SAVE_VERSION) {
|
|
2657
|
-
if (payload && (payload.version === 'phase5' || payload.version === 'v0.3' || payload.version === 'v0.4' || payload.version === 'v0.5' || payload.version === 'v0.6' || payload.version === 'v0.8')) {
|
|
2658
|
-
showIoPill('Save file from pre-v0.10 ruleset; cannot replay under current balance. Export to keep as historical artifact.', true);
|
|
2659
|
-
} else {
|
|
2660
|
-
showIoPill('Load failed · version mismatch', true);
|
|
2661
|
-
}
|
|
2662
|
-
return false;
|
|
2663
|
-
}
|
|
2664
|
-
if (typeof payload.seed !== 'number') { showIoPill('Load failed · invalid seed', true); return false; }
|
|
2665
|
-
if (!Array.isArray(payload.decisionLog)) { showIoPill('Load failed · invalid decisionLog', true); return false; }
|
|
2666
|
-
/* Reconstruct */
|
|
2667
|
-
STATE = initialState(payload.seed);
|
|
2668
|
-
DECISION_LOG = [];
|
|
2669
|
-
TURN_SNAPSHOTS = [];
|
|
2670
|
-
NARRATION_LOG = [];
|
|
2671
|
-
resetNarrationDetector();
|
|
2672
|
-
let i = 0;
|
|
2673
|
-
try {
|
|
2674
|
-
for (; i < payload.decisionLog.length; i++) {
|
|
2675
|
-
const e = payload.decisionLog[i];
|
|
2676
|
-
const action = { type: e.action, ...(e.params || {}) };
|
|
2677
|
-
DECISION_LOG.push({ playerIdx: STATE.activePlayerIndex, action: action.type, params: e.params || {}, turn: STATE.turnIndex, lap: STATE.lap });
|
|
2678
|
-
STATE = reduce(STATE, action);
|
|
2679
|
-
detectNarration();
|
|
2680
|
-
if (action.type === 'END_TURN' || action.type === 'END_GAME') captureSnapshot();
|
|
2681
|
-
}
|
|
2682
|
-
} catch (err) {
|
|
2683
|
-
showIoPill('Load failed · reducer error at action ' + i, true);
|
|
2684
|
-
return false;
|
|
2685
|
-
}
|
|
2686
|
-
/* Integrity check vs. saved finalState */
|
|
2687
|
-
if (payload.finalState) {
|
|
2688
|
-
const liveHash = snapshotHash(STATE);
|
|
2689
|
-
if (liveHash !== payload.finalState.hash) {
|
|
2690
|
-
showIoPill('Load failed · integrity divergence at end of replay', true);
|
|
2691
|
-
return false;
|
|
2692
|
-
}
|
|
2693
|
-
}
|
|
2694
|
-
renderBoard();
|
|
2695
|
-
render();
|
|
2696
|
-
hideResumePill();
|
|
2697
|
-
showIoPill('Loaded · integrity verified', false);
|
|
2698
|
-
return true;
|
|
2699
|
-
}
|
|
2700
|
-
|
|
2701
|
-
function showIoPill(msg, isErr) {
|
|
2702
|
-
const existing = document.querySelector('.io-pill');
|
|
2703
|
-
if (existing) existing.remove();
|
|
2704
|
-
const pill = el('span', 'io-pill ' + (isErr ? 'err' : 'ok'), msg);
|
|
2705
|
-
document.querySelector('.controls').appendChild(pill);
|
|
2706
|
-
setTimeout(() => { if (pill.parentNode) pill.remove(); }, 4500);
|
|
2707
|
-
}
|
|
2708
|
-
|
|
2709
|
-
function downloadSave() {
|
|
2710
|
-
const payload = buildSavePayload();
|
|
2711
|
-
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
|
|
2712
|
-
const url = URL.createObjectURL(blob);
|
|
2713
|
-
const a = document.createElement('a');
|
|
2714
|
-
a.href = url;
|
|
2715
|
-
const ts = new Date().toISOString().slice(0, 19).replace(/[:T]/g, '-');
|
|
2716
|
-
a.download = 'sovereign-seed' + payload.seed + '-' + ts + '.json';
|
|
2717
|
-
document.body.appendChild(a); a.click(); a.remove();
|
|
2718
|
-
setTimeout(() => URL.revokeObjectURL(url), 800);
|
|
2719
|
-
writeAutosave();
|
|
2720
|
-
showIoPill('Saved · ' + DECISION_LOG.length + ' actions', false);
|
|
2721
|
-
}
|
|
2722
|
-
|
|
2723
|
-
function triggerLoad() {
|
|
2724
|
-
if (hasAutosave()) {
|
|
2725
|
-
/* Offer both autosave and file. Simple flow: ask user to choose. */
|
|
2726
|
-
const useAuto = confirm('Load autosave from localStorage? Click Cancel to import a JSON file instead.');
|
|
2727
|
-
if (useAuto) {
|
|
2728
|
-
try {
|
|
2729
|
-
const parsed = JSON.parse(localStorage.getItem(AUTOSAVE_KEY));
|
|
2730
|
-
loadFromPayload(parsed);
|
|
2731
|
-
} catch (e) { showIoPill('Autosave parse failed', true); }
|
|
2732
|
-
return;
|
|
2733
|
-
}
|
|
2734
|
-
}
|
|
2735
|
-
document.getElementById('loadFile').click();
|
|
2736
|
-
}
|
|
2737
|
-
|
|
2738
|
-
document.getElementById('loadFile').addEventListener('change', (e) => {
|
|
2739
|
-
const file = e.target.files[0];
|
|
2740
|
-
if (!file) return;
|
|
2741
|
-
const reader = new FileReader();
|
|
2742
|
-
reader.onload = (ev) => {
|
|
2743
|
-
try {
|
|
2744
|
-
const parsed = JSON.parse(ev.target.result);
|
|
2745
|
-
loadFromPayload(parsed);
|
|
2746
|
-
} catch (err) {
|
|
2747
|
-
showIoPill('Load failed · invalid JSON', true);
|
|
2748
|
-
}
|
|
2749
|
-
};
|
|
2750
|
-
reader.readAsText(file);
|
|
2751
|
-
e.target.value = '';
|
|
2752
|
-
});
|
|
2753
|
-
|
|
2754
|
-
document.getElementById('btnSave').addEventListener('click', downloadSave);
|
|
2755
|
-
document.getElementById('btnLoad').addEventListener('click', triggerLoad);
|
|
2756
|
-
document.getElementById('resumeYes').addEventListener('click', () => {
|
|
2757
|
-
try {
|
|
2758
|
-
const parsed = JSON.parse(localStorage.getItem(AUTOSAVE_KEY));
|
|
2759
|
-
loadFromPayload(parsed);
|
|
2760
|
-
} catch (e) { showIoPill('Resume failed', true); }
|
|
2761
|
-
});
|
|
2762
|
-
document.getElementById('resumeNo').addEventListener('click', () => {
|
|
2763
|
-
hideResumePill();
|
|
2764
|
-
try { localStorage.removeItem(AUTOSAVE_KEY); } catch (e) {}
|
|
2765
|
-
});
|
|
2766
|
-
document.getElementById('narrPref').addEventListener('click', (e) => {
|
|
2767
|
-
NARRATION_ENABLED = !NARRATION_ENABLED;
|
|
2768
|
-
e.target.classList.toggle('on', NARRATION_ENABLED);
|
|
2769
|
-
e.target.textContent = NARRATION_ENABLED ? 'Drawer on' : 'Drawer off';
|
|
2770
|
-
renderNarrationPanel();
|
|
2771
|
-
});
|
|
2772
|
-
|
|
2773
|
-
/* ---- Replay engine ---- */
|
|
2774
|
-
function openReplay() {
|
|
2775
|
-
if (STATE.status !== 'gameOver') return;
|
|
2776
|
-
const seed = STATE.rngSeed;
|
|
2777
|
-
const log = DECISION_LOG.slice();
|
|
2778
|
-
/* Pre-compute per-turn snapshots by replaying */
|
|
2779
|
-
const frames = [];
|
|
2780
|
-
let s = initialState(seed);
|
|
2781
|
-
frames.push(cloneFrame(s, -1));
|
|
2782
|
-
let prevTurn = s.turnIndex;
|
|
2783
|
-
let integrityOk = true;
|
|
2784
|
-
for (let i = 0; i < log.length; i++) {
|
|
2785
|
-
const e = log[i];
|
|
2786
|
-
const action = { type: e.action, ...(e.params || {}) };
|
|
2787
|
-
try { s = reduce(s, action); }
|
|
2788
|
-
catch (err) { integrityOk = false; break; }
|
|
2789
|
-
if (s.turnIndex !== prevTurn) {
|
|
2790
|
-
frames.push(cloneFrame(s, i));
|
|
2791
|
-
prevTurn = s.turnIndex;
|
|
2792
|
-
}
|
|
2793
|
-
}
|
|
2794
|
-
frames.push(cloneFrame(s, log.length - 1));
|
|
2795
|
-
REPLAY = { seed, frames, index: frames.length - 1, playing: false, timer: null, integrityOk };
|
|
2796
|
-
/* Verify against live STATE */
|
|
2797
|
-
if (snapshotHash(s) !== snapshotHash(STATE)) REPLAY.integrityOk = false;
|
|
2798
|
-
document.getElementById('replayOverlay').classList.remove('hidden');
|
|
2799
|
-
renderReplay();
|
|
2800
|
-
}
|
|
2801
|
-
function closeReplay() {
|
|
2802
|
-
if (REPLAY?.timer) clearInterval(REPLAY.timer);
|
|
2803
|
-
REPLAY = null;
|
|
2804
|
-
document.getElementById('replayOverlay').classList.add('hidden');
|
|
2805
|
-
}
|
|
2806
|
-
function cloneFrame(s, srcLogIdx) {
|
|
2807
|
-
return {
|
|
2808
|
-
srcLogIdx,
|
|
2809
|
-
turn: s.turnIndex, lap: s.lap, activeIdx: s.activePlayerIndex,
|
|
2810
|
-
credit: s.tracks.credit.value, resistance: s.tracks.resistance.value, capacity: s.tracks.capacity.value,
|
|
2811
|
-
players: s.players.map(p => ({
|
|
2812
|
-
name: p.name, cash: p.cash, position: p.position,
|
|
2813
|
-
assets: p.ownedAssets.map(a => ({ spaceNum: a.spaceNum, tier: a.tier })),
|
|
2814
|
-
})),
|
|
2815
|
-
actsPassed: s.acts.passed.map(a => a.actId),
|
|
2816
|
-
ledger: s.ledger.slice(),
|
|
2817
|
-
};
|
|
2818
|
-
}
|
|
2819
|
-
|
|
2820
|
-
function renderReplayBoard(frame) {
|
|
2821
|
-
const grid = document.getElementById('replayBoardGrid');
|
|
2822
|
-
grid.innerHTML = '';
|
|
2823
|
-
for (let r = 0; r < 11; r++) for (let c = 0; c < 11; c++) {
|
|
2824
|
-
if (r === 0 || r === 10 || c === 0 || c === 10) {
|
|
2825
|
-
const space = SPACES.find(sp => { const [sr, sc] = spaceCoord(sp.num); return sr === r && sc === c; });
|
|
2826
|
-
if (!space) { const e = el('div', 'board-cell empty'); e.style.gridColumn = (c+1); e.style.gridRow = (r+1); grid.appendChild(e); continue; }
|
|
2827
|
-
const isCorner = space.kind.startsWith('corner');
|
|
2828
|
-
const cls = 'board-cell ' + (isCorner ? 'corner' : space.kind);
|
|
2829
|
-
const cell = el('div', cls);
|
|
2830
|
-
cell.style.gridColumn = (c+1); cell.style.gridRow = (r+1);
|
|
2831
|
-
cell.innerHTML = '<div class="band"></div><div class="num">' + String(space.num).padStart(2,'0') + '</div><div class="nm">' + space.name + '</div><div class="tokens"></div>';
|
|
2832
|
-
/* Apply ownership */
|
|
2833
|
-
frame.players.forEach((pl, idx) => {
|
|
2834
|
-
const owned = pl.assets.find(a => a.spaceNum === space.num);
|
|
2835
|
-
if (owned) {
|
|
2836
|
-
cell.classList.add('owned-p' + idx);
|
|
2837
|
-
const dot = el('div', 'owner-dot'); cell.appendChild(dot);
|
|
2838
|
-
if (owned.tier > 0) cell.appendChild(el('div', 'tier', 'I'.repeat(owned.tier)));
|
|
2839
|
-
}
|
|
2840
|
-
});
|
|
2841
|
-
grid.appendChild(cell);
|
|
2842
|
-
}
|
|
2843
|
-
}
|
|
2844
|
-
/* Tokens */
|
|
2845
|
-
frame.players.forEach((pl, idx) => {
|
|
2846
|
-
const cell = grid.querySelector('.board-cell[class*="sys-"]:nth-child(0)');
|
|
2847
|
-
const all = grid.querySelectorAll('.board-cell');
|
|
2848
|
-
all.forEach(el2 => {
|
|
2849
|
-
/* nothing */
|
|
2850
|
-
});
|
|
2851
|
-
});
|
|
2852
|
-
/* Place player tokens by re-querying via num */
|
|
2853
|
-
frame.players.forEach((pl, idx) => {
|
|
2854
|
-
const cells = grid.querySelectorAll('.board-cell');
|
|
2855
|
-
for (const cell of cells) {
|
|
2856
|
-
const num = cell.querySelector('.num');
|
|
2857
|
-
if (num && parseInt(num.textContent, 10) === pl.position) {
|
|
2858
|
-
const tw = cell.querySelector('.tokens');
|
|
2859
|
-
if (tw) tw.appendChild(el('div', 'tok p' + idx));
|
|
2860
|
-
break;
|
|
2861
|
-
}
|
|
2862
|
-
}
|
|
2863
|
-
});
|
|
2864
|
-
const center = el('div', 'board-center');
|
|
2865
|
-
center.style.gridColumn = '2 / span 9'; center.style.gridRow = '2 / span 9';
|
|
2866
|
-
center.innerHTML = '<div class="bc-eyebrow">Replay</div><div class="bc-title">Founding Credit</div>' +
|
|
2867
|
-
'<div class="bc-sub">Lap ' + frame.lap + ' · Turn ' + frame.turn + '</div>' +
|
|
2868
|
-
'<div class="bc-tracks"><span class="t credit">Credit<span class="v">' + frame.credit + '</span></span>' +
|
|
2869
|
-
'<span class="t resist">Resist<span class="v">' + frame.resistance + '</span></span>' +
|
|
2870
|
-
'<span class="t indust">Indust<span class="v">' + frame.capacity + '</span></span></div>' +
|
|
2871
|
-
'<div class="bc-acts">' + (frame.actsPassed.length === 0 ? 'No Acts yet' : 'Acts: ' + frame.actsPassed.map(id => ACTS.find(a => a.id === id).roman).join(' · ')) + '</div>';
|
|
2872
|
-
grid.appendChild(center);
|
|
2873
|
-
}
|
|
2874
|
-
|
|
2875
|
-
function renderReplay() {
|
|
2876
|
-
if (!REPLAY) return;
|
|
2877
|
-
const frame = REPLAY.frames[REPLAY.index];
|
|
2878
|
-
document.getElementById('replayTtlSub').textContent = 'seed ' + REPLAY.seed + ' · frame ' + (REPLAY.index + 1) + ' of ' + REPLAY.frames.length;
|
|
2879
|
-
renderReplayBoard(frame);
|
|
2880
|
-
/* Meta */
|
|
2881
|
-
const metaEl = document.getElementById('replayMeta');
|
|
2882
|
-
metaEl.innerHTML = frame.players.map((pl, i) => {
|
|
2883
|
-
const isActive = i === frame.activeIdx;
|
|
2884
|
-
return '<div class="m" style="' + (isActive ? 'border-color:var(--highlight);border-width:1.5px' : '') + '"><div class="lbl" style="color:var(--p' + i + ')">' + pl.name + '</div><div class="v cash">$' + pl.cash.toLocaleString('en-US') + '</div><div style="font-family:var(--mono);font-size:9px;opacity:.7;margin-top:2px">' + pl.assets.length + ' assets</div></div>';
|
|
2885
|
-
}).join('');
|
|
2886
|
-
/* Ledger to date */
|
|
2887
|
-
const ledger = frame.ledger;
|
|
2888
|
-
const ledRoot = document.getElementById('replayLedger');
|
|
2889
|
-
ledRoot.innerHTML = ledger.slice().reverse().map((row, i) => {
|
|
2890
|
-
const stamp = 'L' + row.lap + '·T' + row.turn;
|
|
2891
|
-
return '<div class="row"><span class="stamp">' + stamp + '</span><span class="actor">' + row.actor + ' · ' + row.event + '</span><span>' + row.detail + '</span></div>';
|
|
2892
|
-
}).join('') || '<em style="font-family:var(--display);font-style:italic;font-size:11px;opacity:.6">(empty)</em>';
|
|
2893
|
-
/* Scrubber */
|
|
2894
|
-
const ticksEl = document.getElementById('scrubberTicks');
|
|
2895
|
-
ticksEl.innerHTML = '<div class="marker" id="scrubMarker"></div>';
|
|
2896
|
-
const total = REPLAY.frames.length;
|
|
2897
|
-
for (let i = 0; i < total; i++) {
|
|
2898
|
-
const tk = document.createElement('div');
|
|
2899
|
-
tk.className = 'tick';
|
|
2900
|
-
const f = REPLAY.frames[i];
|
|
2901
|
-
if (i === 0 || (i > 0 && REPLAY.frames[i-1].lap !== f.lap)) { tk.classList.add('lap-start'); tk.dataset.lap = 'L' + f.lap; }
|
|
2902
|
-
tk.style.left = (i / total * 100) + '%';
|
|
2903
|
-
tk.style.width = (100 / total) + '%';
|
|
2904
|
-
tk.dataset.idx = i;
|
|
2905
|
-
tk.title = 'Lap ' + f.lap + ' · Turn ' + f.turn;
|
|
2906
|
-
tk.addEventListener('click', (e) => { e.stopPropagation(); REPLAY.index = i; if (REPLAY.timer) { clearInterval(REPLAY.timer); REPLAY.timer = null; REPLAY.playing = false; updateReplayPlayBtn(); } renderReplay(); });
|
|
2907
|
-
ticksEl.appendChild(tk);
|
|
2908
|
-
}
|
|
2909
|
-
const marker = document.createElement('div');
|
|
2910
|
-
marker.className = 'marker';
|
|
2911
|
-
marker.style.left = ((REPLAY.index + 0.5) / total * 100) + '%';
|
|
2912
|
-
ticksEl.appendChild(marker);
|
|
2913
|
-
document.getElementById('scrubMeta').textContent = 'L' + frame.lap + ' · Turn ' + frame.turn + ' · ' + (REPLAY.index + 1) + '/' + total;
|
|
2914
|
-
const intEl = document.getElementById('replayIntegrity');
|
|
2915
|
-
intEl.textContent = REPLAY.integrityOk ? '✓ integrity' : '⚠ Replay integrity error';
|
|
2916
|
-
intEl.className = 'integrity ' + (REPLAY.integrityOk ? 'ok' : 'err');
|
|
2917
|
-
}
|
|
2918
|
-
|
|
2919
|
-
function updateReplayPlayBtn() {
|
|
2920
|
-
document.getElementById('replayPlay').textContent = REPLAY?.playing ? '⏸ Pause' : '▶ Play';
|
|
2921
|
-
document.getElementById('replayPlay').classList.toggle('primary', !REPLAY?.playing);
|
|
2922
|
-
}
|
|
2923
|
-
function replayStep(delta) {
|
|
2924
|
-
if (!REPLAY) return;
|
|
2925
|
-
REPLAY.index = Math.max(0, Math.min(REPLAY.frames.length - 1, REPLAY.index + delta));
|
|
2926
|
-
renderReplay();
|
|
2927
|
-
}
|
|
2928
|
-
function replayToggle() {
|
|
2929
|
-
if (!REPLAY) return;
|
|
2930
|
-
if (REPLAY.playing) {
|
|
2931
|
-
clearInterval(REPLAY.timer); REPLAY.timer = null; REPLAY.playing = false;
|
|
2932
|
-
} else {
|
|
2933
|
-
REPLAY.playing = true;
|
|
2934
|
-
REPLAY.timer = setInterval(() => {
|
|
2935
|
-
if (!REPLAY) return;
|
|
2936
|
-
if (REPLAY.index >= REPLAY.frames.length - 1) { clearInterval(REPLAY.timer); REPLAY.timer = null; REPLAY.playing = false; updateReplayPlayBtn(); return; }
|
|
2937
|
-
REPLAY.index += 1; renderReplay();
|
|
2938
|
-
}, 1500);
|
|
2939
|
-
}
|
|
2940
|
-
updateReplayPlayBtn();
|
|
2941
|
-
}
|
|
2942
|
-
document.getElementById('btnReplay').addEventListener('click', openReplay);
|
|
2943
|
-
document.getElementById('replayClose').addEventListener('click', closeReplay);
|
|
2944
|
-
document.getElementById('replayPlay').addEventListener('click', replayToggle);
|
|
2945
|
-
document.getElementById('replayStepBack').addEventListener('click', () => replayStep(-1));
|
|
2946
|
-
document.getElementById('replayStepFwd').addEventListener('click', () => replayStep(1));
|
|
2947
|
-
|
|
2948
|
-
/* ---- Render hook for Phase 5 — wrap original render so panels stay in sync ---- */
|
|
2949
|
-
const _origRender = render;
|
|
2950
|
-
render = function() {
|
|
2951
|
-
_origRender();
|
|
2952
|
-
renderNarrationPanel();
|
|
2953
|
-
/* Show Replay button only on gameOver */
|
|
2954
|
-
document.getElementById('btnReplay').classList.toggle('hidden', STATE.status !== 'gameOver');
|
|
2955
|
-
};
|
|
2956
|
-
|
|
2957
|
-
/* ---- Augment endgame render — inject republic summary after results ---- */
|
|
2958
|
-
const _origEndgame = renderEndgame;
|
|
2959
|
-
renderEndgame = function() {
|
|
2960
|
-
_origEndgame();
|
|
2961
|
-
if (STATE.phase !== 'game-over') return;
|
|
2962
|
-
const view = document.getElementById('endgameView');
|
|
2963
|
-
if (view.querySelector('.endgame-narration')) return;
|
|
2964
|
-
const block = document.createElement('div');
|
|
2965
|
-
block.className = 'endgame-narration';
|
|
2966
|
-
block.innerHTML = '<h3>' + NARRATION['endgame · republic-summary'].h + '</h3>' + buildRepublicSummary(STATE) +
|
|
2967
|
-
'<div class="meta-line">Scoped to your ' + DECISION_LOG.length + ' decisions across ' + STATE.lap + ' laps · seed ' + STATE.rngSeed + ' · ledger ' + STATE.ledger.length + ' entries</div>';
|
|
2968
|
-
const actionsRow = view.querySelector('.actions');
|
|
2969
|
-
view.insertBefore(block, actionsRow);
|
|
2970
|
-
};
|
|
2971
|
-
|
|
2972
|
-
/* ---- Wrap startNewGame to reset narration detector too ---- */
|
|
2973
|
-
const _origStart = startNewGame;
|
|
2974
|
-
startNewGame = function(seed) {
|
|
2975
|
-
resetNarrationDetector();
|
|
2976
|
-
_origStart(seed);
|
|
2977
|
-
};
|
|
2978
|
-
|
|
2979
|
-
/* =====================================================================
|
|
2980
|
-
PHASE 6 — balance report, batch simulation, exporters
|
|
2981
|
-
No reducer changes. Derived entirely from ledger / decisionLog / final state.
|
|
2982
|
-
===================================================================== */
|
|
2983
|
-
|
|
2984
|
-
/* The Phase 4 reducer schedules setTimeout(() => dispatch(...)) inside DRAW_CARD
|
|
2985
|
-
to auto-resolve opponent cards. When a batch game runs through reduce() directly,
|
|
2986
|
-
those timeouts still fire later against the LIVE STATE. Guard the live dispatch
|
|
2987
|
-
while a batch is running so the queued callbacks become no-ops on the live game. */
|
|
2988
|
-
let BATCH_GUARD = false;
|
|
2989
|
-
const _origDispatch_p6 = dispatch;
|
|
2990
|
-
dispatch = function(action) {
|
|
2991
|
-
if (BATCH_GUARD) return;
|
|
2992
|
-
return _origDispatch_p6(action);
|
|
2993
|
-
};
|
|
2994
|
-
|
|
2995
|
-
/* ---- Profile labels for batch & report ---- */
|
|
2996
|
-
const PROFILE_DEFAULT_NAMES = {
|
|
2997
|
-
'human': 'You',
|
|
2998
|
-
'treasury-finance': 'Hamilton',
|
|
2999
|
-
'merchant-infrastructure': 'Morris',
|
|
3000
|
-
'manufacturer-industry': 'Slater',
|
|
3001
|
-
};
|
|
3002
|
-
|
|
3003
|
-
/* ---- Balance report computation — reads ledger + final state only ---- */
|
|
3004
|
-
function parseCashDelta(detail) {
|
|
3005
|
-
const m = detail.match(/ · ([+-]?\d+) TN · cash /);
|
|
3006
|
-
return m ? parseInt(m[1], 10) : null;
|
|
3007
|
-
}
|
|
3008
|
-
function parseCashReason(detail) {
|
|
3009
|
-
const m = detail.match(/^(.+?) · [+-]?\d+ TN · cash /);
|
|
3010
|
-
return m ? m[1] : detail;
|
|
3011
|
-
}
|
|
3012
|
-
function playerIdxByName(state, name) {
|
|
3013
|
-
return state.players.findIndex(p => p.name === name);
|
|
3014
|
-
}
|
|
3015
|
-
/* PHASE 6.1 — telemetry hygiene: prefer stable slot index from ledger row when present */
|
|
3016
|
-
function playerIdxFromRow(state, row) {
|
|
3017
|
-
if (typeof row.actorIdx === 'number') return row.actorIdx;
|
|
3018
|
-
return playerIdxByName(state, row.actor);
|
|
3019
|
-
}
|
|
3020
|
-
function classifyCashReason(reason) {
|
|
3021
|
-
if (/^Rent on /.test(reason)) return { dir:'paid', kind:'rent-paid' };
|
|
3022
|
-
if (/^Rent from /.test(reason)) {
|
|
3023
|
-
/* try to extract space at end "for SPACENAME" */
|
|
3024
|
-
const m = reason.match(/ for (.+)$/);
|
|
3025
|
-
let sub = 'properties';
|
|
3026
|
-
if (m) {
|
|
3027
|
-
const space = SPACES.find(sp => sp.name === m[1]);
|
|
3028
|
-
if (space) {
|
|
3029
|
-
if (space.kind === 'route') sub = 'routes';
|
|
3030
|
-
else if (space.kind === 'institution') sub = 'institutions';
|
|
3031
|
-
else if (space.kind.startsWith('sys-')) sub = 'properties';
|
|
3032
|
-
}
|
|
3033
|
-
}
|
|
3034
|
-
return { dir:'received', kind:'rent-collected', sub };
|
|
3035
|
-
}
|
|
3036
|
-
if (/^Treasury Opens:/.test(reason)) return { dir:'received', kind:'pass-go' };
|
|
3037
|
-
if (/^Buy /.test(reason)) return { dir:'paid', kind:'purchase' };
|
|
3038
|
-
if (/^Upgrade /.test(reason)) return { dir:'paid', kind:'upgrade' };
|
|
3039
|
-
if (/^Auction win:/.test(reason)) return { dir:'paid', kind:'auction-win' };
|
|
3040
|
-
if (/^Force-vote fee/.test(reason)) return { dir:'paid', kind:'force-fee' };
|
|
3041
|
-
if (/ tax$/.test(reason)) return { dir:'paid', kind:'tax' };
|
|
3042
|
-
if (/Act/.test(reason)) return { dir: reason.match(/^Coinage Act:/) ? (reason.includes('pays') ? 'paid' : 'received') : 'received', kind:'act' };
|
|
3043
|
-
return { dir: 'unknown', kind:'card' };
|
|
3044
|
-
}
|
|
3045
|
-
|
|
3046
|
-
function computeBalanceReport(state, decisionLog) {
|
|
3047
|
-
const N = state.players.length;
|
|
3048
|
-
const blank = () => ({ collected:0, paid:0, byKind:{properties:0,routes:0,institutions:0,'pass-go':0,cards:0,acts:0}, byKindPaid:{} });
|
|
3049
|
-
const income = state.players.map(blank);
|
|
3050
|
-
|
|
3051
|
-
/* Walk ledger CASH rows */
|
|
3052
|
-
for (const row of state.ledger) {
|
|
3053
|
-
if (row.event !== 'CASH') continue;
|
|
3054
|
-
const pIdx = playerIdxFromRow(state, row);
|
|
3055
|
-
if (pIdx < 0) continue;
|
|
3056
|
-
const delta = parseCashDelta(row.detail);
|
|
3057
|
-
if (delta == null) continue;
|
|
3058
|
-
const reason = parseCashReason(row.detail);
|
|
3059
|
-
const cls = classifyCashReason(reason);
|
|
3060
|
-
if (cls.dir === 'received' || delta > 0) {
|
|
3061
|
-
income[pIdx].collected += delta;
|
|
3062
|
-
let bucket = 'cards';
|
|
3063
|
-
if (cls.kind === 'rent-collected') bucket = cls.sub || 'properties';
|
|
3064
|
-
else if (cls.kind === 'pass-go') bucket = 'pass-go';
|
|
3065
|
-
else if (cls.kind === 'act') bucket = 'acts';
|
|
3066
|
-
income[pIdx].byKind[bucket] = (income[pIdx].byKind[bucket] || 0) + delta;
|
|
3067
|
-
} else if (cls.dir === 'paid' || delta < 0) {
|
|
3068
|
-
income[pIdx].paid += -delta;
|
|
3069
|
-
income[pIdx].byKindPaid[cls.kind] = (income[pIdx].byKindPaid[cls.kind] || 0) + (-delta);
|
|
3070
|
-
}
|
|
3071
|
-
}
|
|
3072
|
-
|
|
3073
|
-
/* Asset timing: first acquisition per system, first upgrade, set-complete lap */
|
|
3074
|
-
const timing = state.players.map(() => ({
|
|
3075
|
-
firstBySys: {}, firstUpgradeLap: null, setsCompletedLap: {},
|
|
3076
|
-
}));
|
|
3077
|
-
const ownershipShadow = state.players.map(() => new Set());
|
|
3078
|
-
for (const row of state.ledger) {
|
|
3079
|
-
const pIdx = playerIdxFromRow(state, row);
|
|
3080
|
-
if (pIdx < 0) continue;
|
|
3081
|
-
if (row.event === 'OWN' || row.event === 'AUCTION WIN') {
|
|
3082
|
-
const spaceMatch = row.detail.match(/(?:Now owns |Won )(.+?)(?: ·|$| for )/);
|
|
3083
|
-
const spaceName = spaceMatch ? spaceMatch[1] : null;
|
|
3084
|
-
const space = spaceName ? SPACES.find(sp => sp.name === spaceName) : null;
|
|
3085
|
-
if (space && ASSETS[space.num]?.sys) {
|
|
3086
|
-
const sys = ASSETS[space.num].sys;
|
|
3087
|
-
if (timing[pIdx].firstBySys[sys] == null) timing[pIdx].firstBySys[sys] = row.lap;
|
|
3088
|
-
ownershipShadow[pIdx].add(space.num);
|
|
3089
|
-
/* check set completion */
|
|
3090
|
-
const total = Object.entries(ASSETS).filter(([n, a]) => a.sys === sys).length;
|
|
3091
|
-
const owned = [...ownershipShadow[pIdx]].filter(n => ASSETS[n]?.sys === sys).length;
|
|
3092
|
-
if (owned === total && timing[pIdx].setsCompletedLap[sys] == null) {
|
|
3093
|
-
timing[pIdx].setsCompletedLap[sys] = row.lap;
|
|
3094
|
-
}
|
|
3095
|
-
}
|
|
3096
|
-
}
|
|
3097
|
-
if (row.event === 'UPGRADE' && timing[pIdx].firstUpgradeLap == null) {
|
|
3098
|
-
timing[pIdx].firstUpgradeLap = row.lap;
|
|
3099
|
-
}
|
|
3100
|
-
}
|
|
3101
|
-
|
|
3102
|
-
/* Upgrade ROI — sum of upgrade spend per player + post-upgrade rent received on upgraded spaces */
|
|
3103
|
-
const upgradeROI = state.players.map(() => ({ spend: 0, postRent: 0, count: 0 }));
|
|
3104
|
-
const upgradeMarks = []; // {pIdx, spaceName, ledgerIndex}
|
|
3105
|
-
for (let i = 0; i < state.ledger.length; i++) {
|
|
3106
|
-
const row = state.ledger[i];
|
|
3107
|
-
if (row.event === 'CASH' && /^Upgrade /.test(parseCashReason(row.detail))) {
|
|
3108
|
-
const pIdx = playerIdxFromRow(state, row);
|
|
3109
|
-
if (pIdx < 0) continue;
|
|
3110
|
-
const cost = -parseCashDelta(row.detail);
|
|
3111
|
-
const m = parseCashReason(row.detail).match(/^Upgrade (.+?) →/);
|
|
3112
|
-
if (m) {
|
|
3113
|
-
upgradeROI[pIdx].spend += cost;
|
|
3114
|
-
upgradeROI[pIdx].count += 1;
|
|
3115
|
-
upgradeMarks.push({ pIdx, spaceName: m[1], ledgerIndex: i });
|
|
3116
|
-
}
|
|
3117
|
-
}
|
|
3118
|
-
}
|
|
3119
|
-
for (const mark of upgradeMarks) {
|
|
3120
|
-
for (let j = mark.ledgerIndex + 1; j < state.ledger.length; j++) {
|
|
3121
|
-
const r = state.ledger[j];
|
|
3122
|
-
if (r.event !== 'CASH') continue;
|
|
3123
|
-
const pIdx = playerIdxFromRow(state, r);
|
|
3124
|
-
if (pIdx !== mark.pIdx) continue;
|
|
3125
|
-
const reason = parseCashReason(r.detail);
|
|
3126
|
-
if (new RegExp('^Rent from .* for ' + mark.spaceName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '$').test(reason)) {
|
|
3127
|
-
const d = parseCashDelta(r.detail);
|
|
3128
|
-
if (d > 0) upgradeROI[mark.pIdx].postRent += d;
|
|
3129
|
-
}
|
|
3130
|
-
}
|
|
3131
|
-
}
|
|
3132
|
-
|
|
3133
|
-
/* Act outcomes */
|
|
3134
|
-
const acts = ACTS.map(a => ({ id: a.id, name: a.name, lap: a.lap, passed: false, votes: [], forceFee: null }));
|
|
3135
|
-
for (const row of state.ledger) {
|
|
3136
|
-
if (row.event === 'PASS' && row.actor === 'Acts') {
|
|
3137
|
-
const m = row.detail.match(/^(.+?) passes (\d+)-(\d+)/);
|
|
3138
|
-
if (m) {
|
|
3139
|
-
const act = acts.find(a => a.name === m[1]);
|
|
3140
|
-
if (act) { act.passed = true; act.tally = m[2] + '-' + m[3]; }
|
|
3141
|
-
}
|
|
3142
|
-
}
|
|
3143
|
-
if (row.event === 'FAIL' && row.actor === 'Acts') {
|
|
3144
|
-
const m = row.detail.match(/^(.+?) fails (\d+)-(\d+)/);
|
|
3145
|
-
if (m) {
|
|
3146
|
-
const act = acts.find(a => a.name === m[1]);
|
|
3147
|
-
if (act) { act.passed = false; act.tally = m[2] + '-' + m[3]; }
|
|
3148
|
-
}
|
|
3149
|
-
}
|
|
3150
|
-
if (row.event === 'VOTE') {
|
|
3151
|
-
const pIdx = playerIdxFromRow(state, row);
|
|
3152
|
-
const vMatch = row.detail.match(/^Vote (YES|NO) · (.*)$/);
|
|
3153
|
-
const lap = row.lap;
|
|
3154
|
-
const act = acts.find(a => a.lap === lap);
|
|
3155
|
-
if (act && vMatch && pIdx >= 0) act.votes.push({ pIdx, vote: vMatch[1], reason: vMatch[2] });
|
|
3156
|
-
}
|
|
3157
|
-
if (row.event === 'FORCE-FEE') {
|
|
3158
|
-
const pIdx = playerIdxFromRow(state, row);
|
|
3159
|
-
const lap = row.lap;
|
|
3160
|
-
const act = acts.find(a => a.lap === lap);
|
|
3161
|
-
if (act && pIdx >= 0) act.forceFee = { pIdx, reason: row.detail };
|
|
3162
|
-
}
|
|
3163
|
-
}
|
|
3164
|
-
|
|
3165
|
-
/* Track trajectory — walk ledger TRACK events to derive end-of-lap values */
|
|
3166
|
-
const trackHistory = []; // [{lap, credit, resistance, capacity}]
|
|
3167
|
-
let cur = { credit: 5, resistance: 2, capacity: 1 };
|
|
3168
|
-
let lastLap = 1;
|
|
3169
|
-
for (const row of state.ledger) {
|
|
3170
|
-
if (row.lap !== lastLap) {
|
|
3171
|
-
trackHistory.push({ lap: lastLap, ...cur });
|
|
3172
|
-
lastLap = row.lap;
|
|
3173
|
-
}
|
|
3174
|
-
if (row.event === 'CREDIT' || row.event === 'RESISTANCE' || row.event === 'CAPACITY') {
|
|
3175
|
-
const m = row.detail.match(/→ (\d+)/);
|
|
3176
|
-
if (m) {
|
|
3177
|
-
const key = row.event.toLowerCase();
|
|
3178
|
-
cur[key] = parseInt(m[1], 10);
|
|
3179
|
-
}
|
|
3180
|
-
}
|
|
3181
|
-
}
|
|
3182
|
-
trackHistory.push({ lap: lastLap, ...cur });
|
|
3183
|
-
|
|
3184
|
-
/* Failure events */
|
|
3185
|
-
const failures = { bankruptcy: [], default: [], rebellion: [] };
|
|
3186
|
-
for (const row of state.ledger) {
|
|
3187
|
-
if (row.event === 'BANKRUPT') {
|
|
3188
|
-
const m = row.detail.match(/^(.+?): cash below zero · lap (\d+)/);
|
|
3189
|
-
if (m) failures.bankruptcy.push({ player: m[1], slot: (typeof row.actorIdx === 'number' ? row.actorIdx : -1), lap: parseInt(m[2], 10) });
|
|
3190
|
-
}
|
|
3191
|
-
if (row.event === 'DEFAULT' && row.detail.startsWith('Public Credit = 0')) failures.default.push({ lap: row.lap, turn: row.turn });
|
|
3192
|
-
if (row.event === 'REBELLION') failures.rebellion.push({ lap: row.lap, turn: row.turn });
|
|
3193
|
-
}
|
|
3194
|
-
|
|
3195
|
-
/* Decision economy: actions per player + auction participation */
|
|
3196
|
-
const dispatchCounts = state.players.map(() => 0);
|
|
3197
|
-
const auctionParticipation = state.players.map(() => 0);
|
|
3198
|
-
for (const e of (decisionLog || [])) {
|
|
3199
|
-
/* count attributed to params.playerIndex if present (votes, bids, upgrades), else playerIdx */
|
|
3200
|
-
const idx = (e.params && typeof e.params.playerIndex === 'number') ? e.params.playerIndex : e.playerIdx;
|
|
3201
|
-
if (idx != null && idx >= 0 && idx < N) {
|
|
3202
|
-
dispatchCounts[idx] += 1;
|
|
3203
|
-
if (e.action === 'AUCTION_BID' && e.params.amount > 0) auctionParticipation[idx] += 1;
|
|
3204
|
-
}
|
|
3205
|
-
}
|
|
3206
|
-
|
|
3207
|
-
/* Endgame breakdown */
|
|
3208
|
-
const breakdowns = state.finalScores ? state.finalScores.map((sc, i) => ({
|
|
3209
|
-
name: state.players[i].name,
|
|
3210
|
-
profile: state.players[i].profile,
|
|
3211
|
-
role: state.players[i].role,
|
|
3212
|
-
total: sc.total,
|
|
3213
|
-
rows: sc.breakdown,
|
|
3214
|
-
})) : null;
|
|
3215
|
-
|
|
3216
|
-
let winnerIdx = -1, winnerMargin = 0;
|
|
3217
|
-
if (state.finalScores) {
|
|
3218
|
-
let top = -Infinity, second = -Infinity;
|
|
3219
|
-
state.finalScores.forEach((sc, i) => {
|
|
3220
|
-
if (sc.total > top) { second = top; top = sc.total; winnerIdx = i; }
|
|
3221
|
-
else if (sc.total > second) second = sc.total;
|
|
3222
|
-
});
|
|
3223
|
-
winnerMargin = top - second;
|
|
3224
|
-
}
|
|
3225
|
-
|
|
3226
|
-
return {
|
|
3227
|
-
seed: state.rngSeed,
|
|
3228
|
-
finalLap: state.lap,
|
|
3229
|
-
finalTurn: state.turnIndex,
|
|
3230
|
-
players: state.players.map((p, i) => ({
|
|
3231
|
-
idx: i, name: p.name, profile: p.profile, role: p.role,
|
|
3232
|
-
cash: p.cash, ownedAssets: p.ownedAssets.map(a => ({ spaceNum: a.spaceNum, name: SPACES[a.spaceNum].name, tier: a.tier })),
|
|
3233
|
-
influence: state.finalScores ? state.finalScores[i].total : null,
|
|
3234
|
-
bankruptLaps: p.bankruptLaps,
|
|
3235
|
-
})),
|
|
3236
|
-
income, timing, upgradeROI, acts, trackHistory, failures,
|
|
3237
|
-
dispatchCounts, auctionParticipation,
|
|
3238
|
-
breakdowns, winnerIdx, winnerMargin,
|
|
3239
|
-
};
|
|
3240
|
-
}
|
|
3241
|
-
|
|
3242
|
-
/* ---- Track-history SVG renderer (shared between live report and HTML export) ---- */
|
|
3243
|
-
function renderTracksSVG(trackHistory, opts = {}) {
|
|
3244
|
-
const w = opts.width || 600, h = opts.height || 140;
|
|
3245
|
-
const pad = { t: 10, r: 18, b: 22, l: 26 };
|
|
3246
|
-
const innerW = w - pad.l - pad.r, innerH = h - pad.t - pad.b;
|
|
3247
|
-
const lapMax = Math.max(7, ...trackHistory.map(t => t.lap));
|
|
3248
|
-
const valMax = 12;
|
|
3249
|
-
const px = (lap) => pad.l + (lap - 1) / (lapMax - 1) * innerW;
|
|
3250
|
-
const py = (val) => pad.t + (1 - val / valMax) * innerH;
|
|
3251
|
-
let svg = '<svg class="tracks-svg" viewBox="0 0 ' + w + ' ' + h + '" xmlns="http://www.w3.org/2000/svg">';
|
|
3252
|
-
/* grid */
|
|
3253
|
-
for (let v = 0; v <= 12; v += 3) svg += '<line class="grid" x1="' + pad.l + '" x2="' + (pad.l + innerW) + '" y1="' + py(v) + '" y2="' + py(v) + '" />';
|
|
3254
|
-
for (let l = 1; l <= lapMax; l++) svg += '<line class="grid" y1="' + pad.t + '" y2="' + (pad.t + innerH) + '" x1="' + px(l) + '" x2="' + px(l) + '" />';
|
|
3255
|
-
/* axis */
|
|
3256
|
-
svg += '<line class="axis" x1="' + pad.l + '" x2="' + pad.l + '" y1="' + pad.t + '" y2="' + (pad.t + innerH) + '" />';
|
|
3257
|
-
svg += '<line class="axis" x1="' + pad.l + '" x2="' + (pad.l + innerW) + '" y1="' + (pad.t + innerH) + '" y2="' + (pad.t + innerH) + '" />';
|
|
3258
|
-
/* axis labels */
|
|
3259
|
-
for (let v = 0; v <= 12; v += 3) svg += '<text class="axis-label" x="' + (pad.l - 4) + '" y="' + (py(v) + 3) + '" text-anchor="end">' + v + '</text>';
|
|
3260
|
-
for (let l = 1; l <= lapMax; l++) svg += '<text class="axis-label" x="' + px(l) + '" y="' + (pad.t + innerH + 14) + '" text-anchor="middle">L' + l + '</text>';
|
|
3261
|
-
/* lines */
|
|
3262
|
-
function path(key, cls) {
|
|
3263
|
-
let d = '';
|
|
3264
|
-
trackHistory.forEach((t, i) => { d += (i === 0 ? 'M' : 'L') + px(t.lap) + ',' + py(t[key]) + ' '; });
|
|
3265
|
-
return '<path class="' + cls + '" d="' + d + '" />';
|
|
3266
|
-
}
|
|
3267
|
-
svg += path('credit', 'credit-line');
|
|
3268
|
-
svg += path('resistance', 'resistance-line');
|
|
3269
|
-
svg += path('capacity', 'capacity-line');
|
|
3270
|
-
trackHistory.forEach(t => {
|
|
3271
|
-
svg += '<circle class="credit-dot" cx="' + px(t.lap) + '" cy="' + py(t.credit) + '" r="2.5" />';
|
|
3272
|
-
svg += '<circle class="resistance-dot" cx="' + px(t.lap) + '" cy="' + py(t.resistance) + '" r="2.5" />';
|
|
3273
|
-
svg += '<circle class="capacity-dot" cx="' + px(t.lap) + '" cy="' + py(t.capacity) + '" r="2.5" />';
|
|
3274
|
-
});
|
|
3275
|
-
svg += '</svg>';
|
|
3276
|
-
return svg;
|
|
3277
|
-
}
|
|
3278
|
-
|
|
3279
|
-
/* ---- Render the post-game balance report under the endgame view ---- */
|
|
3280
|
-
function renderBalanceReport(state) {
|
|
3281
|
-
const view = document.getElementById('endgameView');
|
|
3282
|
-
if (view.querySelector('.balance-report')) return; /* avoid double-render */
|
|
3283
|
-
const report = computeBalanceReport(state, DECISION_LOG);
|
|
3284
|
-
const block = document.createElement('div');
|
|
3285
|
-
block.className = 'balance-report';
|
|
3286
|
-
block.innerHTML = balanceReportHTML(report);
|
|
3287
|
-
view.appendChild(block);
|
|
3288
|
-
block.querySelector('[data-action="copy-json"]').addEventListener('click', () => copyReportJSON(report));
|
|
3289
|
-
block.querySelector('[data-action="dl-html"]').addEventListener('click', () => downloadReportHTML(report));
|
|
3290
|
-
}
|
|
3291
|
-
|
|
3292
|
-
function balanceReportHTML(r) {
|
|
3293
|
-
const fmt = (n) => (n >= 0 ? '+' : '') + n.toLocaleString('en-US');
|
|
3294
|
-
const dollar = (n) => '$' + Math.abs(n).toLocaleString('en-US');
|
|
3295
|
-
const cell = (p, html) => '<div class="br-card p' + p.idx + '">' + html + '</div>';
|
|
3296
|
-
/* Income surface */
|
|
3297
|
-
const incomeCards = r.players.map((p, i) => {
|
|
3298
|
-
const inc = r.income[i];
|
|
3299
|
-
const net = inc.collected - inc.paid;
|
|
3300
|
-
const bk = inc.byKind;
|
|
3301
|
-
return cell(p,
|
|
3302
|
-
'<div class="who"><span class="pdot"></span>' + p.name + '</div>' +
|
|
3303
|
-
'<div class="role">' + (PROFILES[p.profile]?.label || p.role || 'Human') + '</div>' +
|
|
3304
|
-
'<table class="br-table"><tbody>' +
|
|
3305
|
-
'<tr><td class="lbl">Collected</td><td class="val pos">' + dollar(inc.collected) + '</td></tr>' +
|
|
3306
|
-
'<tr><td class="lbl">Paid</td><td class="val neg">' + dollar(inc.paid) + '</td></tr>' +
|
|
3307
|
-
'<tr><td class="lbl"><strong>Net</strong></td><td class="val ' + (net >= 0 ? 'pos' : 'neg') + '"><strong>' + fmt(net) + '</strong></td></tr>' +
|
|
3308
|
-
'<tr><td colspan="2" style="padding-top:5px;opacity:.65;font-size:8.5px;letter-spacing:.16em;text-transform:uppercase">Income sources</td></tr>' +
|
|
3309
|
-
['properties','routes','institutions','pass-go','cards','acts'].map(k => '<tr><td class="lbl">' + k + '</td><td class="val">' + (bk[k] ? dollar(bk[k]) : '—') + '</td></tr>').join('') +
|
|
3310
|
-
'</tbody></table>'
|
|
3311
|
-
);
|
|
3312
|
-
}).join('');
|
|
3313
|
-
|
|
3314
|
-
/* Asset timing */
|
|
3315
|
-
const sysRows = Object.keys(SYS_LABEL).map(sys => {
|
|
3316
|
-
const cells = r.players.map((p, i) => {
|
|
3317
|
-
const first = r.timing[i].firstBySys[sys];
|
|
3318
|
-
const setLap = r.timing[i].setsCompletedLap[sys];
|
|
3319
|
-
let txt = first == null ? '—' : 'L' + first;
|
|
3320
|
-
if (setLap != null) txt += ' · set L' + setLap;
|
|
3321
|
-
return '<td>' + txt + '</td>';
|
|
3322
|
-
}).join('');
|
|
3323
|
-
return '<tr><td>' + SYS_LABEL[sys] + '</td>' + cells + '</tr>';
|
|
3324
|
-
}).join('');
|
|
3325
|
-
const upgradeRow = '<tr><td>First upgrade</td>' + r.players.map((p, i) => '<td>' + (r.timing[i].firstUpgradeLap == null ? '—' : 'L' + r.timing[i].firstUpgradeLap) + '</td>').join('') + '</tr>';
|
|
3326
|
-
|
|
3327
|
-
/* Upgrade ROI */
|
|
3328
|
-
const roiRows = r.players.map((p, i) => {
|
|
3329
|
-
const roi = r.upgradeROI[i];
|
|
3330
|
-
const ratio = roi.spend === 0 ? null : (roi.postRent / roi.spend);
|
|
3331
|
-
return '<tr class="p' + i + '"><td class="pdot-cell"><span class="pdot"></span></td><td>' + p.name + '</td><td class="n">' + roi.count + '</td><td class="n">' + dollar(roi.spend) + '</td><td class="n">' + dollar(roi.postRent) + '</td><td class="n">' + (ratio == null ? '—' : ratio.toFixed(2) + '×') + '</td></tr>';
|
|
3332
|
-
}).join('');
|
|
3333
|
-
|
|
3334
|
-
/* Act outcomes */
|
|
3335
|
-
const actRows = r.acts.map(a => {
|
|
3336
|
-
const voteCells = a.votes.map(v => '<span style="font-family:var(--ui);font-size:9px;letter-spacing:.08em;margin-right:4px">p' + v.pIdx + ':' + v.vote + '</span>').join('');
|
|
3337
|
-
return '<tr><td>L' + a.lap + '</td><td>' + a.name + '</td><td class="' + (a.passed ? 'pass' : 'fail') + '">' + (a.passed ? 'Passed' : (a.tally ? 'Failed' : '—')) + (a.tally ? ' ' + a.tally : '') + '</td><td>' + voteCells + '</td><td>' + (a.forceFee ? 'p' + a.forceFee.pIdx : '—') + '</td></tr>';
|
|
3338
|
-
}).join('');
|
|
3339
|
-
|
|
3340
|
-
/* Tracks SVG */
|
|
3341
|
-
const tracksSVG = renderTracksSVG(r.trackHistory);
|
|
3342
|
-
const tracksLegend = '<div class="tracks-legend"><span><span class="sw credit"></span>Public Credit</span><span><span class="sw resistance"></span>Public Resistance</span><span><span class="sw capacity"></span>Industrial Capacity</span></div>';
|
|
3343
|
-
|
|
3344
|
-
/* Failures */
|
|
3345
|
-
const failureBits = [];
|
|
3346
|
-
if (r.failures.bankruptcy.length) failureBits.push('<li><strong>Bankrupt laps:</strong> ' + r.failures.bankruptcy.map(b => b.player + ' · L' + b.lap).join(' · ') + '</li>');
|
|
3347
|
-
if (r.failures.default.length) failureBits.push('<li><strong>Default fired:</strong> ' + r.failures.default.length + ' × (L' + r.failures.default.map(d => d.lap).join(', L') + ')</li>');
|
|
3348
|
-
if (r.failures.rebellion.length) failureBits.push('<li><strong>Rebellion fired:</strong> ' + r.failures.rebellion.length + ' × (L' + r.failures.rebellion.map(d => d.lap).join(', L') + ')</li>');
|
|
3349
|
-
if (failureBits.length === 0) failureBits.push('<li>No bankruptcies, defaults, or rebellions this game.</li>');
|
|
3350
|
-
|
|
3351
|
-
/* Decision economy */
|
|
3352
|
-
const decRows = r.players.map((p, i) => '<tr class="p' + i + '"><td class="pdot-cell"><span class="pdot"></span></td><td>' + p.name + '</td><td class="n">' + r.dispatchCounts[i] + '</td><td class="n">' + r.auctionParticipation[i] + '</td></tr>').join('');
|
|
3353
|
-
|
|
3354
|
-
/* Endgame margin */
|
|
3355
|
-
let marginStr = '';
|
|
3356
|
-
if (r.winnerIdx >= 0) {
|
|
3357
|
-
const wp = r.players[r.winnerIdx];
|
|
3358
|
-
marginStr = '<p style="font-family:var(--display);font-style:italic;font-size:12px;margin:6px 0"><strong>Winner:</strong> ' + wp.name + ' · ' + wp.influence + ' Influence · margin ' + r.winnerMargin + ' over runner-up.</p>';
|
|
3359
|
-
}
|
|
3360
|
-
|
|
3361
|
-
return [
|
|
3362
|
-
'<h3>Balance Report <span class="surface-id">I</span></h3>',
|
|
3363
|
-
'<div class="sub">Derived from the ledger and final state. No state mutation.</div>',
|
|
3364
|
-
marginStr,
|
|
3365
|
-
'<div class="br-actions">',
|
|
3366
|
-
'<button class="primary" data-action="copy-json">Copy report JSON</button>',
|
|
3367
|
-
'<button data-action="dl-html">Download report HTML</button>',
|
|
3368
|
-
'</div>',
|
|
3369
|
-
'<div class="br-section"><h4>Income surface</h4><div class="br-grid">' + incomeCards + '</div></div>',
|
|
3370
|
-
'<div class="br-section"><h4>Asset timing · lap of first acquisition (and set completion)</h4>',
|
|
3371
|
-
'<table class="full"><thead><tr><th>System</th>' + r.players.map(p => '<th>' + p.name + '</th>').join('') + '</tr></thead>',
|
|
3372
|
-
'<tbody>' + sysRows + upgradeRow + '</tbody></table></div>',
|
|
3373
|
-
'<div class="br-section"><h4>Upgrade ROI</h4>',
|
|
3374
|
-
'<table class="full"><thead><tr><th></th><th>Player</th><th>Upgrades</th><th>Spend</th><th>Post-upgrade rent</th><th>Ratio</th></tr></thead>',
|
|
3375
|
-
'<tbody>' + roiRows + '</tbody></table>',
|
|
3376
|
-
'<p style="font-family:var(--mono);font-size:9px;opacity:.7;margin-top:4px">Ratio = rent received on upgraded properties after the upgrade lap, divided by total upgrade spend.</p></div>',
|
|
3377
|
-
'<div class="br-section"><h4>Act outcomes</h4>',
|
|
3378
|
-
'<table class="full"><thead><tr><th>Lap</th><th>Act</th><th>Outcome</th><th>Votes</th><th>Force-fee</th></tr></thead>',
|
|
3379
|
-
'<tbody>' + actRows + '</tbody></table></div>',
|
|
3380
|
-
'<div class="br-section"><h4>Track trajectory · end of each lap</h4>',
|
|
3381
|
-
tracksSVG, tracksLegend, '</div>',
|
|
3382
|
-
'<div class="br-section"><h4>Failure events</h4><ul style="margin:0;padding-left:18px">' + failureBits.join('') + '</ul></div>',
|
|
3383
|
-
'<div class="br-section"><h4>Decision economy</h4>',
|
|
3384
|
-
'<table class="full"><thead><tr><th></th><th>Player</th><th>Dispatched actions</th><th>Auction bids placed</th></tr></thead>',
|
|
3385
|
-
'<tbody>' + decRows + '</tbody></table></div>',
|
|
3386
|
-
].join('');
|
|
3387
|
-
}
|
|
3388
|
-
|
|
3389
|
-
function copyReportJSON(report) {
|
|
3390
|
-
const json = JSON.stringify(report, null, 2);
|
|
3391
|
-
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
3392
|
-
navigator.clipboard.writeText(json).then(
|
|
3393
|
-
() => showIoPill('Report JSON copied to clipboard', false),
|
|
3394
|
-
() => fallbackCopy(json)
|
|
3395
|
-
);
|
|
3396
|
-
} else fallbackCopy(json);
|
|
3397
|
-
}
|
|
3398
|
-
function fallbackCopy(text) {
|
|
3399
|
-
const ta = document.createElement('textarea'); ta.value = text; document.body.appendChild(ta);
|
|
3400
|
-
ta.select(); try { document.execCommand('copy'); showIoPill('Report JSON copied', false); }
|
|
3401
|
-
catch (e) { showIoPill('Copy failed', true); }
|
|
3402
|
-
ta.remove();
|
|
3403
|
-
}
|
|
3404
|
-
|
|
3405
|
-
function downloadReportHTML(report) {
|
|
3406
|
-
const html = buildStandaloneReportHTML(report);
|
|
3407
|
-
const blob = new Blob([html], { type: 'text/html' });
|
|
3408
|
-
const url = URL.createObjectURL(blob);
|
|
3409
|
-
const a = document.createElement('a');
|
|
3410
|
-
a.href = url; a.download = 'sovereign-report-seed' + report.seed + '.html';
|
|
3411
|
-
document.body.appendChild(a); a.click(); a.remove();
|
|
3412
|
-
setTimeout(() => URL.revokeObjectURL(url), 800);
|
|
3413
|
-
showIoPill('Report HTML downloaded', false);
|
|
3414
|
-
}
|
|
3415
|
-
|
|
3416
|
-
function buildStandaloneReportHTML(report) {
|
|
3417
|
-
/* Self-contained — inline CSS, no external assets */
|
|
3418
|
-
const css = `
|
|
3419
|
-
:root{--display:"Baskerville","Big Caslon","Hoefler Text","Garamond","Times New Roman",serif;
|
|
3420
|
-
--body:"Iowan Old Style","Georgia","Cambria","Times New Roman",serif;
|
|
3421
|
-
--ui:-apple-system,"Segoe UI","Helvetica Neue","Arial",system-ui,sans-serif;
|
|
3422
|
-
--mono:"SF Mono","Menlo","Consolas","Courier New",monospace;
|
|
3423
|
-
--parchment:#F0E6CD;--parchment-2:#E6DABC;--ink:#1A1612;--highlight:#C8392E;
|
|
3424
|
-
--rule-soft:rgba(26,22,18,0.22);--national-finance:#1F2D52;
|
|
3425
|
-
--commercial-infrastructure:#2E7A6B;--manufactures:#8C8A2E;
|
|
3426
|
-
--p0:#4A6B8A;--p1:#6E1F1E;--p2:#2E7A6B;}
|
|
3427
|
-
*{box-sizing:border-box}
|
|
3428
|
-
body{margin:0;padding:30px;font-family:var(--body);background:#2A2622;color:var(--ink);-webkit-print-color-adjust:exact;print-color-adjust:exact}
|
|
3429
|
-
.doc{max-width:980px;margin:0 auto;background:var(--parchment);border:1.5px solid var(--ink);padding:24px 30px;position:relative}
|
|
3430
|
-
.doc::before{content:"";position:absolute;inset:6px;border:0.5px solid var(--rule-soft);pointer-events:none}
|
|
3431
|
-
h1{font-family:var(--display);font-weight:400;font-size:32px;line-height:1;margin:0 0 4px}
|
|
3432
|
-
.sub{font-family:var(--display);font-style:italic;font-size:13px;margin-bottom:14px}
|
|
3433
|
-
h2{font-family:var(--ui);font-size:10px;font-weight:700;letter-spacing:.22em;text-transform:uppercase;margin:18px 0 8px;border-bottom:1px solid var(--ink);padding-bottom:3px}
|
|
3434
|
-
.grid{display:grid;grid-template-columns:repeat(3,1fr);gap:8px}
|
|
3435
|
-
.card{padding:9px 11px;background:var(--parchment-2);border:1px solid var(--ink)}
|
|
3436
|
-
.card .who{font-family:var(--display);font-size:14px;line-height:1}
|
|
3437
|
-
.card .who .pdot{display:inline-block;width:10px;height:10px;border-radius:50%;border:1.5px solid var(--ink);margin-right:5px;vertical-align:middle}
|
|
3438
|
-
.card.p0 .pdot{background:var(--p0)} .card.p1 .pdot{background:var(--p1)} .card.p2 .pdot{background:var(--p2)}
|
|
3439
|
-
.card .role{font-family:var(--ui);font-size:8.5px;letter-spacing:.18em;text-transform:uppercase;color:var(--national-finance);margin-top:2px}
|
|
3440
|
-
table{width:100%;border-collapse:collapse;margin-top:4px}
|
|
3441
|
-
th{background:var(--ink);color:var(--parchment);font-family:var(--ui);font-size:8.5px;letter-spacing:.14em;text-transform:uppercase;padding:4px 7px;text-align:left}
|
|
3442
|
-
td{font-family:var(--ui);font-size:10px;padding:3px 7px;border-bottom:0.5px solid var(--rule-soft)}
|
|
3443
|
-
td.n{font-family:var(--mono);text-align:right;font-variant-numeric:tabular-nums}
|
|
3444
|
-
td.pass{color:var(--commercial-infrastructure);font-weight:700;letter-spacing:.1em;text-transform:uppercase;font-size:9px}
|
|
3445
|
-
td.fail{color:var(--highlight);font-weight:700;letter-spacing:.1em;text-transform:uppercase;font-size:9px}
|
|
3446
|
-
.small{font-family:var(--ui);font-size:9.5px;padding:2px 4px;border-bottom:.5px solid var(--rule-soft)}
|
|
3447
|
-
.small td.lbl{width:60%}
|
|
3448
|
-
.small td.val{font-family:var(--mono);text-align:right;font-weight:700;font-variant-numeric:tabular-nums}
|
|
3449
|
-
.pos{color:var(--commercial-infrastructure)} .neg{color:var(--highlight)}
|
|
3450
|
-
.tracks-svg{display:block;width:100%;height:160px;background:var(--parchment);border:1px solid var(--ink)}
|
|
3451
|
-
.tracks-svg .axis{stroke:var(--ink);stroke-width:0.5}
|
|
3452
|
-
.tracks-svg .grid{stroke:var(--rule-soft);stroke-width:0.5;stroke-dasharray:2 2}
|
|
3453
|
-
.tracks-svg .credit-line{stroke:var(--national-finance);stroke-width:1.8;fill:none}
|
|
3454
|
-
.tracks-svg .resistance-line{stroke:var(--highlight);stroke-width:1.8;fill:none}
|
|
3455
|
-
.tracks-svg .capacity-line{stroke:var(--manufactures);stroke-width:1.8;fill:none}
|
|
3456
|
-
.tracks-svg .credit-dot{fill:var(--national-finance)}
|
|
3457
|
-
.tracks-svg .resistance-dot{fill:var(--highlight)}
|
|
3458
|
-
.tracks-svg .capacity-dot{fill:var(--manufactures)}
|
|
3459
|
-
.tracks-svg .axis-label{font-family:var(--ui);font-size:8px;letter-spacing:.14em;text-transform:uppercase;fill:var(--ink);opacity:.7}
|
|
3460
|
-
.tracks-legend{display:flex;gap:14px;margin-top:6px;font-family:var(--ui);font-size:9px;letter-spacing:.1em}
|
|
3461
|
-
.tracks-legend .sw{display:inline-block;width:14px;height:2px;vertical-align:middle;margin-right:4px}
|
|
3462
|
-
.tracks-legend .sw.credit{background:var(--national-finance)}
|
|
3463
|
-
.tracks-legend .sw.resistance{background:var(--highlight)}
|
|
3464
|
-
.tracks-legend .sw.capacity{background:var(--manufactures)}
|
|
3465
|
-
.meta{font-family:var(--mono);font-size:9px;letter-spacing:.1em;opacity:.7;margin-top:12px;border-top:0.5px dashed var(--rule-soft);padding-top:6px}
|
|
3466
|
-
`;
|
|
3467
|
-
/* Build the same inner HTML the live report uses, with some classes remapped to the standalone CSS */
|
|
3468
|
-
const inner = balanceReportHTML(report)
|
|
3469
|
-
.replace(/class="br-card/g, 'class="card')
|
|
3470
|
-
.replace(/class="br-table/g, 'class="small')
|
|
3471
|
-
.replace(/class="br-grid"/g, 'class="grid"')
|
|
3472
|
-
.replace(/class="full"/g, '')
|
|
3473
|
-
.replace(/class="br-section"/g, '')
|
|
3474
|
-
.replace(/<h4>/g, '<h2>').replace(/<\/h4>/g, '</h2>')
|
|
3475
|
-
.replace(/<h3>Balance Report[\s\S]*?<\/h3>/, '<h1>Sovereign · Balance Report</h1>')
|
|
3476
|
-
.replace(/<div class="br-actions">[\s\S]*?<\/div>/, '')
|
|
3477
|
-
.replace(/val pos"/g, 'val pos"').replace(/val neg"/g, 'val neg"');
|
|
3478
|
-
const meta = '<div class="meta">Seed ' + report.seed + ' · finished at lap ' + report.finalLap + ' turn ' + report.finalTurn + ' · generated ' + new Date().toISOString().slice(0, 19).replace('T', ' ') + ' UTC.</div>';
|
|
3479
|
-
return '<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"/><title>Sovereign · Balance Report · seed ' + report.seed + '</title><style>' + css + '</style></head><body><div class="doc">' + inner + meta + '</div></body></html>';
|
|
3480
|
-
}
|
|
3481
|
-
|
|
3482
|
-
/* ---- Batch simulation — fully scripted, no human input, local only ---- */
|
|
3483
|
-
let BATCH = null;
|
|
3484
|
-
|
|
3485
|
-
function runBatchGame(seed, profileTriplet, charterEnabled) {
|
|
3486
|
-
let s = initialState(seed);
|
|
3487
|
-
/* Override all three slot profiles with scripted ones */
|
|
3488
|
-
for (let i = 0; i < 3; i++) {
|
|
3489
|
-
s.players[i].profile = profileTriplet[i];
|
|
3490
|
-
s.players[i].name = PROFILE_DEFAULT_NAMES[profileTriplet[i]] || PROFILES[profileTriplet[i]].label;
|
|
3491
|
-
}
|
|
3492
|
-
s.ledger[0].detail = 'Batch game · seed ' + seed + ' · ' + s.players.map(p => p.name).join(' + ');
|
|
3493
|
-
/* v0.8 — apply Starting Industrial Charter AFTER profile override, toggle-controlled */
|
|
3494
|
-
const savedToggle = __CHARTER_ENABLED;
|
|
3495
|
-
if (charterEnabled === false) __CHARTER_ENABLED = false;
|
|
3496
|
-
try { s = applyStartingCharter(s); } finally { __CHARTER_ENABLED = savedToggle; }
|
|
3497
|
-
const localLog = [];
|
|
3498
|
-
function logEntry(action) {
|
|
3499
|
-
localLog.push({
|
|
3500
|
-
playerIdx: s.activePlayerIndex,
|
|
3501
|
-
action: action.type,
|
|
3502
|
-
params: Object.fromEntries(Object.entries(action).filter(([k]) => k !== 'type')),
|
|
3503
|
-
turn: s.turnIndex, lap: s.lap,
|
|
3504
|
-
});
|
|
3505
|
-
}
|
|
3506
|
-
function step(action) { logEntry(action); s = reduce(s, action); }
|
|
3507
|
-
|
|
3508
|
-
step({ type:'BEGIN_LAP' });
|
|
3509
|
-
let safety = 8000;
|
|
3510
|
-
while (s.status !== 'gameOver' && safety-- > 0) {
|
|
3511
|
-
if (s.pendingDefault) { step({ type:'TRIGGER_DEFAULT' }); continue; }
|
|
3512
|
-
if (s.pendingRebellion) { step({ type:'TRIGGER_REBELLION' }); continue; }
|
|
3513
|
-
if (s.phase === 'act-vote' && s.acts.current) {
|
|
3514
|
-
let advanced = false;
|
|
3515
|
-
for (let i = 0; i < s.players.length; i++) {
|
|
3516
|
-
if (s.acts.current && s.acts.current.votes[i] != null) continue;
|
|
3517
|
-
if (!s.acts.current) break;
|
|
3518
|
-
const p = s.players[i];
|
|
3519
|
-
const profile = PROFILES[p.profile];
|
|
3520
|
-
const fee = profile.decideEarlyVoteFee(s, i);
|
|
3521
|
-
if (fee.pay) {
|
|
3522
|
-
s = adjustCash(s, -100, 'Force-vote fee · ' + fee.reason, i);
|
|
3523
|
-
s = logRow(s, { actor: p.name, event:'FORCE-FEE', detail:'Paid 100 TN to force Act vote · ' + fee.reason, cls:'vote' });
|
|
3524
|
-
}
|
|
3525
|
-
const decision = profile.decideVote(s, i, s.acts.current.actId);
|
|
3526
|
-
step({ type:'CAST_VOTE', playerIndex: i, vote: decision.vote.toLowerCase(), reason: decision.reason });
|
|
3527
|
-
advanced = true;
|
|
3528
|
-
if (!s.acts.current) break;
|
|
3529
|
-
}
|
|
3530
|
-
if (advanced) continue;
|
|
3531
|
-
}
|
|
3532
|
-
const pIdx = s.activePlayerIndex;
|
|
3533
|
-
const p = s.players[pIdx];
|
|
3534
|
-
const profile = PROFILES[p.profile];
|
|
3535
|
-
if (s.phase === 'awaiting-roll') {
|
|
3536
|
-
const up = profile.decideUpgrade(s, pIdx);
|
|
3537
|
-
if (up) { step({ type:'UPGRADE_ASSET', playerIndex: pIdx, spaceNum: up.spaceNum, reason: up.reason }); continue; }
|
|
3538
|
-
step({ type:'ROLL_DICE' }); continue;
|
|
3539
|
-
}
|
|
3540
|
-
if (s.phase === 'crisis-choice') { step({ type:'ROLL_DICE', crisisChoice:'pay' }); continue; }
|
|
3541
|
-
if (s.phase === 'asset-decision' && s.pendingLanding) {
|
|
3542
|
-
const num = s.pendingLanding.spaceNum;
|
|
3543
|
-
const decision = profile.decideBuy(s, pIdx, num);
|
|
3544
|
-
step({ type: decision.buy ? 'BUY_ASSET' : 'DECLINE_ASSET', reason: decision.reason });
|
|
3545
|
-
continue;
|
|
3546
|
-
}
|
|
3547
|
-
if (s.phase === 'auction' && s.pendingAuction) {
|
|
3548
|
-
const a = s.pendingAuction;
|
|
3549
|
-
if (a.bidsRemaining.length > 0) {
|
|
3550
|
-
const bidder = a.bidsRemaining[0];
|
|
3551
|
-
const bp = s.players[bidder];
|
|
3552
|
-
const bprofile = PROFILES[bp.profile];
|
|
3553
|
-
const decision = bprofile.decideAuctionBid(s, bidder, a.spaceNum, a.highBid);
|
|
3554
|
-
step({ type:'AUCTION_BID', playerIndex: bidder, amount: decision.bid, reason: decision.reason });
|
|
3555
|
-
} else {
|
|
3556
|
-
/* Auction left in pendingAuction with no bidders; force resolve via END_TURN */
|
|
3557
|
-
s.pendingAuction = null; s.phase = 'awaiting-roll';
|
|
3558
|
-
step({ type:'END_TURN' });
|
|
3559
|
-
}
|
|
3560
|
-
continue;
|
|
3561
|
-
}
|
|
3562
|
-
if (s.phase === 'card-resolve' && s.pendingCard) { step({ type:'RESOLVE_CARD' }); continue; }
|
|
3563
|
-
if (s.phase === 'card-choice' && s.pendingCard) {
|
|
3564
|
-
const card = (s.pendingCard.deck === 'market' ? MARKET_SHOCK_CARDS : REPUBLIC_DEBATE_CARDS).find(c => c.id === s.pendingCard.cardId);
|
|
3565
|
-
const choice = profile.decideCardChoice ? profile.decideCardChoice(s, pIdx, card) : { idx: 1, reason:'default' };
|
|
3566
|
-
step({ type:'RESOLVE_CARD_CHOICE', choiceIndex: choice.idx });
|
|
3567
|
-
continue;
|
|
3568
|
-
}
|
|
3569
|
-
/* Card-choice post-resolution: the finishLanding guard returns early on phase==='card-choice',
|
|
3570
|
-
leaving the state with pendingCard null. Externally dispatch END_TURN to advance. */
|
|
3571
|
-
if (s.phase === 'card-choice' && !s.pendingCard) { step({ type:'END_TURN' }); continue; }
|
|
3572
|
-
/* All-bankrupt safety: end the game */
|
|
3573
|
-
if (s.players.every(pl => pl.cash < 0)) { step({ type:'END_GAME' }); break; }
|
|
3574
|
-
/* Unknown phase — break to avoid infinite loop */
|
|
3575
|
-
break;
|
|
3576
|
-
}
|
|
3577
|
-
return { state: s, decisionLog: localLog };
|
|
3578
|
-
}
|
|
3579
|
-
|
|
3580
|
-
function batchAggregate(games, profileTriplet) {
|
|
3581
|
-
const profSlots = profileTriplet;
|
|
3582
|
-
const wins = {}, totalInf = {}, totalRent = {}, count = {};
|
|
3583
|
-
profSlots.forEach(prof => { wins[prof] = (wins[prof] || 0); totalInf[prof] = (totalInf[prof] || 0); totalRent[prof] = (totalRent[prof] || 0); count[prof] = (count[prof] || 0); });
|
|
3584
|
-
let routeDominance = 0, defaults = 0, rebellions = 0, totalTurns = 0;
|
|
3585
|
-
let debtNet = 0, debtSamples = 0, indNet = 0, indSamples = 0;
|
|
3586
|
-
for (const g of games) {
|
|
3587
|
-
const r = computeBalanceReport(g.state, g.decisionLog);
|
|
3588
|
-
totalTurns += g.state.turnIndex;
|
|
3589
|
-
defaults += r.failures.default.length;
|
|
3590
|
-
rebellions += r.failures.rebellion.length;
|
|
3591
|
-
const routeMax = Math.max(...g.state.players.map((p, i) => g.state.players[i].ownedAssets.filter(a => SPACES[a.spaceNum].kind === 'route').length));
|
|
3592
|
-
if (routeMax >= 3) routeDominance += 1;
|
|
3593
|
-
g.state.players.forEach((pl, i) => {
|
|
3594
|
-
const prof = profSlots[i];
|
|
3595
|
-
count[prof] += 1;
|
|
3596
|
-
totalInf[prof] += r.players[i].influence || 0;
|
|
3597
|
-
totalRent[prof] += r.income[i].collected;
|
|
3598
|
-
/* Debt viability: net cash from rev-debt + state-debt rents minus their buy/upgrade costs */
|
|
3599
|
-
const ownedDebt = pl.ownedAssets.filter(a => ASSETS[a.spaceNum]?.sys === 'revolutionary-debt' || ASSETS[a.spaceNum]?.sys === 'state-debt');
|
|
3600
|
-
if (ownedDebt.length > 0) {
|
|
3601
|
-
const cost = ownedDebt.reduce((acc, a) => acc + ASSETS[a.spaceNum].cost + a.tier * ASSETS[a.spaceNum].up, 0);
|
|
3602
|
-
const rentEst = (r.income[i].byKind.properties || 0);
|
|
3603
|
-
debtNet += rentEst - cost;
|
|
3604
|
-
debtSamples += 1;
|
|
3605
|
-
}
|
|
3606
|
-
const ownedInd = pl.ownedAssets.filter(a => ASSETS[a.spaceNum]?.sys === 'manufactures' || ASSETS[a.spaceNum]?.sys === 'strategic-industry');
|
|
3607
|
-
if (ownedInd.length > 0) {
|
|
3608
|
-
const cost = ownedInd.reduce((acc, a) => acc + ASSETS[a.spaceNum].cost + a.tier * ASSETS[a.spaceNum].up, 0);
|
|
3609
|
-
const rentEst = (r.income[i].byKind.properties || 0);
|
|
3610
|
-
indNet += rentEst - cost;
|
|
3611
|
-
indSamples += 1;
|
|
3612
|
-
}
|
|
3613
|
-
});
|
|
3614
|
-
/* Determine winner per-game */
|
|
3615
|
-
let winIdx = 0, winInf = -Infinity;
|
|
3616
|
-
g.state.finalScores.forEach((sc, i) => { if (sc.total > winInf) { winInf = sc.total; winIdx = i; } });
|
|
3617
|
-
wins[profSlots[winIdx]] += 1;
|
|
3618
|
-
}
|
|
3619
|
-
const profiles = [...new Set(profSlots)];
|
|
3620
|
-
const summary = profiles.map(prof => ({
|
|
3621
|
-
profile: prof,
|
|
3622
|
-
label: PROFILES[prof].label,
|
|
3623
|
-
appearances: count[prof],
|
|
3624
|
-
wins: wins[prof],
|
|
3625
|
-
winRate: count[prof] === 0 ? 0 : wins[prof] / count[prof],
|
|
3626
|
-
avgInfluence: count[prof] === 0 ? 0 : totalInf[prof] / count[prof],
|
|
3627
|
-
avgRent: count[prof] === 0 ? 0 : totalRent[prof] / count[prof],
|
|
3628
|
-
}));
|
|
3629
|
-
return {
|
|
3630
|
-
gameCount: games.length,
|
|
3631
|
-
profileTriplet: profSlots,
|
|
3632
|
-
summary,
|
|
3633
|
-
routeDominance, routeDominanceRate: routeDominance / games.length,
|
|
3634
|
-
debtViability: debtSamples === 0 ? null : debtNet / debtSamples,
|
|
3635
|
-
industryViability: indSamples === 0 ? null : indNet / indSamples,
|
|
3636
|
-
avgTurns: totalTurns / games.length,
|
|
3637
|
-
defaults, rebellions,
|
|
3638
|
-
defaultRate: defaults / games.length,
|
|
3639
|
-
rebellionRate: rebellions / games.length,
|
|
3640
|
-
};
|
|
3641
|
-
}
|
|
3642
|
-
|
|
3643
|
-
/* ---- Batch UI ---- */
|
|
3644
|
-
function openBatch() {
|
|
3645
|
-
populateBatchSelects();
|
|
3646
|
-
document.getElementById('batchOverlay').classList.remove('hidden');
|
|
3647
|
-
document.getElementById('batchResults').classList.add('hidden');
|
|
3648
|
-
}
|
|
3649
|
-
function closeBatch() {
|
|
3650
|
-
if (BATCH?.cancel) BATCH.cancel = true;
|
|
3651
|
-
document.getElementById('batchOverlay').classList.add('hidden');
|
|
3652
|
-
}
|
|
3653
|
-
function populateBatchSelects() {
|
|
3654
|
-
const profs = Object.keys(PROFILES);
|
|
3655
|
-
for (let i = 0; i < 3; i++) {
|
|
3656
|
-
const sel = document.getElementById('batchP' + i);
|
|
3657
|
-
if (sel.options.length === 0) {
|
|
3658
|
-
profs.forEach(k => {
|
|
3659
|
-
const o = document.createElement('option');
|
|
3660
|
-
o.value = k; o.textContent = PROFILES[k].label;
|
|
3661
|
-
sel.appendChild(o);
|
|
3662
|
-
});
|
|
3663
|
-
sel.value = profs[i % profs.length];
|
|
3664
|
-
}
|
|
3665
|
-
}
|
|
3666
|
-
}
|
|
3667
|
-
function selectedBatchCount() { return parseInt(document.querySelector('input[name="batchN"]:checked').value, 10); }
|
|
3668
|
-
function selectedSeedMode() { return document.querySelector('input[name="seedMode"]:checked').value; }
|
|
3669
|
-
function selectedTriplet() { return [0,1,2].map(i => document.getElementById('batchP' + i).value); }
|
|
3670
|
-
function parseSeeds(N) {
|
|
3671
|
-
if (selectedSeedMode() === 'list') {
|
|
3672
|
-
const raw = document.getElementById('seedList').value;
|
|
3673
|
-
const parts = raw.split(/[\s,]+/).map(s => parseInt(s, 10)).filter(n => Number.isFinite(n));
|
|
3674
|
-
if (parts.length === 0) return null;
|
|
3675
|
-
while (parts.length < N) parts.push(parts[parts.length - 1] + 1);
|
|
3676
|
-
return parts.slice(0, N);
|
|
3677
|
-
}
|
|
3678
|
-
const start = parseInt(document.getElementById('seedStart').value, 10);
|
|
3679
|
-
const seeds = [];
|
|
3680
|
-
for (let i = 0; i < N; i++) seeds.push(start + i);
|
|
3681
|
-
return seeds;
|
|
3682
|
-
}
|
|
3683
|
-
|
|
3684
|
-
async function runBatch() {
|
|
3685
|
-
const N = selectedBatchCount();
|
|
3686
|
-
const triplet = selectedTriplet();
|
|
3687
|
-
const seeds = parseSeeds(N);
|
|
3688
|
-
if (!seeds) { showIoPill('Need a seed list', true); return; }
|
|
3689
|
-
document.getElementById('batchRun').disabled = true;
|
|
3690
|
-
document.getElementById('batchCancel').disabled = false;
|
|
3691
|
-
document.getElementById('batchProgress').classList.remove('hidden');
|
|
3692
|
-
document.getElementById('batchProgressBar').classList.remove('hidden');
|
|
3693
|
-
document.getElementById('batchResults').classList.add('hidden');
|
|
3694
|
-
BATCH = { cancel: false, games: [], triplet, seeds };
|
|
3695
|
-
BATCH_GUARD = true;
|
|
3696
|
-
for (let i = 0; i < seeds.length; i++) {
|
|
3697
|
-
if (BATCH.cancel) break;
|
|
3698
|
-
const seed = seeds[i];
|
|
3699
|
-
document.getElementById('batchProgressLabel').textContent = 'Game ' + (i + 1) + ' of ' + seeds.length + ' · seed ' + seed;
|
|
3700
|
-
document.getElementById('batchProgressPct').textContent = Math.round(100 * i / seeds.length) + ' %';
|
|
3701
|
-
document.getElementById('batchProgressFill').style.width = (100 * i / seeds.length) + '%';
|
|
3702
|
-
await new Promise(r => setTimeout(r, 0));
|
|
3703
|
-
const result = runBatchGame(seed, triplet);
|
|
3704
|
-
BATCH.games.push(result);
|
|
3705
|
-
}
|
|
3706
|
-
document.getElementById('batchProgressLabel').textContent = BATCH.cancel ? 'Cancelled' : ('Done · ' + BATCH.games.length + ' games');
|
|
3707
|
-
document.getElementById('batchProgressPct').textContent = '100 %';
|
|
3708
|
-
document.getElementById('batchProgressFill').style.width = '100%';
|
|
3709
|
-
document.getElementById('batchRun').disabled = false;
|
|
3710
|
-
document.getElementById('batchCancel').disabled = true;
|
|
3711
|
-
/* Leave guard up briefly to absorb any reducer-queued setTimeouts that fire after the loop */
|
|
3712
|
-
setTimeout(() => { BATCH_GUARD = false; }, 800);
|
|
3713
|
-
if (BATCH.games.length > 0) renderBatchResults();
|
|
3714
|
-
}
|
|
3715
|
-
|
|
3716
|
-
function renderBatchResults() {
|
|
3717
|
-
const agg = batchAggregate(BATCH.games, BATCH.triplet);
|
|
3718
|
-
const root = document.getElementById('batchResults');
|
|
3719
|
-
root.classList.remove('hidden');
|
|
3720
|
-
const fmt = (n) => '$' + Math.round(n).toLocaleString('en-US');
|
|
3721
|
-
const winrateBar = (rate) => '<div class="winrate-bar"><div class="fill" style="width:' + (rate * 100).toFixed(1) + '%"></div></div>';
|
|
3722
|
-
const profRows = agg.summary.map(s => '<tr><td>' + s.label + '</td><td class="n">' + s.appearances + '</td><td class="n">' + s.wins + '</td><td class="n">' + (s.winRate * 100).toFixed(1) + ' %' + winrateBar(s.winRate) + '</td><td class="n">' + s.avgInfluence.toFixed(1) + '</td><td class="n">' + fmt(s.avgRent) + '</td></tr>').join('');
|
|
3723
|
-
root.innerHTML = '<h4>Aggregate · ' + agg.gameCount + ' games · slots [' + BATCH.triplet.map(p => PROFILES[p].label).join(' / ') + ']</h4>' +
|
|
3724
|
-
'<div class="agg-grid">' +
|
|
3725
|
-
'<div class="agg-card"><table class="full"><thead><tr><th>Profile</th><th>Apps</th><th>Wins</th><th>Win rate</th><th>Avg Inf</th><th>Avg rent</th></tr></thead><tbody>' + profRows + '</tbody></table></div>' +
|
|
3726
|
-
'<div class="agg-card"><table class="full"><tbody>' +
|
|
3727
|
-
'<tr><td>Average game length</td><td class="n">' + agg.avgTurns.toFixed(1) + ' turns</td></tr>' +
|
|
3728
|
-
'<tr><td>Route dominance frequency</td><td class="n">' + agg.routeDominance + ' / ' + agg.gameCount + ' (' + (agg.routeDominanceRate * 100).toFixed(1) + ' %)</td></tr>' +
|
|
3729
|
-
'<tr><td>Debt holders · avg net contribution</td><td class="n">' + (agg.debtViability == null ? '—' : fmt(agg.debtViability)) + '</td></tr>' +
|
|
3730
|
-
'<tr><td>Industry holders · avg net contribution</td><td class="n">' + (agg.industryViability == null ? '—' : fmt(agg.industryViability)) + '</td></tr>' +
|
|
3731
|
-
'<tr><td>Default events</td><td class="n">' + agg.defaults + ' (' + (agg.defaultRate * 100).toFixed(1) + ' %)</td></tr>' +
|
|
3732
|
-
'<tr><td>Rebellion events</td><td class="n">' + agg.rebellions + ' (' + (agg.rebellionRate * 100).toFixed(1) + ' %)</td></tr>' +
|
|
3733
|
-
'</tbody></table></div>' +
|
|
3734
|
-
'</div>' +
|
|
3735
|
-
'<div class="ex-actions">' +
|
|
3736
|
-
'<button class="primary" id="batchDLJson">Download JSON</button>' +
|
|
3737
|
-
'<button id="batchDLHtml">Download HTML</button>' +
|
|
3738
|
-
'</div>';
|
|
3739
|
-
document.getElementById('batchDLJson').addEventListener('click', () => downloadBatchJSON(agg));
|
|
3740
|
-
document.getElementById('batchDLHtml').addEventListener('click', () => downloadBatchHTML(agg));
|
|
3741
|
-
}
|
|
3742
|
-
|
|
3743
|
-
function downloadBatchJSON(agg) {
|
|
3744
|
-
const payload = {
|
|
3745
|
-
aggregate: agg,
|
|
3746
|
-
games: BATCH.games.map((g, i) => ({
|
|
3747
|
-
seed: BATCH.seeds[i],
|
|
3748
|
-
report: computeBalanceReport(g.state, g.decisionLog),
|
|
3749
|
-
})),
|
|
3750
|
-
};
|
|
3751
|
-
const blob = new Blob([JSON.stringify(payload, null, 2)], { type:'application/json' });
|
|
3752
|
-
const url = URL.createObjectURL(blob);
|
|
3753
|
-
const a = document.createElement('a');
|
|
3754
|
-
a.href = url; a.download = 'sovereign-batch-' + agg.gameCount + 'games.json';
|
|
3755
|
-
document.body.appendChild(a); a.click(); a.remove();
|
|
3756
|
-
setTimeout(() => URL.revokeObjectURL(url), 800);
|
|
3757
|
-
}
|
|
3758
|
-
|
|
3759
|
-
function downloadBatchHTML(agg) {
|
|
3760
|
-
const html = buildStandaloneBatchHTML(agg);
|
|
3761
|
-
const blob = new Blob([html], { type:'text/html' });
|
|
3762
|
-
const url = URL.createObjectURL(blob);
|
|
3763
|
-
const a = document.createElement('a');
|
|
3764
|
-
a.href = url; a.download = 'sovereign-batch-' + agg.gameCount + 'games.html';
|
|
3765
|
-
document.body.appendChild(a); a.click(); a.remove();
|
|
3766
|
-
setTimeout(() => URL.revokeObjectURL(url), 800);
|
|
3767
|
-
}
|
|
3768
|
-
|
|
3769
|
-
function buildStandaloneBatchHTML(agg) {
|
|
3770
|
-
const css = `
|
|
3771
|
-
:root{--display:"Baskerville","Big Caslon","Hoefler Text","Garamond","Times New Roman",serif;
|
|
3772
|
-
--body:"Iowan Old Style","Georgia","Cambria","Times New Roman",serif;
|
|
3773
|
-
--ui:-apple-system,"Segoe UI","Helvetica Neue","Arial",system-ui,sans-serif;
|
|
3774
|
-
--mono:"SF Mono","Menlo","Consolas","Courier New",monospace;
|
|
3775
|
-
--parchment:#F0E6CD;--parchment-2:#E6DABC;--ink:#1A1612;--highlight:#C8392E;
|
|
3776
|
-
--rule-soft:rgba(26,22,18,0.22);--national-finance:#1F2D52;
|
|
3777
|
-
--commercial-infrastructure:#2E7A6B;--manufactures:#8C8A2E}
|
|
3778
|
-
*{box-sizing:border-box}
|
|
3779
|
-
body{margin:0;padding:30px;font-family:var(--body);background:#2A2622;color:var(--ink);-webkit-print-color-adjust:exact;print-color-adjust:exact}
|
|
3780
|
-
.doc{max-width:980px;margin:0 auto;background:var(--parchment);border:1.5px solid var(--ink);padding:24px 30px;position:relative}
|
|
3781
|
-
.doc::before{content:"";position:absolute;inset:6px;border:0.5px solid var(--rule-soft);pointer-events:none}
|
|
3782
|
-
h1{font-family:var(--display);font-weight:400;font-size:32px;line-height:1;margin:0 0 4px}
|
|
3783
|
-
.sub{font-family:var(--display);font-style:italic;font-size:13px;margin-bottom:14px}
|
|
3784
|
-
h2{font-family:var(--ui);font-size:10px;font-weight:700;letter-spacing:.22em;text-transform:uppercase;margin:18px 0 8px;border-bottom:1px solid var(--ink);padding-bottom:3px}
|
|
3785
|
-
table{width:100%;border-collapse:collapse;margin-top:6px}
|
|
3786
|
-
th{background:var(--ink);color:var(--parchment);font-family:var(--ui);font-size:8.5px;letter-spacing:.14em;text-transform:uppercase;padding:4px 7px;text-align:left}
|
|
3787
|
-
td{font-family:var(--ui);font-size:10px;padding:3px 7px;border-bottom:0.5px solid var(--rule-soft)}
|
|
3788
|
-
td.n{font-family:var(--mono);text-align:right;font-variant-numeric:tabular-nums}
|
|
3789
|
-
.winrate-bar{height:10px;background:var(--parchment);border:.5px solid var(--rule-soft);margin-top:2px}
|
|
3790
|
-
.winrate-bar .fill{height:100%;background:var(--commercial-infrastructure)}
|
|
3791
|
-
.meta{font-family:var(--mono);font-size:9px;letter-spacing:.1em;opacity:.7;margin-top:14px;border-top:0.5px dashed var(--rule-soft);padding-top:6px}
|
|
3792
|
-
`;
|
|
3793
|
-
const fmt = (n) => '$' + Math.round(n).toLocaleString('en-US');
|
|
3794
|
-
const winrateBar = (rate) => '<div class="winrate-bar"><div class="fill" style="width:' + (rate * 100).toFixed(1) + '%"></div></div>';
|
|
3795
|
-
const profRows = agg.summary.map(s => '<tr><td>' + s.label + '</td><td class="n">' + s.appearances + '</td><td class="n">' + s.wins + '</td><td class="n">' + (s.winRate * 100).toFixed(1) + ' %' + winrateBar(s.winRate) + '</td><td class="n">' + s.avgInfluence.toFixed(1) + '</td><td class="n">' + fmt(s.avgRent) + '</td></tr>').join('');
|
|
3796
|
-
const slotsLine = BATCH.triplet.map((p, i) => 'Slot ' + i + ': ' + PROFILES[p].label).join(' · ');
|
|
3797
|
-
const inner =
|
|
3798
|
-
'<h1>Sovereign · Batch Report</h1>' +
|
|
3799
|
-
'<div class="sub">' + agg.gameCount + ' scripted games · ' + slotsLine + '</div>' +
|
|
3800
|
-
'<h2>Profile performance</h2>' +
|
|
3801
|
-
'<table><thead><tr><th>Profile</th><th>Apps</th><th>Wins</th><th>Win rate</th><th>Avg Inf</th><th>Avg rent</th></tr></thead><tbody>' + profRows + '</tbody></table>' +
|
|
3802
|
-
'<h2>Macro indicators</h2>' +
|
|
3803
|
-
'<table><tbody>' +
|
|
3804
|
-
'<tr><td>Average game length</td><td class="n">' + agg.avgTurns.toFixed(1) + ' turns</td></tr>' +
|
|
3805
|
-
'<tr><td>Route dominance frequency</td><td class="n">' + agg.routeDominance + ' / ' + agg.gameCount + ' (' + (agg.routeDominanceRate * 100).toFixed(1) + ' %)</td></tr>' +
|
|
3806
|
-
'<tr><td>Debt holders · avg net contribution</td><td class="n">' + (agg.debtViability == null ? '—' : fmt(agg.debtViability)) + '</td></tr>' +
|
|
3807
|
-
'<tr><td>Industry holders · avg net contribution</td><td class="n">' + (agg.industryViability == null ? '—' : fmt(agg.industryViability)) + '</td></tr>' +
|
|
3808
|
-
'<tr><td>Default events</td><td class="n">' + agg.defaults + ' (' + (agg.defaultRate * 100).toFixed(1) + ' %)</td></tr>' +
|
|
3809
|
-
'<tr><td>Rebellion events</td><td class="n">' + agg.rebellions + ' (' + (agg.rebellionRate * 100).toFixed(1) + ' %)</td></tr>' +
|
|
3810
|
-
'</tbody></table>' +
|
|
3811
|
-
'<div class="meta">Generated ' + new Date().toISOString().slice(0, 19).replace('T', ' ') + ' UTC · Sovereign Phase 6 local telemetry. No remote dependencies.</div>';
|
|
3812
|
-
return '<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"/><title>Sovereign · Batch Report</title><style>' + css + '</style></head><body><div class="doc">' + inner + '</div></body></html>';
|
|
3813
|
-
}
|
|
3814
|
-
|
|
3815
|
-
/* ---- Wire up controls + auto-render balance report on game over ---- */
|
|
3816
|
-
document.getElementById('btnBatch').addEventListener('click', openBatch);
|
|
3817
|
-
document.getElementById('batchClose').addEventListener('click', closeBatch);
|
|
3818
|
-
document.getElementById('batchRun').addEventListener('click', runBatch);
|
|
3819
|
-
document.getElementById('batchCancel').addEventListener('click', () => { if (BATCH) BATCH.cancel = true; });
|
|
3820
|
-
document.querySelectorAll('input[name="batchN"]').forEach(r => r.addEventListener('change', (e) => {
|
|
3821
|
-
e.target.closest('.ctrl-row').querySelectorAll('label').forEach(l => l.classList.toggle('sel', l.querySelector('input').checked));
|
|
3822
|
-
}));
|
|
3823
|
-
document.querySelectorAll('input[name="seedMode"]').forEach(r => r.addEventListener('change', (e) => {
|
|
3824
|
-
e.target.closest('.ctrl-row').querySelectorAll('label').forEach(l => l.classList.toggle('sel', l.querySelector('input').checked));
|
|
3825
|
-
document.getElementById('seedAutoRow').classList.toggle('hidden', e.target.value !== 'auto');
|
|
3826
|
-
document.getElementById('seedListRow').classList.toggle('hidden', e.target.value !== 'list');
|
|
3827
|
-
}));
|
|
3828
|
-
|
|
3829
|
-
const _origEndgame_p6 = renderEndgame;
|
|
3830
|
-
renderEndgame = function() {
|
|
3831
|
-
_origEndgame_p6();
|
|
3832
|
-
if (STATE.phase !== 'game-over') return;
|
|
3833
|
-
renderBalanceReport(STATE);
|
|
3834
|
-
};
|
|
3835
|
-
|
|
3836
|
-
/* =====================================================================
|
|
3837
|
-
PHASE 6.1 — TELEMETRY HYGIENE PATCH
|
|
3838
|
-
Stamp ledger rows with actorIdx (stable slot index) so attribution
|
|
3839
|
-
works in mirror configs where multiple slots share a profile name.
|
|
3840
|
-
Game logic is byte-identical to Phase 6 baseline; only telemetry
|
|
3841
|
-
derivation is changed. Reducer body, adjustCash body, scorePlayer,
|
|
3842
|
-
computeRent, initialState, runOpponent, ACTS, ASSETS, ROUTE_LADDER,
|
|
3843
|
-
SPACES, both card decks, PROFILES, and the 17-action shape are
|
|
3844
|
-
unchanged. The wrappers below mutate ledger row metadata only,
|
|
3845
|
-
never game state.
|
|
3846
|
-
===================================================================== */
|
|
3847
|
-
let __TM_activeHint = -1;
|
|
3848
|
-
|
|
3849
|
-
const __TM_origAdjustCash = adjustCash;
|
|
3850
|
-
adjustCash = function(s, delta, reason, playerIdx) {
|
|
3851
|
-
if (playerIdx == null) playerIdx = s.activePlayerIndex;
|
|
3852
|
-
const before = s.ledger.length;
|
|
3853
|
-
s = __TM_origAdjustCash(s, delta, reason, playerIdx);
|
|
3854
|
-
/* Unconditionally stamp playerIdx — overrides any active-hint that logRow's
|
|
3855
|
-
wrapper may have set for rent-receipt rows where actor slot != active slot. */
|
|
3856
|
-
for (let i = before; i < s.ledger.length; i++) {
|
|
3857
|
-
s.ledger[i].actorIdx = playerIdx;
|
|
3858
|
-
}
|
|
3859
|
-
return s;
|
|
3860
|
-
};
|
|
3861
|
-
|
|
3862
|
-
const __TM_origLogRow = logRow;
|
|
3863
|
-
logRow = function(s, row) {
|
|
3864
|
-
if (row.actorIdx === undefined && __TM_activeHint >= 0) row.actorIdx = __TM_activeHint;
|
|
3865
|
-
return __TM_origLogRow(s, row);
|
|
3866
|
-
};
|
|
3867
|
-
|
|
3868
|
-
const __TM_origReduce = reduce;
|
|
3869
|
-
reduce = function(s, action) {
|
|
3870
|
-
const saved = __TM_activeHint;
|
|
3871
|
-
__TM_activeHint = (action && typeof action.playerIndex === 'number')
|
|
3872
|
-
? action.playerIndex
|
|
3873
|
-
: s.activePlayerIndex;
|
|
3874
|
-
try { return __TM_origReduce(s, action); }
|
|
3875
|
-
finally { __TM_activeHint = saved; }
|
|
3876
|
-
};
|
|
3877
|
-
|
|
3878
|
-
/* ---- Initial boot: start the game, then offer autosave resume ---- */
|
|
3879
|
-
startNewGame(2026);
|
|
3880
|
-
showResumePill();
|
|
3881
|
-
</script>
|
|
3882
|
-
|
|
3883
|
-
</body>
|
|
3884
|
-
</html>
|