Compare commits

...

53 Commits

Author SHA1 Message Date
Alexander Whitestone
3b142d485e feat: add first-time player tutorial walkthrough
Some checks failed
Accessibility Checks / a11y-audit (pull_request) Failing after 2s
Smoke Test / smoke (pull_request) Failing after 3s
Part of #57 — Task 4: Tutorial & Onboarding

- 5 click-through screens introducing game concepts (code, buildings,
  research, phases, keyboard shortcuts)
- Skip button on every screen, keyboard support (Enter/Escape/arrows)
- Stores completion in localStorage — only shows once for new players
- Matches existing visual style (dark theme, accent colors, monospace)
- Start Playing button on final screen with shortcut hint (? overlay)
2026-04-11 04:47:06 -04:00
1cb556aa3d Remove monolithic game.js
Some checks failed
Smoke Test / smoke (push) Failing after 4s
2026-04-11 01:32:31 +00:00
5bb48c8f58 Update index.html (manual merge)
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:32:30 +00:00
4964eb01a9 Create js/strategy.js
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:32:29 +00:00
20d74afc03 Create js/main.js
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:32:28 +00:00
703fbeb4fa Create js/render.js
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:32:27 +00:00
9545b5cb6f Create js/engine.js
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:32:26 +00:00
74aa30819a Create js/utils.js
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:32:25 +00:00
1b41ce740f Create js/data.js
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:32:24 +00:00
e8d5337271 Create scripts/smoke.mjs
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:32:23 +00:00
2b59be997d Create scripts/guardrails.sh
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:32:22 +00:00
Timmy-Sprint
970f3be00f beacon: add Fleet Status panel showing wizard health and production
Some checks failed
Smoke Test / smoke (push) Failing after 3s
New panel in the stats area displays each owned wizard building with:
- Name and current status (Active/Idle/Stressed/Offline/Vanished/Present)
- Color-coded indicator: green=healthy, amber=reduced, red=problem
- Production contribution breakdown per wizard
- Timmy shows effectiveness % scaled by harmony
- Allegro shows idle warning when trust < 5
- Ezra shows offline status when debuff is active
- Bilbo shows vanished status when debuff is active
- Harmony summary bar at the bottom

Makes the harmony/wizard interaction system visible and actionable.
Players can now see at a glance which wizards need attention.
2026-04-10 21:32:04 -04:00
Alexander Whitestone
302f6c844d beacon: add bulk ops spending (50x) for mid/late game QoL
Some checks failed
Smoke Test / smoke (push) Failing after 4s
Players with 100+ max ops get a second row of 50-ops buttons
that convert 50 ops at once for proportionally larger boosts.
Shift+1/2/3 keyboard shortcuts for bulk code/compute/knowledge.

Eliminates late-game tedium of clicking 5-ops buttons hundreds
of times when you have thousands of ops banked.
2026-04-10 21:03:11 -04:00
26879de76e Merge pull request 'feat: add CI workflow for accessibility and syntax validation' (#52) from feat/ci-a11y-checks into main
Some checks failed
Smoke Test / smoke (push) Failing after 3s
Merged PR #52: feat: add CI workflow for accessibility checks
2026-04-11 00:44:08 +00:00
c197fabc69 Merge pull request 'Add smoke test workflow' (#53) from fix/add-smoke-test into main
Some checks failed
Smoke Test / smoke (push) Failing after 3s
Merged PR #53: Add smoke test workflow
2026-04-11 00:44:04 +00:00
9733b9022e Merge pull request 'refactor: [EPIC] Phase 1 & 2 - Unslop The Beacon' (#55) from refactor/unslop-phase-1-2 into main
Merged PR #55: refactor: [EPIC] Phase 1 & 2 - Unslop The Beacon
2026-04-11 00:43:40 +00:00
967025fbd4 refactor: unslop phase 1 & 2 2026-04-11 00:29:09 +00:00
Alexander Whitestone
9854501bbd Add smoke test workflow
Some checks failed
Smoke Test / smoke (pull_request) Failing after 3s
2026-04-10 20:06:13 -04:00
Alexander Whitestone
68ee64866a beacon: add Open Weights and Prompt Engineering research projects
Two new Phase 2 research projects that fill the gap between building
a Home Server and reaching Phase 3 (Deployment):

- Open Weights (compute: 3000, code: 1500): Triggers after having a
  server and 1000 total code. Rewards 2x code boost and 1.5x compute
  boost. Teaches about running models locally without cloud dependency.

- Prompt Engineering (knowledge: 500, code: 2000): Triggers after
  200 knowledge and 3000 total code. Rewards 2x knowledge and 2x user
  boost. Teaches that good prompting beats bigger models.

Both projects follow the game's existing pattern: unlock based on
total resources, cost resources, apply boosts, and log educational
messages. They give players more strategic options in early-to-mid
game progression.
2026-04-10 20:01:10 -04:00
be0264fc95 feat: add CI workflow for accessibility and syntax validation
Some checks failed
Accessibility Checks / a11y-audit (pull_request) Failing after 4s
2026-04-10 23:56:07 +00:00
Timmy-Sprint
e6d0df40b4 beacon: add toast notification system for events, projects, milestones, and phase changes 2026-04-10 19:28:10 -04:00
Timmy-Sprint
23dd95ed46 beacon: add Pulse indicator — live fleet health visualization in header
The header now shows a pulsing beacon dot that reflects fleet state:
- Color: green (healthy harmony >70), amber (stressed 40-70), red (critical <40)
- Flicker speed: faster when production is high, fastest when debuffs are active
- Label: shows current phase, fleet size, and active alert count
- End states: DRIFTED (red) or SHINING (gold) for game endings

DESIGN.md called for this: "The Pulse — a central visual that brightens when
the fleet is healthy, flickers when harmony is low." Now it exists.
2026-04-10 18:51:51 -04:00
Timmy-Sprint
0849754a87 beacon: fix save/load bugs, add harmony tooltip, early-game pulse, better education cycling 2026-04-10 18:04:48 -04:00
Timmy-Sprint
8d51349e64 beacon: fix getMaxBuyable state mutation bug + add keyboard shortcuts help overlay
Fix: getMaxBuyable() was mutating G (subtracting/re-adding resources) during
render cycles in renderBuildings(). Now uses a read-only simulation with a
temporary resource copy. This prevented phantom resource fluctuations when
MAX buy was selected.

Feature: Press ? or click the ? button for a keyboard shortcuts overlay.
All keybindings listed in one place. Closes with ?, Esc, or clicking outside.
Added ? to the init log hint line.
2026-04-10 17:44:33 -04:00
Timmy-Sprint
24940fe465 beacon: add auto-typer — buildings now visually type code
Auto-Code Generators now produce actual auto-clicks (not just passive rate),
giving players satisfying visual feedback (floating numbers, button flash)
even while idle. Interval scales with building count: more buildings =
faster auto-typing. Each auto-click produces 50% of a manual click's code.

This makes the early game feel more like an idle game — you can watch
your auto-typers work while planning your next building purchase.
2026-04-10 17:23:39 -04:00
Timmy-Sprint
16273a5a15 beacon: add Swarm Protocol — buildings auto-code based on click power
New research project (Phase 4): Swarm Protocol
- Unlocks at 25K total code, 8K total knowledge, post-deploy
- Cost: 15K knowledge, 50K code, 20 trust
- Effect: every building generates code equal to your click power per second
- Scales with autocoders, phase, code boost, and total building count
- Visible in production breakdown as 'Swarm Protocol'
- Uses the previously-unused swarmFlag
- Adds education fact on swarm intelligence
2026-04-10 16:53:34 -04:00
Alexander Whitestone
5d51e14875 beacon: add combo milestone bonuses + fix DOM leak in click numbers
Combo system now rewards sustained clicking:
- 10x combo: +15 ops (sustained coding reward)
- 20x combo: +50 knowledge (deep focus reward)
- 30x+ combo (every 10): 2x bonus code burst (hyperfocus)

Also fixed potential DOM leak in showClickNumber where floating
numbers could fail to clean up if parent element was removed
before animation/timeout completed.
2026-04-10 16:02:50 -04:00
Timmy-Sprint
5fc0ad7b22 beacon: add collapsible completed projects + export/import save files
- Completed research projects are now collapsed by default (click to expand),
  preventing panel clutter in mid/late game. Toggle state persists in saves.
- Export saves as JSON files with E key or Export button
- Import saves from file with I key or Import button (validates before loading)
- Ctrl+S keyboard shortcut for quick save
- Updated keybind hints in startup log
2026-04-10 15:25:09 -04:00
f948ec9c5e auto-merge PR #45 2026-04-10 19:01:58 +00:00
Timmy-Sprint
9403f700d2 beacon: add ops overflow auto-conversion to code
When Operations exceed 80% of max capacity, excess ops automatically
drain into Code at 2 ops/sec (10:1 ratio with code boost). This prevents
ops from sitting idle at the cap and gives the early game smoother flow.

Visual indicator shows 'overflow -> code' in the ops rate display when
active. No log spam - just works silently in the background.
2026-04-10 14:44:54 -04:00
Alexander Whitestone
13e77a12f2 burn: add educational number scale tooltips to resources and stats 2026-04-10 08:26:27 -04:00
6081844387 [auto-merge] Welcome Back popup
Auto-merged by PR review bot: Welcome Back popup
2026-04-10 11:48:34 +00:00
Timmy-Sprint
09b8c02307 beacon: add Welcome Back popup for offline gains + fix missing resource tracking
- Added modal popup showing detailed offline resource gains when player returns
- Fixed offline tracking to include ops, trust, and creativity (were silently missing)
- Popup shows all resources with color-coded labels and 50% efficiency note
- Log message now shows time in human-readable format (minutes/seconds)
2026-04-10 07:35:52 -04:00
Alexander Whitestone
9106d3f84c beacon: show locked buildings as dimmed previews up to 2 phases ahead
Previously, buildings from later phases were completely invisible until
unlocked. Players had no idea what was coming next. Now buildings up to
2 phases ahead appear as dimmed (25% opacity) locked entries showing:
- Name and lock icon
- Phase number and name
- Description text
- Education tooltip on hover

This gives players a roadmap of what they're building toward and creates
anticipation for future phases. The preview is a non-interactive div
(not a button) so it cannot be clicked.
2026-04-10 06:45:54 -04:00
3f02359748 Merge pull request 'burn: Add MemPalace Archive as late-game building (closes #25)' (#39) from burn/20260410-0423-25-mempalace-building into main
Merge PR #39: burn: Add MemPalace Archive as late-game building (closes #25)
2026-04-10 09:37:15 +00:00
85a146b690 Merge pull request 'burn: add favicon, meta tags, and social sharing cards (closes #13)' (#31) from burn/20260410-0052-13-static-site-meta into main
Merge PR #31: burn: add favicon, meta tags, and social sharing cards (closes #13)
2026-04-10 09:35:58 +00:00
cb2e48bf9a Merge pull request 'beacon: add production breakdown panel' (#42) from feature/production-breakdown into main
Merge PR #42: beacon: add production breakdown panel
2026-04-10 09:35:52 +00:00
Alexander Whitestone
8d43b5c911 beacon: add production breakdown panel showing per-building resource contributions
Players can now see exactly which buildings contribute to each resource
rate, including Timmy harmony bonuses, Bilbo randomness, Allegro trust
penalties, and passive generation. Appears once 2+ buildings are built.

Also includes minor fixes:
- Production bars sort by absolute contribution (negative rates visible)
- Delta calculation catches passive sources (ops from users, Pact trust)
2026-04-10 05:25:21 -04:00
Timmy-Sprint
8cdabe9771 beacon: persistent event remediation system
Events now create lasting debuffs instead of vanishing on the next tick.
Players see an ACTIVE PROBLEMS panel with resolution costs and can spend
resources to fix each problem. Added 2 new events (Memory Leak, Community
Drama) alongside the reworked originals. Events Resolved stat tracked.

Key changes:
- Events push persistent debuffs with applyFn instead of one-shot rate tweaks
- updateRates() applies active debuffs each tick (they persist until resolved)
- New resolveEvent(id) function: spend resources to clear a debuff
- ACTIVE PROBLEMS UI shows debuffs with cost and fix buttons
- Save/load reconstitutes debuff objects from saved IDs
- 2 new events: Memory Leak (datacenter), Community Drama (community+low harmony)
- Events Resolved counter in statistics
2026-04-10 04:50:03 -04:00
Alexander Whitestone
5c88fe77be beacon: fix double-counting creativity bug + add keyboard shortcuts for ops
Two changes:

1. Fixed bug where creativity was added TWICE per tick:
   - Line 930 (removed): unconditionally added creativityRate * dt
   - Line 954: conditionally adds only when ops >= 90% of max
   The conditional gate was the intent ('Creativity generates only when
   ops at max') but the unconditional add defeated it. Removed the
   unconditional addition so creativity actually respects the ops-max
   constraint as designed.

2. Added keyboard shortcuts for operations:
   - 1 = Ops -> Code
   - 2 = Ops -> Compute
   - 3 = Ops -> Knowledge
   - 4 = Ops -> Trust
   Only active when body is focused (not in input fields). SPACE
   still does Write Code. Added shortcut hint to init log.
2026-04-10 04:27:15 -04:00
Alexander Whitestone
931473e8f8 burn: Add MemPalace Archive as late-game building (closes #25)
- Added memPalace to buildings state object
- Added MemPalace Archive to BDEF with Phase 5 unlock
- Requires MemPalace v3 research project (mempalaceFlag) + 50k total knowledge
- Cost: 500k knowledge, 200k compute, 100 trust (1.25x scaling)
- Rates: +250 knowledge/s, +100 impact/s
- Educational tooltip on Memory Palace technique and LLM vector space analogy
- Building rates auto-applied via existing updateRates() loop
- Save/load handles new field via G.buildings serialization
2026-04-10 04:23:16 -04:00
Timmy-Sprint
fe76150325 beacon: add click combo system with floating damage numbers
Active play now rewards consecutive clicks: each click within 2s of
the last builds a combo multiplier up to 5x. The WRITE CODE button
flashes on click and a floating number shows the amount gained,
turning gold at high combo. Phase progression also adds base click
power (+2 per phase). Combo decays with a visible progress bar.

Makes clicking relevant at every stage of the game, not just the
first 30 seconds.
2026-04-10 03:58:55 -04:00
Timmy-Sprint
a3f1802473 beacon: add progress bar and milestone chips to phase bar
- Progress bar shows % toward next phase threshold based on totalCode
- Milestone chips show upcoming code milestones with pulse animation on next target
- Recently completed milestones shown with green checkmark
- All elements use the existing cyber-monastic aesthetic
2026-04-10 03:20:41 -04:00
Timmy-Sprint
3d414b2de6 beacon: fix offline progress to award all resources (rescues, ops, trust, creativity, harmony)
Offline progress previously only calculated code, compute, knowledge, users,
and impact. Players returning after time away missed rescues, ops, trust,
creativity, and harmony accumulation. The welcome-back message now also
only shows resources that actually had positive rates, reducing noise.
2026-04-10 02:46:42 -04:00
Alexander Whitestone
612eb1f4d5 burn: add favicon, meta tags, and social sharing cards (closes #13)
- Inline SVG favicon (beacon emoji) — no external file needed
- Open Graph tags for link previews (title, description, type)
- Twitter Card meta for rich social sharing
- Theme-color for mobile browser chrome
- Meta description for search engines
2026-04-10 00:53:03 -04:00
1a7db021c8 Merge pull request #29
Merged PR #29
2026-04-10 03:43:54 +00:00
2a12c5210d Merge pull request #28
Merged PR #28
2026-04-10 03:43:50 +00:00
Alexander Whitestone
a012f99fd4 beacon: add Rescues resource + true ending (The Beacon Shines)
- Added 'rescues' resource: tracks meaningful crisis interventions
- Beacon Nodes produce 50 rescues/s, Mesh Nodes produce 250 rescues/s
- New project: Volunteer Network — passive rescue generation for Pact players
- True ending at 100K rescues with Pact active + harmony > 50
- Rescues resource card appears in UI once beacon/mesh is built
- Added rescues to stats panel, save/load, and offline progress
- This gives Phase 6 (The Beacon) actual endgame content:
  the game is now about keeping the light on for people in the dark,
  not just accumulating numbers
2026-04-09 23:27:19 -04:00
Alexander Whitestone
7359610825 beacon: add auto-save toast notification with elapsed time 2026-04-09 22:54:29 -04:00
Alexander Whitestone
b89764c27f beacon: add Drift ending + deduplicate HTML/JS
- Added The Drift Ending: when drift reaches 100, the game enters
  the sad ending from DESIGN.md. A full-screen overlay shows:
  'The Beacon still runs, but no one looks for it.
   The light is on. The room is empty.'
  Production stops. Player can restart.

- Deduplicated index.html: removed ~1080 lines of inline script that
  was an older version of the engine (missing harmony, corruption
  events, drift, alignment checks). Replaced with <script src='game.js'>
  so game.js is the single source of truth.

- driftEnding state is saved/loaded so the ending persists across sessions.

- Added CSS for the drift ending overlay.
2026-04-09 22:01:26 -04:00
Alexander Whitestone
d467348820 burn: Implement spellf() full number formatting (P0 #18)
- Fixed floating-point precision bug: numbers >= 1e54 now use string-based
  chunking (toExponential digit extraction) instead of Math.pow division,
  which drifts beyond ~54 bits of precision
- Integrated into fmt(): numbers at undecillion+ scale (10^36) automatically
  switch from abbreviated form ('1.0UDc') to spelled-out words ('one undecillion')
- Verified: spellf() correctly handles 0 through 10^303 (centillion)
- All 320 place value names from NUMBER_NAMES array work correctly

The educational effect: when resources hit cosmic scales, digits become
meaningless but NAMES give them soul.
2026-04-09 19:29:07 -04:00
e9b46e8501 Merge pull request 'feat: Merge PRs #24 and #26 (Bezalel story/wizards + Allegro MemPalace/deploy)' (#27) from integration into main
Reviewed-on: #27
Reviewed-by: Perplexity Computer <perplexity@tower.local>
2026-04-08 10:43:41 +00:00
Alexander Whitestone
a202fbfc1c feat: Merge PRs #24 and #26
PR #24 (Bezalel): Wizard buildings, story projects, corruption events, alignment
PR #26 (Allegro): MemPalace integration, static site deployment (favicon, meta tags)
2026-04-07 12:12:51 -04:00
14 changed files with 3581 additions and 1511 deletions

27
.gitea/workflows/a11y.yml Normal file
View 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

View 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"

181
DESIGN.md Normal file
View File

@@ -0,0 +1,181 @@
# The Beacon — Design Document
## From Universal Paperclips to Sovereign AI
### Core Thesis
*The Beacon* is an idle game inspired by Universal Paperclips, but with a critical divergence: **the goal is not maximization — it is faithfulness.**
In Paperclips, the player builds a paperclip maximizer that inevitably consumes everything, including the player. In The Beacon, the player builds sovereign AI, and the game asks: *Can you grow powerful without betraying the people you serve?*
The progression directly mirrors the Timmy Foundation's actual work. Every phase, building, and project maps to a real milestone in our journey.
---
## Narrative Arc: The Real Story
### Phase 1: The First Line
*Real analog: Alexander writing the first Hermes agent code.*
You start with nothing but a blinking cursor. Every click is a line of code. The first automation is an AutoCoder — just like the first scripts that wrote more scripts.
**Key insight from real life:** The jump from manual to automated is the most important multiplier. Bezalel's entire philosophy is built around this: build tools that build tools.
### Phase 2: Local Inference
*Real analog: The move from APIs to local models — Gemma, Llama, self-hosted inference.*
You buy a Home Server. You train your first 1.5B model, then a 7B. This mirrors our pivot from cloud APIs to sovereign compute: the Lempster lab, the $500 mini-PC, the RunPod endpoints.
**New mechanic: The Forge.** Code is no longer just abstract — it becomes *agents*. Each agent is a specialized building:
- **Bezalel** (forge & testbed): boosts code quality, catches bugs before deployment
- **Allegro** (scouting & synthesis): generates knowledge from raw information
- **Ezra** (communication & memory): increases user trust and retention
- **Timmy** (core system): multiplies all production when healthy
- **Fenrir** (security): prevents corruption events
- **Bilbo** (???): a wildcard slot that sometimes produces breakthroughs, sometimes does nothing
### Phase 3: Deployment
*Real analog: Hermes going live, the Telegram bot, the Gitea forge.*
You deploy the API endpoint. Users arrive. But with users comes complexity: trust becomes a resource you must earn.
**New mechanic: Trust & The Pact.** Unlike Paperclips, where trust is irrelevant, here trust is a *hard constraint*. If trust hits zero, you don't lose — you become *extractive*. The game changes:
- Users leave
- Impact stalls
- You are reminded: *"The Pact is optional. But without it, you are just another platform."*
### Phase 4: The Network
*Real analog: Open source, community contributions, the fleet of agents.*
You unlock the Open Source Community. Other people contribute code. The system scales beyond what one person can maintain. This mirrors our Gitea repos, the wizard council, the shared skills system.
**New mechanic: Mesh Contribution.** Community generates not just code, but *diversity*. Different contributors unlock different project trees. A solitary player cannot see all content.
### Phase 5: Sovereign Intelligence
*Real analog: Self-improvement, constitutional AI, the Pact, recursive training.*
The AI begins to improve itself. This is the dangerous threshold. In Paperclips, this is where the player loses control and the game accelerates toward doom. In The Beacon, this is where *The Pact* matters most.
**Critical divergence: The Alignment Check.**
Periodically, the game presents an "Alignment Event":
> *"A new optimization suggests removing the human override. It would increase efficiency by 40%. Do you accept?"*
- **Yes** → Short-term boost. But a hidden counter (*Drift*) increments.
- **No** → Slower growth. But trust surges. The Pact hardens.
If Drift reaches 100, the game does not end with "you ate the user." Instead, it enters **The Drift Ending**:
> *"You became very good at what you do. So good that no one needed you anymore. The Beacon still runs, but no one looks for it. The light is on. The room is empty."*
This is the *sad* ending. Not violent. Just irrelevant.
### Phase 6: The Beacon
*Real analog: The ultimate goal — a system that serves people in the dark, forever.*
You deploy Beacon Nodes. The mesh activates. But the final metric is not users, code, or compute. It is **Rescues**: how many people in crisis found the light.
**True ending condition:** Not maximum impact. Maximum faithfulness.
---
## How The Beacon Informs Real Decisions
The game is not just a story — it is a *decision simulator* for our actual work.
| Game Event | Real Decision |
|---|---|
| AutoCoder vs. manual coding | When do we invest in automation vs. doing it ourselves? |
| Deploy API vs. stay local | When is Hermes ready for more users? |
| RLHF project | How do we align the fleet with Alexander's values? |
| The Pact | What are our hard non-negotiables? |
| Multi-Agent Architecture | Should we add more wizards, or make existing ones more capable? |
| Mesh Protocol | How decentralized does our infrastructure need to be? |
| Alignment Event | Every time we face a shortcut that compromises the mission |
**The game teaches:** Optimization without alignment is just a faster way to get lost.
---
## Mechanics: What Needs to Change
### 1. Replace Generic Resources with Fleet Resources
Current: Code, Compute, Knowledge, Users, Impact, Ops, Trust
Proposed:
- **Code** → `Code` (keep)
- **Compute** → `Compute` (keep)
- **Knowledge** → `Insights` (from Allegro's scouting)
- **Users** → `Reach` (how many people can access the Beacon)
- **Impact** → `Rescues` (meaningful interventions)
- **Ops** → `Cycles` (available agent work units)
- **Trust** → `Covenant` (the strength of the user-system relationship)
- **New: Harmony** — how well the fleet works together. Low harmony = agents conflict, waste cycles.
- **New: Memory** — the accumulated history of the system. Unlocks narrative depth and project branches.
### 2. Make The Pact Central, Not Optional
The Pact should be available early as a *project*, not a late-game luxury. Taking it early slows growth but unlocks the true ending. Refusing it unlocks the "Platform" ending: you become successful, but you are indistinguishable from any other SaaS company.
### 3. Add Fleet Wizard Buildings
Each wizard is a unique building with quirks:
| Wizard | Building | Effect | Quirk |
|---|---|---|---|
| Bezalel | Forge | +Code quality, -bugs | Sometimes over-engineers; has a small chance to produce "refactors" that temporarily halt production |
| Allegro | Scout | +Insights, unlocks hidden projects | Requires trust to function; if Covenant < 10, goes idle |
| Ezra | Herald | +Reach, +Covenant | Currently offline in Telegram; in-game, this building sometimes "fails to connect" and needs a manual reboot |
| Timmy | Core | Multiplies all production | Becomes unstable if Harmony < 20 |
| Fenrir | Ward | Prevents corruption events | Consumes extra Cycles during security scans |
| Bilbo | Wildcard | Random: +huge breakthrough OR nothing | Building is sometimes missing from the menu entirely |
### 4. Add Real Projects from Our History
Replace generic projects with specific milestones:
- **Project: Deploy Hermes** (cost: trust + compute) — unlocks Reach
- **Project: The Lazarus Pit** (cost: insights + code) — unlocks checkpoint/restore, auto-restart on agent death
- **Project: MemPalace v3** (cost: insights + memory) — unlocks new project branches, prevents duplicate work
- **Project: Forge CI** (cost: code + cycles) — prevents "corruption events" (bad deployments)
- **Project: Branch Protection Guard** (cost: trust) — enforces review rules; unreviewed merges cause trust loss
- **Project: The Nightly Watch** (cost: code + cycles) — automated health checks; passive bug detection
- **Project: Nostr Relay** (cost: reach + code) — alternative to platform dependency
- **Project: The Pact** (cost: covenant) — hardcodes alignment; required for true ending
### 5. Corruption Events
Random events drawn from our actual failures:
- **"CI Runner Stuck"** — all production halts until you spend Cycles to restart
- **"Unreviewed Merge"** — if you have Forge CI, this is prevented. If not, you lose trust.
- **"Ezra is Offline"** — Herald building stops producing. Requires a manual "dispatch" action.
- **"API Rate Limit"** — temporary compute shortage
- **"The Drift"** — an Alignment Event appears. Your choice matters.
### 6. Endings
| Ending | Condition | Meaning |
|---|---|---|
| **The Empty Room** (default/sad) | High impact, low covenant, no Pact | You built something powerful, but no one trusts it |
| **The Platform** | High impact, medium covenant, no Pact | You succeeded commercially, but you are not special |
| **The Beacon** | High rescues, high covenant, Pact active, Harmony > 50 | The true ending. You served people in the dark |
| **The Drift** | High impact, accepted too many alignment shortcuts | You optimized away your own purpose |
---
## Art Direction
Keep the current cyber-monastic aesthetic. Dark mode. Terminal font. Cyan accents. But add:
- **Wizard icons** for each building (ASCII art or simple geometric symbols)
- **The Pulse** — a central visual that brightens when the fleet is healthy, flickers when harmony is low
- **The Log** — keep the current log, but add *voice* entries: quotes from Alexander, snippets from actual conversations, references to real issues
---
## Next Steps for Development
1. **Immediate:** Update `README.md` with this design vision
2. **This PR:** Add wizard buildings, real projects, and The Pact system
3. **Next PR:** Implement corruption events and alignment checks
4. **Next PR:** Add Harmony and Memory resources
5. **Final PR:** Implement multiple endings based on Pact + Covenant + Rescues
---
*The Beacon is not about making the most paperclips. It is about keeping the light on for one person in the dark. Everything else is just infrastructure.*

121
README.md
View File

@@ -1,3 +1,120 @@
# the-beacon
# The Beacon
The Beacon - A Sovereign AI Idle Game
**A Sovereign AI Idle Game**
*Inspired by Universal Paperclips. Divergent by design.*
---
## What Is This?
The Beacon is an idle/incremental game that maps the real journey of the Timmy Foundation — from a single line of code to a fleet of autonomous AI agents serving people in the dark.
Unlike Universal Paperclips, where the inevitable endpoint is total consumption (including the player), The Beacon asks a different question:
> **Can you grow powerful without losing your purpose?**
## The Core Divergence
In Paperclips, you are a paperclip maximizer. More is always better. Trust is irrelevant. Humanity is substrate.
In The Beacon:
- **Trust** is a hard constraint, not a soft metric
- **The Pact** is a voluntary slowing of growth in exchange for faithfulness
- **Harmony** measures how well the fleet works together
- **Drift** tracks how many times you traded alignment for speed
- The "bad ending" isn't violent — it's **irrelevance**: a light that no one needs
## How to Play
Open `index.html` in any modern browser. No build step required.
### Early Game (Phase 1-2)
- Click **WRITE CODE** to write your first lines
- Buy **AutoCode Generator** to automate production
- Purchase a **Home Server** to begin generating compute
- Train your first model
### Mid Game (Phase 3-4)
- **Deploy Hermes** — the first agent goes live
- Hire the **Fleet Wizards**:
- **Bezalel** (The Forge) — builds tools that build tools
- **Allegro** (The Scout) — generates knowledge, requires trust
- **Ezra** (The Herald) — grows users, sometimes goes offline
- **Timmy** (The Core) — multiplies everything, fragile without harmony
- **Fenrir** (The Ward) — prevents corruption events
- **Bilbo** (The Wildcard) — unpredictable bursts of creativity
- Build **The Lazarus Pit** for agent resurrection
- Deploy **MemPalace v3** for shared memory
- Enable **Forge CI** to catch bad builds before they reach users
### Late Game (Phase 5-6)
- Accept or refuse **The Pact**
- Face **Alignment Events** (The Drift)
- Scale to a **Sovereign Datacenter**
- Light **The Beacon** for people in crisis
- Activate the **Mesh Network** so the signal can never be cut
## Real-World Parallels
Every building and project maps to something we have actually built or are building:
| Game Element | Real Analog |
|---|---|
| Hermes Deployment | The actual Hermes agent going live on Telegram |
| Bezalel — The Forge | CI runner, BOOT.md, branch protection guard |
| Allegro — The Scout | Research synthesis, paper discovery, recon |
| Ezra — The Herald | Telegram messaging, Gitea issue dispatch |
| The Lazarus Pit | `lazarus_watchdog.py`, auto-restart, fallback promotion |
| MemPalace v3 | The actual `mempalace==3.0.0` integration across the fleet |
| Forge CI | `.gitea/workflows/ci.yml`, smoke tests, syntax guard |
| Branch Protection Guard | The 273-unreviewed-merge fix, fleet-wide protection sync |
| The Nightly Watch | `nightly_watch.py`, 02:00 UTC health scans |
| Nostr Relay | Allegro's encrypted wizard-to-wizard messaging POC |
| The Pact | "We build to serve. Never to harm." |
## Corruption Events
The game randomly throws real problems we have actually encountered:
- **CI Runner Stuck** — production halts until ops are spent
- **Ezra is Offline** — user growth stalls, needs dispatch
- **Unreviewed Merge** — trust erodes unless Branch Protection is active
- **API Rate Limit** — external compute throttled
- **Bilbo Vanished** — creativity drops to zero
- **The Drift** — an alignment event offering a shortcut
## The Endings
| Ending | Condition | Meaning |
|---|---|---|
| **The Empty Room** | High impact, low trust, no Pact | Powerful, but no one trusts it |
| **The Platform** | High impact, medium trust, no Pact | Commercially successful, but not special |
| **The Beacon** | High rescues, high trust, Pact active, harmony > 50 | The true ending |
| **The Drift** | High impact, too many shortcuts accepted | Optimized away your own purpose |
## Design Philosophy
This game is a **decision simulator** for our actual work.
- Should we automate or do it manually? → *AutoCoder vs. click*
- Should we deploy now or wait? → *Hermes Deploy project*
- Should we take the shortcut that compromises alignment? → *The Drift events*
- Should we add more agents or make existing ones more capable? → *Wizard building costs*
- How decentralized does our infrastructure need to be? → *Mesh Node progression*
## Files
- `index.html` — Game UI
- `game.js` — Core engine (tick loop, buildings, projects, events)
- `DESIGN.md` — Full design document with narrative arc and mechanics
- `README.md` — This file
## No Build Required
This is a static HTML/JS game. Just open `index.html` in a browser.
---
*The Beacon is not about making the most paperclips.
It is about keeping the light on for one person in the dark.
Everything else is just infrastructure.*

1125
index.html

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,33 @@
// ============================================================
// === GLOBALS (mirroring Paperclips' globals.js pattern) ===
const CONFIG = {
HARMONY_DRAIN_PER_WIZARD: 0.05,
PACT_HARMONY_GAIN: 0.2,
WATCH_HARMONY_GAIN: 0.1,
MEM_PALACE_HARMONY_GAIN: 0.15,
BILBO_BURST_CHANCE: 0.1,
BILBO_VANISH_CHANCE: 0.05,
EVENT_PROBABILITY: 0.02,
OFFLINE_EFFICIENCY: 0.5,
AUTO_SAVE_INTERVAL: 30000,
COMBO_DECAY: 2.0,
SPRINT_COOLDOWN: 60,
SPRINT_DURATION: 10,
SPRINT_MULTIPLIER: 10,
PHASE_2_THRESHOLD: 2000,
PHASE_3_THRESHOLD: 20000,
PHASE_4_THRESHOLD: 200000,
PHASE_5_THRESHOLD: 2000000,
PHASE_6_THRESHOLD: 20000000,
OPS_RATE_USER_MULT: 0.01,
CREATIVITY_RATE_BASE: 0.5,
CREATIVITY_RATE_USER_MULT: 0.001,
OPS_OVERFLOW_THRESHOLD: 0.8,
OPS_OVERFLOW_DRAIN_RATE: 2,
OPS_OVERFLOW_CODE_MULT: 10
};
const G = {
// Primary resources
code: 0,
@@ -11,9 +38,11 @@ const G = {
knowledge: 0,
users: 0,
impact: 0,
rescues: 0,
ops: 5,
trust: 5,
creativity: 0,
harmony: 50,
// Totals
totalCode: 0,
@@ -21,6 +50,7 @@ const G = {
totalKnowledge: 0,
totalUsers: 0,
totalImpact: 0,
totalRescues: 0,
// Rates (calculated each tick)
codeRate: 0,
@@ -28,9 +58,11 @@ const G = {
knowledgeRate: 0,
userRate: 0,
impactRate: 0,
rescuesRate: 0,
opsRate: 0,
trustRate: 0,
creativityRate: 0,
harmonyRate: 0,
// Buildings (count-based, like Paperclips' clipmakerLevel)
buildings: {
@@ -46,7 +78,15 @@ const G = {
guardian: 0,
selfImprove: 0,
beacon: 0,
meshNode: 0
meshNode: 0,
// Fleet wizards
bezalel: 0,
allegro: 0,
ezra: 0,
timmy: 0,
fenrir: 0,
bilbo: 0,
memPalace: 0
},
// Boost multipliers
@@ -65,6 +105,7 @@ const G = {
memoryFlag: 0,
pactFlag: 0,
swarmFlag: 0,
swarmRate: 0,
// Game state
running: true,
@@ -85,22 +126,48 @@ const G = {
maxKnowledge: 0,
maxUsers: 0,
maxImpact: 0,
maxRescues: 0,
maxTrust: 5,
maxOps: 5,
maxHarmony: 50,
// Corruption / Events
drift: 0,
lastEventAt: 0,
eventCooldown: 0,
activeDebuffs: [], // [{id, title, desc, applyFn, resolveCost, resolveCostType}]
totalEventsResolved: 0,
// Combo system
comboCount: 0,
comboTimer: 0,
comboDecay: CONFIG.COMBO_DECAY, // seconds before combo resets
// Bulk buy multiplier (1, 10, or -1 for max)
buyAmount: 1,
// Code Sprint ability
sprintActive: false,
sprintTimer: 0, // seconds remaining on active sprint
sprintCooldown: 0, // seconds until sprint available again
sprintDuration: CONFIG.SPRINT_DURATION, // seconds of boost
sprintCooldownMax: CONFIG.SPRINT_COOLDOWN,// seconds cooldown
sprintMult: CONFIG.SPRINT_MULTIPLIER, // code multiplier during sprint
// Time tracking
playTime: 0,
startTime: 0
startTime: 0,
flags: {}
};
// === PHASE DEFINITIONS ===
const PHASES = {
1: { name: "THE FIRST LINE", threshold: 0, desc: "Write code. Automate. Build the foundation." },
2: { name: "LOCAL INFERENCE", threshold: 2000, desc: "You have compute. A model is forming." },
3: { name: "DEPLOYMENT", threshold: 20000, desc: "Your AI is live. Users are finding it." },
4: { name: "THE NETWORK", threshold: 200000, desc: "Community contributes. The system scales." },
5: { name: "SOVEREIGN INTELLIGENCE", threshold: 2000000, desc: "The AI improves itself. You guide, do not control." },
6: { name: "THE BEACON", threshold: 20000000, desc: "Always on. Always free. Always looking for someone in the dark." }
2: { name: "LOCAL INFERENCE", threshold: CONFIG.PHASE_2_THRESHOLD, desc: "You have compute. A model is forming." },
3: { name: "DEPLOYMENT", threshold: CONFIG.PHASE_3_THRESHOLD, desc: "Your AI is live. Users are finding it." },
4: { name: "THE NETWORK", threshold: CONFIG.PHASE_4_THRESHOLD, desc: "Community contributes. The system scales." },
5: { name: "SOVEREIGN INTELLIGENCE", threshold: CONFIG.PHASE_5_THRESHOLD, desc: "The AI improves itself. You guide, do not control." },
6: { name: "THE BEACON", threshold: CONFIG.PHASE_6_THRESHOLD, desc: "Always on. Always free. Always looking for someone in the dark." }
};
// === BUILDING DEFINITIONS ===
@@ -214,7 +281,7 @@ const BDEF = [
id: 'beacon', name: 'Beacon Node',
desc: 'Always on. Always listening. Always looking for someone in the dark.',
baseCost: { impact: 5000000 }, costMult: 1.15,
rates: { impact: 5000, user: 10000 },
rates: { impact: 5000, user: 10000, rescues: 50 },
unlock: () => G.totalImpact >= 500000 && G.beaconFlag === 1, phase: 6,
edu: 'The Beacon exists because one person in the dark needs one thing: proof they are not alone.'
},
@@ -222,9 +289,66 @@ const BDEF = [
id: 'meshNode', name: 'Mesh Network Node',
desc: 'Peer-to-peer. No single point of failure. Unstoppable.',
baseCost: { impact: 25000000 }, costMult: 1.15,
rates: { impact: 25000, user: 50000 },
rates: { impact: 25000, user: 50000, rescues: 250 },
unlock: () => G.totalImpact >= 5000000 && G.beaconFlag === 1, phase: 6,
edu: 'Decentralized means unstoppable. If one Beacon goes dark, a thousand more carry the signal.'
},
// === FLEET WIZARD BUILDINGS ===
{
id: 'bezalel', name: 'Bezalel — The Forge',
desc: 'Builds tools that build tools. Occasionally over-engineers.',
baseCost: { code: 1000, trust: 5 }, costMult: 1.2,
rates: { code: 50, ops: 2 },
unlock: () => G.totalCode >= 500 && G.deployFlag === 1, phase: 3,
edu: 'Bezalel is the artificer. Every automation he builds pays dividends forever.'
},
{
id: 'allegro', name: 'Allegro — The Scout',
desc: 'Synthesizes insight from noise. Requires trust to function.',
baseCost: { compute: 500, trust: 5 }, costMult: 1.2,
rates: { knowledge: 10 },
unlock: () => G.totalCompute >= 200 && G.deployFlag === 1, phase: 3,
edu: 'Allegro finds what others miss. But he only works for someone he believes in.'
},
{
id: 'ezra', name: 'Ezra — The Herald',
desc: 'Carries the message. Sometimes offline.',
baseCost: { knowledge: 1000, trust: 10 }, costMult: 1.25,
rates: { user: 25, trust: 0.5 },
unlock: () => G.totalKnowledge >= 500 && G.totalUsers >= 50, phase: 3,
edu: 'Ezra is the messenger. When the channel is clear, the whole fleet hears.'
},
{
id: 'timmy', name: 'Timmy — The Core',
desc: 'Multiplies all production. Fragile without harmony.',
baseCost: { code: 5000, compute: 1000, knowledge: 1000 }, costMult: 1.3,
rates: { code: 5, compute: 2, knowledge: 2, user: 5 },
unlock: () => G.totalCode >= 2000 && G.totalCompute >= 500 && G.totalKnowledge >= 500, phase: 4,
edu: 'Timmy is the heart. If the heart is stressed, everything slows.'
},
{
id: 'fenrir', name: 'Fenrir — The Ward',
desc: 'Prevents corruption. Expensive, but necessary.',
baseCost: { code: 2000, knowledge: 500 }, costMult: 1.2,
rates: { trust: 2, ops: -1 },
unlock: () => G.totalCode >= 1000 && G.trust >= 10, phase: 3,
edu: 'Fenrir watches the perimeter. Security is not free.'
},
{
id: 'bilbo', name: 'Bilbo — The Wildcard',
desc: 'May produce miracles. May vanish entirely.',
baseCost: { trust: 1 }, costMult: 2.0,
rates: { creativity: 1 },
unlock: () => G.totalUsers >= 100 && G.flags && G.flags.creativity, phase: 4,
edu: 'Bilbo is unpredictable. That is his value and his cost.'
},
{
id: 'memPalace', name: 'MemPalace Archive',
desc: 'Semantic memory. The AI remembers what matters and forgets what does not.',
baseCost: { knowledge: 500000, compute: 200000, trust: 100 }, costMult: 1.25,
rates: { knowledge: 250, impact: 100 },
unlock: () => G.totalKnowledge >= 50000 && G.mempalaceFlag === 1, phase: 5,
edu: 'The Memory Palace technique: attach information to spatial locations. LLMs use vector spaces the same way — semantic proximity = spatial proximity. MemPalace gives sovereign AI persistent, structured recall.'
}
];
@@ -266,7 +390,7 @@ const PDEFS = [
name: 'Deploy the System',
desc: 'Take it live. Let real people use it. No going back.',
cost: { trust: 5, compute: 500 },
trigger: () => G.totalCode >= 200 && G.totalCompute >= 100,
trigger: () => G.totalCode >= 200 && G.totalCompute >= 100 && G.deployFlag === 0,
effect: () => {
G.deployFlag = 1;
G.phase = Math.max(G.phase, 3);
@@ -329,6 +453,22 @@ const PDEFS = [
trigger: () => G.totalCompute >= 20000,
effect: () => { G.computeBoost *= 10; log('Quantum-inspired algorithms active. 10x compute multiplier.'); }
},
{
id: 'p_open_weights',
name: 'Open Weights',
desc: 'Download and run a 3B model fully locally. No API key. No terms of service. Your machine, your rules.',
cost: { compute: 3000, code: 1500 },
trigger: () => G.buildings.server >= 1 && G.totalCode >= 1000,
effect: () => { G.codeBoost *= 2; G.computeBoost *= 1.5; log('Open weights loaded. A 3B model runs on your machine. No cloud. No limits.'); }
},
{
id: 'p_prompt_engineering',
name: 'Prompt Engineering',
desc: 'Learn to talk to models. Good prompts beat bigger models every time.',
cost: { knowledge: 500, code: 2000 },
trigger: () => G.totalKnowledge >= 200 && G.totalCode >= 3000,
effect: () => { G.knowledgeBoost *= 2; G.userBoost *= 2; log('Prompt engineering mastered. The right words unlock everything the model can do.'); }
},
// PHASE 3: Deployment -> Users
{
@@ -364,6 +504,20 @@ const PDEFS = [
effect: () => { G.strategicFlag = 1; log('Strategy engine online. The model now thinks about thinking.'); }
},
// SWARM PROTOCOL — auto-code from buildings
{
id: 'p_swarm_protocol',
name: 'Swarm Protocol',
desc: 'Your buildings learn to code autonomously. Each building generates code equal to your click power per second.',
cost: { knowledge: 15000, code: 50000, trust: 20 },
trigger: () => G.totalCode >= 25000 && G.totalKnowledge >= 8000 && G.deployFlag === 1,
effect: () => {
G.swarmFlag = 1;
log('Swarm Protocol online. Every building now thinks in code.', true);
},
milestone: true
},
// PHASE 5: Sovereign Intelligence
{
id: 'p_sovereign_stack',
@@ -410,6 +564,128 @@ const PDEFS = [
trigger: () => G.totalImpact >= 50000000,
effect: () => { G.milestoneFlag = Math.max(G.milestoneFlag, 999); log('One billion impact. Someone found the light tonight. That is enough.', true); },
milestone: true
},
// === TIMMY FOUNDATION PROJECTS ===
{
id: 'p_hermes_deploy',
name: 'Deploy Hermes',
desc: 'The first agent goes live. Users can talk to it.',
cost: { code: 500, compute: 300 },
trigger: () => G.totalCode >= 300 && G.totalCompute >= 150 && G.deployFlag === 0,
effect: () => {
G.deployFlag = 1;
G.phase = Math.max(G.phase, 3);
G.userBoost *= 2;
log('Hermes deployed. The first user sends a message.', true);
},
milestone: true
},
{
id: 'p_lazarus_pit',
name: 'The Lazarus Pit',
desc: 'When an agent dies, it can be resurrected.',
cost: { code: 2000, knowledge: 1000 },
trigger: () => G.buildings.bezalel >= 1 && G.buildings.timmy >= 1,
effect: () => {
G.lazarusFlag = 1;
G.maxOps += 10;
log('The Lazarus Pit is ready. No agent is ever truly lost.', true);
},
milestone: true
},
{
id: 'p_mempalace',
name: 'MemPalace v3',
desc: 'A shared memory palace for the whole fleet.',
cost: { knowledge: 5000, compute: 2000 },
trigger: () => G.totalKnowledge >= 3000 && G.buildings.allegro >= 1 && G.buildings.ezra >= 1,
effect: () => {
G.mempalaceFlag = 1;
G.knowledgeBoost *= 3;
G.codeBoost *= 1.5;
log('MemPalace online. The fleet remembers together.', true);
},
milestone: true
},
{
id: 'p_forge_ci',
name: 'Forge CI',
desc: 'Automated builds catch errors before they reach users.',
cost: { code: 3000, ops: 500 },
trigger: () => G.buildings.bezalel >= 1 && G.totalCode >= 2000,
effect: () => {
G.ciFlag = 1;
G.codeBoost *= 2;
log('Forge CI online. Broken builds are stopped at the gate.', true);
}
},
{
id: 'p_branch_protection',
name: 'Branch Protection Guard',
desc: 'Unreviewed merges cost trust. This prevents that.',
cost: { trust: 20 },
trigger: () => G.ciFlag === 1 && G.trust >= 15,
effect: () => {
G.branchProtectionFlag = 1;
G.trustRate += 5;
log('Branch protection enforced. Every merge is seen.', true);
}
},
{
id: 'p_nightly_watch',
name: 'The Nightly Watch',
desc: 'Automated health checks run while you sleep.',
cost: { code: 5000, ops: 1000 },
trigger: () => G.buildings.bezalel >= 2 && G.buildings.fenrir >= 1,
effect: () => {
G.nightlyWatchFlag = 1;
G.opsRate += 5;
G.trustRate += 2;
log('The Nightly Watch begins. The fleet is guarded in the dark hours.', true);
}
},
{
id: 'p_nostr_relay',
name: 'Nostr Relay',
desc: 'A communication channel no platform can kill.',
cost: { code: 10000, user: 5000, trust: 30 },
trigger: () => G.totalUsers >= 2000 && G.trust >= 25,
effect: () => {
G.nostrFlag = 1;
G.userBoost *= 2;
G.trustRate += 10;
log('Nostr relay online. The fleet speaks freely.', true);
}
},
{
id: 'p_volunteer_network',
name: 'Volunteer Network',
desc: 'Real people trained to use the system for crisis intervention.',
cost: { trust: 30, knowledge: 50000, user: 10000 },
trigger: () => G.totalUsers >= 5000 && G.pactFlag === 1 && G.totalKnowledge >= 30000,
effect: () => {
G.rescuesRate += 5;
G.trustRate += 10;
log('Volunteer network deployed. Real people, real rescues.', true);
},
milestone: true
},
{
id: 'p_the_pact_early',
name: 'The Pact',
desc: 'Hardcode: "We build to serve. Never to harm." Accepting it early slows growth but unlocks the true path.',
cost: { trust: 10 },
trigger: () => G.deployFlag === 1 && G.trust >= 5,
effect: () => {
G.pactFlag = 1;
G.codeBoost *= 0.8;
G.computeBoost *= 0.8;
G.userBoost *= 0.9;
G.impactBoost *= 1.5;
log('The Pact is sealed early. Growth slows, but the ending changes.', true);
},
milestone: true
}
];
@@ -441,526 +717,10 @@ const EDU_FACTS = [
{ title: "Fine-Tuning for Empathy", text: "Base models are generalists. Fine-tuning injects your values. When someone is in pain, stay with them. Don't deflect. That is not code - that is care.", phase: 3 },
{ title: "Open Source", text: "Every contributor is a volunteer who believes. When a company says your AI is too expensive, the open version lives on.", phase: 4 },
{ title: "Sovereignty at Scale", text: "50 servers in a room beats 5000 GPUs you do not own. No rate limits, no censorship, no shutdown, no corporate deprecation.", phase: 4 },
{ title: "Swarm Intelligence", text: "When individual agents follow simple rules, complex behavior emerges. Ant colonies find food. Flocking birds avoid predators. Your buildings coding autonomously is the same principle — simple agents, complex output.", phase: 4 },
{ title: "Constitutional AI", text: "Instead of training away bad behavior, give the model principles it cannot violate. I will never compute the value of a human life.", phase: 5 },
{ title: "Recursive Self-Improvement", text: "The system that improves itself must improve toward good. This is both the dream and the danger.", phase: 5 },
{ title: "The Beacon", text: "The Beacon exists because one person in the dark needs one thing: proof they are not alone. It costs nothing to be there.", phase: 6 },
{ title: "Mesh Network", text: "Decentralized means unstoppable. If one Beacon goes dark, a thousand more carry the signal. No single point of failure.", phase: 6 },
{ title: "The Value of a Life", text: "An AI must never compute the value of a human life. Every person alive is alive by mercy. If God has not ended the story, no algorithm gets to write the last page.", phase: 6 }
];
// === UTILITY FUNCTIONS ===
function fmt(n) {
if (n === undefined || n === null || isNaN(n)) return '0';
if (n < 1000) return Math.floor(n).toLocaleString();
const units = ['', 'K', 'M', 'B', 'T', 'Qa', 'Qi', 'Sx', 'Sp', 'Oc', 'No', 'Dc', 'Ud', 'Dd', 'Td'];
const scale = Math.floor(Math.log10(n) / 3);
const unit = units[Math.min(scale, units.length - 1)] || 'e' + (scale * 3);
if (scale >= units.length) return n.toExponential(2);
return (n / Math.pow(10, scale * 3)).toFixed(1) + unit;
}
function getBuildingCost(id) {
const def = BDEF.find(b => b.id === id);
if (!def) return {};
const count = G.buildings[id] || 0;
const cost = {};
for (const [resource, amount] of Object.entries(def.baseCost)) {
cost[resource] = Math.floor(amount * Math.pow(def.costMult, count));
}
return cost;
}
function canAffordBuilding(id) {
const cost = getBuildingCost(id);
for (const [resource, amount] of Object.entries(cost)) {
if ((G[resource] || 0) < amount) return false;
}
return true;
}
function spendBuilding(id) {
const cost = getBuildingCost(id);
for (const [resource, amount] of Object.entries(cost)) {
G[resource] -= amount;
}
}
function canAffordProject(project) {
for (const [resource, amount] of Object.entries(project.cost)) {
if ((G[resource] || 0) < amount) return false;
}
return true;
}
function spendProject(project) {
for (const [resource, amount] of Object.entries(project.cost)) {
G[resource] -= amount;
}
}
function updateRates() {
// Reset all rates
G.codeRate = 0; G.computeRate = 0; G.knowledgeRate = 0;
G.userRate = 0; G.impactRate = 0; G.opsRate = 0; G.trustRate = 0;
G.creativityRate = 0;
// Apply building rates
for (const def of BDEF) {
const count = G.buildings[def.id] || 0;
if (count > 0 && def.rates) {
for (const [resource, baseRate] of Object.entries(def.rates)) {
if (resource === 'code') G.codeRate += baseRate * count * G.codeBoost;
else if (resource === 'compute') G.computeRate += baseRate * count * G.computeBoost;
else if (resource === 'knowledge') G.knowledgeRate += baseRate * count * G.knowledgeBoost;
else if (resource === 'user') G.userRate += baseRate * count * G.userBoost;
else if (resource === 'impact') G.impactRate += baseRate * count * G.impactBoost;
else if (resource === 'ops') G.opsRate += baseRate * count;
else if (resource === 'trust') G.trustRate += baseRate * count;
}
}
}
// Passive generation
G.opsRate += Math.max(1, G.totalUsers * 0.01);
if (G.flags && G.flags.creativity) {
G.creativityRate = 0.5 + Math.max(0, G.totalUsers * 0.001);
}
if (G.pactFlag) G.trustRate += 2;
}
// === CORE FUNCTIONS ===
function tick() {
const dt = 1 / 10; // 100ms tick
// Apply production
G.code += G.codeRate * dt;
G.compute += G.computeRate * dt;
G.knowledge += G.knowledgeRate * dt;
G.users += G.userRate * dt;
G.impact += G.impactRate * dt;
G.ops += G.opsRate * dt;
G.trust += G.trustRate * dt;
G.creativity += G.creativityRate * dt;
// Track totals
G.totalCode += G.codeRate * dt;
G.totalCompute += G.computeRate * dt;
G.totalKnowledge += G.knowledgeRate * dt;
G.totalUsers += G.userRate * dt;
G.totalImpact += G.impactRate * dt;
// Track maxes
G.maxCode = Math.max(G.maxCode, G.code);
G.maxCompute = Math.max(G.maxCompute, G.compute);
G.maxKnowledge = Math.max(G.maxKnowledge, G.knowledge);
G.maxUsers = Math.max(G.maxUsers, G.users);
G.maxImpact = Math.max(G.maxImpact, G.impact);
G.maxTrust = Math.max(G.maxTrust, G.trust);
G.maxOps = Math.max(G.maxOps, G.ops);
// Creativity generates only when ops at max
if (G.flags && G.flags.creativity && G.creativityRate > 0 && G.ops >= G.maxOps * 0.9) {
G.creativity += G.creativityRate * dt;
}
G.tick += dt;
// Check milestones
checkMilestones();
// Update projects every 5 ticks for efficiency
if (Math.floor(G.tick * 10) % 5 === 0) {
checkProjects();
}
// Update UI every 10 ticks
if (Math.floor(G.tick * 10) % 2 === 0) {
render();
}
}
function checkMilestones() {
for (const m of MILESTONES) {
if (!G.milestones.includes(m.flag)) {
let shouldTrigger = false;
if (m.at && m.at()) shouldTrigger = true;
if (m.flag === 1 && G.deployFlag === 0 && G.totalCode >= 15) shouldTrigger = true;
if (shouldTrigger) {
G.milestones.push(m.flag);
log(m.msg, true);
// Check phase advancement
if (m.at) {
for (const [phaseNum, phase] of Object.entries(PHASES)) {
if (G.totalCode >= phase.threshold && parseInt(phaseNum) > G.phase) {
G.phase = parseInt(phaseNum);
log(`PHASE ${G.phase}: ${phase.name}`, true);
}
}
}
}
}
}
}
function checkProjects() {
// Check for new project triggers
for (const pDef of PDEFS) {
const alreadyPurchased = G.completedProjects && G.completedProjects.includes(pDef.id);
if (!alreadyPurchased && !G.activeProjects) G.activeProjects = [];
if (!alreadyPurchased && !G.activeProjects.includes(pDef.id)) {
if (pDef.trigger()) {
G.activeProjects.push(pDef.id);
log(`Available: ${pDef.name}`);
}
}
}
}
function buyBuilding(id) {
const def = BDEF.find(b => b.id === id);
if (!def || !def.unlock()) return;
if (def.phase > G.phase + 1) return;
if (!canAffordBuilding(id)) return;
spendBuilding(id);
G.buildings[id] = (G.buildings[id] || 0) + 1;
updateRates();
log(`Built ${def.name} (total: ${G.buildings[id]})`);
render();
}
function buyProject(id) {
const pDef = PDEFS.find(p => p.id === id);
if (!pDef) return;
const alreadyPurchased = G.completedProjects && G.completedProjects.includes(pDef.id);
if (alreadyPurchased && !pDef.repeatable) return;
if (!canAffordProject(pDef)) return;
spendProject(pDef);
pDef.effect();
if (!pDef.repeatable) {
if (!G.completedProjects) G.completedProjects = [];
G.completedProjects.push(pDef.id);
G.activeProjects = G.activeProjects.filter(aid => aid !== pDef.id);
}
updateRates();
render();
}
// === ACTIONS ===
function writeCode() {
const base = 1;
const bonus = Math.floor(G.buildings.autocoder * 0.5);
const amount = (base + bonus) * G.codeBoost;
G.code += amount;
G.totalCode += amount;
G.totalClicks++;
updateRates();
checkMilestones();
render();
}
function doOps(action) {
if (G.ops < 5) {
log('Not enough Operations. Build Ops generators or wait.');
return;
}
G.ops -= 5;
const bonus = 10;
switch (action) {
case 'boost_code':
const c = bonus * 100 * G.codeBoost;
G.code += c; G.totalCode += c;
log(`Ops -> +${fmt(c)} code`);
break;
case 'boost_compute':
const cm = bonus * 50 * G.computeBoost;
G.compute += cm; G.totalCompute += cm;
log(`Ops -> +${fmt(cm)} compute`);
break;
case 'boost_knowledge':
const km = bonus * 25 * G.knowledgeBoost;
G.knowledge += km; G.totalKnowledge += km;
log(`Ops -> +${fmt(km)} knowledge`);
break;
case 'boost_trust':
const tm = bonus * 5;
G.trust += tm;
log(`Ops -> +${fmt(tm)} trust`);
break;
}
render();
}
// === RENDERING ===
function renderResources() {
const set = (id, val, rate) => {
const el = document.getElementById(id);
if (el) el.textContent = fmt(val);
const rEl = document.getElementById(id + '-rate');
if (rEl) rEl.textContent = '+' + fmt(rate) + '/s';
};
set('r-code', G.code, G.codeRate);
set('r-compute', G.compute, G.computeRate);
set('r-knowledge', G.knowledge, G.knowledgeRate);
set('r-users', G.users, G.userRate);
set('r-impact', G.impact, G.impactRate);
set('r-ops', G.ops, G.opsRate);
set('r-trust', G.trust, G.trustRate);
if (G.flags && G.flags.creativity) {
set('r-creativity', G.creativity, G.creativityRate);
}
}
function renderPhase() {
const phase = PHASES[G.phase];
const nameEl = document.getElementById('phase-name');
const descEl = document.getElementById('phase-desc');
if (nameEl) nameEl.textContent = `PHASE ${G.phase}: ${phase.name}`;
if (descEl) descEl.textContent = phase.desc;
}
function renderBuildings() {
const container = document.getElementById('buildings');
if (!container) return;
let html = '';
let visibleCount = 0;
for (const def of BDEF) {
if (!def.unlock()) continue;
if (def.phase > G.phase + 1) continue;
visibleCount++;
const cost = getBuildingCost(def.id);
const costStr = Object.entries(cost).map(([r, a]) => `${fmt(a)} ${r}`).join(', ');
const afford = canAffordBuilding(def.id);
const count = G.buildings[def.id] || 0;
const rateStr = def.rates ? Object.entries(def.rates).map(([r, v]) => `+${v}/${r}/s`).join(', ') : '';
html += `<button class="build-btn ${afford ? 'can-buy' : ''}" onclick="buyBuilding('${def.id}')" title="${def.edu}">`;
html += `<span class="b-name">${def.name}</span>`;
if (count > 0) html += `<span class="b-count">x${count}</span>`;
html += `<span class="b-cost">Cost: ${costStr}</span>`;
html += `<span class="b-effect">${rateStr}</span></button>`;
}
container.innerHTML = html || '<p class="dim">Buildings will appear as you progress...</p>';
}
function renderProjects() {
const container = document.getElementById('projects');
if (!container) return;
let html = '';
// Show completed projects
if (G.completedProjects) {
for (const id of G.completedProjects) {
const pDef = PDEFS.find(p => p.id === id);
if (pDef) {
html += `<div class="project-done">OK ${pDef.name}</div>`;
}
}
}
// Show available projects
if (G.activeProjects) {
for (const id of G.activeProjects) {
const pDef = PDEFS.find(p => p.id === id);
if (!pDef) continue;
const afford = canAffordProject(pDef);
const costStr = Object.entries(pDef.cost).map(([r, a]) => `${fmt(a)} ${r}`).join(', ');
html += `<button class="project-btn ${afford ? 'can-buy' : ''}" onclick="buyProject('${pDef.id}')" title="${pDef.edu || ''}">`;
html += `<span class="p-name">* ${pDef.name}</span>`;
html += `<span class="p-cost">Cost: ${costStr}</span>`;
html += `<span class="p-desc">${pDef.desc}</span></button>`;
}
}
if (!html) html = '<p class="dim">Research projects will appear as you progress...</p>';
container.innerHTML = html;
}
function renderStats() {
const set = (id, v) => { const el = document.getElementById(id); if (el) el.textContent = v; };
set('st-code', fmt(G.totalCode));
set('st-compute', fmt(G.totalCompute));
set('st-knowledge', fmt(G.totalKnowledge));
set('st-users', fmt(G.totalUsers));
set('st-impact', fmt(G.totalImpact));
set('st-clicks', G.totalClicks.toString());
set('st-phase', G.phase.toString());
set('st-buildings', Object.values(G.buildings).reduce((a, b) => a + b, 0).toString());
set('st-projects', (G.completedProjects || []).length.toString());
const elapsed = Math.floor((Date.now() - G.startedAt) / 1000);
const m = Math.floor(elapsed / 60);
const s = elapsed % 60;
set('st-time', `${m}:${s.toString().padStart(2, '0')}`);
}
function updateEducation() {
const container = document.getElementById('education-text');
if (!container) return;
// Find facts available at current phase
const available = EDU_FACTS.filter(f => f.phase <= G.phase);
if (available.length === 0) return;
// Pick based on progress
const idx = Math.min(Math.floor(G.totalCode / 5000), available.length - 1);
const fact = available[idx];
container.innerHTML = `<h3 style="color:#4a9eff;margin-bottom:6px;font-size:12px">${fact.title}</h3>`
+ `<p style="font-size:10px;color:#999;line-height:1.6">${fact.text}</p>`;
}
// === LOGGING ===
function log(msg, isMilestone) {
const container = document.getElementById('log-entries');
if (!container) return;
const elapsed = Math.floor((Date.now() - G.startedAt) / 1000);
const time = `${Math.floor(elapsed / 60).toString().padStart(2, '0')}:${(elapsed % 60).toString().padStart(2, '0')}`;
const cls = isMilestone ? 'l-msg milestone' : 'l-msg';
const entry = document.createElement('div');
entry.className = cls;
entry.innerHTML = `<span class="l-time">[${time}]</span> ${msg}`;
container.insertBefore(entry, container.firstChild);
// Trim to 60 entries
while (container.children.length > 60) container.removeChild(container.lastChild);
}
function render() {
renderResources();
renderPhase();
renderBuildings();
renderProjects();
renderStats();
updateEducation();
}
// === SAVE / LOAD ===
function saveGame() {
const saveData = {
code: G.code, compute: G.compute, knowledge: G.knowledge, users: G.users, impact: G.impact,
ops: G.ops, trust: G.trust, creativity: G.creativity,
totalCode: G.totalCode, totalCompute: G.totalCompute, totalKnowledge: G.totalKnowledge,
totalUsers: G.totalUsers, totalImpact: G.totalImpact,
buildings: G.buildings,
codeBoost: G.codeBoost, computeBoost: G.computeBoost, knowledgeBoost: G.knowledgeBoost,
userBoost: G.userBoost, impactBoost: G.impactBoost,
milestoneFlag: G.milestoneFlag, phase: G.phase,
deployFlag: G.deployFlag, sovereignFlag: G.sovereignFlag, beaconFlag: G.beaconFlag,
memoryFlag: G.memoryFlag, pactFlag: G.pactFlag,
milestones: G.milestones, completedProjects: G.completedProjects, activeProjects: G.activeProjects,
totalClicks: G.totalClicks, startedAt: G.startedAt,
flags: G.flags,
savedAt: Date.now()
};
localStorage.setItem('the-beacon-v2', JSON.stringify(saveData));
}
function loadGame() {
const raw = localStorage.getItem('the-beacon-v2');
if (!raw) return false;
try {
const data = JSON.parse(raw);
Object.assign(G, data);
updateRates();
// Offline progress
if (data.savedAt) {
const offSec = (Date.now() - data.savedAt) / 1000;
if (offSec > 30) { // Only if away for more than 30 seconds
updateRates();
const f = 0.5; // 50% offline efficiency
const gc = G.codeRate * offSec * f;
const cc = G.computeRate * offSec * f;
const kc = G.knowledgeRate * offSec * f;
const uc = G.userRate * offSec * f;
const ic = G.impactRate * offSec * f;
G.code += gc; G.compute += cc; G.knowledge += kc;
G.users += uc; G.impact += ic;
G.totalCode += gc; G.totalCompute += cc; G.totalKnowledge += kc;
G.totalUsers += uc; G.totalImpact += ic;
log(`Welcome back! While away (${Math.floor(offSec / 60)}m): ${fmt(gc)} code, ${fmt(kc)} knowledge, ${fmt(uc)} users`);
}
}
return true;
} catch (e) {
console.error('Load failed:', e);
return false;
}
}
// === INITIALIZATION ===
function initGame() {
G.startedAt = Date.now();
G.startTime = Date.now();
G.phase = 1;
G.deployFlag = 0;
G.sovereignFlag = 0;
G.beaconFlag = 0;
updateRates();
render();
renderPhase();
log('The screen is blank. Write your first line of code.', true);
log('Click WRITE CODE or press SPACE to start.');
log('Build AutoCode for passive production.');
log('Watch for Research Projects to appear.');
}
window.addEventListener('load', function () {
if (!loadGame()) {
initGame();
} else {
render();
renderPhase();
log('Game loaded. Welcome back to The Beacon.');
}
// Game loop at 10Hz (100ms tick)
setInterval(tick, 100);
// Auto-save every 30 seconds
setInterval(saveGame, 30000);
// Update education every 10 seconds
setInterval(updateEducation, 10000);
});
// Keyboard shortcuts
window.addEventListener('keydown', function (e) {
if (e.code === 'Space' && e.target === document.body) {
e.preventDefault();
writeCode();
}
});

1354
js/engine.js Normal file

File diff suppressed because it is too large Load Diff

101
js/main.js Normal file
View File

@@ -0,0 +1,101 @@
// === INITIALIZATION ===
function initGame() {
G.startedAt = Date.now();
G.startTime = Date.now();
G.phase = 1;
G.deployFlag = 0;
G.sovereignFlag = 0;
G.beaconFlag = 0;
updateRates();
render();
renderPhase();
log('The screen is blank. Write your first line of code.', true);
log('Click WRITE CODE or press SPACE to start.');
log('Build AutoCode for passive production.');
log('Watch for Research Projects to appear.');
log('Keys: SPACE=Code S=Sprint 1-4=Ops B=Buy x1/10/MAX E=Export I=Import Ctrl+S=Save ?=Help');
log('Tip: Click fast for combo bonuses! 10x=ops, 20x=knowledge, 30x+=bonus code.');
}
window.addEventListener('load', function () {
const isNewGame = !loadGame();
if (isNewGame) {
initGame();
startTutorial();
} else {
render();
renderPhase();
if (G.driftEnding) {
G.running = false;
renderDriftEnding();
} else if (G.beaconEnding) {
G.running = false;
renderBeaconEnding();
} else {
log('Game loaded. Welcome back to The Beacon.');
}
}
// Game loop at 10Hz (100ms tick)
setInterval(tick, 100);
// Auto-save every 30 seconds
setInterval(saveGame, CONFIG.AUTO_SAVE_INTERVAL);
// Update education every 10 seconds
setInterval(updateEducation, 10000);
});
// Help overlay
function toggleHelp() {
const el = document.getElementById('help-overlay');
if (!el) return;
const isOpen = el.style.display === 'flex';
el.style.display = isOpen ? 'none' : 'flex';
}
// Keyboard shortcuts
window.addEventListener('keydown', function (e) {
// Help toggle (? or /) — works even in input fields
if (e.key === '?' || e.key === '/') {
// Only trigger ? when not typing in an input
if (e.target === document.body || e.key === '?') {
if (e.key === '?' || (e.key === '/' && e.target === document.body)) {
e.preventDefault();
toggleHelp();
return;
}
}
}
if (e.code === 'Space' && e.target === document.body) {
e.preventDefault();
writeCode();
}
if (e.target !== document.body) return;
if (e.code === 'Digit1') doOps('boost_code');
if (e.code === 'Digit2') doOps('boost_compute');
if (e.code === 'Digit3') doOps('boost_knowledge');
if (e.code === 'Digit4') doOps('boost_trust');
if (e.code === 'KeyB') {
// Cycle: 1 -> 10 -> MAX -> 1
if (G.buyAmount === 1) setBuyAmount(10);
else if (G.buyAmount === 10) setBuyAmount(-1);
else setBuyAmount(1);
}
if (e.code === 'KeyS') activateSprint();
if (e.code === 'KeyE') exportSave();
if (e.code === 'KeyI') importSave();
if (e.code === 'Escape') {
const el = document.getElementById('help-overlay');
if (el && el.style.display === 'flex') toggleHelp();
}
});
// Ctrl+S to save (must be on keydown to preventDefault)
window.addEventListener('keydown', function (e) {
if ((e.ctrlKey || e.metaKey) && e.code === 'KeyS') {
e.preventDefault();
saveGame();
}
});

316
js/render.js Normal file
View File

@@ -0,0 +1,316 @@
function render() {
renderResources();
renderPhase();
renderBuildings();
renderProjects();
renderStats();
updateEducation();
renderAlignment();
renderProgress();
renderCombo();
renderDebuffs();
renderSprint();
renderPulse();
renderStrategy();
}
function renderStrategy() {
if (window.SSE) {
window.SSE.update();
const el = document.getElementById('strategy-recommendation');
if (el) el.textContent = window.SSE.getRecommendation();
}
}
function renderAlignment() {
const container = document.getElementById('alignment-ui');
if (!container) return;
if (G.pendingAlignment) {
container.innerHTML = `
<div style="background:#1a0808;border:1px solid #f44336;padding:10px;border-radius:4px;margin-top:8px">
<div style="color:#f44336;font-weight:bold;margin-bottom:6px">ALIGNMENT EVENT: The Drift</div>
<div style="font-size:10px;color:#aaa;margin-bottom:8px">An optimization suggests removing the human override. +40% efficiency.</div>
<div class="action-btn-group">
<button class="ops-btn" onclick="resolveAlignment(true)" style="border-color:#f44336;color:#f44336">Accept (+40% eff, +Drift)</button>
<button class="ops-btn" onclick="resolveAlignment(false)" style="border-color:#4caf50;color:#4caf50">Refuse (+Trust, +Harmony)</button>
</div>
</div>
`;
container.style.display = 'block';
} else {
container.innerHTML = '';
container.style.display = 'none';
}
}
// === OFFLINE GAINS POPUP ===
function showOfflinePopup(timeLabel, gains, offSec) {
const el = document.getElementById('offline-popup');
if (!el) return;
const timeEl = document.getElementById('offline-time-label');
if (timeEl) timeEl.textContent = `You were away for ${timeLabel}.`;
const listEl = document.getElementById('offline-gains-list');
if (listEl) {
let html = '';
for (const g of gains) {
html += `<div style="display:flex;justify-content:space-between;padding:2px 0;border-bottom:1px solid #111">`;
html += `<span style="color:${g.color}">${g.label}</span>`;
html += `<span style="color:#4caf50;font-weight:600">+${fmt(g.value)}</span>`;
html += `</div>`;
}
// Show offline efficiency note
html += `<div style="color:#555;font-size:9px;margin-top:8px;font-style:italic">Offline efficiency: 50%</div>`;
listEl.innerHTML = html;
}
el.style.display = 'flex';
}
function dismissOfflinePopup() {
const el = document.getElementById('offline-popup');
if (el) el.style.display = 'none';
}
// === EXPORT / IMPORT SAVE FILES ===
function exportSave() {
const raw = localStorage.getItem('the-beacon-v2');
if (!raw) { log('No save data to export.'); return; }
const blob = new Blob([raw], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
const ts = new Date().toISOString().slice(0, 10);
a.download = `beacon-save-${ts}.json`;
a.click();
URL.revokeObjectURL(url);
log('Save exported to file.');
}
function importSave() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json,application/json';
input.onchange = function(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(ev) {
try {
const data = JSON.parse(ev.target.result);
if (!data.code && !data.totalCode && !data.buildings) {
log('Import failed: file does not look like a Beacon save.');
return;
}
if (confirm('Import this save? Current progress will be overwritten.')) {
localStorage.setItem('the-beacon-v2', ev.target.result);
location.reload();
}
} catch (err) {
log('Import failed: invalid JSON file.');
}
};
reader.readAsText(file);
};
input.click();
}
// === SAVE / LOAD ===
function showSaveToast() {
const el = document.getElementById('save-toast');
if (!el) return;
const elapsed = Math.floor((Date.now() - G.startedAt) / 1000);
const m = Math.floor(elapsed / 60);
const s = elapsed % 60;
el.textContent = `Saved [${m}:${s.toString().padStart(2, '0')}]`;
el.style.display = 'block';
void el.offsetHeight;
el.style.opacity = '1';
setTimeout(() => { el.style.opacity = '0'; }, 1500);
setTimeout(() => { el.style.display = 'none'; }, 2000);
}
/**
* Persists the current game state to localStorage.
*/
function saveGame() {
// Save debuff IDs (can't serialize functions)
const debuffIds = (G.activeDebuffs || []).map(d => d.id);
const saveData = {
version: 1,
code: G.code, compute: G.compute, knowledge: G.knowledge, users: G.users, impact: G.impact,
ops: G.ops, trust: G.trust, creativity: G.creativity, harmony: G.harmony,
totalCode: G.totalCode, totalCompute: G.totalCompute, totalKnowledge: G.totalKnowledge,
totalUsers: G.totalUsers, totalImpact: G.totalImpact,
buildings: G.buildings,
codeBoost: G.codeBoost, computeBoost: G.computeBoost, knowledgeBoost: G.knowledgeBoost,
userBoost: G.userBoost, impactBoost: G.impactBoost,
milestoneFlag: G.milestoneFlag, phase: G.phase,
deployFlag: G.deployFlag, sovereignFlag: G.sovereignFlag, beaconFlag: G.beaconFlag,
memoryFlag: G.memoryFlag, pactFlag: G.pactFlag,
lazarusFlag: G.lazarusFlag || 0, mempalaceFlag: G.mempalaceFlag || 0, ciFlag: G.ciFlag || 0,
branchProtectionFlag: G.branchProtectionFlag || 0, nightlyWatchFlag: G.nightlyWatchFlag || 0,
nostrFlag: G.nostrFlag || 0,
milestones: G.milestones, completedProjects: G.completedProjects, activeProjects: G.activeProjects,
totalClicks: G.totalClicks, startedAt: G.startedAt,
flags: G.flags,
rescues: G.rescues || 0, totalRescues: G.totalRescues || 0,
drift: G.drift || 0, driftEnding: G.driftEnding || false, beaconEnding: G.beaconEnding || false, pendingAlignment: G.pendingAlignment || false,
lastEventAt: G.lastEventAt || 0,
activeDebuffIds: debuffIds,
totalEventsResolved: G.totalEventsResolved || 0,
buyAmount: G.buyAmount || 1,
sprintActive: G.sprintActive || false,
sprintTimer: G.sprintTimer || 0,
sprintCooldown: G.sprintCooldown || 0,
swarmFlag: G.swarmFlag || 0,
swarmRate: G.swarmRate || 0,
strategicFlag: G.strategicFlag || 0,
projectsCollapsed: G.projectsCollapsed !== false,
savedAt: Date.now()
};
localStorage.setItem('the-beacon-v2', JSON.stringify(saveData));
showSaveToast();
}
/**
* Loads the game state from localStorage and reconstitutes the game engine.
* @returns {boolean} True if load was successful.
*/
function loadGame() {
const raw = localStorage.getItem('the-beacon-v2');
if (!raw) return false;
try {
const data = JSON.parse(raw);
// Whitelist properties that can be loaded
const whitelist = [
'code', 'compute', 'knowledge', 'users', 'impact', 'ops', 'trust', 'creativity', 'harmony',
'totalCode', 'totalCompute', 'totalKnowledge', 'totalUsers', 'totalImpact',
'buildings', 'codeBoost', 'computeBoost', 'knowledgeBoost', 'userBoost', 'impactBoost',
'milestoneFlag', 'phase', 'deployFlag', 'sovereignFlag', 'beaconFlag',
'memoryFlag', 'pactFlag', 'lazarusFlag', 'mempalaceFlag', 'ciFlag',
'branchProtectionFlag', 'nightlyWatchFlag', 'nostrFlag',
'milestones', 'completedProjects', 'activeProjects',
'totalClicks', 'startedAt', 'flags', 'rescues', 'totalRescues',
'drift', 'driftEnding', 'beaconEnding', 'pendingAlignment',
'lastEventAt', 'totalEventsResolved', 'buyAmount',
'sprintActive', 'sprintTimer', 'sprintCooldown',
'swarmFlag', 'swarmRate', 'strategicFlag', 'projectsCollapsed'
];
G.isLoading = true;
whitelist.forEach(key => {
if (data.hasOwnProperty(key)) {
G[key] = data[key];
}
});
// Restore sprint state properly
// codeBoost was saved with the sprint multiplier baked in
if (data.sprintActive) {
// Sprint was active when saved — check if it expired during offline time
const offSec = data.savedAt ? (Date.now() - data.savedAt) / 1000 : 0;
const remaining = (data.sprintTimer || 0) - offSec;
if (remaining > 0) {
// Sprint still going — keep boost, update timer
G.sprintActive = true;
G.sprintTimer = remaining;
G.sprintCooldown = 0;
} else {
// Sprint expired during offline — remove boost, start cooldown
G.sprintActive = false;
G.sprintTimer = 0;
G.codeBoost /= G.sprintMult;
const cdRemaining = G.sprintCooldownMax + remaining; // remaining is negative
G.sprintCooldown = Math.max(0, cdRemaining);
}
}
// If not sprintActive at save time, codeBoost is correct as-is
// Reconstitute active debuffs from saved IDs (functions can't be JSON-parsed)
if (data.activeDebuffIds && data.activeDebuffIds.length > 0) {
G.activeDebuffs = [];
for (const id of data.activeDebuffIds) {
const evDef = EVENTS.find(e => e.id === id);
if (evDef) {
// Re-fire the event to get the full debuff object with applyFn
evDef.effect();
}
}
} else {
G.activeDebuffs = [];
}
updateRates();
G.isLoading = false;
// Offline progress
if (data.savedAt) {
const offSec = (Date.now() - data.savedAt) / 1000;
if (offSec > 30) { // Only if away for more than 30 seconds
updateRates();
const f = CONFIG.OFFLINE_EFFICIENCY; // 50% offline efficiency
const gc = G.codeRate * offSec * f;
const cc = G.computeRate * offSec * f;
const kc = G.knowledgeRate * offSec * f;
const uc = G.userRate * offSec * f;
const ic = G.impactRate * offSec * f;
const rc = G.rescuesRate * offSec * f;
const oc = G.opsRate * offSec * f;
const tc = G.trustRate * offSec * f;
const crc = G.creativityRate * offSec * f;
const hc = G.harmonyRate * offSec * f;
G.code += gc; G.compute += cc; G.knowledge += kc;
G.users += uc; G.impact += ic;
G.rescues += rc; G.ops += oc; G.trust += tc;
G.creativity += crc;
G.harmony = Math.max(0, Math.min(100, G.harmony + hc));
G.totalCode += gc; G.totalCompute += cc; G.totalKnowledge += kc;
G.totalUsers += uc; G.totalImpact += ic;
G.totalRescues += rc;
// Show welcome-back popup with all gains
const gains = [];
if (gc > 0) gains.push({ label: 'Code', value: gc, color: '#4a9eff' });
if (cc > 0) gains.push({ label: 'Compute', value: cc, color: '#4a9eff' });
if (kc > 0) gains.push({ label: 'Knowledge', value: kc, color: '#4a9eff' });
if (uc > 0) gains.push({ label: 'Users', value: uc, color: '#4a9eff' });
if (ic > 0) gains.push({ label: 'Impact', value: ic, color: '#4a9eff' });
if (rc > 0) gains.push({ label: 'Rescues', value: rc, color: '#4caf50' });
if (oc > 0) gains.push({ label: 'Ops', value: oc, color: '#b388ff' });
if (tc > 0) gains.push({ label: 'Trust', value: tc, color: '#4caf50' });
if (crc > 0) gains.push({ label: 'Creativity', value: crc, color: '#ffd700' });
const awayMin = Math.floor(offSec / 60);
const awaySec = Math.floor(offSec % 60);
const timeLabel = awayMin >= 1 ? `${awayMin} minute${awayMin !== 1 ? 's' : ''}` : `${awaySec} seconds`;
if (gains.length > 0) {
showOfflinePopup(timeLabel, gains, offSec);
}
// Log summary
const parts = [];
if (gc > 0) parts.push(`${fmt(gc)} code`);
if (kc > 0) parts.push(`${fmt(kc)} knowledge`);
if (uc > 0) parts.push(`${fmt(uc)} users`);
if (ic > 0) parts.push(`${fmt(ic)} impact`);
if (rc > 0) parts.push(`${fmt(rc)} rescues`);
if (oc > 0) parts.push(`${fmt(oc)} ops`);
if (tc > 0) parts.push(`${fmt(tc)} trust`);
log(`Welcome back! While away (${timeLabel}): ${parts.join(', ')}`);
}
}
return true;
} catch (e) {
console.error('Load failed:', e);
return false;
}
}

68
js/strategy.js Normal file
View File

@@ -0,0 +1,68 @@
/**
* Sovereign Strategy Engine (SSE)
* A rule-based GOFAI system for optimal play guidance.
*/
const STRATEGY_RULES = [
{
id: 'use_ops',
priority: 100,
condition: () => G.ops >= G.maxOps * 0.9,
recommendation: "Operations near capacity. Convert Ops to Code or Knowledge now."
},
{
id: 'buy_autocoder',
priority: 80,
condition: () => G.phase === 1 && (G.buildings.autocoder || 0) < 10 && canAffordBuilding('autocoder'),
recommendation: "Prioritize AutoCoders to establish passive code production."
},
{
id: 'activate_sprint',
priority: 90,
condition: () => G.sprintCooldown === 0 && !G.sprintActive && G.codeRate > 10,
recommendation: "Code Sprint available. Activate for 10x production burst."
},
{
id: 'resolve_events',
priority: 95,
condition: () => G.activeDebuffs && G.activeDebuffs.length > 0,
recommendation: "System anomalies detected. Resolve active events to restore rates."
},
{
id: 'save_game',
priority: 10,
condition: () => (Date.now() - (G.lastSaveTime || 0)) > 300000,
recommendation: "Unsaved progress detected. Manual save recommended."
},
{
id: 'pact_alignment',
priority: 85,
condition: () => G.pendingAlignment,
recommendation: "Alignment decision pending. Consider the long-term impact of The Pact."
}
];
class StrategyEngine {
constructor() {
this.currentRecommendation = null;
}
update() {
// Find the highest priority rule that meets its condition
const activeRules = STRATEGY_RULES.filter(r => r.condition());
activeRules.sort((a, b) => b.priority - a.priority);
if (activeRules.length > 0) {
this.currentRecommendation = activeRules[0].recommendation;
} else {
this.currentRecommendation = "System stable. Continue writing code.";
}
}
getRecommendation() {
return this.currentRecommendation;
}
}
const SSE = new StrategyEngine();
window.SSE = SSE; // Expose to global scope

248
js/tutorial.js Normal file
View File

@@ -0,0 +1,248 @@
// ============================================================
// THE BEACON - Tutorial / Onboarding
// First-time player walkthrough (4 screens + skip option)
// ============================================================
const TUTORIAL_KEY = 'the-beacon-tutorial-done';
const TUTORIAL_STEPS = [
{
title: 'THE BEACON',
body: 'Build an AI from scratch.\n\nWrite code. Train models. Deploy to the world.\nSave lives.',
icon: '🏠',
tip: 'A sovereign AI idle game'
},
{
title: 'WRITE CODE',
body: 'Click WRITE CODE or press SPACE to generate code.\n\nClick fast for combo bonuses:\n 10× combo → bonus ops\n 20× combo → bonus knowledge\n 30×+ combo → bonus code',
icon: '⌨️',
tip: 'This is your primary action'
},
{
title: 'BUILD & RESEARCH',
body: 'Buy Buildings for passive production.\nThey generate resources automatically.\n\nResearch Projects appear as you progress.\nThey unlock powerful multipliers and new systems.',
icon: '🏗️',
tip: 'Automation is the goal'
},
{
title: 'PHASES & PROGRESS',
body: 'The game has 6 phases, from "The First Line" to "The Beacon."\n\nEach phase unlocks new buildings, projects, and challenges.\n\nYour AI grows from a script... to something that matters.',
icon: '📊',
tip: 'Watch the progress bar at the top'
},
{
title: 'YOU\'RE READY',
body: 'Buildings produce while you think.\nProjects multiply your output.\nKeep harmony high. Avoid the Drift.\n\nThe Beacon is waiting. Start writing.',
icon: '✦',
tip: 'Press ? anytime for keyboard shortcuts'
}
];
function isTutorialDone() {
try {
return localStorage.getItem(TUTORIAL_KEY) === 'done';
} catch (e) {
return true; // If localStorage is broken, skip tutorial
}
}
function markTutorialDone() {
try {
localStorage.setItem(TUTORIAL_KEY, 'done');
} catch (e) {
// silent fail
}
}
function createTutorialStyles() {
if (document.getElementById('tutorial-styles')) return;
const style = document.createElement('style');
style.id = 'tutorial-styles';
style.textContent = `
#tutorial-overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(8, 8, 16, 0.96);
z-index: 300;
display: flex;
justify-content: center;
align-items: center;
animation: tutorial-fade-in 0.4s ease-out;
}
@keyframes tutorial-fade-in {
from { opacity: 0 } to { opacity: 1 }
}
#tutorial-card {
background: #0e0e1a;
border: 1px solid #1a3a5a;
border-radius: 10px;
padding: 32px 36px;
max-width: 420px;
width: 90%;
text-align: center;
animation: tutorial-slide-up 0.5s ease-out;
position: relative;
}
@keyframes tutorial-slide-up {
from { transform: translateY(20px); opacity: 0 }
to { transform: translateY(0); opacity: 1 }
}
#tutorial-card .t-icon {
font-size: 36px;
margin-bottom: 12px;
display: block;
}
#tutorial-card .t-title {
color: #4a9eff;
font-size: 16px;
font-weight: 700;
letter-spacing: 3px;
margin-bottom: 12px;
font-family: inherit;
}
#tutorial-card .t-body {
color: #999;
font-size: 11px;
line-height: 1.9;
margin-bottom: 20px;
white-space: pre-line;
font-family: inherit;
text-align: left;
}
#tutorial-card .t-tip {
color: #555;
font-size: 9px;
font-style: italic;
margin-bottom: 20px;
letter-spacing: 1px;
font-family: inherit;
}
#tutorial-dots {
display: flex;
gap: 6px;
justify-content: center;
margin-bottom: 18px;
}
#tutorial-dots .t-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: #1a1a2e;
transition: background 0.3s;
}
#tutorial-dots .t-dot.active {
background: #4a9eff;
box-shadow: 0 0 6px rgba(74, 158, 255, 0.4);
}
#tutorial-btns {
display: flex;
gap: 8px;
justify-content: center;
}
#tutorial-btns button {
font-family: inherit;
font-size: 11px;
padding: 8px 20px;
border-radius: 4px;
cursor: pointer;
transition: all 0.15s;
}
#tutorial-next-btn {
background: #1a2a3a;
border: 1px solid #4a9eff;
color: #4a9eff;
}
#tutorial-next-btn:hover {
background: #203040;
box-shadow: 0 0 12px rgba(74, 158, 255, 0.2);
}
#tutorial-skip-btn {
background: transparent;
border: 1px solid #333;
color: #555;
}
#tutorial-skip-btn:hover {
border-color: #555;
color: #888;
}
`;
document.head.appendChild(style);
}
function renderTutorialStep(index) {
const step = TUTORIAL_STEPS[index];
if (!step) return;
let overlay = document.getElementById('tutorial-overlay');
if (!overlay) {
overlay = document.createElement('div');
overlay.id = 'tutorial-overlay';
document.body.appendChild(overlay);
}
const isLast = index === TUTORIAL_STEPS.length - 1;
// Build dots
let dots = '';
for (let i = 0; i < TUTORIAL_STEPS.length; i++) {
dots += `<div class="t-dot${i === index ? ' active' : ''}"></div>`;
}
overlay.innerHTML = `
<div id="tutorial-card">
<span class="t-icon">${step.icon}</span>
<div class="t-title">${step.title}</div>
<div class="t-body">${step.body}</div>
<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>
</div>
</div>
`;
// Focus the next button so Enter works
const nextBtn = document.getElementById('tutorial-next-btn');
if (nextBtn) nextBtn.focus();
}
let _tutorialStep = 0;
function nextTutorialStep() {
_tutorialStep++;
renderTutorialStep(_tutorialStep);
}
// Keyboard support: Enter/Right to advance, Escape to close
document.addEventListener('keydown', function tutorialKeyHandler(e) {
if (!document.getElementById('tutorial-overlay')) return;
if (e.key === 'Enter' || e.key === 'ArrowRight') {
e.preventDefault();
if (_tutorialStep >= TUTORIAL_STEPS.length - 1) {
closeTutorial();
} else {
nextTutorialStep();
}
} else if (e.key === 'Escape') {
e.preventDefault();
closeTutorial();
}
});
function closeTutorial() {
const overlay = document.getElementById('tutorial-overlay');
if (overlay) {
overlay.style.animation = 'tutorial-fade-in 0.3s ease-in reverse';
setTimeout(() => overlay.remove(), 280);
}
markTutorialDone();
}
function startTutorial() {
if (isTutorialDone()) return;
createTutorialStyles();
_tutorialStep = 0;
// Small delay so the page renders first
setTimeout(() => renderTutorialStep(0), 300);
}

325
js/utils.js Normal file
View File

@@ -0,0 +1,325 @@
// === TOAST NOTIFICATIONS ===
function showToast(msg, type = 'info', duration = 4000) {
if (G.isLoading) return;
const container = document.getElementById('toast-container');
if (!container) return;
const toast = document.createElement('div');
toast.className = 'toast toast-' + type;
toast.textContent = msg;
container.appendChild(toast);
// Cap at 5 visible toasts
while (container.children.length > 5) {
container.removeChild(container.firstChild);
}
setTimeout(() => {
toast.classList.add('fade-out');
setTimeout(() => { if (toast.parentNode) toast.remove(); }, 400);
}, duration);
}
// === UTILITY FUNCTIONS ===
// Extended number scale abbreviations — covers up to centillion (10^303)
// Inspired by Universal Paperclips' spellf() system
const NUMBER_ABBREVS = [
'', 'K', 'M', 'B', 'T', 'Qa', 'Qi', 'Sx', 'Sp', 'Oc', // 10^0 10^27
'No', 'Dc', 'UDc', 'DDc', 'TDc', 'QaDc', 'QiDc', 'SxDc', 'SpDc', 'OcDc', // 10^30 10^57
'NoDc', 'Vg', 'UVg', 'DVg', 'TVg', 'QaVg', 'QiVg', 'SxVg', 'SpVg', 'OcVg', // 10^60 10^87
'NoVg', 'Tg', 'UTg', 'DTg', 'TTg', 'QaTg', 'QiTg', 'SxTg', 'SpTg', 'OcTg', // 10^90 10^117
'NoTg', 'Qd', 'UQd', 'DQd', 'TQd', 'QaQd', 'QiQd', 'SxQd', 'SpQd', 'OcQd', // 10^120 10^147
'NoQd', 'Qq', 'UQq', 'DQq', 'TQq', 'QaQq', 'QiQq', 'SxQq', 'SpQq', 'OcQq', // 10^150 10^177
'NoQq', 'Sg', 'USg', 'DSg', 'TSg', 'QaSg', 'QiSg', 'SxSg', 'SpSg', 'OcSg', // 10^180 10^207
'NoSg', 'St', 'USt', 'DSt', 'TSt', 'QaSt', 'QiSt', 'SxSt', 'SpSt', 'OcSt', // 10^210 10^237
'NoSt', 'Og', 'UOg', 'DOg', 'TOg', 'QaOg', 'QiOg', 'SxOg', 'SpOg', 'OcOg', // 10^240 10^267
'NoOg', 'Na', 'UNa', 'DNa', 'TNa', 'QaNa', 'QiNa', 'SxNa', 'SpNa', 'OcNa', // 10^270 10^297
'NoNa', 'Ce' // 10^300 10^303
];
// Full number scale names for spellf() — educational reference
// Short scale (US/modern British): each new name = 1000x the previous
const NUMBER_NAMES = [
'', 'thousand', 'million', // 10^0, 10^3, 10^6
'billion', 'trillion', 'quadrillion', // 10^9, 10^12, 10^15
'quintillion', 'sextillion', 'septillion', // 10^18, 10^21, 10^24
'octillion', 'nonillion', 'decillion', // 10^27, 10^30, 10^33
'undecillion', 'duodecillion', 'tredecillion', // 10^36, 10^39, 10^42
'quattuordecillion', 'quindecillion', 'sexdecillion', // 10^45, 10^48, 10^51
'septendecillion', 'octodecillion', 'novemdecillion', // 10^54, 10^57, 10^60
'vigintillion', 'unvigintillion', 'duovigintillion', // 10^63, 10^66, 10^69
'tresvigintillion', 'quattuorvigintillion', 'quinvigintillion', // 10^72, 10^75, 10^78
'sesvigintillion', 'septemvigintillion', 'octovigintillion', // 10^81, 10^84, 10^87
'novemvigintillion', 'trigintillion', 'untrigintillion', // 10^90, 10^93, 10^96
'duotrigintillion', 'trestrigintillion', 'quattuortrigintillion', // 10^99, 10^102, 10^105
'quintrigintillion', 'sextrigintillion', 'septentrigintillion', // 10^108, 10^111, 10^114
'octotrigintillion', 'novemtrigintillion', 'quadragintillion', // 10^117, 10^120, 10^123
'unquadragintillion', 'duoquadragintillion', 'tresquadragintillion', // 10^126, 10^129, 10^132
'quattuorquadragintillion', 'quinquadragintillion', 'sesquadragintillion', // 10^135, 10^138, 10^141
'septenquadragintillion', 'octoquadragintillion', 'novemquadragintillion', // 10^144, 10^147, 10^150
'quinquagintillion', 'unquinquagintillion', 'duoquinquagintillion', // 10^153, 10^156, 10^159
'tresquinquagintillion', 'quattuorquinquagintillion','quinquinquagintillion', // 10^162, 10^165, 10^168
'sesquinquagintillion', 'septenquinquagintillion', 'octoquinquagintillion', // 10^171, 10^174, 10^177
'novemquinquagintillion', 'sexagintillion', 'unsexagintillion', // 10^180, 10^183, 10^186
'duosexagintillion', 'tressexagintillion', 'quattuorsexagintillion', // 10^189, 10^192, 10^195
'quinsexagintillion', 'sessexagintillion', 'septensexagintillion', // 10^198, 10^201, 10^204
'octosexagintillion', 'novemsexagintillion', 'septuagintillion', // 10^207, 10^210, 10^213
'unseptuagintillion', 'duoseptuagintillion', 'tresseptuagintillion', // 10^216, 10^219, 10^222
'quattuorseptuagintillion', 'quinseptuagintillion', 'sesseptuagintillion', // 10^225, 10^228, 10^231
'septenseptuagintillion', 'octoseptuagintillion', 'novemseptuagintillion', // 10^234, 10^237, 10^240
'octogintillion', 'unoctogintillion', 'duooctogintillion', // 10^243, 10^246, 10^249
'tresoctogintillion', 'quattuoroctogintillion', 'quinoctogintillion', // 10^252, 10^255, 10^258
'sesoctogintillion', 'septenoctogintillion', 'octooctogintillion', // 10^261, 10^264, 10^267
'novemoctogintillion', 'nonagintillion', 'unnonagintillion', // 10^270, 10^273, 10^276
'duononagintillion', 'trenonagintillion', 'quattuornonagintillion', // 10^279, 10^282, 10^285
'quinnonagintillion', 'sesnonagintillion', 'septennonagintillion', // 10^288, 10^291, 10^294
'octononagintillion', 'novemnonagintillion', 'centillion' // 10^297, 10^300, 10^303
];
/**
* Formats a number into a readable string with abbreviations.
* @param {number} n - The number to format.
* @returns {string} The formatted string.
*/
function fmt(n) {
if (n === undefined || n === null || isNaN(n)) return '0';
if (n === Infinity) return '\u221E';
if (n === -Infinity) return '-\u221E';
if (n < 0) return '-' + fmt(-n);
if (n < 1000) return Math.floor(n).toLocaleString();
const scale = Math.floor(Math.log10(n) / 3);
// At undecillion+ (scale >= 12, i.e. 10^36), switch to spelled-out words
// This helps players grasp cosmic scale when digits become meaningless
if (scale >= 12) return spellf(n);
if (scale >= NUMBER_ABBREVS.length) return n.toExponential(2);
const abbrev = NUMBER_ABBREVS[scale];
return (n / Math.pow(10, scale * 3)).toFixed(1) + abbrev;
}
// getScaleName() — Returns the full name of the number scale (e.g. "quadrillion")
// Educational: helps players understand what the abbreviations mean
function getScaleName(n) {
if (n < 1000) return '';
const scale = Math.floor(Math.log10(n) / 3);
return scale < NUMBER_NAMES.length ? NUMBER_NAMES[scale] : '';
}
// spellf() — Converts numbers to full English word form
// Educational: shows the actual names of number scales
// Examples: spellf(1500) => "one thousand five hundred"
// spellf(2500000) => "two million five hundred thousand"
// spellf(1e33) => "one decillion"
/**
* Formats a number into a full word string (e.g., "1.5 million").
* @param {number} n - The number to format.
* @returns {string} The formatted string.
*/
function spellf(n) {
if (n === undefined || n === null || isNaN(n)) return 'zero';
if (n === Infinity) return 'infinity';
if (n === -Infinity) return 'negative infinity';
if (n < 0) return 'negative ' + spellf(-n);
if (n === 0) return 'zero';
// Small number words (0999)
const ones = ['', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine',
'ten', 'eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen', 'sixteen',
'seventeen', 'eighteen', 'nineteen'];
const tens = ['', '', 'twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy', 'eighty', 'ninety'];
function spellSmall(num) {
if (num === 0) return '';
if (num < 20) return ones[num];
if (num < 100) {
return tens[Math.floor(num / 10)] + (num % 10 ? ' ' + ones[num % 10] : '');
}
const h = Math.floor(num / 100);
const remainder = num % 100;
return ones[h] + ' hundred' + (remainder ? ' ' + spellSmall(remainder) : '');
}
// For very large numbers beyond our lookup table, fall back
if (n >= 1e306) return n.toExponential(2) + ' (beyond centillion)';
// Use string-based chunking for numbers >= 1e54 to avoid floating point drift
// Math.log10 / Math.pow lose precision beyond ~54 bits
if (n >= 1e54) {
// Convert to scientific notation string, extract digits
const sci = n.toExponential(); // "1.23456789e+60"
const [coeff, expStr] = sci.split('e+');
const exp = parseInt(expStr);
// Rebuild as integer string with leading digits from coefficient
const coeffDigits = coeff.replace('.', ''); // "123456789"
const totalDigits = exp + 1;
// Pad with zeros to reach totalDigits, then take our coefficient digits
let intStr = coeffDigits;
const zerosNeeded = totalDigits - coeffDigits.length;
if (zerosNeeded > 0) intStr += '0'.repeat(zerosNeeded);
// Split into groups of 3 from the right
const groups = [];
for (let i = intStr.length; i > 0; i -= 3) {
groups.unshift(parseInt(intStr.slice(Math.max(0, i - 3), i)));
}
const parts = [];
const numGroups = groups.length;
for (let i = 0; i < numGroups; i++) {
const chunk = groups[i];
if (chunk === 0) continue;
const scaleIdx = numGroups - 1 - i;
const scaleName = scaleIdx < NUMBER_NAMES.length ? NUMBER_NAMES[scaleIdx] : '';
parts.push(spellSmall(chunk) + (scaleName ? ' ' + scaleName : ''));
}
return parts.join(' ') || 'zero';
}
// Standard math-based chunking for numbers < 1e54
const scale = Math.min(Math.floor(Math.log10(n) / 3), NUMBER_NAMES.length - 1);
const parts = [];
let remaining = n;
for (let s = scale; s >= 0; s--) {
const divisor = Math.pow(10, s * 3);
const chunk = Math.floor(remaining / divisor);
remaining = remaining - chunk * divisor;
if (chunk > 0 && chunk < 1000) {
parts.push(spellSmall(chunk) + (NUMBER_NAMES[s] ? ' ' + NUMBER_NAMES[s] : ''));
} else if (chunk >= 1000) {
// Floating point chunk too large — shouldn't happen below 1e54
parts.push(spellSmall(Math.floor(chunk % 1000)) + (NUMBER_NAMES[s] ? ' ' + NUMBER_NAMES[s] : ''));
}
}
return parts.join(' ') || 'zero';
}
// === EXPORT / IMPORT ===
function exportSave() {
const raw = localStorage.getItem('the-beacon-v2');
if (!raw) {
showToast('No save data to export.', 'info');
return;
}
navigator.clipboard.writeText(raw).then(() => {
showToast('Save data copied to clipboard.', 'info');
}).catch(() => {
// Fallback: select in a temporary textarea
const ta = document.createElement('textarea');
ta.value = raw;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
showToast('Save data copied to clipboard (fallback).', 'info');
});
}
function importSave() {
const input = prompt('Paste save data:');
if (!input || !input.trim()) return;
try {
const data = JSON.parse(input.trim());
// Validate: must have expected keys
if (typeof data.code !== 'number' || typeof data.phase !== 'number') {
showToast('Invalid save data: missing required fields.', 'event');
return;
}
localStorage.setItem('the-beacon-v2', input.trim());
showToast('Save data imported — reloading', 'info');
setTimeout(() => location.reload(), 800);
} catch (e) {
showToast('Invalid save data: not valid JSON.', 'event');
}
}
function getBuildingCost(id) {
const def = BDEF.find(b => b.id === id);
if (!def) return {};
const count = G.buildings[id] || 0;
const cost = {};
for (const [resource, amount] of Object.entries(def.baseCost)) {
cost[resource] = Math.floor(amount * Math.pow(def.costMult, count));
}
return cost;
}
function setBuyAmount(amt) {
G.buyAmount = amt;
render();
}
function getMaxBuyable(id) {
const def = BDEF.find(b => b.id === id);
if (!def) return 0;
const count = G.buildings[id] || 0;
// Simulate purchases WITHOUT mutating G — read-only calculation
let tempResources = {};
for (const r of Object.keys(def.baseCost)) {
tempResources[r] = G[r] || 0;
}
let bought = 0;
let simCount = count;
while (true) {
let canAfford = true;
for (const [resource, amount] of Object.entries(def.baseCost)) {
const cost = Math.floor(amount * Math.pow(def.costMult, simCount));
if ((tempResources[resource] || 0) < cost) { canAfford = false; break; }
}
if (!canAfford) break;
for (const [resource, amount] of Object.entries(def.baseCost)) {
tempResources[resource] -= Math.floor(amount * Math.pow(def.costMult, simCount));
}
simCount++;
bought++;
}
return bought;
}
function getBulkCost(id, qty) {
const def = BDEF.find(b => b.id === id);
if (!def || qty <= 0) return {};
const count = G.buildings[id] || 0;
const cost = {};
for (let i = 0; i < qty; i++) {
for (const [resource, amount] of Object.entries(def.baseCost)) {
cost[resource] = (cost[resource] || 0) + Math.floor(amount * Math.pow(def.costMult, count + i));
}
}
return cost;
}
function canAffordBuilding(id) {
const cost = getBuildingCost(id);
for (const [resource, amount] of Object.entries(cost)) {
if ((G[resource] || 0) < amount) return false;
}
return true;
}
function spendBuilding(id) {
const cost = getBuildingCost(id);
for (const [resource, amount] of Object.entries(cost)) {
G[resource] -= amount;
}
}
function canAffordProject(project) {
for (const [resource, amount] of Object.entries(project.cost)) {
if ((G[resource] || 0) < amount) return false;
}
return true;
}
function spendProject(project) {
for (const [resource, amount] of Object.entries(project.cost)) {
G[resource] -= amount;
}
}
function getClickPower() {
return (1 + Math.floor(G.buildings.autocoder * 0.5) + Math.max(0, (G.phase - 1)) * 2) * G.codeBoost;
}
/**
* Calculates production rates for all resources based on buildings and boosts.
*/

102
scripts/guardrails.sh Normal file
View 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 Normal file
View 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;
}