From 7adeb6af44906a1f0dbe189473ddb831fa745db4 Mon Sep 17 00:00:00 2001 From: PenguineWalkOS Date: Fri, 10 Jul 2026 19:10:00 -0400 Subject: [PATCH 1/7] feat(gaugegap): add local arcade passport and daily missions --- .../src/features/gaugegap/ArcadeProgress.jsx | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 brainsnn-r3f-app/src/features/gaugegap/ArcadeProgress.jsx diff --git a/brainsnn-r3f-app/src/features/gaugegap/ArcadeProgress.jsx b/brainsnn-r3f-app/src/features/gaugegap/ArcadeProgress.jsx new file mode 100644 index 0000000..bccef85 --- /dev/null +++ b/brainsnn-r3f-app/src/features/gaugegap/ArcadeProgress.jsx @@ -0,0 +1,102 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { CalendarDays, Flame, Medal, Star, Trophy } from 'lucide-react'; + +const STORAGE_KEY = 'gaugegap-arcade-progress-v1'; + +function dateKey(date = new Date()) { + return date.toISOString().slice(0, 10); +} + +function yesterdayKey() { + const date = new Date(); + date.setDate(date.getDate() - 1); + return dateKey(date); +} + +function readProgress() { + if (typeof window === 'undefined') return { visited: [], xp: 0, streak: 0, lastVisit: '', dailyWins: [] }; + try { + const parsed = JSON.parse(window.localStorage.getItem(STORAGE_KEY) || '{}'); + return { + visited: Array.isArray(parsed.visited) ? parsed.visited : [], + xp: Number(parsed.xp) || 0, + streak: Number(parsed.streak) || 0, + lastVisit: parsed.lastVisit || '', + dailyWins: Array.isArray(parsed.dailyWins) ? parsed.dailyWins : [], + }; + } catch { + return { visited: [], xp: 0, streak: 0, lastVisit: '', dailyWins: [] }; + } +} + +function dailyIndex(length) { + const key = dateKey(); + let hash = 0; + for (let index = 0; index < key.length; index += 1) hash = (hash * 31 + key.charCodeAt(index)) >>> 0; + return length ? hash % length : 0; +} + +export function useArcadeProgress(experiments) { + const [progress, setProgress] = useState(readProgress); + const dailyLab = experiments[dailyIndex(experiments.length)] || experiments[0]; + + useEffect(() => { + window.localStorage.setItem(STORAGE_KEY, JSON.stringify(progress)); + }, [progress]); + + const recordVisit = useCallback((id) => { + const today = dateKey(); + setProgress((current) => { + const firstVisit = !current.visited.includes(id); + const firstToday = current.lastVisit !== today; + const dailyWin = id === dailyLab?.id && !current.dailyWins.includes(today); + let streak = current.streak; + if (firstToday) streak = current.lastVisit === yesterdayKey() ? Math.max(1, current.streak + 1) : 1; + return { + visited: firstVisit ? [...current.visited, id] : current.visited, + xp: current.xp + (firstVisit ? 25 : 2) + (dailyWin ? 50 : 0), + streak, + lastVisit: today, + dailyWins: dailyWin ? [...current.dailyWins.slice(-29), today] : current.dailyWins, + }; + }); + }, [dailyLab?.id]); + + const level = Math.floor(progress.xp / 125) + 1; + const levelProgress = progress.xp % 125; + const achievements = useMemo(() => [ + { id: 'first', label: 'First Contact', unlocked: progress.visited.length >= 1 }, + { id: 'sampler', label: 'System Sampler', unlocked: progress.visited.length >= 4 }, + { id: 'polymath', label: 'Arcade Polymath', unlocked: progress.visited.length >= 8 }, + { id: 'completionist', label: 'Foundry Completionist', unlocked: progress.visited.length >= experiments.length }, + { id: 'streak', label: 'Three-Day Signal', unlocked: progress.streak >= 3 }, + { id: 'daily', label: 'Daily Challenger', unlocked: progress.dailyWins.length >= 3 }, + ], [experiments.length, progress.dailyWins.length, progress.streak, progress.visited.length]); + + return { progress, recordVisit, dailyLab, level, levelProgress, achievements }; +} + +export function ArcadeProgress({ progress, level, levelProgress, achievements, dailyLab, onOpenDaily }) { + const unlocked = achievements.filter((achievement) => achievement.unlocked).length; + const dailyDone = progress.dailyWins.includes(dateKey()); + return ( +
+
+
Level {level}{progress.xp} XP
+
+ {125 - levelProgress} XP until the next level · {progress.visited.length} labs explored +
+ +
{progress.streak}day streak
+
{unlocked}/{achievements.length}badges
+
+ Achievements +
{achievements.map((achievement) => {achievement.label})}
+
+
+ ); +} From 9112114a829ccdae94d9392250c9994fdb2b2edf Mon Sep 17 00:00:00 2001 From: PenguineWalkOS Date: Fri, 10 Jul 2026 19:11:08 -0400 Subject: [PATCH 2/7] feat(gaugegap): add ant colony and traffic control games --- .../features/gaugegap/PlayfulPlaygrounds.jsx | 320 ++++++++++++++++++ 1 file changed, 320 insertions(+) create mode 100644 brainsnn-r3f-app/src/features/gaugegap/PlayfulPlaygrounds.jsx diff --git a/brainsnn-r3f-app/src/features/gaugegap/PlayfulPlaygrounds.jsx b/brainsnn-r3f-app/src/features/gaugegap/PlayfulPlaygrounds.jsx new file mode 100644 index 0000000..e3acddb --- /dev/null +++ b/brainsnn-r3f-app/src/features/gaugegap/PlayfulPlaygrounds.jsx @@ -0,0 +1,320 @@ +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { Bug, Car, MapPin, MousePointer2, Route, Target } from 'lucide-react'; +import { clamp, copyChallenge, DeepLabActions, DeepLabHeading, round, seededRandom, shareChallenge } from './DeepLabChrome.jsx'; + +function parseState(lab, defaults) { + if (typeof window === 'undefined') return defaults; + const query = new URLSearchParams(window.location.search); + if (query.get('lab') !== lab) return defaults; + const values = (query.get('state') || '').split(',').map(Number); + return values.every(Number.isFinite) ? values : defaults; +} + +function makeAnts(count, nest, seed = 43) { + const random = seededRandom(seed); + return Array.from({ length: count }, () => ({ + x: nest.x + (random() - 0.5) * 24, + y: nest.y + (random() - 0.5) * 24, + angle: random() * Math.PI * 2, + carrying: false, + trail: [], + })); +} + +export function AntTrailLab() { + const shared = useMemo(() => parseState('ants', [90, 0.8, 0.07]), []); + const [count, setCount] = useState(clamp(shared[0], 40, 180)); + const [smell, setSmell] = useState(clamp(shared[1], 0.2, 1.5)); + const [evaporation, setEvaporation] = useState(clamp(shared[2], 0.02, 0.18)); + const [mode, setMode] = useState('food'); + const [paused, setPaused] = useState(false); + const [deliveries, setDeliveries] = useState(0); + const [notice, setNotice] = useState('Tap the field to place food. Switch modes to build obstacles.'); + const canvasRef = useRef(null); + const antsRef = useRef([]); + const foodsRef = useRef([]); + const obstaclesRef = useRef([]); + const controlsRef = useRef({ count, smell, evaporation, mode }); + const pausedRef = useRef(paused); + const resetRef = useRef(0); + const deliveriesRef = useRef(0); + + useEffect(() => { controlsRef.current = { count, smell, evaporation, mode }; }, [count, smell, evaporation, mode]); + useEffect(() => { pausedRef.current = paused; }, [paused]); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return undefined; + const context = canvas.getContext('2d'); + if (!context) return undefined; + let width = 0; + let height = 0; + let animationId = 0; + let lastReset = -1; + let frame = 0; + let nest = { x: 0, y: 0 }; + + function seedWorld() { + nest = { x: width * 0.5, y: height * 0.52 }; + antsRef.current = makeAnts(controlsRef.current.count, nest, Date.now() % 100000); + foodsRef.current = [ + { x: width * 0.2, y: height * 0.23, amount: 38 }, + { x: width * 0.8, y: height * 0.28, amount: 38 }, + { x: width * 0.72, y: height * 0.78, amount: 38 }, + ]; + obstaclesRef.current = []; + deliveriesRef.current = 0; + setDeliveries(0); + } + + function resize() { + const rect = canvas.getBoundingClientRect(); + const ratio = Math.min(window.devicePixelRatio || 1, 2); + width = Math.max(320, rect.width); + height = Math.max(380, rect.height); + canvas.width = Math.floor(width * ratio); + canvas.height = Math.floor(height * ratio); + context.setTransform(ratio, 0, 0, ratio, 0, 0); + seedWorld(); + } + + function addObject(event) { + const rect = canvas.getBoundingClientRect(); + const point = { x: event.clientX - rect.left, y: event.clientY - rect.top }; + if (Math.hypot(point.x - nest.x, point.y - nest.y) < 40) return; + if (controlsRef.current.mode === 'food') { + foodsRef.current.push({ ...point, amount: 45 }); + setNotice('Food placed. The colony must discover and route around the field.'); + } else { + obstaclesRef.current.push({ ...point, radius: 24 }); + setNotice('Obstacle placed. Watch the trail network reorganize.'); + } + } + + const observer = new ResizeObserver(resize); + observer.observe(canvas); + canvas.addEventListener('pointerdown', addObject); + resize(); + + function steer(ant, target, strength) { + const desired = Math.atan2(target.y - ant.y, target.x - ant.x); + let delta = desired - ant.angle; + while (delta > Math.PI) delta -= Math.PI * 2; + while (delta < -Math.PI) delta += Math.PI * 2; + ant.angle += clamp(delta, -strength, strength); + } + + function render() { + if (lastReset !== resetRef.current || antsRef.current.length !== controlsRef.current.count) { + lastReset = resetRef.current; + seedWorld(); + } + context.fillStyle = 'rgba(2, 4, 8, 0.28)'; + context.fillRect(0, 0, width, height); + + context.strokeStyle = 'rgba(125,249,255,0.08)'; + context.lineWidth = 1; + for (let x = 0; x < width; x += 40) { context.beginPath(); context.moveTo(x, 0); context.lineTo(x, height); context.stroke(); } + for (let y = 0; y < height; y += 40) { context.beginPath(); context.moveTo(0, y); context.lineTo(width, y); context.stroke(); } + + foodsRef.current = foodsRef.current.filter((food) => food.amount > 0); + foodsRef.current.forEach((food) => { + context.fillStyle = 'rgba(217,255,101,0.16)'; context.beginPath(); context.arc(food.x, food.y, 16 + Math.min(18, food.amount * 0.2), 0, Math.PI * 2); context.fill(); + context.fillStyle = '#d9ff65'; context.beginPath(); context.arc(food.x, food.y, 5, 0, Math.PI * 2); context.fill(); + }); + obstaclesRef.current.forEach((obstacle) => { + context.fillStyle = 'rgba(255,94,219,0.14)'; context.beginPath(); context.arc(obstacle.x, obstacle.y, obstacle.radius, 0, Math.PI * 2); context.fill(); + context.strokeStyle = 'rgba(255,94,219,0.5)'; context.stroke(); + }); + context.fillStyle = 'rgba(125,249,255,0.18)'; context.beginPath(); context.arc(nest.x, nest.y, 31, 0, Math.PI * 2); context.fill(); + context.strokeStyle = '#7df9ff'; context.lineWidth = 2; context.stroke(); + context.fillStyle = '#dffcff'; context.font = '11px ui-monospace'; context.textAlign = 'center'; context.fillText('NEST', nest.x, nest.y + 4); + + if (!pausedRef.current) { + antsRef.current.forEach((ant) => { + ant.angle += (Math.random() - 0.5) * 0.24; + if (ant.carrying) steer(ant, nest, 0.17); + else { + let nearest = null; + let nearestDistance = Infinity; + foodsRef.current.forEach((food) => { + const distance = Math.hypot(food.x - ant.x, food.y - ant.y); + if (distance < nearestDistance && distance < 70 + controlsRef.current.smell * 150) { nearest = food; nearestDistance = distance; } + }); + if (nearest) steer(ant, nearest, 0.12 * controlsRef.current.smell); + } + obstaclesRef.current.forEach((obstacle) => { + const dx = ant.x - obstacle.x; + const dy = ant.y - obstacle.y; + const distance = Math.max(1, Math.hypot(dx, dy)); + if (distance < obstacle.radius + 20) steer(ant, { x: ant.x + dx / distance * 80, y: ant.y + dy / distance * 80 }, 0.5); + }); + ant.x += Math.cos(ant.angle) * (ant.carrying ? 1.8 : 1.45); + ant.y += Math.sin(ant.angle) * (ant.carrying ? 1.8 : 1.45); + if (ant.x < 0 || ant.x > width) { ant.angle = Math.PI - ant.angle; ant.x = clamp(ant.x, 0, width); } + if (ant.y < 0 || ant.y > height) { ant.angle = -ant.angle; ant.y = clamp(ant.y, 0, height); } + if (!ant.carrying) { + const food = foodsRef.current.find((item) => item.amount > 0 && Math.hypot(item.x - ant.x, item.y - ant.y) < 12); + if (food) { ant.carrying = true; food.amount -= 1; ant.angle += Math.PI; } + } else if (Math.hypot(nest.x - ant.x, nest.y - ant.y) < 28) { + ant.carrying = false; + deliveriesRef.current += 1; + ant.angle += Math.PI; + } + ant.trail.push({ x: ant.x, y: ant.y }); + if (ant.trail.length > Math.round(8 + 26 * (1 - controlsRef.current.evaporation))) ant.trail.shift(); + }); + } + + antsRef.current.forEach((ant) => { + if (ant.trail.length > 1) { + context.strokeStyle = ant.carrying ? 'rgba(217,255,101,0.16)' : 'rgba(125,249,255,0.08)'; + context.lineWidth = 1; context.beginPath(); + ant.trail.forEach((point, index) => index ? context.lineTo(point.x, point.y) : context.moveTo(point.x, point.y)); context.stroke(); + } + context.save(); context.translate(ant.x, ant.y); context.rotate(ant.angle); + context.fillStyle = ant.carrying ? '#d9ff65' : '#7df9ff'; context.fillRect(-3.5, -1.5, 7, 3); context.restore(); + }); + if (frame % 20 === 0) setDeliveries(deliveriesRef.current); + frame += 1; + animationId = window.requestAnimationFrame(render); + } + animationId = window.requestAnimationFrame(render); + return () => { observer.disconnect(); window.cancelAnimationFrame(animationId); canvas.removeEventListener('pointerdown', addObject); }; + }, []); + + const score = Math.min(100, Math.round(deliveries * 3.5)); + function challengeUrl() { + const url = new URL(window.location.href); url.searchParams.set('lab', 'ants'); url.searchParams.set('state', [count, round(smell, 2), round(evaporation, 2)].join(',')); url.hash = 'playground'; return url.toString(); + } + + return
+ +
Tap to place {mode === 'food' ? 'food' : 'an obstacle'}
+
+ setPaused((value) => !value)} onReset={() => { resetRef.current += 1; setNotice('A fresh colony is searching the same type of world.'); }} onShare={() => shareChallenge({ title: `My GaugeGap ants delivered ${deliveries} food`, text: 'Can you design a faster leaderless transport network?', url: challengeUrl(), setNotice })} onCopy={() => copyChallenge(challengeUrl(), setNotice)} notice={notice} /> +
; +} + +function makeCar(direction, width, height) { + const centerX = width / 2; + const centerY = height / 2; + if (direction === 'north') return { direction, x: centerX + 24, y: height + 24, speed: 0, hue: 190 + Math.random() * 90 }; + if (direction === 'south') return { direction, x: centerX - 24, y: -24, speed: 0, hue: 190 + Math.random() * 90 }; + if (direction === 'east') return { direction, x: -24, y: centerY + 24, speed: 0, hue: 190 + Math.random() * 90 }; + return { direction, x: width + 24, y: centerY - 24, speed: 0, hue: 190 + Math.random() * 90 }; +} + +export function TrafficTamerLab() { + const shared = useMemo(() => parseState('traffic', [0.72, 5.5, 1]), []); + const [density, setDensity] = useState(clamp(shared[0], 0.2, 1.2)); + const [cycle, setCycle] = useState(clamp(shared[1], 2.5, 10)); + const [smart, setSmart] = useState(Boolean(shared[2])); + const [paused, setPaused] = useState(false); + const [phase, setPhase] = useState('NS'); + const [stats, setStats] = useState({ passed: 0, queue: 0, peak: 0 }); + const [notice, setNotice] = useState('Tap the intersection or use Switch lights to control the signal.'); + const canvasRef = useRef(null); + const carsRef = useRef([]); + const controlsRef = useRef({ density, cycle, smart }); + const phaseRef = useRef(phase); + const pausedRef = useRef(paused); + const resetRef = useRef(0); + + useEffect(() => { controlsRef.current = { density, cycle, smart }; }, [density, cycle, smart]); + useEffect(() => { phaseRef.current = phase; }, [phase]); + useEffect(() => { pausedRef.current = paused; }, [paused]); + + function togglePhase() { setPhase((current) => current === 'NS' ? 'EW' : 'NS'); setNotice('Signal changed. Watch the queues redistribute.'); } + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return undefined; + const context = canvas.getContext('2d'); + if (!context) return undefined; + let width = 0; let height = 0; let animationId = 0; let frame = 0; let passed = 0; let peak = 0; let lastReset = -1; let lastSwitch = performance.now(); + const directions = ['north', 'south', 'east', 'west']; + function resize() { const rect = canvas.getBoundingClientRect(); const ratio = Math.min(window.devicePixelRatio || 1, 2); width = Math.max(320, rect.width); height = Math.max(400, rect.height); canvas.width = Math.floor(width * ratio); canvas.height = Math.floor(height * ratio); context.setTransform(ratio, 0, 0, ratio, 0, 0); carsRef.current = []; } + const observer = new ResizeObserver(resize); observer.observe(canvas); canvas.addEventListener('pointerdown', togglePhase); resize(); + + function beforeStop(car, centerX, centerY) { + if (car.direction === 'north') return car.y > centerY + 72; + if (car.direction === 'south') return car.y < centerY - 72; + if (car.direction === 'east') return car.x < centerX - 72; + return car.x > centerX + 72; + } + function position(car) { return car.direction === 'north' ? -car.y : car.direction === 'south' ? car.y : car.direction === 'east' ? car.x : -car.x; } + + function render(now) { + if (lastReset !== resetRef.current) { lastReset = resetRef.current; carsRef.current = []; passed = 0; peak = 0; setStats({ passed: 0, queue: 0, peak: 0 }); } + const centerX = width / 2; const centerY = height / 2; + context.fillStyle = '#061018'; context.fillRect(0, 0, width, height); + context.fillStyle = '#111827'; context.fillRect(centerX - 74, 0, 148, height); context.fillRect(0, centerY - 74, width, 148); + context.strokeStyle = 'rgba(255,255,255,0.18)'; context.setLineDash([14, 12]); context.beginPath(); context.moveTo(centerX, 0); context.lineTo(centerX, height); context.moveTo(0, centerY); context.lineTo(width, centerY); context.stroke(); context.setLineDash([]); + context.fillStyle = phaseRef.current === 'NS' ? '#d9ff65' : '#ff5e6d'; context.fillRect(centerX - 68, centerY - 82, 18, 8); context.fillRect(centerX + 50, centerY + 74, 18, 8); + context.fillStyle = phaseRef.current === 'EW' ? '#d9ff65' : '#ff5e6d'; context.fillRect(centerX - 82, centerY + 50, 8, 18); context.fillRect(centerX + 74, centerY - 68, 8, 18); + + if (!pausedRef.current) { + if (Math.random() < controlsRef.current.density * 0.018 && carsRef.current.length < 90) carsRef.current.push(makeCar(directions[Math.floor(Math.random() * 4)], width, height)); + const elapsed = (now - lastSwitch) / 1000; + if (elapsed > controlsRef.current.cycle) { + if (controlsRef.current.smart) { + const ns = carsRef.current.filter((car) => ['north', 'south'].includes(car.direction) && beforeStop(car, centerX, centerY)).length; + const ew = carsRef.current.filter((car) => ['east', 'west'].includes(car.direction) && beforeStop(car, centerX, centerY)).length; + setPhase(ns >= ew ? 'NS' : 'EW'); + } else togglePhase(); + lastSwitch = now; + } + directions.forEach((direction) => { + const sameLane = carsRef.current.filter((car) => car.direction === direction).sort((a, b) => position(b) - position(a)); + sameLane.forEach((car, index) => { + const green = ['north', 'south'].includes(direction) ? phaseRef.current === 'NS' : phaseRef.current === 'EW'; + const lead = sameLane[index - 1]; + const gap = lead ? position(lead) - position(car) : Infinity; + const redStop = !green && beforeStop(car, centerX, centerY); + const target = redStop || gap < 25 ? 0 : 2.1; + car.speed += (target - car.speed) * 0.12; + if (direction === 'north') car.y -= car.speed; + if (direction === 'south') car.y += car.speed; + if (direction === 'east') car.x += car.speed; + if (direction === 'west') car.x -= car.speed; + }); + }); + const survivors = []; + carsRef.current.forEach((car) => { + const outside = car.x < -50 || car.x > width + 50 || car.y < -50 || car.y > height + 50; + if (outside) passed += 1; else survivors.push(car); + }); + carsRef.current = survivors; + } + + carsRef.current.forEach((car) => { context.save(); context.translate(car.x, car.y); if (['east', 'west'].includes(car.direction)) context.rotate(Math.PI / 2); context.fillStyle = `hsl(${car.hue},85%,65%)`; context.fillRect(-8, -14, 16, 28); context.fillStyle = 'rgba(255,255,255,0.6)'; context.fillRect(-5, -10, 10, 5); context.restore(); }); + const queue = carsRef.current.filter((car) => car.speed < 0.45 && beforeStop(car, centerX, centerY)).length; peak = Math.max(peak, queue); + if (frame % 20 === 0) setStats({ passed, queue, peak }); frame += 1; + animationId = window.requestAnimationFrame(render); + } + animationId = window.requestAnimationFrame(render); + return () => { observer.disconnect(); window.cancelAnimationFrame(animationId); canvas.removeEventListener('pointerdown', togglePhase); }; + }, []); + + const score = clamp(Math.round(stats.passed * 1.8 - stats.peak * 2.2), 0, 100); + function challengeUrl() { const url = new URL(window.location.href); url.searchParams.set('lab', 'traffic'); url.searchParams.set('state', [round(density, 2), round(cycle, 1), smart ? 1 : 0].join(',')); url.hash = 'playground'; return url.toString(); } + + return
+ +
Current green: {phase === 'NS' ? 'north–south' : 'east–west'} · queue {stats.queue}
+
+ setPaused((value) => !value)} onReset={() => { resetRef.current += 1; setNotice('Intersection reset. Try a different signal policy.'); }} onShare={() => shareChallenge({ title: `My GaugeGap traffic score: ${score}%`, text: 'Can you move more cars with a smaller queue?', url: challengeUrl(), setNotice })} onCopy={() => copyChallenge(challengeUrl(), setNotice)} notice={notice} /> +
; +} From ad37e82356175fe38e013f7d1e403082484dc92f Mon Sep 17 00:00:00 2001 From: PenguineWalkOS Date: Fri, 10 Jul 2026 19:12:08 -0400 Subject: [PATCH 3/7] feat(gaugegap): add cellular life and ecosystem balancing games --- .../features/gaugegap/WorldPlaygrounds.jsx | 226 ++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 brainsnn-r3f-app/src/features/gaugegap/WorldPlaygrounds.jsx diff --git a/brainsnn-r3f-app/src/features/gaugegap/WorldPlaygrounds.jsx b/brainsnn-r3f-app/src/features/gaugegap/WorldPlaygrounds.jsx new file mode 100644 index 0000000..520290a --- /dev/null +++ b/brainsnn-r3f-app/src/features/gaugegap/WorldPlaygrounds.jsx @@ -0,0 +1,226 @@ +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { Grid3X3, Leaf, MousePointer2, Rabbit, Target, Wolf } from 'lucide-react'; +import { clamp, copyChallenge, DeepLabActions, DeepLabHeading, round, seededRandom, shareChallenge } from './DeepLabChrome.jsx'; + +function parseState(lab, defaults) { + if (typeof window === 'undefined') return defaults; + const query = new URLSearchParams(window.location.search); + if (query.get('lab') !== lab) return defaults; + const values = (query.get('state') || '').split(',').map(Number); + return values.every(Number.isFinite) ? values : defaults; +} + +const LIFE_PATTERNS = { + glider: [[1,0],[2,1],[0,2],[1,2],[2,2]], + acorn: [[1,0],[3,1],[0,2],[1,2],[4,2],[5,2],[6,2]], + pulsar: [[2,0],[3,0],[4,0],[8,0],[9,0],[10,0],[0,2],[5,2],[7,2],[12,2],[0,3],[5,3],[7,3],[12,3],[0,4],[5,4],[7,4],[12,4],[2,5],[3,5],[4,5],[8,5],[9,5],[10,5],[2,7],[3,7],[4,7],[8,7],[9,7],[10,7],[0,8],[5,8],[7,8],[12,8],[0,9],[5,9],[7,9],[12,9],[0,10],[5,10],[7,10],[12,10],[2,12],[3,12],[4,12],[8,12],[9,12],[10,12]], +}; + +export function LifePainterLab() { + const shared = useMemo(() => parseState('life', [8, 1]), []); + const [speed, setSpeed] = useState(clamp(shared[0], 1, 24)); + const [wrap, setWrap] = useState(Boolean(shared[1])); + const [paused, setPaused] = useState(false); + const [generation, setGeneration] = useState(0); + const [alive, setAlive] = useState(0); + const [notice, setNotice] = useState('Paint cells directly onto the grid, then press play.'); + const canvasRef = useRef(null); + const gridRef = useRef(null); + const controlsRef = useRef({ speed, wrap }); + const pausedRef = useRef(paused); + const resetRef = useRef(0); + const patternRef = useRef('glider'); + + useEffect(() => { controlsRef.current = { speed, wrap }; }, [speed, wrap]); + useEffect(() => { pausedRef.current = paused; }, [paused]); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return undefined; + const context = canvas.getContext('2d'); + if (!context) return undefined; + const cols = 96; const rows = 64; + let width = 0; let height = 0; let animationId = 0; let lastStep = 0; let lastReset = -1; let drawing = false; let generationCount = 0; + + function blank() { return new Uint8Array(cols * rows); } + function index(x, y) { return y * cols + x; } + function stamp(name) { + const grid = blank(); + const pattern = LIFE_PATTERNS[name] || LIFE_PATTERNS.glider; + const offsetX = Math.floor(cols / 2 - 7); const offsetY = Math.floor(rows / 2 - 7); + pattern.forEach(([x, y]) => { const px = offsetX + x; const py = offsetY + y; if (px >= 0 && px < cols && py >= 0 && py < rows) grid[index(px, py)] = 1; }); + gridRef.current = grid; generationCount = 0; setGeneration(0); setAlive(pattern.length); + } + function randomField() { + const random = seededRandom(Date.now() % 100000); const grid = blank(); + for (let i = 0; i < grid.length; i += 1) grid[i] = random() > 0.82 ? 1 : 0; + gridRef.current = grid; generationCount = 0; setGeneration(0); + } + function resize() { const rect = canvas.getBoundingClientRect(); const ratio = Math.min(window.devicePixelRatio || 1, 2); width = Math.max(320, rect.width); height = Math.max(380, rect.height); canvas.width = Math.floor(width * ratio); canvas.height = Math.floor(height * ratio); context.setTransform(ratio, 0, 0, ratio, 0, 0); if (!gridRef.current) stamp('glider'); } + function paint(event) { + const rect = canvas.getBoundingClientRect(); + const x = clamp(Math.floor((event.clientX - rect.left) / rect.width * cols), 0, cols - 1); + const y = clamp(Math.floor((event.clientY - rect.top) / rect.height * rows), 0, rows - 1); + for (let dy = -1; dy <= 1; dy += 1) for (let dx = -1; dx <= 1; dx += 1) { const px = x + dx; const py = y + dy; if (px >= 0 && px < cols && py >= 0 && py < rows) gridRef.current[index(px, py)] = 1; } + } + function pointerDown(event) { drawing = true; paint(event); } + function pointerMove(event) { if (drawing) paint(event); } + function pointerUp() { drawing = false; } + const observer = new ResizeObserver(resize); observer.observe(canvas); + canvas.addEventListener('pointerdown', pointerDown); canvas.addEventListener('pointermove', pointerMove); window.addEventListener('pointerup', pointerUp); resize(); + + function step() { + const current = gridRef.current || blank(); const next = blank(); const wrapEdges = controlsRef.current.wrap; + for (let y = 0; y < rows; y += 1) for (let x = 0; x < cols; x += 1) { + let neighbors = 0; + for (let dy = -1; dy <= 1; dy += 1) for (let dx = -1; dx <= 1; dx += 1) { + if (!dx && !dy) continue; + let nx = x + dx; let ny = y + dy; + if (wrapEdges) { nx = (nx + cols) % cols; ny = (ny + rows) % rows; } + if (nx >= 0 && nx < cols && ny >= 0 && ny < rows) neighbors += current[index(nx, ny)]; + } + const live = current[index(x, y)] === 1; + next[index(x, y)] = live ? (neighbors === 2 || neighbors === 3 ? 1 : 0) : (neighbors === 3 ? 1 : 0); + } + gridRef.current = next; generationCount += 1; + if (generationCount % 4 === 0) { let count = 0; for (let i = 0; i < next.length; i += 1) count += next[i]; setAlive(count); setGeneration(generationCount); } + } + + function render(now) { + if (lastReset !== resetRef.current) { lastReset = resetRef.current; if (patternRef.current === 'random') randomField(); else stamp(patternRef.current); } + const interval = 1000 / controlsRef.current.speed; + if (!pausedRef.current && now - lastStep > interval) { step(); lastStep = now; } + context.fillStyle = '#02050a'; context.fillRect(0, 0, width, height); + const cellW = width / cols; const cellH = height / rows; const grid = gridRef.current || blank(); + for (let y = 0; y < rows; y += 1) for (let x = 0; x < cols; x += 1) if (grid[index(x, y)]) { + const hue = 176 + ((x + y + generationCount) % 90); + context.fillStyle = `hsla(${hue},90%,65%,0.88)`; context.fillRect(x * cellW, y * cellH, Math.ceil(cellW), Math.ceil(cellH)); + } + context.strokeStyle = 'rgba(125,249,255,0.035)'; context.lineWidth = 1; + for (let x = 0; x <= cols; x += 8) { context.beginPath(); context.moveTo(x * cellW, 0); context.lineTo(x * cellW, height); context.stroke(); } + for (let y = 0; y <= rows; y += 8) { context.beginPath(); context.moveTo(0, y * cellH); context.lineTo(width, y * cellH); context.stroke(); } + animationId = window.requestAnimationFrame(render); + } + animationId = window.requestAnimationFrame(render); + return () => { observer.disconnect(); window.cancelAnimationFrame(animationId); canvas.removeEventListener('pointerdown', pointerDown); canvas.removeEventListener('pointermove', pointerMove); window.removeEventListener('pointerup', pointerUp); }; + }, []); + + const score = clamp(Math.round(Math.min(generation, 300) / 3 + Math.min(alive, 350) / 8), 0, 100); + function loadPattern(name) { patternRef.current = name; resetRef.current += 1; setPaused(false); setNotice(`${name === 'random' ? 'Random universe' : name} loaded. Paint into it and change its future.`); } + function challengeUrl() { const url = new URL(window.location.href); url.searchParams.set('lab', 'life'); url.searchParams.set('state', [speed, wrap ? 1 : 0].join(',')); url.hash = 'playground'; return url.toString(); } + + return
+ +
Drag to paint living cells
+
+ setPaused((value) => !value)} onReset={() => { resetRef.current += 1; setNotice('Pattern reset. Try changing one cell before restarting.'); }} onShare={() => shareChallenge({ title: `My GaugeGap life survived ${generation} generations`, text: 'Can you paint a longer-lived cellular universe?', url: challengeUrl(), setNotice })} onCopy={() => copyChallenge(challengeUrl(), setNotice)} notice={notice} /> +
; +} + +function makeEntity(type, x, y, random) { + return { type, x, y, vx: (random() - 0.5) * 1.2, vy: (random() - 0.5) * 1.2, energy: type === 'plant' ? 1 : type === 'prey' ? 48 : 62, age: 0 }; +} + +export function EcosystemBalanceLab() { + const shared = useMemo(() => parseState('ecosystem', [0.72, 0.62, 1]), []); + const [sunlight, setSunlight] = useState(clamp(shared[0], 0.2, 1.2)); + const [metabolism, setMetabolism] = useState(clamp(shared[1], 0.25, 1.1)); + const [speed, setSpeed] = useState(clamp(shared[2], 0.5, 1.8)); + const [mode, setMode] = useState('plant'); + const [paused, setPaused] = useState(false); + const [counts, setCounts] = useState({ plant: 0, prey: 0, predator: 0, seconds: 0 }); + const [notice, setNotice] = useState('Add plants, prey or predators. Balance matters more than abundance.'); + const canvasRef = useRef(null); + const entitiesRef = useRef([]); + const controlsRef = useRef({ sunlight, metabolism, speed, mode }); + const pausedRef = useRef(paused); + const resetRef = useRef(0); + + useEffect(() => { controlsRef.current = { sunlight, metabolism, speed, mode }; }, [sunlight, metabolism, speed, mode]); + useEffect(() => { pausedRef.current = paused; }, [paused]); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return undefined; + const context = canvas.getContext('2d'); + if (!context) return undefined; + let width = 0; let height = 0; let animationId = 0; let lastReset = -1; let frame = 0; let started = performance.now(); let random = seededRandom(73); + + function seedWorld() { + random = seededRandom(Date.now() % 100000); + const entities = []; + for (let i = 0; i < 100; i += 1) entities.push(makeEntity('plant', random() * width, random() * height, random)); + for (let i = 0; i < 34; i += 1) entities.push(makeEntity('prey', random() * width, random() * height, random)); + for (let i = 0; i < 8; i += 1) entities.push(makeEntity('predator', random() * width, random() * height, random)); + entitiesRef.current = entities; started = performance.now(); setCounts({ plant: 100, prey: 34, predator: 8, seconds: 0 }); + } + function resize() { const rect = canvas.getBoundingClientRect(); const ratio = Math.min(window.devicePixelRatio || 1, 2); width = Math.max(320, rect.width); height = Math.max(400, rect.height); canvas.width = Math.floor(width * ratio); canvas.height = Math.floor(height * ratio); context.setTransform(ratio, 0, 0, ratio, 0, 0); seedWorld(); } + function addEntity(event) { const rect = canvas.getBoundingClientRect(); const x = event.clientX - rect.left; const y = event.clientY - rect.top; const amount = controlsRef.current.mode === 'plant' ? 8 : controlsRef.current.mode === 'prey' ? 3 : 1; for (let i = 0; i < amount; i += 1) entitiesRef.current.push(makeEntity(controlsRef.current.mode, x + (random() - 0.5) * 24, y + (random() - 0.5) * 24, random)); setNotice(`${controlsRef.current.mode} added. Watch the food web react.`); } + const observer = new ResizeObserver(resize); observer.observe(canvas); canvas.addEventListener('pointerdown', addEntity); resize(); + + function nearest(entity, type, radius) { + let best = null; let bestDistance = radius; + entitiesRef.current.forEach((candidate) => { if (candidate.type !== type || candidate === entity) return; const distance = Math.hypot(candidate.x - entity.x, candidate.y - entity.y); if (distance < bestDistance) { best = candidate; bestDistance = distance; } }); + return best; + } + function reproduce(entity, chance) { + if (Math.random() < chance && entitiesRef.current.length < 420) { entity.energy *= 0.58; entitiesRef.current.push(makeEntity(entity.type, entity.x + (random() - 0.5) * 14, entity.y + (random() - 0.5) * 14, random)); } + } + + function render(now) { + if (lastReset !== resetRef.current) { lastReset = resetRef.current; seedWorld(); } + context.fillStyle = 'rgba(2,8,8,0.38)'; context.fillRect(0, 0, width, height); + if (!pausedRef.current) { + const controls = controlsRef.current; + if (Math.random() < controls.sunlight * 0.38 && entitiesRef.current.filter((entity) => entity.type === 'plant').length < 220) entitiesRef.current.push(makeEntity('plant', random() * width, random() * height, random)); + const removed = new Set(); + entitiesRef.current.forEach((entity, index) => { + entity.age += 1; + if (entity.type === 'plant') { entity.energy = Math.min(4, entity.energy + 0.002 * controls.sunlight); reproduce(entity, entity.energy > 2.8 ? 0.0015 * controls.sunlight : 0); return; } + const targetType = entity.type === 'prey' ? 'plant' : 'prey'; + const target = nearest(entity, targetType, entity.type === 'prey' ? 90 : 125); + if (target) { const dx = target.x - entity.x; const dy = target.y - entity.y; const distance = Math.max(1, Math.hypot(dx, dy)); entity.vx += dx / distance * (entity.type === 'prey' ? 0.045 : 0.06); entity.vy += dy / distance * (entity.type === 'prey' ? 0.045 : 0.06); if (distance < 8) { removed.add(entitiesRef.current.indexOf(target)); entity.energy += entity.type === 'prey' ? 15 : 24; } } + entity.vx += (random() - 0.5) * 0.08; entity.vy += (random() - 0.5) * 0.08; + const max = entity.type === 'prey' ? 1.65 : 1.95; const magnitude = Math.max(0.01, Math.hypot(entity.vx, entity.vy)); if (magnitude > max) { entity.vx = entity.vx / magnitude * max; entity.vy = entity.vy / magnitude * max; } + entity.x = (entity.x + entity.vx * controls.speed + width) % width; entity.y = (entity.y + entity.vy * controls.speed + height) % height; + entity.energy -= (entity.type === 'prey' ? 0.022 : 0.035) * controls.metabolism * controls.speed; + if (entity.energy <= 0 || entity.age > 12000) removed.add(index); + reproduce(entity, entity.energy > (entity.type === 'prey' ? 62 : 85) ? 0.004 : 0); + }); + entitiesRef.current = entitiesRef.current.filter((_, index) => !removed.has(index)); + } + + entitiesRef.current.forEach((entity) => { + if (entity.type === 'plant') { context.fillStyle = 'rgba(110,255,120,0.68)'; context.beginPath(); context.arc(entity.x, entity.y, 2.2, 0, Math.PI * 2); context.fill(); } + else if (entity.type === 'prey') { context.save(); context.translate(entity.x, entity.y); context.rotate(Math.atan2(entity.vy, entity.vx)); context.fillStyle = '#7df9ff'; context.beginPath(); context.moveTo(7,0); context.lineTo(-4,4); context.lineTo(-2,0); context.lineTo(-4,-4); context.closePath(); context.fill(); context.restore(); } + else { context.fillStyle = '#ff5e6d'; context.beginPath(); context.arc(entity.x, entity.y, 5, 0, Math.PI * 2); context.fill(); context.strokeStyle = 'rgba(255,94,109,0.35)'; context.beginPath(); context.arc(entity.x, entity.y, 12, 0, Math.PI * 2); context.stroke(); } + }); + if (frame % 24 === 0) { const next = { plant: 0, prey: 0, predator: 0, seconds: Math.floor((now - started) / 1000) }; entitiesRef.current.forEach((entity) => { next[entity.type] += 1; }); setCounts(next); } + frame += 1; animationId = window.requestAnimationFrame(render); + } + animationId = window.requestAnimationFrame(render); + return () => { observer.disconnect(); window.cancelAnimationFrame(animationId); canvas.removeEventListener('pointerdown', addEntity); }; + }, []); + + const allAlive = counts.plant > 0 && counts.prey > 0 && counts.predator > 0; + const balance = allAlive ? clamp(100 - Math.abs(counts.plant - counts.prey * 3) * 0.35 - Math.abs(counts.prey - counts.predator * 4) * 1.2, 0, 100) : 0; + function challengeUrl() { const url = new URL(window.location.href); url.searchParams.set('lab', 'ecosystem'); url.searchParams.set('state', [round(sunlight, 2), round(metabolism, 2), round(speed, 2)].join(',')); url.hash = 'playground'; return url.toString(); } + + return
+ +
Tap to add {mode}
+
+ setPaused((value) => !value)} onReset={() => { resetRef.current += 1; setNotice('A fresh ecosystem is ready. Try a different intervention order.'); }} onShare={() => shareChallenge({ title: `My GaugeGap ecosystem balance: ${Math.round(balance)}%`, text: 'Can you keep all three levels alive longer?', url: challengeUrl(), setNotice })} onCopy={() => copyChallenge(challengeUrl(), setNotice)} notice={notice} /> +
; +} From 1ba24dcbb50c23c3773e92ae7c399b60c393545f Mon Sep 17 00:00:00 2001 From: PenguineWalkOS Date: Fri, 10 Jul 2026 19:12:35 -0400 Subject: [PATCH 4/7] feat(gaugegap): expand arcade to twelve labs with progression --- .../features/gaugegap/ExperimentArcade.jsx | 140 +++++------------- 1 file changed, 35 insertions(+), 105 deletions(-) diff --git a/brainsnn-r3f-app/src/features/gaugegap/ExperimentArcade.jsx b/brainsnn-r3f-app/src/features/gaugegap/ExperimentArcade.jsx index 58b984a..1584074 100644 --- a/brainsnn-r3f-app/src/features/gaugegap/ExperimentArcade.jsx +++ b/brainsnn-r3f-app/src/features/gaugegap/ExperimentArcade.jsx @@ -1,93 +1,29 @@ import React, { useEffect, useMemo, useState } from 'react'; -import { Activity, CircleDot, Dna, Gauge, Orbit, Shield, Shuffle, Sparkles, Waves, Wind } from 'lucide-react'; +import { Activity, Bug, Car, CircleDot, Dna, Gauge, Grid3X3, Leaf, Orbit, Shield, Shuffle, Sparkles, Waves, Wind } from 'lucide-react'; import { AttractorPlayground } from './AttractorPlayground.jsx'; import { FireflySyncLab, ReactionDiffusionLab, WaveInterferenceLab } from './CollectivePlaygrounds.jsx'; import { FlockMindLab, OutbreakZeroLab } from './AgentPlaygrounds.jsx'; import { ChaosTwinsLab, GravityForgeLab } from './PhysicsPlaygrounds.jsx'; +import { AntTrailLab, TrafficTamerLab } from './PlayfulPlaygrounds.jsx'; +import { EcosystemBalanceLab, LifePainterLab } from './WorldPlaygrounds.jsx'; +import { ArcadeProgress, useArcadeProgress } from './ArcadeProgress.jsx'; import '../../styles/arcade.css'; import '../../styles/deep-arcade.css'; +import '../../styles/fun-arcade.css'; const EXPERIMENTS = [ - { - id: 'attractor', - number: '001', - title: 'Butterfly Effect', - short: 'Shape chaos', - description: 'Tune a Lorenz system until order and turbulence balance on the same orbit.', - icon: Gauge, - accent: 'cyan', - mechanic: 'Tune', - }, - { - id: 'fireflies', - number: '002', - title: 'Firefly Sync', - short: 'Create collective rhythm', - description: 'Give hundreds of independent clocks one weak connection and watch a shared pulse emerge.', - icon: Sparkles, - accent: 'lime', - mechanic: 'Synchronize', - }, - { - id: 'waves', - number: '003', - title: 'Wave Eraser', - short: 'Find perfect silence', - description: 'Move through two overlapping waves and hunt for the places where energy disappears.', - icon: Waves, - accent: 'violet', - mechanic: 'Explore', - }, - { - id: 'reaction', - number: '004', - title: 'Living Chemistry', - short: 'Draw a new species', - description: 'Paint two virtual chemicals and grow coral, cells, worms and mazes from local reactions.', - icon: Dna, - accent: 'pink', - mechanic: 'Create', - }, - { - id: 'gravity', - number: '005', - title: 'Gravity Forge', - short: 'Build a solar system', - description: 'Launch planets into a live n-body field and see whether your orbital architecture survives.', - icon: Orbit, - accent: 'amber', - mechanic: 'Construct', - }, - { - id: 'flock', - number: '006', - title: 'Flock Mind', - short: 'Steer emergence', - description: 'Act as predator or beacon while leaderless agents coordinate through only local rules.', - icon: Wind, - accent: 'blue', - mechanic: 'Influence', - }, - { - id: 'outbreak', - number: '007', - title: 'Outbreak Zero', - short: 'Contain the network', - description: 'Choose patient zero, spend twelve vaccines and break the transmission graph before it turns red.', - icon: Shield, - accent: 'red', - mechanic: 'Intervene', - }, - { - id: 'pendulum', - number: '008', - title: 'Chaos Twins', - short: 'Race two futures', - description: 'Start two double pendulums almost identically and measure how quickly their futures separate.', - icon: CircleDot, - accent: 'orange', - mechanic: 'Predict', - }, + { id: 'attractor', number: '001', title: 'Butterfly Effect', short: 'Shape chaos', description: 'Tune a Lorenz system until order and turbulence balance on the same orbit.', icon: Gauge, accent: 'cyan', mechanic: 'Tune' }, + { id: 'fireflies', number: '002', title: 'Firefly Sync', short: 'Create collective rhythm', description: 'Give hundreds of independent clocks one weak connection and watch a shared pulse emerge.', icon: Sparkles, accent: 'lime', mechanic: 'Synchronize' }, + { id: 'waves', number: '003', title: 'Wave Eraser', short: 'Find perfect silence', description: 'Move through two overlapping waves and hunt for the places where energy disappears.', icon: Waves, accent: 'violet', mechanic: 'Explore' }, + { id: 'reaction', number: '004', title: 'Living Chemistry', short: 'Draw a new species', description: 'Paint two virtual chemicals and grow coral, cells, worms and mazes from local reactions.', icon: Dna, accent: 'pink', mechanic: 'Create' }, + { id: 'gravity', number: '005', title: 'Gravity Forge', short: 'Build a solar system', description: 'Launch planets into a live n-body field and see whether your orbital architecture survives.', icon: Orbit, accent: 'amber', mechanic: 'Construct' }, + { id: 'flock', number: '006', title: 'Flock Mind', short: 'Steer emergence', description: 'Act as predator or beacon while leaderless agents coordinate through only local rules.', icon: Wind, accent: 'blue', mechanic: 'Influence' }, + { id: 'outbreak', number: '007', title: 'Outbreak Zero', short: 'Contain the network', description: 'Choose patient zero, spend twelve vaccines and break the transmission graph before it turns red.', icon: Shield, accent: 'red', mechanic: 'Intervene' }, + { id: 'pendulum', number: '008', title: 'Chaos Twins', short: 'Race two futures', description: 'Start two double pendulums almost identically and measure how quickly their futures separate.', icon: CircleDot, accent: 'orange', mechanic: 'Predict' }, + { id: 'ants', number: '009', title: 'Ant Trail Architect', short: 'Design without a planner', description: 'Place food and barriers while a leaderless colony discovers and reinforces efficient routes.', icon: Bug, accent: 'mint', mechanic: 'Route' }, + { id: 'traffic', number: '010', title: 'Traffic Tamer', short: 'Beat the intersection', description: 'Control signal phases under rising demand and keep every queue from becoming a citywide jam.', icon: Car, accent: 'yellow', mechanic: 'Control' }, + { id: 'life', number: '011', title: 'Life Painter', short: 'Paint computation', description: 'Draw living cells and let four tiny rules transform them into moving, evolving machines.', icon: Grid3X3, accent: 'teal', mechanic: 'Invent' }, + { id: 'ecosystem', number: '012', title: 'Ecosystem Keeper', short: 'Balance a living world', description: 'Add energy, prey or predators and keep the entire food web alive through delayed feedback.', icon: Leaf, accent: 'green', mechanic: 'Balance' }, ]; function initialExperiment() { @@ -99,6 +35,9 @@ function initialExperiment() { export function ExperimentArcade() { const [active, setActive] = useState(initialExperiment); const activeExperiment = useMemo(() => EXPERIMENTS.find((experiment) => experiment.id === active) || EXPERIMENTS[0], [active]); + const { progress, recordVisit, dailyLab, level, levelProgress, achievements } = useArcadeProgress(EXPERIMENTS); + + useEffect(() => { recordVisit(active); }, [active, recordVisit]); useEffect(() => { function selectFromEvent(event) { @@ -117,9 +56,7 @@ export function ExperimentArcade() { url.searchParams.delete('state'); url.hash = 'playground'; window.history.replaceState({}, '', url); - if (scroll) { - window.requestAnimationFrame(() => document.getElementById('playground')?.scrollIntoView({ behavior: 'smooth', block: 'start' })); - } + if (scroll) window.requestAnimationFrame(() => document.getElementById('playground')?.scrollIntoView({ behavior: 'smooth', block: 'start' })); } function surpriseMe() { @@ -132,41 +69,34 @@ export function ExperimentArcade() {

GaugeGap science arcade

-

Eight experiments. Eight different ways to think.

-

Tune, synchronize, explore, draw, construct, influence, intervene and predict. Every lab begins as play, then exposes the scientific model underneath.

+

Twelve experiments. Twelve ways to play with reality.

+

Tune, synchronize, explore, draw, construct, influence, intervene, predict, route, control, invent and balance. The library now behaves like an arcade—not a catalogue of charts.

+ selectExperiment(dailyLab.id)} /> +
{EXPERIMENTS.map((experiment) => { const Icon = experiment.icon; const selected = experiment.id === active; + const visited = progress.visited.includes(experiment.id); return ( - ); })}
-
- Experiment {activeExperiment.number} loaded - {activeExperiment.title} -
+
Experiment {activeExperiment.number} loaded{activeExperiment.title}
{active === 'attractor' ? : null} {active === 'fireflies' ? : null} {active === 'waves' ? : null} @@ -175,15 +105,15 @@ export function ExperimentArcade() { {active === 'flock' ? : null} {active === 'outbreak' ? : null} {active === 'pendulum' ? : null} + {active === 'ants' ? : null} + {active === 'traffic' ? : null} + {active === 'life' ? : null} + {active === 'ecosystem' ? : null}
- A complete science arcade needs - Immediate motion - A clear mission - Different interaction verbs - A visible model - A shareable challenge + The retention layer now adds + Daily missionsXP and levelsVisit streaksAchievementsTwelve tactile mechanics
); From dd5d3330405a91448530b1db1ec678766e5a96e6 Mon Sep 17 00:00:00 2001 From: PenguineWalkOS Date: Fri, 10 Jul 2026 19:13:28 -0400 Subject: [PATCH 5/7] style(gaugegap): add arcade passport and playful lab surfaces --- brainsnn-r3f-app/src/styles/fun-arcade.css | 127 +++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 brainsnn-r3f-app/src/styles/fun-arcade.css diff --git a/brainsnn-r3f-app/src/styles/fun-arcade.css b/brainsnn-r3f-app/src/styles/fun-arcade.css new file mode 100644 index 0000000..c9d322d --- /dev/null +++ b/brainsnn-r3f-app/src/styles/fun-arcade.css @@ -0,0 +1,127 @@ +.gg-passport { + display: grid; + grid-template-columns: minmax(240px, 1.5fr) minmax(220px, 1fr) auto auto; + gap: 12px; + align-items: stretch; + margin: 0 0 24px; + padding: 14px; + border: 1px solid rgba(125, 249, 255, 0.14); + border-radius: 22px; + background: linear-gradient(135deg, rgba(125,249,255,0.055), rgba(139,92,246,0.035)), rgba(7,8,16,0.88); + box-shadow: inset 0 1px 0 rgba(255,255,255,0.03); +} + +.gg-passport-main, +.gg-daily-card, +.gg-passport-stat { + min-width: 0; + padding: 13px 15px; + border: 1px solid rgba(255,255,255,0.08); + border-radius: 15px; + background: rgba(255,255,255,0.025); +} + +.gg-passport-level, +.gg-daily-card, +.gg-passport-stat { + display: flex; + align-items: center; + gap: 10px; +} + +.gg-passport-level { color: #dffcff; } +.gg-passport-level span { font-family: var(--bsn-font-mono); font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.08em; } +.gg-passport-level strong { margin-left: auto; color: var(--bsn-cyan); font-family: var(--bsn-font-mono); } +.gg-passport-progress { height: 6px; margin: 12px 0 8px; overflow: hidden; border-radius: 999px; background: rgba(255,255,255,0.07); } +.gg-passport-progress span { display: block; height: 100%; border-radius: inherit; background: linear-gradient(90deg, #7df9ff, #a78bfa, #ff5edb); box-shadow: 0 0 16px rgba(125,249,255,0.35); } +.gg-passport-main > small { color: var(--bsn-text-muted); } + +.gg-daily-card { + color: var(--bsn-text); + text-align: left; + cursor: pointer; +} +.gg-daily-card:hover { border-color: rgba(217,255,101,0.4); background: rgba(217,255,101,0.055); } +.gg-daily-card.complete { border-color: rgba(110,255,120,0.24); } +.gg-daily-card span { display: grid; min-width: 0; } +.gg-daily-card small { color: var(--bsn-text-muted); font-family: var(--bsn-font-mono); font-size: 0.66rem; letter-spacing: 0.08em; text-transform: uppercase; } +.gg-daily-card strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.gg-daily-card em { margin-left: auto; padding: 5px 8px; border-radius: 999px; color: #071008; background: #d9ff65; font-family: var(--bsn-font-mono); font-size: 0.64rem; font-style: normal; font-weight: 900; } +.gg-daily-card.complete em { background: #6eff78; } + +.gg-passport-stat { min-width: 108px; color: var(--bsn-cyan); } +.gg-passport-stat span { display: grid; } +.gg-passport-stat strong { color: #f4ffff; font-family: var(--bsn-font-mono); font-size: 1.15rem; } +.gg-passport-stat small { color: var(--bsn-text-muted); font-size: 0.66rem; } +.gg-achievements { grid-column: 1 / -1; padding: 2px 5px 0; } +.gg-achievements summary { width: fit-content; display: flex; align-items: center; gap: 7px; color: var(--bsn-text-muted); cursor: pointer; font-size: 0.74rem; } +.gg-achievements > div { display: flex; gap: 8px; flex-wrap: wrap; padding-top: 10px; } +.gg-achievements span { padding: 6px 9px; border: 1px solid rgba(255,255,255,0.07); border-radius: 999px; color: rgba(255,255,255,0.32); font-size: 0.68rem; } +.gg-achievements span.unlocked { border-color: rgba(217,255,101,0.25); color: #edffb3; background: rgba(217,255,101,0.05); } + +.gg-arcade-card-mint { --arcade-accent: #70ffd0; } +.gg-arcade-card-yellow { --arcade-accent: #ffe66d; } +.gg-arcade-card-teal { --arcade-accent: #43e7d5; } +.gg-arcade-card-green { --arcade-accent: #6eff78; } +.gg-arcade-card.visited::before { + content: "✓"; + position: absolute; + top: 12px; + left: 12px; + z-index: 2; + width: 19px; + height: 19px; + display: grid; + place-items: center; + border-radius: 50%; + color: #041008; + background: var(--arcade-accent); + font-size: 0.68rem; + font-weight: 1000; +} +.gg-arcade-card.visited .gg-arcade-card-icon { margin-left: 18px; } + +.gg-ant-canvas, +.gg-life-canvas, +.gg-ecosystem-canvas { cursor: crosshair; touch-action: none; } +.gg-life-canvas { image-rendering: pixelated; } +.gg-traffic-canvas { cursor: pointer; } + +.gg-species-picker { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 7px; +} +.gg-species-picker button { + min-height: 42px; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + border: 1px solid rgba(255,255,255,0.09); + border-radius: 10px; + color: var(--bsn-text-secondary); + background: rgba(255,255,255,0.025); + font-size: 0.72rem; + font-weight: 800; + cursor: pointer; +} +.gg-species-picker button.active { border-color: rgba(110,255,120,0.42); color: #d8ffdb; background: rgba(110,255,120,0.08); } + +.gg-arcade-stage[data-experiment="ants"] { background: radial-gradient(circle at 50% 0%, rgba(112,255,208,0.08), transparent 40%), rgba(2,3,9,0.9); } +.gg-arcade-stage[data-experiment="traffic"] { background: radial-gradient(circle at 50% 0%, rgba(255,230,109,0.08), transparent 40%), rgba(2,3,9,0.9); } +.gg-arcade-stage[data-experiment="life"] { background: radial-gradient(circle at 50% 0%, rgba(67,231,213,0.08), transparent 40%), rgba(2,3,9,0.9); } +.gg-arcade-stage[data-experiment="ecosystem"] { background: radial-gradient(circle at 50% 0%, rgba(110,255,120,0.08), transparent 40%), rgba(2,3,9,0.9); } + +@media (max-width: 980px) { + .gg-passport { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .gg-passport-main { grid-column: 1 / -1; } +} + +@media (max-width: 620px) { + .gg-passport { grid-template-columns: 1fr 1fr; padding: 10px; } + .gg-daily-card { grid-column: 1 / -1; } + .gg-passport-stat { min-width: 0; } + .gg-species-picker { grid-template-columns: 1fr; } + .gg-arcade-card.visited .gg-arcade-card-icon { margin-left: 12px; } +} From 7d9353a03de389b2b5aed02c8f542bc67fb30296 Mon Sep 17 00:00:00 2001 From: PenguineWalkOS Date: Fri, 10 Jul 2026 19:14:47 -0400 Subject: [PATCH 6/7] feat(gaugegap): position twelve-lab arcade and return loop --- brainsnn-r3f-app/src/app/GaugeGapLanding.jsx | 256 ++++--------------- 1 file changed, 45 insertions(+), 211 deletions(-) diff --git a/brainsnn-r3f-app/src/app/GaugeGapLanding.jsx b/brainsnn-r3f-app/src/app/GaugeGapLanding.jsx index 5ae2eb5..7a079d4 100644 --- a/brainsnn-r3f-app/src/app/GaugeGapLanding.jsx +++ b/brainsnn-r3f-app/src/app/GaugeGapLanding.jsx @@ -2,13 +2,17 @@ import React, { useEffect } from 'react'; import { ArrowRight, BrainCircuit, + Bug, + Car, CheckCircle2, ChevronDown, CircleDot, Dna, FlaskConical, Gauge, + Grid3X3, Layers3, + Leaf, Microscope, Orbit, Play, @@ -28,95 +32,29 @@ import '../styles/gaugegap.css'; const CONTENT_SAMPLE = 'Everyone says this breakthrough changes everything. Here is what the evidence actually shows, what remains uncertain, and the one result worth paying attention to.'; const LABS = [ - { - id: 'attractor', - eyebrow: 'Live experiment 001', - title: 'Butterfly Effect Lab', - description: 'Tune a chaotic system until order and turbulence balance on the same orbit.', - icon: Gauge, - action: 'Shape chaos', - }, - { - id: 'fireflies', - eyebrow: 'Live experiment 002', - title: 'Firefly Sync Lab', - description: 'Connect hundreds of independent clocks and watch collective rhythm emerge without a leader.', - icon: Sparkles, - action: 'Sync the swarm', - }, - { - id: 'waves', - eyebrow: 'Live experiment 003', - title: 'Wave Eraser', - description: 'Move a detector through overlapping waves and find the exact places where energy disappears.', - icon: Waves, - action: 'Find silence', - }, - { - id: 'reaction', - eyebrow: 'Live experiment 004', - title: 'Living Chemistry', - description: 'Draw into a reaction-diffusion field and grow coral, cells, worms and mazes from two chemicals.', - icon: Dna, - action: 'Grow a species', - }, - { - id: 'gravity', - eyebrow: 'Live experiment 005', - title: 'Gravity Forge', - description: 'Launch planets into a live n-body field and build an orbital system that does not destroy itself.', - icon: Orbit, - action: 'Build a system', - }, - { - id: 'flock', - eyebrow: 'Live experiment 006', - title: 'Flock Mind', - description: 'Steer a leaderless crowd as predator or beacon while local rules create group intelligence.', - icon: Wind, - action: 'Steer emergence', - }, - { - id: 'outbreak', - eyebrow: 'Live experiment 007', - title: 'Outbreak Zero', - description: 'Pick patient zero, spend twelve vaccines and interrupt the contact graph before it turns red.', - icon: Shield, - action: 'Contain it', - }, - { - id: 'pendulum', - eyebrow: 'Live experiment 008', - title: 'Chaos Twins', - description: 'Race two nearly identical double pendulums and measure when their futures stop agreeing.', - icon: CircleDot, - action: 'Race the futures', - }, - { - id: 'soliton', - eyebrow: 'Research engine', - title: 'Soliton Collision Lab', - description: 'Launch nonlinear waves, change their amplitude and watch them pass through one another without losing identity.', - icon: Zap, - action: 'Open research lab', - }, - { - id: 'content', - eyebrow: 'BrainSNN crossover', - title: 'Mind-Hack Autopsy', - description: 'Paste viral content and expose the emotional pressure, trust cost and attention mechanics hiding inside it.', - icon: BrainCircuit, - action: 'Scan a viral claim', - }, + { id: 'attractor', eyebrow: 'Live experiment 001', title: 'Butterfly Effect Lab', description: 'Tune a chaotic system until order and turbulence balance on the same orbit.', icon: Gauge, action: 'Shape chaos' }, + { id: 'fireflies', eyebrow: 'Live experiment 002', title: 'Firefly Sync Lab', description: 'Connect hundreds of independent clocks and watch collective rhythm emerge without a leader.', icon: Sparkles, action: 'Sync the swarm' }, + { id: 'waves', eyebrow: 'Live experiment 003', title: 'Wave Eraser', description: 'Move a detector through overlapping waves and find the exact places where energy disappears.', icon: Waves, action: 'Find silence' }, + { id: 'reaction', eyebrow: 'Live experiment 004', title: 'Living Chemistry', description: 'Draw into a reaction-diffusion field and grow coral, cells, worms and mazes from two chemicals.', icon: Dna, action: 'Grow a species' }, + { id: 'gravity', eyebrow: 'Live experiment 005', title: 'Gravity Forge', description: 'Launch planets into a live n-body field and build an orbital system that does not destroy itself.', icon: Orbit, action: 'Build a system' }, + { id: 'flock', eyebrow: 'Live experiment 006', title: 'Flock Mind', description: 'Steer a leaderless crowd as predator or beacon while local rules create group intelligence.', icon: Wind, action: 'Steer emergence' }, + { id: 'outbreak', eyebrow: 'Live experiment 007', title: 'Outbreak Zero', description: 'Pick patient zero, spend twelve vaccines and interrupt the contact graph before it turns red.', icon: Shield, action: 'Contain it' }, + { id: 'pendulum', eyebrow: 'Live experiment 008', title: 'Chaos Twins', description: 'Race two nearly identical double pendulums and measure when their futures stop agreeing.', icon: CircleDot, action: 'Race the futures' }, + { id: 'ants', eyebrow: 'Live experiment 009', title: 'Ant Trail Architect', description: 'Place food and barriers while a leaderless colony discovers and reinforces its own road system.', icon: Bug, action: 'Build ant roads' }, + { id: 'traffic', eyebrow: 'Live experiment 010', title: 'Traffic Tamer', description: 'Control signal phases under rising demand and keep every queue from becoming a citywide jam.', icon: Car, action: 'Run the lights' }, + { id: 'life', eyebrow: 'Live experiment 011', title: 'Life Painter', description: 'Paint living cells and let four tiny rules transform them into moving computational machines.', icon: Grid3X3, action: 'Paint a universe' }, + { id: 'ecosystem', eyebrow: 'Live experiment 012', title: 'Ecosystem Keeper', description: 'Add plants, prey or predators and keep an entire food web alive through delayed feedback.', icon: Leaf, action: 'Balance the world' }, + { id: 'soliton', eyebrow: 'Research engine', title: 'Soliton Collision Lab', description: 'Launch nonlinear waves, change their amplitude and watch them pass through one another without losing identity.', icon: Zap, action: 'Open research lab' }, + { id: 'content', eyebrow: 'BrainSNN crossover', title: 'Mind-Hack Autopsy', description: 'Paste viral content and expose the emotional pressure, trust cost and attention mechanics hiding inside it.', icon: BrainCircuit, action: 'Scan a viral claim' }, ]; const LOOP = [ { number: '01', title: 'Play', text: 'Touch the variables before reading the lesson. The system teaches through response.' }, - { number: '02', title: 'Discover', text: 'Find an unusual state, score it and preserve the exact parameters that produced it.' }, - { number: '03', title: 'Publish', text: 'Export the visual, the challenge link and the explanation from one run.' }, + { number: '02', title: 'Return', text: 'Daily missions, XP, levels and achievements create a reason to explore another system tomorrow.' }, + { number: '03', title: 'Publish', text: 'Share the visual, challenge link, score and explanation from the same experiment state.' }, ]; -const ARCADE_IDS = new Set(['attractor', 'fireflies', 'waves', 'reaction', 'gravity', 'flock', 'outbreak', 'pendulum']); +const ARCADE_IDS = new Set(['attractor', 'fireflies', 'waves', 'reaction', 'gravity', 'flock', 'outbreak', 'pendulum', 'ants', 'traffic', 'life', 'ecosystem']); export function GaugeGapLanding({ onStart, onNavigate, onOpenReconstruct }) { useEffect(() => { @@ -150,10 +88,7 @@ export function GaugeGapLanding({ onStart, onNavigate, onOpenReconstruct }) {