app.mjs
javascript · 38 lines · Exact bytes
Version & provenance · browser-local-cm7-v1
Recorded SHA-256 6ca1e65fc4ef7ff7d66944440247f62442acb4e31c96b671825799f619995502Rendered SHA-256 6ca1e65fc4ef7ff7d66944440247f62442acb4e31c96b671825799f619995502
Apache-2.0 License notes. Line links are stable within this snapshot. CM-7 schema-2 state-input browser integration; FP32 selected update2232 checkpoint 29e68a8e85336cbb1f56efdad36bd76217728b6a376d1e5f74e89887d21383da. ONNX Runtime Web 1.24.3, chess.js 1.4.0. Exact integrated source; standard-game UI, fixed masked120-slot prompt,512 raw output classes. Three-ply browser helper is separate from live seven-ply search. Original UI and terminal PGN export preserved; prototype scorer/codec/worker/runner unchanged except adding PGN export utility.
1import {Chess} from 'chess.js';
2import {uci,outcome,exportPgn} from './policy.mjs';
3const $=id=>document.getElementById(id), game=new Chess();let worker,manifest,ready=false,busy=false,selected=null,epoch=0;
4const glyphs={w:{k:'♔',q:'♕',r:'♖',b:'♗',n:'♘',p:'♙'},b:{k:'♚',q:'♛',r:'♜',b:'♝',n:'♞',p:'♟'}};
5function status(text){$('status').textContent=text;}
6function render(){
7 $('board').replaceChildren();const reverse=$('color').value==='b';
8 const targets=selected?game.moves({square:selected,verbose:true}):[];
9 for(let row=0;row<8;row++)for(let col=0;col<8;col++){
10 const f=reverse?7-col:col,r=reverse?row:7-row,s=String.fromCharCode(97+f)+(r+1),p=game.get(s),button=document.createElement('button');
11 button.className=`square ${(f+r)%2===0?'dark':''} ${selected===s?'selected':''} ${targets.some(m=>m.to===s)?'target':''} ${p?.color==='w'?'white-piece':'black-piece'}`;
12 button.setAttribute('aria-label',`${s}${p?' '+(p.color==='w'?'white':'black')+' '+({k:'king',q:'queen',r:'rook',b:'bishop',n:'knight',p:'pawn'})[p.type]:''}`);button.dataset.square=s;
13 button.textContent=p?glyphs[p.color][p.type]:'';const label=document.createElement('small');label.textContent=s;button.append(label);
14 button.onclick=()=>click(s);$('board').append(button);
15 }
16 game.header('Event','LittlePly local browser game','ModelVersion',manifest?.version||'cm-7-u2232-29e68a8e8533-fp32-v1','CheckpointSHA256',manifest?.checkpointSha256||'29e68a8e85336cbb1f56efdad36bd76217728b6a376d1e5f74e89887d21383da');$('moves').textContent=game.pgn();
17}
18function click(s){
19 if(!ready||busy||game.turn()!==$('color').value||outcome(game))return;
20 if(selected){const moves=game.moves({square:selected,verbose:true}).filter(m=>m.to===s);if(moves.length){const move=moves.find(m=>m.promotion===$('promotion').value)||moves[0];game.move(move);selected=null;render();void turn();return;}}
21 selected=game.get(s)?.color===game.turn()?s:null;render();
22}
23async function turn(){const end=outcome(game);if(end){game.header('Result',end==='checkmate'?(game.turn()==='w'?'0-1':'1-0'):'1/2-1/2');render();status(end==='checkmate'?'Checkmate. Game over.':'Draw. Game over.');return;}if(game.turn()===$('color').value){status('Your move.');return;}busy=true;status('LittlePly is thinking…');worker.postMessage({id:epoch,type:'move',moves:game.history({verbose:true}).map(uci)});}
24function makeWorker(){worker=new Worker(new URL('./worker.js',import.meta.url),{type:'module'});worker.onmessage=({data})=>{
25 if(data.id!==epoch)return;
26 if(data.type==='progress')status(`Downloading model: ${Math.round(data.progress*100)}%`);
27 if(data.type==='ready'){ready=true;busy=false;status('Model ready.');$('load').textContent='Model loaded';void turn();}
28 if(data.type==='result'){busy=false;try{game.move({from:data.move.slice(0,2),to:data.move.slice(2,4),promotion:data.move[4]});if(data.tactical?.status==='proven'){game.setComment('Mate-helper override; model not queried');$('decision').textContent='Last bot move: bounded mate-helper override (model not queried).';}else{$('decision').textContent='Last bot move: legal candidate ranked by the model.';}render();void turn();}catch(e){status(`Invalid model move: ${e.message}`);}}
29 if(data.type==='error'){busy=false;status(`Unable to continue: ${data.message}`);if(!ready){$('load').disabled=false;$('load').textContent='Retry model download';}}
30};worker.onerror=e=>{busy=false;ready=false;status(`Worker failed: ${e.message}`);$('load').disabled=false;};}
31$('load').onclick=()=>{if(busy)return;epoch++;worker?.terminate();makeWorker();busy=true;ready=false;$('load').disabled=true;status('Preparing model download…');worker.postMessage({id:epoch,type:'load',manifest});};
32$('reset').onclick=()=>{epoch++;if(busy){worker?.terminate();ready=false;busy=false;$('load').disabled=!manifest;$('load').textContent='Download and load model';}game.reset();$('decision').textContent='';selected=null;render();if(ready)void turn();else status('Game reset. Load the model to play.');};
33$('color').onchange=()=>$('reset').click();
34$('export').onclick=()=>{const url=URL.createObjectURL(new Blob([exportPgn(game)],{type:'application/x-chess-pgn'}));const a=document.createElement('a');a.href=url;a.download='littleply.pgn';a.click();setTimeout(()=>URL.revokeObjectURL(url),1000);};
35render();
36try{if(!navigator.gpu)throw Error('This browser does not support WebGPU. Use a supported desktop browser with hardware acceleration.');const url=new URL('./models/manifest.json',import.meta.url);const response=await fetch(url);if(!response.ok)throw Error('Model manifest is unavailable');manifest=await response.json();manifest.url=url.href;$('details').textContent=`${(manifest.bytes/1e6).toFixed(1)} MB model + ${(manifest.runtime.bytes/1e6).toFixed(1)} MB runtime download · FP32 · Desktop WebGPU · No server inference`;$('load').textContent=`Download ${((manifest.bytes+manifest.runtime.bytes)/1e6).toFixed(1)} MB and play`;$('load').disabled=false;status('Ready to download. Model files may be cached by your browser.');}catch(e){status(e.message);}
37
38