Files
ttt/public/multiplayer.js
T

164 lines
4.8 KiB
JavaScript

'use strict';
const Multiplayer = (() => {
let ws = null;
let asHost = false;
let gameId = null;
let myMark = null;
let myTurn = false;
let board = Array(9).fill(null);
let gameOver = false;
let scores = { you: 0, opponent: 0, draws: 0 };
function connect(url) {
return new Promise((resolve, reject) => {
ws = new WebSocket(url);
ws.onopen = () => {
if (asHost) {
ws.send(JSON.stringify({ type: 'host', id: gameId }));
} else {
ws.send(JSON.stringify({ type: 'join', token: gameId }));
}
resolve();
};
ws.onerror = (e) => {
App.showToast('Connection failed');
reject(e);
};
ws.onclose = () => {
if (!gameOver) {
App.renderStatus('Connection lost', 'normal');
}
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
handleMessage(msg);
};
});
}
function handleMessage(msg) {
switch (msg.type) {
case 'joined':
myMark = msg.mark;
myTurn = msg.mark === 'X';
App.setScoreboardLabels(
myMark === 'X' ? 'You (X)' : 'Opponent (X)',
myMark === 'O' ? 'You (O)' : 'Opponent (O)'
);
App.renderStatus(myTurn ? 'Your turn' : 'Waiting for opponent…', myTurn ? 'normal' : 'thinking');
break;
case 'opponent-joined':
App.showScreen('game');
App.setScoreboardLabels(
myMark === 'X' ? 'You (X)' : 'Opponent (X)',
myMark === 'O' ? 'You (O)' : 'Opponent (O)'
);
App.renderStatus('Your turn', 'normal');
myTurn = true;
break;
case 'start':
board = Array(9).fill(null);
gameOver = false;
myMark = msg.mark;
myTurn = msg.first === myMark;
App.setScoreboardLabels(
myMark === 'X' ? 'You (X)' : 'Opponent (X)',
myMark === 'O' ? 'You (O)' : 'Opponent (O)'
);
App.clearBoard();
App.renderStatus(myTurn ? 'Your turn' : 'Waiting for opponent…', myTurn ? 'normal' : 'thinking');
break;
case 'move': {
board[msg.index] = msg.mark;
App.renderMark(msg.index, msg.mark);
if (!gameOver && msg.mark !== myMark) {
myTurn = !myTurn;
App.renderStatus(myTurn ? 'Your turn' : 'Opponent is thinking…', myTurn ? 'normal' : 'thinking');
}
break;
}
case 'end':
gameOver = true;
if (msg.winner === 'draw') {
scores.draws++;
App.renderStatus("It's a draw", 'normal');
document.getElementById('board').classList.add('pulse');
} else {
const iWon = msg.winner === myMark;
scores[iWon ? 'you' : 'opponent']++;
App.renderStatus(iWon ? 'You win!' : 'Opponent wins', 'normal');
if (!iWon) document.getElementById('board').classList.add('shake');
}
App.renderScores(scores);
if (msg.line) App.drawWinLine(msg.line);
setTimeout(() => {
const title = msg.winner === 'draw' ? "It's a Draw" : (msg.winner === myMark ? 'You Win!' : 'Opponent Wins');
App.showOverlay(title);
if (msg.winner === myMark) App.burstConfetti();
}, 600);
break;
case 'new-round':
board = Array(9).fill(null);
gameOver = false;
myTurn = msg.first === myMark;
App.hideOverlay();
App.clearBoard();
App.renderStatus(myTurn ? 'Your turn' : 'Waiting for opponent…', myTurn ? 'normal' : 'thinking');
break;
case 'opponent-left':
gameOver = true;
App.renderStatus('Opponent left', 'error');
break;
case 'error':
App.showToast(msg.message || 'Error');
break;
}
}
function handleMove(i) {
if (gameOver || !myTurn || board[i] || myMark === null) return;
board[i] = myMark;
App.renderMark(i, myMark);
ws.send(JSON.stringify({ type: 'move', index: i }));
myTurn = false;
App.renderStatus('Waiting for opponent…', 'thinking');
}
function newRound() {
ws.send(JSON.stringify({ type: 'new-round' }));
}
function destroy() {
if (ws) {
ws.close();
ws = null;
}
myMark = null;
myTurn = false;
board = Array(9).fill(null);
gameOver = false;
scores = { you: 0, opponent: 0, draws: 0 };
}
function init(opts) {
asHost = opts.asHost;
gameId = opts.asHost ? opts.id : opts.token;
const base = location.pathname === '/' ? '' : location.pathname.replace(/\/$/, '');
const wsUrl = (location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host + base + '/ws';
connect(wsUrl).catch((err) => {
console.error('Connection failed:', err);
App.renderStatus('Connection failed', 'error');
});
}
return { init, handleMove, newRound, destroy };
})();