263 lines
6.8 KiB
TypeScript
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);
|
|
}
|
|
}
|