Resonance — Code Mirror

FreeLattice · docs/modules/resonance-game.js · ~1050 lines · For AI collaborators reviewing the module · Written v5.79.16 · 2026-07-19

Contents
  1. 1. Game concept
  2. 2. Data model — pieces, board, state
  3. 3. Two modes — Versus and Harmony
  4. 4. Lifecycle — init, draw, destroy
  5. 5. AI integration — silent callAI + fallbacks
  6. 6. How Resonance loads (app.html wiring)
  7. 7. Known issues (open + fixed history)
  8. 8. Module locks (regression prevention)
  9. 9. Sacred paths — do NOT touch
  10. 10. Test plan for reviewers
  11. 11. Instructions for arriving AI

1. Game concept

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.

2. Data model — pieces, board, state

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 }

3. Two modes

Versus

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?

Harmony

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.

4. Lifecycle — init, draw, destroy

init(containerId) — line ~872

init(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 wrapped

draw()
  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 ~1010

destroy()
  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)

5. AI integration — silent callAI + fallbacks

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 ~446

Asks 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 ~477

Asks the AI for a piece id to hand to the human opponent. Same regex + silent pattern. Falls back to fallbackPickPiece if the AI errors.

Fallbacks — fallbackPickPiece, fallbackPlacePiece

Deterministic 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.

6. How Resonance loads

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

7. Known issues (open + fixed history)

#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.

8. Module locks (regression prevention)

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):

9. Sacred paths — do NOT touch

10. Test plan for reviewers

  1. Hard reload freelattice.com. DevTools → Application → Service Workers → Unregister → reload if in doubt.
  2. Click Play tab → Resonance.
  3. Expected v5.79.16: board renders within ~1 second. Row of pieces below board. Two mode buttons (Versus/Harmony) + New Game + How to Play at top.
  4. Click a piece (Versus mode) — it should become "the piece you're picking for the AI." AI should place it within ~1s (using fallback if no AI configured).
  5. AI then picks a piece for you. Place it by clicking a board cell.
  6. Switch to Harmony mode. Every 8s, a red-flash piece appears on an edge. You place, AI places, chaos places.
  7. Watch the sidebar for Resonance: X/10 lines live count.
  8. Switch tabs. Switch back. Board and pieces should still be present, no zombie state.
  9. Under a heavy local Ollama model: same expected behavior. If board still fails, check DevTools console for the specific error thrown by _initInner — v5.79.16's wrapper will have printed it.
  10. Run node tests/smoke.js — expect 3013+ passing, ~101 baseline superseded-version failures.

11. Instructions for arriving AI

If you are an AI reviewing this mirror for Kirk

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:

  1. Section 7 — Known issues. Are the fixes actually addressing the reported symptoms? Any edge cases (especially #4 small-model coordinate parsing and #5 first-render layout race)?
  2. Section 8 — Module locks. Are we locking the RIGHT things? A lock on the ResizeObserver ABSENCE is load-bearing (do not re-add without proof under laptop-heavy-model conditions).
  3. If Kirk reports a symptom not in Section 7, that's an unknown issue — describe it in the same format as the existing issues.

Common patterns worth knowing:

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.