'use strict'; /* ================= translations ================= */ const I18N = { en: { title: 'FlyBrain Tic-Tac-Toe', subtitle: 'Teach a fruit-fly brain to play — and watch its memory form, synapse by synapse.', play_title: '1. Play & teach', you_start: 'You start', fly_starts: 'Fly starts', step_mode: 'Step by step', next: 'Next step ▶', speed: 'Speed', lg_active: 'firing neuron', lg_good: 'memory: “approach” (good)', lg_bad: 'memory: “avoid” (bad)', lg_neutral: 'no memory yet', memory_title: '2. Memory', journal_title: 'Learning journal', journal_hint: 'After every game dopamine neurons compare what the fly expected with what happened. The difference (prediction error δ) rewrites its synapses.', train_title: '3. Training camp', train_hint: 'Let the fly play many fast games against a sparring partner and watch the memory map and learning curve change.', opponent: 'Opponent', opp_mix: 'mixed', opp_random: 'random', opp_heuristic: 'clever (wins & blocks)', opp_self: 'itself', stop: 'Stop', wins: 'wins', draws: 'draws', losses: 'losses', exam: 'Exam (no learning)', reset: 'New fly (forget everything)', about_title: 'How does it work?', footer: 'Model inspired by the published Drosophila connectome. Brain sizes are scaled down.', footer_fw: 'Wiring from FlyWire v783 (Dorkenwald et al. 2024, Schlegel et al. 2024; CC-BY 4.0).', badge_fw: 'FlyWire connectome · {side} mushroom body', badge_syn: 'synthetic wiring', side_right: 'right', side_left: 'left', notice_rewired: '
The server now uses a different wiring diagram, so your previous fly could not keep its memories. This is a new, naive fly.
', symbols: 'You: {h} · Fly: {f}', symbols_auto: 'Fly: {f} · {opp}: {h}', st_your_turn: 'Your turn — click a cell.', st_thinking: 'The fly is thinking…', st_fly_won: 'The fly won.', st_you_won: 'You won!', st_draw: 'Draw.', curve_empty: 'No games played yet.', st_new: 'Start a new game.', st_training: 'Training in progress…', st_games: 'games played', st_record: 'wins / draws / losses', st_strong: 'KCs signalling “approach”', st_weak: 'KCs signalling “avoid”', st_plastic: 'plasticity (learning rate)', st_level: 'level (from last exam)', lvl_none: 'take an exam', lvl: ['Larva', 'Novice', 'Apprentice', 'Skilled', 'Master'], j_game: 'Game {n}', j_move: '#', j_cell: 'cell', j_expected: 'expected', j_outcome: 'outcome', j_delta: 'δ', j_dopa: 'dopamine', j_empty: 'Play a game to see how its memory is written.', j_observed: 'Watching you: PAM {pam}, PPL1 {ppl1}, {k} KCs changed', res_win: 'fly won', res_loss: 'fly lost', res_draw: 'draw', ex_games: 'after games', ex_random: 'vs random (W/D/L)', ex_heur: 'vs clever (W/D/L)', ex_level: 'level', confirm_reset: 'Replace this fly with a new, naive one? All its memories will be lost.', c_eyes: 'Eyes · optic lobe', c_cells: 'cell detectors: own / opponent / empty', c_lines: 'line detectors (own·opponent marks)', c_mb: 'Mushroom body · {n} Kenyon cells', c_apl: 'APL inhibition: only {k} fire at once', c_lh: 'Lateral horn', c_app: 'MBON approach', c_av: 'MBON avoid', c_mbon_n: '{n} MBONs', c_pam: 'PAM · reward', c_ppl1: 'PPL1 · punishment', c_pam_n: 'PAM ×{n} · reward', c_ppl1_n: 'PPL1 ×{n} · punishment', c_cx: 'Central complex', c_cx2: 'compares the options', c_dn: 'Descending neurons', c_move: 'move → cell {c}', ph_intro_tag: 'start', ph_intro_title: 'A naive fly', ph_intro_title_exp: 'Your fly ({n} games of experience)', ph_intro_text: 'This is a model of the fly’s learning centre, the mushroom body. Each dot in the big circle is one Kenyon cell. Grey dots have no memory yet; green means “this situation felt good”, red means “avoid this”.
Play a game or send the fly to the training camp and watch the memory form. Turn on Step by step to walk through every stage of thinking and learning.
', ph_see_tag: '1 · perception', ph_see_title: 'The fly sees your move', ph_see_text: 'Your mark in cell {cell} lights up the photoreceptors. {n} sensory neurons fire: one per cell (own / opponent / empty) and one detector for each of the 8 lines.
', ph_imagine_tag: '2 · imagination', ph_imagine_title: 'Imagining a move in cell {cell}', ph_imagine_text: 'The fly pictures the board after this move. APL inhibition lets only {k} of {n} Kenyon cells fire — a sparse “fingerprint” of this situation.
Their synapses recall: mushroom body {kcv}, lateral horn {lhv} → value {val}.
', ph_imagine_new: 'These synapses are still untouched — no opinion yet.
', ph_decide_tag: '3 · decision', ph_decide_title: 'Decision: cell {cell}', ph_decide_text: 'The central complex compares all imagined outcomes. The best value {val} belongs to cell {cell}, so descending neurons fire and the fly moves there.
', ph_decide_random: 'All options feel the same, so the choice is random: the fly has nothing to go on yet.
', ph_outcome_tag: '4 · outcome', ph_outcome_win: 'The fly won: reward!', ph_outcome_loss: 'The fly lost: punishment', ph_outcome_draw: 'Draw: small reward', ph_outcome_text: 'As in a living fly, the result arrives as a dopamine signal: reward value {r}.
Now the fly replays each of its moves and compares what it expected with what actually happened.
', ph_replay_tag: '5 · memory', ph_replay_title: 'Remembering move {i} of {n} (cell {cell})', ph_replay_text: 'Expected {e}. Outcome {o} (reward {r} × {g}{k}, fading with distance from the end). Prediction error δ = {d}
', ph_replay_pam: 'PAM dopamine neurons fire → the {k} active Kenyon cells strengthen their synapses onto “approach” and weaken those onto “avoid”. This situation is now worth {after}.
', ph_replay_ppl1: 'PPL1 dopamine neurons fire → the {k} active Kenyon cells strengthen their synapses onto “avoid”. This situation is now worth {after}.
', ph_replay_none: 'No surprise, so there is almost nothing to learn from this move.
', ph_watch_tag: '6 · observation', ph_watch_title: 'Learning by watching', ph_watch_text: 'The fly also replays your moves from your point of view, so it learns what worked for you as well. Dopamine: PAM {pam}, PPL1 {ppl1}; {k} Kenyon cells changed.
', ph_done_tag: '7 · consolidation', ph_done_title: 'Memory updated', ph_done_text: '{s} Kenyon cells now signal “approach” and {w} signal “avoid”. Next time the fly meets a similar board it will react differently.
Play again or send it to the training camp.
', ph_train_tag: 'training', ph_train_title: 'Training camp: game {g} of {t}', ph_train_text: 'Opponent: {opp}. In this batch: {w} wins, {d} draws, {l} losses.
Dopamine released: PAM {pam}, PPL1 {ppl1}. {k} Kenyon cells changed their synapses.
', ph_train_done: 'Training finished', auto: 'Train automatically ({n} games)', auto_stop: 'Stop training', auto_hint: 'Watch the fly learn on its own, then play against the trained brain.', ph_auto_tag: 'auto training', ph_auto_exam_title: 'Exam before training', ph_auto_exam_text: 'First the fly takes an exam with learning switched off. Against the clever player it loses {hl} of games, against the random one it wins {rw}.
Now it plays {n} games against mixed opponents and learns after each one. From every batch of 10 games one is replayed on the board.
', ph_auto_title: 'Auto training: game {g} of {t}', ph_auto_text: 'Replay of game {g}: the fly plays {f} against {opp}, result: {res}.
Last 10 games: {w} wins, {d} draws, {l} losses. Dopamine released: PAM {pam}, PPL1 {ppl1}. {k} Kenyon cells changed their synapses.
', ph_auto_done_title: 'Trained fly: your turn!', ph_auto_done_text: 'Exam after {n} games of training (before → after), learning switched off:
Random player: wins {rw0} → {rw}, losses {rl0} → {rl}.
Clever player: draws {hd0} → {hd}, losses {hl0} → {hl}.
Level: {lvl0} → {lvl}.
You start: click a cell to play against the trained brain. It keeps learning from your games.
', ph_exam_tag: 'exam', ph_exam_title: 'Exam results', ph_exam_text: '100 games against each opponent, with learning switched off.
Random player: {rw} wins, {rd} draws, {rl} losses.
Clever player: {hw} wins, {hd} draws, {hl} losses.
Level: {lvl}.
', err: 'Something went wrong: {e}', about: `In 2024–2025 teams from FlyWire (Princeton), HHMI Janelia, the University of Cambridge and Google mapped every neuron and synapse of the fruit fly Drosophila melanogaster: its connectome. This page runs a small model built on one well-understood circuit from that map: the mushroom body, where flies learn which smells, and here which boards, lead to reward or punishment.
Win = reward (+1), draw = small reward (+0.3), loss = punishment (−1). {tail} It is a model of the learning principle, not a neuron-by-neuron simulation.
`, about_kc_syn: 'Serwer używa teraz innej mapy połączeń, więc poprzednia mucha nie mogła zachować wspomnień. To nowa, niedoświadczona mucha.
', symbols: 'Ty: {h} · Mucha: {f}', symbols_auto: 'Mucha: {f} · {opp}: {h}', st_your_turn: 'Twój ruch: kliknij pole.', st_thinking: 'Mucha myśli…', st_fly_won: 'Mucha wygrała.', st_you_won: 'Wygrywasz!', st_draw: 'Remis.', curve_empty: 'Brak rozegranych gier.', st_new: 'Rozpocznij nową grę.', st_training: 'Trwa trening…', st_games: 'rozegrane gry', st_record: 'wygrane / remisy / przegrane', st_strong: 'KC sygnalizujące „zbliż się”', st_weak: 'KC sygnalizujące „unikaj”', st_plastic: 'plastyczność (tempo uczenia)', st_level: 'poziom (ostatni egzamin)', lvl_none: 'zrób egzamin', lvl: ['Larwa', 'Nowicjuszka', 'Uczennica', 'Wprawna', 'Mistrzyni'], j_game: 'Gra {n}', j_move: '#', j_cell: 'pole', j_expected: 'oczekiwanie', j_outcome: 'wynik', j_delta: 'δ', j_dopa: 'dopamina', j_empty: 'Zagraj, aby zobaczyć, jak zapisuje się pamięć.', j_observed: 'Obserwacja Twoich ruchów: PAM {pam}, PPL1 {ppl1}, zmienionych KC: {k}', res_win: 'mucha wygrała', res_loss: 'mucha przegrała', res_draw: 'remis', ex_games: 'po grach', ex_random: 'z losowym (W/R/P)', ex_heur: 'ze sprytnym (W/R/P)', ex_level: 'poziom', confirm_reset: 'Zastąpić tę muchę nową, niedoświadczoną? Wszystkie jej wspomnienia przepadną.', c_eyes: 'Oczy · płat wzrokowy', c_cells: 'detektory pól: moje / przeciwnika / puste', c_lines: 'detektory linii (znaki moje·przeciwnika)', c_mb: 'Ciało grzybkowate · {n} komórek Kenyona', c_apl: 'hamowanie APL: aktywnych naraz tylko {k}', c_lh: 'Róg boczny', c_app: 'MBON zbliż się', c_av: 'MBON unikaj', c_mbon_n: 'MBON: {n}', c_pam_n: 'PAM ×{n} · nagroda', c_ppl1_n: 'PPL1 ×{n} · kara', c_pam: 'PAM · nagroda', c_ppl1: 'PPL1 · kara', c_cx: 'Kompleks centralny', c_cx2: 'porównuje opcje', c_dn: 'Neurony zstępujące', c_move: 'ruch → pole {c}', ph_intro_tag: 'start', ph_intro_title: 'Niedoświadczona mucha', ph_intro_title_exp: 'Twoja mucha (doświadczenie: {n} gier)', ph_intro_text: 'To model ośrodka uczenia się muchy, czyli ciała grzybkowatego. Każda kropka w dużym kole to jedna komórka Kenyona. Szare kropki nie mają jeszcze wspomnień, zielone znaczą „ta sytuacja była dobra”, a czerwone znaczą „unikaj tego”.
Zagraj albo wyślij muchę na obóz treningowy i obserwuj, jak powstaje pamięć. Włącz Krok po kroku, aby przejść przez każdy etap myślenia i uczenia.
', ph_see_tag: '1 · percepcja', ph_see_title: 'Mucha widzi Twój ruch', ph_see_text: 'Twój znak w polu {cell} pobudza fotoreceptory. Aktywnych jest {n} neuronów czuciowych: po jednym na każde pole (moje / przeciwnika / puste) i po jednym detektorze na każdą z 8 linii.
', ph_imagine_tag: '2 · wyobraźnia', ph_imagine_title: 'Wyobraża sobie ruch na pole {cell}', ph_imagine_text: 'Mucha wyobraża sobie planszę po tym ruchu. Hamowanie APL pozwala odpalić tylko {k} z {n} komórek Kenyona, co daje rzadki „odcisk palca” tej sytuacji.
Ich synapsy przypominają: ciało grzybkowate {kcv}, róg boczny {lhv} → wartość {val}.
', ph_imagine_new: 'Te synapsy są jeszcze nietknięte, więc mucha nie ma zdania.
', ph_decide_tag: '3 · decyzja', ph_decide_title: 'Decyzja: pole {cell}', ph_decide_text: 'Kompleks centralny porównuje wszystkie wyobrażone wyniki. Najlepszą wartość {val} ma pole {cell}, więc neurony zstępujące wysyłają sygnał i mucha tam się rusza.
', ph_decide_random: 'Wszystkie opcje wydają się takie same, więc wybór jest losowy: mucha nie ma jeszcze na czym się oprzeć.
', ph_outcome_tag: '4 · wynik', ph_outcome_win: 'Mucha wygrała: nagroda!', ph_outcome_loss: 'Mucha przegrała: kara', ph_outcome_draw: 'Remis: mała nagroda', ph_outcome_text: 'Jak w żywej musze, wynik dociera jako sygnał dopaminowy: wartość nagrody {r}.
Teraz mucha odtwarza każdy swój ruch i porównuje, czego się spodziewała, z tym, co się naprawdę stało.
', ph_replay_tag: '5 · pamięć', ph_replay_title: 'Wspomnienie ruchu {i} z {n} (pole {cell})', ph_replay_text: 'Oczekiwanie {e}. Wynik {o} (nagroda {r} × {g}{k}, słabnie z odległością od końca). Błąd przewidywania δ = {d}
', ph_replay_pam: 'Neurony dopaminowe PAM strzelają → {k} aktywnych komórek Kenyona wzmacnia synapsy do „zbliż się” i osłabia do „unikaj”. Ta sytuacja jest teraz warta {after}.
', ph_replay_ppl1: 'Neurony dopaminowe PPL1 strzelają → {k} aktywnych komórek Kenyona wzmacnia synapsy do „unikaj”. Ta sytuacja jest teraz warta {after}.
', ph_replay_none: 'Brak zaskoczenia, więc z tego ruchu prawie nie ma czego się uczyć.
', ph_watch_tag: '6 · obserwacja', ph_watch_title: 'Uczenie przez obserwację', ph_watch_text: 'Mucha odtwarza też Twoje ruchy z Twojej perspektywy, więc uczy się również tego, co działało u Ciebie. Dopamina: PAM {pam}, PPL1 {ppl1}; zmienionych komórek Kenyona: {k}.
', ph_done_tag: '7 · konsolidacja', ph_done_title: 'Pamięć zaktualizowana', ph_done_text: 'Komórek Kenyona sygnalizujących „zbliż się”: {s}, a „unikaj”: {w}. Gdy mucha znów zobaczy podobną planszę, zareaguje inaczej.
Zagraj ponownie albo wyślij ją na obóz treningowy.
', ph_train_tag: 'trening', ph_train_title: 'Obóz treningowy: gra {g} z {t}', ph_train_text: 'Przeciwnik: {opp}. W tej serii: {w} wygranych, {d} remisów, {l} przegranych.
Wydzielona dopamina: PAM {pam}, PPL1 {ppl1}. Komórek Kenyona ze zmienionymi synapsami: {k}.
', ph_train_done: 'Trening zakończony', auto: 'Trenuj automatycznie ({n} gier)', auto_stop: 'Zatrzymaj trening', auto_hint: 'Zobacz, jak mucha uczy się sama, a potem zagraj z wytrenowanym mózgiem.', ph_auto_tag: 'trening automatyczny', ph_auto_exam_title: 'Egzamin przed treningiem', ph_auto_exam_text: 'Najpierw mucha zdaje egzamin z wyłączonym uczeniem. Ze sprytnym graczem przegrywa {hl} gier, a z losowym wygrywa {rw}.
Teraz rozegra {n} gier z różnymi przeciwnikami i po każdej będzie się uczyć. Z każdej serii 10 gier jedna jest odtwarzana na planszy.
', ph_auto_title: 'Trening automatyczny: gra {g} z {t}', ph_auto_text: 'Powtórka gry {g}: mucha gra jako {f}, przeciwnik: {opp}, wynik: {res}.
Ostatnie 10 gier: {w} wygranych, {d} remisów, {l} przegranych. Wydzielona dopamina: PAM {pam}, PPL1 {ppl1}. Komórek Kenyona ze zmienionymi synapsami: {k}.
', ph_auto_done_title: 'Mucha wytrenowana: Twój ruch!', ph_auto_done_text: 'Egzamin po {n} grach treningu (przed → po), z wyłączonym uczeniem:
Gracz losowy: wygrane {rw0} → {rw}, przegrane {rl0} → {rl}.
Sprytny gracz: remisy {hd0} → {hd}, przegrane {hl0} → {hl}.
Poziom: {lvl0} → {lvl}.
Zaczynasz: kliknij pole, aby zagrać z wytrenowanym mózgiem. Mucha nadal uczy się z Waszych gier.
', ph_exam_tag: 'egzamin', ph_exam_title: 'Wyniki egzaminu', ph_exam_text: 'Po 100 gier z każdym przeciwnikiem, z wyłączonym uczeniem.
Gracz losowy: {rw} wygranych, {rd} remisów, {rl} przegranych.
Sprytny gracz: {hw} wygranych, {hd} remisów, {hl} przegranych.
Poziom: {lvl}.
', err: 'Coś poszło nie tak: {e}', about: `W latach 2024–2025 zespoły FlyWire (Princeton), HHMI Janelia, Uniwersytetu Cambridge i Google zmapowały każdy neuron i każdą synapsę muszki owocowej Drosophila melanogaster, czyli jej konektom. Ta strona uruchamia mały model oparty na jednym dobrze poznanym obwodzie z tej mapy: ciele grzybkowatym, w którym muchy uczą się, które zapachy (a tutaj: które plansze) prowadzą do nagrody lub kary.
Wygrana = nagroda (+1), remis = mała nagroda (+0,3), przegrana = kara (−1). {tail} To model zasady uczenia, a nie symulacja neuron po neuronie.
`, about_kc_syn: '${t('j_empty')}
`; return; } el.innerHTML = journal.map((j) => { const res = j.result === 1 ? ['res-win', t('res_win')] : j.result === -1 ? ['res-loss', t('res_loss')] : ['res-draw', t('res_draw')]; const rows = j.replay.map((s, i) => `| ${t('j_move')} | ${t('j_cell')} | ${t('j_expected')} | ${t('j_outcome')} | ` + `${t('j_delta')} | ${t('j_dopa')} |
|---|
${t('j_observed', { pam: j.observed.pam.toFixed(2), ppl1: j.observed.ppl1.toFixed(2), k: j.observed.kc_changed })}
| ${t('ex_games')} | ${t('ex_random')} | ${t('ex_heur')} | ${t('ex_level')} |
|---|---|---|---|
| ${e.games} | ${f(e.random)} | ${f(e.heuristic)} | ${t('lvl')[levelOf(e)]} |
${t('err', { e: e.message })}
` }); } async function onCellClick(i) { if (busy || training || !game || game.over || game.turn !== -game.fly || game.board[i] !== 0) return; notice = null; busy = true; const board = game.board.slice(); board[i] = -game.fly; clearThinking(); renderBoard(board); refreshControls(); let d; try { d = await api('/api/move', { cell: i }); } catch (e) { busy = false; if (e.data && e.data.game) game = e.data.game; renderBoard(game.board); refreshControls(); showError(e); return; } const phases = [perceptionPhase(d.perception, i)]; if (d.think) phases.push(...thinkPhases(d.think)); if (d.learning) phases.push(...learningPhases(d.learning, d.game, d.state)); await runPhases(phases); game = d.game; if (!d.learning) { applyState(d.state, false); renderBoard(game.board, game.line); } renderStatus(); } async function newGame(flyStarts) { if (busy || training) return; notice = null; busy = true; refreshControls(); let d; try { d = await api('/api/new_game', { fly_starts: flyStarts }); } catch (e) { busy = false; refreshControls(); showError(e); return; } game = d.game; clearThinking(); V.eyeBoard = Array(9).fill(0); V.cxBoard = Array(9).fill(0); renderSymbols(); renderBoard(Array(9).fill(0)); if (d.think) await runPhases(thinkPhases(d.think)); else { busy = false; showPhase(introPhase()); } renderBoard(game.board); refreshControls(); } async function train(total) { if (busy || training) return; training = true; stopRequested = false; refreshControls(); clearThinking(); const opponent = $('#opponent').value; const oppName = $('#opponent').selectedOptions[0].textContent; let done = 0; let last = null; try { while (done < total && !stopRequested) { const n = Math.min(250, total - done); const d = await api('/api/train', { games: n, opponent }); done += n; applyState(d, false); setMemory(d.memory, true); const c = d.chunk; const w = c.results.filter((r) => r === 1).length; const l = c.results.filter((r) => r === -1).length; V.pam = clamp(c.pam / c.games, 0.15, 1); V.ppl1 = clamp(c.ppl1 / c.games, 0.15, 1); $('#progress-bar').style.width = pct(done / total); const g = done; last = { tag: () => t('ph_train_tag'), title: () => t('ph_train_title', { g, t: total }), html: () => t('ph_train_text', { opp: oppName, w, d: c.games - w - l, l, pam: c.pam.toFixed(1), ppl1: c.ppl1.toFixed(1), k: c.kc_changed, }), }; showPhase(last); await new Promise((r) => setTimeout(r, 120)); } } catch (e) { showError(e); last = null; } training = false; if (last) showPhase({ ...last, title: () => t('ph_train_done') }); setTimeout(() => { if (!training) $('#progress-bar').style.width = '0'; }, 1200); refreshControls(); } /* ---- automatic training: exam, 300 visible games, exam, then play ---- */ const AUTO_GAMES = 300; const AUTO_BATCH = 10; function nap(ms) { return new Promise((r) => setTimeout(r, ms * speedFactor())); } function winLineOf(b) { return LINE_ICON.find(([a, c, d]) => b[a] !== 0 && b[a] === b[c] && b[a] === b[d]) || null; } async function replayGame(lg) { viewFly = lg.fly; renderSymbols(); clearThinking(); V.eyeBoard = Array(9).fill(0); V.cxBoard = Array(9).fill(0); renderBoard(V.eyeBoard); let prev = Array(9).fill(0); for (const s of lg.steps) { if (stopRequested) return; V.cx = Array(9).fill(null); if (s.kc) { V.pn = new Set(s.pn); V.kc = s.kc; V.kcSet = new Set(s.kc); V.value = s.value; V.cx[s.cell] = s.value; V.chosen = s.cell; V.dn = 1; } else { V.pn = new Set(); V.kc = []; V.kcSet = new Set(); V.value = null; V.chosen = null; } V.cxBoard = prev; V.eyeBoard = s.board.slice(); prev = s.board.slice(); ui.focus = s.cell; renderBoard(s.board); await nap(s.kc ? 240 : 150); } ui.focus = null; renderBoard(prev, winLineOf(prev)); } function examNumbers(e) { return { rw: pct(e.random[0]), rd: pct(e.random[1]), rl: pct(e.random[2]), hw: pct(e.heuristic[0]), hd: pct(e.heuristic[1]), hl: pct(e.heuristic[2]), lvl: t('lvl')[levelOf(e)], }; } async function autoTrain() { if (busy || training) return; notice = null; training = autoRunning = true; stopRequested = false; refreshControls(); clearThinking(); const tag = () => t('ph_auto_tag'); let before = null; let after = null; let done = 0; try { let d = await api('/api/exam', {}); applyState(d, false); before = d.exam; const b = examNumbers(before); showPhase({ tag, title: () => t('ph_auto_exam_title'), html: () => t('ph_auto_exam_text', { ...b, n: AUTO_GAMES }), }); await nap(3000); while (done < AUTO_GAMES && !stopRequested) { d = await api('/api/train', { games: AUTO_BATCH, opponent: 'mix', show: true }); const c = d.chunk; const lg = c.last_game; const g = done + c.games; const w = c.results.filter((r) => r === 1).length; const l = c.results.filter((r) => r === -1).length; showPhase({ tag, title: () => t('ph_auto_title', { g, t: AUTO_GAMES }), html: () => t('ph_auto_text', { g, f: sym(lg.fly), opp: t('opp_' + lg.opponent), res: t(lg.result === 1 ? 'res_win' : lg.result === -1 ? 'res_loss' : 'res_draw'), w, d: c.games - w - l, l, pam: c.pam.toFixed(1), ppl1: c.ppl1.toFixed(1), k: c.kc_changed, }), }); await replayGame(lg); done = g; applyState(d, false); setMemory(d.memory, true); V.pam = clamp(c.pam / c.games, 0.15, 1); V.ppl1 = clamp(c.ppl1 / c.games, 0.15, 1); $('#progress-bar').style.width = pct(done / AUTO_GAMES); await nap(350); } d = await api('/api/exam', {}); applyState(d, false); after = d.exam; } catch (e) { showError(e); } viewFly = null; renderSymbols(); training = autoRunning = false; setTimeout(() => { if (!training) $('#progress-bar').style.width = '0'; }, 1200); refreshControls(); if (!after) return; await newGame(false); const b = examNumbers(before); const a = examNumbers(after); showPhase({ tag, title: () => t('ph_auto_done_title'), html: () => t('ph_auto_done_text', { ...a, n: done, rw0: b.rw, rl0: b.rl, hd0: b.hd, hl0: b.hl, lvl0: b.lvl, }), }); } async function exam() { if (busy || training) return; busy = true; refreshControls(); try { const d = await api('/api/exam', {}); applyState(d, false); const e = d.exam; showPhase({ tag: () => t('ph_exam_tag'), title: () => t('ph_exam_title'), html: () => t('ph_exam_text', { rw: pct(e.random[0]), rd: pct(e.random[1]), rl: pct(e.random[2]), hw: pct(e.heuristic[0]), hd: pct(e.heuristic[1]), hl: pct(e.heuristic[2]), lvl: t('lvl')[levelOf(e)], }), }); } catch (e) { showError(e); } busy = false; refreshControls(); } async function resetFly() { if (busy || training || !confirm(t('confirm_reset'))) return; try { const d = await api('/api/reset', {}); applyState(d); game = d.game; journal = []; renderJournal(); clearThinking(); V.eyeBoard = Array(9).fill(0); V.cxBoard = Array(9).fill(0); renderSymbols(); renderBoard(game.board); notice = null; showPhase(introPhase()); refreshControls(); } catch (e) { showError(e); } } function introPhase() { return { tag: () => t('ph_intro_tag'), title: () => (state && state.games ? t('ph_intro_title_exp', { n: state.games }) : t('ph_intro_title')), html: () => (notice ? t(notice) : '') + t('ph_intro_text'), }; } /* ================= brain canvas ================= */ const W = 1000; const H = 550; const MB = { x: 470, y: 272, r: 165 }; const LH = { x: 350, y: 500, cols: 27, gap: 9 }; const MBON_APP = { x: 725, y: 205 }; const MBON_AV = { x: 725, y: 345 }; const PAM = { x: 690, y: 88 }; const PPL1 = { x: 660, y: 470 }; const CX = { x: 810, y: 200, gap: 56 }; const DN = { x: 885, y: 470 }; const LINE_ICON = [[0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6]]; const COLORS = { fly: '#ffb547', human: '#4cc9f0', good: [61, 220, 132], bad: [255, 92, 122], neutral: [52, 62, 96], text: '#c9cee3', muted: '#7f89aa', active: '#fff6d6', }; let cv; let ctx; let pnPos = []; let kcPos = null; let lhPos = []; let scale = 1; let dpr = 1; function layout() { pnPos = []; for (let i = 0; i < 9; i++) { const bx = 24 + (i % 3) * 64; const by = 52 + Math.floor(i / 3) * 64; for (let k = 0; k < 3; k++) pnPos.push([bx + 13 + k * 16, by + 46]); } const np = S.line_patterns.length; for (let li = 0; li < 8; li++) { for (let p = 0; p < np; p++) pnPos.push([62 + p * 15, 318 + li * 21]); } kcPos = new Float32Array(S.n_kc * 2); const golden = Math.PI * (3 - Math.sqrt(5)); for (let i = 0; i < S.n_kc; i++) { const r = MB.r * Math.sqrt((i + 0.5) / S.n_kc); kcPos[i * 2] = MB.x + r * Math.cos(i * golden); kcPos[i * 2 + 1] = MB.y + r * Math.sin(i * golden); } lhPos = []; for (let i = 0; i < S.n_pn; i++) { lhPos.push([LH.x + (i % LH.cols) * LH.gap, LH.y + Math.floor(i / LH.cols) * LH.gap]); } } function resize() { dpr = window.devicePixelRatio || 1; const w = cv.clientWidth; scale = w / W; cv.width = Math.round(w * dpr); cv.height = Math.round(w * H / W * dpr); } function memColor(m, scaleTo = 1) { const s = clamp(Math.sqrt(Math.abs(m) / scaleTo) * 1.1, 0, 1); const c = m >= 0 ? COLORS.good : COLORS.bad; const n = COLORS.neutral; return `rgb(${Math.round(n[0] + (c[0] - n[0]) * s)},${Math.round(n[1] + (c[1] - n[1]) * s)},${Math.round(n[2] + (c[2] - n[2]) * s)})`; } function buildBuckets() { const buckets = new Map(); for (let i = 0; i < V.mem.length; i++) { const level = Math.round(clamp(Math.sign(V.mem[i]) * Math.sqrt(Math.abs(V.mem[i])) * 1.1, -1, 1) * 12); if (!buckets.has(level)) buckets.set(level, []); buckets.get(level).push(i); } V.buckets = [...buckets.entries()].map(([level, idx]) => ({ color: memColor(Math.sign(level) * (level / 12 / 1.1) ** 2), idx, })); } function text(s, x, y, opts = {}) { const size = (opts.size || 12) * (opts.raw ? 1 : 1.3); ctx.font = `${opts.bold ? '600 ' : ''}${size}px system-ui, -apple-system, "Segoe UI", sans-serif`; ctx.fillStyle = opts.color || COLORS.muted; ctx.textAlign = opts.align || 'left'; ctx.textBaseline = opts.base || 'alphabetic'; if (opts.max) ctx.fillText(s, x, y, opts.max); else ctx.fillText(s, x, y); } function circle(x, y, r, fill, stroke, lw = 1) { ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); if (fill) { ctx.fillStyle = fill; ctx.fill(); } if (stroke) { ctx.strokeStyle = stroke; ctx.lineWidth = lw; ctx.stroke(); } } function glow(x, y, r, rgb, a) { if (a <= 0.01) return; const g = ctx.createRadialGradient(x, y, 0, x, y, r); g.addColorStop(0, `rgba(${rgb[0]},${rgb[1]},${rgb[2]},${a})`); g.addColorStop(1, `rgba(${rgb[0]},${rgb[1]},${rgb[2]},0)`); ctx.fillStyle = g; ctx.fillRect(x - r, y - r, r * 2, r * 2); } function drawEyes() { text(t('c_eyes'), 24, 30, { size: 14, bold: true, color: COLORS.text }); for (let i = 0; i < 9; i++) { const bx = 24 + (i % 3) * 64; const by = 52 + Math.floor(i / 3) * 64; ctx.fillStyle = '#0f1528'; ctx.strokeStyle = V.imagined === i ? COLORS.fly : '#27304f'; ctx.lineWidth = V.imagined === i ? 2 : 1; ctx.setLineDash(V.imagined === i ? [4, 3] : []); ctx.beginPath(); ctx.roundRect(bx, by, 58, 58, 8); ctx.fill(); ctx.stroke(); ctx.setLineDash([]); const v = V.eyeBoard[i]; if (v) { const isFly = v === flySymbol(); text(sym(v), bx + 29, by + 30, { size: 22, bold: true, align: 'center', base: 'middle', color: isFly ? 'rgba(255,181,71,.55)' : 'rgba(76,201,240,.55)' }); } for (let k = 0; k < 3; k++) { const [x, y] = pnPos[i * 3 + k]; const on = V.pn.has(i * 3 + k); const col = k === 0 ? COLORS.fly : k === 1 ? COLORS.human : '#dfe4f5'; if (on) glow(x, y, 12, k === 0 ? [255, 181, 71] : k === 1 ? [76, 201, 240] : [223, 228, 245], 0.5); circle(x, y, 5, on ? col : '#232b47'); } } text(t('c_cells'), 24, 258, { size: 10, max: 262 }); text(t('c_lines'), 24, 290, { size: 10, max: 262 }); const np = S.line_patterns.length; S.line_patterns.forEach((p, k) => text(`${p[0]}·${p[1]}`, 62 + k * 15, 306, { size: 8, align: 'center', raw: true })); for (let li = 0; li < 8; li++) { const y = 318 + li * 21; for (let c = 0; c < 9; c++) { const on = LINE_ICON[li].includes(c); ctx.fillStyle = on ? '#9aa3c0' : '#232b47'; ctx.fillRect(24 + (c % 3) * 5, y - 7 + Math.floor(c / 3) * 5, 4, 4); } for (let p = 0; p < np; p++) { const idx = S.n_cell + li * np + p; const [x] = pnPos[idx]; const on = V.pn.has(idx); let col = '#232b47'; if (on) { const [m, o] = S.line_patterns[p]; col = m && !o ? COLORS.fly : o && !m ? COLORS.human : m && o ? '#a78bfa' : '#dfe4f5'; glow(x, y, 10, [255, 240, 200], 0.35); } circle(x, y, 4.5, col); } } } function drawMB() { text(t('c_mb', { n: S.n_kc }), MB.x, 30, { size: 14, bold: true, align: 'center', color: COLORS.text, max: 420 }); text(t('c_apl', { k: S.k_active }), MB.x, 52, { size: 11, align: 'center', max: 420 }); circle(MB.x, MB.y, MB.r + 9, '#0c1226', '#1f2848', 1.5); glow(MB.x, MB.y, MB.r * 1.25, COLORS.good, V.pam * 0.35); glow(MB.x, MB.y, MB.r * 1.25, COLORS.bad, V.ppl1 * 0.35); if (!V.buckets) buildBuckets(); const s = 2.4; for (const b of V.buckets) { ctx.fillStyle = b.color; ctx.beginPath(); for (const i of b.idx) ctx.rect(kcPos[i * 2] - s / 2, kcPos[i * 2 + 1] - s / 2, s, s); ctx.fill(); } if (V.flashing) { let any = false; for (let i = 0; i < V.flash.length; i++) { const f = V.flash[i]; if (f <= 0) continue; any = true; const c = V.flashSign[i] > 0 ? COLORS.good : COLORS.bad; ctx.fillStyle = `rgba(${c[0]},${c[1]},${c[2]},${(f * 0.9).toFixed(3)})`; ctx.fillRect(kcPos[i * 2] - 2.5, kcPos[i * 2 + 1] - 2.5, 5, 5); } V.flashing = any; } if (V.kc.length) { ctx.fillStyle = COLORS.active; ctx.shadowColor = '#ffe08a'; ctx.shadowBlur = 6; ctx.beginPath(); for (const i of V.kc) ctx.rect(kcPos[i * 2] - 2.2, kcPos[i * 2 + 1] - 2.2, 4.4, 4.4); ctx.fill(); ctx.shadowBlur = 0; } // lateral horn text(t('c_lh'), LH.x, LH.y - 14, { size: 12, bold: true, color: COLORS.text }); for (let i = 0; i < lhPos.length; i++) { const [x, y] = lhPos[i]; circle(x, y, 3, V.lh ? memColor(V.lh[i], 4) : '#232b47'); if (V.pn.has(i)) circle(x, y, 4.5, null, COLORS.active, 1.2); } } function drawLinks() { if (!V.kc.length) return; const sample = V.kc.filter((_, j) => j % 10 === 0); ctx.lineWidth = 0.7; if (V.pn.size) { ctx.strokeStyle = 'rgba(255,224,138,0.16)'; ctx.beginPath(); let j = 0; for (const p of V.pn) { const [x, y] = pnPos[p]; for (let r = 0; r < 2; r++) { const k = sample[(j * 3 + r * 7) % sample.length]; ctx.moveTo(x, y); ctx.lineTo(kcPos[k * 2], kcPos[k * 2 + 1]); } j++; } ctx.stroke(); } for (const k of sample.slice(0, 12)) { const m = V.mem[k]; const target = m >= 0 ? MBON_APP : MBON_AV; const c = m >= 0 ? COLORS.good : COLORS.bad; ctx.strokeStyle = `rgba(${c[0]},${c[1]},${c[2]},${(0.12 + Math.min(0.5, Math.abs(m))).toFixed(2)})`; ctx.beginPath(); ctx.moveTo(kcPos[k * 2], kcPos[k * 2 + 1]); ctx.lineTo(target.x, target.y); ctx.stroke(); } } function drawOutputs() { const v = V.value; const actApp = v === null ? 0 : clamp(0.5 + v / 2, 0, 1); const actAv = v === null ? 0 : clamp(0.5 - v / 2, 0, 1); glow(MBON_APP.x, MBON_APP.y, 50, COLORS.good, actApp * 0.6); glow(MBON_AV.x, MBON_AV.y, 50, COLORS.bad, actAv * 0.6); circle(MBON_APP.x, MBON_APP.y, 12 + 12 * actApp, `rgba(61,220,132,${0.2 + 0.7 * actApp})`, '#3ddc84', 1.5); circle(MBON_AV.x, MBON_AV.y, 12 + 12 * actAv, `rgba(255,92,122,${0.2 + 0.7 * actAv})`, '#ff5c7a', 1.5); text(t('c_app'), MBON_APP.x, MBON_APP.y - 32, { size: 11, align: 'center', color: COLORS.text }); text(t('c_av'), MBON_AV.x, MBON_AV.y + 40, { size: 11, align: 'center', color: COLORS.text }); const c = fw(); if (c) { const n = (v) => c.mbons.filter((m) => m.valence === v).length; text(t('c_mbon_n', { n: n(1) }), MBON_APP.x, MBON_APP.y - 48, { size: 9, align: 'center' }); text(t('c_mbon_n', { n: n(-1) }), MBON_AV.x, MBON_AV.y + 56, { size: 9, align: 'center' }); } if (v !== null) { text(fmt(v), (MBON_APP.x + MBON_AV.x) / 2, (MBON_APP.y + MBON_AV.y) / 2 + 4, { size: 13, bold: true, align: 'center', color: v >= 0 ? '#3ddc84' : '#ff5c7a' }); } // dopamine neurons for (const [pos, level, rgb, label, below] of [[PAM, V.pam, COLORS.good, fw() ? t('c_pam_n', { n: fw().n_pam }) : t('c_pam'), false], [PPL1, V.ppl1, COLORS.bad, fw() ? t('c_ppl1_n', { n: fw().n_ppl1 }) : t('c_ppl1'), true]]) { glow(pos.x, pos.y, 60, rgb, level * 0.8); if (level > 0.05) { ctx.strokeStyle = `rgba(${rgb[0]},${rgb[1]},${rgb[2]},${(level * 0.7).toFixed(2)})`; ctx.lineWidth = 2; for (const a of [-0.5, 0, 0.5]) { const tx = MB.x + MB.r * Math.cos(a + (below ? 0.9 : -0.9)); const ty = MB.y + MB.r * Math.sin(a + (below ? 0.9 : -0.9)); ctx.beginPath(); ctx.moveTo(pos.x, pos.y); ctx.quadraticCurveTo((pos.x + tx) / 2, pos.y, tx, ty); ctx.stroke(); } } circle(pos.x, pos.y, 14, `rgba(${rgb[0]},${rgb[1]},${rgb[2]},${0.25 + 0.75 * level})`, `rgb(${rgb[0]},${rgb[1]},${rgb[2]})`, 1.5); text(label, pos.x + 22, pos.y + 4, { size: 11, color: COLORS.text }); } } function drawCX() { const cx0 = CX.x + CX.gap; text(t('c_cx'), cx0, 138, { size: 14, bold: true, align: 'center', color: COLORS.text }); text(t('c_cx2'), cx0, 160, { size: 11, align: 'center' }); for (let i = 0; i < 9; i++) { const x = CX.x + (i % 3) * CX.gap; const y = CX.y + Math.floor(i / 3) * CX.gap; const occ = V.cxBoard[i]; const val = V.cx[i]; let fill = '#141b33'; if (val !== null && !occ) fill = memColor(val); circle(x, y, 20, fill, i === V.chosen ? COLORS.fly : i === V.cxFocus ? '#ffffff' : '#27304f', i === V.chosen || i === V.cxFocus ? 2.5 : 1); if (occ) { const isFly = occ === flySymbol(); text(sym(occ), x, y + 1, { size: 16, bold: true, align: 'center', base: 'middle', color: isFly ? 'rgba(255,181,71,.6)' : 'rgba(76,201,240,.6)' }); } else if (val !== null) { text(fmt(val), x, y + 1, { size: 10, bold: true, align: 'center', base: 'middle', color: '#0b1020' }); } else { text(String(i + 1), x, y + 1, { size: 10, align: 'center', base: 'middle' }); } } if (V.cxFocus !== null && V.value !== null) { const x = CX.x + (V.cxFocus % 3) * CX.gap; const y = CX.y + Math.floor(V.cxFocus / 3) * CX.gap; ctx.strokeStyle = V.value >= 0 ? 'rgba(61,220,132,.6)' : 'rgba(255,92,122,.6)'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(MBON_APP.x + 18, (MBON_APP.y + MBON_AV.y) / 2); ctx.lineTo(x - 20, y); ctx.stroke(); } // descending neurons text(t('c_dn'), DN.x, DN.y - 34, { size: 12, bold: true, align: 'center', color: COLORS.text }); if (V.chosen !== null && V.dn > 0.02) { const x = CX.x + (V.chosen % 3) * CX.gap; const y = CX.y + Math.floor(V.chosen / 3) * CX.gap; ctx.strokeStyle = `rgba(255,181,71,${V.dn.toFixed(2)})`; ctx.lineWidth = 3; ctx.beginPath(); ctx.moveTo(x, y + 20); ctx.lineTo(DN.x, DN.y - 20); ctx.stroke(); glow(DN.x, DN.y, 60, [255, 181, 71], V.dn * 0.7); } circle(DN.x, DN.y, 18, `rgba(255,181,71,${0.15 + 0.85 * V.dn})`, COLORS.fly, 1.5); if (V.chosen !== null) text(t('c_move', { c: V.chosen + 1 }), DN.x, DN.y + 38, { size: 12, align: 'center', color: COLORS.text }); } let lastT = 0; function frame(now) { const dt = Math.min(0.1, (now - lastT) / 1000 || 0); lastT = now; V.pam = Math.max(0, V.pam - dt * 0.45); V.ppl1 = Math.max(0, V.ppl1 - dt * 0.45); V.dn = Math.max(0, V.dn - dt * 0.35); if (V.flashing) { for (let i = 0; i < V.flash.length; i++) if (V.flash[i] > 0) V.flash[i] = Math.max(0, V.flash[i] - dt * 0.6); } ctx.setTransform(dpr * scale, 0, 0, dpr * scale, 0, 0); ctx.clearRect(0, 0, W, H); if (S && V.mem) { drawLinks(); drawEyes(); drawMB(); drawOutputs(); drawCX(); } requestAnimationFrame(frame); } /* ================= language ================= */ function applyLang() { document.documentElement.lang = lang; document.title = t('title'); document.querySelectorAll('[data-i18n]').forEach((el) => { el.textContent = t(el.dataset.i18n); }); document.querySelectorAll('[data-lang]').forEach((b) => b.classList.toggle('on', b.dataset.lang === lang)); renderAbout(); renderSymbols(); refreshControls(); renderStats(); renderJournal(); renderExams(); renderCurve(); if (currentPhase) showPhase(currentPhase); } function fw() { return S && S.connectome && S.connectome.kind === 'flywire' ? S.connectome : null; } function renderAbout() { const c = fw(); let kc; let mbon = ''; if (c) { const n = (v) => c.mbons.filter((m) => m.valence === v).length; const avgSyn = Math.round(c.pn_kc_synapses / (c.n_kc - c.n_kc_silent)); kc = t('about_kc_fw', { n: c.n_kc, side: t('side_' + c.side), inputs: c.inputs.length, ex: c.inputs.slice(0, 3).join(', '), syn: avgSyn, claws: c.claws }); mbon = t('about_mbon_fw', { n: c.mbons.length, app: n(1), av: n(-1) }); } else { kc = t('about_kc_syn', { n: S ? S.n_kc : 4000, claws: S && S.connectome ? S.connectome.claws : 7 }); } $('#about').innerHTML = t('about', { kc, mbon, tail: t(c ? 'about_tail_fw' : 'about_tail_syn') }); $('#footer').textContent = t(c ? 'footer_fw' : 'footer'); const badge = $('#connectome'); badge.textContent = !S ? '' : c ? t('badge_fw', { side: t('side_' + c.side) }) : t('badge_syn'); badge.classList.toggle('real', !!c); } /* ================= start ================= */ async function init() { cv = $('#brain'); ctx = cv.getContext('2d'); buildBoard(); document.querySelectorAll('[data-lang]').forEach((b) => b.addEventListener('click', () => { lang = b.dataset.lang; try { localStorage.setItem('lang', lang); } catch (e) { /* storage unavailable */ } applyLang(); })); $('#btn-new-human').addEventListener('click', () => newGame(false)); $('#btn-new-fly').addEventListener('click', () => newGame(true)); $('#btn-next').addEventListener('click', releaseWait); $('#step-mode').addEventListener('change', armWait); $('#speed').addEventListener('input', armWait); document.querySelectorAll('[data-train]').forEach((b) => b.addEventListener('click', () => train(+b.dataset.train))); $('#btn-stop').addEventListener('click', () => { stopRequested = true; }); $('#btn-auto').addEventListener('click', () => { if (autoRunning) stopRequested = true; else autoTrain(); }); $('#btn-exam').addEventListener('click', exam); $('#btn-reset').addEventListener('click', resetFly); window.addEventListener('resize', () => { resize(); renderCurve(); }); applyLang(); try { S = await api('/api/structure'); const st = await api('/api/state'); V.flash = new Float32Array(S.n_kc); V.flashSign = new Int8Array(S.n_kc); layout(); resize(); renderAbout(); if (st.notice === 'connectome_changed') notice = 'notice_rewired'; applyState(st); game = st.game; V.eyeBoard = game.board.slice(); V.cxBoard = game.board.slice(); renderSymbols(); renderBoard(game.board, game.line); showPhase(introPhase()); refreshControls(); } catch (e) { showError(e); } requestAnimationFrame(frame); } init();