From d352cfb1ee450dff184bb4d32928379c5dec8eb3 Mon Sep 17 00:00:00 2001 From: emilis Date: Sun, 28 Sep 2025 02:13:34 +0100 Subject: [PATCH] target split, more prompts, improved kills --- werewolves-proto/src/diedto.rs | 30 +- werewolves-proto/src/error.rs | 4 + werewolves-proto/src/game/kill.rs | 292 +++++++++++++++++ werewolves-proto/src/game/mod.rs | 21 ++ werewolves-proto/src/game/night.rs | 249 ++++++-------- werewolves-proto/src/game/village.rs | 13 + werewolves-proto/src/message.rs | 2 +- werewolves-proto/src/message/host.rs | 1 + werewolves-proto/src/message/night.rs | 147 +++++---- werewolves-server/Cargo.toml | 2 +- werewolves/Cargo.toml | 4 +- werewolves/index.scss | 37 ++- werewolves/src/components/action/binary.rs | 18 +- werewolves/src/components/action/prompt.rs | 159 +++++++-- werewolves/src/components/action/target.rs | 362 ++++++++++++++++----- werewolves/src/components/host/mod.rs | 2 +- werewolves/src/pages/host.rs | 7 +- 17 files changed, 1000 insertions(+), 350 deletions(-) create mode 100644 werewolves-proto/src/game/kill.rs diff --git a/werewolves-proto/src/diedto.rs b/werewolves-proto/src/diedto.rs index 4ef858f..3f96e21 100644 --- a/werewolves-proto/src/diedto.rs +++ b/werewolves-proto/src/diedto.rs @@ -1,14 +1,16 @@ use core::{fmt::Debug, num::NonZeroU8}; use serde::{Deserialize, Serialize}; +use werewolves_macros::Extract; use crate::{game::DateTime, player::CharacterId}; -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Extract)] pub enum DiedTo { Execution { day: NonZeroU8, }, + #[extract(source as killer)] MapleWolf { source: CharacterId, night: NonZeroU8, @@ -17,13 +19,17 @@ pub enum DiedTo { MapleWolfStarved { night: NonZeroU8, }, + #[extract(killer as killer)] Militia { killer: CharacterId, night: NonZeroU8, }, + #[extract(killing_wolf as killer)] Wolfpack { + killing_wolf: CharacterId, night: NonZeroU8, }, + #[extract(killer as killer)] AlphaWolf { killer: CharacterId, night: NonZeroU8, @@ -32,12 +38,17 @@ pub enum DiedTo { into: CharacterId, night: NonZeroU8, }, + #[extract(killer as killer)] Hunter { killer: CharacterId, night: NonZeroU8, }, - Guardian { - killer: CharacterId, + #[extract(source as killer)] + GuardianProtecting { + source: CharacterId, + protecting: CharacterId, + protecting_from: CharacterId, + protecting_from_cause: Box, night: NonZeroU8, }, } @@ -47,7 +58,13 @@ impl DiedTo { match self { DiedTo::Execution { day } => DateTime::Day { number: *day }, - DiedTo::Guardian { killer: _, night } + DiedTo::GuardianProtecting { + source: _, + protecting: _, + protecting_from: _, + protecting_from_cause: _, + night, + } | DiedTo::MapleWolf { source: _, night, @@ -55,7 +72,10 @@ impl DiedTo { } | DiedTo::MapleWolfStarved { night } | DiedTo::Militia { killer: _, night } - | DiedTo::Wolfpack { night } + | DiedTo::Wolfpack { + night, + killing_wolf: _, + } | DiedTo::AlphaWolf { killer: _, night } | DiedTo::Shapeshift { into: _, night } | DiedTo::Hunter { killer: _, night } => DateTime::Night { diff --git a/werewolves-proto/src/error.rs b/werewolves-proto/src/error.rs index 1c9a917..813bb25 100644 --- a/werewolves-proto/src/error.rs +++ b/werewolves-proto/src/error.rs @@ -69,4 +69,8 @@ pub enum GameError { GameOngoing, #[error("needs a role reveal")] NeedRoleReveal, + #[error("no previous state")] + NoPreviousState, + #[error("invalid original kill for guardian guard")] + GuardianInvalidOriginalKill, } diff --git a/werewolves-proto/src/game/kill.rs b/werewolves-proto/src/game/kill.rs new file mode 100644 index 0000000..ff78ebb --- /dev/null +++ b/werewolves-proto/src/game/kill.rs @@ -0,0 +1,292 @@ +use core::{ + num::NonZeroU8, + ops::{Deref, Not}, +}; + +use super::Result; +use crate::{ + diedto::DiedTo, + error::GameError, + game::{Village, kill::taken::Taken, night::NightChange}, + player::{CharacterId, Protection}, +}; + +#[derive(Debug, PartialEq)] +pub enum KillOutcome { + Single(CharacterId, DiedTo), + Guarding { + original_killer: CharacterId, + original_target: CharacterId, + original_kill: DiedTo, + guardian: CharacterId, + night: NonZeroU8, + }, +} + +impl KillOutcome { + pub fn apply_to_village(self, village: &mut Village) -> Result<()> { + match self { + KillOutcome::Single(character_id, died_to) => Ok(village + .character_by_id_mut(&character_id) + .ok_or(GameError::InvalidTarget)? + .kill(died_to)), + KillOutcome::Guarding { + original_killer, + original_target, + original_kill, + guardian, + night, + } => { + // check if guardian exists before we mutably borrow killer, which would + // prevent us from borrowing village to check after. + village + .character_by_id(&guardian) + .ok_or(GameError::InvalidTarget)?; + village + .character_by_id_mut(&original_killer) + .ok_or(GameError::InvalidTarget)? + .kill(DiedTo::GuardianProtecting { + night, + source: guardian.clone(), + protecting: original_target, + protecting_from: original_killer, + protecting_from_cause: Box::new(original_kill.clone()), + }); + village + .character_by_id_mut(&guardian) + .ok_or(GameError::InvalidTarget)? + .kill(original_kill); + Ok(()) + } + } + } +} + +fn resolve_protection( + killer: CharacterId, + killed_with: &DiedTo, + target: &CharacterId, + protection: &Protection, + night: NonZeroU8, +) -> Option { + match protection { + Protection::Guardian { + source, + guarding: true, + } => Some(KillOutcome::Guarding { + original_killer: killer, + guardian: source.clone(), + original_target: target.clone(), + original_kill: killed_with.clone(), + night, + }), + Protection::Guardian { + source: _, + guarding: false, + } + | Protection::Protector { source: _ } => None, + } +} + +pub fn resolve_kill( + changes: &mut ChangesLookup<'_>, + target: &CharacterId, + died_to: &DiedTo, + night: u8, + village: &Village, +) -> Result> { + if let DiedTo::MapleWolf { + source, + night, + starves_if_fails: true, + } = died_to + && let Some(protection) = changes.protected_take(target) + { + return Ok(Some( + resolve_protection(source.clone(), died_to, target, &protection, *night).unwrap_or( + KillOutcome::Single(source.clone(), DiedTo::MapleWolfStarved { night: *night }), + ), + )); + } + if let DiedTo::Wolfpack { + night, + killing_wolf, + } = died_to + && let Some(ss_source) = changes.shapeshifter() + { + let killing_wolf = village + .character_by_id(killing_wolf) + .ok_or(GameError::InvalidTarget)?; + + match changes.protected_take(target) { + Some(protection) => { + return Ok(resolve_protection( + killing_wolf.character_id().clone(), + died_to, + target, + &protection, + *night, + )); + } + None => { + // Wolf kill went through -- can kill shifter + return Ok(Some(KillOutcome::Single( + ss_source.clone(), + DiedTo::Shapeshift { + into: target.clone(), + night: *night, + }, + ))); + } + }; + } + + let protection = match changes.protected_take(target) { + Some(prot) => prot, + None => return Ok(Some(KillOutcome::Single(target.clone(), died_to.clone()))), + }; + + match protection.deref() { + Protection::Guardian { + source, + guarding: true, + } => Ok(Some(KillOutcome::Guarding { + original_killer: died_to + .killer() + .ok_or(GameError::GuardianInvalidOriginalKill)? + .clone(), + original_target: target.clone(), + original_kill: died_to.clone(), + guardian: source.clone(), + night: NonZeroU8::new(night).unwrap(), + })), + Protection::Guardian { + source: _, + guarding: false, + } + | Protection::Protector { source: _ } => Ok(None), + } +} + +pub struct ChangesLookup<'a>(&'a [NightChange], Vec); + +impl<'a> ChangesLookup<'a> { + pub fn new(changes: &'a [NightChange]) -> Self { + Self(changes, Vec::new()) + } + + pub fn killed(&self, target: &CharacterId) -> Option<&'a DiedTo> { + self.0.iter().enumerate().find_map(|(idx, c)| { + self.1 + .contains(&idx) + .not() + .then(|| match c { + NightChange::Kill { target: t, died_to } => (t == target).then_some(died_to), + _ => None, + }) + .flatten() + }) + } + + pub fn release(&mut self, taken: Taken<'a, T>) { + self.1.swap_remove( + self.1 + .iter() + .enumerate() + .find_map(|(idx, c)| (*c == taken.idx()).then_some(idx)) + .unwrap(), + ); + } + + pub fn protected_take(&mut self, target: &CharacterId) -> Option> { + if let Some((idx, c)) = self.0.iter().enumerate().find_map(|(idx, c)| { + self.1 + .contains(&idx) + .not() + .then(|| match c { + NightChange::Protection { + target: t, + protection, + } => (t == target).then_some((idx, protection)), + _ => None, + }) + .flatten() + }) { + self.1.push(idx); + Some(Taken::new(idx, c)) + } else { + None + } + } + pub fn protected(&self, target: &CharacterId) -> Option<&'a Protection> { + self.0.iter().enumerate().find_map(|(idx, c)| { + self.1 + .contains(&idx) + .not() + .then(|| match c { + NightChange::Protection { + target: t, + protection, + } => (t == target).then_some(protection), + _ => None, + }) + .flatten() + }) + } + + pub fn shapeshifter(&self) -> Option<&'a CharacterId> { + self.0.iter().enumerate().find_map(|(idx, c)| { + self.1 + .contains(&idx) + .not() + .then_some(match c { + NightChange::Shapeshift { source } => Some(source), + _ => None, + }) + .flatten() + }) + } + + pub fn wolf_pack_kill_target(&self) -> Option<&'a CharacterId> { + self.0.iter().enumerate().find_map(|(idx, c)| { + self.1 + .contains(&idx) + .not() + .then_some(match c { + NightChange::Kill { + target, + died_to: + DiedTo::Wolfpack { + night: _, + killing_wolf: _, + }, + } => Some(target), + _ => None, + }) + .flatten() + }) + } +} + +mod taken { + use core::ops::Deref; + + pub struct Taken<'a, T>(usize, &'a T); + impl<'a, T> Taken<'a, T> { + pub const fn new(idx: usize, item: &'a T) -> Self { + Self(idx, item) + } + + pub const fn idx(&self) -> usize { + self.0 + } + } + + impl<'a, T> Deref for Taken<'a, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.1 + } + } +} diff --git a/werewolves-proto/src/game/mod.rs b/werewolves-proto/src/game/mod.rs index a0b6b7c..da12aee 100644 --- a/werewolves-proto/src/game/mod.rs +++ b/werewolves-proto/src/game/mod.rs @@ -1,3 +1,4 @@ +mod kill; mod night; mod settings; mod village; @@ -27,12 +28,14 @@ type Result = core::result::Result; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Game { previous: Vec, + next: Vec, state: GameState, } impl Game { pub fn new(players: &[Identification], settings: GameSettings) -> Result { Ok(Self { + next: Vec::new(), previous: Vec::new(), state: GameState::Night { night: Night::new(Village::new(players, settings)?)?, @@ -161,6 +164,24 @@ impl Game { }, HostGameMessage::Night(_), ) => Err(GameError::InvalidMessageForGameState), + ( + GameState::Day { + village: _, + marked: _, + }, + HostGameMessage::PreviousState, + ) => { + let mut prev = self.previous.pop().ok_or(GameError::NoPreviousState)?; + log::info!("previous state loaded: {prev:?}"); + core::mem::swap(&mut prev, &mut self.state); + self.next.push(prev); + + self.process(HostGameMessage::GetState) + } + (GameState::Night { night }, HostGameMessage::PreviousState) => { + night.previous_state()?; + self.process(HostGameMessage::GetState) + } } } diff --git a/werewolves-proto/src/game/night.rs b/werewolves-proto/src/game/night.rs index 7aae4f6..590259b 100644 --- a/werewolves-proto/src/game/night.rs +++ b/werewolves-proto/src/game/night.rs @@ -8,7 +8,10 @@ use super::Result; use crate::{ diedto::DiedTo, error::GameError, - game::{DateTime, Village}, + game::{ + DateTime, Village, + kill::{self, ChangesLookup}, + }, message::night::{ActionPrompt, ActionResponse, ActionResult}, player::{Character, CharacterId, Protection}, role::{PreviousGuardianAction, Role, RoleBlock, RoleTitle}, @@ -19,6 +22,7 @@ pub struct Night { village: Village, night: u8, action_queue: VecDeque<(ActionPrompt, Character)>, + used_actions: Vec<(ActionPrompt, Character)>, changes: Vec, night_state: NightState, } @@ -166,15 +170,62 @@ impl Night { village, night_state, action_queue, + used_actions: Vec::new(), }) } + pub fn previous_state(&mut self) -> Result<()> { + let (prev_act, prev_char) = 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.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, + }; + } + } + Ok(()) + } + pub fn collect_completed(&self) -> Result { if !matches!(self.night_state, NightState::Complete) { return Err(GameError::NotEndOfNight); } let mut new_village = self.village.clone(); - let changes = ChangesLookup(&self.changes); + let mut changes = ChangesLookup::new(&self.changes); for change in self.changes.iter() { match change { NightChange::RoleChange(character_id, role_title) => new_village @@ -201,108 +252,17 @@ impl Night { } } NightChange::Kill { target, died_to } => { - if let DiedTo::MapleWolf { - source, - night, - starves_if_fails: true, - } = died_to - && changes.protected(target).is_some() - { - // kill maple first, then act as if they get their kill attempt - new_village - .character_by_id_mut(source) - .unwrap() - .kill(DiedTo::MapleWolfStarved { night: *night }); + if let Some(kill) = kill::resolve_kill( + &mut changes, + target, + died_to, + self.night, + &self.village, + )? { + kill.apply_to_village(&mut new_village)?; } - - if let Some(prot) = changes.protected(target) { - match prot { - Protection::Guardian { - source, - guarding: true, - } => { - let kill_source = match died_to { - DiedTo::MapleWolfStarved { night } => { - new_village - .character_by_id_mut(target) - .unwrap() - .kill(DiedTo::MapleWolfStarved { night: *night }); - continue; - } - DiedTo::Execution { day: _ } => unreachable!(), - DiedTo::MapleWolf { - source, - night: _, - starves_if_fails: _, - } - | DiedTo::Militia { - killer: source, - night: _, - } - | DiedTo::AlphaWolf { - killer: source, - night: _, - } - | DiedTo::Hunter { - killer: source, - night: _, - } => source.clone(), - DiedTo::Wolfpack { night: _ } => { - if let Some(wolf_to_kill) = new_village - .living_wolf_pack_players() - .into_iter() - .find(|w| matches!(w.role(), Role::Werewolf)) - .map(|w| w.character_id().clone()) - .or_else(|| { - new_village - .living_wolf_pack_players() - .into_iter() - .next() - .map(|w| w.character_id().clone()) - }) - { - wolf_to_kill - } else { - // No wolves? Game over? - continue; - } - } - DiedTo::Shapeshift { into: _, night: _ } => target.clone(), - DiedTo::Guardian { - killer: _, - night: _, - } => continue, - }; - new_village.character_by_id_mut(&kill_source).unwrap().kill( - DiedTo::Guardian { - killer: source.clone(), - night: NonZeroU8::new(self.night).unwrap(), - }, - ); - new_village.character_by_id_mut(source).unwrap().kill( - DiedTo::Wolfpack { - night: NonZeroU8::new(self.night).unwrap(), - }, - ); - continue; - } - Protection::Guardian { - source: _, - guarding: false, - } => continue, - Protection::Protector { source: _ } => continue, - } - } - - new_village - .character_by_id_mut(target) - .unwrap() - .kill(DiedTo::Wolfpack { - night: NonZeroU8::new(self.night).unwrap(), - }); } NightChange::Shapeshift { source } => { - // TODO: shapeshift should probably notify immediately after it happens if let Some(target) = changes.wolf_pack_kill_target() && changes.protected(target).is_none() { @@ -317,13 +277,15 @@ impl Night { into: target.clone(), night: NonZeroU8::new(self.night).unwrap(), }); - let target = new_village.find_by_character_id_mut(target).unwrap(); - target - .role_change( - RoleTitle::Werewolf, - DateTime::Night { number: 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 { @@ -347,7 +309,11 @@ impl Night { if let Some(kill_target) = self.changes.iter().find_map(|c| match c { NightChange::Kill { target, - died_to: DiedTo::Wolfpack { night: _ }, + died_to: + DiedTo::Wolfpack { + night: _, + killing_wolf: _, + }, } => Some(target.clone()), _ => None, }) { @@ -361,22 +327,42 @@ impl Night { // there is protection, so the kill doesn't happen -> no shapeshift return Ok(ActionResult::GoBackToSleep); } - if let Some(kill) = self.changes.iter_mut().find(|c| { + // 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, NightChange::Kill { target: _, - died_to: DiedTo::Wolfpack { night: _ } + died_to: DiedTo::Wolfpack { + night: _, + killing_wolf: _ + } } ) }) { - *kill = NightChange::Kill { + self.changes.push(NightChange::Kill { target: source.clone(), died_to: DiedTo::Shapeshift { into: kill_target.clone(), night: NonZeroU8::new(self.night).unwrap(), }, - } + }); } self.changes.push(NightChange::Shapeshift { source: source.clone(), @@ -780,7 +766,10 @@ impl Night { result: ActionResult::GoBackToSleep, change: Some(NightChange::Kill { target: target.clone(), - died_to: DiedTo::Wolfpack { night }, + died_to: DiedTo::Wolfpack { + night, + killing_wolf: self.village.killing_wolf_id(), + }, }), unless: Some(Unless::TargetBlocked(target)), }) @@ -980,6 +969,7 @@ impl Night { NightState::Complete => return Err(GameError::NightOver), } if let Some((prompt, character)) = self.action_queue.pop_front() { + self.used_actions.push((prompt.clone(), character.clone())); self.night_state = NightState::Active { current_prompt: prompt, current_char: character.character_id().clone(), @@ -996,34 +986,3 @@ impl Night { self.changes.as_slice() } } - -struct ChangesLookup<'a>(&'a [NightChange]); - -impl<'a> ChangesLookup<'a> { - pub fn killed(&self, target: &CharacterId) -> Option<&'a DiedTo> { - self.0.iter().find_map(|c| match c { - NightChange::Kill { target: t, died_to } => (t == target).then_some(died_to), - _ => None, - }) - } - - pub fn protected(&self, target: &CharacterId) -> Option<&'a Protection> { - self.0.iter().find_map(|c| match c { - NightChange::Protection { - target: t, - protection, - } => (t == target).then_some(protection), - _ => None, - }) - } - - pub fn wolf_pack_kill_target(&self) -> Option<&'a CharacterId> { - self.0.iter().find_map(|c| match c { - NightChange::Kill { - target, - died_to: DiedTo::Wolfpack { night: _ }, - } => Some(target), - _ => None, - }) - } -} diff --git a/werewolves-proto/src/game/village.rs b/werewolves-proto/src/game/village.rs index 2a85098..b8f410f 100644 --- a/werewolves-proto/src/game/village.rs +++ b/werewolves-proto/src/game/village.rs @@ -153,6 +153,19 @@ impl Village { .collect() } + pub fn killing_wolf_id(&self) -> CharacterId { + let wolves = self.living_wolf_pack_players(); + if let Some(ww) = wolves.iter().find(|w| matches!(w.role(), Role::Werewolf)) { + ww.character_id().clone() + } else if let Some(non_ss_wolf) = wolves.iter().find(|w| { + w.role().wolf() && !matches!(w.role(), Role::Shapeshifter { shifted_into: _ }) + }) { + non_ss_wolf.character_id().clone() + } else { + wolves.into_iter().next().unwrap().character_id().clone() + } + } + pub fn living_players(&self) -> Box<[Target]> { self.characters .iter() diff --git a/werewolves-proto/src/message.rs b/werewolves-proto/src/message.rs index f34229e..557c057 100644 --- a/werewolves-proto/src/message.rs +++ b/werewolves-proto/src/message.rs @@ -5,7 +5,7 @@ pub mod night; use core::{fmt::Display, num::NonZeroU8}; pub use ident::*; -use night::{ActionPrompt, ActionResponse, ActionResult, RoleChange}; +use night::{ActionPrompt, ActionResponse, ActionResult}; use serde::{Deserialize, Serialize}; use crate::{ diff --git a/werewolves-proto/src/message/host.rs b/werewolves-proto/src/message/host.rs index b06a7f0..849362a 100644 --- a/werewolves-proto/src/message/host.rs +++ b/werewolves-proto/src/message/host.rs @@ -28,6 +28,7 @@ pub enum HostMessage { pub enum HostGameMessage { Day(HostDayMessage), Night(HostNightMessage), + PreviousState, GetState, } diff --git a/werewolves-proto/src/message/night.rs b/werewolves-proto/src/message/night.rs index c26b600..009295d 100644 --- a/werewolves-proto/src/message/night.rs +++ b/werewolves-proto/src/message/night.rs @@ -2,93 +2,107 @@ use serde::{Deserialize, Serialize}; use werewolves_macros::ChecksAs; use crate::{ + diedto::DiedTo, + message::PublicIdentity, player::CharacterId, role::{Alignment, PreviousGuardianAction, Role, RoleTitle}, }; use super::Target; -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)] +pub enum ActionType { + Protect = 0, + WolfPackKill = 1, + Direwolf = 2, + Wolf = 3, + Block = 4, + Other = 5, + RoleChange = 6, +} + +impl PartialOrd for ActionType { + fn partial_cmp(&self, other: &Self) -> Option { + (*self as u8).partial_cmp(&(*other as u8)) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ChecksAs)] pub enum ActionPrompt { - WolvesIntro { - wolves: Box<[(Target, RoleTitle)]>, - }, - RoleChange { - new_role: RoleTitle, - }, - Seer { - living_players: Box<[Target]>, - }, - Protector { - targets: Box<[Target]>, - }, - Arcanist { - living_players: Box<[Target]>, - }, - Gravedigger { - dead_players: Box<[Target]>, - }, + #[checks(ActionType::WolfPackKill)] + WolvesIntro { wolves: Box<[(Target, RoleTitle)]> }, + #[checks(ActionType::RoleChange)] + RoleChange { new_role: RoleTitle }, + #[checks(ActionType::Other)] + Seer { living_players: Box<[Target]> }, + #[checks(ActionType::Protect)] + Protector { targets: Box<[Target]> }, + #[checks(ActionType::Other)] + Arcanist { living_players: Box<[Target]> }, + #[checks(ActionType::Other)] + Gravedigger { dead_players: Box<[Target]> }, + #[checks(ActionType::Other)] Hunter { current_target: Option, living_players: Box<[Target]>, }, - Militia { - living_players: Box<[Target]>, - }, + #[checks(ActionType::Other)] + Militia { living_players: Box<[Target]> }, + #[checks(ActionType::Other)] MapleWolf { kill_or_die: bool, living_players: Box<[Target]>, }, + #[checks(ActionType::Protect)] Guardian { previous: Option, living_players: Box<[Target]>, }, - WolfPackKill { - living_villagers: Box<[Target]>, - }, + #[checks(ActionType::Wolf)] + WolfPackKill { living_villagers: Box<[Target]> }, + #[checks(ActionType::Wolf)] Shapeshifter, - AlphaWolf { - living_villagers: Box<[Target]>, - }, - DireWolf { - living_players: Box<[Target]>, - }, + #[checks(ActionType::Wolf)] + AlphaWolf { living_villagers: Box<[Target]> }, + #[checks(ActionType::Direwolf)] + DireWolf { living_players: Box<[Target]> }, } 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)) + // 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()) } } @@ -121,10 +135,3 @@ pub enum ActionResult { GoBackToSleep, WolvesIntroDone, } - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum RoleChange { - Elder(Role), - Apprentice(Role), - Shapeshift(Role), -} diff --git a/werewolves-server/Cargo.toml b/werewolves-server/Cargo.toml index ade8660..f635740 100644 --- a/werewolves-server/Cargo.toml +++ b/werewolves-server/Cargo.toml @@ -25,5 +25,5 @@ ciborium = { version = "0.2", optional = true } colored = { version = "3.0" } [features] -# default = ["cbor"] +default = ["cbor"] cbor = ["dep:ciborium"] diff --git a/werewolves/Cargo.toml b/werewolves/Cargo.toml index 7e995f2..a0bff82 100644 --- a/werewolves/Cargo.toml +++ b/werewolves/Cargo.toml @@ -36,7 +36,7 @@ convert_case = { version = "0.8" } ciborium = { version = "0.2", optional = true } [features] -# default = ["cbor"] -default = ["json"] +default = ["cbor"] +# default = ["json"] cbor = ["dep:ciborium"] json = ["dep:serde_json"] diff --git a/werewolves/index.scss b/werewolves/index.scss index 426d5e9..2041b93 100644 --- a/werewolves/index.scss +++ b/werewolves/index.scss @@ -836,9 +836,18 @@ clients { flex-wrap: wrap; flex-direction: row; font-size: 2rem; - margin-left: 20px; - margin-right: 20px; + justify-content: center; + + &.margin-20 { + margin-left: 20px; + margin-right: 20px; + } + + &.margin-5 { + margin-left: 5px; + margin-right: 5px; + } } .gap { @@ -1002,7 +1011,7 @@ error { background-color: $village_color; border: 3px solid darken($village_color, 20%); - &.on-the-block { + &.marked { // background-color: brighten($village_color, 100%); filter: hue-rotate(90deg); } @@ -1021,3 +1030,25 @@ error { filter: grayscale(100%); } } + +.binary { + .button-container { + background-color: $village_color; + border: 3px solid darken($village_color, 20%); + text-align: center; + padding: 0; + margin: 0; + display: flex; + flex: 1 1 0; + + button { + font-size: 3rem; + font-weight: bold; + align-self: center; + padding: 20px; + width: 100%; + height: 100%; + margin: 0; + } + } +} diff --git a/werewolves/src/components/action/binary.rs b/werewolves/src/components/action/binary.rs index 119dc4a..0c6aa68 100644 --- a/werewolves/src/components/action/binary.rs +++ b/werewolves/src/components/action/binary.rs @@ -4,7 +4,7 @@ use crate::components::Button; #[derive(Debug, Clone, PartialEq, Properties)] pub struct BinaryChoiceProps { - pub on_chosen: Callback, + pub on_chosen: Option>, #[prop_or_default] pub children: Html, } @@ -17,15 +17,19 @@ pub fn BinaryChoice( }: &BinaryChoiceProps, ) -> Html { let on_chosen_yes = on_chosen.clone(); - let yes = move |_| on_chosen_yes.emit(true); + let yes = on_chosen_yes + .map(|on_chosen| Callback::from(move |_| on_chosen.emit(true))) + .unwrap_or_default(); let on_chosen = on_chosen.clone(); - let no = move |_| on_chosen.emit(false); + let no = on_chosen + .map(|on_chosen| Callback::from(move |_| on_chosen.emit(false))) + .unwrap_or_default(); html! { -
+
{children.clone()} -
- - +
+ +
} diff --git a/werewolves/src/components/action/prompt.rs b/werewolves/src/components/action/prompt.rs index 8386cc8..9f65237 100644 --- a/werewolves/src/components/action/prompt.rs +++ b/werewolves/src/components/action/prompt.rs @@ -7,12 +7,13 @@ use werewolves_proto::{ night::{ActionPrompt, ActionResponse}, }, player::CharacterId, + role::PreviousGuardianAction, }; use yew::prelude::*; use crate::components::{ Identity, - action::{BinaryChoice, SingleTarget, TargetSelection, WolvesIntro}, + action::{BinaryChoice, OptionalSingleTarget, SingleTarget, TwoTarget, WolvesIntro}, }; #[derive(Debug, Clone, PartialEq, Properties)] @@ -54,7 +55,6 @@ pub fn Prompt(props: &ActionPromptProps) -> Html { HostNightMessage::ActionResponse(ActionResponse::Seer(target)), ))); }) - .into() }); html! {
@@ -96,7 +96,6 @@ pub fn Prompt(props: &ActionPromptProps) -> Html { HostNightMessage::ActionResponse(ActionResponse::Protector(target)), ))); }) - .into() }); html! {
@@ -108,7 +107,25 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
} } - ActionPrompt::Arcanist { living_players } => todo!(), + ActionPrompt::Arcanist { 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! { +
+ +
+ } + } ActionPrompt::Gravedigger { dead_players } => { let on_complete = props.on_complete.clone(); let on_select = props.big_screen.not().then(|| { @@ -117,7 +134,6 @@ pub fn Prompt(props: &ActionPromptProps) -> Html { HostNightMessage::ActionResponse(ActionResponse::Gravedigger(target)), ))); }) - .into() }); html! {
@@ -132,16 +148,112 @@ pub fn Prompt(props: &ActionPromptProps) -> Html { ActionPrompt::Hunter { current_target, living_players, - } => todo!(), - ActionPrompt::Militia { living_players } => todo!(), + } => { + 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! { + +

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

+
+ } + } + ActionPrompt::Militia { 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! { + + } + } ActionPrompt::MapleWolf { kill_or_die, living_players, - } => todo!(), + } => { + 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! { + + {kill_or_die} + + } + } ActionPrompt::Guardian { previous, living_players, - } => todo!(), + } => { + let last_protect = previous.as_ref().map(|prev| match prev { + PreviousGuardianAction::Protect(target) => { + html! { + <> + {"last night you protected: "} + + + } + } + PreviousGuardianAction::Guard(target) => html! { + <> + {"last night you guarded: "} + + + }, + }); + 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)), + ))); + } + }); + + html! { + + {last_protect} + + } + } ActionPrompt::WolfPackKill { living_villagers } => { let on_complete = props.on_complete.clone(); let on_select = props.big_screen.not().then(|| { @@ -150,25 +262,24 @@ pub fn Prompt(props: &ActionPromptProps) -> Html { HostNightMessage::ActionResponse(ActionResponse::WolfPackKillVote(target)), ))); }) - .into() }); html! { -
- -
+ } } ActionPrompt::Shapeshifter => { let on_complete = props.on_complete.clone(); - let on_select = move |shift| { - on_complete.emit(HostMessage::InGame(HostGameMessage::Night( - HostNightMessage::ActionResponse(ActionResponse::Shapeshifter(shift)), - ))); - }; + let on_select = props.big_screen.not().then_some({ + move |shift| { + on_complete.emit(HostMessage::InGame(HostGameMessage::Night( + HostNightMessage::ActionResponse(ActionResponse::Shapeshifter(shift)), + ))); + } + }); html! {

{"shapeshift?"}

@@ -183,10 +294,9 @@ pub fn Prompt(props: &ActionPromptProps) -> Html { HostNightMessage::ActionResponse(ActionResponse::AlphaWolf(target)), ))); }) - .into() }); html! { - Html { HostNightMessage::ActionResponse(ActionResponse::Direwolf(target)), ))); }) - .into() }); html! { >), - Single(Callback), -} - -impl From> for TargetSelection { - fn from(value: Callback) -> Self { - Self::Single(value) - } -} - -impl From>> for TargetSelection { - fn from(value: Callback>) -> Self { - Self::SingleOptional(value) - } -} - -impl TargetSelection { - pub const fn button_disabled(&self, selected: &[CharacterId]) -> bool { - match self { - TargetSelection::SingleOptional(_) => selected.len() > 1, - TargetSelection::Single(_) => selected.len() != 1, - } - } -} +use crate::components::{Button, Identity}; #[derive(Debug, Clone, PartialEq, Properties)] -pub struct SingleTargetProps { +pub struct TwoTargetProps { pub targets: Box<[Target]>, #[prop_or_default] pub headline: &'static str, #[prop_or_default] - pub target_selection: Option, + pub target_selection: Option>, } -pub struct SingleTarget { - selected: Vec, +#[derive(ChecksAs, Clone)] +enum TwoTargetSelection { + None, + One(CharacterId), + #[checks] + Two(CharacterId, CharacterId), } -impl Component for SingleTarget { +impl TwoTargetSelection { + fn is_selected(&self, id: &CharacterId) -> bool { + match self { + TwoTargetSelection::None => false, + TwoTargetSelection::One(character_id) => id == character_id, + TwoTargetSelection::Two(cid1, cid2) => id == cid1 || id == cid2, + } + } +} + +pub struct TwoTarget(TwoTargetSelection); + +impl Component for TwoTarget { type Message = CharacterId; - type Properties = SingleTargetProps; + type Properties = TwoTargetProps; fn create(_: &Context) -> Self { - Self { - selected: Vec::new(), - } + Self(TwoTargetSelection::None) } fn view(&self, ctx: &Context) -> Html { - let SingleTargetProps { - headline, + let TwoTargetProps { targets, + headline, target_selection, } = ctx.props(); + let target_selection = target_selection.clone(); let scope = ctx.link().clone(); let card_select = Callback::from(move |target| { @@ -73,44 +62,27 @@ impl Component for SingleTarget { html! { } }) .collect::(); - let headline = if headline.trim().is_empty() { - html!() - } else { - html!(

{headline}

) - }; - - let on_click = - target_selection - .as_ref() - .and_then(|target_selection| match target_selection { - TargetSelection::SingleOptional(on_click) => { - if self.selected.len() > 1 { - None - } else { - let selected = self.selected.first().cloned(); - let on_click = on_click.clone(); - Some(Callback::from(move |_| on_click.emit(selected.clone()))) - } - } - TargetSelection::Single(on_click) => { - if self.selected.len() != 1 { - None - } else { - let selected = self.selected[0].clone(); - let on_click = on_click.clone(); - Some(Callback::from(move |_| on_click.emit(selected.clone()))) - } - } - }); + let headline = headline + .trim() + .is_empty() + .not() + .then(|| html!(

{headline}

)); let submit = target_selection.as_ref().map(|target_selection| { - let disabled = target_selection.button_disabled(&self.selected); + let selected = match &self.0 { + TwoTargetSelection::None | TwoTargetSelection::One(_) => None, + TwoTargetSelection::Two(t1, t2) => Some((t1.clone(), t2.clone())), + }; + let target_selection = target_selection.clone(); + let disabled = selected.is_none(); + let on_click = + selected.map(|(t1, t2)| move |_| target_selection.emit((t1.clone(), t2.clone()))); html! {
+
+ } + }); + + html! { +
+ {headline} + {children.clone()} +
+ {targets} +
+ {submit} +
+ } + } + + fn update(&mut self, _: &Context, msg: Self::Message) -> bool { + match &self.0 { + Some(t) => { + if t == &msg { + self.0 = None + } else { + self.0 = Some(msg); + } + } + None => self.0 = Some(msg), + } + true + } +} + +#[derive(Debug, Clone, PartialEq, Properties)] +pub struct SingleTargetProps { + pub targets: Box<[Target]>, + #[prop_or_default] + pub headline: &'static str, + #[prop_or_default] + pub target_selection: Option>, + #[prop_or_default] + pub children: Html, +} + +pub struct SingleTarget { + selected: Option, +} + +impl Component for SingleTarget { + type Message = CharacterId; + + type Properties = SingleTargetProps; + + fn create(_: &Context) -> Self { + Self { selected: None } + } + + fn view(&self, ctx: &Context) -> Html { + let SingleTargetProps { + headline, + targets, + target_selection, + children, + } = ctx.props(); + let target_selection = target_selection.clone(); + let scope = ctx.link().clone(); + let card_select = Callback::from(move |target| { + scope.send_message(target); + }); + let targets = targets + .iter() + .map(|t| { + html! { + + } + }) + .collect::(); + let headline = headline + .trim() + .is_empty() + .not() + .then(|| html!(

{headline}

)); + + let submit = target_selection.as_ref().map(|target_selection| { + let disabled = self.selected.is_none(); + let target_selection = target_selection.clone(); + let on_click = self + .selected + .clone() + .map(|t| move |_| target_selection.emit(t.clone())); + html! { +
+ +
+ } + }); + + html! { +
+ {headline} + {children.clone()} +
+ {targets} +
+ {submit} +
+ } + } + + fn update(&mut self, _: &Context, msg: Self::Message) -> bool { + match &self.selected { + Some(current) => { + if current == &msg { + self.selected = None; + } else { + self.selected = Some(msg); + } + } + None => self.selected = Some(msg), } true } @@ -158,17 +335,24 @@ pub struct TargetCardProps { #[function_component] fn TargetCard(props: &TargetCardProps) -> Html { - let on_select = props.on_select.clone(); - let target = props.target.character_id.clone(); - let on_click = Callback::from(move |_| { - on_select.emit(target.clone()); - }); + let submenu = { + let button_text = if props.selected { "unpick" } else { "pick" }; + let character_id = props.target.character_id.clone(); + let on_select = props.on_select.clone(); + let on_click = Callback::from(move |_| on_select.emit(character_id.clone())); + html! { + + } + }; + let marked = props.selected.then_some("marked"); html! { -
- +
+
+ + {submenu} +
} } diff --git a/werewolves/src/components/host/mod.rs b/werewolves/src/components/host/mod.rs index 3600998..5bd4f81 100644 --- a/werewolves/src/components/host/mod.rs +++ b/werewolves/src/components/host/mod.rs @@ -84,7 +84,7 @@ pub fn DaytimePlayer( ) -> Html { let dead = died_to.is_some().then_some("dead"); let button_text = if *on_the_block { "unmark" } else { "mark" }; - let on_the_block = on_the_block.then_some("on-the-block"); + let on_the_block = on_the_block.then_some("marked"); let submenu = died_to.is_none().then_some(()).and_then(|_| { on_select.as_ref().map(|on_select| { let character_id = character_id.clone(); diff --git a/werewolves/src/pages/host.rs b/werewolves/src/pages/host.rs index d0c09a4..4764658 100644 --- a/werewolves/src/pages/host.rs +++ b/werewolves/src/pages/host.rs @@ -469,10 +469,15 @@ impl Component for Host { HostMessage::Echo(ServerToHostMessage::Error(GameError::NoApprenticeMentor)), self.send.clone(), ); + let on_prev_click = callback::send_message( + HostMessage::InGame(HostGameMessage::PreviousState), + self.send.clone(), + ); html! { -