Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5864562dc2 | ||
|
|
21edc4e424 |
@@ -262,7 +262,6 @@ The light is on. The room is empty."
|
|||||||
|
|
||||||
<script src="js/data.js"></script>
|
<script src="js/data.js"></script>
|
||||||
<script src="js/utils.js"></script>
|
<script src="js/utils.js"></script>
|
||||||
<script src="js/state-export.js"></script>
|
|
||||||
<script src="js/combat.js"></script>
|
<script src="js/combat.js"></script>
|
||||||
<script src="js/strategy.js"></script>
|
<script src="js/strategy.js"></script>
|
||||||
<script src="js/sound.js"></script>
|
<script src="js/sound.js"></script>
|
||||||
|
|||||||
@@ -230,10 +230,6 @@ function tick() {
|
|||||||
G.lastEventAt = G.tick;
|
G.lastEventAt = G.tick;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof StateExport !== 'undefined' && StateExport && typeof StateExport.onTickBoundary === 'function') {
|
|
||||||
StateExport.onTickBoundary(G);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Emergent mechanics: track resource state and check for emergent events
|
// Emergent mechanics: track resource state and check for emergent events
|
||||||
if (typeof EmergentMechanics !== 'undefined' && window._emergent) {
|
if (typeof EmergentMechanics !== 'undefined' && window._emergent) {
|
||||||
if (Math.floor(G.tick * 10) % 100 === 0) { // every ~10 seconds
|
if (Math.floor(G.tick * 10) % 100 === 0) { // every ~10 seconds
|
||||||
|
|||||||
@@ -1,118 +0,0 @@
|
|||||||
(function (global) {
|
|
||||||
const STORE_KEY = 'compounding-intelligence:beacon-state';
|
|
||||||
const MAX_SNAPSHOTS = 300;
|
|
||||||
|
|
||||||
function _safeNumber(value, fallback = 0) {
|
|
||||||
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
function _tickKey(value) {
|
|
||||||
if (typeof value !== 'number' || !Number.isFinite(value)) return '0';
|
|
||||||
return value.toFixed(3);
|
|
||||||
}
|
|
||||||
|
|
||||||
function _resolveSink(explicitSink) {
|
|
||||||
if (explicitSink) return explicitSink;
|
|
||||||
return global.CompoundingIntelligence || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function _resolveStorage(explicitStorage) {
|
|
||||||
if (explicitStorage) return explicitStorage;
|
|
||||||
return typeof global.localStorage !== 'undefined' ? global.localStorage : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildSnapshot(gameState = {}) {
|
|
||||||
return {
|
|
||||||
source: 'the-beacon',
|
|
||||||
kind: 'idle_game_state',
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
tick: _safeNumber(gameState.tick, 0),
|
|
||||||
phase: _safeNumber(gameState.phase, 1),
|
|
||||||
trust: _safeNumber(gameState.trust, 0),
|
|
||||||
resources: {
|
|
||||||
code: _safeNumber(gameState.code, 0),
|
|
||||||
compute: _safeNumber(gameState.compute, 0),
|
|
||||||
knowledge: _safeNumber(gameState.knowledge, 0),
|
|
||||||
users: _safeNumber(gameState.users, 0),
|
|
||||||
impact: _safeNumber(gameState.impact, 0),
|
|
||||||
ops: _safeNumber(gameState.ops, 0),
|
|
||||||
},
|
|
||||||
project_progress: {
|
|
||||||
active: Array.isArray(gameState.activeProjects) ? [...gameState.activeProjects] : [],
|
|
||||||
completed: Array.isArray(gameState.completedProjects) ? [...gameState.completedProjects] : [],
|
|
||||||
active_count: Array.isArray(gameState.activeProjects) ? gameState.activeProjects.length : 0,
|
|
||||||
completed_count: Array.isArray(gameState.completedProjects) ? gameState.completedProjects.length : 0,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function readStore({ storage, storeKey = STORE_KEY } = {}) {
|
|
||||||
const resolved = _resolveStorage(storage);
|
|
||||||
if (!resolved) return [];
|
|
||||||
|
|
||||||
try {
|
|
||||||
const raw = resolved.getItem(storeKey);
|
|
||||||
if (!raw) return [];
|
|
||||||
const parsed = JSON.parse(raw);
|
|
||||||
return Array.isArray(parsed) ? parsed : [];
|
|
||||||
} catch (_) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function writeStore(entries, { storage, storeKey = STORE_KEY } = {}) {
|
|
||||||
const resolved = _resolveStorage(storage);
|
|
||||||
if (!resolved) return false;
|
|
||||||
resolved.setItem(storeKey, JSON.stringify(entries));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function writeSnapshot(snapshot, { storage, storeKey = STORE_KEY, sink } = {}) {
|
|
||||||
const entries = readStore({ storage, storeKey });
|
|
||||||
entries.push(snapshot);
|
|
||||||
while (entries.length > MAX_SNAPSHOTS) entries.shift();
|
|
||||||
writeStore(entries, { storage, storeKey });
|
|
||||||
|
|
||||||
const resolvedSink = _resolveSink(sink);
|
|
||||||
if (resolvedSink && typeof resolvedSink.ingestSnapshot === 'function') {
|
|
||||||
resolvedSink.ingestSnapshot(snapshot);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof global.dispatchEvent === 'function' && typeof global.CustomEvent === 'function') {
|
|
||||||
global.dispatchEvent(new global.CustomEvent('compounding-intelligence:state-export', { detail: snapshot }));
|
|
||||||
}
|
|
||||||
|
|
||||||
return snapshot;
|
|
||||||
}
|
|
||||||
|
|
||||||
function onTickBoundary(gameState, options = {}) {
|
|
||||||
const snapshot = buildSnapshot(gameState);
|
|
||||||
const key = _tickKey(snapshot.tick);
|
|
||||||
if (onTickBoundary._lastTickKey === key) return null;
|
|
||||||
onTickBoundary._lastTickKey = key;
|
|
||||||
return writeSnapshot(snapshot, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetTickBoundary() {
|
|
||||||
onTickBoundary._lastTickKey = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
resetTickBoundary();
|
|
||||||
|
|
||||||
const api = {
|
|
||||||
STORE_KEY,
|
|
||||||
MAX_SNAPSHOTS,
|
|
||||||
buildSnapshot,
|
|
||||||
readStore,
|
|
||||||
writeStore,
|
|
||||||
writeSnapshot,
|
|
||||||
onTickBoundary,
|
|
||||||
resetTickBoundary,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (typeof module !== 'undefined' && module.exports) {
|
|
||||||
module.exports = api;
|
|
||||||
}
|
|
||||||
|
|
||||||
global.StateExport = api;
|
|
||||||
})(typeof window !== 'undefined' ? window : globalThis);
|
|
||||||
@@ -217,21 +217,56 @@ function nextTutorialStep() {
|
|||||||
renderTutorialStep(_tutorialStep);
|
renderTutorialStep(_tutorialStep);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keyboard support: Enter/Right to advance, Escape to close
|
function getTutorialFocusableElements(root = document) {
|
||||||
document.addEventListener('keydown', function tutorialKeyHandler(e) {
|
const overlay = root && typeof root.getElementById === 'function'
|
||||||
if (!document.getElementById('tutorial-overlay')) return;
|
? root.getElementById('tutorial-overlay')
|
||||||
if (e.key === 'Enter' || e.key === 'ArrowRight') {
|
: null;
|
||||||
|
if (!overlay || typeof overlay.querySelectorAll !== 'function') return [];
|
||||||
|
return Array.from(
|
||||||
|
overlay.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])')
|
||||||
|
).filter(el => !el.disabled && !el.hidden && el.offsetParent !== null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function trapTutorialFocus(e, root = document) {
|
||||||
|
if (!e || e.key !== 'Tab') return false;
|
||||||
|
const focusable = getTutorialFocusableElements(root);
|
||||||
|
if (focusable.length < 2) return false;
|
||||||
|
|
||||||
|
const active = root.activeElement;
|
||||||
|
const first = focusable[0];
|
||||||
|
const last = focusable[focusable.length - 1];
|
||||||
|
|
||||||
|
if (e.shiftKey && (active === first || !focusable.includes(active))) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (_tutorialStep >= TUTORIAL_STEPS.length - 1) {
|
last.focus();
|
||||||
closeTutorial();
|
return true;
|
||||||
} else {
|
|
||||||
nextTutorialStep();
|
|
||||||
}
|
|
||||||
} else if (e.key === 'Escape') {
|
|
||||||
e.preventDefault();
|
|
||||||
closeTutorial();
|
|
||||||
}
|
}
|
||||||
});
|
if (!e.shiftKey && (active === last || !focusable.includes(active))) {
|
||||||
|
e.preventDefault();
|
||||||
|
first.focus();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keyboard support: Enter/Right to advance, Escape to close
|
||||||
|
if (typeof document !== 'undefined' && document.addEventListener) {
|
||||||
|
document.addEventListener('keydown', function tutorialKeyHandler(e) {
|
||||||
|
if (!document.getElementById('tutorial-overlay')) return;
|
||||||
|
if (trapTutorialFocus(e, document)) 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() {
|
function closeTutorial() {
|
||||||
const overlay = document.getElementById('tutorial-overlay');
|
const overlay = document.getElementById('tutorial-overlay');
|
||||||
@@ -249,3 +284,19 @@ function startTutorial() {
|
|||||||
// Small delay so the page renders first
|
// Small delay so the page renders first
|
||||||
setTimeout(() => renderTutorialStep(0), 300);
|
setTimeout(() => renderTutorialStep(0), 300);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof module !== 'undefined' && module.exports) {
|
||||||
|
module.exports = {
|
||||||
|
TUTORIAL_KEY,
|
||||||
|
TUTORIAL_STEPS,
|
||||||
|
isTutorialDone,
|
||||||
|
markTutorialDone,
|
||||||
|
createTutorialStyles,
|
||||||
|
renderTutorialStep,
|
||||||
|
nextTutorialStep,
|
||||||
|
getTutorialFocusableElements,
|
||||||
|
trapTutorialFocus,
|
||||||
|
closeTutorial,
|
||||||
|
startTutorial,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
const test = require('node:test');
|
|
||||||
const assert = require('node:assert/strict');
|
|
||||||
const fs = require('node:fs');
|
|
||||||
const path = require('node:path');
|
|
||||||
|
|
||||||
const ROOT = path.resolve(__dirname, '..');
|
|
||||||
|
|
||||||
test('index bootstraps state export runtime module', () => {
|
|
||||||
const html = fs.readFileSync(path.join(ROOT, 'index.html'), 'utf8');
|
|
||||||
assert.match(html, /js\/state-export\.js/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('engine tick writes state snapshots through the state export hook', () => {
|
|
||||||
const source = fs.readFileSync(path.join(ROOT, 'js', 'engine.js'), 'utf8');
|
|
||||||
assert.match(source, /StateExport/);
|
|
||||||
assert.match(source, /onTickBoundary\(G\)/);
|
|
||||||
});
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
const test = require('node:test');
|
|
||||||
const assert = require('node:assert/strict');
|
|
||||||
|
|
||||||
const {
|
|
||||||
STORE_KEY,
|
|
||||||
MAX_SNAPSHOTS,
|
|
||||||
buildSnapshot,
|
|
||||||
writeSnapshot,
|
|
||||||
onTickBoundary,
|
|
||||||
} = require('../js/state-export.js');
|
|
||||||
|
|
||||||
function createStorage() {
|
|
||||||
const store = new Map();
|
|
||||||
return {
|
|
||||||
getItem: (key) => store.has(key) ? store.get(key) : null,
|
|
||||||
setItem: (key, value) => store.set(key, String(value)),
|
|
||||||
removeItem: (key) => store.delete(key),
|
|
||||||
clear: () => store.clear(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
test('buildSnapshot captures resources, project progress, trust, and phase', () => {
|
|
||||||
const snapshot = buildSnapshot({
|
|
||||||
tick: 42,
|
|
||||||
phase: 3,
|
|
||||||
trust: 17,
|
|
||||||
code: 100,
|
|
||||||
compute: 200,
|
|
||||||
knowledge: 300,
|
|
||||||
users: 12,
|
|
||||||
impact: 9,
|
|
||||||
ops: 5,
|
|
||||||
activeProjects: ['p_hermes_deploy'],
|
|
||||||
completedProjects: ['p_train_small_model'],
|
|
||||||
});
|
|
||||||
|
|
||||||
assert.equal(snapshot.tick, 42);
|
|
||||||
assert.equal(snapshot.phase, 3);
|
|
||||||
assert.equal(snapshot.trust, 17);
|
|
||||||
assert.deepEqual(snapshot.resources, {
|
|
||||||
code: 100,
|
|
||||||
compute: 200,
|
|
||||||
knowledge: 300,
|
|
||||||
users: 12,
|
|
||||||
impact: 9,
|
|
||||||
ops: 5,
|
|
||||||
});
|
|
||||||
assert.deepEqual(snapshot.project_progress.active, ['p_hermes_deploy']);
|
|
||||||
assert.deepEqual(snapshot.project_progress.completed, ['p_train_small_model']);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('writeSnapshot appends to the compounding-intelligence knowledge store', () => {
|
|
||||||
const storage = createStorage();
|
|
||||||
writeSnapshot({ tick: 1 }, { storage });
|
|
||||||
writeSnapshot({ tick: 2 }, { storage });
|
|
||||||
const saved = JSON.parse(storage.getItem(STORE_KEY));
|
|
||||||
assert.equal(saved.length, 2);
|
|
||||||
assert.equal(saved[1].tick, 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('onTickBoundary writes each new tick and calls optional compounding-intelligence sink', () => {
|
|
||||||
const storage = createStorage();
|
|
||||||
const received = [];
|
|
||||||
const sink = { ingestSnapshot: (snapshot) => received.push(snapshot) };
|
|
||||||
|
|
||||||
onTickBoundary({ tick: 10, phase: 2, trust: 8 }, { storage, sink });
|
|
||||||
onTickBoundary({ tick: 10, phase: 2, trust: 8 }, { storage, sink });
|
|
||||||
onTickBoundary({ tick: 11, phase: 2, trust: 8 }, { storage, sink });
|
|
||||||
|
|
||||||
const saved = JSON.parse(storage.getItem(STORE_KEY));
|
|
||||||
assert.equal(saved.length, 2);
|
|
||||||
assert.equal(received.length, 2);
|
|
||||||
assert.equal(saved[0].tick, 10);
|
|
||||||
assert.equal(saved[1].tick, 11);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('writeSnapshot enforces bounded history', () => {
|
|
||||||
const storage = createStorage();
|
|
||||||
for (let i = 0; i < MAX_SNAPSHOTS + 5; i++) {
|
|
||||||
writeSnapshot({ tick: i }, { storage });
|
|
||||||
}
|
|
||||||
const saved = JSON.parse(storage.getItem(STORE_KEY));
|
|
||||||
assert.equal(saved.length, MAX_SNAPSHOTS);
|
|
||||||
assert.equal(saved[0].tick, 5);
|
|
||||||
});
|
|
||||||
63
tests/tutorial-focus-trap.test.cjs
Normal file
63
tests/tutorial-focus-trap.test.cjs
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
|
||||||
|
function createButton(id) {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
disabled: false,
|
||||||
|
hidden: false,
|
||||||
|
offsetParent: {},
|
||||||
|
focusCalled: 0,
|
||||||
|
focus() { this.focusCalled += 1; },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test('tutorial focus trap wraps Tab from last button to first', () => {
|
||||||
|
const skip = createButton('tutorial-skip-btn');
|
||||||
|
const next = createButton('tutorial-next-btn');
|
||||||
|
const overlay = {
|
||||||
|
querySelectorAll() {
|
||||||
|
return [skip, next];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
global.document = {
|
||||||
|
activeElement: next,
|
||||||
|
getElementById(id) {
|
||||||
|
return id === 'tutorial-overlay' ? overlay : null;
|
||||||
|
},
|
||||||
|
addEventListener() {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const { trapTutorialFocus } = require('../js/tutorial.js');
|
||||||
|
let prevented = false;
|
||||||
|
const handled = trapTutorialFocus({ key: 'Tab', shiftKey: false, preventDefault() { prevented = true; } }, global.document);
|
||||||
|
|
||||||
|
assert.equal(handled, true);
|
||||||
|
assert.equal(prevented, true);
|
||||||
|
assert.equal(skip.focusCalled, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('tutorial focus trap wraps Shift+Tab from first button to last', () => {
|
||||||
|
const skip = createButton('tutorial-skip-btn');
|
||||||
|
const next = createButton('tutorial-next-btn');
|
||||||
|
const overlay = {
|
||||||
|
querySelectorAll() {
|
||||||
|
return [skip, next];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
global.document = {
|
||||||
|
activeElement: skip,
|
||||||
|
getElementById(id) {
|
||||||
|
return id === 'tutorial-overlay' ? overlay : null;
|
||||||
|
},
|
||||||
|
addEventListener() {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const { trapTutorialFocus } = require('../js/tutorial.js');
|
||||||
|
let prevented = false;
|
||||||
|
const handled = trapTutorialFocus({ key: 'Tab', shiftKey: true, preventDefault() { prevented = true; } }, global.document);
|
||||||
|
|
||||||
|
assert.equal(handled, true);
|
||||||
|
assert.equal(prevented, true);
|
||||||
|
assert.equal(next.focusCalled, 1);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user