created the tournament ui and fixed the turns sequence
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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<string, StrictGameState> = 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<string, PlayerState> = {};
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
275
src/server/managers/TournamentManager.ts
Normal file
275
src/server/managers/TournamentManager.ts
Normal file
@@ -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<string, Tournament> = 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<string, any[]> } | 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: {} };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user