mlpcardgame/src/mlpccg/draft/session.ts

263 lines
6.8 KiB
TypeScript

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<Session> {
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);
}
}