(function(){const o=document.createElement("link").relList;if(o&&o.supports&&o.supports("modulepreload"))return;for(const n of document.querySelectorAll('link[rel="modulepreload"]'))s(n);new MutationObserver(n=>{for(const r of n)if(r.type==="childList")for(const i of r.addedNodes)i.tagName==="LINK"&&i.rel==="modulepreload"&&s(i)}).observe(document,{childList:!0,subtree:!0});function t(n){const r={};return n.integrity&&(r.integrity=n.integrity),n.referrerPolicy&&(r.referrerPolicy=n.referrerPolicy),n.crossOrigin==="use-credentials"?r.credentials="include":n.crossOrigin==="anonymous"?r.credentials="omit":r.credentials="same-origin",r}function s(n){if(n.ep)return;n.ep=!0;const r=t(n);fetch(n.href,r)}})();const G="ws://localhost:9090";new TextEncoder;const K=new TextDecoder;function W(){const e=new Uint8Array(16);return crypto.getRandomValues(e),"12"+Array.from(e,o=>o.toString(16).padStart(2,"0")).join("")}function z(){return new Promise((e,o)=>{const t=W(),s=new WebSocket(G),n=new Set,r=[];s.binaryType="arraybuffer",s.onopen=()=>{e({peerId:{toString:()=>t},services:{pubsub:{subscribe(i){n.add(i)},unsubscribe(i){n.delete(i)},async publish(i,l){s.readyState===WebSocket.OPEN&&s.send(JSON.stringify({topic:i,data:Array.from(l)}))},addEventListener(i,l){r.push(l)},removeEventListener(i,l){const a=r.indexOf(l);a>=0&&r.splice(a,1)}}}})},s.onmessage=i=>{try{const l=typeof i.data=="string"?i.data:K.decode(new Uint8Array(i.data)),a=JSON.parse(l);if(a.topic&&n.has(a.topic)&&a.data!=null){const d=new Uint8Array(a.data),c=new CustomEvent("message",{detail:{topic:a.topic,data:d}});r.forEach(u=>u.call(null,c))}}catch{}},s.onerror=()=>o(new Error("Relay connection failed")),s.onclose=()=>{}})}const k="bombo-lobby",A="bombo-signal:",P=new TextEncoder,M=new TextDecoder;function x(e,o){const t=e.services.pubsub;if(!t||typeof t.publish!="function")return;const s={type:"host",gameId:o,peerId:e.peerId.toString(),ts:Date.now()};t.publish(k,P.encode(JSON.stringify(s))).catch(()=>{})}function j(e,o){const t=e.services.pubsub;if(!t||typeof t.subscribe!="function")return()=>{};const s=n=>{const r=n.detail;if(!(!r||r.topic!==k))try{const i=JSON.parse(M.decode(r.data));i.type==="host"&&i.gameId&&i.peerId&&o(i)}catch{}};return t.addEventListener("message",s),t.subscribe(k),()=>{t.removeEventListener("message",s),t.unsubscribe(k)}}function O(e,o,t,s,n=""){const r=e.services.pubsub;if(!r||typeof r.publish!="function")return;const i={type:s,gameId:o,fromPeerId:e.peerId.toString(),toPeerId:t,payload:n},l=`${A}${o}`;r.publish(l,P.encode(JSON.stringify(i))).catch(()=>{})}function B(e,o,t,s){const n=e.services.pubsub;if(!n||typeof n.subscribe!="function")return()=>{};const r=`${A}${o}`,i=e.peerId.toString(),l=a=>{const d=a.detail;if(!(!d||d.topic!==r))try{const c=JSON.parse(M.decode(d.data)),u=c.toPeerId===i,w=s?.forHost&&c.type==="join-request";(u||w)&&t(c)}catch{}};return n.addEventListener("message",l),n.subscribe(r),()=>{n.removeEventListener("message",l),n.unsubscribe(r)}}const q=new TextDecoder;function U(e,o,t,s){const n=new RTCPeerConnection({iceServers:[{urls:"stun:stun.l.google.com:19302"}]}),r=n.createDataChannel("game",{ordered:!0});J(r,s),n.onicecandidate=l=>{l.candidate&&O(t,e,o,"ice",JSON.stringify(l.candidate.toJSON()))},n.createOffer().then(l=>n.setLocalDescription(l)).then(()=>O(t,e,o,"offer",JSON.stringify(n.localDescription)));const i=B(t,e,l=>{l.fromPeerId===o&&(l.type==="answer"?n.setRemoteDescription(JSON.parse(l.payload)).catch(()=>{}):l.type==="ice"&&n.addIceCandidate(JSON.parse(l.payload)).catch(()=>{}))});return()=>{i(),r.close(),n.close()}}function Y(e,o,t,s){const n=new RTCPeerConnection({iceServers:[{urls:"stun:stun.l.google.com:19302"}]}),r=B(t,e,i=>{i.fromPeerId===o&&(i.type==="offer"?n.setRemoteDescription(JSON.parse(i.payload)).then(()=>n.createAnswer()).then(l=>n.setLocalDescription(l)).then(()=>O(t,e,o,"answer",JSON.stringify(n.localDescription))).catch(()=>{}):i.type==="ice"&&n.addIceCandidate(JSON.parse(i.payload)).catch(()=>{}))});return n.ondatachannel=i=>{i.channel.label==="game"&&J(i.channel,s)},n.onicecandidate=i=>{i.candidate&&O(t,e,o,"ice",JSON.stringify(i.candidate.toJSON()))},()=>{r(),n.close()}}function J(e,o){e.binaryType="arraybuffer",e.onopen=()=>o.onOpen(e),e.onclose=o.onClose,e.onmessage=t=>{try{const s=typeof t.data=="string"?t.data:q.decode(new Uint8Array(t.data)),n=JSON.parse(s);o.onMessage(n)}catch{}}}function C(e,o){!e||e.readyState!=="open"||e.send(JSON.stringify(o))}const b=15,y=11,f=40,F=10,X=30,V=10;function Z(e,o,t){return o<0||o>=b||t<0||t>=y?!1:e[t][o]==="empty"}function Q(e,o,t,s){o>=0&&o=0&&t=b-3&&n<=2||r<=2&&n>=y-3||r>=b-3&&n>=y-3?o[n][r]="empty":o[n][r]=Math.random()<.5?"crate":"empty"}const t=[[1,1],[b-2,1],[1,y-2],[b-2,y-2]],s=e.map((n,r)=>({id:n,x:t[r%t.length][0],y:t[r%t.length][1],alive:!0,bombsLeft:1,maxBombs:1,blastRadius:2}));return{tick:0,grid:o,players:s,bombs:[],explosions:[],gameOver:!1,winnerId:null}}function ne(e,o,t,s){const n=o.find(a=>a.id===t);if(!n||!n.alive)return;const r=n.x+ee[s],i=n.y+te[s];!Z(e,r,i)||o.some(a=>a.alive&&a.id!==t&&a.x===r&&a.y===i)||(n.x=r,n.y=i)}function oe(e,o){const t=e.players.find(n=>n.id===o);return!t||!t.alive||t.bombsLeft<=0||e.bombs.some(n=>n.x===t.x&&n.y===t.y)?!1:(e.bombs.push({id:`bomb-${e.tick}-${o}`,ownerId:o,x:t.x,y:t.y,ticksLeft:X,radius:t.blastRadius}),t.bombsLeft--,!0)}function I(e,o,t,s){return t<0||t>=b||s<0||s>=y||e[s][t]==="wall"?!1:(o.push({x:t,y:s,ticksLeft:V}),e[s][t]==="crate"?(Q(e,t,s,"empty"),!1):!0)}function re(e,o){const{grid:t,players:s,explosions:n}=e;I(t,n,o.x,o.y);for(let r=1;r<=o.radius&&I(t,n,o.x+r,o.y);r++);for(let r=1;r<=o.radius&&I(t,n,o.x-r,o.y);r++);for(let r=1;r<=o.radius&&I(t,n,o.x,o.y+r);r++);for(let r=1;r<=o.radius&&I(t,n,o.x,o.y-r);r++);for(const r of s){if(!r.alive)continue;n.some(l=>l.x===r.x&&l.y===r.y)&&(r.alive=!1)}}function se(e,o){const t=e.players.find(s=>s.id===o);t&&t.bombsLeft[...c]),s=e.players.map(c=>({...c})),n=e.bombs.map(c=>({...c})),r=e.explosions.map(c=>({...c}));for(const[c,u]of o)u.move&&ne(t,s,c,u.move),u.placeBomb&&oe({...e,players:s,bombs:n},c);const i=[];for(const c of n)c.ticksLeft--,c.ticksLeft<=0&&i.push(c);const l=n.filter(c=>c.ticksLeft>0),a={tick:e.tick+1,grid:t,players:s,bombs:l,explosions:r,gameOver:e.gameOver,winnerId:e.winnerId};for(const c of i)re(a,c),se(a,c.ownerId);for(const c of a.explosions)c.ticksLeft--;a.explosions=a.explosions.filter(c=>c.ticksLeft>0);for(const c of a.explosions)for(const u of a.players)u.alive&&u.x===c.x&&u.y===c.y&&(u.alive=!1);const d=a.players.filter(c=>c.alive);return d.length<=1&&(a.gameOver=!0,a.winnerId=d.length===1?d[0].id:null),a}const p={bg:"#0f0f12",grid:"#1a1a22",wall:"#2d2d3a",crate:"#8b7355",empty:"#252530",player1:"#22c55e",player2:"#3b82f6",player3:"#f59e0b",player4:"#ec4899",bomb:"#4b5563",explosion:"#ef4444",text:"#e2e8f0"},N=[p.player1,p.player2,p.player3,p.player4];function ae(e,o,t,s,n){const r=Math.min(s/(b*f),n/(y*f)),i=(s-b*f*r)/2,l=(n-y*f*r)/2;e.fillStyle=p.bg,e.fillRect(0,0,s,n),e.save(),e.translate(i,l),e.scale(r,r);for(let a=0;a{if(!a.alive)return;const c=N[d%N.length];e.fillStyle=c,e.strokeStyle=a.id===t?p.text:p.grid,e.lineWidth=a.id===t?2:1,e.beginPath(),e.arc(a.x*f+f/2,a.y*f+f/2,f/2-2,0,Math.PI*2),e.fill(),e.stroke()}),e.restore(),o.gameOver&&(e.fillStyle="rgba(0,0,0,0.7)",e.fillRect(0,0,s,n),e.fillStyle=p.text,e.font="bold 24px Outfit, sans-serif",e.textAlign="center",e.fillText(o.winnerId?o.winnerId===t?"You win!":"Game over":"Draw!",s/2,n/2))}const E={ArrowUp:"up",KeyW:"up",ArrowDown:"down",KeyS:"down",ArrowLeft:"left",KeyA:"left",ArrowRight:"right",KeyD:"right"};function _(e){let o=null,t=!1;const s=()=>e({move:o,placeBomb:t}),n=i=>{i.repeat||(E[i.code]!=null&&(i.preventDefault(),o=E[i.code],s()),i.code==="Space"&&(i.preventDefault(),t=!0,s()))},r=i=>{E[i.code]!=null&&(i.preventDefault(),o===E[i.code]&&(o=null),s()),i.code==="Space"&&(i.preventDefault(),t=!1,s())};return window.addEventListener("keydown",n),window.addEventListener("keyup",r),()=>{window.removeEventListener("keydown",n),window.removeEventListener("keyup",r)}}const D=document.getElementById("app"),L=()=>m?.peerId.toString()??"";let m=null,h=null,g=null,v=null,S=null,T=null;function le(){D.innerHTML=`

Bombo

P2P Bomberman over libp2p

Open games

    Connecting…

    `;const e=document.getElementById("btn-host"),o=document.getElementById("game-list"),t=document.getElementById("lobby-status"),s=new Map;e.addEventListener("click",()=>{e.disabled=!0;const r=`bombo-${Date.now()}-${Math.random().toString(36).slice(2,9)}`;ce(r)}),j(m,r=>{r.peerId!==L()&&(s.set(r.gameId,{peerId:r.peerId,ts:r.ts}),n())});function n(){const r=Date.now(),i=3e4;for(const[l,a]of s)r-a.ts>i&&s.delete(l);o.innerHTML=Array.from(s.entries()).map(([l,a])=>`
  • `).join(""),o.querySelectorAll("button").forEach(l=>{l.addEventListener("click",()=>{const a=l.dataset.gameId,d=l.dataset.peerId;fe(a,d)})}),t.textContent=m?"Connected. Host or pick a game.":"Connecting…"}n()}function ce(e){const o=[L()];let t=R(o);h=t;let s=null,n={move:null,placeBomb:!1};B(m,e,a=>{if(a.type==="join-request"&&a.toPeerId===L()){const d=a.fromPeerId;if(o.includes(d))return;o.push(d),s=d,t=R(o),h=t,U(e,d,m,{onOpen(c){g=c,C(c,{type:"start",playerIds:o}),C(c,{type:"state",snapshot:t}),v==null&&i()},onMessage(c){c.type==="input"&&(n=c.input)},onClose(){g=null,l()}})}},{forHost:!0});const r=setInterval(()=>x(m,e),2e3);window.__stopLobby=()=>clearInterval(r),x(m,e),H(),i();function i(){if(v!=null)return;const a=new Map;S=_(d=>{a.set(L(),d)}),v=window.setInterval(()=>{s&&a.set(s,n),t=ie(t,a),h=t,n={move:null,placeBomb:!1},a.clear(),g?.readyState==="open"&&C(g,{type:"state",snapshot:t}),t.gameOver&&l()},1e3/F)}function l(){v!=null&&(clearInterval(v),v=null),S?.(),S=null}}function fe(e,o){window.__stopLobby?.(),O(m,e,o,"join-request",""),T=Y(e,o,m,{onOpen(t){g=t,S=_(s=>{g?.readyState==="open"&&C(g,{type:"input",tick:h?.tick??0,input:s})})},onMessage(t){t.type==="state"?h=t.snapshot:t.type==="start"&&(h=R(t.playerIds))},onClose(){g=null,S?.(),S=null,T?.()}}),H()}function H(){D.innerHTML=`

    WASD / Arrows move, Space to place bomb

    `;const e=document.getElementById("game-canvas"),o=e.getContext("2d");function t(){e.width=window.innerWidth,e.height=window.innerHeight}t(),window.addEventListener("resize",t);function s(){h&&ae(o,h,L(),e.width,e.height),requestAnimationFrame(s)}s()}async function de(){D.innerHTML=`

    Bombo

    Connecting to network…

    `;try{m=await z(),le()}catch(e){D.innerHTML=`

    Bombo

    Failed to connect. Is the relay server running?

    Run: npm run relay

    `,console.error(e)}}const $=document.createElement("style");$.textContent=` * { box-sizing: border-box; } body { margin: 0; background: #0f0f12; color: #e2e8f0; font-family: 'Outfit', sans-serif; } #app { min-height: 100vh; display: flex; align-items: center; justify-content: center; } .screen { padding: 2rem; text-align: center; max-width: 480px; } .screen.game { padding: 0; max-width: none; } .screen.game canvas { display: block; width: 100%; height: 100vh; } h1 { font-family: 'JetBrains Mono', monospace; font-size: 2rem; margin: 0 0 0.5rem; } .tagline { color: #64748b; margin-bottom: 2rem; } .lobby-actions { display: flex; flex-direction: column; gap: 1.5rem; } button { font: 600 1rem Outfit; padding: 0.75rem 1.5rem; border: none; border-radius: 8px; background: #22c55e; color: #0f0f12; cursor: pointer; } button:hover:not(:disabled) { background: #16a34a; } button:disabled { opacity: 0.6; cursor: not-allowed; } .label { font-size: 0.875rem; color: #94a3b8; margin-bottom: 0.5rem; } #game-list { list-style: none; padding: 0; margin: 0 0 0.5rem; } #game-list button { background: #3b82f6; width: 100%; } #game-list button:hover { background: #2563eb; } .status { font-size: 0.875rem; color: #64748b; } .hint { font-size: 0.75rem; color: #64748b; position: absolute; bottom: 1rem; left: 50%; transform: translateX(-50%); } .error .hint { position: static; transform: none; margin-top: 1rem; } code { background: #1a1a22; padding: 0.2em 0.4em; border-radius: 4px; font-size: 0.9em; } `;document.head.appendChild($);de(); //# sourceMappingURL=index-CiLgYCFU.js.map