import { PackBuilder, Cube, DraftOptions } from "."; import EventEmitter from "eventemitter3"; import { Card } from "@/mlpccg"; import { Pack, Direction } from "./types"; import { DraftProvider } from "./provider"; import { DraftBot } from "./bot"; import { I8PCube } from "./i8pcube"; export class Session extends EventEmitter { private options: DraftOptions; private provider: DraftProvider; private pod: SessionPlayer[] = []; private players: string[] = []; private pending: number[] = []; private assigned: boolean = false; private direction: Direction = "cw"; constructor(options: DraftOptions, provider: DraftProvider) { super(); this.options = options; this.provider = provider; this.pod = new Array(options.players).fill(0).map((x, i) => { const player = new SessionPlayer(provider.getPacks()); player.on("pick", this.picked.bind(this, i)); return player; }); // Populate prev/next references this.pod.forEach((val, i) => { if (i > 0) { val.prev = this.pod[i - 1]; } else { val.prev = this.pod[this.pod.length - 1]; } if (i < this.pod.length - 1) { val.next = this.pod[i + 1]; } else { val.next = this.pod[0]; } }); } public assign( players: string[], assignFn: (name: string, instance: SessionPlayer) => void ) { // Figure out how many players there are vs spots to be filled this.players = players; const spots = this.options.players; const playerNum = players.length; if (playerNum > spots) { throw new Error("too many players in the pod"); } if (playerNum < 1) { throw new Error("not enough players"); } // Place players in the pod switch (this.options.spacing) { case "evenly": { const playerRatio = spots / playerNum; let i = 0; for (const player of players) { const pos = Math.floor(playerRatio * i); this.pod[pos].name = player; assignFn(player, this.pod[pos]); i += 1; } break; } case "randomly": for (const player of players) { const free = [...Array(spots).keys()].filter( i => this.pod[i].name == "" ); const idx = Math.floor(Math.random() * free.length); const chosen = free[idx]; assignFn(player, this.pod[chosen]); } break; } // All the non-assigned places go to bots! this.pod.forEach(p => { if (p.name == "") { p.name = "bot"; const bot = new DraftBot(); bot.assign(p); } }); this.assigned = true; } public start() { if (!this.assigned) { throw new Error("Must assign players first (see assign())"); } this.emit("start", this.order); this.nextPack(); } public get order(): string[] { return this.pod.map(p => p.name); } private picked( playerIndex: number, _card: string, lastPick: boolean, lastPack: boolean ) { if (!this.pending.includes(playerIndex)) { // Uh oh. throw new Error( `unexpected pick: player "${this.pod[playerIndex].name}" already picked their card` ); } const idx = this.pending.indexOf(playerIndex); this.pending.splice(idx, 1); this.emit("player-pick", this.pod[playerIndex].name); // Don't continue unless everyone picked their card if (this.pending.length > 0) { return; } // Was this the last pick for this round of packs? if (lastPick) { // Was the it the last pack? if (lastPack) { this.emit("draft-over"); this.pod.forEach(p => p.emit("your-picks", p.picks)); return; } this.nextPack(); return; } // Pass packs between players for next pick this.resetPending(); this.pod.forEach(p => p.sendPack(this.direction)); this.emit("next-pick"); } private nextPack() { this.resetPending(); this.flipOrder(); this.pod.forEach(p => p.nextPack()); this.emit("next-pack"); } private resetPending() { this.pending = this.pod.map((_, i) => i); } private flipOrder() { if (this.direction == "cw") { this.direction = "ccw"; } else { this.direction = "cw"; } } static async create(options: DraftOptions): Promise { switch (options.source) { case "set": { const factory = await PackBuilder.fromSet(options.set); const provider = DraftProvider.set(factory, options.packs); return new Session(options, provider); } case "block": throw new Error("not implemented"); case "cube": { const cube = await Cube.fromURL(options.url); const factory = new PackBuilder(cube.schema()); const provider = DraftProvider.set(factory, options.packs); return new Session(options, provider); } case "i8pcube": { const cube = await I8PCube.fromURL(options.url); const provider = new DraftProvider(cube.schema()); return new Session(options, provider); } default: throw new Error("Unknown draft source"); } } } export class SessionPlayer extends EventEmitter { public name: string = ""; public currentPack?: Pack; public picks: Pack; public packs: Pack[]; public toSend: Pack | null = null; public next?: SessionPlayer; public prev?: SessionPlayer; public ready: boolean = false; constructor(packs: Pack[]) { super(); this.packs = packs; this.picks = []; } public pick(card: string) { if (!this.ready) { throw new Error("not ready to pick"); } if (!this.currentPack) { throw new Error("no pack to pick from"); } const idx = this.currentPack.findIndex(c => c.ID == card); if (idx < 0) { throw new Error("card not in available picks"); } const pick = this.currentPack.splice(idx, 1); this.picks.push(pick[0]); this.toSend = this.currentPack; this.ready = false; this.emit("pick", card, this.currentPack.length < 1, this.packs.length < 1); } public sendPack(direction: Direction) { if (!this.toSend) { throw new Error("no pack to pass"); } if (this.toSend.length < 1) { throw new Error("empty pack"); } if (direction == "cw") { if (!this.next) { throw new Error("no player to pass cards to"); } this.next.receivePack(this.toSend); } else { if (!this.prev) { throw new Error("no player to pass cards to"); } this.prev.receivePack(this.toSend); } this.toSend = null; } public receivePack(cards: Pack) { this.currentPack = cards; this.ready = true; this.emit("available-picks", cards); } public nextPack() { // Open new pack const newPack = this.packs.shift(); if (!newPack) { throw new Error("no packs left"); } this.receivePack(newPack); } }