From ceba5178f3cf50be4fad141daa0a025aa1539c60 Mon Sep 17 00:00:00 2001 From: AfonsoCMSousa Date: Sat, 3 Jan 2026 13:14:59 +0000 Subject: [PATCH] SyntaxSquad/DADProject#16 fix: i forgot this --- api/app/Http/Controllers/MatchController.php | 51 +++- api/app/Models/CoinTransactionType.php | 16 +- api/routes/api.php | 1 + websockets/classes/BiscaGame.js | 7 +- websockets/classes/BiscaMatch.js | 270 ++++++++----------- websockets/events/match.js | 250 +++++++++-------- 6 files changed, 316 insertions(+), 279 deletions(-) diff --git a/api/app/Http/Controllers/MatchController.php b/api/app/Http/Controllers/MatchController.php index bcedc9a..220e291 100644 --- a/api/app/Http/Controllers/MatchController.php +++ b/api/app/Http/Controllers/MatchController.php @@ -255,7 +255,6 @@ class MatchController extends Controller public function open(Request $request) { - // FIX: Allow matches where P2 is NULL OR 'Ghost' (ID 1) $query = MatchGame::where('status', 'Pending') ->where(function($q) { $q->whereNull('player2_user_id') @@ -272,4 +271,54 @@ class MatchController extends Controller return response()->json($matches); } + + // --- FIX: Safely Delete Match by removing dependencies first --- + public function destroy(MatchGame $match) + { + $user = request()->user(); + + if ($match->player1_user_id !== $user->id) { + return response()->json(['message' => 'Unauthorized'], 403); + } + + if ($match->status !== 'Pending') { + return response()->json(['message' => 'Cannot cancel a match that has already started'], 400); + } + + return DB::transaction(function () use ($match, $user) { + // 1. Delete associated Games first (The FK constraint cause) + DB::table('games')->where('match_id', $match->id)->delete(); + + // 2. Unlink existing Coin Transactions (Set match_id to NULL) + CoinTransaction::where('match_id', $match->id)->update(['match_id' => null]); + + // 3. Process Refund + if ($match->stake > 0) { + $user->increment('coins_balance', $match->stake); + + $refundType = CoinTransactionType::firstOrCreate( + ['name' => 'Refund'], + ['type' => 'C'] + ); + + // Create Refund Transaction with NO LINK to the deleted match + CoinTransaction::create([ + 'user_id' => $user->id, + 'match_id' => null, // IMPORTANT: Must be null + 'coin_transaction_type_id' => $refundType->id, + 'transaction_datetime' => now(), + 'coins' => $match->stake, + 'custom' => "Refund for Match #{$match->id}" + ]); + } + + // 4. Finally delete the match + $match->delete(); + + return response()->json([ + 'message' => 'Match canceled and refunded', + 'balance' => $user->coins_balance + ]); + }); + } } diff --git a/api/app/Models/CoinTransactionType.php b/api/app/Models/CoinTransactionType.php index 6caca4b..cb42d53 100644 --- a/api/app/Models/CoinTransactionType.php +++ b/api/app/Models/CoinTransactionType.php @@ -4,17 +4,17 @@ namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Eloquent\SoftDeletes; class CoinTransactionType extends Model { - use HasFactory, SoftDeletes; + use HasFactory; - protected $table = 'coin_transaction_types'; + // FIX: Disable auto-timestamps because the table doesn't have updated_at/created_at columns + public $timestamps = false; - protected $fillable = ['name', 'type', 'custom']; - - protected $casts = [ - 'custom' => 'array', + protected $fillable = [ + 'name', + 'type', + 'description' // Include description if your table has it ]; -} \ No newline at end of file +} diff --git a/api/routes/api.php b/api/routes/api.php index 7698599..0a57f82 100644 --- a/api/routes/api.php +++ b/api/routes/api.php @@ -62,6 +62,7 @@ Route::prefix('v1')->group(function () { Route::get('open', [MatchController::class, 'open']); Route::post('/{match}/join', [MatchController::class, 'join']); Route::post('/{match}/start', [MatchController::class, 'start']); // Changed to /start to be explicit + Route::delete('/{match}', [MatchController::class, 'destroy']); Route::apiResource('/', MatchController::class)->parameters(['' => 'match']); }); diff --git a/websockets/classes/BiscaGame.js b/websockets/classes/BiscaGame.js index 6542894..73d4e90 100644 --- a/websockets/classes/BiscaGame.js +++ b/websockets/classes/BiscaGame.js @@ -189,7 +189,9 @@ class BiscaGame { this.players[winnerId].tricks += 1; this.currentTurn = winnerId; - if (this.type == 3) { + // --- CRITICAL FIX: Always draw if cards remain --- + // Removed "if (this.type == 3)" check + if (this.deck.length > 0 || this.trumpCard) { this.drawCard(winnerId); this.drawCard(this.getOpponentId(winnerId)); } @@ -221,7 +223,7 @@ class BiscaGame { ? p2Obj.id : p1Obj.id; - return { + const result = { action: "game_ended", trickResult, winnerId: finalWinner, @@ -233,6 +235,7 @@ class BiscaGame { player2_points: p2Obj.points, totalTime: totalTime, }; + if (this.onGameEnd) { this.onGameEnd(result); } diff --git a/websockets/classes/BiscaMatch.js b/websockets/classes/BiscaMatch.js index 891c8dd..cb7aebe 100644 --- a/websockets/classes/BiscaMatch.js +++ b/websockets/classes/BiscaMatch.js @@ -4,9 +4,11 @@ import axios from "axios"; const API_URL = "http://localhost:8000/api/v1"; class BiscaMatch { - constructor(apiData, emitStateCallback, token) { + // FIX: Added autoPlayCallback parameter + constructor(apiData, emitStateCallback, token, autoPlayCallback) { this.id = apiData.id; this.emitStateCallback = emitStateCallback; + this.autoPlayCallback = autoPlayCallback; this.token = token; this.stake = apiData.stake; this.type = apiData.type; @@ -14,6 +16,8 @@ class BiscaMatch { this.player1Id = apiData.player1_user_id; this.player2Id = apiData.player2_user_id; + this.player1 = apiData.player1; + this.player2 = apiData.player2; this.marks = { [this.player1Id]: apiData.player1_marks || 0, @@ -33,11 +37,12 @@ class BiscaMatch { } } - async joinPlayer(userId) { + async joinPlayer(userId, user) { if (this.player2Id !== this.GHOST_ID) return false; if (userId === this.player1Id) return false; this.player2Id = userId; + this.player2 = user; this.marks[userId] = 0; this.points[userId] = 0; this.status = "Playing"; @@ -48,226 +53,185 @@ class BiscaMatch { getAuthHeaders() { return { - headers: { Authorization: this.token }, + headers: { + Authorization: this.token, + "Content-Type": "application/json", + "Accept": "application/json" + }, }; } async startNewGame() { - console.log(`[Match ${this.id}] A iniciar nova mão (Game DB)...`); - + console.log(`[Match ${this.id}] Starting new round...`); let newGameDbId = null; - try { - let payloadP1 = this.player1Id; - let payloadP2 = this.player2Id; - - if (this.player2Id !== this.GHOST_ID) { - console.log( - "🔄 Multiplayer detetado: Trocando ordem para satisfazer a API..." - ); - payloadP1 = this.player2Id; - payloadP2 = this.player1Id; - } - const payload = { type: this.type, - player1_user_id: payloadP1, - player2_user_id: payloadP2, + player1_user_id: this.player1Id, + player2_user_id: this.player2Id, match_id: this.id, status: "Playing", began_at: new Date().toISOString(), }; - - console.log("📤 [Payload Adaptado]:", JSON.stringify(payload, null, 2)); - - const res = await axios.post( - `${API_URL}/games`, - payload, - this.getAuthHeaders() - ); - + + const res = await axios.post(`${API_URL}/games`, payload, this.getAuthHeaders()); const gameData = res.data.game || res.data.data || res.data; newGameDbId = gameData.id; - console.log(`[Match ${this.id}] Game DB criado: #${newGameDbId}`); - - const apiP1 = gameData.player1_user_id; - const apiP2 = gameData.player2_user_id; - - const p1 = { id: apiP1, name: `User ${apiP1}` }; - const p2 = { id: apiP2, name: `User ${apiP2}` }; + const p1Name = this.player1?.nickname || `User ${this.player1Id}`; + const p2Name = this.player2?.nickname || `User ${this.player2Id}`; this.currentGame = new BiscaGame( this.id, this.type, - p1, - p2, + { id: this.player1Id, name: p1Name }, + { id: this.player2Id, name: p2Name }, newGameDbId, - (result) => { - this.handleGameEnd(result); - } + (result) => { this.handleGameEnd(result); } ); this.currentGame.init(); + + // FIX: Start Timer & Connect Callback + this.currentGame.startTurnTimer((uid, cid) => { + if (this.autoPlayCallback) this.autoPlayCallback(uid, cid); + }); if (this.emitStateCallback) { - this.emitStateCallback("match:new-round", this.getState()); + this.emitStateCallback("match:new-round-start", null); } } catch (e) { - if (e.response) { - console.error( - `❌ Erro API (${e.response.status}):`, - JSON.stringify(e.response.data) - ); - } else { - console.error(`❌ Erro Código: ${e.message}`); - } + console.error(`❌ Start Game Error: ${e.message}`); } } - async handleGameEnd(result) { - console.log( - `[Match ${this.id}] Mão terminada. Vencedor: ${result.winnerId}` - ); + // --- FIX: Close DB Game row on surrender --- + async abortCurrentGame(loserId) { + if (this.currentGame && this.currentGame.dbId) { + console.log(`[Match ${this.id}] Aborting Game #${this.currentGame.dbId}`); + try { + // Determine winner for the DB record based on who DIDN'T lose + const winnerId = loserId == this.player1Id ? this.player2Id : this.player1Id; + + const payload = { + status: "Ended", + winner_user_id: winnerId, + loser_user_id: loserId, + is_draw: false, + ended_at: new Date().toISOString(), + }; + await axios.put(`${API_URL}/games/${this.currentGame.dbId}`, payload, this.getAuthHeaders()); + } catch(e) { console.error("Error aborting game:", e.message); } + } + } - this.points[result.player1_id] += result.player1_points; - this.points[result.player2_id] += result.player2_points; + async handleGameEnd(result) { + console.log(`[Match ${this.id}] Round ended. Winner: ${result.winnerId}`); + + if (this.points[result.player1_id] !== undefined) this.points[result.player1_id] += result.player1_points; + if (this.points[result.player2_id] !== undefined) this.points[result.player2_id] += result.player2_points; if (this.currentGame && this.currentGame.dbId) { - try { - const payload = { - status: "Ended", - winner_user_id: result.winnerId, - loser_user_id: result.loserId, - is_draw: result.isDraw, - player1_points: result.player1_points, - player2_points: result.player2_points, - ended_at: new Date().toISOString(), - }; - - console.log( - `📤 A enviar update para Game #${this.currentGame.dbId}...` - ); - - // --- CORREÇÃO IMPORTANTE: Forçar o header aqui --- - const config = { - headers: { - Authorization: this.token, - "Content-Type": "application/json", - Accept: "application/json", - }, - }; - - await axios.put( - `${API_URL}/games/${this.currentGame.dbId}`, - payload, - config - ); - - console.log( - `✅ Game #${this.currentGame.dbId} atualizado com sucesso.` - ); - } catch (e) { - // --- LOG DETALHADO PARA VER O MOTIVO DO 403 --- - if (e.response) { - console.error(`❌ ERRO CRÍTICO API (${e.response.status}):`); - // Isto vai imprimir o JSON exato que o Laravel devolve - console.error(JSON.stringify(e.response.data, null, 2)); - } else { - console.error("Erro Axios:", e.message); - } - } - } else { - console.warn("⚠️ Ignorado: Não há ID de jogo na DB para atualizar."); + try { + const payload = { + status: "Ended", + winner_user_id: result.winnerId, + loser_user_id: result.loserId, + is_draw: result.isDraw, + player1_points: result.player1_points, + player2_points: result.player2_points, + ended_at: new Date().toISOString(), + }; + await axios.put(`${API_URL}/games/${this.currentGame.dbId}`, payload, this.getAuthHeaders()); + } catch (e) { console.error("DB Game Update Error:", e.message); } } - // ... (resto da lógica de atribuir marcas e finishMatch mantém-se igual) let marksToAdd = 0; let roundWinnerId = result.winnerId; if (roundWinnerId) { - const winningScore = - roundWinnerId == result.player1_id - ? result.player1_points - : result.player2_points; + const winningScore = roundWinnerId == result.player1_id ? result.player1_points : result.player2_points; if (winningScore === 120) marksToAdd = 4; else if (winningScore > 90) marksToAdd = 2; else if (winningScore >= 60) marksToAdd = 1; - this.marks[roundWinnerId] += marksToAdd; + + if (this.marks[roundWinnerId] !== undefined) this.marks[roundWinnerId] += marksToAdd; + } + + if (this.emitStateCallback) { + this.emitStateCallback("match:round-ended", { + winnerId: roundWinnerId, + marksAdded: marksToAdd, + p1Points: result.player1_points, + p2Points: result.player2_points, + p1Marks: this.marks[this.player1Id], + p2Marks: this.marks[this.player2Id], + reason: result.reason || 'points' + }); } if (this.marks[this.player1Id] >= 4 || this.marks[this.player2Id] >= 4) { await this.finishMatch(); - if (this.emitStateCallback) { - this.emitStateCallback("match:ended", this.getState()); - } + if (this.emitStateCallback) this.emitStateCallback("match:ended-signal", null); } else { - console.log(`[Match ${this.id}] Ninguém ganhou 4 marcas. A baralhar...`); - await this.startNewGame(); + setTimeout(() => { + this.startNewGame(); + }, 1000); } } async finishMatch() { this.status = "Ended"; + const winnerId = this.marks[this.player1Id] >= 4 ? this.player1Id : this.player2Id; + const loserId = winnerId == this.player1Id ? this.player2Id : this.player1Id; - const winnerId = - this.marks[this.player1Id] >= 4 ? this.player1Id : this.player2Id; - const loserId = - winnerId == this.player1Id ? this.player2Id : this.player1Id; + const payload = { + status: "Ended", + winner_user_id: winnerId, + loser_user_id: loserId, + player1_marks: this.marks[this.player1Id], + player2_marks: this.marks[this.player2Id], + player1_points: this.points[this.player1Id], + player2_points: this.points[this.player2Id], + }; try { - await axios.put( - `${API_URL}/matches/${this.id}`, - { - status: "Ended", - winner_user_id: winnerId, - loser_user_id: loserId, - player1_marks: this.marks[this.player1Id], - player2_marks: this.marks[this.player2Id], - player1_points: this.points[this.player1Id], - player2_points: this.points[this.player2Id], - }, - this.getAuthHeaders() - ); - console.log(`[Match ${this.id}] Encerrado com sucesso na API.`); + await axios.put(`${API_URL}/matches/${this.id}`, payload, this.getAuthHeaders()); + console.log(`[Match ${this.id}] Match Closed in DB.`); } catch (e) { - console.error(`Erro ao fechar Match API: ${e.message}`); + console.error(`Match Close Error: ${e.message}`); } } playCard(userId, cardId) { if (!this.currentGame || this.status !== "Playing") return; - return this.currentGame.playCard(userId, cardId); + + const result = this.currentGame.playCard(userId, cardId); + + // FIX: Restart timer if game continues + if (result && (result.action === 'next_turn' || result.action === 'trick_resolved')) { + this.currentGame.startTurnTimer((uid, cid) => { + if (this.autoPlayCallback) this.autoPlayCallback(uid, cid); + }); + } + + return result; } - getState() { - let gameState = null; - - if (this.currentGame) { - const p1Id = this.player1Id; - const p2Id = this.player2Id; - - if (this.currentGame.players && this.currentGame.players[p1Id]) { - gameState = this.currentGame.getStateForPlayer(p1Id); - } else if (this.currentGame.players && this.currentGame.players[p2Id]) { - gameState = this.currentGame.getStateForPlayer(p2Id); - } else if (this.currentGame.players) { - const availableIds = Object.keys(this.currentGame.players); - if (availableIds.length > 0) { - console.warn( - `⚠️ [Match ${this.id}] IDs desalinhados! A usar vista do user ${availableIds[0]}` - ); - gameState = this.currentGame.getStateForPlayer(availableIds[0]); - } - } - } + getGameState(userId) { + if (!this.currentGame) return null; + if (this.currentGame.players[userId]) return this.currentGame.getStateForPlayer(userId); + return null; + } + getPublicState() { return { matchId: this.id, status: this.status, marks: this.marks, points: this.points, - game: gameState, + player1: this.player1, + player2: this.player2 }; } } diff --git a/websockets/events/match.js b/websockets/events/match.js index 7cff0a7..7be44cc 100644 --- a/websockets/events/match.js +++ b/websockets/events/match.js @@ -5,134 +5,154 @@ import axios from "axios"; const API_URL = "http://localhost:8000/api/v1"; export default (io, socket) => { - const createMatchEmitter = (matchId) => (eventName, payload) => { - io.to(`match_${matchId}`).emit(eventName, payload); - }; + + const broadcastMatchUpdate = (matchId, match) => { + const roomName = `match_${matchId}`; + const room = io.sockets.adapter.rooms.get(roomName); + if (room) { + for (const socketId of room) { + const clientSocket = io.sockets.sockets.get(socketId); + if (!clientSocket || !clientSocket.user) continue; + const userId = clientSocket.user.id; + const publicState = match.getPublicState(); + const gameState = match.getGameState(userId); + clientSocket.emit("match:update", { ...publicState, game: gameState }); + } + } + }; - socket.on("match:enter", async (data) => { - const { matchId } = data; + const createMatchEmitter = (matchId) => (eventName, payload) => { + const match = getMatch(matchId); + if (!match) return; - if (!socket.user || !socket.user.id) { - return socket.emit("error", { message: "User not authenticated" }); - } + if (eventName === 'match:new-round-start' || eventName === 'match:ended-signal') { + broadcastMatchUpdate(matchId, match); + } else if (eventName === 'match:round-ended') { + io.to(`match_${matchId}`).emit('match:round-ended', payload); + } else { + io.to(`match_${matchId}`).emit(eventName, payload); + } + }; - const userId = socket.user.id; - const room = `match_${matchId}`; + // --- FIX: Timer Callback --- + const handleAutoPlay = (matchId) => (userId, cardId) => { + const match = getMatch(matchId); + if (!match || match.status !== "Playing") return; - let match = getMatch(matchId); + console.log(`[Match ${matchId}] Auto-playing for User ${userId}`); + const result = match.playCard(userId, cardId); - if (!match) { - try { - console.log(`A pedir match ${matchId} à API...`); - const res = await axios.get(`${API_URL}/matches/${matchId}`, { - headers: { Authorization: socket.token }, - }); + if (result && result.action === "trick_resolved") { + io.to(`match_${matchId}`).emit("game:trick-end", result.trickResult); + setTimeout(() => { broadcastMatchUpdate(matchId, match); }, 1500); + } else { + broadcastMatchUpdate(matchId, match); + } + }; - const matchData = res.data.data || res.data; + socket.on("match:enter", async (data) => { + const { matchId } = data; + if (!socket.user || !socket.user.id) return socket.emit("error", { message: "Auth failed" }); + const userId = socket.user.id; + socket.activeMatchId = matchId; - match = new BiscaMatch( - matchData, - createMatchEmitter(matchId), - socket.token - ); - addMatch(match); - console.log(`Match ${matchId} carregado em memória.`); - } catch (e) { - console.error("Erro ao carregar Match da API:", e.message); - socket.emit("error", "Match not found or API error"); - return; - } - } else { - match.token = socket.token; - } + const room = `match_${matchId}`; + let match = getMatch(matchId); - if (match) { - match.token = socket.token; - } + if (!match) { + try { + const res = await axios.get(`${API_URL}/matches/${matchId}`, { + headers: { Authorization: socket.token || socket.handshake.auth.token }, + }); + const matchData = res.data.data || res.data; + + // Pass handleAutoPlay to constructor + match = new BiscaMatch( + matchData, + createMatchEmitter(matchId), + socket.token, + handleAutoPlay(matchId) + ); + addMatch(match); + } catch (e) { + return socket.emit("error", "Match not found"); + } + } + match.emitStateCallback = createMatchEmitter(matchId); + if (userId !== match.player1Id && match.player2Id === 1) await match.joinPlayer(userId, socket.user); - match.emitStateCallback = createMatchEmitter(matchId); + socket.join(room); + const publicState = match.getPublicState(); + const gameState = match.getGameState(userId); + socket.emit("match:update", { ...publicState, game: gameState }); + if (match.status === 'Playing') broadcastMatchUpdate(matchId, match); + }); - if (userId !== match.player1Id && match.player2Id === 1) { - console.log(`User ${userId} a substituir o Ghost no Match ${matchId}`); - await match.joinPlayer(userId); - } + socket.on("game:play-card", (data) => { + const { matchId, cardId } = data; + const match = getMatch(matchId); + if (!match || match.status !== "Playing") return; + const result = match.playCard(socket.user.id, cardId); + if (result && result.error) return socket.emit("error", { message: result.error }); + if (result && result.action === "trick_resolved") { + io.to(`match_${matchId}`).emit("game:trick-end", result.trickResult); + setTimeout(() => { broadcastMatchUpdate(matchId, match); }, 1500); + } else { + broadcastMatchUpdate(matchId, match); + } + }); - socket.join(room); - console.log(`Socket ${socket.id} entrou na sala ${room}`); + socket.on("game:surrender", async ({ matchId }) => { + const match = getMatch(matchId); + if(!match || match.status !== "Playing") return; + const userId = socket.user.id; + console.log(`[Match ${matchId}] User ${userId} surrendered ROUND.`); + const opponentId = match.player1Id == userId ? match.player2Id : match.player1Id; + const result = { + winnerId: opponentId, + loserId: userId, + isDraw: false, + player1_id: match.player1Id, + player2_id: match.player2Id, + player1_points: match.player1Id == opponentId ? 91 : 0, + player2_points: match.player2Id == opponentId ? 91 : 0, + reason: 'surrender' + }; + await match.handleGameEnd(result); + }); - if (match.status === "Playing") { - socket.emit("match:update", match.getState()); - } else { - socket.emit("match:waiting", { msg: "Waiting for opponent..." }); - } - }); + socket.on("match:surrender", async ({ matchId }) => { + const match = getMatch(matchId); + if(!match || match.status !== "Playing") return; + const userId = socket.user.id; + + // FIX: Abort current game to remove ghost + await match.abortCurrentGame(userId); - socket.on("game:play-card", (data) => { - const { matchId, cardId } = data; - const match = getMatch(matchId); + console.log(`[Match ${matchId}] User ${userId} surrendered MATCH.`); + const opponentId = match.player1Id == userId ? match.player2Id : match.player1Id; + match.marks[opponentId] = 4; + await match.finishMatch(); + broadcastMatchUpdate(matchId, match); + }); - if (!match || match.status !== "Playing") { - console.log( - "Tentativa de jogar sem match ativo ou match não encontrado." - ); - return; - } + socket.on("disconnect", async () => { + if (socket.activeMatchId) { + const match = getMatch(socket.activeMatchId); + if (match && match.status === 'Playing') { + const userId = socket.user?.id; + if (userId === match.player1Id || userId === match.player2Id) { + console.log(`[Match ${match.id}] Player ${userId} disconnected.`); + + // FIX: Abort current game + await match.abortCurrentGame(userId); - const result = match.playCard(socket.user.id, cardId); - - if (result && result.error) { - console.log( - `❌ [Erro Jogo] User ${socket.user.id} tentou jogar carta ${cardId} mas: ${result.error}` - ); - - socket.emit("error", { message: result.error }); - return; - } - - if (match.currentGame) { - io.to(`match_${matchId}`).emit( - "game:update", - match.currentGame.getStateForPlayer(socket.user.id) - ); - } - }); - socket.on("debug:end-game", async (data) => { - const { matchId } = data; - const match = getMatch(matchId); - if (match) { - console.log(`[DEBUG] User ${socket.user.id} a forçar fim de jogo.`); - // Simula que o user que clicou ganhou 120 pontos - const winnerId = socket.user.id; - const loserId = - winnerId == match.player1Id ? match.player2Id : match.player1Id; - - // Objeto de resultado falso para fechar o jogo - const fakeResult = { - winnerId: winnerId, - loserId: loserId, - isDraw: false, - player1_points: winnerId == match.player1Id ? 120 : 0, - player2_points: winnerId == match.player2Id ? 120 : 0, - player1_id: match.player1Id, - player2_id: match.player2Id, - }; - - await match.handleGameEnd(fakeResult); - } - }); - - // DEBUG: Forçar fim do Match Completo - socket.on("debug:end-match", async (data) => { - const { matchId } = data; - const match = getMatch(matchId); - if (match) { - console.log(`[DEBUG] User ${socket.user.id} a forçar fim de MATCH.`); - // Dá 4 marcas a quem clicou - match.marks[socket.user.id] = 4; - await match.finishMatch(); - - // Emite o estado final - io.to(`match_${matchId}`).emit("match:ended", match.getState()); - } - }); + const opponentId = match.player1Id == userId ? match.player2Id : match.player1Id; + match.marks[opponentId] = 4; + await match.finishMatch(); + broadcastMatchUpdate(match.id, match); + } + } + } + }); };