From 997d0e946610efc317d7826de100b673b350529c Mon Sep 17 00:00:00 2001 From: Andy Phan Date: Wed, 23 Apr 2025 11:43:45 -0400 Subject: [PATCH 1/3] alignment finangling --- common/game/game.ts | 2 +- common/game/roles.ts | 62 +++++++++++++++++++------------------------- server/src/app.ts | 3 +++ 3 files changed, 30 insertions(+), 37 deletions(-) diff --git a/common/game/game.ts b/common/game/game.ts index c9ee486..476240d 100644 --- a/common/game/game.ts +++ b/common/game/game.ts @@ -22,7 +22,7 @@ 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 = diff --git a/common/game/roles.ts b/common/game/roles.ts index e07e734..a9d87b7 100644 --- a/common/game/roles.ts +++ b/common/game/roles.ts @@ -7,25 +7,21 @@ 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; +export type RoleIdOfTeam = (typeof TEAMS)[T][number]; /** * Describes the identifier of a certain role */ export type RoleId = (typeof TEAMS)[keyof typeof TEAMS][number]; +export type AlignmentOf = + T extends RoleIdOfTeam<"good"> + ? "good" + : T extends RoleIdOfTeam<"evil"> + ? "evil" + : Alignment; /** * Describes a single role or set of roles @@ -33,7 +29,9 @@ export type RoleId = (typeof TEAMS)[keyof typeof TEAMS][number]; * 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); @@ -91,9 +89,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); @@ -119,12 +115,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 */ @@ -138,7 +129,7 @@ export class RoleData { /** * The role's alignment */ - public readonly alignment: Alignment; + public readonly alignment: AlignmentOf; /** * The set of roles this role can see @@ -151,16 +142,15 @@ export class RoleData { public readonly dependencies: RoleSet; public constructor( - id: RoleId, name: string, description: string, + alignment: AlignmentOf, information: RoleSet = RoleSet.of(), dependencies: RoleSet = RoleSet.of(), ) { - this.id = id; this.name = name; this.description = description; - this.alignment = RoleSet.GOOD.has(id) ? "good" : "evil"; + this.alignment = alignment; this.information = information; this.dependencies = dependencies; } @@ -190,61 +180,61 @@ export class RoleData { /** * Holds the properties of all roles in the game */ -export const ROLES: { [role in RoleId]: RoleData } = { +export const ROLES: { [role 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", RoleSet.EVIL.andNot("mordred"), ), percival: new RoleData( - "percival", "Percival", "A loyal servant of Arthur. Sees Merlin and Morgana, but not which is which.", + "good", RoleSet.of("merlin", "morgana"), RoleSet.of("merlin", "morgana"), ), minion: new RoleData( - "minion", "Minion of Mordred", "A servant of Mordred. Knows the other evil players (except Oberon).", + "evil", RoleSet.EVIL.andNot("oberon"), ), morgana: new RoleData( - "morgana", "Morgana", "A servant of Mordred. Knows the other evil players (except Oberon). Appears as Merlin to Percival.", + "evil", RoleSet.EVIL.andNot("oberon"), RoleSet.of("merlin", "percival"), ), mordred: new RoleData( - "mordred", "Mordred", "The evil sorcerer. Knows the other evil players (except Oberon). Unknown to Merlin.", + "evil", RoleSet.EVIL.andNot("oberon"), ), 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", RoleSet.EVIL.andNot("oberon"), RoleSet.of("merlin"), ), 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!; From e15aa421bfe2abe8a4de3cf083f48dee67e60c9c Mon Sep 17 00:00:00 2001 From: enwask Date: Wed, 23 Apr 2025 12:30:14 -0400 Subject: [PATCH 2/3] Doc comments --- common/game/roles.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/common/game/roles.ts b/common/game/roles.ts index a9d87b7..329c334 100644 --- a/common/game/roles.ts +++ b/common/game/roles.ts @@ -11,15 +11,23 @@ export const TEAMS = { 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)[keyof typeof TEAMS][number]; -export type AlignmentOf = - T extends RoleIdOfTeam<"good"> + +/** + * Describes the alignment of a role given its identifier + */ +export type AlignmentOf = + TRole extends RoleIdOfTeam<"good"> ? "good" - : T extends RoleIdOfTeam<"evil"> + : TRole extends RoleIdOfTeam<"evil"> ? "evil" : Alignment; From 33bf447d6ffbe7eaca3efd296b7010ba1f8e1099 Mon Sep 17 00:00:00 2001 From: enwask Date: Wed, 23 Apr 2025 13:08:41 -0400 Subject: [PATCH 3/3] Better role dependencies + info --- common/game/roles.ts | 91 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 70 insertions(+), 21 deletions(-) diff --git a/common/game/roles.ts b/common/game/roles.ts index 329c334..e9cd2b8 100644 --- a/common/game/roles.ts +++ b/common/game/roles.ts @@ -21,6 +21,45 @@ export type RoleIdOfTeam = (typeof TEAMS)[T][number]; */ export type RoleId = (typeof TEAMS)[keyof typeof TEAMS][number]; +/** + * Describes information and dependencies for all roles + */ +export const ROLE_RELATIONS = { + servant: { + dependencies: [], + information: [], + }, + merlin: { + dependencies: [], + information: TEAMS.evil.filter((role) => role !== "mordred"), + }, + percival: { + dependencies: ["merlin", "morgana"], + information: ["merlin", "morgana"], + }, + + minion: { + dependencies: [], + information: TEAMS.evil.filter((role) => role !== "oberon"), + }, + morgana: { + dependencies: ["merlin", "percival"], + information: TEAMS.evil.filter((role) => role !== "oberon"), + }, + mordred: { + dependencies: [], + information: TEAMS.evil.filter((role) => role !== "oberon"), + }, + assassin: { + dependencies: ["merlin"], + information: TEAMS.evil.filter((role) => role !== "oberon"), + }, + oberon: { + dependencies: [], + information: [], + }, +} as const; + /** * Describes the alignment of a role given its identifier */ @@ -31,6 +70,18 @@ export type AlignmentOf = ? "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 * @@ -123,7 +174,7 @@ export class RoleSet { /** * Describes information about a role for display and logic */ -export class RoleData { +export class RoleData { /** * The human-readable name of the role */ @@ -137,30 +188,29 @@ export class RoleData { /** * The role's alignment */ - public readonly alignment: AlignmentOf; + 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: TRole, name: string, description: string, - alignment: AlignmentOf, - information: RoleSet = RoleSet.of(), - dependencies: RoleSet = RoleSet.of(), + alignment: AlignmentOf, ) { this.name = name; this.description = description; this.alignment = alignment; - this.information = information; - this.dependencies = dependencies; + this.dependencies = RoleSet.of(...ROLE_RELATIONS[id].dependencies); + this.information = RoleSet.of(...ROLE_RELATIONS[id].information); } /** @@ -188,59 +238,58 @@ 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", - RoleSet.EVIL.andNot("mordred"), ), percival: new RoleData( + "percival", "Percival", "A loyal servant of Arthur. Sees Merlin and Morgana, but not which is which.", "good", - RoleSet.of("merlin", "morgana"), - RoleSet.of("merlin", "morgana"), ), minion: new RoleData( + "minion", "Minion of Mordred", "A servant of Mordred. Knows the other evil players (except Oberon).", "evil", - RoleSet.EVIL.andNot("oberon"), ), morgana: new RoleData( + "morgana", "Morgana", "A servant of Mordred. Knows the other evil players (except Oberon). Appears as Merlin to Percival.", "evil", - RoleSet.EVIL.andNot("oberon"), - RoleSet.of("merlin", "percival"), ), mordred: new RoleData( + "mordred", "Mordred", "The evil sorcerer. Knows the other evil players (except Oberon). Unknown to Merlin.", "evil", - RoleSet.EVIL.andNot("oberon"), ), 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", - RoleSet.EVIL.andNot("oberon"), - RoleSet.of("merlin"), ), oberon: new RoleData( + "oberon", "Oberon", "A servant of Mordred. Does not know and is unknown by the other evil players.", "evil",