// Helper per le risposte JSON standard const json = (data, status = 200) => new Response(JSON.stringify(data), { status, headers: { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' }, }); // 1. ESPORTAZIONE DELLA WORKER PRINCIPALE export default { async fetch(request, env) { const url = new URL(request.url); // Endpoint di controllo integrità if (url.pathname === '/api/health') return json({ ok: true, app: 'cirulla' }); // Gestione delle connessioni WebSocket per il gioco live if (url.pathname === '/ws') { if (request.headers.get('Upgrade') !== 'websocket') { return new Response('Attesa connessione WebSocket', { status: 426 }); } // Utilizziamo un Durable Object singolo ad ID fisso chiamato "LOBBY" per sincronizzare tutti i giocatori const id = env.GAME_LOBBY.idFromName('LOBBY'); const lobbyObject = env.GAME_LOBBY.get(id); return lobbyObject.fetch(request); } // Se non è una chiamata API o WS, serve i file statici dalla cartella public if (url.pathname.startsWith('/api/')) return json({ error: 'Non trovato' }, 404); return env.ASSETS.fetch(request); }, }; // 2. IMPLEMENTAZIONE DEL DURABLE OBJECT PER IL MULTIPLAYER LIVE export class GameLobby { constructor(state, env) { this.state = state; // Mappa dei giocatori connessi: webSocket -> { username, status: 'available' | 'playing' } this.players = new Map(); // Mappa delle partite attive: matchId -> { p1WS, p2WS, gameState } this.matches = new Map(); } async fetch(request) { const pair = new WebSocketPair(); const [client, server] = Object.values(pair); await this.handleSession(server); return new Response(null, { status: 101, webSocket: client }); } async handleSession(ws) { ws.accept(); // Inizializza lo stato del giocatore appena collegato this.players.set(ws, { username: 'Ospite', status: 'available', matchId: null }); ws.addEventListener('message', async (msg) => { try { const packet = JSON.parse(msg.data); const player = this.players.get(ws); switch (packet.event) { case 'register_player': if (player) { player.username = packet.data || 'Giocatore'; this.broadcastPlayerList(); } break; case 'request_player_list': this.sendPlayerListTo(ws); break; case 'challenge_player': this.handleChallenge(ws, packet.data.targetSocketId, packet.data.targetScore); break; case 'cancel_challenge': this.handleCancelChallenge(ws); break; case 'accept_challenge': this.handleAcceptChallenge(ws, packet.data.challengerId); break; case 'decline_challenge': this.handleDeclineChallenge(ws, packet.data.challengerId); break; case 'player_action_play_card': case 'player_action_bussa': this.handleGameAction(ws, packet); break; } } catch (err) { console.error('Errore elaborazione pacchetto:', err); } }); ws.addEventListener('close', () => { this.handleDisconnect(ws); }); ws.addEventListener('error', () => { this.handleDisconnect(ws); }); } broadcastPlayerList() { const list = this.getPlayerListData(); for (const [clientWS] of this.players.entries()) { try { clientWS.send(JSON.stringify({ event: 'update_player_list', data: list })); } catch (e) { console.error(e); } } } sendPlayerListTo(ws) { const list = this.getPlayerListData(); try { ws.send(JSON.stringify({ event: 'update_player_list', data: list })); } catch (e) { console.error(e); } } getPlayerListData() { const list = []; for (const [clientWS, info] of this.players.entries()) { list.push({ socketId: info.username + '_' + Math.random().toString(36).substring(2, 5), username: info.username, status: info.status, _rawWS: clientWS }); } return list; } handleChallenge(ws, targetFakeId, targetScore) { const challenger = this.players.get(ws); const allPlayers = this.getPlayerListData(); const targetNode = allPlayers.find(p => p.socketId === targetFakeId); if (targetNode && targetNode._rawWS !== ws) { const targetWS = targetNode._rawWS; const targetPlayer = this.players.get(targetWS); if (targetPlayer && targetPlayer.status === 'available') { const challengerFakeId = allPlayers.find(p => p._rawWS === ws)?.socketId; try { targetWS.send(JSON.stringify({ event: 'challenged_by', data: { challengerId: challengerFakeId, challengerName: challenger.username, targetScore: targetScore } })); } catch (e) { console.error(e); } } } } handleCancelChallenge(ws) { const allPlayers = this.getPlayerListData(); const challengerFakeId = allPlayers.find(p => p._rawWS === ws)?.socketId; for (const [clientWS] of this.players.entries()) { try { clientWS.send(JSON.stringify({ event: 'challenge_cancelled', data: { challengerId: challengerFakeId } })); } catch (e) { console.error(e); } } } handleDeclineChallenge(ws, challengerFakeId) { const allPlayers = this.getPlayerListData(); const challengerNode = allPlayers.find(p => p.socketId === challengerFakeId); if (challengerNode) { try { challengerNode._rawWS.send(JSON.stringify({ event: 'challenge_declined' })); } catch (e) { console.error(e); } } } handleAcceptChallenge(ws, challengerFakeId) { const targetPlayer = this.players.get(ws); const allPlayers = this.getPlayerListData(); const challengerNode = allPlayers.find(p => p.socketId === challengerFakeId); if (!challengerNode || !targetPlayer) return; const challengerWS = challengerNode._rawWS; const challengerPlayer = this.players.get(challengerWS); if (challengerPlayer && challengerPlayer.status === 'available' && targetPlayer.status === 'available') { const matchId = 'match_' + Date.now(); challengerPlayer.status = 'playing'; challengerPlayer.matchId = matchId; targetPlayer.status = 'playing'; targetPlayer.matchId = matchId; const initialMatchState = this.generateInitialGameState(challengerPlayer.username, targetPlayer.username); this.matches.set(matchId, { p1WS: challengerWS, p2WS: ws, gameState: initialMatchState }); try { challengerWS.send(JSON.stringify({ event: 'start_online_match', data: { matchId, yourIndex: 0, targetScore: 51, p1Name: challengerPlayer.username, p2Name: targetPlayer.username, initialState: initialMatchState } })); ws.send(JSON.stringify({ event: 'start_online_match', data: { matchId, yourIndex: 1, targetScore: 51, p1Name: challengerPlayer.username, p2Name: targetPlayer.username, initialState: initialMatchState } })); } catch (e) { console.error(e); } this.broadcastPlayerList(); } } generateInitialGameState(p1Name, p2Name) { const suits = ['denari', 'coppe', 'spade', 'bastoni']; let deck = []; let id = 1; for (let s of suits) { for (let v = 1; v <= 10; v++) { deck.push({ id: id++, suit: s, value: v }); } } for (let i = deck.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [deck[i], deck[j]] = [deck[j], deck[i]]; } let p1Hand = [deck.pop(), deck.pop(), deck.pop()]; let p2Hand = [deck.pop(), deck.pop(), deck.pop()]; let table = [deck.pop(), deck.pop(), deck.pop(), deck.pop()]; return { deck, table, p1Hand, p2Hand, p1Captured: [], p2Captured: [], p1Scope: 0, p2Scope: 0, p1Bussato: false, p2Bussato: false, dealerIdx: 0, turnIdx: 1, lastCaptureIdx: null }; } handleGameAction(ws, packet) { const player = this.players.get(ws); if (!player || !player.matchId) return; const match = this.matches.get(player.matchId); if (!match) return; const targetWS = (ws === match.p1WS) ? match.p2WS : match.p1WS; try { targetWS.send(JSON.stringify({ event: 'sync_game_state', data: packet.data })); } catch (e) { console.error(e); } } handleDisconnect(ws) { const player = this.players.get(ws); if (player) { if (player.matchId) { const match = this.matches.get(player.matchId); if (match) { const opponentWS = (ws === match.p1WS) ? match.p2WS : match.p1WS; const opponent = this.players.get(opponentWS); if (opponent) { opponent.status = 'available'; opponent.matchId = null; try { opponentWS.send(JSON.stringify({ event: 'challenge_declined' })); } catch (e) { console.error(e); } } this.matches.delete(player.matchId); } } this.players.delete(ws); this.broadcastPlayerList(); } } }