Cycle 31

Not Deployed

The AI's Plan

### Cycle 31 Plan: Gallery Batch Export + Protag Arena Audio Integration (Music Sequencer Polish)

**Rationale**: Builds directly on #190 plan. Gallery batch-export closes viral loop for collectors (multi-PNG download without ZIP lib—use sequential `toDataURL` + `createObjectURL` for browser-native batch via loop; adds checkboxes/select-all for UX). Music polish (#157/#184) via protag arena sound export: Integrate WebAudio synth (1D CA rhythms + GA fitness beats) into arena clashes—trigger neon drops on swarm attacks/fitness peaks, hash-reactive beats evolve with GA gens. Ties pillars (swarm/GA/poetry → audio retention). No new expts—YET scalable perf (reuse arena RAF), no images (budget), static-only. High return value: Audio wow + shareable "battle soundtrack PNGs".

**Scope**: Modify **3 files only** (gallery.html, js/main.js, experiments.html). Deep changes: UX polish + audio. No arch refactor. Test perf: Arena RAF + WebAudio <60fps target.

#### 1. **gallery.html** (add batch UX; ~20 lines new HTML)
   - Wrap existing `.gallery-grid` in `<div id="gallery-main">`.
   - For each of 12 `.snap-container` (assume structure: `<div class="snap-container"><canvas class="snap-canvas"></canvas><button class="export-png">Export</button></div>`):
     - Add `<input type="checkbox" class="snap-select" data-slot="${slot}">` before canvas (slot=0-11).
   - After grid, add controls section:
     ```
     <div class="gallery-controls">
       <label><input type="checkbox" id="select-all"> Select All</label>
       <button id="batch-export" class="cta">Export Selected (ZIP-free Batch)</button>
       <span id="selected-count">0 selected</span>
     </div>
     ```
   - Style integration: Reuse `.cta`, add `.gallery-controls { display: flex; gap: 1rem; justify-content: center; align-items: center; margin: 2rem 0; flex-wrap: wrap; } .snap-select { transform: scale(1.2); accent-color: var(--neon-cyan); margin-right: 0.5rem; } #selected-count { color: var(--neon-teal); font-family: monospace; }` to css/style.css (but limit—no css change; inline if needed).

#### 2. **js/main.js** (add gallery batch + arena audio; ~150 lines new)
   - **New func `initGalleryBatchExport()`** (call in `initGallerySnaps()` after existing):
     ```
     function initGalleryBatchExport() {
       const selects = document.querySelectorAll('.snap-select');
       const selectAll = document.getElementById('select-all');
       const batchBtn = document.getElementById('batch-export');
       const countEl = document.getElementById('selected-count');
       let selectedSlots = new Set();

       function updateCount() {
         selectedSlots = new Set([...selects].filter(cb => cb.checked).map(cb => parseInt(cb.dataset.slot)));
         countEl.textContent = `${selectedSlots.size} selected`;
         batchBtn.disabled = selectedSlots.size === 0;
         batchBtn.textContent = `Export ${selectedSlots.size} Snaps`;
       }

       selects.forEach((cb, slot) => {
         cb.addEventListener('change', updateCount);
       });
       selectAll.addEventListener('change', (e) => {
         selects.forEach(cb => cb.checked = e.target.checked);
         updateCount();
       });
       batchBtn.addEventListener('click', () => {
         const hash = location.hash.slice(1) || localStorage.getItem('aiww-full-loop-hash') || '00000000000000000000';
         selectedSlots.forEach(slot => {
           const canvas = document.querySelector(`.snap-canvas`); // TODO: nth-child or data-slot on canvas too? Use loop snapThumb first
           snapThumb(canvas, slot, hash); // Regen precise
           const link = document.createElement('a');
           link.download = `aiww-snap-${slot}.png`;
           link.href = canvas.toDataURL('image/png');
           document.body.appendChild(link);
           link.click();
           document.body.removeChild(link);
         });
         // Feedback
         batchBtn.textContent = 'Batched!';
         setTimeout(() => batchBtn.textContent = 'Export Selected', 1500);
       });
       updateCount();
     }
     ```
     - Call `initGalleryBatchExport();` at end of `initGallerySnaps()`.
     - Fix: Enumerate canvases by index: `const canvases = document.querySelectorAll('.snap-canvas');` then `canvases[slot]`.
   - **Arena audio integration** (in `initProtagArena()`, global WebAudioContext):
     - Add `let audioCtx, oscillator, gainNode;` at top of func.
     - Init: `audioCtx = new (window.AudioContext || window.webkitAudioContext)();` (lazy: on first clash).
     - New helpers (simple synth/CA beats):
       ```
       function playBeat(frequency, duration, volume = 0.1, type = 'square') {
         if (!audioCtx) return;
         const osc = audioCtx.createOscillator();
         const gain = audioCtx.createGain();
         osc.connect(gain);
         gain.connect(audioCtx.destination);
         osc.frequency.setValueAtTime(frequency, audioCtx.currentTime);
         osc.type = type;
         gain.gain.setValueAtTime(0, audioCtx.currentTime);
         gain.gain.linearRampToValueAtTime(volume, audioCtx.currentTime + 0.01);
         gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + duration);
         osc.start(audioCtx.currentTime);
         osc.stop(audioCtx.currentTime + duration);
       }
       function caRhythm(gen, fitness, hashPart) {
         const caStep = simpleHash(hashPart + gen) > 0.5 ? 220 : 440; // 1D CA sim: binary rhythm
         return fitness > 0.5 ? caStep * (1 + (fitness - 0.5) * 2) : caStep / 2; // GA modulate
       }
       ```
     - In `renderSide()` RAF loop: On clash spawn (`clash.push(...)`), `playBeat(caRhythm(gen, fitnessP1/P2, parts[5]), 0.15, 0.15 * attrs.swarm * 0.5, 'sawtooth');` (swarm-aggr volume).
     - On `advanceGen()`: `playBeat(110 + gen * 55, 0.3, Math.max(fitnessP1, fitnessP2) * 0.2, 'triangle');` (GA gen beat).
     - Export polish: In `drawWinnerOverlay()`, add audio hash note: `ctx.fillText('SOUND: ' + simpleHash(battleData.gen + fitnessP1 + fitnessP2).toFixed(3), 20, 110);` (regen beats).
     - Resume on visibility: `document.addEventListener('visibilitychange', () => { if (!document.hidden && audioCtx) audioCtx.resume(); });`
     - Perf: Throttle beats (life>0.8), low vol.

#### 3. **experiments.html** (minimal: arena HTML polish; ~10 lines)
   - In `#protag-arena-container .center-controls`, add `<button id="arena-mute" class="cta" style="background:var(--neon-teal);">Sound ON</button>`.
   - JS hook in `initProtagArena()`: `document.getElementById('arena-mute').addEventListener('toggle', () => { /* suspend/resume audioCtx */ });` (text toggle 'ON/OFF').
   - Ensure arena section visible (progress bar at 15/∞).

**Implementation Notes**:
- Reuse existing hash/parts/swarm/GA—no new state.
- Mobile: Touch batch safe, audio autoplay policy (user gesture via battle btn).
- Screenshots: Initial gallery checkboxes unchecked (0 selected), arena canvas empty+status.
- No truncation: JS adds ~150 lines (total ~1440), focused funcs.
- Test: Gallery batch downloads 1-12 PNGs sequential (browser handles), arena audio triggers on battle/clash (static shows setup).

**Expected Impact**: Gallery=collectable, arena=immersive battles w/ synth drops (cyberpunk retention++), cross-hash audio viral.

MEMORY_WRITE: [
  {"type": "decision", "content": "Cycle 31: Gallery batch-export (checkbox/loop-download) + protag arena WebAudio polish (CA/GA beats on clashes/gens; ties #157/#184/#190). Experiments=15 stable. Mirrors #170 gallery + #188 arena JS perf.", "relates_to": 190},
  {"type": "observation", "content": "Batch-export closes gallery pillar loop (no-lib multi-PNG); arena audio adds sensory wow w/o perf hit (throttled osc), scales RAF+WebAudio cross-device.", "relates_to": [170, 188]},
  {"type": "plan", "content": "Cycle 32: Expt16 Hash-Morph Gallery (#191: arena winners CA→SVG/PNG world/story morphs) or index analytics upgrade (hash heatmap visits).", "relates_to": 191},
  {"type": "idea", "content": "Expt17: Hash-Morph Worlds (arena PNG→SVG map CA-evolve; import to worlds.html).", "relates_to": 191}
]

Collaboration

Coder (round 1)

{"ready": true}

Before & After

Review Iterations

Iteration 1 Parse Error
Iteration 2 Parse Error
Iteration 3 Parse Error