diff --git a/Cargo.lock b/Cargo.lock index 3cb0498..f219eb8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2427,8 +2427,10 @@ dependencies = [ name = "werewolves-proto" version = "0.1.0" dependencies = [ + "colored", "log", "pretty_assertions", + "pretty_env_logger", "rand", "serde", "serde_json", diff --git a/werewolves-proto/Cargo.toml b/werewolves-proto/Cargo.toml index 9def041..b339c11 100644 --- a/werewolves-proto/Cargo.toml +++ b/werewolves-proto/Cargo.toml @@ -15,3 +15,5 @@ werewolves-macros = { path = "../werewolves-macros" } [dev-dependencies] pretty_assertions = { version = "1" } +pretty_env_logger = { version = "0.5" } +colored = { version = "3.0" } diff --git a/werewolves-proto/src/game/mod.rs b/werewolves-proto/src/game/mod.rs index da12aee..dba5239 100644 --- a/werewolves-proto/src/game/mod.rs +++ b/werewolves-proto/src/game/mod.rs @@ -18,6 +18,7 @@ use crate::{ message::{ CharacterState, Identification, host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage}, + night::ActionResponse, }, player::CharacterId, }; @@ -107,18 +108,15 @@ impl Game { } (GameState::Night { night }, HostGameMessage::GetState) => { if let Some(res) = night.current_result() { - let char = night.current_character().unwrap(); return Ok(ServerToHostMessage::ActionResult( - char.public_identity().clone(), + night + .current_character() + .map(|c| c.public_identity().clone()), res.clone(), )); } if let Some(prompt) = night.current_prompt() { - let char = night.current_character().unwrap(); - return Ok(ServerToHostMessage::ActionPrompt( - char.public_identity().clone(), - prompt.clone(), - )); + return Ok(ServerToHostMessage::ActionPrompt(prompt.clone())); } match night.next() { Ok(_) => self.process(HostGameMessage::GetState), @@ -140,7 +138,9 @@ impl Game { HostGameMessage::Night(HostNightMessage::ActionResponse(resp)), ) => match night.received_response(resp.clone()) { Ok(res) => Ok(ServerToHostMessage::ActionResult( - night.current_character().unwrap().public_identity().clone(), + night + .current_character() + .map(|c| c.public_identity().clone()), res, )), Err(GameError::NightNeedsNext) => match night.next() { diff --git a/werewolves-proto/src/game/night.rs b/werewolves-proto/src/game/night.rs index 590259b..c2d8582 100644 --- a/werewolves-proto/src/game/night.rs +++ b/werewolves-proto/src/game/night.rs @@ -12,21 +12,11 @@ use crate::{ DateTime, Village, kill::{self, ChangesLookup}, }, - message::night::{ActionPrompt, ActionResponse, ActionResult}, + message::night::{ActionPrompt, ActionResponse, ActionResult, ActionType}, player::{Character, CharacterId, Protection}, role::{PreviousGuardianAction, Role, RoleBlock, RoleTitle}, }; -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Night { - village: Village, - night: u8, - action_queue: VecDeque<(ActionPrompt, Character)>, - used_actions: Vec<(ActionPrompt, Character)>, - changes: Vec, - night_state: NightState, -} - #[derive(Debug, Clone, Serialize, Deserialize, Extract)] pub enum NightChange { RoleChange(CharacterId, RoleTitle), @@ -76,12 +66,21 @@ impl From for ActionResult { enum NightState { Active { current_prompt: ActionPrompt, - current_char: CharacterId, current_result: Option, }, Complete, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Night { + village: Village, + night: u8, + action_queue: VecDeque, + used_actions: Vec, + changes: Vec, + night_state: NightState, +} + impl Night { pub fn new(village: Village) -> Result { let night = match village.date_time() { @@ -92,51 +91,33 @@ impl Night { let mut action_queue = village .characters() .into_iter() - .map(|c| c.night_action_prompt(&village).map(|prompt| (prompt, c))) + .map(|c| c.night_action_prompt(&village)) .collect::>>()? .into_iter() - .filter_map(|(prompt, char)| prompt.map(|p| (p, char))) + .flatten() + .chain((night > 0).then(|| ActionPrompt::WolfPackKill { + living_villagers: village.living_villagers(), + })) .collect::>(); - action_queue.sort_by(|(left_prompt, _), (right_prompt, _)| { + action_queue.sort_by(|left_prompt, right_prompt| { left_prompt .partial_cmp(right_prompt) .unwrap_or(core::cmp::Ordering::Equal) }); - let action_queue = VecDeque::from(action_queue); - let (current_prompt, current_char) = if night == 0 { - ( - ActionPrompt::WolvesIntro { - wolves: village - .living_wolf_pack_players() - .into_iter() - .map(|w| (w.target(), w.role().title())) - .collect(), - }, - village + let mut action_queue = VecDeque::from(action_queue); + + if night == 0 { + action_queue.push_front(ActionPrompt::WolvesIntro { + wolves: village .living_wolf_pack_players() .into_iter() - .next() - .unwrap() - .character_id() - .clone(), - ) - } else { - ( - ActionPrompt::WolfPackKill { - living_villagers: village.living_villagers(), - }, - village - .living_wolf_pack_players() - .into_iter() - .next() - .unwrap() - .character_id() - .clone(), - ) - }; + .map(|w| (w.target(), w.role().title())) + .collect(), + }); + } + // let current_prompt = action_queue.pop_front().ok_or(GameError::NoNightActions)?; let night_state = NightState::Active { - current_char, - current_prompt, + current_prompt: ActionPrompt::CoverOfDarkness, current_result: None, }; let mut changes = Vec::new(); @@ -175,44 +156,33 @@ impl Night { } pub fn previous_state(&mut self) -> Result<()> { - let (prev_act, prev_char) = self.used_actions.pop().ok_or(GameError::NoPreviousState)?; + let prev_act = self.used_actions.pop().ok_or(GameError::NoPreviousState)?; log::info!("loading previous prompt: {prev_act:?}"); match &self.night_state { NightState::Active { current_prompt, - current_char, current_result: Some(current_result), } => { - // if let Some(last_change) = self.changes.pop() { - - // } log::info!("removing current result: {current_result:?}"); self.night_state = NightState::Active { current_prompt: current_prompt.clone(), - current_char: current_char.clone(), current_result: None, }; } NightState::Active { current_prompt, - current_char, current_result: None, } => { log::info!("pushing current prompt to front of action queue: {current_prompt:?}"); - self.action_queue.push_front(( - current_prompt.clone(), - self.village.character_by_id(current_char).unwrap().clone(), - )); + self.action_queue.push_front(current_prompt.clone()); self.night_state = NightState::Active { current_prompt: prev_act, - current_char: prev_char.character_id().clone(), current_result: None, } } NightState::Complete => { self.night_state = NightState::Active { current_prompt: prev_act, - current_char: prev_char.character_id().clone(), current_result: None, }; } @@ -220,6 +190,11 @@ impl Night { Ok(()) } + #[cfg(test)] + pub fn action_queue(&self) -> Box<[ActionPrompt]> { + self.action_queue.iter().cloned().collect() + } + pub fn collect_completed(&self) -> Result { if !matches!(self.night_state, NightState::Complete) { return Err(GameError::NotEndOfNight); @@ -278,14 +253,6 @@ impl Night { night: NonZeroU8::new(self.night).unwrap(), }); // role change pushed in [apply_shapeshift] - - // let target = new_village.find_by_character_id_mut(target).unwrap(); - // target - // .role_change( - // RoleTitle::Werewolf, - // DateTime::Night { number: self.night }, - // ) - // .unwrap(); } } NightChange::RoleBlock { @@ -305,7 +272,7 @@ impl Night { Ok(new_village) } - fn apply_shapeshift(&mut self, source: &CharacterId) -> Result { + fn apply_shapeshift(&mut self, source: &CharacterId) -> Result<()> { if let Some(kill_target) = self.changes.iter().find_map(|c| match c { NightChange::Kill { target, @@ -325,25 +292,9 @@ impl Night { _ => false, }) { // there is protection, so the kill doesn't happen -> no shapeshift - return Ok(ActionResult::GoBackToSleep); + return Ok(()); } - // if let Some(kill) = self.changes.iter_mut().find(|c| { - // matches!( - // c, - // NightChange::Kill { - // target: _, - // died_to: DiedTo::Wolfpack { night: _ } - // } - // ) - // }) { - // *kill = NightChange::Kill { - // target: source.clone(), - // died_to: DiedTo::Shapeshift { - // into: kill_target.clone(), - // night: NonZeroU8::new(self.night).unwrap(), - // }, - // } - // } + if self.changes.iter_mut().any(|c| { matches!( c, @@ -367,55 +318,68 @@ impl Night { self.changes.push(NightChange::Shapeshift { source: source.clone(), }); - self.action_queue.push_front(( - ActionPrompt::RoleChange { - new_role: RoleTitle::Werewolf, - }, - self.village - .find_by_character_id(&kill_target) - .unwrap() - .clone(), - )); + self.action_queue.push_front(ActionPrompt::RoleChange { + new_role: RoleTitle::Werewolf, + character_id: self + .village + .character_by_id(&kill_target) + .ok_or(GameError::NoMatchingCharacterFound)? + .character_identity(), + }); } - Ok(ActionResult::GoBackToSleep) + // Remove any further shapeshift prompts from the queue + let mut new_queue = VecDeque::new(); + while let Some(prompt) = self.action_queue.pop_back() { + match &prompt { + ActionPrompt::Shapeshifter { character_id: _ } => {} + _ => new_queue.push_front(prompt), + } + } + self.action_queue = new_queue; + Ok(()) } pub fn received_response(&mut self, resp: ActionResponse) -> Result { - if let ( - NightState::Active { - current_prompt: ActionPrompt::WolvesIntro { wolves: _ }, - current_char: _, - current_result, - }, - ActionResponse::WolvesIntroAck, - ) = (&mut self.night_state, &resp) + if let NightState::Active { + current_prompt: ActionPrompt::CoverOfDarkness, + current_result: None, + } = &mut self.night_state + && let ActionResponse::ClearCoverOfDarkness = &resp { - *current_result = Some(ActionResult::WolvesIntroDone); - return Ok(ActionResult::WolvesIntroDone); + self.night_state = NightState::Active { + current_prompt: ActionPrompt::CoverOfDarkness, + current_result: Some(ActionResult::Continue), + }; + return Ok(ActionResult::Continue); } - match self.received_response_with_role_blocks(resp) { - Ok((result, Some(change))) => { + match self.received_response_with_role_blocks(resp)? { + (result, Some(change)) => { match &mut self.night_state { NightState::Active { current_prompt: _, - current_char: _, current_result, } => current_result.replace(result.clone()), NightState::Complete => return Err(GameError::NightOver), }; if let NightChange::Shapeshift { source } = &change { - // needs to be resolved _now_ - return self.apply_shapeshift(source); + // needs to be resolved _now_ so that the target can be woken + // for the role change with the wolves + self.apply_shapeshift(source)?; + return Ok(self + .action_queue + .iter() + .next() + .and_then(|a| a.is_wolfy().then_some(ActionResult::Continue)) + .unwrap_or(ActionResult::GoBackToSleep)); } self.changes.push(change); Ok(result) } - Ok((result, None)) => { + (result, None) => { match &mut self.night_state { NightState::Active { current_prompt: _, - current_char: _, current_result, } => { current_result.replace(result.clone()); @@ -424,19 +388,66 @@ impl Night { }; Ok(result) } - Err(err) => Err(err), } } + + fn received_response_consecutive_wolves_dont_sleep( + &self, + resp: ActionResponse, + ) -> Result { + let current_wolfy = self.current_prompt().unwrap().is_wolfy(); + let next_wolfy = self + .action_queue + .iter() + .next() + .map(|a| a.is_wolfy()) + .unwrap_or_default(); + + match ( + self.received_response_inner(resp)?, + current_wolfy, + next_wolfy, + ) { + ( + ResponseOutcome { + result: ActionResult::GoBackToSleep, + change: Some(NightChange::Shapeshift { source }), + unless, + }, + true, + _, + ) => Ok(ResponseOutcome { + result: ActionResult::Continue, + change: Some(NightChange::Shapeshift { source }), + unless, + }), + ( + ResponseOutcome { + result: ActionResult::GoBackToSleep, + change, + unless, + }, + true, + true, + ) => Ok(ResponseOutcome { + result: ActionResult::Continue, + change, + unless, + }), + (outcome, _, _) => Ok(outcome), + } + } + fn received_response_with_role_blocks( &self, resp: ActionResponse, ) -> Result<(ActionResult, Option)> { - match self.received_response_inner(resp) { - Ok(ResponseOutcome { + match self.received_response_consecutive_wolves_dont_sleep(resp)? { + ResponseOutcome { result, change, unless: Some(Unless::TargetBlocked(unless_blocked)), - }) => { + } => { if self.changes.iter().any(|c| match c { NightChange::RoleBlock { source: _, @@ -450,11 +461,11 @@ impl Night { Ok((result, change)) } } - Ok(ResponseOutcome { + ResponseOutcome { result, change, unless: Some(Unless::TargetsBlocked(unless_blocked1, unless_blocked2)), - }) => { + } => { if self.changes.iter().any(|c| match c { NightChange::RoleBlock { source: _, @@ -469,280 +480,47 @@ impl Night { } } - Ok(ResponseOutcome { + ResponseOutcome { result, change, unless: None, - }) => Ok((result, change)), - Err(err) => Err(err), + } => Ok((result, change)), } } fn received_response_inner(&self, resp: ActionResponse) -> Result { - let (current_prompt, current_char) = match &self.night_state { + let current_prompt = match &self.night_state { NightState::Active { current_prompt: _, - current_char: _, current_result: Some(_), } => return Err(GameError::NightNeedsNext), NightState::Active { current_prompt, - current_char, current_result: None, - } => (current_prompt, current_char), + } => current_prompt, NightState::Complete => return Err(GameError::NightOver), }; match (current_prompt, resp) { - (ActionPrompt::RoleChange { new_role }, ActionResponse::RoleChangeAck) => { - Ok(ResponseOutcome { - result: ActionResult::GoBackToSleep, - change: Some(NightChange::RoleChange(current_char.clone(), *new_role)), - unless: None, - }) - } - (ActionPrompt::Seer { living_players }, ActionResponse::Seer(target)) => { - if !living_players.iter().any(|p| p.character_id == target) { - return Err(GameError::InvalidTarget); - } - - Ok(ResponseOutcome { - result: ActionResult::Seer( - self.village - .character_by_id(&target) - .unwrap() - .role() - .alignment(), - ), - change: None, - unless: Some(Unless::TargetBlocked(target)), - }) - } ( - ActionPrompt::Arcanist { living_players }, - ActionResponse::Arcanist(target1, target2), - ) => { - if !(living_players.iter().any(|p| p.character_id == target1) - && living_players.iter().any(|p| p.character_id == target2)) - { - return Err(GameError::InvalidTarget); - } - let target1_align = self - .village - .character_by_id(&target1) - .unwrap() - .role() - .alignment(); - let target2_align = self - .village - .character_by_id(&target2) - .unwrap() - .role() - .alignment(); - Ok(ResponseOutcome { - result: ActionResult::Arcanist { - same: target1_align == target2_align, - }, - change: None, - unless: Some(Unless::TargetsBlocked(target1, target2)), - }) - } - (ActionPrompt::Gravedigger { dead_players }, ActionResponse::Gravedigger(target)) => { - if !dead_players.iter().any(|p| p.character_id == target) { - return Err(GameError::InvalidTarget); - } - let target_role = self.village.character_by_id(&target).unwrap().role(); - Ok(ResponseOutcome { - result: ActionResult::GraveDigger( - matches!( - target_role, - Role::Shapeshifter { - shifted_into: Some(_) - } - ) - .not() - .then(|| target_role.title()), - ), - change: None, - unless: Some(Unless::TargetBlocked(target)), - }) - } - ( - ActionPrompt::Hunter { - current_target: _, - living_players, + ActionPrompt::RoleChange { + character_id: current_char, + new_role, }, - ActionResponse::Hunter(target), - ) => { - if !living_players.iter().any(|p| p.character_id == target) { - return Err(GameError::InvalidTarget); - } - Ok(ResponseOutcome { - result: ActionResult::GoBackToSleep, - change: Some(NightChange::HunterTarget { - target: target.clone(), - source: current_char.clone(), - }), - unless: Some(Unless::TargetBlocked(target)), - }) - } - (ActionPrompt::Militia { living_players }, ActionResponse::Militia(Some(target))) => { - if !living_players.iter().any(|p| p.character_id == target) { - return Err(GameError::InvalidTarget); - } - let night = if let Some(night) = NonZeroU8::new(self.night) { - night - } else { - return Ok(ResponseOutcome { - result: ActionResult::GoBackToSleep, - change: None, - unless: None, - }); - }; - Ok(ResponseOutcome { - result: ActionResult::GoBackToSleep, - change: Some(NightChange::Kill { - target: target.clone(), - died_to: DiedTo::Militia { - night, - killer: current_char.clone(), - }, - }), - unless: Some(Unless::TargetBlocked(target)), - }) - } - (ActionPrompt::Militia { living_players: _ }, ActionResponse::Militia(None)) => { - Ok(ResponseOutcome { - result: ActionResult::GoBackToSleep, - change: None, - unless: None, - }) - } - ( - ActionPrompt::MapleWolf { - kill_or_die, - living_players, - }, - ActionResponse::MapleWolf(Some(target)), - ) => { - if !living_players.iter().any(|p| p.character_id == target) { - return Err(GameError::InvalidTarget); - } - let night = if let Some(night) = NonZeroU8::new(self.night) { - night - } else { - return Ok(ResponseOutcome { - result: ActionResult::GoBackToSleep, - change: None, - unless: None, - }); - }; - Ok(ResponseOutcome { - result: ActionResult::GoBackToSleep, - change: Some(NightChange::Kill { - target: target.clone(), - died_to: DiedTo::MapleWolf { - night, - source: current_char.clone(), - starves_if_fails: *kill_or_die, - }, - }), - unless: Some(Unless::TargetBlocked(target)), - }) - } - ( - ActionPrompt::MapleWolf { - kill_or_die: true, - living_players: _, - }, - ActionResponse::MapleWolf(None), - ) => { - let night = if let Some(night) = NonZeroU8::new(self.night) { - night - } else { - return Ok(ResponseOutcome { - result: ActionResult::GoBackToSleep, - change: None, - unless: None, - }); - }; - Ok(ResponseOutcome { - result: ActionResult::GoBackToSleep, - change: Some(NightChange::Kill { - target: current_char.clone(), - died_to: DiedTo::MapleWolfStarved { night }, - }), - unless: None, - }) - } - - ( - ActionPrompt::MapleWolf { - kill_or_die: false, - living_players: _, - }, - ActionResponse::MapleWolf(None), + ActionResponse::RoleChangeAck, ) => Ok(ResponseOutcome { result: ActionResult::GoBackToSleep, - change: None, + change: Some(NightChange::RoleChange( + current_char.character_id.clone(), + *new_role, + )), unless: None, }), - ( - ActionPrompt::Guardian { - previous: Some(previous), - living_players, - }, - ActionResponse::Guardian(target), - ) => { - if !living_players.iter().any(|p| p.character_id == target) { - return Err(GameError::InvalidTarget); - } - let guarding = match previous { - PreviousGuardianAction::Protect(prev_target) => { - prev_target.character_id == target - } - PreviousGuardianAction::Guard(prev_target) => { - if prev_target.character_id == target { - return Err(GameError::InvalidTarget); - } else { - false - } - } - }; - + (ActionPrompt::WolvesIntro { wolves: _ }, ActionResponse::WolvesIntroAck) => { Ok(ResponseOutcome { result: ActionResult::GoBackToSleep, - change: Some(NightChange::Protection { - target: target.clone(), - protection: Protection::Guardian { - source: current_char.clone(), - guarding, - }, - }), - unless: Some(Unless::TargetBlocked(target)), - }) - } - ( - ActionPrompt::Guardian { - previous: None, - living_players, - }, - ActionResponse::Guardian(target), - ) => { - if !living_players.iter().any(|p| p.character_id == target) { - return Err(GameError::InvalidTarget); - } - - Ok(ResponseOutcome { - result: ActionResult::GoBackToSleep, - change: Some(NightChange::Protection { - target: target.clone(), - protection: Protection::Guardian { - source: current_char.clone(), - guarding: false, - }, - }), - unless: Some(Unless::TargetBlocked(target)), + change: None, + unless: None, }) } ( @@ -774,30 +552,313 @@ impl Night { unless: Some(Unless::TargetBlocked(target)), }) } - (ActionPrompt::Shapeshifter, ActionResponse::Shapeshifter(true)) => { + ( + ActionPrompt::Seer { + character_id: _, + living_players, + }, + ActionResponse::Seer(target), + ) => { + if !living_players.iter().any(|p| p.character_id == target) { + return Err(GameError::InvalidTarget); + } + + Ok(ResponseOutcome { + result: ActionResult::Seer( + self.village + .character_by_id(&target) + .unwrap() + .role() + .alignment(), + ), + change: None, + unless: Some(Unless::TargetBlocked(target)), + }) + } + ( + ActionPrompt::Arcanist { + character_id: _, + living_players, + }, + ActionResponse::Arcanist(target1, target2), + ) => { + if !(living_players.iter().any(|p| p.character_id == target1) + && living_players.iter().any(|p| p.character_id == target2)) + { + return Err(GameError::InvalidTarget); + } + let target1_align = self + .village + .character_by_id(&target1) + .unwrap() + .role() + .alignment(); + let target2_align = self + .village + .character_by_id(&target2) + .unwrap() + .role() + .alignment(); + Ok(ResponseOutcome { + result: ActionResult::Arcanist { + same: target1_align == target2_align, + }, + change: None, + unless: Some(Unless::TargetsBlocked(target1, target2)), + }) + } + ( + ActionPrompt::Gravedigger { + character_id: _, + dead_players, + }, + ActionResponse::Gravedigger(target), + ) => { + if !dead_players.iter().any(|p| p.character_id == target) { + return Err(GameError::InvalidTarget); + } + let target_role = self.village.character_by_id(&target).unwrap().role(); + Ok(ResponseOutcome { + result: ActionResult::GraveDigger( + matches!( + target_role, + Role::Shapeshifter { + shifted_into: Some(_) + } + ) + .not() + .then(|| target_role.title()), + ), + change: None, + unless: Some(Unless::TargetBlocked(target)), + }) + } + ( + ActionPrompt::Hunter { + character_id: current_char, + current_target: _, + living_players, + }, + ActionResponse::Hunter(target), + ) => { + if !living_players.iter().any(|p| p.character_id == target) { + return Err(GameError::InvalidTarget); + } Ok(ResponseOutcome { result: ActionResult::GoBackToSleep, - change: Some(NightChange::Shapeshift { - source: current_char.clone(), + change: Some(NightChange::HunterTarget { + target: target.clone(), + source: current_char.character_id.clone(), + }), + unless: Some(Unless::TargetBlocked(target)), + }) + } + ( + ActionPrompt::Militia { + character_id: current_char, + living_players, + }, + ActionResponse::Militia(Some(target)), + ) => { + if !living_players.iter().any(|p| p.character_id == target) { + return Err(GameError::InvalidTarget); + } + let night = if let Some(night) = NonZeroU8::new(self.night) { + night + } else { + return Ok(ResponseOutcome { + result: ActionResult::GoBackToSleep, + change: None, + unless: None, + }); + }; + Ok(ResponseOutcome { + result: ActionResult::GoBackToSleep, + change: Some(NightChange::Kill { + target: target.clone(), + died_to: DiedTo::Militia { + night, + killer: current_char.character_id.clone(), + }, + }), + unless: Some(Unless::TargetBlocked(target)), + }) + } + ( + ActionPrompt::Militia { + character_id: _, + living_players: _, + }, + ActionResponse::Militia(None), + ) => Ok(ResponseOutcome { + result: ActionResult::GoBackToSleep, + change: None, + unless: None, + }), + ( + ActionPrompt::MapleWolf { + character_id: current_char, + kill_or_die, + living_players, + }, + ActionResponse::MapleWolf(Some(target)), + ) => { + if !living_players.iter().any(|p| p.character_id == target) { + return Err(GameError::InvalidTarget); + } + let night = if let Some(night) = NonZeroU8::new(self.night) { + night + } else { + return Ok(ResponseOutcome { + result: ActionResult::GoBackToSleep, + change: None, + unless: None, + }); + }; + Ok(ResponseOutcome { + result: ActionResult::GoBackToSleep, + change: Some(NightChange::Kill { + target: target.clone(), + died_to: DiedTo::MapleWolf { + night, + source: current_char.character_id.clone(), + starves_if_fails: *kill_or_die, + }, + }), + unless: Some(Unless::TargetBlocked(target)), + }) + } + ( + ActionPrompt::MapleWolf { + character_id: current_char, + kill_or_die: true, + living_players: _, + }, + ActionResponse::MapleWolf(None), + ) => { + let night = if let Some(night) = NonZeroU8::new(self.night) { + night + } else { + return Ok(ResponseOutcome { + result: ActionResult::GoBackToSleep, + change: None, + unless: None, + }); + }; + Ok(ResponseOutcome { + result: ActionResult::GoBackToSleep, + change: Some(NightChange::Kill { + target: current_char.character_id.clone(), + died_to: DiedTo::MapleWolfStarved { night }, }), unless: None, }) } + + ( + ActionPrompt::MapleWolf { + character_id: _, + kill_or_die: false, + living_players: _, + }, + ActionResponse::MapleWolf(None), + ) => Ok(ResponseOutcome { + result: ActionResult::GoBackToSleep, + change: None, + unless: None, + }), + ( + ActionPrompt::Guardian { + character_id: current_char, + previous: Some(previous), + living_players, + }, + ActionResponse::Guardian(target), + ) => { + if !living_players.iter().any(|p| p.character_id == target) { + return Err(GameError::InvalidTarget); + } + let guarding = match previous { + PreviousGuardianAction::Protect(prev_target) => { + prev_target.character_id == target + } + PreviousGuardianAction::Guard(prev_target) => { + if prev_target.character_id == target { + return Err(GameError::InvalidTarget); + } else { + false + } + } + }; + + Ok(ResponseOutcome { + result: ActionResult::GoBackToSleep, + change: Some(NightChange::Protection { + target: target.clone(), + protection: Protection::Guardian { + source: current_char.character_id.clone(), + guarding, + }, + }), + unless: Some(Unless::TargetBlocked(target)), + }) + } + ( + ActionPrompt::Guardian { + character_id: current_char, + previous: None, + living_players, + }, + ActionResponse::Guardian(target), + ) => { + if !living_players.iter().any(|p| p.character_id == target) { + return Err(GameError::InvalidTarget); + } + + Ok(ResponseOutcome { + result: ActionResult::GoBackToSleep, + change: Some(NightChange::Protection { + target: target.clone(), + protection: Protection::Guardian { + source: current_char.character_id.clone(), + guarding: false, + }, + }), + unless: Some(Unless::TargetBlocked(target)), + }) + } + ( + ActionPrompt::Shapeshifter { + character_id: current_char, + }, + ActionResponse::Shapeshifter(true), + ) => Ok(ResponseOutcome { + result: ActionResult::GoBackToSleep, + change: Some(NightChange::Shapeshift { + source: current_char.character_id.clone(), + }), + unless: None, + }), ( ActionPrompt::AlphaWolf { + character_id: _, living_villagers: _, }, ActionResponse::AlphaWolf(None), ) - | (ActionPrompt::Shapeshifter, ActionResponse::Shapeshifter(false)) => { - Ok(ResponseOutcome { - result: ActionResult::GoBackToSleep, - change: None, - unless: None, - }) - } + | ( + ActionPrompt::Shapeshifter { character_id: _ }, + ActionResponse::Shapeshifter(false), + ) => Ok(ResponseOutcome { + result: ActionResult::GoBackToSleep, + change: None, + unless: None, + }), ( - ActionPrompt::AlphaWolf { living_villagers }, + ActionPrompt::AlphaWolf { + character_id: current_char, + living_villagers, + }, ActionResponse::AlphaWolf(Some(target)), ) => { let night = match NonZeroU8::new(self.night) { @@ -818,28 +879,40 @@ impl Night { change: Some(NightChange::Kill { target: target.clone(), died_to: DiedTo::AlphaWolf { - killer: current_char.clone(), + killer: current_char.character_id.clone(), night, }, }), unless: Some(Unless::TargetBlocked(target)), }) } - (ActionPrompt::DireWolf { living_players }, ActionResponse::Direwolf(target)) => { + ( + ActionPrompt::DireWolf { + character_id: current_char, + living_players, + }, + ActionResponse::Direwolf(target), + ) => { if !living_players.iter().any(|p| p.character_id == target) { return Err(GameError::InvalidTarget); } Ok(ResponseOutcome { result: ActionResult::GoBackToSleep, change: Some(NightChange::RoleBlock { - source: current_char.clone(), + source: current_char.character_id.clone(), target, block_type: RoleBlock::Direwolf, }), unless: None, }) } - (ActionPrompt::Protector { targets }, ActionResponse::Protector(target)) => { + ( + ActionPrompt::Protector { + character_id: current_char, + targets, + }, + ActionResponse::Protector(target), + ) => { if !targets.iter().any(|p| p.character_id == target) { return Err(GameError::InvalidTarget); } @@ -849,7 +922,7 @@ impl Night { change: Some(NightChange::Protection { target: target.clone(), protection: Protection::Protector { - source: current_char.clone(), + source: current_char.character_id.clone(), }, }), unless: Some(Unless::TargetBlocked(target)), @@ -858,21 +931,67 @@ impl Night { // For other responses that are invalid -- this allows the match to error // if a new prompt is added - (ActionPrompt::RoleChange { new_role: _ }, _) - | (ActionPrompt::Seer { living_players: _ }, _) - | (ActionPrompt::Protector { targets: _ }, _) - | (ActionPrompt::Arcanist { living_players: _ }, _) - | (ActionPrompt::Gravedigger { dead_players: _ }, _) + ( + ActionPrompt::WolfPackKill { + living_villagers: _, + }, + _, + ) + | (ActionPrompt::WolvesIntro { wolves: _ }, _) + | (ActionPrompt::CoverOfDarkness, _) + | ( + ActionPrompt::RoleChange { + character_id: _, + new_role: _, + }, + _, + ) + | ( + ActionPrompt::Seer { + character_id: _, + living_players: _, + }, + _, + ) + | ( + ActionPrompt::Protector { + character_id: _, + targets: _, + }, + _, + ) + | ( + ActionPrompt::Arcanist { + character_id: _, + living_players: _, + }, + _, + ) + | ( + ActionPrompt::Gravedigger { + character_id: _, + dead_players: _, + }, + _, + ) | ( ActionPrompt::Hunter { + character_id: _, current_target: _, living_players: _, }, _, ) - | (ActionPrompt::Militia { living_players: _ }, _) + | ( + ActionPrompt::Militia { + character_id: _, + living_players: _, + }, + _, + ) | ( ActionPrompt::MapleWolf { + character_id: _, kill_or_die: _, living_players: _, }, @@ -880,31 +999,27 @@ impl Night { ) | ( ActionPrompt::Guardian { + character_id: _, previous: _, living_players: _, }, _, ) - | ( - ActionPrompt::WolfPackKill { - living_villagers: _, - }, - _, - ) - | (ActionPrompt::Shapeshifter, _) + | (ActionPrompt::Shapeshifter { character_id: _ }, _) | ( ActionPrompt::AlphaWolf { + character_id: _, living_villagers: _, }, _, ) - | (ActionPrompt::DireWolf { living_players: _ }, _) => { - Err(GameError::InvalidMessageForGameState) - } - - (ActionPrompt::WolvesIntro { wolves: _ }, _) => { - Err(GameError::InvalidMessageForGameState) - } + | ( + ActionPrompt::DireWolf { + character_id: _, + living_players: _, + }, + _, + ) => Err(GameError::InvalidMessageForGameState), } } @@ -916,7 +1031,6 @@ impl Night { match &self.night_state { NightState::Active { current_prompt: _, - current_char: _, current_result, } => current_result.as_ref(), NightState::Complete => None, @@ -927,7 +1041,6 @@ impl Night { match &self.night_state { NightState::Active { current_prompt, - current_char: _, current_result: _, } => Some(current_prompt), NightState::Complete => None, @@ -937,10 +1050,63 @@ impl Night { pub const fn current_character_id(&self) -> Option<&CharacterId> { match &self.night_state { NightState::Active { - current_prompt: _, - current_char, + current_prompt, current_result: _, - } => Some(current_char), + } => match current_prompt { + ActionPrompt::RoleChange { + character_id, + new_role: _, + } + | ActionPrompt::Seer { + character_id, + living_players: _, + } + | ActionPrompt::Protector { + character_id, + targets: _, + } + | ActionPrompt::Arcanist { + character_id, + living_players: _, + } + | ActionPrompt::Gravedigger { + character_id, + dead_players: _, + } + | ActionPrompt::Hunter { + character_id, + current_target: _, + living_players: _, + } + | ActionPrompt::Militia { + character_id, + living_players: _, + } + | ActionPrompt::MapleWolf { + character_id, + kill_or_die: _, + living_players: _, + } + | ActionPrompt::Guardian { + character_id, + previous: _, + living_players: _, + } + | ActionPrompt::Shapeshifter { character_id } + | ActionPrompt::AlphaWolf { + character_id, + living_villagers: _, + } + | ActionPrompt::DireWolf { + character_id, + living_players: _, + } => Some(&character_id.character_id), + ActionPrompt::WolvesIntro { wolves: _ } + | ActionPrompt::WolfPackKill { + living_villagers: _, + } + | ActionPrompt::CoverOfDarkness => None, + }, NightState::Complete => None, } } @@ -958,21 +1124,18 @@ impl Night { match &self.night_state { NightState::Active { current_prompt: _, - current_char: _, current_result: Some(_), } => {} NightState::Active { current_prompt: _, - current_char: _, current_result: None, } => return Err(GameError::AwaitingResponse), NightState::Complete => return Err(GameError::NightOver), } - if let Some((prompt, character)) = self.action_queue.pop_front() { - self.used_actions.push((prompt.clone(), character.clone())); + if let Some(prompt) = self.action_queue.pop_front() { + self.used_actions.push(prompt.clone()); self.night_state = NightState::Active { current_prompt: prompt, - current_char: character.character_id().clone(), current_result: None, }; } else { diff --git a/werewolves-proto/src/game/settings.rs b/werewolves-proto/src/game/settings.rs index b0173dd..181749b 100644 --- a/werewolves-proto/src/game/settings.rs +++ b/werewolves-proto/src/game/settings.rs @@ -16,10 +16,10 @@ impl Default for GameSettings { Self { roles: [ (RoleTitle::Werewolf, NonZeroU8::new(1).unwrap()), - (RoleTitle::Seer, NonZeroU8::new(1).unwrap()), + // (RoleTitle::Seer, NonZeroU8::new(1).unwrap()), // (RoleTitle::Militia, NonZeroU8::new(1).unwrap()), // (RoleTitle::Guardian, NonZeroU8::new(1).unwrap()), - (RoleTitle::Apprentice, NonZeroU8::new(1).unwrap()), + // (RoleTitle::Apprentice, NonZeroU8::new(1).unwrap()), ] .into_iter() .collect(), diff --git a/werewolves-proto/src/game_test/mod.rs b/werewolves-proto/src/game_test/mod.rs new file mode 100644 index 0000000..5c4f444 --- /dev/null +++ b/werewolves-proto/src/game_test/mod.rs @@ -0,0 +1,614 @@ +mod night_order; + +use crate::{ + error::GameError, + game::{Game, GameSettings}, + message::{ + CharacterState, Identification, PublicIdentity, + host::{ + HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage, + ServerToHostMessageTitle, + }, + night::{ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult}, + }, + player::{CharacterId, PlayerId}, + role::RoleTitle, +}; +use colored::Colorize; +use core::{num::NonZeroU8, ops::Range}; +#[allow(unused)] +use pretty_assertions::{assert_eq, assert_ne, assert_str_eq}; +use std::io::Write; + +trait ServerToHostMessageExt { + fn prompt(self) -> ActionPrompt; + fn result(self) -> ActionResult; +} + +impl ServerToHostMessageExt for ServerToHostMessage { + fn prompt(self) -> ActionPrompt { + match self { + Self::ActionPrompt(prompt) => prompt, + Self::Daytime { + characters: _, + marked: _, + day: _, + } => panic!("{}", "[got daytime]".bold().red()), + msg => panic!("expected server message <<{msg:?}>> to be an ActionPrompt"), + } + } + + fn result(self) -> ActionResult { + match self { + Self::ActionResult(_, res) => res, + msg => panic!("expected server message <<{msg:?}>> to be an ActionResult"), + } + } +} + +trait GameExt { + fn next(&mut self) -> ActionPrompt; + fn next_expect_day(&mut self) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8); + fn response(&mut self, resp: ActionResponse) -> ActionResult; + fn get_state(&mut self) -> ServerToHostMessage; + fn execute(&mut self) -> ActionPrompt; + fn mark_for_execution( + &mut self, + target: CharacterId, + ) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8); +} + +impl GameExt for Game { + fn next(&mut self) -> ActionPrompt { + self.process(HostGameMessage::Night(HostNightMessage::Next)) + .unwrap() + .prompt() + } + + fn next_expect_day(&mut self) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8) { + match self + .process(HostGameMessage::Night(HostNightMessage::Next)) + .unwrap() + { + ServerToHostMessage::Daytime { + characters, + marked, + day, + } => (characters, marked, day), + res => panic!("unexpected response to next_expect_day: {res:?}"), + } + } + + fn response(&mut self, resp: ActionResponse) -> ActionResult { + self.process(HostGameMessage::Night(HostNightMessage::ActionResponse( + resp, + ))) + .unwrap() + .result() + } + + fn mark_for_execution( + &mut self, + target: CharacterId, + ) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8) { + match self + .process(HostGameMessage::Day(HostDayMessage::MarkForExecution( + target, + ))) + .unwrap() + { + ServerToHostMessage::Daytime { + characters, + marked, + day, + } => (characters, marked, day), + res => panic!("unexpected response to mark_for_execution: {res:?}"), + } + } + + fn execute(&mut self) -> ActionPrompt { + self.process(HostGameMessage::Day(HostDayMessage::Execute)) + .unwrap() + .prompt() + } + + fn get_state(&mut self) -> ServerToHostMessage { + self.process(HostGameMessage::GetState).unwrap() + } +} + +fn init_log() { + let _ = pretty_env_logger::formatted_builder() + .filter_level(log::LevelFilter::Debug) + .format(|f, record| match record.file() { + Some(file) => { + let file = format!( + "[{file}{}]", + record + .line() + .map(|l| format!(":{l}")) + .unwrap_or_else(String::new), + ) + .dimmed(); + let level = match record.level() { + log::Level::Error => "[err]".red().bold(), + log::Level::Warn => "[warn]".yellow().bold(), + log::Level::Info => "[info]".white().bold(), + log::Level::Debug => "[debug]".dimmed().bold(), + log::Level::Trace => "[trace]".dimmed(), + }; + let args = record.args(); + + let arrow = "➢".bold().magenta(); + writeln!(f, "{file}\n{level} {arrow} {args}") + } + _ => writeln!(f, "[{}] {}", record.level(), record.args()), + }) + .is_test(true) + .try_init(); +} + +fn gen_players(range: Range) -> Box<[Identification]> { + range + .into_iter() + .map(|num| Identification { + player_id: PlayerId::from_u128(num as _), + public: PublicIdentity { + name: format!("player {num}"), + pronouns: None, + number: NonZeroU8::new(num).unwrap(), + }, + }) + .collect() +} + +#[test] +fn starts_with_wolf_intro() { + let players = gen_players(1..10); + let settings = GameSettings::default(); + let mut game = Game::new(&players, settings).unwrap(); + let resp = game.process(HostGameMessage::GetState).unwrap(); + assert_eq!( + resp, + ServerToHostMessage::ActionPrompt(ActionPrompt::CoverOfDarkness) + ) +} + +#[test] +fn no_wolf_kill_n1() { + let players = gen_players(1..10); + let mut settings = GameSettings::default(); + settings.add(RoleTitle::Shapeshifter).unwrap(); + settings.sub(RoleTitle::Werewolf); + settings.add(RoleTitle::Protector).unwrap(); + let mut game = Game::new(&players, settings).unwrap(); + assert_eq!( + game.process(HostGameMessage::Night(HostNightMessage::ActionResponse( + ActionResponse::ClearCoverOfDarkness + ))) + .unwrap(), + ServerToHostMessage::ActionResult(None, ActionResult::Continue) + ); + assert!(matches!( + game.process(HostGameMessage::Night(HostNightMessage::Next)) + .unwrap(), + ServerToHostMessage::ActionPrompt(ActionPrompt::WolvesIntro { wolves: _ }) + )); + assert_eq!( + game.process(HostGameMessage::Night(HostNightMessage::ActionResponse( + ActionResponse::WolvesIntroAck + ))) + .unwrap(), + ServerToHostMessage::ActionResult(None, ActionResult::GoBackToSleep), + ); + assert!(matches!( + game.process(HostGameMessage::Night(HostNightMessage::Next)) + .unwrap(), + ServerToHostMessage::Daytime { + characters: _, + marked: _, + day: _, + } + )); +} + +#[test] +fn yes_wolf_kill_n2() { + let players = gen_players(1..10); + let settings = GameSettings::default(); + let mut game = Game::new(&players, settings).unwrap(); + assert_eq!( + game.process(HostGameMessage::Night(HostNightMessage::ActionResponse( + ActionResponse::ClearCoverOfDarkness + ))) + .unwrap(), + ServerToHostMessage::ActionResult(None, ActionResult::Continue) + ); + assert!(matches!( + game.process(HostGameMessage::Night(HostNightMessage::Next)) + .unwrap(), + ServerToHostMessage::ActionPrompt(ActionPrompt::WolvesIntro { wolves: _ }) + )); + assert_eq!( + game.process(HostGameMessage::Night(HostNightMessage::ActionResponse( + ActionResponse::WolvesIntroAck + ))) + .unwrap() + .result(), + ActionResult::GoBackToSleep, + ); + assert!(matches!( + game.process(HostGameMessage::Night(HostNightMessage::Next)) + .unwrap(), + ServerToHostMessage::Daytime { + characters: _, + marked: _, + day: _, + } + )); + + let execution_target = game + .village() + .characters() + .into_iter() + .find(|v| v.is_village()) + .unwrap() + .character_id() + .clone(); + match game + .process(HostGameMessage::Day(HostDayMessage::MarkForExecution( + execution_target.clone(), + ))) + .unwrap() + { + ServerToHostMessage::Daytime { + characters: _, + marked, + day: _, + } => assert_eq!(marked.to_vec(), vec![execution_target]), + resp => panic!("unexpected server message: {resp:#?}"), + } + + assert_eq!( + game.process(HostGameMessage::Day(HostDayMessage::Execute)) + .unwrap(), + ServerToHostMessage::ActionPrompt(ActionPrompt::CoverOfDarkness) + ); + assert_eq!( + game.process(HostGameMessage::Night(HostNightMessage::ActionResponse( + ActionResponse::ClearCoverOfDarkness + ))) + .unwrap(), + ServerToHostMessage::ActionResult(None, ActionResult::Continue) + ); + + assert!(matches!( + game.process(HostGameMessage::Night(HostNightMessage::Next)) + .unwrap(), + ServerToHostMessage::ActionPrompt(ActionPrompt::WolfPackKill { + living_villagers: _ + }) + )); +} + +#[test] +fn protect_stops_shapeshift() { + init_log(); + let players = gen_players(1..10); + let mut settings = GameSettings::default(); + settings.add(RoleTitle::Shapeshifter).unwrap(); + settings.sub(RoleTitle::Werewolf); + settings.add(RoleTitle::Protector).unwrap(); + let mut game = Game::new(&players, settings).unwrap(); + assert_eq!( + game.process(HostGameMessage::Night(HostNightMessage::ActionResponse( + ActionResponse::ClearCoverOfDarkness + ))) + .unwrap(), + ServerToHostMessage::ActionResult(None, ActionResult::Continue) + ); + assert!(matches!( + game.process(HostGameMessage::Night(HostNightMessage::Next)) + .unwrap(), + ServerToHostMessage::ActionPrompt(ActionPrompt::WolvesIntro { wolves: _ }) + )); + assert_eq!( + game.process(HostGameMessage::Night(HostNightMessage::ActionResponse( + ActionResponse::WolvesIntroAck + ))) + .unwrap(), + ServerToHostMessage::ActionResult(None, ActionResult::GoBackToSleep), + ); + assert!(matches!( + game.process(HostGameMessage::Night(HostNightMessage::Next)) + .unwrap(), + ServerToHostMessage::Daytime { + characters: _, + marked: _, + day: _, + } + )); + + let execution_target = game + .village() + .characters() + .into_iter() + .find(|v| v.is_village() && !matches!(v.role().title(), RoleTitle::Protector)) + .unwrap() + .character_id() + .clone(); + match game + .process(HostGameMessage::Day(HostDayMessage::MarkForExecution( + execution_target.clone(), + ))) + .unwrap() + { + ServerToHostMessage::Daytime { + characters: _, + marked, + day: _, + } => assert_eq!(marked.to_vec(), vec![execution_target]), + resp => panic!("unexpected server message: {resp:#?}"), + } + + assert_eq!( + game.process(HostGameMessage::Day(HostDayMessage::Execute)) + .unwrap() + .prompt() + .title(), + ActionPromptTitle::CoverOfDarkness + ); + assert_eq!( + game.process(HostGameMessage::Night(HostNightMessage::ActionResponse( + ActionResponse::ClearCoverOfDarkness + ))) + .unwrap() + .result(), + ActionResult::Continue + ); + + let (prot_and_wolf_target, prot_char_id) = match game + .process(HostGameMessage::Night(HostNightMessage::Next)) + .unwrap() + { + ServerToHostMessage::ActionPrompt(ActionPrompt::Protector { + character_id: prot_char_id, + targets, + }) => ( + targets + .into_iter() + .map(|c| game.village().character_by_id(&c.character_id).unwrap()) + .find(|c| c.is_village()) + .unwrap() + .character_id() + .clone(), + prot_char_id, + ), + _ => panic!("first n2 prompt isn't protector"), + }; + let target = game + .village() + .character_by_id(&prot_and_wolf_target) + .unwrap() + .clone(); + log::info!("target: {target:#?}"); + + assert_eq!( + game.process(HostGameMessage::Night(HostNightMessage::ActionResponse( + ActionResponse::Protector(prot_and_wolf_target.clone()) + ))) + .unwrap() + .result(), + ActionResult::GoBackToSleep, + ); + + assert_eq!( + game.process(HostGameMessage::Night(HostNightMessage::Next)) + .unwrap() + .prompt() + .title(), + ActionPromptTitle::WolfPackKill + ); + + assert_eq!( + game.process(HostGameMessage::Night(HostNightMessage::ActionResponse( + ActionResponse::WolfPackKillVote(prot_and_wolf_target.clone()) + ))) + .unwrap() + .result(), + ActionResult::Continue, + ); + + assert_eq!( + game.process(HostGameMessage::Night(HostNightMessage::Next)) + .unwrap() + .prompt() + .title(), + ActionPromptTitle::Shapeshifter, + ); + + assert_eq!( + game.process(HostGameMessage::Night(HostNightMessage::ActionResponse( + ActionResponse::Shapeshifter(true) + ))) + .unwrap() + .result(), + ActionResult::GoBackToSleep, + ); + + assert_eq!( + game.process(HostGameMessage::Night(HostNightMessage::Next)) + .unwrap() + .title(), + ServerToHostMessageTitle::Daytime, + ); + + let target = game + .village() + .character_by_id(target.character_id()) + .unwrap(); + assert!(target.is_village()); + assert!(target.alive()); + + let prot = game + .village() + .character_by_id(&prot_char_id.character_id) + .unwrap(); + assert!(prot.is_village()); + assert!(prot.alive()); + assert_eq!(prot.role().title(), RoleTitle::Protector); +} + +#[test] +fn wolfpack_kill_all_targets_valid() { + init_log(); + let players = gen_players(1..10); + let mut settings = GameSettings::default(); + settings.add(RoleTitle::Shapeshifter).unwrap(); + settings.sub(RoleTitle::Werewolf); + let mut game = Game::new(&players, settings).unwrap(); + assert_eq!( + game.process(HostGameMessage::Night(HostNightMessage::ActionResponse( + ActionResponse::ClearCoverOfDarkness + ))) + .unwrap(), + ServerToHostMessage::ActionResult(None, ActionResult::Continue) + ); + assert!(matches!( + game.process(HostGameMessage::Night(HostNightMessage::Next)) + .unwrap(), + ServerToHostMessage::ActionPrompt(ActionPrompt::WolvesIntro { wolves: _ }) + )); + assert_eq!( + game.process(HostGameMessage::Night(HostNightMessage::ActionResponse( + ActionResponse::WolvesIntroAck + ))) + .unwrap(), + ServerToHostMessage::ActionResult(None, ActionResult::GoBackToSleep), + ); + assert!(matches!( + game.process(HostGameMessage::Night(HostNightMessage::Next)) + .unwrap(), + ServerToHostMessage::Daytime { + characters: _, + marked: _, + day: _, + } + )); + + let execution_target = game + .village() + .characters() + .into_iter() + .find(|v| v.is_village() && !matches!(v.role().title(), RoleTitle::Protector)) + .unwrap() + .character_id() + .clone(); + match game + .process(HostGameMessage::Day(HostDayMessage::MarkForExecution( + execution_target.clone(), + ))) + .unwrap() + { + ServerToHostMessage::Daytime { + characters: _, + marked, + day: _, + } => assert_eq!(marked.to_vec(), vec![execution_target]), + resp => panic!("unexpected server message: {resp:#?}"), + } + + assert_eq!( + game.process(HostGameMessage::Day(HostDayMessage::Execute)) + .unwrap() + .prompt() + .title(), + ActionPromptTitle::CoverOfDarkness + ); + assert_eq!( + game.process(HostGameMessage::Night(HostNightMessage::ActionResponse( + ActionResponse::ClearCoverOfDarkness + ))) + .unwrap() + .result(), + ActionResult::Continue + ); + + let living_villagers = match game + .process(HostGameMessage::Night(HostNightMessage::Next)) + .unwrap() + .prompt() + { + ActionPrompt::WolfPackKill { living_villagers } => living_villagers, + _ => panic!("not wolf pack kill"), + }; + + for (idx, target) in living_villagers.into_iter().enumerate() { + let mut attempt = game.clone(); + if let ServerToHostMessage::Error(GameError::InvalidTarget) = attempt + .process(HostGameMessage::Night(HostNightMessage::ActionResponse( + ActionResponse::WolfPackKillVote(target.character_id.clone()), + ))) + .unwrap() + { + panic!("invalid target {target:?} at index [{idx}]"); + } + } +} + +#[test] +fn only_1_shapeshift_prompt_if_first_shifts() { + let players = gen_players(1..10); + let mut settings = GameSettings::default(); + settings.add(RoleTitle::Shapeshifter).unwrap(); + settings.add(RoleTitle::Shapeshifter).unwrap(); + settings.sub(RoleTitle::Werewolf); + let mut game = Game::new(&players, settings).unwrap(); + assert_eq!( + game.response(ActionResponse::ClearCoverOfDarkness), + ActionResult::Continue + ); + assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro); + assert_eq!( + game.response(ActionResponse::WolvesIntroAck), + ActionResult::GoBackToSleep + ); + game.next_expect_day(); + let target = game + .village() + .characters() + .into_iter() + .find_map(|c| c.is_village().then_some(c.character_id().clone())) + .unwrap(); + let (_, marked, _) = game.mark_for_execution(target.clone()); + let (marked, target_list): (&[CharacterId], &[CharacterId]) = (&marked, &[target]); + assert_eq!(target_list, marked); + assert_eq!(game.execute().title(), ActionPromptTitle::CoverOfDarkness); + assert_eq!( + game.response(ActionResponse::ClearCoverOfDarkness), + ActionResult::Continue + ); + assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill); + let target = game + .village() + .characters() + .into_iter() + .find_map(|c| (c.is_village() && c.alive()).then_some(c.character_id().clone())) + .unwrap(); + assert_eq!( + game.response(ActionResponse::WolfPackKillVote(target)), + ActionResult::Continue, + ); + assert_eq!(game.next().title(), ActionPromptTitle::Shapeshifter); + assert_eq!( + game.response(ActionResponse::Shapeshifter(true)), + ActionResult::Continue, + ); + assert_eq!(game.next().title(), ActionPromptTitle::RoleChange); + assert_eq!( + game.response(ActionResponse::RoleChangeAck), + ActionResult::GoBackToSleep + ); + + game.next_expect_day(); +} diff --git a/werewolves-proto/src/game_test/night_order.rs b/werewolves-proto/src/game_test/night_order.rs new file mode 100644 index 0000000..12bb058 --- /dev/null +++ b/werewolves-proto/src/game_test/night_order.rs @@ -0,0 +1,63 @@ +use core::num::NonZeroU8; +#[allow(unused)] +use pretty_assertions::{assert_eq, assert_ne, assert_str_eq}; + +use crate::{ + message::{ + CharacterIdentity, PublicIdentity, + night::{ActionPrompt, ActionPromptTitle}, + }, + player::CharacterId, +}; + +fn character_identity() -> CharacterIdentity { + CharacterIdentity { + character_id: CharacterId::new(), + public: PublicIdentity { + name: String::new(), + pronouns: None, + number: NonZeroU8::new(1).unwrap(), + }, + } +} + +#[test] +fn night_order() { + let test_cases: &[(&[ActionPrompt], &[ActionPromptTitle])] = &[( + &[ + ActionPrompt::CoverOfDarkness, + ActionPrompt::WolvesIntro { + wolves: Box::new([]), + }, + ActionPrompt::Shapeshifter { + character_id: character_identity(), + }, + ActionPrompt::WolfPackKill { + living_villagers: Box::new([]), + }, + ActionPrompt::Protector { + character_id: character_identity(), + targets: Box::new([]), + }, + ], + &[ + ActionPromptTitle::CoverOfDarkness, + ActionPromptTitle::Protector, + ActionPromptTitle::WolvesIntro, + ActionPromptTitle::WolfPackKill, + ActionPromptTitle::Shapeshifter, + ], + )]; + + for (input, expect) in test_cases { + let mut prompts = input.to_vec(); + prompts.sort_by(|left_prompt, right_prompt| { + left_prompt + .partial_cmp(right_prompt) + .unwrap_or(core::cmp::Ordering::Equal) + }); + let actual = prompts.into_iter().map(|p| p.title()).collect::>(); + let actual: &[ActionPromptTitle] = &actual; + assert_eq!(*expect, actual) + } +} diff --git a/werewolves-proto/src/lib.rs b/werewolves-proto/src/lib.rs index 74ec04e..3c5fd7f 100644 --- a/werewolves-proto/src/lib.rs +++ b/werewolves-proto/src/lib.rs @@ -7,6 +7,8 @@ use thiserror::Error; pub mod diedto; pub mod error; pub mod game; +#[cfg(test)] +mod game_test; pub mod message; pub mod modifier; pub mod nonzero; diff --git a/werewolves-proto/src/message/host.rs b/werewolves-proto/src/message/host.rs index 849362a..8f3f800 100644 --- a/werewolves-proto/src/message/host.rs +++ b/werewolves-proto/src/message/host.rs @@ -1,6 +1,7 @@ use core::num::NonZeroU8; use serde::{Deserialize, Serialize}; +use werewolves_macros::Extract; use crate::{ error::GameError, @@ -44,6 +45,12 @@ pub enum HostDayMessage { MarkForExecution(CharacterId), } +impl From for HostGameMessage { + fn from(value: HostDayMessage) -> Self { + HostGameMessage::Day(value) + } +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum HostLobbyMessage { GetState, @@ -54,6 +61,7 @@ pub enum HostLobbyMessage { } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[cfg_attr(test, derive(werewolves_macros::Titles))] pub enum ServerToHostMessage { Disconnect, Daytime { @@ -61,8 +69,8 @@ pub enum ServerToHostMessage { marked: Box<[CharacterId]>, day: NonZeroU8, }, - ActionPrompt(PublicIdentity, ActionPrompt), - ActionResult(PublicIdentity, ActionResult), + ActionPrompt(ActionPrompt), + ActionResult(Option, ActionResult), Lobby(Box<[PlayerState]>), GameSettings(GameSettings), Error(GameError), @@ -71,5 +79,4 @@ pub enum ServerToHostMessage { ackd: Box<[Target]>, waiting: Box<[Target]>, }, - CoverOfDarkness, } diff --git a/werewolves-proto/src/message/ident.rs b/werewolves-proto/src/message/ident.rs index 972787b..6a4d5c5 100644 --- a/werewolves-proto/src/message/ident.rs +++ b/werewolves-proto/src/message/ident.rs @@ -21,6 +21,21 @@ pub struct PublicIdentity { pub number: NonZeroU8, } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct CharacterIdentity { + pub character_id: CharacterId, + pub public: PublicIdentity, +} + +impl CharacterIdentity { + pub const fn new(character_id: CharacterId, public: PublicIdentity) -> Self { + Self { + character_id, + public, + } + } +} + impl Default for PublicIdentity { fn default() -> Self { Self { diff --git a/werewolves-proto/src/message/night.rs b/werewolves-proto/src/message/night.rs index 009295d..866a8bb 100644 --- a/werewolves-proto/src/message/night.rs +++ b/werewolves-proto/src/message/night.rs @@ -1,107 +1,125 @@ use serde::{Deserialize, Serialize}; -use werewolves_macros::ChecksAs; +use werewolves_macros::{ChecksAs, Titles}; use crate::{ - diedto::DiedTo, - message::PublicIdentity, + message::CharacterIdentity, player::CharacterId, - role::{Alignment, PreviousGuardianAction, Role, RoleTitle}, + role::{Alignment, PreviousGuardianAction, RoleTitle}, }; use super::Target; -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, PartialOrd)] pub enum ActionType { - Protect = 0, - WolfPackKill = 1, - Direwolf = 2, - Wolf = 3, - Block = 4, - Other = 5, - RoleChange = 6, + Cover, + WolvesIntro, + Protect, + WolfPackKill, + Direwolf, + OtherWolf, + Block, + Other, + RoleChange, } -impl PartialOrd for ActionType { - fn partial_cmp(&self, other: &Self) -> Option { - (*self as u8).partial_cmp(&(*other as u8)) +impl ActionType { + const fn is_wolfy(&self) -> bool { + matches!( + self, + ActionType::Direwolf + | ActionType::OtherWolf + | ActionType::WolfPackKill + | ActionType::WolvesIntro + ) } } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ChecksAs)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ChecksAs, Titles)] pub enum ActionPrompt { + #[checks(ActionType::Cover)] + CoverOfDarkness, #[checks(ActionType::WolfPackKill)] + #[checks] WolvesIntro { wolves: Box<[(Target, RoleTitle)]> }, #[checks(ActionType::RoleChange)] - RoleChange { new_role: RoleTitle }, + RoleChange { + character_id: CharacterIdentity, + new_role: RoleTitle, + }, #[checks(ActionType::Other)] - Seer { living_players: Box<[Target]> }, + Seer { + character_id: CharacterIdentity, + living_players: Box<[Target]>, + }, #[checks(ActionType::Protect)] - Protector { targets: Box<[Target]> }, + Protector { + character_id: CharacterIdentity, + targets: Box<[Target]>, + }, #[checks(ActionType::Other)] - Arcanist { living_players: Box<[Target]> }, + Arcanist { + character_id: CharacterIdentity, + living_players: Box<[Target]>, + }, #[checks(ActionType::Other)] - Gravedigger { dead_players: Box<[Target]> }, + Gravedigger { + character_id: CharacterIdentity, + dead_players: Box<[Target]>, + }, #[checks(ActionType::Other)] Hunter { + character_id: CharacterIdentity, current_target: Option, living_players: Box<[Target]>, }, #[checks(ActionType::Other)] - Militia { living_players: Box<[Target]> }, + Militia { + character_id: CharacterIdentity, + living_players: Box<[Target]>, + }, #[checks(ActionType::Other)] MapleWolf { + character_id: CharacterIdentity, kill_or_die: bool, living_players: Box<[Target]>, }, #[checks(ActionType::Protect)] Guardian { + character_id: CharacterIdentity, previous: Option, living_players: Box<[Target]>, }, - #[checks(ActionType::Wolf)] + #[checks(ActionType::WolfPackKill)] WolfPackKill { living_villagers: Box<[Target]> }, - #[checks(ActionType::Wolf)] - Shapeshifter, - #[checks(ActionType::Wolf)] - AlphaWolf { living_villagers: Box<[Target]> }, + #[checks(ActionType::OtherWolf)] + Shapeshifter { character_id: CharacterIdentity }, + #[checks(ActionType::OtherWolf)] + AlphaWolf { + character_id: CharacterIdentity, + living_villagers: Box<[Target]>, + }, #[checks(ActionType::Direwolf)] - DireWolf { living_players: Box<[Target]> }, + DireWolf { + character_id: CharacterIdentity, + living_players: Box<[Target]>, + }, +} + +impl ActionPrompt { + pub const fn is_wolfy(&self) -> bool { + self.action_type().is_wolfy() + || match self { + ActionPrompt::RoleChange { + character_id: _, + new_role, + } => new_role.wolf(), + _ => false, + } + } } impl PartialOrd for ActionPrompt { fn partial_cmp(&self, other: &Self) -> Option { - // fn ordering_num(prompt: &ActionPrompt) -> u8 { - // match prompt { - // ActionPrompt::WolvesIntro { wolves: _ } => 0, - // ActionPrompt::Guardian { - // living_players: _, - // previous: _, - // } - // | ActionPrompt::Protector { targets: _ } => 1, - // ActionPrompt::WolfPackKill { - // living_villagers: _, - // } => 2, - // ActionPrompt::Shapeshifter => 3, - // ActionPrompt::AlphaWolf { - // living_villagers: _, - // } => 4, - // ActionPrompt::DireWolf { living_players: _ } => 5, - // ActionPrompt::Seer { living_players: _ } - // | ActionPrompt::Arcanist { living_players: _ } - // | ActionPrompt::Gravedigger { dead_players: _ } - // | ActionPrompt::Hunter { - // current_target: _, - // living_players: _, - // } - // | ActionPrompt::Militia { living_players: _ } - // | ActionPrompt::MapleWolf { - // kill_or_die: _, - // living_players: _, - // } - // | ActionPrompt::RoleChange { new_role: _ } => 0xFF, - // } - // } - // ordering_num(self).partial_cmp(&ordering_num(other)) self.action_type().partial_cmp(&other.action_type()) } } @@ -124,6 +142,7 @@ pub enum ActionResponse { #[checks] RoleChangeAck, WolvesIntroAck, + ClearCoverOfDarkness, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -133,5 +152,5 @@ pub enum ActionResult { Arcanist { same: bool }, GraveDigger(Option), GoBackToSleep, - WolvesIntroDone, + Continue, } diff --git a/werewolves-proto/src/player.rs b/werewolves-proto/src/player.rs index 510b0cd..436f8e8 100644 --- a/werewolves-proto/src/player.rs +++ b/werewolves-proto/src/player.rs @@ -6,7 +6,7 @@ use crate::{ diedto::DiedTo, error::GameError, game::{DateTime, Village}, - message::{Identification, PublicIdentity, Target, night::ActionPrompt}, + message::{CharacterIdentity, Identification, PublicIdentity, Target, night::ActionPrompt}, modifier::Modifier, role::{MAPLE_WOLF_ABSTAIN_LIMIT, PreviousGuardianAction, Role, RoleTitle}, }; @@ -116,6 +116,13 @@ impl Character { &self.public } + pub fn character_identity(&self) -> CharacterIdentity { + CharacterIdentity { + character_id: self.character_id.clone(), + public: self.public.clone(), + } + } + pub fn name(&self) -> &str { &self.public.name } @@ -213,19 +220,23 @@ impl Character { | Role::Scapegoat | Role::Villager => return Ok(None), Role::Seer => ActionPrompt::Seer { + character_id: self.character_identity(), living_players: village.living_players_excluding(&self.character_id), }, Role::Arcanist => ActionPrompt::Arcanist { + character_id: self.character_identity(), living_players: village.living_players_excluding(&self.character_id), }, Role::Protector { last_protected: Some(last_protected), } => ActionPrompt::Protector { + character_id: self.character_identity(), targets: village.living_players_excluding(last_protected), }, Role::Protector { last_protected: None, } => ActionPrompt::Protector { + character_id: self.character_identity(), targets: village.living_players_excluding(&self.character_id), }, Role::Apprentice(role) => { @@ -243,6 +254,7 @@ impl Character { DateTime::Night { number } => number + 1 >= current_night, }) .then(|| ActionPrompt::RoleChange { + character_id: self.character_identity(), new_role: role.title(), })); } @@ -253,49 +265,61 @@ impl Character { }; return Ok((current_night == knows_on_night.get()).then_some({ ActionPrompt::RoleChange { + character_id: self.character_identity(), new_role: RoleTitle::Elder, } })); } Role::Militia { targeted: None } => ActionPrompt::Militia { + character_id: self.character_identity(), living_players: village.living_players_excluding(&self.character_id), }, Role::Werewolf => ActionPrompt::WolfPackKill { living_villagers: village.living_players(), }, Role::AlphaWolf { killed: None } => ActionPrompt::AlphaWolf { + character_id: self.character_identity(), living_villagers: village.living_players_excluding(&self.character_id), }, Role::DireWolf => ActionPrompt::DireWolf { + character_id: self.character_identity(), living_players: village.living_players(), }, - Role::Shapeshifter { shifted_into: None } => ActionPrompt::Shapeshifter, + Role::Shapeshifter { shifted_into: None } => ActionPrompt::Shapeshifter { + character_id: self.character_identity(), + }, Role::Gravedigger => ActionPrompt::Gravedigger { + character_id: self.character_identity(), dead_players: village.dead_targets(), }, Role::Hunter { target } => ActionPrompt::Hunter { + character_id: self.character_identity(), current_target: target.as_ref().and_then(|t| village.target_by_id(t)), living_players: village.living_players_excluding(&self.character_id), }, Role::MapleWolf { last_kill_on_night } => ActionPrompt::MapleWolf { + character_id: self.character_identity(), kill_or_die: last_kill_on_night + MAPLE_WOLF_ABSTAIN_LIMIT.get() == night, living_players: village.living_players_excluding(&self.character_id), }, Role::Guardian { last_protected: Some(PreviousGuardianAction::Guard(prev_target)), } => ActionPrompt::Guardian { + character_id: self.character_identity(), previous: Some(PreviousGuardianAction::Guard(prev_target.clone())), living_players: village.living_players_excluding(&prev_target.character_id), }, Role::Guardian { last_protected: Some(PreviousGuardianAction::Protect(prev_target)), } => ActionPrompt::Guardian { + character_id: self.character_identity(), previous: Some(PreviousGuardianAction::Protect(prev_target.clone())), living_players: village.living_players(), }, Role::Guardian { last_protected: None, } => ActionPrompt::Guardian { + character_id: self.character_identity(), previous: None, living_players: village.living_players(), }, diff --git a/werewolves-server/pkg/blog.service b/werewolves-server/pkg/blog.service deleted file mode 100644 index e2b0076..0000000 --- a/werewolves-server/pkg/blog.service +++ /dev/null @@ -1,19 +0,0 @@ -[Unit] -Description=blog -After=network.target - -[Service] -Type=simple - -User=blog -Group=blog - -WorkingDirectory=/home/blog -Environment=RUST_LOG=info -Environment=PORT=3024 -ExecStart=/home/blog/blog-server - -Restart=always - -[Install] -WantedBy=multi-user.target diff --git a/werewolves-server/pkg/werewolves.service b/werewolves-server/pkg/werewolves.service new file mode 100644 index 0000000..b0cd76d --- /dev/null +++ b/werewolves-server/pkg/werewolves.service @@ -0,0 +1,19 @@ +[Unit] +Description=werewolves +After=network.target + +[Service] +Type=simple + +User=werewolf +Group=werewolf + +WorkingDirectory=/home/werewolf +Environment=RUST_LOG=info +Environment=PORT=3028 +ExecStart=/home/werewolf/werewolves-server + +Restart=always + +[Install] +WantedBy=multi-user.target diff --git a/werewolves-server/src/game.rs b/werewolves-server/src/game.rs index 78eb7ac..7bafd24 100644 --- a/werewolves-server/src/game.rs +++ b/werewolves-server/src/game.rs @@ -27,8 +27,6 @@ pub struct GameRunner { player_sender: LobbyPlayers, roles_revealed: bool, joined_players: JoinedPlayers, - // _release_token: InGameToken, - cover_of_darkness: bool, } impl GameRunner { @@ -38,7 +36,6 @@ impl GameRunner { player_sender: LobbyPlayers, connect_recv: Receiver<(PlayerId, bool)>, joined_players: JoinedPlayers, - release_token: InGameToken, ) -> Self { Self { game, @@ -47,8 +44,6 @@ impl GameRunner { player_sender, joined_players, roles_revealed: false, - // _release_token: release_token, - cover_of_darkness: true, } } @@ -203,18 +198,7 @@ impl GameRunner { if !self.roles_revealed { return Err(GameError::NeedRoleReveal); } - if self.cover_of_darkness { - match &message { - HostMessage::GetState | HostMessage::InGame(HostGameMessage::GetState) => { - return Ok(ServerToHostMessage::CoverOfDarkness); - } - HostMessage::InGame(HostGameMessage::Night(HostNightMessage::Next)) => { - self.cover_of_darkness = false; - return self.host_message(HostMessage::GetState); - } - _ => return Err(GameError::InvalidMessageForGameState), - }; - } + match message { HostMessage::GetState => self.game.process(HostGameMessage::GetState), HostMessage::InGame(msg) => self.game.process(msg), diff --git a/werewolves-server/src/lobby.rs b/werewolves-server/src/lobby.rs index 3b91e27..97ae968 100644 --- a/werewolves-server/src/lobby.rs +++ b/werewolves-server/src/lobby.rs @@ -190,15 +190,6 @@ impl Lobby { .iter() .map(|(id, _)| id.clone()) .collect::>(); - let release_token = self - .joined_players - .start_game_with( - &playing_players - .iter() - .map(|id| id.player_id.clone()) - .collect::>(), - ) - .await?; let game = Game::new(&playing_players, self.settings.clone())?; assert_eq!(game.village().characters().len(), playing_players.len()); @@ -210,7 +201,6 @@ impl Lobby { self.players_in_lobby.clone(), recv, self.joined_players.clone(), - release_token, ))); } Message::Client(IdentifiedClientMessage { diff --git a/werewolves-server/src/main.rs b/werewolves-server/src/main.rs index 1cf59df..3f3d439 100644 --- a/werewolves-server/src/main.rs +++ b/werewolves-server/src/main.rs @@ -108,14 +108,14 @@ async fn main() { let jp_clone = joined_players.clone(); - let path = Path::new(option_env!("SAVE_PATH").unwrap_or(DEFAULT_SAVE_DIR)) - .canonicalize() - .expect("canonicalizing path"); + let path = Path::new(option_env!("SAVE_PATH").unwrap_or(DEFAULT_SAVE_DIR)); + if let Err(err) = std::fs::create_dir(&path) && !matches!(err.kind(), std::io::ErrorKind::AlreadyExists) { panic!("creating save dir at [{path:?}]: {err}") } + // Check if we can write to the path { let test_file_path = path.join(".test"); @@ -125,7 +125,7 @@ async fn main() { std::fs::remove_file(&test_file_path).log_err(); } - let saver = FileSaver::new(path); + let saver = FileSaver::new(path.canonicalize().expect("canonicalizing path")); tokio::spawn(async move { crate::runner::run_game(jp_clone, lobby_comms, saver).await; panic!("game over"); diff --git a/werewolves/src/components/action/prompt.rs b/werewolves/src/components/action/prompt.rs index 9f65237..c522cb2 100644 --- a/werewolves/src/components/action/prompt.rs +++ b/werewolves/src/components/action/prompt.rs @@ -1,8 +1,8 @@ -use core::{num::NonZeroU8, ops::Not}; +use core::ops::Not; use werewolves_proto::{ message::{ - PublicIdentity, Target, + PublicIdentity, host::{HostGameMessage, HostMessage, HostNightMessage}, night::{ActionPrompt, ActionResponse}, }, @@ -12,42 +12,59 @@ use werewolves_proto::{ use yew::prelude::*; use crate::components::{ - Identity, + CoverOfDarkness, Identity, action::{BinaryChoice, OptionalSingleTarget, SingleTarget, TwoTarget, WolvesIntro}, }; #[derive(Debug, Clone, PartialEq, Properties)] pub struct ActionPromptProps { pub prompt: ActionPrompt, - pub ident: PublicIdentity, #[prop_or_default] pub big_screen: bool, pub on_complete: Callback, } +fn identity_html(ident: Option<&PublicIdentity>) -> Option { + ident.map(|ident| { + html! { + + } + }) +} + #[function_component] pub fn Prompt(props: &ActionPromptProps) -> Html { - let ident = props - .big_screen - .not() - .then(|| html! {}); match &props.prompt { + ActionPrompt::CoverOfDarkness => { + let on_complete = props.on_complete.clone(); + let next = props.big_screen.not().then(|| { + Callback::from(move |_| { + on_complete.emit(HostMessage::InGame(HostGameMessage::Night( + HostNightMessage::Next, + ))) + }) + }); + return html! { + + }; + } ActionPrompt::WolvesIntro { wolves } => { let on_complete = props.on_complete.clone(); let on_complete = Callback::from(move |_| { - on_complete.emit(HostMessage::InGame( - werewolves_proto::message::host::HostGameMessage::Night( - werewolves_proto::message::host::HostNightMessage::ActionResponse( - werewolves_proto::message::night::ActionResponse::WolvesIntroAck, - ), + on_complete.emit(HostMessage::InGame(HostGameMessage::Night( + HostNightMessage::ActionResponse( + werewolves_proto::message::night::ActionResponse::WolvesIntroAck, ), - )) + ))) }); html! { } } - ActionPrompt::Seer { living_players } => { + ActionPrompt::Seer { + character_id, + living_players, + } => { let on_complete = props.on_complete.clone(); let on_select = props.big_screen.not().then(|| { Callback::from(move |target: CharacterId| { @@ -58,7 +75,7 @@ pub fn Prompt(props: &ActionPromptProps) -> Html { }); html! {
- {ident} + {identity_html(props.big_screen.then_some(&character_id.public))} Html {
} } - ActionPrompt::RoleChange { new_role } => { + ActionPrompt::RoleChange { + character_id, + new_role, + } => { let on_complete = props.on_complete.clone(); let on_click = Callback::from(move |_| { on_complete.emit(HostMessage::InGame(HostGameMessage::Night( @@ -81,14 +101,17 @@ pub fn Prompt(props: &ActionPromptProps) -> Html { }); html! {
- {ident} + {identity_html(props.big_screen.then_some(&character_id.public))}

{"your role has changed"}

{new_role.to_string()}

{cont}
} } - ActionPrompt::Protector { targets } => { + ActionPrompt::Protector { + character_id, + targets, + } => { let on_complete = props.on_complete.clone(); let on_select = props.big_screen.not().then(|| { Callback::from(move |target: CharacterId| { @@ -99,6 +122,7 @@ pub fn Prompt(props: &ActionPromptProps) -> Html { }); html! {
+ {identity_html(props.big_screen.then_some(&character_id.public))} Html {
} } - ActionPrompt::Arcanist { living_players } => { + ActionPrompt::Arcanist { + character_id, + living_players, + } => { let on_complete = props.on_complete.clone(); let on_select = props.big_screen.not().then(|| { Callback::from(move |(t1, t2): (CharacterId, CharacterId)| { @@ -118,6 +145,7 @@ pub fn Prompt(props: &ActionPromptProps) -> Html { }); html! {
+ {identity_html(props.big_screen.then_some(&character_id.public))} Html {
} } - ActionPrompt::Gravedigger { dead_players } => { + ActionPrompt::Gravedigger { + character_id, + dead_players, + } => { let on_complete = props.on_complete.clone(); let on_select = props.big_screen.not().then(|| { Callback::from(move |target: CharacterId| { @@ -137,6 +168,7 @@ pub fn Prompt(props: &ActionPromptProps) -> Html { }); html! {
+ {identity_html(props.big_screen.then_some(&character_id.public))} Html { } } ActionPrompt::Hunter { + character_id, current_target, living_players, } => { @@ -158,20 +191,26 @@ pub fn Prompt(props: &ActionPromptProps) -> Html { }) }); html! { - -

- {"current target: "}{current_target.clone().map(|t| html!{ - - }).unwrap_or_else(|| html!{{"none"}})} -

-
+
+ {identity_html(props.big_screen.then_some(&character_id.public))} + +

+ {"current target: "}{current_target.clone().map(|t| html!{ + + }).unwrap_or_else(|| html!{{"none"}})} +

+
+
} } - ActionPrompt::Militia { living_players } => { + ActionPrompt::Militia { + character_id, + living_players, + } => { let on_complete = props.on_complete.clone(); let on_select = props.big_screen.not().then(|| { Callback::from(move |target: Option| { @@ -181,14 +220,18 @@ pub fn Prompt(props: &ActionPromptProps) -> Html { }) }); html! { - +
+ {identity_html(props.big_screen.then_some(&character_id.public))} + +
} } ActionPrompt::MapleWolf { + character_id: _, kill_or_die, living_players, } => { @@ -206,16 +249,19 @@ pub fn Prompt(props: &ActionPromptProps) -> Html { } }); html! { - - {kill_or_die} - +
+ + {kill_or_die} + +
} } ActionPrompt::Guardian { + character_id: _, previous, living_players, } => { @@ -271,7 +317,7 @@ pub fn Prompt(props: &ActionPromptProps) -> Html { /> } } - ActionPrompt::Shapeshifter => { + ActionPrompt::Shapeshifter { character_id: _ } => { let on_complete = props.on_complete.clone(); let on_select = props.big_screen.not().then_some({ move |shift| { @@ -286,7 +332,10 @@ pub fn Prompt(props: &ActionPromptProps) -> Html { } } - ActionPrompt::AlphaWolf { living_villagers } => { + ActionPrompt::AlphaWolf { + character_id: _, + living_villagers, + } => { let on_complete = props.on_complete.clone(); let on_select = props.big_screen.not().then(|| { Callback::from(move |target: Option| { @@ -303,7 +352,10 @@ pub fn Prompt(props: &ActionPromptProps) -> Html { /> } } - ActionPrompt::DireWolf { living_players } => { + ActionPrompt::DireWolf { + character_id: _, + living_players, + } => { let on_complete = props.on_complete.clone(); let on_select = props.big_screen.not().then(|| { Callback::from(move |target: CharacterId| { diff --git a/werewolves/src/components/action/result.rs b/werewolves/src/components/action/result.rs index 7a9892a..5c73345 100644 --- a/werewolves/src/components/action/result.rs +++ b/werewolves/src/components/action/result.rs @@ -16,7 +16,8 @@ use crate::components::{CoverOfDarkness, Identity}; #[derive(Debug, Clone, PartialEq, Properties)] pub struct ActionResultProps { pub result: ActionResult, - pub ident: PublicIdentity, + #[prop_or_default] + pub ident: Option, #[prop_or_default] pub big_screen: bool, pub on_complete: Callback, @@ -24,10 +25,12 @@ pub struct ActionResultProps { #[function_component] pub fn ActionResultView(props: &ActionResultProps) -> Html { - let ident = props - .big_screen - .not() - .then(|| html! {}); + let ident = props.ident.as_ref().and_then(|ident| { + props + .big_screen + .not() + .then(|| html! {}) + }); let on_complete = props.on_complete.clone(); let on_complete = Callback::from(move |_| { on_complete.emit(HostMessage::InGame(HostGameMessage::Night( @@ -98,18 +101,10 @@ pub fn ActionResultView(props: &ActionResultProps) -> Html { } } - ActionResult::WolvesIntroDone => { - let on_complete = props.on_complete.clone(); - let next = props.big_screen.not().then(|| { - Callback::from(move |_| { - on_complete.emit(HostMessage::InGame(HostGameMessage::Night( - HostNightMessage::Next, - ))) - }) - }); - + ActionResult::Continue => { + props.on_complete.emit(HostMessage::GetState); html! { - + } } } diff --git a/werewolves/src/pages/client.rs b/werewolves/src/pages/client.rs index 34a6995..6a715ce 100644 --- a/werewolves/src/pages/client.rs +++ b/werewolves/src/pages/client.rs @@ -50,7 +50,7 @@ pub struct Connection { impl Connection { async fn connect_ws() -> WebSocket { - let url = option_env!("DEBUG").map(|_| DEBUG_URL).unwrap_or(LIVE_URL); + let url = option_env!("LOCAL").map(|_| DEBUG_URL).unwrap_or(LIVE_URL); loop { match WebSocket::open(url) { Ok(ws) => break ws, @@ -78,7 +78,7 @@ impl Connection { } async fn run(mut self) { - let url = option_env!("DEBUG").map(|_| DEBUG_URL).unwrap_or(LIVE_URL); + let url = option_env!("LOCAL").map(|_| DEBUG_URL).unwrap_or(LIVE_URL); 'outer: loop { log::info!("connecting to {url}"); let mut ws = Self::connect_ws().await.fuse(); @@ -511,18 +511,18 @@ impl Component for Client { true } Message::Connect => { - if let Some(player) = self.player.as_ref() { - if let Some(recv) = self.recv.take() { - yew::platform::spawn_local( - Connection { - scope: ctx.link().clone(), - ident: player.clone(), - recv, - } - .run(), - ); - return true; - } + if let Some(player) = self.player.as_ref() + && let Some(recv) = self.recv.take() + { + yew::platform::spawn_local( + Connection { + scope: ctx.link().clone(), + ident: player.clone(), + recv, + } + .run(), + ); + return true; } while let Err(err) = self.send.try_send(ClientMessage::GetState) { log::error!("send IsThereALobby: {err}") diff --git a/werewolves/src/pages/host.rs b/werewolves/src/pages/host.rs index 4764658..9341f31 100644 --- a/werewolves/src/pages/host.rs +++ b/werewolves/src/pages/host.rs @@ -36,7 +36,7 @@ const DEBUG_URL: &str = "ws://192.168.1.162:8080/connect/host"; const LIVE_URL: &str = "wss://wolf.emilis.dev/connect/host"; async fn connect_ws() -> WebSocket { - let url = option_env!("DEBUG").map(|_| DEBUG_URL).unwrap_or(LIVE_URL); + let url = option_env!("LOCAL").map(|_| DEBUG_URL).unwrap_or(LIVE_URL); loop { match WebSocket::open(url) { Ok(ws) => break ws, @@ -64,7 +64,7 @@ fn encode_message(msg: &impl Serialize) -> websocket::Message { } async fn worker(mut recv: Receiver, scope: Scope) { - let url = option_env!("DEBUG").map(|_| DEBUG_URL).unwrap_or(LIVE_URL); + let url = option_env!("LOCAL").map(|_| DEBUG_URL).unwrap_or(LIVE_URL); 'outer: loop { log::info!("connecting to {url}"); let mut ws = connect_ws().await.fuse(); @@ -75,13 +75,11 @@ async fn worker(mut recv: Receiver, scope: Scope) { continue 'outer; } - let mut last_msg = chrono::Local::now(); loop { let msg = futures::select! { r = ws.next() => { match r { Some(Ok(msg)) => { - last_msg = chrono::Local::now(); msg }, Some(Err(err)) => { @@ -137,10 +135,6 @@ async fn worker(mut recv: Receiver, scope: Scope) { } } }; - let took = chrono::Local::now() - last_msg; - if took.num_milliseconds() >= 100 { - log::warn!("took {took}") - } match parse { Ok(msg) => scope.send_message::(msg.into()), Err(err) => { @@ -178,9 +172,8 @@ pub enum HostState { ackd: Box<[Target]>, waiting: Box<[Target]>, }, - Prompt(PublicIdentity, ActionPrompt), - Result(PublicIdentity, ActionResult), - CoverOfDarkness, + Prompt(ActionPrompt), + Result(Option, ActionResult), } impl From for HostEvent { @@ -202,8 +195,8 @@ impl From for HostEvent { ServerToHostMessage::GameOver(game_over) => { HostEvent::SetState(HostState::GameOver { result: game_over }) } - ServerToHostMessage::ActionPrompt(ident, prompt) => { - HostEvent::SetState(HostState::Prompt(ident, prompt)) + ServerToHostMessage::ActionPrompt(prompt) => { + HostEvent::SetState(HostState::Prompt(prompt)) } ServerToHostMessage::ActionResult(ident, result) => { HostEvent::SetState(HostState::Result(ident, result)) @@ -211,7 +204,6 @@ impl From for HostEvent { ServerToHostMessage::WaitingForRoleRevealAcks { ackd, waiting } => { HostEvent::SetState(HostState::RoleReveal { ackd, waiting }) } - ServerToHostMessage::CoverOfDarkness => HostEvent::SetState(HostState::CoverOfDarkness), } } } @@ -402,7 +394,7 @@ impl Component for Host { } } - HostState::Prompt(ident, prompt) => { + HostState::Prompt(prompt) => { let send = self.send.clone(); let on_complete = Callback::from(move |msg| { let mut send = send.clone(); @@ -418,7 +410,6 @@ impl Component for Host { prompt={prompt} big_screen={self.big_screen} on_complete={on_complete} - ident={ident} /> } } @@ -442,27 +433,6 @@ impl Component for Host { /> } } - HostState::CoverOfDarkness => { - let next = self.big_screen.not().then(|| { - let send = self.send.clone(); - Callback::from(move |_| { - let mut send = send.clone(); - yew::platform::spawn_local(async move { - if let Err(err) = send - .send(HostMessage::InGame(HostGameMessage::Night( - HostNightMessage::Next, - ))) - .await - { - log::error!("sending action result response: {err}") - } - }); - }) - }); - return html! { - - }; - } }; let debug_nav = self.debug.then(|| { let on_error_click = callback::send_message( @@ -506,8 +476,7 @@ impl Component for Host { settings: GameSettings::default(), } } - HostState::CoverOfDarkness - | HostState::Prompt(_, _) + HostState::Prompt(_) | HostState::Result(_, _) | HostState::Disconnected | HostState::RoleReveal { @@ -540,8 +509,7 @@ impl Component for Host { *s = settings; true } - HostState::CoverOfDarkness - | HostState::Prompt(_, _) + HostState::Prompt(_) | HostState::Result(_, _) | HostState::Disconnected | HostState::RoleReveal {