-
Notifications
You must be signed in to change notification settings - Fork 0
add game logic #18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Xylenox
wants to merge
2
commits into
rewrite
Choose a base branch
from
game-logic
base: rewrite
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
add game logic #18
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,10 @@ | ||
| import { InformationOf, RoleId, ROLES, RoleSet } from "@common/game/roles.js"; | ||
| import { | ||
| Alignment, | ||
| InformationOf, | ||
| RoleId, | ||
| ROLES, | ||
| RoleSet, | ||
| } from "@common/game/roles.js"; | ||
|
|
||
| /** | ||
| * The type of a unique identifier for a user in the lobby | ||
|
|
@@ -60,6 +66,14 @@ export type GamePhase = | |
| | "assassination" | ||
| | "game_over"; | ||
|
|
||
| /** | ||
| * Describes the ways in which a game can finish | ||
| */ | ||
| export type GameResult = | ||
| | { reason: "vote_tracker"; winning_team: "evil" } | ||
| | { reason: "assassination_result"; winning_team: Alignment } | ||
| | { reason: "mission_result"; winning_team: Alignment }; | ||
|
|
||
| /** | ||
| * Includes how many votes have failed in a row | ||
| */ | ||
|
|
@@ -83,6 +97,13 @@ export type WithVotes<TPhase> = TPhase extends "round:mission_reveal" | |
| ? { votes: { success: number; fail: number } } | ||
| : unknown; | ||
|
|
||
| /** | ||
| * Includes the result of the game | ||
| */ | ||
| export type WithGameResult<TPhase> = TPhase extends "game_over" | ||
| ? { result: GameResult } | ||
| : unknown; | ||
|
|
||
| /** | ||
| * Describes the current state of the game, along with any available | ||
| * information relevant to the current phase | ||
|
|
@@ -113,20 +134,26 @@ export type GameState = { | |
| /** | ||
| * The results of missions so far | ||
| */ | ||
| missionResults: boolean[]; | ||
| missionResults: Alignment[]; | ||
|
|
||
| /** | ||
| * The current round of the game | ||
| */ | ||
| currentMission: number; | ||
| missionIndex: number; | ||
|
|
||
| /** | ||
| * The index of the current leader in the player list | ||
| */ | ||
| leaderIndex: number; | ||
|
|
||
| /** | ||
| * The index of the current Lady of the Lake in the player list | ||
| */ | ||
| ladyIndex: number; | ||
| } & WithVotesFailed<TPhase> & | ||
| WithCurrentTeam<TPhase> & | ||
| WithVotes<TPhase>; | ||
| WithVotes<TPhase> & | ||
| WithGameResult<TPhase>; | ||
| }[GamePhase]; | ||
|
|
||
| /** | ||
|
|
@@ -193,14 +220,15 @@ export type UserInputByPhase = { | |
| "round:team_select": UserId[]; | ||
| "round:team_vote": boolean; | ||
| "round:mission": boolean; | ||
| "round:lady_choice": UserId; | ||
| assassination: UserId; | ||
| }; | ||
|
|
||
| /** | ||
| * Describes the type of user input required for a given phase | ||
| */ | ||
| export type UserInput<TPhase extends GamePhase> = | ||
| TPhase extends keyof UserInputByPhase ? UserInputByPhase[TPhase] : unknown; | ||
| TPhase extends keyof UserInputByPhase ? UserInputByPhase[TPhase] : never; | ||
|
|
||
| /** | ||
| * Describes a transition from some game state (in a specific phase) to another, | ||
|
|
@@ -215,10 +243,47 @@ export abstract class GameTransition<TFrom extends GamePhase> { | |
| ): GameState; | ||
| } | ||
|
|
||
| /** | ||
| * team select/vote failed, advance leader and vote tracker | ||
| */ | ||
| function restartTeamSelect( | ||
| state: GameStateInPhase<`round:${"team_select" | "team_vote"}`>, | ||
| ): GameState { | ||
| if (state.votesFailed == 4) { | ||
| return { | ||
| ...state, | ||
| phase: "game_over", | ||
| result: { reason: "vote_tracker", winning_team: "evil" }, | ||
| }; | ||
| } | ||
| return { | ||
| ...state, | ||
| phase: "round:team_select", | ||
| leaderIndex: (state.leaderIndex + 1) % state.players.length, | ||
| votesFailed: state.votesFailed + 1, | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * Describes the flow of the game through transitions from all phases | ||
| */ | ||
| export const GAME_FLOW: { [TFrom in GamePhase]: GameTransition<TFrom> } = { | ||
| /** | ||
| * Role reveal: all players receive their role assignment + information | ||
| */ | ||
| role_reveal: { | ||
| getTargetPlayers(_state): UserId[] { | ||
| return []; | ||
| }, | ||
| processInteraction(state, _responses): GameState { | ||
| return { | ||
| ...state, | ||
| phase: "round:team_select", | ||
| votesFailed: 0, | ||
| }; | ||
| }, | ||
| }, | ||
|
|
||
| /** | ||
| * Team selection: the current leader selects a team to go on this mission | ||
| */ | ||
|
|
@@ -230,10 +295,16 @@ export const GAME_FLOW: { [TFrom in GamePhase]: GameTransition<TFrom> } = { | |
| state: GameStateInPhase<"round:team_select">, | ||
| responses: Map<UserId, UserId[]>, | ||
| ): GameState { | ||
| const team = [ | ||
| ...new Set(responses.get(state.players[state.leaderIndex]) ?? []), | ||
| ].filter((player) => player in state.players); | ||
| if (team.length != state.config.rounds[state.missionIndex].teamSize) { | ||
| return restartTeamSelect(state); | ||
| } | ||
| return { | ||
| ...state, | ||
| phase: "round:team_vote", | ||
| currentTeam: responses.get(this.getTargetPlayers(state)[0]) ?? [], | ||
| currentTeam: team, | ||
| }; | ||
| }, | ||
| }, | ||
|
|
@@ -248,21 +319,16 @@ export const GAME_FLOW: { [TFrom in GamePhase]: GameTransition<TFrom> } = { | |
| processInteraction(state, responses): GameState { | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. def need to store player votes for non-anonymous voting |
||
| let votes = 0; | ||
| for (const player in state.players) { | ||
| if (responses.get(player)) { | ||
| if (responses.get(player) === true) { | ||
| votes++; | ||
| } | ||
| } | ||
| if (2 * votes > state.players.length) { | ||
| return { | ||
| ...state, | ||
| phase: "round:mission", | ||
| }; | ||
| if (2 * votes <= state.players.length) { | ||
| return restartTeamSelect(state); | ||
| } | ||
| return { | ||
| ...state, | ||
| phase: "round:team_select", | ||
| leaderIndex: (state.leaderIndex + 1) % state.players.length, | ||
| votesFailed: state.votesFailed + 1, | ||
| phase: "round:mission", | ||
| }; | ||
| }, | ||
| }, | ||
|
|
@@ -286,12 +352,15 @@ export const GAME_FLOW: { [TFrom in GamePhase]: GameTransition<TFrom> } = { | |
| } | ||
| } | ||
| // Retrieve the number of fails required from round config | ||
| const reqFails = state.config.rounds[state.currentMission].reqFail; | ||
| const reqFails = state.config.rounds[state.missionIndex].reqFail; | ||
| return { | ||
| ...state, | ||
| phase: "round:mission_reveal", | ||
| votes: { success, fail }, | ||
| missionResults: [...state.missionResults, fail >= reqFails], | ||
| missionResults: [ | ||
| ...state.missionResults, | ||
| fail < reqFails ? "good" : "evil", | ||
| ], | ||
| }; | ||
| }, | ||
| }, | ||
|
|
@@ -301,29 +370,50 @@ export const GAME_FLOW: { [TFrom in GamePhase]: GameTransition<TFrom> } = { | |
| */ | ||
| "round:mission_reveal": { | ||
| getTargetPlayers(_state): UserId[] { | ||
| throw Error("not implemented"); | ||
| return []; | ||
| }, | ||
| processInteraction(_state, _responses): GameState { | ||
| throw Error("not implemented"); | ||
| }, | ||
| }, | ||
|
|
||
| /** | ||
| * Role reveal: all players receive their role assignment + information | ||
| */ | ||
| role_reveal: { | ||
| getTargetPlayers(_state): UserId[] { | ||
| throw Error("not implemented"); | ||
| }, | ||
| processInteraction(_state, _responses): GameState { | ||
| throw Error("not implemented"); | ||
| processInteraction(state, _responses): GameState { | ||
| if (state.missionResults.filter((x) => x == "good").length >= 3) { | ||
| // good team win | ||
| if (state.config.roles.has("assassin")) { | ||
| return { | ||
| ...state, | ||
| phase: "assassination", | ||
| }; | ||
| } | ||
| return { | ||
| ...state, | ||
| phase: "game_over", | ||
| result: { reason: "mission_result", winning_team: "good" }, | ||
| }; | ||
| } | ||
| if (state.missionResults.filter((x) => x == "evil").length >= 3) { | ||
| // evil team win | ||
| return { | ||
| ...state, | ||
| phase: "game_over", | ||
| result: { reason: "mission_result", winning_team: "evil" }, | ||
| }; | ||
| } | ||
| if (state.config.ladyOfTheLake) { | ||
| return { | ||
| ...state, | ||
| phase: "round:lady_choice", | ||
| }; | ||
| } | ||
| return { | ||
| ...state, | ||
| phase: "round:team_select", | ||
| votesFailed: 0, | ||
| leaderIndex: (state.leaderIndex + 1) % state.players.length, | ||
| }; | ||
| }, | ||
| }, | ||
|
|
||
| /** | ||
| * Assassination: the assassin (or all evil players) guess(es) the identity of Merlin | ||
| * Lady of the Lake: the current player chooses a player whose role to see | ||
| */ | ||
| assassination: { | ||
| "round:lady_choice": { | ||
| getTargetPlayers(_state): UserId[] { | ||
| throw Error("not implemented"); | ||
| }, | ||
|
|
@@ -333,9 +423,9 @@ export const GAME_FLOW: { [TFrom in GamePhase]: GameTransition<TFrom> } = { | |
| }, | ||
|
|
||
| /** | ||
| * Game over: the game ends and results are shown to all players | ||
| * Lady of the Lake reveal: the chosen player's role is revealed to the player with the Lady | ||
| */ | ||
| game_over: { | ||
| "round:lady_reveal": { | ||
| getTargetPlayers(_state): UserId[] { | ||
| throw Error("not implemented"); | ||
| }, | ||
|
|
@@ -345,26 +435,44 @@ export const GAME_FLOW: { [TFrom in GamePhase]: GameTransition<TFrom> } = { | |
| }, | ||
|
|
||
| /** | ||
| * Lady of the Lake: the current player chooses a player whose role to see | ||
| * Assassination: the assassin (or all evil players) guess(es) the identity of Merlin | ||
| */ | ||
| "round:lady_choice": { | ||
| getTargetPlayers(_state): UserId[] { | ||
| throw Error("not implemented"); | ||
| assassination: { | ||
| getTargetPlayers(state): UserId[] { | ||
| return [ | ||
| state.players.filter((player) => state.roles[player] == "assassin")[0], | ||
| ]; | ||
| }, | ||
| processInteraction(_state, _responses): GameState { | ||
| throw Error("not implemented"); | ||
| processInteraction(state, responses): GameState { | ||
| const assassin = state.players.filter( | ||
| (player) => state.roles[player] == "assassin", | ||
| )[0]; | ||
| const target = responses.get(assassin); | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. need to store assassination and target information for game over |
||
| if (typeof target == "string" && state.roles[target] == "merlin") { | ||
| return { | ||
| ...state, | ||
| phase: "game_over", | ||
| result: { reason: "assassination_result", winning_team: "evil" }, | ||
| }; | ||
| } else { | ||
| return { | ||
| ...state, | ||
| phase: "game_over", | ||
| result: { reason: "assassination_result", winning_team: "good" }, | ||
| }; | ||
| } | ||
| }, | ||
| }, | ||
|
|
||
| /** | ||
| * Lady of the Lake reveal: the chosen player's role is revealed to the player with the Lady | ||
| * Game over: the game ends and results are shown to all players | ||
| */ | ||
| "round:lady_reveal": { | ||
| game_over: { | ||
| getTargetPlayers(_state): UserId[] { | ||
| throw Error("not implemented"); | ||
| return []; | ||
| }, | ||
| processInteraction(_state, _responses): GameState { | ||
| throw Error("not implemented"); | ||
| processInteraction(state, _responses): GameState { | ||
| return state; | ||
| }, | ||
| }, | ||
| }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
store past teams/team proposals?