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',`
64 LIF neurons · 10 simulation steps · smooth control on
`,'TRAINED SNN · SMOOTH CONTROL')+card('Change the way neurons fire',traceHTML()+rangeHTML('Firing threshold','threshold',config.optimal_threshold)+`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.
`,'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