Download brain_lab/static/games.js from Chungulus/brain-lab: direct link, hf CLI and curl.
- Browser
- Download file 7.53 kB
-
https://huggingface.co/spaces/Chungulus/brain-lab/resolve/main/brain_lab/static/games.js
- Command line
-
hf download hf://spaces/Chungulus/brain-lab/brain_lab/static/games.js
-
curl -L -o games.js https://huggingface.co/spaces/Chungulus/brain-lab/resolve/main/brain_lab/static/games.js
7.53 kB
| import {api,state,toast,card,traceHTML,drawTrace,activity,rangeHTML} from './common.js'; | |
| import {clamp,paddleMotion} from './pong-motion.js?v=brain-polish-39'; | |
| export async function arcade(root){ | |
| const config=await api('pong/config');if(!root.isConnected)return()=>{}; | |
| root.innerHTML=card('Human vs. spiking network',`<div class="scorebar"><span class="eyebrow">YOU / LEFT PADDLE</span><span class="score" id="pong-score">0 : 0</span><span class="eyebrow">SNN / RIGHT PADDLE</span></div><canvas class="pong-board" id="pong" width="760" height="430" tabindex="0" aria-label="Pong. Move pointer or use up and down arrows to control left paddle."></canvas><div class="toolbar"><span class="note">Move your mouse or touch. ↑ / ↓ also work.</span><div class="button-row"><button id="pong-start" class="primary">Start a match</button><button id="pong-reset" class="secondary">Reset score</button></div></div><p class="message" id="pong-message">Ready with optimal defaults. The SNN predicts a paddle target; smooth movement follows that target.</p><p class="note" id="pong-controller-status">64 LIF neurons · 10 simulation steps · smooth control on</p>`,'TRAINED SNN · SMOOTH CONTROL')+card('Change the way neurons fire',traceHTML()+rangeHTML('Firing threshold','threshold',config.optimal_threshold)+`<div class="world-controls"><button id="pong-optimal" class="secondary">Restore optimal defaults</button><span class="note" id="pong-preset">Optimal default · threshold ${config.optimal_threshold.toFixed(1)}</span></div><p class="note">The threshold was selected on separate validation states. The spike raster shows 32 of 64 LIF neurons from the same forward pass that predicted the paddle target. The movement controller follows that prediction; it does not calculate a ball-interception solution.</p>`,'ACTUAL SPIKES · LEARNED TARGET'); | |
| const $=selector=>root.querySelector(selector),canvas=$('#pong'),ctx=canvas.getContext('2d'); | |
| const keys=new Set(),height=.19,step=1/120; | |
| let alive=true,running=false,busy=false,py=.5,ay=.5,velocity=0,target=.5,filteredTarget=.5; | |
| let ball={x:.5,y:.5,vx:.4,vy:.2},human=0,ai=0,anim,last=performance.now(),lastInference=0,accumulator=0,rally=0,request=0,lastReply=0; | |
| function resetBall(direction){ball={x:.5,y:.3+Math.random()*.4,vx:.4*direction,vy:(Math.random()-.5)*.5};rally++;request++;target=ay;filteredTarget=ay;velocity=0;lastReply=0;lastInference=0} | |
| const move=e=>{const bounds=canvas.getBoundingClientRect();py=clamp((e.clientY-bounds.top)/bounds.height,height/2,1-height/2)}; | |
| canvas.onpointermove=move;canvas.onpointerdown=e=>{canvas.setPointerCapture(e.pointerId);move(e)}; | |
| const kd=e=>{if(['ArrowUp','ArrowDown'].includes(e.key)&&document.activeElement===canvas){keys.add(e.key);e.preventDefault()}}; | |
| const ku=e=>keys.delete(e.key);const blur=()=>keys.clear(); | |
| document.addEventListener('keydown',kd);document.addEventListener('keyup',ku);window.addEventListener('blur',blur); | |
| async function infer(){ | |
| busy=true;const seen=++request,seenRally=rally; | |
| try{ | |
| const result=await api('pong',{state:[ball.x,ball.y,ball.vx/.65,ball.vy/.65,ay],threshold:Number($('#threshold').value)}); | |
| if(!alive||!running||seen!==request||seenRally!==rally)return; | |
| target=result.target_y;lastReply=performance.now(); | |
| drawTrace(result);activity(['occipital','basal',...(result.action?['motor']:[])]); | |
| $('#pong-controller-status').textContent=`64 LIF neurons · ${result.spike_count} spikes · ${state.modules.cerebellum?'smooth control on':'smoothing module off'}`; | |
| }catch(error){if(alive&&seen===request){lastReply=0;target=ay;velocity=0;$('#pong-message').textContent=error.message}} | |
| finally{busy=false} | |
| } | |
| function tick(){ | |
| if(keys.has('ArrowUp'))py-=step*.9;if(keys.has('ArrowDown'))py+=step*.9;py=clamp(py,height/2,1-height/2); | |
| const fresh=lastReply>0&&performance.now()-lastReply<400; | |
| filteredTarget+=(target-filteredTarget)*(1-Math.exp(-step/.065)); | |
| if(state.modules.motor&&fresh){const next=paddleMotion(ay,velocity,filteredTarget,step,state.modules.cerebellum);ay=next.position;velocity=next.velocity}else velocity=0; | |
| const previousX=ball.x;ball.x+=ball.vx*step;ball.y+=ball.vy*step; | |
| if(ball.y<.02){ball.y=.04-ball.y;ball.vy=Math.abs(ball.vy)}if(ball.y>.98){ball.y=1.96-ball.y;ball.vy=-Math.abs(ball.vy)} | |
| for(const [plane,paddle,direction] of [[.058,py,1],[.942,ay,-1]]){ | |
| const crossing=direction===1?previousX>=plane&&ball.x<plane:previousX<=plane&&ball.x>plane; | |
| if(crossing&&ball.vx*direction<0&&Math.abs(ball.y-paddle)<height/2+.012){ | |
| ball.x=plane+direction*.001;ball.vx=direction*Math.min(.85,Math.abs(ball.vx)*1.03);ball.vy=clamp((ball.y-paddle)*6,-.9,.9); | |
| rally++;request++;lastInference=0;lastReply=0;target=ay;filteredTarget=ay;velocity=0; | |
| } | |
| } | |
| if(ball.x<0){ai++;resetBall(1)}if(ball.x>1){human++;resetBall(-1)}$('#pong-score').textContent=`${human} : ${ai}`; | |
| } | |
| function draw(){ | |
| ctx.fillStyle='#183d31';ctx.fillRect(0,0,760,430);ctx.strokeStyle='#436451';ctx.setLineDash([5,9]);ctx.beginPath();ctx.moveTo(380,15);ctx.lineTo(380,415);ctx.stroke();ctx.setLineDash([]); | |
| if(running&&lastReply&&state.modules.motor){ctx.strokeStyle='#d6b76488';ctx.setLineDash([3,4]);ctx.beginPath();ctx.moveTo(695,filteredTarget*430);ctx.lineTo(739,filteredTarget*430);ctx.stroke();ctx.setLineDash([])} | |
| ctx.fillStyle='#eff4e2';ctx.fillRect(760*.058-13,430*(py-height/2),8,height*430);ctx.fillStyle=state.modules.motor?'#e1b45a':'#6e7c64';ctx.fillRect(760*.942+5,430*(ay-height/2),8,height*430); | |
| ctx.fillStyle='#e8b55b';ctx.beginPath();ctx.arc(ball.x*760,ball.y*430,5,0,Math.PI*2);ctx.fill(); | |
| canvas.dataset.paddleY=ay.toFixed(5);canvas.dataset.targetY=target.toFixed(5);canvas.dataset.ballX=ball.x.toFixed(5);canvas.dataset.ballY=ball.y.toFixed(5); | |
| if(!running){ctx.fillStyle='#b3c6a5';ctx.font='12px sans-serif';ctx.textAlign='center';ctx.fillText('A little friendly neural competition.',380,190);ctx.textAlign='left'} | |
| } | |
| function frame(t){ | |
| const elapsed=clamp((t-last)/1000,0,.05);last=t; | |
| if(running){ | |
| if(t-lastInference>=50&&!busy){lastInference=t;infer()} | |
| accumulator+=elapsed;while(accumulator>=step){tick();accumulator-=step} | |
| }else accumulator=0; | |
| draw();if(alive)anim=requestAnimationFrame(frame); | |
| } | |
| function preset(){const value=Number($('#threshold').value),optimal=Math.abs(value-config.optimal_threshold)<.001;$('#threshold').nextElementSibling.textContent=value.toFixed(1);$('#pong-preset').textContent=optimal?`Optimal default · threshold ${value.toFixed(1)}`:'Experimenting · restore optimal defaults to compare';request++;lastReply=0;velocity=0;lastInference=0} | |
| $('#threshold').oninput=preset; | |
| $('#pong-optimal').onclick=()=>{$('#threshold').value=config.optimal_threshold;preset();target=ay;filteredTarget=ay;toast('Optimal firing threshold restored.')}; | |
| $('#pong-start').onclick=()=>{running=!running;request++;lastReply=0;velocity=0;keys.clear();$('#pong-start').textContent=running?'Pause match':'Resume match';if(running)canvas.focus();$('#pong-message').textContent=running?'Playing. The dotted mark is the SNN’s aim. Try changing the firing threshold and watch its spikes.':'Paused. Spikes stay at their last recorded values.'}; | |
| $('#pong-reset').onclick=()=>{human=ai=0;py=ay=.5;resetBall(1);$('#pong-score').textContent='0 : 0'}; | |
| anim=requestAnimationFrame(frame); | |
| return()=>{alive=false;running=false;request++;cancelAnimationFrame(anim);document.removeEventListener('keydown',kd);document.removeEventListener('keyup',ku);window.removeEventListener('blur',blur)}; | |
| } | |