feat: Implement draft and game phases with client views, dedicated managers, and server-side card image caching.
This commit is contained in:
166
src/server/managers/DraftManager.ts
Normal file
166
src/server/managers/DraftManager.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
interface Card {
|
||||
id: string; // instanceid or scryfall id
|
||||
name: string;
|
||||
image_uris?: { normal: string };
|
||||
card_faces?: { image_uris: { normal: string } }[];
|
||||
// ... other props
|
||||
}
|
||||
|
||||
interface Pack {
|
||||
id: string;
|
||||
cards: Card[];
|
||||
}
|
||||
|
||||
interface DraftState {
|
||||
roomId: string;
|
||||
seats: string[]; // PlayerIDs in seating order
|
||||
packNumber: number; // 1, 2, 3
|
||||
|
||||
// State per player
|
||||
players: Record<string, {
|
||||
id: string;
|
||||
queue: Pack[]; // Packs passed to this player waiting to be viewed
|
||||
activePack: Pack | null; // The pack currently being looked at
|
||||
pool: Card[]; // Picked cards
|
||||
unopenedPacks: Pack[]; // Pack 2 and 3 kept aside
|
||||
isWaiting: boolean; // True if finished current pack round
|
||||
}>;
|
||||
|
||||
status: 'drafting' | 'deck_building' | 'complete';
|
||||
startTime?: number; // For timer
|
||||
}
|
||||
|
||||
export class DraftManager extends EventEmitter {
|
||||
private drafts: Map<string, DraftState> = new Map();
|
||||
|
||||
createDraft(roomId: string, players: string[], allPacks: Pack[]): DraftState {
|
||||
// Distribute 3 packs to each player
|
||||
const numPlayers = players.length;
|
||||
// Assume allPacks contains (3 * numPlayers) packs
|
||||
|
||||
// Shuffle packs just in case (optional, but good practice)
|
||||
const shuffledPacks = [...allPacks].sort(() => Math.random() - 0.5);
|
||||
|
||||
const draftState: DraftState = {
|
||||
roomId,
|
||||
seats: players, // Assume order is randomized or fixed
|
||||
packNumber: 1,
|
||||
players: {},
|
||||
status: 'drafting',
|
||||
startTime: Date.now()
|
||||
};
|
||||
|
||||
players.forEach((pid, index) => {
|
||||
const playerPacks = shuffledPacks.slice(index * 3, (index + 1) * 3);
|
||||
const firstPack = playerPacks.shift(); // Open Pack 1 immediately
|
||||
|
||||
draftState.players[pid] = {
|
||||
id: pid,
|
||||
queue: [],
|
||||
activePack: firstPack || null,
|
||||
pool: [],
|
||||
unopenedPacks: playerPacks,
|
||||
isWaiting: false
|
||||
};
|
||||
});
|
||||
|
||||
this.drafts.set(roomId, draftState);
|
||||
return draftState;
|
||||
}
|
||||
|
||||
getDraft(roomId: string): DraftState | undefined {
|
||||
return this.drafts.get(roomId);
|
||||
}
|
||||
|
||||
pickCard(roomId: string, playerId: string, cardId: string): DraftState | null {
|
||||
const draft = this.drafts.get(roomId);
|
||||
if (!draft) return null;
|
||||
|
||||
const playerState = draft.players[playerId];
|
||||
if (!playerState || !playerState.activePack) return null;
|
||||
|
||||
// Find card
|
||||
const cardIndex = playerState.activePack.cards.findIndex(c => c.id === cardId || (c as any).uniqueId === cardId);
|
||||
// uniqueId check implies if cards have unique instance IDs in pack, if not we rely on strict equality or assume 1 instance per pack
|
||||
|
||||
// Fallback: If we can't find by ID (if Scryfall ID generic), just pick the first matching ID?
|
||||
// We should ideally assume the frontend sends the exact card object or unique index.
|
||||
// For now assuming cardId is unique enough or we pick first match.
|
||||
// Better: In a draft, a pack might have 2 duplicates. We need index or unique ID.
|
||||
// Let's assume the pack generation gave unique IDs or we just pick by index.
|
||||
// I'll stick to ID for now, assuming unique.
|
||||
|
||||
const card = playerState.activePack.cards.find(c => c.id === cardId);
|
||||
if (!card) return null;
|
||||
|
||||
// 1. Add to pool
|
||||
playerState.pool.push(card);
|
||||
|
||||
// 2. Remove from pack
|
||||
playerState.activePack.cards = playerState.activePack.cards.filter(c => c !== card);
|
||||
|
||||
const passedPack = playerState.activePack;
|
||||
playerState.activePack = null;
|
||||
|
||||
// 3. Logic for Passing or Discarding (End of Pack)
|
||||
if (passedPack.cards.length > 0) {
|
||||
// Pass to neighbor
|
||||
const seatIndex = draft.seats.indexOf(playerId);
|
||||
let nextSeatIndex;
|
||||
|
||||
// Pack 1: Left (Increase Index), Pack 2: Right (Decrease), Pack 3: Left
|
||||
if (draft.packNumber === 2) {
|
||||
nextSeatIndex = (seatIndex - 1 + draft.seats.length) % draft.seats.length;
|
||||
} else {
|
||||
nextSeatIndex = (seatIndex + 1) % draft.seats.length;
|
||||
}
|
||||
|
||||
const neighborId = draft.seats[nextSeatIndex];
|
||||
draft.players[neighborId].queue.push(passedPack);
|
||||
|
||||
// Try to assign active pack for neighbor if they are empty
|
||||
this.processQueue(draft, neighborId);
|
||||
} else {
|
||||
// Pack is empty/exhausted
|
||||
playerState.isWaiting = true;
|
||||
this.checkRoundCompletion(draft);
|
||||
}
|
||||
|
||||
// 4. Try to assign new active pack for self from queue
|
||||
this.processQueue(draft, playerId);
|
||||
|
||||
return draft;
|
||||
}
|
||||
|
||||
private processQueue(draft: DraftState, playerId: string) {
|
||||
const p = draft.players[playerId];
|
||||
if (!p.activePack && p.queue.length > 0) {
|
||||
p.activePack = p.queue.shift()!;
|
||||
}
|
||||
}
|
||||
|
||||
private checkRoundCompletion(draft: DraftState) {
|
||||
const allWaiting = Object.values(draft.players).every(p => p.isWaiting);
|
||||
if (allWaiting) {
|
||||
// Start Next Round
|
||||
if (draft.packNumber < 3) {
|
||||
draft.packNumber++;
|
||||
// Open next pack for everyone
|
||||
Object.values(draft.players).forEach(p => {
|
||||
p.isWaiting = false;
|
||||
const nextPack = p.unopenedPacks.shift();
|
||||
if (nextPack) {
|
||||
p.activePack = nextPack;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Draft Complete
|
||||
draft.status = 'deck_building';
|
||||
draft.startTime = Date.now(); // Start deck building timer
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user