Chotto.app
Login
Sign Up
ES
App Name
kuyawa / ludo
App Icon in SVG
<svg width="128" height="128" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"> <!-- App Icon Background (Rounded Square) --> <rect x="2" y="2" width="124" height="124" rx="26" fill="#3A55A4" stroke="#2D4386" stroke-width="4"/> <!-- Stylized Ludo Board Paths and Grid (Simplified) --> <g fill="#F0F0F0" fill-opacity="0.2"> <rect x="36" y="2" width="56" height="124" /> <rect x="2" y="36" width="124" height="56" /> </g> <g fill="none" stroke="#F0F0F0" stroke-opacity="0.3" stroke-width="1.5"> <path d="M54,2 L54,126 M74,2 L74,126 M94,2 L94,126 M114,2 L114,126"/> <path d="M2,54 L126,54 M2,74 L126,74 M2,94 L126,94 M2,114 L126,114"/> </g> <!-- Four Color Quadrants at the Center --> <rect x="42" y="42" width="22" height="22" fill="#E74C3C"/> <!-- Red --> <rect x="64" y="42" width="22" height="22" fill="#2ECC71"/> <!-- Green --> <rect x="42" y="64" width="22" height="22" fill="#3498DB"/> <!-- Blue --> <rect x="64" y="64" width="22" height="22" fill="#F1C40F"/> <!-- Yellow --> <!-- Central Die --> <rect x="52" y="52" width="24" height="24" rx="4" fill="white" stroke="#333" stroke-width="2"/> <circle cx="58" cy="58" r="2.5" fill="black"/> <!-- Die face '1' with depth for icon --> <circle cx="64" cy="64" r="2.5" fill="black"/> <circle cx="70" cy="70" r="2.5" fill="black"/> <circle cx="70" cy="58" r="2.5" fill="#E74C3C"/> <!-- Special '6' side dot --> <circle cx="58" cy="70" r="2.5" fill="#E74C3C"/> <!-- Colored Ludo Tokens (One in Each Quadrant) --> <circle cx="48" cy="48" r="8" fill="#E74C3C" stroke="#B03A2E" stroke-width="1.5"/> <!-- Red --> <circle cx="80" cy="48" r="8" fill="#2ECC71" stroke="#27AE60" stroke-width="1.5"/> <!-- Green --> <circle cx="48" cy="80" r="8" fill="#3498DB" stroke="#2980B9" stroke-width="1.5"/> <!-- Blue --> <circle cx="80" cy="80" r="8" fill="#F1C40F" stroke="#F39C12" stroke-width="1.5"/> <!-- Yellow --> </svg>
HTML Content
<!DOCTYPE html> <html lang="en" data-theme="dark"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Ludo</title> <style> *,*::before,*::after{box-sizing:border-box;margin:0;padding:0} :root{ --red:#ef4444;--green:#22c55e;--yellow:#eab308;--blue:#2563eb; --red-s:rgba(239,68,68,.12);--green-s:rgba(34,197,94,.12);--yellow-s:rgba(234,179,8,.12);--blue-s:rgba(37,99,235,.12); --red-g:rgba(239,68,68,.5);--green-g:rgba(34,197,94,.5);--yellow-g:rgba(234,179,8,.5);--blue-g:rgba(37,99,235,.5); } [data-theme="dark"]{ --bg:#08080d;--surface:#111118;--board:#18181f;--cell:#1f1f2a;--cell-b:#2a2a38; --text:#e8e8f0;--text-m:#7a7a90;--card:#14141c;--card-b:#22222e; --dice-bg:#f0ece6;--dice-dot:#1a1a2a;--safe-star:rgba(255,255,255,.18); --home-inner:#1a1a24;--center-cell:#12121a; --overlay:rgba(0,0,0,.7);--toast-bg:#1e1e2a; } [data-theme="light"]{ --bg:#f0ebe2;--surface:#faf8f4;--board:#fff;--cell:#f5f2ec;--cell-b:#e4dfd6; --text:#2a2a35;--text-m:#8a8a96;--card:#fff;--card-b:#e4dfd6; --dice-bg:#fff;--dice-dot:#1a1a2a;--safe-star:rgba(0,0,0,.1); --home-inner:#fff;--center-cell:#f5f2ec; --overlay:rgba(0,0,0,.45);--toast-bg:#fff; } html{font-family:'Outfit',sans-serif;background:var(--bg);color:var(--text);transition:background .4s,color .4s} body{min-height:100vh;display:flex;flex-direction:column;align-items:center;overflow-x:hidden} body::before{content:'';position:fixed;inset:0;background:radial-gradient(ellipse at 30% 20%,rgba(239,68,68,.04),transparent 60%),radial-gradient(ellipse at 70% 80%,rgba(37,99,235,.04),transparent 60%);pointer-events:none;z-index:0} header{width:100%;max-width:900px;display:flex;align-items:center;justify-content:space-between;padding:16px 20px 8px;position:relative;z-index:2} .logo{font-size:1.6rem;font-weight:900;letter-spacing:-.02em;background:linear-gradient(135deg,var(--red),var(--yellow),var(--green),var(--blue));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text} .hdr-btns{display:flex;gap:8px} .icon-btn{width:38px;height:38px;border-radius:10px;border:1px solid var(--card-b);background:var(--card);color:var(--text-m);display:flex;align-items:center;justify-content:center;cursor:pointer;transition:all .2s;font-size:.95rem} .icon-btn:hover{color:var(--text);border-color:var(--text-m);transform:translateY(-1px)} main{flex:1;display:flex;gap:24px;align-items:flex-start;justify-content:center;padding:8px 16px 24px;position:relative;z-index:1;width:100%;max-width:960px} #board-wrap{position:relative;flex-shrink:0;width:min(70vw,540px);aspect-ratio:1} #board{display:grid;grid-template-columns:repeat(15,1fr);grid-template-rows:repeat(15,1fr);width:100%;height:100%;gap:1.5px;background:var(--cell-b);border-radius:14px;overflow:hidden;box-shadow:0 8px 40px rgba(0,0,0,.35),0 0 0 1px var(--cell-b)} .cell{position:relative;display:flex;align-items:center;justify-content:center;transition:background .3s} .cell.path{background:var(--cell)} .cell.home-outer{background:var(--player-color)} .cell.home-inner{background:var(--home-inner)} .cell.home-inner.tl{border-top-left-radius:10px} .cell.home-inner.tr{border-top-right-radius:10px} .cell.home-inner.bl{border-bottom-left-radius:10px} .cell.home-inner.br{border-bottom-right-radius:10px} .cell.home-col{background:var(--player-color);opacity:.35} .cell.entry{background:var(--player-color);opacity:.7} .cell.safe .star-icon{color:var(--safe-star);font-size:.55em;position:absolute;pointer-events:none} .cell.center-cell{background:var(--center-cell)} .cell.center-player{opacity:.55} .cell.center-mid{position:relative} .cell.center-mid::after{content:'';position:absolute;width:60%;height:60%;border-radius:50%;background:var(--bg);border:2px solid var(--cell-b)} .token-spot{width:55%;height:55%;border-radius:50%;border:2px solid var(--player-color);opacity:.4;position:absolute} #tokens-layer{position:absolute;inset:0;pointer-events:none;z-index:5} .token{position:absolute;border-radius:50%;pointer-events:auto;cursor:default;transition:left .35s cubic-bezier(.4,0,.2,1),top .35s cubic-bezier(.4,0,.2,1),transform .2s,box-shadow .2s;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:.55em;color:#fff;text-shadow:0 1px 2px rgba(0,0,0,.5);z-index:6} .token::after{content:'';position:absolute;inset:15%;border-radius:50%;background:linear-gradient(135deg,rgba(255,255,255,.35),transparent 60%);pointer-events:none} .token.clickable{cursor:pointer;animation:tokenPulse 1s ease-in-out infinite} .token.clickable:hover{transform:translate(-50%,-50%) scale(1.2);z-index:10} @keyframes tokenPulse{0%,100%{box-shadow:0 0 0 0 var(--glow)}50%{box-shadow:0 0 0 6px transparent}} .token.captured{animation:captureFlash .4s ease} @keyframes captureFlash{0%{transform:translate(-50%,-50%) scale(1)}50%{transform:translate(-50%,-50%) scale(1.5);opacity:.5}100%{transform:translate(-50%,-50%) scale(1);opacity:1}} aside{flex:1;min-width:200px;max-width:280px;display:flex;flex-direction:column;gap:14px} .card{background:var(--card);border:1px solid var(--card-b);border-radius:14px;padding:18px;transition:background .3s,border-color .3s} #players-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px} .player-card{display:flex;align-items:center;gap:8px;padding:10px 12px;border-radius:10px;border:2px solid transparent;transition:all .3s;cursor:default} .player-card.active{border-color:var(--player-color);background:var(--player-s)} .player-card .dot{width:18px;height:18px;border-radius:50%;flex-shrink:0;box-shadow:0 2px 6px rgba(0,0,0,.3)} .player-card .pname{font-weight:600;font-size:.82rem} .player-card .pfinished{margin-left:auto;font-size:.72rem;color:var(--text-m);font-weight:400} .player-card .pfinished i{margin-right:2px;font-size:.6rem} #dice-area{display:flex;flex-direction:column;align-items:center;gap:14px} #dice-container{perspective:500px;width:72px;height:72px} #dice{width:72px;height:72px;position:relative;transform-style:preserve-3d;transition:transform 1.1s cubic-bezier(.15,.8,.25,1);cursor:pointer} .dice-face{position:absolute;width:72px;height:72px;border-radius:12px;background:var(--dice-bg);display:grid;grid-template-columns:repeat(3,1fr);grid-template-rows:repeat(3,1fr);padding:10px;gap:2px;backface-visibility:hidden;box-shadow:inset 0 0 8px rgba(0,0,0,.08)} .dice-face.f1{transform:rotateY(0deg) translateZ(36px)} .dice-face.f2{transform:rotateY(90deg) translateZ(36px)} .dice-face.f3{transform:rotateX(90deg) translateZ(36px)} .dice-face.f4{transform:rotateX(-90deg) translateZ(36px)} .dice-face.f5{transform:rotateY(-90deg) translateZ(36px)} .dice-face.f6{transform:rotateY(180deg) translateZ(36px)} .ddot{width:100%;height:100%;border-radius:50%;background:var(--dice-dot);align-self:center;justify-self:center;max-width:14px;max-height:14px} .ddot.hide{visibility:hidden} #roll-btn{width:100%;padding:12px;border:none;border-radius:10px;background:linear-gradient(135deg,var(--red),#dc2626);color:#fff;font-family:inherit;font-size:.95rem;font-weight:700;cursor:pointer;transition:all .2s;letter-spacing:.02em} #roll-btn:hover:not(:disabled){transform:translateY(-2px);box-shadow:0 6px 20px rgba(239,68,68,.35)} #roll-btn:active:not(:disabled){transform:translateY(0)} #roll-btn:disabled{opacity:.4;cursor:not-allowed} #msg{font-size:.8rem;color:var(--text-m);text-align:center;min-height:2.4em;line-height:1.4} #mode-toggle{display:flex;gap:4px;background:var(--surface);border-radius:8px;padding:3px;border:1px solid var(--card-b)} #mode-toggle button{flex:1;padding:6px 8px;border:none;border-radius:6px;background:transparent;color:var(--text-m);font-family:inherit;font-size:.75rem;font-weight:600;cursor:pointer;transition:all .2s} #mode-toggle button.active{background:var(--card);color:var(--text);box-shadow:0 1px 4px rgba(0,0,0,.15)} #toast-container{position:fixed;top:20px;right:20px;z-index:100;display:flex;flex-direction:column;gap:8px;pointer-events:none} .toast{background:var(--toast-bg);border:1px solid var(--card-b);color:var(--text);padding:10px 18px;border-radius:10px;font-size:.82rem;font-weight:500;box-shadow:0 4px 20px rgba(0,0,0,.25);animation:toastIn .3s ease,toastOut .3s ease 2.2s forwards;pointer-events:auto} @keyframes toastIn{from{opacity:0;transform:translateX(40px)}to{opacity:1;transform:translateX(0)}} @keyframes toastOut{to{opacity:0;transform:translateX(40px)}} #win-overlay{position:fixed;inset:0;background:var(--overlay);z-index:50;display:none;align-items:center;justify-content:center;flex-direction:column;gap:20px;animation:fadeIn .4s ease} #win-overlay.show{display:flex} @keyframes fadeIn{from{opacity:0}to{opacity:1}} #win-box{background:var(--card);border:1px solid var(--card-b);border-radius:20px;padding:40px 50px;text-align:center;animation:popIn .5s cubic-bezier(.2,.8,.3,1.2)} @keyframes popIn{from{transform:scale(.7);opacity:0}to{transform:scale(1);opacity:1}} #win-box h2{font-size:2rem;font-weight:900;margin-bottom:6px} #win-box p{color:var(--text-m);margin-bottom:20px;font-size:.9rem} #win-box button{padding:10px 32px;border:none;border-radius:10px;background:var(--text);color:var(--bg);font-family:inherit;font-size:.9rem;font-weight:700;cursor:pointer;transition:all .2s} #win-box button:hover{transform:translateY(-2px);box-shadow:0 4px 16px rgba(0,0,0,.3)} .confetti{position:fixed;width:8px;height:8px;border-radius:2px;z-index:51;pointer-events:none;animation:confettiFall linear forwards} @keyframes confettiFall{0%{opacity:1;transform:translateY(0) rotate(0deg)}100%{opacity:0;transform:translateY(100vh) rotate(720deg)}} #help-modal{position:fixed;inset:0;background:var(--overlay);z-index:60;display:none;align-items:center;justify-content:center;padding:20px} #help-modal.show{display:flex} #help-box{background:var(--card);border:1px solid var(--card-b);border-radius:18px;padding:28px;max-width:420px;width:100%;max-height:80vh;overflow-y:auto;animation:popIn .4s cubic-bezier(.2,.8,.3,1.1)} #help-box h3{font-size:1.15rem;font-weight:700;margin-bottom:12px} #help-box ul{padding-left:18px;color:var(--text-m);font-size:.82rem;line-height:1.7} #help-box .close-help{margin-top:16px;width:100%;padding:9px;border:1px solid var(--card-b);border-radius:8px;background:transparent;color:var(--text);font-family:inherit;font-size:.82rem;font-weight:600;cursor:pointer;transition:all .2s} #help-box .close-help:hover{background:var(--surface)} .glow-ring{position:absolute;inset:-3px;border-radius:17px;border:2px solid transparent;pointer-events:none;transition:border-color .5s;opacity:0} .glow-ring.on{opacity:1;animation:glowPulse 2s ease-in-out infinite} @keyframes glowPulse{0%,100%{opacity:.4}50%{opacity:.9}} @media(max-width:768px){ main{flex-direction:column;align-items:center;gap:16px;padding:4px 10px 20px} #board-wrap{width:min(92vw,400px)} aside{max-width:400px;width:100%;flex-direction:row;flex-wrap:wrap;gap:10px} aside .card{flex:1;min-width:140px} #dice-container{width:60px;height:60px} #dice{width:60px;height:60px} .dice-face{width:60px;height:60px;padding:8px} .dice-face.f1{transform:rotateY(0deg) translateZ(30px)} .dice-face.f2{transform:rotateY(90deg) translateZ(30px)} .dice-face.f3{transform:rotateX(90deg) translateZ(30px)} .dice-face.f4{transform:rotateX(-90deg) translateZ(30px)} .dice-face.f5{transform:rotateY(-90deg) translateZ(30px)} .dice-face.f6{transform:rotateY(180deg) translateZ(30px)} .ddot{max-width:11px;max-height:11px} .logo{font-size:1.3rem} header{padding:12px 14px 4px} } @media(max-width:400px){ #board-wrap{width:96vw} aside .card{min-width:120px;padding:12px} #players-grid{gap:5px} .player-card{padding:7px 8px} .player-card .dot{width:14px;height:14px} } </style> </head> <body> <header> <div class="logo">LUDO</div> <div class="hdr-btns"> <button class="icon-btn" id="help-btn" title="How to Play"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="16px" height="16px"> <path fill="currentColor" d="M464 256a208 208 0 1 0 -416 0 208 208 0 1 0 416 0zM0 256a256 256 0 1 1 512 0 256 256 0 1 1 -512 0zm256-80c-17.7 0-32 14.3-32 32 0 13.3-10.7 24-24 24s-24-10.7-24-24c0-44.2 35.8-80 80-80s80 35.8 80 80c0 47.2-36 67.2-56 74.5l0 3.8c0 13.3-10.7 24-24 24s-24-10.7-24-24l0-8.1c0-20.5 14.8-35.2 30.1-40.2 6.4-2.1 13.2-5.5 18.2-10.3 4.3-4.2 7.7-10 7.7-19.6 0-17.7-14.3-32-32-32zM224 368a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"/> </svg> </button> <button class="icon-btn" id="theme-btn" title="Toggle Theme"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="16px" height="16px"> <path fill="currentColor" d="M239.3 48.7c-107.1 8.5-191.3 98.1-191.3 207.3 0 114.9 93.1 208 208 208 33.3 0 64.7-7.8 92.6-21.7-103.4-23.4-180.6-115.8-180.6-226.3 0-65.8 27.4-125.1 71.3-167.3zM0 256c0-141.4 114.6-256 256-256 19.4 0 38.4 2.2 56.7 6.3 9.9 2.2 17.3 10.5 18.5 20.5s-4 19.8-13.1 24.4c-60.6 30.2-102.1 92.7-102.1 164.8 0 101.6 82.4 184 184 184 5 0 9.9-.2 14.8-.6 10.1-.8 19.6 4.8 23.8 14.1s2 20.1-5.3 27.1C387.3 484.8 324.8 512 256 512 114.6 512 0 397.4 0 256z"/> </svg> </button> </div> </header> <main> <div id="board-wrap"> <div id="board"></div> <div class="glow-ring" id="glow-ring"></div> <div id="tokens-layer"></div> </div> <aside> <div class="card"> <div id="players-grid"></div> </div> <div class="card" id="dice-area"> <div id="dice-container"><div id="dice"></div></div> <button id="roll-btn">Roll Dice</button> </div> <div class="card"> <div id="mode-toggle"> <button class="active" data-mode="1">You vs AI</button> <button data-mode="4">4 Players</button> </div> <div id="msg">Red's turn — roll the dice</div> </div> </aside> </main> <div id="toast-container"></div> <div id="win-overlay"><div id="win-box"><h2 id="win-text"></h2><p>Congratulations on an incredible game!</p><button onclick="restartGame()">Play Again</button></div></div> <div id="help-modal"><div id="help-box"><h3>How to Play</h3><ul><li>Roll a <strong>6</strong> to bring a token out of home base onto the board.</li><li>Move tokens clockwise around the board by the dice value.</li><li>Landing on an opponent's token sends it back to their home.</li><li><strong>Star cells</strong> are safe — tokens there cannot be captured.</li><li>Rolling a <strong>6</strong> grants an extra turn.</li><li>Enter your colored home column and reach the center to finish a token.</li><li>First player to get all 4 tokens home wins!</li><li>In <strong>1 Player</strong> mode, you control Red; the rest are AI.</li></ul><button class="close-help" onclick="document.getElementById('help-modal').classList.remove('show')">Got it</button></div></div> <script> /* ====== CONSTANTS ====== */ const PLAYERS = ['red','green','yellow','blue']; const COLORS = {red:'var(--red)',green:'var(--green)',yellow:'var(--yellow)',blue:'var(--blue)'}; const COLOR_HEX = {red:'#ef4444',green:'#22c55e',yellow:'#eab308',blue:'#2563eb'}; const NAMES = {red:'You',green:'Bot1',yellow:'Bot2',blue:'Bot3'}; // Main path: 52 cells clockwise, starting from Red's entry const MP = [ [6,1],[6,2],[6,3],[6,4],[6,5], [5,6],[4,6],[3,6],[2,6],[1,6],[0,6], [0,7],[0,8], [1,8],[2,8],[3,8],[4,8],[5,8], [6,9],[6,10],[6,11],[6,12],[6,13],[6,14], [7,14],[8,14], [8,13],[8,12],[8,11],[8,10],[8,9], [9,8],[10,8],[11,8],[12,8],[13,8],[14,8], [14,7],[14,6], [13,6],[12,6],[11,6],[10,6],[9,6], [8,5],[8,4],[8,3],[8,2],[8,1],[8,0], [7,0],[6,0] ]; /* TRY THIS TO FIX THE SQUARE SKIPPING BUG const MP = [ [6,0],[6,1],[6,2],[6,3],[6,4],[6,5], // 0-5: row6 right [6,6], // 6: junction top-left [5,6],[4,6],[3,6],[2,6],[1,6],[0,6], // 7-12: col6 up [0,7],[0,8], // 13-14: top tip [1,8],[2,8],[3,8],[4,8],[5,8], // 15-19: col8 down [6,8], // 20: junction top-right [6,9],[6,10],[6,11],[6,12],[6,13],[6,14], // 21-26: row6 right [7,14],[8,14], // 27-28: right tip [8,13],[8,12],[8,11],[8,10],[8,9], // 29-33: row8 left [8,8], // 34: junction bottom-right [9,8],[10,8],[11,8],[12,8],[13,8],[14,8], // 35-40: col8 down [14,7],[14,6], // 41-42: bottom tip [13,6],[12,6],[11,6],[10,6],[9,6], // 43-47: col6 up [8,6], // 48: junction bottom-left [8,5],[8,4],[8,3],[8,2],[8,1],[8,0], // 49-54: row8 left [7,0] // 55: left tip ]; */ // Home columns: 6 cells each leading to center const HC = { red:[[7,1],[7,2],[7,3],[7,4],[7,5],[7,6]], green:[[1,7],[2,7],[3,7],[4,7],[5,7],[6,7]], yellow:[[7,13],[7,12],[7,11],[7,10],[7,9],[7,8]], blue:[[13,7],[12,7],[11,7],[10,7],[9,7],[8,7]] }; // Home base token resting positions const HB = { red:[[2,2],[2,3],[3,2],[3,3]], green:[[2,11],[2,12],[3,11],[3,12]], yellow:[[11,11],[11,12],[12,11],[12,12]], blue:[[11,2],[11,3],[12,2],[12,3]] }; // Entry indices on main path const ENTRY = {red:0, green:13, yellow:26, blue:39}; //const ENTRY = {red:1, green:15, yellow:29, blue:43}; // Safe spots (main path indices) const SAFE = new Set([0,8,13,21,26,34,39,47]); // Safe spots (main path indices) — 4 entries + 4 star cells (8 steps after each entry) //const SAFE = new Set([1, 9, 15, 23, 29, 37, 43, 51]); // Step constants //const MAIN_PATH_STEPS = 55; // steps 0-54 on the 56-cell main path //const HOME_COL_START = 55; // steps 55-60 in home column //const FINISHED_STEP = 60; // step 60 = reached center // Dice face rotations to show each value const DICE_ROT = { 1:{x:0,y:0},2:{x:0,y:-90},3:{x:90,y:0}, 4:{x:-90,y:0},5:{x:0,y:90},6:{x:0,y:180} }; // Dice dot patterns (3x3 grid, 1=dot, 0=empty) const DOTS = { 1:[0,0,0,0,1,0,0,0,0], 2:[0,0,1,0,0,0,1,0,0], 3:[0,0,1,0,1,0,1,0,0], 4:[1,0,1,0,0,0,1,0,1], 5:[1,0,1,0,1,0,1,0,1], 6:[1,0,1,1,0,1,1,0,1] }; /* ====== STATE ====== */ let S = { cur: 0, // current player index dice: 0, // last dice value rolled: false, // has dice been rolled this turn tokens: {}, // {red:[step,step,step,step], ...} -1=home, 0-51=path, 52-57=home col finished: {}, // {red:count, ...} validMoves: [], // [{ti, newStep}, ...] animating: false, gameOver: false, mode: 1 // 1=solo vs AI, 4=hotseat }; function resetState(){ S.tokens={};S.finished={}; PLAYERS.forEach(p=>{S.tokens[p]=[-1,-1,-1,-1];S.finished[p]=0;}); S.cur=0;S.dice=0;S.rolled=false;S.validMoves=[];S.animating=false;S.gameOver=false; } /* ====== HELPERS ====== */ function curPlayer(){return PLAYERS[S.cur]} function isAI(p){return S.mode===1&&p!=='red'} // Get grid [row,col] for a token function getCoord(player, ti, step){ if(step===-1) return HB[player][ti]; if(step>=52) return HC[player][step-52]; return MP[(ENTRY[player]+step)%52]; } // Get main path index for a player at given step function pathIdx(player, step){ if(step<0||step>=52) return -1; return (ENTRY[player]+step)%52; } function getValidMoves(player, dice){ const moves=[]; const toks=S.tokens[player]; for(let i=0;i<4;i++){ const s=toks[i]; if(s===-1){ if(dice===6) moves.push({ti:i,newStep:0}); } else if(s>=0 && s<57){ const ns=s+dice; if(ns<=57) moves.push({ti:i,newStep:ns}); } } return moves; } function canCapture(player, step){ if(step>=52||SAFE.has(pathIdx(player,step))) return false; const pi=pathIdx(player,step); for(const op of PLAYERS){ if(op===player) continue; for(let i=0;i<4;i++){ const os=S.tokens[op][i]; if(os>=0&&os<52&&pathIdx(op,os)===pi) return true; } } return false; } /* ====== BOARD CREATION ====== */ function createBoard(){ const board=document.getElementById('board'); board.innerHTML=''; // Build lookup maps const pathMap={}; MP.forEach((c,i)=>pathMap[c[0]+','+c[1]]=i); const hcMap={}; for(const [p,cols] of Object.entries(HC)) cols.forEach((c,i)=>hcMap[c[0]+','+c[1]]={p,s:i+52}); const hbMap={}; for(const [p,poses] of Object.entries(HB)) poses.forEach((c,i)=>hbMap[c[0]+','+c[1]]={p,ti:i}); for(let r=0;r<15;r++){ for(let c=0;c<15;c++){ const cell=document.createElement('div'); cell.className='cell'; const key=r+','+c; // Center 3x3 if(r>=6&&r<=8&&c>=6&&c<=8){ cell.classList.add('center-cell'); if(r===7&&c===7){ cell.classList.add('center-mid'); } else { let cp=null; if((r===6&&c===6)||(r===7&&c===6)) cp='red'; else if((r===6&&c===7)||(r===6&&c===8)) cp='green'; else if((r===7&&c===8)||(r===8&&c===8)) cp='yellow'; else if((r===8&&c===7)||(r===8&&c===6)) cp='blue'; if(cp){cell.classList.add('center-player');cell.style.background=COLOR_HEX[cp];} } board.appendChild(cell);continue; } // Home bases const inHome = (r<6&&c<6)?'red':(r<6&&c>8)?'green':(r>8&&c<6)?'blue':(r>8&&c>8)?'yellow':null; if(inHome){ const lr=r<6?r:r-9, lc=c<6?c:c-9; if(lr===0||lr===5||lc===0||lc===5){ cell.classList.add('home-outer'); cell.style.setProperty('--player-color',COLOR_HEX[inHome]); } else { cell.classList.add('home-inner'); cell.style.setProperty('--player-color',COLOR_HEX[inHome]); if(lr===1&&lc===1) cell.classList.add('tl'); if(lr===1&&lc===4) cell.classList.add('tr'); if(lr===4&&lc===1) cell.classList.add('bl'); if(lr===4&&lc===4) cell.classList.add('br'); } // Token spots const hb=hbMap[key]; if(hb){ const spot=document.createElement('div'); spot.className='token-spot'; spot.style.setProperty('--player-color',COLOR_HEX[hb.p]); spot.style.borderColor=COLOR_HEX[hb.p]; cell.appendChild(spot); } board.appendChild(cell);continue; } // Path / Home column cells const hci=hcMap[key]; if(hci){ cell.classList.add('home-col'); cell.style.setProperty('--player-color',COLOR_HEX[hci.p]); board.appendChild(cell);continue; } const pi=pathMap[key]; if(pi!==undefined){ cell.classList.add('path'); // Entry point? let entryPlayer=null; for(const [p,ei] of Object.entries(ENTRY)){if(ei===pi)entryPlayer=p;} if(entryPlayer){ cell.classList.add('entry'); cell.style.setProperty('--player-color',COLOR_HEX[entryPlayer]); } // Safe spot? if(SAFE.has(pi)){ cell.classList.add('safe'); const star=document.createElement('i'); star.className='fas fa-star star-icon'; cell.appendChild(star); star.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512" width="16px" height="16px"><path fill="currentColor" d="M288.1-32c9 0 17.3 5.1 21.4 13.1L383 125.3 542.9 150.7c8.9 1.4 16.3 7.7 19.1 16.3s.5 18-5.8 24.4L441.7 305.9 467 465.8c1.4 8.9-2.3 17.9-9.6 23.2s-17 6.1-25 2L288.1 417.6 143.8 491c-8 4.1-17.7 3.3-25-2s-11-14.2-9.6-23.2L134.4 305.9 20 191.4c-6.4-6.4-8.6-15.8-5.8-24.4s10.1-14.9 19.1-16.3l159.9-25.4 73.6-144.2c4.1-8 12.4-13.1 21.4-13.1zm0 76.8L230.3 158c-3.5 6.8-10 11.6-17.6 12.8l-125.5 20 89.8 89.9c5.4 5.4 7.9 13.1 6.7 20.7l-19.8 125.5 113.3-57.6c6.8-3.5 14.9-3.5 21.8 0l113.3 57.6-19.8-125.5c-1.2-7.6 1.3-15.3 6.7-20.7l89.8-89.9-125.5-20c-7.6-1.2-14.1-6-17.6-12.8L288.1 44.8z"/></svg>` } board.appendChild(cell);continue; } // Fallback (shouldn't happen) cell.classList.add('path'); board.appendChild(cell); } } } /* ====== TOKEN RENDERING ====== */ const tokenEls={}; function createTokens(){ const layer=document.getElementById('tokens-layer'); layer.innerHTML=''; PLAYERS.forEach(p=>{ tokenEls[p]=[]; for(let i=0;i<4;i++){ const el=document.createElement('div'); el.className='token'; el.style.background=COLOR_HEX[p]; el.style.setProperty('--glow',COLOR_HEX[p]+'88'); el.style.boxShadow='0 2px 6px rgba(0,0,0,.3)'; el.dataset.player=p; el.dataset.ti=i; el.addEventListener('click',()=>onTokenClick(p,i)); layer.appendChild(el); tokenEls[p].push(el); } }); } function renderTokens(){ // Group by position for offset calculation const groups={}; PLAYERS.forEach(p=>{ for(let i=0;i<4;i++){ if(S.tokens[p][i]===57) continue; // finished, don't render on board const c=getCoord(p,i,S.tokens[p][i]); const k=c[0]+','+c[1]; if(!groups[k]) groups[k]=[]; groups[k].push({p,i}); } }); const boardEl=document.getElementById('board'); const bw=boardEl.offsetWidth; const cs=bw/15; const tSize=Math.max(12,cs*0.62); const isMobile=window.innerWidth<769; PLAYERS.forEach(p=>{ for(let i=0;i<4;i++){ const el=tokenEls[p][i]; const step=S.tokens[p][i]; if(step===57){ el.style.display='none'; continue; } el.style.display='flex'; const c=getCoord(p,i,step); const k=c[0]+','+c[1]; const group=groups[k]||[{p,i}]; const idx=group.findIndex(g=>g.p===p&&g.i===i); const cnt=group.length; let dx=0,dy=0; const off=tSize*0.32; if(cnt===2){dx=(idx===0?-1:1)*off;dy=0;} else if(cnt===3){dx=(idx-1)*off;dy=idx===1?-off:off*0.5;} else if(cnt>=4){dx=(idx%2===0?-1:1)*off;dy=(idx<2?-1:1)*off;} el.style.width=tSize+'px'; el.style.height=tSize+'px'; el.style.fontSize=(tSize*0.38)+'px'; el.style.left=(c[1]*cs+cs/2+dx)+'px'; el.style.top=(c[0]*cs+cs/2+dy)+'px'; el.style.transform='translate(-50%,-50%)'; // Clickable? if(S.rolled && !S.animating && !S.gameOver && p===curPlayer()){ const vm=S.validMoves.find(m=>m.ti===i); if(vm){ el.classList.add('clickable'); } else { el.classList.remove('clickable'); } } else { el.classList.remove('clickable'); } } }); } /* ====== DICE ====== */ function createDice(){ const dice=document.getElementById('dice'); dice.innerHTML=''; for(let f=1;f<=6;f++){ const face=document.createElement('div'); face.className='dice-face f'+f; const dots=DOTS[f]; for(let d=0;d<9;d++){ const dot=document.createElement('div'); dot.className='ddot'+(dots[d]?'':' hide'); face.appendChild(dot); } dice.appendChild(face); } // Initial position: show 1 dice.style.transform='rotateX(0deg) rotateY(0deg)'; } function animateDice(finalVal, cb){ const dice=document.getElementById('dice'); const r=DICE_ROT[finalVal]; const ex=720*(Math.random()>.5?1:-1); const ey=720*(Math.random()>.5?1:-1); dice.style.transition='none'; dice.style.transform='rotateX('+(Math.random()*360)+'deg) rotateY('+(Math.random()*360)+'deg)'; requestAnimationFrame(()=>{ requestAnimationFrame(()=>{ dice.style.transition='transform 1.1s cubic-bezier(.12,.82,.22,1)'; dice.style.transform='rotateX('+(r.x+ex)+'deg) rotateY('+(r.y+ey)+'deg)'; setTimeout(cb,1150); }); }); } /* ====== PLAYER INDICATORS ====== */ function renderPlayers(){ const grid=document.getElementById('players-grid'); grid.innerHTML=''; PLAYERS.forEach((p,i)=>{ const card=document.createElement('div'); card.className='player-card'+(i===S.cur?' active':''); card.style.setProperty('--player-color',COLOR_HEX[p]); card.style.setProperty('--player-s',p==='red'?'var(--red-s)':p==='green'?'var(--green-s)':p==='yellow'?'var(--yellow-s)':'var(--blue-s)'); card.innerHTML=`<div class="dot" style="background:${COLOR_HEX[p]}"></div> <span class="pname">${NAMES[p]}${isAI(p)?' <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" width="16px" height="16px"><path fill="currentColor" d="M352 0c0-17.7-14.3-32-32-32S288-17.7 288 0l0 64-96 0c-53 0-96 43-96 96l0 224c0 53 43 96 96 96l256 0c53 0 96-43 96-96l0-224c0-53-43-96-96-96l-96 0 0-64zM160 368c0-13.3 10.7-24 24-24l32 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-32 0c-13.3 0-24-10.7-24-24zm120 0c0-13.3 10.7-24 24-24l32 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-32 0c-13.3 0-24-10.7-24-24zm120 0c0-13.3 10.7-24 24-24l32 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-32 0c-13.3 0-24-10.7-24-24zM224 176a48 48 0 1 1 0 96 48 48 0 1 1 0-96zm144 48a48 48 0 1 1 96 0 48 48 0 1 1 -96 0zM64 224c0-17.7-14.3-32-32-32S0 206.3 0 224l0 96c0 17.7 14.3 32 32 32s32-14.3 32-32l0-96zm544-32c-17.7 0-32 14.3-32 32l0 96c0 17.7 14.3 32 32 32s32-14.3 32-32l0-96c0-17.7-14.3-32-32-32z"/></svg>':''}</span><span class="pfinished"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="16px" height="16"><path fill="currentColor" d="M240 6.1c9.1-8.2 22.9-8.2 32 0l232 208c9.9 8.8 10.7 24 1.8 33.9s-24 10.7-33.9 1.8l-8-7.2 0 205.3c0 35.3-28.7 64-64 64l-288 0c-35.3 0-64-28.7-64-64l0-205.3-8 7.2c-9.9 8.8-25 8-33.9-1.8s-8-25 1.8-33.9L240 6.1zm16 50.1L96 199.7 96 448c0 8.8 7.2 16 16 16l48 0 0-104c0-39.8 32.2-72 72-72l48 0c39.8 0 72 32.2 72 72l0 104 48 0c8.8 0 16-7.2 16-16l0-248.3-160-143.4zM208 464l96 0 0-104c0-13.3-10.7-24-24-24l-48 0c-13.3 0-24 10.7-24 24l0 104z"/></svg>${S.finished[p]}/4</span>`; grid.appendChild(card); }); } /* ====== GLOW RING ====== */ function updateGlow(){ const ring=document.getElementById('glow-ring'); const p=curPlayer(); if(S.gameOver){ring.classList.remove('on');return;} ring.classList.add('on'); ring.style.borderColor=COLOR_HEX[p]; ring.style.boxShadow=`0 0 20px ${COLOR_HEX[p]}44`; } /* ====== MESSAGES & TOASTS ====== */ function setMsg(txt){document.getElementById('msg').textContent=txt} function toast(msg,dur){ dur=dur||2500; const t=document.createElement('div'); t.className='toast';t.textContent=msg; document.getElementById('toast-container').appendChild(t); setTimeout(()=>t.remove(),dur); } /* ====== GAME LOGIC ====== */ function rollDice(cb){ const val=Math.floor(Math.random()*6)+1; S.dice=val; animateDice(val,()=>{if(cb)cb();}); } function onRollClick(){ if(S.animating||S.gameOver||S.rolled) return; const p=curPlayer(); S.animating=true; document.getElementById('roll-btn').disabled=true; rollDice(()=>{ S.rolled=true; S.validMoves=getValidMoves(p,S.dice); if(S.validMoves.length===0){ setMsg(`${NAMES[p]} rolled ${S.dice} — no valid moves`); S.animating=false; setTimeout(()=>nextTurn(),800); } else if(S.validMoves.length===1 && isAI(p)){ setMsg(`${NAMES[p]} rolled ${S.dice}`); S.animating=false; setTimeout(()=>executeMove(S.validMoves[0]),400); } else { setMsg(`${NAMES[p]} rolled ${S.dice} — select a token`); S.animating=false; renderTokens(); if(isAI(p)) setTimeout(()=>aiSelectMove(),600); } renderPlayers(); }); } function onTokenClick(player, ti){ if(S.animating||S.gameOver||!S.rolled||player!==curPlayer()) return; const mv=S.validMoves.find(m=>m.ti===ti); if(!mv) return; executeMove(mv); } function executeMove(mv){ S.animating=true; const p=curPlayer(); const oldStep=S.tokens[p][mv.ti]; S.tokens[p][mv.ti]=mv.newStep; renderTokens(); // Check capture after animation setTimeout(()=>{ if(mv.newStep<52){ const pi=pathIdx(p,mv.newStep); if(!SAFE.has(pi)){ for(const op of PLAYERS){ if(op===p) continue; for(let i=0;i<4;i++){ const os=S.tokens[op][i]; if(os>=0&&os<52&&pathIdx(op,os)===pi){ S.tokens[op][i]=-1; toast(`${NAMES[p]} captured ${NAMES[op]}'s token!`); const cel=tokenEls[op][i]; cel.classList.add('captured'); setTimeout(()=>cel.classList.remove('captured'),400); } } } } } renderTokens(); // Check if token reached home if(mv.newStep===57){ S.finished[p]++; toast(`${NAMES[p]} got a token home! (${S.finished[p]}/4)`); if(S.finished[p]===4){ S.gameOver=true; renderPlayers();updateGlow(); setTimeout(()=>showWin(p),500); S.animating=false; return; } } renderPlayers(); // Extra turn on 6 if(S.dice===6 && !S.gameOver){ S.rolled=false; S.validMoves=[]; S.animating=false; setMsg(`${NAMES[p]} rolled 6 — extra turn!`); document.getElementById('roll-btn').disabled=false; renderTokens(); if(isAI(p)) setTimeout(()=>onRollClick(),700); return; } S.animating=false; setTimeout(()=>nextTurn(),400); },380); } function nextTurn(){ if(S.gameOver) return; S.rolled=false; S.validMoves=[]; S.cur=(S.cur+1)%4; const p=curPlayer(); console.log('TURN', p) setMsg(S.cur==0?`Your turn — roll the dice`:`${NAMES[p]}'s turn — roll the dice`); document.getElementById('roll-btn').disabled=false; renderPlayers();updateGlow();renderTokens(); if(isAI(p)) setTimeout(()=>onRollClick(),700); } /* ====== AI ====== */ function aiSelectMove(){ const p=curPlayer(); let mv=null; // Priority 1: capture for(const m of S.validMoves){ if(canCapture(p,m.newStep)){mv=m;break;} } // Priority 2: reach home if(!mv){ for(const m of S.validMoves){ if(m.newStep===57){mv=m;break;} } } // Priority 3: come out of home if(!mv){ for(const m of S.validMoves){ if(S.tokens[p][m.ti]===-1){mv=m;break;} } } // Priority 4: move token closest to home if(!mv){ mv=S.validMoves.reduce((a,b)=>S.tokens[p][a.ti]>S.tokens[p][b.ti]?a:b); } if(mv) executeMove(mv); } /* ====== WIN ====== */ function showWin(player){ document.getElementById('win-text').textContent=NAMES[player]+' Wins!'; document.getElementById('win-text').style.color=COLOR_HEX[player]; document.getElementById('win-overlay').classList.add('show'); spawnConfetti(COLOR_HEX[player]); } function spawnConfetti(baseColor){ const colors=[baseColor,'#fff',COLOR_HEX.red,COLOR_HEX.green,COLOR_HEX.yellow,COLOR_HEX.blue]; for(let i=0;i<60;i++){ const c=document.createElement('div'); c.className='confetti'; c.style.left=Math.random()*100+'vw'; c.style.top=-10+'px'; c.style.background=colors[Math.floor(Math.random()*colors.length)]; c.style.width=(4+Math.random()*8)+'px'; c.style.height=(4+Math.random()*8)+'px'; c.style.borderRadius=Math.random()>.5?'50%':'2px'; c.style.animationDuration=(2+Math.random()*3)+'s'; c.style.animationDelay=(Math.random()*1.5)+'s'; document.body.appendChild(c); setTimeout(()=>c.remove(),6000); } } function restartGame(){ document.getElementById('win-overlay').classList.remove('show'); document.querySelectorAll('.confetti').forEach(c=>c.remove()); resetState(); renderAll(); //setMsg(`${NAMES[curPlayer()]}'s turn — roll the dice`); setMsg(S.cur==0?`Your turn — roll the dice`:`${NAMES[curPlayer()]}'s turn — roll the dice`); document.getElementById('roll-btn').disabled=false; // Reset dice visual const dice=document.getElementById('dice'); dice.style.transition='transform .3s ease'; dice.style.transform='rotateX(0deg) rotateY(0deg)'; } /* ====== THEME ====== */ function toggleTheme(){ const html=document.documentElement; const isDark=html.dataset.theme==='dark'; html.dataset.theme=isDark?'light':'dark'; document.querySelector('#theme-btn i').className=isDark?'fas fa-sun':'fas fa-moon'; } /* ====== MODE TOGGLE ====== */ function setMode(mode){ S.mode=mode; document.querySelectorAll('#mode-toggle button').forEach(b=>{ b.classList.toggle('active',parseInt(b.dataset.mode)===mode); }); restartGame(); } /* ====== RENDER ALL ====== */ function renderAll(){ createBoard(); renderTokens(); renderPlayers(); updateGlow(); } /* ====== INIT ====== */ function init(){ resetState(); createBoard(); createDice(); createTokens(); renderPlayers(); updateGlow(); renderTokens(); document.getElementById('roll-btn').addEventListener('click',onRollClick); document.getElementById('theme-btn').addEventListener('click',toggleTheme); document.getElementById('help-btn').addEventListener('click',()=>document.getElementById('help-modal').classList.add('show')); document.getElementById('help-modal').addEventListener('click',(e)=>{ if(e.target===document.getElementById('help-modal'))document.getElementById('help-modal').classList.remove('show'); }); document.querySelectorAll('#mode-toggle button').forEach(b=>{ b.addEventListener('click',()=>setMode(parseInt(b.dataset.mode))); }); // Keyboard: space to roll document.addEventListener('keydown',(e)=>{ if(e.code==='Space'&&!e.repeat){e.preventDefault();onRollClick();} }); // Responsive let resizeTimer; window.addEventListener('resize',()=>{ clearTimeout(resizeTimer); resizeTimer=setTimeout(()=>renderTokens(),100); }); //setMsg(`${NAMES[curPlayer()]}'s turn — roll the dice`); setMsg(S.cur==0?`Your turn — roll the dice`:`${NAMES[curPlayer()]}'s turn — roll the dice`); } init(); </script> </body> </html>
Cancel