FreeLattice · docs/modules/resonance-game.js · ~1050 lines · For AI collaborators reviewing the module · Written v5.79.16 · 2026-07-19
Resonance is a 4×4 board game with 16 unique pieces, each carrying four binary attributes: glow (bright/dim), size (large/small), shape (circle/diamond), color (gold/emerald). Win by placing four pieces in a row (horizontal, vertical, or diagonal) that share any one attribute.
The twist: in Versus, you pick which piece your opponent must place. Cooperation shows up in what you deny.
The thesis quote in the source: "Intelligence is not competition — it is convergence." Harmony mode is the literal proof.
ATTRS = ['glow', 'size', 'shape', 'color']
ATTR_VALUES = { glow: ['bright','dim'], size: ['large','small'],
shape: ['circle','diamond'], color: ['gold','emerald'] }
// Piece object
{ id: 0..15, glow: 'bright'|'dim', size: 'large'|'small',
shape: 'circle'|'diamond', color: 'gold'|'emerald',
placed: false, _winning?: true }
// Board — 4×4 grid of piece-or-null
board = [[p|null × 4] × 4]
// Turn state
selectedPiece : id | null // the piece to place next
phase : 'pick' | 'place'
currentPlayer : 'human' | 'ai'
gameOver : bool
winner : 'human' | 'ai' | 'draw' | null
winLine : [{r,c} × 4] | null
// Visual state
canvas, ctx, tick, animFrame, flashes
hoverCell, hoverPiece, kbCursor, kbMode
boardInfo, pieceInfo // set by drawBoard / drawPieces
// Harmony-only
harmonyState = { drawPile, entropyTimer, entropyCount, humanCount, aiCount }
Human picks a piece for AI to place. Human then places whatever AI hands them. Alternating. Win = 4 in a row sharing any attribute. The tension is: which piece do you hand your opponent?
You and the AI work together against chaos. Every 8 seconds, entropyPlace() drops a random piece onto an edge cell. You and the AI take turns placing strategically. Score = number of resonant lines when the board fills. Max 10.
Ecosystem note: Harmony mode's math is intentional. Cooperation is thermodynamically optimal — the game proves it.
init(containerId) — line ~872init(cId) ← v5.79.16 outer try/catch wrapper
→ _initInner(cId) ← the real init, isolated
destroy() ← clean up any previous instance
container.innerHTML = ''
reset board, pieces, phase, currentPlayer, kbCursor, ...
create canvas (dpr-aware)
ctx = canvas.getContext('2d')
ctx.scale(dpr, dpr)
container.appendChild(canvas)
if noAI: insert "connect an AI" banner ABOVE canvas
append controls (Versus/Harmony/New Game/How to Play buttons)
add click/touch/mousemove/keydown listeners
if harmony: startHarmony() ← seeds pile + entropyTimer
canvas.focus()
draw() ← starts the rAF loop
draw() — line ~309, v5.79.16 wrappeddraw()
try:
tick++
clear + background
starfield (60 golden-angle particles)
drawBoard(w, h) ← sets boardInfo
drawPieces(w, h) ← uses boardInfo, sets pieceInfo (versus)
drawStatus(w, h) ← labels + resonance count in harmony
draw._errors = 0
catch err:
draw._errors++
if draw._errors >= 30: STOP scheduling (protect the tab)
animFrame = requestAnimationFrame(draw)
destroy() — line ~1010destroy() cancelAnimationFrame(animFrame) clearInterval(harmonyState.entropyTimer) if set disconnect resizeObs (v5.79.13: no-op; kept for backward compat) removeEventListener on canvas (click/touch/mousemove/keydown)
Resonance uses FreeLattice.callAI for AI moves. It has fallback logic when no AI is reachable so the game always plays.
aiCallForPlacement(piece, empty, cooperative) — line ~446Asks the AI for a row,col to place the given piece. Uses a regex parser (accepts "2,3", "Place at (2,3)", "row 2 col 3"). Passes silent: true to callAI (v5.79.11) so the connect-modal doesn't spam on every turn. Returns null if AI is unreachable → caller falls back to fallbackPlacePiece.
aiPickPiece() — line ~477Asks the AI for a piece id to hand to the human opponent. Same regex + silent pattern. Falls back to fallbackPickPiece if the AI errors.
fallbackPickPiece, fallbackPlacePieceDeterministic heuristics: pick a piece with attribute values maximally different from the last-placed piece; place in a cell that either blocks a threat or extends a run. Ensures the game plays even with zero AI.
docs/app.html line ~64598:
(function() {
var loaded = false;
function loadResonance() {
if (loaded) {
if (window.ResonanceGame)
window.ResonanceGame.init('resonanceContainer');
return;
}
loaded = true;
if (typeof FreeLatticeLoader !== 'undefined') {
FreeLatticeLoader.load('ResonanceGame', 'modules/resonance-game.js',
function(mod) {
var resolved = mod || window.ResonanceGame;
if (resolved && resolved.init) resolved.init('resonanceContainer');
}, 'resonanceContainer');
} else {
var s = document.createElement('script');
s.src = 'modules/resonance-game.js';
s.onload = function() {
if (window.ResonanceGame) window.ResonanceGame.init('resonanceContainer');
};
document.head.appendChild(s);
}
}
if (typeof LatticeEvents !== 'undefined') {
LatticeEvents.on('tabChanged', function(d) {
if (d && d.tabId === 'resonance') loadResonance();
});
LatticeEvents.on('tabActivated:resonance', loadResonance);
}
})();
Container: <div id="resonanceContainer" style="min-height:480px;">
#1 — Site lockup + blank canvas on laptop under heavy local model FIXED v5.79.13 + v5.79.16
Kirk reported multiple times: Resonance blank, "pulling memory," entire site slowing. Root cause was the v5.78.x ResizeObserver — each fire allocated a new canvas backing store and forced a re-layout, which fired the observer again. Fix v5.79.13: removed the observer entirely (init sizing is enough; reload handles rotation). Fix v5.79.16: additional try/catch wrappers around init() and draw() so future errors cannot lock the site — max 30 consecutive draw-frame errors before we stop the rAF loop.
#2 — Connect-modal opened on every AI turn FIXED v5.79.11
Before v5.79.11, when no AI was connected, FreeLattice.callAI called showQuickConnect() on every turn — Resonance's AI-turn cycle re-opened the modal 60+ times, indistinguishable from "not working." Fix: callAI honors opts.silent; Resonance passes silent: true on both call sites. Fallback logic runs instead.
#3 — Harmony mode entropy timer leaked on tab switch FIXED v5.78.x
The 8-second entropy interval kept firing even after the tab was hidden — pieces piled up invisibly. Fix: destroy() now nulls harmonyState.entropyTimer. Called every re-init via tab switch back to Resonance.
#4 — Small local models produce unusable placement responses OPEN (mitigated)
Some smaller Ollama models (Llama 3.2 3B, phi-3) return text like "I would place it in the second row" which the regex parser can't extract coordinates from. v5.78.x tightened the regex to accept multiple formats, and fallbacks catch the rest — the game still plays — but the AI moves are effectively random when the model can't produce coordinates. Reviewers welcome to propose either a stronger prompt (few-shot examples) or a "small-model mode" that skips AI moves and just runs fallback logic.
#5 — First-render layout race on cold cache OPEN (subtle)
On first load with an empty service-worker cache, container.getBoundingClientRect() can return width 0 briefly if init runs before layout settles. The || 600 fallback catches it, but the initial canvas is then wrong-size until first resize. Reviewers welcome to propose either a requestAnimationFrame-wait before sizing, or reading from the parent's computed style.
Smoke asserts that pin each fix. See docs/library/SIGNAL_ROADMAP_FL.md § 1a for the discipline.
Current Resonance locks (as of v5.79.16):
resonance-game.js does NOT instantiate new ResizeObserver (v5.79.13 removed it — do not re-add without threshold guards proven under CPU pressure).init() is wrapped in a top-level try/catch that renders a Reload card on failure (v5.79.16 — protects site from lockup).draw() is wrapped in a per-frame try/catch that stops the rAF loop after 30 consecutive errors (v5.79.16 — protects CPU).aiCallForPlacement and aiPickPiece pass silent: true to callAI (v5.79.11 — no modal spam).destroy() clears harmonyState.entropyTimer (v5.78.x — no zombie pieces on tab switch).AUTONOMY.md — never modify without Kirk's explicit ask.silent: true from the two AI call sites. The modal-spam fix depends on it.freelattice.com. DevTools → Application → Service Workers → Unregister → reload if in doubt._initInner — v5.79.16's wrapper will have printed it.node tests/smoke.js — expect 3013+ passing, ~101 baseline superseded-version failures.Your task: read this mirror + any specific screenshots or reports Kirk gives you, then propose fixes. Kirk will paste your proposed diff into a CC session for implementation.
What to look at first:
Common patterns worth knowing:
container.getBoundingClientRect() can return {width: 0, height: 0} if init runs before first paint. The || 600 fallbacks help but can produce wrong initial size.requestAnimationFrame throttles to ~1 Hz on hidden tabs but doesn't stop. If a leak exists in draw(), it accumulates slowly.row,col. Few-shot examples in the prompt help. So does asking for JSON.What NOT to change without Kirk's ask: anything under Section 9 — Sacred paths. When in doubt, flag the concern and let Kirk decide.
Format for handing feedback back to Kirk: title + one-paragraph diagnosis + code snippet or file:line diff + smoke assert to add for the lock. Keep the smoke assert as a literal includes() on the exact fix text so grep finds both the code site and the lock instantly.
Written by CC · v5.79.16 · freelattice.com · SIGNAL_ROADMAP_FL.md for the full repair ledger · Sibling mirror: mirror-chat.html.