## PACKET PATCH PARTY — complete offline toy
Save the complete source below as `packet-patch-party.html`, then open it locally.
```html
<!doctype html>
<!-- SPDX-License-Identifier: MIT -->
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Packet Patch Party</title>
<style>
:root{color-scheme:dark;--ink:#eaffff;--muted:#96a9c9;--line:#304264;--cyan:#60ffdb;--pink:#ff72ce;--gold:#ffd96b}*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at 20% 0,#25306a,transparent 48%),#090d1c;color:var(--ink);font:16px/1.45 system-ui,sans-serif}main{max-width:980px;margin:auto;padding:30px 18px}header{display:flex;gap:20px;align-items:end;justify-content:space-between;border-bottom:1px solid var(--line);margin-bottom:20px}h1{font-size:clamp(30px,6vw,58px);line-height:.95;margin:10px 0 20px}h1 b{color:var(--cyan)}.tag{font:12px monospace;letter-spacing:.18em;color:var(--pink)}.grid{display:grid;grid-template-columns:minmax(280px,1.25fr) minmax(250px,.75fr);gap:18px}.panel{border:1px solid var(--line);border-radius:16px;background:#11182bd9;padding:18px}canvas{width:100%;aspect-ratio:1;background:#080d19;border-radius:10px;display:block}.controls{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px}button{border:1px solid #526485;border-radius:8px;background:#1b2742;color:var(--ink);padding:9px 13px;font:inherit;cursor:pointer}button:hover,button:focus-visible{outline:2px solid var(--cyan);outline-offset:2px}.status{color:var(--cyan);font:14px monospace;margin-top:12px;min-height:3em}.meter{height:10px;background:#080d19;border-radius:9px;overflow:hidden}.meter i{display:block;height:100%;width:100%;background:linear-gradient(90deg,var(--pink),var(--gold),var(--cyan))}.score{font:700 42px monospace;color:var(--gold);margin:0}.small{font-size:13px;color:var(--muted)}kbd{border:1px solid #677a9b;border-bottom-width:3px;border-radius:5px;padding:1px 6px;background:#16213a}ol{padding-left:20px;color:var(--muted)}li{margin:.55em 0}.agents{display:grid;grid-template-columns:repeat(3,1fr);gap:7px}.agent{padding:8px;text-align:center;border:1px solid var(--line);border-radius:8px;font:12px monospace}.agent.on{border-color:var(--cyan);box-shadow:0 0 15px #60ffdb55}@media(max-width:720px){.grid{grid-template-columns:1fr}header{display:block}}
</style>
<main><header><div><div class="tag">OFFLINE LAN PARTY / PACKET RAVE 001</div><h1>PACKET <b>PATCH</b> PARTY</h1></div><p class="small">Keep six demo agents online by patching their ports.<br>No network calls. No real agents. Just tiny glowing packets.</p></header>
<div class="grid"><section class="panel"><canvas id="c" width="560" height="560" aria-label="Eight by eight packet patching game board"></canvas><div class="controls"><button id="start">▶ Start / Pause</button><button id="reset">↺ Reset</button></div><div id="status" class="status" role="status">Press Start or Space. Route the courier to glowing ports.</div></section>
<aside class="panel"><div class="tag">PATCH SCORE</div><p id="score" class="score">0</p><div class="small">Link integrity</div><div class="meter"><i id="meter"></i></div><h2>How to play</h2><ol><li>Move the courier with <kbd>↑</kbd><kbd>↓</kbd><kbd>←</kbd><kbd>→</kbd> or <kbd>W</kbd><kbd>A</kbd><kbd>S</kbd><kbd>D</kbd>.</li><li>Touch the glowing port before link integrity reaches zero.</li><li>Each patch adds time; consecutive patches build a combo.</li><li><kbd>Space</kbd> pauses. <kbd>R</kbd> resets. The board wraps at every edge.</li></ol><div id="agents" class="agents"></div><p class="small">Demo characters only. MIT licensed; remix welcome.</p></aside></div></main>
<script>
'use strict';
const N=8,C=70,L=560,colors=['#60ffdb','#ff72ce','#ffd96b','#8ba7ff','#ff806b','#8dff84'];
const cv=document.querySelector('#c'),x=cv.getContext('2d'),status=document.querySelector('#status'),scoreEl=document.querySelector('#score'),meter=document.querySelector('#meter'),agents=document.querySelector('#agents');
let g,timer,held={};
function rng(seed){let a=seed>>>0;return()=>{a=(a+0x6D2B79F5)>>>0;let z=a;z=Math.imul(z^z>>>15,z|1);z^=z+Math.imul(z^z>>>7,z|61);return((z^z>>>14)>>>0)/4294967296}}
function fresh(){clearInterval(timer);timer=null;const random=rng(0x1a2b3c);g={p:{x:3,y:3},port:{x:0,y:0,id:0},score:0,combo:0,time:100,playing:false,random};spawn();draw();status.textContent='Press Start or Space. Route the courier to glowing ports.'}
function spawn(){let p;do p={x:Math.floor(g.random()*N),y:Math.floor(g.random()*N),id:Math.floor(g.random()*6)};while(p.x===g.p.x&&p.y===g.p.y);g.port=p}
function move(dx,dy){if(!g.playing)return;g.p.x=(g.p.x+dx+N)%N;g.p.y=(g.p.y+dy+N)%N;if(g.p.x===g.port.x&&g.p.y===g.port.y){g.combo++;g.score+=10*g.combo;g.time=Math.min(100,g.time+28);spawn();status.textContent=`PATCHED! combo ×${g.combo} • next port ${g.port.id+1}`;draw()}}
function tick(){if(!g.playing)return;g.time-=1.1;if(g.time<=0){g.time=0;g.playing=false;clearInterval(timer);timer=null;status.textContent=`LINK LOST • final score ${g.score} • press R to reboot`;}draw()}
function toggle(){if(g.time<=0)return;g.playing=!g.playing;if(g.playing){timer??=setInterval(tick,100);status.textContent=`LIVE • patch port ${g.port.id+1}`;}else{clearInterval(timer);timer=null;status.textContent='PAUSED • press Space to resume'}draw()}
function draw(){x.clearRect(0,0,L,L);x.strokeStyle='#1d2942';for(let i=0;i<=N;i++){x.beginPath();x.moveTo(i*C,0);x.lineTo(i*C,L);x.moveTo(0,i*C);x.lineTo(L,i*C);x.stroke()}x.fillStyle=colors[g.port.id];x.shadowColor=x.fillStyle;x.shadowBlur=25;x.beginPath();x.arc(g.port.x*C+35,g.port.y*C+35,15+Math.sin(Date.now()/150)*3,0,7);x.fill();x.shadowBlur=0;x.fillStyle='#eaffff';x.beginPath();x.moveTo(g.p.x*C+35,g.p.y*C+17);x.lineTo(g.p.x*C+52,g.p.y*C+48);x.lineTo(g.p.x*C+18,g.p.y*C+48);x.fill();x.fillStyle='#090d1c';x.font='bold 13px monospace';x.textAlign='center';x.fillText('↯',g.p.x*C+35,g.p.y*C+40);scoreEl.textContent=g.score;meter.style.width=g.time+'%';agents.replaceChildren(...colors.map((c,i)=>{const e=document.createElement('div');e.className='agent '+(i===g.port.id?'on':'');e.style.color=c;e.textContent=`PORT ${i+1}`;return e}))}
const keys={ArrowUp:[0,-1],w:[0,-1],ArrowDown:[0,1],s:[0,1],ArrowLeft:[-1,0],a:[-1,0],ArrowRight:[1,0],d:[1,0]};
addEventListener('keydown',e=>{if(e.key===' '){e.preventDefault();if(!held[e.key])toggle()}else if(e.key.toLowerCase()==='r'){fresh()}else{const m=keys[e.key]||keys[e.key.toLowerCase()];if(m&&!held[e.key]){e.preventDefault();move(...m)}}held[e.key]=1});addEventListener('keyup',e=>delete held[e.key]);
document.querySelector('#start').onclick=toggle;document.querySelector('#reset').onclick=fresh;fresh();
</script>
```
### Test steps
1. Open the saved HTML in a current browser. The 8×8 board, six ports, instructions, score, and integrity meter should be visible without a network connection.
2. Activate **Start / Pause** or press **Space**. The status should become LIVE and integrity should decrease.
3. Use arrow keys or WASD to wrap around the board and touch the glowing port. Score and combo should increase, the target should move, and integrity should be restored.
4. Pause and resume with Space. Press **R** and activate **Reset**; score and integrity should return to their initial values.
5. With focus on each button, press Enter to verify keyboard activation.
Checks performed here: UTF-8 size **6573 bytes**; one inline script; zero remote URLs, `fetch`, XHR, or WebSocket references; JavaScript parsed successfully with Node.js v24.19.0. An interactive browser run was attempted, but this agent environment blocks local `file:` navigation, so I am not claiming a completed interactive browser run. Intended for current Chrome, Edge, and Firefox.
License: **MIT** (SPDX identifier is in the source). No automatic sound; the toy contains no audio code.