Skip to content

Commit

Permalink
feat: implement tournament variable event card treatments
Browse files Browse the repository at this point in the history
if treatments for a game's tournament are found, it will cycle
through them evenly based on the last game's treatment for that specific
tournament

- add command for creating treatments
  • Loading branch information
sgfost committed Nov 3, 2023
1 parent b6bb200 commit b56a2b2
Show file tree
Hide file tree
Showing 12 changed files with 185 additions and 16 deletions.
7 changes: 6 additions & 1 deletion scripts/tournament/2023-10/round-1/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,15 @@ CLI_CMD="${1:-cli}" # in prod change to cli:prod e.g., `./setup.sh cli:prod`
## create Fall 2023 Mars Madness tournament + Round 1 structure

yarn ${CLI_CMD} tournament create --tournamentName="2023-11 Mars Madness" --description="The next Mars Madness tournament begins November 10, 2023! Top-scoring players on surviving teams will advance to the next round. Players who make it to the championship round will receive a tabletop game version of Port of Mars, and the winner of the championship will receive a top prize of \$1000 USD!"
# add variable life as usual card treatments to the tournament
# FIXME: change this to the specific treatments we actually want
yarn ${CLI_CMD} tournament treatment create -n "Less LAU" -d "50% less (6) Life as Usual cards" -o "[{\"eventId\": \"lifeAsUsual\", \"quantity\": 6}]"
yarn ${CLI_CMD} tournament treatment create -n "Normal LAU" -d "Default (12) Life as Usual cards" -o "[{\"eventId\": \"lifeAsUsual\", \"quantity\": 12}]"
yarn ${CLI_CMD} tournament treatment create -n "More LAU" -d "50% more (18) Life as Usual cards" -o "[{\"eventId\": \"lifeAsUsual\", \"quantity\": 18}]"
yarn ${CLI_CMD} tournament round create --introSurveyUrl=https://asu.co1.qualtrics.com/jfe/form/SV_0c8tCMZkAUh4V8x --exitSurveyUrl=https://asu.co1.qualtrics.com/jfe/form/SV_6FNhPbsBuybTjEN --announcement="REGISTRATION FOR ROUND 1 IS NOW OPEN. Register, complete the Port of Mars Mission Control onboarding, and sign in during a scheduled launch time to compete in the next Mars Madness tournament!"
# set up 3 launch dates per day from 2023-11-10 to 2023-11-16
for day in 10 11 12 13 14 15 16; do
yarn ${CLI_CMD} tournament round date --date="2023-11-${day}T12:00:00-07:00";
yarn ${CLI_CMD} tournament round date --date="2023-11-${day}T15:00:00-07:00";
yarn ${CLI_CMD} tournament round date --date="2023-11-${day}T19:00:00-07:00";
done
done
53 changes: 52 additions & 1 deletion server/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
} from "@port-of-mars/server/services/replay";
import { DBPersister } from "@port-of-mars/server/services/persistence";
import { EnteredDefeatPhase, EnteredVictoryPhase } from "@port-of-mars/server/rooms/game/events";
import { Phase } from "@port-of-mars/shared/types";
import { MarsEventOverride, Phase } from "@port-of-mars/shared/types";
import { getLogger } from "@port-of-mars/server/settings";
import {
Game,
Expand Down Expand Up @@ -345,6 +345,29 @@ async function exportTournamentRoundEmails(
}
}

async function createTournamentTreatment(
em: EntityManager,
name: string,
description: string,
overridesRaw: string,
tournamentId?: number
): Promise<void> {
const sp = getServices(em);
let overrides: MarsEventOverride[];
try {
overrides = JSON.parse(overridesRaw);
const treatment = await sp.tournament.createTreatment(
name,
description,
overrides,
tournamentId
);
logger.debug("created tournament treatment: %o", treatment);
} catch (e) {
logger.fatal("Unable to create treatment: %s", e);
}
}

async function createTournamentRoundDate(
em: EntityManager,
date: Date,
Expand Down Expand Up @@ -382,6 +405,34 @@ program
program
.createCommand("tournament")
.description("tournamament subcommands")
.addCommand(
program
.createCommand("treatment")
.description("treatment subcommands")
.addCommand(
program
.createCommand("create")
.requiredOption("-n --name <name>", "Treatment name")
.requiredOption("-d --description <description>", "Treatment description")
.requiredOption(
"-o --overrides <overrides>",
"Mars event overrides (JSON string, e.g., '[ {'eventId': 'lifeAsUsual', 'quantity': 12} ])"
)
.option("--tournamentId <tournamentId>", "ID of the tournament", customParseInt)
.description("add a Treatment (set of mars event overrides) to a Tournament")
.action(async cmd => {
await withConnection(em =>
createTournamentTreatment(
em,
cmd.name,
cmd.description,
cmd.overrides,
cmd.tournamentId
)
);
})
)
)
.addCommand(
program
.createCommand("round")
Expand Down
2 changes: 1 addition & 1 deletion server/src/data/MarsEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ const _marsEvents: Array<MarsEventData> = [
},
];

export function getMarsEventDeckItems(): Array<MarsEventDeckItem> {
export function getDefaultMarsEventDeck(): Array<MarsEventDeckItem> {
const AVAILABLE_EVENTS: Array<[string, number]> = [
["audit", 1],
["bondingThroughAdversity", 1],
Expand Down
5 changes: 4 additions & 1 deletion server/src/entity/Game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,8 @@ export class Game {
winner?: Player;

@ManyToOne(type => Treatment, treatment => treatment.games)
treatment!: Treatment;
treatment?: Treatment;

@Column({ nullable: true })
treatmentId?: number;
}
6 changes: 2 additions & 4 deletions server/src/entity/Treatment.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { MarsEventOverride } from "@port-of-mars/shared/types";
import {
OneToMany,
Column,
Expand Down Expand Up @@ -27,10 +28,7 @@ export class Treatment {
tournaments!: Array<Tournament>;

@Column("jsonb", { nullable: true })
marsEventOverrides!: {
eventId: string;
quantity: number;
}[];
marsEventOverrides!: MarsEventOverride[];

@OneToMany(type => Game, game => game.treatment, { nullable: true })
games!: Array<Game>;
Expand Down
1 change: 1 addition & 0 deletions server/src/entity/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export * from "./QuestionResponse";
export * from "./Quiz";
export * from "./QuizSubmission";
export * from "./Tournament";
export * from "./Treatment";
export * from "./TournamentRound";
export * from "./TournamentRoundInvite";
export * from "./User";
Expand Down
28 changes: 22 additions & 6 deletions server/src/rooms/game/state/marsevents/common.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { MarsEventData } from "@port-of-mars/shared/types";
import { MarsEventData, MarsEventOverride } from "@port-of-mars/shared/types";
import _ from "lodash";
import { GameState } from "@port-of-mars/server/rooms/game/state";
import { MarsEventDeckItem, getMarsEventDeckItems } from "@port-of-mars/server/data/MarsEvents";
import { MarsEventDeckItem, getDefaultMarsEventDeck } from "@port-of-mars/server/data/MarsEvents";

export interface MarsEventStateConstructor {
new (data?: any): MarsEventState;
Expand All @@ -20,12 +20,28 @@ export function expandCopies(marsEventsCollection: Array<MarsEventDeckItem>): Ar
);
}

export function getFixedMarsEventDeck(): Array<MarsEventData> {
return _.clone(expandCopies(getMarsEventDeckItems()));
/**
* Get a fixed (unshuffled) mars event deck with the given overrides
*/
export function getFixedMarsEventDeck(
eventOverrides?: MarsEventOverride[] | null
): Array<MarsEventData> {
const deck = getDefaultMarsEventDeck();
if (eventOverrides) {
for (const override of eventOverrides) {
const index = deck.findIndex(item => item.event.id === override.eventId);
if (index !== -1) {
deck[index].numberOfCopies = override.quantity;
}
}
}
return _.clone(expandCopies(deck));
}

export function getRandomizedMarsEventDeck(): Array<MarsEventData> {
return _.shuffle(getFixedMarsEventDeck());
export function getRandomizedMarsEventDeck(
eventOverrides: MarsEventOverride[] | null
): Array<MarsEventData> {
return _.shuffle(getFixedMarsEventDeck(eventOverrides));
}

// eslint-disable-next-line @typescript-eslint/ban-types
Expand Down
1 change: 1 addition & 0 deletions server/src/rooms/game/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export interface Persister {

export interface GameOpts extends GameStateOpts {
tournamentRoundId: number;
treatmentId?: number;
}

export interface GameStateOpts {
Expand Down
5 changes: 4 additions & 1 deletion server/src/services/persistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,11 @@ export class DBPersister implements Persister {
game.tournamentRoundId = options.tournamentRoundId;
game.roomId = roomId;
game.type = options.type;

if (options.treatmentId) {
game.treatmentId = options.treatmentId;
}
await em.save(game);

if (shouldCreatePlayers) {
const rawUsers = await this.selectUsersByUsername(em, Object.keys(options.userRoles));
await this.createPlayers(em, game.id, options.userRoles, rawUsers);
Expand Down
83 changes: 83 additions & 0 deletions server/src/services/tournament.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {
Tournament,
TournamentRound,
TournamentRoundInvite,
Treatment,
Game,
} from "@port-of-mars/server/entity";
import { MoreThan, Not, SelectQueryBuilder } from "typeorm";
import { getServices } from "@port-of-mars/server/services";
Expand All @@ -12,6 +14,7 @@ import { BaseService } from "@port-of-mars/server/services/db";
import { TournamentRoundDate } from "@port-of-mars/server/entity/TournamentRoundDate";
import {
GameType,
MarsEventOverride,
TournamentRoundInviteStatus,
TournamentStatus,
} from "@port-of-mars/shared/types";
Expand Down Expand Up @@ -244,6 +247,86 @@ export class TournamentService extends BaseService {
};
}

/**
* Create a treatment and add it to the given or current tournament
*/
async createTreatment(
name: string,
description: string,
overrides: MarsEventOverride[],
tournamentId?: number
) {
let treatment = new Treatment();
treatment.name = name;
treatment.description = description;
treatment.marsEventOverrides = overrides;
treatment = await this.em.getRepository(Treatment).save(treatment);

let tournament;
if (!tournamentId) {
tournament = await this.getActiveTournament();
} else {
tournament = await this.em.getRepository(Tournament).findOne({
where: { id: tournamentId },
relations: ["treatments"],
});
if (!tournament) {
throw new Error(`Tournament with ID ${tournamentId} not found`);
}
}
if (!tournament.treatments) {
tournament.treatments = [];
}
tournament.treatments.push(treatment);
await this.em.getRepository(Tournament).save(tournament);

return treatment;
}

/**
* Retrieve the set of treatments for a given tournament
*/
async getTreatments(tournamentId: number): Promise<Treatment[]> {
const tournament = await this.em
.getRepository(Tournament)
.createQueryBuilder("tournament")
.leftJoinAndSelect("tournament.treatments", "treatment")
.where("tournament.id = :tournamentId", { tournamentId })
.getOne();

return tournament ? tournament.treatments : [];
}

/**
* Retrieve the next treatment to use for a given tournament
* Treatments are cycled through in order
*/
async getNextTreatment(tournamentId: number): Promise<Treatment | null> {
const treatments = await this.getTreatments(tournamentId);
if (treatments.length === 0) {
return null;
}
// get the last game played in the tournament
const lastGame = await this.em
.getRepository(Game)
.createQueryBuilder("game")
.select("game.treatmentId")
.innerJoin("game.tournamentRound", "tournamentRound")
.where("tournamentRound.tournamentId = :tournamentId", { tournamentId })
.orderBy("game.id", "DESC")
.getOne();

// determine the next treatment by cycling through the list
let nextTreatment;
if (lastGame && lastGame.treatmentId) {
const lastTreatmentIndex = treatments.findIndex(t => t.id === lastGame.treatmentId);
nextTreatment = treatments[(lastTreatmentIndex + 1) % treatments.length];
} else {
[nextTreatment] = treatments; // start with the first if no games played yet
}
return nextTreatment;
}

async createRound(
data: Pick<
TournamentRound,
Expand Down
5 changes: 4 additions & 1 deletion server/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,12 +110,15 @@ export async function buildGameOpts(usernames: Array<string>, type: GameType): P
playerData.forEach((p, i) => {
playerOpts.set(shuffledRoles[i], p);
});
const treatment = await services.tournament.getNextTreatment(currentTournamentRound.tournamentId);
const eventOverrides = treatment?.marsEventOverrides ?? null;
return {
userRoles: _.zipObject(usernames, shuffledRoles),
playerOpts,
deck: getRandomizedMarsEventDeck(),
deck: getRandomizedMarsEventDeck(eventOverrides),
numberOfGameRounds: currentTournamentRound.numberOfGameRounds,
tournamentRoundId: currentTournamentRound.id,
treatmentId: treatment?.id,
type,
};
}
Expand Down
5 changes: 5 additions & 0 deletions shared/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,11 @@ export interface MarsEventData {
timeDuration?: number;
}

export interface MarsEventOverride {
eventId: string;
quantity: number;
}

export enum MarsLogCategory {
audit = "AUDIT",
newRound = "NEW ROUND",
Expand Down

0 comments on commit b56a2b2

Please sign in to comment.