garrec2 commited on
Commit
88848a4
Β·
verified Β·
1 Parent(s): 87887ad

Upload index.html

Browse files
Files changed (1) hide show
  1. index.html +783 -18
index.html CHANGED
@@ -1,19 +1,784 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Snakes & Ladders Darts</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Luckiest+Guy&display=swap" rel="stylesheet">
8
+ <style>
9
+ * { margin: 0; padding: 0; box-sizing: border-box; }
10
+ body {
11
+ min-height: 100vh;
12
+ background: radial-gradient(circle at 50% 15%, #241a3d, #0d0a18);
13
+ color: #f4f2ff;
14
+ font-family: "Courier New", monospace;
15
+ display: flex; flex-direction: column; align-items: center;
16
+ padding: 14px; gap: 10px;
17
+ -webkit-user-select: none; user-select: none;
18
+ }
19
+ h1 { letter-spacing: 5px; font-size: clamp(17px, 4vw, 26px); }
20
+ h1 .s { color: #ff5b7b; } h1 .l { color: #5ce8a0; }
21
+ #layout { display: flex; gap: 16px; align-items: flex-start; flex-wrap: wrap; justify-content: center; width: 100%; max-width: 1150px; }
22
+ .panel { position: relative; }
23
+ canvas { display: block; border-radius: 14px; touch-action: manipulation; }
24
+ #sl { width: min(94vw, 640px); height: auto; background: #221a38; box-shadow: 0 14px 40px rgba(0,0,0,.55); border-radius: 14px; }
25
+ #playerStrip { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; margin-top: 10px; font-weight: bold; font-size: 13px; letter-spacing: 1px; }
26
+ #playerStrip span { padding: 3px 10px; border-radius: 7px; border: 1.5px solid transparent; }
27
+ #playerStrip span.me { border-color: currentColor; background: rgba(255,255,255,.07); }
28
+ #hint { margin-top: 7px; text-align: center; font-size: 11.5px; opacity: .6; }
29
+ #bust { position: absolute; inset: 0; display: none; align-items: center; justify-content: center; z-index: 5; pointer-events: none; }
30
+ #bust.show { display: flex; }
31
+ #bust span {
32
+ font-size: 68px; font-weight: bold; color: #ff4a4a; letter-spacing: 8px;
33
+ text-shadow: 0 0 26px rgba(255,74,74,.65), 4px 4px 0 #3a0505;
34
+ animation: bustpop .35s ease-out;
35
+ }
36
+ @keyframes bustpop { from { transform: scale(.3) rotate(-6deg); opacity: 0; } to { transform: scale(1) rotate(0deg); opacity: 1; } }
37
+ #winmsg { position: fixed; inset: 0; display: none; align-items: center; justify-content: center; z-index: 150; pointer-events: none; background: rgba(10,6,20,.45); }
38
+ #winmsg.show { display: flex; }
39
+ #winmsg span {
40
+ font-family: 'Luckiest Guy', 'Courier New', monospace;
41
+ font-size: clamp(36px, 9vw, 80px); color: #ffd75e; letter-spacing: 4px; text-align: center;
42
+ text-shadow: 0 0 30px rgba(255,215,94,.7), 4px 4px 0 #7a2020;
43
+ animation: winpop .4s ease-out;
44
+ }
45
+ @keyframes winpop { from { transform: scale(.4); opacity: 0; } to { transform: scale(1); opacity: 1; } }
46
+ #confetti { position: fixed; inset: 0; z-index: 200; pointer-events: none; }
47
+ #dart { width: min(94vw, 420px); height: auto; background: radial-gradient(circle at 50% 45%, #23202e, #14111d); cursor: crosshair; box-shadow: 0 14px 40px rgba(0,0,0,.55); }
48
+ #side { width: min(94vw, 420px); display: flex; flex-direction: column; gap: 8px; }
49
+ #banner {
50
+ background: linear-gradient(90deg, #2a2150, #1c1638);
51
+ border: 2px solid #4a3d85; border-radius: 10px;
52
+ padding: 9px 12px; font-size: 14px; font-weight: bold; letter-spacing: 1px;
53
+ min-height: 42px; display: flex; align-items: center; gap: 10px;
54
+ }
55
+ #banner .dot { width: 14px; height: 14px; border-radius: 50%; border: 2px solid #fff; flex: none; }
56
+ #legend { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; font-size: 10.5px; text-align: center; }
57
+ #legend div { background: rgba(255,255,255,.06); border-radius: 6px; padding: 4px 1px; line-height: 1.35; }
58
+ #legend b { display: block; font-size: 15px; }
59
+ #log { font-size: 12px; opacity: .75; line-height: 1.55; min-height: 76px; background: rgba(0,0,0,.28); border-radius: 8px; padding: 7px 10px; }
60
+ #controls { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
61
+ button, label.toggle {
62
+ font-family: inherit; font-weight: bold; font-size: 12px; letter-spacing: 1px;
63
+ background: #2a2150; color: #f4f2ff; border: 2px solid #4a3d85;
64
+ border-radius: 7px; padding: 7px 12px; cursor: pointer;
65
+ }
66
+ button:hover { background: #37296b; }
67
+ label.toggle input { vertical-align: middle; margin-right: 5px; }
68
+ #overlay { position: fixed; inset: 0; z-index: 50; display: none; align-items: center; justify-content: center; background: rgba(6,4,14,.9); }
69
+ #card { background: #1c1638; border: 3px solid #ffd75e; border-radius: 16px; padding: 26px; max-width: 500px; width: 93vw; text-align: center; max-height: 92vh; overflow-y: auto; }
70
+ #card h2 { color: #ffd75e; letter-spacing: 3px; margin-bottom: 14px; }
71
+ #card p { font-size: 13px; line-height: 1.65; margin-bottom: 12px; opacity: .9; }
72
+ #countrow { display: flex; align-items: center; justify-content: center; gap: 16px; margin: 10px 0 14px; }
73
+ #countrow b { font-size: 30px; color: #ffd75e; min-width: 44px; }
74
+ #countrow button { font-size: 20px; padding: 4px 16px; }
75
+ .nameinp {
76
+ display: block; width: 78%; margin: 7px auto; padding: 8px 12px;
77
+ background: #120e22; color: #fff; border: 2px solid #4a3d85; border-radius: 8px;
78
+ font-family: inherit; font-size: 14px; text-align: center;
79
+ }
80
+ .nameinp:focus { outline: none; border-color: #ffd75e; }
81
+ .bigbtn {
82
+ margin-top: 12px; font-family: inherit; font-weight: bold; font-size: 15px; letter-spacing: 2px;
83
+ background: #ffd75e; color: #1c1638; border: none; padding: 11px 30px; border-radius: 9px; cursor: pointer;
84
+ }
85
+ .bigbtn:hover { filter: brightness(1.08); }
86
+ #podium { font-size: 15px; line-height: 2; margin-bottom: 8px; }
87
+ </style>
88
+ </head>
89
+ <body>
90
+ <h1><span class="s">SNAKES</span> &amp; <span class="l">LADDERS</span> DARTS</h1>
91
+ <div id="layout">
92
+ <div class="panel">
93
+ <canvas id="sl" width="640" height="552"></canvas>
94
+ <div id="playerStrip"></div>
95
+ <div id="hint">Tap the dartboard where each dart lands Β· UNDO takes back a misclick</div>
96
+ <div id="bust"><span>BUST!</span></div>
97
+ </div>
98
+ <div id="side">
99
+ <div class="panel"><canvas id="dart" width="480" height="480"></canvas></div>
100
+ <div id="banner"></div>
101
+ <div id="legend">
102
+ <div style="color:#ff8080"><b>50</b>BULL=6</div>
103
+ <div style="color:#ffb36b"><b>25</b>BULL=5</div>
104
+ <div style="color:#e8d8ff"><b>IN</b>SINGLE=4</div>
105
+ <div style="color:#ffd75e"><b>T</b>TREBLE=3</div>
106
+ <div><b>OUT</b>SINGLE=2</div>
107
+ <div style="color:#8fe8a0"><b>D</b>DOUBLE=1</div>
108
+ <div style="opacity:.55"><b>βœ•</b>MISS=0</div>
109
+ </div>
110
+ <div id="log"></div>
111
+ <div id="controls">
112
+ <button id="undo">UNDO DART</button>
113
+ <button id="soundbtn">πŸ”Š</button>
114
+ </div>
115
+ </div>
116
+ </div>
117
+
118
+ <div id="overlay"><div id="card"></div></div>
119
+ <div id="winmsg"><span></span></div>
120
+ <canvas id="confetti"></canvas>
121
+
122
+ <script>
123
+ "use strict";
124
+ const SECTORS = [20, 1, 18, 4, 13, 6, 10, 15, 2, 17, 3, 19, 7, 16, 8, 11, 14, 9, 12, 5];
125
+ const R = { bullIn: 6.35, bullOut: 15.9, triIn: 99, triOut: 107, dblIn: 162, dblOut: 170 };
126
+
127
+ const LADDERS = [[4, 14, ""], [13, 47, "STAIRWAY TO HEAVEN"], [21, 38, ""], [24, 40, ""], [41, 57, ""], [49, 63, ""]];
128
+ const SNAKES = [
129
+ [27, 9, ""],
130
+ [33, 18, ""],
131
+ [44, 20, "MONTY PYTHON"],
132
+ [54, 28, "JAKE SNAKE"],
133
+ [59, 52, ""],
134
+ [61, 2, "BIG MAMBA", { bias: 30, amp: 13, waves: 3.2, phase: 0.9 }]
135
+ ];
136
+
137
+ const TOKEN_COLORS = ["#ff5b7b", "#ffd75e", "#4ad6ff", "#9d8bff", "#5ce8a0", "#ff9df0"];
138
+
139
+ const slCvs = document.getElementById("sl");
140
+ const slCtx = slCvs.getContext("2d");
141
+ const dCvs = document.getElementById("dart");
142
+ const dCtx = dCvs.getContext("2d");
143
+
144
+ let players = [], turn = 0, phase = "setup", muted = false;
145
+ let pins = [], lastThrow = null, rotToken = 0, busyAnim = false, throwGen = 0;
146
+ let gameWinners = [], catchupQueue = [];
147
+
148
+ const COLS = 8, ROWS = 8, WIN = 64;
149
+ const X0 = 104, Y0 = 20, CW = 64, CH = 64;
150
+
151
+ function sqXY(n) {
152
+ if (n <= 0) return { x: 56, y: Y0 + 7 * CH + CH / 2 };
153
+ const idx = Math.min(WIN, n) - 1;
154
+ const row = Math.floor(idx / COLS), cIn = idx % COLS;
155
+ const col = row % 2 === 0 ? cIn : COLS - 1 - cIn;
156
+ return { x: X0 + col * CW + CW / 2, y: Y0 + (7 - row) * CH + CH / 2 };
157
+ }
158
+
159
+ function snakePathPoint(a, b, si, t, offs) {
160
+ const dx = b.x - a.x, dy = b.y - a.y, len = Math.hypot(dx, dy) || 1;
161
+ const nx = -dy / len, ny = dx / len;
162
+ let w;
163
+ if (offs && offs.length >= 5) {
164
+ const seg = Math.min(3, Math.floor(t * 4));
165
+ const f = t * 4 - seg;
166
+ w = offs[seg] * (1 - f) + offs[seg + 1] * f;
167
+ } else if (offs && offs.bias !== undefined) {
168
+ w = (Math.sin(t * Math.PI * offs.waves + (offs.phase || 0)) * (offs.amp || 13) - offs.bias) * Math.sin(t * Math.PI);
169
+ } else {
170
+ w = Math.sin(t * Math.PI * 2.2 + si * 1.7) * 16 * Math.sin(t * Math.PI);
171
+ }
172
+ return { x: a.x + dx * t + nx * w, y: a.y + dy * t + ny * w };
173
+ }
174
+
175
+ const tokenOverride = {};
176
+ function lerpPt(a, b, t) { return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t }; }
177
+
178
+ const LABEL_FONT = "13px 'Luckiest Guy', 'Courier New', monospace";
179
+ function dirForSquare(n) {
180
+ const idx = n - 1, row = Math.floor(idx / COLS), cIn = idx % COLS;
181
+ const col = row % 2 === 0 ? cIn : COLS - 1 - cIn;
182
+ if (row % 2 === 0) return col === COLS - 1 ? "up" : "right";
183
+ return col === 0 ? "up" : "left";
184
+ }
185
+ function drawArrow(ctx, p, dir, col, alpha) {
186
+ const ang = { right: 0, down: Math.PI / 2, left: Math.PI, up: -Math.PI / 2 }[dir];
187
+ ctx.save();
188
+ ctx.translate(p.x, p.y); ctx.rotate(ang);
189
+ ctx.globalAlpha = alpha;
190
+ ctx.beginPath();
191
+ ctx.moveTo(9, 0); ctx.lineTo(-6, -7); ctx.lineTo(-2.5, 0); ctx.lineTo(-6, 7);
192
+ ctx.closePath();
193
+ ctx.fillStyle = col; ctx.fill();
194
+ ctx.globalAlpha = 1;
195
+ ctx.lineWidth = 1.2; ctx.strokeStyle = "rgba(40,40,60,.75)"; ctx.stroke();
196
+ ctx.restore();
197
+ }
198
+ function drawSquareLabel(n, name, color) {
199
+ const p = sqXY(n);
200
+ const lines = [];
201
+ let cur = "";
202
+ for (const w of name.split(" ")) {
203
+ const test = cur ? cur + " " + w : w;
204
+ if (test.length > 9 && cur) { lines.push(cur); cur = w; } else cur = test;
205
+ }
206
+ if (cur) lines.push(cur);
207
+ const ctx = slCtx;
208
+ ctx.save();
209
+ ctx.font = LABEL_FONT;
210
+ ctx.textAlign = "center"; ctx.textBaseline = "middle";
211
+ ctx.fillStyle = color;
212
+ const lh = 14;
213
+ const y0 = p.y - (lines.length - 1) * lh / 2 + 3;
214
+ lines.forEach((ln, i) => ctx.fillText(ln, p.x, y0 + i * lh));
215
+ ctx.restore();
216
+ }
217
+
218
+ function renderSL() {
219
+ const ctx = slCtx;
220
+ ctx.clearRect(0, 0, 640, 552);
221
+ ctx.fillStyle = "#221a38";
222
+ ctx.fillRect(0, 0, 640, 552);
223
+
224
+ for (let r = 0; r < ROWS; r++) {
225
+ for (let c = 0; c < COLS; c++) {
226
+ const n = r % 2 === 0 ? r * COLS + c + 1 : r * COLS + (COLS - c);
227
+ const x = X0 + c * CW, y = Y0 + (ROWS - 1 - r) * CH;
228
+ ctx.fillStyle = (r + c) % 2 === 0 ? "#f9f4c8" : "#fffdf2";
229
+ ctx.fillRect(x + 1, y + 1, CW - 2, CH - 2);
230
+ ctx.strokeStyle = "rgba(130,120,80,.3)"; ctx.lineWidth = 1;
231
+ ctx.strokeRect(x + 1, y + 1, CW - 2, CH - 2);
232
+ ctx.fillStyle = "#3c3a55";
233
+ ctx.font = "bold 12px 'Courier New', monospace";
234
+ ctx.textAlign = "left"; ctx.textBaseline = "top";
235
+ ctx.fillText(n, x + 6, y + 4);
236
+ if (n === WIN) {
237
+ ctx.font = "50px 'Segoe UI Emoji', 'Apple Color Emoji', 'Noto Color Emoji', sans-serif";
238
+ ctx.textAlign = "center"; ctx.textBaseline = "middle";
239
+ ctx.fillText("πŸ†", x + CW / 2, y + CH / 2 + 2);
240
+ }
241
+ }
242
+ }
243
+
244
+ ctx.save();
245
+ const sy = Y0 + (ROWS - 1) * CH;
246
+ ctx.fillStyle = "#ffd75e";
247
+ ctx.beginPath();
248
+ const rx = 18, ry = sy + 4, rw = 76, rh = CH - 8, rr2 = 10;
249
+ ctx.moveTo(rx + rr2, ry);
250
+ ctx.arcTo(rx + rw, ry, rx + rw, ry + rh, rr2);
251
+ ctx.arcTo(rx + rw, ry + rh, rx, ry + rh, rr2);
252
+ ctx.arcTo(rx, ry + rh, rx, ry, rr2);
253
+ ctx.arcTo(rx, ry, rx + rw, ry, rr2);
254
+ ctx.closePath(); ctx.fill();
255
+ ctx.strokeStyle = "#c9a227"; ctx.lineWidth = 2; ctx.stroke();
256
+ ctx.fillStyle = "#5a4500";
257
+ ctx.font = "bold 13px 'Courier New', monospace";
258
+ ctx.textAlign = "center"; ctx.textBaseline = "middle";
259
+ ctx.fillText("START", rx + rw / 2, ry + rh / 2 - 9);
260
+ ctx.fillText("HERE", rx + rw / 2, ry + rh / 2 + 9);
261
+ ctx.restore();
262
+
263
+ LADDERS.forEach(([from, to, name], li) => {
264
+ const a = sqXY(from), b = sqXY(to);
265
+ const dx = b.x - a.x, dy = b.y - a.y, len = Math.hypot(dx, dy) || 1;
266
+ const ux = -dy / len, uy = dx / len, off = 6;
267
+ ctx.save();
268
+ ctx.strokeStyle = "#e05252"; ctx.lineWidth = 3.5; ctx.lineCap = "round";
269
+ for (const s of [-off, off]) {
270
+ ctx.beginPath();
271
+ ctx.moveTo(a.x + ux * s, a.y + uy * s);
272
+ ctx.lineTo(b.x + ux * s, b.y + uy * s);
273
+ ctx.stroke();
274
+ }
275
+ ctx.lineWidth = 2.5;
276
+ const rungs = Math.max(2, Math.floor(len / 16));
277
+ for (let i = 1; i < rungs; i++) {
278
+ const t = i / rungs, px = a.x + dx * t, py = a.y + dy * t;
279
+ ctx.beginPath();
280
+ ctx.moveTo(px - ux * off, py - uy * off);
281
+ ctx.lineTo(px + ux * off, py + uy * off);
282
+ ctx.stroke();
283
+ }
284
+ ctx.restore();
285
+ });
286
+ LADDERS.forEach(([from, , name]) => { if (name) drawSquareLabel(from, name, "#b23a3a"); });
287
+ SNAKES.forEach((s, si) => {
288
+ const [from, to, name, offs] = s;
289
+ const a = sqXY(from), b = sqXY(to);
290
+ const steps = Math.max(10, Math.floor(Math.hypot(b.x - a.x, b.y - a.y) / 7));
291
+ ctx.save();
292
+ ctx.strokeStyle = "#2fb3a3"; ctx.lineWidth = 7; ctx.lineCap = "round"; ctx.lineJoin = "round";
293
+ ctx.beginPath();
294
+ ctx.moveTo(a.x, a.y);
295
+ for (let i = 1; i <= steps; i++) {
296
+ const p = snakePathPoint(a, b, si, i / steps, offs);
297
+ ctx.lineTo(p.x, p.y);
298
+ }
299
+ ctx.stroke();
300
+ ctx.fillStyle = "#2fb3a3";
301
+ ctx.beginPath(); ctx.arc(b.x, b.y, 7.5, 0, Math.PI * 2); ctx.fill();
302
+ ctx.fillStyle = "#0e2a26";
303
+ ctx.beginPath(); ctx.arc(b.x - 2.4, b.y - 2, 1.7, 0, Math.PI * 2); ctx.fill();
304
+ ctx.beginPath(); ctx.arc(b.x + 2.4, b.y - 2, 1.7, 0, Math.PI * 2); ctx.fill();
305
+ ctx.restore();
306
+ });
307
+ SNAKES.forEach(([from, , name]) => { if (name) drawSquareLabel(from, name, "#0b6b5d"); });
308
+
309
+ if (phase === "play" && !busyAnim && players.length) {
310
+ const me = players[turn];
311
+ const flash = 0.45 + 0.35 * Math.sin(performance.now() / 160);
312
+ for (let k = 1; k <= 6; k++) {
313
+ const n = me.pos + k;
314
+ if (n > WIN) break;
315
+ drawArrow(slCtx, sqXY(n), dirForSquare(n), TOKEN_COLORS[turn], flash);
316
+ }
317
+ }
318
+
319
+ players.forEach((pl, i) => {
320
+ if (phase === "setup") return;
321
+ const over = tokenOverride[i];
322
+ const base = over || sqXY(pl.pos);
323
+ const sameSquare = over ? [pl] : players.filter(o => o.pos === pl.pos);
324
+ const k = sameSquare.indexOf(pl);
325
+ const spread = (!over && sameSquare.length > 1) ? 9 : 0;
326
+ const ang = (k / Math.max(1, sameSquare.length)) * Math.PI * 2;
327
+ const ox = Math.cos(ang) * spread, oy = Math.sin(ang) * spread;
328
+ ctx.save();
329
+ ctx.beginPath(); ctx.arc(base.x + ox, base.y + oy, 15, 0, Math.PI * 2);
330
+ ctx.fillStyle = TOKEN_COLORS[i]; ctx.fill();
331
+ ctx.lineWidth = 2.5; ctx.strokeStyle = "#fff"; ctx.stroke();
332
+ if (i === turn && phase === "play") {
333
+ ctx.beginPath(); ctx.arc(base.x + ox, base.y + oy, 20, 0, Math.PI * 2);
334
+ ctx.strokeStyle = TOKEN_COLORS[i]; ctx.lineWidth = 2; ctx.setLineDash([4, 4]);
335
+ ctx.stroke(); ctx.setLineDash([]);
336
+ }
337
+ ctx.restore();
338
+ });
339
+ }
340
+
341
+ function segAt(px, py) {
342
+ const cx = 240, cy = 240, scale = 240 * 0.81 / 170;
343
+ const r = Math.hypot(px - cx, py - cy) / scale;
344
+ let deg = Math.atan2(py - cy, px - cx) * 180 / Math.PI + 90 + 9;
345
+ deg = ((deg % 360) + 360) % 360;
346
+ if (r <= R.bullIn) return { move: 6, label: "INNER BULL! +6", pinCol: "#ff4a4a" };
347
+ if (r <= R.bullOut) return { move: 5, label: "Outer bull +5", pinCol: "#c62828" };
348
+ if (r > R.dblOut) return null;
349
+ const numStr = String(SECTORS[Math.floor(deg / 18) % 20]);
350
+ if (r >= R.triIn && r <= R.triOut) return { move: 3, label: "TREBLE " + numStr + " +3", pinCol: "#ffd75e" };
351
+ if (r >= R.dblIn && r <= R.dblOut) return { move: 1, label: "DOUBLE " + numStr + " +1", pinCol: "#8fe8a0" };
352
+ if (r < R.triIn) return { move: 4, label: "Inner single " + numStr + " +4", pinCol: "#e8d8ff" };
353
+ return { move: 2, label: "Outer single " + numStr + " +2", pinCol: "#bdbdbd" };
354
+ }
355
+
356
+ function drawDartboard() {
357
+ const ctx = dCtx;
358
+ ctx.clearRect(0, 0, 480, 480);
359
+ const cx = 240, cy = 240, S = 240 * 0.81 / 170;
360
+ const rr = mm => mm * S;
361
+ ctx.save();
362
+ ctx.beginPath(); ctx.arc(cx, cy, rr(210), 0, Math.PI * 2);
363
+ const felt = ctx.createRadialGradient(cx, cy, rr(60), cx, cy, rr(212));
364
+ felt.addColorStop(0, "#2b2f36"); felt.addColorStop(1, "#15181d");
365
+ ctx.fillStyle = felt; ctx.fill();
366
+ for (let i = 0; i < 20; i++) {
367
+ const a0 = (-90 - 9 + i * 18) * Math.PI / 180, a1 = a0 + 18 * Math.PI / 180;
368
+ const dark = i % 2 === 0;
369
+ const single = dark ? "#101114" : "#54565e";
370
+ const ring = dark ? "#c62828" : "#8e1414";
371
+ ctx.beginPath();
372
+ ctx.arc(cx, cy, rr(R.triOut), a0, a1); ctx.arc(cx, cy, rr(R.bullOut), a1, a0, true);
373
+ ctx.closePath(); ctx.fillStyle = single; ctx.fill();
374
+ ctx.beginPath();
375
+ ctx.arc(cx, cy, rr(R.triOut), a0, a1); ctx.arc(cx, cy, rr(R.triIn), a1, a0, true);
376
+ ctx.closePath(); ctx.fillStyle = ring; ctx.fill();
377
+ ctx.beginPath();
378
+ ctx.arc(cx, cy, rr(R.dblOut), a0, a1); ctx.arc(cx, cy, rr(R.dblIn), a1, a0, true);
379
+ ctx.closePath(); ctx.fillStyle = ring; ctx.fill();
380
+ }
381
+ ctx.strokeStyle = "rgba(240,240,245,.7)"; ctx.lineWidth = 1.1;
382
+ for (let i = 0; i < 20; i++) {
383
+ const a = (-90 - 9 + i * 18) * Math.PI / 180;
384
+ ctx.beginPath();
385
+ ctx.moveTo(cx + Math.cos(a) * rr(R.bullOut), cy + Math.sin(a) * rr(R.bullOut));
386
+ ctx.lineTo(cx + Math.cos(a) * rr(R.dblOut), cy + Math.sin(a) * rr(R.dblOut));
387
+ ctx.stroke();
388
+ }
389
+ [R.triIn, R.triOut, R.dblIn, R.dblOut, R.bullIn, R.bullOut].forEach(rd => {
390
+ ctx.beginPath(); ctx.arc(cx, cy, rr(rd), 0, Math.PI * 2); ctx.stroke();
391
+ });
392
+ ctx.beginPath(); ctx.arc(cx, cy, rr(R.bullOut), 0, Math.PI * 2); ctx.fillStyle = "#c62828"; ctx.fill();
393
+ ctx.beginPath(); ctx.arc(cx, cy, rr(R.bullIn), 0, Math.PI * 2); ctx.fillStyle = "#ff4a4a"; ctx.fill();
394
+ ctx.fillStyle = "#f4f4f6"; ctx.font = "bold 17px 'Courier New', monospace";
395
+ ctx.textAlign = "center"; ctx.textBaseline = "middle";
396
+ for (let i = 0; i < 20; i++) {
397
+ const a = (-90 + i * 18) * Math.PI / 180;
398
+ ctx.fillText(String(SECTORS[i]), cx + Math.cos(a) * rr(189), cy + Math.sin(a) * rr(189));
399
+ }
400
+ const vig = ctx.createRadialGradient(cx, cy, rr(120), cx, cy, rr(212));
401
+ vig.addColorStop(0, "rgba(0,0,0,0)"); vig.addColorStop(1, "rgba(0,0,0,.38)");
402
+ ctx.beginPath(); ctx.arc(cx, cy, rr(210), 0, Math.PI * 2); ctx.fillStyle = vig; ctx.fill();
403
+
404
+ pins.forEach(p => {
405
+ ctx.beginPath(); ctx.arc(p.x, p.y, 5, 0, Math.PI * 2);
406
+ ctx.fillStyle = p.col || "#eee"; ctx.fill();
407
+ ctx.lineWidth = 1.4; ctx.strokeStyle = "#444"; ctx.stroke();
408
+ });
409
+
410
+ ctx.restore();
411
+ }
412
+
413
+ function banner(html) { document.getElementById("banner").innerHTML = html; }
414
+ function logMsg(t) {
415
+ const l = document.getElementById("log");
416
+ const line = document.createElement("div");
417
+ line.textContent = "β€Ί " + t;
418
+ l.prepend(line);
419
+ while (l.children.length > 5) l.lastChild.remove();
420
+ }
421
+ function updateBanner(extra) {
422
+ const pl = players[turn];
423
+ banner("<span class='dot' style='background:" + TOKEN_COLORS[turn] + "'></span>" +
424
+ "<span>" + pl.name + " β€” dart " + (pl.dartsThisTurn + 1) + " of 3" + (extra ? " Β· " + extra : "") + "</span>");
425
+ renderPlayers();
426
+ }
427
+ function renderPlayers() {
428
+ const el = document.getElementById("playerStrip");
429
+ el.innerHTML = players.map((p, i) => {
430
+ const act = i === turn && phase === "play";
431
+ return "<span class='" + (act ? "me" : "") + "' style='color:" + TOKEN_COLORS[i] + "'>" +
432
+ (act ? "β–Ά " : "") + p.name + "</span>";
433
+ }).join("");
434
+ }
435
+
436
+ async function animateMove(pIdx, from, to) {
437
+ busyAnim = true;
438
+ const step = to > from ? 1 : -1;
439
+ for (let pos = from + step; ; pos += step) {
440
+ players[pIdx].pos = pos;
441
+ renderSL();
442
+ tone(step > 0 ? 380 + pos * 2 : 300, .05, "square", .04);
443
+ await sleep(130);
444
+ if (pos === to) break;
445
+ }
446
+ const land = players[pIdx].pos;
447
+ const ladder = LADDERS.find(l => l[0] === land);
448
+ const snakeIdx = SNAKES.findIndex(s => s[0] === land);
449
+ if (ladder) {
450
+ logMsg(players[pIdx].name + " climbs a ladder " + ladder[0] + " β†’ " + ladder[1] + "!");
451
+ sfxClimb();
452
+ await glideAlong(pIdx, t => lerpPt(sqXY(land), sqXY(ladder[1]), t), 640);
453
+ players[pIdx].pos = ladder[1];
454
+ } else if (snakeIdx >= 0) {
455
+ const sn = SNAKES[snakeIdx];
456
+ logMsg(players[pIdx].name + " slides down a snake " + sn[0] + " β†’ " + sn[1] + "... ouch.");
457
+ sfxSlide();
458
+ await glideAlong(pIdx, t => snakePathPoint(sqXY(land), sqXY(sn[1]), snakeIdx, t, sn[3]), 760);
459
+ players[pIdx].pos = sn[1];
460
+ }
461
+ renderSL();
462
+ busyAnim = false;
463
+ }
464
+
465
+ function glideAlong(pIdx, getPt, ms) {
466
+ return new Promise(res => {
467
+ const t0 = performance.now();
468
+ function fr(now) {
469
+ const t = Math.min(1, (now - t0) / ms);
470
+ const e = t < .5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
471
+ tokenOverride[pIdx] = getPt(e);
472
+ renderSL();
473
+ if (t < 1) requestAnimationFrame(fr);
474
+ else { delete tokenOverride[pIdx]; res(); }
475
+ }
476
+ requestAnimationFrame(fr);
477
+ });
478
+ }
479
+
480
+ async function resolveThrow(seg) {
481
+ const pl = players[turn];
482
+ logMsg(pl.name + ": " + seg.label);
483
+ const from = pl.pos;
484
+ const dest = from + seg.move;
485
+ if (seg.move > 0 && dest > WIN) return "bust";
486
+ await animateMove(turn, from, dest);
487
+ renderSL();
488
+ if (pl.pos === WIN) {
489
+ onWin(turn);
490
+ return true;
491
+ }
492
+ return false;
493
+ }
494
+
495
+ async function bustSequence(pIdx) {
496
+ busyAnim = true;
497
+ rotToken++;
498
+ logMsg(players[pIdx].name + " went BUST β€” stays on " + players[pIdx].pos + "!");
499
+ sfxBust();
500
+ const el = document.getElementById("bust");
501
+ el.classList.add("show");
502
+ await sleep(1500);
503
+ el.classList.remove("show");
504
+ busyAnim = false;
505
+ if (phase !== "play") return;
506
+ endTurnCommon();
507
+ }
508
+
509
+ dCvs.addEventListener("pointerdown", e => {
510
+ if (phase !== "play" || busyAnim || e.button !== 0) return;
511
+ initAudio();
512
+ sfxThock();
513
+ const r = dCvs.getBoundingClientRect();
514
+ registerDart((e.clientX - r.left) / r.width * 480, (e.clientY - r.top) / r.height * 480);
515
+ });
516
+
517
+ async function registerDart(tx, ty) {
518
+ const pl = players[turn];
519
+ const seg = segAt(tx, ty);
520
+ pins.push({ x: tx, y: ty, col: TOKEN_COLORS[turn] });
521
+ drawDartboard();
522
+ pl.thrown++;
523
+ pl.dartsThisTurn++;
524
+ throwGen++;
525
+ lastThrow = { turn, posBefore: pl.pos, gen: throwGen };
526
+ if (!seg) {
527
+ logMsg(pl.name + ": scored zero β€” no move, keep throwing!");
528
+ sfxMiss();
529
+ } else {
530
+ const r = await resolveThrow(seg);
531
+ if (r === true) return;
532
+ if (r === "bust") { await bustSequence(turn); return; }
533
+ }
534
+ if (phase !== "play") return;
535
+ if (pl.dartsThisTurn >= 3) {
536
+ const token = ++rotToken;
537
+ setTimeout(() => {
538
+ if (phase !== "play" || token !== rotToken || busyAnim) return;
539
+ endTurnCommon();
540
+ }, 400);
541
+ } else {
542
+ updateBanner();
543
+ }
544
+ }
545
+
546
+ function endTurnCommon() {
547
+ players[turn].turns++;
548
+ pins = [];
549
+ if (gameWinners.length) { afterWinFlash(); return; }
550
+ turn = (turn + 1) % players.length;
551
+ players[turn].dartsThisTurn = 0;
552
+ updateBanner();
553
+ renderSL();
554
+ logMsg(players[turn].name + "'s turn.");
555
+ }
556
+
557
+ function afterWinFlash() {
558
+ if (catchupQueue.length) {
559
+ phase = "play";
560
+ turn = catchupQueue.shift();
561
+ players[turn].dartsThisTurn = 0;
562
+ pins = [];
563
+ updateBanner();
564
+ renderSL();
565
+ logMsg("CATCH-UP TURN: " + players[turn].name + " β€” reach 64 to tie!");
566
+ } else {
567
+ showResultsCard();
568
+ }
569
+ }
570
+
571
+ function onWin(pIdx) {
572
+ if (!gameWinners.length) {
573
+ gameWinners.push(pIdx);
574
+ const T = players[pIdx].turns;
575
+ const order = [];
576
+ for (let i = 1; i <= players.length; i++) {
577
+ const j = (pIdx + i) % players.length;
578
+ if (players[j].turns < T) order.push(j);
579
+ }
580
+ catchupQueue = order;
581
+ } else if (!gameWinners.includes(pIdx)) {
582
+ gameWinners.push(pIdx);
583
+ catchupQueue = catchupQueue.filter(i => i !== pIdx);
584
+ }
585
+ phase = "celebrate";
586
+ winSequence(pIdx, afterWinFlash);
587
+ }
588
+ dCvs.addEventListener("contextmenu", e => e.preventDefault());
589
+
590
+ document.getElementById("undo").onclick = () => {
591
+ if (phase !== "play" || !lastThrow || busyAnim) return;
592
+ if (throwGen !== lastThrow.gen) {
593
+ logMsg("Too late to undo β€” the next player has already thrown.");
594
+ return;
595
+ }
596
+ rotToken++;
597
+ const lt = lastThrow;
598
+ const pl = players[lt.turn];
599
+ pl.pos = lt.posBefore;
600
+ pl.thrown--;
601
+ pl.dartsThisTurn = Math.max(0, pl.dartsThisTurn - 1);
602
+ pins.pop();
603
+ lastThrow = null;
604
+ turn = lt.turn;
605
+ logMsg("Last dart taken back.");
606
+ updateBanner();
607
+ renderSL();
608
+ };
609
+
610
+ document.getElementById("soundbtn").onclick = e => {
611
+ muted = !muted;
612
+ e.target.textContent = muted ? "πŸ”‡" : "πŸ”Š";
613
+ };
614
+
615
+ let AC = null;
616
+ function initAudio() {
617
+ if (!AC) { try { AC = new (window.AudioContext || window.webkitAudioContext)(); } catch (err) {} }
618
+ if (AC && AC.state === "suspended") AC.resume();
619
+ }
620
+ function tone(f, dur, type, vol, delay) {
621
+ if (muted || !AC) return;
622
+ try {
623
+ const t0 = AC.currentTime + (delay || 0);
624
+ const o = AC.createOscillator(), g = AC.createGain();
625
+ o.type = type || "square"; o.frequency.setValueAtTime(f, t0);
626
+ g.gain.setValueAtTime(vol || .05, t0);
627
+ g.gain.exponentialRampToValueAtTime(.0001, t0 + dur);
628
+ o.connect(g); g.connect(AC.destination);
629
+ o.start(t0); o.stop(t0 + dur + .02);
630
+ } catch (err) {}
631
+ }
632
+
633
+ function sfxThock() { tone(120, .06, "square", .09); }
634
+ function sfxMiss() { tone(55, .12, "triangle", .05); }
635
+ function sfxClimb() { [330, 415, 494, 622, 740].forEach((f, i) => tone(f, .1, "square", .05, i * .07)); }
636
+ function sfxSlide() { [600, 500, 410, 330, 260, 200].forEach((f, i) => tone(f, .12, "triangle", .05, i * .08)); }
637
+ function sfxBust() { [320, 250, 190, 130, 90].forEach((f, i) => tone(f, .16, "sawtooth", .07, i * .1)); }
638
+ function sfxConfirm() { tone(660, .06, "square", .05); }
639
+ function sfxFanfare() {
640
+ const lead = [[392, 0, .18], [523, .18, .18], [659, .36, .18], [784, .54, .32], [659, .92, .15], [784, 1.12, .15], [1047, 1.32, .5], [784, 1.95, .15], [1047, 2.15, .75]];
641
+ lead.forEach(([f, d, len]) => tone(f, len, "square", .06, d));
642
+ [[131, 0, .5], [196, .6, .5], [262, 1.3, .7], [196, 2.1, .85]].forEach(([f, d, len]) => tone(f, len, "triangle", .08, d));
643
+ [[262, 2.15, .75], [330, 2.15, .75]].forEach(([f, d, len]) => tone(f, len, "square", .035, d));
644
+ }
645
+ function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
646
+
647
+ const cCvs = document.getElementById("confetti");
648
+ const cCtx = cCvs.getContext("2d");
649
+ let confettiRAF = 0, confettiLast = 0;
650
+ function startConfetti(ms) {
651
+ cCvs.width = innerWidth; cCvs.height = innerHeight;
652
+ const COLORS = ["#ff5b7b", "#ffd75e", "#4ad6ff", "#5ce8a0", "#9d8bff", "#ff9df0", "#ffffff"];
653
+ const bits = [];
654
+ for (let i = 0; i < 170; i++) {
655
+ bits.push({
656
+ x: Math.random() * cCvs.width, y: -20 - Math.random() * cCvs.height * .6,
657
+ w: 5 + Math.random() * 6, h: 8 + Math.random() * 8,
658
+ vy: 110 + Math.random() * 150, vx: (Math.random() - .5) * 40,
659
+ rot: Math.random() * Math.PI * 2, vr: (Math.random() - .5) * 7,
660
+ col: COLORS[i % COLORS.length], sway: Math.random() * Math.PI * 2
661
+ });
662
+ }
663
+ const t0 = performance.now();
664
+ confettiLast = t0;
665
+ cancelAnimationFrame(confettiRAF);
666
+ function fr(now) {
667
+ const dt = Math.min(.05, (now - confettiLast) / 1000);
668
+ confettiLast = now;
669
+ cCtx.clearRect(0, 0, cCvs.width, cCvs.height);
670
+ let alive = false;
671
+ for (const p of bits) {
672
+ p.y += p.vy * dt;
673
+ p.sway += dt * 3;
674
+ p.x += (p.vx + Math.sin(p.sway) * 34) * dt;
675
+ p.rot += p.vr * dt;
676
+ if (p.y < cCvs.height + 24) alive = true;
677
+ cCtx.save();
678
+ cCtx.translate(p.x, p.y); cCtx.rotate(p.rot);
679
+ cCtx.fillStyle = p.col;
680
+ cCtx.fillRect(-p.w / 2, -p.h / 2, p.w, p.h);
681
+ cCtx.restore();
682
+ }
683
+ if (alive && now - t0 < ms) confettiRAF = requestAnimationFrame(fr);
684
+ else cCtx.clearRect(0, 0, cCvs.width, cCvs.height);
685
+ }
686
+ confettiRAF = requestAnimationFrame(fr);
687
+ }
688
+
689
+ async function winSequence(w, cb) {
690
+ sfxFanfare();
691
+ const el = document.getElementById("winmsg");
692
+ el.querySelector("span").textContent = players[w].name + " WINS!";
693
+ el.classList.add("show");
694
+ startConfetti(3000);
695
+ await sleep(3000);
696
+ el.classList.remove("show");
697
+ if (cb) cb();
698
+ }
699
+
700
+ function setupCard() {
701
+ phase = "setup";
702
+ const card = document.getElementById("card");
703
+ card.innerHTML =
704
+ "<h2>SNAKES &amp; LADDERS DARTS</h2>" +
705
+ "<p>Your dart is the dice: double 1 Β· outer single 2 Β· treble 3 Β· inner single 4 Β· outer bull 5 Β· inner bull 6.<br>Ladders lift you, snakes drag you down. Land exactly on 64 β€” overshooting is a BUST: you stay put and your turn ends!</p>" +
706
+ "<p>Play with real darts β€” after each throw, tap the spot on the board where it landed.</p>" +
707
+ "<div id='countrow'><button id='minus'>βˆ’</button><b id='cnt'>2</b><button id='plus'>+</button></div>" +
708
+ "<p style='font-size:11px'>PLAYERS (tap a name to edit)</p>" +
709
+ "<div id='names'></div>" +
710
+ "<button class='bigbtn' id='startbtn'>START GAME</button>";
711
+ document.getElementById("overlay").style.display = "flex";
712
+ let count = 2;
713
+ const namesDiv = document.getElementById("names");
714
+ function buildNames() {
715
+ namesDiv.innerHTML = "";
716
+ for (let i = 0; i < count; i++) {
717
+ const inp = document.createElement("input");
718
+ inp.className = "nameinp";
719
+ inp.maxLength = 12;
720
+ inp.placeholder = "PLAYER " + (i + 1);
721
+ inp.style.borderTopColor = TOKEN_COLORS[i];
722
+ namesDiv.appendChild(inp);
723
+ }
724
+ }
725
+ buildNames();
726
+ document.getElementById("minus").onclick = () => { if (count > 2) { count--; buildNamesKeep(namesDiv, count); document.getElementById("cnt").textContent = count; sfxConfirm(); } };
727
+ document.getElementById("plus").onclick = () => { if (count < 6) { count++; buildNamesKeep(namesDiv, count); document.getElementById("cnt").textContent = count; sfxConfirm(); } };
728
+ document.getElementById("startbtn").onclick = () => {
729
+ initAudio(); sfxConfirm();
730
+ const names = [...namesDiv.querySelectorAll("input")].map((el, i) => el.value.trim() || ("PLAYER " + (i + 1)));
731
+ startGame(names);
732
+ };
733
+ }
734
+ function buildNamesKeep(div, count) {
735
+ const old = [...div.querySelectorAll("input")].map(el => el.value);
736
+ div.innerHTML = "";
737
+ for (let i = 0; i < count; i++) {
738
+ const inp = document.createElement("input");
739
+ inp.className = "nameinp"; inp.maxLength = 12;
740
+ inp.placeholder = "PLAYER " + (i + 1);
741
+ inp.value = old[i] || "";
742
+ div.appendChild(inp);
743
+ }
744
+ }
745
+
746
+ function startGame(names) {
747
+ players = names.map((n, i) => ({ name: n.toUpperCase(), pos: 0, thrown: 0, dartsThisTurn: 0, turns: 0 }));
748
+ turn = 0; pins = []; lastThrow = null; phase = "play";
749
+ gameWinners = []; catchupQueue = [];
750
+ document.getElementById("log").innerHTML = "";
751
+ hideOverlay();
752
+ updateBanner();
753
+ renderSL();
754
+ logMsg(players[0].name + " throws first β€” tap where each dart lands!");
755
+ }
756
+
757
+ function showResultsCard() {
758
+ phase = "over";
759
+ const tie = gameWinners.length > 1;
760
+ const order = [...players].sort((a, b) => b.pos - a.pos);
761
+ const card = document.getElementById("card");
762
+ const title = tie ? "IT'S A TIE!" : "πŸ† " + players[gameWinners[0]].name + " WINS!";
763
+ card.innerHTML =
764
+ "<h2>" + title + "</h2>" +
765
+ (tie ? "<p>" + gameWinners.map(i => players[i].name).join(" & ") + " all reached square 64!</p>" : "") +
766
+ "<div id='podium'>" + order.map((pl, i) =>
767
+ (i + 1) + ". " + pl.name + " β€” square " + pl.pos + (gameWinners.includes(i) ? " πŸ†" : "")).join("<br>") + "</div>" +
768
+ "<button class='bigbtn' id='rematch'>PLAY AGAIN</button>";
769
+ document.getElementById("overlay").style.display = "flex";
770
+ document.getElementById("rematch").onclick = () => { sfxConfirm(); setupCard(); };
771
+ }
772
+ function hideOverlay() { document.getElementById("overlay").style.display = "none"; }
773
+
774
+ renderSL();
775
+ drawDartboard();
776
+ setupCard();
777
+ if (document.fonts && document.fonts.ready) document.fonts.ready.then(() => renderSL());
778
+ (function slLoop() {
779
+ if (phase === "play" && !busyAnim) renderSL();
780
+ requestAnimationFrame(slLoop);
781
+ })();
782
+ </script>
783
+ </body>
784
  </html>