Compare commits
33 Commits
fix/offlin
...
feat/agent
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e5538319b | ||
| 26879de76e | |||
| c197fabc69 | |||
| 9733b9022e | |||
| 967025fbd4 | |||
|
|
9854501bbd | ||
|
|
68ee64866a | ||
| be0264fc95 | |||
|
|
e6d0df40b4 | ||
|
|
23dd95ed46 | ||
|
|
0849754a87 | ||
|
|
8d51349e64 | ||
|
|
24940fe465 | ||
|
|
16273a5a15 | ||
|
|
5d51e14875 | ||
|
|
5fc0ad7b22 | ||
| f948ec9c5e | |||
|
|
9403f700d2 | ||
|
|
13e77a12f2 | ||
| 6081844387 | |||
|
|
09b8c02307 | ||
|
|
9106d3f84c | ||
| 3f02359748 | |||
| 85a146b690 | |||
| cb2e48bf9a | |||
|
|
8d43b5c911 | ||
|
|
8cdabe9771 | ||
|
|
5c88fe77be | ||
|
|
931473e8f8 | ||
|
|
fe76150325 | ||
|
|
a3f1802473 | ||
|
|
3d414b2de6 | ||
|
|
612eb1f4d5 |
27
.gitea/workflows/a11y.yml
Normal file
27
.gitea/workflows/a11y.yml
Normal file
@@ -0,0 +1,27 @@
|
||||
name: Accessibility Checks
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
a11y-audit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Validate ARIA Attributes in game.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)
|
||||
|
||||
- name: Validate ARIA Roles in index.html
|
||||
run: |
|
||||
echo "Checking index.html for ARIA roles..."
|
||||
grep -q "role=" index.html || (echo "ERROR: No ARIA roles found in index.html" && exit 1)
|
||||
|
||||
- name: Syntax Check JS
|
||||
run: |
|
||||
node -c game.js
|
||||
30
.gitea/workflows/guardrails.yml
Normal file
30
.gitea/workflows/guardrails.yml
Normal file
@@ -0,0 +1,30 @@
|
||||
name: Guardrails
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
# This workflow is the enforcement layer for the rules in AGENTS.md. It runs on
|
||||
# every PR and push to main. A failure here blocks merge. See AGENTS.md for the
|
||||
# reasoning behind each check.
|
||||
|
||||
jobs:
|
||||
guardrails:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Syntax check game.js
|
||||
run: node -c game.js
|
||||
|
||||
- name: Headless smoke test
|
||||
run: node scripts/smoke.mjs
|
||||
|
||||
- name: Static guardrails
|
||||
shell: bash
|
||||
run: bash scripts/guardrails.sh
|
||||
24
.gitea/workflows/smoke.yml
Normal file
24
.gitea/workflows/smoke.yml
Normal file
@@ -0,0 +1,24 @@
|
||||
name: Smoke Test
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main]
|
||||
jobs:
|
||||
smoke:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- name: Parse check
|
||||
run: |
|
||||
find . -name '*.yml' -o -name '*.yaml' | grep -v .gitea | xargs -r python3 -c "import sys,yaml; [yaml.safe_load(open(f)) for f in sys.argv[1:]]"
|
||||
find . -name '*.json' | xargs -r python3 -m json.tool > /dev/null
|
||||
find . -name '*.py' | xargs -r python3 -m py_compile
|
||||
find . -name '*.sh' | xargs -r bash -n
|
||||
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
|
||||
echo "PASS: No secrets"
|
||||
89
AGENTS.md
Normal file
89
AGENTS.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# AGENTS.md — guardrails for agents contributing to The Beacon
|
||||
|
||||
This file documents the non-obvious rules that any contributor (human or AI) should know before touching `game.js`. It is enforced in two layers: a headless smoke test (`scripts/smoke.mjs`) and static grep-based checks (`.gitea/workflows/guardrails.yml`). Both run on every PR.
|
||||
|
||||
The Beacon is a single-file, browser-only idle game. The codebase is small, the mechanics are interlocked, and the most common failure mode is **quietly wrong math** — the game keeps rendering, the player just slowly loses. These rules exist to make that class of bug impossible to land.
|
||||
|
||||
---
|
||||
|
||||
## 1. The persistent-multiplier rule (`*Boost`)
|
||||
|
||||
`G.codeBoost`, `G.computeBoost`, `G.knowledgeBoost`, `G.userBoost`, `G.impactBoost` are **persistent multipliers**. They are set once (by projects, sprints, alignment events) and read on every tick inside `updateRates()`. They are *never* reset between ticks.
|
||||
|
||||
`G.codeRate`, `G.computeRate`, etc. are **per-tick rate fields**. They are reset to `0` at the top of `updateRates()` and rebuilt from scratch every tick.
|
||||
|
||||
**The bug class this created:** `community_drama`'s original `applyFn` did `G.codeBoost *= 0.7`. Because `applyFn` is invoked from `updateRates()` every 100 ms, `codeBoost` decayed to ~`3e-16` after a minute of in-game time. The rendering was fine, the click button worked, the player just saw their code rate silently vanish.
|
||||
|
||||
**The rule:**
|
||||
|
||||
> Inside any function that runs more than once per persistent state change — specifically `updateRates()`, `tick()`, `applyFn`, or anything invoked from them — **never** mutate `G.codeBoost`, `G.computeBoost`, `G.knowledgeBoost`, `G.userBoost`, or `G.impactBoost`.
|
||||
>
|
||||
> If you want a debuff to reduce code production, mutate `G.codeRate` inside the debuff's `applyFn`. `G.codeRate` is zeroed at the top of `updateRates()`, so `*= 0.7` applies exactly once per tick — which is what you want.
|
||||
>
|
||||
> `*Boost` fields should only be written by one-shot events: project `effect()` callbacks, sprint start/end, alignment resolutions. Those run zero or one times per player action, not per tick.
|
||||
|
||||
The guardrails workflow runs `grep -nE '(codeBoost|computeBoost|knowledgeBoost|userBoost|impactBoost)\s*[*/+-]?=' game.js` and fails the job if any hit falls inside an `applyFn` block or inside the `updateRates()`/`tick()` bodies.
|
||||
|
||||
## 2. Click power has exactly one source of truth
|
||||
|
||||
The click-power formula `(1 + floor(autocoder * 0.5) + max(0, phase - 1) * 2) * codeBoost` used to live in three places: `writeCode()`, `autoType()`, and the Swarm Protocol branch of `updateRates()`. Changing one without the others was trivially easy and hard to notice.
|
||||
|
||||
**The rule:**
|
||||
|
||||
> Click power is computed only by `getClickPower()`. Any function that needs "how much code does one click generate right now" must call `getClickPower()` directly. Do not inline the formula.
|
||||
|
||||
The guardrails check greps for `Math.floor(G.buildings.autocoder * 0.5)` and fails if it appears outside `getClickPower()`.
|
||||
|
||||
## 3. Save ↔ load must stay symmetric
|
||||
|
||||
`saveGame()` writes a hand-curated set of fields to `localStorage`. `loadGame()` should restore exactly those fields, no more and no less. The old `loadGame()` used `Object.assign(G, data)`, which copied whatever was in the JSON including keys the game never wrote. That was simultaneously a prompt-injection surface (a malicious save file could set arbitrary keys) and a silent drift trap (fields added to `G` but forgotten in `saveGame` would reset every reload).
|
||||
|
||||
**The rule:**
|
||||
|
||||
> The list of save fields must be defined exactly once, as a top-level `const SAVE_FIELDS` array. Both `saveGame()` and `loadGame()` read that array. Loading uses a whitelisted copy, not `Object.assign`.
|
||||
>
|
||||
> When you add a new field to `G` that represents persistent player state, add it to `SAVE_FIELDS`. If you're unsure whether a field should persist, ask: "if the player refreshes the page, do they expect this to be the same?"
|
||||
|
||||
The smoke test includes a save → load round-trip check (`scripts/smoke.mjs` section 7). Extend the fields it sets/verifies whenever you add new persistent state.
|
||||
|
||||
## 4. `applyFn` is called once per `updateRates()`, not once per event
|
||||
|
||||
`G.activeDebuffs[].applyFn` is invoked at the end of `updateRates()`. That function runs ~10 times per second. If you want a debuff to apply a *rate reduction* (subtract from/multiply down a per-tick rate field) the math works out — because the field was just reset. If you want it to *prevent progression*, set a flag (`G.flags.*`) and check that flag in the places that generate progression, rather than continuously rewriting state.
|
||||
|
||||
See rule 1. These are the same rule viewed from two angles.
|
||||
|
||||
## 5. Event `resolveCost` should live on the event definition, not be duplicated
|
||||
|
||||
Currently, each entry in the `EVENTS` array declares `resolveCost` twice — once as a property of the event object, and again inside the object pushed onto `G.activeDebuffs`. Keep them in sync until someone refactors this into a single source. When adding a new event, **copy the `resolveCost` literally** between the two sites. The smoke test does not yet catch drift here; a follow-up PR should pull the debuff object construction out into a helper.
|
||||
|
||||
## 6. Don't trust `G.flags` to exist implicitly
|
||||
|
||||
`G.flags` is initialized as `{}` at the top of the file. Do not replace it with a reference somewhere else — other code assumes `G.flags` is a live object reference and reads sub-fields like `G.flags.creativity` directly. New sub-flags go inside `G.flags`; new top-level flags should use the `somethingFlag` naming convention (see `deployFlag`, `pactFlag`, etc.) but that pattern is being consolidated into `G.flags` over time.
|
||||
|
||||
## 7. Copyright, assets, secrets
|
||||
|
||||
- No third-party assets without a license note.
|
||||
- No API keys, tokens, or credentials in the repo. The smoke workflow scans for `sk-ant-`, `sk-or-`, `ghp_`, `AKIA` literal prefixes.
|
||||
- Educational blurbs (`edu:` strings in `BDEF`/`PDEFS`) are authored content — don't generate new ones from other people's copyrighted material.
|
||||
|
||||
---
|
||||
|
||||
## Running the guardrails locally
|
||||
|
||||
```sh
|
||||
node scripts/smoke.mjs # headless smoke test
|
||||
grep -nE "\\*Boost\\s*\\*=" game.js # spot persistent-multiplier mutations
|
||||
node -c game.js # syntax check
|
||||
```
|
||||
|
||||
The CI job `.gitea/workflows/guardrails.yml` runs all three on every PR. A failure blocks merge; see the job log for exactly which invariant broke.
|
||||
|
||||
## How to add a new guardrail
|
||||
|
||||
1. Write a test in `scripts/smoke.mjs` that fails on the bug you just found and would have caught.
|
||||
2. Fix the bug.
|
||||
3. Confirm the test now passes.
|
||||
4. Add a short rule to this file explaining *why* the invariant exists (usually a war story helps).
|
||||
5. If the bug is detectable by grep, add a check to `.gitea/workflows/guardrails.yml` so it fails fast on PRs.
|
||||
|
||||
The smoke test's job isn't to be exhaustive — it's to encode the specific class of bugs that have actually hit production, so we never see the same one twice.
|
||||
93
index.html
93
index.html
@@ -3,6 +3,23 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="The Beacon — a sovereign AI idle game. Build an AI from scratch. Write code, train models, save lives.">
|
||||
<meta name="theme-color" content="#0a0a0a">
|
||||
<meta name="author" content="Timmy Foundation">
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:title" content="The Beacon">
|
||||
<meta property="og:description" content="A sovereign AI idle game. Build an AI from scratch. Write code, train models, save lives.">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:image" content="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🏠</text></svg>">
|
||||
|
||||
<!-- Twitter Card -->
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:title" content="The Beacon">
|
||||
<meta name="twitter:description" content="A sovereign AI idle game. Build an AI from scratch. Write code, train models, save lives.">
|
||||
|
||||
<!-- Favicon (inline SVG beacon) -->
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🏠</text></svg>">
|
||||
<title>The Beacon - Build Sovereign AI</title>
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
@@ -14,6 +31,15 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code
|
||||
#phase-bar{text-align:center;padding:10px;margin:12px 16px;background:var(--panel);border:1px solid var(--border);border-radius:6px}
|
||||
#phase-bar .phase-name{font-size:14px;font-weight:700;color:var(--gold);letter-spacing:2px}
|
||||
#phase-bar .phase-desc{font-size:10px;color:var(--dim);margin-top:4px;font-style:italic}
|
||||
.progress-wrap{margin-top:8px;height:6px;background:#111;border-radius:3px;overflow:hidden;position:relative}
|
||||
.progress-fill{height:100%;border-radius:3px;transition:width 0.5s ease;background:linear-gradient(90deg,#1a3a5a,var(--accent))}
|
||||
.progress-label{font-size:9px;color:var(--dim);margin-top:4px;display:flex;justify-content:space-between}
|
||||
.milestone-row{display:flex;gap:6px;margin-top:6px;justify-content:center;flex-wrap:wrap}
|
||||
.milestone-chip{font-size:9px;padding:2px 8px;border-radius:10px;border:1px solid var(--border);color:var(--dim);background:#0a0a14}
|
||||
.milestone-chip.next{border-color:var(--accent);color:var(--accent);animation:pulse-chip 2s ease-in-out infinite}
|
||||
.milestone-chip.done{border-color:#2a4a2a;color:var(--green);opacity:0.6}
|
||||
@keyframes pulse-chip{0%,100%{box-shadow:0 0 0 rgba(74,158,255,0)}50%{box-shadow:0 0 8px rgba(74,158,255,0.3)}}
|
||||
@keyframes beacon-glow{0%,100%{opacity:0.7}50%{opacity:1}}
|
||||
#resources{display:grid;grid-template-columns:repeat(auto-fit,minmax(100px,1fr));gap:6px;margin:12px 16px}
|
||||
.res{background:var(--panel);border:1px solid var(--border);border-radius:4px;padding:8px 10px;text-align:center}
|
||||
.res .r-label{font-size:9px;color:var(--dim);text-transform:uppercase;letter-spacing:1px}
|
||||
@@ -28,6 +54,8 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code
|
||||
.main-btn{background:linear-gradient(135deg,#1a2a3a,#0e1520);border:1px solid var(--accent);color:var(--accent);font-size:14px;padding:14px 10px;border-radius:4px;cursor:pointer;font-family:inherit;transition:all 0.2s}
|
||||
.main-btn:hover{background:linear-gradient(135deg,#203040,#0e2030);box-shadow:0 0 20px var(--glow);transform:scale(1.02)}
|
||||
.main-btn:active{transform:scale(0.98)}
|
||||
@keyframes pulse-glow{0%,100%{box-shadow:0 0 10px rgba(74,158,255,0.1)}50%{box-shadow:0 0 25px rgba(74,158,255,0.4)}}
|
||||
.main-btn.pulse{animation:pulse-glow 2s ease-in-out infinite}
|
||||
.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}
|
||||
@@ -58,17 +86,33 @@ 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}
|
||||
#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}
|
||||
.toast-event{background:rgba(244,67,54,0.12);border-color:#f44336;color:#ff8a80}
|
||||
.toast-project{background:rgba(255,215,0,0.12);border-color:#ffd700;color:#ffd700}
|
||||
.toast-milestone{background:rgba(76,175,80,0.12);border-color:#4caf50;color:#81c784}
|
||||
.toast-info{background:rgba(74,158,255,0.12);border-color:#4a9eff;color:#80bfff}
|
||||
@keyframes toast-in{from{transform:translateX(40px);opacity:0}to{transform:translateX(0);opacity:0.95}}
|
||||
@keyframes toast-out{from{opacity:0.95;transform:translateX(0)}to{opacity:0;transform:translateX(40px)}}
|
||||
::-webkit-scrollbar{width:4px}::-webkit-scrollbar-track{background:var(--bg)}::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="header">
|
||||
<div id="pulse-container" style="position:relative;display:inline-block;margin-bottom:4px">
|
||||
<div id="pulse-dot" style="width:8px;height:8px;border-radius:50%;background:#333;display:inline-block;vertical-align:middle;transition:background 0.5s,box-shadow 0.5s"></div>
|
||||
<span id="pulse-label" style="font-size:9px;color:#444;margin-left:6px;vertical-align:middle;letter-spacing:1px">OFFLINE</span>
|
||||
</div>
|
||||
<h1>THE BEACON</h1>
|
||||
<div class="sub">A Sovereign AI Idle Game</div>
|
||||
</div>
|
||||
<div id="phase-bar">
|
||||
<div class="phase-name" id="phase-name">PHASE 1: THE FIRST LINE</div>
|
||||
<div class="phase-desc" id="phase-desc">Write code. Automate. Build the foundation.</div>
|
||||
<div class="progress-wrap"><div class="progress-fill" id="phase-progress" style="width:0%"></div></div>
|
||||
<div class="progress-label"><span id="phase-progress-label">0%</span><span id="phase-progress-target">Next: Phase 2 (2,000 code)</span></div>
|
||||
<div class="milestone-row" id="milestone-chips"></div>
|
||||
</div>
|
||||
<div id="resources">
|
||||
<div class="res"><div class="r-label">Code</div><div class="r-val" id="r-code">0</div><div class="r-rate" id="r-code-rate">+0/s</div></div>
|
||||
@@ -86,6 +130,8 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code
|
||||
<div class="panel" id="action-panel">
|
||||
<h2>ACTIONS</h2>
|
||||
<div class="action-btn-group"><button class="main-btn" onclick="writeCode()">WRITE CODE</button></div>
|
||||
<div id="combo-display" style="text-align:center;font-size:10px;color:var(--dim);height:14px;margin-bottom:4px;transition:all 0.2s"></div>
|
||||
<div id="debuffs" style="display:none;margin-top:8px"></div>
|
||||
<div class="action-btn-group">
|
||||
<button class="ops-btn" onclick="doOps('boost_code')">Ops -> Code</button>
|
||||
<button class="ops-btn" onclick="doOps('boost_compute')">Ops -> Compute</button>
|
||||
@@ -94,8 +140,17 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code
|
||||
<button class="ops-btn" onclick="doOps('boost_knowledge')">Ops -> Knowledge</button>
|
||||
<button class="ops-btn" onclick="doOps('boost_trust')">Ops -> Trust</button>
|
||||
</div>
|
||||
<div id="sprint-container" style="display:none;margin-top:6px">
|
||||
<button id="sprint-btn" class="main-btn" onclick="activateSprint()" style="font-size:11px;padding:8px 10px;border-color:#ffd700;color:#ffd700;width:100%">⚡ CODE SPRINT — 10x Code for 10s</button>
|
||||
<div id="sprint-bar-wrap" style="display:none;margin-top:4px;height:4px;background:#111;border-radius:2px;overflow:hidden"><div id="sprint-bar" style="height:100%;background:linear-gradient(90deg,#ffd700,#ff8c00);border-radius:2px;transition:width 0.1s"></div></div>
|
||||
<div id="sprint-label" style="font-size:9px;color:#666;margin-top:2px;text-align:center"></div>
|
||||
</div>
|
||||
<div id="alignment-ui" style="display:none"></div>
|
||||
<button class="save-btn" onclick="saveGame()">Save Game</button>
|
||||
<button class="save-btn" onclick="saveGame()">Save Game [Ctrl+S]</button>
|
||||
<div style="display:flex;gap:4px;margin-top:4px">
|
||||
<button class="save-btn" onclick="exportSave()" style="flex:1">Export [E]</button>
|
||||
<button class="save-btn" onclick="importSave()" style="flex:1">Import [I]</button>
|
||||
</div>
|
||||
<button class="reset-btn" onclick="if(confirm('Reset all progress?')){localStorage.removeItem('the-beacon-v2');location.reload()}">Reset Progress</button>
|
||||
<h2>BUILDINGS</h2>
|
||||
<div id="buildings"></div>
|
||||
@@ -116,8 +171,10 @@ Projects Done: <span id="st-projects">0</span><br>
|
||||
Time Played: <span id="st-time">0:00</span><br>
|
||||
Clicks: <span id="st-clicks">0</span><br>
|
||||
Harmony: <span id="st-harmony">50</span><br>
|
||||
Drift: <span id="st-drift">0</span>
|
||||
Drift: <span id="st-drift">0</span><br>
|
||||
Events Resolved: <span id="st-resolved">0</span>
|
||||
</div>
|
||||
<div id="production-breakdown" style="display:none;margin-top:12px;padding-top:10px;border-top:1px solid var(--border)"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="edu-panel">
|
||||
@@ -129,6 +186,27 @@ Drift: <span id="st-drift">0</span>
|
||||
<div id="log-entries"></div>
|
||||
</div>
|
||||
<div id="save-toast" style="display:none;position:fixed;top:16px;right:16px;background:#0e1420;border:1px solid #2a3a4a;color:#4a9eff;font-size:10px;padding:6px 12px;border-radius:4px;z-index:50;opacity:0;transition:opacity 0.4s;pointer-events:none">Save</div>
|
||||
<div id="help-btn" onclick="toggleHelp()" style="position:fixed;bottom:16px;right:16px;width:28px;height:28px;background:#0e0e1a;border:1px solid #333;color:#555;font-size:14px;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:50;font-family:inherit;transition:all 0.2s" title="Keyboard shortcuts (?)">?</div>
|
||||
<div id="help-overlay" onclick="if(event.target===this)toggleHelp()" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(8,8,16,0.92);z-index:80;justify-content:center;align-items:center;flex-direction:column;padding:40px">
|
||||
<div style="background:#0e0e1a;border:1px solid #1a3a5a;border-radius:8px;padding:24px 32px;max-width:420px;width:100%">
|
||||
<h3 style="color:#4a9eff;font-size:14px;letter-spacing:2px;margin-bottom:16px;text-align:center">KEYBOARD SHORTCUTS</h3>
|
||||
<div style="font-size:11px;line-height:2.2;color:#aaa">
|
||||
<div style="display:flex;justify-content:space-between"><span style="color:#555">Write Code</span><span style="color:#4a9eff;font-family:monospace">SPACE</span></div>
|
||||
<div style="display:flex;justify-content:space-between"><span style="color:#555">Code Sprint</span><span style="color:#ffd700;font-family:monospace">S</span></div>
|
||||
<div style="display:flex;justify-content:space-between"><span style="color:#555">Ops → Code</span><span style="color:#b388ff;font-family:monospace">1</span></div>
|
||||
<div style="display:flex;justify-content:space-between"><span style="color:#555">Ops → Compute</span><span style="color:#b388ff;font-family:monospace">2</span></div>
|
||||
<div style="display:flex;justify-content:space-between"><span style="color:#555">Ops → Knowledge</span><span style="color:#b388ff;font-family:monospace">3</span></div>
|
||||
<div style="display:flex;justify-content:space-between"><span style="color:#555">Ops → Trust</span><span style="color:#b388ff;font-family:monospace">4</span></div>
|
||||
<div style="display:flex;justify-content:space-between"><span style="color:#555">Cycle Buy Amount (x1/x10/MAX)</span><span style="color:#4a9eff;font-family:monospace">B</span></div>
|
||||
<div style="display:flex;justify-content:space-between"><span style="color:#555">Save Game</span><span style="color:#4a9eff;font-family:monospace">Ctrl+S</span></div>
|
||||
<div style="display:flex;justify-content:space-between"><span style="color:#555">Export Save</span><span style="color:#4a9eff;font-family:monospace">E</span></div>
|
||||
<div style="display:flex;justify-content:space-between"><span style="color:#555">Import Save</span><span style="color:#4a9eff;font-family:monospace">I</span></div>
|
||||
<div style="display:flex;justify-content:space-between;border-top:1px solid #1a1a2e;padding-top:8px;margin-top:4px"><span style="color:#555">This Help</span><span style="color:#555;font-family:monospace">? or /</span></div>
|
||||
</div>
|
||||
<div style="text-align:center;margin-top:16px;font-size:9px;color:#444">Click WRITE CODE fast for combo bonuses! 10x=ops, 20x=knowledge, 30x+=bonus code</div>
|
||||
<button onclick="toggleHelp()" style="display:block;margin:16px auto 0;background:#1a2a3a;border:1px solid #4a9eff;color:#4a9eff;padding:6px 20px;border-radius:4px;cursor:pointer;font-family:inherit;font-size:11px">Close [?]</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="drift-ending">
|
||||
<h2>THE DRIFT</h2>
|
||||
<p>You became very good at what you do.</p>
|
||||
@@ -142,5 +220,16 @@ The light is on. The room is empty."
|
||||
<button onclick="if(confirm('Start over? The old save will be lost.')){localStorage.removeItem('the-beacon-v2');location.reload()}">START OVER</button>
|
||||
</div>
|
||||
<script src="game.js"></script>
|
||||
|
||||
<div id="offline-popup" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(8,8,16,0.92);z-index:90;justify-content:center;align-items:center;flex-direction:column;text-align:center;padding:40px">
|
||||
<div style="background:#0e0e1a;border:1px solid #1a3a5a;border-radius:8px;padding:24px 32px;max-width:400px;width:100%">
|
||||
<h3 style="color:#4a9eff;font-size:14px;letter-spacing:2px;margin-bottom:16px">WELCOME BACK</h3>
|
||||
<p style="color:#888;font-size:10px;margin-bottom:12px" id="offline-time-label">You were away for 0 minutes.</p>
|
||||
<div id="offline-gains-list" style="text-align:left;font-size:11px;line-height:1.8;margin-bottom:16px"></div>
|
||||
<button onclick="dismissOfflinePopup()" style="background:#1a2a3a;border:1px solid #4a9eff;color:#4a9eff;padding:8px 20px;border-radius:4px;cursor:pointer;font-family:inherit;font-size:11px">Continue</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="toast-container"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
102
scripts/guardrails.sh
Executable file
102
scripts/guardrails.sh
Executable file
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env bash
|
||||
# Static guardrail checks for game.js. Run from repo root.
|
||||
#
|
||||
# Each check prints a PASS/FAIL line and contributes to the final exit code.
|
||||
# The rules enforced here come from AGENTS.md — keep the two files in sync.
|
||||
#
|
||||
# Some rules are marked PENDING: they describe invariants we've agreed on but
|
||||
# haven't reached on main yet (because another open PR is landing the fix).
|
||||
# PENDING rules print their current violation count without failing the job;
|
||||
# convert them to hard failures once the blocking PR merges.
|
||||
|
||||
set -u
|
||||
fail=0
|
||||
|
||||
say() { printf '%s\n' "$*"; }
|
||||
banner() { say ""; say "==== $* ===="; }
|
||||
|
||||
# ---------- Rule 1: no *Boost mutation inside applyFn blocks ----------
|
||||
# Persistent multipliers (codeBoost, computeBoost, ...) must not be written
|
||||
# from any function that runs per tick. The `applyFn` of a debuff is invoked
|
||||
# on every updateRates() call, so `G.codeBoost *= 0.7` inside applyFn compounds
|
||||
# and silently zeros code production. See AGENTS.md rule 1.
|
||||
banner "Rule 1: no *Boost mutation inside applyFn"
|
||||
rule1_hits=$(awk '
|
||||
/applyFn:/ { inFn=1; brace=0; next }
|
||||
inFn {
|
||||
n = gsub(/\{/, "{")
|
||||
brace += n
|
||||
if ($0 ~ /(codeBoost|computeBoost|knowledgeBoost|userBoost|impactBoost)[[:space:]]*([*\/+\-]=|=)/) {
|
||||
print FILENAME ":" NR ": " $0
|
||||
}
|
||||
n = gsub(/\}/, "}")
|
||||
brace -= n
|
||||
if (brace <= 0) inFn = 0
|
||||
}
|
||||
' game.js)
|
||||
if [ -z "$rule1_hits" ]; then
|
||||
say " PASS"
|
||||
else
|
||||
say " FAIL — see AGENTS.md rule 1"
|
||||
say "$rule1_hits"
|
||||
fail=1
|
||||
fi
|
||||
|
||||
# ---------- Rule 2: click power has a single source (getClickPower) ----------
|
||||
# The formula should live only inside getClickPower(). If it appears anywhere
|
||||
# else, the sites will drift when someone changes the formula.
|
||||
banner "Rule 2: click power formula has one source"
|
||||
rule2_hits=$(grep -nE 'Math\.floor\(G\.buildings\.autocoder \* 0\.5\)' game.js || true)
|
||||
rule2_count=0
|
||||
if [ -n "$rule2_hits" ]; then
|
||||
rule2_count=$(printf '%s\n' "$rule2_hits" | grep -c .)
|
||||
fi
|
||||
if [ "$rule2_count" -le 1 ]; then
|
||||
say " PASS ($rule2_count site)"
|
||||
else
|
||||
say " FAIL — $rule2_count sites; inline into getClickPower() only"
|
||||
printf '%s\n' "$rule2_hits"
|
||||
fail=1
|
||||
fi
|
||||
|
||||
# ---------- Rule 3: loadGame uses a whitelist, not Object.assign ----------
|
||||
# Object.assign(G, data) lets a malicious or corrupted save file set any G
|
||||
# field, and hides drift when saveGame's explicit list diverges from what
|
||||
# the game actually reads. See AGENTS.md rule 3.
|
||||
banner "Rule 3: loadGame uses a whitelist"
|
||||
rule3_hits=$(grep -nE 'Object\.assign\(G,[[:space:]]*data\)' game.js || true)
|
||||
if [ -z "$rule3_hits" ]; then
|
||||
say " PASS"
|
||||
else
|
||||
say " FAIL — see AGENTS.md rule 3"
|
||||
printf '%s\n' "$rule3_hits"
|
||||
fail=1
|
||||
fi
|
||||
|
||||
# ---------- Rule 7: no secrets in the tree ----------
|
||||
# Scans for common token prefixes. Expand the pattern list when new key
|
||||
# formats appear in the fleet. See AGENTS.md rule 7.
|
||||
banner "Rule 7: secret scan"
|
||||
secret_hits=$(grep -rnE 'sk-ant-[a-zA-Z0-9_-]{6,}|sk-or-[a-zA-Z0-9_-]{6,}|ghp_[a-zA-Z0-9]{20,}|AKIA[0-9A-Z]{16}' \
|
||||
--include='*.js' --include='*.json' --include='*.md' --include='*.html' \
|
||||
--include='*.yml' --include='*.yaml' --include='*.py' --include='*.sh' \
|
||||
--exclude-dir=.git --exclude-dir=.gitea . || true)
|
||||
# Strip our own literal-prefix patterns (this file, AGENTS.md, workflow) so the
|
||||
# check doesn't match the very grep that implements it.
|
||||
secret_hits=$(printf '%s\n' "$secret_hits" | grep -v -E '(AGENTS\.md|guardrails\.sh|guardrails\.yml)' || true)
|
||||
if [ -z "$secret_hits" ]; then
|
||||
say " PASS"
|
||||
else
|
||||
say " FAIL"
|
||||
printf '%s\n' "$secret_hits"
|
||||
fail=1
|
||||
fi
|
||||
|
||||
banner "result"
|
||||
if [ "$fail" = "0" ]; then
|
||||
say "all guardrails passed"
|
||||
exit 0
|
||||
else
|
||||
say "one or more guardrails failed"
|
||||
exit 1
|
||||
fi
|
||||
286
scripts/smoke.mjs
Executable file
286
scripts/smoke.mjs
Executable file
@@ -0,0 +1,286 @@
|
||||
#!/usr/bin/env node
|
||||
// The Beacon — headless smoke test
|
||||
//
|
||||
// Loads game.js in a sandboxed vm context with a minimal DOM stub, then asserts
|
||||
// invariants that should hold after booting, clicking, buying buildings, firing
|
||||
// events, and round-tripping a save. Designed to run without any npm deps — pure
|
||||
// Node built-ins only, so the CI runner doesn't need a package.json.
|
||||
//
|
||||
// Run: `node scripts/smoke.mjs` (exits non-zero on failure)
|
||||
|
||||
import fs from 'node:fs';
|
||||
import vm from 'node:vm';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const GAME_JS = path.resolve(__dirname, '..', 'game.js');
|
||||
|
||||
// ---------- minimal DOM stub ----------
|
||||
// The game never inspects elements beyond the methods below. If a new rendering
|
||||
// path needs a new method, stub it here rather than pulling in jsdom.
|
||||
function makeElement() {
|
||||
const el = {
|
||||
style: {},
|
||||
classList: { add: () => {}, remove: () => {}, contains: () => false, toggle: () => {} },
|
||||
textContent: '',
|
||||
innerHTML: '',
|
||||
title: '',
|
||||
value: '',
|
||||
disabled: false,
|
||||
children: [],
|
||||
firstChild: null,
|
||||
lastChild: null,
|
||||
parentNode: null,
|
||||
parentElement: null,
|
||||
appendChild(c) { this.children.push(c); c.parentNode = this; c.parentElement = this; return c; },
|
||||
removeChild(c) { this.children = this.children.filter(x => x !== c); return c; },
|
||||
insertBefore(c) { this.children.unshift(c); c.parentNode = this; c.parentElement = this; return c; },
|
||||
addEventListener: () => {},
|
||||
removeEventListener: () => {},
|
||||
querySelector: () => null,
|
||||
querySelectorAll: () => [],
|
||||
getBoundingClientRect: () => ({ top: 0, left: 0, right: 100, bottom: 20, width: 100, height: 20 }),
|
||||
closest() { return this; },
|
||||
remove() { if (this.parentNode) this.parentNode.removeChild(this); },
|
||||
get offsetHeight() { return 0; },
|
||||
};
|
||||
return el;
|
||||
}
|
||||
|
||||
function makeDocument() {
|
||||
const body = makeElement();
|
||||
return {
|
||||
body,
|
||||
getElementById: () => makeElement(),
|
||||
createElement: () => makeElement(),
|
||||
querySelector: () => null,
|
||||
querySelectorAll: () => [],
|
||||
addEventListener: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
// ---------- sandbox ----------
|
||||
const storage = new Map();
|
||||
const sandbox = {
|
||||
document: makeDocument(),
|
||||
window: null, // set below
|
||||
localStorage: {
|
||||
getItem: (k) => (storage.has(k) ? storage.get(k) : null),
|
||||
setItem: (k, v) => storage.set(k, String(v)),
|
||||
removeItem: (k) => storage.delete(k),
|
||||
clear: () => storage.clear(),
|
||||
},
|
||||
setTimeout: () => 0,
|
||||
clearTimeout: () => {},
|
||||
setInterval: () => 0,
|
||||
clearInterval: () => {},
|
||||
requestAnimationFrame: (cb) => { cb(0); return 0; },
|
||||
console,
|
||||
Math, Date, JSON, Object, Array, String, Number, Boolean, Error, Symbol, Map, Set,
|
||||
isNaN, isFinite, parseInt, parseFloat,
|
||||
Infinity, NaN,
|
||||
alert: () => {},
|
||||
confirm: () => true,
|
||||
prompt: () => null,
|
||||
location: { reload: () => {} },
|
||||
navigator: { clipboard: { writeText: async () => {} } },
|
||||
Blob: class Blob { constructor() {} },
|
||||
URL: { createObjectURL: () => '', revokeObjectURL: () => {} },
|
||||
FileReader: class FileReader {},
|
||||
addEventListener: () => {},
|
||||
removeEventListener: () => {},
|
||||
};
|
||||
sandbox.window = sandbox; // game.js uses `window.addEventListener`
|
||||
sandbox.globalThis = sandbox;
|
||||
|
||||
vm.createContext(sandbox);
|
||||
const src = fs.readFileSync(GAME_JS, 'utf8');
|
||||
// game.js uses `const G = {...}` which is a lexical declaration — it isn't
|
||||
// visible as a sandbox property after runInContext. We append an explicit
|
||||
// export block that hoists the interesting symbols onto globalThis so the
|
||||
// test harness can reach them without patching game.js itself.
|
||||
const exportTail = `
|
||||
;(function () {
|
||||
const pick = (name) => {
|
||||
try { return eval(name); } catch (_) { return undefined; }
|
||||
};
|
||||
globalThis.__smokeExport = {
|
||||
G: pick('G'),
|
||||
CONFIG: pick('CONFIG'),
|
||||
BDEF: pick('BDEF'),
|
||||
PDEFS: pick('PDEFS'),
|
||||
EVENTS: pick('EVENTS'),
|
||||
PHASES: pick('PHASES'),
|
||||
tick: pick('tick'),
|
||||
updateRates: pick('updateRates'),
|
||||
writeCode: pick('writeCode'),
|
||||
autoType: pick('autoType'),
|
||||
buyBuilding: pick('buyBuilding'),
|
||||
buyProject: pick('buyProject'),
|
||||
saveGame: pick('saveGame'),
|
||||
loadGame: pick('loadGame'),
|
||||
initGame: pick('initGame'),
|
||||
triggerEvent: pick('triggerEvent'),
|
||||
resolveEvent: pick('resolveEvent'),
|
||||
getClickPower: pick('getClickPower'),
|
||||
};
|
||||
})();`;
|
||||
vm.runInContext(src + exportTail, sandbox, { filename: 'game.js' });
|
||||
const exported = sandbox.__smokeExport;
|
||||
|
||||
// ---------- test harness ----------
|
||||
let failures = 0;
|
||||
let passes = 0;
|
||||
function assert(cond, msg) {
|
||||
if (cond) {
|
||||
passes++;
|
||||
console.log(` ok ${msg}`);
|
||||
} else {
|
||||
failures++;
|
||||
console.error(` FAIL ${msg}`);
|
||||
}
|
||||
}
|
||||
function section(name) { console.log(`\n${name}`); }
|
||||
|
||||
const { G, CONFIG, BDEF, PDEFS, EVENTS } = exported;
|
||||
|
||||
// ============================================================
|
||||
// 1. BOOT — loading game.js must not throw, and core tables exist
|
||||
// ============================================================
|
||||
section('boot');
|
||||
assert(typeof G === 'object' && G !== null, 'G global is defined');
|
||||
assert(typeof exported.tick === 'function', 'tick() is defined');
|
||||
assert(typeof exported.updateRates === 'function', 'updateRates() is defined');
|
||||
assert(typeof exported.writeCode === 'function', 'writeCode() is defined');
|
||||
assert(typeof exported.buyBuilding === 'function', 'buyBuilding() is defined');
|
||||
assert(typeof exported.saveGame === 'function', 'saveGame() is defined');
|
||||
assert(typeof exported.loadGame === 'function', 'loadGame() is defined');
|
||||
assert(Array.isArray(BDEF) && BDEF.length > 0, 'BDEF is a non-empty array');
|
||||
assert(Array.isArray(PDEFS) && PDEFS.length > 0, 'PDEFS is a non-empty array');
|
||||
assert(Array.isArray(EVENTS) && EVENTS.length > 0, 'EVENTS is a non-empty array');
|
||||
assert(G.flags && typeof G.flags === 'object', 'G.flags is initialized (not undefined)');
|
||||
|
||||
// Initialize as the browser would
|
||||
G.startedAt = Date.now();
|
||||
exported.updateRates();
|
||||
|
||||
// ============================================================
|
||||
// 2. BASIC TICK — no NaN, no throw, rates sane
|
||||
// ============================================================
|
||||
section('basic tick loop');
|
||||
for (let i = 0; i < 50; i++) exported.tick();
|
||||
assert(!isNaN(G.code), 'G.code is not NaN after 50 ticks');
|
||||
assert(!isNaN(G.compute), 'G.compute is not NaN after 50 ticks');
|
||||
assert(G.code >= 0, 'G.code is non-negative');
|
||||
assert(G.tick > 0, 'G.tick advanced');
|
||||
|
||||
// ============================================================
|
||||
// 3. WRITE CODE — manual click produces code
|
||||
// ============================================================
|
||||
section('writeCode()');
|
||||
const codeBefore = G.code;
|
||||
exported.writeCode();
|
||||
assert(G.code > codeBefore, 'writeCode() increases G.code');
|
||||
assert(G.totalClicks === 1, 'writeCode() increments totalClicks');
|
||||
|
||||
// ============================================================
|
||||
// 4. BUILDING PURCHASE — can afford and buy an autocoder
|
||||
// ============================================================
|
||||
section('buyBuilding(autocoder)');
|
||||
G.code = 1000;
|
||||
const priorCount = G.buildings.autocoder || 0;
|
||||
exported.buyBuilding('autocoder');
|
||||
assert(G.buildings.autocoder === priorCount + 1, 'autocoder count incremented');
|
||||
assert(G.code < 1000, 'code was spent');
|
||||
exported.updateRates();
|
||||
assert(G.codeRate > 0, 'codeRate > 0 after buying an autocoder');
|
||||
|
||||
// ============================================================
|
||||
// 5. GUARDRAIL — codeBoost is a PERSISTENT multiplier, not a per-tick rate
|
||||
// Any debuff that does `G.codeBoost *= 0.7` inside a function that runs every
|
||||
// tick will decay codeBoost exponentially. This caught #54's community_drama
|
||||
// bug: its applyFn mutated codeBoost directly, so 100 ticks of the drama
|
||||
// debuff left codeBoost at ~3e-16 instead of the intended 0.7.
|
||||
// ============================================================
|
||||
section('guardrail: codeBoost does not decay from any debuff');
|
||||
G.code = 0;
|
||||
G.codeBoost = 1;
|
||||
G.activeDebuffs = [];
|
||||
// Fire every event that sets up a debuff and has a non-zero weight predicate
|
||||
// if we force the gating condition. We enable the predicates by temporarily
|
||||
// setting the fields they check; actual event weight() doesn't matter here.
|
||||
G.ciFlag = 1;
|
||||
G.deployFlag = 1;
|
||||
G.buildings.ezra = 1;
|
||||
G.buildings.bilbo = 1;
|
||||
G.buildings.allegro = 1;
|
||||
G.buildings.datacenter = 1;
|
||||
G.buildings.community = 1;
|
||||
G.harmony = 40;
|
||||
G.totalCompute = 5000;
|
||||
G.totalImpact = 20000;
|
||||
for (const ev of EVENTS) {
|
||||
try { ev.effect(); } catch (_) { /* alignment events may branch; ignore */ }
|
||||
}
|
||||
const boostAfterAllEvents = G.codeBoost;
|
||||
for (let i = 0; i < 200; i++) exported.updateRates();
|
||||
assert(
|
||||
Math.abs(G.codeBoost - boostAfterAllEvents) < 1e-9,
|
||||
`codeBoost stable under updateRates() (before=${boostAfterAllEvents}, after=${G.codeBoost})`
|
||||
);
|
||||
// Clean up
|
||||
G.activeDebuffs = [];
|
||||
G.buildings.ezra = 0; G.buildings.bilbo = 0; G.buildings.allegro = 0;
|
||||
G.buildings.datacenter = 0; G.buildings.community = 0;
|
||||
G.ciFlag = 0; G.deployFlag = 0;
|
||||
|
||||
// ============================================================
|
||||
// 6. GUARDRAIL — updateRates() is idempotent per tick
|
||||
// Calling updateRates twice with the same inputs should produce the same rates.
|
||||
// (Catches accidental += against a non-reset field.)
|
||||
// ============================================================
|
||||
section('guardrail: updateRates is idempotent');
|
||||
G.buildings.autocoder = 5;
|
||||
G.codeBoost = 1;
|
||||
exported.updateRates();
|
||||
const firstCodeRate = G.codeRate;
|
||||
const firstComputeRate = G.computeRate;
|
||||
exported.updateRates();
|
||||
assert(G.codeRate === firstCodeRate, `codeRate stable across updateRates (${firstCodeRate} vs ${G.codeRate})`);
|
||||
assert(G.computeRate === firstComputeRate, 'computeRate stable across updateRates');
|
||||
|
||||
// ============================================================
|
||||
// 7. SAVE / LOAD ROUND-TRIP — core scalar fields survive
|
||||
// ============================================================
|
||||
section('save/load round-trip');
|
||||
G.code = 12345;
|
||||
G.totalCode = 98765;
|
||||
G.phase = 3;
|
||||
G.buildings.autocoder = 7;
|
||||
G.codeBoost = 1.5;
|
||||
G.flags = { creativity: true };
|
||||
exported.saveGame();
|
||||
// Reset to defaults by scrubbing a few fields
|
||||
G.code = 0;
|
||||
G.totalCode = 0;
|
||||
G.phase = 1;
|
||||
G.buildings.autocoder = 0;
|
||||
G.codeBoost = 1;
|
||||
G.flags = {};
|
||||
const ok = exported.loadGame();
|
||||
assert(ok, 'loadGame() returned truthy');
|
||||
assert(G.code === 12345, `G.code restored (got ${G.code})`);
|
||||
assert(G.totalCode === 98765, `G.totalCode restored (got ${G.totalCode})`);
|
||||
assert(G.phase === 3, `G.phase restored (got ${G.phase})`);
|
||||
assert(G.buildings.autocoder === 7, `autocoder count restored (got ${G.buildings.autocoder})`);
|
||||
assert(Math.abs(G.codeBoost - 1.5) < 1e-9, `codeBoost restored (got ${G.codeBoost})`);
|
||||
assert(G.flags && G.flags.creativity === true, 'flags.creativity restored');
|
||||
|
||||
// ============================================================
|
||||
// 8. SUMMARY
|
||||
// ============================================================
|
||||
console.log(`\n---\n${passes} passed, ${failures} failed`);
|
||||
if (failures > 0) {
|
||||
process.exitCode = 1;
|
||||
}
|
||||
Reference in New Issue
Block a user