diff --git a/common/game/game.ts b/common/game/game.ts index 4852063..476240d 100644 --- a/common/game/game.ts +++ b/common/game/game.ts @@ -22,13 +22,13 @@ type GamePhase = | "assassination" | "game_over"; -type WithVotesFailed = T extends `round:${RoundPhase}` +type WithVotesFailed = T extends "round:team_select" | "round:team_vote" ? { votesFailed: number } : unknown; type WithCurrentTeam = T extends Exclude<`round:${RoundPhase}`, "round:team_select"> - ? { currentTeam: UserId[] } - : unknown; + ? { currentTeam: UserId[] } + : unknown; type WithVotes = T extends "round:mission_reveal" ? { votes: { success: number; fail: number } } : unknown; @@ -42,8 +42,8 @@ type GameState = { currentMission: number; leaderIndex: number; } & WithVotesFailed

& - WithCurrentTeam

& - WithVotes

; + WithCurrentTeam

& + WithVotes

; }[GamePhase]; type GameStateInPhase = GameState & { phase: T }; diff --git a/common/game/roles.ts b/common/game/roles.ts index de529e2..e9cd2b8 100644 --- a/common/game/roles.ts +++ b/common/game/roles.ts @@ -7,35 +7,24 @@ export type Alignment = "good" | "evil"; * Map of alignments to role identifiers (values are used for RoleId) */ export const TEAMS = { - "good": [ - "servant", - "merlin", - "percival", - ], - - "evil": [ - "minion", - "morgana", - "mordred", - "assassin", - "oberon", - ], + good: ["servant", "merlin", "percival"], + evil: ["minion", "morgana", "mordred", "assassin", "oberon"], } as const; +/** + * Describes the identifier of a role given a certain alignment + */ +export type RoleIdOfTeam = (typeof TEAMS)[T][number]; + /** * Describes the identifier of a certain role */ -export type RoleId = (typeof TEAMS)[Alignment][number]; +export type RoleId = (typeof TEAMS)[keyof typeof TEAMS][number]; /** * Describes information and dependencies for all roles */ -export const RoleRelations: { - readonly [role in RoleId]: { - dependencies: readonly RoleId[], - information: readonly RoleId[] - } -} = { +export const ROLE_RELATIONS = { servant: { dependencies: [], information: [], @@ -69,7 +58,29 @@ export const RoleRelations: { dependencies: [], information: [], }, -}; +} as const; + +/** + * Describes the alignment of a role given its identifier + */ +export type AlignmentOf = + TRole extends RoleIdOfTeam<"good"> + ? "good" + : TRole extends RoleIdOfTeam<"evil"> + ? "evil" + : Alignment; + +/** + * Describes the dependencies of a role given its identifier + */ +export type DependenciesOf = + (typeof ROLE_RELATIONS)[TRole]["dependencies"]; + +/** + * Describes the information a role can see given its identifier + */ +export type InformationOf = + (typeof ROLE_RELATIONS)[TRole]["information"]; /** * Describes a single role or set of roles @@ -77,7 +88,9 @@ export const RoleRelations: { * We have to override set operations for proper typing */ export class RoleSet { - public static readonly ALL: RoleSet = new RoleSet(...Object.values(TEAMS).flat()); + public static readonly ALL: RoleSet = new RoleSet( + ...Object.values(TEAMS).flat(), + ); public static readonly NONE: RoleSet = new RoleSet(); public static readonly GOOD: RoleSet = new RoleSet(...TEAMS.good); public static readonly EVIL: RoleSet = new RoleSet(...TEAMS.evil); @@ -135,9 +148,7 @@ export class RoleSet { /** * Filters this set based on a given predicate for RoleData */ - public filter( - predicate: (role_id: RoleId) => boolean, - ): RoleSet { + public filter(predicate: (role_id: RoleId) => boolean): RoleSet { const roles = new Set(); for (const role_id of this.roles) { if (predicate(role_id)) roles.add(role_id); @@ -163,12 +174,7 @@ export class RoleSet { /** * Describes information about a role for display and logic */ -export class RoleData { - /** - * The role's identifier - */ - public readonly id: RoleId; - +export class RoleData { /** * The human-readable name of the role */ @@ -182,29 +188,29 @@ export class RoleData { /** * The role's alignment */ - public readonly alignment: Alignment; + public readonly alignment: AlignmentOf; /** - * The set of roles this role can see + * The set of roles that must be present for this role to be enabled */ - public readonly information: RoleSet; + public readonly dependencies: RoleSet; /** - * The set of roles that must be present for this role to be enabled + * The set of roles this role can see */ - public readonly dependencies: RoleSet; + public readonly information: RoleSet; public constructor( - id: RoleId, + id: TRole, name: string, description: string, + alignment: AlignmentOf, ) { - this.id = id; this.name = name; this.description = description; - this.alignment = RoleSet.GOOD.has(id) ? "good" : "evil"; - this.information = RoleSet.of(...RoleRelations[id].information); - this.dependencies = RoleSet.of(...RoleRelations[id].dependencies); + this.alignment = alignment; + this.dependencies = RoleSet.of(...ROLE_RELATIONS[id].dependencies); + this.information = RoleSet.of(...ROLE_RELATIONS[id].information); } /** @@ -232,52 +238,60 @@ export class RoleData { /** * Holds the properties of all roles in the game */ -export const ROLES: { [role in RoleId]: RoleData } = { +export const ROLES: { [TRole in RoleId]: RoleData } = { servant: new RoleData( "servant", "Servant of Arthur", "A loyal servant of Arthur. Does not know any other players' roles.", + "good", ), merlin: new RoleData( "merlin", "Merlin", "A loyal servant of Arthur. Knows the evil players (except Mordred), but must be careful not to reveal himself.", + "good", ), percival: new RoleData( "percival", "Percival", "A loyal servant of Arthur. Sees Merlin and Morgana, but not which is which.", + "good", ), minion: new RoleData( "minion", "Minion of Mordred", "A servant of Mordred. Knows the other evil players (except Oberon).", + "evil", ), morgana: new RoleData( "morgana", "Morgana", "A servant of Mordred. Knows the other evil players (except Oberon). Appears as Merlin to Percival.", + "evil", ), mordred: new RoleData( "mordred", "Mordred", "The evil sorcerer. Knows the other evil players (except Oberon). Unknown to Merlin.", + "evil", ), assassin: new RoleData( "assassin", "Assassin", "A servant of Mordred. Knows the other evil players (except Oberon). Can assassinate Merlin at the end of the game.", + "evil", ), oberon: new RoleData( "oberon", "Oberon", "A servant of Mordred. Does not know and is unknown by the other evil players.", + "evil", ), }; diff --git a/server/src/app.ts b/server/src/app.ts index 9024d26..d1d55f1 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -9,6 +9,9 @@ import get_user from "@api/routes/get_user.js"; import history from "@api/routes/history.js"; import stats from "@api/routes/stats.js"; +import "@common/game/roles.js"; +import "@common/game/game.js"; + // Constants (duh) import {} from "dotenv/config"; const PORT = process.env.PORT!;