From 62d650fb34ee3f5c6bea964aa69fa6c062e748e9 Mon Sep 17 00:00:00 2001 From: emilis Date: Tue, 7 Oct 2025 21:18:31 +0100 Subject: [PATCH] reimplement previous --- werewolves-proto/src/character.rs | 4 + werewolves-proto/src/game/kill.rs | 2 +- werewolves-proto/src/game/mod.rs | 5 + werewolves-proto/src/game/night.rs | 227 ++++++++++++------ werewolves-proto/src/game_test/mod.rs | 1 + werewolves-proto/src/game_test/previous.rs | 121 ++++++++++ .../src/game_test/role/scapegoat.rs | 33 +-- .../src/game_test/role/shapeshifter.rs | 32 ++- werewolves-proto/src/player.rs | 2 +- werewolves-proto/src/role.rs | 2 +- 10 files changed, 327 insertions(+), 102 deletions(-) create mode 100644 werewolves-proto/src/game_test/previous.rs diff --git a/werewolves-proto/src/character.rs b/werewolves-proto/src/character.rs index 35bb896..38d2eee 100644 --- a/werewolves-proto/src/character.rs +++ b/werewolves-proto/src/character.rs @@ -198,6 +198,10 @@ impl Character { Ok(()) } + #[cfg(test)] + pub fn role_changes(&self) -> &[RoleChange] { + &self.role_changes + } pub const fn is_wolf(&self) -> bool { self.role.wolf() diff --git a/werewolves-proto/src/game/kill.rs b/werewolves-proto/src/game/kill.rs index 62771b9..a02e8b0 100644 --- a/werewolves-proto/src/game/kill.rs +++ b/werewolves-proto/src/game/kill.rs @@ -220,7 +220,7 @@ impl<'a> ChangesLookup<'a> { .contains(&idx) .not() .then_some(match c { - NightChange::Shapeshift { source } => Some(source), + NightChange::Shapeshift { source, .. } => Some(source), _ => None, }) .flatten() diff --git a/werewolves-proto/src/game/mod.rs b/werewolves-proto/src/game/mod.rs index 18439dd..82f915c 100644 --- a/werewolves-proto/src/game/mod.rs +++ b/werewolves-proto/src/game/mod.rs @@ -192,6 +192,11 @@ impl Game { &self.state } + #[cfg(test)] + pub fn game_state_mut(&mut self) -> &mut GameState { + &mut self.state + } + pub fn previous_game_states(&self) -> &[GameState] { &self.previous } diff --git a/werewolves-proto/src/game/night.rs b/werewolves-proto/src/game/night.rs index b17eebd..6849c9f 100644 --- a/werewolves-proto/src/game/night.rs +++ b/werewolves-proto/src/game/night.rs @@ -21,7 +21,7 @@ use crate::{ role::{PYREMASTER_VILLAGER_KILLS_TO_DIE, PreviousGuardianAction, RoleBlock, RoleTitle}, }; -#[derive(Debug, Clone, Serialize, Deserialize, Extract)] +#[derive(Debug, Clone, Serialize, PartialEq, Deserialize, Extract)] pub enum NightChange { RoleChange(CharacterId, RoleTitle), HunterTarget { @@ -39,6 +39,7 @@ pub enum NightChange { }, Shapeshift { source: CharacterId, + into: CharacterId, }, Protection { target: CharacterId, @@ -234,6 +235,7 @@ enum NightState { Active { current_prompt: ActionPrompt, current_result: Option, + current_changes: Vec, }, Complete, } @@ -243,8 +245,7 @@ pub struct Night { village: Village, night: u8, action_queue: VecDeque, - used_actions: Vec<(ActionPrompt, ActionResult)>, - changes: Vec, + used_actions: Vec<(ActionPrompt, ActionResult, Vec)>, night_state: NightState, } @@ -291,14 +292,12 @@ impl Night { // let current_prompt = action_queue.pop_front().ok_or(GameError::NoNightActions)?; let night_state = NightState::Active { current_prompt: ActionPrompt::CoverOfDarkness, + current_changes: Vec::new(), current_result: None, }; - let changes = Self::automatic_changes(&village, night); - Ok(Self { night, - changes, village, night_state, action_queue, @@ -337,39 +336,40 @@ impl Night { } pub fn previous_state(&mut self) -> Result<()> { - return Err(GameError::NoPreviousState); - let (prev_act, prev_result) = self.used_actions.pop().ok_or(GameError::NoPreviousState)?; - log::info!("loading previous prompt: {prev_act:?}"); - match &self.night_state { + let (current_prompt, current_result, current_changes) = match &mut self.night_state { NightState::Active { current_prompt, - current_result: Some(current_result), - } => { - log::info!("removing current result: {current_result:?}"); - self.night_state = NightState::Active { - current_prompt: current_prompt.clone(), - current_result: None, - }; - } - NightState::Active { - current_prompt, - current_result: None, - } => { - log::info!("pushing current prompt to front of action queue: {current_prompt:?}"); - self.action_queue.push_front(current_prompt.clone()); - self.night_state = NightState::Active { - current_prompt: prev_act, - current_result: None, - } - } - NightState::Complete => { - self.night_state = NightState::Active { - current_prompt: prev_act, - current_result: None, - }; + current_result, + current_changes, + } => (current_prompt, current_result, current_changes), + NightState::Complete => return Err(GameError::NightOver), + }; + if let Some((prompt, _, changes)) = self.used_actions.pop() { + // Remove the shapeshifter role change from the queue + if let ActionPrompt::Shapeshifter { + character_id: ss_char, + } = &prompt + && let Some(change) = changes.first() + && let NightChange::Shapeshift { source, into } = change + && ss_char.character_id == *source + && let Some(next) = self.action_queue.pop_front() + && let ActionPrompt::RoleChange { + character_id: role_change_char, + .. + } = &next + && role_change_char.character_id != *into + { + // put it back in + self.action_queue.push_front(next); } + // panic!("{:#?}", self.action_queue.pop_front()); + *current_prompt = prompt; + *current_result = None; + *current_changes = Vec::new(); + Ok(()) + } else { + Err(GameError::NoPreviousState) } - Ok(()) } #[cfg(test)] @@ -382,8 +382,10 @@ impl Night { return Err(GameError::NotEndOfNight); } let mut new_village = self.village.clone(); - let mut changes = ChangesLookup::new(&self.changes); - for change in self.changes.iter() { + let mut all_changes = Self::automatic_changes(&self.village, self.night); + all_changes.append(&mut self.changes_from_actions().into_vec()); + let mut changes = ChangesLookup::new(&all_changes); + for change in all_changes.iter() { match change { NightChange::ElderReveal { elder } => { new_village.character_by_id_mut(*elder)?.elder_reveal() @@ -418,10 +420,14 @@ impl Night { kill.apply_to_village(&mut new_village)?; } } - NightChange::Shapeshift { source } => { + NightChange::Shapeshift { source, into } => { if let Some(target) = changes.wolf_pack_kill_target() && changes.protected(target).is_none() { + if *target != *into { + log::error!("shapeshift into({into}) != target({target})"); + continue; + } let ss = new_village.character_by_id_mut(*source).unwrap(); ss.shapeshifter_mut().unwrap().replace(*target); ss.kill(DiedTo::Shapeshift { @@ -538,29 +544,33 @@ impl Night { } fn apply_shapeshift(&mut self, source: &CharacterId) -> Result<()> { - if let Some(kill_target) = self.changes.iter().find_map(|c| match c { - NightChange::Kill { - target, - died_to: - DiedTo::Wolfpack { - night: _, - killing_wolf: _, - }, - } => Some(*target), - _ => None, - }) { - if self.changes.iter().any(|c| match c { + if let Some(kill_target) = self + .changes_from_actions() + .into_iter() + .find_map(|c| match c { + NightChange::Kill { + target, + died_to: + DiedTo::Wolfpack { + night: _, + killing_wolf: _, + }, + } => Some(target), + _ => None, + }) + { + if self.changes_from_actions().into_iter().any(|c| match c { NightChange::Protection { target, protection: _, - } => target == &kill_target, + } => target == kill_target, _ => false, }) { // there is protection, so the kill doesn't happen -> no shapeshift return Ok(()); } - if self.changes.iter_mut().any(|c| { + if self.changes_from_actions().into_iter().any(|c| { matches!( c, NightChange::Kill { @@ -572,16 +582,28 @@ impl Night { } ) }) { - self.changes.push(NightChange::Kill { - target: *source, - died_to: DiedTo::Shapeshift { - into: kill_target, - night: NonZeroU8::new(self.night).unwrap(), - }, - }); + match &mut self.night_state { + NightState::Active { + current_changes, .. + } => current_changes.push(NightChange::Kill { + target: *source, + died_to: DiedTo::Shapeshift { + into: kill_target, + night: NonZeroU8::new(self.night).unwrap(), + }, + }), + _ => return Err(GameError::InvalidMessageForGameState), + } + } + match &mut self.night_state { + NightState::Active { + current_changes, .. + } => current_changes.push(NightChange::Shapeshift { + source: *source, + into: kill_target, + }), + _ => return Err(GameError::InvalidMessageForGameState), } - self.changes - .push(NightChange::Shapeshift { source: *source }); self.action_queue.push_front(ActionPrompt::RoleChange { new_role: RoleTitle::Werewolf, character_id: self.village.character_by_id(kill_target)?.identity(), @@ -617,10 +639,11 @@ impl Night { NightState::Active { current_prompt: _, current_result, + .. } => current_result.replace(result.clone()), NightState::Complete => return Err(GameError::NightOver), }; - if let NightChange::Shapeshift { source } = &change { + if let NightChange::Shapeshift { source, into } = &change { // needs to be resolved _now_ so that the target can be woken // for the role change with the wolves self.apply_shapeshift(source)?; @@ -639,7 +662,12 @@ impl Night { { result = self.apply_mason_recruit(*mason_leader, *recruiting)?; } - self.changes.push(change); + match &mut self.night_state { + NightState::Active { + current_changes, .. + } => current_changes.push(change), + NightState::Complete => return Err(GameError::InvalidMessageForGameState), + } Ok(ServerAction::Result(result)) } BlockResolvedOutcome::ActionComplete(result, None) => { @@ -647,6 +675,7 @@ impl Night { NightState::Active { current_prompt: _, current_result, + .. } => { current_result.replace(result.clone()); } @@ -693,13 +722,13 @@ impl Night { ( ResponseOutcome::ActionComplete(ActionComplete { result: ActionResult::GoBackToSleep, - change: Some(NightChange::Shapeshift { source }), + change: Some(NightChange::Shapeshift { source, into }), }), true, _, ) => Ok(ResponseOutcome::ActionComplete(ActionComplete { result: ActionResult::Continue, - change: Some(NightChange::Shapeshift { source }), + change: Some(NightChange::Shapeshift { source, into }), })), ( ResponseOutcome::ActionComplete(ActionComplete { @@ -725,12 +754,12 @@ impl Night { ResponseOutcome::ActionComplete(ActionComplete { result, change }) => { match self.current_prompt().ok_or(GameError::NightOver)?.unless() { Some(Unless::TargetBlocked(unless_blocked)) => { - if self.changes.iter().any(|c| match c { + if self.changes_from_actions().into_iter().any(|c| match c { NightChange::RoleBlock { source: _, target, block_type: _, - } => target == &unless_blocked, + } => target == unless_blocked, _ => false, }) { Ok(BlockResolvedOutcome::ActionComplete( @@ -742,12 +771,12 @@ impl Night { } } Some(Unless::TargetsBlocked(unless_blocked1, unless_blocked2)) => { - if self.changes.iter().any(|c| match c { + if self.changes_from_actions().into_iter().any(|c| match c { NightChange::RoleBlock { source: _, target, block_type: _, - } => target == &unless_blocked1 || target == &unless_blocked2, + } => target == unless_blocked1 || target == unless_blocked2, _ => false, }) { Ok(BlockResolvedOutcome::ActionComplete( @@ -769,10 +798,12 @@ impl Night { NightState::Active { current_prompt: _, current_result: Some(_), + .. } => return Err(GameError::NightNeedsNext), NightState::Active { current_prompt, current_result: None, + .. } => current_prompt, NightState::Complete => return Err(GameError::NightOver), }; @@ -791,6 +822,17 @@ impl Night { result: ActionResult::GoBackToSleep, change: Some(NightChange::Shapeshift { source: source.character_id, + into: self + .changes_from_actions() + .into_iter() + .find_map(|c| match c { + NightChange::Kill { + target, + died_to: DiedTo::Wolfpack { .. }, + } => Some(target), + _ => None, + }) + .ok_or(GameError::InvalidTarget)?, }), })), _ => Err(GameError::InvalidMessageForGameState), @@ -1019,9 +1061,24 @@ impl Night { ActionPrompt::Shapeshifter { character_id } => { Ok(ResponseOutcome::ActionComplete(ActionComplete { result: ActionResult::GoBackToSleep, - change: Some(NightChange::Shapeshift { - source: character_id.character_id, - }), + change: match &resp { + ActionResponse::Continue => None, + ActionResponse::Shapeshift => Some(NightChange::Shapeshift { + source: character_id.character_id, + into: self + .changes_from_actions() + .into_iter() + .find_map(|c| match c { + NightChange::Kill { + target, + died_to: DiedTo::Wolfpack { .. }, + } => Some(target), + _ => None, + }) + .ok_or(GameError::InvalidTarget)?, + }), + _ => return Err(GameError::InvalidMessageForGameState), + }, })) } ActionPrompt::AlphaWolf { @@ -1092,7 +1149,7 @@ impl Night { marked: Some(marked), .. } => { - if let Some(result) = self.used_actions.iter().find_map(|(prompt, result)| { + if let Some(result) = self.used_actions.iter().find_map(|(prompt, result, _)| { prompt.matches_beholding(*marked).then_some(result) }) { Ok(ActionComplete { @@ -1222,6 +1279,7 @@ impl Night { NightState::Active { current_prompt: _, current_result, + .. } => current_result.as_ref(), NightState::Complete => None, } @@ -1232,6 +1290,7 @@ impl Night { NightState::Active { current_prompt, current_result: _, + .. } => Some(current_prompt), NightState::Complete => None, } @@ -1242,6 +1301,7 @@ impl Night { NightState::Active { current_prompt, current_result: _, + .. } => match current_prompt { ActionPrompt::Insomniac { character_id, .. } | ActionPrompt::LoneWolfKill { character_id, .. } @@ -1290,13 +1350,18 @@ impl Night { NightState::Active { current_prompt, current_result: Some(result), + current_changes, } => { - self.used_actions - .push((current_prompt.clone(), result.clone())); + self.used_actions.push(( + current_prompt.clone(), + result.clone(), + current_changes.clone(), + )); } NightState::Active { current_prompt: _, current_result: None, + .. } => return Err(GameError::AwaitingResponse), NightState::Complete => return Err(GameError::NightOver), } @@ -1311,6 +1376,7 @@ impl Night { self.night_state = NightState::Active { current_prompt: prompt, current_result: None, + current_changes: Vec::new(), }; } else { self.night_state = NightState::Complete; @@ -1319,15 +1385,20 @@ impl Night { Ok(()) } - pub const fn changes(&self) -> &[NightChange] { - self.changes.as_slice() + fn changes_from_actions(&self) -> Box<[NightChange]> { + self.used_actions + .iter() + .map(|(_, _, act)| act.into_iter()) + .flatten() + .cloned() + .collect() } pub fn get_visits_for(&self, visit_char: CharacterId) -> Visits { Visits::new( self.used_actions .iter() - .filter_map(|(prompt, _)| match prompt { + .filter_map(|(prompt, _, _)| match prompt { ActionPrompt::Arcanist { character_id, marked: (Some(marked1), Some(marked2)), diff --git a/werewolves-proto/src/game_test/mod.rs b/werewolves-proto/src/game_test/mod.rs index 89d127a..861d8ea 100644 --- a/werewolves-proto/src/game_test/mod.rs +++ b/werewolves-proto/src/game_test/mod.rs @@ -1,4 +1,5 @@ mod night_order; +mod previous; mod role; use crate::{ diff --git a/werewolves-proto/src/game_test/previous.rs b/werewolves-proto/src/game_test/previous.rs new file mode 100644 index 0000000..beba9ce --- /dev/null +++ b/werewolves-proto/src/game_test/previous.rs @@ -0,0 +1,121 @@ +#[allow(unused)] +use pretty_assertions::{assert_eq, assert_ne, assert_str_eq}; + +use crate::{ + game::{Game, GameSettings, GameState, SetupRole}, + game_test::{ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players}, + message::{ + host::ServerToHostMessage, + night::{ActionPrompt, ActionPromptTitle, ActionResponse}, + }, + role::RoleTitle, +}; + +#[test] +fn previous_shapeshifter_undone_redone() { + let players = gen_players(1..21); + let shapeshifter_player_id = players[0].player_id; + let wolf_player_id = players[1].player_id; + let mut settings = GameSettings::empty(); + settings.add_and_assign(SetupRole::Shapeshifter, shapeshifter_player_id); + settings.add_and_assign(SetupRole::Werewolf, wolf_player_id); + settings.fill_remaining_slots_with_villagers(20); + let mut game = Game::new(&players, settings).unwrap(); + game.r#continue().r#continue(); + assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro); + game.r#continue().sleep(); + + game.next_expect_day(); + + game.execute().title().wolf_pack_kill(); + let ss_target = game.living_villager(); + game.mark(ss_target.character_id()); + game.r#continue().r#continue(); + + game.next().title().shapeshifter(); + game.response(ActionResponse::Shapeshift).r#continue(); + + assert_eq!( + game.next(), + ActionPrompt::RoleChange { + character_id: ss_target.identity(), + new_role: RoleTitle::Werewolf + } + ); + match game.game_state_mut() { + GameState::Night { night } => night.previous_state().unwrap(), + GameState::Day { .. } => unreachable!(), + } + assert_eq!( + game.get_state(), + ServerToHostMessage::ActionPrompt(ActionPrompt::Shapeshifter { + character_id: game + .character_by_player_id(shapeshifter_player_id) + .identity() + }) + ); + game.response(ActionResponse::Shapeshift).r#continue(); + assert_eq!( + game.next(), + ActionPrompt::RoleChange { + character_id: ss_target.identity(), + new_role: RoleTitle::Werewolf + } + ); + game.r#continue().sleep(); + + game.next_expect_day(); +} + +#[test] +fn previous_shapeshifter_undone_and_changed_to_no() { + let players = gen_players(1..21); + let shapeshifter_player_id = players[0].player_id; + let wolf_player_id = players[1].player_id; + let mut settings = GameSettings::empty(); + settings.add_and_assign(SetupRole::Shapeshifter, shapeshifter_player_id); + settings.add_and_assign(SetupRole::Werewolf, wolf_player_id); + settings.fill_remaining_slots_with_villagers(20); + let mut game = Game::new(&players, settings).unwrap(); + game.r#continue().r#continue(); + assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro); + game.r#continue().sleep(); + + game.next_expect_day(); + + game.execute().title().wolf_pack_kill(); + let ss_target = game.living_villager(); + game.mark(ss_target.character_id()); + game.r#continue().r#continue(); + + game.next().title().shapeshifter(); + game.response(ActionResponse::Shapeshift).r#continue(); + + assert_eq!( + game.next(), + ActionPrompt::RoleChange { + character_id: ss_target.identity(), + new_role: RoleTitle::Werewolf + } + ); + match game.game_state_mut() { + GameState::Night { night } => night.previous_state().unwrap(), + GameState::Day { .. } => unreachable!(), + } + assert_eq!( + game.get_state(), + ServerToHostMessage::ActionPrompt(ActionPrompt::Shapeshifter { + character_id: game + .character_by_player_id(shapeshifter_player_id) + .identity() + }) + ); + game.r#continue().sleep(); + + game.next_expect_day(); + assert_eq!( + game.character_by_player_id(ss_target.player_id()) + .role_changes(), + &[] + ); +} diff --git a/werewolves-proto/src/game_test/role/scapegoat.rs b/werewolves-proto/src/game_test/role/scapegoat.rs index 5146230..a8157c4 100644 --- a/werewolves-proto/src/game_test/role/scapegoat.rs +++ b/werewolves-proto/src/game_test/role/scapegoat.rs @@ -5,7 +5,8 @@ use crate::{ game::{Game, GameSettings, OrRandom, SetupRole, night::NightChange}, game_test::{ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players}, message::night::{ActionPrompt, ActionPromptTitle}, - role::{Alignment, RoleTitle}, + player::RoleChange, + role::{Alignment, Role, RoleTitle}, }; #[allow(unused)] use pretty_assertions::{assert_eq, assert_ne, assert_str_eq}; @@ -128,26 +129,18 @@ fn redeemed_scapegoat_role_changes() { ); game.r#continue().sleep(); - match game.game_state() { - crate::game::GameState::Night { night } => night - .changes() - .iter() - .find(|c| match c { - NightChange::RoleChange(char, role) => { - char == &scapegoat.character_id() && role == &RoleTitle::Seer - } - _ => false, - }) - .expect("no role change"), - _ => unreachable!(), - }; - game.next_expect_day(); - let day_scapegoat = game - .village() - .character_by_id(scapegoat.character_id()) - .unwrap(); - assert_eq!(day_scapegoat.role().title(), RoleTitle::Seer); + + let scapegoat = game.character_by_player_id(scapegoat_player_id); + assert_eq!(*scapegoat.role(), Role::Seer); + assert_eq!( + scapegoat.role_changes(), + &[RoleChange { + role: Role::Scapegoat { redeemed: true }, + new_role: RoleTitle::Seer, + changed_on_night: 2, + }] + ); } #[test] diff --git a/werewolves-proto/src/game_test/role/shapeshifter.rs b/werewolves-proto/src/game_test/role/shapeshifter.rs index 713a4f4..cfab08f 100644 --- a/werewolves-proto/src/game_test/role/shapeshifter.rs +++ b/werewolves-proto/src/game_test/role/shapeshifter.rs @@ -4,7 +4,10 @@ use pretty_assertions::{assert_eq, assert_ne, assert_str_eq}; use crate::{ character::CharacterId, game::{Game, GameSettings, SetupRole}, - game_test::{ActionResultExt, GameExt, ServerToHostMessageExt, gen_players, init_log}, + game_test::{ + ActionPromptTitleExt, ActionResultExt, GameExt, ServerToHostMessageExt, SettingsExt, + gen_players, init_log, + }, message::{ host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage}, night::{ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult}, @@ -204,3 +207,30 @@ fn only_1_shapeshift_prompt_if_first_shifts() { game.next_expect_day(); } + +#[test] +fn i_would_simply_refuse() { + let players = gen_players(1..21); + let shapeshifter_player_id = players[0].player_id; + let wolf_player_id = players[1].player_id; + let mut settings = GameSettings::empty(); + settings.add_and_assign(SetupRole::Shapeshifter, shapeshifter_player_id); + settings.add_and_assign(SetupRole::Werewolf, wolf_player_id); + settings.fill_remaining_slots_with_villagers(20); + let mut game = Game::new(&players, settings).unwrap(); + game.r#continue().r#continue(); + assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro); + game.r#continue().sleep(); + + game.next_expect_day(); + + game.execute().title().wolf_pack_kill(); + let ss_target = game.living_villager(); + game.mark(ss_target.character_id()); + game.r#continue().r#continue(); + + game.next().title().shapeshifter(); + game.r#continue().sleep(); + + game.next_expect_day(); +} diff --git a/werewolves-proto/src/player.rs b/werewolves-proto/src/player.rs index 0cb8d40..b734c2a 100644 --- a/werewolves-proto/src/player.rs +++ b/werewolves-proto/src/player.rs @@ -53,7 +53,7 @@ pub enum KillOutcome { Failed, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct RoleChange { pub role: Role, pub new_role: RoleTitle, diff --git a/werewolves-proto/src/role.rs b/werewolves-proto/src/role.rs index ff76893..bc68efe 100644 --- a/werewolves-proto/src/role.rs +++ b/werewolves-proto/src/role.rs @@ -273,7 +273,7 @@ pub enum ArcanistCheck { pub const MAPLE_WOLF_ABSTAIN_LIMIT: NonZeroU8 = NonZeroU8::new(3).unwrap(); pub const PYREMASTER_VILLAGER_KILLS_TO_DIE: NonZeroU8 = NonZeroU8::new(2).unwrap(); -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum RoleBlock { Direwolf, }