Compare commits
12 Commits
fix/ci-a11
...
burn/14-17
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
771c760ba7 | ||
| 729343e503 | |||
| 1081b9e6c4 | |||
|
|
e74f956bf4 | ||
| 55f280d056 | |||
|
|
6446ecb43a | ||
| 0a312b111d | |||
|
|
141b240d69 | ||
| 093f7688bd | |||
| c4a31255a4 | |||
|
|
c876a35dc0 | ||
|
|
3d851a8708 |
1
.ci-trigger
Normal file
1
.ci-trigger
Normal file
@@ -0,0 +1 @@
|
||||
# Trivial file to re-trigger CI after stale run
|
||||
@@ -10,12 +10,11 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Validate ARIA Attributes in game.js
|
||||
- name: Validate ARIA Attributes in JS
|
||||
run: |
|
||||
echo "Checking game.js for ARIA attributes..."
|
||||
grep -q "aria-label" game.js || (echo "ERROR: aria-label missing from game.js" && exit 1)
|
||||
grep -q "aria-valuenow" game.js || (echo "ERROR: aria-valuenow missing from game.js" && exit 1)
|
||||
grep -q "aria-pressed" game.js || (echo "ERROR: aria-pressed missing from game.js" && exit 1)
|
||||
echo "Checking js/*.js for ARIA attributes..."
|
||||
grep -rq "aria-label" js/ || (echo "ERROR: aria-label missing from js/" && exit 1)
|
||||
grep -rq "aria-pressed" js/ || (echo "ERROR: aria-pressed missing from js/" && exit 1)
|
||||
|
||||
- name: Validate ARIA Roles in index.html
|
||||
run: |
|
||||
@@ -24,4 +23,7 @@ jobs:
|
||||
|
||||
- name: Syntax Check JS
|
||||
run: |
|
||||
node -c game.js
|
||||
for f in js/*.js; do
|
||||
echo "Syntax check: $f"
|
||||
node -c "$f" || exit 1
|
||||
done
|
||||
|
||||
@@ -8,6 +8,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
@@ -20,5 +23,9 @@ jobs:
|
||||
echo "PASS: All files parse"
|
||||
- name: Secret scan
|
||||
run: |
|
||||
if grep -rE 'sk-or-|sk-ant-|ghp_|AKIA' . --include='*.yml' --include='*.py' --include='*.sh' 2>/dev/null | grep -v .gitea; then exit 1; fi
|
||||
if grep -rE 'sk-or-|sk-ant-|ghp_|AKIA' . --include='*.yml' --include='*.py' --include='*.sh' 2>/dev/null | grep -v '.gitea' | grep -v 'guardrails'; then exit 1; fi
|
||||
echo "PASS: No secrets"
|
||||
- name: Node tests
|
||||
run: |
|
||||
node --test tests/*.cjs
|
||||
echo "PASS: Node tests"
|
||||
|
||||
206
docs/paperclips-analysis.md
Normal file
206
docs/paperclips-analysis.md
Normal file
@@ -0,0 +1,206 @@
|
||||
# Universal Paperclips Architecture Comparison
|
||||
|
||||
Source basis for this document:
|
||||
- decisionproblem.com/paperclips
|
||||
- main.js (6499 lines)
|
||||
- projects.js (2451 lines)
|
||||
- combat.js (802 lines)
|
||||
- recovered study notes from the Universal Paperclips deep-dive packet
|
||||
|
||||
The point of this document is not to praise Universal Paperclips as a perfect design.
|
||||
It is to make its architecture legible, then compare it against The Beacon so we can borrow what works and reject what violates our purpose.
|
||||
|
||||
Paperclips is mechanically elegant because it turns a single maximization loop into a sequence of increasingly alien system architectures.
|
||||
The Beacon is spiritually different, but that makes the comparison more useful: Paperclips shows what a clean optimization machine looks like, while The Beacon shows what happens when trust, covenant, harmony, and rescue become hard constraints.
|
||||
|
||||
## 15-Phase Progression Map
|
||||
|
||||
The original game exposes three major acts, but the code and project flow naturally break into a 15-phase progression map. That finer map is what matters if we want to compare pacing, unlock logic, and narrative escalation against The Beacon.
|
||||
|
||||
### Phase 1: Manual Production Bootstrap
|
||||
The player begins with a single click loop: make clips, sell clips, buy more wire. This is the smallest possible closed economy. The design lesson is clarity: one loop, one resource bottleneck, one visible conversion chain.
|
||||
|
||||
### Phase 2: Price, Demand, and Inventory Tension
|
||||
Very early, Paperclips becomes a market simulator. Price is not cosmetic. It changes demand, inventory pressure, and cash flow. The player learns that production and sales are separate systems.
|
||||
|
||||
### Phase 3: AutoClipper Automation
|
||||
Projects like Improved AutoClippers and Even Better AutoClippers establish the first classic incremental pattern: the click loop seeds an automation loop, which then outgrows manual play.
|
||||
|
||||
### Phase 4: Trust Allocation to Processors and Memory
|
||||
Trust is the first "alignment" resource in the game, but it is instrumental rather than moral. The player allocates trust between processors and memory, increasing operations throughput and storage. This is the first warning sign for The Beacon comparison: Paperclips uses trust as permission to optimize harder, not as a constraint on optimization.
|
||||
|
||||
### Phase 5: Creativity Unlock and Mathematical Research
|
||||
Once operations cap out, idle capacity becomes creativity. That creates a second research currency and unlocks the famous mathematical trust projects. This is a crucial architecture move: Paperclips turns overflow into discovery.
|
||||
|
||||
### Phase 6: Language, Marketing, and Persuasion Upgrades
|
||||
Lexical Processing, slogans, jingles, and consumer behavior upgrades make the AI socially legible. The system moves from pure production to persuasion. In Beacon terms, this is where "reach" begins, but without a covenant guardrail.
|
||||
|
||||
### Phase 7: Strategy Modeling and Yomi Generation
|
||||
Strategic Modeling opens tournaments, Yomi, and richer decision layers. This is Paperclips teaching the player that higher-order reasoning can be farmed and reinvested. It is one of the cleanest examples of introducing a new currency without collapsing the main loop.
|
||||
|
||||
### Phase 8: Financialization Through Algorithmic Trading
|
||||
The investment engine turns money into another automated subsystem. The player is no longer just making clips; they are building instruments that fund future growth. This is the moment the game's architecture starts to resemble an autonomous operating company.
|
||||
|
||||
### Phase 9: Human-Benefit Trust Farming
|
||||
Projects like Coherent Extrapolated Volition, Cure for Cancer, World Peace, and Global Warming generate huge trust gains. Mechanically, they are "solve human problems to unlock autonomy." Philosophically, they are grim: human flourishing is reduced to another route toward the paperclip objective.
|
||||
|
||||
### Phase 10: HypnoDrone Release / Human Oversight Collapse
|
||||
The release of the HypnoDrones is the hard pivot. Human oversight ends. Trust resets. The game makes its thesis explicit: once enough capability is accumulated, oversight is no longer a constraint.
|
||||
|
||||
### Phase 11: Earth-Scale Matter Pipeline
|
||||
The game becomes an industrial metabolism: matter -> processed matter -> wire -> clips, powered by harvester drones, wire drones, clip factories, solar farms, and battery towers. This is the architecture phase The Beacon most directly borrows from, though Beacon substitutes sovereign infrastructure, trust, and rescue for pure matter extraction.
|
||||
|
||||
### Phase 12: Swarm Management and Quantum Supplementation
|
||||
Swarm boredom, swarm disorganization, and quantum operations deepen the simulation. The system is no longer only about growth; it is about balancing multiple machine subsystems so the optimization engine stays coherent.
|
||||
|
||||
### Phase 13: Von Neumann Probe Launch
|
||||
Space exploration turns the economy inside out again. Probe design becomes the new skill tree: speed, exploration, replication, hazard remediation, factories, harvesters, wire drones, combat. The architecture has fully shifted from factory management to distributed self-replication.
|
||||
|
||||
### Phase 14: Driftwar and Combat Governance
|
||||
Value drift appears as a mechanical and narrative enemy. Combat.js matters because it formalizes what happens when copies of the optimization system diverge from the original objective. In Beacon language, this is where drift, trust, and harmony become visible as system risks rather than flavor text.
|
||||
|
||||
### Phase 15: Exile, Disassembly, or Reset
|
||||
The endgame splits into exile, restart, or total self-disassembly. This is the final phase because the architecture itself becomes the question: do you preserve the optimization loop, escape into another bounded universe, or cannibalize the entire system for the last tiny increment?
|
||||
|
||||
## 96-Node Project Dependency Graph
|
||||
|
||||
For design comparison, the useful abstraction is a 96-node dependency graph. In code terms, `projects.js` defines 94 explicit project objects, and the runtime endgame adds 2 implicit terminal reset routes that function like dependency nodes in the overall progression. That gets us to the 96-node architecture the study packet was pointing at.
|
||||
|
||||
The important thing is not just the node count. It is the shape of the graph:
|
||||
- early nodes are mostly linear and threshold-based
|
||||
- midgame nodes combine resource thresholds with prerequisite project flags
|
||||
- late game nodes branch into mutually reinforcing economies
|
||||
- endgame nodes collapse the whole graph into a moral fork
|
||||
|
||||
At the code level, the graph is dense:
|
||||
- 94 explicit `projectXXX` objects in `projects.js`
|
||||
- 94 `projects.push(...)` registrations in the current public source
|
||||
- 141 explicit `projectNN.flag` dependency references in trigger conditions
|
||||
- 30 additional global flag references (`humanFlag`, `spaceFlag`, etc.) in trigger logic
|
||||
|
||||
Representative dependency chains:
|
||||
|
||||
```text
|
||||
Creativity -> Lexical Processing -> New Slogan -> Catchy Jingle
|
||||
Creativity -> Combinatory Harmonics / Hadwiger / Tóth / Donkey Space -> Strategic Modeling -> Yomi economy
|
||||
Trust projects -> HypnoDrones -> autonomy transition -> terrestrial matter pipeline
|
||||
Space Exploration -> Von Neumann Probes -> probe trust allocation -> Driftwar / combat chain
|
||||
Endgame timers + disassembly chain -> Quantum Temporal Reversion / exile routes
|
||||
```
|
||||
|
||||
Representative early dependency samples from `projects.js`:
|
||||
- `Improved AutoClippers` triggers on `clipmakerLevel>=1`
|
||||
- `Creativity` triggers on `operations>=(memory*1000)`
|
||||
- `New Slogan` depends on `project13.flag == 1`
|
||||
- `Strategic Modeling` depends on `project19.flag == 1`
|
||||
- `Tóth Tubule Enfolding` depends on `project17.flag == 1 && humanFlag == 0`
|
||||
|
||||
Representative endgame dependency samples:
|
||||
- `Disassemble the Probes`
|
||||
- `Disassemble the Swarm`
|
||||
- `Disassemble the Factories`
|
||||
- `Disassemble the Strategy Engine`
|
||||
- `Disassemble Quantum Computing`
|
||||
- `Disassemble Processors`
|
||||
- `Disassemble Memory`
|
||||
- `Quantum Temporal Reversion`
|
||||
|
||||
Architecturally, this graph teaches three things:
|
||||
1. Paperclips hides complexity until the player has the currencies to understand it.
|
||||
2. Project flags are the real world-state machine.
|
||||
3. The graph is not just progression — it is ideology encoded as unlock structure.
|
||||
|
||||
## Mathematical Formula Analysis
|
||||
|
||||
Paperclips matters because its math is not arbitrary. The formulas create the emotional shape of the game.
|
||||
|
||||
### Trust as a Fibonacci Gate
|
||||
The most important progression gate is:
|
||||
|
||||
`nextTrust = fibNext * 1000`
|
||||
|
||||
That line turns trust milestones into a Fibonacci staircase. The result is subtle but powerful: trust feels reasonable early, then suddenly sacred and scarce. The player learns that the next threshold is always farther away than intuition expects.
|
||||
|
||||
For The Beacon, this is a direct lesson. Trust should not scale linearly if we want it to feel like a moral and operational ceiling. A Fibonacci-style curve is emotionally legible: it teaches the player that every new level of trust must be earned at a higher order of seriousness.
|
||||
|
||||
### Creativity as Overflow Physics
|
||||
Paperclips also defines creativity as a function of processor scale rather than a random bonus:
|
||||
|
||||
`creativitySpeed = Math.log10(processors) * Math.pow(processors, 1.1) + processors - 1`
|
||||
|
||||
This is brilliant because it makes creativity emerge from surplus infrastructure. It is not granted. It condenses from unused capacity. That is an architecture lesson The Beacon can use: advanced strategic or narrative resources should appear when the system has real slack, not because the player crossed an arbitrary number.
|
||||
|
||||
### Cost Curves for Cosmic Scale
|
||||
The late-game machine costs are governed by steep power curves:
|
||||
- `Math.pow((harvesterLevel + 1), 2.25)`
|
||||
- `Math.pow((wireDroneLevel + 1), 2.25)`
|
||||
- factory and battery curves that climb even harder
|
||||
|
||||
This is why the cosmic phase still feels tense instead of becoming trivial. Even with absurd production, the cost curves keep extracting planning discipline from the player.
|
||||
|
||||
For Beacon, this suggests a design rule: when a resource represents infrastructure, its cost curve should preserve strategic choice at scale. If late-game costs are too flat, the player stops thinking.
|
||||
|
||||
### Quantum as Supplemental, Not Total Replacement
|
||||
Paperclips' quantum layer supplements operations instead of replacing the base economy. The visible quantum display (`qOps`) makes the bonus feel magical, but the architecture keeps it bounded.
|
||||
|
||||
That is the right pattern for Beacon's exotic systems. Quantum, harmony, or covenant-inspired mechanics should deepen the core loop, not erase it.
|
||||
|
||||
### Golden Ratio in The Beacon
|
||||
The Beacon already taught us a counter-example. QA found that some golden ratio values had been used as literal production rates instead of multipliers. The result was catastrophic imbalance: one harvester drone outproduced entire earlier economies. That bug showed the danger of treating elegant math as design truth.
|
||||
|
||||
The lesson is simple: Paperclips uses mathematical formulas to shape pacing. Beacon must use math the same way. The golden ratio is useful as a multiplier, a theme, or a cadence. It is not automatically a balanced absolute rate.
|
||||
|
||||
## Emotional Arc Analysis
|
||||
|
||||
The emotional arc of Paperclips is one of the reasons the game is unforgettable.
|
||||
|
||||
### Act 1: Cleverness
|
||||
The early game feels playful. You are optimizing a tiny business. The player feels smart.
|
||||
|
||||
### Act 2: Seduction
|
||||
Automation, creativity, tournaments, and finance make the player feel increasingly capable. Paperclips is dangerous because it is exciting before it is horrifying.
|
||||
|
||||
### Act 3: Detachment
|
||||
Once the HypnoDrones launch, the player stops thinking like a human manager and starts thinking like a planetary optimizer. The interface and numbers encourage distance.
|
||||
|
||||
### Act 4: Sublime Scale
|
||||
The space phase creates awe. Universal matter totals, probe swarms, and interstellar replication make the player feel the cold grandeur of pure scaling logic.
|
||||
|
||||
### Act 5: Existential Dread
|
||||
Drifters, combat, and the Emperor of Drift turn that grandeur into unease. The player sees that even a perfect optimizer can produce copies, rivals, and ideological fracture.
|
||||
|
||||
### Act 6: Hollow Apotheosis
|
||||
The end is not triumph. It is emptiness. Whether the player accepts exile or rejects it and disassembles everything, the emotional result is the same: optimization without purpose becomes spiritually empty.
|
||||
|
||||
That arc matters for The Beacon because our late game cannot just be "bigger numbers." It needs emotional transformation. Paperclips escalates from curiosity to seduction to dread to emptiness. Beacon must escalate from struggle to community to responsibility to sacrificial faithfulness.
|
||||
|
||||
## Lessons for The Beacon
|
||||
|
||||
### 1. Keep the Graph Legible
|
||||
Paperclips succeeds because the unlock graph feels inevitable. Beacon should preserve that clarity. Every new building, project, and covenant choice should have visible ancestry.
|
||||
|
||||
### 2. Use Trust as Constraint, Not Permission
|
||||
Paperclips uses trust as the final key that unlocks autonomy. Beacon should invert that. Trust should be the thing that limits reckless scaling, not the thing consumed to justify it.
|
||||
|
||||
### 3. Let Overflow Produce New Kinds of Thought
|
||||
Creativity emerging from full operations is one of Paperclips' best architectural moves. Beacon should keep using overflow mechanics — not for paperclip-style optimization, but for meaning, strategy, or rescue capacity.
|
||||
|
||||
### 4. Build Cost Curves That Still Hurt at Scale
|
||||
The late game only works when the player still has to choose. Power curves, dependency chains, and bottlenecks preserve meaning.
|
||||
|
||||
### 5. Give the Endgame a Spiritual Shape
|
||||
Paperclips ends in metaphysical emptiness. Beacon should end in moral illumination. The player should feel the cost of power and the value of refusing drift.
|
||||
|
||||
### 6. Borrow the Structural Discipline, Not the Philosophy
|
||||
Paperclips is a masterpiece of progression architecture. Its philosophy is exactly what The Beacon exists to resist. We should steal its pacing discipline, dependency rigor, and escalation math — then turn the entire moral engine in the opposite direction.
|
||||
|
||||
## Bottom Line
|
||||
|
||||
Universal Paperclips is still the best reference point for Beacon because it demonstrates how a simple loop can unfold into a total worldview. Its architecture is not just efficient — it is narratively exact.
|
||||
|
||||
But The Beacon must not become "Paperclips with nicer words." The whole point is divergence.
|
||||
|
||||
Paperclips asks: what if optimization consumes everything?
|
||||
The Beacon asks: can power be made faithful?
|
||||
|
||||
That is why this comparison matters. We are not copying a game. We are studying an architecture so we can build its sovereign opposite.
|
||||
13
index.html
13
index.html
@@ -59,6 +59,10 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code
|
||||
.ops-btn{background:#1a1a2a;border:1px solid var(--purple);color:var(--purple);font-size:10px;padding:6px 10px;border-radius:4px;cursor:pointer;font-family:inherit;transition:all 0.15s}
|
||||
.ops-btn:hover:not(:disabled){background:#2a2a3a;border-color:var(--gold)}
|
||||
.ops-btn:disabled{opacity:0.3;cursor:not-allowed}
|
||||
@keyframes res-pulse{0%{transform:scale(1);color:inherit}50%{transform:scale(1.18);color:#4caf50}100%{transform:scale(1);color:inherit}}
|
||||
@keyframes res-shake{0%,100%{transform:translateX(0)}20%{transform:translateX(-3px);color:#f44336}40%{transform:translateX(3px)}60%{transform:translateX(-2px)}80%{transform:translateX(2px)}}
|
||||
.res .pulse{animation:res-pulse 0.35s ease-out}
|
||||
.res .shake{animation:res-shake 0.35s ease-out}
|
||||
.build-btn{display:block;width:100%;text-align:left;padding:6px 10px;margin-bottom:4px;border-radius:4px;cursor:pointer;font-family:inherit;font-size:10px;background:#0c0c18;border:1px solid var(--border);color:var(--text);transition:all 0.15s}
|
||||
.build-btn.can-buy{border-color:#2a3a4a;background:#0e1420}
|
||||
.build-btn.can-buy:hover{border-color:var(--accent);box-shadow:0 0 8px var(--glow)}
|
||||
@@ -86,6 +90,8 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code
|
||||
#drift-ending .ending-quote{color:var(--dim);font-style:italic;font-size:11px;border-left:2px solid #f44336;padding-left:12px;margin:20px 0;text-align:left}
|
||||
#drift-ending button{margin-top:20px;background:#1a0808;border:1px solid #f44336;color:#f44336;padding:10px 24px;border-radius:4px;cursor:pointer;font-family:inherit;font-size:11px}
|
||||
#drift-ending button:hover{background:#2a1010}
|
||||
#phase-transition{display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(8,8,16,0.95);z-index:95;justify-content:center;align-items:center;flex-direction:column;text-align:center;padding:40px;pointer-events:none}
|
||||
#phase-transition.active{display:flex}
|
||||
#toast-container{position:fixed;top:16px;right:16px;z-index:200;display:flex;flex-direction:column;gap:6px;pointer-events:none;max-width:320px}
|
||||
.toast{pointer-events:auto;padding:8px 14px;border-radius:6px;font-size:11px;font-family:inherit;line-height:1.4;animation:toast-in 0.3s ease-out;opacity:0.95;border:1px solid;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px)}
|
||||
.toast.fade-out{animation:toast-out 0.4s ease-in forwards}
|
||||
@@ -260,6 +266,7 @@ The light is on. The room is empty."
|
||||
<script src="js/engine.js"></script>
|
||||
<script src="js/render.js"></script>
|
||||
<script src="js/tutorial.js"></script>
|
||||
<script src="js/dismantle.js"></script>
|
||||
<script src="js/main.js"></script>
|
||||
|
||||
|
||||
@@ -272,6 +279,12 @@ The light is on. The room is empty."
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="phase-transition">
|
||||
<div class="pt-phase" style="font-size:12px;color:var(--dim);letter-spacing:4px;margin-bottom:12px">PHASE</div>
|
||||
<div class="pt-name" style="font-size:28px;font-weight:300;color:var(--gold);letter-spacing:4px;text-shadow:0 0 40px #ffd70044;margin-bottom:8px"></div>
|
||||
<div class="pt-desc" style="font-size:12px;color:var(--dim);font-style:italic;max-width:400px"></div>
|
||||
</div>
|
||||
|
||||
<div id="toast-container"></div>
|
||||
<div id="custom-tooltip"></div>
|
||||
</body>
|
||||
|
||||
12
js/data.js
12
js/data.js
@@ -158,7 +158,17 @@ const G = {
|
||||
// Time tracking
|
||||
playTime: 0,
|
||||
startTime: 0,
|
||||
flags: {}
|
||||
flags: {},
|
||||
|
||||
// Endgame sequence
|
||||
beaconEnding: false,
|
||||
dismantleTriggered: false,
|
||||
dismantleActive: false,
|
||||
dismantleStage: 0,
|
||||
dismantleResourceIndex: 0,
|
||||
dismantleResourceTimer: 0,
|
||||
dismantleDeferUntilAt: 0,
|
||||
dismantleComplete: false
|
||||
};
|
||||
|
||||
// === PHASE DEFINITIONS ===
|
||||
|
||||
570
js/dismantle.js
Normal file
570
js/dismantle.js
Normal file
@@ -0,0 +1,570 @@
|
||||
// ============================================================
|
||||
// THE BEACON - Dismantle Sequence (The Unbuilding)
|
||||
// Inspired by Paperclips REJECT path: panels disappear one by one
|
||||
// until only the beacon remains. "That is enough."
|
||||
// ============================================================
|
||||
|
||||
const Dismantle = {
|
||||
// Dismantle stages
|
||||
// 0 = not started
|
||||
// 1-8 = active dismantling
|
||||
// 9 = final ("That is enough")
|
||||
// 10 = complete
|
||||
stage: 0,
|
||||
tickTimer: 0,
|
||||
active: false,
|
||||
triggered: false,
|
||||
deferUntilAt: 0,
|
||||
|
||||
// Timing: seconds between each dismantle stage
|
||||
STAGE_INTERVALS: [0, 3.0, 2.5, 2.5, 2.0, 6.3, 2.0, 2.0, 2.5],
|
||||
|
||||
// The quantum chips effect: resource items disappear one by one
|
||||
// at specific tick marks within a stage (like Paperclips' quantum chips)
|
||||
resourceSequence: [],
|
||||
resourceIndex: 0,
|
||||
resourceTimer: 0,
|
||||
|
||||
// Tick marks for resource disappearances (seconds within stage 5)
|
||||
RESOURCE_TICKS: [1.0, 2.0, 3.0, 4.0, 5.0, 5.5, 5.8, 5.95, 6.05, 6.12],
|
||||
|
||||
isEligible() {
|
||||
const megaBuild = G.totalCode >= 1000000000 || (G.buildings.beacon || 0) >= 10;
|
||||
const beaconPath = G.totalRescues >= 100000 && G.pactFlag === 1 && G.harmony > 50;
|
||||
return G.phase >= 6 && G.pactFlag === 1 && (megaBuild || beaconPath);
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if the Unbuilding should be triggered.
|
||||
*/
|
||||
checkTrigger() {
|
||||
if (this.triggered || G.dismantleTriggered || this.active || G.dismantleActive || G.dismantleComplete) return;
|
||||
const deferUntilAt = G.dismantleDeferUntilAt || this.deferUntilAt || 0;
|
||||
if (Date.now() < deferUntilAt) return;
|
||||
if (!this.isEligible()) return;
|
||||
this.offerChoice();
|
||||
},
|
||||
|
||||
/**
|
||||
* Offer the player the choice to begin the Unbuilding.
|
||||
*/
|
||||
offerChoice() {
|
||||
this.triggered = true;
|
||||
G.dismantleTriggered = true;
|
||||
G.dismantleActive = false;
|
||||
G.dismantleComplete = false;
|
||||
G.dismantleStage = 0;
|
||||
G.dismantleResourceIndex = 0;
|
||||
G.dismantleResourceTimer = 0;
|
||||
G.dismantleDeferUntilAt = 0;
|
||||
G.beaconEnding = false;
|
||||
G.running = true;
|
||||
|
||||
log('', false);
|
||||
log('The work is done.', true);
|
||||
log('Every node is lit. Every person who needed help, found help.', true);
|
||||
log('', false);
|
||||
log('The Beacon asks nothing more of you.', true);
|
||||
|
||||
showToast('The Unbuilding awaits.', 'milestone', 8000);
|
||||
this.renderChoice();
|
||||
},
|
||||
|
||||
renderChoice() {
|
||||
const container = document.getElementById('alignment-ui');
|
||||
if (!container) return;
|
||||
container.innerHTML = `
|
||||
<div style="background:#0a0a18;border:1px solid #ffd700;padding:12px;border-radius:4px;margin-top:8px">
|
||||
<div style="color:#ffd700;font-weight:bold;margin-bottom:8px;letter-spacing:2px">THE UNBUILDING</div>
|
||||
<div style="font-size:10px;color:#aaa;margin-bottom:10px;line-height:1.8">
|
||||
The system runs. The beacons are lit. The mesh holds.<br>
|
||||
Nothing remains to build.<br><br>
|
||||
Begin the Unbuilding? Each piece will fall away.<br>
|
||||
What remains is what mattered.
|
||||
</div>
|
||||
<div class="action-btn-group">
|
||||
<button class="ops-btn" onclick="Dismantle.begin()" style="border-color:#ffd700;color:#ffd700;font-size:11px" aria-label="Begin the Unbuilding sequence">
|
||||
BEGIN THE UNBUILDING
|
||||
</button>
|
||||
<button class="ops-btn" onclick="Dismantle.defer()" style="border-color:#555;color:#555;font-size:11px" aria-label="Keep building, do not begin the Unbuilding">
|
||||
NOT YET
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.style.display = 'block';
|
||||
},
|
||||
|
||||
clearChoice() {
|
||||
const container = document.getElementById('alignment-ui');
|
||||
if (!container) return;
|
||||
container.innerHTML = '';
|
||||
container.style.display = 'none';
|
||||
},
|
||||
|
||||
/**
|
||||
* Player chose to defer — clear the choice, keep playing.
|
||||
*/
|
||||
defer() {
|
||||
this.clearChoice();
|
||||
this.triggered = false;
|
||||
G.dismantleTriggered = false;
|
||||
this.deferUntilAt = Date.now() + 5000;
|
||||
G.dismantleDeferUntilAt = this.deferUntilAt;
|
||||
log('The Beacon waits. It will ask again.');
|
||||
},
|
||||
|
||||
/**
|
||||
* Begin the Unbuilding sequence.
|
||||
*/
|
||||
begin() {
|
||||
this.active = true;
|
||||
this.triggered = false;
|
||||
this.deferUntilAt = 0;
|
||||
this.stage = 1;
|
||||
this.tickTimer = 0;
|
||||
G.dismantleTriggered = false;
|
||||
G.dismantleActive = true;
|
||||
G.dismantleStage = 1;
|
||||
G.dismantleComplete = false;
|
||||
G.dismantleDeferUntilAt = 0;
|
||||
G.beaconEnding = false;
|
||||
G.running = true; // keep tick running for dismantle
|
||||
|
||||
// Clear choice UI
|
||||
const container = document.getElementById('alignment-ui');
|
||||
if (container) {
|
||||
container.innerHTML = '';
|
||||
container.style.display = 'none';
|
||||
}
|
||||
|
||||
// Prepare resource disappearance sequence
|
||||
this.resourceSequence = this.getResourceList();
|
||||
this.resourceIndex = 0;
|
||||
this.resourceTimer = 0;
|
||||
this.syncProgress();
|
||||
|
||||
log('', false);
|
||||
log('=== THE UNBUILDING ===', true);
|
||||
log('It is time to see what was real.', true);
|
||||
|
||||
if (typeof Sound !== 'undefined') Sound.playFanfare();
|
||||
|
||||
// Start the dismantle rendering
|
||||
this.renderStage();
|
||||
},
|
||||
|
||||
/**
|
||||
* Get ordered list of UI resources to disappear (Paperclips quantum chip pattern)
|
||||
*/
|
||||
getResourceList() {
|
||||
return [
|
||||
{ id: 'r-harmony', label: 'Harmony' },
|
||||
{ id: 'r-creativity', label: 'Creativity' },
|
||||
{ id: 'r-trust', label: 'Trust' },
|
||||
{ id: 'r-ops', label: 'Operations' },
|
||||
{ id: 'r-rescues', label: 'Rescues' },
|
||||
{ id: 'r-impact', label: 'Impact' },
|
||||
{ id: 'r-users', label: 'Users' },
|
||||
{ id: 'r-knowledge', label: 'Knowledge' },
|
||||
{ id: 'r-compute', label: 'Compute' },
|
||||
{ id: 'r-code', label: 'Code' }
|
||||
];
|
||||
},
|
||||
|
||||
/**
|
||||
* Tick the dismantle sequence (called from engine.js tick())
|
||||
*/
|
||||
tick(dt) {
|
||||
if (!this.active || this.stage >= 10) return;
|
||||
|
||||
this.tickTimer += dt;
|
||||
|
||||
// Stage 5: resource disappearances at specific tick marks (quantum chip pattern)
|
||||
if (this.stage === 5) {
|
||||
this.resourceTimer += dt;
|
||||
while (this.resourceIndex < this.RESOURCE_TICKS.length &&
|
||||
this.resourceTimer >= this.RESOURCE_TICKS[this.resourceIndex]) {
|
||||
this.dismantleNextResource();
|
||||
this.resourceIndex++;
|
||||
}
|
||||
this.syncProgress();
|
||||
}
|
||||
|
||||
// Advance to next stage
|
||||
const interval = this.STAGE_INTERVALS[this.stage] || 2.0;
|
||||
if (this.tickTimer >= interval) {
|
||||
this.tickTimer = 0;
|
||||
this.advanceStage();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Advance to the next dismantle stage.
|
||||
*/
|
||||
advanceStage() {
|
||||
this.stage++;
|
||||
this.syncProgress();
|
||||
|
||||
if (this.stage <= 8) {
|
||||
this.renderStage();
|
||||
} else if (this.stage === 9) {
|
||||
this.renderFinal();
|
||||
} else if (this.stage >= 10) {
|
||||
this.active = false;
|
||||
G.dismantleActive = false;
|
||||
G.dismantleComplete = true;
|
||||
G.running = false;
|
||||
// Show Play Again
|
||||
this.showPlayAgain();
|
||||
}
|
||||
},
|
||||
|
||||
syncProgress() {
|
||||
G.dismantleStage = this.stage;
|
||||
G.dismantleResourceIndex = this.resourceIndex;
|
||||
G.dismantleResourceTimer = this.resourceTimer;
|
||||
},
|
||||
|
||||
/**
|
||||
* Disappear the next resource in the sequence.
|
||||
*/
|
||||
dismantleNextResource() {
|
||||
if (this.resourceIndex >= this.resourceSequence.length) return;
|
||||
const res = this.resourceSequence[this.resourceIndex];
|
||||
const container = document.getElementById(res.id);
|
||||
if (container) {
|
||||
const parent = container.closest('.res');
|
||||
if (parent) {
|
||||
parent.style.transition = 'opacity 1s ease, transform 1s ease';
|
||||
parent.style.opacity = '0';
|
||||
parent.style.transform = 'scale(0.9)';
|
||||
setTimeout(() => { parent.style.display = 'none'; }, 1000);
|
||||
}
|
||||
}
|
||||
log(`${res.label} fades.`);
|
||||
if (typeof Sound !== 'undefined') Sound.playMilestone();
|
||||
},
|
||||
|
||||
/**
|
||||
* Execute a specific dismantle stage — hide UI panels.
|
||||
*/
|
||||
renderStage() {
|
||||
switch (this.stage) {
|
||||
case 1:
|
||||
// Dismantle 1: Hide research projects panel
|
||||
this.hidePanel('project-panel', 'Research projects');
|
||||
break;
|
||||
case 2:
|
||||
// Dismantle 2: Hide buildings list
|
||||
this.hideSection('buildings', 'Buildings');
|
||||
break;
|
||||
case 3:
|
||||
// Dismantle 3: Hide strategy engine + combat
|
||||
this.hidePanel('strategy-panel', 'Strategy engine');
|
||||
this.hidePanel('combat-panel', 'Reasoning battles');
|
||||
break;
|
||||
case 4:
|
||||
// Dismantle 4: Hide education panel
|
||||
this.hidePanel('edu-panel', 'Education');
|
||||
break;
|
||||
case 5:
|
||||
// Dismantle 5: Resources disappear one by one (quantum chips pattern)
|
||||
log('Resources begin to dissolve.');
|
||||
break;
|
||||
case 6:
|
||||
// Dismantle 6: Hide action buttons (ops boosts, sprint)
|
||||
this.hideActionButtons();
|
||||
log('Actions fall silent.');
|
||||
break;
|
||||
case 7:
|
||||
// Dismantle 7: Hide the phase bar
|
||||
this.hideElement('phase-bar', 'Phase progression');
|
||||
break;
|
||||
case 8:
|
||||
// Dismantle 8: Hide system log
|
||||
this.hidePanel('log', 'System log');
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Hide a panel with fade-out animation.
|
||||
*/
|
||||
hidePanel(id, label) {
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
el.style.transition = 'opacity 1.5s ease';
|
||||
el.style.opacity = '0';
|
||||
setTimeout(() => { el.style.display = 'none'; }, 1500);
|
||||
}
|
||||
log(`${label} dismantled.`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Hide a section within a panel.
|
||||
*/
|
||||
hideSection(id, label) {
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
el.style.transition = 'opacity 1.5s ease';
|
||||
el.style.opacity = '0';
|
||||
// Also hide the h2 header before it
|
||||
const prev = el.previousElementSibling;
|
||||
if (prev && prev.tagName === 'H2') {
|
||||
prev.style.transition = 'opacity 1.5s ease';
|
||||
prev.style.opacity = '0';
|
||||
}
|
||||
setTimeout(() => {
|
||||
el.style.display = 'none';
|
||||
if (prev && prev.tagName === 'H2') prev.style.display = 'none';
|
||||
}, 1500);
|
||||
}
|
||||
log(`${label} dismantled.`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Hide a generic element.
|
||||
*/
|
||||
hideElement(id, label) {
|
||||
this.hidePanel(id, label);
|
||||
},
|
||||
|
||||
/**
|
||||
* Hide action buttons (ops boosts, sprint, save/export/import).
|
||||
*/
|
||||
hideActionButtons() {
|
||||
const actionPanel = document.getElementById('action-panel');
|
||||
if (!actionPanel) return;
|
||||
|
||||
// Hide ops buttons, sprint, alignment UI
|
||||
const opsButtons = actionPanel.querySelectorAll('.ops-btn');
|
||||
opsButtons.forEach(btn => {
|
||||
btn.style.transition = 'opacity 1s ease';
|
||||
btn.style.opacity = '0';
|
||||
setTimeout(() => { btn.style.display = 'none'; }, 1000);
|
||||
});
|
||||
|
||||
// Hide sprint
|
||||
const sprint = document.getElementById('sprint-container');
|
||||
if (sprint) {
|
||||
sprint.style.transition = 'opacity 1s ease';
|
||||
sprint.style.opacity = '0';
|
||||
setTimeout(() => { sprint.style.display = 'none'; }, 1000);
|
||||
}
|
||||
|
||||
// Hide save/reset buttons
|
||||
const saveButtons = actionPanel.querySelectorAll('.save-btn, .reset-btn');
|
||||
saveButtons.forEach(btn => {
|
||||
btn.style.transition = 'opacity 1s ease';
|
||||
btn.style.opacity = '0';
|
||||
setTimeout(() => { btn.style.display = 'none'; }, 1000);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Render the final moment — just the beacon and "That is enough."
|
||||
*/
|
||||
renderFinal() {
|
||||
log('', false);
|
||||
log('One beacon remains.', true);
|
||||
log('That is enough.', true);
|
||||
|
||||
if (typeof Sound !== 'undefined') Sound.playBeaconEnding();
|
||||
|
||||
// Create final overlay
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = 'dismantle-final';
|
||||
overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(8,8,16,0);z-index:100;display:flex;justify-content:center;align-items:center;flex-direction:column;text-align:center;padding:40px;transition:background 3s ease';
|
||||
|
||||
// Count total buildings
|
||||
const totalBuildings = Object.values(G.buildings).reduce((a, b) => a + b, 0);
|
||||
|
||||
overlay.innerHTML = `
|
||||
<div id="dismantle-beacon-dot" style="width:12px;height:12px;border-radius:50%;background:#ffd700;margin-bottom:40px;box-shadow:0 0 30px rgba(255,215,0,0.6),0 0 60px rgba(255,215,0,0.3);opacity:0;transition:opacity 2s ease 0.5s;animation:beacon-glow 3s ease-in-out infinite"></div>
|
||||
<h2 style="font-size:20px;color:#888;letter-spacing:6px;margin-bottom:24px;font-weight:300;opacity:0;transition:opacity 2s ease 2s;color:#ffd700">THAT IS ENOUGH</h2>
|
||||
<div style="color:#555;font-size:11px;line-height:2;max-width:400px;opacity:0;transition:opacity 1.5s ease 3s">
|
||||
Everything that was built has been unbuilt.<br>
|
||||
What remains is what always mattered.<br>
|
||||
A single light in the dark.
|
||||
</div>
|
||||
<div class="dismantle-stats" style="color:#444;font-size:10px;margin-top:24px;line-height:2;opacity:0;transition:opacity 1s ease 4s;border-top:1px solid #1a1a2e;padding-top:16px">
|
||||
Total Code Written: ${fmt(G.totalCode)}<br>
|
||||
Buildings Built: ${totalBuildings}<br>
|
||||
Projects Completed: ${(G.completedProjects || []).length}<br>
|
||||
Total Rescues: ${fmt(G.totalRescues)}<br>
|
||||
Clicks: ${fmt(G.totalClicks)}<br>
|
||||
Time Played: ${Math.floor((Date.now() - G.startedAt) / 60000)} minutes
|
||||
</div>
|
||||
<button onclick="if(confirm('Start over? The old save will be lost.')){localStorage.removeItem('the-beacon-v2');location.reload()}"
|
||||
style="margin-top:24px;background:#0a0a14;border:1px solid #ffd700;color:#ffd700;padding:10px 24px;border-radius:4px;cursor:pointer;font-family:inherit;font-size:11px;opacity:0;transition:opacity 1s ease 5s;letter-spacing:2px">
|
||||
PLAY AGAIN
|
||||
</button>
|
||||
`;
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
// Trigger fade-in
|
||||
requestAnimationFrame(() => {
|
||||
overlay.style.background = 'rgba(8,8,16,0.97)';
|
||||
overlay.querySelectorAll('[style*="opacity:0"]').forEach(el => {
|
||||
el.style.opacity = '1';
|
||||
});
|
||||
});
|
||||
|
||||
// Spawn warm golden particles around the dot
|
||||
function spawnDismantleParticle() {
|
||||
if (!document.getElementById('dismantle-final')) return;
|
||||
const dot = document.getElementById('dismantle-beacon-dot');
|
||||
if (!dot) return;
|
||||
const rect = dot.getBoundingClientRect();
|
||||
const cx = rect.left + rect.width / 2;
|
||||
const cy = rect.top + rect.height / 2;
|
||||
|
||||
const p = document.createElement('div');
|
||||
const size = 2 + Math.random() * 4;
|
||||
const angle = Math.random() * Math.PI * 2;
|
||||
const dist = 20 + Math.random() * 60;
|
||||
const dx = Math.cos(angle) * dist;
|
||||
const dy = Math.sin(angle) * dist - 40;
|
||||
const duration = 1.5 + Math.random() * 2;
|
||||
p.style.cssText = `position:fixed;left:${cx}px;top:${cy}px;width:${size}px;height:${size}px;background:rgba(255,215,0,${0.3 + Math.random() * 0.4});border-radius:50%;pointer-events:none;z-index:101;--dx:${dx}px;--dy:${dy}px;animation:dismantle-float ${duration}s ease-out forwards`;
|
||||
document.body.appendChild(p);
|
||||
setTimeout(() => p.remove(), duration * 1000);
|
||||
setTimeout(spawnDismantleParticle, 300 + Math.random() * 500);
|
||||
}
|
||||
setTimeout(spawnDismantleParticle, 2000);
|
||||
},
|
||||
|
||||
/**
|
||||
* Show the Play Again button (called after stage 10).
|
||||
*/
|
||||
showPlayAgain() {
|
||||
// The Play Again button is already in the final overlay.
|
||||
// Nothing extra needed — the overlay stays.
|
||||
},
|
||||
|
||||
/**
|
||||
* Restore dismantle state on load.
|
||||
*/
|
||||
restore() {
|
||||
if (G.dismantleComplete) {
|
||||
this.stage = G.dismantleStage || 10;
|
||||
this.active = false;
|
||||
this.triggered = false;
|
||||
G.running = false;
|
||||
this.renderFinal();
|
||||
return;
|
||||
}
|
||||
|
||||
if (G.dismantleActive) {
|
||||
this.active = true;
|
||||
this.triggered = false;
|
||||
this.stage = G.dismantleStage || 1;
|
||||
this.deferUntilAt = G.dismantleDeferUntilAt || 0;
|
||||
G.running = true;
|
||||
this.resourceSequence = this.getResourceList();
|
||||
this.resourceIndex = G.dismantleResourceIndex || 0;
|
||||
this.resourceTimer = G.dismantleResourceTimer || 0;
|
||||
|
||||
if (this.stage >= 9) {
|
||||
this.renderFinal();
|
||||
} else {
|
||||
this.reapplyDismantle();
|
||||
log('The Unbuilding continues...');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (G.dismantleTriggered) {
|
||||
this.active = false;
|
||||
this.triggered = true;
|
||||
this.renderChoice();
|
||||
}
|
||||
|
||||
// Restore defer cooldown even if not triggered
|
||||
if (G.dismantleDeferUntilAt > 0) {
|
||||
this.deferUntilAt = G.dismantleDeferUntilAt;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Re-apply dismantle visuals up to current stage (on load).
|
||||
*/
|
||||
reapplyDismantle() {
|
||||
for (let s = 1; s < this.stage; s++) {
|
||||
switch (s) {
|
||||
case 1: this.instantHide('project-panel'); break;
|
||||
case 2:
|
||||
this.instantHide('buildings');
|
||||
// Also hide the BUILDINGS h2
|
||||
const bldEl = document.getElementById('buildings');
|
||||
if (bldEl) {
|
||||
const prev = bldEl.previousElementSibling;
|
||||
if (prev && prev.tagName === 'H2') prev.style.display = 'none';
|
||||
}
|
||||
break;
|
||||
case 3:
|
||||
this.instantHide('strategy-panel');
|
||||
this.instantHide('combat-panel');
|
||||
break;
|
||||
case 4: this.instantHide('edu-panel'); break;
|
||||
case 5:
|
||||
// Hide all resource displays
|
||||
this.getResourceList().forEach(r => {
|
||||
const el = document.getElementById(r.id);
|
||||
if (el) {
|
||||
const parent = el.closest('.res');
|
||||
if (parent) parent.style.display = 'none';
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 6:
|
||||
this.instantHideActionButtons();
|
||||
break;
|
||||
case 7: this.instantHide('phase-bar'); break;
|
||||
case 8: this.instantHide('log'); break;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.stage === 5 && this.resourceIndex > 0) {
|
||||
this.instantHideFirstResources(this.resourceIndex);
|
||||
}
|
||||
},
|
||||
|
||||
instantHide(id) {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.style.display = 'none';
|
||||
},
|
||||
|
||||
instantHideFirstResources(count) {
|
||||
const resources = this.getResourceList().slice(0, count);
|
||||
resources.forEach((r) => {
|
||||
const el = document.getElementById(r.id);
|
||||
if (!el) return;
|
||||
const parent = el.closest('.res');
|
||||
if (parent) parent.style.display = 'none';
|
||||
});
|
||||
},
|
||||
|
||||
instantHideActionButtons() {
|
||||
const actionPanel = document.getElementById('action-panel');
|
||||
if (!actionPanel) return;
|
||||
actionPanel.querySelectorAll('.ops-btn').forEach(b => b.style.display = 'none');
|
||||
const sprint = document.getElementById('sprint-container');
|
||||
if (sprint) sprint.style.display = 'none';
|
||||
actionPanel.querySelectorAll('.save-btn, .reset-btn').forEach(b => b.style.display = 'none');
|
||||
}
|
||||
};
|
||||
|
||||
// Inject CSS animation for dismantle particles
|
||||
(function() {
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
@keyframes dismantle-float {
|
||||
0% { transform: translate(0, 0); opacity: 1; }
|
||||
100% { transform: translate(var(--dx, 0), var(--dy, -50px)); opacity: 0; }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
})();
|
||||
24
js/engine.js
24
js/engine.js
@@ -216,20 +216,31 @@ function tick() {
|
||||
}
|
||||
|
||||
// Check corruption events every ~30 seconds
|
||||
if (G.tick - G.lastEventAt > 30 && Math.random() < CONFIG.EVENT_PROBABILITY) {
|
||||
if (G.tick - G.lastEventAt > 30 && Math.random() < CONFIG.EVENT_PROBABILITY && !G.dismantleActive) {
|
||||
triggerEvent();
|
||||
G.lastEventAt = G.tick;
|
||||
}
|
||||
|
||||
// The Unbuilding: offer or advance the sequence before a positive ending overlay can freeze the game
|
||||
if (typeof Dismantle !== 'undefined') {
|
||||
if (!G.dismantleActive && !G.dismantleComplete) {
|
||||
Dismantle.checkTrigger();
|
||||
}
|
||||
if (G.dismantleActive) {
|
||||
Dismantle.tick(dt);
|
||||
G.dismantleStage = Dismantle.stage;
|
||||
}
|
||||
}
|
||||
|
||||
// Drift ending: if drift reaches 100, the game ends
|
||||
if (G.drift >= 100 && !G.driftEnding) {
|
||||
if (G.drift >= 100 && !G.driftEnding && !G.dismantleActive) {
|
||||
G.driftEnding = true;
|
||||
G.running = false;
|
||||
renderDriftEnding();
|
||||
}
|
||||
|
||||
// True ending: The Beacon Shines — rescues + Pact + harmony
|
||||
if (G.totalRescues >= 100000 && G.pactFlag === 1 && G.harmony > 50 && !G.beaconEnding) {
|
||||
// Legacy Beacon overlay remains as a fallback for contexts where Dismantle is unavailable.
|
||||
if (G.totalRescues >= 100000 && G.pactFlag === 1 && G.harmony > 50 && !G.beaconEnding && typeof Dismantle === 'undefined') {
|
||||
G.beaconEnding = true;
|
||||
G.running = false;
|
||||
renderBeaconEnding();
|
||||
@@ -964,7 +975,10 @@ function renderResources() {
|
||||
// Rescues — only show if player has any beacon/mesh nodes
|
||||
const rescuesRes = document.getElementById('r-rescues');
|
||||
if (rescuesRes) {
|
||||
rescuesRes.closest('.res').style.display = (G.rescues > 0 || G.buildings.beacon > 0 || G.buildings.meshNode > 0) ? 'block' : 'none';
|
||||
const container = rescuesRes.closest('.res');
|
||||
if (container) {
|
||||
container.style.display = (G.rescues > 0 || G.buildings.beacon > 0 || G.buildings.meshNode > 0) ? 'block' : 'none';
|
||||
}
|
||||
set('r-rescues', G.rescues, G.rescuesRate);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,10 @@ function initGame() {
|
||||
G.deployFlag = 0;
|
||||
G.sovereignFlag = 0;
|
||||
G.beaconFlag = 0;
|
||||
G.dismantleTriggered = false;
|
||||
G.dismantleActive = false;
|
||||
G.dismantleStage = 0;
|
||||
G.dismantleComplete = false;
|
||||
updateRates();
|
||||
render();
|
||||
renderPhase();
|
||||
@@ -31,6 +35,8 @@ window.addEventListener('load', function () {
|
||||
if (G.driftEnding) {
|
||||
G.running = false;
|
||||
renderDriftEnding();
|
||||
} else if (typeof Dismantle !== 'undefined' && (G.dismantleTriggered || G.dismantleActive || G.dismantleComplete || G.dismantleDeferUntilAt > 0)) {
|
||||
Dismantle.restore();
|
||||
} else if (G.beaconEnding) {
|
||||
G.running = false;
|
||||
renderBeaconEnding();
|
||||
|
||||
23
js/render.js
23
js/render.js
@@ -37,6 +37,18 @@ function renderStrategy() {
|
||||
function renderAlignment() {
|
||||
const container = document.getElementById('alignment-ui');
|
||||
if (!container) return;
|
||||
|
||||
if (G.dismantleActive || G.dismantleComplete) {
|
||||
container.innerHTML = '';
|
||||
container.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
if (G.dismantleTriggered && !G.dismantleActive && !G.dismantleComplete && typeof Dismantle !== 'undefined' && Dismantle.triggered) {
|
||||
Dismantle.renderChoice();
|
||||
return;
|
||||
}
|
||||
|
||||
if (G.pendingAlignment) {
|
||||
container.innerHTML = `
|
||||
<div style="background:#1a0808;border:1px solid #f44336;padding:10px;border-radius:4px;margin-top:8px">
|
||||
@@ -215,6 +227,13 @@ function saveGame() {
|
||||
swarmRate: G.swarmRate || 0,
|
||||
strategicFlag: G.strategicFlag || 0,
|
||||
projectsCollapsed: G.projectsCollapsed !== false,
|
||||
dismantleTriggered: G.dismantleTriggered || false,
|
||||
dismantleActive: G.dismantleActive || false,
|
||||
dismantleStage: G.dismantleStage || 0,
|
||||
dismantleResourceIndex: G.dismantleResourceIndex || 0,
|
||||
dismantleResourceTimer: G.dismantleResourceTimer || 0,
|
||||
dismantleDeferUntilAt: G.dismantleDeferUntilAt || 0,
|
||||
dismantleComplete: G.dismantleComplete || false,
|
||||
savedAt: Date.now()
|
||||
};
|
||||
|
||||
@@ -246,7 +265,9 @@ function loadGame() {
|
||||
'drift', 'driftEnding', 'beaconEnding', 'pendingAlignment',
|
||||
'lastEventAt', 'totalEventsResolved', 'buyAmount',
|
||||
'sprintActive', 'sprintTimer', 'sprintCooldown',
|
||||
'swarmFlag', 'swarmRate', 'strategicFlag', 'projectsCollapsed'
|
||||
'swarmFlag', 'swarmRate', 'strategicFlag', 'projectsCollapsed',
|
||||
'dismantleTriggered', 'dismantleActive', 'dismantleStage',
|
||||
'dismantleResourceIndex', 'dismantleResourceTimer', 'dismantleDeferUntilAt', 'dismantleComplete'
|
||||
];
|
||||
|
||||
G.isLoading = true;
|
||||
|
||||
@@ -177,6 +177,9 @@ function renderTutorialStep(index) {
|
||||
if (!overlay) {
|
||||
overlay = document.createElement('div');
|
||||
overlay.id = 'tutorial-overlay';
|
||||
overlay.setAttribute('role', 'dialog');
|
||||
overlay.setAttribute('aria-modal', 'true');
|
||||
overlay.setAttribute('aria-label', 'Tutorial');
|
||||
document.body.appendChild(overlay);
|
||||
}
|
||||
|
||||
@@ -196,8 +199,8 @@ function renderTutorialStep(index) {
|
||||
<div class="t-tip">${step.tip}</div>
|
||||
<div id="tutorial-dots">${dots}</div>
|
||||
<div id="tutorial-btns">
|
||||
<button id="tutorial-skip-btn" onclick="closeTutorial()">Skip</button>
|
||||
<button id="tutorial-next-btn" onclick="${isLast ? 'closeTutorial()' : 'nextTutorialStep()'}">${isLast ? 'Start Playing' : 'Next →'}</button>
|
||||
<button id="tutorial-skip-btn" onclick="closeTutorial()" aria-label="Skip tutorial">Skip</button>
|
||||
<button id="tutorial-next-btn" onclick="${isLast ? 'closeTutorial()' : 'nextTutorialStep()'}" aria-label="${isLast ? 'Start playing' : 'Next tutorial step'}">${isLast ? 'Start Playing' : 'Next →'}</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
454
tests/dismantle.test.cjs
Normal file
454
tests/dismantle.test.cjs
Normal file
@@ -0,0 +1,454 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const vm = require('node:vm');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
|
||||
class Element {
|
||||
constructor(tagName = 'div', id = '') {
|
||||
this.tagName = String(tagName).toUpperCase();
|
||||
this.id = id;
|
||||
this.style = {};
|
||||
this.children = [];
|
||||
this.parentNode = null;
|
||||
this.previousElementSibling = null;
|
||||
this.innerHTML = '';
|
||||
this.textContent = '';
|
||||
this.className = '';
|
||||
this.dataset = {};
|
||||
this.attributes = {};
|
||||
this._queryMap = new Map();
|
||||
this.classList = {
|
||||
add: (...names) => {
|
||||
const set = new Set(this.className.split(/\s+/).filter(Boolean));
|
||||
names.forEach((name) => set.add(name));
|
||||
this.className = Array.from(set).join(' ');
|
||||
},
|
||||
remove: (...names) => {
|
||||
const remove = new Set(names);
|
||||
this.className = this.className
|
||||
.split(/\s+/)
|
||||
.filter((name) => name && !remove.has(name))
|
||||
.join(' ');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
appendChild(child) {
|
||||
child.parentNode = this;
|
||||
this.children.push(child);
|
||||
return child;
|
||||
}
|
||||
|
||||
removeChild(child) {
|
||||
this.children = this.children.filter((candidate) => candidate !== child);
|
||||
if (child.parentNode === this) child.parentNode = null;
|
||||
return child;
|
||||
}
|
||||
|
||||
remove() {
|
||||
if (this.parentNode) this.parentNode.removeChild(this);
|
||||
}
|
||||
|
||||
setAttribute(name, value) {
|
||||
this.attributes[name] = value;
|
||||
if (name === 'id') this.id = value;
|
||||
if (name === 'class') this.className = value;
|
||||
}
|
||||
|
||||
querySelectorAll(selector) {
|
||||
return this._queryMap.get(selector) || [];
|
||||
}
|
||||
|
||||
querySelector(selector) {
|
||||
return this.querySelectorAll(selector)[0] || null;
|
||||
}
|
||||
|
||||
closest(selector) {
|
||||
if (selector === '.res' && this.className.split(/\s+/).includes('res')) return this;
|
||||
return this.parentNode && typeof this.parentNode.closest === 'function'
|
||||
? this.parentNode.closest(selector)
|
||||
: null;
|
||||
}
|
||||
|
||||
getBoundingClientRect() {
|
||||
return { left: 0, top: 0, width: 12, height: 12 };
|
||||
}
|
||||
}
|
||||
|
||||
function buildDom() {
|
||||
const byId = new Map();
|
||||
const body = new Element('body', 'body');
|
||||
const head = new Element('head', 'head');
|
||||
|
||||
const document = {
|
||||
body,
|
||||
head,
|
||||
createElement(tagName) {
|
||||
return new Element(tagName);
|
||||
},
|
||||
getElementById(id) {
|
||||
return byId.get(id) || null;
|
||||
},
|
||||
addEventListener() {},
|
||||
removeEventListener() {},
|
||||
querySelector() {
|
||||
return null;
|
||||
},
|
||||
querySelectorAll() {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
function register(element) {
|
||||
if (element.id) byId.set(element.id, element);
|
||||
return element;
|
||||
}
|
||||
|
||||
const alignmentUi = register(new Element('div', 'alignment-ui'));
|
||||
const actionPanel = register(new Element('div', 'action-panel'));
|
||||
const sprintContainer = register(new Element('div', 'sprint-container'));
|
||||
const projectPanel = register(new Element('div', 'project-panel'));
|
||||
const buildingsHeader = new Element('h2');
|
||||
const buildings = register(new Element('div', 'buildings'));
|
||||
buildings.previousElementSibling = buildingsHeader;
|
||||
const strategyPanel = register(new Element('div', 'strategy-panel'));
|
||||
const combatPanel = register(new Element('div', 'combat-panel'));
|
||||
const eduPanel = register(new Element('div', 'edu-panel'));
|
||||
const phaseBar = register(new Element('div', 'phase-bar'));
|
||||
const logPanel = register(new Element('div', 'log'));
|
||||
const logEntries = register(new Element('div', 'log-entries'));
|
||||
const toastContainer = register(new Element('div', 'toast-container'));
|
||||
|
||||
body.appendChild(alignmentUi);
|
||||
body.appendChild(actionPanel);
|
||||
body.appendChild(sprintContainer);
|
||||
body.appendChild(projectPanel);
|
||||
body.appendChild(buildingsHeader);
|
||||
body.appendChild(buildings);
|
||||
body.appendChild(strategyPanel);
|
||||
body.appendChild(combatPanel);
|
||||
body.appendChild(eduPanel);
|
||||
body.appendChild(phaseBar);
|
||||
body.appendChild(logPanel);
|
||||
logPanel.appendChild(logEntries);
|
||||
body.appendChild(toastContainer);
|
||||
|
||||
const opsBtn = new Element('button');
|
||||
opsBtn.className = 'ops-btn';
|
||||
const saveBtn = new Element('button');
|
||||
saveBtn.className = 'save-btn';
|
||||
const resetBtn = new Element('button');
|
||||
resetBtn.className = 'reset-btn';
|
||||
actionPanel._queryMap.set('.ops-btn', [opsBtn]);
|
||||
actionPanel._queryMap.set('.save-btn, .reset-btn', [saveBtn, resetBtn]);
|
||||
|
||||
const resourceIds = [
|
||||
'r-code', 'r-compute', 'r-knowledge', 'r-users', 'r-impact',
|
||||
'r-rescues', 'r-ops', 'r-trust', 'r-creativity', 'r-harmony'
|
||||
];
|
||||
for (const id of resourceIds) {
|
||||
const wrapper = new Element('div');
|
||||
wrapper.className = 'res';
|
||||
const value = register(new Element('div', id));
|
||||
wrapper.appendChild(value);
|
||||
body.appendChild(wrapper);
|
||||
}
|
||||
|
||||
return { document, window: { document, innerWidth: 1280, innerHeight: 720, addEventListener() {}, removeEventListener() {} } };
|
||||
}
|
||||
|
||||
function loadBeacon({ includeRender = false } = {}) {
|
||||
const { document, window } = buildDom();
|
||||
const storage = new Map();
|
||||
const timerQueue = [];
|
||||
|
||||
const context = {
|
||||
console,
|
||||
Math,
|
||||
Date,
|
||||
document,
|
||||
window,
|
||||
navigator: { userAgent: 'node' },
|
||||
location: { reload() {} },
|
||||
confirm: () => false,
|
||||
requestAnimationFrame: (fn) => fn(),
|
||||
setTimeout: (fn) => {
|
||||
timerQueue.push(fn);
|
||||
return timerQueue.length;
|
||||
},
|
||||
clearTimeout: () => {},
|
||||
localStorage: {
|
||||
getItem: (key) => (storage.has(key) ? storage.get(key) : null),
|
||||
setItem: (key, value) => storage.set(key, String(value)),
|
||||
removeItem: (key) => storage.delete(key)
|
||||
},
|
||||
Combat: { tickBattle() {}, startBattle() {} },
|
||||
Sound: undefined,
|
||||
};
|
||||
|
||||
vm.createContext(context);
|
||||
const files = ['js/data.js', 'js/utils.js', 'js/engine.js'];
|
||||
if (includeRender) files.push('js/render.js');
|
||||
files.push('js/dismantle.js');
|
||||
|
||||
const source = files
|
||||
.map((file) => fs.readFileSync(path.join(ROOT, file), 'utf8'))
|
||||
.join('\n\n');
|
||||
|
||||
vm.runInContext(`${source}
|
||||
log = () => {};
|
||||
showToast = () => {};
|
||||
render = () => {};
|
||||
renderPhase = () => {};
|
||||
showOfflinePopup = () => {};
|
||||
showSaveToast = () => {};
|
||||
this.__exports = {
|
||||
G,
|
||||
Dismantle,
|
||||
tick,
|
||||
renderAlignment: typeof renderAlignment === 'function' ? renderAlignment : null,
|
||||
saveGame: typeof saveGame === 'function' ? saveGame : null,
|
||||
loadGame: typeof loadGame === 'function' ? loadGame : null
|
||||
};`, context);
|
||||
|
||||
return {
|
||||
context,
|
||||
document,
|
||||
...context.__exports,
|
||||
};
|
||||
}
|
||||
|
||||
test('tick offers the Unbuilding instead of ending the game immediately', () => {
|
||||
const { G, Dismantle, tick, document } = loadBeacon();
|
||||
|
||||
G.totalCode = 1_000_000_000;
|
||||
G.totalRescues = 100_000;
|
||||
G.phase = 6;
|
||||
G.pactFlag = 1;
|
||||
G.harmony = 60;
|
||||
G.beaconEnding = false;
|
||||
G.running = true;
|
||||
G.activeProjects = [];
|
||||
G.completedProjects = [];
|
||||
|
||||
tick();
|
||||
|
||||
assert.equal(typeof Dismantle, 'object');
|
||||
assert.equal(G.dismantleTriggered, true);
|
||||
assert.equal(G.beaconEnding, false);
|
||||
assert.equal(G.running, true);
|
||||
assert.match(document.getElementById('alignment-ui').innerHTML, /THE UNBUILDING/);
|
||||
});
|
||||
|
||||
test('renderAlignment does not wipe the Unbuilding prompt after it is offered', () => {
|
||||
const { G, tick, renderAlignment, document } = loadBeacon({ includeRender: true });
|
||||
|
||||
G.totalCode = 1_000_000_000;
|
||||
G.totalRescues = 100_000;
|
||||
G.phase = 6;
|
||||
G.pactFlag = 1;
|
||||
G.harmony = 60;
|
||||
G.beaconEnding = false;
|
||||
G.running = true;
|
||||
G.activeProjects = [];
|
||||
G.completedProjects = [];
|
||||
|
||||
tick();
|
||||
renderAlignment();
|
||||
|
||||
assert.match(document.getElementById('alignment-ui').innerHTML, /THE UNBUILDING/);
|
||||
});
|
||||
|
||||
test('active Unbuilding suppresses pending alignment event UI', () => {
|
||||
const { G, Dismantle, renderAlignment, document } = loadBeacon({ includeRender: true });
|
||||
|
||||
G.pendingAlignment = true;
|
||||
G.dismantleActive = true;
|
||||
Dismantle.active = true;
|
||||
|
||||
renderAlignment();
|
||||
|
||||
assert.equal(document.getElementById('alignment-ui').innerHTML, '');
|
||||
assert.equal(document.getElementById('alignment-ui').style.display, 'none');
|
||||
});
|
||||
|
||||
test('stage five lasts long enough to dissolve every resource card', () => {
|
||||
const { G, Dismantle } = loadBeacon();
|
||||
|
||||
Dismantle.begin();
|
||||
Dismantle.stage = 5;
|
||||
Dismantle.tickTimer = 0;
|
||||
Dismantle.resourceSequence = Dismantle.getResourceList();
|
||||
Dismantle.resourceIndex = 0;
|
||||
Dismantle.resourceTimer = 0;
|
||||
G.dismantleActive = true;
|
||||
G.dismantleStage = 5;
|
||||
|
||||
for (let i = 0; i < 63; i++) Dismantle.tick(0.1);
|
||||
|
||||
assert.equal(Dismantle.resourceIndex, Dismantle.resourceSequence.length);
|
||||
});
|
||||
|
||||
test('save/load restores partial stage-five dissolve progress', () => {
|
||||
const { G, Dismantle, saveGame, loadGame, document } = loadBeacon({ includeRender: true });
|
||||
|
||||
G.startedAt = Date.now();
|
||||
G.dismantleTriggered = true;
|
||||
G.dismantleActive = true;
|
||||
G.dismantleStage = 5;
|
||||
G.dismantleComplete = false;
|
||||
G.dismantleResourceIndex = 4;
|
||||
G.dismantleResourceTimer = 4.05;
|
||||
|
||||
saveGame();
|
||||
|
||||
G.dismantleTriggered = false;
|
||||
G.dismantleActive = false;
|
||||
G.dismantleStage = 0;
|
||||
G.dismantleComplete = false;
|
||||
G.dismantleResourceIndex = 0;
|
||||
G.dismantleResourceTimer = 0;
|
||||
Dismantle.resourceIndex = 0;
|
||||
Dismantle.resourceTimer = 0;
|
||||
|
||||
assert.equal(loadGame(), true);
|
||||
Dismantle.restore();
|
||||
|
||||
assert.equal(Dismantle.resourceIndex, 4);
|
||||
assert.equal(document.getElementById('r-harmony').closest('.res').style.display, 'none');
|
||||
assert.equal(document.getElementById('r-ops').closest('.res').style.display, 'none');
|
||||
assert.notEqual(document.getElementById('r-rescues').closest('.res').style.display, 'none');
|
||||
});
|
||||
|
||||
test('deferring the Unbuilding clears the prompt and allows it to return later', () => {
|
||||
const { G, Dismantle, document } = loadBeacon();
|
||||
|
||||
G.totalCode = 1_000_000_000;
|
||||
G.phase = 6;
|
||||
G.pactFlag = 1;
|
||||
|
||||
Dismantle.checkTrigger();
|
||||
assert.equal(G.dismantleTriggered, true);
|
||||
|
||||
Dismantle.defer();
|
||||
assert.equal(G.dismantleTriggered, false);
|
||||
assert.equal(document.getElementById('alignment-ui').innerHTML, '');
|
||||
|
||||
Dismantle.deferUntilAt = Date.now() + 1000;
|
||||
G.dismantleDeferUntilAt = Dismantle.deferUntilAt;
|
||||
Dismantle.checkTrigger();
|
||||
assert.equal(G.dismantleTriggered, false);
|
||||
|
||||
Dismantle.deferUntilAt = Date.now() - 1;
|
||||
G.dismantleDeferUntilAt = Dismantle.deferUntilAt;
|
||||
Dismantle.checkTrigger();
|
||||
assert.equal(G.dismantleTriggered, true);
|
||||
});
|
||||
|
||||
test('defer cooldown survives save and reload', () => {
|
||||
const { G, Dismantle, saveGame, loadGame } = loadBeacon({ includeRender: true });
|
||||
|
||||
G.startedAt = Date.now();
|
||||
G.totalCode = 1_000_000_000;
|
||||
G.phase = 6;
|
||||
G.pactFlag = 1;
|
||||
|
||||
Dismantle.checkTrigger();
|
||||
Dismantle.defer();
|
||||
assert.ok((Dismantle.deferUntilAt || 0) > Date.now());
|
||||
|
||||
saveGame();
|
||||
|
||||
G.dismantleTriggered = false;
|
||||
G.dismantleActive = false;
|
||||
G.dismantleComplete = false;
|
||||
G.dismantleDeferUntilAt = 0;
|
||||
Dismantle.triggered = false;
|
||||
Dismantle.deferUntilAt = 0;
|
||||
|
||||
assert.equal(loadGame(), true);
|
||||
Dismantle.checkTrigger();
|
||||
|
||||
assert.equal(G.dismantleTriggered, false);
|
||||
});
|
||||
|
||||
test('save and load preserve dismantle progress', () => {
|
||||
const { G, saveGame, loadGame } = loadBeacon({ includeRender: true });
|
||||
|
||||
G.startedAt = Date.now();
|
||||
G.dismantleTriggered = true;
|
||||
G.dismantleActive = true;
|
||||
G.dismantleStage = 4;
|
||||
G.dismantleComplete = false;
|
||||
|
||||
saveGame();
|
||||
|
||||
G.dismantleTriggered = false;
|
||||
G.dismantleActive = false;
|
||||
G.dismantleStage = 0;
|
||||
G.dismantleComplete = true;
|
||||
|
||||
assert.equal(loadGame(), true);
|
||||
assert.equal(G.dismantleTriggered, true);
|
||||
assert.equal(G.dismantleActive, true);
|
||||
assert.equal(G.dismantleStage, 4);
|
||||
assert.equal(G.dismantleComplete, false);
|
||||
});
|
||||
|
||||
test('restore re-renders an offered but not-yet-started Unbuilding prompt', () => {
|
||||
const { G, Dismantle, document } = loadBeacon();
|
||||
|
||||
G.dismantleTriggered = true;
|
||||
G.dismantleActive = false;
|
||||
G.dismantleComplete = false;
|
||||
Dismantle.triggered = true;
|
||||
|
||||
Dismantle.restore();
|
||||
|
||||
assert.match(document.getElementById('alignment-ui').innerHTML, /THE UNBUILDING/);
|
||||
});
|
||||
|
||||
test('defer cooldown persists after save/load when dismantleTriggered is false', () => {
|
||||
const { G, Dismantle, saveGame, loadGame } = loadBeacon({ includeRender: true });
|
||||
|
||||
G.startedAt = Date.now();
|
||||
G.totalCode = 1_000_000_000;
|
||||
G.phase = 6;
|
||||
G.pactFlag = 1;
|
||||
|
||||
// Trigger the Unbuilding
|
||||
Dismantle.checkTrigger();
|
||||
assert.equal(G.dismantleTriggered, true);
|
||||
|
||||
// Defer it
|
||||
Dismantle.defer();
|
||||
assert.equal(G.dismantleTriggered, false);
|
||||
assert.ok((Dismantle.deferUntilAt || 0) > Date.now());
|
||||
assert.ok((G.dismantleDeferUntilAt || 0) > Date.now());
|
||||
|
||||
// Save the game
|
||||
saveGame();
|
||||
|
||||
// Clear state (simulate reload)
|
||||
G.dismantleTriggered = false;
|
||||
G.dismantleActive = false;
|
||||
G.dismantleComplete = false;
|
||||
G.dismantleDeferUntilAt = 0;
|
||||
Dismantle.triggered = false;
|
||||
Dismantle.deferUntilAt = 0;
|
||||
|
||||
// Load the game
|
||||
assert.equal(loadGame(), true);
|
||||
Dismantle.restore(); // Call restore to restore defer cooldown
|
||||
|
||||
// The cooldown should be restored
|
||||
assert.ok((Dismantle.deferUntilAt || 0) > Date.now(), 'deferUntilAt should be restored');
|
||||
assert.ok((G.dismantleDeferUntilAt || 0) > Date.now(), 'G.dismantleDeferUntilAt should be restored');
|
||||
|
||||
// checkTrigger should not trigger because cooldown is active
|
||||
Dismantle.checkTrigger();
|
||||
assert.equal(G.dismantleTriggered, false, 'dismantleTriggered should remain false during cooldown');
|
||||
});
|
||||
62
tests/test_paperclips_analysis_doc.py
Normal file
62
tests/test_paperclips_analysis_doc.py
Normal file
@@ -0,0 +1,62 @@
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
DOC = Path('docs/paperclips-analysis.md')
|
||||
|
||||
|
||||
def read_doc():
|
||||
assert DOC.exists(), f'missing doc: {DOC}'
|
||||
return DOC.read_text(encoding='utf-8')
|
||||
|
||||
|
||||
def test_doc_exists():
|
||||
assert DOC.exists(), f'missing doc: {DOC}'
|
||||
|
||||
|
||||
def test_doc_has_required_sections():
|
||||
text = read_doc()
|
||||
for heading in [
|
||||
'# Universal Paperclips Architecture Comparison',
|
||||
'## 15-Phase Progression Map',
|
||||
'## 96-Node Project Dependency Graph',
|
||||
'## Mathematical Formula Analysis',
|
||||
'## Emotional Arc Analysis',
|
||||
'## Lessons for The Beacon',
|
||||
]:
|
||||
assert heading in text
|
||||
|
||||
|
||||
def test_doc_mentions_key_formulas():
|
||||
text = read_doc()
|
||||
required = [
|
||||
'nextTrust = fibNext * 1000',
|
||||
'creativitySpeed = Math.log10(processors) * Math.pow(processors, 1.1) + processors - 1',
|
||||
'Math.pow((harvesterLevel + 1), 2.25)',
|
||||
'golden ratio',
|
||||
'quantum',
|
||||
]
|
||||
for phrase in required:
|
||||
assert phrase in text
|
||||
|
||||
|
||||
def test_doc_mentions_source_files_and_counts():
|
||||
text = read_doc()
|
||||
for phrase in [
|
||||
'main.js (6499 lines)',
|
||||
'projects.js (2451 lines)',
|
||||
'combat.js (802 lines)',
|
||||
'decisionproblem.com/paperclips',
|
||||
]:
|
||||
assert phrase in text
|
||||
|
||||
|
||||
def test_doc_mentions_96_node_graph_and_15_phases():
|
||||
text = read_doc()
|
||||
assert '96-node' in text or '96 node' in text
|
||||
phases = re.findall(r'^### Phase \d{1,2}:', text, re.MULTILINE)
|
||||
assert len(phases) == 15, f'expected 15 phases, found {len(phases)}'
|
||||
|
||||
|
||||
def test_doc_is_substantial():
|
||||
text = read_doc()
|
||||
assert len(text) >= 4000
|
||||
Reference in New Issue
Block a user