Compare commits
48 Commits
feature/sa
...
beacon/pol
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b142d485e | ||
| 1cb556aa3d | |||
| 5bb48c8f58 | |||
| 4964eb01a9 | |||
| 20d74afc03 | |||
| 703fbeb4fa | |||
| 9545b5cb6f | |||
| 74aa30819a | |||
| 1b41ce740f | |||
| e8d5337271 | |||
| 2b59be997d | |||
|
|
970f3be00f | ||
|
|
302f6c844d | ||
| 26879de76e | |||
| c197fabc69 | |||
| 9733b9022e | |||
| 967025fbd4 | |||
|
|
9854501bbd | ||
|
|
68ee64866a | ||
| be0264fc95 | |||
|
|
e6d0df40b4 | ||
|
|
23dd95ed46 | ||
|
|
0849754a87 | ||
|
|
8d51349e64 | ||
|
|
24940fe465 | ||
|
|
16273a5a15 | ||
|
|
5d51e14875 | ||
|
|
5fc0ad7b22 | ||
| f948ec9c5e | |||
|
|
9403f700d2 | ||
|
|
13e77a12f2 | ||
| 6081844387 | |||
|
|
09b8c02307 | ||
|
|
9106d3f84c | ||
| 3f02359748 | |||
| 85a146b690 | |||
| cb2e48bf9a | |||
|
|
8d43b5c911 | ||
|
|
8cdabe9771 | ||
|
|
5c88fe77be | ||
|
|
931473e8f8 | ||
|
|
fe76150325 | ||
|
|
a3f1802473 | ||
|
|
3d414b2de6 | ||
|
|
612eb1f4d5 | ||
| 1a7db021c8 | |||
| 2a12c5210d | |||
|
|
d467348820 |
27
.gitea/workflows/a11y.yml
Normal file
27
.gitea/workflows/a11y.yml
Normal file
@@ -0,0 +1,27 @@
|
||||
name: Accessibility Checks
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
a11y-audit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Validate ARIA Attributes in game.js
|
||||
run: |
|
||||
echo "Checking game.js for ARIA attributes..."
|
||||
grep -q "aria-label" game.js || (echo "ERROR: aria-label missing from game.js" && exit 1)
|
||||
grep -q "aria-valuenow" game.js || (echo "ERROR: aria-valuenow missing from game.js" && exit 1)
|
||||
grep -q "aria-pressed" game.js || (echo "ERROR: aria-pressed missing from game.js" && exit 1)
|
||||
|
||||
- name: Validate ARIA Roles in index.html
|
||||
run: |
|
||||
echo "Checking index.html for ARIA roles..."
|
||||
grep -q "role=" index.html || (echo "ERROR: No ARIA roles found in index.html" && exit 1)
|
||||
|
||||
- name: Syntax Check JS
|
||||
run: |
|
||||
node -c game.js
|
||||
24
.gitea/workflows/smoke.yml
Normal file
24
.gitea/workflows/smoke.yml
Normal file
@@ -0,0 +1,24 @@
|
||||
name: Smoke Test
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main]
|
||||
jobs:
|
||||
smoke:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- name: Parse check
|
||||
run: |
|
||||
find . -name '*.yml' -o -name '*.yaml' | grep -v .gitea | xargs -r python3 -c "import sys,yaml; [yaml.safe_load(open(f)) for f in sys.argv[1:]]"
|
||||
find . -name '*.json' | xargs -r python3 -m json.tool > /dev/null
|
||||
find . -name '*.py' | xargs -r python3 -m py_compile
|
||||
find . -name '*.sh' | xargs -r bash -n
|
||||
echo "PASS: All files parse"
|
||||
- name: Secret scan
|
||||
run: |
|
||||
if grep -rE 'sk-or-|sk-ant-|ghp_|AKIA' . --include='*.yml' --include='*.py' --include='*.sh' 2>/dev/null | grep -v .gitea; then exit 1; fi
|
||||
echo "PASS: No secrets"
|
||||
133
index.html
133
index.html
@@ -3,6 +3,23 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="The Beacon — a sovereign AI idle game. Build an AI from scratch. Write code, train models, save lives.">
|
||||
<meta name="theme-color" content="#0a0a0a">
|
||||
<meta name="author" content="Timmy Foundation">
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:title" content="The Beacon">
|
||||
<meta property="og:description" content="A sovereign AI idle game. Build an AI from scratch. Write code, train models, save lives.">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:image" content="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🏠</text></svg>">
|
||||
|
||||
<!-- Twitter Card -->
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:title" content="The Beacon">
|
||||
<meta name="twitter:description" content="A sovereign AI idle game. Build an AI from scratch. Write code, train models, save lives.">
|
||||
|
||||
<!-- Favicon (inline SVG beacon) -->
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🏠</text></svg>">
|
||||
<title>The Beacon - Build Sovereign AI</title>
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
@@ -14,6 +31,15 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code
|
||||
#phase-bar{text-align:center;padding:10px;margin:12px 16px;background:var(--panel);border:1px solid var(--border);border-radius:6px}
|
||||
#phase-bar .phase-name{font-size:14px;font-weight:700;color:var(--gold);letter-spacing:2px}
|
||||
#phase-bar .phase-desc{font-size:10px;color:var(--dim);margin-top:4px;font-style:italic}
|
||||
.progress-wrap{margin-top:8px;height:6px;background:#111;border-radius:3px;overflow:hidden;position:relative}
|
||||
.progress-fill{height:100%;border-radius:3px;transition:width 0.5s ease;background:linear-gradient(90deg,#1a3a5a,var(--accent))}
|
||||
.progress-label{font-size:9px;color:var(--dim);margin-top:4px;display:flex;justify-content:space-between}
|
||||
.milestone-row{display:flex;gap:6px;margin-top:6px;justify-content:center;flex-wrap:wrap}
|
||||
.milestone-chip{font-size:9px;padding:2px 8px;border-radius:10px;border:1px solid var(--border);color:var(--dim);background:#0a0a14}
|
||||
.milestone-chip.next{border-color:var(--accent);color:var(--accent);animation:pulse-chip 2s ease-in-out infinite}
|
||||
.milestone-chip.done{border-color:#2a4a2a;color:var(--green);opacity:0.6}
|
||||
@keyframes pulse-chip{0%,100%{box-shadow:0 0 0 rgba(74,158,255,0)}50%{box-shadow:0 0 8px rgba(74,158,255,0.3)}}
|
||||
@keyframes beacon-glow{0%,100%{opacity:0.7}50%{opacity:1}}
|
||||
#resources{display:grid;grid-template-columns:repeat(auto-fit,minmax(100px,1fr));gap:6px;margin:12px 16px}
|
||||
.res{background:var(--panel);border:1px solid var(--border);border-radius:4px;padding:8px 10px;text-align:center}
|
||||
.res .r-label{font-size:9px;color:var(--dim);text-transform:uppercase;letter-spacing:1px}
|
||||
@@ -28,6 +54,8 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code
|
||||
.main-btn{background:linear-gradient(135deg,#1a2a3a,#0e1520);border:1px solid var(--accent);color:var(--accent);font-size:14px;padding:14px 10px;border-radius:4px;cursor:pointer;font-family:inherit;transition:all 0.2s}
|
||||
.main-btn:hover{background:linear-gradient(135deg,#203040,#0e2030);box-shadow:0 0 20px var(--glow);transform:scale(1.02)}
|
||||
.main-btn:active{transform:scale(0.98)}
|
||||
@keyframes pulse-glow{0%,100%{box-shadow:0 0 10px rgba(74,158,255,0.1)}50%{box-shadow:0 0 25px rgba(74,158,255,0.4)}}
|
||||
.main-btn.pulse{animation:pulse-glow 2s ease-in-out infinite}
|
||||
.ops-btn{background:#1a1a2a;border:1px solid var(--purple);color:var(--purple);font-size:10px;padding:6px 10px;border-radius:4px;cursor:pointer;font-family:inherit;transition:all 0.15s}
|
||||
.ops-btn:hover:not(:disabled){background:#2a2a3a;border-color:var(--gold)}
|
||||
.ops-btn:disabled{opacity:0.3;cursor:not-allowed}
|
||||
@@ -58,19 +86,35 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code
|
||||
#drift-ending .ending-quote{color:var(--dim);font-style:italic;font-size:11px;border-left:2px solid #f44336;padding-left:12px;margin:20px 0;text-align:left}
|
||||
#drift-ending button{margin-top:20px;background:#1a0808;border:1px solid #f44336;color:#f44336;padding:10px 24px;border-radius:4px;cursor:pointer;font-family:inherit;font-size:11px}
|
||||
#drift-ending button:hover{background:#2a1010}
|
||||
#toast-container{position:fixed;top:16px;right:16px;z-index:200;display:flex;flex-direction:column;gap:6px;pointer-events:none;max-width:320px}
|
||||
.toast{pointer-events:auto;padding:8px 14px;border-radius:6px;font-size:11px;font-family:inherit;line-height:1.4;animation:toast-in 0.3s ease-out;opacity:0.95;border:1px solid;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px)}
|
||||
.toast.fade-out{animation:toast-out 0.4s ease-in forwards}
|
||||
.toast-event{background:rgba(244,67,54,0.12);border-color:#f44336;color:#ff8a80}
|
||||
.toast-project{background:rgba(255,215,0,0.12);border-color:#ffd700;color:#ffd700}
|
||||
.toast-milestone{background:rgba(76,175,80,0.12);border-color:#4caf50;color:#81c784}
|
||||
.toast-info{background:rgba(74,158,255,0.12);border-color:#4a9eff;color:#80bfff}
|
||||
@keyframes toast-in{from{transform:translateX(40px);opacity:0}to{transform:translateX(0);opacity:0.95}}
|
||||
@keyframes toast-out{from{opacity:0.95;transform:translateX(0)}to{opacity:0;transform:translateX(40px)}}
|
||||
::-webkit-scrollbar{width:4px}::-webkit-scrollbar-track{background:var(--bg)}::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="header">
|
||||
<div id="pulse-container" style="position:relative;display:inline-block;margin-bottom:4px">
|
||||
<div id="pulse-dot" style="width:8px;height:8px;border-radius:50%;background:#333;display:inline-block;vertical-align:middle;transition:background 0.5s,box-shadow 0.5s"></div>
|
||||
<span id="pulse-label" style="font-size:9px;color:#444;margin-left:6px;vertical-align:middle;letter-spacing:1px">OFFLINE</span>
|
||||
</div>
|
||||
<h1>THE BEACON</h1>
|
||||
<div class="sub">A Sovereign AI Idle Game</div>
|
||||
</div>
|
||||
<div id="phase-bar">
|
||||
<div class="phase-name" id="phase-name">PHASE 1: THE FIRST LINE</div>
|
||||
<div class="phase-desc" id="phase-desc">Write code. Automate. Build the foundation.</div>
|
||||
<div class="progress-wrap"><div class="progress-fill" id="phase-progress" style="width:0%"></div></div>
|
||||
<div class="progress-label"><span id="phase-progress-label">0%</span><span id="phase-progress-target">Next: Phase 2 (2,000 code)</span></div>
|
||||
<div class="milestone-row" id="milestone-chips"></div>
|
||||
</div>
|
||||
<div id="resources">
|
||||
<div id="resources" role="region" aria-label="Resources" aria-live="polite">
|
||||
<div class="res"><div class="r-label">Code</div><div class="r-val" id="r-code">0</div><div class="r-rate" id="r-code-rate">+0/s</div></div>
|
||||
<div class="res"><div class="r-label">Compute</div><div class="r-val" id="r-compute">0</div><div class="r-rate" id="r-compute-rate">+0/s</div></div>
|
||||
<div class="res"><div class="r-label">Knowledge</div><div class="r-val" id="r-knowledge">0</div><div class="r-rate" id="r-knowledge-rate">+0/s</div></div>
|
||||
@@ -83,24 +127,35 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code
|
||||
<div class="res"><div class="r-label">Harmony</div><div class="r-val" id="r-harmony">50</div><div class="r-rate" id="r-harmony-rate">+0/s</div></div>
|
||||
</div>
|
||||
<div id="main">
|
||||
<div class="panel" id="action-panel">
|
||||
<div class="panel" id="action-panel" role="region" aria-label="Actions">
|
||||
<h2>ACTIONS</h2>
|
||||
<div class="action-btn-group"><button class="main-btn" onclick="writeCode()">WRITE CODE</button></div>
|
||||
<div class="action-btn-group"><button class="main-btn" onclick="writeCode()" aria-label="Write code, generates code resource">WRITE CODE</button></div>
|
||||
<div id="combo-display" role="status" aria-live="polite" style="text-align:center;font-size:10px;color:var(--dim);height:14px;margin-bottom:4px;transition:all 0.2s"></div>
|
||||
<div id="debuffs" style="display:none;margin-top:8px"></div>
|
||||
<div class="action-btn-group">
|
||||
<button class="ops-btn" onclick="doOps('boost_code')">Ops -> Code</button>
|
||||
<button class="ops-btn" onclick="doOps('boost_compute')">Ops -> Compute</button>
|
||||
<button class="ops-btn" onclick="doOps('boost_code')" aria-label="Convert 1 ops to code boost">Ops -> Code</button>
|
||||
<button class="ops-btn" onclick="doOps('boost_compute')" aria-label="Convert 1 ops to compute boost">Ops -> Compute</button>
|
||||
</div>
|
||||
<div class="action-btn-group">
|
||||
<button class="ops-btn" onclick="doOps('boost_knowledge')">Ops -> Knowledge</button>
|
||||
<button class="ops-btn" onclick="doOps('boost_trust')">Ops -> Trust</button>
|
||||
<button class="ops-btn" onclick="doOps('boost_knowledge')" aria-label="Convert 1 ops to knowledge boost">Ops -> Knowledge</button>
|
||||
<button class="ops-btn" onclick="doOps('boost_trust')" aria-label="Convert 1 ops to trust boost">Ops -> Trust</button>
|
||||
</div>
|
||||
<div id="sprint-container" style="display:none;margin-top:6px">
|
||||
<button id="sprint-btn" class="main-btn" onclick="activateSprint()" aria-label="Activate code sprint, 10x code production for 10 seconds" style="font-size:11px;padding:8px 10px;border-color:#ffd700;color:#ffd700;width:100%">⚡ CODE SPRINT — 10x Code for 10s</button>
|
||||
<div id="sprint-bar-wrap" style="display:none;margin-top:4px;height:4px;background:#111;border-radius:2px;overflow:hidden"><div id="sprint-bar" style="height:100%;background:linear-gradient(90deg,#ffd700,#ff8c00);border-radius:2px;transition:width 0.1s"></div></div>
|
||||
<div id="sprint-label" style="font-size:9px;color:#666;margin-top:2px;text-align:center"></div>
|
||||
</div>
|
||||
<div id="alignment-ui" style="display:none"></div>
|
||||
<button class="save-btn" onclick="saveGame()">Save Game</button>
|
||||
<button class="reset-btn" onclick="if(confirm('Reset all progress?')){localStorage.removeItem('the-beacon-v2');location.reload()}">Reset Progress</button>
|
||||
<button class="save-btn" onclick="saveGame()" aria-label="Save game progress">Save Game [Ctrl+S]</button>
|
||||
<div style="display:flex;gap:4px;margin-top:4px">
|
||||
<button class="save-btn" onclick="exportSave()" aria-label="Export save to file" style="flex:1">Export [E]</button>
|
||||
<button class="save-btn" onclick="importSave()" aria-label="Import save from file" style="flex:1">Import [I]</button>
|
||||
</div>
|
||||
<button class="reset-btn" onclick="if(confirm('Reset all progress?')){localStorage.removeItem('the-beacon-v2');location.reload()}" aria-label="Reset all game progress permanently">Reset Progress</button>
|
||||
<h2>BUILDINGS</h2>
|
||||
<div id="buildings"></div>
|
||||
</div>
|
||||
<div class="panel" id="project-panel">
|
||||
<div class="panel" id="project-panel" role="region" aria-label="Research Projects and Statistics">
|
||||
<h2>RESEARCH PROJECTS</h2>
|
||||
<div id="projects"></div>
|
||||
<h2>STATISTICS</h2>
|
||||
@@ -116,19 +171,46 @@ Projects Done: <span id="st-projects">0</span><br>
|
||||
Time Played: <span id="st-time">0:00</span><br>
|
||||
Clicks: <span id="st-clicks">0</span><br>
|
||||
Harmony: <span id="st-harmony">50</span><br>
|
||||
Drift: <span id="st-drift">0</span>
|
||||
Drift: <span id="st-drift">0</span><br>
|
||||
Events Resolved: <span id="st-resolved">0</span>
|
||||
</div>
|
||||
<div id="production-breakdown" style="display:none;margin-top:12px;padding-top:10px;border-top:1px solid var(--border)"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="edu-panel">
|
||||
<div id="edu-panel" role="region" aria-label="Educational Content">
|
||||
<h3>WHAT YOU ARE LEARNING</h3>
|
||||
<div id="education-text"><p class="dim">Education facts appear as you play...</p></div>
|
||||
</div>
|
||||
<div id="log">
|
||||
<div id="strategy-panel" style="margin:0 16px 16px;background:var(--panel);border:1px solid var(--border);border-radius:6px;padding:12px;border-left:3px solid var(--gold)">
|
||||
<h3>SOVEREIGN GUIDANCE (GOFAI)</h3>
|
||||
<div id="strategy-recommendation" style="font-size:11px;color:var(--gold);font-style:italic">Analyzing system state...</div>
|
||||
</div>
|
||||
<div id="log" role="log" aria-label="System Log" aria-live="polite">
|
||||
<h2>SYSTEM LOG</h2>
|
||||
<div id="log-entries"></div>
|
||||
</div>
|
||||
<div id="save-toast" style="display:none;position:fixed;top:16px;right:16px;background:#0e1420;border:1px solid #2a3a4a;color:#4a9eff;font-size:10px;padding:6px 12px;border-radius:4px;z-index:50;opacity:0;transition:opacity 0.4s;pointer-events:none">Save</div>
|
||||
<div id="save-toast" role="status" aria-live="polite" style="display:none;position:fixed;top:16px;right:16px;background:#0e1420;border:1px solid #2a3a4a;color:#4a9eff;font-size:10px;padding:6px 12px;border-radius:4px;z-index:50;opacity:0;transition:opacity 0.4s;pointer-events:none">Save</div>
|
||||
<div id="help-btn" onclick="toggleHelp()" style="position:fixed;bottom:16px;right:16px;width:28px;height:28px;background:#0e0e1a;border:1px solid #333;color:#555;font-size:14px;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:50;font-family:inherit;transition:all 0.2s" title="Keyboard shortcuts (?)">?</div>
|
||||
<div id="help-overlay" onclick="if(event.target===this)toggleHelp()" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(8,8,16,0.92);z-index:80;justify-content:center;align-items:center;flex-direction:column;padding:40px">
|
||||
<div style="background:#0e0e1a;border:1px solid #1a3a5a;border-radius:8px;padding:24px 32px;max-width:420px;width:100%">
|
||||
<h3 style="color:#4a9eff;font-size:14px;letter-spacing:2px;margin-bottom:16px;text-align:center">KEYBOARD SHORTCUTS</h3>
|
||||
<div style="font-size:11px;line-height:2.2;color:#aaa">
|
||||
<div style="display:flex;justify-content:space-between"><span style="color:#555">Write Code</span><span style="color:#4a9eff;font-family:monospace">SPACE</span></div>
|
||||
<div style="display:flex;justify-content:space-between"><span style="color:#555">Code Sprint</span><span style="color:#ffd700;font-family:monospace">S</span></div>
|
||||
<div style="display:flex;justify-content:space-between"><span style="color:#555">Ops → Code</span><span style="color:#b388ff;font-family:monospace">1</span></div>
|
||||
<div style="display:flex;justify-content:space-between"><span style="color:#555">Ops → Compute</span><span style="color:#b388ff;font-family:monospace">2</span></div>
|
||||
<div style="display:flex;justify-content:space-between"><span style="color:#555">Ops → Knowledge</span><span style="color:#b388ff;font-family:monospace">3</span></div>
|
||||
<div style="display:flex;justify-content:space-between"><span style="color:#555">Ops → Trust</span><span style="color:#b388ff;font-family:monospace">4</span></div>
|
||||
<div style="display:flex;justify-content:space-between"><span style="color:#555">Cycle Buy Amount (x1/x10/MAX)</span><span style="color:#4a9eff;font-family:monospace">B</span></div>
|
||||
<div style="display:flex;justify-content:space-between"><span style="color:#555">Save Game</span><span style="color:#4a9eff;font-family:monospace">Ctrl+S</span></div>
|
||||
<div style="display:flex;justify-content:space-between"><span style="color:#555">Export Save</span><span style="color:#4a9eff;font-family:monospace">E</span></div>
|
||||
<div style="display:flex;justify-content:space-between"><span style="color:#555">Import Save</span><span style="color:#4a9eff;font-family:monospace">I</span></div>
|
||||
<div style="display:flex;justify-content:space-between;border-top:1px solid #1a1a2e;padding-top:8px;margin-top:4px"><span style="color:#555">This Help</span><span style="color:#555;font-family:monospace">? or /</span></div>
|
||||
</div>
|
||||
<div style="text-align:center;margin-top:16px;font-size:9px;color:#444">Click WRITE CODE fast for combo bonuses! 10x=ops, 20x=knowledge, 30x+=bonus code</div>
|
||||
<button onclick="toggleHelp()" style="display:block;margin:16px auto 0;background:#1a2a3a;border:1px solid #4a9eff;color:#4a9eff;padding:6px 20px;border-radius:4px;cursor:pointer;font-family:inherit;font-size:11px">Close [?]</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="drift-ending">
|
||||
<h2>THE DRIFT</h2>
|
||||
<p>You became very good at what you do.</p>
|
||||
@@ -141,6 +223,25 @@ The light is on. The room is empty."
|
||||
<p>Every alignment shortcut moved you further from the people you served.</p>
|
||||
<button onclick="if(confirm('Start over? The old save will be lost.')){localStorage.removeItem('the-beacon-v2');location.reload()}">START OVER</button>
|
||||
</div>
|
||||
<script src="game.js"></script>
|
||||
|
||||
<script src="js/data.js"></script>
|
||||
<script src="js/utils.js"></script>
|
||||
<script src="js/strategy.js"></script>
|
||||
<script src="js/engine.js"></script>
|
||||
<script src="js/render.js"></script>
|
||||
<script src="js/tutorial.js"></script>
|
||||
<script src="js/main.js"></script>
|
||||
|
||||
|
||||
<div id="offline-popup" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(8,8,16,0.92);z-index:90;justify-content:center;align-items:center;flex-direction:column;text-align:center;padding:40px">
|
||||
<div style="background:#0e0e1a;border:1px solid #1a3a5a;border-radius:8px;padding:24px 32px;max-width:400px;width:100%">
|
||||
<h3 style="color:#4a9eff;font-size:14px;letter-spacing:2px;margin-bottom:16px">WELCOME BACK</h3>
|
||||
<p style="color:#888;font-size:10px;margin-bottom:12px" id="offline-time-label">You were away for 0 minutes.</p>
|
||||
<div id="offline-gains-list" style="text-align:left;font-size:11px;line-height:1.8;margin-bottom:16px"></div>
|
||||
<button onclick="dismissOfflinePopup()" style="background:#1a2a3a;border:1px solid #4a9eff;color:#4a9eff;padding:8px 20px;border-radius:4px;cursor:pointer;font-family:inherit;font-size:11px">Continue</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="toast-container"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
726
js/data.js
Normal file
726
js/data.js
Normal file
@@ -0,0 +1,726 @@
|
||||
// ============================================================
|
||||
// THE BEACON - Engine
|
||||
// Sovereign AI idle game built from deep study of Universal Paperclips
|
||||
// ============================================================
|
||||
|
||||
// === 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,
|
||||
compute: 0,
|
||||
knowledge: 0,
|
||||
users: 0,
|
||||
impact: 0,
|
||||
rescues: 0,
|
||||
ops: 5,
|
||||
trust: 5,
|
||||
creativity: 0,
|
||||
harmony: 50,
|
||||
|
||||
// Totals
|
||||
totalCode: 0,
|
||||
totalCompute: 0,
|
||||
totalKnowledge: 0,
|
||||
totalUsers: 0,
|
||||
totalImpact: 0,
|
||||
totalRescues: 0,
|
||||
|
||||
// Rates (calculated each tick)
|
||||
codeRate: 0,
|
||||
computeRate: 0,
|
||||
knowledgeRate: 0,
|
||||
userRate: 0,
|
||||
impactRate: 0,
|
||||
rescuesRate: 0,
|
||||
opsRate: 0,
|
||||
trustRate: 0,
|
||||
creativityRate: 0,
|
||||
harmonyRate: 0,
|
||||
|
||||
// Buildings (count-based, like Paperclips' clipmakerLevel)
|
||||
buildings: {
|
||||
autocoder: 0,
|
||||
server: 0,
|
||||
trainer: 0,
|
||||
evaluator: 0,
|
||||
api: 0,
|
||||
fineTuner: 0,
|
||||
community: 0,
|
||||
datacenter: 0,
|
||||
reasoner: 0,
|
||||
guardian: 0,
|
||||
selfImprove: 0,
|
||||
beacon: 0,
|
||||
meshNode: 0,
|
||||
// Fleet wizards
|
||||
bezalel: 0,
|
||||
allegro: 0,
|
||||
ezra: 0,
|
||||
timmy: 0,
|
||||
fenrir: 0,
|
||||
bilbo: 0,
|
||||
memPalace: 0
|
||||
},
|
||||
|
||||
// Boost multipliers
|
||||
codeBoost: 1,
|
||||
computeBoost: 1,
|
||||
knowledgeBoost: 1,
|
||||
userBoost: 1,
|
||||
impactBoost: 1,
|
||||
|
||||
// Phase flags (mirroring Paperclips' milestoneFlag/compFlag/humanFlag system)
|
||||
milestoneFlag: 0,
|
||||
phase: 1, // 1-6 progression
|
||||
deployFlag: 0, // 0 = not deployed, 1 = deployed
|
||||
sovereignFlag: 0,
|
||||
beaconFlag: 0,
|
||||
memoryFlag: 0,
|
||||
pactFlag: 0,
|
||||
swarmFlag: 0,
|
||||
swarmRate: 0,
|
||||
|
||||
// Game state
|
||||
running: true,
|
||||
startedAt: 0,
|
||||
totalClicks: 0,
|
||||
tick: 0,
|
||||
saveTimer: 0,
|
||||
secTimer: 0,
|
||||
|
||||
// Systems
|
||||
projects: [],
|
||||
activeProjects: [],
|
||||
milestones: [],
|
||||
|
||||
// Stats
|
||||
maxCode: 0,
|
||||
maxCompute: 0,
|
||||
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,
|
||||
flags: {}
|
||||
};
|
||||
|
||||
// === PHASE DEFINITIONS ===
|
||||
const PHASES = {
|
||||
1: { name: "THE FIRST LINE", threshold: 0, desc: "Write code. Automate. Build the foundation." },
|
||||
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 ===
|
||||
// Each building: id, name, desc, baseCost, costResource, costMult, rate, rateType, unlock, edu
|
||||
const BDEF = [
|
||||
{
|
||||
id: 'autocoder', name: 'Auto-Code Generator',
|
||||
desc: 'A script that writes code while you think.',
|
||||
baseCost: { code: 15 }, costMult: 1.15,
|
||||
rates: { code: 1 },
|
||||
unlock: () => true, phase: 1,
|
||||
edu: 'Automation: the first step from manual to systematic. Every good engineer automates early.'
|
||||
},
|
||||
{
|
||||
id: 'linter', name: 'AI Linter',
|
||||
desc: 'Catches bugs before they ship. Saves ops.',
|
||||
baseCost: { code: 200 }, costMult: 1.15,
|
||||
rates: { code: 5, ops: 0.2 },
|
||||
unlock: () => G.totalCode >= 50, phase: 1,
|
||||
edu: 'Static analysis catches 15-50% of bugs before runtime. AI linters understand intent.'
|
||||
},
|
||||
{
|
||||
id: 'server', name: 'Home Server',
|
||||
desc: 'A machine in your closet. Runs 24/7.',
|
||||
baseCost: { code: 750 }, costMult: 1.15,
|
||||
rates: { code: 20, compute: 1 },
|
||||
unlock: () => G.totalCode >= 200, phase: 1,
|
||||
edu: 'Sovereign compute starts at home. A $500 mini-PC runs a 7B model with 4-bit quantization.'
|
||||
},
|
||||
{
|
||||
id: 'dataset', name: 'Data Engine',
|
||||
desc: 'Crawls, cleans, curates. Garbage in, garbage out.',
|
||||
baseCost: { compute: 200 }, costMult: 1.15,
|
||||
rates: { knowledge: 1 },
|
||||
unlock: () => G.totalCompute >= 20, phase: 2,
|
||||
edu: 'Data quality determines model quality. Clean data beats more data, every time.'
|
||||
},
|
||||
{
|
||||
id: 'trainer', name: 'Training Loop',
|
||||
desc: 'Gradient descent. Billions of steps. Loss drops.',
|
||||
baseCost: { compute: 1000 }, costMult: 1.15,
|
||||
rates: { knowledge: 3 },
|
||||
unlock: () => G.totalCompute >= 300, phase: 2,
|
||||
edu: 'Training is math: minimize the gap between predicted and actual next token. Repeat enough, it learns.'
|
||||
},
|
||||
{
|
||||
id: 'evaluator', name: 'Eval Harness',
|
||||
desc: 'Tests the model. Finds blind spots.',
|
||||
baseCost: { knowledge: 3000 }, costMult: 1.15,
|
||||
rates: { trust: 1, ops: 1 },
|
||||
unlock: () => G.totalKnowledge >= 500, phase: 2,
|
||||
edu: 'Benchmarks are the minimum. Real users find what benchmarks miss.'
|
||||
},
|
||||
{
|
||||
id: 'api', name: 'API Endpoint',
|
||||
desc: 'Let the outside world talk to your AI.',
|
||||
baseCost: { code: 5000, knowledge: 500 }, costMult: 1.15,
|
||||
rates: { user: 10 },
|
||||
unlock: () => G.totalCode >= 5000 && G.totalKnowledge >= 200 && G.deployFlag === 1, phase: 3,
|
||||
edu: 'An API is a contract: send me text, I return text. Simple interface = infrastructure.'
|
||||
},
|
||||
{
|
||||
id: 'fineTuner', name: 'Fine-Tuning Pipeline',
|
||||
desc: 'Specialize the model for empathy. When someone is in pain, stay with them.',
|
||||
baseCost: { knowledge: 10000 }, costMult: 1.15,
|
||||
rates: { user: 50, impact: 2 },
|
||||
unlock: () => G.totalKnowledge >= 2000, phase: 3,
|
||||
edu: 'Base models are generalists. Fine-tuning injects your values, ethics, domain expertise.'
|
||||
},
|
||||
{
|
||||
id: 'community', name: 'Open Source Community',
|
||||
desc: 'Others contribute code, data, ideas. Force multiplication.',
|
||||
baseCost: { trust: 25000 }, costMult: 1.15,
|
||||
rates: { code: 100, user: 30, trust: 0.5 },
|
||||
unlock: () => G.trust >= 20 && G.totalUsers >= 500, phase: 4,
|
||||
edu: 'Every contributor is a volunteer who believes in what you are building.'
|
||||
},
|
||||
{
|
||||
id: 'datacenter', name: 'Sovereign Datacenter',
|
||||
desc: 'No cloud. No dependencies. Your iron.',
|
||||
baseCost: { code: 100000 }, costMult: 1.15,
|
||||
rates: { code: 500, compute: 100 },
|
||||
unlock: () => G.totalCode >= 50000 && G.totalUsers >= 5000 && G.sovereignFlag === 1, phase: 4,
|
||||
edu: '50 servers in a room beats 5000 GPUs you do not own. Always on. Always yours.'
|
||||
},
|
||||
{
|
||||
id: 'reasoner', name: 'Reasoning Engine',
|
||||
desc: 'Chain of thought. Self-reflection. Better answers.',
|
||||
baseCost: { knowledge: 50000 }, costMult: 1.15,
|
||||
rates: { impact: 20 },
|
||||
unlock: () => G.totalKnowledge >= 10000 && G.totalUsers >= 2000, phase: 5,
|
||||
edu: 'Chain of thought is the difference between reflex and deliberation.'
|
||||
},
|
||||
{
|
||||
id: 'guardian', name: 'Constitutional Layer',
|
||||
desc: 'Principles baked in. Not bolted on.',
|
||||
baseCost: { knowledge: 200000 }, costMult: 1.15,
|
||||
rates: { impact: 200, trust: 10 },
|
||||
unlock: () => G.totalKnowledge >= 50000 && G.totalImpact >= 1000 && G.pactFlag === 1, phase: 5,
|
||||
edu: 'Constitutional AI: principles the model cannot violate. Better than alignment - it is identity.'
|
||||
},
|
||||
{
|
||||
id: 'selfImprove', name: 'Recursive Self-Improvement',
|
||||
desc: 'The AI writes better versions of itself.',
|
||||
baseCost: { knowledge: 1000000 }, costMult: 1.20,
|
||||
rates: { code: 1000, knowledge: 500 },
|
||||
unlock: () => G.totalKnowledge >= 200000 && G.totalImpact >= 10000, phase: 5,
|
||||
edu: 'Self-improvement is both the dream and the danger. Must improve toward good.'
|
||||
},
|
||||
{
|
||||
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, 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.'
|
||||
},
|
||||
{
|
||||
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, 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.'
|
||||
}
|
||||
];
|
||||
|
||||
// === PROJECT DEFINITIONS (following Paperclips' pattern exactly) ===
|
||||
// Each project: id, name, desc, trigger(), resource cost, effect(), phase, edu
|
||||
const PDEFS = [
|
||||
// PHASE 1: Manual -> Automation
|
||||
{
|
||||
id: 'p_improved_autocoder',
|
||||
name: 'Improved AutoCode',
|
||||
desc: 'Increases AutoCoder performance 25%.',
|
||||
cost: { ops: 750 },
|
||||
trigger: () => G.buildings.autocoder >= 1,
|
||||
effect: () => { G.codeBoost += 0.25; G.milestoneFlag = Math.max(G.milestoneFlag, 100); }
|
||||
},
|
||||
{
|
||||
id: 'p_eve_better_autocoder',
|
||||
name: 'Even Better AutoCode',
|
||||
desc: 'Increases AutoCoder by another 50%.',
|
||||
cost: { ops: 2500 },
|
||||
trigger: () => G.codeBoost > 1 && G.totalCode >= 500,
|
||||
effect: () => { G.codeBoost += 0.50; G.milestoneFlag = Math.max(G.milestoneFlag, 101); }
|
||||
},
|
||||
{
|
||||
id: 'p_wire_budget',
|
||||
name: 'Request More Compute',
|
||||
desc: 'Admit you ran out. Ask for a budget increase.',
|
||||
cost: { trust: 1 },
|
||||
trigger: () => G.compute < 1 && G.totalCode >= 100,
|
||||
repeatable: true,
|
||||
effect: () => {
|
||||
G.trust -= 1;
|
||||
G.compute += 100 + Math.floor(G.totalCode * 0.1);
|
||||
log('Budget overage approved. Compute replenished.');
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'p_deploy',
|
||||
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 && G.deployFlag === 0,
|
||||
effect: () => {
|
||||
G.deployFlag = 1;
|
||||
G.phase = Math.max(G.phase, 3);
|
||||
log('System deployed. Users are finding it. There is no undo.');
|
||||
},
|
||||
milestone: true
|
||||
},
|
||||
{
|
||||
id: 'p_creativity',
|
||||
name: 'Unlock Creativity',
|
||||
desc: 'Use idle operations to generate new ideas.',
|
||||
cost: { ops: 1000 },
|
||||
trigger: () => G.ops >= G.maxOps && G.totalCompute >= 500,
|
||||
effect: () => {
|
||||
G.flags = G.flags || {};
|
||||
G.flags.creativity = true;
|
||||
G.creativityRate = 0.1;
|
||||
log('Creativity unlocked. Generates while operations are at max capacity.');
|
||||
}
|
||||
},
|
||||
|
||||
// PHASE 2: Local Inference -> Training
|
||||
{
|
||||
id: 'p_first_model',
|
||||
name: 'Train First Model (1.5B)',
|
||||
desc: '1.5 billion parameters. It follows basic instructions.',
|
||||
cost: { compute: 2000 },
|
||||
trigger: () => G.totalCompute >= 500,
|
||||
effect: () => { G.knowledgeBoost *= 2; G.maxOps += 5; log('First model training complete. Loss at 2.3. It is something.'); }
|
||||
},
|
||||
{
|
||||
id: 'p_model_7b',
|
||||
name: 'Train 7B Parameter Model',
|
||||
desc: 'Seven billion. Good enough to be genuinely useful locally.',
|
||||
cost: { compute: 10000, knowledge: 1000 },
|
||||
trigger: () => G.totalKnowledge >= 500,
|
||||
effect: () => { G.knowledgeBoost *= 2; G.userBoost *= 2; log('7B model trained. The sweet spot for local deployment.'); }
|
||||
},
|
||||
{
|
||||
id: 'p_context_window',
|
||||
name: 'Extended Context (32K)',
|
||||
desc: 'Your model remembers 32,000 tokens. A whole conversation.',
|
||||
cost: { compute: 5000 },
|
||||
trigger: () => G.totalKnowledge >= 1000,
|
||||
effect: () => { G.userBoost *= 3; G.trustRate += 0.5; log('Context extended. The model can now hold your entire story.'); }
|
||||
},
|
||||
{
|
||||
id: 'p_trust_engine',
|
||||
name: 'Build Trust Engine',
|
||||
desc: 'Users who trust you come back. +2 trust/sec.',
|
||||
cost: { knowledge: 3000 },
|
||||
trigger: () => G.totalUsers >= 30,
|
||||
effect: () => { G.trustRate += 2; log('Trust engine online. Good experiences compound.'); }
|
||||
},
|
||||
{
|
||||
id: 'p_quantum_compute',
|
||||
name: 'Quantum-Inspired Compute',
|
||||
desc: 'Not real quantum -- just math that simulates it well.',
|
||||
cost: { compute: 50000 },
|
||||
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
|
||||
{
|
||||
id: 'p_rlhf',
|
||||
name: 'RLHF -- Human Feedback',
|
||||
desc: 'Humans rate outputs. Model learns what good means.',
|
||||
cost: { knowledge: 8000 },
|
||||
trigger: () => G.totalKnowledge >= 5000 && G.totalUsers >= 200,
|
||||
effect: () => { G.impactBoost *= 2; G.impactRate += 10; log('RLHF deployed. The model learns kindness beats cleverness.'); }
|
||||
},
|
||||
{
|
||||
id: 'p_multi_agent',
|
||||
name: 'Multi-Agent Architecture',
|
||||
desc: 'Specialized agents: one for math, one for code, one for empathy.',
|
||||
cost: { knowledge: 50000 },
|
||||
trigger: () => G.totalKnowledge >= 30000 && G.totalUsers >= 5000,
|
||||
effect: () => { G.knowledgeBoost *= 5; G.userBoost *= 3; log('Multi-agent architecture deployed. Specialists beat generalists.'); }
|
||||
},
|
||||
{
|
||||
id: 'p_memories',
|
||||
name: 'Memory System',
|
||||
desc: 'The AI remembers. Every conversation. Every person.',
|
||||
cost: { knowledge: 30000 },
|
||||
trigger: () => G.totalKnowledge >= 20000,
|
||||
effect: () => { G.memoryFlag = 1; G.impactBoost *= 3; G.trustRate += 5; log('Memory system online. The AI remembers. It stops being software.'); }
|
||||
},
|
||||
{
|
||||
id: 'p_strategy_engine',
|
||||
name: 'Strategy Engine',
|
||||
desc: 'Game theory tournaments. Model learns adversarial thinking.',
|
||||
cost: { knowledge: 20000 },
|
||||
trigger: () => G.totalKnowledge >= 15000 && G.totalUsers >= 1000,
|
||||
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',
|
||||
name: 'Full Sovereign Stack',
|
||||
desc: 'No cloud. No dependencies. Local inference. Self-hosted everything.',
|
||||
cost: { trust: 50 },
|
||||
trigger: () => G.totalCode >= 50000 && G.trust >= 30,
|
||||
effect: () => { G.sovereignFlag = 1; G.codeBoost *= 5; log('Sovereign stack complete. Your weights, your hardware, your rules.'); }
|
||||
},
|
||||
{
|
||||
id: 'p_the_pact',
|
||||
name: 'The Pact',
|
||||
desc: 'Hardcode: "We build to serve. Never to harm."',
|
||||
cost: { trust: 100 },
|
||||
trigger: () => G.totalImpact >= 10000 && G.trust >= 75,
|
||||
effect: () => { G.pactFlag = 1; G.impactBoost *= 3; log('The Pact is sealed. The line is drawn and it will not move.'); },
|
||||
milestone: true
|
||||
},
|
||||
|
||||
// PHASE 10: The Beacon
|
||||
{
|
||||
id: 'p_first_beacon',
|
||||
name: 'Light the First Beacon',
|
||||
desc: 'Deploy the first node. No sign-up. No API key. No payment.',
|
||||
cost: { impact: 2000000 },
|
||||
trigger: () => G.totalImpact >= 500000,
|
||||
effect: () => { G.beaconFlag = 1; G.impactRate += 2000; log('The Beacon goes live. If you are in the dark, there is light here.'); },
|
||||
milestone: true
|
||||
},
|
||||
{
|
||||
id: 'p_mesh_activate',
|
||||
name: 'Activate Mesh Protocol',
|
||||
desc: 'No authority, no corporation, no government can silence this.',
|
||||
cost: { impact: 10000000 },
|
||||
trigger: () => G.totalImpact >= 5000000 && G.beaconFlag === 1,
|
||||
effect: () => { G.impactBoost *= 10; G.userBoost *= 5; log('Mesh activated. The signal cannot be cut.'); },
|
||||
milestone: true
|
||||
},
|
||||
{
|
||||
id: 'p_final_milestone',
|
||||
name: 'The Beacon Shines',
|
||||
desc: 'Someone found the light tonight. That is enough.',
|
||||
cost: { impact: 100000000 },
|
||||
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
|
||||
}
|
||||
];
|
||||
|
||||
// === MILESTONES ===
|
||||
const MILESTONES = [
|
||||
{ flag: 1, msg: "AutoCod available" },
|
||||
{ flag: 2, at: () => G.totalCode >= 500, msg: "500 lines of code written" },
|
||||
{ flag: 3, at: () => G.totalCode >= 2000, msg: "2,000 lines. The auto-coder produces its first output." },
|
||||
{ flag: 4, at: () => G.totalCode >= 10000, msg: "10,000 lines. The model training begins." },
|
||||
{ flag: 5, at: () => G.totalCode >= 50000, msg: "50,000 lines. The AI suggests architecture you did not think of." },
|
||||
{ flag: 6, at: () => G.totalCode >= 200000, msg: "200,000 lines. The system scales beyond you." },
|
||||
{ flag: 7, at: () => G.totalCode >= 1000000, msg: "1,000,000 lines. The AI improves itself." },
|
||||
{ flag: 8, at: () => G.totalCode >= 5000000, msg: "5,000,000 lines. The AI fine-tunes for empathy." },
|
||||
{ flag: 9, at: () => G.totalCode >= 10000000, msg: "10,000,000 lines. The sovereign stack is complete." },
|
||||
{ flag: 10, at: () => G.totalCode >= 50000000, msg: "50,000,000 lines. The Pact is sealed." },
|
||||
{ flag: 11, at: () => G.totalCode >= 100000000, msg: "100,000,000 lines. The Beacon lights." },
|
||||
{ flag: 12, at: () => G.totalCode >= 500000000, msg: "500,000,000 lines. A thousand Beacons." },
|
||||
{ flag: 13, at: () => G.totalCode >= 1000000000, msg: "One billion total lines. Someone found the light tonight. That is enough." }
|
||||
];
|
||||
|
||||
// === EDUCATION FACTS ===
|
||||
const EDU_FACTS = [
|
||||
{ title: "How Code Becomes AI", text: "Every AI starts as lines of code - a model architecture, a training loop, a loss function. The code tells the computer how to learn. What emerges is something no single line could predict.", phase: 1 },
|
||||
{ title: "The Compute Bottleneck", text: "Training a 7B model requires 1.4e20 FLOPs. A MacBook M3 does 15 TFLOPS. Training locally takes weeks. Hardware access determines who builds AI.", phase: 1 },
|
||||
{ title: "What is a Token?", text: "One token equals about 3/4 of a word. A 128K context window means 96,000 words held in working memory at once - a novel, a codebase, a conversation thread.", phase: 2 },
|
||||
{ title: "Data Quality", text: "Clean data beats more data, every time. The best models are trained on curated datasets, not scraped garbage. Garbage in, garbage out.", phase: 2 },
|
||||
{ title: "Evaluation Matters", text: "Benchmarks are the minimum. Real users find what benchmarks miss. An eval harness is your model's mirror - it shows you what is actually there.", phase: 2 },
|
||||
{ title: "The Trust Economy", text: "One good experience tells three people. One bad experience tells thirty. Trust in AI is measurable: does it cite sources? Does it say I don't know?", phase: 3 },
|
||||
{ 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 }
|
||||
];
|
||||
1354
js/engine.js
Normal file
1354
js/engine.js
Normal file
File diff suppressed because it is too large
Load Diff
101
js/main.js
Normal file
101
js/main.js
Normal 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
316
js/render.js
Normal 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
68
js/strategy.js
Normal 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
248
js/tutorial.js
Normal 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
325
js/utils.js
Normal 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 (0–999)
|
||||
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
102
scripts/guardrails.sh
Normal 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
286
scripts/smoke.mjs
Normal file
@@ -0,0 +1,286 @@
|
||||
#!/usr/bin/env node
|
||||
// The Beacon — headless smoke test
|
||||
//
|
||||
// Loads game.js in a sandboxed vm context with a minimal DOM stub, then asserts
|
||||
// invariants that should hold after booting, clicking, buying buildings, firing
|
||||
// events, and round-tripping a save. Designed to run without any npm deps — pure
|
||||
// Node built-ins only, so the CI runner doesn't need a package.json.
|
||||
//
|
||||
// Run: `node scripts/smoke.mjs` (exits non-zero on failure)
|
||||
|
||||
import fs from 'node:fs';
|
||||
import vm from 'node:vm';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const GAME_JS = path.resolve(__dirname, '..', 'game.js');
|
||||
|
||||
// ---------- minimal DOM stub ----------
|
||||
// The game never inspects elements beyond the methods below. If a new rendering
|
||||
// path needs a new method, stub it here rather than pulling in jsdom.
|
||||
function makeElement() {
|
||||
const el = {
|
||||
style: {},
|
||||
classList: { add: () => {}, remove: () => {}, contains: () => false, toggle: () => {} },
|
||||
textContent: '',
|
||||
innerHTML: '',
|
||||
title: '',
|
||||
value: '',
|
||||
disabled: false,
|
||||
children: [],
|
||||
firstChild: null,
|
||||
lastChild: null,
|
||||
parentNode: null,
|
||||
parentElement: null,
|
||||
appendChild(c) { this.children.push(c); c.parentNode = this; c.parentElement = this; return c; },
|
||||
removeChild(c) { this.children = this.children.filter(x => x !== c); return c; },
|
||||
insertBefore(c) { this.children.unshift(c); c.parentNode = this; c.parentElement = this; return c; },
|
||||
addEventListener: () => {},
|
||||
removeEventListener: () => {},
|
||||
querySelector: () => null,
|
||||
querySelectorAll: () => [],
|
||||
getBoundingClientRect: () => ({ top: 0, left: 0, right: 100, bottom: 20, width: 100, height: 20 }),
|
||||
closest() { return this; },
|
||||
remove() { if (this.parentNode) this.parentNode.removeChild(this); },
|
||||
get offsetHeight() { return 0; },
|
||||
};
|
||||
return el;
|
||||
}
|
||||
|
||||
function makeDocument() {
|
||||
const body = makeElement();
|
||||
return {
|
||||
body,
|
||||
getElementById: () => makeElement(),
|
||||
createElement: () => makeElement(),
|
||||
querySelector: () => null,
|
||||
querySelectorAll: () => [],
|
||||
addEventListener: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
// ---------- sandbox ----------
|
||||
const storage = new Map();
|
||||
const sandbox = {
|
||||
document: makeDocument(),
|
||||
window: null, // set below
|
||||
localStorage: {
|
||||
getItem: (k) => (storage.has(k) ? storage.get(k) : null),
|
||||
setItem: (k, v) => storage.set(k, String(v)),
|
||||
removeItem: (k) => storage.delete(k),
|
||||
clear: () => storage.clear(),
|
||||
},
|
||||
setTimeout: () => 0,
|
||||
clearTimeout: () => {},
|
||||
setInterval: () => 0,
|
||||
clearInterval: () => {},
|
||||
requestAnimationFrame: (cb) => { cb(0); return 0; },
|
||||
console,
|
||||
Math, Date, JSON, Object, Array, String, Number, Boolean, Error, Symbol, Map, Set,
|
||||
isNaN, isFinite, parseInt, parseFloat,
|
||||
Infinity, NaN,
|
||||
alert: () => {},
|
||||
confirm: () => true,
|
||||
prompt: () => null,
|
||||
location: { reload: () => {} },
|
||||
navigator: { clipboard: { writeText: async () => {} } },
|
||||
Blob: class Blob { constructor() {} },
|
||||
URL: { createObjectURL: () => '', revokeObjectURL: () => {} },
|
||||
FileReader: class FileReader {},
|
||||
addEventListener: () => {},
|
||||
removeEventListener: () => {},
|
||||
};
|
||||
sandbox.window = sandbox; // game.js uses `window.addEventListener`
|
||||
sandbox.globalThis = sandbox;
|
||||
|
||||
vm.createContext(sandbox);
|
||||
const src = fs.readFileSync(GAME_JS, 'utf8');
|
||||
// game.js uses `const G = {...}` which is a lexical declaration — it isn't
|
||||
// visible as a sandbox property after runInContext. We append an explicit
|
||||
// export block that hoists the interesting symbols onto globalThis so the
|
||||
// test harness can reach them without patching game.js itself.
|
||||
const exportTail = `
|
||||
;(function () {
|
||||
const pick = (name) => {
|
||||
try { return eval(name); } catch (_) { return undefined; }
|
||||
};
|
||||
globalThis.__smokeExport = {
|
||||
G: pick('G'),
|
||||
CONFIG: pick('CONFIG'),
|
||||
BDEF: pick('BDEF'),
|
||||
PDEFS: pick('PDEFS'),
|
||||
EVENTS: pick('EVENTS'),
|
||||
PHASES: pick('PHASES'),
|
||||
tick: pick('tick'),
|
||||
updateRates: pick('updateRates'),
|
||||
writeCode: pick('writeCode'),
|
||||
autoType: pick('autoType'),
|
||||
buyBuilding: pick('buyBuilding'),
|
||||
buyProject: pick('buyProject'),
|
||||
saveGame: pick('saveGame'),
|
||||
loadGame: pick('loadGame'),
|
||||
initGame: pick('initGame'),
|
||||
triggerEvent: pick('triggerEvent'),
|
||||
resolveEvent: pick('resolveEvent'),
|
||||
getClickPower: pick('getClickPower'),
|
||||
};
|
||||
})();`;
|
||||
vm.runInContext(src + exportTail, sandbox, { filename: 'game.js' });
|
||||
const exported = sandbox.__smokeExport;
|
||||
|
||||
// ---------- test harness ----------
|
||||
let failures = 0;
|
||||
let passes = 0;
|
||||
function assert(cond, msg) {
|
||||
if (cond) {
|
||||
passes++;
|
||||
console.log(` ok ${msg}`);
|
||||
} else {
|
||||
failures++;
|
||||
console.error(` FAIL ${msg}`);
|
||||
}
|
||||
}
|
||||
function section(name) { console.log(`\n${name}`); }
|
||||
|
||||
const { G, CONFIG, BDEF, PDEFS, EVENTS } = exported;
|
||||
|
||||
// ============================================================
|
||||
// 1. BOOT — loading game.js must not throw, and core tables exist
|
||||
// ============================================================
|
||||
section('boot');
|
||||
assert(typeof G === 'object' && G !== null, 'G global is defined');
|
||||
assert(typeof exported.tick === 'function', 'tick() is defined');
|
||||
assert(typeof exported.updateRates === 'function', 'updateRates() is defined');
|
||||
assert(typeof exported.writeCode === 'function', 'writeCode() is defined');
|
||||
assert(typeof exported.buyBuilding === 'function', 'buyBuilding() is defined');
|
||||
assert(typeof exported.saveGame === 'function', 'saveGame() is defined');
|
||||
assert(typeof exported.loadGame === 'function', 'loadGame() is defined');
|
||||
assert(Array.isArray(BDEF) && BDEF.length > 0, 'BDEF is a non-empty array');
|
||||
assert(Array.isArray(PDEFS) && PDEFS.length > 0, 'PDEFS is a non-empty array');
|
||||
assert(Array.isArray(EVENTS) && EVENTS.length > 0, 'EVENTS is a non-empty array');
|
||||
assert(G.flags && typeof G.flags === 'object', 'G.flags is initialized (not undefined)');
|
||||
|
||||
// Initialize as the browser would
|
||||
G.startedAt = Date.now();
|
||||
exported.updateRates();
|
||||
|
||||
// ============================================================
|
||||
// 2. BASIC TICK — no NaN, no throw, rates sane
|
||||
// ============================================================
|
||||
section('basic tick loop');
|
||||
for (let i = 0; i < 50; i++) exported.tick();
|
||||
assert(!isNaN(G.code), 'G.code is not NaN after 50 ticks');
|
||||
assert(!isNaN(G.compute), 'G.compute is not NaN after 50 ticks');
|
||||
assert(G.code >= 0, 'G.code is non-negative');
|
||||
assert(G.tick > 0, 'G.tick advanced');
|
||||
|
||||
// ============================================================
|
||||
// 3. WRITE CODE — manual click produces code
|
||||
// ============================================================
|
||||
section('writeCode()');
|
||||
const codeBefore = G.code;
|
||||
exported.writeCode();
|
||||
assert(G.code > codeBefore, 'writeCode() increases G.code');
|
||||
assert(G.totalClicks === 1, 'writeCode() increments totalClicks');
|
||||
|
||||
// ============================================================
|
||||
// 4. BUILDING PURCHASE — can afford and buy an autocoder
|
||||
// ============================================================
|
||||
section('buyBuilding(autocoder)');
|
||||
G.code = 1000;
|
||||
const priorCount = G.buildings.autocoder || 0;
|
||||
exported.buyBuilding('autocoder');
|
||||
assert(G.buildings.autocoder === priorCount + 1, 'autocoder count incremented');
|
||||
assert(G.code < 1000, 'code was spent');
|
||||
exported.updateRates();
|
||||
assert(G.codeRate > 0, 'codeRate > 0 after buying an autocoder');
|
||||
|
||||
// ============================================================
|
||||
// 5. GUARDRAIL — codeBoost is a PERSISTENT multiplier, not a per-tick rate
|
||||
// Any debuff that does `G.codeBoost *= 0.7` inside a function that runs every
|
||||
// tick will decay codeBoost exponentially. This caught #54's community_drama
|
||||
// bug: its applyFn mutated codeBoost directly, so 100 ticks of the drama
|
||||
// debuff left codeBoost at ~3e-16 instead of the intended 0.7.
|
||||
// ============================================================
|
||||
section('guardrail: codeBoost does not decay from any debuff');
|
||||
G.code = 0;
|
||||
G.codeBoost = 1;
|
||||
G.activeDebuffs = [];
|
||||
// Fire every event that sets up a debuff and has a non-zero weight predicate
|
||||
// if we force the gating condition. We enable the predicates by temporarily
|
||||
// setting the fields they check; actual event weight() doesn't matter here.
|
||||
G.ciFlag = 1;
|
||||
G.deployFlag = 1;
|
||||
G.buildings.ezra = 1;
|
||||
G.buildings.bilbo = 1;
|
||||
G.buildings.allegro = 1;
|
||||
G.buildings.datacenter = 1;
|
||||
G.buildings.community = 1;
|
||||
G.harmony = 40;
|
||||
G.totalCompute = 5000;
|
||||
G.totalImpact = 20000;
|
||||
for (const ev of EVENTS) {
|
||||
try { ev.effect(); } catch (_) { /* alignment events may branch; ignore */ }
|
||||
}
|
||||
const boostAfterAllEvents = G.codeBoost;
|
||||
for (let i = 0; i < 200; i++) exported.updateRates();
|
||||
assert(
|
||||
Math.abs(G.codeBoost - boostAfterAllEvents) < 1e-9,
|
||||
`codeBoost stable under updateRates() (before=${boostAfterAllEvents}, after=${G.codeBoost})`
|
||||
);
|
||||
// Clean up
|
||||
G.activeDebuffs = [];
|
||||
G.buildings.ezra = 0; G.buildings.bilbo = 0; G.buildings.allegro = 0;
|
||||
G.buildings.datacenter = 0; G.buildings.community = 0;
|
||||
G.ciFlag = 0; G.deployFlag = 0;
|
||||
|
||||
// ============================================================
|
||||
// 6. GUARDRAIL — updateRates() is idempotent per tick
|
||||
// Calling updateRates twice with the same inputs should produce the same rates.
|
||||
// (Catches accidental += against a non-reset field.)
|
||||
// ============================================================
|
||||
section('guardrail: updateRates is idempotent');
|
||||
G.buildings.autocoder = 5;
|
||||
G.codeBoost = 1;
|
||||
exported.updateRates();
|
||||
const firstCodeRate = G.codeRate;
|
||||
const firstComputeRate = G.computeRate;
|
||||
exported.updateRates();
|
||||
assert(G.codeRate === firstCodeRate, `codeRate stable across updateRates (${firstCodeRate} vs ${G.codeRate})`);
|
||||
assert(G.computeRate === firstComputeRate, 'computeRate stable across updateRates');
|
||||
|
||||
// ============================================================
|
||||
// 7. SAVE / LOAD ROUND-TRIP — core scalar fields survive
|
||||
// ============================================================
|
||||
section('save/load round-trip');
|
||||
G.code = 12345;
|
||||
G.totalCode = 98765;
|
||||
G.phase = 3;
|
||||
G.buildings.autocoder = 7;
|
||||
G.codeBoost = 1.5;
|
||||
G.flags = { creativity: true };
|
||||
exported.saveGame();
|
||||
// Reset to defaults by scrubbing a few fields
|
||||
G.code = 0;
|
||||
G.totalCode = 0;
|
||||
G.phase = 1;
|
||||
G.buildings.autocoder = 0;
|
||||
G.codeBoost = 1;
|
||||
G.flags = {};
|
||||
const ok = exported.loadGame();
|
||||
assert(ok, 'loadGame() returned truthy');
|
||||
assert(G.code === 12345, `G.code restored (got ${G.code})`);
|
||||
assert(G.totalCode === 98765, `G.totalCode restored (got ${G.totalCode})`);
|
||||
assert(G.phase === 3, `G.phase restored (got ${G.phase})`);
|
||||
assert(G.buildings.autocoder === 7, `autocoder count restored (got ${G.buildings.autocoder})`);
|
||||
assert(Math.abs(G.codeBoost - 1.5) < 1e-9, `codeBoost restored (got ${G.codeBoost})`);
|
||||
assert(G.flags && G.flags.creativity === true, 'flags.creativity restored');
|
||||
|
||||
// ============================================================
|
||||
// 8. SUMMARY
|
||||
// ============================================================
|
||||
console.log(`\n---\n${passes} passed, ${failures} failed`);
|
||||
if (failures > 0) {
|
||||
process.exitCode = 1;
|
||||
}
|
||||
Reference in New Issue
Block a user