summaryrefslogtreecommitdiffstats
path: root/web
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-11-08 03:32:24 +0100
committersalmonik <salmonik@tuta.io>2026-11-08 03:32:24 +0100
commitfda24c47a8262388ada92722850a362da65aaa6e (patch)
tree13363fa1461ae4e10087acba562a527b8bde4228 /web
parentv5.0.0: protocol v5 and the browser frontend (diff)
downloadzagan-bet-fda24c47a8262388ada92722850a362da65aaa6e.tar.gz
zagan-bet-fda24c47a8262388ada92722850a362da65aaa6e.zip
web: accept the serialized reels array and stop game loops on navigation
The Rust SlotsView serializes reels as an array of chars, so the browser joins it instead of String(array). The crash chart, reel animation and roulette observer now stop when another screen takes over, and a returning crash screen rebuilds its detached canvas. Action buttons ignore a double click before the server view arrives. Also tightens the gateway session registry with a generation token, caps WebSocket messages at 64 KiB, uses the __Host- cookie prefix on HTTPS, and ships the systemd unit.
Diffstat (limited to 'web')
-rw-r--r--web/play/app.js2
-rw-r--r--web/play/src/blackjack.ts14
-rw-r--r--web/play/src/crash.ts14
-rw-r--r--web/play/src/main.ts6
-rw-r--r--web/play/src/roulette.ts15
-rw-r--r--web/play/src/slots.ts18
6 files changed, 60 insertions, 9 deletions
diff --git a/web/play/app.js b/web/play/app.js
index 3320693..4950c5e 100644
--- a/web/play/app.js
+++ b/web/play/app.js
@@ -1 +1 @@
"use strict";(()=>{function k(e){let n=document.getElementById(e);if(!n)throw new Error(`missing #${e}`);return n}function o(e,n,t){let s=document.createElement(e);return n&&(s.className=n),t!==void 0&&(s.textContent=t),s}function H(e){e.replaceChildren()}function Oe(e){let n=e<0,t=String(Math.abs(Math.trunc(e))),s="";for(let a=0;a<t.length;a+=1)a>0&&(t.length-a)%3===0&&(s+=" "),s+=t[a];return n?`-${s}`:s}function L(e){return e==null||Number.isNaN(e)?"— Kr":`${Oe(e)} Kr`}function me(e){return`${e<0?"-":"+"}${L(Math.abs(e))}`}function J(e,n="info"){let t=document.getElementById("toasts");if(!t||!e)return;let s=o("div",`toast ${n}`,e);for(t.append(s),window.setTimeout(()=>{s.style.opacity="0",s.style.transition="opacity 220ms ease",window.setTimeout(()=>s.remove(),240)},4200);t.children.length>4;)t.firstElementChild?.remove()}function he(e){let n=e.toLowerCase();return e.startsWith("ERROR")||n.includes("failed")||n.includes("lost")||n.includes("invalid")||n.includes("forfeit")?"bad":e.startsWith("Logged in")||e.startsWith("Account created")||n.includes("won")||n.includes("cashed")?"good":"info"}async function fe(e){let n=new TextEncoder().encode(e),t=await crypto.subtle.digest("SHA-256",n);return[...new Uint8Array(t)].map(s=>s.toString(16).padStart(2,"0")).join("")}function O(e,n=10){return e.length<=n*2?e:`${e.slice(0,n)}…${e.slice(-n)}`}function be(e){let n=Date.parse(e.includes("T")?e:e.replace(" ","T")+"Z");return Number.isNaN(n)?e:new Date(n).toLocaleString()}function ge(e){if(e==="?"||!e)return{rank:"?",suit:"",red:!1};let n=e.slice(-1);return{rank:e.slice(0,-1),suit:n==="C"?"♣":n==="D"?"♦":n==="H"?"♥":n==="S"?"♠":n,red:n==="D"||n==="H"}}var j=class{constructor(n,t){this.url=n;this.handlers=t;this.socket=null;this.stopped=!1;this.replaced=!1;this.attempts=0}connect(){if(this.stopped)return;this.clearTimer();let n;try{n=new WebSocket(this.url)}catch{this.scheduleReconnect()||this.handlers.onClosed(!1);return}this.socket=n,n.addEventListener("open",()=>{this.attempts=0,this.handlers.onOpen()}),n.addEventListener("message",t=>{let s;try{s=JSON.parse(String(t.data))}catch{return}switch(s.type){case"hello":this.handlers.onHello(s);break;case"view":this.handlers.onView(s.view);break;case"notice":this.handlers.onNotice(String(s.text??""));break;case"error":this.handlers.onError(String(s.text??""));break;case"replaced":this.replaced=!0,this.handlers.onReplaced();break;case"pong":this.handlers.onPong();break}}),n.addEventListener("close",()=>{if(this.socket===n&&(this.socket=null),this.replaced||this.stopped){this.handlers.onClosed(!1);return}let s=this.scheduleReconnect();this.handlers.onClosed(s)}),n.addEventListener("error",()=>{})}send(n){this.socket?.readyState===WebSocket.OPEN&&this.socket.send(JSON.stringify({type:"command",command:n}))}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 n=Math.min(250*2**(this.attempts-1),4e3);return this.timer=window.setTimeout(()=>this.connect(),n),!0}clearTimer(){this.timer!==void 0&&(window.clearTimeout(this.timer),this.timer=void 0)}};function ze(e){if(e==="?"){let s=o("div","card back");return s.append(o("span","rank","?"),o("span","suit","")),s}let n=ge(e),t=o("div","card");return n.red&&t.classList.add("red"),t.append(o("span","rank",n.rank),o("span","suit",n.suit)),t}function ye(e,n,t){let s=o("div","hand");s.append(o("div","label",e));let a=o("div","cards");for(let d of n)a.append(ze(d));let r=o("div","total");return r.append(document.createTextNode("Total "),o("strong",void 0,String(t))),s.append(a,r),s}function Ve(e,n){let t=String(n?.result??""),s=Number(n?.winnings??0);switch(t){case"blackjack":return`Blackjack — won ${L(s)}`;case"win":return`You win — ${L(s)}`;case"push":return`Push — ${L(s)} 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 e.status==="player"?"Your move.":"Dealing…";default:return"Ready to deal."}}function ke(e,n,t,s){H(e),H(n);let a=t.hand,r=a?.phase==="player"||t.status==="player",d=a?.phase==="done"||t.status==="done",i=!a||d;if(e.append(o("h2",void 0,"Blackjack")),e.append(o("p","note","Six decks. Dealer stands on all 17. Blackjack pays 3:2.")),a)e.append(ye("Dealer",a.dealer??[],Number(a.dealer_total??0))),e.append(ye("You",a.player??[],Number(a.player_total??0)));else{let h=o("div","outcome");h.textContent="Press DEAL to start a hand.",e.append(h)}if(d||a&&!r){let h=o("div","outcome");h.textContent=Ve(t,a),(String(a?.result)==="win"||String(a?.result)==="blackjack"||String(a?.result)==="push")&&h.classList.add("win-text"),e.append(h)}t.error&&e.append(o("p","error-text",String(t.error))),n.append(o("div","label","Blackjack"));let m=Number(a?.bet??t.bet??0),p=t.balance??t.hand?.balance;if(n.append(o("p","note","Hit, stand or double. Forfeiting loses the reserved bet.")),i){let h=o("div","row"),v=o("button","btn small","−10"),y=o("strong","mono",L(m)),w=o("button","btn small","+10");v.addEventListener("click",()=>s({AdjustBet:-10})),w.addEventListener("click",()=>s({AdjustBet:10})),h.append(v,y,w),n.append(o("div","label","Bet"),h)}else{let h=o("div","stat");h.append(o("span","muted","Bet"),o("strong",void 0,L(m))),n.append(h)}if(p!==void 0){let h=o("div","stat");h.append(o("span","muted","Balance"),o("strong",void 0,L(Number(p)))),n.append(h)}let u=o("div","actions");if(r){let h=o("div","row wrap");for(let y of a?.actions??[]){let w=y==="hit"?"HIT":y==="stand"?"STAND":"DOUBLE",E=y==="stand"?o("button","btn primary",w):o("button","btn",w);E.addEventListener("click",()=>{s(y==="hit"?"Hit":y==="stand"?"Stand":"Double")}),h.append(E)}u.append(h);let v=o("button","btn danger wide","Forfeit hand");v.addEventListener("click",()=>{window.confirm("Forfeit this hand? The reserved bet is lost.")&&s("Forfeit")}),u.append(v)}else if(i){let h=o("button","btn primary wide big","DEAL");h.addEventListener("click",()=>s("Deal")),u.append(h)}let g=o("button","btn ghost wide",r?"Leave (forfeits)":"Leave table");g.addEventListener("click",()=>{r&&!window.confirm("Leaving forfeits this hand. Continue?")||s("Leave")}),u.append(g),n.append(u)}var q=Math.LN2/5,R=null,z=null,ve=null,P=null,W,Le=0,Y="";function _e(e,n,t,s){H(e),H(n),P=t,ve=s,e.append(o("h2",void 0,"Crash")),e.append(o("p","note","The multiplier rises from 1.00×. Cash out before it crashes."));let a=o("div","crash-wrap"),r=o("div","crash-number","1.00×"),d=o("div","crash-meta"),i=o("canvas","crash-canvas"),m=o("div","verify","Waiting for a round commitment.");a.append(r,d,i,m),e.append(a),n.append(o("div","label","Crash")),n.append(o("p","note","The server commits to a seed before the round. The seed is revealed when it ends."));let p=o("div","row"),u=o("button","btn small","−10"),g=o("strong","mono","—"),h=o("button","btn small","+10");u.addEventListener("click",()=>P?.({AdjustBet:-10})),h.addEventListener("click",()=>P?.({AdjustBet:10})),p.append(u,g,h),n.append(o("div","label","Bet"),p);let v=o("strong","mono","—"),y=o("strong","mono","—");for(let[M,N]of[["Potential",v],["Balance",y]]){let b=o("div","stat");b.append(o("span","muted",M),N),n.append(b)}let w=o("p","note","Ready."),E=o("p","note",""),S=o("p","error-text");n.append(w,E,S);let l=o("div","actions"),c=o("button","btn primary wide big","START"),f=o("button","btn ghost wide","Leave table");return c.addEventListener("click",()=>{z?.status==="running"?P?.("CashOut"):P?.("Start")}),f.addEventListener("click",()=>{z?.status==="running"&&!window.confirm("Forfeit this round? The bet is lost.")||P?.("Leave")}),l.append(c,f),n.append(l),{board:e,side:n,number:r,meta:d,verify:m,canvas:i,bet:g,potential:v,balance:y,status:w,lastCrash:E,error:S,primary:c,leave:f,minus:u,plus:h}}function we(e,n,t,s,a){(!R||R.board!==e)&&(R=_e(e,n,s,a),Y="");let r=R;P=s,ve=a,z=t,t.status!==Y&&((t.status==="starting"||t.status==="running")&&Y!=="running"&&(r.verify.className="verify",r.verify.textContent="Round committed; seed hidden until the round ends."),Y=String(t.status));let d=t.status==="running",i=t.status==="starting",m=d||i;r.bet.textContent=L(Number(t.bet??0)),r.potential.textContent=L(Number(t.potential??t.bet??0)),r.balance.textContent=t.balance===null||t.balance===void 0?"—":L(Number(t.balance)),r.error.textContent=t.error?String(t.error):"",r.minus.disabled=m,r.plus.disabled=m,r.primary.textContent=d?"CASH OUT":i?"STARTING…":"START",r.primary.disabled=i,r.primary.className="btn primary wide big",r.leave.textContent=d?"Forfeit round":"Leave table",d?(r.status.textContent="Rising — cash out before the crash.",r.status.className="note win-text"):i?(r.status.textContent="Starting the round…",r.status.className="note"):t.status==="cashed"?(r.status.textContent=`Cashed out at ${Number(t.multiplier??0).toFixed(2)}× for ${L(Number(t.payout??0))}.`,r.status.className="note win-text"):t.status==="crashed"?(r.status.textContent=`Crashed at ${Number(t.crash_point??t.multiplier??0).toFixed(2)}×. The bet is lost.`,r.status.className="note error-text"):t.status==="forfeit"?(r.status.textContent="Round forfeited.",r.status.className="note error-text"):(r.status.textContent="Ready. Start a round when you want.",r.status.className="note"),t.crash_point!==null&&t.crash_point!==void 0&&t.status==="crashed"&&(r.lastCrash.textContent=`Last crash: ${Number(t.crash_point).toFixed(2)}×`);let p=t.commit?String(t.commit):"";if(r.meta.textContent=p?`commit ${O(p,12)}`:"no commitment yet",t.seed&&p){let u=++Le,g=String(t.seed),h=Number(t.crash_point??0);Xe(g,p,h).then(([v,y])=>{if(u!==Le||!R)return;let w=v&&y;R.verify.className=`verify ${w?"ok":"bad"}`,R.verify.textContent=w?`Seed ${O(g,12)} · sha256 matches commit · crash point recomputed`:`VERIFICATION FAILED · seed ${O(g,12)} · commit ${O(p,12)}`})}else!m&&!p&&(r.verify.className="verify",r.verify.textContent="Waiting for a round commitment.");if(W!==void 0&&(cancelAnimationFrame(W),W=void 0),d){let u=()=>{if(!R||z?.status!=="running"){W=void 0;return}xe(R,z,a),W=requestAnimationFrame(u)};W=requestAnimationFrame(u)}else xe(r,t,a)}function je(e,n){if(e.status==="running"){let t=Math.max(0,(n.now()-Number(e.started_at??0))/1e3),s=Math.min(1e3,Math.exp(q*t));return Math.max(s,Number(e.multiplier??1))}return e.status==="cashed"?Number(e.multiplier??1):e.status==="crashed"?Number(e.crash_point??e.multiplier??1):1}function xe(e,n,t){let s=je(n,t);e.number.textContent=`${s.toFixed(2)}×`,e.number.className=`crash-number ${n.status==="running"?"running":n.status==="cashed"?"cashed":n.status==="crashed"?"crashed":""}`,e.potential.textContent=L(Math.floor(Number(n.bet??0)*s)),Ye(e.canvas,n,s,t)}function Ye(e,n,t,s){let a=e.clientWidth||600,r=e.clientHeight||360,d=window.devicePixelRatio||1;(e.width!==Math.round(a*d)||e.height!==Math.round(r*d))&&(e.width=Math.round(a*d),e.height=Math.round(r*d));let i=e.getContext("2d");if(!i)return;i.setTransform(d,0,0,d,0,0),i.clearRect(0,0,a,r);let m=54,p=16,u=18,g=34,h=Math.max(1,a-m-p),v=Math.max(1,r-u-g),y=n.status==="running"?Math.max(.25,(s.now()-Number(n.started_at??0))/1e3):t>1?Math.log(t)/q:1,w=Math.max(5,y*1.08),E=Math.max(2,Math.ceil(t*1.15*2)/2),S=b=>m+b/w*h,l=b=>u+v-(b-1)/(E-1)*v;i.strokeStyle="#242e38",i.fillStyle="#8a98a6",i.font="11px ui-monospace, monospace",i.lineWidth=1;for(let b=0;b<=4;b+=1){let T=1+(E-1)*b/4,C=l(T);i.beginPath(),i.moveTo(m,C),i.lineTo(a-p,C),i.stroke(),i.fillText(`${T.toFixed(1)}×`,6,C+4)}for(let b=0;b<=4;b+=1){let T=w*b/4,C=S(T);i.beginPath(),i.moveTo(C,u),i.lineTo(C,u+v),i.strokeStyle="#1b232c",i.stroke(),i.fillStyle="#5b6773",i.fillText(`${T.toFixed(0)}s`,C-10,r-12)}let c=n.status==="crashed"?"#e05a5a":n.status==="cashed"?"#4ec27b":"#e0b64a";i.beginPath();let f=180;for(let b=0;b<=f;b+=1){let T=y*b/f,C=Math.min(E,Math.exp(q*T)),ue=S(T),pe=l(C);b===0?i.moveTo(ue,pe):i.lineTo(ue,pe)}i.strokeStyle=c,i.lineWidth=2.4,i.stroke();let M=S(y),N=l(Math.min(E,t));i.beginPath(),i.arc(M,N,4.5,0,Math.PI*2),i.fillStyle=c,i.fill(),i.fillStyle=c,i.font="600 12px ui-monospace, monospace",i.fillText(`${t.toFixed(2)}×`,Math.min(M+8,a-70),Math.max(u+12,N-10))}async function Xe(e,n,t){let s=new Uint8Array(e.length/2);for(let u=0;u<s.length;u+=1)s[u]=Number.parseInt(e.slice(u*2,u*2+2),16);let a=await crypto.subtle.digest("SHA-256",s),d=[...new Uint8Array(a)].map(u=>u.toString(16).padStart(2,"0")).join("")===n.toLowerCase(),i=0;for(let u=0;u<8;u+=1)i=i*256+s[u];i/=2**64;let m=Math.max(1,Math.min(1e3,(1-.01)/(1-i))),p=Math.abs(m-t)<1e-6;return[d,p]}function Ge(e){let n=e.details??{};switch(e.game){case"slots":return String(n.win_type??"spin");case"roulette":return`${n.number??"?"} ${n.color??""}`.trim();case"blackjack":return String(n.result??"");case"crash":{let t=String(n.result??""),s=n.crash_point;return s==null?t:`${t} @ ${Number(s).toFixed(2)}×`}default:return""}}function Te(e,n){H(e);let t=n.records??[];if(t.length===0){let m=o("div","board");m.append(o("h2",void 0,"No plays yet"),o("p","muted","Your settled plays will appear here.")),e.append(m);return}let s=o("div","table-wrap"),a=o("table"),r=o("thead"),d=o("tr");for(let m of["Time","Game","Bet","Result","Net","Balance"])d.append(o("th",void 0,m));r.append(d);let i=o("tbody");for(let m of t){let p=o("tr");p.append(o("td","mono",be(String(m.at??"")))),p.append(o("td",void 0,String(m.game??""))),p.append(o("td","num",L(Number(m.bet??0)))),p.append(o("td","muted",Ge(m)));let u=Number(m.winnings??0)-Number(m.bet??0),g=o("td",`num ${u>0?"pos":u<0?"neg":""}`,u===0?"0 Kr":me(u));p.append(g),p.append(o("td","num",L(Number(m.balance_after??0)))),i.append(p)}a.append(r,i),s.append(a),e.append(s)}var Ke="#b0313a",Ue="#151b21",Ee="#106b45",Ze="#0d5c39",Je="#09472c",qe="#1f7d52",Qe="#ecf7f0",et="#e0b64a";function tt(e){return e===0?"green":[1,3,5,7,9,12,14,16,18,19,21,23,25,27,30,32,34,36].includes(e)?"red":"black"}function nt(e,n){let s=Math.max(38,e*.055),a=Math.max(34,e*.05),r=Math.max(30,n*.07),d=6,i=e-28-s-a,m=i/12,p=(n-28-r*2-d*2)/3,u=14+s,g=14,h=g+p*3+d,v=h+r+d,y=[];for(let l=0;l<12;l+=1)for(let c=0;c<3;c+=1){let f=l*3+(3-c);y.push({x:u+l*m,y:g+c*p,w:m,h:p,target:{bet:"straight",number:f},label:String(f),kind:"number",number:f})}y.push({x:14,y:g,w:s,h:p*3,target:{bet:"straight",number:0},label:"0",kind:"zero",number:0});for(let l=0;l<3;l+=1){let c=2-l;y.push({x:u+i,y:g+l*p,w:a,h:p,target:{bet:"column",index:c},label:"2:1",kind:"column"})}for(let l=0;l<3;l+=1)y.push({x:u+i/3*l,y:h,w:i/3,h:r,target:{bet:"dozen",index:l},label:["1st 12","2nd 12","3rd 12"][l],kind:"dozen"});[{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((l,c)=>{y.push({x:u+i/6*c,y:v,w:i/6,h:r,target:{bet:"outside",kind:l.kind},label:l.label,kind:"outside"})});let E=[],S=l=>{let c=y.find(f=>f.kind==="number"&&f.number===l);return c?{x:c.x+c.w/2,y:c.y+c.h/2}:null};for(let l=0;l<11;l+=1)for(let c=0;c<2;c+=1){let f=l*3+(3-c);E.push({x:u+(l+1)*m,y:g+(c+1)*p,radius:12,rank:0,target:{bet:"corner",numbers:[f,f-1,f+3,f+2]}})}for(let l=0;l<12;l+=1)for(let c=0;c<2;c+=1){let f=l*3+(3-c),M=S(f);M&&E.push({x:M.x,y:g+(c+1)*p,radius:10,rank:1,target:{bet:"split",a:f,b:f-1}})}for(let l=0;l<11;l+=1)for(let c=0;c<3;c+=1){let f=l*3+(3-c);E.push({x:u+(l+1)*m,y:g+c*p+p/2,radius:10,rank:1,target:{bet:"split",a:f,b:f+3}})}for(let l=0;l<3;l+=1){let c=3-l;E.push({x:u,y:g+l*p+p/2,radius:10,rank:1,target:{bet:"split",a:0,b:c}})}return{width:e,height:n,cellW:m,cellH:p,zeroX:14,zeroW:s,gridX:u,gridY:g,gridW:i,columnX:u+i,dozensY:h,outsideY:v,outsideH:r,zones:y,hotspots:E}}function Me(e,n,t,s,a,r){let d=Math.min(r,s/2,a/2);e.beginPath(),e.moveTo(n+d,t),e.arcTo(n+s,t,n+s,t+a,d),e.arcTo(n+s,t+a,n,t+a,d),e.arcTo(n,t+a,n,t,d),e.arcTo(n,t,n+s,t,d),e.closePath()}function Ce(e,n){let t=e.clientWidth||800,s=e.clientHeight||540,a=window.devicePixelRatio||1;(e.width!==Math.round(t*a)||e.height!==Math.round(s*a))&&(e.width=Math.round(t*a),e.height=Math.round(s*a));let r=e.getContext("2d"),d=nt(t,s);if(!r)return d;r.setTransform(a,0,0,a,0,0),r.clearRect(0,0,t,s),r.fillStyle=Je,Me(r,0,0,t,s,10),r.fill();for(let i of d.zones){let m=Ze;i.kind==="number"?m=i.number===0?Ee:tt(i.number)==="red"?Ke:Ue:i.kind==="zero"&&(m=Ee),r.fillStyle=m,r.strokeStyle=qe,r.lineWidth=1,Me(r,i.x+1,i.y+1,i.w-2,i.h-2,3),r.fill(),r.stroke(),r.fillStyle=Qe,r.textAlign="center",r.textBaseline="middle",i.kind==="number"||i.kind==="zero"?r.font=`700 ${Math.min(24,d.cellH*.3)}px ui-monospace, monospace`:r.font=`600 ${Math.min(12,d.outsideH*.4)}px ui-monospace, monospace`,r.fillText(i.label,i.x+i.w/2,i.y+i.h/2)}return st(r,d,n),d}function Se(e){return JSON.stringify(e)}function ot(e,n){let t=Se(n),s=e.zones.find(a=>Se(a.target)===t);if(s)return{x:s.x+s.w/2,y:s.y+s.h/2};if(n?.bet==="split"||n?.bet==="corner"){let a=n.bet==="split"?[n.a,n.b]:n.numbers,r=e.zones.filter(d=>(d.kind==="number"||d.kind==="zero")&&a.includes(d.number));if(r.length>0)return{x:r.reduce((d,i)=>d+i.x+i.w/2,0)/r.length,y:r.reduce((d,i)=>d+i.y+i.h/2,0)/r.length}}return null}function st(e,n,t){let s=t.bets?.chips??[];for(let a of s){let r=ot(n,a.target);if(!r)continue;let d=Math.max(12,Math.min(18,n.cellW*.26));e.beginPath(),e.arc(r.x,r.y,d,0,Math.PI*2),e.fillStyle=et,e.fill(),e.strokeStyle="#7a5a12",e.lineWidth=2,e.stroke(),e.fillStyle="#221a05",e.textAlign="center",e.textBaseline="middle",e.font=`700 ${d>15?11:10}px ui-monospace, monospace`,e.fillText(String(a.amount),r.x,r.y+.5)}}function Ne(e,n,t,s){H(e),H(n);let a=t.phase==="spinning";e.append(o("h2",void 0,"Roulette")),e.append(o("p","note","European single-zero wheel. Click a number, line or corner to add a chip; right-click takes one back."));let r=o("div","roulette-wrap"),d=o("canvas","roulette-canvas");if(r.append(d),t.phase==="result"&&t.number!==null&&t.number!==void 0){let l=o("div",`wheel-result ${t.color??""}`);l.append(o("div","label","Result")),l.append(o("div","num",String(t.number)));let c=Number(t.winnings??0);l.append(o("div","note",c>0?`Won ${L(c)}`:"No win")),r.append(l)}e.append(r);let m=Ce(d,t),p=(l,c)=>{let f=d.getBoundingClientRect(),M=l-f.left,N=c-f.top,b=null;for(let T of m.hotspots){let C=Math.hypot(M-T.x,N-T.y);C>T.radius||(!b||T.rank<b.rank||T.rank===b.rank&&C<Math.hypot(M-b.x,N-b.y))&&(b=T)}if(b)return{x:b.x-b.radius,y:b.y-b.radius,w:b.radius*2,h:b.radius*2,target:b.target,label:"",kind:"number"};for(let T=m.zones.length-1;T>=0;T-=1){let C=m.zones[T];if(M>=C.x&&M<=C.x+C.w&&N>=C.y&&N<=C.y+C.h)return C}return null};d.addEventListener("click",l=>{if(a)return;let c=p(l.clientX,l.clientY);c&&s({PlaceBetAt:c.target})}),d.addEventListener("contextmenu",l=>{if(l.preventDefault(),a)return;let c=p(l.clientX,l.clientY);c&&s({RemoveBetAt:c.target})}),d.addEventListener("mousemove",l=>{let c=p(l.clientX,l.clientY);d.style.cursor=c&&!a?"pointer":"default"}),new ResizeObserver(()=>{m=Ce(d,t)}).observe(d),n.append(o("div","label","Roulette")),n.append(o("p","note","Chips sit on the felt. The total is reserved when you spin.")),n.append(o("div","label","Chip"));let u=o("div","chip-row");for(let l of[10,25,50,100,500]){let c=o("button",`chip-btn ${Number(t.chip)===l?"active":""}`,String(l));c.disabled=a,c.addEventListener("click",()=>s({SetChip:l})),u.append(c)}n.append(u);let g=(t.bets?.chips??[]).reduce((l,c)=>l+Number(c.amount??0),0),h=o("div","stat");if(h.append(o("span","muted","On the felt"),o("strong","mono",L(g))),n.append(h),t.balance!==null&&t.balance!==void 0){let l=o("div","stat");l.append(o("span","muted","Balance"),o("strong","mono",L(Number(t.balance)))),n.append(l)}t.error&&n.append(o("p","error-text",String(t.error)));let v=o("p","note");if(a)v.textContent="Spinning…";else if(t.phase==="result"){let l=Number(t.winnings??0);v.textContent=`${t.number} ${t.color} · ${l>0?`won ${L(l)}`:"no win"}`,v.className=l>0?"note win-text":"note"}else v.textContent=g>0?"Ready to spin.":"Place at least one chip.";n.append(v);let y=o("div","actions"),w=o("button","btn primary wide big",a?"SPINNING…":"SPIN WHEEL");w.disabled=a||g===0,w.addEventListener("click",()=>s("Spin"));let E=o("button","btn wide","Clear bets");E.disabled=a||g===0,E.addEventListener("click",()=>s("ClearBets"));let S=o("button","btn ghost wide","Leave table");S.disabled=a,S.addEventListener("click",()=>{a||s("Leave")}),y.append(w,E,S),n.append(y)}var Q=5,ee=3,D="CLOGDS",rt=["top","middle","bottom","V","inverted V"],oe=0,I,te=null,ne=null;function at(){oe=Date.now()+900}function it(e){return e.phase==="spinning"||Date.now()<oe}function se(e,n,t,s){te=t,ne=e,I!==void 0&&(window.clearInterval(I),I=void 0);let a=it(t);H(e),H(n);let r=o("h2",void 0,"Slots"),d=o("p","note","Match three or more symbols from the left. Five paylines.");e.append(r,d);let i=o("div","reels"),m=[],p=String(t.reels??"");for(let c=0;c<Q;c+=1){let f=o("div","reel");for(let M=0;M<ee;M+=1){let N=c*ee+M,b=a?D[(N*3+Math.floor(Date.now()/70))%D.length]:p.length===Q*ee?p[N]:p.length===Q?p[c]:D[(N*5+c)%D.length],T=o("div","tile",b);T.classList.add(`sym-${b}`),a&&T.classList.add("spinning"),!a&&(t.win_cells??[]).includes(N)&&T.classList.add("win"),m.push(T),f.append(T)}i.append(f)}e.append(i);let u=o("div","win-lines");if(!a&&t.phase==="result"){let c=Number(t.winnings??0);if(c>0){let f=(t.win_lines??[]).map(M=>`${M} (${rt[M-1]??"line"})`).join(" · ");u.textContent=`${t.win_type==="mega"?"MEGA WIN":t.win_type==="big"?"BIG WIN":"WIN"} ${L(c)}${f?` · ${f}`:""}`,u.classList.add("win-text")}else u.textContent="No win — spin again."}else a?u.textContent="Spinning…":u.textContent="Pick a bet and spin.";if(e.append(u),a){let c=0;I=window.setInterval(()=>{c+=1;for(let f=0;f<m.length;f+=1){let M=D[(f*3+c)%D.length];m[f].textContent=M,m[f].className=`tile spinning sym-${M}`}Date.now()>=oe&&te&&ne&&(window.clearInterval(I),I=void 0,se(ne,n,te,s))},70)}n.append(o("div","label","Slots")),n.append(o("p","note","5 reels × 3 rows, 5 paylines."));let g=o("div","row"),h=o("button","btn small","−10"),v=o("strong","mono",L(Number(t.bet??0))),y=o("button","btn small","+10");h.disabled=a,y.disabled=a,h.addEventListener("click",()=>s({AdjustBet:-10})),y.addEventListener("click",()=>s({AdjustBet:10})),g.append(h,v,y),n.append(o("div","label","Bet"),g),t.error&&n.append(o("p","error-text",String(t.error)));let w=o("p","note");if(a)w.textContent="Spinning…";else if(t.phase==="result"){let c=Number(t.winnings??0);w.textContent=c>0?`Won ${L(c)}`:"No win this spin.",c>0&&w.classList.add("win-text")}else w.textContent="Ready when you are.";n.append(w);let E=o("div","actions"),S=o("button","btn primary wide big",a?"SPINNING…":"SPIN");S.disabled=a,S.addEventListener("click",()=>{at(),s("Spin")});let l=o("button","btn ghost wide","Leave machine");l.disabled=a,l.addEventListener("click",()=>s("Leave")),E.append(S,l),n.append(E)}var lt=k("auth-screen"),ct=k("lobby-screen"),dt=k("game-screen"),ut=k("history-screen"),He=k("session-bar"),pt=k("account-name"),mt=k("account-balance"),re=k("game-grid"),X=k("game-board"),G=k("game-side"),ht=k("game-title"),$e=k("game-badge"),ft=k("history-body"),bt=k("auth-form"),Ae=k("auth-user"),U=k("auth-pass"),F=k("auth-error"),A=k("auth-submit"),Pe=k("tab-login"),We=k("tab-register"),K=k("reconnect-overlay"),Be=k("reconnect-message"),Re=k("replaced-overlay"),x=null,$=null,ce=null,_="login",ae=null,De=0,ie=0,le=new Map([["slots",10],["roulette",10],["blackjack",10],["crash",10]]),Ie={slots:"Slots",roulette:"Roulette",blackjack:"Blackjack",crash:"Crash"},gt={slots:"Five reels, five paylines. Match symbols from the left.",roulette:"European single-zero wheel with inside and outside bets.",blackjack:"Six-deck shoe. Blackjack pays 3:2.",crash:"A rising multiplier. Cash out before it crashes."},yt=location.pathname.includes("/play/")?location.pathname.slice(0,location.pathname.indexOf("/play/")):"",kt=`${location.protocol==="https:"?"wss":"ws"}://${location.host}${yt}/ws`,Z=new j(kt,{onHello(e){De=e.time-Date.now(),$=e.user??null,$e.textContent=`v${e.version}`},onView(e){x=e,typeof e.balance=="number"&&(ce=e.balance),e.screen==="lobby"&&"user"in e&&($=e.user??null,$?U.value="":ce=null),ie=0,K.hidden=!0,Fe(),$&&x.balance===null&&ae!==$&&(ae=$,Z.send("Info")),$||(ae=null)},onNotice(e){e.startsWith("Connected to:")||e.startsWith("Fingerprint:")||e.startsWith("User:")||e.startsWith("Account:")||e.startsWith("Balance:")||(J(e,he(e)),e.startsWith("Account created")&&(de("login"),U.value="",Ae.focus()),A.disabled=!1,A.textContent=_==="login"?"Log in":"Create account")},onError(e){J(e,"bad"),A.disabled=!1,A.textContent=_==="login"?"Log in":"Create account"},onReplaced(){Re.hidden=!1},onPong(){},onOpen(){K.hidden=!0},onClosed(e){if(Re.hidden!==!1){if(!e){Be.textContent="The connection could not be restored. Reload the page to try again.",K.hidden=!1;return}ie+=1,Be.textContent=ie>1?"Still trying to reach the game server…":"Trying to reach the game server…",K.hidden=!1}}});function B(e){Z.send(e)}function Lt(){return Date.now()+De}function de(e){_=e,Pe.classList.toggle("active",e==="login"),We.classList.toggle("active",e==="register"),A.textContent=e==="login"?"Log in":"Create account",U.autocomplete=e==="login"?"current-password":"new-password",F.textContent=""}function V(e){lt.hidden=e!=="auth",ct.hidden=e!=="lobby",dt.hidden=e!=="game",ut.hidden=e!=="history"}function xt(e){mt.textContent=L(e??null)}function Fe(){if(!x)return;let e=$;if(e?(He.hidden=!1,pt.textContent=String(e)):He.hidden=!0,xt(ce),!e){V("auth"),F.textContent=x.login_error?String(x.login_error):"",x.screen==="lobby"&&x.connected===!1&&(F.textContent="The game server is not reachable yet. Retrying…");return}switch(x.screen){case"lobby":V("lobby"),wt(x);break;case"history":V("history"),Te(ft,x);break;case"slots":case"roulette":case"blackjack":case"crash":V("game"),ht.textContent=Ie[x.screen]??x.screen,$e.textContent=vt(x),x.screen==="slots"?se(X,G,x,B):x.screen==="roulette"?Ne(X,G,x,B):x.screen==="blackjack"?ke(X,G,x,B):we(X,G,x,B,{now:Lt});break;default:V("auth")}}function vt(e){return e.screen==="crash"||e.screen==="blackjack"?String(e.status??""):e.screen==="slots"||e.screen==="roulette"?String(e.phase??""):""}function wt(e){re.replaceChildren();let n=e.games??[];for(let[t,s]of n){let a=o("article",`game-card ${t}`);a.append(o("h3",void 0,s||Ie[t]||t)),a.append(o("p",void 0,gt[t]??"Play a round."));let r=o("div","row wrap"),d=o("label","field");d.style.flex="1",d.append(o("span","label","Bet"));let i=o("input","input bet-input");i.type="number",i.min="1",i.step="10",i.value=String(le.get(t)??10),i.addEventListener("change",()=>{let p=Math.max(1,Math.floor(Number(i.value)||1));i.value=String(p),le.set(t,p)}),d.append(i);let m=o("button","btn primary",t==="roulette"?"Open table":"Play");m.addEventListener("click",()=>{let p=Math.max(1,Math.floor(Number(i.value)||1));le.set(t,p),B({Play:{game:t,bet:p}})}),r.append(d,m),a.append(r),re.append(a)}n.length===0&&re.append(o("p","muted","The server did not advertise any games."))}Pe.addEventListener("click",()=>de("login"));We.addEventListener("click",()=>de("register"));bt.addEventListener("submit",async e=>{e.preventDefault();let n=Ae.value.trim(),t=U.value;if(!n||!t){F.textContent="Enter a username and a password.";return}A.disabled=!0,A.textContent="Working…",F.textContent="";try{let s=await fe(t);B(_==="login"?{Login:{user:n,credential:s}}:{Register:{user:n,credential:s}})}catch{F.textContent="This browser cannot hash the password (needs a secure context).",A.disabled=!1,A.textContent=_==="login"?"Log in":"Create account"}});k("logout").addEventListener("click",()=>B("Logout"));k("show-history").addEventListener("click",()=>B({History:{limit:50}}));k("refresh-lobby").addEventListener("click",()=>B("Info"));k("history-back").addEventListener("click",()=>B("ShowLobby"));k("leave-game").addEventListener("click",()=>{!x||(x.screen==="blackjack"&&x.status==="player"||x.screen==="crash"&&x.status==="running")&&!window.confirm("Leaving now forfeits the active bet. Continue?")||B("Leave")});k("reload-replaced").addEventListener("click",()=>location.reload());k("reload-reconnect").addEventListener("click",()=>location.reload());window.addEventListener("resize",()=>{window.clearTimeout(window.__resizeTimer),window.__resizeTimer=window.setTimeout(()=>{x&&Fe()},140)});window.setInterval(()=>{$&&Z.ping()},25e3);Z.connect();})(); "use strict";(()=>{function k(e){let n=document.getElementById(e);if(!n)throw new Error(`missing #${e}`);return n}function o(e,n,t){let s=document.createElement(e);return n&&(s.className=n),t!==void 0&&(s.textContent=t),s}function H(e){e.replaceChildren()}function Ke(e){let n=e<0,t=String(Math.abs(Math.trunc(e))),s="";for(let a=0;a<t.length;a+=1)a>0&&(t.length-a)%3===0&&(s+=" "),s+=t[a];return n?`-${s}`:s}function L(e){return e==null||Number.isNaN(e)?"— Kr":`${Ke(e)} Kr`}function he(e){return`${e<0?"-":"+"}${L(Math.abs(e))}`}function ee(e,n="info"){let t=document.getElementById("toasts");if(!t||!e)return;let s=o("div",`toast ${n}`,e);for(t.append(s),window.setTimeout(()=>{s.style.opacity="0",s.style.transition="opacity 220ms ease",window.setTimeout(()=>s.remove(),240)},4200);t.children.length>4;)t.firstElementChild?.remove()}function be(e){let n=e.toLowerCase();return e.startsWith("ERROR")||n.includes("failed")||n.includes("lost")||n.includes("invalid")||n.includes("forfeit")?"bad":e.startsWith("Logged in")||e.startsWith("Account created")||n.includes("won")||n.includes("cashed")?"good":"info"}async function ge(e){let n=new TextEncoder().encode(e),t=await crypto.subtle.digest("SHA-256",n);return[...new Uint8Array(t)].map(s=>s.toString(16).padStart(2,"0")).join("")}function z(e,n=10){return e.length<=n*2?e:`${e.slice(0,n)}…${e.slice(-n)}`}function ye(e){let n=Date.parse(e.includes("T")?e:e.replace(" ","T")+"Z");return Number.isNaN(n)?e:new Date(n).toLocaleString()}function ke(e){if(e==="?"||!e)return{rank:"?",suit:"",red:!1};let n=e.slice(-1);return{rank:e.slice(0,-1),suit:n==="C"?"♣":n==="D"?"♦":n==="H"?"♥":n==="S"?"♠":n,red:n==="D"||n==="H"}}var j=class{constructor(n,t){this.url=n;this.handlers=t;this.socket=null;this.stopped=!1;this.replaced=!1;this.attempts=0}connect(){if(this.stopped)return;this.clearTimer();let n;try{n=new WebSocket(this.url)}catch{this.scheduleReconnect()||this.handlers.onClosed(!1);return}this.socket=n,n.addEventListener("open",()=>{this.attempts=0,this.handlers.onOpen()}),n.addEventListener("message",t=>{let s;try{s=JSON.parse(String(t.data))}catch{return}switch(s.type){case"hello":this.handlers.onHello(s);break;case"view":this.handlers.onView(s.view);break;case"notice":this.handlers.onNotice(String(s.text??""));break;case"error":this.handlers.onError(String(s.text??""));break;case"replaced":this.replaced=!0,this.handlers.onReplaced();break;case"pong":this.handlers.onPong();break}}),n.addEventListener("close",()=>{if(this.socket===n&&(this.socket=null),this.replaced||this.stopped){this.handlers.onClosed(!1);return}let s=this.scheduleReconnect();this.handlers.onClosed(s)}),n.addEventListener("error",()=>{})}send(n){this.socket?.readyState===WebSocket.OPEN&&this.socket.send(JSON.stringify({type:"command",command:n}))}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 n=Math.min(250*2**(this.attempts-1),4e3);return this.timer=window.setTimeout(()=>this.connect(),n),!0}clearTimer(){this.timer!==void 0&&(window.clearTimeout(this.timer),this.timer=void 0)}};var Le=0;function te(){return Date.now()-Le<400?!1:(Le=Date.now(),!0)}function Ue(e){if(e==="?"){let s=o("div","card back");return s.append(o("span","rank","?"),o("span","suit","")),s}let n=ke(e),t=o("div","card");return n.red&&t.classList.add("red"),t.append(o("span","rank",n.rank),o("span","suit",n.suit)),t}function xe(e,n,t){let s=o("div","hand");s.append(o("div","label",e));let a=o("div","cards");for(let d of n)a.append(Ue(d));let r=o("div","total");return r.append(document.createTextNode("Total "),o("strong",void 0,String(t))),s.append(a,r),s}function Ze(e,n){let t=String(n?.result??""),s=Number(n?.winnings??0);switch(t){case"blackjack":return`Blackjack — won ${L(s)}`;case"win":return`You win — ${L(s)}`;case"push":return`Push — ${L(s)} 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 e.status==="player"?"Your move.":"Dealing…";default:return"Ready to deal."}}function we(e,n,t,s){H(e),H(n);let a=t.hand,r=a?.phase==="player"||t.status==="player",d=a?.phase==="done"||t.status==="done",i=!a||d;if(e.append(o("h2",void 0,"Blackjack")),e.append(o("p","note","Six decks. Dealer stands on all 17. Blackjack pays 3:2.")),a)e.append(xe("Dealer",a.dealer??[],Number(a.dealer_total??0))),e.append(xe("You",a.player??[],Number(a.player_total??0)));else{let f=o("div","outcome");f.textContent="Press DEAL to start a hand.",e.append(f)}if(d||a&&!r){let f=o("div","outcome");f.textContent=Ze(t,a),(String(a?.result)==="win"||String(a?.result)==="blackjack"||String(a?.result)==="push")&&f.classList.add("win-text"),e.append(f)}t.error&&e.append(o("p","error-text",String(t.error))),n.append(o("div","label","Blackjack"));let m=Number(a?.bet??t.bet??0),p=t.balance??t.hand?.balance;if(n.append(o("p","note","Hit, stand or double. Forfeiting loses the reserved bet.")),i){let f=o("div","row"),w=o("button","btn small","−10"),y=o("strong","mono",L(m)),v=o("button","btn small","+10");w.addEventListener("click",()=>s({AdjustBet:-10})),v.addEventListener("click",()=>s({AdjustBet:10})),f.append(w,y,v),n.append(o("div","label","Bet"),f)}else{let f=o("div","stat");f.append(o("span","muted","Bet"),o("strong",void 0,L(m))),n.append(f)}if(p!==void 0){let f=o("div","stat");f.append(o("span","muted","Balance"),o("strong",void 0,L(Number(p)))),n.append(f)}let u=o("div","actions");if(r){let f=o("div","row wrap");for(let y of a?.actions??[]){let v=y==="hit"?"HIT":y==="stand"?"STAND":"DOUBLE",E=y==="stand"?o("button","btn primary",v):o("button","btn",v);E.addEventListener("click",()=>{te()&&s(y==="hit"?"Hit":y==="stand"?"Stand":"Double")}),f.append(E)}u.append(f);let w=o("button","btn danger wide","Forfeit hand");w.addEventListener("click",()=>{te()&&window.confirm("Forfeit this hand? The reserved bet is lost.")&&s("Forfeit")}),u.append(w)}else if(i){let f=o("button","btn primary wide big","DEAL");f.addEventListener("click",()=>{te()&&s("Deal")}),u.append(f)}let g=o("button","btn ghost wide",r?"Leave (forfeits)":"Leave table");g.addEventListener("click",()=>{r&&!window.confirm("Leaving forfeits this hand. Continue?")||s("Leave")}),u.append(g),n.append(u)}var ne=Math.LN2/5,R=null,I=null,Me=null,W=null,$,ve=0,Y="",Te=0;function Je(e,n,t,s){H(e),H(n),W=t,Me=s,e.append(o("h2",void 0,"Crash")),e.append(o("p","note","The multiplier rises from 1.00×. Cash out before it crashes."));let a=o("div","crash-wrap"),r=o("div","crash-number","1.00×"),d=o("div","crash-meta"),i=o("canvas","crash-canvas"),m=o("div","verify","Waiting for a round commitment.");a.append(r,d,i,m),e.append(a),n.append(o("div","label","Crash")),n.append(o("p","note","The server commits to a seed before the round. The seed is revealed when it ends."));let p=o("div","row"),u=o("button","btn small","−10"),g=o("strong","mono","—"),f=o("button","btn small","+10");u.addEventListener("click",()=>W?.({AdjustBet:-10})),f.addEventListener("click",()=>W?.({AdjustBet:10})),p.append(u,g,f),n.append(o("div","label","Bet"),p);let w=o("strong","mono","—"),y=o("strong","mono","—");for(let[M,N]of[["Potential",w],["Balance",y]]){let b=o("div","stat");b.append(o("span","muted",M),N),n.append(b)}let v=o("p","note","Ready."),E=o("p","note",""),S=o("p","error-text");n.append(v,E,S);let l=o("div","actions"),c=o("button","btn primary wide big","START"),h=o("button","btn ghost wide","Leave table");return c.addEventListener("click",()=>{Date.now()-Te<500||(Te=Date.now(),I?.status==="running"?W?.("CashOut"):W?.("Start"))}),h.addEventListener("click",()=>{I?.status==="running"&&!window.confirm("Forfeit this round? The bet is lost.")||W?.("Leave")}),l.append(c,h),n.append(l),{board:e,side:n,number:r,meta:d,verify:m,canvas:i,bet:g,potential:w,balance:y,status:v,lastCrash:E,error:S,primary:c,leave:h,minus:u,plus:f}}function Ce(){$!==void 0&&(cancelAnimationFrame($),$=void 0),I=null}function Se(e,n,t,s,a){(!R||R.board!==e||!R.canvas.isConnected)&&(R=Je(e,n,s,a),Y="");let r=R;W=s,Me=a,I=t,t.status!==Y&&((t.status==="starting"||t.status==="running")&&Y!=="running"&&(r.verify.className="verify",r.verify.textContent="Round committed; seed hidden until the round ends."),Y=String(t.status));let d=t.status==="running",i=t.status==="starting",m=d||i;r.bet.textContent=L(Number(t.bet??0)),r.potential.textContent=L(Number(t.potential??t.bet??0)),r.balance.textContent=t.balance===null||t.balance===void 0?"—":L(Number(t.balance)),r.error.textContent=t.error?String(t.error):"",r.minus.disabled=m,r.plus.disabled=m,r.primary.textContent=d?"CASH OUT":i?"STARTING…":"START",r.primary.disabled=i,r.primary.className="btn primary wide big",r.leave.textContent=d?"Forfeit round":"Leave table",d?(r.status.textContent="Rising — cash out before the crash.",r.status.className="note win-text"):i?(r.status.textContent="Starting the round…",r.status.className="note"):t.status==="cashed"?(r.status.textContent=`Cashed out at ${Number(t.multiplier??0).toFixed(2)}× for ${L(Number(t.payout??0))}.`,r.status.className="note win-text"):t.status==="crashed"?(r.status.textContent=`Crashed at ${Number(t.crash_point??t.multiplier??0).toFixed(2)}×. The bet is lost.`,r.status.className="note error-text"):t.status==="forfeit"?(r.status.textContent="Round forfeited.",r.status.className="note error-text"):(r.status.textContent="Ready. Start a round when you want.",r.status.className="note"),t.crash_point!==null&&t.crash_point!==void 0&&t.status==="crashed"&&(r.lastCrash.textContent=`Last crash: ${Number(t.crash_point).toFixed(2)}×`);let p=t.commit?String(t.commit):"";if(r.meta.textContent=p?`commit ${z(p,12)}`:"no commitment yet",t.seed&&p){let u=++ve,g=String(t.seed),f=Number(t.crash_point??0);et(g,p,f).then(([w,y])=>{if(u!==ve||!R)return;let v=w&&y;R.verify.className=`verify ${v?"ok":"bad"}`,R.verify.textContent=v?`Seed ${z(g,12)} · sha256 matches commit · crash point recomputed`:`VERIFICATION FAILED · seed ${z(g,12)} · commit ${z(p,12)}`})}else!m&&!p&&(r.verify.className="verify",r.verify.textContent="Waiting for a round commitment.");if($!==void 0&&(cancelAnimationFrame($),$=void 0),d){let u=()=>{if(!R||I?.status!=="running"){$=void 0;return}Ee(R,I,a),$=requestAnimationFrame(u)};$=requestAnimationFrame(u)}else Ee(r,t,a)}function qe(e,n){if(e.status==="running"){let t=Math.max(0,(n.now()-Number(e.started_at??0))/1e3),s=Math.min(1e3,Math.exp(ne*t));return Math.max(s,Number(e.multiplier??1))}return e.status==="cashed"?Number(e.multiplier??1):e.status==="crashed"?Number(e.crash_point??e.multiplier??1):1}function Ee(e,n,t){let s=qe(n,t);e.number.textContent=`${s.toFixed(2)}×`,e.number.className=`crash-number ${n.status==="running"?"running":n.status==="cashed"?"cashed":n.status==="crashed"?"crashed":""}`,e.potential.textContent=L(Math.floor(Number(n.bet??0)*s)),Qe(e.canvas,n,s,t)}function Qe(e,n,t,s){let a=e.clientWidth||600,r=e.clientHeight||360,d=window.devicePixelRatio||1;(e.width!==Math.round(a*d)||e.height!==Math.round(r*d))&&(e.width=Math.round(a*d),e.height=Math.round(r*d));let i=e.getContext("2d");if(!i)return;i.setTransform(d,0,0,d,0,0),i.clearRect(0,0,a,r);let m=54,p=16,u=18,g=34,f=Math.max(1,a-m-p),w=Math.max(1,r-u-g),y=n.status==="running"?Math.max(.25,(s.now()-Number(n.started_at??0))/1e3):t>1?Math.log(t)/ne:1,v=Math.max(5,y*1.08),E=Math.max(2,Math.ceil(t*1.15*2)/2),S=b=>m+b/v*f,l=b=>u+w-(b-1)/(E-1)*w;i.strokeStyle="#242e38",i.fillStyle="#8a98a6",i.font="11px ui-monospace, monospace",i.lineWidth=1;for(let b=0;b<=4;b+=1){let T=1+(E-1)*b/4,C=l(T);i.beginPath(),i.moveTo(m,C),i.lineTo(a-p,C),i.stroke(),i.fillText(`${T.toFixed(1)}×`,6,C+4)}for(let b=0;b<=4;b+=1){let T=v*b/4,C=S(T);i.beginPath(),i.moveTo(C,u),i.lineTo(C,u+w),i.strokeStyle="#1b232c",i.stroke(),i.fillStyle="#5b6773",i.fillText(`${T.toFixed(0)}s`,C-10,r-12)}let c=n.status==="crashed"?"#e05a5a":n.status==="cashed"?"#4ec27b":"#e0b64a";i.beginPath();let h=180;for(let b=0;b<=h;b+=1){let T=y*b/h,C=Math.min(E,Math.exp(ne*T)),me=S(T),fe=l(C);b===0?i.moveTo(me,fe):i.lineTo(me,fe)}i.strokeStyle=c,i.lineWidth=2.4,i.stroke();let M=S(y),N=l(Math.min(E,t));i.beginPath(),i.arc(M,N,4.5,0,Math.PI*2),i.fillStyle=c,i.fill(),i.fillStyle=c,i.font="600 12px ui-monospace, monospace",i.fillText(`${t.toFixed(2)}×`,Math.min(M+8,a-70),Math.max(u+12,N-10))}async function et(e,n,t){let s=new Uint8Array(e.length/2);for(let u=0;u<s.length;u+=1)s[u]=Number.parseInt(e.slice(u*2,u*2+2),16);let a=await crypto.subtle.digest("SHA-256",s),d=[...new Uint8Array(a)].map(u=>u.toString(16).padStart(2,"0")).join("")===n.toLowerCase(),i=0;for(let u=0;u<8;u+=1)i=i*256+s[u];i/=2**64;let m=Math.max(1,Math.min(1e3,(1-.01)/(1-i))),p=Math.abs(m-t)<1e-6;return[d,p]}function tt(e){let n=e.details??{};switch(e.game){case"slots":return String(n.win_type??"spin");case"roulette":return`${n.number??"?"} ${n.color??""}`.trim();case"blackjack":return String(n.result??"");case"crash":{let t=String(n.result??""),s=n.crash_point;return s==null?t:`${t} @ ${Number(s).toFixed(2)}×`}default:return""}}function Ne(e,n){H(e);let t=n.records??[];if(t.length===0){let m=o("div","board");m.append(o("h2",void 0,"No plays yet"),o("p","muted","Your settled plays will appear here.")),e.append(m);return}let s=o("div","table-wrap"),a=o("table"),r=o("thead"),d=o("tr");for(let m of["Time","Game","Bet","Result","Net","Balance"])d.append(o("th",void 0,m));r.append(d);let i=o("tbody");for(let m of t){let p=o("tr");p.append(o("td","mono",ye(String(m.at??"")))),p.append(o("td",void 0,String(m.game??""))),p.append(o("td","num",L(Number(m.bet??0)))),p.append(o("td","muted",tt(m)));let u=Number(m.winnings??0)-Number(m.bet??0),g=o("td",`num ${u>0?"pos":u<0?"neg":""}`,u===0?"0 Kr":he(u));p.append(g),p.append(o("td","num",L(Number(m.balance_after??0)))),i.append(p)}a.append(r,i),s.append(a),e.append(s)}var oe=null,He=0,nt="#b0313a",ot="#151b21",Be="#106b45",st="#0d5c39",rt="#09472c",at="#1f7d52",it="#ecf7f0",lt="#e0b64a";function ct(e){return e===0?"green":[1,3,5,7,9,12,14,16,18,19,21,23,25,27,30,32,34,36].includes(e)?"red":"black"}function dt(e,n){let s=Math.max(38,e*.055),a=Math.max(34,e*.05),r=Math.max(30,n*.07),d=6,i=e-28-s-a,m=i/12,p=(n-28-r*2-d*2)/3,u=14+s,g=14,f=g+p*3+d,w=f+r+d,y=[];for(let l=0;l<12;l+=1)for(let c=0;c<3;c+=1){let h=l*3+(3-c);y.push({x:u+l*m,y:g+c*p,w:m,h:p,target:{bet:"straight",number:h},label:String(h),kind:"number",number:h})}y.push({x:14,y:g,w:s,h:p*3,target:{bet:"straight",number:0},label:"0",kind:"zero",number:0});for(let l=0;l<3;l+=1){let c=2-l;y.push({x:u+i,y:g+l*p,w:a,h:p,target:{bet:"column",index:c},label:"2:1",kind:"column"})}for(let l=0;l<3;l+=1)y.push({x:u+i/3*l,y:f,w:i/3,h:r,target:{bet:"dozen",index:l},label:["1st 12","2nd 12","3rd 12"][l],kind:"dozen"});[{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((l,c)=>{y.push({x:u+i/6*c,y:w,w:i/6,h:r,target:{bet:"outside",kind:l.kind},label:l.label,kind:"outside"})});let E=[],S=l=>{let c=y.find(h=>h.kind==="number"&&h.number===l);return c?{x:c.x+c.w/2,y:c.y+c.h/2}:null};for(let l=0;l<11;l+=1)for(let c=0;c<2;c+=1){let h=l*3+(3-c);E.push({x:u+(l+1)*m,y:g+(c+1)*p,radius:12,rank:0,target:{bet:"corner",numbers:[h,h-1,h+3,h+2]}})}for(let l=0;l<12;l+=1)for(let c=0;c<2;c+=1){let h=l*3+(3-c),M=S(h);M&&E.push({x:M.x,y:g+(c+1)*p,radius:10,rank:1,target:{bet:"split",a:h,b:h-1}})}for(let l=0;l<11;l+=1)for(let c=0;c<3;c+=1){let h=l*3+(3-c);E.push({x:u+(l+1)*m,y:g+c*p+p/2,radius:10,rank:1,target:{bet:"split",a:h,b:h+3}})}for(let l=0;l<3;l+=1){let c=3-l;E.push({x:u,y:g+l*p+p/2,radius:10,rank:1,target:{bet:"split",a:0,b:c}})}return{width:e,height:n,cellW:m,cellH:p,zeroX:14,zeroW:s,gridX:u,gridY:g,gridW:i,columnX:u+i,dozensY:f,outsideY:w,outsideH:r,zones:y,hotspots:E}}function Re(e,n,t,s,a,r){let d=Math.min(r,s/2,a/2);e.beginPath(),e.moveTo(n+d,t),e.arcTo(n+s,t,n+s,t+a,d),e.arcTo(n+s,t+a,n,t+a,d),e.arcTo(n,t+a,n,t,d),e.arcTo(n,t,n+s,t,d),e.closePath()}function $e(e,n){let t=e.clientWidth||800,s=e.clientHeight||540,a=window.devicePixelRatio||1;(e.width!==Math.round(t*a)||e.height!==Math.round(s*a))&&(e.width=Math.round(t*a),e.height=Math.round(s*a));let r=e.getContext("2d"),d=dt(t,s);if(!r)return d;r.setTransform(a,0,0,a,0,0),r.clearRect(0,0,t,s),r.fillStyle=rt,Re(r,0,0,t,s,10),r.fill();for(let i of d.zones){let m=st;i.kind==="number"?m=i.number===0?Be:ct(i.number)==="red"?nt:ot:i.kind==="zero"&&(m=Be),r.fillStyle=m,r.strokeStyle=at,r.lineWidth=1,Re(r,i.x+1,i.y+1,i.w-2,i.h-2,3),r.fill(),r.stroke(),r.fillStyle=it,r.textAlign="center",r.textBaseline="middle",i.kind==="number"||i.kind==="zero"?r.font=`700 ${Math.min(24,d.cellH*.3)}px ui-monospace, monospace`:r.font=`600 ${Math.min(12,d.outsideH*.4)}px ui-monospace, monospace`,r.fillText(i.label,i.x+i.w/2,i.y+i.h/2)}return pt(r,d,n),d}function Ae(e){return JSON.stringify(e)}function ut(e,n){let t=Ae(n),s=e.zones.find(a=>Ae(a.target)===t);if(s)return{x:s.x+s.w/2,y:s.y+s.h/2};if(n?.bet==="split"||n?.bet==="corner"){let a=n.bet==="split"?[n.a,n.b]:n.numbers,r=e.zones.filter(d=>(d.kind==="number"||d.kind==="zero")&&a.includes(d.number));if(r.length>0)return{x:r.reduce((d,i)=>d+i.x+i.w/2,0)/r.length,y:r.reduce((d,i)=>d+i.y+i.h/2,0)/r.length}}return null}function pt(e,n,t){let s=t.bets?.chips??[];for(let a of s){let r=ut(n,a.target);if(!r)continue;let d=Math.max(12,Math.min(18,n.cellW*.26));e.beginPath(),e.arc(r.x,r.y,d,0,Math.PI*2),e.fillStyle=lt,e.fill(),e.strokeStyle="#7a5a12",e.lineWidth=2,e.stroke(),e.fillStyle="#221a05",e.textAlign="center",e.textBaseline="middle",e.font=`700 ${d>15?11:10}px ui-monospace, monospace`,e.fillText(String(a.amount),r.x,r.y+.5)}}function De(e,n,t,s){H(e),H(n);let a=t.phase==="spinning";e.append(o("h2",void 0,"Roulette")),e.append(o("p","note","European single-zero wheel. Click a number, line or corner to add a chip; right-click takes one back."));let r=o("div","roulette-wrap"),d=o("canvas","roulette-canvas");if(r.append(d),t.phase==="result"&&t.number!==null&&t.number!==void 0){let l=o("div",`wheel-result ${t.color??""}`);l.append(o("div","label","Result")),l.append(o("div","num",String(t.number)));let c=Number(t.winnings??0);l.append(o("div","note",c>0?`Won ${L(c)}`:"No win")),r.append(l)}e.append(r);let m=$e(d,t),p=(l,c)=>{let h=d.getBoundingClientRect(),M=l-h.left,N=c-h.top,b=null;for(let T of m.hotspots){let C=Math.hypot(M-T.x,N-T.y);C>T.radius||(!b||T.rank<b.rank||T.rank===b.rank&&C<Math.hypot(M-b.x,N-b.y))&&(b=T)}if(b)return{x:b.x-b.radius,y:b.y-b.radius,w:b.radius*2,h:b.radius*2,target:b.target,label:"",kind:"number"};for(let T=m.zones.length-1;T>=0;T-=1){let C=m.zones[T];if(M>=C.x&&M<=C.x+C.w&&N>=C.y&&N<=C.y+C.h)return C}return null};d.addEventListener("click",l=>{if(a)return;let c=p(l.clientX,l.clientY);c&&s({PlaceBetAt:c.target})}),d.addEventListener("contextmenu",l=>{if(l.preventDefault(),a)return;let c=p(l.clientX,l.clientY);c&&s({RemoveBetAt:c.target})}),d.addEventListener("mousemove",l=>{let c=p(l.clientX,l.clientY);d.style.cursor=c&&!a?"pointer":"default"}),oe?.disconnect(),oe=new ResizeObserver(()=>{m=$e(d,t)}),oe.observe(d),n.append(o("div","label","Roulette")),n.append(o("p","note","Chips sit on the felt. The total is reserved when you spin.")),n.append(o("div","label","Chip"));let u=o("div","chip-row");for(let l of[10,25,50,100,500]){let c=o("button",`chip-btn ${Number(t.chip)===l?"active":""}`,String(l));c.disabled=a,c.addEventListener("click",()=>s({SetChip:l})),u.append(c)}n.append(u);let g=(t.bets?.chips??[]).reduce((l,c)=>l+Number(c.amount??0),0),f=o("div","stat");if(f.append(o("span","muted","On the felt"),o("strong","mono",L(g))),n.append(f),t.balance!==null&&t.balance!==void 0){let l=o("div","stat");l.append(o("span","muted","Balance"),o("strong","mono",L(Number(t.balance)))),n.append(l)}t.error&&n.append(o("p","error-text",String(t.error)));let w=o("p","note");if(a)w.textContent="Spinning…";else if(t.phase==="result"){let l=Number(t.winnings??0);w.textContent=`${t.number} ${t.color} · ${l>0?`won ${L(l)}`:"no win"}`,w.className=l>0?"note win-text":"note"}else w.textContent=g>0?"Ready to spin.":"Place at least one chip.";n.append(w);let y=o("div","actions"),v=o("button","btn primary wide big",a?"SPINNING…":"SPIN WHEEL");v.disabled=a||g===0,v.addEventListener("click",()=>{Date.now()-He<600||(He=Date.now(),s("Spin"))});let E=o("button","btn wide","Clear bets");E.disabled=a||g===0,E.addEventListener("click",()=>s("ClearBets"));let S=o("button","btn ghost wide","Leave table");S.disabled=a,S.addEventListener("click",()=>{a||s("Leave")}),y.append(v,E,S),n.append(y)}var se=5,re=3,F="CLOGDS",mt=["top","middle","bottom","V","inverted V"],K=0,Pe=0,A,X=null,G=null;function ft(){K=Date.now()+900}function We(){A!==void 0&&(window.clearInterval(A),A=void 0),X=null,G=null,K=0}function ht(e){return e.phase==="spinning"||Date.now()<K}function ae(e,n,t,s){X=t,G=e,A!==void 0&&(window.clearInterval(A),A=void 0);let a=ht(t);H(e),H(n);let r=o("h2",void 0,"Slots"),d=o("p","note","Match three or more symbols from the left. Five paylines.");e.append(r,d);let i=o("div","reels"),m=[],p=Array.isArray(t.reels)?t.reels.join(""):String(t.reels??"");for(let c=0;c<se;c+=1){let h=o("div","reel");for(let M=0;M<re;M+=1){let N=c*re+M,b=a?F[(N*3+Math.floor(Date.now()/70))%F.length]:p.length===se*re?p[N]:p.length===se?p[c]:F[(N*5+c)%F.length],T=o("div","tile",b);T.classList.add(`sym-${b}`),a&&T.classList.add("spinning"),!a&&(t.win_cells??[]).includes(N)&&T.classList.add("win"),m.push(T),h.append(T)}i.append(h)}e.append(i);let u=o("div","win-lines");if(!a&&t.phase==="result"){let c=Number(t.winnings??0);if(c>0){let h=(t.win_lines??[]).map(M=>`${M} (${mt[M-1]??"line"})`).join(" · ");u.textContent=`${t.win_type==="mega"?"MEGA WIN":t.win_type==="big"?"BIG WIN":"WIN"} ${L(c)}${h?` · ${h}`:""}`,u.classList.add("win-text")}else u.textContent="No win — spin again."}else a?u.textContent="Spinning…":u.textContent="Pick a bet and spin.";if(e.append(u),a){let c=0;A=window.setInterval(()=>{c+=1;for(let h=0;h<m.length;h+=1){let M=F[(h*3+c)%F.length];m[h].textContent=M,m[h].className=`tile spinning sym-${M}`}Date.now()>=K&&X&&G&&(window.clearInterval(A),A=void 0,ae(G,n,X,s))},70)}n.append(o("div","label","Slots")),n.append(o("p","note","5 reels × 3 rows, 5 paylines."));let g=o("div","row"),f=o("button","btn small","−10"),w=o("strong","mono",L(Number(t.bet??0))),y=o("button","btn small","+10");f.disabled=a,y.disabled=a,f.addEventListener("click",()=>s({AdjustBet:-10})),y.addEventListener("click",()=>s({AdjustBet:10})),g.append(f,w,y),n.append(o("div","label","Bet"),g),t.error&&n.append(o("p","error-text",String(t.error)));let v=o("p","note");if(a)v.textContent="Spinning…";else if(t.phase==="result"){let c=Number(t.winnings??0);v.textContent=c>0?`Won ${L(c)}`:"No win this spin.",c>0&&v.classList.add("win-text")}else v.textContent="Ready when you are.";n.append(v);let E=o("div","actions"),S=o("button","btn primary wide big",a?"SPINNING…":"SPIN");S.disabled=a,S.addEventListener("click",()=>{Date.now()-Pe<900||(Pe=Date.now(),ft(),s("Spin"))});let l=o("button","btn ghost wide","Leave machine");l.disabled=a,l.addEventListener("click",()=>s("Leave")),E.append(S,l),n.append(E)}var bt=k("auth-screen"),gt=k("lobby-screen"),yt=k("game-screen"),kt=k("history-screen"),Ie=k("session-bar"),Lt=k("account-name"),xt=k("account-balance"),ie=k("game-grid"),U=k("game-board"),Z=k("game-side"),wt=k("game-title"),ze=k("game-badge"),vt=k("history-body"),Tt=k("auth-form"),Ve=k("auth-user"),q=k("auth-pass"),O=k("auth-error"),P=k("auth-submit"),_e=k("tab-login"),je=k("tab-register"),J=k("reconnect-overlay"),Fe=k("reconnect-message"),Oe=k("replaced-overlay"),x=null,D=null,ue=null,_="login",le=null,Ye=0,ce=0,de=new Map([["slots",10],["roulette",10],["blackjack",10],["crash",10]]),Xe={slots:"Slots",roulette:"Roulette",blackjack:"Blackjack",crash:"Crash"},Et={slots:"Five reels, five paylines. Match symbols from the left.",roulette:"European single-zero wheel with inside and outside bets.",blackjack:"Six-deck shoe. Blackjack pays 3:2.",crash:"A rising multiplier. Cash out before it crashes."},Mt=location.pathname.includes("/play/")?location.pathname.slice(0,location.pathname.indexOf("/play/")):"",Ct=`${location.protocol==="https:"?"wss":"ws"}://${location.host}${Mt}/ws`,Q=new j(Ct,{onHello(e){Ye=e.time-Date.now(),D=e.user??null,ze.textContent=`v${e.version}`},onView(e){x=e,e.screen!=="slots"&&We(),e.screen!=="crash"&&Ce(),typeof e.balance=="number"&&(ue=e.balance),e.screen==="lobby"&&"user"in e&&(D=e.user??null,D?q.value="":ue=null),ce=0,J.hidden=!0,Ge(),D&&x.balance===null&&le!==D&&(le=D,Q.send("Info")),D||(le=null)},onNotice(e){e.startsWith("Connected to:")||e.startsWith("Fingerprint:")||e.startsWith("User:")||e.startsWith("Account:")||e.startsWith("Balance:")||(ee(e,be(e)),e.startsWith("Account created")&&(pe("login"),q.value="",Ve.focus()),P.disabled=!1,P.textContent=_==="login"?"Log in":"Create account")},onError(e){ee(e,"bad"),P.disabled=!1,P.textContent=_==="login"?"Log in":"Create account"},onReplaced(){Oe.hidden=!1},onPong(){},onOpen(){J.hidden=!0},onClosed(e){if(Oe.hidden!==!1){if(!e){Fe.textContent="The connection could not be restored. Reload the page to try again.",J.hidden=!1;return}ce+=1,Fe.textContent=ce>1?"Still trying to reach the game server…":"Trying to reach the game server…",J.hidden=!1}}});function B(e){Q.send(e)}function St(){return Date.now()+Ye}function pe(e){_=e,_e.classList.toggle("active",e==="login"),je.classList.toggle("active",e==="register"),P.textContent=e==="login"?"Log in":"Create account",q.autocomplete=e==="login"?"current-password":"new-password",O.textContent=""}function V(e){bt.hidden=e!=="auth",gt.hidden=e!=="lobby",yt.hidden=e!=="game",kt.hidden=e!=="history"}function Nt(e){xt.textContent=L(e??null)}function Ge(){if(!x)return;let e=D;if(e?(Ie.hidden=!1,Lt.textContent=String(e)):Ie.hidden=!0,Nt(ue),!e){V("auth"),O.textContent=x.login_error?String(x.login_error):"",x.screen==="lobby"&&x.connected===!1&&(O.textContent="The game server is not reachable yet. Retrying…");return}switch(x.screen){case"lobby":V("lobby"),Bt(x);break;case"history":V("history"),Ne(vt,x);break;case"slots":case"roulette":case"blackjack":case"crash":V("game"),wt.textContent=Xe[x.screen]??x.screen,ze.textContent=Ht(x),x.screen==="slots"?ae(U,Z,x,B):x.screen==="roulette"?De(U,Z,x,B):x.screen==="blackjack"?we(U,Z,x,B):Se(U,Z,x,B,{now:St});break;default:V("auth")}}function Ht(e){return e.screen==="crash"||e.screen==="blackjack"?String(e.status??""):e.screen==="slots"||e.screen==="roulette"?String(e.phase??""):""}function Bt(e){ie.replaceChildren();let n=e.games??[];for(let[t,s]of n){let a=o("article",`game-card ${t}`);a.append(o("h3",void 0,s||Xe[t]||t)),a.append(o("p",void 0,Et[t]??"Play a round."));let r=o("div","row wrap"),d=o("label","field");d.style.flex="1",d.append(o("span","label","Bet"));let i=o("input","input bet-input");i.type="number",i.min="1",i.step="10",i.value=String(de.get(t)??10),i.addEventListener("change",()=>{let p=Math.max(1,Math.floor(Number(i.value)||1));i.value=String(p),de.set(t,p)}),d.append(i);let m=o("button","btn primary",t==="roulette"?"Open table":"Play");m.addEventListener("click",()=>{let p=Math.max(1,Math.floor(Number(i.value)||1));de.set(t,p),B({Play:{game:t,bet:p}})}),r.append(d,m),a.append(r),ie.append(a)}n.length===0&&ie.append(o("p","muted","The server did not advertise any games."))}_e.addEventListener("click",()=>pe("login"));je.addEventListener("click",()=>pe("register"));Tt.addEventListener("submit",async e=>{e.preventDefault();let n=Ve.value.trim(),t=q.value;if(!n||!t){O.textContent="Enter a username and a password.";return}P.disabled=!0,P.textContent="Working…",O.textContent="";try{let s=await ge(t);B(_==="login"?{Login:{user:n,credential:s}}:{Register:{user:n,credential:s}})}catch{O.textContent="This browser cannot hash the password (needs a secure context).",P.disabled=!1,P.textContent=_==="login"?"Log in":"Create account"}});k("logout").addEventListener("click",()=>B("Logout"));k("show-history").addEventListener("click",()=>B({History:{limit:50}}));k("refresh-lobby").addEventListener("click",()=>B("Info"));k("history-back").addEventListener("click",()=>B("ShowLobby"));k("leave-game").addEventListener("click",()=>{!x||(x.screen==="blackjack"&&x.status==="player"||x.screen==="crash"&&x.status==="running")&&!window.confirm("Leaving now forfeits the active bet. Continue?")||B("Leave")});k("reload-replaced").addEventListener("click",()=>location.reload());k("reload-reconnect").addEventListener("click",()=>location.reload());window.addEventListener("resize",()=>{window.clearTimeout(window.__resizeTimer),window.__resizeTimer=window.setTimeout(()=>{x&&Ge()},140)});window.setInterval(()=>{D&&Q.ping()},25e3);Q.connect();})();
diff --git a/web/play/src/blackjack.ts b/web/play/src/blackjack.ts
index 3537f45..49d46fa 100644
--- a/web/play/src/blackjack.ts
+++ b/web/play/src/blackjack.ts
@@ -1,6 +1,14 @@
1import type { View } from "./types"; 1import type { View } from "./types";
2import { cardFace, clear, money, node } from "./dom"; 2import { cardFace, clear, money, node } from "./dom";
3 3
4let lastAction = 0;
5
6function actionAllowed(): boolean {
7 if (Date.now() - lastAction < 400) return false;
8 lastAction = Date.now();
9 return true;
10}
11
4function cardElement(card: string): HTMLElement { 12function cardElement(card: string): HTMLElement {
5 if (card === "?") { 13 if (card === "?") {
6 const back = node("div", "card back"); 14 const back = node("div", "card back");
@@ -124,6 +132,7 @@ export function renderBlackjack(
124 ? node("button", "btn primary", label) 132 ? node("button", "btn primary", label)
125 : node("button", "btn", label); 133 : node("button", "btn", label);
126 button.addEventListener("click", () => { 134 button.addEventListener("click", () => {
135 if (!actionAllowed()) return;
127 if (action === "hit") send("Hit"); 136 if (action === "hit") send("Hit");
128 else if (action === "stand") send("Stand"); 137 else if (action === "stand") send("Stand");
129 else send("Double"); 138 else send("Double");
@@ -133,12 +142,15 @@ export function renderBlackjack(
133 actions.append(actionsRow); 142 actions.append(actionsRow);
134 const forfeit = node("button", "btn danger wide", "Forfeit hand"); 143 const forfeit = node("button", "btn danger wide", "Forfeit hand");
135 forfeit.addEventListener("click", () => { 144 forfeit.addEventListener("click", () => {
145 if (!actionAllowed()) return;
136 if (window.confirm("Forfeit this hand? The reserved bet is lost.")) send("Forfeit"); 146 if (window.confirm("Forfeit this hand? The reserved bet is lost.")) send("Forfeit");
137 }); 147 });
138 actions.append(forfeit); 148 actions.append(forfeit);
139 } else if (canDeal) { 149 } else if (canDeal) {
140 const deal = node("button", "btn primary wide big", "DEAL"); 150 const deal = node("button", "btn primary wide big", "DEAL");
141 deal.addEventListener("click", () => send("Deal")); 151 deal.addEventListener("click", () => {
152 if (actionAllowed()) send("Deal");
153 });
142 actions.append(deal); 154 actions.append(deal);
143 } 155 }
144 156
diff --git a/web/play/src/crash.ts b/web/play/src/crash.ts
index 6f3c8dd..a683dcd 100644
--- a/web/play/src/crash.ts
+++ b/web/play/src/crash.ts
@@ -31,6 +31,7 @@ let sendCommand: ((command: unknown) => void) | null = null;
31let frameHandle: number | undefined; 31let frameHandle: number | undefined;
32let verifyToken = 0; 32let verifyToken = 0;
33let lastStatus = ""; 33let lastStatus = "";
34let lastAction = 0;
34 35
35function build( 36function build(
36 board: HTMLElement, 37 board: HTMLElement,
@@ -88,6 +89,8 @@ function build(
88 const primary = node("button", "btn primary wide big", "START") as HTMLButtonElement; 89 const primary = node("button", "btn primary wide big", "START") as HTMLButtonElement;
89 const leave = node("button", "btn ghost wide", "Leave table") as HTMLButtonElement; 90 const leave = node("button", "btn ghost wide", "Leave table") as HTMLButtonElement;
90 primary.addEventListener("click", () => { 91 primary.addEventListener("click", () => {
92 if (Date.now() - lastAction < 500) return;
93 lastAction = Date.now();
91 if (current?.status === "running") sendCommand?.("CashOut"); 94 if (current?.status === "running") sendCommand?.("CashOut");
92 else sendCommand?.("Start"); 95 else sendCommand?.("Start");
93 }); 96 });
@@ -118,6 +121,15 @@ function build(
118 }; 121 };
119} 122}
120 123
124/// Stop the chart loop when another screen takes over.
125export function stopCrashAnimation(): void {
126 if (frameHandle !== undefined) {
127 cancelAnimationFrame(frameHandle);
128 frameHandle = undefined;
129 }
130 current = null;
131}
132
121export function renderCrash( 133export function renderCrash(
122 board: HTMLElement, 134 board: HTMLElement,
123 side: HTMLElement, 135 side: HTMLElement,
@@ -125,7 +137,7 @@ export function renderCrash(
125 send: (command: unknown) => void, 137 send: (command: unknown) => void,
126 now: Clock, 138 now: Clock,
127): void { 139): void {
128 if (!parts || parts.board !== board) { 140 if (!parts || parts.board !== board || !parts.canvas.isConnected) {
129 parts = build(board, side, send, now); 141 parts = build(board, side, send, now);
130 lastStatus = ""; 142 lastStatus = "";
131 } 143 }
diff --git a/web/play/src/main.ts b/web/play/src/main.ts
index fd55250..f051429 100644
--- a/web/play/src/main.ts
+++ b/web/play/src/main.ts
@@ -1,10 +1,10 @@
1import { byId, money, node, sha256Hex, toast, toneFor } from "./dom"; 1import { byId, money, node, sha256Hex, toast, toneFor } from "./dom";
2import { Gateway } from "./net"; 2import { Gateway } from "./net";
3import { renderBlackjack } from "./blackjack"; 3import { renderBlackjack } from "./blackjack";
4import { renderCrash } from "./crash"; 4import { renderCrash, stopCrashAnimation } from "./crash";
5import { renderHistory } from "./history"; 5import { renderHistory } from "./history";
6import { renderRoulette } from "./roulette"; 6import { renderRoulette } from "./roulette";
7import { renderSlots } from "./slots"; 7import { renderSlots, stopSlotsAnimation } from "./slots";
8import type { View } from "./types"; 8import type { View } from "./types";
9 9
10const authScreen = byId("auth-screen"); 10const authScreen = byId("auth-screen");
@@ -72,6 +72,8 @@ const gateway = new Gateway(wsUrl, {
72 }, 72 },
73 onView(next) { 73 onView(next) {
74 view = next; 74 view = next;
75 if (next.screen !== "slots") stopSlotsAnimation();
76 if (next.screen !== "crash") stopCrashAnimation();
75 // Game and history views carry no account field; the lobby is the source 77 // Game and history views carry no account field; the lobby is the source
76 // of truth for who is signed in. A null lobby user is a logout or an 78 // of truth for who is signed in. A null lobby user is a logout or an
77 // expired session. 79 // expired session.
diff --git a/web/play/src/roulette.ts b/web/play/src/roulette.ts
index cbb7c6e..a42686a 100644
--- a/web/play/src/roulette.ts
+++ b/web/play/src/roulette.ts
@@ -20,6 +20,9 @@ type Hotspot = {
20 rank: number; 20 rank: number;
21}; 21};
22 22
23let observer: ResizeObserver | null = null;
24let lastSpin = 0;
25
23type Layout = { 26type Layout = {
24 width: number; 27 width: number;
25 height: number; 28 height: number;
@@ -413,9 +416,11 @@ export function renderRoulette(
413 const zone = hit(event.clientX, event.clientY); 416 const zone = hit(event.clientX, event.clientY);
414 canvas.style.cursor = zone && !spinning ? "pointer" : "default"; 417 canvas.style.cursor = zone && !spinning ? "pointer" : "default";
415 }); 418 });
416 new ResizeObserver(() => { 419 observer?.disconnect();
420 observer = new ResizeObserver(() => {
417 current = draw(canvas, view); 421 current = draw(canvas, view);
418 }).observe(canvas); 422 });
423 observer.observe(canvas);
419 424
420 // Sidebar 425 // Sidebar
421 side.append(node("div", "label", "Roulette")); 426 side.append(node("div", "label", "Roulette"));
@@ -459,7 +464,11 @@ export function renderRoulette(
459 const actions = node("div", "actions"); 464 const actions = node("div", "actions");
460 const spin = node("button", "btn primary wide big", spinning ? "SPINNING…" : "SPIN WHEEL"); 465 const spin = node("button", "btn primary wide big", spinning ? "SPINNING…" : "SPIN WHEEL");
461 spin.disabled = spinning || total === 0; 466 spin.disabled = spinning || total === 0;
462 spin.addEventListener("click", () => send("Spin")); 467 spin.addEventListener("click", () => {
468 if (Date.now() - lastSpin < 600) return;
469 lastSpin = Date.now();
470 send("Spin");
471 });
463 const clearBets = node("button", "btn wide", "Clear bets"); 472 const clearBets = node("button", "btn wide", "Clear bets");
464 clearBets.disabled = spinning || total === 0; 473 clearBets.disabled = spinning || total === 0;
465 clearBets.addEventListener("click", () => send("ClearBets")); 474 clearBets.addEventListener("click", () => send("ClearBets"));
diff --git a/web/play/src/slots.ts b/web/play/src/slots.ts
index dba1e25..7c4677d 100644
--- a/web/play/src/slots.ts
+++ b/web/play/src/slots.ts
@@ -7,6 +7,7 @@ const SYMBOLS = "CLOGDS";
7const LINE_NAMES = ["top", "middle", "bottom", "V", "inverted V"]; 7const LINE_NAMES = ["top", "middle", "bottom", "V", "inverted V"];
8 8
9let spinUntil = 0; 9let spinUntil = 0;
10let lastSpin = 0;
10let animation: number | undefined; 11let animation: number | undefined;
11let latest: View | null = null; 12let latest: View | null = null;
12let latestBoard: HTMLElement | null = null; 13let latestBoard: HTMLElement | null = null;
@@ -15,6 +16,17 @@ export function requestSlotsSpin(): void {
15 spinUntil = Date.now() + 900; 16 spinUntil = Date.now() + 900;
16} 17}
17 18
19/// Stop the reel animation when another screen takes over.
20export function stopSlotsAnimation(): void {
21 if (animation !== undefined) {
22 window.clearInterval(animation);
23 animation = undefined;
24 }
25 latest = null;
26 latestBoard = null;
27 spinUntil = 0;
28}
29
18function spinning(view: View): boolean { 30function spinning(view: View): boolean {
19 return view.phase === "spinning" || Date.now() < spinUntil; 31 return view.phase === "spinning" || Date.now() < spinUntil;
20} 32}
@@ -42,7 +54,9 @@ export function renderSlots(
42 54
43 const reels = node("div", "reels"); 55 const reels = node("div", "reels");
44 const tiles: HTMLElement[] = []; 56 const tiles: HTMLElement[] = [];
45 const flat = String(view.reels ?? ""); 57 const flat = Array.isArray(view.reels)
58 ? view.reels.join("")
59 : String(view.reels ?? "");
46 for (let column = 0; column < REELS; column += 1) { 60 for (let column = 0; column < REELS; column += 1) {
47 const reel = node("div", "reel"); 61 const reel = node("div", "reel");
48 for (let row = 0; row < ROWS; row += 1) { 62 for (let row = 0; row < ROWS; row += 1) {
@@ -133,6 +147,8 @@ export function renderSlots(
133 const spin = node("button", "btn primary wide big", active ? "SPINNING…" : "SPIN"); 147 const spin = node("button", "btn primary wide big", active ? "SPINNING…" : "SPIN");
134 spin.disabled = active; 148 spin.disabled = active;
135 spin.addEventListener("click", () => { 149 spin.addEventListener("click", () => {
150 if (Date.now() - lastSpin < 900) return;
151 lastSpin = Date.now();
136 requestSlotsSpin(); 152 requestSlotsSpin();
137 send("Spin"); 153 send("Spin");
138 }); 154 });