164 lines
4.8 KiB
JavaScript
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 };
|
|
})();
|