Spaces:
Running
Running
Upload index.html
Browse files- index.html +783 -18
index.html
CHANGED
|
@@ -1,19 +1,784 @@
|
|
| 1 |
-
<!
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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> & <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 & 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>
|