diff options
| author | salmonik <salmonik@tuta.io> | 2026-11-08 03:37:19 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-11-08 03:37:19 +0100 |
| commit | 7ebf8c7876b9540843d2635baf55ca4061267e40 (patch) | |
| tree | 3444f06cc3386285ae5d3b6e0f916b5ec7fd6f93 /web | |
| parent | crash: show and verify the revealed seed in the TUI and GUI (diff) | |
| download | zagan-bet-7ebf8c7876b9540843d2635baf55ca4061267e40.tar.gz zagan-bet-7ebf8c7876b9540843d2635baf55ca4061267e40.zip | |
web: show the account number and stop reel spin on an error
Also documents the __Host- cookie prefix used on HTTPS.
Diffstat (limited to '')
| -rw-r--r-- | web/play/app.js | 2 | ||||
| -rw-r--r-- | web/play/index.html | 1 | ||||
| -rw-r--r-- | web/play/src/main.ts | 2 | ||||
| -rw-r--r-- | web/play/src/slots.ts | 1 | ||||
| -rw-r--r-- | webd/README.md | 6 |
5 files changed, 9 insertions, 3 deletions
diff --git a/web/play/app.js b/web/play/app.js index 25b10a6..8044e8f 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 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 te(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 ne(){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",()=>{ne()&&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",()=>{ne()&&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",()=>{ne()&&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 oe=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(oe*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)/oe: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(oe*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 se=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"}),se?.disconnect(),se=new ResizeObserver(()=>{m=$e(d,t)}),se.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 re=5,ae=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 ie(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<re;c+=1){let h=o("div","reel");for(let M=0;M<ae;M+=1){let N=c*ae+M,b=a?F[(N*3+Math.floor(Date.now()/70))%F.length]:p.length===re*ae?p[N]:p.length===re?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,ie(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"),le=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,q=null,_="login",ce=null,Ye=0,de=0,ue=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`,ee=new j(Ct,{onHello(e){Ye=e.time-Date.now(),D=e.user??null,typeof e.balance=="number"&&(q=e.balance),ze.textContent=`v${e.version}`},onView(e){x=e,e.screen!=="slots"&&We(),e.screen!=="crash"&&Ce(),typeof e.balance=="number"&&(q=e.balance),e.screen==="lobby"&&"user"in e&&(D=e.user??null,D?Q.value="":q=null),de=0,J.hidden=!0,Ge(),D&&x.balance===null&&ce!==D&&(ce=D,ee.send("Info")),D||(ce=null)},onNotice(e){e.startsWith("Connected to:")||e.startsWith("Fingerprint:")||e.startsWith("User:")||e.startsWith("Account:")||e.startsWith("Balance:")||(te(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){te(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}de+=1,Fe.textContent=de>1?"Still trying to reach the game server…":"Trying to reach the game server…",J.hidden=!1}}});function B(e){ee.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(q),!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"?ie(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){le.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(ue.get(t)??10),i.addEventListener("change",()=>{let p=Math.max(1,Math.floor(Number(i.value)||1));i.value=String(p),ue.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));ue.set(t,p),B({Play:{game:t,bet:p}})}),r.append(d,m),a.append(r),le.append(a)}n.length===0&&le.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&&ee.ping()},25e3);ee.connect();})(); | "use strict";(()=>{function g(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 x(e){return e==null||Number.isNaN(e)?"— Kr":`${Ke(e)} Kr`}function he(e){return`${e<0?"-":"+"}${x(Math.abs(e))}`}function te(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 Y=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 xe=0;function ne(){return Date.now()-xe<400?!1:(xe=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 Le(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 ${x(s)}`;case"win":return`You win — ${x(s)}`;case"push":return`Push — ${x(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(Le("Dealer",a.dealer??[],Number(a.dealer_total??0))),e.append(Le("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"),k=o("strong","mono",x(m)),v=o("button","btn small","+10");w.addEventListener("click",()=>s({AdjustBet:-10})),v.addEventListener("click",()=>s({AdjustBet:10})),f.append(w,k,v),n.append(o("div","label","Bet"),f)}else{let f=o("div","stat");f.append(o("span","muted","Bet"),o("strong",void 0,x(m))),n.append(f)}if(p!==void 0){let f=o("div","stat");f.append(o("span","muted","Balance"),o("strong",void 0,x(Number(p)))),n.append(f)}let u=o("div","actions");if(r){let f=o("div","row wrap");for(let k of a?.actions??[]){let v=k==="hit"?"HIT":k==="stand"?"STAND":"DOUBLE",E=k==="stand"?o("button","btn primary",v):o("button","btn",v);E.addEventListener("click",()=>{ne()&&s(k==="hit"?"Hit":k==="stand"?"Stand":"Double")}),f.append(E)}u.append(f);let w=o("button","btn danger wide","Forfeit hand");w.addEventListener("click",()=>{ne()&&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",()=>{ne()&&s("Deal")}),u.append(f)}let y=o("button","btn ghost wide",r?"Leave (forfeits)":"Leave table");y.addEventListener("click",()=>{r&&!window.confirm("Leaving forfeits this hand. Continue?")||s("Leave")}),u.append(y),n.append(u)}var oe=Math.LN2/5,R=null,I=null,Me=null,W=null,$,ve=0,X="",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"),y=o("strong","mono","—"),f=o("button","btn small","+10");u.addEventListener("click",()=>W?.({AdjustBet:-10})),f.addEventListener("click",()=>W?.({AdjustBet:10})),p.append(u,y,f),n.append(o("div","label","Bet"),p);let w=o("strong","mono","—"),k=o("strong","mono","—");for(let[M,N]of[["Potential",w],["Balance",k]]){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:y,potential:w,balance:k,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),X="");let r=R;W=s,Me=a,I=t,t.status!==X&&((t.status==="starting"||t.status==="running")&&X!=="running"&&(r.verify.className="verify",r.verify.textContent="Round committed; seed hidden until the round ends."),X=String(t.status));let d=t.status==="running",i=t.status==="starting",m=d||i;r.bet.textContent=x(Number(t.bet??0)),r.potential.textContent=x(Number(t.potential??t.bet??0)),r.balance.textContent=t.balance===null||t.balance===void 0?"—":x(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 ${x(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,y=String(t.seed),f=Number(t.crash_point??0);et(y,p,f).then(([w,k])=>{if(u!==ve||!R)return;let v=w&&k;R.verify.className=`verify ${v?"ok":"bad"}`,R.verify.textContent=v?`Seed ${z(y,12)} · sha256 matches commit · crash point recomputed`:`VERIFICATION FAILED · seed ${z(y,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(oe*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=x(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,y=34,f=Math.max(1,a-m-p),w=Math.max(1,r-u-y),k=n.status==="running"?Math.max(.25,(s.now()-Number(n.started_at??0))/1e3):t>1?Math.log(t)/oe:1,v=Math.max(5,k*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=k*b/h,C=Math.min(E,Math.exp(oe*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(k),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",x(Number(m.bet??0)))),p.append(o("td","muted",tt(m)));let u=Number(m.winnings??0)-Number(m.bet??0),y=o("td",`num ${u>0?"pos":u<0?"neg":""}`,u===0?"0 Kr":he(u));p.append(y),p.append(o("td","num",x(Number(m.balance_after??0)))),i.append(p)}a.append(r,i),s.append(a),e.append(s)}var se=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,y=14,f=y+p*3+d,w=f+r+d,k=[];for(let l=0;l<12;l+=1)for(let c=0;c<3;c+=1){let h=l*3+(3-c);k.push({x:u+l*m,y:y+c*p,w:m,h:p,target:{bet:"straight",number:h},label:String(h),kind:"number",number:h})}k.push({x:14,y,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;k.push({x:u+i,y:y+l*p,w:a,h:p,target:{bet:"column",index:c},label:"2:1",kind:"column"})}for(let l=0;l<3;l+=1)k.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)=>{k.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=k.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:y+(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:y+(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:y+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:y+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:y,gridW:i,columnX:u+i,dozensY:f,outsideY:w,outsideH:r,zones:k,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 ${x(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"}),se?.disconnect(),se=new ResizeObserver(()=>{m=$e(d,t)}),se.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 y=(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",x(y))),n.append(f),t.balance!==null&&t.balance!==void 0){let l=o("div","stat");l.append(o("span","muted","Balance"),o("strong","mono",x(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 ${x(l)}`:"no win"}`,w.className=l>0?"note win-text":"note"}else w.textContent=y>0?"Ready to spin.":"Place at least one chip.";n.append(w);let k=o("div","actions"),v=o("button","btn primary wide big",a?"SPINNING…":"SPIN WHEEL");v.disabled=a||y===0,v.addEventListener("click",()=>{Date.now()-He<600||(He=Date.now(),s("Spin"))});let E=o("button","btn wide","Clear bets");E.disabled=a||y===0,E.addEventListener("click",()=>s("ClearBets"));let S=o("button","btn ghost wide","Leave table");S.disabled=a,S.addEventListener("click",()=>{a||s("Leave")}),k.append(v,E,S),n.append(k)}var re=5,ae=3,F="CLOGDS",mt=["top","middle","bottom","V","inverted V"],V=0,Pe=0,A,G=null,K=null;function ft(){V=Date.now()+900}function We(){A!==void 0&&(window.clearInterval(A),A=void 0),G=null,K=null,V=0}function ht(e){return e.phase==="spinning"||Date.now()<V}function ie(e,n,t,s){G=t,K=e,t.error&&(V=0),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<re;c+=1){let h=o("div","reel");for(let M=0;M<ae;M+=1){let N=c*ae+M,b=a?F[(N*3+Math.floor(Date.now()/70))%F.length]:p.length===re*ae?p[N]:p.length===re?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"} ${x(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()>=V&&G&&K&&(window.clearInterval(A),A=void 0,ie(K,n,G,s))},70)}n.append(o("div","label","Slots")),n.append(o("p","note","5 reels × 3 rows, 5 paylines."));let y=o("div","row"),f=o("button","btn small","−10"),w=o("strong","mono",x(Number(t.bet??0))),k=o("button","btn small","+10");f.disabled=a,k.disabled=a,f.addEventListener("click",()=>s({AdjustBet:-10})),k.addEventListener("click",()=>s({AdjustBet:10})),y.append(f,w,k),n.append(o("div","label","Bet"),y),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 ${x(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=g("auth-screen"),gt=g("lobby-screen"),yt=g("game-screen"),kt=g("history-screen"),Ie=g("session-bar"),xt=g("account-name"),Lt=g("account-balance"),le=g("game-grid"),U=g("game-board"),Z=g("game-side"),wt=g("game-title"),ze=g("game-badge"),vt=g("lobby-account"),Tt=g("history-body"),Et=g("auth-form"),Ve=g("auth-user"),Q=g("auth-pass"),O=g("auth-error"),P=g("auth-submit"),_e=g("tab-login"),je=g("tab-register"),J=g("reconnect-overlay"),Fe=g("reconnect-message"),Oe=g("replaced-overlay"),L=null,D=null,q=null,j="login",ce=null,Ye=0,de=0,ue=new Map([["slots",10],["roulette",10],["blackjack",10],["crash",10]]),Xe={slots:"Slots",roulette:"Roulette",blackjack:"Blackjack",crash:"Crash"},Mt={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."},Ct=location.pathname.includes("/play/")?location.pathname.slice(0,location.pathname.indexOf("/play/")):"",St=`${location.protocol==="https:"?"wss":"ws"}://${location.host}${Ct}/ws`,ee=new Y(St,{onHello(e){Ye=e.time-Date.now(),D=e.user??null,typeof e.balance=="number"&&(q=e.balance),ze.textContent=`v${e.version}`},onView(e){L=e,e.screen!=="slots"&&We(),e.screen!=="crash"&&Ce(),typeof e.balance=="number"&&(q=e.balance),e.screen==="lobby"&&"user"in e&&(D=e.user??null,D?Q.value="":q=null),de=0,J.hidden=!0,Ge(),D&&L.balance===null&&ce!==D&&(ce=D,ee.send("Info")),D||(ce=null)},onNotice(e){e.startsWith("Connected to:")||e.startsWith("Fingerprint:")||e.startsWith("User:")||e.startsWith("Account:")||e.startsWith("Balance:")||(te(e,be(e)),e.startsWith("Account created")&&(pe("login"),Q.value="",Ve.focus()),P.disabled=!1,P.textContent=j==="login"?"Log in":"Create account")},onError(e){te(e,"bad"),P.disabled=!1,P.textContent=j==="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}de+=1,Fe.textContent=de>1?"Still trying to reach the game server…":"Trying to reach the game server…",J.hidden=!1}}});function B(e){ee.send(e)}function Nt(){return Date.now()+Ye}function pe(e){j=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 _(e){bt.hidden=e!=="auth",gt.hidden=e!=="lobby",yt.hidden=e!=="game",kt.hidden=e!=="history"}function Ht(e){Lt.textContent=x(e??null)}function Ge(){if(!L)return;let e=D;if(e?(Ie.hidden=!1,xt.textContent=String(e)):Ie.hidden=!0,Ht(q),!e){_("auth"),O.textContent=L.login_error?String(L.login_error):"",L.screen==="lobby"&&L.connected===!1&&(O.textContent="The game server is not reachable yet. Retrying…");return}switch(L.screen){case"lobby":_("lobby"),Rt(L);break;case"history":_("history"),Ne(Tt,L);break;case"slots":case"roulette":case"blackjack":case"crash":_("game"),wt.textContent=Xe[L.screen]??L.screen,ze.textContent=Bt(L),L.screen==="slots"?ie(U,Z,L,B):L.screen==="roulette"?De(U,Z,L,B):L.screen==="blackjack"?we(U,Z,L,B):Se(U,Z,L,B,{now:Nt});break;default:_("auth")}}function Bt(e){return e.screen==="crash"||e.screen==="blackjack"?String(e.status??""):e.screen==="slots"||e.screen==="roulette"?String(e.phase??""):""}function Rt(e){vt.textContent=e.account?`Account ${e.account}`:"",le.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,Mt[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(ue.get(t)??10),i.addEventListener("change",()=>{let p=Math.max(1,Math.floor(Number(i.value)||1));i.value=String(p),ue.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));ue.set(t,p),B({Play:{game:t,bet:p}})}),r.append(d,m),a.append(r),le.append(a)}n.length===0&&le.append(o("p","muted","The server did not advertise any games."))}_e.addEventListener("click",()=>pe("login"));je.addEventListener("click",()=>pe("register"));Et.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(j==="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=j==="login"?"Log in":"Create account"}});g("logout").addEventListener("click",()=>B("Logout"));g("show-history").addEventListener("click",()=>B({History:{limit:50}}));g("refresh-lobby").addEventListener("click",()=>B("Info"));g("history-back").addEventListener("click",()=>B("ShowLobby"));g("leave-game").addEventListener("click",()=>{!L||(L.screen==="blackjack"&&L.status==="player"||L.screen==="crash"&&L.status==="running")&&!window.confirm("Leaving now forfeits the active bet. Continue?")||B("Leave")});g("reload-replaced").addEventListener("click",()=>location.reload());g("reload-reconnect").addEventListener("click",()=>location.reload());window.addEventListener("resize",()=>{window.clearTimeout(window.__resizeTimer),window.__resizeTimer=window.setTimeout(()=>{L&&Ge()},140)});window.setInterval(()=>{D&&ee.ping()},25e3);ee.connect();})(); | ||
diff --git a/web/play/index.html b/web/play/index.html index e9d4b79..7cb902f 100644 --- a/web/play/index.html +++ b/web/play/index.html | |||
| @@ -55,6 +55,7 @@ | |||
| 55 | <h1>Lobby</h1> | 55 | <h1>Lobby</h1> |
| 56 | <p class="muted" style="margin: 4px 0 0">Choose a game. Bets are play credits.</p> | 56 | <p class="muted" style="margin: 4px 0 0">Choose a game. Bets are play credits.</p> |
| 57 | </div> | 57 | </div> |
| 58 | <span id="lobby-account" class="label"></span> | ||
| 58 | <span class="spacer"></span> | 59 | <span class="spacer"></span> |
| 59 | <button id="show-history" class="btn ghost" type="button">Play history</button> | 60 | <button id="show-history" class="btn ghost" type="button">Play history</button> |
| 60 | <button id="refresh-lobby" class="btn ghost" type="button">Refresh</button> | 61 | <button id="refresh-lobby" class="btn ghost" type="button">Refresh</button> |
diff --git a/web/play/src/main.ts b/web/play/src/main.ts index 2dd3ce9..15eecf5 100644 --- a/web/play/src/main.ts +++ b/web/play/src/main.ts | |||
| @@ -19,6 +19,7 @@ const gameBoard = byId("game-board"); | |||
| 19 | const gameSide = byId("game-side"); | 19 | const gameSide = byId("game-side"); |
| 20 | const gameTitle = byId("game-title"); | 20 | const gameTitle = byId("game-title"); |
| 21 | const gameBadge = byId("game-badge"); | 21 | const gameBadge = byId("game-badge"); |
| 22 | const lobbyAccount = byId("lobby-account"); | ||
| 22 | const historyBody = byId("history-body"); | 23 | const historyBody = byId("history-body"); |
| 23 | const authForm = byId<HTMLFormElement>("auth-form"); | 24 | const authForm = byId<HTMLFormElement>("auth-form"); |
| 24 | const authUser = byId<HTMLInputElement>("auth-user"); | 25 | const authUser = byId<HTMLInputElement>("auth-user"); |
| @@ -226,6 +227,7 @@ function badge(view: View): string { | |||
| 226 | } | 227 | } |
| 227 | 228 | ||
| 228 | function renderLobby(lobby: View): void { | 229 | function renderLobby(lobby: View): void { |
| 230 | lobbyAccount.textContent = lobby.account ? `Account ${lobby.account}` : ""; | ||
| 229 | gameGrid.replaceChildren(); | 231 | gameGrid.replaceChildren(); |
| 230 | const list: [string, string][] = lobby.games ?? []; | 232 | const list: [string, string][] = lobby.games ?? []; |
| 231 | for (const [id, name] of list) { | 233 | for (const [id, name] of list) { |
diff --git a/web/play/src/slots.ts b/web/play/src/slots.ts index 7c4677d..8b821cf 100644 --- a/web/play/src/slots.ts +++ b/web/play/src/slots.ts | |||
| @@ -39,6 +39,7 @@ export function renderSlots( | |||
| 39 | ): void { | 39 | ): void { |
| 40 | latest = view; | 40 | latest = view; |
| 41 | latestBoard = board; | 41 | latestBoard = board; |
| 42 | if (view.error) spinUntil = 0; | ||
| 42 | if (animation !== undefined) { | 43 | if (animation !== undefined) { |
| 43 | window.clearInterval(animation); | 44 | window.clearInterval(animation); |
| 44 | animation = undefined; | 45 | animation = undefined; |
diff --git a/webd/README.md b/webd/README.md index ac95fdb..7765d54 100644 --- a/webd/README.md +++ b/webd/README.md | |||
| @@ -94,8 +94,10 @@ location = /ws { | |||
| 94 | 94 | ||
| 95 | ## Behaviour | 95 | ## Behaviour |
| 96 | 96 | ||
| 97 | - The WebSocket handshake sets an HttpOnly, SameSite=Strict session cookie. | 97 | - The WebSocket handshake sets an HttpOnly, SameSite=Strict session cookie |
| 98 | There is no credential storage in the browser or gateway. | 98 | (`__Host-zagan_session` on HTTPS, `zagan_session` for local HTTP testing; the |
| 99 | `__Host-` prefix stops a sibling origin from fixing a domain cookie). There is | ||
| 100 | no credential storage in the browser or gateway. | ||
| 99 | - A browser close starts the grace window. Upstream stays connected, crash | 101 | - A browser close starts the grace window. Upstream stays connected, crash |
| 100 | ticks are still applied to the core, and a reconnect with the same cookie | 102 | ticks are still applied to the core, and a reconnect with the same cookie |
| 101 | gets the current `View`. Expiry closes upstream; the server forfeits a live | 103 | gets the current `View`. Expiry closes upstream; the server forfeits a live |
