summaryrefslogtreecommitdiffstats
path: root/web/play
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-11-08 12:28:48 +0100
committersalmonik <salmonik@tuta.io>2026-11-08 12:28:48 +0100
commit8681fcf565643f67a36cb1fe571fac7843d2b6af (patch)
tree1f022edb267281da0a2fd0fde3b5336293f14cea /web/play
parentweb: match the desktop layout and remove play-money copy (diff)
downloadzagan-bet-8681fcf565643f67a36cb1fe571fac7843d2b6af.tar.gz
zagan-bet-8681fcf565643f67a36cb1fe571fac7843d2b6af.zip
web: disallow crawlers and quiet the game port
Adds robots.txt with Disallow: / plus noindex/X-Robots-Tag on the site and app. The v5 server treats an HTTP request as a malformed frame, closes silently before HELLO, and suppresses the common TLS scanner noise. Deploy now verifies robots.txt.
Diffstat (limited to '')
-rw-r--r--web/play/app.js2
-rw-r--r--web/play/index.html1
-rw-r--r--web/play/src/slots.ts1
-rw-r--r--web/play/style.css27
4 files changed, 22 insertions, 9 deletions
diff --git a/web/play/app.js b/web/play/app.js
index e76f5a6..78e9930 100644
--- a/web/play/app.js
+++ b/web/play/app.js
@@ -1 +1 @@
"use strict";(()=>{function x(t){let e=document.getElementById(t);if(!e)throw new Error(`missing #${t}`);return e}function o(t,e,n){let r=document.createElement(t);return e&&(r.className=e),n!==void 0&&(r.textContent=n),r}function v(t){t.replaceChildren()}function Jt(t){let e=t<0,n=String(Math.abs(Math.trunc(t))),r="";for(let s=0;s<n.length;s+=1)s>0&&(n.length-s)%3===0&&(r+=" "),r+=n[s];return e?`-${r}`:r}function T(t){return t==null||Number.isNaN(t)?"— Kr":`${Jt(t)} Kr`}function st(t,e="info"){let n=document.getElementById("toasts");if(!n||!t)return;let r=o("div",`toast ${e}`,t);for(n.append(r),window.setTimeout(()=>{r.style.opacity="0",r.style.transition="opacity 220ms ease",window.setTimeout(()=>r.remove(),240)},4200);n.children.length>4;)n.firstElementChild?.remove()}function yt(t){let e=t.toLowerCase();return t.startsWith("ERROR")||e.includes("failed")||e.includes("lost")||e.includes("invalid")||e.includes("forfeit")?"bad":t.startsWith("Logged in")||t.startsWith("Account created")||e.includes("won")||e.includes("cashed")?"good":"info"}async function xt(t){let e=new TextEncoder().encode(t),n=await crypto.subtle.digest("SHA-256",e);return[...new Uint8Array(n)].map(r=>r.toString(16).padStart(2,"0")).join("")}function j(t,e=10){return t.length<=e*2?t:`${t.slice(0,e)}…${t.slice(-e)}`}function kt(t){if(t==="?"||!t)return{rank:"?",suit:"",red:!1};let e=t.slice(-1);return{rank:t.slice(0,-1),suit:e==="C"?"♣":e==="D"?"♦":e==="H"?"♥":e==="S"?"♠":e,red:e==="D"||e==="H"}}var G=class{constructor(e,n){this.url=e;this.handlers=n;this.socket=null;this.stopped=!1;this.replaced=!1;this.attempts=0}connect(){if(this.stopped)return;this.clearTimer();let e;try{e=new WebSocket(this.url)}catch{this.scheduleReconnect()||this.handlers.onClosed(!1);return}this.socket=e,e.addEventListener("open",()=>{this.attempts=0,this.handlers.onOpen()}),e.addEventListener("message",n=>{let r;try{r=JSON.parse(String(n.data))}catch{return}switch(r.type){case"hello":this.handlers.onHello(r);break;case"view":this.handlers.onView(r.view);break;case"notice":this.handlers.onNotice(String(r.text??""));break;case"error":this.handlers.onError(String(r.text??""));break;case"replaced":this.replaced=!0,this.handlers.onReplaced();break;case"pong":this.handlers.onPong();break}}),e.addEventListener("close",()=>{if(this.socket===e&&(this.socket=null),this.replaced||this.stopped){this.handlers.onClosed(!1);return}let r=this.scheduleReconnect();this.handlers.onClosed(r)}),e.addEventListener("error",()=>{})}send(e){this.socket?.readyState===WebSocket.OPEN&&this.socket.send(JSON.stringify({type:"command",command:e}))}ping(){this.socket?.readyState===WebSocket.OPEN&&this.socket.send(JSON.stringify({type:"ping"}))}close(){this.stopped=!0,this.clearTimer(),this.socket?.close()}scheduleReconnect(){if(this.stopped||this.replaced||this.attempts>=6)return!1;this.attempts+=1;let e=Math.min(250*2**(this.attempts-1),4e3);return this.timer=window.setTimeout(()=>this.connect(),e),!0}clearTimer(){this.timer!==void 0&&(window.clearTimeout(this.timer),this.timer=void 0)}};var Tt=0;function rt(){return Date.now()-Tt<400?!1:(Tt=Date.now(),!0)}function Qt(t){if(t==="?"){let r=o("div","card back");return r.append(o("span","rank","?"),o("span","suit","")),r}let e=kt(t),n=o("div","card");return e.red&&n.classList.add("red"),n.append(o("span","rank",e.rank),o("span","suit",e.suit)),n}function Lt(t,e,n){let r=o("div","hand"),s=o("div","hand-head");s.append(o("span","label",t),o("strong","total",String(n)));let a=o("div","cards");for(let l of e)a.append(Qt(l));return r.append(s,a),r}function te(t,e){let n=String(e?.result??""),r=Number(e?.winnings??0);switch(n){case"blackjack":return`Blackjack — won ${T(r)}`;case"win":return`You win — ${T(r)}`;case"push":return`Push — ${T(r)} returned`;case"bust":return"Bust — the bet is lost";case"lose":return"Dealer wins — the bet is lost";case"forfeit":return"Hand forfeited — the bet is lost";case"playing":return t.status==="player"?"Your move.":"Dealing…";default:return"Ready to deal."}}function wt(t,e,n,r){v(t),v(e);let s=n.hand,a=s?.phase==="player"||n.status==="player",l=s?.phase==="done"||n.status==="done",i=!s||l,d=o("div","bj-table");if(s)d.append(Lt("DEALER",s.dealer??[],Number(s.dealer_total??0))),d.append(o("div","bj-divider")),d.append(Lt("YOU",s.player??[],Number(s.player_total??0)));else{let b=o("div","empty-table","Place a bet and deal a hand.");d.append(b)}t.append(d);let m=o("div","board-foot");m.append(o("span","label","Hand"),o("span",void 0,s?te(n,s):"Waiting")),t.append(m),n.error&&t.append(o("p","error-text",String(n.error))),e.append(o("h2",void 0,"Blackjack"));let f=Number(s?.bet??n.bet??0),L=n.balance??n.hand?.balance;if(e.append(o("p","note","Get closer to 21 without going over.")),i){let b=o("div","row"),y=o("button","btn small","−10"),S=o("strong","mono",T(f)),c=o("button","btn small","+10");y.addEventListener("click",()=>r({AdjustBet:-10})),c.addEventListener("click",()=>r({AdjustBet:10})),b.append(y,S,c),e.append(o("div","label","Bet"),b)}else{let b=o("div","stat");b.append(o("span","muted","Bet"),o("strong",void 0,T(f))),e.append(b)}if(L!==void 0){let b=o("div","stat");b.append(o("span","muted","Balance"),o("strong",void 0,T(Number(L)))),e.append(b)}if(l||s&&!a){let b=o("p","note win-text");b.textContent=`${String(s?.result??"").toUpperCase()} ${T(Number(s?.winnings??0))}`,["win","blackjack","push"].includes(String(s?.result))||(b.className="note"),e.append(b)}else if(a){let b=(s?.actions??[]).includes("double")?"Hit, stand or double.":"Hit or stand.";e.append(o("p","note",b))}else s||e.append(o("p","note","Press DEAL to start a hand."));let w=o("div","actions");if(a){let b=o("div","row wrap");for(let S of s?.actions??[]){let c=S==="hit"?"HIT":S==="stand"?"STAND":"DOUBLE",p=S==="stand"?o("button","btn primary",c):o("button","btn",c);p.addEventListener("click",()=>{rt()&&r(S==="hit"?"Hit":S==="stand"?"Stand":"Double")}),b.append(p)}w.append(b);let y=o("button","btn danger wide","Forfeit hand");y.addEventListener("click",()=>{rt()&&window.confirm("Forfeit this hand? The reserved bet is lost.")&&r("Forfeit")}),w.append(y)}else if(i){let b=o("button","btn primary wide big","DEAL");b.addEventListener("click",()=>{rt()&&r("Deal")}),w.append(b)}let k=o("button","btn ghost wide",a?"Leave (forfeits)":"Leave table");k.addEventListener("click",()=>{a&&!window.confirm("Leaving forfeits this hand. Continue?")||r("Leave")}),w.append(k),e.append(w)}var at=Math.LN2/5,B=null,z=null,Mt=null,D=null,$,Et=0,X="",St=0;function ee(t,e,n,r){v(t),v(e),D=n,Mt=r;let s=o("div","crash-wrap"),a=o("div","crash-number","1.00×"),l=o("div","label","READY"),i=o("div","crash-meta"),d=o("canvas","crash-canvas"),m=o("div","verify","Waiting for a round commitment.");s.append(a,l,i,d,m),t.append(s),e.append(o("h2",void 0,"Crash")),e.append(o("p","note","Cash out before the multiplier crashes."));let f=o("div","row"),L=o("button","btn small","−"),w=o("strong","mono","—"),k=o("button","btn small","+");L.addEventListener("click",()=>D?.({AdjustBet:-10})),k.addEventListener("click",()=>D?.({AdjustBet:10})),f.append(L,w,k),e.append(o("div","label","Bet"),f);let b=o("strong","mono","—"),y=o("strong","mono","—");for(let[R,E]of[["Potential",b],["Balance",y]]){let M=o("div","stat");M.append(o("span","muted",R),E),e.append(M)}let S=o("p","note","Press START to play."),c=o("p","note",""),p=o("p","error-text");e.append(S,c,p);let h=o("div","actions"),u=o("button","btn primary wide big","START"),g=o("button","btn ghost wide","Leave table");return u.addEventListener("click",()=>{Date.now()-St<500||(St=Date.now(),z?.status==="running"?D?.("CashOut"):D?.("Start"))}),g.addEventListener("click",()=>{z?.status==="running"&&!window.confirm("Forfeit this round? The bet is lost.")||D?.("Leave")}),h.append(u,g),e.append(h),{board:t,side:e,number:a,meta:i,verify:m,canvas:d,bet:w,potential:b,balance:y,status:S,lastCrash:c,error:p,primary:u,leave:g,minus:L,plus:k}}function Nt(){$!==void 0&&(cancelAnimationFrame($),$=void 0),z=null}function vt(t,e,n,r,s){(!B||B.board!==t||!B.canvas.isConnected)&&(B=ee(t,e,r,s),X="");let a=B;D=r,Mt=s,z=n,n.status!==X&&((n.status==="starting"||n.status==="running")&&X!=="running"&&(a.verify.className="verify",a.verify.textContent="Round committed; seed hidden until the round ends."),X=String(n.status));let l=n.status==="running",i=n.status==="starting",d=l||i;a.bet.textContent=T(Number(n.bet??0)),a.potential.textContent=T(Number(n.potential??n.bet??0)),a.balance.textContent=n.balance===null||n.balance===void 0?"—":T(Number(n.balance)),a.error.textContent=n.error?String(n.error):"",a.minus.disabled=d,a.plus.disabled=d,a.primary.textContent=l?"CASH OUT":i?"STARTING…":"START",a.primary.disabled=i,a.primary.className="btn primary wide big",a.leave.textContent=l?"Forfeit round":"Leave table",l?(a.status.textContent="Rising — cash out before the crash.",a.status.className="note win-text"):i?(a.status.textContent="Starting the round…",a.status.className="note"):n.status==="cashed"?(a.status.textContent=`Cashed out at ${Number(n.multiplier??0).toFixed(2)}× for ${T(Number(n.payout??0))}.`,a.status.className="note win-text"):n.status==="crashed"?(a.status.textContent=`Crashed at ${Number(n.crash_point??n.multiplier??0).toFixed(2)}×. The bet is lost.`,a.status.className="note error-text"):n.status==="forfeit"?(a.status.textContent="Round forfeited.",a.status.className="note error-text"):(a.status.textContent="Press START to play.",a.status.className="note"),n.crash_point!==null&&n.crash_point!==void 0&&n.status==="crashed"&&(a.lastCrash.textContent=`Last crash: ${Number(n.crash_point).toFixed(2)}×`);let m=n.commit?String(n.commit):"";if(a.meta.textContent=m?`commit ${j(m,12)}`:"",a.meta.style.display=m?"":"none",a.verify.style.display=m?"":"none",n.seed&&m){let f=++Et,L=String(n.seed),w=Number(n.crash_point??0);se(L,m,w).then(([k,b])=>{if(f!==Et||!B)return;let y=k&&b;B.verify.className=`verify ${y?"ok":"bad"}`,B.verify.textContent=y?`Seed ${j(L,12)} · sha256 matches commit · crash point recomputed`:`VERIFICATION FAILED · seed ${j(L,12)} · commit ${j(m,12)}`})}else!d&&!m&&(a.verify.className="verify",a.verify.textContent="Waiting for a round commitment.");if($!==void 0&&(cancelAnimationFrame($),$=void 0),l){let f=()=>{if(!B||z?.status!=="running"){$=void 0;return}Ct(B,z,s),$=requestAnimationFrame(f)};$=requestAnimationFrame(f)}else Ct(a,n,s)}function ne(t,e){if(t.status==="running"){let n=Math.max(0,(e.now()-Number(t.started_at??0))/1e3),r=Math.min(1e3,Math.exp(at*n));return Math.max(r,Number(t.multiplier??1))}return t.status==="cashed"?Number(t.multiplier??1):t.status==="crashed"?Number(t.crash_point??t.multiplier??1):1}function Ct(t,e,n){let r=ne(e,n);t.number.textContent=`${r.toFixed(2)}×`;let s=t.number.nextElementSibling;s&&(s.textContent=e.status==="running"?"RUNNING":e.status==="cashed"?"CASHED OUT":e.status==="crashed"?"CRASHED":e.status==="starting"?"STARTING":"READY"),t.number.className=`crash-number ${e.status==="running"?"running":e.status==="cashed"?"cashed":e.status==="crashed"?"crashed":""}`,t.potential.textContent=T(Math.floor(Number(e.bet??0)*r)),oe(t.canvas,e,r,n)}function oe(t,e,n,r){let s=t.clientWidth||600,a=t.clientHeight||360,l=window.devicePixelRatio||1;(t.width!==Math.round(s*l)||t.height!==Math.round(a*l))&&(t.width=Math.round(s*l),t.height=Math.round(a*l));let i=t.getContext("2d");if(!i)return;i.setTransform(l,0,0,l,0,0),i.clearRect(0,0,s,a);let d=54,m=16,f=18,L=34,w=Math.max(1,s-d-m),k=Math.max(1,a-f-L),b=e.status==="running"?Math.max(.25,(r.now()-Number(e.started_at??0))/1e3):n>1?Math.log(n)/at:1,y=Math.max(5,b*1.08),S=Math.max(2,Math.ceil(n*1.15*2)/2),c=E=>d+E/y*w,p=E=>f+k-(E-1)/(S-1)*k;i.strokeStyle="#242e38",i.fillStyle="#8a98a6",i.font="11px ui-monospace, monospace",i.textAlign="left",i.textBaseline="middle",i.lineWidth=1;for(let E=0;E<=4;E+=1){let M=1+(S-1)*E/4,C=p(M);i.beginPath(),i.moveTo(d,C),i.lineTo(s-m,C),i.stroke(),i.fillText(`${M.toFixed(1)}×`,10,C)}for(let E=0;E<=4;E+=1){let M=y*E/4,C=c(M);i.beginPath(),i.moveTo(C,f),i.lineTo(C,f+k),i.strokeStyle="#1b232c",i.stroke(),i.fillStyle="#5b6773",i.textAlign="center",i.textBaseline="alphabetic",i.fillText(`${M.toFixed(0)}s`,C,a-12),i.textAlign="left",i.textBaseline="middle"}if(!["running","cashed","crashed"].includes(String(e.status))){i.fillStyle="#5b6773",i.textAlign="center",i.textBaseline="middle",i.font='13px "Open Sans", sans-serif',i.fillText("Press START to play",d+w/2,f+k/2);return}let h=e.status==="crashed"?"#e05a5a":e.status==="cashed"?"#4ec27b":"#e0b64a";i.beginPath();let u=180;for(let E=0;E<=u;E+=1){let M=b*E/u,C=Math.min(S,Math.exp(at*M)),H=c(M),A=p(C);E===0?i.moveTo(H,A):i.lineTo(H,A)}i.strokeStyle=h,i.lineWidth=2.4,i.stroke();let g=c(b),R=p(Math.min(S,n));i.beginPath(),i.arc(g,R,4.5,0,Math.PI*2),i.fillStyle=h,i.fill(),i.fillStyle=h,i.font="600 12px ui-monospace, monospace",i.fillText(`${n.toFixed(2)}×`,Math.min(g+8,s-70),Math.max(f+12,R-10))}async function se(t,e,n){let r=new Uint8Array(t.length/2);for(let f=0;f<r.length;f+=1)r[f]=Number.parseInt(t.slice(f*2,f*2+2),16);let s=await crypto.subtle.digest("SHA-256",r),l=[...new Uint8Array(s)].map(f=>f.toString(16).padStart(2,"0")).join("")===e.toLowerCase(),i=0;for(let f=0;f<8;f+=1)i=i*256+r[f];i/=2**64;let d=Math.max(1,Math.min(1e3,(1-.01)/(1-i))),m=Math.abs(d-n)<1e-6;return[l,m]}function re(t){let e=t.details??{};switch(t.game){case"slots":return String(e.win_type??"spin");case"roulette":return`${e.number??"?"} ${e.color??""}`.trim();case"blackjack":return String(e.result??"");case"crash":{let n=String(e.result??""),r=e.crash_point;return r==null?n:`${n} @ ${Number(r).toFixed(2)}×`}default:return""}}function Ht(t,e){v(t);let n=e.records??[];if(n.length===0){let d=o("div","board");d.append(o("h2",void 0,"No plays yet"),o("p","muted","Your settled plays will appear here.")),t.append(d);return}let r=o("div","table-wrap"),s=o("table"),a=o("thead"),l=o("tr");for(let d of["When","Game","Bet","Won","Balance","Result"])l.append(o("th",void 0,d));a.append(l);let i=o("tbody");for(let d of n){let m=o("tr");m.append(o("td","mono",String(d.at??"").slice(0,16))),m.append(o("td","mono",String(d.game??"").toUpperCase())),m.append(o("td","num",T(Number(d.bet??0))));let f=Number(d.winnings??0);m.append(o("td",`num ${f>0?"pos":""}`,T(f))),m.append(o("td","num",T(Number(d.balance_after??0)))),m.append(o("td","muted",re(d))),i.append(m)}s.append(a,i),r.append(s),t.append(r)}var it=[0,32,15,19,4,21,2,25,17,34,6,27,13,36,11,30,8,23,10,5,24,16,33,1,20,14,31,9,22,18,29,7,28,12,35,3,26],ae=new Set([1,3,5,7,9,12,14,16,18,19,21,23,25,27,30,32,34,36]),Wt="#b0313a",It="#151b21",Ot="#106b45",At="#0d5c39",ct="#1f7d52",dt="#ecf7f0",ie="#e0b64a",le="#11171d",lt=null;function ut(t){return t===0?"green":ae.has(t)?"red":"black"}function ce(t,e){let r=Math.max(44,t*.058),s=Math.max(28,Math.min(38,e*.075)),a=t-28-r,l=a/12,i=(e-28-s*3)/3,d=14+r,m=14,f=m+i*3,L=f+s,w=L+s,k=[];for(let c=0;c<12;c+=1)for(let p=0;p<3;p+=1){let h=c*3+(3-p);k.push({x:d+c*l,y:m+p*i,w:l,h:i,target:{bet:"straight",number:h},label:String(h),kind:"number",number:h})}k.push({x:14,y:m,w:r,h:i*3,target:{bet:"straight",number:0},label:"0",kind:"zero",number:0});for(let c=0;c<3;c+=1)k.push({x:d+a/3*c,y:f,w:a/3,h:s,target:{bet:"dozen",index:c},label:["1st 12","2nd 12","3rd 12"][c],kind:"dozen"}),k.push({x:d+a/3*c,y:L,w:a/3,h:s,target:{bet:"column",index:2-c},label:"2:1",kind:"column"});[{kind:"low",label:"1–18"},{kind:"even",label:"EVEN"},{kind:"red",label:"RED"},{kind:"black",label:"BLACK"},{kind:"odd",label:"ODD"},{kind:"high",label:"19–36"}].forEach((c,p)=>{k.push({x:d+a/6*p,y:w,w:a/6,h:s,target:{bet:"outside",kind:c.kind},label:c.label,kind:"outside"})});let y=[],S=c=>{let p=k.find(h=>h.kind==="number"&&h.number===c);return p?{x:p.x+p.w/2,y:p.y+p.h/2}:null};for(let c=0;c<11;c+=1)for(let p=0;p<2;p+=1){let h=c*3+(3-p);y.push({x:d+(c+1)*l,y:m+(p+1)*i,radius:12,rank:0,target:{bet:"corner",numbers:[h,h-1,h+3,h+2]}})}for(let c=0;c<12;c+=1)for(let p=0;p<2;p+=1){let h=c*3+(3-p),u=S(h);u&&y.push({x:u.x,y:m+(p+1)*i,radius:10,rank:1,target:{bet:"split",a:h,b:h-1}})}for(let c=0;c<11;c+=1)for(let p=0;p<3;p+=1){let h=c*3+(3-p);y.push({x:d+(c+1)*l,y:m+p*i+i/2,radius:10,rank:1,target:{bet:"split",a:h,b:h+3}})}for(let c=0;c<3;c+=1)y.push({x:d,y:m+c*i+i/2,radius:10,rank:1,target:{bet:"split",a:0,b:3-c}});return{width:t,height:e,pad:14,cellW:l,cellH:i,zeroX:14,zeroW:r,gridX:d,gridY:m,gridW:a,dozensY:f,columnsY:L,outsideY:w,outsideH:s,zones:k,hotspots:y}}function Rt(t,e,n,r,s){t.beginPath(),t.rect(e,n,r,s),t.closePath()}function Bt(t,e){let n=t.clientWidth||900,r=t.clientHeight||500,s=window.devicePixelRatio||1;(t.width!==Math.round(n*s)||t.height!==Math.round(r*s))&&(t.width=Math.round(n*s),t.height=Math.round(r*s));let a=t.getContext("2d"),l=ce(n,r);if(!a)return l;a.setTransform(s,0,0,s,0,0),a.clearRect(0,0,n,r),a.fillStyle=At,Rt(a,l.pad,l.gridY,l.zeroW+l.gridW,l.cellH*3+l.outsideH*3),a.fill();for(let i of l.zones){let d=At;i.kind==="number"?d=ut(i.number)==="red"?Wt:It:i.kind==="zero"&&(d=Ot),a.fillStyle=d,a.strokeStyle=ct,a.lineWidth=1,Rt(a,i.x+.5,i.y+.5,i.w-1,i.h-1),a.fill(),a.stroke(),a.fillStyle=dt,a.textAlign="center",a.textBaseline="middle",i.kind==="number"||i.kind==="zero"?a.font=`500 ${Math.min(18,l.cellH*.16)}px "Open Sans", sans-serif`:a.font=`500 ${Math.min(12,l.outsideH*.4)}px "Open Sans", sans-serif`,i.label==="RED"&&(a.fillStyle="#e05a5a"),i.label==="BLACK"&&(a.fillStyle="#8a98a6"),a.fillText(i.label,i.x+i.w/2,i.y+i.h/2)}return ue(a,l,e),l}function Pt(t){return JSON.stringify(t)}function de(t,e){let n=Pt(e),r=t.zones.find(s=>Pt(s.target)===n);if(r)return{x:r.x+r.w/2,y:r.y+r.h/2};if(e?.bet==="split"||e?.bet==="corner"){let s=e.bet==="split"?[e.a,e.b]:e.numbers,a=t.zones.filter(l=>l.kind==="number"&&s.includes(l.number));if(a.length>0)return{x:a.reduce((l,i)=>l+i.x+i.w/2,0)/a.length,y:a.reduce((l,i)=>l+i.y+i.h/2,0)/a.length}}return null}function ue(t,e,n){let r=n.bets?.chips??[];for(let s of r){let a=de(e,s.target);if(!a)continue;let l=Math.max(12,Math.min(17,e.cellW*.28));t.beginPath(),t.arc(a.x,a.y,l,0,Math.PI*2),t.fillStyle=ie,t.fill(),t.strokeStyle="#7a5a12",t.lineWidth=2,t.stroke(),t.fillStyle="#221a05",t.textAlign="center",t.textBaseline="middle",t.font=`700 ${l>15?11:10}px "Hack", monospace`,t.fillText(String(s.amount),a.x,a.y+.5)}}function pe(t,e){let r=window.devicePixelRatio||1;t.width=Math.round(170*r),t.height=Math.round(170*r),t.style.width="170px",t.style.height="170px";let s=t.getContext("2d");if(!s)return;s.setTransform(r,0,0,r,0,0);let a=e.phase==="result"&&e.number!==null&&e.number!==void 0?Number(e.number):null,l=a===null?-1:it.indexOf(a),i=Math.PI*2/it.length,d=l<0?0:-l*i,m=performance.now(),f=650,L=k=>{s.clearRect(0,0,170,170),s.fillStyle=le,s.beginPath(),s.arc(85,85,78.2+6,0,Math.PI*2),s.fill(),it.forEach((p,h)=>{let u=-Math.PI/2+h*i+k;s.beginPath(),s.moveTo(85,85),s.arc(85,85,78.2,u-i/2,u+i/2),s.closePath(),s.fillStyle=ut(p)==="red"?Wt:ut(p)==="green"?Ot:It,s.fill(),s.strokeStyle=ct,s.lineWidth=.8,s.stroke()}),s.beginPath(),s.arc(85,85,49.3,0,Math.PI*2),s.fillStyle="#0a0e12",s.fill(),s.strokeStyle=ct,s.lineWidth=1,s.stroke(),s.beginPath(),s.moveTo(85,8),s.lineTo(80,18),s.lineTo(90,18),s.closePath(),s.fillStyle=dt,s.fill(),a!==null&&(s.fillStyle=dt,s.textAlign="center",s.textBaseline="middle",s.font='600 15px "Hack", monospace',s.fillText(String(a),85,85))},w=k=>{let b=Math.min(1,(k-m)/f),y=1-(1-b)**3;L(d*y),b<1&&requestAnimationFrame(w)};requestAnimationFrame(w)}function Dt(t,e,n,r){v(t),v(e);let s=n.phase==="spinning",a=o("canvas","roulette-canvas");t.append(a);let l=o("div","board-foot tight");if(l.append(o("span","label","Last")),n.phase==="result"&&n.number!==null&&n.number!==void 0){let u=o("span",n.winnings&&Number(n.winnings)>0?"win-text":void 0,`${n.number} ${n.color} · ${Number(n.winnings??0)>0?T(Number(n.winnings)):"no win"}`);l.append(u)}else l.append(o("span",void 0,"No spins yet"));l.append(o("span","note","Click to place a chip · right-click to take one back")),t.append(l);let d=Bt(a,n),m=(u,g)=>{let R=a.getBoundingClientRect(),E=u-R.left,M=g-R.top,C=null;for(let H of d.hotspots){let A=Math.hypot(E-H.x,M-H.y);A>H.radius||(!C||H.rank<C.rank||H.rank===C.rank&&A<Math.hypot(E-C.x,M-C.y))&&(C=H)}if(C)return{x:C.x-C.radius,y:C.y-C.radius,w:C.radius*2,h:C.radius*2,target:C.target,label:"",kind:"number"};for(let H=d.zones.length-1;H>=0;H-=1){let A=d.zones[H];if(E>=A.x&&E<=A.x+A.w&&M>=A.y&&M<=A.y+A.h)return A}return null};a.addEventListener("click",u=>{if(s)return;let g=m(u.clientX,u.clientY);g&&r({PlaceBetAt:g.target})}),a.addEventListener("contextmenu",u=>{if(u.preventDefault(),s)return;let g=m(u.clientX,u.clientY);g&&r({RemoveBetAt:g.target})}),a.addEventListener("mousemove",u=>{let g=m(u.clientX,u.clientY);a.style.cursor=g&&!s?"pointer":"default"}),lt?.disconnect(),lt=new ResizeObserver(()=>{d=Bt(a,n)}),lt.observe(a),e.append(o("h2",void 0,"Roulette")),e.append(o("p","note","Pick numbers or colours, then spin."));let f=o("canvas","wheel-canvas");e.append(f),pe(f,n),e.append(o("div","label","Place your bets"));let L=(n.bets?.chips??[]).reduce((u,g)=>u+Number(g.amount??0),0);e.append(o("p","note",`${L} ${L===1?"chip":"chips"}`)),e.append(o("div","label","Chip"));let w=o("div","chip-row");for(let u of[10,25,50,100]){let g=o("button",`chip-btn ${Number(n.chip)===u?"active":""}`,String(u));g.dataset.chip=String(u),g.disabled=s,g.addEventListener("click",()=>r({SetChip:u})),w.append(g)}e.append(w),e.append(o("div","label","Bets"));let k=n.bets?.chips??[];if(k.length===0)e.append(o("p","note","No chips on the felt"));else for(let u of k){let g=o("div","stat");g.append(o("span","muted",me(u.target)),o("strong","mono",T(Number(u.amount??0)))),e.append(g)}let b=o("div","stat");b.append(o("span","muted","Total"),o("strong","mono",T(L))),e.append(b),n.error&&e.append(o("p","error-text",String(n.error)));let y=o("p","note");if(s)y.textContent="SPINNING…";else if(n.phase==="result"){let u=Number(n.winnings??0);y.textContent=u>0?`WON ${T(u)}`:"NO WIN",u>0&&y.classList.add("win-text")}else y.textContent=L>0?"Ready to spin.":"Place at least one chip.";e.append(y);let S=o("div","actions"),c=o("button","btn primary wide big",s?"SPINNING…":"SPIN");c.disabled=s||L===0,c.addEventListener("click",()=>{Date.now()-$t<600||($t=Date.now(),r("Spin"))});let p=o("button","btn wide","CLEAR");p.disabled=s||L===0,p.addEventListener("click",()=>r("ClearBets"));let h=o("button","btn ghost wide","Leave table");h.disabled=s,h.addEventListener("click",()=>r("Leave")),S.append(c,p,h),e.append(S)}var $t=0;function me(t){switch(t?.bet){case"straight":return String(t.number);case"split":return`${t.a}/${t.b}`;case"corner":return t.numbers.join("/");case"dozen":return["1st 12","2nd 12","3rd 12"][t.index]??"dozen";case"column":return`col ${Number(t.index)+1}`;case"outside":return String(t.kind??"");default:return""}}var pt=5,mt=3,F="CLOGDS",he=["top","middle","bottom","V","inverted V"],U=0,zt=0,W,K=null,Z=null;function fe(){U=Date.now()+900}function Ft(){W!==void 0&&(window.clearInterval(W),W=void 0),K=null,Z=null,U=0}function be(t){return t.phase==="spinning"||Date.now()<U}function ht(t,e,n,r){K=n,Z=t,n.error&&(U=0),W!==void 0&&(window.clearInterval(W),W=void 0);let s=be(n);v(t),v(e);let a=o("div","reels"),l=[],i=Array.isArray(n.reels)?n.reels.join(""):String(n.reels??"");for(let h=0;h<pt;h+=1){let u=o("div","reel");for(let g=0;g<mt;g+=1){let R=h*mt+g,E=s?F[(R*3+Math.floor(Date.now()/70))%F.length]:i.length===pt*mt?i[R]:i.length===pt?i[h]:F[(R*5+h)%F.length],M=o("div","tile",E);M.classList.add(`sym-${E}`),s&&M.classList.add("spinning"),!s&&(n.win_cells??[]).includes(R)&&M.classList.add("win"),l.push(M),u.append(M)}a.append(u)}t.append(a);let d=o("div","win-lines"),m="Ready";if(!s&&n.phase==="result"){let h=Number(n.winnings??0);if(h>0){let u=(n.win_lines??[]).map(g=>`${g} (${he[g-1]??"line"})`).join(" · ");m=`${n.win_type==="mega"?"MEGA WIN":n.win_type==="big"?"BIG WIN":"WIN"} ${T(h)}${u?` · ${u}`:""}`,d.textContent=u,d.classList.add("win-text")}else m="No win"}else s&&(m="Spinning…");t.append(d);let f=o("div","board-foot");if(f.append(o("span","label","Result"),o("span",void 0,m)),t.append(f),s){let h=0;W=window.setInterval(()=>{h+=1;for(let u=0;u<l.length;u+=1){let g=F[(u*3+h)%F.length];l[u].textContent=g,l[u].className=`tile spinning sym-${g}`}Date.now()>=U&&K&&Z&&(window.clearInterval(W),W=void 0,ht(Z,e,K,r))},70)}e.append(o("h2",void 0,"Slots")),e.append(o("p","note","Match three or more symbols from the left."));let L=o("div","row"),w=o("button","btn small","−"),k=o("strong","mono",T(Number(n.bet??0))),b=o("button","btn small","+");w.disabled=s,b.disabled=s,w.addEventListener("click",()=>r({AdjustBet:-10})),b.addEventListener("click",()=>r({AdjustBet:10})),L.append(w,k,b),e.append(o("div","label","Bet"),L),n.error&&e.append(o("p","error-text",String(n.error)));let y=o("p","note");if(s)y.textContent="SPINNING…";else if(n.phase==="result"){let h=Number(n.winnings??0);y.textContent=h>0?`WON ${T(h)}`:"NO WIN",h>0&&y.classList.add("win-text")}else y.textContent="Press SPIN to play.";e.append(y);let S=o("div","actions"),c=o("button","btn primary wide big",s?"SPINNING…":"SPIN");c.disabled=s,c.addEventListener("click",()=>{Date.now()-zt<900||(zt=Date.now(),fe(),r("Spin"))});let p=o("button","btn ghost wide","Leave machine");p.disabled=s,p.addEventListener("click",()=>r("Leave")),S.append(c,p),e.append(S)}var ge=x("auth-screen"),ye=x("lobby-screen"),xe=x("game-screen"),ke=x("history-screen"),Vt=x("session-bar"),Te=x("account-name"),Le=x("account-balance"),ft=x("game-grid"),q=x("game-board"),J=x("game-side"),we=x("version-pill"),Yt=x("account-number"),Ee=x("lobby-count"),Se=x("history-body"),Ce=x("auth-form"),Gt=x("auth-user"),nt=x("auth-pass"),V=x("auth-error"),O=x("auth-submit"),Xt=x("auth-switch"),Me=x("auth-title"),Q=x("reconnect-overlay"),_t=x("reconnect-message"),jt=x("replaced-overlay"),N=null,I=null,et=null,_="login",bt=null,Kt=0,gt=0,Ut=null,Ne=new Map([["slots",10],["roulette",10],["blackjack",10],["crash",10]]),ve={slots:"Slots",roulette:"Roulette",blackjack:"Blackjack",crash:"Crash"},He={slots:"Spin the reels. Three or more matching symbols from the left win.",roulette:"Put chips on the table, then spin. Straight numbers pay the most.",blackjack:"Get closer to 21 than the dealer without going over.",crash:"Start the round, then cash out before the multiplier crashes."},Ae=location.pathname.includes("/play/")?location.pathname.slice(0,location.pathname.indexOf("/play/")):"",Re=`${location.protocol==="https:"?"wss":"ws"}://${location.host}${Ae}/ws`,tt=new URLSearchParams(location.search).get("ws"),Be=location.hostname==="127.0.0.1"||location.hostname==="localhost"||location.hostname==="::1",Pe=tt&&Be&&(tt.startsWith("ws://")||tt.startsWith("wss://"))?tt:Re,ot=new G(Pe,{onHello(t){Kt=t.time-Date.now(),I=t.user??null,typeof t.balance=="number"&&(et=t.balance),we.textContent=`v${t.version}`},onView(t){t.screen!==Ut&&(x("toasts").replaceChildren(),Ut=t.screen),N=t,t.screen!=="slots"&&Ft(),t.screen!=="crash"&&Nt(),typeof t.balance=="number"&&(et=t.balance),t.screen==="lobby"&&"user"in t&&(I=t.user??null,I?nt.value="":et=null),gt=0,Q.hidden=!0,qt(),I&&N.balance===null&&bt!==I&&(bt=I,ot.send("Info")),I||(bt=null)},onNotice(t){t.startsWith("Connected to:")||t.startsWith("Fingerprint:")||t.startsWith("User:")||t.startsWith("Account:")||t.startsWith("Balance:")||(st(t,yt(t)),t.startsWith("Account created")&&(Zt("login"),nt.value="",Gt.focus()),O.disabled=!1,O.textContent=_==="login"?"LOG IN":"CREATE ACCOUNT")},onError(t){st(t,"bad"),O.disabled=!1,O.textContent=_==="login"?"LOG IN":"CREATE ACCOUNT"},onReplaced(){jt.hidden=!1},onPong(){},onOpen(){Q.hidden=!0},onClosed(t){if(jt.hidden!==!1){if(!t){_t.textContent="The connection could not be restored. Reload the page to try again.",Q.hidden=!1;return}gt+=1,_t.textContent=gt>1?"Still trying to reach the game server…":"Trying to reach the game server…",Q.hidden=!1}}});function P(t){ot.send(t)}function $e(){return Date.now()+Kt}function Zt(t){_=t,Me.textContent=t==="login"?"Sign in":"Create account",O.textContent=t==="login"?"LOG IN":"CREATE ACCOUNT",Xt.textContent=t==="login"?"CREATE ACCOUNT":"BACK TO SIGN IN",nt.autocomplete=t==="login"?"current-password":"new-password",V.textContent=""}function Y(t){ge.hidden=t!=="auth",ye.hidden=t!=="lobby",xe.hidden=t!=="game",ke.hidden=t!=="history"}function We(t){Le.textContent=T(t??null)}function qt(){if(!N)return;let t=I;if(t?(Vt.hidden=!1,Te.textContent=String(t)):(Vt.hidden=!0,Yt.textContent="—"),We(et),!t){Y("auth"),V.textContent=N.login_error?String(N.login_error):"",N.screen==="lobby"&&N.connected===!1&&(V.textContent="The game server is not reachable yet. Retrying…");return}switch(N.screen){case"lobby":Y("lobby"),Ie(N);break;case"history":Y("history"),Ht(Se,N);break;case"slots":case"roulette":case"blackjack":case"crash":Y("game"),N.screen==="slots"?ht(q,J,N,P):N.screen==="roulette"?Dt(q,J,N,P):N.screen==="blackjack"?wt(q,J,N,P):vt(q,J,N,P,{now:$e});break;default:Y("auth")}}function Ie(t){Yt.textContent=t.account?String(t.account):"—";let e=["slots","roulette","blackjack","crash"],n=[...t.games??[]].sort((r,s)=>e.indexOf(r[0])-e.indexOf(s[0]));Ee.textContent=`${n.length} ${n.length===1?"game":"games"}`,ft.replaceChildren();for(let[r,s]of n){let a=o("article",`game-card ${r}`);a.append(o("h3",void 0,s||ve[r]||r)),a.append(o("p",void 0,He[r]??"Play a round."));let l=o("button","btn primary wide","PLAY");l.addEventListener("click",()=>{P({Play:{game:r,bet:Ne.get(r)??10}})}),a.append(l),ft.append(a)}n.length===0&&ft.append(o("p","muted","The server did not advertise any games."))}Xt.addEventListener("click",()=>Zt(_==="login"?"register":"login"));Ce.addEventListener("submit",async t=>{t.preventDefault();let e=Gt.value.trim(),n=nt.value;if(!e||!n){V.textContent="Enter a username and a password.";return}O.disabled=!0,O.textContent="Working…",V.textContent="";try{let r=await xt(n);P(_==="login"?{Login:{user:e,credential:r}}:{Register:{user:e,credential:r}})}catch{V.textContent="This browser cannot hash the password (needs a secure context).",O.disabled=!1,O.textContent=_==="login"?"Log in":"Create account"}});x("logout").addEventListener("click",()=>P("Logout"));x("show-history").addEventListener("click",()=>P({History:{limit:50}}));x("history-back").addEventListener("click",()=>P("ShowLobby"));x("reload-replaced").addEventListener("click",()=>location.reload());x("reload-reconnect").addEventListener("click",()=>location.reload());window.addEventListener("resize",()=>{window.clearTimeout(window.__resizeTimer),window.__resizeTimer=window.setTimeout(()=>{N&&qt()},140)});window.setInterval(()=>{I&&ot.ping()},25e3);ot.connect();})(); "use strict";(()=>{function x(t){let e=document.getElementById(t);if(!e)throw new Error(`missing #${t}`);return e}function o(t,e,n){let r=document.createElement(t);return e&&(r.className=e),n!==void 0&&(r.textContent=n),r}function v(t){t.replaceChildren()}function Jt(t){let e=t<0,n=String(Math.abs(Math.trunc(t))),r="";for(let s=0;s<n.length;s+=1)s>0&&(n.length-s)%3===0&&(r+=" "),r+=n[s];return e?`-${r}`:r}function T(t){return t==null||Number.isNaN(t)?"— Kr":`${Jt(t)} Kr`}function st(t,e="info"){let n=document.getElementById("toasts");if(!n||!t)return;let r=o("div",`toast ${e}`,t);for(n.append(r),window.setTimeout(()=>{r.style.opacity="0",r.style.transition="opacity 220ms ease",window.setTimeout(()=>r.remove(),240)},4200);n.children.length>4;)n.firstElementChild?.remove()}function yt(t){let e=t.toLowerCase();return t.startsWith("ERROR")||e.includes("failed")||e.includes("lost")||e.includes("invalid")||e.includes("forfeit")?"bad":t.startsWith("Logged in")||t.startsWith("Account created")||e.includes("won")||e.includes("cashed")?"good":"info"}async function xt(t){let e=new TextEncoder().encode(t),n=await crypto.subtle.digest("SHA-256",e);return[...new Uint8Array(n)].map(r=>r.toString(16).padStart(2,"0")).join("")}function j(t,e=10){return t.length<=e*2?t:`${t.slice(0,e)}…${t.slice(-e)}`}function kt(t){if(t==="?"||!t)return{rank:"?",suit:"",red:!1};let e=t.slice(-1);return{rank:t.slice(0,-1),suit:e==="C"?"♣":e==="D"?"♦":e==="H"?"♥":e==="S"?"♠":e,red:e==="D"||e==="H"}}var G=class{constructor(e,n){this.url=e;this.handlers=n;this.socket=null;this.stopped=!1;this.replaced=!1;this.attempts=0}connect(){if(this.stopped)return;this.clearTimer();let e;try{e=new WebSocket(this.url)}catch{this.scheduleReconnect()||this.handlers.onClosed(!1);return}this.socket=e,e.addEventListener("open",()=>{this.attempts=0,this.handlers.onOpen()}),e.addEventListener("message",n=>{let r;try{r=JSON.parse(String(n.data))}catch{return}switch(r.type){case"hello":this.handlers.onHello(r);break;case"view":this.handlers.onView(r.view);break;case"notice":this.handlers.onNotice(String(r.text??""));break;case"error":this.handlers.onError(String(r.text??""));break;case"replaced":this.replaced=!0,this.handlers.onReplaced();break;case"pong":this.handlers.onPong();break}}),e.addEventListener("close",()=>{if(this.socket===e&&(this.socket=null),this.replaced||this.stopped){this.handlers.onClosed(!1);return}let r=this.scheduleReconnect();this.handlers.onClosed(r)}),e.addEventListener("error",()=>{})}send(e){this.socket?.readyState===WebSocket.OPEN&&this.socket.send(JSON.stringify({type:"command",command:e}))}ping(){this.socket?.readyState===WebSocket.OPEN&&this.socket.send(JSON.stringify({type:"ping"}))}close(){this.stopped=!0,this.clearTimer(),this.socket?.close()}scheduleReconnect(){if(this.stopped||this.replaced||this.attempts>=6)return!1;this.attempts+=1;let e=Math.min(250*2**(this.attempts-1),4e3);return this.timer=window.setTimeout(()=>this.connect(),e),!0}clearTimer(){this.timer!==void 0&&(window.clearTimeout(this.timer),this.timer=void 0)}};var Tt=0;function rt(){return Date.now()-Tt<400?!1:(Tt=Date.now(),!0)}function Qt(t){if(t==="?"){let r=o("div","card back");return r.append(o("span","rank","?"),o("span","suit","")),r}let e=kt(t),n=o("div","card");return e.red&&n.classList.add("red"),n.append(o("span","rank",e.rank),o("span","suit",e.suit)),n}function Lt(t,e,n){let r=o("div","hand"),s=o("div","hand-head");s.append(o("span","label",t),o("strong","total",String(n)));let a=o("div","cards");for(let l of e)a.append(Qt(l));return r.append(s,a),r}function te(t,e){let n=String(e?.result??""),r=Number(e?.winnings??0);switch(n){case"blackjack":return`Blackjack — won ${T(r)}`;case"win":return`You win — ${T(r)}`;case"push":return`Push — ${T(r)} returned`;case"bust":return"Bust — the bet is lost";case"lose":return"Dealer wins — the bet is lost";case"forfeit":return"Hand forfeited — the bet is lost";case"playing":return t.status==="player"?"Your move.":"Dealing…";default:return"Ready to deal."}}function wt(t,e,n,r){v(t),v(e);let s=n.hand,a=s?.phase==="player"||n.status==="player",l=s?.phase==="done"||n.status==="done",i=!s||l,d=o("div","bj-table");if(s)d.append(Lt("DEALER",s.dealer??[],Number(s.dealer_total??0))),d.append(o("div","bj-divider")),d.append(Lt("YOU",s.player??[],Number(s.player_total??0)));else{let b=o("div","empty-table","Place a bet and deal a hand.");d.append(b)}t.append(d);let m=o("div","board-foot");m.append(o("span","label","Hand"),o("span",void 0,s?te(n,s):"Waiting")),t.append(m),n.error&&t.append(o("p","error-text",String(n.error))),e.append(o("h2",void 0,"Blackjack"));let f=Number(s?.bet??n.bet??0),L=n.balance??n.hand?.balance;if(e.append(o("p","note","Get closer to 21 without going over.")),i){let b=o("div","row"),y=o("button","btn small","−10"),S=o("strong","mono",T(f)),c=o("button","btn small","+10");y.addEventListener("click",()=>r({AdjustBet:-10})),c.addEventListener("click",()=>r({AdjustBet:10})),b.append(y,S,c),e.append(o("div","label","Bet"),b)}else{let b=o("div","stat");b.append(o("span","muted","Bet"),o("strong",void 0,T(f))),e.append(b)}if(L!==void 0){let b=o("div","stat");b.append(o("span","muted","Balance"),o("strong",void 0,T(Number(L)))),e.append(b)}if(l||s&&!a){let b=o("p","note win-text");b.textContent=`${String(s?.result??"").toUpperCase()} ${T(Number(s?.winnings??0))}`,["win","blackjack","push"].includes(String(s?.result))||(b.className="note"),e.append(b)}else if(a){let b=(s?.actions??[]).includes("double")?"Hit, stand or double.":"Hit or stand.";e.append(o("p","note",b))}else s||e.append(o("p","note","Press DEAL to start a hand."));let w=o("div","actions");if(a){let b=o("div","row wrap");for(let S of s?.actions??[]){let c=S==="hit"?"HIT":S==="stand"?"STAND":"DOUBLE",p=S==="stand"?o("button","btn primary",c):o("button","btn",c);p.addEventListener("click",()=>{rt()&&r(S==="hit"?"Hit":S==="stand"?"Stand":"Double")}),b.append(p)}w.append(b);let y=o("button","btn danger wide","Forfeit hand");y.addEventListener("click",()=>{rt()&&window.confirm("Forfeit this hand? The reserved bet is lost.")&&r("Forfeit")}),w.append(y)}else if(i){let b=o("button","btn primary wide big","DEAL");b.addEventListener("click",()=>{rt()&&r("Deal")}),w.append(b)}let k=o("button","btn ghost wide",a?"Leave (forfeits)":"Leave table");k.addEventListener("click",()=>{a&&!window.confirm("Leaving forfeits this hand. Continue?")||r("Leave")}),w.append(k),e.append(w)}var at=Math.LN2/5,B=null,z=null,Mt=null,D=null,$,Et=0,X="",St=0;function ee(t,e,n,r){v(t),v(e),D=n,Mt=r;let s=o("div","crash-wrap"),a=o("div","crash-number","1.00×"),l=o("div","label","READY"),i=o("div","crash-meta"),d=o("canvas","crash-canvas"),m=o("div","verify","Waiting for a round commitment.");s.append(a,l,i,d,m),t.append(s),e.append(o("h2",void 0,"Crash")),e.append(o("p","note","Cash out before the multiplier crashes."));let f=o("div","row"),L=o("button","btn small","−"),w=o("strong","mono","—"),k=o("button","btn small","+");L.addEventListener("click",()=>D?.({AdjustBet:-10})),k.addEventListener("click",()=>D?.({AdjustBet:10})),f.append(L,w,k),e.append(o("div","label","Bet"),f);let b=o("strong","mono","—"),y=o("strong","mono","—");for(let[R,E]of[["Potential",b],["Balance",y]]){let M=o("div","stat");M.append(o("span","muted",R),E),e.append(M)}let S=o("p","note","Press START to play."),c=o("p","note",""),p=o("p","error-text");e.append(S,c,p);let h=o("div","actions"),u=o("button","btn primary wide big","START"),g=o("button","btn ghost wide","Leave table");return u.addEventListener("click",()=>{Date.now()-St<500||(St=Date.now(),z?.status==="running"?D?.("CashOut"):D?.("Start"))}),g.addEventListener("click",()=>{z?.status==="running"&&!window.confirm("Forfeit this round? The bet is lost.")||D?.("Leave")}),h.append(u,g),e.append(h),{board:t,side:e,number:a,meta:i,verify:m,canvas:d,bet:w,potential:b,balance:y,status:S,lastCrash:c,error:p,primary:u,leave:g,minus:L,plus:k}}function Nt(){$!==void 0&&(cancelAnimationFrame($),$=void 0),z=null}function vt(t,e,n,r,s){(!B||B.board!==t||!B.canvas.isConnected)&&(B=ee(t,e,r,s),X="");let a=B;D=r,Mt=s,z=n,n.status!==X&&((n.status==="starting"||n.status==="running")&&X!=="running"&&(a.verify.className="verify",a.verify.textContent="Round committed; seed hidden until the round ends."),X=String(n.status));let l=n.status==="running",i=n.status==="starting",d=l||i;a.bet.textContent=T(Number(n.bet??0)),a.potential.textContent=T(Number(n.potential??n.bet??0)),a.balance.textContent=n.balance===null||n.balance===void 0?"—":T(Number(n.balance)),a.error.textContent=n.error?String(n.error):"",a.minus.disabled=d,a.plus.disabled=d,a.primary.textContent=l?"CASH OUT":i?"STARTING…":"START",a.primary.disabled=i,a.primary.className="btn primary wide big",a.leave.textContent=l?"Forfeit round":"Leave table",l?(a.status.textContent="Rising — cash out before the crash.",a.status.className="note win-text"):i?(a.status.textContent="Starting the round…",a.status.className="note"):n.status==="cashed"?(a.status.textContent=`Cashed out at ${Number(n.multiplier??0).toFixed(2)}× for ${T(Number(n.payout??0))}.`,a.status.className="note win-text"):n.status==="crashed"?(a.status.textContent=`Crashed at ${Number(n.crash_point??n.multiplier??0).toFixed(2)}×. The bet is lost.`,a.status.className="note error-text"):n.status==="forfeit"?(a.status.textContent="Round forfeited.",a.status.className="note error-text"):(a.status.textContent="Press START to play.",a.status.className="note"),n.crash_point!==null&&n.crash_point!==void 0&&n.status==="crashed"&&(a.lastCrash.textContent=`Last crash: ${Number(n.crash_point).toFixed(2)}×`);let m=n.commit?String(n.commit):"";if(a.meta.textContent=m?`commit ${j(m,12)}`:"",a.meta.style.display=m?"":"none",a.verify.style.display=m?"":"none",n.seed&&m){let f=++Et,L=String(n.seed),w=Number(n.crash_point??0);se(L,m,w).then(([k,b])=>{if(f!==Et||!B)return;let y=k&&b;B.verify.className=`verify ${y?"ok":"bad"}`,B.verify.textContent=y?`Seed ${j(L,12)} · sha256 matches commit · crash point recomputed`:`VERIFICATION FAILED · seed ${j(L,12)} · commit ${j(m,12)}`})}else!d&&!m&&(a.verify.className="verify",a.verify.textContent="Waiting for a round commitment.");if($!==void 0&&(cancelAnimationFrame($),$=void 0),l){let f=()=>{if(!B||z?.status!=="running"){$=void 0;return}Ct(B,z,s),$=requestAnimationFrame(f)};$=requestAnimationFrame(f)}else Ct(a,n,s)}function ne(t,e){if(t.status==="running"){let n=Math.max(0,(e.now()-Number(t.started_at??0))/1e3),r=Math.min(1e3,Math.exp(at*n));return Math.max(r,Number(t.multiplier??1))}return t.status==="cashed"?Number(t.multiplier??1):t.status==="crashed"?Number(t.crash_point??t.multiplier??1):1}function Ct(t,e,n){let r=ne(e,n);t.number.textContent=`${r.toFixed(2)}×`;let s=t.number.nextElementSibling;s&&(s.textContent=e.status==="running"?"RUNNING":e.status==="cashed"?"CASHED OUT":e.status==="crashed"?"CRASHED":e.status==="starting"?"STARTING":"READY"),t.number.className=`crash-number ${e.status==="running"?"running":e.status==="cashed"?"cashed":e.status==="crashed"?"crashed":""}`,t.potential.textContent=T(Math.floor(Number(e.bet??0)*r)),oe(t.canvas,e,r,n)}function oe(t,e,n,r){let s=t.clientWidth||600,a=t.clientHeight||360,l=window.devicePixelRatio||1;(t.width!==Math.round(s*l)||t.height!==Math.round(a*l))&&(t.width=Math.round(s*l),t.height=Math.round(a*l));let i=t.getContext("2d");if(!i)return;i.setTransform(l,0,0,l,0,0),i.clearRect(0,0,s,a);let d=54,m=16,f=18,L=34,w=Math.max(1,s-d-m),k=Math.max(1,a-f-L),b=e.status==="running"?Math.max(.25,(r.now()-Number(e.started_at??0))/1e3):n>1?Math.log(n)/at:1,y=Math.max(5,b*1.08),S=Math.max(2,Math.ceil(n*1.15*2)/2),c=E=>d+E/y*w,p=E=>f+k-(E-1)/(S-1)*k;i.strokeStyle="#242e38",i.fillStyle="#8a98a6",i.font="11px ui-monospace, monospace",i.textAlign="left",i.textBaseline="middle",i.lineWidth=1;for(let E=0;E<=4;E+=1){let M=1+(S-1)*E/4,C=p(M);i.beginPath(),i.moveTo(d,C),i.lineTo(s-m,C),i.stroke(),i.fillText(`${M.toFixed(1)}×`,10,C)}for(let E=0;E<=4;E+=1){let M=y*E/4,C=c(M);i.beginPath(),i.moveTo(C,f),i.lineTo(C,f+k),i.strokeStyle="#1b232c",i.stroke(),i.fillStyle="#5b6773",i.textAlign="center",i.textBaseline="alphabetic",i.fillText(`${M.toFixed(0)}s`,C,a-12),i.textAlign="left",i.textBaseline="middle"}if(!["running","cashed","crashed"].includes(String(e.status))){i.fillStyle="#5b6773",i.textAlign="center",i.textBaseline="middle",i.font='13px "Open Sans", sans-serif',i.fillText("Press START to play",d+w/2,f+k/2);return}let h=e.status==="crashed"?"#e05a5a":e.status==="cashed"?"#4ec27b":"#e0b64a";i.beginPath();let u=180;for(let E=0;E<=u;E+=1){let M=b*E/u,C=Math.min(S,Math.exp(at*M)),H=c(M),A=p(C);E===0?i.moveTo(H,A):i.lineTo(H,A)}i.strokeStyle=h,i.lineWidth=2.4,i.stroke();let g=c(b),R=p(Math.min(S,n));i.beginPath(),i.arc(g,R,4.5,0,Math.PI*2),i.fillStyle=h,i.fill(),i.fillStyle=h,i.font="600 12px ui-monospace, monospace",i.fillText(`${n.toFixed(2)}×`,Math.min(g+8,s-70),Math.max(f+12,R-10))}async function se(t,e,n){let r=new Uint8Array(t.length/2);for(let f=0;f<r.length;f+=1)r[f]=Number.parseInt(t.slice(f*2,f*2+2),16);let s=await crypto.subtle.digest("SHA-256",r),l=[...new Uint8Array(s)].map(f=>f.toString(16).padStart(2,"0")).join("")===e.toLowerCase(),i=0;for(let f=0;f<8;f+=1)i=i*256+r[f];i/=2**64;let d=Math.max(1,Math.min(1e3,(1-.01)/(1-i))),m=Math.abs(d-n)<1e-6;return[l,m]}function re(t){let e=t.details??{};switch(t.game){case"slots":return String(e.win_type??"spin");case"roulette":return`${e.number??"?"} ${e.color??""}`.trim();case"blackjack":return String(e.result??"");case"crash":{let n=String(e.result??""),r=e.crash_point;return r==null?n:`${n} @ ${Number(r).toFixed(2)}×`}default:return""}}function Ht(t,e){v(t);let n=e.records??[];if(n.length===0){let d=o("div","board");d.append(o("h2",void 0,"No plays yet"),o("p","muted","Your settled plays will appear here.")),t.append(d);return}let r=o("div","table-wrap"),s=o("table"),a=o("thead"),l=o("tr");for(let d of["When","Game","Bet","Won","Balance","Result"])l.append(o("th",void 0,d));a.append(l);let i=o("tbody");for(let d of n){let m=o("tr");m.append(o("td","mono",String(d.at??"").slice(0,16))),m.append(o("td","mono",String(d.game??"").toUpperCase())),m.append(o("td","num",T(Number(d.bet??0))));let f=Number(d.winnings??0);m.append(o("td",`num ${f>0?"pos":""}`,T(f))),m.append(o("td","num",T(Number(d.balance_after??0)))),m.append(o("td","muted",re(d))),i.append(m)}s.append(a,i),r.append(s),t.append(r)}var it=[0,32,15,19,4,21,2,25,17,34,6,27,13,36,11,30,8,23,10,5,24,16,33,1,20,14,31,9,22,18,29,7,28,12,35,3,26],ae=new Set([1,3,5,7,9,12,14,16,18,19,21,23,25,27,30,32,34,36]),Wt="#b0313a",It="#151b21",Ot="#106b45",At="#0d5c39",ct="#1f7d52",dt="#ecf7f0",ie="#e0b64a",le="#11171d",lt=null;function ut(t){return t===0?"green":ae.has(t)?"red":"black"}function ce(t,e){let r=Math.max(44,t*.058),s=Math.max(28,Math.min(38,e*.075)),a=t-28-r,l=a/12,i=(e-28-s*3)/3,d=14+r,m=14,f=m+i*3,L=f+s,w=L+s,k=[];for(let c=0;c<12;c+=1)for(let p=0;p<3;p+=1){let h=c*3+(3-p);k.push({x:d+c*l,y:m+p*i,w:l,h:i,target:{bet:"straight",number:h},label:String(h),kind:"number",number:h})}k.push({x:14,y:m,w:r,h:i*3,target:{bet:"straight",number:0},label:"0",kind:"zero",number:0});for(let c=0;c<3;c+=1)k.push({x:d+a/3*c,y:f,w:a/3,h:s,target:{bet:"dozen",index:c},label:["1st 12","2nd 12","3rd 12"][c],kind:"dozen"}),k.push({x:d+a/3*c,y:L,w:a/3,h:s,target:{bet:"column",index:2-c},label:"2:1",kind:"column"});[{kind:"low",label:"1–18"},{kind:"even",label:"EVEN"},{kind:"red",label:"RED"},{kind:"black",label:"BLACK"},{kind:"odd",label:"ODD"},{kind:"high",label:"19–36"}].forEach((c,p)=>{k.push({x:d+a/6*p,y:w,w:a/6,h:s,target:{bet:"outside",kind:c.kind},label:c.label,kind:"outside"})});let y=[],S=c=>{let p=k.find(h=>h.kind==="number"&&h.number===c);return p?{x:p.x+p.w/2,y:p.y+p.h/2}:null};for(let c=0;c<11;c+=1)for(let p=0;p<2;p+=1){let h=c*3+(3-p);y.push({x:d+(c+1)*l,y:m+(p+1)*i,radius:12,rank:0,target:{bet:"corner",numbers:[h,h-1,h+3,h+2]}})}for(let c=0;c<12;c+=1)for(let p=0;p<2;p+=1){let h=c*3+(3-p),u=S(h);u&&y.push({x:u.x,y:m+(p+1)*i,radius:10,rank:1,target:{bet:"split",a:h,b:h-1}})}for(let c=0;c<11;c+=1)for(let p=0;p<3;p+=1){let h=c*3+(3-p);y.push({x:d+(c+1)*l,y:m+p*i+i/2,radius:10,rank:1,target:{bet:"split",a:h,b:h+3}})}for(let c=0;c<3;c+=1)y.push({x:d,y:m+c*i+i/2,radius:10,rank:1,target:{bet:"split",a:0,b:3-c}});return{width:t,height:e,pad:14,cellW:l,cellH:i,zeroX:14,zeroW:r,gridX:d,gridY:m,gridW:a,dozensY:f,columnsY:L,outsideY:w,outsideH:s,zones:k,hotspots:y}}function Rt(t,e,n,r,s){t.beginPath(),t.rect(e,n,r,s),t.closePath()}function Bt(t,e){let n=t.clientWidth||900,r=t.clientHeight||500,s=window.devicePixelRatio||1;(t.width!==Math.round(n*s)||t.height!==Math.round(r*s))&&(t.width=Math.round(n*s),t.height=Math.round(r*s));let a=t.getContext("2d"),l=ce(n,r);if(!a)return l;a.setTransform(s,0,0,s,0,0),a.clearRect(0,0,n,r),a.fillStyle=At,Rt(a,l.pad,l.gridY,l.zeroW+l.gridW,l.cellH*3+l.outsideH*3),a.fill();for(let i of l.zones){let d=At;i.kind==="number"?d=ut(i.number)==="red"?Wt:It:i.kind==="zero"&&(d=Ot),a.fillStyle=d,a.strokeStyle=ct,a.lineWidth=1,Rt(a,i.x+.5,i.y+.5,i.w-1,i.h-1),a.fill(),a.stroke(),a.fillStyle=dt,a.textAlign="center",a.textBaseline="middle",i.kind==="number"||i.kind==="zero"?a.font=`500 ${Math.min(18,l.cellH*.16)}px "Open Sans", sans-serif`:a.font=`500 ${Math.min(12,l.outsideH*.4)}px "Open Sans", sans-serif`,i.label==="RED"&&(a.fillStyle="#e05a5a"),i.label==="BLACK"&&(a.fillStyle="#8a98a6"),a.fillText(i.label,i.x+i.w/2,i.y+i.h/2)}return ue(a,l,e),l}function Pt(t){return JSON.stringify(t)}function de(t,e){let n=Pt(e),r=t.zones.find(s=>Pt(s.target)===n);if(r)return{x:r.x+r.w/2,y:r.y+r.h/2};if(e?.bet==="split"||e?.bet==="corner"){let s=e.bet==="split"?[e.a,e.b]:e.numbers,a=t.zones.filter(l=>l.kind==="number"&&s.includes(l.number));if(a.length>0)return{x:a.reduce((l,i)=>l+i.x+i.w/2,0)/a.length,y:a.reduce((l,i)=>l+i.y+i.h/2,0)/a.length}}return null}function ue(t,e,n){let r=n.bets?.chips??[];for(let s of r){let a=de(e,s.target);if(!a)continue;let l=Math.max(12,Math.min(17,e.cellW*.28));t.beginPath(),t.arc(a.x,a.y,l,0,Math.PI*2),t.fillStyle=ie,t.fill(),t.strokeStyle="#7a5a12",t.lineWidth=2,t.stroke(),t.fillStyle="#221a05",t.textAlign="center",t.textBaseline="middle",t.font=`700 ${l>15?11:10}px "Hack", monospace`,t.fillText(String(s.amount),a.x,a.y+.5)}}function pe(t,e){let r=window.devicePixelRatio||1;t.width=Math.round(170*r),t.height=Math.round(170*r),t.style.width="170px",t.style.height="170px";let s=t.getContext("2d");if(!s)return;s.setTransform(r,0,0,r,0,0);let a=e.phase==="result"&&e.number!==null&&e.number!==void 0?Number(e.number):null,l=a===null?-1:it.indexOf(a),i=Math.PI*2/it.length,d=l<0?0:-l*i,m=performance.now(),f=650,L=k=>{s.clearRect(0,0,170,170),s.fillStyle=le,s.beginPath(),s.arc(85,85,78.2+6,0,Math.PI*2),s.fill(),it.forEach((p,h)=>{let u=-Math.PI/2+h*i+k;s.beginPath(),s.moveTo(85,85),s.arc(85,85,78.2,u-i/2,u+i/2),s.closePath(),s.fillStyle=ut(p)==="red"?Wt:ut(p)==="green"?Ot:It,s.fill(),s.strokeStyle=ct,s.lineWidth=.8,s.stroke()}),s.beginPath(),s.arc(85,85,49.3,0,Math.PI*2),s.fillStyle="#0a0e12",s.fill(),s.strokeStyle=ct,s.lineWidth=1,s.stroke(),s.beginPath(),s.moveTo(85,8),s.lineTo(80,18),s.lineTo(90,18),s.closePath(),s.fillStyle=dt,s.fill(),a!==null&&(s.fillStyle=dt,s.textAlign="center",s.textBaseline="middle",s.font='600 15px "Hack", monospace',s.fillText(String(a),85,85))},w=k=>{let b=Math.min(1,(k-m)/f),y=1-(1-b)**3;L(d*y),b<1&&requestAnimationFrame(w)};requestAnimationFrame(w)}function Dt(t,e,n,r){v(t),v(e);let s=n.phase==="spinning",a=o("canvas","roulette-canvas");t.append(a);let l=o("div","board-foot tight");if(l.append(o("span","label","Last")),n.phase==="result"&&n.number!==null&&n.number!==void 0){let u=o("span",n.winnings&&Number(n.winnings)>0?"win-text":void 0,`${n.number} ${n.color} · ${Number(n.winnings??0)>0?T(Number(n.winnings)):"no win"}`);l.append(u)}else l.append(o("span",void 0,"No spins yet"));l.append(o("span","note","Click to place a chip · right-click to take one back")),t.append(l);let d=Bt(a,n),m=(u,g)=>{let R=a.getBoundingClientRect(),E=u-R.left,M=g-R.top,C=null;for(let H of d.hotspots){let A=Math.hypot(E-H.x,M-H.y);A>H.radius||(!C||H.rank<C.rank||H.rank===C.rank&&A<Math.hypot(E-C.x,M-C.y))&&(C=H)}if(C)return{x:C.x-C.radius,y:C.y-C.radius,w:C.radius*2,h:C.radius*2,target:C.target,label:"",kind:"number"};for(let H=d.zones.length-1;H>=0;H-=1){let A=d.zones[H];if(E>=A.x&&E<=A.x+A.w&&M>=A.y&&M<=A.y+A.h)return A}return null};a.addEventListener("click",u=>{if(s)return;let g=m(u.clientX,u.clientY);g&&r({PlaceBetAt:g.target})}),a.addEventListener("contextmenu",u=>{if(u.preventDefault(),s)return;let g=m(u.clientX,u.clientY);g&&r({RemoveBetAt:g.target})}),a.addEventListener("mousemove",u=>{let g=m(u.clientX,u.clientY);a.style.cursor=g&&!s?"pointer":"default"}),lt?.disconnect(),lt=new ResizeObserver(()=>{d=Bt(a,n)}),lt.observe(a),e.append(o("h2",void 0,"Roulette")),e.append(o("p","note","Pick numbers or colours, then spin."));let f=o("canvas","wheel-canvas");e.append(f),pe(f,n),e.append(o("div","label","Place your bets"));let L=(n.bets?.chips??[]).reduce((u,g)=>u+Number(g.amount??0),0);e.append(o("p","note",`${L} ${L===1?"chip":"chips"}`)),e.append(o("div","label","Chip"));let w=o("div","chip-row");for(let u of[10,25,50,100]){let g=o("button",`chip-btn ${Number(n.chip)===u?"active":""}`,String(u));g.dataset.chip=String(u),g.disabled=s,g.addEventListener("click",()=>r({SetChip:u})),w.append(g)}e.append(w),e.append(o("div","label","Bets"));let k=n.bets?.chips??[];if(k.length===0)e.append(o("p","note","No chips on the felt"));else for(let u of k){let g=o("div","stat");g.append(o("span","muted",me(u.target)),o("strong","mono",T(Number(u.amount??0)))),e.append(g)}let b=o("div","stat");b.append(o("span","muted","Total"),o("strong","mono",T(L))),e.append(b),n.error&&e.append(o("p","error-text",String(n.error)));let y=o("p","note");if(s)y.textContent="SPINNING…";else if(n.phase==="result"){let u=Number(n.winnings??0);y.textContent=u>0?`WON ${T(u)}`:"NO WIN",u>0&&y.classList.add("win-text")}else y.textContent=L>0?"Ready to spin.":"Place at least one chip.";e.append(y);let S=o("div","actions"),c=o("button","btn primary wide big",s?"SPINNING…":"SPIN");c.disabled=s||L===0,c.addEventListener("click",()=>{Date.now()-$t<600||($t=Date.now(),r("Spin"))});let p=o("button","btn wide","CLEAR");p.disabled=s||L===0,p.addEventListener("click",()=>r("ClearBets"));let h=o("button","btn ghost wide","Leave table");h.disabled=s,h.addEventListener("click",()=>r("Leave")),S.append(c,p,h),e.append(S)}var $t=0;function me(t){switch(t?.bet){case"straight":return String(t.number);case"split":return`${t.a}/${t.b}`;case"corner":return t.numbers.join("/");case"dozen":return["1st 12","2nd 12","3rd 12"][t.index]??"dozen";case"column":return`col ${Number(t.index)+1}`;case"outside":return String(t.kind??"");default:return""}}var pt=5,mt=3,F="CLOGDS",he=["top","middle","bottom","V","inverted V"],U=0,zt=0,W,K=null,Z=null;function fe(){U=Date.now()+900}function Ft(){W!==void 0&&(window.clearInterval(W),W=void 0),K=null,Z=null,U=0}function be(t){return t.phase==="spinning"||Date.now()<U}function ht(t,e,n,r){K=n,Z=t,n.error&&(U=0),W!==void 0&&(window.clearInterval(W),W=void 0);let s=be(n);v(t),v(e);let a=o("div","reels"),l=[],i=Array.isArray(n.reels)?n.reels.join(""):String(n.reels??"");for(let h=0;h<pt;h+=1){let u=o("div","reel");for(let g=0;g<mt;g+=1){let R=h*mt+g,E=s?F[(R*3+Math.floor(Date.now()/70))%F.length]:i.length===pt*mt?i[R]:i.length===pt?i[h]:F[(R*5+h)%F.length],M=o("div","tile",E);M.classList.add(`sym-${E}`),s&&M.classList.add("spinning"),!s&&i.length===0&&M.classList.add("placeholder"),!s&&(n.win_cells??[]).includes(R)&&M.classList.add("win"),l.push(M),u.append(M)}a.append(u)}t.append(a);let d=o("div","win-lines"),m="Ready";if(!s&&n.phase==="result"){let h=Number(n.winnings??0);if(h>0){let u=(n.win_lines??[]).map(g=>`${g} (${he[g-1]??"line"})`).join(" · ");m=`${n.win_type==="mega"?"MEGA WIN":n.win_type==="big"?"BIG WIN":"WIN"} ${T(h)}${u?` · ${u}`:""}`,d.textContent=u,d.classList.add("win-text")}else m="No win"}else s&&(m="Spinning…");t.append(d);let f=o("div","board-foot");if(f.append(o("span","label","Result"),o("span",void 0,m)),t.append(f),s){let h=0;W=window.setInterval(()=>{h+=1;for(let u=0;u<l.length;u+=1){let g=F[(u*3+h)%F.length];l[u].textContent=g,l[u].className=`tile spinning sym-${g}`}Date.now()>=U&&K&&Z&&(window.clearInterval(W),W=void 0,ht(Z,e,K,r))},70)}e.append(o("h2",void 0,"Slots")),e.append(o("p","note","Match three or more symbols from the left."));let L=o("div","row"),w=o("button","btn small","−"),k=o("strong","mono",T(Number(n.bet??0))),b=o("button","btn small","+");w.disabled=s,b.disabled=s,w.addEventListener("click",()=>r({AdjustBet:-10})),b.addEventListener("click",()=>r({AdjustBet:10})),L.append(w,k,b),e.append(o("div","label","Bet"),L),n.error&&e.append(o("p","error-text",String(n.error)));let y=o("p","note");if(s)y.textContent="SPINNING…";else if(n.phase==="result"){let h=Number(n.winnings??0);y.textContent=h>0?`WON ${T(h)}`:"NO WIN",h>0&&y.classList.add("win-text")}else y.textContent="Press SPIN to play.";e.append(y);let S=o("div","actions"),c=o("button","btn primary wide big",s?"SPINNING…":"SPIN");c.disabled=s,c.addEventListener("click",()=>{Date.now()-zt<900||(zt=Date.now(),fe(),r("Spin"))});let p=o("button","btn ghost wide","Leave machine");p.disabled=s,p.addEventListener("click",()=>r("Leave")),S.append(c,p),e.append(S)}var ge=x("auth-screen"),ye=x("lobby-screen"),xe=x("game-screen"),ke=x("history-screen"),Vt=x("session-bar"),Te=x("account-name"),Le=x("account-balance"),ft=x("game-grid"),q=x("game-board"),J=x("game-side"),we=x("version-pill"),Yt=x("account-number"),Ee=x("lobby-count"),Se=x("history-body"),Ce=x("auth-form"),Gt=x("auth-user"),nt=x("auth-pass"),V=x("auth-error"),O=x("auth-submit"),Xt=x("auth-switch"),Me=x("auth-title"),Q=x("reconnect-overlay"),_t=x("reconnect-message"),jt=x("replaced-overlay"),N=null,I=null,et=null,_="login",bt=null,Kt=0,gt=0,Ut=null,Ne=new Map([["slots",10],["roulette",10],["blackjack",10],["crash",10]]),ve={slots:"Slots",roulette:"Roulette",blackjack:"Blackjack",crash:"Crash"},He={slots:"Spin the reels. Three or more matching symbols from the left win.",roulette:"Put chips on the table, then spin. Straight numbers pay the most.",blackjack:"Get closer to 21 than the dealer without going over.",crash:"Start the round, then cash out before the multiplier crashes."},Ae=location.pathname.includes("/play/")?location.pathname.slice(0,location.pathname.indexOf("/play/")):"",Re=`${location.protocol==="https:"?"wss":"ws"}://${location.host}${Ae}/ws`,tt=new URLSearchParams(location.search).get("ws"),Be=location.hostname==="127.0.0.1"||location.hostname==="localhost"||location.hostname==="::1",Pe=tt&&Be&&(tt.startsWith("ws://")||tt.startsWith("wss://"))?tt:Re,ot=new G(Pe,{onHello(t){Kt=t.time-Date.now(),I=t.user??null,typeof t.balance=="number"&&(et=t.balance),we.textContent=`v${t.version}`},onView(t){t.screen!==Ut&&(x("toasts").replaceChildren(),Ut=t.screen),N=t,t.screen!=="slots"&&Ft(),t.screen!=="crash"&&Nt(),typeof t.balance=="number"&&(et=t.balance),t.screen==="lobby"&&"user"in t&&(I=t.user??null,I?nt.value="":et=null),gt=0,Q.hidden=!0,qt(),I&&N.balance===null&&bt!==I&&(bt=I,ot.send("Info")),I||(bt=null)},onNotice(t){t.startsWith("Connected to:")||t.startsWith("Fingerprint:")||t.startsWith("User:")||t.startsWith("Account:")||t.startsWith("Balance:")||(st(t,yt(t)),t.startsWith("Account created")&&(Zt("login"),nt.value="",Gt.focus()),O.disabled=!1,O.textContent=_==="login"?"LOG IN":"CREATE ACCOUNT")},onError(t){st(t,"bad"),O.disabled=!1,O.textContent=_==="login"?"LOG IN":"CREATE ACCOUNT"},onReplaced(){jt.hidden=!1},onPong(){},onOpen(){Q.hidden=!0},onClosed(t){if(jt.hidden!==!1){if(!t){_t.textContent="The connection could not be restored. Reload the page to try again.",Q.hidden=!1;return}gt+=1,_t.textContent=gt>1?"Still trying to reach the game server…":"Trying to reach the game server…",Q.hidden=!1}}});function P(t){ot.send(t)}function $e(){return Date.now()+Kt}function Zt(t){_=t,Me.textContent=t==="login"?"Sign in":"Create account",O.textContent=t==="login"?"LOG IN":"CREATE ACCOUNT",Xt.textContent=t==="login"?"CREATE ACCOUNT":"BACK TO SIGN IN",nt.autocomplete=t==="login"?"current-password":"new-password",V.textContent=""}function Y(t){ge.hidden=t!=="auth",ye.hidden=t!=="lobby",xe.hidden=t!=="game",ke.hidden=t!=="history"}function We(t){Le.textContent=T(t??null)}function qt(){if(!N)return;let t=I;if(t?(Vt.hidden=!1,Te.textContent=String(t)):(Vt.hidden=!0,Yt.textContent="—"),We(et),!t){Y("auth"),V.textContent=N.login_error?String(N.login_error):"",N.screen==="lobby"&&N.connected===!1&&(V.textContent="The game server is not reachable yet. Retrying…");return}switch(N.screen){case"lobby":Y("lobby"),Ie(N);break;case"history":Y("history"),Ht(Se,N);break;case"slots":case"roulette":case"blackjack":case"crash":Y("game"),N.screen==="slots"?ht(q,J,N,P):N.screen==="roulette"?Dt(q,J,N,P):N.screen==="blackjack"?wt(q,J,N,P):vt(q,J,N,P,{now:$e});break;default:Y("auth")}}function Ie(t){Yt.textContent=t.account?String(t.account):"—";let e=["slots","roulette","blackjack","crash"],n=[...t.games??[]].sort((r,s)=>e.indexOf(r[0])-e.indexOf(s[0]));Ee.textContent=`${n.length} ${n.length===1?"game":"games"}`,ft.replaceChildren();for(let[r,s]of n){let a=o("article",`game-card ${r}`);a.append(o("h3",void 0,s||ve[r]||r)),a.append(o("p",void 0,He[r]??"Play a round."));let l=o("button","btn primary wide","PLAY");l.addEventListener("click",()=>{P({Play:{game:r,bet:Ne.get(r)??10}})}),a.append(l),ft.append(a)}n.length===0&&ft.append(o("p","muted","The server did not advertise any games."))}Xt.addEventListener("click",()=>Zt(_==="login"?"register":"login"));Ce.addEventListener("submit",async t=>{t.preventDefault();let e=Gt.value.trim(),n=nt.value;if(!e||!n){V.textContent="Enter a username and a password.";return}O.disabled=!0,O.textContent="Working…",V.textContent="";try{let r=await xt(n);P(_==="login"?{Login:{user:e,credential:r}}:{Register:{user:e,credential:r}})}catch{V.textContent="This browser cannot hash the password (needs a secure context).",O.disabled=!1,O.textContent=_==="login"?"Log in":"Create account"}});x("logout").addEventListener("click",()=>P("Logout"));x("show-history").addEventListener("click",()=>P({History:{limit:50}}));x("history-back").addEventListener("click",()=>P("ShowLobby"));x("reload-replaced").addEventListener("click",()=>location.reload());x("reload-reconnect").addEventListener("click",()=>location.reload());window.addEventListener("resize",()=>{window.clearTimeout(window.__resizeTimer),window.__resizeTimer=window.setTimeout(()=>{N&&qt()},140)});window.setInterval(()=>{I&&ot.ping()},25e3);ot.connect();})();
diff --git a/web/play/index.html b/web/play/index.html
index 7bebe50..f792ae3 100644
--- a/web/play/index.html
+++ b/web/play/index.html
@@ -6,6 +6,7 @@
6<title>Play — żagań.bet</title> 6<title>Play — żagań.bet</title>
7<meta name="description" content="Play slots, roulette, blackjack and crash in your browser."> 7<meta name="description" content="Play slots, roulette, blackjack and crash in your browser.">
8<meta name="color-scheme" content="dark"> 8<meta name="color-scheme" content="dark">
9<meta name="robots" content="noindex, nofollow, noarchive">
9<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' ws: wss:; img-src 'self' data:; base-uri 'none'; form-action 'self'"> 10<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' ws: wss:; img-src 'self' data:; base-uri 'none'; form-action 'self'">
10<link rel="stylesheet" href="style.css"> 11<link rel="stylesheet" href="style.css">
11</head> 12</head>
diff --git a/web/play/src/slots.ts b/web/play/src/slots.ts
index cfc9070..c8af52a 100644
--- a/web/play/src/slots.ts
+++ b/web/play/src/slots.ts
@@ -68,6 +68,7 @@ export function renderSlots(
68 const tile = node("div", "tile", symbol); 68 const tile = node("div", "tile", symbol);
69 tile.classList.add(`sym-${symbol}`); 69 tile.classList.add(`sym-${symbol}`);
70 if (active) tile.classList.add("spinning"); 70 if (active) tile.classList.add("spinning");
71 if (!active && flat.length === 0) tile.classList.add("placeholder");
71 if (!active && (view.win_cells ?? []).includes(index)) tile.classList.add("win"); 72 if (!active && (view.win_cells ?? []).includes(index)) tile.classList.add("win");
72 tiles.push(tile); 73 tiles.push(tile);
73 reel.append(tile); 74 reel.append(tile);
diff --git a/web/play/style.css b/web/play/style.css
index a0e4cca..7f0c2cd 100644
--- a/web/play/style.css
+++ b/web/play/style.css
@@ -311,7 +311,7 @@ h2 { font-size: 18px; }
311.tile.sym-D { color: #4fb8c9; } 311.tile.sym-D { color: #4fb8c9; }
312.tile.sym-S { color: #4ec27b; } 312.tile.sym-S { color: #4ec27b; }
313.tile.win { border-color: var(--gold); background: rgba(224, 182, 74, 0.14); } 313.tile.win { border-color: var(--gold); background: rgba(224, 182, 74, 0.14); }
314.tile.spinning { opacity: 0.35; } 314.tile.spinning, .tile.placeholder { opacity: 0.35; }
315.win-lines { text-align: center; margin-top: 18px; color: var(--dim); font-size: 12px; min-height: 18px; } 315.win-lines { text-align: center; margin-top: 18px; color: var(--dim); font-size: 12px; min-height: 18px; }
316 316
317/* Cards */ 317/* Cards */
@@ -348,13 +348,24 @@ h2 { font-size: 18px; }
348.outcome { margin-top: 20px; padding: 14px 16px; border: 1px solid var(--line); border-radius: var(--radius-md); background: var(--bg); } 348.outcome { margin-top: 20px; padding: 14px 16px; border: 1px solid var(--line); border-radius: var(--radius-md); background: var(--bg); }
349 349
350/* Crash */ 350/* Crash */
351.crash-wrap { position: relative; flex: 1; display: flex; flex-direction: column; min-height: 0; padding-top: 40px; } 351.crash-wrap {
352 position: relative;
353 flex: 1;
354 display: flex;
355 flex-direction: column;
356 min-height: 0;
357 padding: 22px 24px;
358 background: var(--surface);
359 border: 1px solid var(--line);
360 border-radius: var(--radius-lg);
361}
352.crash-wrap > .label { margin-top: 2px; color: var(--dim); } 362.crash-wrap > .label { margin-top: 2px; color: var(--dim); }
353.crash-number { 363.crash-number {
354 font-family: var(--mono); 364 font-family: var(--mono);
355 font-size: clamp(44px, 9vw, 86px); 365 font-size: clamp(40px, 6vw, 68px);
356 line-height: 1; 366 line-height: 1;
357 letter-spacing: -0.04em; 367 letter-spacing: -0.04em;
368 color: var(--dim);
358} 369}
359.crash-number.running { color: var(--gold); } 370.crash-number.running { color: var(--gold); }
360.crash-number.crashed { color: var(--loss); } 371.crash-number.crashed { color: var(--loss); }
@@ -369,11 +380,11 @@ h2 { font-size: 18px; }
369canvas { display: block; width: 100%; } 380canvas { display: block; width: 100%; }
370.roulette-canvas { height: 500px; margin: 80px 0 0; } 381.roulette-canvas { height: 500px; margin: 80px 0 0; }
371.crash-canvas { 382.crash-canvas {
372 height: 500px; 383 height: 460px;
373 margin-top: 14px; 384 margin-top: 10px;
374 background: var(--surface); 385 background: transparent;
375 border: 1px solid var(--line); 386 border: 0;
376 border-radius: var(--radius-md); 387 border-radius: 0;
377} 388}
378.wheel-canvas { 389.wheel-canvas {
379 display: block; 390 display: block;