Commit 7361d8ea authored by Mahmoud Aglan's avatar Mahmoud Aglan

fix(ludo): agent modifications — bot personalities, bounce effect, move preview

Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent a1a13d94
......@@ -51,18 +51,18 @@ export function mountGame(el, params) {
el.innerHTML = `
<div style="display:flex;flex-direction:column;height:100%;background:#1a1a2e;">
<div style="display:flex;justify-content:space-between;padding:6px 12px;background:#0f0f1e;">
<div class="pp" id="pp-1" style="--pc:${COLORS[1]};"><div class="pp-dot"></div><span>${PLAYER_NAMES[1]}</span></div>
<div class="pp" id="pp-0" style="--pc:${COLORS[0]};"><div class="pp-dot"></div><span>${PLAYER_NAMES[0]}</span></div>
<div class="pp" id="pp-1" style="--pc:${COLORS[1]};"><div class="pp-dot"></div><span>${PLAYER_NAMES[1]}</span><div class="pp-dice" id="dice-1"></div></div>
<div class="pp" id="pp-0" style="--pc:${COLORS[0]};"><div class="pp-dot"></div><span>${PLAYER_NAMES[0]}</span><div class="pp-dice" id="dice-0"></div></div>
</div>
<div id="ludo-wrap" style="flex:1;display:flex;align-items:center;justify-content:center;padding:4px;min-height:0;"></div>
<div style="display:flex;justify-content:space-between;padding:6px 12px;background:#0f0f1e;">
<div class="pp" id="pp-2" style="--pc:${COLORS[2]};"><div class="pp-dot"></div><span>${PLAYER_NAMES[2]}</span></div>
<div class="pp" id="pp-3" style="--pc:${COLORS[3]};"><div class="pp-dot"></div><span>${PLAYER_NAMES[3]}</span></div>
<div class="pp" id="pp-2" style="--pc:${COLORS[2]};"><div class="pp-dot"></div><span>${PLAYER_NAMES[2]}</span><div class="pp-dice" id="dice-2"></div></div>
<div class="pp" id="pp-3" style="--pc:${COLORS[3]};"><div class="pp-dot"></div><span>${PLAYER_NAMES[3]}</span><div class="pp-dice" id="dice-3"></div></div>
</div>
<div id="dice-area" style="display:flex;align-items:center;gap:12px;padding:10px 16px;background:#0f0f1e;border-top:1px solid rgba(255,255,255,0.06);justify-content:center;">
<div id="dice-box" style="width:50px;height:50px;background:#f8fafc;border-radius:10px;display:grid;grid-template:repeat(3,1fr)/repeat(3,1fr);padding:6px;box-shadow:0 3px 10px rgba(0,0,0,0.4),inset 0 1px 0 rgba(255,255,255,0.8);transition:transform 0.15s cubic-bezier(0.34,1.56,0.64,1);">
</div>
<button class="btn btn-primary" id="roll-btn" style="font-size:15px;padding:12px 32px;min-height:48px;">ارمِ النرد</button>
<button class="btn btn-primary" id="roll-btn" style="font-size:15px;padding:12px 32px;min-height:48px;" disabled>ارمِ النرد</button>
</div>
</div>
<style>
......@@ -71,6 +71,8 @@ export function mountGame(el, params) {
.pp-dot{width:10px;height:10px;border-radius:50%;background:var(--pc);}
.pp span{font-size:12px;font-weight:600;color:#94a3b8;}
.pp.active span{color:#f8fafc;}
.pp-dice{width:28px;height:28px;background:#f8fafc;border-radius:5px;display:grid;grid-template:repeat(3,1fr)/repeat(3,1fr);padding:3px;box-shadow:0 2px 6px rgba(0,0,0,0.3);transition:transform 0.15s;opacity:0.4;}
.pp.active .pp-dice{opacity:1;}
</style>
`;
......@@ -84,6 +86,10 @@ export function mountGame(el, params) {
canvas = c; ctx = cx;
renderDiceFace(el.querySelector('#dice-box'), 1);
for (let i = 0; i < 4; i++) {
const md = el.querySelector(`#dice-${i}`);
if (md) renderMiniDice(md, 1);
}
drawBoard();
updatePanels(el);
el.querySelector('#roll-btn').addEventListener('click', () => handleRoll(el));
......@@ -171,64 +177,31 @@ function isMyTurn() {
return game.currentPlayer === myPlayerIndex;
}
function handleRoll(el) {
async function handleRoll(el) {
if (diceAnimating || game.rolled || game.gameOver || !isMyTurn()) return;
diceAnimating = true;
const btn = el.querySelector('#roll-btn');
const diceBox = el.querySelector('#dice-box');
const diceArea = el.querySelector('#dice-area');
btn.disabled = true;
btn.style.opacity = '0.5';
// Rapid shake animation with random faces
let count = 0;
const shakeAnim = setInterval(() => {
const randVal = Math.floor(Math.random()*6)+1;
renderDiceFace(diceBox, randVal);
const rx = (Math.random()-0.5)*16;
const ry = (Math.random()-0.5)*16;
const rot = (Math.random()-0.5)*30;
diceBox.style.transform = `translate(${rx}px,${ry}px) rotate(${rot}deg) scale(0.9)`;
count++;
if (count > 14) {
clearInterval(shakeAnim);
// SLAM — final value
const dice = rules.rollDice();
game.diceValue = dice;
game.rolled = true;
renderDiceFace(diceBox, dice);
// Slam effect
diceBox.style.transform = 'scale(1.25)';
setTimeout(() => { diceBox.style.transform = 'scale(1)'; }, 150);
diceAnimating = false;
audio.play('dice', 'game');
juice.hapticHeavy();
juice.shake(diceArea, 3, 150);
// 6 = golden glow + stars
if (dice === 6) {
diceBox.style.boxShadow = '0 0 20px #E4AC38, 0 4px 14px rgba(0,0,0,0.4)';
const rect = diceBox.getBoundingClientRect();
juice.starBurst(rect.left+31, rect.top+31, 8);
juice.hapticSuccess();
setTimeout(() => { diceBox.style.boxShadow = '0 4px 14px rgba(0,0,0,0.4),inset 0 1px 0 rgba(255,255,255,0.8)'; }, 600);
}
updatePanels(el);
validMoves = rules.getValidMoves(game, myPlayerIndex, dice);
if (validMoves.length === 0) {
setTimeout(() => { game.rolled = false; rules.nextTurn(game); btn.disabled = false; btn.style.opacity = '1'; updatePanels(el); drawBoard(); if (!isMyTurn()) handleNonPlayerTurn(el); }, 800);
} else if (validMoves.length === 1) {
// Only one option — still highlight it briefly then execute
highlightMovablePieces(validMoves);
setTimeout(() => doMove(el, validMoves[0]), 600);
} else {
// Multiple options — let player TAP which piece to move
highlightMovablePieces(validMoves);
waitForPieceSelection(el, validMoves);
}
}
}, 55);
const dice = await animateDice(el, myPlayerIndex);
game.rolled = true;
diceAnimating = false;
// Star burst on 6
if (dice === 6) {
const rect = el.querySelector('#dice-box').getBoundingClientRect();
juice.starBurst(rect.left + 31, rect.top + 31, 8);
}
validMoves = rules.getValidMoves(game, myPlayerIndex, dice);
if (validMoves.length === 0) {
setTimeout(() => { game.rolled = false; rules.nextTurn(game); updatePanels(el); drawBoard(); if (!isMyTurn()) handleNonPlayerTurn(el); }, 800);
} else if (validMoves.length === 1) {
highlightMovablePieces(validMoves);
setTimeout(() => doMove(el, validMoves[0]), 600);
} else {
highlightMovablePieces(validMoves);
waitForPieceSelection(el, validMoves);
}
}
function doMove(el, move) {
......@@ -275,17 +248,9 @@ async function botLoop(el) {
await new Promise(r => setTimeout(r, thinkDelay));
if (game.gameOver || isMyTurn()) { if (botPanelSpan) botPanelSpan.textContent = originalPanelText; return; }
// 2. Roll dice with animation
const dice = rules.rollDice();
game.diceValue = dice;
const diceBox = el.querySelector('#dice-box');
renderDiceFace(diceBox, dice);
diceBox.style.transform = 'scale(1.15)';
audio.play('dice', 'game');
setTimeout(() => { diceBox.style.transform = 'scale(1)'; }, 150);
// Restore panel text after roll
// 2. Roll dice with SAME animation as player
if (botPanelSpan) botPanelSpan.textContent = originalPanelText;
const dice = await animateDice(el, game.currentPlayer);
// 3. "Deciding" which piece to move — personality-driven delay
const decideDelay = personality.thinkMin * 0.5 + Math.random() * (personality.thinkMax - personality.thinkMin) * 0.5;
......@@ -350,8 +315,8 @@ async function botLoop(el) {
// After bot, sync state if live + host
if (game.mode === 'live' && isHost) syncLudoState();
updatePanels(el);
if (!isMyTurn()) handleNonPlayerTurn(el);
else { el.querySelector('#roll-btn').disabled = false; el.querySelector('#roll-btn').style.opacity = '1'; }
}
// === LIVE SYNC ===
......@@ -387,8 +352,6 @@ function startLudoPolling(el) {
game.diceValue = data.dice_value;
updatePanels(el);
drawBoard();
el.querySelector('#roll-btn').disabled = false;
el.querySelector('#roll-btn').style.opacity = '1';
}
} catch (e) {}
}, 2000);
......@@ -546,9 +509,7 @@ function afterMove(el, move) {
if (game.mode === 'live') syncLudoState();
const btn = el.querySelector('#roll-btn');
if (isMyTurn()) { btn.disabled = false; btn.style.opacity = '1'; }
else handleNonPlayerTurn(el);
if (!isMyTurn()) handleNonPlayerTurn(el);
}
// ===== END PIECE SELECTION + ANIMATION =====
......@@ -696,6 +657,61 @@ function updatePanels(el) {
const p = el.querySelector(`#pp-${i}`);
if (p) p.classList.toggle('active', i === game.currentPlayer);
}
const btn = el.querySelector('#roll-btn');
if (btn) {
const canRoll = isMyTurn() && !game.rolled && !diceAnimating && !game.gameOver;
btn.disabled = !canRoll;
btn.style.opacity = canRoll ? '1' : '0.4';
}
}
function animateDice(el, playerIdx) {
return new Promise(resolve => {
const mainDice = el.querySelector('#dice-box');
const miniDice = el.querySelector(`#dice-${playerIdx}`);
let count = 0;
const shakeAnim = setInterval(() => {
const randVal = Math.floor(Math.random() * 6) + 1;
renderDiceFace(mainDice, randVal);
if (miniDice) renderMiniDice(miniDice, randVal);
const rx = (Math.random() - 0.5) * 16;
const ry = (Math.random() - 0.5) * 16;
const rot = (Math.random() - 0.5) * 30;
mainDice.style.transform = `translate(${rx}px,${ry}px) rotate(${rot}deg) scale(0.9)`;
if (miniDice) miniDice.style.transform = `rotate(${rot * 0.5}deg) scale(0.85)`;
count++;
if (count > 14) {
clearInterval(shakeAnim);
const dice = rules.rollDice();
game.diceValue = dice;
renderDiceFace(mainDice, dice);
if (miniDice) renderMiniDice(miniDice, dice);
mainDice.style.transform = 'scale(1.25)';
if (miniDice) miniDice.style.transform = 'scale(1.15)';
setTimeout(() => {
mainDice.style.transform = 'scale(1)';
if (miniDice) miniDice.style.transform = 'scale(1)';
}, 150);
audio.play('dice', 'game');
juice.hapticHeavy();
juice.shake(el.querySelector('#dice-area'), 3, 150);
if (dice === 6) {
mainDice.style.boxShadow = '0 0 20px #E4AC38, 0 4px 14px rgba(0,0,0,0.4)';
setTimeout(() => { mainDice.style.boxShadow = '0 4px 14px rgba(0,0,0,0.4),inset 0 1px 0 rgba(255,255,255,0.8)'; }, 600);
juice.hapticSuccess();
}
resolve(dice);
}
}, 55);
});
}
function renderMiniDice(miniDice, value) {
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] };
const pattern = dots[value] || dots[1];
miniDice.innerHTML = pattern.map(d =>
`<div style="width:5px;height:5px;border-radius:50%;background:${d ? '#1a1a2e' : 'transparent'};margin:auto;"></div>`
).join('');
}
function endGame(el) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment