Commit 25a80cf0 authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat: multiplayer dominoes — fix invite/accept flow + add domino to challenge menus

- friends.php: create domino invites in domino_matches table (not chess matches)
- friends.php: check-invites scans domino_matches + ludo_matches tables
- friends.php: accept/decline route to correct game table by game_key
- domino-match.php: merge hands on move (preserve other player's hand data)
- game.js: host deals and syncs both hands; guest waits for deal via polling
- game.js: new rounds in live mode dealt by host only
- lobby.js: pass playerIndex from accept response
- chat.js: add domino option to in-chat challenge dialog
- friends.js: pass game_key on accept/decline calls
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 3e5614d1
......@@ -158,8 +158,15 @@ function handleMove(string $userId, array $input): void {
$update['board'] = is_string($board) ? $board : json_encode($board);
}
if (isset($input['hands'])) {
$hands = $input['hands'];
$update['hands'] = is_string($hands) ? $hands : json_encode($hands);
$newHands = is_string($input['hands']) ? json_decode($input['hands'], true) : $input['hands'];
// Merge with existing hands to preserve other player's data
$existingMatch = $sdb->get('domino_matches', ['id' => 'eq.' . $matchId, 'select' => 'hands', 'limit' => 1]);
$existingHands = [];
if (!empty($existingMatch) && !isset($existingMatch['error']) && !empty($existingMatch[0]['hands'])) {
$existingHands = is_string($existingMatch[0]['hands']) ? json_decode($existingMatch[0]['hands'], true) : $existingMatch[0]['hands'];
}
$mergedHands = array_merge($existingHands ?: [], $newHands ?: []);
$update['hands'] = json_encode($mergedHands);
}
if (isset($input['boneyard'])) {
$boneyard = $input['boneyard'];
......
This diff is collapsed.
......@@ -23,7 +23,10 @@ export function mountGame(el, params) {
scene.enterGameMode();
state = createInitialState(mode, matchId, playerIndex, botLevel, players);
dealNewRound();
if (mode !== 'live') {
dealNewRound();
}
el.innerHTML = buildLayout(mode);
......@@ -179,7 +182,37 @@ function setupLiveMultiplayer(el, matchId) {
fetchOpponentProfile(el);
mp.startDisconnectWatch?.(matchId, 'domino', 60000);
fetchInitialState(el, matchId);
if (state.myPlayerIndex === 0) {
dealAndSyncToServer(el, matchId);
} else {
fetchInitialState(el, matchId);
}
}
async function dealAndSyncToServer(el, matchId) {
dealNewRound();
board.setChain(state.chain);
updateUI(el);
refreshHand();
try {
await net.post('domino-match.php', {
action: 'move',
match_id: matchId,
board: state.chain,
boneyard: state.boneyard,
hands: JSON.stringify({ '0': state.hands[0], '1': state.hands[1] }),
current_turn: state.currentPlayer,
game_state: JSON.stringify({
move_count: 0,
round: 1,
left_end: state.leftEnd,
right_end: state.rightEnd,
scores: { '0': 0, '1': 0 },
dealt: true,
})
});
} catch (e) {}
}
async function fetchOpponentProfile(el) {
......@@ -203,9 +236,16 @@ async function fetchOpponentProfile(el) {
} catch (e) {}
}
async function fetchInitialState(el, matchId) {
async function fetchInitialState(el, matchId, retries = 0) {
try {
const data = await net.post('domino-match.php', { action: 'get', match_id: matchId });
// If hands not dealt yet (host hasn't synced), retry
if ((!data?.my_hand || data.my_hand.length === 0) && retries < 10) {
setTimeout(() => fetchInitialState(el, matchId, retries + 1), 1000);
return;
}
if (data && data.board) {
const board_data = typeof data.board === 'string' ? JSON.parse(data.board) : data.board;
if (board_data.length > 0) {
......@@ -698,19 +738,26 @@ function showRoundOverlay(el, winnerIdx, points) {
function startNewRound(el) {
state.roundNumber++;
dealNewRound();
state.currentPlayer = state.matchScores[0] <= state.matchScores[1] ? 0 : 1;
if (state.mode === 'live') {
syncNewRoundToServer();
}
board.setChain(state.chain);
updateUI(el);
refreshHand();
if (state.myPlayerIndex === 0) {
dealNewRound();
syncNewRoundToServer();
board.setChain(state.chain);
updateUI(el);
refreshHand();
}
// Guest will receive the new round data via poll
} else {
dealNewRound();
board.setChain(state.chain);
updateUI(el);
refreshHand();
if (state.mode === 'bot' && state.currentPlayer !== state.myPlayerIndex) {
scheduleBotTurn(el);
if (state.currentPlayer !== state.myPlayerIndex) {
scheduleBotTurn(el);
}
}
}
......@@ -722,7 +769,7 @@ async function syncNewRoundToServer() {
match_id: state.matchId,
board: state.chain,
boneyard: state.boneyard,
hands: JSON.stringify({ [state.myPlayerIndex]: state.hands[state.myPlayerIndex] }),
hands: JSON.stringify({ '0': state.hands[0], '1': state.hands[1] }),
current_turn: state.currentPlayer,
game_state: JSON.stringify({
move_count: state.moveCount,
......@@ -730,6 +777,7 @@ async function syncNewRoundToServer() {
round: state.roundNumber,
left_end: state.leftEnd,
right_end: state.rightEnd,
dealt: true,
})
});
} catch (e) {}
......
......@@ -7,12 +7,14 @@ import { emoji } from '../../../core/theme.js';
let pollTimer = null;
let matchId = null;
let currentGameKey = 'chess';
let lobbyState = 'waiting'; // waiting | ready | starting
export function mountLobby(el, params = {}) {
matchId = params.matchId;
currentGameKey = params.gameKey || 'chess';
const color = params.color;
const gameKey = params.gameKey || 'chess';
const gameKey = currentGameKey;
const timeControl = params.timeControl || 'rapid_10_0';
const friendProfile = params.friendProfile || {};
const isHost = params.isHost ?? true;
......@@ -187,7 +189,7 @@ function startGame(el, params) {
scene.replace('domino-game', {
mode: 'live',
matchId: params.matchId,
playerIndex: params.isHost ? 0 : 1,
playerIndex: params.playerIndex ?? (params.isHost ? 0 : 1),
isFriendly: true
});
}
......@@ -198,7 +200,7 @@ async function cancelAndLeave(el) {
if (matchId && lobbyState === 'waiting') {
try {
await net.post('friends.php', { action: 'decline-invite', match_id: matchId });
await net.post('friends.php', { action: 'decline-invite', match_id: matchId, game_key: currentGameKey });
} catch (e) {}
}
......
......@@ -265,6 +265,7 @@ function showInviteFromChat(el) {
<div style="display:flex;gap:8px;justify-content:center;margin-bottom:12px;">
<button class="cig active" data-game="chess" style="flex:1;padding:10px;border-radius:10px;background:#2563EB;border:2px solid #2563EB;color:#fff;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;">♟ شطرنج</button>
<button class="cig" data-game="domino" style="flex:1;padding:10px;border-radius:10px;background:#1a1a2e;border:2px solid rgba(255,255,255,0.08);color:#94a3b8;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;">🁣 دومينو</button>
<button class="cig" data-game="ludo" style="flex:1;padding:10px;border-radius:10px;background:#1a1a2e;border:2px solid rgba(255,255,255,0.08);color:#94a3b8;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;">🎲 لودو</button>
</div>
......@@ -290,7 +291,7 @@ function showInviteFromChat(el) {
dialog.querySelectorAll('.cig').forEach(b => { b.style.background = '#1a1a2e'; b.style.borderColor = 'rgba(255,255,255,0.08)'; b.style.color = '#94a3b8'; b.classList.remove('active'); });
btn.style.background = '#2563EB'; btn.style.borderColor = '#2563EB'; btn.style.color = '#fff'; btn.classList.add('active');
selectedGame = btn.dataset.game;
dialog.querySelector('#cig-time').style.display = selectedGame === 'ludo' ? 'none' : 'flex';
dialog.querySelector('#cig-time').style.display = (selectedGame === 'ludo' || selectedGame === 'domino') ? 'none' : 'flex';
});
});
......@@ -321,7 +322,7 @@ function showInviteFromChat(el) {
await net.post('chat.php', {
action: 'send',
friend_id: friendId,
content: `أرسل تحدي ${selectedGame === 'ludo' ? 'لودو' : 'شطرنج'}`,
content: `أرسل تحدي ${selectedGame === 'ludo' ? 'لودو' : selectedGame === 'domino' ? 'دومينو' : 'شطرنج'}`,
message_type: 'invite',
metadata: { game_key: selectedGame, time_control: selectedTc, match_id: res.match_id }
});
......
......@@ -120,18 +120,18 @@ async function checkInvites(el) {
btn.disabled = true;
btn.textContent = '...';
try {
const res = await net.post('friends.php', { action: 'accept-invite', match_id: btn.dataset.acceptInvite });
const inv = invites.find(i => i.match_id === btn.dataset.acceptInvite);
const res = await net.post('friends.php', { action: 'accept-invite', match_id: btn.dataset.acceptInvite, game_key: inv?.game_key || 'chess' });
if (res.error) { btn.textContent = 'خطأ'; return; }
audio.play('reward');
juice.hapticSuccess();
// Navigate to lobby then game
const inv = invites.find(i => i.match_id === btn.dataset.acceptInvite);
scene.push('game-lobby', {
matchId: res.match_id,
color: res.color,
gameKey: inv?.game_key || 'chess',
timeControl: inv?.time_control || 'rapid_10_0',
friendId: inv?.from_id,
playerIndex: res.player_index,
isHost: false
});
} catch (e) {
......@@ -143,7 +143,8 @@ async function checkInvites(el) {
banner.querySelectorAll('[data-decline-invite]').forEach(btn => {
btn.addEventListener('click', async () => {
try {
await net.post('friends.php', { action: 'decline-invite', match_id: btn.dataset.declineInvite });
const inv = invites.find(i => i.match_id === btn.dataset.declineInvite);
await net.post('friends.php', { action: 'decline-invite', match_id: btn.dataset.declineInvite, game_key: inv?.game_key || 'chess' });
btn.closest('div[style*="display:flex"]').remove();
if (banner.children.length === 0) banner.style.display = 'none';
} catch (e) {}
......
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