diff --git a/werewolves-proto/src/character.rs b/werewolves-proto/src/character.rs index 4459b7e..35bb896 100644 --- a/werewolves-proto/src/character.rs +++ b/werewolves-proto/src/character.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; use crate::{ diedto::DiedTo, error::GameError, - game::{DateTime, Village}, + game::{DateTime, Village, night::NightChange}, message::{CharacterIdentity, Identification, PublicIdentity, night::ActionPrompt}, modifier::Modifier, player::{PlayerId, RoleChange}, @@ -217,7 +217,58 @@ impl Character { ) } + fn mason_prompts(&self, village: &Village) -> Result> { + if !self.role.wakes(village) { + return Ok(Box::new([])); + } + let (recruits, recruits_available) = match &self.role { + Role::MasonLeader { + recruits, + recruits_available, + } => (recruits, *recruits_available), + _ => { + return Err(GameError::InvalidRole { + expected: RoleTitle::MasonLeader, + got: self.role_title(), + }); + } + }; + let recruits = recruits + .iter() + .filter_map(|r| village.character_by_id(*r).ok()) + .filter_map(|c| c.is_village().then_some(c.identity())) + .chain((self.is_village() && self.alive()).then_some(self.identity())) + .collect::>(); + Ok(recruits + .is_empty() + .not() + .then_some(ActionPrompt::MasonsWake { + leader: self.character_id(), + masons: recruits.clone(), + }) + .into_iter() + .chain( + self.alive() + .then_some(()) + .and_then(|_| NonZeroU8::new(recruits_available)) + .map(|recruits_available| ActionPrompt::MasonLeaderRecruit { + character_id: self.identity(), + recruits_left: recruits_available, + potential_recruits: village + .living_players_excluding(self.character_id()) + .into_iter() + .filter(|c| !recruits.iter().any(|r| r.character_id == c.character_id)) + .collect(), + marked: None, + }), + ) + .collect()) + } + pub fn night_action_prompts(&self, village: &Village) -> Result> { + if self.mason_leader().is_ok() { + return self.mason_prompts(village); + } if !self.alive() || !self.role.wakes(village) { return Ok(Box::new([])); } @@ -241,6 +292,11 @@ impl Character { .. } | Role::Villager => return Ok(Box::new([])), + + Role::Insomniac => ActionPrompt::Insomniac { + character_id: self.identity(), + }, + Role::Scapegoat { redeemed: true } => { let mut dead = village.dead_characters(); dead.shuffle(&mut rand::rng()); @@ -414,36 +470,11 @@ impl Character { living_players: village.living_players_excluding(self.character_id()), marked: None, }, - Role::MasonLeader { - recruits_available, - recruits, - } => { - return Ok(recruits - .is_empty() - .not() - .then_some(ActionPrompt::MasonsWake { - character_id: self.identity(), - masons: recruits - .iter() - .map(|r| village.character_by_id(*r).map(|c| c.identity())) - .collect::>>()?, - }) - .into_iter() - .chain( - NonZeroU8::new(*recruits_available).map(|recruits_available| { - ActionPrompt::MasonLeaderRecruit { - character_id: self.identity(), - recruits_left: recruits_available, - potential_recruits: village - .living_players_excluding(self.character_id()) - .into_iter() - .filter(|c| !recruits.contains(&c.character_id)) - .collect(), - marked: None, - } - }), - ) - .collect()); + Role::MasonLeader { .. } => { + log::error!( + "night_action_prompts got to MasonLeader, should be handled before the living check" + ); + return Ok(Box::new([])); } Role::Empath { cursed: false } => ActionPrompt::Empath { character_id: self.identity(), diff --git a/werewolves-proto/src/game/night.rs b/werewolves-proto/src/game/night.rs index 76cacf4..b17eebd 100644 --- a/werewolves-proto/src/game/night.rs +++ b/werewolves-proto/src/game/night.rs @@ -13,7 +13,10 @@ use crate::{ DateTime, Village, kill::{self, ChangesLookup}, }, - message::night::{ActionPrompt, ActionResponse, ActionResult}, + message::{ + CharacterIdentity, + night::{ActionPrompt, ActionResponse, ActionResult, Visits}, + }, player::Protection, role::{PYREMASTER_VILLAGER_KILLS_TO_DIE, PreviousGuardianAction, RoleBlock, RoleTitle}, }; @@ -78,7 +81,8 @@ impl From for ResponseOutcome { impl ActionPrompt { fn unless(&self) -> Option { match &self { - ActionPrompt::MasonsWake { .. } + ActionPrompt::Insomniac { .. } + | ActionPrompt::MasonsWake { .. } | ActionPrompt::WolvesIntro { .. } | ActionPrompt::RoleChange { .. } | ActionPrompt::Shapeshifter { .. } @@ -513,10 +517,9 @@ impl Night { ) -> Result { if self.village.character_by_id(recruiting)?.is_village() { if let Some(masons) = self.action_queue.iter_mut().find_map(|a| match a { - ActionPrompt::MasonsWake { - character_id, - masons, - } => (character_id.character_id == mason_leader).then_some(masons), + ActionPrompt::MasonsWake { leader, masons, .. } => { + (*leader == mason_leader).then_some(masons) + } _ => None, }) { let mut ext_masons = masons.to_vec(); @@ -524,7 +527,7 @@ impl Night { *masons = ext_masons.into_boxed_slice(); } else { self.action_queue.push_front(ActionPrompt::MasonsWake { - character_id: self.village.character_by_id(mason_leader)?.identity(), + leader: self.village.character_by_id(mason_leader)?.character_id(), masons: Box::new([self.village.character_by_id(recruiting)?.identity()]), }); } @@ -794,6 +797,15 @@ impl Night { }; } ActionResponse::Continue => { + if let ActionPrompt::Insomniac { character_id } = current_prompt { + return Ok(ActionComplete { + result: ActionResult::Insomniac( + self.get_visits_for(character_id.character_id), + ), + change: None, + } + .into()); + } if let ActionPrompt::RoleChange { character_id, new_role, @@ -813,8 +825,8 @@ impl Night { match current_prompt { ActionPrompt::LoneWolfKill { character_id, - living_players, marked: Some(marked), + .. } => Ok(ActionComplete { result: ActionResult::GoBackToSleep, change: Some(NightChange::Kill { @@ -1144,6 +1156,11 @@ impl Night { }), } .into()), + ActionPrompt::Insomniac { .. } => Ok(ActionComplete { + result: ActionResult::GoBackToSleep, + change: None, + } + .into()), ActionPrompt::PyreMaster { character_id, marked: Some(marked), @@ -1226,7 +1243,8 @@ impl Night { current_prompt, current_result: _, } => match current_prompt { - ActionPrompt::LoneWolfKill { character_id, .. } + ActionPrompt::Insomniac { character_id, .. } + | ActionPrompt::LoneWolfKill { character_id, .. } | ActionPrompt::ElderReveal { character_id } | ActionPrompt::RoleChange { character_id, .. } | ActionPrompt::Seer { character_id, .. } @@ -1243,13 +1261,14 @@ impl Night { | ActionPrompt::PowerSeer { character_id, .. } | ActionPrompt::Mortician { character_id, .. } | ActionPrompt::Beholder { character_id, .. } - | ActionPrompt::MasonsWake { character_id, .. } | ActionPrompt::MasonLeaderRecruit { character_id, .. } | ActionPrompt::Empath { character_id, .. } | ActionPrompt::Vindicator { character_id, .. } | ActionPrompt::PyreMaster { character_id, .. } | ActionPrompt::DireWolf { character_id, .. } => Some(character_id.character_id), + ActionPrompt::WolvesIntro { wolves: _ } + | ActionPrompt::MasonsWake { .. } | ActionPrompt::WolfPackKill { .. } | ActionPrompt::CoverOfDarkness => None, }, @@ -1282,6 +1301,13 @@ impl Night { NightState::Complete => return Err(GameError::NightOver), } if let Some(prompt) = self.action_queue.pop_front() { + if let ActionPrompt::Insomniac { character_id } = &prompt + && self.get_visits_for(character_id.character_id).is_empty() + { + // skip! + self.used_actions.pop(); // it will be re-added + return self.next(); + } self.night_state = NightState::Active { current_prompt: prompt, current_result: None, @@ -1296,6 +1322,147 @@ impl Night { pub const fn changes(&self) -> &[NightChange] { self.changes.as_slice() } + + pub fn get_visits_for(&self, visit_char: CharacterId) -> Visits { + Visits::new( + self.used_actions + .iter() + .filter_map(|(prompt, _)| match prompt { + ActionPrompt::Arcanist { + character_id, + marked: (Some(marked1), Some(marked2)), + .. + } => (*marked1 == visit_char || *marked2 == visit_char) + .then_some(character_id.clone()), + ActionPrompt::WolfPackKill { + marked: Some(marked), + .. + } => (*marked == visit_char) + .then(|| self.village.killing_wolf().map(|c| c.identity())) + .flatten(), + + ActionPrompt::Seer { + character_id, + marked: Some(marked), + .. + } + | ActionPrompt::Protector { + character_id, + marked: Some(marked), + .. + } + | ActionPrompt::Gravedigger { + character_id, + marked: Some(marked), + .. + } + | ActionPrompt::Hunter { + character_id, + marked: Some(marked), + .. + } + | ActionPrompt::Militia { + character_id, + marked: Some(marked), + .. + } + | ActionPrompt::MapleWolf { + character_id, + marked: Some(marked), + .. + } + | ActionPrompt::Guardian { + character_id, + marked: Some(marked), + .. + } + | ActionPrompt::Adjudicator { + character_id, + marked: Some(marked), + .. + } + | ActionPrompt::PowerSeer { + character_id, + marked: Some(marked), + .. + } + | ActionPrompt::Mortician { + character_id, + marked: Some(marked), + .. + } + | ActionPrompt::Beholder { + character_id, + marked: Some(marked), + .. + } + | ActionPrompt::MasonLeaderRecruit { + character_id, + marked: Some(marked), + .. + } + | ActionPrompt::Empath { + character_id, + marked: Some(marked), + .. + } + | ActionPrompt::Vindicator { + character_id, + marked: Some(marked), + .. + } + | ActionPrompt::PyreMaster { + character_id, + marked: Some(marked), + .. + } + | ActionPrompt::AlphaWolf { + character_id, + marked: Some(marked), + .. + } + | ActionPrompt::DireWolf { + character_id, + marked: Some(marked), + .. + } + | ActionPrompt::LoneWolfKill { + character_id, + marked: Some(marked), + .. + } => (*marked == visit_char).then(|| character_id.clone()), + + ActionPrompt::WolfPackKill { marked: None, .. } + | ActionPrompt::Arcanist { marked: _, .. } + | ActionPrompt::LoneWolfKill { marked: None, .. } + | ActionPrompt::Seer { marked: None, .. } + | ActionPrompt::Protector { marked: None, .. } + | ActionPrompt::Gravedigger { marked: None, .. } + | ActionPrompt::Hunter { marked: None, .. } + | ActionPrompt::Militia { marked: None, .. } + | ActionPrompt::MapleWolf { marked: None, .. } + | ActionPrompt::Guardian { marked: None, .. } + | ActionPrompt::Adjudicator { marked: None, .. } + | ActionPrompt::PowerSeer { marked: None, .. } + | ActionPrompt::Mortician { marked: None, .. } + | ActionPrompt::Beholder { marked: None, .. } + | ActionPrompt::MasonLeaderRecruit { marked: None, .. } + | ActionPrompt::Empath { marked: None, .. } + | ActionPrompt::Vindicator { marked: None, .. } + | ActionPrompt::PyreMaster { marked: None, .. } + | ActionPrompt::AlphaWolf { marked: None, .. } + | ActionPrompt::DireWolf { marked: None, .. } + | ActionPrompt::CoverOfDarkness + | ActionPrompt::WolvesIntro { .. } + | ActionPrompt::RoleChange { .. } + | ActionPrompt::ElderReveal { .. } + | ActionPrompt::Shapeshifter { .. } + | ActionPrompt::MasonsWake { .. } + | ActionPrompt::Insomniac { .. } => None, + }) + .collect(), + ) + } } pub enum ServerAction { diff --git a/werewolves-proto/src/game/settings/settings_role.rs b/werewolves-proto/src/game/settings/settings_role.rs index 29a6fd5..f923d17 100644 --- a/werewolves-proto/src/game/settings/settings_role.rs +++ b/werewolves-proto/src/game/settings/settings_role.rs @@ -117,6 +117,8 @@ pub enum SetupRole { #[checks(Category::Intel)] Adjudicator, #[checks(Category::Intel)] + Insomniac, + #[checks(Category::Intel)] PowerSeer, #[checks(Category::Intel)] Mortician, @@ -141,6 +143,7 @@ pub enum SetupRole { impl SetupRoleTitle { pub fn into_role(self) -> Role { match self { + SetupRoleTitle::Insomniac => Role::Insomniac, SetupRoleTitle::LoneWolf => Role::LoneWolf, SetupRoleTitle::Villager => Role::Villager, SetupRoleTitle::Scapegoat => Role::Scapegoat { redeemed: false }, @@ -191,6 +194,7 @@ impl SetupRoleTitle { impl Display for SetupRole { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(match self { + SetupRole::Insomniac => "Insomniac", SetupRole::LoneWolf => "Lone Wolf", SetupRole::Villager => "Villager", SetupRole::Scapegoat { .. } => "Scapegoat", @@ -226,6 +230,7 @@ impl Display for SetupRole { impl SetupRole { pub fn into_role(self, roles_in_game: &[RoleTitle]) -> Result { Ok(match self { + SetupRole::Insomniac => Role::Insomniac, SetupRole::LoneWolf => Role::LoneWolf, SetupRole::Villager => Role::Villager, SetupRole::Scapegoat { redeemed } => Role::Scapegoat { @@ -289,34 +294,12 @@ impl SetupRole { impl From for RoleTitle { fn from(value: SetupRole) -> Self { match value { - SetupRole::LoneWolf => RoleTitle::LoneWolf, - SetupRole::Villager => RoleTitle::Villager, SetupRole::Scapegoat { .. } => RoleTitle::Scapegoat, - SetupRole::Seer => RoleTitle::Seer, - SetupRole::Arcanist => RoleTitle::Arcanist, - SetupRole::Gravedigger => RoleTitle::Gravedigger, - SetupRole::Hunter => RoleTitle::Hunter, - SetupRole::Militia => RoleTitle::Militia, - SetupRole::MapleWolf => RoleTitle::MapleWolf, - SetupRole::Guardian => RoleTitle::Guardian, - SetupRole::Protector => RoleTitle::Protector, SetupRole::Apprentice { .. } => RoleTitle::Apprentice, - SetupRole::Elder { .. } => RoleTitle::Elder, - SetupRole::Werewolf => RoleTitle::Werewolf, - SetupRole::AlphaWolf => RoleTitle::AlphaWolf, - SetupRole::DireWolf => RoleTitle::DireWolf, - SetupRole::Shapeshifter => RoleTitle::Shapeshifter, - SetupRole::Adjudicator => RoleTitle::Adjudicator, - SetupRole::PowerSeer => RoleTitle::PowerSeer, - SetupRole::Mortician => RoleTitle::Mortician, - SetupRole::Beholder => RoleTitle::Beholder, - SetupRole::MasonLeader { .. } => RoleTitle::MasonLeader, - SetupRole::Empath => RoleTitle::Empath, - SetupRole::Vindicator => RoleTitle::Vindicator, - SetupRole::Diseased => RoleTitle::Diseased, - SetupRole::BlackKnight => RoleTitle::BlackKnight, - SetupRole::Weightlifter => RoleTitle::Weightlifter, - SetupRole::PyreMaster => RoleTitle::PyreMaster, + other => other + .into_role(&[]) + .map(|r| r.title()) + .unwrap_or(RoleTitle::Villager), } } } @@ -324,6 +307,7 @@ impl From for RoleTitle { impl From for SetupRole { fn from(value: RoleTitle) -> Self { match value { + RoleTitle::Insomniac => SetupRole::Insomniac, RoleTitle::LoneWolf => SetupRole::LoneWolf, RoleTitle::Villager => SetupRole::Villager, RoleTitle::Scapegoat => SetupRole::Scapegoat { diff --git a/werewolves-proto/src/game/village.rs b/werewolves-proto/src/game/village.rs index 4862b65..a1df358 100644 --- a/werewolves-proto/src/game/village.rs +++ b/werewolves-proto/src/game/village.rs @@ -286,6 +286,7 @@ impl Village { impl RoleTitle { pub fn title_to_role_excl_apprentice(self) -> Role { match self { + RoleTitle::Insomniac => Role::Insomniac, RoleTitle::LoneWolf => Role::LoneWolf, RoleTitle::Villager => Role::Villager, RoleTitle::Scapegoat => Role::Scapegoat { diff --git a/werewolves-proto/src/game_test/mod.rs b/werewolves-proto/src/game_test/mod.rs index 3a8991e..89d127a 100644 --- a/werewolves-proto/src/game_test/mod.rs +++ b/werewolves-proto/src/game_test/mod.rs @@ -8,7 +8,7 @@ use crate::{ message::{ CharacterState, Identification, PublicIdentity, host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage}, - night::{ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult}, + night::{ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult, Visits}, }, player::PlayerId, role::{Alignment, RoleTitle}, @@ -63,6 +63,7 @@ pub trait ActionPromptTitleExt { fn empath(&self); fn adjudicator(&self); fn lone_wolf(&self); + fn insomniac(&self); } impl ActionPromptTitleExt for ActionPromptTitle { @@ -135,12 +136,17 @@ impl ActionPromptTitleExt for ActionPromptTitle { fn lone_wolf(&self) { assert_eq!(*self, ActionPromptTitle::LoneWolfKill) } + fn insomniac(&self) { + assert_eq!(*self, ActionPromptTitle::Insomniac) + } } pub trait ActionResultExt { fn sleep(&self); fn r#continue(&self); fn seer(&self) -> Alignment; + fn insomniac(&self) -> Visits; + fn arcanist(&self) -> bool; } impl ActionResultExt for ActionResult { @@ -158,6 +164,20 @@ impl ActionResultExt for ActionResult { _ => panic!("expected a seer result"), } } + + fn arcanist(&self) -> bool { + match self { + ActionResult::Arcanist { same } => *same, + _ => panic!("expected an arcanist result"), + } + } + + fn insomniac(&self) -> Visits { + match self { + ActionResult::Insomniac(v) => v.clone(), + _ => panic!("expected an insomniac result"), + } + } } pub trait AlignmentExt { @@ -304,7 +324,8 @@ impl GameExt for Game { fn mark_and_check(&mut self, mark: CharacterId) { let prompt = self.mark(mark); match prompt { - ActionPrompt::MasonsWake { .. } + ActionPrompt::Insomniac { .. } + | ActionPrompt::MasonsWake { .. } | ActionPrompt::ElderReveal { .. } | ActionPrompt::CoverOfDarkness | ActionPrompt::WolvesIntro { .. } diff --git a/werewolves-proto/src/game_test/role/insomniac.rs b/werewolves-proto/src/game_test/role/insomniac.rs new file mode 100644 index 0000000..ecff6b6 --- /dev/null +++ b/werewolves-proto/src/game_test/role/insomniac.rs @@ -0,0 +1,76 @@ +#[allow(unused)] +use pretty_assertions::{assert_eq, assert_ne, assert_str_eq}; + +use crate::{ + diedto::DiedTo, + game::{Game, GameSettings, OrRandom, SetupRole}, + game_test::{ + ActionPromptTitleExt, ActionResultExt, AlignmentExt, GameExt, SettingsExt, gen_players, + }, + message::night::{ActionPromptTitle, Visits}, + role::{Role, RoleTitle}, +}; + +#[test] +fn is_told_theyre_villager() { + assert_eq!(Role::Insomniac.initial_shown_role(), RoleTitle::Villager); +} + +#[test] +fn sees_visits() { + let players = gen_players(1..21); + let insomniac_player_id = players[0].player_id; + let wolf_player_id = players[1].player_id; + let seer_player_id = players[2].player_id; + let arcanist_player_id = players[3].player_id; + let mut settings = GameSettings::empty(); + settings.add_and_assign(SetupRole::Insomniac, insomniac_player_id); + settings.add_and_assign(SetupRole::Werewolf, wolf_player_id); + settings.add_and_assign(SetupRole::Seer, seer_player_id); + settings.add_and_assign(SetupRole::Arcanist, arcanist_player_id); + settings.fill_remaining_slots_with_villagers(20); + 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().title().seer(); + game.mark(game.living_villager().character_id()); + game.r#continue().seer().village(); + + game.next().title().arcanist(); + let mut villagers = game.villager_character_ids().into_iter(); + game.mark(villagers.next().unwrap()); + game.mark(villagers.next().unwrap()); + assert_eq!(game.r#continue().arcanist(), true); + + game.next_expect_day(); + + game.execute().title().wolf_pack_kill(); + game.mark(game.living_villager().character_id()); + game.r#continue().sleep(); + + game.next().title().seer(); + game.mark( + game.character_by_player_id(insomniac_player_id) + .character_id(), + ); + game.r#continue().seer().village(); + + game.next().title().arcanist(); + game.mark(game.character_by_player_id(seer_player_id).character_id()); + game.mark( + game.character_by_player_id(insomniac_player_id) + .character_id(), + ); + assert_eq!(game.r#continue().arcanist(), true); + + game.next().title().insomniac(); + assert_eq!( + game.r#continue().insomniac(), + Visits::new(Box::new([ + game.character_by_player_id(seer_player_id).identity(), + game.character_by_player_id(arcanist_player_id).identity() + ])) + ); +} diff --git a/werewolves-proto/src/game_test/role/mason.rs b/werewolves-proto/src/game_test/role/mason.rs index e6b0869..94a7bf7 100644 --- a/werewolves-proto/src/game_test/role/mason.rs +++ b/werewolves-proto/src/game_test/role/mason.rs @@ -45,12 +45,15 @@ fn recruits_decrement() { assert_eq!( game.next(), ActionPrompt::MasonsWake { - character_id: game + leader: game .character_by_player_id(mason_leader_player_id) - .identity(), - masons: Box::new([game - .character_by_player_id(recruited.player_id()) - .identity()]) + .character_id(), + masons: Box::new([ + game.character_by_player_id(mason_leader_player_id) + .identity(), + game.character_by_player_id(recruited.player_id()) + .identity(), + ]) } ); game.r#continue().sleep(); @@ -97,6 +100,19 @@ fn dies_recruiting_wolf() { game.mark(game.character_by_player_id(wolf_player_id).character_id()); game.r#continue().sleep(); + assert_eq!( + game.next(), + ActionPrompt::MasonsWake { + leader: game + .character_by_player_id(mason_leader_player_id) + .character_id(), + masons: Box::new([game + .character_by_player_id(mason_leader_player_id) + .identity()]) + } + ); + game.r#continue().sleep(); + game.next_expect_day(); assert_eq!( @@ -111,3 +127,105 @@ fn dies_recruiting_wolf() { } // todo: masons wake even if leader dead +#[test] +fn masons_wake_even_if_leader_died() { + let players = gen_players(1..10); + let mason_leader_player_id = players[0].player_id; + let wolf_player_id = players[1].player_id; + let black_knight_player_id = players[2].player_id; + let mut settings = GameSettings::empty(); + settings.add_and_assign( + SetupRole::MasonLeader { + recruits_available: NonZeroU8::new(3).unwrap(), + }, + mason_leader_player_id, + ); + settings.add_and_assign(SetupRole::Werewolf, wolf_player_id); + settings.add_and_assign(SetupRole::BlackKnight, black_knight_player_id); + settings.fill_remaining_slots_with_villagers(9); + let mut game = Game::new(&players, settings).unwrap(); + let blk_knight_cid = game + .character_by_player_id(black_knight_player_id) + .character_id(); + game.r#continue().r#continue(); + assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro); + game.r#continue().sleep(); + game.next_expect_day(); + + assert_eq!(game.execute().title(), ActionPromptTitle::WolfPackKill); + game.mark(blk_knight_cid); + game.r#continue().sleep(); + + assert_eq!(game.next().title(), ActionPromptTitle::MasonLeaderRecruit); + game.mark(blk_knight_cid); + game.r#continue().r#continue(); + + assert_eq!( + game.next(), + ActionPrompt::MasonsWake { + leader: game + .character_by_player_id(mason_leader_player_id) + .character_id(), + masons: Box::new([ + game.character_by_player_id(mason_leader_player_id) + .identity(), + game.character_by_player_id(black_knight_player_id) + .identity() + ]) + } + ); + game.r#continue().sleep(); + + game.next_expect_day(); + + game.execute().title().wolf_pack_kill(); + game.mark(blk_knight_cid); + game.r#continue().sleep(); + + let second_recruit = game.living_villager(); + assert_eq!(game.next().title(), ActionPromptTitle::MasonLeaderRecruit); + game.mark(second_recruit.character_id()); + game.r#continue().r#continue(); + + assert_eq!( + game.next(), + ActionPrompt::MasonsWake { + leader: game + .character_by_player_id(mason_leader_player_id) + .character_id(), + masons: Box::new([ + game.character_by_player_id(black_knight_player_id) + .identity(), + game.character_by_player_id(mason_leader_player_id) + .identity(), + second_recruit.identity() + ]) + } + ); + + game.r#continue().sleep(); + + game.next_expect_day(); + game.mark_for_execution( + game.character_by_player_id(mason_leader_player_id) + .character_id(), + ); + + game.execute().title().wolf_pack_kill(); + game.mark(blk_knight_cid); + game.r#continue().sleep(); + + assert_eq!( + game.next(), + ActionPrompt::MasonsWake { + leader: game + .character_by_player_id(mason_leader_player_id) + .character_id(), + masons: Box::new([ + game.character_by_player_id(black_knight_player_id) + .identity(), + second_recruit.identity() + ]) + } + ); +} diff --git a/werewolves-proto/src/game_test/role/mod.rs b/werewolves-proto/src/game_test/role/mod.rs index 9ac2005..f2aa815 100644 --- a/werewolves-proto/src/game_test/role/mod.rs +++ b/werewolves-proto/src/game_test/role/mod.rs @@ -3,6 +3,7 @@ mod black_knight; mod diseased; mod elder; mod empath; +mod insomniac; mod lone_wolf; mod mason; mod mortician; diff --git a/werewolves-proto/src/message/night.rs b/werewolves-proto/src/message/night.rs index 9808e27..df725b8 100644 --- a/werewolves-proto/src/message/night.rs +++ b/werewolves-proto/src/message/night.rs @@ -1,4 +1,4 @@ -use core::num::NonZeroU8; +use core::{num::NonZeroU8, ops::Deref}; use serde::{Deserialize, Serialize}; use werewolves_macros::{ChecksAs, Titles}; @@ -27,6 +27,7 @@ pub enum ActionType { Other, MasonRecruit, MasonsWake, + Insomniac, Beholder, RoleChange, } @@ -136,7 +137,7 @@ pub enum ActionPrompt { }, #[checks(ActionType::MasonsWake)] MasonsWake { - character_id: CharacterIdentity, + leader: CharacterId, masons: Box<[CharacterIdentity]>, }, #[checks(ActionType::MasonRecruit)] @@ -190,12 +191,15 @@ pub enum ActionPrompt { living_players: Box<[CharacterIdentity]>, marked: Option, }, + #[checks(ActionType::Insomniac)] + Insomniac { character_id: CharacterIdentity }, } impl ActionPrompt { pub(crate) fn matches_beholding(&self, target: CharacterId) -> bool { match self { - ActionPrompt::Seer { character_id, .. } + ActionPrompt::Insomniac { character_id, .. } + | ActionPrompt::Seer { character_id, .. } | ActionPrompt::Arcanist { character_id, .. } | ActionPrompt::Gravedigger { character_id, .. } | ActionPrompt::Adjudicator { character_id, .. } @@ -227,7 +231,8 @@ impl ActionPrompt { pub(crate) fn with_mark(&self, mark: CharacterId) -> Result { let mut prompt = self.clone(); match &mut prompt { - ActionPrompt::MasonsWake { .. } + ActionPrompt::Insomniac { .. } + | ActionPrompt::MasonsWake { .. } | ActionPrompt::ElderReveal { .. } | ActionPrompt::WolvesIntro { .. } | ActionPrompt::RoleChange { .. } @@ -435,7 +440,25 @@ pub enum ActionResult { Arcanist { same: bool }, GraveDigger(Option), Mortician(DiedToTitle), + Insomniac(Visits), Empath { scapegoat: bool }, GoBackToSleep, Continue, } + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Visits(Box<[CharacterIdentity]>); + +impl Visits { + pub(crate) const fn new(visits: Box<[CharacterIdentity]>) -> Self { + Self(visits) + } +} + +impl Deref for Visits { + type Target = [CharacterIdentity]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/werewolves-proto/src/role.rs b/werewolves-proto/src/role.rs index cca8e47..ff76893 100644 --- a/werewolves-proto/src/role.rs +++ b/werewolves-proto/src/role.rs @@ -44,14 +44,12 @@ pub enum Role { Beholder, #[checks(Alignment::Village)] #[checks("powerful")] - #[checks("is_mentor")] MasonLeader { recruits_available: u8, recruits: Box<[CharacterId]>, }, #[checks(Alignment::Village)] #[checks("powerful")] - #[checks("is_mentor")] Empath { cursed: bool }, #[checks(Alignment::Village)] #[checks("powerful")] @@ -115,6 +113,9 @@ pub enum Role { woken_for_reveal: bool, lost_protection_night: Option, }, + #[checks(Alignment::Village)] + #[checks("powerful")] + Insomniac, #[checks(Alignment::Wolves)] #[checks("killer")] @@ -147,14 +148,15 @@ impl Role { /// [RoleTitle] as shown to the player on role assignment pub const fn initial_shown_role(&self) -> RoleTitle { match self { - Role::Apprentice(_) | Role::Elder { .. } => RoleTitle::Villager, + Role::Apprentice(_) | Role::Elder { .. } | Role::Insomniac => RoleTitle::Villager, _ => self.title(), } } pub const fn wakes_night_zero(&self) -> bool { match self { - Role::PowerSeer + Role::Insomniac + | Role::PowerSeer | Role::Beholder | Role::Adjudicator | Role::DireWolf @@ -211,7 +213,8 @@ impl Role { .map(|execs| execs.iter().any(|e| e.is_wolf())) .unwrap_or_default(), - Role::PowerSeer + Role::Insomniac + | Role::PowerSeer | Role::Mortician | Role::Beholder | Role::MasonLeader { .. } diff --git a/werewolves-server/src/game.rs b/werewolves-server/src/game.rs index 0fa3c30..da0f469 100644 --- a/werewolves-server/src/game.rs +++ b/werewolves-server/src/game.rs @@ -228,7 +228,24 @@ impl GameRunner { } pub async fn next(&mut self) -> Option { - let msg = self.comms.host().recv().await.expect("host channel closed"); + let msg = match self.comms.message().await { + Ok(Message::ConnectedList(_)) => return None, + Ok(Message::Client(IdentifiedClientMessage { + identity: Identification { player_id, .. }, + .. + })) => { + if let Some(send) = self.joined_players.get_sender(player_id).await { + send.send(ServerMessage::GameInProgress).log_debug(); + } + return None; + } + Ok(Message::Host(msg)) => msg, + Err(err) => { + log::error!("game next message: {err}"); + return None; + } + }; + match self.host_message(msg) { Ok(resp) => { self.comms.host().send(resp).log_warn(); diff --git a/werewolves/index.scss b/werewolves/index.scss index 396723a..491468a 100644 --- a/werewolves/index.scss +++ b/werewolves/index.scss @@ -67,6 +67,19 @@ body { background: black; } +.big-screen { + align-content: center; + align-items: center; + justify-content: center; + height: 100vh; + width: 100%; + position: fixed; + left: 0; + top: 0; + margin: 0; + font-size: 2rem; +} + $link_color: #432054; $link_hover_color: hsl(280, 55%, 61%); $link_bg_color: #fff6d5; @@ -936,6 +949,11 @@ input { } &.dead { + filter: saturate(0%); + border: 1px solid rgba(255, 255, 255, 0.05); + } + + &.recent-death { $bg: rgba(128, 128, 128, 0.5); background-color: $bg; border: 1px solid color.change($bg, $alpha: 1.0); @@ -955,14 +973,6 @@ input { } } -.big-screen { - align-content: center; - align-items: center; - justify-content: center; - height: 100vh; - position: fixed; -} - .align-start { align-self: flex-start; } @@ -994,6 +1004,18 @@ input { color: white; cursor: pointer; } + + &>.submenu { + min-width: 5cm; + + .assign-list { + min-width: 5cm; + + & .submenu button { + width: inherit; + } + } + } } .add-role { @@ -1105,6 +1127,7 @@ input { position: fixed; left: 10%; top: 10%; + font-size: 1rem; .setup { display: flex; @@ -1122,6 +1145,10 @@ input { display: flex; flex-direction: column; + &.final { + margin-top: 1cm; + } + & .title { margin-bottom: 10px; } @@ -1162,10 +1189,6 @@ input { filter: contrast(120%) brightness(120%); } } - - .inactive { - filter: grayscale(100%) brightness(30%); - } } .role { @@ -1183,10 +1206,15 @@ input { } } +.inactive { + filter: grayscale(100%) brightness(30%); +} + .qrcode { display: flex; flex-direction: column; flex-wrap: nowrap; + z-index: 100; position: fixed; top: 0; left: 0; @@ -1196,7 +1224,7 @@ input { gap: 1cm; img { - height: 100%; + height: 70%; width: 100%; } @@ -1205,5 +1233,74 @@ input { // width: 100%; border: 1px solid $village_border; background-color: color.change($village_color, $alpha: 0.3); + + text-align: center; + // width: fit-content; + + &>* { + margin-top: 0.5cm; + margin-bottom: 0.5cm; + // padding: 0; + } + } +} + +.result { + display: flex; + justify-content: center; + align-content: center; + align-items: center; + flex-direction: column; + width: 100%; + height: 100%; +} + +.result-list { + display: flex; + flex-direction: row; + flex-wrap: wrap; + width: 100%; + justify-content: space-evenly; + row-gap: 0.5cm; + + .identity { + padding: 1cm; + border: 1px solid white; + font-size: 2em; + text-align: center; + } +} + +.check-icon { + width: 40vw; + height: 40vh; + // margin-top: 10%; + align-self: center; +} + +.insomniac { + display: flex; + width: 100%; + height: 100%; + flex-direction: column; + justify-content: center; + align-items: center; + + &.prompt { + font-size: 2em; + } +} + +.arcanist-result { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + width: 100%; + gap: 10px; + + img { + // flex-shrink: 1 !important; + width: max-content !important; + height: max-content !important; } } diff --git a/werewolves/src/clients/host/host.rs b/werewolves/src/clients/host/host.rs index 9c555a2..866f373 100644 --- a/werewolves/src/clients/host/host.rs +++ b/werewolves/src/clients/host/host.rs @@ -550,10 +550,8 @@ impl Component for Host { } HostEvent::SetBigScreenState(state) => { self.big_screen = state; - if self.big_screen - && let Ok(Some(root)) = gloo::utils::document().query_selector(".content") - { - root.set_class_name("content big-screen") + if self.big_screen { + gloo::utils::document_element().set_class_name("big-screen") } if state { diff --git a/werewolves/src/components/action/prompt.rs b/werewolves/src/components/action/prompt.rs index 1acbdc3..7a99ca7 100644 --- a/werewolves/src/components/action/prompt.rs +++ b/werewolves/src/components/action/prompt.rs @@ -88,6 +88,15 @@ pub fn Prompt(props: &ActionPromptProps) -> Html { }; } + ActionPrompt::Insomniac { character_id } => { + return html! { +
+ {identity_html(props, Some(character_id))} +

{"you are the insomniac"}

+ {cont} +
+ }; + } ActionPrompt::RoleChange { character_id, new_role, @@ -102,22 +111,19 @@ pub fn Prompt(props: &ActionPromptProps) -> Html { }; } - ActionPrompt::MasonsWake { - character_id, - masons, - } => { + ActionPrompt::MasonsWake { leader, masons } => { let masons = masons .into_iter() .map(|c| { + let leader = (c.character_id == *leader).then_some("leader"); let ident: PublicIdentity = c.into(); html! { - + } }) .collect::(); return html! {
- {identity_html(props, Some(character_id))}

{"these are the masons"}

{masons} diff --git a/werewolves/src/components/action/result.rs b/werewolves/src/components/action/result.rs index f44ce76..17df4b7 100644 --- a/werewolves/src/components/action/result.rs +++ b/werewolves/src/components/action/result.rs @@ -11,7 +11,7 @@ use werewolves_proto::{ }; use yew::prelude::*; -use crate::components::{Button, CoverOfDarkness, Identity}; +use crate::components::{Button, CoverOfDarkness, Icon, IconSource, Identity}; #[derive(Debug, Clone, PartialEq, Properties)] pub struct ActionResultProps { @@ -56,12 +56,16 @@ pub fn ActionResultView(props: &ActionResultProps) -> Html { } } ActionResult::Adjudicator { killer } => { - let inactive = killer.not().then_some("inactive"); - let text = if *killer { "killer" } else { "not a killer" }; + let text = if *killer { + "is a killer" + } else { + "is NOT a killer" + }; html! { <> - -

{text}

+

{"your target..."}

+ +

{text}

} } @@ -77,6 +81,25 @@ pub fn ActionResultView(props: &ActionResultProps) -> Html { ActionResult::Empath { scapegoat: false } => html! {

{"not the scapegoat"}

}, + ActionResult::Insomniac(visits) => { + let visits = visits + .iter() + .map(|v| { + let ident: PublicIdentity = v.clone().into(); + html! { + + } + }) + .collect::(); + html! { + <> +

{"tonight you were visited by..."}

+
+ {visits} +
+ + } + } ActionResult::RoleBlocked => { html! {

{"you were role blocked"}

@@ -85,17 +108,49 @@ pub fn ActionResultView(props: &ActionResultProps) -> Html { ActionResult::Seer(alignment) => html! { <>

{"the alignment was"}

-

{match alignment { - Alignment::Village => "village", - Alignment::Wolves => "wolfpack", - }}

+ {match alignment { + Alignment::Village => html!{ + <> + +

{"village"}

+ + }, + Alignment::Wolves => html!{ + <> + +

{"wolves"}

+ + }, + }} }, ActionResult::Arcanist { same } => { - let outcome = if *same { "same" } else { "different" }; + let outcome = if *same { + html! { + <> +
+ + + + +
+

{"the same"}

+ + } + } else { + html! { + <> +
+ + +
+

{"different"}

+ + } + }; html! { <> -

{"the alignments are:"}

+

{"the alignments are:"}

{outcome}

} diff --git a/werewolves/src/components/host/daytime.rs b/werewolves/src/components/host/daytime.rs index 45bfde7..672bcac 100644 --- a/werewolves/src/components/host/daytime.rs +++ b/werewolves/src/components/host/daytime.rs @@ -2,6 +2,7 @@ use core::{num::NonZeroU8, ops::Not}; use werewolves_proto::{ character::CharacterId, + game::DateTime, message::{CharacterState, PublicIdentity}, }; use yew::prelude::*; @@ -35,10 +36,25 @@ pub fn DaytimePlayerList( let chars = characters .iter() .map(|c| { + let mark_state = match c.died_to.as_ref() { + None => marked + .contains(&c.identity.character_id) + .then_some(MarkState::Marked), + Some(died_to) => match died_to.date_time() { + DateTime::Day { .. } => Some(MarkState::Dead), + DateTime::Night { number } => { + if number == day.get() - 1 { + Some(MarkState::DiedLastNight) + } else { + Some(MarkState::Dead) + } + } + }, + }; html! { } @@ -66,11 +82,28 @@ pub fn DaytimePlayerList(
} } +#[derive(Debug, Clone, PartialEq)] +pub enum MarkState { + Marked, + DiedLastNight, + Dead, +} + +impl MarkState { + pub const fn class(&self) -> &'static str { + match self { + MarkState::Marked => "marked", + MarkState::DiedLastNight => "recent-death", + MarkState::Dead => "dead", + } + } +} #[derive(Debug, Clone, PartialEq, Properties)] pub struct DaytimePlayerProps { pub character: CharacterState, - pub on_the_block: bool, + #[prop_or_default] + pub mark_state: Option, pub on_select: Option>, } @@ -78,7 +111,7 @@ pub struct DaytimePlayerProps { pub fn DaytimePlayer( DaytimePlayerProps { on_select, - on_the_block, + mark_state, character: CharacterState { player_id: _, @@ -88,8 +121,7 @@ pub fn DaytimePlayer( }, }: &DaytimePlayerProps, ) -> Html { - let dead = died_to.is_some().then_some("dead"); - let marked = on_the_block.then_some("marked"); + let class = mark_state.as_ref().map(|s| s.class()); let character_id = identity.character_id; let on_click: Callback<_> = died_to .is_none() @@ -102,7 +134,7 @@ pub fn DaytimePlayer( .unwrap_or_default(); let identity: PublicIdentity = identity.into(); html! { - } diff --git a/werewolves/src/components/host/setup.rs b/werewolves/src/components/host/setup.rs index 9bc52c9..99f496d 100644 --- a/werewolves/src/components/host/setup.rs +++ b/werewolves/src/components/host/setup.rs @@ -67,7 +67,7 @@ pub fn Setup(SetupProps { settings }: &SetupProps) -> Html {
{categories}
-
+
{power_roles_count} {"Power roles from..."}
diff --git a/werewolves/src/components/icon.rs b/werewolves/src/components/icon.rs new file mode 100644 index 0000000..f685234 --- /dev/null +++ b/werewolves/src/components/icon.rs @@ -0,0 +1,67 @@ +use yew::prelude::*; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum IconSource { + Village, + Wolves, + Killer, + Powerful, +} + +impl IconSource { + pub const fn source(&self) -> &'static str { + match self { + IconSource::Village => "/img/village.svg", + IconSource::Wolves => "/img/wolf.svg", + IconSource::Killer => "/img/killer.svg", + IconSource::Powerful => "/img/powerful.svg", + } + } + pub const fn class(&self) -> Option<&'static str> { + match self { + IconSource::Village | IconSource::Wolves => None, + IconSource::Killer => Some("killer"), + IconSource::Powerful => Some("powerful"), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub enum IconType { + SetupList, + #[default] + RoleCheck, +} + +impl IconType { + pub const fn class(&self) -> &'static str { + match self { + IconType::SetupList => "icon", + IconType::RoleCheck => "check-icon", + } + } +} + +#[derive(Debug, Clone, PartialEq, Properties)] +pub struct IconProps { + pub source: IconSource, + #[prop_or_default] + pub inactive: bool, + #[prop_or_default] + pub icon_type: IconType, +} +#[function_component] +pub fn Icon( + IconProps { + source, + inactive, + icon_type, + }: &IconProps, +) -> Html { + html! { + + } +} diff --git a/werewolves/src/components/identity.rs b/werewolves/src/components/identity.rs index 170b181..babf763 100644 --- a/werewolves/src/components/identity.rs +++ b/werewolves/src/components/identity.rs @@ -5,7 +5,7 @@ use yew::prelude::*; pub struct IdentityProps { pub ident: PublicIdentity, #[prop_or_default] - pub class: Option, + pub class: Classes, } #[function_component] @@ -29,7 +29,7 @@ pub fn Identity(props: &IdentityProps) -> Html { .map(|n| n.to_string()) .unwrap_or_else(|| String::from("???")); html! { -
+

{number}

{name}

{pronouns} diff --git a/werewolves/src/main.rs b/werewolves/src/main.rs index 39bc5b5..0fc594c 100644 --- a/werewolves/src/main.rs +++ b/werewolves/src/main.rs @@ -47,6 +47,8 @@ fn main() { let error_callback = Callback::from(move |err: Option| cb_clone.send_message(err)); + gloo::utils::document().set_title("werewolves"); + if path.starts_with("/host") { let host = yew::Renderer::::with_root(app_element).render(); host.send_message(HostEvent::SetErrorCallback(error_callback));