Commit 1c68160e authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat: Chess.com-style smart tap-to-move controls for backgammon

Smart behaviors:
- Auto-roll dice at start of each turn
- If only one move exists for a piece, execute immediately on tap
- If only one complete sequence possible, auto-execute entire turn
- Auto-end turn when no moves remain
- Movable pieces highlighted with golden glow (tap targets)
- Tap piece → valid destinations highlighted → tap destination → done
- Undo button for unlimited undo before turn ends

UX flow:
1. Dice auto-roll
2. Movable sources highlighted
3. Tap source → if single destination, auto-move
4. If multiple destinations → show targets → tap one
5. Repeat until dice exhausted
6. Turn auto-ends

Minimal taps, zero rules knowledge needed, fast animations.
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 48a4a508
......@@ -30,10 +30,11 @@
<!-- Side panel -->
<div class="bg-side-panel">
<div id="bg-turn" class="bg-turn-indicator"></div>
<div id="bg-dice-container" class="bg-dice-area"></div>
<div id="bg-status" class="bg-status"></div>
<div class="bg-controls">
<button id="bg-roll-btn" class="bg-roll-btn">ارمِ النرد</button>
<button id="bg-undo-btn" class="bg-pass-btn" style="display:none;">تراجع</button>
</div>
<div id="bg-log" class="bg-log"></div>
</div>
......
......@@ -176,12 +176,14 @@
border-radius: 4px;
}
.bg-point--clickable {
.bg-point--source {
cursor: pointer;
}
.bg-point--clickable .bg-checker:last-child {
box-shadow: 0 0 0 2px rgba(255,200,50,0.4), 0 2px 4px rgba(0,0,0,0.3);
.bg-point--source .bg-checker:last-child,
.bg-point--source .bg-checkers-stack .bg-checker:last-child {
box-shadow: 0 0 0 3px rgba(255,200,50,0.5), 0 0 10px rgba(255,200,50,0.3);
transform: scale(1.05);
}
.bg-point--hover-preview {
......
......@@ -6,6 +6,7 @@ var BackgammonGame = (function() {
var Bot = BackgammonBot;
var state = null;
var undoStack = [];
function init(opts) {
state = {
......@@ -58,14 +59,15 @@ var BackgammonGame = (function() {
UI.render(state);
if (state.players[state.currentTurn].type === 'bot') {
UI.setStatus('انتظر... البوت يفكر');
setTimeout(function() { executeBotTurn(); }, 800);
UI.setStatus('انتظر...');
setTimeout(function() { executeBotTurn(); }, 600);
} else {
// Human's turn - show helpful status
UI.setStatus('دورك — حرّك قطعك بالنرد ' + d1 + ' و ' + d2);
beginHumanTurn();
}
}
// ─── Auto-roll at start of turn ────────────────────────────────────────────
function rollDice() {
if (state.phase !== 'awaiting_roll') return;
if (state.players[state.currentTurn].type !== 'human') return;
......@@ -75,273 +77,330 @@ var BackgammonGame = (function() {
state.dice = [d1, d2];
state.diceRemaining = C.getDiceRemaining(d1, d2);
state.phase = 'moving';
undoStack = [];
UI.showLog(state.players[state.currentTurn].name + ' رمى ' + d1 + ' و ' + d2);
UI.render(state);
beginHumanTurn();
}
// ─── Smart turn start ──────────────────────────────────────────────────────
function beginHumanTurn() {
if (!C.canMove(state.board, state.bar, state.borneOff, state.currentTurn, state.diceRemaining)) {
UI.setStatus('لا توجد حركة — ينتقل الدور');
UI.showLog('لا توجد حركة متاحة — تخطي الدور');
setTimeout(function() { endTurn(); }, 1200);
} else {
UI.setStatus('اختر قطعة لتحريكها');
UI.setStatus('لا توجد حركة متاحة');
UI.showLog('لا توجد حركة — تخطي');
setTimeout(function() { endTurn(); }, 1000);
return;
}
// Check if only one complete sequence exists — auto-execute with confirmation
var allSeqs = C.getAllMovesForDice(state.board, state.bar, state.borneOff, state.currentTurn, state.diceRemaining);
if (allSeqs.length === 1 && allSeqs[0].length === state.diceRemaining.length) {
UI.setStatus('حركة واحدة ممكنة — جاري التنفيذ...');
autoExecuteSequence(allSeqs[0]);
return;
}
UI.setStatus('اختر قطعة');
showMovableSources();
}
function handlePointClick(pointOrAction) {
if (state.phase !== 'moving') return;
if (state.players[state.currentTurn].type !== 'human') return;
// ─── Show which pieces can move ───────────────────────────────────────────
if (pointOrAction === 'roll') {
rollDice();
return;
function showMovableSources() {
var sources = getMovableSources();
UI.highlightSources(sources);
}
function getMovableSources() {
var sources = [];
var player = state.currentTurn;
// Bar first
if (state.bar[player] > 0) {
return ['bar'];
}
// Pass/skip action
if (pointOrAction === 'pass') {
if (state.diceRemaining.length > 0 &&
!C.canMove(state.board, state.bar, state.borneOff, state.currentTurn, state.diceRemaining)) {
UI.setStatus('لا توجد حركة — ينتقل الدور');
UI.showLog('تخطي الدور — لا توجد حركة');
UI.clearSelected();
UI.clearHighlights();
setTimeout(function() { endTurn(); }, 600);
for (var i = 0; i < 24; i++) {
var count = state.board[i];
var isOwn = (player === 0) ? count > 0 : count < 0;
if (isOwn) {
var moves = getValidMovesForFrom(i);
if (moves.length > 0) sources.push(i);
}
return;
}
return sources;
}
// ─── Main click handler ────────────────────────────────────────────────────
function handlePointClick(pointOrAction) {
if (pointOrAction === 'roll') { rollDice(); return; }
if (pointOrAction === 'undo') { undoMove(); return; }
if (state.phase !== 'moving') return;
if (state.players[state.currentTurn].type !== 'human') return;
var selected = UI.getSelected();
// CASE 1: Nothing selected yet — select a source
// Nothing selected — try to select source
if (selected === null) {
selectSource(pointOrAction);
return;
}
// CASE 2: Something is already selected
var from = selected;
// Check if user clicked the same point (deselect)
if (isSamePoint(from, pointOrAction)) {
UI.clearSelected();
UI.clearHighlights();
UI.setStatus('اختر قطعة لتحريكها');
UI.render(state);
// Same point clicked — deselect
if (isSamePoint(selected, pointOrAction)) {
deselect();
return;
}
// Determine the target
// Try as destination
var to = parseTarget(pointOrAction);
// Check if target is a valid destination
var validMoves = getValidMovesForFrom(from);
var validMoves = getValidMovesForFrom(selected);
var validMove = null;
for (var i = 0; i < validMoves.length; i++) {
if (validMoves[i].to === to) {
validMove = validMoves[i];
break;
}
if (validMoves[i].to === to) { validMove = validMoves[i]; break; }
}
if (validMove) {
// Execute the valid move
UI.animateMove(from, to);
UI.clearSelected();
UI.clearHighlights();
executeMove(validMove);
} else {
// Not a valid destination — check if it's the user's own piece (swap selection)
if (isOwnPiece(pointOrAction)) {
UI.clearSelected();
UI.clearHighlights();
selectSource(pointOrAction);
} else {
// Invalid click — flash feedback, keep selection
UI.setStatus('وجهة غير صالحة — اختر مكان آخر');
setTimeout(function() {
if (UI.getSelected() !== null) {
UI.setStatus('اختر وجهة');
}
}, 1500);
}
} else if (isOwnPiece(pointOrAction)) {
// Clicked another own piece — swap selection
deselect();
selectSource(pointOrAction);
}
}
function isSamePoint(selected, pointOrAction) {
if (selected === 'bar' && pointOrAction === 'bar') return true;
if (pointOrAction === 'bar' || pointOrAction === 'off' || pointOrAction === 'pass') return false;
var parsed = parseInt(pointOrAction);
return selected === parsed;
}
// ─── Source selection ──────────────────────────────────────────────────────
function parseTarget(pointOrAction) {
if (pointOrAction === 'off') return 'off';
if (pointOrAction === 'bar') return 'bar';
var parsed = parseInt(pointOrAction);
if (isNaN(parsed)) return pointOrAction;
return parsed;
}
function selectSource(pointOrAction) {
var player = state.currentTurn;
function isOwnPiece(pointOrAction) {
if (pointOrAction === 'bar') {
return state.bar[state.currentTurn] > 0;
}
if (pointOrAction === 'off') return false;
var point = parseInt(pointOrAction);
if (isNaN(point)) return false;
var count = state.board[point];
return (state.currentTurn === 0) ? count > 0 : count < 0;
}
// Must enter from bar first
if (state.bar[player] > 0) {
if (pointOrAction !== 'bar') {
UI.setStatus('أدخل قطعتك من البار أولاً');
return;
}
var moves = getValidMovesForFrom('bar');
if (moves.length === 0) return;
function selectSource(pointOrAction) {
// If player has checkers on bar, they MUST enter from bar first
if (state.bar[state.currentTurn] > 0) {
if (pointOrAction === 'bar') {
// Auto-move if only one destination
if (moves.length === 1) {
UI.setSelected('bar');
highlightValidTargets('bar');
UI.setStatus('اختر وجهة الدخول من البار');
} else {
UI.setStatus('يجب إدخال القطع من البار أولاً');
setTimeout(function() { executeMove(moves[0]); }, 150);
return;
}
UI.setSelected('bar');
UI.highlightTargets(moves.map(function(m) { return m.to; }));
UI.setStatus('اختر وجهة');
return;
}
if (pointOrAction === 'bar') return; // No checkers on bar
var point = parseInt(pointOrAction);
if (isNaN(point)) return;
var count = state.board[point];
var isOwn = (state.currentTurn === 0) ? count > 0 : count < 0;
if (isOwn) {
// Check if this piece actually has valid moves
var moves = getValidMovesForFrom(point);
if (moves.length > 0) {
UI.setSelected(point);
highlightValidTargets(point);
UI.setStatus('اختر وجهة');
} else {
UI.setStatus('هذه القطعة لا تملك حركة — اختر قطعة أخرى');
}
} else {
UI.setStatus('اختر قطعة من قطعك');
}
}
var isOwn = (player === 0) ? count > 0 : count < 0;
if (!isOwn) return;
function getValidMovesForFrom(from) {
var allMoves = [];
for (var i = 0; i < state.diceRemaining.length; i++) {
var die = state.diceRemaining[i];
var moves = C.getValidMoves(state.board, state.bar, state.borneOff, state.currentTurn, die);
for (var j = 0; j < moves.length; j++) {
if (moves[j].from === from) {
// Include dieUsed for proper deduplication
var move = {
from: moves[j].from,
to: moves[j].to,
hit: moves[j].hit,
dieUsed: die
};
// Deduplicate by destination (keep first occurrence)
var isDup = false;
for (var k = 0; k < allMoves.length; k++) {
if (allMoves[k].to === move.to) { isDup = true; break; }
}
if (!isDup) allMoves.push(move);
}
}
}
return allMoves;
}
var moves = getValidMovesForFrom(point);
if (moves.length === 0) return;
function highlightValidTargets(from) {
var moves = getValidMovesForFrom(from);
var targets = [];
for (var i = 0; i < moves.length; i++) {
targets.push(moves[i].to);
// Auto-move if only one destination
if (moves.length === 1) {
UI.setSelected(point);
UI.render(state);
setTimeout(function() { executeMove(moves[0]); }, 150);
return;
}
UI.highlightPoints(targets);
UI.setSelected(point);
UI.highlightTargets(moves.map(function(m) { return m.to; }));
UI.setStatus('اختر وجهة');
}
// ─── Execute move ──────────────────────────────────────────────────────────
function executeMove(move) {
// Save undo state
undoStack.push({
board: state.board.slice(),
bar: state.bar.slice(),
borneOff: state.borneOff.slice(),
diceRemaining: state.diceRemaining.slice()
});
var result = C.applyMove(state.board, state.bar, state.borneOff, state.currentTurn, move);
state.board = result.board;
state.bar = result.bar;
state.borneOff = result.borneOff;
var dieUsed = move.dieUsed;
var idx = state.diceRemaining.indexOf(dieUsed);
// Remove used die
var idx = state.diceRemaining.indexOf(move.dieUsed);
if (idx !== -1) state.diceRemaining.splice(idx, 1);
state.moves.push({
player: state.currentTurn,
from: move.from,
to: move.to,
die: dieUsed,
hit: move.hit
});
state.moves.push({ player: state.currentTurn, from: move.from, to: move.to, die: move.dieUsed, hit: move.hit });
if (move.hit) {
UI.showLog(state.players[state.currentTurn].name + ' ضرب قطعة!');
}
if (move.hit) UI.showLog('ضرب!');
UI.clearSelected();
UI.clearHighlights();
UI.render(state);
// Check win
if (C.checkWin(state.borneOff, state.currentTurn)) {
handleWin();
return;
}
// Check if more moves available
if (state.diceRemaining.length === 0) {
UI.setStatus('انتهت حركاتك');
UI.render(state);
setTimeout(function() { endTurn(); }, 600);
} else if (!C.canMove(state.board, state.bar, state.borneOff, state.currentTurn, state.diceRemaining)) {
// Auto-pass with remaining dice
UI.setStatus('لا توجد حركة — ينتقل الدور');
UI.showLog('لا توجد حركة متاحة بالنرد المتبقي');
UI.render(state);
setTimeout(function() { endTurn(); }, 1200);
} else {
// More moves available
UI.setStatus('اختر قطعة لتحريكها — المتبقي: ' + state.diceRemaining.join('، '));
setTimeout(function() { endTurn(); }, 400);
return;
}
if (!C.canMove(state.board, state.bar, state.borneOff, state.currentTurn, state.diceRemaining)) {
UI.setStatus('لا توجد حركة أخرى');
setTimeout(function() { endTurn(); }, 800);
return;
}
// Continue — show next movable sources
beginHumanTurn();
}
// ─── Auto-execute single sequence ─────────────────────────────────────────
function autoExecuteSequence(moves) {
var i = 0;
function next() {
if (i >= moves.length) {
setTimeout(function() { endTurn(); }, 300);
return;
}
var move = moves[i];
var result = C.applyMove(state.board, state.bar, state.borneOff, state.currentTurn, move);
state.board = result.board;
state.bar = result.bar;
state.borneOff = result.borneOff;
var dIdx = state.diceRemaining.indexOf(move.dieUsed);
if (dIdx !== -1) state.diceRemaining.splice(dIdx, 1);
state.moves.push({ player: state.currentTurn, from: move.from, to: move.to, die: move.dieUsed, hit: move.hit });
UI.render(state);
i++;
if (C.checkWin(state.borneOff, state.currentTurn)) { handleWin(); return; }
setTimeout(next, 200);
}
next();
}
// ─── Undo ──────────────────────────────────────────────────────────────────
function undoMove() {
if (undoStack.length === 0) return;
var prev = undoStack.pop();
state.board = prev.board;
state.bar = prev.bar;
state.borneOff = prev.borneOff;
state.diceRemaining = prev.diceRemaining;
state.moves.pop();
UI.clearSelected();
UI.clearHighlights();
UI.render(state);
beginHumanTurn();
}
// ─── Helpers ───────────────────────────────────────────────────────────────
function deselect() {
UI.clearSelected();
UI.clearHighlights();
UI.setStatus('اختر قطعة');
showMovableSources();
}
function isSamePoint(a, b) {
if (a === 'bar' && b === 'bar') return true;
return parseInt(a) === parseInt(b);
}
function parseTarget(val) {
if (val === 'off') return 'off';
if (val === 'bar') return 'bar';
var n = parseInt(val);
return isNaN(n) ? val : n;
}
function isOwnPiece(val) {
if (val === 'bar') return state.bar[state.currentTurn] > 0;
if (val === 'off') return false;
var p = parseInt(val);
if (isNaN(p)) return false;
var count = state.board[p];
return (state.currentTurn === 0) ? count > 0 : count < 0;
}
function getValidMovesForFrom(from) {
var allMoves = [];
for (var i = 0; i < state.diceRemaining.length; i++) {
var die = state.diceRemaining[i];
var moves = C.getValidMoves(state.board, state.bar, state.borneOff, state.currentTurn, die);
for (var j = 0; j < moves.length; j++) {
if (moves[j].from === from) {
var move = { from: moves[j].from, to: moves[j].to, hit: moves[j].hit, dieUsed: die };
var isDup = false;
for (var k = 0; k < allMoves.length; k++) {
if (allMoves[k].to === move.to && allMoves[k].dieUsed === move.dieUsed) { isDup = true; break; }
}
if (!isDup) allMoves.push(move);
}
}
}
return allMoves;
}
// ─── End turn ──────────────────────────────────────────────────────────────
function endTurn() {
undoStack = [];
state.currentTurn = 1 - state.currentTurn;
state.phase = 'awaiting_roll';
state.dice = null;
state.diceRemaining = [];
state.phase = 'awaiting_roll';
UI.clearSelected();
UI.clearHighlights();
UI.render(state);
if (state.players[state.currentTurn].type === 'bot') {
UI.setStatus('انتظر... البوت يفكر');
UI.render(state);
setTimeout(function() { executeBotTurn(); }, 800);
UI.setStatus('انتظر...');
setTimeout(function() { executeBotTurn(); }, 600);
} else {
UI.setStatus('دورك — ارمِ النرد');
UI.render(state);
// Auto-roll for human
UI.setStatus('');
setTimeout(function() { rollDice(); }, 400);
}
}
// ─── Bot turn ──────────────────────────────────────────────────────────────
function executeBotTurn() {
if (state.phase === 'awaiting_roll') {
var d1 = Math.ceil(Math.random() * 6);
var d2 = Math.ceil(Math.random() * 6);
state.dice = [d1, d2];
state.diceRemaining = C.getDiceRemaining(d1, d2);
state.phase = 'moving';
UI.showLog(state.players[state.currentTurn].name + ' رمى ' + d1 + ' و ' + d2);
UI.setStatus('البوت يفكر...');
UI.render(state);
}
// Roll for bot
var d1 = Math.ceil(Math.random() * 6);
var d2 = Math.ceil(Math.random() * 6);
state.dice = [d1, d2];
state.diceRemaining = C.getDiceRemaining(d1, d2);
state.phase = 'moving';
UI.showLog(state.players[state.currentTurn].name + ': ' + d1 + '، ' + d2);
UI.render(state);
if (!C.canMove(state.board, state.bar, state.borneOff, state.currentTurn, state.diceRemaining)) {
UI.showLog(state.players[state.currentTurn].name + ' — لا توجد حركة');
UI.setStatus('البوت لا يستطيع الحركة');
UI.showLog(state.players[state.currentTurn].name + ' — لا حركة');
setTimeout(function() { endTurn(); }, 800);
return;
}
......@@ -350,7 +409,7 @@ var BackgammonGame = (function() {
var moves = Bot.chooseMove(state.board, state.bar, state.borneOff, state.currentTurn, state.diceRemaining, difficulty);
if (!moves || moves.length === 0) {
setTimeout(function() { endTurn(); }, 800);
setTimeout(function() { endTurn(); }, 600);
return;
}
......@@ -359,7 +418,7 @@ var BackgammonGame = (function() {
function executeBotMoves(moves, idx) {
if (idx >= moves.length) {
setTimeout(function() { endTurn(); }, 600);
setTimeout(function() { endTurn(); }, 400);
return;
}
......@@ -372,23 +431,8 @@ var BackgammonGame = (function() {
var dieIdx = state.diceRemaining.indexOf(move.dieUsed);
if (dieIdx !== -1) state.diceRemaining.splice(dieIdx, 1);
state.moves.push({
player: state.currentTurn,
from: move.from,
to: move.to,
die: move.dieUsed,
hit: move.hit
});
state.moves.push({ player: state.currentTurn, from: move.from, to: move.to, die: move.dieUsed, hit: move.hit });
// Log each bot move individually
var fromLabel = (move.from === 'bar') ? 'البار' : (move.from + 1);
var toLabel = (move.to === 'off') ? 'خارج' : (move.to + 1);
UI.showLog(state.players[state.currentTurn].name + ': ' + fromLabel + ' → ' + toLabel + ' (' + move.dieUsed + ')');
if (move.hit) UI.showLog(state.players[state.currentTurn].name + ' ضرب قطعة!');
UI.setStatus('البوت يتحرك... (' + (idx + 1) + '/' + moves.length + ')');
UI.animateMove(move.from, move.to);
UI.render(state);
if (C.checkWin(state.borneOff, state.currentTurn)) {
......@@ -396,9 +440,11 @@ var BackgammonGame = (function() {
return;
}
setTimeout(function() { executeBotMoves(moves, idx + 1); }, 500);
setTimeout(function() { executeBotMoves(moves, idx + 1); }, 300);
}
// ─── Win ───────────────────────────────────────────────────────────────────
function handleWin() {
var winner = state.currentTurn;
var winType = C.getWinType(state.board, state.bar, state.borneOff, winner);
......@@ -409,8 +455,7 @@ var BackgammonGame = (function() {
state.phase = 'game_over';
var winLabel = (winType === 'backgammon') ? 'باكغمّون!' : (winType === 'gammon') ? 'غمّون!' : 'فوز!';
UI.showLog(state.players[winner].name + ' — ' + winLabel + ' (+' + points + ')');
UI.setStatus(state.players[winner].name + ' فاز!');
UI.showLog(state.players[winner].name + ' — ' + winLabel);
UI.showResult(state.players[winner].name, winType, points);
UI.render(state);
......@@ -419,9 +464,7 @@ var BackgammonGame = (function() {
}
}
function getState() {
return state;
}
function getState() { return state; }
return {
init: init,
......
......@@ -3,139 +3,70 @@ var BackgammonUI = (function() {
var C = BackgammonConstants;
var selected = null;
var highlights = [];
var gameState = null;
var statusText = '';
var movablePoints = [];
function init(state) {
gameState = state;
var board = document.getElementById('bg-board');
if (!board) return;
board.innerHTML = buildBoardHTML();
// Ensure status element exists
ensureStatusElement();
bindEvents();
}
function ensureStatusElement() {
var existing = document.getElementById('bg-status');
if (!existing) {
var diceContainer = document.getElementById('bg-dice-container');
if (diceContainer && diceContainer.parentNode) {
var statusEl = document.createElement('div');
statusEl.id = 'bg-status';
statusEl.className = 'bg-status';
diceContainer.parentNode.insertBefore(statusEl, diceContainer.nextSibling);
}
}
}
function buildBoardHTML() {
var html = '';
html += '<div class="bg-board-inner">';
var html = '<div class="bg-board-inner">';
html += '<div class="bg-half bg-half--top">';
html += '<div class="bg-quadrant bg-quadrant--top-left">';
for (var i = 12; i <= 17; i++) {
html += '<div class="bg-point bg-point--top" data-point="' + i + '"></div>';
}
for (var i = 12; i <= 17; i++) html += '<div class="bg-point bg-point--top" data-point="' + i + '"></div>';
html += '</div>';
html += '<div class="bg-bar-area bg-bar--top" data-action="bar"></div>';
html += '<div class="bg-quadrant bg-quadrant--top-right">';
for (var i = 18; i <= 23; i++) {
html += '<div class="bg-point bg-point--top" data-point="' + i + '"></div>';
}
html += '</div>';
html += '</div>';
for (var i = 18; i <= 23; i++) html += '<div class="bg-point bg-point--top" data-point="' + i + '"></div>';
html += '</div></div>';
html += '<div class="bg-half bg-half--bottom">';
html += '<div class="bg-quadrant bg-quadrant--bottom-left">';
for (var i = 11; i >= 6; i--) {
html += '<div class="bg-point bg-point--bottom" data-point="' + i + '"></div>';
}
for (var i = 11; i >= 6; i--) html += '<div class="bg-point bg-point--bottom" data-point="' + i + '"></div>';
html += '</div>';
html += '<div class="bg-bar-area bg-bar--bottom" data-action="bar"></div>';
html += '<div class="bg-quadrant bg-quadrant--bottom-right">';
for (var i = 5; i >= 0; i--) {
html += '<div class="bg-point bg-point--bottom" data-point="' + i + '"></div>';
}
html += '</div>';
html += '</div>';
html += '</div>';
for (var i = 5; i >= 0; i--) html += '<div class="bg-point bg-point--bottom" data-point="' + i + '"></div>';
html += '</div></div></div>';
html += '<div class="bg-borne-off-area">';
html += '<div class="bg-borne-off bg-borne-off--black" data-action="off"></div>';
html += '<div class="bg-borne-off bg-borne-off--white" data-action="off"></div>';
html += '</div>';
return html;
}
function bindEvents() {
var board = document.getElementById('bg-board');
if (!board) return;
board.addEventListener('click', function(e) {
var point = e.target.closest('[data-point]');
var action = e.target.closest('[data-action]');
// Tap feedback
var clickTarget = point || action;
if (clickTarget) {
clickTarget.classList.add('bg-tap-flash');
setTimeout(function() {
clickTarget.classList.remove('bg-tap-flash');
}, 200);
}
if (point) {
BackgammonGame.handlePointClick(point.dataset.point);
} else if (action) {
BackgammonGame.handlePointClick(action.dataset.action);
}
});
// Hover preview for valid targets
board.addEventListener('mouseover', function(e) {
if (selected === null) return;
var point = e.target.closest('[data-point]');
var action = e.target.closest('[data-action]');
var el = point || action;
if (el && el.classList.contains('bg-point--valid')) {
el.classList.add('bg-point--hover-preview');
}
});
board.addEventListener('mouseout', function(e) {
var point = e.target.closest('[data-point]');
var action = e.target.closest('[data-action]');
var el = point || action;
if (el) {
el.classList.remove('bg-point--hover-preview');
}
if (point) BackgammonGame.handlePointClick(point.dataset.point);
else if (action) BackgammonGame.handlePointClick(action.dataset.action);
});
var rollBtn = document.getElementById('bg-roll-btn');
if (rollBtn) {
rollBtn.addEventListener('click', function() {
BackgammonGame.rollDice();
});
}
if (rollBtn) rollBtn.addEventListener('click', function() { BackgammonGame.rollDice(); });
var undoBtn = document.getElementById('bg-undo-btn');
if (undoBtn) undoBtn.addEventListener('click', function() { BackgammonGame.handlePointClick('undo'); });
}
// ─── Render ────────────────────────────────────────────────────────────────
function render(state) {
gameState = state;
renderCheckers(state);
renderBar(state);
renderBorneOff(state);
renderDice(state);
renderTurnIndicator(state);
renderControls(state);
renderPipCount(state);
renderStatus();
renderMovableCheckers(state);
renderActivePlayerHighlight(state);
}
function renderCheckers(state) {
......@@ -146,29 +77,24 @@ var BackgammonUI = (function() {
var absCount = Math.abs(count);
var player = (count > 0) ? 0 : (count < 0) ? 1 : -1;
var checkersHTML = '';
var html = '';
if (absCount > 0) {
var display = Math.min(absCount, 5);
var compact = absCount > 3 ? ' bg-checkers-stack--compact' : '';
checkersHTML = '<div class="bg-checkers-stack' + compact + '">';
html = '<div class="bg-checkers-stack' + compact + '">';
for (var c = 0; c < display; c++) {
var cls = 'bg-checker bg-checker--' + (player === 0 ? 'white' : 'black');
if (selected === idx && c === display - 1) cls += ' bg-checker--selected';
if (movablePoints.indexOf(idx) !== -1 && c === display - 1) cls += ' bg-checker--movable';
checkersHTML += '<div class="' + cls + '"></div>';
}
checkersHTML += '</div>';
if (absCount > 5) {
checkersHTML += '<span class="bg-checker-count-badge">' + absCount + '</span>';
html += '<div class="' + cls + '"></div>';
}
html += '</div>';
if (absCount > 5) html += '<span class="bg-checker-count-badge">' + absCount + '</span>';
}
el.innerHTML = html;
el.innerHTML = checkersHTML;
el.classList.remove('bg-point--valid', 'bg-point--selected', 'bg-point--clickable');
// Clear all state classes
el.classList.remove('bg-point--valid', 'bg-point--selected', 'bg-point--source');
if (selected === idx) el.classList.add('bg-point--selected');
if (highlights.indexOf(idx) !== -1) el.classList.add('bg-point--valid');
if (movablePoints.indexOf(idx) !== -1) el.classList.add('bg-point--clickable');
});
}
......@@ -177,29 +103,18 @@ var BackgammonUI = (function() {
var barBottom = document.querySelector('.bg-bar--bottom');
if (!barTop || !barBottom) return;
var blackBar = state.bar[1];
var whiteBar = state.bar[0];
barTop.innerHTML = buildBarHTML(state.bar[1], 'black');
barBottom.innerHTML = buildBarHTML(state.bar[0], 'white');
barTop.innerHTML = buildBarCheckers(blackBar, 'black', selected === 'bar' && state.currentTurn === 1);
barBottom.innerHTML = buildBarCheckers(whiteBar, 'white', selected === 'bar' && state.currentTurn === 0);
// Mark bar as clickable if player has checkers on bar
barTop.classList.toggle('bg-point--clickable', state.currentTurn === 1 && blackBar > 0 && state.phase === 'moving');
barBottom.classList.toggle('bg-point--clickable', state.currentTurn === 0 && whiteBar > 0 && state.phase === 'moving');
barTop.classList.toggle('bg-point--selected', selected === 'bar' && state.currentTurn === 1);
barBottom.classList.toggle('bg-point--selected', selected === 'bar' && state.currentTurn === 0);
barBottom.classList.toggle('bg-point--source', selected === 'bar');
}
function buildBarCheckers(count, color, isSelected) {
function buildBarHTML(count, color) {
var html = '';
var display = Math.min(count, 4);
for (var i = 0; i < display; i++) {
var cls = 'bg-checker bg-checker--' + color;
if (isSelected && i === display - 1) cls += ' bg-checker--selected';
if (isSelected) cls += ' bg-checker--movable';
html += '<div class="' + cls + '"></div>';
for (var i = 0; i < Math.min(count, 4); i++) {
html += '<div class="bg-checker bg-checker--' + color + '"></div>';
}
if (count > 4) html += '<span class="bg-checker-count">' + count + '</span>';
if (count > 4) html += '<span class="bg-checker-count-badge">' + count + '</span>';
return html;
}
......@@ -207,137 +122,57 @@ var BackgammonUI = (function() {
var whiteOff = document.querySelector('.bg-borne-off--white');
var blackOff = document.querySelector('.bg-borne-off--black');
if (!whiteOff || !blackOff) return;
whiteOff.innerHTML = buildBorneOff(state.borneOff[0], 'white');
blackOff.innerHTML = buildBorneOff(state.borneOff[1], 'black');
whiteOff.classList.toggle('bg-point--valid', highlights.indexOf('off') !== -1 && state.currentTurn === 0);
blackOff.classList.toggle('bg-point--valid', highlights.indexOf('off') !== -1 && state.currentTurn === 1);
whiteOff.innerHTML = buildBorneHTML(state.borneOff[0], 'white');
blackOff.innerHTML = buildBorneHTML(state.borneOff[1], 'black');
}
function buildBorneOff(count, color) {
var html = '<div class="bg-borne-off-stack">';
for (var i = 0; i < count; i++) {
html += '<div class="bg-borne-chip bg-borne-chip--' + color + '"></div>';
}
html += '</div>';
if (count > 0) html += '<span class="bg-borne-count">' + count + '/15</span>';
function buildBorneHTML(count, color) {
var html = '';
for (var i = 0; i < count; i++) html += '<div class="bg-borne-chip bg-borne-chip--' + color + '"></div>';
if (count > 0) html += '<span class="bg-borne-count">' + count + '</span>';
return html;
}
function renderDice(state) {
var container = document.getElementById('bg-dice-container');
if (!container) return;
if (!state.dice) {
container.innerHTML = '';
return;
}
// For doubles, show 4 dice; for normal, show 2
var diceToRender = state.dice;
var isDoubles = (state.dice[0] === state.dice[1]);
if (!state.dice) { container.innerHTML = ''; return; }
var html = '<div class="bg-dice-pair">';
if (isDoubles) {
// Show 4 dice for doubles
var usedCount = 4 - state.diceRemaining.length;
for (var d = 0; d < 4; d++) {
var used = (d < usedCount);
html += renderSingleDie(state.dice[0], used);
}
} else {
// Show 2 dice - mark each as used if not in diceRemaining
var remaining = state.diceRemaining.slice();
for (var d = 0; d < 2; d++) {
var val = state.dice[d];
var rIdx = remaining.indexOf(val);
var used = (rIdx === -1);
if (rIdx !== -1) remaining.splice(rIdx, 1);
html += renderSingleDie(val, used);
}
var total = state.dice[0] === state.dice[1] ? 4 : 2;
var remaining = state.diceRemaining.slice();
for (var d = 0; d < total; d++) {
var val = state.dice[d < 2 ? d : 0];
var usedIdx = remaining.indexOf(val);
var isUsed = (usedIdx === -1);
if (!isUsed) remaining.splice(usedIdx, 1);
html += renderDie(val, isUsed);
}
html += '</div>';
// Dice remaining indicator text
if (state.diceRemaining.length > 0 && state.phase === 'moving') {
html += '<div class="bg-dice-remaining-text">المتبقي: ' + state.diceRemaining.join('، ') + '</div>';
}
container.innerHTML = html;
}
function renderSingleDie(value, used) {
function renderDie(value, used) {
var cls = 'bg-dice' + (used ? ' bg-dice--used' : '');
var dots = C.DICE_FACES[value] || [];
var html = '<div class="' + cls + '">';
for (var i = 0; i < 9; i++) {
var visible = dots.indexOf(i) !== -1;
html += '<span class="bg-dice-dot' + (visible ? ' bg-dice-dot--filled' : '') + '"></span>';
html += '<span class="bg-dice-dot' + (dots.indexOf(i) !== -1 ? ' bg-dice-dot--filled' : '') + '"></span>';
}
html += '</div>';
return html;
}
function renderTurnIndicator(state) {
var el = document.getElementById('bg-turn');
if (!el) return;
var player = state.players[state.currentTurn];
var color = state.currentTurn === 0 ? 'white' : 'black';
var phaseText = state.phase === 'awaiting_roll' ? 'ارمِ النرد' : state.phase === 'moving' ? 'حرّك قطعك' : '';
var isActive = state.players[state.currentTurn].type === 'human';
if (isActive) {
el.classList.add('bg-turn-indicator--active');
} else {
el.classList.remove('bg-turn-indicator--active');
}
el.innerHTML = '<div class="bg-player-dot bg-player-dot--' + color + '"></div>' +
'<span class="bg-player-name">' + player.name + '</span>' +
(phaseText ? '<span class="bg-player-pip">' + phaseText + '</span>' : '');
return html + '</div>';
}
function renderControls(state) {
var rollBtn = document.getElementById('bg-roll-btn');
if (!rollBtn) return;
var isHumanTurn = state.players[state.currentTurn].type === 'human';
var canRoll = state.phase === 'awaiting_roll' && isHumanTurn;
var undoBtn = document.getElementById('bg-undo-btn');
rollBtn.style.display = canRoll ? '' : 'none';
rollBtn.disabled = !canRoll;
// Pass/Skip button
renderPassButton(state);
}
function renderPassButton(state) {
var existing = document.getElementById('bg-pass-btn');
var isHumanTurn = state.players[state.currentTurn].type === 'human';
var canMoveAny = state.phase === 'moving' && state.diceRemaining.length > 0 &&
C.canMove(state.board, state.bar, state.borneOff, state.currentTurn, state.diceRemaining);
var shouldShowPass = state.phase === 'moving' && isHumanTurn &&
state.diceRemaining.length > 0 && !canMoveAny;
if (shouldShowPass) {
if (!existing) {
var btn = document.createElement('button');
btn.id = 'bg-pass-btn';
btn.className = 'bg-pass-btn';
btn.textContent = 'تخطي الدور';
btn.addEventListener('click', function() {
BackgammonGame.handlePointClick('pass');
});
var rollBtn = document.getElementById('bg-roll-btn');
if (rollBtn && rollBtn.parentNode) {
rollBtn.parentNode.insertBefore(btn, rollBtn.nextSibling);
}
}
} else {
if (existing) {
existing.parentNode.removeChild(existing);
}
if (rollBtn) {
var canRoll = state.phase === 'awaiting_roll' && state.players[state.currentTurn].type === 'human';
rollBtn.style.display = canRoll ? '' : 'none';
}
if (undoBtn) {
undoBtn.style.display = state.phase === 'moving' && state.players[state.currentTurn].type === 'human' ? '' : 'none';
}
}
......@@ -348,168 +183,68 @@ var BackgammonUI = (function() {
if (el1) el1.textContent = C.pipCount(state.board, state.bar, 1);
}
function renderStatus() {
var el = document.getElementById('bg-status');
if (!el) return;
el.textContent = statusText;
el.style.display = statusText ? '' : 'none';
}
function renderMovableCheckers(state) {
movablePoints = [];
if (state.phase !== 'moving') return;
if (state.players[state.currentTurn].type !== 'human') return;
if (selected !== null) return; // Don't show movable when already selected
var sign = (state.currentTurn === 0) ? 1 : -1;
// If player has checkers on bar, only bar is movable
if (state.bar[state.currentTurn] > 0) {
// Bar is handled separately, no point highlights needed
return;
}
// Find all points that have valid moves
for (var i = 0; i < 24; i++) {
if (state.board[i] * sign <= 0) continue;
var hasMove = false;
for (var d = 0; d < state.diceRemaining.length; d++) {
var moves = C.getValidMoves(state.board, state.bar, state.borneOff, state.currentTurn, state.diceRemaining[d]);
for (var m = 0; m < moves.length; m++) {
if (moves[m].from === i) { hasMove = true; break; }
}
if (hasMove) break;
}
if (hasMove) movablePoints.push(i);
}
// Apply classes
var points = document.querySelectorAll('.bg-point');
points.forEach(function(el) {
var idx = parseInt(el.dataset.point);
if (movablePoints.indexOf(idx) !== -1) {
el.classList.add('bg-point--clickable');
var topChecker = el.querySelector('.bg-checker:last-child');
if (topChecker) topChecker.classList.add('bg-checker--movable');
} else {
el.classList.remove('bg-point--clickable');
}
});
}
function renderActivePlayerHighlight(state) {
var info0 = document.getElementById('bg-player-info-0');
var info1 = document.getElementById('bg-player-info-1');
if (info0) {
info0.classList.toggle('bg-player-info--active', state.currentTurn === 0);
}
if (info1) {
info1.classList.toggle('bg-player-info--active', state.currentTurn === 1);
}
}
// ─── Selection & Highlighting ──────────────────────────────────────────────
function setSelected(val) {
selected = val;
// Immediately apply visual feedback to DOM without waiting for full render
// Clear previous selections
var prevSelected = document.querySelectorAll('.bg-point--selected, .bg-checker--selected');
prevSelected.forEach(function(el) {
el.classList.remove('bg-point--selected');
el.classList.remove('bg-checker--selected');
});
var prevBarSelected = document.querySelectorAll('.bg-bar-area.bg-point--selected');
prevBarSelected.forEach(function(el) { el.classList.remove('bg-point--selected'); });
// Remove movable indicators when selecting
var movableEls = document.querySelectorAll('.bg-checker--movable, .bg-point--clickable');
movableEls.forEach(function(el) {
el.classList.remove('bg-checker--movable');
el.classList.remove('bg-point--clickable');
});
if (val === null) return;
// Apply visual immediately
if (val === 'bar') {
// Highlight the bar area
var turn = gameState ? gameState.currentTurn : 0;
var barEl = turn === 0 ? document.querySelector('.bg-bar--bottom') : document.querySelector('.bg-bar--top');
if (barEl) {
barEl.classList.add('bg-point--selected');
var topChecker = barEl.querySelector('.bg-checker:last-child');
if (topChecker) topChecker.classList.add('bg-checker--selected');
}
} else {
var pointEl = document.querySelector('[data-point="' + val + '"]');
if (pointEl) {
pointEl.classList.add('bg-point--selected');
var topChecker = pointEl.querySelector('.bg-checker:last-child');
if (topChecker) topChecker.classList.add('bg-checker--selected');
var bar = document.querySelector('.bg-bar--bottom');
if (bar) bar.classList.add('bg-point--selected');
} else if (val !== null) {
var el = document.querySelector('[data-point="' + val + '"]');
if (el) {
el.classList.add('bg-point--selected');
var checker = el.querySelector('.bg-checker:last-child');
if (checker) checker.classList.add('bg-checker--selected');
}
}
}
function getSelected() {
return selected;
}
function getSelected() { return selected; }
function clearSelected() {
selected = null;
var points = document.querySelectorAll('.bg-point--selected');
points.forEach(function(el) { el.classList.remove('bg-point--selected'); });
var checkers = document.querySelectorAll('.bg-checker--selected');
checkers.forEach(function(el) { el.classList.remove('bg-checker--selected'); });
var bars = document.querySelectorAll('.bg-bar-area.bg-point--selected');
bars.forEach(function(el) { el.classList.remove('bg-point--selected'); });
document.querySelectorAll('.bg-point--selected').forEach(function(el) { el.classList.remove('bg-point--selected'); });
document.querySelectorAll('.bg-checker--selected').forEach(function(el) { el.classList.remove('bg-checker--selected'); });
}
function highlightPoints(targets) {
highlights = targets;
var points = document.querySelectorAll('.bg-point');
points.forEach(function(el) {
var idx = parseInt(el.dataset.point);
el.classList.toggle('bg-point--valid', targets.indexOf(idx) !== -1);
});
var borneOffs = document.querySelectorAll('.bg-borne-off');
borneOffs.forEach(function(el) {
el.classList.toggle('bg-point--valid', targets.indexOf('off') !== -1);
function highlightSources(sources) {
clearHighlights();
sources.forEach(function(src) {
if (src === 'bar') {
var bar = document.querySelector('.bg-bar--bottom');
if (bar) bar.classList.add('bg-point--source');
} else {
var el = document.querySelector('[data-point="' + src + '"]');
if (el) el.classList.add('bg-point--source');
}
});
}
// Also ensure the selected point stays visually marked
if (selected !== null && selected !== 'bar') {
var selEl = document.querySelector('[data-point="' + selected + '"]');
if (selEl) selEl.classList.add('bg-point--selected');
}
function highlightTargets(targets) {
clearHighlights();
targets.forEach(function(t) {
if (t === 'off') {
document.querySelectorAll('.bg-borne-off').forEach(function(el) { el.classList.add('bg-point--valid'); });
} else {
var el = document.querySelector('[data-point="' + t + '"]');
if (el) el.classList.add('bg-point--valid');
}
});
}
function clearHighlights() {
highlights = [];
var els = document.querySelectorAll('.bg-point--valid');
els.forEach(function(el) { el.classList.remove('bg-point--valid'); });
var hovers = document.querySelectorAll('.bg-point--hover-preview');
hovers.forEach(function(el) { el.classList.remove('bg-point--hover-preview'); });
document.querySelectorAll('.bg-point--valid, .bg-point--source').forEach(function(el) {
el.classList.remove('bg-point--valid', 'bg-point--source');
});
}
function setStatus(msg) {
statusText = msg;
renderStatus();
}
// ─── Status & Log ──────────────────────────────────────────────────────────
function animateMove(fromPoint, toPoint) {
// Brief animation class on destination
var targetEl = null;
if (toPoint === 'off') {
var turn = gameState ? gameState.currentTurn : 0;
targetEl = document.querySelector(turn === 0 ? '.bg-borne-off--white' : '.bg-borne-off--black');
} else if (typeof toPoint === 'number') {
targetEl = document.querySelector('[data-point="' + toPoint + '"]');
}
if (targetEl) {
targetEl.classList.add('bg-move-flash');
setTimeout(function() {
targetEl.classList.remove('bg-move-flash');
}, 400);
}
function setStatus(msg) {
var el = document.getElementById('bg-status');
if (el) el.textContent = msg;
}
function showLog(msg) {
......@@ -519,45 +254,35 @@ var BackgammonUI = (function() {
entry.className = 'bg-log-entry';
entry.textContent = msg;
log.prepend(entry);
if (log.children.length > 20) log.removeChild(log.lastChild);
if (log.children.length > 15) log.removeChild(log.lastChild);
}
function showResult(winnerName, winType, points) {
var overlay = document.getElementById('bg-result-overlay');
if (!overlay) return;
var label = (winType === 'backgammon') ? 'باكغمّون!' : (winType === 'gammon') ? 'غمّون!' : 'فوز عادي';
var label = (winType === 'backgammon') ? 'باكغمّون!' : (winType === 'gammon') ? 'غمّون!' : 'فوز!';
overlay.innerHTML = '<div class="bg-result-card">' +
'<h2>' + winnerName + '</h2>' +
'<p class="bg-result-type">' + label + '</p>' +
'<p class="bg-result-points">+' + points + ' نقاط</p>' +
'<button class="bg-new-game-btn" id="bg-new-game-btn">لعبة جديدة</button>' +
'</div>';
'<p class="bg-result-points">+' + points + '</p>' +
'<button class="bg-new-game-btn" onclick="location.reload()">لعبة جديدة</button></div>';
overlay.style.display = 'flex';
// Bind new game button
var newGameBtn = document.getElementById('bg-new-game-btn');
if (newGameBtn) {
newGameBtn.addEventListener('click', function() {
overlay.style.display = 'none';
if (typeof BackgammonGame.init === 'function' && gameState) {
// Restart with same options
location.reload();
}
});
}
}
function animateMove() {}
return {
init: init,
render: render,
setSelected: setSelected,
getSelected: getSelected,
clearSelected: clearSelected,
highlightPoints: highlightPoints,
highlightSources: highlightSources,
highlightTargets: highlightTargets,
clearHighlights: clearHighlights,
setStatus: setStatus,
animateMove: animateMove,
showLog: showLog,
showResult: showResult
showResult: showResult,
animateMove: animateMove
};
})();
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