created the tournament ui and fixed the turns sequence
This commit is contained in:
@@ -82,7 +82,7 @@ define(['./workbox-5a5d9309'], (function (workbox) { 'use strict';
|
|||||||
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
||||||
}, {
|
}, {
|
||||||
"url": "index.html",
|
"url": "index.html",
|
||||||
"revision": "0.gg4oatbh7is"
|
"revision": "0.ca9afac9bpo"
|
||||||
}], {});
|
}], {});
|
||||||
workbox.cleanupOutdatedCaches();
|
workbox.cleanupOutdatedCaches();
|
||||||
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Layers, Box, Trophy, Users, Play } from 'lucide-react';
|
import { Layers, Box, Trophy, Users, Play } from 'lucide-react';
|
||||||
import { CubeManager } from './modules/cube/CubeManager';
|
import { CubeManager } from './modules/cube/CubeManager';
|
||||||
import { TournamentManager } from './modules/tournament/TournamentManager';
|
|
||||||
import { LobbyManager } from './modules/lobby/LobbyManager';
|
import { LobbyManager } from './modules/lobby/LobbyManager';
|
||||||
import { DeckTester } from './modules/tester/DeckTester';
|
import { DeckTester } from './modules/tester/DeckTester';
|
||||||
import { Pack } from './services/PackGeneratorService';
|
import { Pack } from './services/PackGeneratorService';
|
||||||
@@ -130,7 +129,13 @@ export const App: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
{activeTab === 'lobby' && <LobbyManager generatedPacks={generatedPacks} availableLands={availableLands} />}
|
{activeTab === 'lobby' && <LobbyManager generatedPacks={generatedPacks} availableLands={availableLands} />}
|
||||||
{activeTab === 'tester' && <DeckTester />}
|
{activeTab === 'tester' && <DeckTester />}
|
||||||
{activeTab === 'bracket' && <TournamentManager />}
|
{activeTab === 'bracket' && (
|
||||||
|
<div className="flex flex-col items-center justify-center h-full text-slate-400">
|
||||||
|
<Trophy className="w-16 h-16 mb-4 opacity-50" />
|
||||||
|
<h2 className="text-xl font-bold">Tournament Manager</h2>
|
||||||
|
<p>Tournaments are now managed within the Online Lobby.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer className="bg-slate-900 border-t border-slate-800 p-2 text-center text-xs text-slate-500 shrink-0">
|
<footer className="bg-slate-900 border-t border-slate-800 p-2 text-center text-xs text-slate-500 shrink-0">
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ interface SidePanelPreviewProps {
|
|||||||
onToggleCollapse: (collapsed: boolean) => void;
|
onToggleCollapse: (collapsed: boolean) => void;
|
||||||
onResizeStart?: (e: React.MouseEvent | React.TouchEvent) => void;
|
onResizeStart?: (e: React.MouseEvent | React.TouchEvent) => void;
|
||||||
className?: string; // For additional styling (positioning, z-index, etc)
|
className?: string; // For additional styling (positioning, z-index, etc)
|
||||||
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SidePanelPreview: React.FC<SidePanelPreviewProps> = ({
|
export const SidePanelPreview: React.FC<SidePanelPreviewProps> = ({
|
||||||
|
|||||||
@@ -17,7 +17,10 @@ interface DeckBuilderViewProps {
|
|||||||
roomId: string;
|
roomId: string;
|
||||||
currentPlayerId: string;
|
currentPlayerId: string;
|
||||||
initialPool: any[];
|
initialPool: any[];
|
||||||
|
initialDeck?: any[];
|
||||||
availableBasicLands?: any[];
|
availableBasicLands?: any[];
|
||||||
|
onSubmit?: (deck: any[]) => void;
|
||||||
|
submitLabel?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ManaCurve = ({ deck }: { deck: any[] }) => {
|
const ManaCurve = ({ deck }: { deck: any[] }) => {
|
||||||
@@ -176,6 +179,40 @@ const ListItem: React.FC<{ card: DraftCard; onClick?: () => void; onHover?: (c:
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DeckCardItem = ({ card, useArtCrop, isFoil, onCardClick, onHover }: any) => {
|
||||||
|
const displayImage = useArtCrop ? card.imageArtCrop : card.image;
|
||||||
|
const { onTouchStart, onTouchEnd, onTouchMove, onClick } = useCardTouch(onHover, () => {
|
||||||
|
if (window.matchMedia('(pointer: coarse)').matches) {
|
||||||
|
onHover(card);
|
||||||
|
} else {
|
||||||
|
onCardClick(card);
|
||||||
|
}
|
||||||
|
}, card);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={onClick}
|
||||||
|
onMouseEnter={() => onHover(card)}
|
||||||
|
onMouseLeave={() => onHover(null)}
|
||||||
|
onTouchStart={onTouchStart}
|
||||||
|
onTouchEnd={onTouchEnd}
|
||||||
|
onTouchMove={onTouchMove}
|
||||||
|
className="relative group bg-slate-900 rounded-lg shrink-0 cursor-pointer hover:scale-105 transition-transform"
|
||||||
|
>
|
||||||
|
<div className={`relative ${useArtCrop ? 'aspect-square' : 'aspect-[2.5/3.5]'} overflow-hidden rounded-lg shadow-xl border transition-all duration-200 group-hover:ring-2 group-hover:ring-purple-400 group-hover:shadow-purple-500/30 ${isFoil ? 'border-purple-400 shadow-purple-500/20' : 'border-slate-800'}`}>
|
||||||
|
{isFoil && <FoilOverlay />}
|
||||||
|
{isFoil && <div className="absolute top-1 right-1 z-30 text-[10px] font-bold text-white bg-purple-600/80 px-1.5 rounded backdrop-blur-sm">FOIL</div>}
|
||||||
|
{displayImage ? (
|
||||||
|
<img src={displayImage} alt={card.name} className="w-full h-full object-cover" draggable={false} />
|
||||||
|
) : (
|
||||||
|
<div className="w-full h-full flex items-center justify-center text-xs text-center p-1 text-slate-500 font-bold border-2 border-slate-700 m-1 rounded">{card.name}</div>
|
||||||
|
)}
|
||||||
|
<div className={`absolute bottom-0 left-0 right-0 h-1.5 ${card.rarity === 'mythic' ? 'bg-gradient-to-r from-orange-500 to-red-600' : card.rarity === 'rare' ? 'bg-gradient-to-r from-yellow-400 to-yellow-600' : card.rarity === 'uncommon' ? 'bg-gradient-to-r from-gray-300 to-gray-500' : 'bg-black'}`} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// Extracted Component to avoid re-mounting issues
|
// Extracted Component to avoid re-mounting issues
|
||||||
const CardsDisplay: React.FC<{
|
const CardsDisplay: React.FC<{
|
||||||
cards: any[];
|
cards: any[];
|
||||||
@@ -273,7 +310,7 @@ const CardsDisplay: React.FC<{
|
|||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, availableBasicLands = [] }) => {
|
export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, initialDeck = [], availableBasicLands = [], onSubmit, submitLabel }) => {
|
||||||
// Unlimited Timer (Static for now)
|
// Unlimited Timer (Static for now)
|
||||||
const [timer] = useState<string>("Unlimited");
|
const [timer] = useState<string>("Unlimited");
|
||||||
/* --- Hooks --- */
|
/* --- Hooks --- */
|
||||||
@@ -359,8 +396,17 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
|
|||||||
useEffect(() => localStorage.setItem('deck_groupBy', groupBy), [groupBy]);
|
useEffect(() => localStorage.setItem('deck_groupBy', groupBy), [groupBy]);
|
||||||
useEffect(() => localStorage.setItem('deck_cardWidth', cardWidth.toString()), [cardWidth]);
|
useEffect(() => localStorage.setItem('deck_cardWidth', cardWidth.toString()), [cardWidth]);
|
||||||
|
|
||||||
const [pool, setPool] = useState<any[]>(initialPool);
|
const [deck, setDeck] = useState<any[]>(initialDeck);
|
||||||
const [deck, setDeck] = useState<any[]>([]);
|
const [pool, setPool] = useState<any[]>(() => {
|
||||||
|
if (initialDeck && initialDeck.length > 0) {
|
||||||
|
// Need to be careful about IDs.
|
||||||
|
// If initialDeck cards are from the pool, they share IDs?
|
||||||
|
// Usually yes.
|
||||||
|
const deckIds = new Set(initialDeck.map(c => c.id));
|
||||||
|
return initialPool.filter(c => !deckIds.has(c.id));
|
||||||
|
}
|
||||||
|
return initialPool;
|
||||||
|
});
|
||||||
// const [lands, setLands] = useState(...); // REMOVED: Managed directly in deck now
|
// const [lands, setLands] = useState(...); // REMOVED: Managed directly in deck now
|
||||||
const [hoveredCard, setHoveredCard] = useState<any>(null);
|
const [hoveredCard, setHoveredCard] = useState<any>(null);
|
||||||
const [displayCard, setDisplayCard] = useState<any>(null);
|
const [displayCard, setDisplayCard] = useState<any>(null);
|
||||||
@@ -498,7 +544,13 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
|
|||||||
return cardWithDefinition;
|
return cardWithDefinition;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if (onSubmit) {
|
||||||
|
onSubmit(preparedDeck);
|
||||||
|
} else {
|
||||||
socketService.socket.emit('player_ready', { deck: preparedDeck });
|
socketService.socket.emit('player_ready', { deck: preparedDeck });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAutoBuild = async () => {
|
const handleAutoBuild = async () => {
|
||||||
@@ -876,7 +928,7 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
|
|||||||
onClick={submitDeck}
|
onClick={submitDeck}
|
||||||
className="bg-emerald-600 hover:bg-emerald-500 text-white px-4 py-2 rounded-lg font-bold shadow-lg flex items-center gap-2 transition-transform hover:scale-105 text-sm"
|
className="bg-emerald-600 hover:bg-emerald-500 text-white px-4 py-2 rounded-lg font-bold shadow-lg flex items-center gap-2 transition-transform hover:scale-105 text-sm"
|
||||||
>
|
>
|
||||||
<Save className="w-4 h-4" /> <span className="hidden sm:inline">Submit Deck</span><span className="sm:hidden">Save</span>
|
<Save className="w-4 h-4" /> <span className="hidden sm:inline">{submitLabel || 'Submit Deck'}</span><span className="sm:hidden">Save</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1004,36 +1056,4 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const DeckCardItem = ({ card, useArtCrop, isFoil, onCardClick, onHover }: any) => {
|
|
||||||
const displayImage = useArtCrop ? card.imageArtCrop : card.image;
|
|
||||||
const { onTouchStart, onTouchEnd, onTouchMove, onClick } = useCardTouch(onHover, () => {
|
|
||||||
if (window.matchMedia('(pointer: coarse)').matches) {
|
|
||||||
onHover(card);
|
|
||||||
} else {
|
|
||||||
onCardClick(card);
|
|
||||||
}
|
|
||||||
}, card);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
onClick={onClick}
|
|
||||||
onMouseEnter={() => onHover(card)}
|
|
||||||
onMouseLeave={() => onHover(null)}
|
|
||||||
onTouchStart={onTouchStart}
|
|
||||||
onTouchEnd={onTouchEnd}
|
|
||||||
onTouchMove={onTouchMove}
|
|
||||||
className="relative group bg-slate-900 rounded-lg shrink-0 cursor-pointer hover:scale-105 transition-transform"
|
|
||||||
>
|
|
||||||
<div className={`relative ${useArtCrop ? 'aspect-square' : 'aspect-[2.5/3.5]'} overflow-hidden rounded-lg shadow-xl border transition-all duration-200 group-hover:ring-2 group-hover:ring-purple-400 group-hover:shadow-purple-500/30 ${isFoil ? 'border-purple-400 shadow-purple-500/20' : 'border-slate-800'}`}>
|
|
||||||
{isFoil && <FoilOverlay />}
|
|
||||||
{isFoil && <div className="absolute top-1 right-1 z-30 text-[10px] font-bold text-white bg-purple-600/80 px-1.5 rounded backdrop-blur-sm">FOIL</div>}
|
|
||||||
{displayImage ? (
|
|
||||||
<img src={displayImage} alt={card.name} className="w-full h-full object-cover" draggable={false} />
|
|
||||||
) : (
|
|
||||||
<div className="w-full h-full flex items-center justify-center text-xs text-center p-1 text-slate-500 font-bold border-2 border-slate-700 m-1 rounded">{card.name}</div>
|
|
||||||
)}
|
|
||||||
<div className={`absolute bottom-0 left-0 right-0 h-1.5 ${card.rarity === 'mythic' ? 'bg-gradient-to-r from-orange-500 to-red-600' : card.rarity === 'rare' ? 'bg-gradient-to-r from-yellow-400 to-yellow-600' : card.rarity === 'uncommon' ? 'bg-gradient-to-r from-gray-300 to-gray-500' : 'bg-black'}`} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -77,7 +77,10 @@ export const MulliganView: React.FC<MulliganViewProps> = ({ hand, mulliganCount,
|
|||||||
{/* Controls */}
|
{/* Controls */}
|
||||||
<div className="flex gap-8">
|
<div className="flex gap-8">
|
||||||
<button
|
<button
|
||||||
onClick={() => onDecision(false, [])}
|
onClick={() => {
|
||||||
|
console.log("Mulligan Clicked");
|
||||||
|
onDecision(false, []);
|
||||||
|
}}
|
||||||
className="px-8 py-4 bg-red-600/20 hover:bg-red-600/40 border border-red-500 text-red-100 rounded-xl font-bold text-lg transition-all flex flex-col items-center gap-1 group"
|
className="px-8 py-4 bg-red-600/20 hover:bg-red-600/40 border border-red-500 text-red-100 rounded-xl font-bold text-lg transition-all flex flex-col items-center gap-1 group"
|
||||||
>
|
>
|
||||||
<span>Mulligan</span>
|
<span>Mulligan</span>
|
||||||
@@ -85,7 +88,12 @@ export const MulliganView: React.FC<MulliganViewProps> = ({ hand, mulliganCount,
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => isSelectionValid && onDecision(true, Array.from(selectedToBottom))}
|
onClick={() => {
|
||||||
|
if (isSelectionValid) {
|
||||||
|
console.log("Keep Hand Clicked", Array.from(selectedToBottom));
|
||||||
|
onDecision(true, Array.from(selectedToBottom));
|
||||||
|
}
|
||||||
|
}}
|
||||||
disabled={!isSelectionValid}
|
disabled={!isSelectionValid}
|
||||||
className={`px-8 py-4 rounded-xl font-bold text-lg transition-all flex flex-col items-center gap-1 min-w-[200px] ${isSelectionValid
|
className={`px-8 py-4 rounded-xl font-bold text-lg transition-all flex flex-col items-center gap-1 min-w-[200px] ${isSelectionValid
|
||||||
? 'bg-emerald-600 hover:bg-emerald-500 text-white shadow-[0_0_20px_rgba(16,185,129,0.4)]'
|
? 'bg-emerald-600 hover:bg-emerald-500 text-white shadow-[0_0_20px_rgba(16,185,129,0.4)]'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { GameState, Phase, Step } from '../../types/game';
|
import { GameState, Phase, Step } from '../../types/game';
|
||||||
import { ManaIcon } from '../../components/ManaIcon';
|
import { ManaIcon } from '../../components/ManaIcon';
|
||||||
import { Shield, Swords, Hourglass, Zap, Hand, ChevronRight, XCircle, Play, Clock, Files, Crosshair, Skull, Flag, Moon, Trash2 } from 'lucide-react';
|
import { Shield, Swords, Hourglass, Zap, Hand, ChevronRight, XCircle, Clock, Files, Crosshair, Skull, Flag, Moon, Trash2 } from 'lucide-react';
|
||||||
|
|
||||||
interface PhaseStripProps {
|
interface PhaseStripProps {
|
||||||
gameState: GameState;
|
gameState: GameState;
|
||||||
@@ -75,7 +75,7 @@ export const PhaseStrip: React.FC<PhaseStripProps> = ({
|
|||||||
else actionLabel = "Pass";
|
else actionLabel = "Pass";
|
||||||
} else {
|
} else {
|
||||||
// Resolve
|
// Resolve
|
||||||
const topItem = gameState.stack![gameState.stack!.length - 1];
|
// const topItem = gameState.stack![gameState.stack!.length - 1]; // Unused
|
||||||
actionLabel = "Resolve";
|
actionLabel = "Resolve";
|
||||||
actionType = 'PASS_PRIORITY';
|
actionType = 'PASS_PRIORITY';
|
||||||
ActionIcon = Zap;
|
ActionIcon = Zap;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Modal } from '../../components/Modal';
|
|||||||
import { useToast } from '../../components/Toast';
|
import { useToast } from '../../components/Toast';
|
||||||
import { GameView } from '../game/GameView';
|
import { GameView } from '../game/GameView';
|
||||||
import { DraftView } from '../draft/DraftView';
|
import { DraftView } from '../draft/DraftView';
|
||||||
|
import { TournamentManager as TournamentView } from '../tournament/TournamentManager';
|
||||||
import { DeckBuilderView } from '../draft/DeckBuilderView';
|
import { DeckBuilderView } from '../draft/DeckBuilderView';
|
||||||
|
|
||||||
interface Player {
|
interface Player {
|
||||||
@@ -71,6 +72,8 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
|
|||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
const [gameState, setGameState] = useState<any>(initialGameState || null);
|
const [gameState, setGameState] = useState<any>(initialGameState || null);
|
||||||
const [draftState, setDraftState] = useState<any>(initialDraftState || null);
|
const [draftState, setDraftState] = useState<any>(initialDraftState || null);
|
||||||
|
const [tournamentState, setTournamentState] = useState<any>((initialRoom as any).tournament || null);
|
||||||
|
const [preparingMatchId, setPreparingMatchId] = useState<string | null>(null);
|
||||||
const [mobileTab, setMobileTab] = useState<'game' | 'chat'>('game'); // Keep for mobile
|
const [mobileTab, setMobileTab] = useState<'game' | 'chat'>('game'); // Keep for mobile
|
||||||
|
|
||||||
// Derived State
|
// Derived State
|
||||||
@@ -180,14 +183,32 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
|
|||||||
setGameState(data);
|
setGameState(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleTournamentUpdate = (data: any) => {
|
||||||
|
setTournamentState(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Also handle finish
|
||||||
|
const handleTournamentFinished = (data: any) => {
|
||||||
|
showToast(`Tournament Winner: ${data.winner.name}!`, 'success');
|
||||||
|
};
|
||||||
|
|
||||||
socket.on('draft_update', handleDraftUpdate);
|
socket.on('draft_update', handleDraftUpdate);
|
||||||
socket.on('draft_error', handleDraftError);
|
socket.on('draft_error', handleDraftError);
|
||||||
socket.on('game_update', handleGameUpdate);
|
socket.on('game_update', handleGameUpdate);
|
||||||
|
socket.on('tournament_update', handleTournamentUpdate);
|
||||||
|
socket.on('tournament_finished', handleTournamentFinished);
|
||||||
|
|
||||||
|
socket.on('match_start', () => {
|
||||||
|
setPreparingMatchId(null);
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
socket.off('draft_update', handleDraftUpdate);
|
socket.off('draft_update', handleDraftUpdate);
|
||||||
socket.off('draft_error', handleDraftError);
|
socket.off('draft_error', handleDraftError);
|
||||||
socket.off('game_update', handleGameUpdate);
|
socket.off('game_update', handleGameUpdate);
|
||||||
|
socket.off('tournament_update', handleTournamentUpdate);
|
||||||
|
socket.off('tournament_finished', handleTournamentFinished);
|
||||||
|
socket.off('match_start');
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -271,6 +292,29 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
|
|||||||
return <DeckBuilderView roomId={room.id} currentPlayerId={currentPlayerId} initialPool={myPool} availableBasicLands={room.basicLands} />;
|
return <DeckBuilderView roomId={room.id} currentPlayerId={currentPlayerId} initialPool={myPool} availableBasicLands={room.basicLands} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (room.status === 'tournament' && tournamentState) {
|
||||||
|
if (preparingMatchId) {
|
||||||
|
const myTournamentPlayer = tournamentState.players.find((p: any) => p.id === currentPlayerId);
|
||||||
|
const myPool = draftState?.players[currentPlayerId]?.pool || [];
|
||||||
|
const myDeck = myTournamentPlayer?.deck || [];
|
||||||
|
|
||||||
|
return <DeckBuilderView
|
||||||
|
roomId={room.id}
|
||||||
|
currentPlayerId={currentPlayerId}
|
||||||
|
initialPool={myPool}
|
||||||
|
initialDeck={myDeck}
|
||||||
|
availableBasicLands={room.basicLands}
|
||||||
|
onSubmit={(deck) => {
|
||||||
|
socketService.socket.emit('match_ready', { matchId: preparingMatchId, deck });
|
||||||
|
setPreparingMatchId(null);
|
||||||
|
showToast("Deck ready! Waiting for game to start...", 'success');
|
||||||
|
}}
|
||||||
|
submitLabel="Ready for Match"
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
return <TournamentView tournament={tournamentState} currentPlayerId={currentPlayerId} onJoinMatch={setPreparingMatchId} />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 bg-slate-800 rounded-xl p-6 border border-slate-700 shadow-xl flex flex-col items-center justify-center">
|
<div className="flex-1 bg-slate-800 rounded-xl p-6 border border-slate-700 shadow-xl flex flex-col items-center justify-center">
|
||||||
<h2 className="text-3xl font-bold text-white mb-4">Waiting for Players...</h2>
|
<h2 className="text-3xl font-bold text-white mb-4">Waiting for Players...</h2>
|
||||||
|
|||||||
@@ -1,97 +1,119 @@
|
|||||||
import React, { useState } from 'react';
|
import React from 'react';
|
||||||
import { Users } from 'lucide-react';
|
import { Trophy, Play } from 'lucide-react';
|
||||||
import { useToast } from '../../components/Toast';
|
import { socketService } from '../../services/SocketService';
|
||||||
|
|
||||||
|
interface TournamentPlayer {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
isBot: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
interface Match {
|
interface Match {
|
||||||
id: number;
|
id: string;
|
||||||
p1: string;
|
round: number;
|
||||||
p2: string;
|
matchIndex: number;
|
||||||
|
player1: TournamentPlayer | null;
|
||||||
|
player2: TournamentPlayer | null;
|
||||||
|
winnerId?: string;
|
||||||
|
status: 'pending' | 'ready' | 'in_progress' | 'finished';
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Bracket {
|
interface Tournament {
|
||||||
round1: Match[];
|
id: string;
|
||||||
totalPlayers: number;
|
players: TournamentPlayer[];
|
||||||
|
rounds: Match[][];
|
||||||
|
currentRound: number;
|
||||||
|
status: 'setup' | 'active' | 'finished';
|
||||||
|
winner?: TournamentPlayer;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TournamentManager: React.FC = () => {
|
interface TournamentManagerProps {
|
||||||
const [playerInput, setPlayerInput] = useState('');
|
tournament: Tournament;
|
||||||
const [bracket, setBracket] = useState<Bracket | null>(null);
|
currentPlayerId: string;
|
||||||
const { showToast } = useToast();
|
onJoinMatch: (matchId: string) => void;
|
||||||
|
|
||||||
const shuffleArray = (array: any[]) => {
|
|
||||||
let currentIndex = array.length, randomIndex;
|
|
||||||
const newArray = [...array];
|
|
||||||
while (currentIndex !== 0) {
|
|
||||||
randomIndex = Math.floor(Math.random() * currentIndex);
|
|
||||||
currentIndex--;
|
|
||||||
[newArray[currentIndex], newArray[randomIndex]] = [newArray[randomIndex], newArray[currentIndex]];
|
|
||||||
}
|
|
||||||
return newArray;
|
|
||||||
};
|
|
||||||
|
|
||||||
const generateBracket = () => {
|
|
||||||
if (!playerInput.trim()) return;
|
|
||||||
const names = playerInput.split('\n').filter(n => n.trim() !== '').map(n => n.trim());
|
|
||||||
if (names.length < 2) {
|
|
||||||
showToast("Enter at least 2 players.", 'error');
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const shuffled = shuffleArray(names);
|
export const TournamentManager: React.FC<TournamentManagerProps> = ({ tournament, currentPlayerId, onJoinMatch }) => {
|
||||||
const nextPowerOf2 = Math.pow(2, Math.ceil(Math.log2(shuffled.length)));
|
const { rounds, winner } = tournament;
|
||||||
const byesNeeded = nextPowerOf2 - shuffled.length;
|
|
||||||
|
|
||||||
const fullRoster = [...shuffled];
|
const handleJoinMatch = (matchId: string) => {
|
||||||
for (let i = 0; i < byesNeeded; i++) fullRoster.push("BYE");
|
socketService.socket.emit('join_match', { matchId }, (response: any) => {
|
||||||
|
if (!response.success) {
|
||||||
const pairings: Match[] = [];
|
console.error(response.message);
|
||||||
for (let i = 0; i < fullRoster.length; i += 2) {
|
// Ideally show toast
|
||||||
pairings.push({ id: i, p1: fullRoster[i], p2: fullRoster[i + 1] });
|
alert(response.message); // Fallback
|
||||||
|
} else {
|
||||||
|
onJoinMatch(matchId);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
setBracket({ round1: pairings, totalPlayers: names.length });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full overflow-y-auto max-w-4xl mx-auto p-4 md:p-6">
|
<div className="h-full overflow-y-auto max-w-6xl mx-auto p-4 md:p-6 text-slate-100">
|
||||||
<div className="bg-slate-800 rounded-xl p-6 border border-slate-700 shadow-xl mb-8">
|
|
||||||
<h2 className="text-xl font-bold text-white mb-4 flex items-center gap-2">
|
{/* Header */}
|
||||||
<Users className="w-5 h-5 text-blue-400" /> Players
|
<div className="bg-slate-800 rounded-xl p-6 border border-slate-700 shadow-xl mb-8 flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-bold text-white flex items-center gap-2">
|
||||||
|
<Trophy className="w-6 h-6 text-yellow-500" /> Tournament Bracket
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-sm text-slate-400 mb-2">Enter one name per line</p>
|
<p className="text-slate-400 text-sm mt-1">Round {tournament.currentRound}</p>
|
||||||
<textarea
|
</div>
|
||||||
className="w-full h-32 bg-slate-900 border border-slate-700 rounded-lg p-3 text-sm text-slate-300 focus:ring-2 focus:ring-blue-500 outline-none resize-none mb-4"
|
{winner && (
|
||||||
placeholder={`Player 1\nPlayer 2...`}
|
<div className="bg-yellow-500/20 border border-yellow-500 text-yellow-200 px-6 py-3 rounded-xl flex items-center gap-3 animate-pulse">
|
||||||
value={playerInput}
|
<Trophy className="w-8 h-8" />
|
||||||
onChange={(e) => setPlayerInput(e.target.value)}
|
<div>
|
||||||
/>
|
<div className="text-xs uppercase font-bold tracking-wider">Winner</div>
|
||||||
<button
|
<div className="text-xl font-bold">{winner.name}</div>
|
||||||
onClick={generateBracket}
|
</div>
|
||||||
className="bg-blue-600 hover:bg-blue-500 text-white px-6 py-2 rounded-lg font-bold w-full md:w-auto transition-colors"
|
</div>
|
||||||
>
|
)}
|
||||||
Generate Bracket
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{bracket && (
|
<div className="flex gap-8 overflow-x-auto pb-8 snap-x">
|
||||||
<div className="bg-slate-800 rounded-xl p-6 border border-slate-700 shadow-xl overflow-x-auto">
|
{rounds.map((roundMatches, roundIndex) => (
|
||||||
<h3 className="text-lg font-bold text-white mb-6 border-b border-slate-700 pb-2">Round 1 (Single Elimination)</h3>
|
<div key={roundIndex} className="flex flex-col justify-center gap-16 min-w-[280px] snap-center">
|
||||||
<div className="flex flex-col gap-4 min-w-[300px]">
|
<h3 className="text-center font-bold text-slate-500 uppercase tracking-widest text-sm mb-4">
|
||||||
{bracket.round1.map((match, i) => (
|
{roundIndex === rounds.length - 1 ? "Finals" : `Round ${roundIndex + 1}`}
|
||||||
<div key={i} className="bg-slate-900 border border-slate-700 rounded-lg p-4 flex flex-col gap-2 relative">
|
</h3>
|
||||||
<div className="absolute -left-3 top-1/2 w-3 h-px bg-slate-600"></div>
|
<div className="flex flex-col gap-8 justify-center flex-1">
|
||||||
<div className="flex justify-between items-center bg-slate-800/50 p-2 rounded border border-slate-700/50">
|
{roundMatches.map((match) => {
|
||||||
<span className={match.p1 === 'BYE' ? 'text-slate-500 italic' : 'font-bold text-white'}>{match.p1}</span>
|
const isMyMatch = (match.player1?.id === currentPlayerId || match.player2?.id === currentPlayerId);
|
||||||
|
const isPlayable = isMyMatch && match.status === 'ready' && !match.winnerId;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={match.id} className={`bg-slate-900 border ${isMyMatch ? 'border-blue-500 ring-1 ring-blue-500/50' : 'border-slate-700'} rounded-lg p-0 overflow-hidden relative shadow-lg`}>
|
||||||
|
{/* Status Indicator */}
|
||||||
|
{match.status === 'in_progress' && <div className="absolute top-0 right-0 bg-green-500 text-xs text-black font-bold px-2 py-0.5">LIVE</div>}
|
||||||
|
|
||||||
|
<div className={`p-3 border-b border-slate-800 flex justify-between items-center ${match.winnerId === match.player1?.id ? 'bg-emerald-900/30' : ''}`}>
|
||||||
|
<span className={match.player1 ? 'font-bold' : 'text-slate-600 italic'}>
|
||||||
|
{match.player1 ? match.player1.name : 'Waiting...'}
|
||||||
|
</span>
|
||||||
|
{match.winnerId === match.player1?.id && <Trophy className="w-4 h-4 text-emerald-500" />}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-center text-slate-500">VS</div>
|
<div className={`p-3 flex justify-between items-center ${match.winnerId === match.player2?.id ? 'bg-emerald-900/30' : ''}`}>
|
||||||
<div className="flex justify-between items-center bg-slate-800/50 p-2 rounded border border-slate-700/50">
|
<span className={match.player2 ? 'font-bold' : 'text-slate-600 italic'}>
|
||||||
<span className={match.p2 === 'BYE' ? 'text-slate-500 italic' : 'font-bold text-white'}>{match.p2}</span>
|
{match.player2 ? match.player2.name : 'Waiting...'}
|
||||||
|
</span>
|
||||||
|
{match.winnerId === match.player2?.id && <Trophy className="w-4 h-4 text-emerald-500" />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isPlayable && (
|
||||||
|
<button
|
||||||
|
onClick={() => handleJoinMatch(match.id)}
|
||||||
|
className="w-full bg-blue-600 hover:bg-blue-500 text-white font-bold py-2 flex items-center justify-center gap-2 transition-colors"
|
||||||
|
>
|
||||||
|
<Play className="w-4 h-4" /> Play Match
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,6 +11,13 @@ class SocketService {
|
|||||||
this.socket = io(URL, {
|
this.socket = io(URL, {
|
||||||
autoConnect: false
|
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() {
|
connect() {
|
||||||
|
|||||||
@@ -429,10 +429,15 @@ export class RulesEngine {
|
|||||||
|
|
||||||
// 0. Mulligan Step
|
// 0. Mulligan Step
|
||||||
if (step === 'mulligan') {
|
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
|
// Draw 7 for everyone if they have 0 cards in hand and haven't kept
|
||||||
Object.values(this.state.players).forEach(p => {
|
Object.values(this.state.players).forEach(p => {
|
||||||
const hand = Object.values(this.state.cards).filter(c => c.ownerId === p.id && c.zone === 'hand');
|
const hand = Object.values(this.state.cards).filter(c => c.ownerId === p.id && c.zone === 'hand');
|
||||||
if (hand.length === 0 && !p.handKept) {
|
if (hand.length === 0 && !p.handKept) {
|
||||||
|
console.log(`[RulesEngine] Initial Draw 7 for ${p.name}`);
|
||||||
// Initial Draw
|
// Initial Draw
|
||||||
for (let i = 0; i < 7; i++) {
|
for (let i = 0; i < 7; i++) {
|
||||||
this.drawCard(p.id);
|
this.drawCard(p.id);
|
||||||
@@ -442,10 +447,12 @@ export class RulesEngine {
|
|||||||
// Check if all kept
|
// Check if all kept
|
||||||
const allKept = Object.values(this.state.players).every(p => p.handKept);
|
const allKept = Object.values(this.state.players).every(p => p.handKept);
|
||||||
if (allKept) {
|
if (allKept) {
|
||||||
console.log("All players kept hand. Starting game.");
|
console.log("[RulesEngine] All players kept hand. Advancing Step.");
|
||||||
// Normally untap is automatic?
|
// Normally untap is automatic?
|
||||||
// advanceStep will go to beginning/untap
|
// advanceStep will go to beginning/untap
|
||||||
this.advanceStep();
|
this.advanceStep();
|
||||||
|
} else {
|
||||||
|
console.log("[RulesEngine] Waiting for more mulligan decisions.");
|
||||||
}
|
}
|
||||||
return; // Wait for actions
|
return; // Wait for actions
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { fileURLToPath } from 'url';
|
|||||||
import { RoomManager } from './managers/RoomManager';
|
import { RoomManager } from './managers/RoomManager';
|
||||||
import { GameManager } from './managers/GameManager';
|
import { GameManager } from './managers/GameManager';
|
||||||
import { DraftManager } from './managers/DraftManager';
|
import { DraftManager } from './managers/DraftManager';
|
||||||
|
import { TournamentManager } from './managers/TournamentManager';
|
||||||
import { CardService } from './services/CardService';
|
import { CardService } from './services/CardService';
|
||||||
import { ScryfallService } from './services/ScryfallService';
|
import { ScryfallService } from './services/ScryfallService';
|
||||||
import { PackGeneratorService } from './services/PackGeneratorService';
|
import { PackGeneratorService } from './services/PackGeneratorService';
|
||||||
@@ -31,8 +32,35 @@ const io = new Server(httpServer, {
|
|||||||
const roomManager = new RoomManager();
|
const roomManager = new RoomManager();
|
||||||
const gameManager = new GameManager();
|
const gameManager = new GameManager();
|
||||||
const draftManager = new DraftManager();
|
const draftManager = new DraftManager();
|
||||||
|
const tournamentManager = new TournamentManager();
|
||||||
const persistenceManager = new PersistenceManager(roomManager, draftManager, gameManager);
|
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
|
// Load previous state
|
||||||
persistenceManager.load();
|
persistenceManager.load();
|
||||||
|
|
||||||
@@ -281,40 +309,20 @@ const draftInterval = setInterval(() => {
|
|||||||
// Check if EVERYONE is ready to start game automatically
|
// Check if EVERYONE is ready to start game automatically
|
||||||
const activePlayers = room.players.filter(p => p.role === 'player');
|
const activePlayers = room.players.filter(p => p.role === 'player');
|
||||||
if (activePlayers.length > 0 && activePlayers.every(p => p.ready)) {
|
if (activePlayers.length > 0 && activePlayers.every(p => p.ready)) {
|
||||||
console.log(`All players ready (including bots) in room ${roomId}. Starting game.`);
|
console.log(`All players ready (including bots) in room ${roomId}. Starting TOURNAMENT.`);
|
||||||
room.status = 'playing';
|
room.status = 'tournament';
|
||||||
io.to(roomId).emit('room_update', room);
|
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
|
room.tournament = tournament;
|
||||||
activePlayers.forEach(p => {
|
io.to(roomId).emit('tournament_update', tournament);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -411,7 +419,12 @@ io.on('connection', (socket) => {
|
|||||||
if (currentDraft) socket.emit('draft_update', currentDraft);
|
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 {
|
} else {
|
||||||
callback({ success: false, message: 'Room not found or full' });
|
callback({ success: false, message: 'Room not found or full' });
|
||||||
}
|
}
|
||||||
@@ -451,11 +464,27 @@ io.on('connection', (socket) => {
|
|||||||
if (room.status === 'playing') {
|
if (room.status === 'playing') {
|
||||||
currentGame = gameManager.getGame(roomId);
|
currentGame = gameManager.getGame(roomId);
|
||||||
if (currentGame) socket.emit('game_update', currentGame);
|
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
|
// ACK Callback
|
||||||
if (typeof callback === 'function') {
|
if (typeof callback === 'function') {
|
||||||
callback({ success: true, room, draftState: currentDraft, gameState: currentGame });
|
callback({ success: true, room, draftState: currentDraft, gameState: currentGame, tournament: room.tournament });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Room found but player not in it? Or room not found?
|
// 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);
|
io.to(room.id).emit('room_update', updatedRoom);
|
||||||
const activePlayers = updatedRoom.players.filter(p => p.role === 'player');
|
const activePlayers = updatedRoom.players.filter(p => p.role === 'player');
|
||||||
if (activePlayers.length > 0 && activePlayers.every(p => p.ready)) {
|
if (activePlayers.length > 0 && activePlayers.every(p => p.ready)) {
|
||||||
updatedRoom.status = 'playing';
|
updatedRoom.status = 'tournament';
|
||||||
io.to(room.id).emit('room_update', updatedRoom);
|
io.to(room.id).emit('room_update', updatedRoom);
|
||||||
|
|
||||||
const game = gameManager.createGame(room.id, updatedRoom.players);
|
const tournament = tournamentManager.createTournament(room.id, updatedRoom.players.map(p => ({
|
||||||
activePlayers.forEach(p => {
|
id: p.id,
|
||||||
if (p.deck) {
|
name: p.name,
|
||||||
p.deck.forEach((card: any) => {
|
isBot: !!p.isBot,
|
||||||
gameManager.addCardToGame(room.id, {
|
deck: p.deck
|
||||||
ownerId: p.id,
|
})));
|
||||||
controllerId: p.id,
|
updatedRoom.tournament = tournament;
|
||||||
oracleId: card.oracle_id || card.id,
|
io.to(room.id).emit('tournament_update', tournament);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -714,9 +721,12 @@ io.on('connection', (socket) => {
|
|||||||
if (!context) return;
|
if (!context) return;
|
||||||
const { room, player } = context;
|
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) {
|
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;
|
if (!context) return;
|
||||||
const { room, player } = context;
|
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) {
|
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 { StrictGameState, PlayerState, CardObject } from '../game/types';
|
||||||
import { RulesEngine } from '../game/RulesEngine';
|
import { RulesEngine } from '../game/RulesEngine';
|
||||||
|
|
||||||
export class GameManager {
|
import { EventEmitter } from 'events';
|
||||||
|
|
||||||
|
export class GameManager extends EventEmitter {
|
||||||
public games: Map<string, StrictGameState> = new Map();
|
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
|
// Convert array to map
|
||||||
const playerRecord: Record<string, PlayerState> = {};
|
const playerRecord: Record<string, PlayerState> = {};
|
||||||
@@ -26,7 +28,7 @@ export class GameManager {
|
|||||||
const firstPlayerId = players.length > 0 ? players[0].id : '';
|
const firstPlayerId = players.length > 0 ? players[0].id : '';
|
||||||
|
|
||||||
const gameState: StrictGameState = {
|
const gameState: StrictGameState = {
|
||||||
roomId,
|
roomId: gameId,
|
||||||
players: playerRecord,
|
players: playerRecord,
|
||||||
cards: {}, // Populated later
|
cards: {}, // Populated later
|
||||||
stack: [],
|
stack: [],
|
||||||
@@ -50,7 +52,7 @@ export class GameManager {
|
|||||||
gameState.players[firstPlayerId].isActive = true;
|
gameState.players[firstPlayerId].isActive = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.games.set(roomId, gameState);
|
this.games.set(gameId, gameState);
|
||||||
return 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
|
// Bot Cycle: If priority passed to a bot, or it's a bot's turn to act
|
||||||
const MAX_LOOPS = 50;
|
const MAX_LOOPS = 50;
|
||||||
let loops = 0;
|
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) {
|
while (game.players[game.priorityPlayerId]?.isBot && loops < MAX_LOOPS) {
|
||||||
loops++;
|
loops++;
|
||||||
this.processBotActions(game);
|
this.processBotActions(game);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check Win Condition
|
||||||
|
this.checkWinCondition(game, roomId);
|
||||||
|
|
||||||
return game;
|
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 ---
|
// --- Bot AI Logic ---
|
||||||
private processBotActions(game: StrictGameState) {
|
private processBotActions(game: StrictGameState) {
|
||||||
const engine = new RulesEngine(game);
|
const engine = new RulesEngine(game);
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Tournament } from './TournamentManager';
|
||||||
|
|
||||||
interface Player {
|
interface Player {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -8,6 +10,7 @@ interface Player {
|
|||||||
socketId?: string; // Current or last known socket
|
socketId?: string; // Current or last known socket
|
||||||
isOffline?: boolean;
|
isOffline?: boolean;
|
||||||
isBot?: boolean;
|
isBot?: boolean;
|
||||||
|
matchId?: string; // Current match in tournament
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ChatMessage {
|
interface ChatMessage {
|
||||||
@@ -23,10 +26,11 @@ interface Room {
|
|||||||
players: Player[];
|
players: Player[];
|
||||||
packs: any[]; // Store generated packs (JSON)
|
packs: any[]; // Store generated packs (JSON)
|
||||||
basicLands?: any[];
|
basicLands?: any[];
|
||||||
status: 'waiting' | 'drafting' | 'deck_building' | 'playing' | 'finished';
|
status: 'waiting' | 'drafting' | 'deck_building' | 'playing' | 'finished' | 'tournament';
|
||||||
messages: ChatMessage[];
|
messages: ChatMessage[];
|
||||||
maxPlayers: number;
|
maxPlayers: number;
|
||||||
lastActive: number; // For persistence cleanup
|
lastActive: number; // For persistence cleanup
|
||||||
|
tournament?: Tournament | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RoomManager {
|
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