diff --git a/werewolves-proto/src/character.rs b/werewolves-proto/src/character.rs index 5f35cbf..7ff9348 100644 --- a/werewolves-proto/src/character.rs +++ b/werewolves-proto/src/character.rs @@ -47,6 +47,12 @@ impl CharacterId { pub const fn from_u128(v: u128) -> Self { Self(uuid::Uuid::from_u128(v)) } + pub const fn from_uuid(v: uuid::Uuid) -> Self { + Self(v) + } + pub const fn into_uuid(self) -> uuid::Uuid { + self.0 + } } impl Display for CharacterId { @@ -66,7 +72,11 @@ pub struct Character { } impl Character { - pub fn new( + pub fn new(ident: Identification, role: Role, auras: Vec) -> Option { + Self::new_with_character_id(ident, role, auras, CharacterId::new()) + } + + pub(crate) fn new_with_character_id( Identification { player_id, public: @@ -78,6 +88,7 @@ impl Character { }: Identification, role: Role, auras: Vec, + character_id: CharacterId, ) -> Option { Some(Self { role, @@ -86,7 +97,7 @@ impl Character { auras: Auras::new(auras), role_changes: Vec::new(), identity: CharacterIdentity { - character_id: CharacterId::new(), + character_id, name, pronouns, number: number?, diff --git a/werewolves-proto/src/error.rs b/werewolves-proto/src/error.rs index ecd68fc..7e2c7db 100644 --- a/werewolves-proto/src/error.rs +++ b/werewolves-proto/src/error.rs @@ -109,4 +109,16 @@ pub enum GameError { NoCurrentPromptForAura, #[error("you're not dead")] NotDead, + #[error("invalid character id assignment for player ID {for_player}")] + InvalidCharacterIdAssignment { for_player: PlayerId }, + #[error("already joined")] + AlreadyJoined, + #[error("cannot join own game")] + CannotJoinOwnGame, + #[error("cannot leave a started game")] + CannotLeaveOnceStarted, + #[error("cannot join a started game")] + CannotJoinStartedGame, + #[error("game already started")] + GameAlreadyStarted, } diff --git a/werewolves-proto/src/game/mod.rs b/werewolves-proto/src/game/mod.rs index bf41b1d..57dd245 100644 --- a/werewolves-proto/src/game/mod.rs +++ b/werewolves-proto/src/game/mod.rs @@ -61,6 +61,19 @@ pub struct Game { } impl Game { + pub fn new_with_assigned_character_ids( + players: &[(Identification, CharacterId)], + settings: GameSettings, + ) -> Result { + let village = Village::new_with_assigned_character_ids(players, settings)?; + Ok(Self { + started: Utc::now(), + history: GameStory::new(village.clone()), + state: GameState::Night { + night: Night::new(village)?, + }, + }) + } pub fn new(players: &[Identification], settings: GameSettings) -> Result { let village = Village::new(players, settings)?; Ok(Self { diff --git a/werewolves-proto/src/game/night.rs b/werewolves-proto/src/game/night.rs index fe5cc18..65fc557 100644 --- a/werewolves-proto/src/game/night.rs +++ b/werewolves-proto/src/game/night.rs @@ -829,7 +829,7 @@ impl Night { NightChange::Protection { target, protection: _, - } => target == kill_target, + } => target == kill_target || target == *source, _ => false, }) { // there is protection, so the kill doesn't happen -> no shapeshift diff --git a/werewolves-proto/src/game/settings.rs b/werewolves-proto/src/game/settings.rs index 1a34964..a170671 100644 --- a/werewolves-proto/src/game/settings.rs +++ b/werewolves-proto/src/game/settings.rs @@ -23,7 +23,12 @@ use super::Result; use serde::{Deserialize, Serialize}; -use crate::{character::Character, error::GameError, message::Identification, role::RoleTitle}; +use crate::{ + character::{Character, CharacterId}, + error::GameError, + message::Identification, + role::RoleTitle, +}; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct GameSettings { @@ -115,8 +120,12 @@ impl GameSettings { } } - pub fn assign(&self, players: &[Identification]) -> Result> { - self.check_with_player_list(players)?; + pub fn assign_with_set_character_ids( + &self, + players: &[(Identification, CharacterId)], + ) -> Result> { + let idents_only = players.iter().map(|i| i.0.clone()).collect::>(); + self.check_with_player_list(&idents_only)?; let roles_in_game = self .roles @@ -131,7 +140,7 @@ impl GameSettings { s.assign_to.as_ref().map(|assign_to| { players .iter() - .find(|pid| pid.player_id == *assign_to) + .find(|(pid, _)| pid.player_id == *assign_to) .ok_or(GameError::AssignedPlayerMissing(*assign_to)) .map(|id| (id, s)) }) @@ -140,10 +149,10 @@ impl GameSettings { let mut random_assign_players = players .iter() - .filter(|p| { + .filter(|(p, _)| { !with_assigned_roles .iter() - .any(|(r, _)| r.player_id == p.player_id) + .any(|((r, _), _)| r.player_id == p.player_id) }) .collect::>(); @@ -156,10 +165,21 @@ impl GameSettings { .into_iter() .zip(self.roles.iter().filter(|s| s.assign_to.is_none())), ) - .map(|(id, slot)| slot.clone().into_character(id.clone(), &roles_in_game)) + .map(|((ident, char_id), slot)| { + slot.clone() + .into_character_with_id(ident.clone(), &roles_in_game, *char_id) + }) .collect::>>() } + pub fn assign(&self, players: &[Identification]) -> Result> { + let with_cids = players + .iter() + .map(|ident| (ident.clone(), CharacterId::new())) + .collect::>(); + self.assign_with_set_character_ids(&with_cids) + } + pub fn check_with_player_list(&self, players: &[Identification]) -> Result<()> { self.check()?; let (p_len, r_len) = (players.len(), self.roles.len()); diff --git a/werewolves-proto/src/game/settings/settings_role.rs b/werewolves-proto/src/game/settings/settings_role.rs index 89a400d..1b6cc5c 100644 --- a/werewolves-proto/src/game/settings/settings_role.rs +++ b/werewolves-proto/src/game/settings/settings_role.rs @@ -24,7 +24,7 @@ use werewolves_macros::{All, ChecksAs, Titles}; use crate::{ aura::AuraTitle, - character::Character, + character::{Character, CharacterId}, error::GameError, message::Identification, player::PlayerId, @@ -435,6 +435,12 @@ impl SlotId { } } +impl Display for SlotId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Display::fmt(&self.0, f) + } +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct SetupSlot { pub slot_id: SlotId, @@ -470,17 +476,22 @@ impl SetupSlot { ) .ok_or(GameError::PlayerNotAssignedNumber(ident.to_string())) } -} -impl Category { - pub const fn class(&self) -> &'static str { - match self { - Category::Wolves => "wolves", - Category::Villager => "village", - Category::Intel => "intel", - Category::Defensive => "defensive", - Category::Offensive => "offensive", - Category::StartsAsVillager => "starts-as-villager", - } + pub fn into_character_with_id( + self, + ident: Identification, + roles_in_game: &[RoleTitle], + id: CharacterId, + ) -> Result { + Character::new_with_character_id( + ident.clone(), + self.role.into_role(roles_in_game)?, + self.auras + .into_iter() + .map(|aura| aura.into_aura()) + .collect(), + id, + ) + .ok_or(GameError::PlayerNotAssignedNumber(ident.to_string())) } } diff --git a/werewolves-proto/src/game/village.rs b/werewolves-proto/src/game/village.rs index aa95f15..713f9fc 100644 --- a/werewolves-proto/src/game/village.rs +++ b/werewolves-proto/src/game/village.rs @@ -43,6 +43,27 @@ pub struct Village { } impl Village { + pub fn new_with_assigned_character_ids( + players: &[(Identification, CharacterId)], + settings: GameSettings, + ) -> Result { + if settings.min_players_needed() > players.len() { + return Err(GameError::TooManyRoles { + players: players.len() as u8, + roles: settings.min_players_needed() as u8, + }); + } + let mut characters = settings.assign_with_set_character_ids(players)?; + assert_eq!(characters.len(), players.len()); + characters.sort_by_key(|l| l.number()); + + Ok(Self { + settings, + characters, + time: GameTime::Night { number: 0 }, + dead_chat: DeadChat::new(), + }) + } pub fn new(players: &[Identification], settings: GameSettings) -> Result { if settings.min_players_needed() > players.len() { return Err(GameError::TooManyRoles { diff --git a/werewolves-proto/src/game_test/mod.rs b/werewolves-proto/src/game_test/mod.rs index 95f4d78..20fc82f 100644 --- a/werewolves-proto/src/game_test/mod.rs +++ b/werewolves-proto/src/game_test/mod.rs @@ -1119,7 +1119,7 @@ fn big_game_test_based_on_story_test() { ); game.execute().title().vindicator(); - game.mark(game.character_by_player_id(shapeshifter).character_id()); + game.mark(game.character_by_player_id(insomniac).character_id()); game.r#continue().sleep(); game.next().title().wolf_pack_kill(); @@ -1127,11 +1127,16 @@ fn big_game_test_based_on_story_test() { game.r#continue().r#continue(); game.next().title().shapeshifter(); - game.process(HostGameMessage::Night(HostNightMessage::ActionResponse( - ActionResponse::Shapeshift, - ))) - .expect("shapeshift"); - // game.r#continue().r#continue(); + assert_eq!( + game.process(HostGameMessage::Night(HostNightMessage::ActionResponse( + ActionResponse::Shapeshift, + ))) + .expect("shapeshift"), + ServerToHostMessage::ActionResult( + Some(game.character_by_player_id(shapeshifter).identity()), + ActionResult::Continue + ) + ); assert_eq!( game.next(), diff --git a/werewolves-proto/src/game_test/role/shapeshifter.rs b/werewolves-proto/src/game_test/role/shapeshifter.rs index 11b0c62..1ac8944 100644 --- a/werewolves-proto/src/game_test/role/shapeshifter.rs +++ b/werewolves-proto/src/game_test/role/shapeshifter.rs @@ -444,3 +444,52 @@ fn shapeshift_removes_village_prompt_but_previous_can_bring_it_back() { None ); } + +#[test] +fn shapeshifter_protected_when_shifting_prevents_shift() { + init_log(); + let players = gen_players(1..21); + let mut player_ids = players.iter().map(|p| p.player_id); + let shapeshifter = player_ids.next().unwrap(); + let wolf = player_ids.next().unwrap(); + let protector = player_ids.next().unwrap(); + let hunter = player_ids.next().unwrap(); + let mut settings = GameSettings::empty(); + settings.add_and_assign(SetupRole::Shapeshifter, shapeshifter); + settings.add_and_assign(SetupRole::Werewolf, wolf); + settings.add_and_assign(SetupRole::Protector, protector); + settings.add_and_assign(SetupRole::Hunter, hunter); + settings.fill_remaining_slots_with_villagers(players.len()); + let mut game = Game::new(&players, settings).unwrap(); + game.r#continue().r#continue(); + assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro); + game.r#continue().sleep(); + + game.next_expect_day(); + game.execute().title().protector(); + game.mark(game.character_by_player_id(shapeshifter).character_id()); + game.r#continue().sleep(); + + game.next().title().wolf_pack_kill(); + game.mark(game.character_by_player_id(hunter).character_id()); + game.r#continue().r#continue(); + + game.next().title().shapeshifter(); + match game + .process(HostGameMessage::Night(HostNightMessage::ActionResponse( + ActionResponse::Shapeshift, + ))) + .unwrap() + { + ServerToHostMessage::ActionResult(_, res) => assert_eq!(res, ActionResult::ShiftFailed), + other => panic!("expected action result, got {other:?}"), + } + + game.r#continue().sleep(); + + game.next().title().hunter(); + game.mark_villager(); + game.r#continue().sleep(); + + game.next_expect_day(); +} diff --git a/werewolves-proto/src/message.rs b/werewolves-proto/src/message.rs index 31b50d4..79a96b0 100644 --- a/werewolves-proto/src/message.rs +++ b/werewolves-proto/src/message.rs @@ -22,6 +22,7 @@ use core::num::NonZeroU8; use chrono::{DateTime, Utc}; pub use ident::*; use serde::{Deserialize, Serialize}; +use werewolves_macros::Titles; use crate::{ character::CharacterId, @@ -62,7 +63,7 @@ pub struct DayCharacter { pub alive: bool, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Titles)] pub enum ServerToClientMessage { Disconnect, LobbyInfo { @@ -73,9 +74,6 @@ pub enum ServerToClientMessage { GameStart { role: RoleTitle, }, - InvalidMessageForGameState, - NoSuchTarget, - GameOver(GameOver), Story(GameStory), Update(PlayerUpdate), DeadChat(Box<[DeadChatMessage]>), @@ -85,7 +83,7 @@ pub enum ServerToClientMessage { Error(GameError), } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum PlayerUpdate { Number(NonZeroU8), } diff --git a/werewolves-proto/src/message/host.rs b/werewolves-proto/src/message/host.rs index 62a6ad8..5b0259d 100644 --- a/werewolves-proto/src/message/host.rs +++ b/werewolves-proto/src/message/host.rs @@ -105,8 +105,8 @@ pub enum ServerToHostMessage { Lobby { players: Box<[PlayerState]>, settings: GameSettings, + qr_mode: bool, }, - QrMode(bool), Error(GameError), GameOver(GameOver), WaitingForRoleRevealAcks { diff --git a/werewolves-proto/src/player.rs b/werewolves-proto/src/player.rs index f2da251..9113503 100644 --- a/werewolves-proto/src/player.rs +++ b/werewolves-proto/src/player.rs @@ -31,6 +31,15 @@ impl PlayerId { pub const fn from_u128(v: u128) -> Self { Self(uuid::Uuid::from_u128(v)) } + pub const fn from_uuid(uuid: uuid::Uuid) -> Self { + Self(uuid) + } +} + +impl From for uuid::Uuid { + fn from(value: PlayerId) -> Self { + value.0 + } } impl Display for PlayerId { diff --git a/werewolves-proto/src/role.rs b/werewolves-proto/src/role.rs index 9fe74aa..d3e3f25 100644 --- a/werewolves-proto/src/role.rs +++ b/werewolves-proto/src/role.rs @@ -20,7 +20,7 @@ use werewolves_macros::{All, ChecksAs, RefAndMut, Titles}; use crate::{ character::CharacterId, diedto::DiedTo, - game::{GameTime, Village}, + game::{Category, GameTime, Village}, message::CharacterIdentity, }; @@ -122,50 +122,59 @@ pub enum Role { #[checks(Alignment::Village)] #[checks(Killer::NotKiller)] #[checks(Powerful::NotPowerful)] + #[checks(Category::Villager)] Villager, #[checks(Alignment::Wolves)] #[checks(Killer::Killer)] #[checks(Powerful::Powerful)] + #[checks(Category::Villager)] Scapegoat { redeemed: bool }, #[checks(Alignment::Village)] #[checks(Powerful::Powerful)] #[checks(Killer::NotKiller)] #[checks("is_mentor")] #[checks("doesnt_wake_if_died_tonight")] + #[checks(Category::Intel)] Seer, #[checks(Alignment::Village)] #[checks(Powerful::Powerful)] #[checks(Killer::NotKiller)] #[checks("is_mentor")] #[checks("doesnt_wake_if_died_tonight")] + #[checks(Category::Intel)] Arcanist, #[checks(Alignment::Village)] #[checks(Powerful::Powerful)] #[checks(Killer::NotKiller)] #[checks("is_mentor")] #[checks("doesnt_wake_if_died_tonight")] + #[checks(Category::Intel)] Adjudicator, #[checks(Alignment::Village)] #[checks(Powerful::Powerful)] #[checks(Killer::NotKiller)] #[checks("is_mentor")] #[checks("doesnt_wake_if_died_tonight")] + #[checks(Category::Intel)] PowerSeer, #[checks(Alignment::Village)] #[checks(Powerful::Powerful)] #[checks(Killer::NotKiller)] #[checks("is_mentor")] #[checks("doesnt_wake_if_died_tonight")] + #[checks(Category::Intel)] Mortician, #[checks(Alignment::Village)] #[checks(Powerful::Powerful)] #[checks(Killer::NotKiller)] #[checks("is_mentor")] #[checks("doesnt_wake_if_died_tonight")] + #[checks(Category::Intel)] Beholder, #[checks(Alignment::Village)] #[checks(Powerful::Powerful)] #[checks(Killer::NotKiller)] + #[checks(Category::Intel)] MasonLeader { recruits_available: u8, recruits: Box<[CharacterId]>, @@ -173,58 +182,69 @@ pub enum Role { #[checks(Alignment::Village)] #[checks(Powerful::Powerful)] #[checks(Killer::NotKiller)] + #[checks(Category::Intel)] Empath { cursed: bool }, #[checks(Alignment::Village)] #[checks(Powerful::Powerful)] #[checks(Killer::NotKiller)] #[checks("is_mentor")] + #[checks(Category::Defensive)] Vindicator, #[checks(Alignment::Village)] #[checks(Powerful::Powerful)] #[checks(Killer::NotKiller)] #[checks("is_mentor")] + #[checks(Category::Defensive)] Diseased, #[checks(Alignment::Village)] #[checks(Powerful::Powerful)] #[checks(Killer::NotKiller)] #[checks("is_mentor")] + #[checks(Category::Defensive)] BlackKnight { attacked: Option }, #[checks(Alignment::Village)] #[checks(Powerful::Powerful)] #[checks(Killer::NotKiller)] #[checks("is_mentor")] + #[checks(Category::Offensive)] Weightlifter, #[checks(Alignment::Village)] #[checks(Powerful::Powerful)] #[checks(Killer::Killer)] #[checks("is_mentor")] + #[checks(Category::Offensive)] PyreMaster { villagers_killed: u8 }, #[checks(Alignment::Village)] #[checks(Powerful::Powerful)] #[checks(Killer::NotKiller)] #[checks("is_mentor")] #[checks("doesnt_wake_if_died_tonight")] + #[checks(Category::Intel)] Gravedigger, #[checks(Alignment::Village)] #[checks(Killer::Killer)] #[checks(Powerful::Powerful)] #[checks("is_mentor")] #[checks] + #[checks(Category::Offensive)] Hunter { target: Option }, #[checks(Alignment::Village)] #[checks(Killer::Killer)] #[checks(Powerful::Powerful)] #[checks("is_mentor")] + #[checks(Category::Offensive)] Militia { targeted: Option }, #[checks(Alignment::Wolves)] #[checks(Killer::Killer)] #[checks(Powerful::Powerful)] #[checks("is_mentor")] + #[checks(Category::Offensive)] MapleWolf { last_kill_on_night: u8 }, #[checks(Alignment::Village)] #[checks(Powerful::Powerful)] #[checks(Killer::Killer)] #[checks("is_mentor")] + #[checks(Category::Defensive)] Guardian { last_protected: Option, }, @@ -232,14 +252,17 @@ pub enum Role { #[checks(Powerful::Powerful)] #[checks(Killer::NotKiller)] #[checks("is_mentor")] + #[checks(Category::Defensive)] Protector { last_protected: Option }, #[checks(Alignment::Village)] #[checks(Powerful::Powerful)] #[checks(Killer::NotKiller)] + #[checks(Category::StartsAsVillager)] Apprentice(RoleTitle), #[checks(Alignment::Village)] #[checks(Powerful::Powerful)] #[checks(Killer::NotKiller)] + #[checks(Category::StartsAsVillager)] Elder { knows_on_night: NonZeroU8, woken_for_reveal: bool, @@ -249,6 +272,7 @@ pub enum Role { #[checks(Powerful::Powerful)] #[checks(Killer::NotKiller)] #[checks("doesnt_wake_if_died_tonight")] + #[checks(Category::Intel)] Insomniac, #[checks(Alignment::Wolves)] @@ -256,33 +280,39 @@ pub enum Role { #[checks(Powerful::Powerful)] #[checks("wolf")] #[checks("killing_wolf")] + #[checks(Category::Wolves)] Werewolf, #[checks(Alignment::Wolves)] #[checks(Killer::Killer)] #[checks(Powerful::Powerful)] #[checks("wolf")] #[checks("killing_wolf")] + #[checks(Category::Wolves)] AlphaWolf { killed: Option }, #[checks(Alignment::Village)] #[checks(Killer::Killer)] #[checks(Powerful::Powerful)] #[checks("wolf")] + #[checks(Category::Wolves)] DireWolf { last_blocked: Option }, #[checks(Alignment::Wolves)] #[checks(Killer::Killer)] #[checks(Powerful::Powerful)] #[checks("wolf")] #[checks("killing_wolf")] + #[checks(Category::Wolves)] Shapeshifter { shifted_into: Option }, #[checks(Alignment::Wolves)] #[checks(Killer::Killer)] #[checks(Powerful::Powerful)] #[checks("wolf")] + #[checks(Category::Wolves)] LoneWolf, #[checks(Alignment::Wolves)] #[checks(Killer::Killer)] #[checks(Powerful::Powerful)] #[checks("wolf")] + #[checks(Category::Wolves)] Bloodletter, }