diff --git a/werewolves-proto/src/game/mod.rs b/werewolves-proto/src/game/mod.rs index 55f5934..73aeb2c 100644 --- a/werewolves-proto/src/game/mod.rs +++ b/werewolves-proto/src/game/mod.rs @@ -14,7 +14,7 @@ use serde::{Deserialize, Serialize}; use crate::{ error::GameError, - game::night::Night, + game::night::{Night, ServerAction}, message::{ CharacterState, Identification, host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage}, @@ -133,7 +133,8 @@ impl Game { GameState::Night { night }, HostGameMessage::Night(HostNightMessage::ActionResponse(resp)), ) => match night.received_response(resp.clone()) { - Ok(res) => Ok(ServerToHostMessage::ActionResult( + Ok(ServerAction::Prompt(prompt)) => Ok(ServerToHostMessage::ActionPrompt(prompt)), + Ok(ServerAction::Result(res)) => Ok(ServerToHostMessage::ActionResult( night.current_character().map(|c| c.identity()), res, )), diff --git a/werewolves-proto/src/game/night.rs b/werewolves-proto/src/game/night.rs index daeaffb..9419129 100644 --- a/werewolves-proto/src/game/night.rs +++ b/werewolves-proto/src/game/night.rs @@ -1,4 +1,4 @@ -use core::{num::NonZeroU8, ops::Not}; +use core::num::NonZeroU8; use std::collections::VecDeque; use serde::{Deserialize, Serialize}; @@ -42,12 +42,32 @@ pub enum NightChange { }, } -struct ResponseOutcome { +enum BlockResolvedOutcome { + PromptUpdate(ActionPrompt), + ActionComplete(ActionResult, Option), +} + +enum ResponseOutcome { + PromptUpdate(ActionPrompt), + ActionComplete(ActionComplete), +} + +struct ActionComplete { pub result: ActionResult, pub change: Option, pub unless: Option, } +impl Default for ActionComplete { + fn default() -> Self { + Self { + result: ActionResult::GoBackToSleep, + change: None, + unless: None, + } + } +} + enum Unless { TargetBlocked(CharacterId), TargetsBlocked(CharacterId, CharacterId), @@ -96,6 +116,7 @@ impl Night { .into_iter() .flatten() .chain((night > 0).then(|| ActionPrompt::WolfPackKill { + marked: None, living_villagers: village.living_villagers(), })) .collect::>(); @@ -339,22 +360,33 @@ impl Night { Ok(()) } - pub fn received_response(&mut self, resp: ActionResponse) -> Result { - if let NightState::Active { - current_prompt: ActionPrompt::CoverOfDarkness, - current_result: None, - } = &mut self.night_state - && let ActionResponse::ClearCoverOfDarkness = &resp - { - self.night_state = NightState::Active { - current_prompt: ActionPrompt::CoverOfDarkness, - current_result: Some(ActionResult::Continue), - }; - return Ok(ActionResult::Continue); - } + pub fn received_response(&mut self, resp: ActionResponse) -> Result { + // if let NightState::Active { + // current_prompt: ActionPrompt::CoverOfDarkness, + // current_result: None, + // } = &mut self.night_state + // && let ActionResponse::ClearCoverOfDarkness = &resp + // { + // 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)? { - (result, Some(change)) => { + BlockResolvedOutcome::PromptUpdate(prompt) => match &mut self.night_state { + NightState::Active { + current_result: Some(_), + .. + } => return Err(GameError::AwaitingResponse), + NightState::Active { current_prompt, .. } => { + *current_prompt = prompt.clone(); + Ok(ServerAction::Prompt(prompt)) + } + NightState::Complete => return Err(GameError::NightOver), + }, + BlockResolvedOutcome::ActionComplete(result, Some(change)) => { match &mut self.night_state { NightState::Active { current_prompt: _, @@ -366,17 +398,18 @@ impl Night { // 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)); + return Ok(ServerAction::Result( + 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(ServerAction::Result(result)) } - (result, None) => { + BlockResolvedOutcome::ActionComplete(result, None) => { match &mut self.night_state { NightState::Active { current_prompt: _, @@ -386,7 +419,7 @@ impl Night { } NightState::Complete => return Err(GameError::NightOver), }; - Ok(result) + Ok(ServerAction::Result(result)) } } } @@ -395,7 +428,15 @@ impl Night { &self, resp: ActionResponse, ) -> Result { - let current_wolfy = self.current_prompt().unwrap().is_wolfy(); + let (current_cover, current_wolfy) = self + .current_prompt() + .map(|current_prompt| { + ( + *current_prompt == ActionPrompt::CoverOfDarkness, + current_prompt.is_wolfy(), + ) + }) + .unwrap_or_default(); let next_wolfy = self .action_queue .iter() @@ -403,37 +444,46 @@ impl Night { .map(|a| a.is_wolfy()) .unwrap_or_default(); + if current_cover && let ActionResponse::Continue = &resp { + return Ok(ResponseOutcome::ActionComplete(ActionComplete { + result: ActionResult::Continue, + change: None, + unless: None, + })); + } + match ( self.received_response_inner(resp)?, current_wolfy, next_wolfy, ) { + (ResponseOutcome::PromptUpdate(p), _, _) => Ok(ResponseOutcome::PromptUpdate(p)), ( - ResponseOutcome { + ResponseOutcome::ActionComplete(ActionComplete { result: ActionResult::GoBackToSleep, change: Some(NightChange::Shapeshift { source }), unless, - }, + }), true, _, - ) => Ok(ResponseOutcome { + ) => Ok(ResponseOutcome::ActionComplete(ActionComplete { result: ActionResult::Continue, change: Some(NightChange::Shapeshift { source }), unless, - }), + })), ( - ResponseOutcome { + ResponseOutcome::ActionComplete(ActionComplete { result: ActionResult::GoBackToSleep, change, unless, - }, + }), true, true, - ) => Ok(ResponseOutcome { + ) => Ok(ResponseOutcome::ActionComplete(ActionComplete { result: ActionResult::Continue, change, unless, - }), + })), (outcome, _, _) => Ok(outcome), } } @@ -441,13 +491,14 @@ impl Night { fn received_response_with_role_blocks( &self, resp: ActionResponse, - ) -> Result<(ActionResult, Option)> { + ) -> Result { match self.received_response_consecutive_wolves_dont_sleep(resp)? { - ResponseOutcome { + ResponseOutcome::PromptUpdate(update) => Ok(BlockResolvedOutcome::PromptUpdate(update)), + ResponseOutcome::ActionComplete(ActionComplete { result, change, unless: Some(Unless::TargetBlocked(unless_blocked)), - } => { + }) => { if self.changes.iter().any(|c| match c { NightChange::RoleBlock { source: _, @@ -456,16 +507,19 @@ impl Night { } => target == &unless_blocked, _ => false, }) { - Ok((ActionResult::RoleBlocked, None)) + Ok(BlockResolvedOutcome::ActionComplete( + ActionResult::RoleBlocked, + None, + )) } else { - Ok((result, change)) + Ok(BlockResolvedOutcome::ActionComplete(result, change)) } } - ResponseOutcome { + ResponseOutcome::ActionComplete(ActionComplete { result, change, unless: Some(Unless::TargetsBlocked(unless_blocked1, unless_blocked2)), - } => { + }) => { if self.changes.iter().any(|c| match c { NightChange::RoleBlock { source: _, @@ -474,17 +528,20 @@ impl Night { } => target == &unless_blocked1 || target == &unless_blocked2, _ => false, }) { - Ok((ActionResult::RoleBlocked, None)) + Ok(BlockResolvedOutcome::ActionComplete( + ActionResult::RoleBlocked, + None, + )) } else { - Ok((result, change)) + Ok(BlockResolvedOutcome::ActionComplete(result, change)) } } - ResponseOutcome { + ResponseOutcome::ActionComplete(ActionComplete { result, change, unless: None, - } => Ok((result, change)), + }) => Ok(BlockResolvedOutcome::ActionComplete(result, change)), } } @@ -501,525 +558,290 @@ impl Night { NightState::Complete => return Err(GameError::NightOver), }; - match (current_prompt, resp) { - ( - ActionPrompt::RoleChange { - character_id: current_char, - new_role, - }, - ActionResponse::RoleChangeAck, - ) => Ok(ResponseOutcome { - result: ActionResult::GoBackToSleep, - change: Some(NightChange::RoleChange( - current_char.character_id.clone(), - *new_role, - )), - unless: None, - }), - (ActionPrompt::WolvesIntro { wolves: _ }, ActionResponse::WolvesIntroAck) => { - Ok(ResponseOutcome { + match resp { + ActionResponse::MarkTarget(mark) => { + return Ok(ResponseOutcome::PromptUpdate( + current_prompt.with_mark(mark)?, + )); + } + ActionResponse::Shapeshift => { + return match current_prompt { + ActionPrompt::Shapeshifter { + character_id: source, + } => Ok(ResponseOutcome::ActionComplete(ActionComplete { + result: ActionResult::GoBackToSleep, + change: Some(NightChange::Shapeshift { + source: source.character_id.clone(), + }), + unless: None, + })), + _ => Err(GameError::InvalidMessageForGameState), + }; + } + ActionResponse::Continue => {} + }; + + match current_prompt { + ActionPrompt::RoleChange { .. } + | ActionPrompt::WolvesIntro { .. } + | ActionPrompt::CoverOfDarkness => { + Ok(ResponseOutcome::ActionComplete(ActionComplete { result: ActionResult::GoBackToSleep, change: None, unless: None, - }) + })) } - ( - ActionPrompt::WolfPackKill { living_villagers }, - ActionResponse::WolfPackKillVote(target), - ) => { - let night = match NonZeroU8::new(self.night) { - Some(night) => night, - None => { - return Ok(ResponseOutcome { - result: ActionResult::GoBackToSleep, - change: None, - unless: None, - }); - } - }; - if !living_villagers.iter().any(|p| p.character_id == target) { - return Err(GameError::InvalidTarget); - } - Ok(ResponseOutcome { - result: ActionResult::GoBackToSleep, - change: Some(NightChange::Kill { - target: target.clone(), - died_to: DiedTo::Wolfpack { - night, - killing_wolf: self.village.killing_wolf_id(), - }, - }), - unless: Some(Unless::TargetBlocked(target)), - }) - } - ( - 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(), - ), + ActionPrompt::Seer { + marked: Some(marked), + .. + } => { + let alignment = self + .village + .character_by_id(marked) + .ok_or(GameError::InvalidTarget)? + .alignment(); + Ok(ResponseOutcome::ActionComplete(ActionComplete { + result: ActionResult::Seer(alignment), change: None, - unless: Some(Unless::TargetBlocked(target)), - }) + unless: Some(Unless::TargetBlocked(marked.clone())), + })) } - ( - 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, + ActionPrompt::Protector { + marked: Some(marked), + character_id, + .. + } => Ok(ResponseOutcome::ActionComplete(ActionComplete { + result: ActionResult::GoBackToSleep, + change: Some(NightChange::Protection { + target: marked.clone(), + protection: Protection::Protector { + source: character_id.character_id.clone(), }, - 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::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, - }) - } + }), + unless: Some(Unless::TargetBlocked(marked.clone())), + })), + ActionPrompt::Arcanist { + marked: (Some(marked1), Some(marked2)), + .. + } => { + let same = self + .village + .character_by_id(marked1) + .ok_or(GameError::InvalidMessageForGameState)? + .alignment() + == self + .village + .character_by_id(marked2) + .ok_or(GameError::InvalidMessageForGameState)? + .alignment(); - ( - ActionPrompt::MapleWolf { - character_id: _, - kill_or_die: false, - living_players: _, - }, - ActionResponse::MapleWolf(None), - ) => Ok(ResponseOutcome { + Ok(ResponseOutcome::ActionComplete(ActionComplete { + result: ActionResult::Arcanist { same }, + change: None, + unless: Some(Unless::TargetsBlocked(marked1.clone(), marked2.clone())), + })) + } + ActionPrompt::Gravedigger { + marked: Some(marked), + .. + } => { + let dig_role = self + .village + .character_by_id(marked) + .ok_or(GameError::InvalidMessageForGameState)? + .gravedigger_dig(); + Ok(ResponseOutcome::ActionComplete(ActionComplete { + result: ActionResult::GraveDigger(dig_role), + change: None, + unless: Some(Unless::TargetBlocked(marked.clone())), + })) + } + ActionPrompt::Hunter { + character_id, + marked: Some(marked), + .. + } => Ok(ResponseOutcome::ActionComplete(ActionComplete { 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) { + change: Some(NightChange::HunterTarget { + source: character_id.character_id.clone(), + target: marked.clone(), + }), + unless: Some(Unless::TargetBlocked(marked.clone())), + })), + ActionPrompt::Militia { + character_id, + marked: Some(marked), + .. + } => Ok(ResponseOutcome::ActionComplete(ActionComplete { + result: ActionResult::GoBackToSleep, + change: Some(NightChange::Kill { + target: marked.clone(), + died_to: DiedTo::Militia { + killer: character_id.character_id.clone(), + night: NonZeroU8::new(self.night) + .ok_or(GameError::InvalidMessageForGameState)?, + }, + }), + unless: Some(Unless::TargetBlocked(marked.clone())), + })), + ActionPrompt::Militia { marked: None, .. } => { + Ok(ResponseOutcome::ActionComplete(Default::default())) + } + ActionPrompt::MapleWolf { + character_id, + kill_or_die, + marked: Some(marked), + .. + } => Ok(ResponseOutcome::ActionComplete(ActionComplete { + result: ActionResult::GoBackToSleep, + change: Some(NightChange::Kill { + target: marked.clone(), + died_to: DiedTo::MapleWolf { + source: character_id.character_id.clone(), + night: NonZeroU8::new(self.night) + .ok_or(GameError::InvalidMessageForGameState)?, + starves_if_fails: *kill_or_die, + }, + }), + unless: Some(Unless::TargetBlocked(marked.clone())), + })), + ActionPrompt::MapleWolf { marked: None, .. } => { + Ok(ResponseOutcome::ActionComplete(Default::default())) + } + ActionPrompt::Guardian { + character_id, + previous: None, + marked: Some(marked), + .. + } => Ok(ResponseOutcome::ActionComplete(ActionComplete { + result: ActionResult::GoBackToSleep, + change: Some(NightChange::Protection { + target: marked.clone(), + protection: Protection::Guardian { + source: character_id.character_id.clone(), + guarding: false, + }, + }), + unless: Some(Unless::TargetBlocked(marked.clone())), + })), + ActionPrompt::Guardian { + character_id, + previous: Some(PreviousGuardianAction::Guard(prev_target)), + marked: Some(marked), + .. + } => { + if prev_target.character_id == *marked { 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 { + Ok(ResponseOutcome::ActionComplete(ActionComplete { result: ActionResult::GoBackToSleep, change: Some(NightChange::Protection { - target: target.clone(), + target: marked.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(), + source: character_id.character_id.clone(), guarding: false, }, }), - unless: Some(Unless::TargetBlocked(target)), - }) + unless: Some(Unless::TargetBlocked(marked.clone())), + })) } - ( - ActionPrompt::Shapeshifter { - character_id: current_char, - }, - ActionResponse::Shapeshifter(true), - ) => Ok(ResponseOutcome { + ActionPrompt::Guardian { + character_id, + previous: Some(PreviousGuardianAction::Protect(prev_protect)), + marked: Some(marked), + .. + } => Ok(ResponseOutcome::ActionComplete(ActionComplete { result: ActionResult::GoBackToSleep, - change: Some(NightChange::Shapeshift { - source: current_char.character_id.clone(), + change: Some(NightChange::Protection { + target: marked.clone(), + protection: Protection::Guardian { + source: character_id.character_id.clone(), + guarding: prev_protect.character_id == *marked, + }, }), - unless: None, - }), - ( - ActionPrompt::AlphaWolf { - character_id: _, - living_villagers: _, - }, - ActionResponse::AlphaWolf(None), - ) - | ( - ActionPrompt::Shapeshifter { character_id: _ }, - ActionResponse::Shapeshifter(false), - ) => Ok(ResponseOutcome { + unless: Some(Unless::TargetBlocked(marked.clone())), + })), + ActionPrompt::WolfPackKill { + marked: Some(marked), + .. + } => Ok(ResponseOutcome::ActionComplete(ActionComplete { result: ActionResult::GoBackToSleep, - change: None, - unless: None, - }), - ( - ActionPrompt::AlphaWolf { - character_id: current_char, - living_villagers, - }, - ActionResponse::AlphaWolf(Some(target)), - ) => { - let night = match NonZeroU8::new(self.night) { - Some(night) => night, - None => { - return Ok(ResponseOutcome { - result: ActionResult::GoBackToSleep, - change: None, - unless: None, - }); - } - }; - if !living_villagers.iter().any(|p| p.character_id == target) { - return Err(GameError::InvalidTarget); - } - Ok(ResponseOutcome { + change: Some(NightChange::Kill { + target: marked.clone(), + died_to: DiedTo::Wolfpack { + killing_wolf: self + .village + .killing_wolf() + .ok_or(GameError::NoWolves)? + .character_id() + .clone(), + night: NonZeroU8::new(self.night) + .ok_or(GameError::InvalidMessageForGameState)?, + }, + }), + unless: Some(Unless::TargetBlocked(marked.clone())), + })), + ActionPrompt::Shapeshifter { character_id } => { + Ok(ResponseOutcome::ActionComplete(ActionComplete { result: ActionResult::GoBackToSleep, - change: Some(NightChange::Kill { - target: target.clone(), - died_to: DiedTo::AlphaWolf { - killer: current_char.character_id.clone(), - night, - }, - }), - unless: Some(Unless::TargetBlocked(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.character_id.clone(), - target, - block_type: RoleBlock::Direwolf, + change: Some(NightChange::Shapeshift { + source: character_id.character_id.clone(), }), unless: None, - }) + })) } - ( - ActionPrompt::Protector { - character_id: current_char, - targets, - }, - ActionResponse::Protector(target), - ) => { - if !targets.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::Protector { - source: current_char.character_id.clone(), - }, - }), - unless: Some(Unless::TargetBlocked(target)), - }) + ActionPrompt::AlphaWolf { + character_id, + marked: Some(marked), + .. + } => Ok(ResponseOutcome::ActionComplete(ActionComplete { + result: ActionResult::GoBackToSleep, + change: Some(NightChange::Kill { + target: marked.clone(), + died_to: DiedTo::AlphaWolf { + killer: character_id.character_id.clone(), + night: NonZeroU8::new(self.night) + .ok_or(GameError::InvalidMessageForGameState)?, + }, + }), + unless: Some(Unless::TargetBlocked(marked.clone())), + })), + ActionPrompt::AlphaWolf { marked: None, .. } => { + Ok(ResponseOutcome::ActionComplete(Default::default())) } + ActionPrompt::DireWolf { + character_id, + marked: Some(marked), + .. + } => Ok(ResponseOutcome::ActionComplete(ActionComplete { + result: ActionResult::GoBackToSleep, + change: Some(NightChange::RoleBlock { + source: character_id.character_id.clone(), + target: marked.clone(), + block_type: RoleBlock::Direwolf, + }), + unless: Some(Unless::TargetBlocked(marked.clone())), + })), - // For other responses that are invalid -- this allows the match to error - // if a new prompt is added - ( - 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 { - 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: _, - }, - _, - ) => Err(GameError::InvalidMessageForGameState), + ActionPrompt::Protector { marked: None, .. } + | ActionPrompt::Arcanist { + marked: (None, None), + .. + } + | ActionPrompt::Arcanist { + marked: (None, Some(_)), + .. + } + | ActionPrompt::Arcanist { + marked: (Some(_), None), + .. + } + | ActionPrompt::Gravedigger { marked: None, .. } + | ActionPrompt::Hunter { marked: None, .. } + | ActionPrompt::Guardian { marked: None, .. } + | ActionPrompt::WolfPackKill { marked: None, .. } + | ActionPrompt::DireWolf { marked: None, .. } + | ActionPrompt::Seer { marked: None, .. } => Err(GameError::InvalidMessageForGameState), } } @@ -1053,58 +875,20 @@ impl Night { current_prompt, current_result: _, } => 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::RoleChange { character_id, .. } + | ActionPrompt::Seer { character_id, .. } + | ActionPrompt::Protector { character_id, .. } + | ActionPrompt::Arcanist { character_id, .. } + | ActionPrompt::Gravedigger { character_id, .. } + | ActionPrompt::Hunter { character_id, .. } + | ActionPrompt::Militia { character_id, .. } + | ActionPrompt::MapleWolf { character_id, .. } + | ActionPrompt::Guardian { character_id, .. } | ActionPrompt::Shapeshifter { character_id } - | ActionPrompt::AlphaWolf { - character_id, - living_villagers: _, - } - | ActionPrompt::DireWolf { - character_id, - living_players: _, - } => Some(&character_id.character_id), + | ActionPrompt::AlphaWolf { character_id, .. } + | ActionPrompt::DireWolf { character_id, .. } => Some(&character_id.character_id), ActionPrompt::WolvesIntro { wolves: _ } - | ActionPrompt::WolfPackKill { - living_villagers: _, - } + | ActionPrompt::WolfPackKill { .. } | ActionPrompt::CoverOfDarkness => None, }, NightState::Complete => None, @@ -1149,3 +933,8 @@ impl Night { self.changes.as_slice() } } + +pub enum ServerAction { + Prompt(ActionPrompt), + Result(ActionResult), +} diff --git a/werewolves-proto/src/game/village.rs b/werewolves-proto/src/game/village.rs index c10e2a5..061a884 100644 --- a/werewolves-proto/src/game/village.rs +++ b/werewolves-proto/src/game/village.rs @@ -68,6 +68,28 @@ impl Village { }) } + pub fn killing_wolf(&self) -> Option<&Character> { + let wolves = self.characters.iter().filter(|c| c.is_wolf()); + + { + let ww = wolves + .clone() + .filter(|w| matches!(w.role().title(), RoleTitle::Werewolf)) + .collect::>(); + if !ww.is_empty() { + return Some(ww[rand::random_range(0..ww.len())]); + } + } + { + let wolves = wolves.collect::>(); + if wolves.is_empty() { + return None; + } + + Some(wolves[rand::random_range(0..wolves.len())]) + } + } + pub const fn date_time(&self) -> DateTime { self.date_time } diff --git a/werewolves-proto/src/game_test/mod.rs b/werewolves-proto/src/game_test/mod.rs index 7033293..8b7a2ba 100644 --- a/werewolves-proto/src/game_test/mod.rs +++ b/werewolves-proto/src/game_test/mod.rs @@ -5,10 +5,7 @@ use crate::{ game::{Game, GameSettings}, message::{ CharacterState, Identification, PublicIdentity, - host::{ - HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage, - ServerToHostMessageTitle, - }, + host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage}, night::{ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult}, }, player::{CharacterId, PlayerId}, @@ -20,12 +17,39 @@ use core::{num::NonZeroU8, ops::Range}; use pretty_assertions::{assert_eq, assert_ne, assert_str_eq}; use std::io::Write; +trait ActionResultExt { + fn sleep(&self); + fn r#continue(&self); +} + +impl ActionResultExt for ActionResult { + fn sleep(&self) { + assert_eq!(*self, ActionResult::GoBackToSleep) + } + + fn r#continue(&self) { + assert_eq!(*self, ActionResult::Continue) + } +} + trait ServerToHostMessageExt { fn prompt(self) -> ActionPrompt; fn result(self) -> ActionResult; + fn daytime(self) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8); } impl ServerToHostMessageExt for ServerToHostMessage { + fn daytime(self) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8) { + match self { + Self::Daytime { + characters, + marked, + day, + } => (characters, marked, day), + resp => panic!("expected daytime, got {resp:?}"), + } + } + fn prompt(self) -> ActionPrompt { match self { Self::ActionPrompt(prompt) => prompt, @@ -48,7 +72,10 @@ impl ServerToHostMessageExt for ServerToHostMessage { trait GameExt { fn next(&mut self) -> ActionPrompt; + fn r#continue(&mut self) -> ActionResult; fn next_expect_day(&mut self) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8); + fn mark(&mut self, mark: &CharacterId) -> ActionPrompt; + fn mark_and_check(&mut self, mark: &CharacterId, check: impl FnOnce(&ActionPrompt) -> bool); fn response(&mut self, resp: ActionResponse) -> ActionResult; fn execute(&mut self) -> ActionPrompt; fn mark_for_execution( @@ -58,6 +85,29 @@ trait GameExt { } impl GameExt for Game { + fn r#continue(&mut self) -> ActionResult { + self.process(HostGameMessage::Night(HostNightMessage::ActionResponse( + ActionResponse::Continue, + ))) + .unwrap() + .result() + } + + fn mark(&mut self, mark: &CharacterId) -> ActionPrompt { + self.process(HostGameMessage::Night(HostNightMessage::ActionResponse( + ActionResponse::MarkTarget(mark.clone()), + ))) + .unwrap() + .prompt() + } + + fn mark_and_check(&mut self, mark: &CharacterId, check: impl FnOnce(&ActionPrompt) -> bool) { + let prompt = self.mark(mark); + if !check(&prompt) { + panic!("unexpected prompt: {prompt:?}"); + } + } + fn next(&mut self) -> ActionPrompt { self.process(HostGameMessage::Night(HostNightMessage::Next)) .unwrap() @@ -179,7 +229,7 @@ fn no_wolf_kill_n1() { let mut game = Game::new(&players, settings).unwrap(); assert_eq!( game.process(HostGameMessage::Night(HostNightMessage::ActionResponse( - ActionResponse::ClearCoverOfDarkness + ActionResponse::Continue ))) .unwrap(), ServerToHostMessage::ActionResult(None, ActionResult::Continue) @@ -191,7 +241,7 @@ fn no_wolf_kill_n1() { )); assert_eq!( game.process(HostGameMessage::Night(HostNightMessage::ActionResponse( - ActionResponse::WolvesIntroAck + ActionResponse::Continue ))) .unwrap(), ServerToHostMessage::ActionResult(None, ActionResult::GoBackToSleep), @@ -214,7 +264,7 @@ fn yes_wolf_kill_n2() { let mut game = Game::new(&players, settings).unwrap(); assert_eq!( game.process(HostGameMessage::Night(HostNightMessage::ActionResponse( - ActionResponse::ClearCoverOfDarkness + ActionResponse::Continue ))) .unwrap(), ServerToHostMessage::ActionResult(None, ActionResult::Continue) @@ -226,7 +276,7 @@ fn yes_wolf_kill_n2() { )); assert_eq!( game.process(HostGameMessage::Night(HostNightMessage::ActionResponse( - ActionResponse::WolvesIntroAck + ActionResponse::Continue ))) .unwrap() .result(), @@ -271,7 +321,7 @@ fn yes_wolf_kill_n2() { ); assert_eq!( game.process(HostGameMessage::Night(HostNightMessage::ActionResponse( - ActionResponse::ClearCoverOfDarkness + ActionResponse::Continue ))) .unwrap(), ServerToHostMessage::ActionResult(None, ActionResult::Continue) @@ -281,7 +331,8 @@ fn yes_wolf_kill_n2() { game.process(HostGameMessage::Night(HostNightMessage::Next)) .unwrap(), ServerToHostMessage::ActionPrompt(ActionPrompt::WolfPackKill { - living_villagers: _ + living_villagers: _, + marked: _, }) )); } @@ -297,7 +348,7 @@ fn protect_stops_shapeshift() { let mut game = Game::new(&players, settings).unwrap(); assert_eq!( game.process(HostGameMessage::Night(HostNightMessage::ActionResponse( - ActionResponse::ClearCoverOfDarkness + ActionResponse::Continue, ))) .unwrap(), ServerToHostMessage::ActionResult(None, ActionResult::Continue) @@ -309,7 +360,7 @@ fn protect_stops_shapeshift() { )); assert_eq!( game.process(HostGameMessage::Night(HostNightMessage::ActionResponse( - ActionResponse::WolvesIntroAck + ActionResponse::Continue ))) .unwrap(), ServerToHostMessage::ActionResult(None, ActionResult::GoBackToSleep), @@ -353,14 +404,7 @@ fn protect_stops_shapeshift() { .title(), ActionPromptTitle::CoverOfDarkness ); - assert_eq!( - game.process(HostGameMessage::Night(HostNightMessage::ActionResponse( - ActionResponse::ClearCoverOfDarkness - ))) - .unwrap() - .result(), - ActionResult::Continue - ); + game.r#continue().r#continue(); let (prot_and_wolf_target, prot_char_id) = match game .process(HostGameMessage::Night(HostNightMessage::Next)) @@ -369,6 +413,7 @@ fn protect_stops_shapeshift() { ServerToHostMessage::ActionPrompt(ActionPrompt::Protector { character_id: prot_char_id, targets, + marked: None, }) => ( targets .into_iter() @@ -388,55 +433,35 @@ fn protect_stops_shapeshift() { .clone(); log::info!("target: {target:#?}"); - assert_eq!( - game.process(HostGameMessage::Night(HostNightMessage::ActionResponse( - ActionResponse::Protector(prot_and_wolf_target.clone()) + match game + .process(HostGameMessage::Night(HostNightMessage::ActionResponse( + ActionResponse::MarkTarget(prot_and_wolf_target.clone()), ))) .unwrap() - .result(), - ActionResult::GoBackToSleep, - ); + { + ServerToHostMessage::ActionPrompt(ActionPrompt::Protector { + marked: Some(mark), .. + }) => assert_eq!(mark, prot_and_wolf_target, "marked target"), + resp => panic!("unexpected response: {resp:?}"), + } - assert_eq!( - game.process(HostGameMessage::Night(HostNightMessage::Next)) - .unwrap() - .prompt() - .title(), - ActionPromptTitle::WolfPackKill - ); + game.r#continue().sleep(); - assert_eq!( - game.process(HostGameMessage::Night(HostNightMessage::ActionResponse( - ActionResponse::WolfPackKillVote(prot_and_wolf_target.clone()) - ))) - .unwrap() - .result(), - ActionResult::Continue, - ); + assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill); - assert_eq!( - game.process(HostGameMessage::Night(HostNightMessage::Next)) - .unwrap() - .prompt() - .title(), - ActionPromptTitle::Shapeshifter, - ); + game.mark_and_check(&prot_and_wolf_target, |c| match c { + ActionPrompt::WolfPackKill { + marked: Some(mark), .. + } => prot_and_wolf_target == *mark, + _ => false, + }); + game.r#continue().r#continue(); - assert_eq!( - game.process(HostGameMessage::Night(HostNightMessage::ActionResponse( - ActionResponse::Shapeshifter(true) - ))) - .unwrap() - .result(), - ActionResult::GoBackToSleep, - ); + assert_eq!(game.next().title(), ActionPromptTitle::Shapeshifter,); - assert_eq!( - game.process(HostGameMessage::Night(HostNightMessage::Next)) - .unwrap() - .title(), - ServerToHostMessageTitle::Daytime, - ); + game.response(ActionResponse::Shapeshift); + + game.next_expect_day(); let target = game .village() @@ -462,34 +487,11 @@ fn wolfpack_kill_all_targets_valid() { 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: _, - } - )); + game.r#continue().r#continue(); + + assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro); + game.r#continue().sleep(); + game.next_expect_day(); let execution_target = game .village() @@ -513,28 +515,18 @@ fn wolfpack_kill_all_targets_valid() { 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 - ); + assert_eq!(game.execute().title(), ActionPromptTitle::CoverOfDarkness); + game.r#continue().r#continue(); let living_villagers = match game .process(HostGameMessage::Night(HostNightMessage::Next)) .unwrap() .prompt() { - ActionPrompt::WolfPackKill { living_villagers } => living_villagers, + ActionPrompt::WolfPackKill { + living_villagers, + marked: _, + } => living_villagers, _ => panic!("not wolf pack kill"), }; @@ -542,12 +534,14 @@ fn wolfpack_kill_all_targets_valid() { let mut attempt = game.clone(); if let ServerToHostMessage::Error(GameError::InvalidTarget) = attempt .process(HostGameMessage::Night(HostNightMessage::ActionResponse( - ActionResponse::WolfPackKillVote(target.character_id.clone()), + ActionResponse::MarkTarget(target.character_id.clone()), ))) .unwrap() { panic!("invalid target {target:?} at index [{idx}]"); } + attempt.r#continue().r#continue(); + assert_eq!(attempt.next().title(), ActionPromptTitle::Shapeshifter); } } @@ -559,15 +553,9 @@ fn only_1_shapeshift_prompt_if_first_shifts() { 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 - ); + game.r#continue().r#continue(); assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro); - assert_eq!( - game.response(ActionResponse::WolvesIntroAck), - ActionResult::GoBackToSleep - ); + game.r#continue().sleep(); game.next_expect_day(); let target = game .village() @@ -579,10 +567,7 @@ fn only_1_shapeshift_prompt_if_first_shifts() { 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 - ); + game.r#continue().r#continue(); assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill); let target = game .village() @@ -590,20 +575,18 @@ fn only_1_shapeshift_prompt_if_first_shifts() { .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, - ); + + game.mark_and_check(&target, |p| match p { + ActionPrompt::WolfPackKill { + marked: Some(t), .. + } => *t == target, + _ => false, + }); + game.r#continue().r#continue(); assert_eq!(game.next().title(), ActionPromptTitle::Shapeshifter); - assert_eq!( - game.response(ActionResponse::Shapeshifter(true)), - ActionResult::Continue, - ); + game.response(ActionResponse::Shapeshift).r#continue(); assert_eq!(game.next().title(), ActionPromptTitle::RoleChange); - assert_eq!( - game.response(ActionResponse::RoleChangeAck), - ActionResult::GoBackToSleep - ); + game.r#continue().sleep(); game.next_expect_day(); } diff --git a/werewolves-proto/src/game_test/night_order.rs b/werewolves-proto/src/game_test/night_order.rs index ed7888f..ae05e38 100644 --- a/werewolves-proto/src/game_test/night_order.rs +++ b/werewolves-proto/src/game_test/night_order.rs @@ -31,9 +31,11 @@ fn night_order() { character_id: character_identity(), }, ActionPrompt::WolfPackKill { + marked: None, living_villagers: Box::new([]), }, ActionPrompt::Protector { + marked: None, character_id: character_identity(), targets: Box::new([]), }, diff --git a/werewolves-proto/src/message/night.rs b/werewolves-proto/src/message/night.rs index 28a208f..5148898 100644 --- a/werewolves-proto/src/message/night.rs +++ b/werewolves-proto/src/message/night.rs @@ -2,11 +2,14 @@ use serde::{Deserialize, Serialize}; use werewolves_macros::{ChecksAs, Titles}; use crate::{ + error::GameError, message::CharacterIdentity, player::CharacterId, role::{Alignment, PreviousGuardianAction, RoleTitle}, }; +type Result = core::result::Result; + #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, PartialOrd)] pub enum ActionType { Cover, @@ -37,7 +40,6 @@ pub enum ActionPrompt { #[checks(ActionType::Cover)] CoverOfDarkness, #[checks(ActionType::WolfPackKill)] - #[checks] WolvesIntro { wolves: Box<[(CharacterIdentity, RoleTitle)]>, }, @@ -50,48 +52,57 @@ pub enum ActionPrompt { Seer { character_id: CharacterIdentity, living_players: Box<[CharacterIdentity]>, + marked: Option, }, #[checks(ActionType::Protect)] Protector { character_id: CharacterIdentity, targets: Box<[CharacterIdentity]>, + marked: Option, }, #[checks(ActionType::Other)] Arcanist { character_id: CharacterIdentity, living_players: Box<[CharacterIdentity]>, + marked: (Option, Option), }, #[checks(ActionType::Other)] Gravedigger { character_id: CharacterIdentity, dead_players: Box<[CharacterIdentity]>, + marked: Option, }, #[checks(ActionType::Other)] Hunter { character_id: CharacterIdentity, current_target: Option, living_players: Box<[CharacterIdentity]>, + marked: Option, }, #[checks(ActionType::Other)] Militia { character_id: CharacterIdentity, living_players: Box<[CharacterIdentity]>, + marked: Option, }, #[checks(ActionType::Other)] MapleWolf { character_id: CharacterIdentity, kill_or_die: bool, living_players: Box<[CharacterIdentity]>, + marked: Option, }, #[checks(ActionType::Protect)] Guardian { character_id: CharacterIdentity, previous: Option, living_players: Box<[CharacterIdentity]>, + marked: Option, }, #[checks(ActionType::WolfPackKill)] WolfPackKill { living_villagers: Box<[CharacterIdentity]>, + marked: Option, }, #[checks(ActionType::OtherWolf)] Shapeshifter { character_id: CharacterIdentity }, @@ -99,15 +110,147 @@ pub enum ActionPrompt { AlphaWolf { character_id: CharacterIdentity, living_villagers: Box<[CharacterIdentity]>, + marked: Option, }, #[checks(ActionType::Direwolf)] DireWolf { character_id: CharacterIdentity, living_players: Box<[CharacterIdentity]>, + marked: Option, }, } impl ActionPrompt { + pub fn with_mark(&self, mark: CharacterId) -> Result { + let mut prompt = self.clone(); + match &mut prompt { + ActionPrompt::WolvesIntro { .. } + | ActionPrompt::RoleChange { .. } + | ActionPrompt::Shapeshifter { .. } + | ActionPrompt::CoverOfDarkness => Err(GameError::InvalidMessageForGameState), + + ActionPrompt::Guardian { + previous, + living_players, + marked, + .. + } => { + if !living_players.iter().any(|c| c.character_id == mark) + || previous + .as_ref() + .and_then(|p| match p { + PreviousGuardianAction::Protect(_) => None, + PreviousGuardianAction::Guard(c) => Some(c.character_id == mark), + }) + .unwrap_or_default() + { + // not in target list OR guarded target previous night + return Err(GameError::InvalidTarget); + } + match marked.as_mut() { + Some(marked_cid) => { + if marked_cid == &mark { + marked.take(); + } else { + marked.replace(mark); + } + } + None => { + marked.replace(mark); + } + } + Ok(prompt) + } + ActionPrompt::Arcanist { + living_players: targets, + marked, + .. + } => { + if !targets.iter().any(|t| t.character_id == mark) { + return Err(GameError::InvalidTarget); + } + match marked { + (None, Some(m)) | (Some(m), None) => { + if *m == mark { + *marked = (None, None); + } else { + *marked = (Some(m.clone()), Some(mark)); + } + } + (None, None) => *marked = (Some(mark), None), + (Some(m1), Some(m2)) => { + if *m1 == mark { + *marked = (Some(m2.clone()), None); + } else if *m2 == mark { + *marked = (Some(m1.clone()), None); + } else { + *marked = (Some(m2.clone()), Some(mark)); + } + } + } + + Ok(prompt) + } + + ActionPrompt::Protector { + targets, marked, .. + } + | ActionPrompt::Seer { + living_players: targets, + marked, + .. + } + | ActionPrompt::Gravedigger { + dead_players: targets, + marked, + .. + } + | ActionPrompt::Hunter { + living_players: targets, + marked, + .. + } + | ActionPrompt::Militia { + living_players: targets, + marked, + .. + } + | ActionPrompt::MapleWolf { + living_players: targets, + marked, + .. + } + | ActionPrompt::WolfPackKill { + living_villagers: targets, + marked, + .. + } + | ActionPrompt::AlphaWolf { + living_villagers: targets, + marked, + .. + } + | ActionPrompt::DireWolf { + living_players: targets, + marked, + .. + } => { + if !targets.iter().any(|t| t.character_id == mark) { + return Err(GameError::InvalidTarget); + } + if let Some(marked_char) = marked.as_ref() + && *marked_char == mark + { + marked.take(); + } else { + marked.replace(mark); + } + + Ok(prompt) + } + } + } + pub const fn is_wolfy(&self) -> bool { self.action_type().is_wolfy() || match self { @@ -126,25 +269,22 @@ impl PartialOrd for ActionPrompt { } } -#[derive(Debug, Clone, Serialize, PartialEq, Deserialize, ChecksAs)] +#[derive(Debug, Clone, Serialize, PartialEq, Deserialize)] pub enum ActionResponse { - Seer(CharacterId), - Arcanist(CharacterId, CharacterId), - Gravedigger(CharacterId), - Hunter(CharacterId), - Militia(Option), - MapleWolf(Option), - Guardian(CharacterId), - WolfPackKillVote(CharacterId), - #[checks] - Shapeshifter(bool), - AlphaWolf(Option), - Direwolf(CharacterId), - Protector(CharacterId), - #[checks] - RoleChangeAck, - WolvesIntroAck, - ClearCoverOfDarkness, + // Seer(CharacterId), + // Arcanist(Option, Option), + // Gravedigger(CharacterId), + // Hunter(CharacterId), + // Militia(Option), + // MapleWolf(Option), + // Guardian(CharacterId), + // WolfPackKillVote(CharacterId), + // AlphaWolf(Option), + // Direwolf(CharacterId), + // Protector(CharacterId), + MarkTarget(CharacterId), + Shapeshift, + Continue, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] diff --git a/werewolves-proto/src/player.rs b/werewolves-proto/src/player.rs index d74a1c8..9dfa102 100644 --- a/werewolves-proto/src/player.rs +++ b/werewolves-proto/src/player.rs @@ -8,7 +8,7 @@ use crate::{ game::{DateTime, Village}, message::{CharacterIdentity, Identification, PublicIdentity, night::ActionPrompt}, modifier::Modifier, - role::{MAPLE_WOLF_ABSTAIN_LIMIT, PreviousGuardianAction, Role, RoleTitle}, + role::{Alignment, MAPLE_WOLF_ABSTAIN_LIMIT, PreviousGuardianAction, Role, RoleTitle}, }; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] @@ -173,6 +173,19 @@ impl Character { &self.role } + pub const fn gravedigger_dig(&self) -> Option { + match &self.role { + Role::Shapeshifter { + shifted_into: Some(_), + } => None, + _ => Some(self.role.title()), + } + } + + pub const fn alignment(&self) -> Alignment { + self.role.alignment() + } + pub const fn role_mut(&mut self) -> &mut Role { &mut self.role } @@ -222,22 +235,26 @@ impl Character { Role::Seer => ActionPrompt::Seer { character_id: self.identity(), living_players: village.living_players_excluding(self.character_id()), + marked: None, }, Role::Arcanist => ActionPrompt::Arcanist { character_id: self.identity(), living_players: village.living_players_excluding(self.character_id()), + marked: (None, None), }, Role::Protector { last_protected: Some(last_protected), } => ActionPrompt::Protector { character_id: self.identity(), targets: village.living_players_excluding(last_protected), + marked: None, }, Role::Protector { last_protected: None, } => ActionPrompt::Protector { character_id: self.identity(), targets: village.living_players_excluding(self.character_id()), + marked: None, }, Role::Apprentice(role) => { let current_night = match village.date_time() { @@ -273,17 +290,21 @@ impl Character { Role::Militia { targeted: None } => ActionPrompt::Militia { character_id: self.identity(), living_players: village.living_players_excluding(self.character_id()), + marked: None, }, Role::Werewolf => ActionPrompt::WolfPackKill { living_villagers: village.living_players(), + marked: None, }, Role::AlphaWolf { killed: None } => ActionPrompt::AlphaWolf { character_id: self.identity(), living_villagers: village.living_players_excluding(self.character_id()), + marked: None, }, Role::DireWolf => ActionPrompt::DireWolf { character_id: self.identity(), living_players: village.living_players(), + marked: None, }, Role::Shapeshifter { shifted_into: None } => ActionPrompt::Shapeshifter { character_id: self.identity(), @@ -291,16 +312,19 @@ impl Character { Role::Gravedigger => ActionPrompt::Gravedigger { character_id: self.identity(), dead_players: village.dead_targets(), + marked: None, }, Role::Hunter { target } => ActionPrompt::Hunter { character_id: self.identity(), current_target: target.as_ref().and_then(|t| village.target_by_id(t)), living_players: village.living_players_excluding(self.character_id()), + marked: None, }, Role::MapleWolf { last_kill_on_night } => ActionPrompt::MapleWolf { character_id: self.identity(), kill_or_die: last_kill_on_night + MAPLE_WOLF_ABSTAIN_LIMIT.get() == night, living_players: village.living_players_excluding(self.character_id()), + marked: None, }, Role::Guardian { last_protected: Some(PreviousGuardianAction::Guard(prev_target)), @@ -308,6 +332,7 @@ impl Character { character_id: self.identity(), previous: Some(PreviousGuardianAction::Guard(prev_target.clone())), living_players: village.living_players_excluding(&prev_target.character_id), + marked: None, }, Role::Guardian { last_protected: Some(PreviousGuardianAction::Protect(prev_target)), @@ -315,6 +340,7 @@ impl Character { character_id: self.identity(), previous: Some(PreviousGuardianAction::Protect(prev_target.clone())), living_players: village.living_players(), + marked: None, }, Role::Guardian { last_protected: None, @@ -322,6 +348,7 @@ impl Character { character_id: self.identity(), previous: None, living_players: village.living_players(), + marked: None, }, })) } diff --git a/werewolves-server/src/client.rs b/werewolves-server/src/client.rs index 6539488..fcd7b15 100644 --- a/werewolves-server/src/client.rs +++ b/werewolves-server/src/client.rs @@ -121,7 +121,6 @@ struct Client { who: String, sender: Sender, receiver: Receiver, - // message_history: Vec, } impl Client { @@ -140,7 +139,6 @@ impl Client { who, sender, receiver, - // message_history: Vec::new(), } } #[cfg(feature = "cbor")] diff --git a/werewolves-server/src/lobby.rs b/werewolves-server/src/lobby.rs index 860e121..69a00f8 100644 --- a/werewolves-server/src/lobby.rs +++ b/werewolves-server/src/lobby.rs @@ -91,7 +91,7 @@ impl Lobby { .unwrap() .next_message() .await - .expect("get next message"); + .expect("get next message"); // TODO: keeps happening match self.next_inner(msg.clone()).await.map_err(|err| (msg, err)) { Ok(None) => {} diff --git a/werewolves/index.scss b/werewolves/index.scss index e2c39a7..48ee25d 100644 --- a/werewolves/index.scss +++ b/werewolves/index.scss @@ -863,7 +863,8 @@ input { } -.character-picker { +.character-picker, +.target-picker { display: flex; flex-direction: column; width: 100%; @@ -891,6 +892,16 @@ input { } } + &.dead { + $bg: rgba(128, 128, 128, 0.5); + background-color: $bg; + border: 1px solid color.change($bg, $alpha: 1.0); + + &:hover { + background-color: color.change($bg, $alpha: 1.0); + } + } + background-color: $village_bg; border: 1px solid $village_border; diff --git a/werewolves/src/components/action/picker.rs b/werewolves/src/components/action/picker.rs new file mode 100644 index 0000000..75d3499 --- /dev/null +++ b/werewolves/src/components/action/picker.rs @@ -0,0 +1,76 @@ +use werewolves_proto::{ + message::{CharacterIdentity, PublicIdentity}, + player::CharacterId, +}; +use yew::prelude::*; + +use crate::components::{Button, Identity}; + +#[derive(Debug, Clone, PartialEq, Properties)] +pub struct TargetPickerProps { + pub targets: Box<[CharacterIdentity]>, + pub marked: Box<[CharacterId]>, + pub mark_callback: Option>, + pub continue_callback: Option>, +} + +#[function_component] +pub fn TargetPicker( + TargetPickerProps { + targets, + marked, + mark_callback, + continue_callback, + }: &TargetPickerProps, +) -> Html { + let targets = targets + .iter() + .map(|t| { + let cb = mark_callback.clone(); + let marked = marked.contains(&t.character_id); + html! { + + } + }) + .collect::(); + + html! { +
+
+ {targets} +
+ +
+ } +} + +#[derive(Debug, Clone, PartialEq, Properties)] +pub struct TargetCardProps { + pub target: CharacterIdentity, + pub marked: bool, + pub mark_callback: Option>, +} + +#[function_component] +pub fn TargetCard( + TargetCardProps { + target, + marked, + mark_callback, + }: &TargetCardProps, +) -> Html { + let click_target = target.character_id.clone(); + let on_click = mark_callback + .clone() + .map(|cb| Callback::from(move |_| cb.emit(click_target.clone()))) + .unwrap_or_default(); + let marked = marked.then_some("marked"); + let ident: PublicIdentity = target.into(); + html! { + + } +} diff --git a/werewolves/src/components/action/prompt.rs b/werewolves/src/components/action/prompt.rs index ef0466b..0f8a698 100644 --- a/werewolves/src/components/action/prompt.rs +++ b/werewolves/src/components/action/prompt.rs @@ -13,7 +13,7 @@ use yew::prelude::*; use crate::components::{ Button, CoverOfDarkness, Identity, - action::{BinaryChoice, OptionalSingleTarget, SingleTarget, TwoTarget, WolvesIntro}, + action::{BinaryChoice, TargetPicker, WolvesIntro}, }; #[derive(Debug, Clone, PartialEq, Properties)] @@ -41,241 +41,62 @@ fn identity_html(props: &ActionPromptProps, ident: Option<&CharacterIdentity>) - #[function_component] pub fn Prompt(props: &ActionPromptProps) -> Html { - match &props.prompt { + let on_complete = props.on_complete.clone(); + let continue_callback = props.big_screen.not().then(|| { + Callback::from(move |_| { + on_complete.emit(HostMessage::InGame(HostGameMessage::Night( + HostNightMessage::ActionResponse(ActionResponse::Continue), + ))) + }) + }); + let on_complete = props.on_complete.clone(); + let mark_callback = props.big_screen.not().then(|| { + Callback::from(move |target: CharacterId| { + on_complete.emit(HostMessage::InGame(HostGameMessage::Night( + HostNightMessage::ActionResponse(ActionResponse::MarkTarget(target)), + ))); + }) + }); + let (character_id, targets, marked, role_info) = 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::ActionResponse(ActionResponse::ClearCoverOfDarkness), - ))) - }) - }); return html! { - + }; } ActionPrompt::WolvesIntro { wolves } => { - let on_complete = props.on_complete.clone(); - let on_complete = Callback::from(move |_| { - on_complete.emit(HostMessage::InGame(HostGameMessage::Night( - HostNightMessage::ActionResponse( - werewolves_proto::message::night::ActionResponse::WolvesIntroAck, - ), - ))) - }); - html! { + return html! { - } - } - 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| { - on_complete.emit(HostMessage::InGame(HostGameMessage::Night( - HostNightMessage::ActionResponse(ActionResponse::Seer(target)), - ))); - }) - }); - html! { -
- {identity_html(props, Some(&character_id))} - -
- } + }; } 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( - HostNightMessage::ActionResponse(ActionResponse::RoleChangeAck), - ))) - }); - let cont = props.big_screen.not().then(|| { + let cont = continue_callback.map(|continue_callback| { html! { - + } }); - html! { -
- {identity_html(props, Some(&character_id))} + return html! { +
+ {identity_html(props, Some(character_id))}

{"your role has changed"}

-

{new_role.to_string()}

+

{new_role.to_string()}

{cont}
- } - } - 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| { - on_complete.emit(HostMessage::InGame(HostGameMessage::Night( - HostNightMessage::ActionResponse(ActionResponse::Protector(target)), - ))); - }) - }); - html! { -
- {identity_html(props, Some(&character_id))} - -
- } - } - 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)| { - on_complete.emit(HostMessage::InGame(HostGameMessage::Night( - HostNightMessage::ActionResponse(ActionResponse::Arcanist(t1, t2)), - ))); - }) - }); - html! { -
- {identity_html(props, Some(&character_id))} - -
- } - } - 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| { - on_complete.emit(HostMessage::InGame(HostGameMessage::Night( - HostNightMessage::ActionResponse(ActionResponse::Gravedigger(target)), - ))); - }) - }); - html! { -
- {identity_html(props, Some(&character_id))} - -
- } - } - ActionPrompt::Hunter { - character_id, - current_target, - living_players, - } => { - let on_complete = props.on_complete.clone(); - let on_select = props.big_screen.not().then(|| { - Callback::from(move |target: CharacterId| { - on_complete.emit(HostMessage::InGame(HostGameMessage::Night( - HostNightMessage::ActionResponse(ActionResponse::Hunter(target)), - ))); - }) - }); - html! { -
- {identity_html(props, Some(&character_id))} - -

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

-
-
- } - } - 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| { - on_complete.emit(HostMessage::InGame(HostGameMessage::Night( - HostNightMessage::ActionResponse(ActionResponse::Militia(target)), - ))); - }) - }); - html! { -
- {identity_html(props, Some(&character_id))} - -
- } - } - ActionPrompt::MapleWolf { - character_id, - kill_or_die, - living_players, - } => { - let on_complete = props.on_complete.clone(); - let on_select = props.big_screen.not().then(|| { - Callback::from(move |target: Option| { - on_complete.emit(HostMessage::InGame(HostGameMessage::Night( - HostNightMessage::ActionResponse(ActionResponse::MapleWolf(target)), - ))); - }) - }); - let kill_or_die = kill_or_die.then(|| { - html! { - {"if you fail to eat tonight, you will starve"} - } - }); - html! { -
- {identity_html(props, Some(&character_id))} - - {kill_or_die} - -
- } + }; } + ActionPrompt::Guardian { character_id, previous, living_players, + marked, } => { let last_protect = previous.as_ref().map(|prev| match prev { PreviousGuardianAction::Protect(target) => { @@ -293,108 +114,515 @@ pub fn Prompt(props: &ActionPromptProps) -> Html { }, }); - let on_complete = props.on_complete.clone(); - let on_select = props.big_screen.not().then_some({ - move |prot| { - on_complete.emit(HostMessage::InGame(HostGameMessage::Night( - HostNightMessage::ActionResponse(ActionResponse::Guardian(prot)), - ))); - } - }); + let marked = marked.iter().cloned().collect::>(); - html! { + return html! {
- {identity_html(props, Some(&character_id))} - - {last_protect} - + {identity_html(props, Some(character_id))} +

{"guardian"}

+ {last_protect} +
- } - } - ActionPrompt::WolfPackKill { living_villagers } => { - let on_complete = props.on_complete.clone(); - let on_select = props.big_screen.not().then(|| { - Callback::from(move |target: CharacterId| { - on_complete.emit(HostMessage::InGame(HostGameMessage::Night( - HostNightMessage::ActionResponse(ActionResponse::WolfPackKillVote(target)), - ))); - }) - }); - html! { - - } + }; } + + ActionPrompt::Seer { + character_id, + living_players, + marked, + } => ( + Some(character_id), + living_players, + marked.iter().cloned().collect::>(), + html! {{"seer"}}, + ), + ActionPrompt::Protector { + character_id, + targets, + marked, + } => ( + Some(character_id), + targets, + marked.iter().cloned().collect(), + html! {{"protector"}}, + ), + ActionPrompt::Arcanist { + character_id, + living_players, + marked, + } => ( + Some(character_id), + living_players, + [&marked.0, &marked.1] + .iter() + .filter_map(|c| (*c).clone()) + .collect(), + html! {{"arcanist"}}, + ), + ActionPrompt::Gravedigger { + character_id, + dead_players, + marked, + } => ( + Some(character_id), + dead_players, + marked.iter().cloned().collect(), + html! {{"gravedigger"}}, + ), + ActionPrompt::Hunter { + character_id, + current_target, + living_players, + marked, + } => ( + Some(character_id), + living_players, + marked.iter().cloned().collect(), + { + let current_target = current_target.as_ref().cloned().map(|t| { + html! { + <> +

{"current target:"}

+ ::into(t)} /> + + } + }); + html! { + <> +

{"hunter"}

+ {current_target} + + } + }, + ), + ActionPrompt::Militia { + character_id, + living_players, + marked, + } => ( + Some(character_id), + living_players, + marked.iter().cloned().collect(), + html! {{"militia"}}, + ), + ActionPrompt::MapleWolf { + character_id, + kill_or_die, + living_players, + marked, + } => ( + Some(character_id), + living_players, + marked.iter().cloned().collect(), + html! {<>{"maple wolf"} {kill_or_die.then_some(" — starving")}}, + ), + ActionPrompt::WolfPackKill { + living_villagers, + marked, + } => ( + None, + living_villagers, + marked.iter().cloned().collect(), + html! {{"wolfpack kill"}}, + ), ActionPrompt::Shapeshifter { character_id } => { let on_complete = props.on_complete.clone(); let on_select = props.big_screen.not().then_some({ move |shift| { on_complete.emit(HostMessage::InGame(HostGameMessage::Night( - HostNightMessage::ActionResponse(ActionResponse::Shapeshifter(shift)), + HostNightMessage::ActionResponse(if shift { + ActionResponse::Shapeshift + } else { + ActionResponse::Continue + }), ))); } }); - html! { + return html! {
- {identity_html(props, Some(&character_id))} + {identity_html(props, Some(character_id))}

{"shapeshift?"}

- } + }; } 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| { - on_complete.emit(HostMessage::InGame(HostGameMessage::Night( - HostNightMessage::ActionResponse(ActionResponse::AlphaWolf(target)), - ))); - }) - }); - html! { -
- {identity_html(props, Some(&character_id))} - -
- } - } + marked, + } => ( + Some(character_id), + living_villagers, + marked.iter().cloned().collect(), + html! {{"alpha wolf"}}, + ), 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| { - on_complete.emit(HostMessage::InGame(HostGameMessage::Night( - HostNightMessage::ActionResponse(ActionResponse::Direwolf(target)), - ))); - }) - }); - html! { -
- {identity_html(props, Some(&character_id))} - -
- } - } + marked, + } => ( + Some(character_id), + living_players, + marked.iter().cloned().collect(), + html! {{"dire wolf"}}, + ), + }; + + html! { +
+ {identity_html(props, character_id)} +

{role_info}

+ +
} + + // 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::ActionResponse(ActionResponse::ClearCoverOfDarkness), + // ))) + // }) + // }); + // return html! { + // + // }; + // } + // ActionPrompt::WolvesIntro { wolves } => { + // let on_complete = props.on_complete.clone(); + // let on_complete = Callback::from(move |_| { + // on_complete.emit(HostMessage::InGame(HostGameMessage::Night( + // HostNightMessage::ActionResponse( + // werewolves_proto::message::night::ActionResponse::WolvesIntroAck, + // ), + // ))) + // }); + // html! { + // + // } + // } + // 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| { + // on_complete.emit(HostMessage::InGame(HostGameMessage::Night( + // HostNightMessage::ActionResponse(ActionResponse::Seer(target)), + // ))); + // }) + // }); + // html! { + //
+ // {identity_html(props, Some(&character_id))} + // + //
+ // } + // } + // 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( + // HostNightMessage::ActionResponse(ActionResponse::RoleChangeAck), + // ))) + // }); + // let cont = props.big_screen.not().then(|| { + // html! { + // + // } + // }); + // html! { + //
+ // {identity_html(props, Some(&character_id))} + //

{"your role has changed"}

+ //

{new_role.to_string()}

+ // {cont} + //
+ // } + // } + // 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| { + // on_complete.emit(HostMessage::InGame(HostGameMessage::Night( + // HostNightMessage::ActionResponse(ActionResponse::Protector(target)), + // ))); + // }) + // }); + // html! { + //
+ // {identity_html(props, Some(&character_id))} + // + //
+ // } + // } + // 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)| { + // on_complete.emit(HostMessage::InGame(HostGameMessage::Night( + // HostNightMessage::ActionResponse(ActionResponse::Arcanist(t1, t2)), + // ))); + // }) + // }); + // html! { + //
+ // {identity_html(props, Some(&character_id))} + // + //
+ // } + // } + // 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| { + // on_complete.emit(HostMessage::InGame(HostGameMessage::Night( + // HostNightMessage::ActionResponse(ActionResponse::Gravedigger(target)), + // ))); + // }) + // }); + // html! { + //
+ // {identity_html(props, Some(&character_id))} + // + //
+ // } + // } + // ActionPrompt::Hunter { + // character_id, + // current_target, + // living_players, + // } => { + // let on_complete = props.on_complete.clone(); + // let on_select = props.big_screen.not().then(|| { + // Callback::from(move |target: CharacterId| { + // on_complete.emit(HostMessage::InGame(HostGameMessage::Night( + // HostNightMessage::ActionResponse(ActionResponse::Hunter(target)), + // ))); + // }) + // }); + // html! { + //
+ // {identity_html(props, Some(&character_id))} + // + //

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

+ //
+ //
+ // } + // } + // 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| { + // on_complete.emit(HostMessage::InGame(HostGameMessage::Night( + // HostNightMessage::ActionResponse(ActionResponse::Militia(target)), + // ))); + // }) + // }); + // html! { + //
+ // {identity_html(props, Some(&character_id))} + // + //
+ // } + // } + // ActionPrompt::MapleWolf { + // character_id, + // kill_or_die, + // living_players, + // marked, + // } => { + // let kill_or_die = kill_or_die.then(|| { + // html! { + // {"if you fail to eat tonight, you will starve"} + // } + // }); + // html! { + //
+ // {identity_html(props, Some(&character_id))} + // + // {kill_or_die} + // + //
+ // } + // } + // ActionPrompt::Guardian { + // character_id, + // previous, + // living_players, + // marked, + // } => { + // let last_protect = previous.as_ref().map(|prev| match prev { + // PreviousGuardianAction::Protect(target) => { + // html! { + // <> + // {"last night you protected: "} + // ::into(target)}/> + // + // } + // } + // PreviousGuardianAction::Guard(target) => html! { + // <> + // {"last night you guarded: "} + // ::into(target)}/> + // + // }, + // }); + // let marked = marked.iter().cloned().collect(); + + // html! { + //
+ // {identity_html(props, Some(&character_id))} + //

{"guardian"}

+ // {last_protect} + // + //
+ // } + // } + // ActionPrompt::WolfPackKill { living_villagers } => { + // let on_complete = props.on_complete.clone(); + + // html! { + // + // } + // } + // ActionPrompt::Shapeshifter { character_id } => { + // let on_complete = props.on_complete.clone(); + // let on_select = props.big_screen.not().then_some({ + // move |shift| { + // on_complete.emit(HostMessage::InGame(HostGameMessage::Night( + // HostNightMessage::ActionResponse(ActionResponse::Shapeshifter(shift)), + // ))); + // } + // }); + // html! { + //
+ // {identity_html(props, Some(&character_id))} + // + //

{"shapeshift?"}

+ //
+ //
+ // } + // } + // 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| { + // on_complete.emit(HostMessage::InGame(HostGameMessage::Night( + // HostNightMessage::ActionResponse(ActionResponse::AlphaWolf(target)), + // ))); + // }) + // }); + // html! { + //
+ // {identity_html(props, Some(&character_id))} + // + //
+ // } + // } + // 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| { + // on_complete.emit(HostMessage::InGame(HostGameMessage::Night( + // HostNightMessage::ActionResponse(ActionResponse::Direwolf(target)), + // ))); + // }) + // }); + // html! { + //
+ // {identity_html(props, Some(&character_id))} + // + //
+ // } + // } + // } } diff --git a/werewolves/src/components/action/result.rs b/werewolves/src/components/action/result.rs index 5c73345..96ffa5a 100644 --- a/werewolves/src/components/action/result.rs +++ b/werewolves/src/components/action/result.rs @@ -11,14 +11,13 @@ use werewolves_proto::{ }; use yew::prelude::*; -use crate::components::{CoverOfDarkness, Identity}; +use crate::components::{Button, CoverOfDarkness, Identity}; #[derive(Debug, Clone, PartialEq, Properties)] pub struct ActionResultProps { pub result: ActionResult, #[prop_or_default] pub ident: Option, - #[prop_or_default] pub big_screen: bool, pub on_complete: Callback, } @@ -40,7 +39,7 @@ pub fn ActionResultView(props: &ActionResultProps) -> Html { let cont = props .big_screen .not() - .then(|| html! {}); + .then(|| html! {}); match &props.result { ActionResult::RoleBlocked => { html! { diff --git a/werewolves/src/components/action/wolves.rs b/werewolves/src/components/action/wolves.rs index c673ef0..fbe8197 100644 --- a/werewolves/src/components/action/wolves.rs +++ b/werewolves/src/components/action/wolves.rs @@ -1,5 +1,3 @@ -use core::ops::Not; - use werewolves_proto::{ message::{CharacterIdentity, PublicIdentity}, role::RoleTitle, @@ -11,14 +9,18 @@ use crate::components::{Button, Identity}; #[derive(Debug, Clone, PartialEq, Properties)] pub struct WolvesIntroProps { pub wolves: Box<[(CharacterIdentity, RoleTitle)]>, - pub big_screen: bool, - pub on_complete: Callback<()>, + pub on_complete: Option>, } #[function_component] pub fn WolvesIntro(props: &WolvesIntroProps) -> Html { - let on_complete = props.on_complete.clone(); - let on_complete = Callback::from(move |_| on_complete.emit(())); + let on_complete = props.on_complete.clone().map(|on_complete| { + html! { + + } + }); html! {

{"these are the wolves:"}

@@ -32,11 +34,7 @@ pub fn WolvesIntro(props: &WolvesIntroProps) -> Html { }).collect::() }
- { - props.big_screen.not().then_some(html!{ - - }) - } + {on_complete}
} } diff --git a/werewolves/src/components/host/daytime.rs b/werewolves/src/components/host/daytime.rs index f2b94c5..2d8e585 100644 --- a/werewolves/src/components/host/daytime.rs +++ b/werewolves/src/components/host/daytime.rs @@ -91,9 +91,14 @@ pub fn DaytimePlayer( let dead = died_to.is_some().then_some("dead"); let marked = on_the_block.then_some("marked"); let character_id = identity.character_id.clone(); - let on_click: Callback<_> = on_select - .clone() - .map(|on_select| Callback::from(move |_| on_select.emit(character_id.clone()))) + let on_click: Callback<_> = died_to + .is_none() + .then_some(()) + .and( + on_select + .clone() + .map(|on_select| Callback::from(move |_| on_select.emit(character_id.clone()))), + ) .unwrap_or_default(); let identity: PublicIdentity = identity.into(); html! {