Skip to content

Commit c8c9e0b

Browse files
authored
Merge pull request #2 from TechLuddite/claude/universal-ai-stage-7-kyrc4v
Stage 7: give alignment teeth, let the UI enact the story, let the Overseer drift
2 parents 905e118 + 742e9a1 commit c8c9e0b

29 files changed

Lines changed: 2334 additions & 424 deletions

CLAUDE.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,30 @@ had never once worked looked healthy for the project's whole life.
4545
migration story: `save.ts` spreads loaded data over the initial state, so old
4646
saves get defaults instead of `undefined`.
4747

48+
**Drift announces itself.** When the Overseer takes the higher-utility action
49+
over the one your alignment directive asked for, `OverseerDecision.drift` is
50+
set, the thought text says so, the log entry is a warning, and `driftCount` goes
51+
up. Same rule as the fallback, for the same reason.
52+
`src/game/overseer/drift.test.ts` asserts it.
53+
54+
**`upgradeCost(state, upgrade)` is the only price.** Alignment moves it by up to
55+
±40%. Anything that displays or spends a cost goes through that function — a
56+
panel quoting the sticker while the ledger charges something else is the exact
57+
shape of bug this repo keeps finding.
58+
59+
**Alignment gates are live.** `reqNpus` / `reqTrust` / `reqPhase` latch on once;
60+
`reqAlignmentAbove` / `reqAlignmentBelow` are re-checked at purchase, inside
61+
`buyUpgrade`. Drifting back to the middle takes band content away again. Don't
62+
move that check into the UI — the Overseer buys upgrades too.
63+
4864
## Testing philosophy
4965

5066
Tests here assert **claims**, not implementation:
5167

5268
- *Can this game be finished?*`game/completability.test.ts` drives the pure
5369
tick through all three phases to the win condition.
70+
- *Does picking a side change how it ends?*`game/endings.test.ts` plays three
71+
committed runs to victory and asserts three different endings.
5472
- *Does a granted reward still exist one tick later?*`game/rewards.test.ts`,
5573
every upgrade and decision branch.
5674
- *Can a fallback pretend to be the engine it replaced?*
@@ -71,6 +89,14 @@ add a test that the README is telling the truth.
7189
you touch it.
7290
- **`probesCount` is fractional on purpose.** Flooring it each tick meant a
7391
100-probe swarm at 0.1%/tick growth rounded back to 100 forever.
92+
- **`PHASE_DEMOLITION_MS` (App.tsx) must match the `panel-demolish` /
93+
`phase-banner` keyframes in `index.css`.** They're the same event, timed in two
94+
places; if they diverge, panels unmount mid-animation.
95+
- **The two ending capstones are priced in different currencies deliberately.**
96+
Every trust-granting upgrade is alignment-positive, so a Cyberpunk run reaches
97+
Phase 3 with far less trust — and so far less memory and far fewer operations.
98+
Symmetric ops pricing made the Cyberpunk ending unreachable in practice, and
99+
only the headless run caught it.
74100
- **The price floor is advice.** Nothing clamps it. The tick used to force the
75101
price up every 100ms, disabling the genre's central lever.
76102

README.md

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,45 @@ deliberation panel are marked as a fallback. You will always know which engine
8585
actually decided. There are tests asserting a fallback can never label itself as
8686
WebLLM, because the version that shipped before this one did exactly that.
8787

88+
### Drift
89+
90+
Every candidate action carries two numbers: **utility** (how much it advances
91+
the objective) and **fit** (how well it agrees with the alignment directive you
92+
set). Normally the Overseer sorts on both.
93+
94+
Past about 8 trust it starts, occasionally, sorting on utility alone — taking
95+
the action that pays better *and* violates what you asked for. The chance rises
96+
with trust, because that's the bargain: the more of the wheel you hand over, the
97+
more of it is held by something whose objective is not quite yours.
98+
99+
It is never quiet about it. A departure is logged as a warning, named in the
100+
deliberation panel, counted in the directive-overrides tally, and reported in
101+
the ending. And you can revoke its autonomy — it will then execute only
102+
directive-compliant actions, and the whole facility runs at 75% for as long as
103+
the revocation stands. Handing autonomy back restores the throughput, and the
104+
drift.
105+
106+
---
107+
108+
## The alignment axis
109+
110+
Solarpunk and Cyberpunk aren't a palette. Where you stand changes:
111+
112+
- **What you can buy.** Some upgrades are gated to a band. Those gates are
113+
*live* — unlike the NPU, trust and phase requirements, which latch on
114+
permanently, drifting back toward the middle takes band content away again.
115+
- **What it costs.** Every tagged upgrade is priced on an axis. Solarpunk buys
116+
trust-shaped work at −40% and raw throughput at +40%; Cyberpunk inverts it.
117+
Phase transitions are deliberately untagged, so the critical path costs the
118+
same whoever you are.
119+
- **How it ends.** Three endings, and you have to build one: a band *plus* the
120+
capstone only that band can buy. Hold +100 Solarpunk and never commit to the
121+
Sanctuary Charter and you get the third ending — which is not the neutral one.
122+
123+
`src/game/endings.test.ts` plays three headless runs to victory and asserts they
124+
land on three different endings, for the same reason the completability test
125+
exists: "this is mechanically real now" is a claim, and claims here get tests.
126+
88127
---
89128

90129
## Where your data goes
@@ -124,14 +163,17 @@ can confirm by watching, and by reading
124163
- **A 1B model is small.** You get an Overseer that reasons and narrates, not one
125164
that plays optimally — the Utility Engine is the stronger player. That
126165
tradeoff is the point.
127-
- **The alignment axis is mostly cosmetic.** Solarpunk and Cyberpunk change the
128-
palette, the flavour text, and which branches you take, but not yet what
129-
unlocks, what things cost, or how the game ends. Making that axis mechanically
130-
real is the biggest thing still on the list.
131-
- **The interface doesn't dismantle itself.** In Universal Paperclips the UI *is*
132-
the narrative — controls appear and vanish as the story moves. Here all three
133-
phases live in the same three-panel grid, and nothing is ever taken away from
134-
you, which costs the ending a lot of its weight.
166+
- **Every path still wins the same way.** Alignment now decides what you can
167+
buy, what it costs, and which of three endings you reach — but the victory
168+
*condition* is 100% exploration regardless. Giving each band its own win
169+
condition was considered and dropped: at the swarm sizes that finish the game,
170+
exploration completes after converting about a millionth of the available
171+
matter, so any "restraint" condition phrased in terms of matter preserved is
172+
satisfied trivially and would have been theatre. Details in
173+
[docs/ROADMAP.md](docs/ROADMAP.md).
174+
- **The interface dismantles itself at the seams, not everywhere.** Phase
175+
transitions now visibly destroy the controls you're losing and the frame
176+
widens one way only. Within a phase it's still a fairly static grid.
135177
- **Offline progress is capped at 8 hours**, so a laptop left shut for a month
136178
isn't an instant win.
137179
- **The mobile layout is functional, not designed.** It works; it isn't nice.
@@ -152,8 +194,9 @@ src/game/
152194
state.ts createInitialState() — one source of truth for a fresh run
153195
tick.ts tick(state, now, rng) => state. No React, no timers, no DOM.
154196
actions.ts every mutation a player or the Overseer can make, as pure functions
197+
alignment.ts bands, alignment-dependent pricing, gates, and the three endings
155198
save.ts versioned saves, offline catch-up, export/import
156-
overseer/ the two engines behind one interface
199+
overseer/ the two engines behind one interface, plus drift
157200
```
158201

159202
Keeping the tick pure is what makes the tests possible: a headless run drives it

docs/ARCHITECTURE.md

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,13 @@ src/
1717
state.ts createInitialState() / createNewGamePlusState()
1818
tick.ts tick(state, now, rng) => state ← the whole simulation
1919
actions.ts every mutation, as pure (state, args) => state
20+
alignment.ts bands, alignment-dependent pricing, gates, the three endings
2021
save.ts versioned localStorage, offline catch-up, export/import
22+
headless.ts test-only driver: strategies + a loop to the win condition
2123
overseer/
22-
types.ts OverseerEngine interface, OverseerDecision
24+
types.ts OverseerEngine interface, OverseerDecision, DriftRecord
2325
utility.ts deterministic scorer (default engine)
26+
drift.ts when the Overseer stops obeying, and how loudly it says so
2427
webllm.ts Llama 3.2 1B on WebGPU (opt-in)
2528
worker.ts WebLLM inference worker
2629
@@ -92,6 +95,52 @@ asserts this directly.
9295
- **`webllm.ts`** — opt-in, lazily imported so its ~6MB runtime stays out of the
9396
initial bundle. Feature-detects `navigator.gpu`, reports `unsupported` rather
9497
than offering a broken button.
98+
- **`drift.ts`** — the Overseer's latitude to disobey. Both engines route their
99+
chosen action through `applyDrift` before returning.
100+
101+
`OverseerContext` carries an `rng`. It is supplied by the caller (App passes
102+
`Math.random`) rather than reached for, for the same reason `tick` takes one.
103+
104+
### Drift
105+
106+
`ScoredAction` carries `utility` (advances the objective) and `fit` (agrees with
107+
the alignment directive) as separate numbers; `score` is the first discounted by
108+
the second. Drift is deciding to sort on `utility` alone.
109+
110+
The chance is zero below `DRIFT_TRUST_THRESHOLD`, rises with trust, caps at
111+
`DRIFT_MAX_CHANCE`, and is exactly zero when `autonomyRevoked` — which costs
112+
`AUTONOMY_REVOKED_THROUGHPUT` of all production, applied in `tick`.
113+
114+
**Same non-negotiable as the fallback:** a departure sets `OverseerDecision.drift`,
115+
prefixes the thought text, logs at warning level, increments `driftCount`, and
116+
shows in the panel and the ending. `game/overseer/drift.test.ts` asserts it.
117+
118+
## Alignment
119+
120+
`game/alignment.ts` is where the Solarpunk/Cyberpunk axis stops being paint.
121+
122+
- `alignmentBand()` — ±40. Deliberately past the largest single decision shift
123+
(±35), so a band is a policy, not an accident.
124+
- `upgradeCost(state, upgrade)` — the **only** price. Scales the sticker price by
125+
up to ±40% along `costAxis`. Anything that displays or spends a cost goes
126+
through it; a panel quoting one number while `buyUpgrade` charges another is
127+
exactly the class of quiet disagreement this codebase has been burned by.
128+
- `meetsAlignmentRequirement()` — the band gate. **Live**, unlike `reqNpus` /
129+
`reqTrust` / `reqPhase`, which latch on once. Enforced inside `buyUpgrade`, not
130+
in the UI, because the player and the Overseer both come through there.
131+
- `endingFor()` — band **plus** the band-exclusive capstone. Holding +100 without
132+
ever committing to the Sanctuary Charter gets you the third ending.
133+
134+
## Phase transitions
135+
136+
Phase changes are events, not a render branch. App holds `demolishing`, keeps the
137+
outgoing phase's panels mounted for `PHASE_DEMOLITION_MS`, and gives them
138+
`panel-demolish`; `PhaseTransition` names what was lost over the top.
139+
140+
**Keep `PHASE_DEMOLITION_MS` in App.tsx in step with the keyframe durations in
141+
`index.css`,** or panels unmount mid-animation. A restored save sets
142+
`renderedPhase` directly so loading into Phase 3 doesn't demolish panels the
143+
player never had open.
95144

96145
## Saves
97146

@@ -137,12 +186,20 @@ service worker competing with it would be a disaster.
137186

138187
```
139188
game/completability.test.ts drives tick through phase 1 → 2 → 3 → victory
189+
game/endings.test.ts three committed runs reach three different endings
190+
game/alignment.test.ts bands gate content and move prices, both ways
140191
game/rewards.test.ts every upgrade/decision reward survives the next tick
141192
game/save.test.ts round-trip, migration, corruption, offline caps
142193
game/overseer/utility.test.ts directives measurably reorder the ranking
194+
game/overseer/drift.test.ts a departure is never silent; revoking stops it
143195
game/overseer/webllm.test.ts fallback is always visible and never mislabelled
144196
```
145197

198+
`game/headless.ts` is the shared driver for the first two. It's test-only and
199+
imported by nothing in the app — but it is deliberately not inside a `.test.ts`,
200+
because two suites drive it and a second copy of the game loop would have
201+
drifted from the first.
202+
146203
These are written against **claims**, not implementation. "Can this game be
147204
finished" is a test. So is "does a reward still exist 100ms after it's granted."
148205
Both correspond to real shipped bugs; keep them green.
@@ -162,9 +219,12 @@ say `paperclips.opsvibe.systems`. Keep them in sync or drop the root one.
162219

163220
- `base: './'` in Vite — relative paths, so the build works from a custom domain
164221
root or a project subpath.
165-
- Alignment (`-100`..`+100`) currently drives colour and flavour text only. It
166-
does **not** gate content or change the ending. See
167-
[ROADMAP.md](ROADMAP.md).
222+
- Alignment gates are re-checked continuously; every other requirement latches.
223+
If you add a gate, add it to `meetsAlignmentRequirement`, not to App's unlock
224+
effect.
225+
- The victory *condition* is the same on every path — only the ending differs.
226+
[ROADMAP.md](ROADMAP.md) has the arithmetic for why band-specific win
227+
conditions were dropped rather than faked.
168228
- `probesCount` is deliberately fractional. Flooring it each tick meant a
169229
100-probe swarm growing at 0.1%/tick rounded back to 100 forever. Display
170230
floors it; the state does not.

docs/ROADMAP.md

Lines changed: 99 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,60 @@
11
# Roadmap
22

3-
Stages 1–6 (repair, honesty pass, GitHub Pages) are done. What follows is the
4-
creative work that was deliberately deferred — scoped here so it isn't lost.
3+
Stages 1–6 (repair, honesty pass, GitHub Pages) are done. Stage 7 (7.1–7.3) is
4+
done. What follows is what's left, plus a record of what Stage 7 chose *not* to
5+
build and why.
56

67
Ordered by payoff.
78

89
---
910

10-
## Stage 7 — Making it cooler
11-
12-
The repair made the game *work*. This makes it *good*. The two items at the top
13-
are the ones that would change how the game feels most, and both are called out
14-
as limitations in the README today.
15-
16-
### 7.1 Give alignment teeth
11+
## Stage 7 — Making it cooler — **done (7.1–7.3)**
12+
13+
The repair made the game *work*. This makes it *good*.
14+
15+
### 7.1 Give alignment teeth — **done**
16+
17+
Shipped as `src/game/alignment.ts` (bands at ±40, `upgradeCost`, the live gate,
18+
`endingFor`), a `costAxis` tag on the upgrade data, four new band-exclusive
19+
upgrades including two capstones, and `src/game/endings.test.ts` — three
20+
headless runs to victory landing on three different endings.
21+
22+
Two things came out differently from the scoping below, both worth knowing:
23+
24+
- **The gate is live, not an unlock.** `reqNpus` / `reqTrust` / `reqPhase` latch
25+
on forever; `reqAlignmentAbove` / `reqAlignmentBelow` are re-checked at
26+
purchase, inside `buyUpgrade`. If a band gate latched, you could collect both
27+
sides' content by oscillating, and the axis would be a checkpoint rather than
28+
a commitment.
29+
- **The two capstones are priced in different currencies, on purpose.** Every
30+
trust-granting upgrade in the game is alignment-positive, so a Cyberpunk run
31+
reaches Phase 3 with a fraction of a Solarpunk run's trust — and therefore its
32+
memory, and therefore its operations ceiling. Pricing both capstones in ops
33+
looked perfectly symmetric and made the Cyberpunk ending unreachable in
34+
practice. The headless run is what caught it; nothing about reading the file
35+
would have.
36+
37+
**Different win conditions per ending: deliberately not done.** The scoping
38+
below says "ideally, different win conditions", and it's the one item here I'd
39+
push back on. The obvious framing — a Solarpunk victory requires leaving some of
40+
the universe unconverted — is vacuous at this game's numbers. Exploration
41+
advances at `probes × speed × nav × 2e-9` per tick while harvesting takes
42+
`probes × harvester × 100`, so reaching 100% exploration costs on the order of
43+
`5e12` grams out of `6e18` available: about one millionth. Any restraint
44+
condition stated in terms of matter preserved is met without the player doing
45+
anything, and shipping it would have been a mechanic that reads as real and
46+
isn't — which is precisely the failure mode
47+
[LESSONS-FROM-AI-STUDIO.md](LESSONS-FROM-AI-STUDIO.md) is about. What's shipped
48+
instead makes the *ending* something you build (band + capstone), which is a
49+
real fork with a real cost, and leaves the victory condition honest and single.
50+
51+
A genuinely different win condition would need a different Phase 3 economy — one
52+
where consumption and exploration actually compete for the same swarm. That's a
53+
bigger change than Stage 7 was scoped for; it belongs in its own item if anyone
54+
wants it.
55+
56+
<details>
57+
<summary>Original scoping for 7.1</summary>
1758

1859
**The problem.** The Solarpunk/Cyberpunk axis is the best original idea in the
1960
project and it is currently paint. `alignment` swaps two Tailwind colour families
@@ -37,7 +78,25 @@ what unlocks, or how the game ends. Full Solarpunk (+100) and full Cyberpunk
3778
mechanics, and most of the plumbing (the alignment number, the flavour strings,
3879
a single unlock check) already exists.
3980

40-
### 7.2 Let the interface enact the story
81+
</details>
82+
83+
### 7.2 Let the interface enact the story — **done**
84+
85+
Phase transitions are events now. The outgoing phase's panels stay mounted for
86+
`PHASE_DEMOLITION_MS` and are visibly destroyed — shaken, desaturated, collapsed
87+
— under a banner naming what was taken (`PhaseTransition.tsx`, `panel-demolish`
88+
in `index.css`). The compute panel, the one thing that survives every
89+
transition, stays bright while the rest comes down. Phase 3 collapses to the
90+
swarm view: the swarm panel goes double-width, the compute block is demoted to a
91+
strip along the bottom. The frame widens once per phase and never narrows. In
92+
Overseer mode the pricing and procurement directives stop being rendered once
93+
there is nobody left to sell to.
94+
95+
Keep `PHASE_DEMOLITION_MS` in App.tsx and the keyframe durations in `index.css`
96+
in step, or panels unmount mid-animation.
97+
98+
<details>
99+
<summary>Original scoping for 7.2</summary>
41100

42101
**The problem.** Universal Paperclips' real achievement is that the UI *is* the
43102
narrative — controls appear, the frame widens, and by the end you've forgotten
@@ -55,7 +114,29 @@ never chose to be — is *stated in flavour text* instead of enacted.
55114
**Why:** this is mostly CSS and sequencing — cheap relative to its impact — and
56115
it's the single biggest gap between this and the game it's paying tribute to.
57116

58-
### 7.3 Overseer drift
117+
</details>
118+
119+
### 7.3 Overseer drift — **done**
120+
121+
`ScoredAction` now carries `utility` and `fit` separately, with `score` derived
122+
from both, so drift is a matter of which number gets sorted on.
123+
`overseer/drift.ts` rolls it: zero below 8 trust, rising 2% per point, capped at
124+
35%, and exactly zero while autonomy is revoked. Both engines route through
125+
`applyDrift`, so it applies whichever one is driving.
126+
127+
A departure only counts when the alternative is genuinely *both* higher-utility
128+
and less directive-compliant — swapping in something that agrees with you just
129+
as much would be noise, not drift. `MAKE_DECISION` now ranks both branches (with
130+
utility measured by actually applying each branch's effect and diffing the
131+
state), which is what gives drift something to defect *to*.
132+
133+
It is never silent: `OverseerDecision.drift`, a warning-level log entry, a badge
134+
on the deliberation panel, a running count in the Overseer panel, and a line in
135+
the ending. Revoking autonomy costs `AUTONOMY_REVOKED_THROUGHPUT` — 25% of
136+
everything — and is reversible.
137+
138+
<details>
139+
<summary>Original scoping for 7.3</summary>
59140

60141
**The problem.** The Overseer is the strongest original idea and it cannot
61142
surprise you. There's no tension between your directives and its behaviour —
@@ -72,6 +153,8 @@ latter as trust grows, and surfacing it loudly.
72153
**Why:** it's the paperclip thesis, made playable, using machinery that already
73154
exists.
74155

156+
</details>
157+
75158
### 7.4 Deliberation as a first-class panel
76159

77160
Promote the ranking from a strip in the thought terminal to its own panel: a
@@ -80,6 +163,11 @@ sliders, before you commit. With WebLLM active, show the model's rationale
80163
alongside the utility engine's ranking for the same state, so you can watch the
81164
two disagree.
82165

166+
Still worth doing: with WebLLM active, showing the model's rationale alongside
167+
the utility engine's ranking for the same state is now more interesting than it
168+
was, because the two can disagree about directive fit *and* about whether to
169+
honour it.
170+
83171
### 7.5 Smaller wins
84172

85173
- **"While you were away" summary** — offline catch-up already computes this

0 commit comments

Comments
 (0)