Multiplayer mode: server, room tokens, docker setup, and client UI
This commit is contained in:
1 parent
176d7cc300
commit
434c81d0b6
19 files changed
+2563
-382
No files matched your search
+131
@@ -0,0 +1,131 @@
|
||||
'use strict';
|
||||
|
||||
const CpuGame = (() => {
|
||||
const SCORES_KEY = 'ttt-scores';
|
||||
const CPU_DELAY = 600;
|
||||
|
||||
const state = {
|
||||
board: Array(9).fill(null),
|
||||
turn: 'X',
|
||||
gameOver: false,
|
||||
scores: { you: 0, opponent: 0, draws: 0 },
|
||||
};
|
||||
|
||||
let cpuTimer = 0;
|
||||
|
||||
function persistScores() {
|
||||
try {
|
||||
localStorage.setItem(SCORES_KEY, JSON.stringify(state.scores));
|
||||
} catch (e) { /* storage unavailable */ }
|
||||
}
|
||||
|
||||
function loadScores() {
|
||||
try {
|
||||
const raw = localStorage.getItem(SCORES_KEY);
|
||||
if (raw) {
|
||||
const s = JSON.parse(raw);
|
||||
state.scores = {
|
||||
you: Number(s.you) || 0,
|
||||
opponent: Number(s.cpu || s.opponent) || 0,
|
||||
draws: Number(s.draws) || 0,
|
||||
};
|
||||
}
|
||||
} catch (e) { /* corrupted storage */ }
|
||||
}
|
||||
|
||||
function handleMove(i) {
|
||||
if (state.gameOver || state.turn !== 'X' || state.board[i]) return;
|
||||
state.board[i] = 'X';
|
||||
App.renderMark(i, 'X');
|
||||
const result = App.checkWinner(state.board);
|
||||
if (result) {
|
||||
endRound(result);
|
||||
return;
|
||||
}
|
||||
state.turn = 'O';
|
||||
App.renderStatus('CPU is thinking…', 'thinking');
|
||||
cpuTimer = setTimeout(cpuMove, CPU_DELAY);
|
||||
}
|
||||
|
||||
function cpuMove() {
|
||||
if (state.gameOver) return;
|
||||
const empty = [];
|
||||
state.board.forEach((v, i) => { if (v === null) empty.push(i); });
|
||||
if (!empty.length) return;
|
||||
const i = empty[Math.floor(Math.random() * empty.length)];
|
||||
state.board[i] = 'O';
|
||||
App.renderMark(i, 'O');
|
||||
const result = App.checkWinner(state.board);
|
||||
if (result) {
|
||||
endRound(result);
|
||||
return;
|
||||
}
|
||||
state.turn = 'X';
|
||||
App.renderStatus('Your turn', 'normal');
|
||||
}
|
||||
|
||||
function endRound(result) {
|
||||
state.gameOver = true;
|
||||
let title;
|
||||
let key;
|
||||
if (result.winner === 'X') {
|
||||
key = 'you';
|
||||
title = 'You Win!';
|
||||
App.renderStatus('You win!', 'normal');
|
||||
} else if (result.winner === 'O') {
|
||||
key = 'opponent';
|
||||
title = 'CPU Wins';
|
||||
App.renderStatus('CPU wins', 'normal');
|
||||
document.getElementById('board').classList.add('shake');
|
||||
} else {
|
||||
key = 'draws';
|
||||
title = "It's a Draw";
|
||||
App.renderStatus("It's a draw", 'normal');
|
||||
document.getElementById('board').classList.add('pulse');
|
||||
}
|
||||
|
||||
state.scores[key]++;
|
||||
persistScores();
|
||||
App.renderScores(state.scores);
|
||||
|
||||
if (result.line) App.drawWinLine(result.line);
|
||||
|
||||
setTimeout(() => {
|
||||
App.showOverlay(title);
|
||||
if (result.winner === 'X') App.burstConfetti();
|
||||
}, result.winner === 'draw' ? 400 : 600);
|
||||
}
|
||||
|
||||
function newRound() {
|
||||
clearTimeout(cpuTimer);
|
||||
state.board = Array(9).fill(null);
|
||||
state.turn = 'X';
|
||||
state.gameOver = false;
|
||||
App.clearBoard();
|
||||
App.renderStatus('Your turn', 'normal');
|
||||
}
|
||||
|
||||
function resetScores() {
|
||||
state.scores = { you: 0, opponent: 0, draws: 0 };
|
||||
persistScores();
|
||||
App.renderScores(state.scores);
|
||||
App.showToast('Scores reset');
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
clearTimeout(cpuTimer);
|
||||
}
|
||||
|
||||
function init() {
|
||||
clearTimeout(cpuTimer);
|
||||
App.clearBoard();
|
||||
state.board = Array(9).fill(null);
|
||||
state.turn = 'X';
|
||||
state.gameOver = false;
|
||||
loadScores();
|
||||
App.renderScores(state.scores);
|
||||
App.renderStatus('Your turn', 'normal');
|
||||
}
|
||||
|
||||
return { init, handleMove, newRound, resetScores, destroy };
|
||||
})();
|
||||
Reference in new issue
Block a user