'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 }; })();