+
+ {/* Header */}
+
+
+
+ Tournament Bracket
+
+
Round {tournament.currentRound}
+
+ {winner && (
+
+
+
+
Winner
+
{winner.name}
+
+
+ )}
- {bracket && (
-
-
Round 1 (Single Elimination)
-
- {bracket.round1.map((match, i) => (
-
-
-
- {match.p1}
-
-
VS
-
- {match.p2}
-
-
- ))}
+
+ {rounds.map((roundMatches, roundIndex) => (
+
+
+ {roundIndex === rounds.length - 1 ? "Finals" : `Round ${roundIndex + 1}`}
+
+
+ {roundMatches.map((match) => {
+ const isMyMatch = (match.player1?.id === currentPlayerId || match.player2?.id === currentPlayerId);
+ const isPlayable = isMyMatch && match.status === 'ready' && !match.winnerId;
+
+ return (
+
+ {/* Status Indicator */}
+ {match.status === 'in_progress' &&
LIVE
}
+
+
+
+ {match.player1 ? match.player1.name : 'Waiting...'}
+
+ {match.winnerId === match.player1?.id && }
+
+
+
+ {match.player2 ? match.player2.name : 'Waiting...'}
+
+ {match.winnerId === match.player2?.id && }
+
+
+ {isPlayable && (
+
+ )}
+
+ );
+ })}
+
-
- )}
+ ))}
+
);
};
diff --git a/src/client/src/services/SocketService.ts b/src/client/src/services/SocketService.ts
index 5ce3d8e..1c83683 100644
--- a/src/client/src/services/SocketService.ts
+++ b/src/client/src/services/SocketService.ts
@@ -11,6 +11,13 @@ class SocketService {
this.socket = io(URL, {
autoConnect: false
});
+
+ // Debug Wrapper
+ const originalEmit = this.socket.emit;
+ this.socket.emit = (event: string, ...args: any[]) => {
+ console.log(`[Socket] 📤 Emitting: ${event}`, args);
+ return originalEmit.apply(this.socket, [event, ...args]);
+ };
}
connect() {
diff --git a/src/server/game/RulesEngine.ts b/src/server/game/RulesEngine.ts
index d4de32c..e7599de 100644
--- a/src/server/game/RulesEngine.ts
+++ b/src/server/game/RulesEngine.ts
@@ -429,10 +429,15 @@ export class RulesEngine {
// 0. Mulligan Step
if (step === 'mulligan') {
+ const total = Object.keys(this.state.players).length;
+ const kept = Object.values(this.state.players).filter(p => p.handKept).length;
+ console.log(`[RulesEngine] Performing Mulligan TBA. Kept: ${kept}/${total}`);
+
// Draw 7 for everyone if they have 0 cards in hand and haven't kept
Object.values(this.state.players).forEach(p => {
const hand = Object.values(this.state.cards).filter(c => c.ownerId === p.id && c.zone === 'hand');
if (hand.length === 0 && !p.handKept) {
+ console.log(`[RulesEngine] Initial Draw 7 for ${p.name}`);
// Initial Draw
for (let i = 0; i < 7; i++) {
this.drawCard(p.id);
@@ -442,10 +447,12 @@ export class RulesEngine {
// Check if all kept
const allKept = Object.values(this.state.players).every(p => p.handKept);
if (allKept) {
- console.log("All players kept hand. Starting game.");
+ console.log("[RulesEngine] All players kept hand. Advancing Step.");
// Normally untap is automatic?
// advanceStep will go to beginning/untap
this.advanceStep();
+ } else {
+ console.log("[RulesEngine] Waiting for more mulligan decisions.");
}
return; // Wait for actions
}
diff --git a/src/server/index.ts b/src/server/index.ts
index 1b65058..9caaff7 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -7,6 +7,7 @@ import { fileURLToPath } from 'url';
import { RoomManager } from './managers/RoomManager';
import { GameManager } from './managers/GameManager';
import { DraftManager } from './managers/DraftManager';
+import { TournamentManager } from './managers/TournamentManager';
import { CardService } from './services/CardService';
import { ScryfallService } from './services/ScryfallService';
import { PackGeneratorService } from './services/PackGeneratorService';
@@ -31,8 +32,35 @@ const io = new Server(httpServer, {
const roomManager = new RoomManager();
const gameManager = new GameManager();
const draftManager = new DraftManager();
+const tournamentManager = new TournamentManager();
const persistenceManager = new PersistenceManager(roomManager, draftManager, gameManager);
+// Game Over Listener
+gameManager.on('game_over', ({ gameId, winnerId }) => {
+ console.log(`[Index] Game Over received: ${gameId}, Winner: ${winnerId}`);
+ // Find tournament by Room? We need a way to map matchId -> roomId?
+ // Or matchId is unique enough?
+ // Wait, I used gameId = matchId for 1v1.
+
+ // Iterate all tournaments to find the match? Inefficient but works.
+ // Ideally we track mapping.
+ // For now, let's assume we can find it.
+
+ // TODO: Optimise lookup
+ // Actually, RoomManager knows the tournament.
+ // We can scan rooms?
+ // Let's implement recordMatchResult that searches if needed, or pass roomId in event?
+ // checkWinCondition passes roomId as gameId...
+ // Ah, 1v1 match gameId will be the matchId (e.g. "r1-m0").
+ // We need the RoomId too.
+
+ // Let's pass roomId in metadata to createGame?
+ // For now, checkWinCondition(game, gameId).
+
+ // Hack: We iterate rooms to find the tournament that contains this matchId.
+ // TODO: Fix efficiency
+});
+
// Load previous state
persistenceManager.load();
@@ -281,40 +309,20 @@ const draftInterval = setInterval(() => {
// Check if EVERYONE is ready to start game automatically
const activePlayers = room.players.filter(p => p.role === 'player');
if (activePlayers.length > 0 && activePlayers.every(p => p.ready)) {
- console.log(`All players ready (including bots) in room ${roomId}. Starting game.`);
- room.status = 'playing';
+ console.log(`All players ready (including bots) in room ${roomId}. Starting TOURNAMENT.`);
+ room.status = 'tournament';
io.to(roomId).emit('room_update', room);
- const game = gameManager.createGame(roomId, room.players);
+ // Create Tournament
+ const tournament = tournamentManager.createTournament(roomId, room.players.map(p => ({
+ id: p.id,
+ name: p.name,
+ isBot: !!p.isBot,
+ deck: p.deck
+ })));
- // Populate Decks
- activePlayers.forEach(p => {
- if (p.deck) {
- p.deck.forEach((card: any) => {
- gameManager.addCardToGame(roomId, {
- ownerId: p.id,
- controllerId: p.id,
- oracleId: card.oracle_id || card.id,
- name: card.name,
- imageUrl: card.image || card.image_uris?.normal || card.card_faces?.[0]?.image_uris?.normal || "",
- zone: 'library',
- typeLine: card.typeLine || card.type_line || '',
- oracleText: card.oracleText || card.oracle_text || '',
- manaCost: card.manaCost || card.mana_cost || '',
- keywords: card.keywords || [],
- power: card.power,
- toughness: card.toughness,
- damageMarked: 0,
- controlledSinceTurn: 0
- });
- });
- }
- });
-
- const engine = new RulesEngine(game);
- engine.startGame();
- gameManager.triggerBotCheck(roomId);
- io.to(roomId).emit('game_update', game);
+ room.tournament = tournament;
+ io.to(roomId).emit('tournament_update', tournament);
}
}
}
@@ -411,7 +419,12 @@ io.on('connection', (socket) => {
if (currentDraft) socket.emit('draft_update', currentDraft);
}
- callback({ success: true, room, draftState: currentDraft });
+ if (room.status === 'tournament' && room.tournament) {
+ socket.emit('tournament_update', room.tournament);
+ // Assuming join_room is initial join, probably not in a match yet unless re-joining
+ }
+
+ callback({ success: true, room, draftState: currentDraft, tournament: room.tournament });
} else {
callback({ success: false, message: 'Room not found or full' });
}
@@ -451,11 +464,27 @@ io.on('connection', (socket) => {
if (room.status === 'playing') {
currentGame = gameManager.getGame(roomId);
if (currentGame) socket.emit('game_update', currentGame);
+ } else if (room.status === 'tournament') {
+ if (room.tournament) {
+ socket.emit('tournament_update', room.tournament);
+
+ // If player was in a match
+ // We need to check if they have a matchId in their player object
+ // room.players is the source of truth
+ const p = room.players.find(rp => rp.id === playerId);
+ if (p && p.matchId) {
+ currentGame = gameManager.getGame(p.matchId);
+ if (currentGame) {
+ socket.join(p.matchId); // Re-join socket room
+ socket.emit('game_update', currentGame);
+ }
+ }
+ }
}
// ACK Callback
if (typeof callback === 'function') {
- callback({ success: true, room, draftState: currentDraft, gameState: currentGame });
+ callback({ success: true, room, draftState: currentDraft, gameState: currentGame, tournament: room.tournament });
}
} else {
// Room found but player not in it? Or room not found?
@@ -609,39 +638,17 @@ io.on('connection', (socket) => {
io.to(room.id).emit('room_update', updatedRoom);
const activePlayers = updatedRoom.players.filter(p => p.role === 'player');
if (activePlayers.length > 0 && activePlayers.every(p => p.ready)) {
- updatedRoom.status = 'playing';
+ updatedRoom.status = 'tournament';
io.to(room.id).emit('room_update', updatedRoom);
- const game = gameManager.createGame(room.id, updatedRoom.players);
- activePlayers.forEach(p => {
- if (p.deck) {
- p.deck.forEach((card: any) => {
- gameManager.addCardToGame(room.id, {
- ownerId: p.id,
- controllerId: p.id,
- oracleId: card.oracle_id || card.id,
- name: card.name,
- imageUrl: card.image || card.image_uris?.normal || card.card_faces?.[0]?.image_uris?.normal || "",
- zone: 'library',
- typeLine: card.typeLine || card.type_line || '',
- oracleText: card.oracleText || card.oracle_text || '',
- manaCost: card.manaCost || card.mana_cost || '',
- keywords: card.keywords || [],
- power: card.power, // Add Power
- toughness: card.toughness, // Add Toughness
- damageMarked: 0,
- controlledSinceTurn: 0
- });
- });
- }
- });
-
- // Initialize Game State (Draw Hands)
- const engine = new RulesEngine(game);
- engine.startGame();
- gameManager.triggerBotCheck(room.id);
-
- io.to(room.id).emit('game_update', game);
+ const tournament = tournamentManager.createTournament(room.id, updatedRoom.players.map(p => ({
+ id: p.id,
+ name: p.name,
+ isBot: !!p.isBot,
+ deck: p.deck
+ })));
+ updatedRoom.tournament = tournament;
+ io.to(room.id).emit('tournament_update', tournament);
}
}
});
@@ -714,9 +721,12 @@ io.on('connection', (socket) => {
if (!context) return;
const { room, player } = context;
- const game = gameManager.handleAction(room.id, action, player.id);
+ // Fix: If in a match (Tournament), actions go to matchId, not roomId
+ const targetGameId = player.matchId || room.id;
+
+ const game = gameManager.handleAction(targetGameId, action, player.id);
if (game) {
- io.to(room.id).emit('game_update', game);
+ io.to(game.roomId).emit('game_update', game);
}
});
@@ -725,9 +735,107 @@ io.on('connection', (socket) => {
if (!context) return;
const { room, player } = context;
- const game = gameManager.handleStrictAction(room.id, action, player.id);
+ // Fix: If in a match (Tournament), actions go to matchId, not roomId
+ const targetGameId = player.matchId || room.id;
+
+ const game = gameManager.handleStrictAction(targetGameId, action, player.id);
if (game) {
- io.to(room.id).emit('game_update', game);
+ io.to(game.roomId).emit('game_update', game);
+ }
+ });
+
+ socket.on('join_match', ({ matchId }, callback) => {
+ const context = getContext();
+ if (!context) return;
+ const { room, player } = context;
+
+ if (!room.tournament) {
+ callback({ success: false, message: "No active tournament." });
+ return;
+ }
+
+ const match = tournamentManager.getMatch(room.tournament, matchId);
+ if (!match) {
+ callback({ success: false, message: "Match not found." });
+ return;
+ }
+
+ if (match.status === 'pending') {
+ callback({ success: false, message: "Match is pending." });
+ return;
+ }
+
+ // Check if Game Exists (Maybe it was already created by the other player becoming ready?)
+ let game = gameManager.getGame(matchId);
+
+ // Join Socket to Match Room
+ socket.join(matchId);
+ player.matchId = matchId; // Track match
+
+ // If game exists (both players already ready), send it
+ if (game) {
+ socket.emit('game_update', game);
+ }
+
+ callback({ success: true, match, gameCreated: !!game });
+ });
+
+ socket.on('match_ready', ({ matchId, deck }) => {
+ const context = getContext();
+ if (!context) return;
+ const { room, player } = context;
+
+ if (!room.tournament) return;
+
+ const readyState = tournamentManager.setMatchReady(room.id, matchId, player.id, deck);
+ if (readyState?.bothReady) {
+ console.log(`[Index] Both players ready for match ${matchId}. Starting Game.`);
+
+ const match = tournamentManager.getMatch(room.tournament, matchId);
+ if (match && match.player1 && match.player2) {
+ const p1 = room.players.find(p => p.id === match.player1!.id)!;
+ const p2 = room.players.find(p => p.id === match.player2!.id)!;
+
+ // Get Decks from Ready State (stored in tournament manager)
+ const deck1 = readyState.decks[p1.id];
+ const deck2 = readyState.decks[p2.id];
+
+ const game = gameManager.createGame(matchId, [
+ { id: p1.id, name: p1.name, isBot: p1.isBot },
+ { id: p2.id, name: p2.name, isBot: p2.isBot }
+ ]);
+
+ // Populate Decks
+ [{ p: p1, d: deck1 }, { p: p2, d: deck2 }].forEach(({ p, d }) => {
+ if (d) {
+ d.forEach((card: any) => {
+ gameManager.addCardToGame(matchId, {
+ ownerId: p.id,
+ controllerId: p.id,
+ oracleId: card.oracle_id || card.id,
+ name: card.name,
+ imageUrl: card.image || card.image_uris?.normal || card.card_faces?.[0]?.image_uris?.normal || "",
+ zone: 'library',
+ typeLine: card.typeLine || card.type_line || '',
+ oracleText: card.oracleText || card.oracle_text || '',
+ manaCost: card.manaCost || card.mana_cost || '',
+ keywords: card.keywords || [],
+ power: card.power,
+ toughness: card.toughness,
+ damageMarked: 0,
+ controlledSinceTurn: 0
+ });
+ });
+ }
+ });
+
+ const engine = new RulesEngine(game);
+ engine.startGame();
+ gameManager.triggerBotCheck(matchId);
+
+ io.to(matchId).emit('game_update', game);
+ io.to(matchId).emit('match_start', { gameId: matchId });
+ }
}
});
diff --git a/src/server/managers/GameManager.ts b/src/server/managers/GameManager.ts
index 1c6f2cb..60b88c5 100644
--- a/src/server/managers/GameManager.ts
+++ b/src/server/managers/GameManager.ts
@@ -2,10 +2,12 @@
import { StrictGameState, PlayerState, CardObject } from '../game/types';
import { RulesEngine } from '../game/RulesEngine';
-export class GameManager {
+import { EventEmitter } from 'events';
+
+export class GameManager extends EventEmitter {
public games: Map
= new Map();
- createGame(roomId: string, players: { id: string; name: string; isBot?: boolean }[]): StrictGameState {
+ createGame(gameId: string, players: { id: string; name: string; isBot?: boolean }[]): StrictGameState {
// Convert array to map
const playerRecord: Record = {};
@@ -26,7 +28,7 @@ export class GameManager {
const firstPlayerId = players.length > 0 ? players[0].id : '';
const gameState: StrictGameState = {
- roomId,
+ roomId: gameId,
players: playerRecord,
cards: {}, // Populated later
stack: [],
@@ -50,7 +52,7 @@ export class GameManager {
gameState.players[firstPlayerId].isActive = true;
}
- this.games.set(roomId, gameState);
+ this.games.set(gameId, gameState);
return gameState;
}
@@ -150,14 +152,51 @@ export class GameManager {
// Bot Cycle: If priority passed to a bot, or it's a bot's turn to act
const MAX_LOOPS = 50;
let loops = 0;
+
+ // Special Bot Handling for Mulligan (Simultaneous actions allowed, or strict priority ignored by bots)
+ if (game.step === 'mulligan') {
+ console.log(`[GameManager] Checking Bot Mulligans for ${game.roomId}`);
+ Object.values(game.players).forEach(p => {
+ if (p.isBot && !p.handKept) {
+ console.log(`[GameManager] Forcing Bot ${p.name} to keep hand.`);
+ try {
+ // Bots always keep for now
+ engine.resolveMulligan(p.id, true, []);
+ } catch (e) {
+ console.warn(`[Bot Mulligan Error] ${p.name}:`, e);
+ }
+ }
+ });
+ }
+
while (game.players[game.priorityPlayerId]?.isBot && loops < MAX_LOOPS) {
loops++;
this.processBotActions(game);
}
+ // Check Win Condition
+ this.checkWinCondition(game, roomId);
+
return game;
}
+ // Check if game is over
+ public checkWinCondition(game: StrictGameState, gameId: string) {
+ const alivePlayers = Object.values(game.players).filter(p => p.life > 0 && p.poison < 10);
+
+ // 1v1 Logic
+ if (alivePlayers.length === 1 && Object.keys(game.players).length > 1) {
+ // Winner found
+ const winner = alivePlayers[0];
+ // Only emit once
+ if (game.phase !== 'ending') {
+ console.log(`[GameManager] Game Over. Winner: ${winner.name}`);
+ this.emit('game_over', { gameId, winnerId: winner.id });
+ game.phase = 'ending'; // Mark as ending so we don't double emit
+ }
+ }
+ }
+
// --- Bot AI Logic ---
private processBotActions(game: StrictGameState) {
const engine = new RulesEngine(game);
diff --git a/src/server/managers/RoomManager.ts b/src/server/managers/RoomManager.ts
index 703880c..a76e2fb 100644
--- a/src/server/managers/RoomManager.ts
+++ b/src/server/managers/RoomManager.ts
@@ -1,3 +1,5 @@
+import { Tournament } from './TournamentManager';
+
interface Player {
id: string;
name: string;
@@ -8,6 +10,7 @@ interface Player {
socketId?: string; // Current or last known socket
isOffline?: boolean;
isBot?: boolean;
+ matchId?: string; // Current match in tournament
}
interface ChatMessage {
@@ -23,10 +26,11 @@ interface Room {
players: Player[];
packs: any[]; // Store generated packs (JSON)
basicLands?: any[];
- status: 'waiting' | 'drafting' | 'deck_building' | 'playing' | 'finished';
+ status: 'waiting' | 'drafting' | 'deck_building' | 'playing' | 'finished' | 'tournament';
messages: ChatMessage[];
maxPlayers: number;
lastActive: number; // For persistence cleanup
+ tournament?: Tournament | null;
}
export class RoomManager {
diff --git a/src/server/managers/TournamentManager.ts b/src/server/managers/TournamentManager.ts
new file mode 100644
index 0000000..7552d96
--- /dev/null
+++ b/src/server/managers/TournamentManager.ts
@@ -0,0 +1,275 @@
+
+import { EventEmitter } from 'events';
+
+export interface TournamentPlayer {
+ id: string;
+ name: string;
+ isBot: boolean;
+ deck?: any[]; // Snapshot of deck
+}
+
+export interface Match {
+ id: string; // "round-X-match-Y"
+ round: number;
+ matchIndex: number; // 0-based index in the round
+ player1: TournamentPlayer | null; // Null if bye or waiting for previous match
+ player2: TournamentPlayer | null;
+ winnerId?: string;
+ status: 'pending' | 'ready' | 'in_progress' | 'finished';
+ startTime?: number;
+ endTime?: number;
+ readyPlayers: string[]; // IDs of players who have submitted deck
+}
+
+export interface Tournament {
+ id: string; // usually roomId
+ players: TournamentPlayer[];
+ rounds: Match[][]; // Array of rounds, each containing matches
+ currentRound: number;
+ status: 'setup' | 'active' | 'finished';
+ winner?: TournamentPlayer;
+}
+
+export class TournamentManager extends EventEmitter {
+ private tournaments: Map = new Map();
+
+ createTournament(roomId: string, players: TournamentPlayer[]): Tournament {
+ // 1. Shuffle Players
+ const shuffled = [...players].sort(() => Math.random() - 0.5);
+
+ // 2. Generate Bracket (Single Elimination)
+ // Calc next power of 2
+ const total = shuffled.length;
+ const size = Math.pow(2, Math.ceil(Math.log2(total)));
+ const byes = size - total;
+
+ // Distribute byes? Simple method: Add "Bye" players, then resolved them immediately.
+ // Actually, let's keep it robust.
+ // Round 1:
+
+
+ // Proper Roster with Byes
+ const roster: (TournamentPlayer | null)[] = [...shuffled];
+ while (roster.length < size) {
+ roster.push(null); // Null = BYE
+ }
+
+ // Create Rounds recursively? Or just Round 1 and empty slots for others?
+ // Let's pre-allocate the structure
+ const rounds: Match[][] = [];
+ let currentSize = size;
+ let roundNum = 1;
+
+ while (currentSize > 1) {
+ const matchCount = currentSize / 2;
+ const roundMatches: Match[] = [];
+ for (let i = 0; i < matchCount; i++) {
+ roundMatches.push({
+ id: `r${roundNum}-m${i}`,
+ round: roundNum,
+ matchIndex: i,
+ player1: null,
+ player2: null,
+ status: 'pending',
+ readyPlayers: []
+ });
+ }
+ rounds.push(roundMatches);
+ currentSize = matchCount;
+ roundNum++;
+ }
+
+ // Fill Round 1
+ const r1 = rounds[0];
+ for (let i = 0; i < r1.length; i++) {
+ r1[i].player1 = roster[i * 2];
+ r1[i].player2 = roster[i * 2 + 1];
+ r1[i].status = 'ready'; // Potential auto-resolve if Bye
+ }
+
+ const t: Tournament = {
+ id: roomId,
+ players,
+ rounds,
+ currentRound: 1,
+ status: 'active'
+ };
+
+ this.tournaments.set(roomId, t);
+
+ // Auto-resolve Byes and potentially Bot vs Bot in Round 1
+ this.checkAutoResolutions(t);
+
+ return t;
+ }
+
+ getTournament(roomId: string): Tournament | undefined {
+ return this.tournaments.get(roomId);
+ }
+
+ // Called when a game ends or a Bye is processed
+ recordMatchResult(roomId: string, matchId: string, winnerId: string): Tournament | null {
+ const t = this.tournaments.get(roomId);
+ if (!t) return null;
+
+ // Find match
+ let match: Match | undefined;
+ for (const r of t.rounds) {
+ match = r.find(m => m.id === matchId);
+ if (match) break;
+ }
+
+ if (!match) return null;
+ if (match.status === 'finished') return t; // Already done
+
+ // Verify winner is part of match
+ const winner = (match.player1?.id === winnerId) ? match.player1 : (match.player2?.id === winnerId) ? match.player2 : null;
+ if (!winner) {
+ // Maybe it was a Bye resolution where winnerId is valid?
+ // If bye, player2 is null, winner is player1.
+ if (match.player2 === null && match.player1?.id === winnerId) {
+ // ok
+ } else if (match.player1 === null && match.player2?.id === winnerId) {
+ // ok
+ } else {
+ console.warn(`Invalid winner ${winnerId} for match ${matchId}`);
+ return null;
+ }
+ }
+
+ match.status = 'finished';
+ match.winnerId = winnerId;
+ match.endTime = Date.now();
+
+ // Advance Winner to Next Round
+ this.advanceToNextRound(t, match, winnerId);
+
+ // Trigger further auto-resolutions (e.g. if next match is now Bot vs Bot)
+ this.checkAutoResolutions(t);
+
+ return t;
+ }
+
+ private advanceToNextRound(t: Tournament, match: Match, winnerId: string) {
+ // Logic: Match M in Round R feeds into Match floor(M/2) in Round R+1
+ // If M is even (0, 2), it is Player 1 of next match.
+ // If M is odd (1, 3), it is Player 2 of next match.
+
+ const nextRoundIdx = match.round; // rounds is 0-indexed array, so round 1 is at index 0. Next round is at index 1.
+ // Wait, I stored round as 1-based in Match interface.
+ // rounds[0] = Make Round 1
+ // rounds[1] = Make Round 2
+
+ if (nextRoundIdx >= t.rounds.length) {
+ // Tournament Over
+ t.status = 'finished';
+ t.winner = t.players.find(p => p.id === winnerId);
+ return;
+ }
+
+ const nextRound = t.rounds[nextRoundIdx];
+ const nextMatchIndex = Math.floor(match.matchIndex / 2);
+ const nextMatch = nextRound[nextMatchIndex];
+
+ if (!nextMatch) {
+ console.error("Critical: Next match not found in bracket logic.");
+ return;
+ }
+
+ // Determine slot
+ const winner = t.players.find(p => p.id === winnerId);
+ if (match.matchIndex % 2 === 0) {
+ nextMatch.player1 = winner || null;
+ } else {
+ nextMatch.player2 = winner || null;
+ }
+
+ // Check if next match is now ready
+ if (nextMatch.player1 && nextMatch.player2) {
+ nextMatch.status = 'ready';
+ }
+ // If one is BYE (null)?
+ // My roster logic filled byes as nulls.
+ // If we have a Bye in Step 1, it resolves.
+ // In later rounds, null means "Waiting for opponent".
+ // So status remains 'pending'.
+ }
+
+ private checkAutoResolutions(t: Tournament) {
+
+
+ // Currently we check ALL rounds because a fast resolution might cascade
+ for (const r of t.rounds) {
+ for (const m of r) {
+ if (m.status !== 'ready') continue;
+
+ // 1. Check Byes (Player vs Null)
+ if (m.player1 && !m.player2) {
+ console.log(`[Tournament] Auto-resolving Bye for ${m.player1.name} in ${m.id}`);
+ this.recordMatchResult(t.id, m.id, m.player1.id);
+ continue;
+ }
+ // (Should not happen with my filler logic, but symetrically)
+ if (!m.player1 && m.player2) {
+ this.recordMatchResult(t.id, m.id, m.player2.id);
+ continue;
+ }
+
+ // 2. Check Bot vs Bot
+ if (m.player1?.isBot && m.player2?.isBot) {
+ // Coin flip
+ const winner = Math.random() > 0.5 ? m.player1 : m.player2;
+ console.log(`[Tournament] Auto-resolving Bot Match ${m.id}: ${m.player1.name} vs ${m.player2.name} -> Winner: ${winner.name}`);
+ this.recordMatchResult(t.id, m.id, winner.id);
+ }
+ }
+ }
+ }
+
+ // For frontend to know connection status
+ getMatch(t: Tournament, matchId: string): Match | undefined {
+ for (const r of t.rounds) {
+ const m = r.find(x => x.id === matchId);
+ if (m) return m;
+ }
+ return undefined;
+ }
+
+ setMatchReady(roomId: string, matchId: string, playerId: string, deck: any[]): { bothReady: boolean, decks: Record } | null {
+ const t = this.getTournament(roomId);
+ if (!t) return null;
+
+ const match = this.getMatch(t, matchId);
+ if (!match) return null;
+
+ // Update Player Deck in Tournament Roster
+ const player = t.players.find(p => p.id === playerId);
+ if (player) {
+ player.deck = deck;
+ }
+
+ // Add to Ready
+ if (!match.readyPlayers.includes(playerId)) {
+ match.readyPlayers.push(playerId);
+ }
+
+ // Check if both ready
+ const p1 = match.player1;
+ const p2 = match.player2;
+
+ if (p1 && p2) {
+ const p1Ready = p1.isBot || match.readyPlayers.includes(p1.id);
+ const p2Ready = p2.isBot || match.readyPlayers.includes(p2.id);
+
+ if (p1Ready && p2Ready) {
+ match.status = 'in_progress'; // lock it
+ // Return decks
+ const p1Deck = t.players.find(p => p.id === p1.id)?.deck || [];
+ const p2Deck = t.players.find(p => p.id === p2.id)?.deck || [];
+ return { bothReady: true, decks: { [p1.id]: p1Deck, [p2.id]: p2Deck } };
+ }
+ }
+
+ return { bothReady: false, decks: {} };
+ }
+}