Commit 0f9efb00 authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat(chess): live polling, match history scene, coin economy on game end

- GET match endpoint for live polling during multiplayer
- Match history scene (placeholder)
- Live sync module with poll/sendMove/drawOffer/resign/rematch
- Economy: coins actually granted to player profile on game completion
- Economy transactions recorded with reason
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 4b119421
......@@ -30,10 +30,22 @@ switch ($action) {
case 'complete':
handleComplete($db, $userId, $input);
break;
case 'get':
handleGet($db, $userId, $input);
break;
default:
jsonError('Invalid action');
}
function handleGet($db, string $userId, array $input): void {
$matchId = $_GET['match_id'] ?? ($input['match_id'] ?? '');
if (!$matchId) jsonError('match_id required');
$matches = $db->get('matches', ['id' => 'eq.' . $matchId, 'limit' => 1]);
$match = is_array($matches) && !empty($matches) && !isset($matches['error']) ? $matches[0] : null;
if (!$match) jsonError('Match not found', 404);
jsonResponse($match);
}
function handleStart($db, string $userId, array $input): void {
$gameKey = $input['game_key'] ?? 'chess';
$mode = $input['mode'] ?? 'bot';
......@@ -116,7 +128,7 @@ function handleComplete($db, string $userId, array $input): void {
// Calculate Elo rating change
$ratingCol = getRatingColumn($timeControl);
$profiles = $db->get('profiles', ['id' => 'eq.' . $userId, 'select' => $ratingCol . ',games_played,total_wins,total_draws,total_losses', 'limit' => 1]);
$profiles = $db->get('profiles', ['id' => 'eq.' . $userId, 'select' => $ratingCol . ',games_played,total_wins,total_draws,total_losses,coins,xp,level', 'limit' => 1]);
$profile = is_array($profiles) && !empty($profiles) ? $profiles[0] : null;
if ($profile) {
......@@ -145,7 +157,20 @@ function handleComplete($db, string $userId, array $input): void {
'result' => $result
]);
jsonResponse(['success' => true, 'result' => $result, 'rating_before' => $playerRating, 'rating_after' => $newRating, 'rating_change' => $ratingChange]);
// Grant coins
$coins = ($result === 'win') ? 50 : (($result === 'draw') ? 20 : 10);
$currentCoins = $profile['coins'] ?? 0;
$db->update('profiles', ['coins' => $currentCoins + $coins], ['id' => 'eq.' . $userId]);
$sdb->insert('economy_transactions', [
'player_id' => $userId,
'type' => 'game_reward',
'currency' => 'coins',
'amount' => $coins,
'balance_after' => $currentCoins + $coins,
'reason' => 'Chess ' . $result
]);
jsonResponse(['success' => true, 'result' => $result, 'rating_before' => $playerRating, 'rating_after' => $newRating, 'rating_change' => $ratingChange, 'coins_earned' => $coins]);
}
jsonResponse(['success' => true, 'result' => $result]);
......
import * as net from '../../../core/net.js';
import * as store from '../../../core/store.js';
let pollInterval = null;
let onMoveCallback = null;
let onDrawOfferCallback = null;
let lastMoveCount = 0;
export function startSync(matchId, callbacks) {
onMoveCallback = callbacks.onMove;
onDrawOfferCallback = callbacks.onDrawOffer;
lastMoveCount = 0;
pollInterval = setInterval(() => pollMatch(matchId), 2000);
}
export function stopSync() {
if (pollInterval) {
clearInterval(pollInterval);
pollInterval = null;
}
}
async function pollMatch(matchId) {
try {
const data = await net.get('game.php', { action: 'get', match_id: matchId });
if (!data || data.error) return;
if (data.move_count > lastMoveCount && data.current_fen) {
lastMoveCount = data.move_count;
if (onMoveCallback) onMoveCallback(data);
}
if (data.game_state?.draw_offer && onDrawOfferCallback) {
const offer = data.game_state.draw_offer;
if (offer.from !== store.get('auth.userId')) {
onDrawOfferCallback(offer);
}
}
} catch (e) {}
}
export async function sendMove(matchId, fen, move, moveCount) {
return net.post('game.php', {
action: 'move',
match_id: matchId,
fen,
move: JSON.stringify(move),
move_count: moveCount
});
}
export async function sendDrawOffer(matchId) {
return net.post('game.php', {
action: 'draw_offer',
match_id: matchId
});
}
export async function acceptDraw(matchId) {
return net.post('game.php', {
action: 'complete',
match_id: matchId,
result: 'draw'
});
}
export async function sendResign(matchId) {
return net.post('game.php', {
action: 'resign',
match_id: matchId
});
}
export async function requestRematch(matchId) {
return net.post('game.php', {
action: 'start',
game_key: 'chess',
mode: 'rematch',
rematch_of: matchId
});
}
......@@ -2,7 +2,9 @@ import * as scene from '../../core/scene.js';
import { mountGame } from './scenes/game.js';
import { mountResult } from './scenes/result.js';
import { mountAnalysis } from './scenes/analysis.js';
import { mountHistory } from './scenes/history.js';
scene.register('chess-game', mountGame);
scene.register('chess-result', mountResult);
scene.register('chess-analysis', mountAnalysis);
scene.register('chess-history', mountHistory);
import * as scene from '../../../core/scene.js';
import * as net from '../../../core/net.js';
import * as audio from '../../../core/audio.js';
import * as store from '../../../core/store.js';
import { t } from '../../../core/i18n.js';
export async function mountHistory(el) {
el.innerHTML = `
<div style="padding:16px;display:flex;flex-direction:column;gap:12px;">
<div style="display:flex;align-items:center;gap:12px;">
<button class="btn btn-secondary" id="back-btn" style="width:36px;height:36px;padding:0;">←</button>
<h2 style="font-size:18px;font-weight:700;">${t('profile.history')}</h2>
</div>
<div id="history-list">
<div class="skeleton" style="height:60px;margin-bottom:8px;"></div>
<div class="skeleton" style="height:60px;margin-bottom:8px;"></div>
<div class="skeleton" style="height:60px;"></div>
</div>
</div>
`;
el.querySelector('#back-btn').addEventListener('click', () => { audio.play('click'); scene.pop(); });
try {
const token = store.get('auth.token');
const res = await fetch('/api/leaderboard.php?game_key=chess&limit=1', {
headers: { 'Authorization': 'Bearer ' + token }
});
// For now show a placeholder since we don't have a match history endpoint yet
const list = el.querySelector('#history-list');
list.innerHTML = `<p style="color:#94a3b8;text-align:center;padding:32px;">لا توجد مباريات سابقة</p>`;
} catch (e) {
el.querySelector('#history-list').innerHTML = `<p style="color:#ef4444;text-align:center;">${t('common.error')}</p>`;
}
}
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