feat: complete human-vs-ESP32 gameplay and cumulative statistics

This commit is contained in:
2026-08-29 23:33:27 +03:00
parent 650ade2726
commit c8e0c9168b
14 changed files with 269 additions and 19 deletions
+22 -6
View File
@@ -16,6 +16,8 @@
let info;
let state;
let selectedTarget;
let cumulativeStatistics;
let statisticsGameId;
let activeBoard = 'own';
let socket;
let retryTimer;
@@ -106,7 +108,7 @@
function ownIndex() { return role === 'player2' ? 1 : 0; }
function opponentIndex() { return ownIndex() ^ 1; }
function canShoot() { return state?.phase === 'in_progress' && state.turn === role && isPlayer(); }
function playerLabel(index) { return `Игрок ${index + 1}`; }
function playerLabel(index) { return index === 1 && state?.mode === 'bot' ? 'ESP32' : `Игрок ${index + 1}`; }
function cellInfo(value) {
if (value === '1') return ['ship', 'Корабль'];
@@ -157,7 +159,7 @@
];
return [
{ index: ownIndex(), title: 'Моё поле', key: 'own', targetable: false },
{ index: opponentIndex(), title: 'Поле соперника', key: 'opponent', targetable: canShoot() }
{ index: opponentIndex(), title: state.mode === 'bot' ? 'Поле ESP32' : 'Поле соперника', key: 'opponent', targetable: canShoot() }
];
}
@@ -198,7 +200,7 @@
function renderGame() {
if (!state) return;
showScreen('game');
ui.turn.textContent = state.turn === role ? 'Ваш ход. Выберите клетку соперника.' : `Ходит ${state.turn === 'player1' ? 'игрок 1' : 'игрок 2'}.`;
ui.turn.textContent = state.turn === role ? 'Ваш ход. Выберите клетку соперника.' : `Ходит ${playerLabel(state.turn === 'player2' ? 1 : 0)}.`;
ui.wins.textContent = `Победы: ${state.wins[0]} : ${state.wins[1]}`;
renderBoards(ui.boards);
const available = canShoot();
@@ -211,10 +213,12 @@
function renderResult() {
showScreen('result');
if (statisticsGameId !== state.gameId) refreshStatistics();
const waiting = state.phase === 'rematch_wait';
const winner = state.winner === 0 || state.winner === 1 ? `Победил игрок ${state.winner + 1}. ` : '';
const statistics = state.statistics.map((entry, index) => `Игрок ${index + 1}: выстрелы ${entry[0]}, попадания ${entry[1]}, промахи ${entry[2]}, потоплено ${entry[3]}.`).join(' ');
ui.result.textContent = `${winner}${statistics} ${waiting ? 'Ожидается подтверждение повторной игры.' : 'Поля раскрыты. Можно подтвердить повторную игру.'}`;
const winner = state.winner === 0 || state.winner === 1 ? `Победил ${playerLabel(state.winner)}. ` : '';
const matchSummary = state.statistics.map((entry, index) => `${playerLabel(index)}: выстрелы ${entry[0]}, попадания ${entry[1]}, промахи ${entry[2]}, потоплено ${entry[3]}.`).join(' ');
const cumulative = cumulativeStatistics?.cumulative?.map((entry, index) => `${playerLabel(index)} всего: игр ${entry[0]}, побед ${entry[1]}, поражений ${entry[2]}, выстрелов ${entry[4]}, попаданий ${entry[5]}, промахов ${entry[6]}, потоплено ${entry[3]}.`).join(' ') || '';
ui.result.textContent = `${winner}${matchSummary} ${cumulative} ${waiting ? 'Ожидается подтверждение повторной игры.' : 'Поля раскрыты. Можно подтвердить повторную игру.'}`;
ui.rematch.hidden = !isPlayer();
ui.rematch.disabled = !isPlayer();
ui.rematch.textContent = waiting ? 'Подтвердить повторно' : 'Сыграть ещё';
@@ -242,6 +246,18 @@
} catch (error) { notify(error.message, true); throw error; }
}
async function refreshStatistics() {
const requestedGameId = state?.gameId;
try {
const response = await request('/api/statistics', { cache: 'no-store', headers: token ? { 'X-Session-Token': token } : {} });
if (state?.gameId === requestedGameId && response.gameId === requestedGameId && Array.isArray(response.cumulative)) {
cumulativeStatistics = response;
statisticsGameId = requestedGameId;
renderResult();
}
} catch (_) { /* The state snapshot still renders the current-match statistics. */ }
}
async function join(requestedRole) {
const name = ui.name.value.trim();
if (!name) { notify('Введите имя.', true); ui.name.focus(); return; }