diff --git a/werewolves-proto/src/character.rs b/werewolves-proto/src/character.rs index 0389fc7..eb92566 100644 --- a/werewolves-proto/src/character.rs +++ b/werewolves-proto/src/character.rs @@ -777,6 +777,28 @@ impl Character { } } + pub const fn militia<'a>(&'a self) -> Result> { + let title = self.role.title(); + match &self.role { + Role::Militia { targeted } => Ok(Militia(targeted)), + _ => Err(GameError::InvalidRole { + expected: RoleTitle::Militia, + got: title, + }), + } + } + + pub const fn militia_mut<'a>(&'a mut self) -> Result> { + let title = self.role.title(); + match &mut self.role { + Role::Militia { targeted } => Ok(MilitiaMut(targeted)), + _ => Err(GameError::InvalidRole { + expected: RoleTitle::Militia, + got: title, + }), + } + } + pub const fn initial_shown_role(&self) -> RoleTitle { self.role.initial_shown_role() } @@ -818,6 +840,7 @@ decl_ref_and_mut!( BlackKnight, BlackKnightMut: Option; Guardian, GuardianMut: Option; Direwolf, DirewolfMut: Option; + Militia, MilitiaMut: Option; ); pub struct BlackKnightKill<'a> { diff --git a/werewolves-proto/src/error.rs b/werewolves-proto/src/error.rs index 50741b3..575f638 100644 --- a/werewolves-proto/src/error.rs +++ b/werewolves-proto/src/error.rs @@ -95,4 +95,6 @@ pub enum GameError { MissingTime(GameTime), #[error("no previous during day")] NoPreviousDuringDay, + #[error("militia already spent")] + MilitiaSpent, } diff --git a/werewolves-proto/src/game/kill.rs b/werewolves-proto/src/game/kill.rs index e6dd15d..d1223f8 100644 --- a/werewolves-proto/src/game/kill.rs +++ b/werewolves-proto/src/game/kill.rs @@ -39,7 +39,19 @@ impl KillOutcome { pub fn apply_to_village(self, village: &mut Village) -> Result<()> { match self { KillOutcome::Single(character_id, died_to) => { - village.character_by_id_mut(character_id)?.kill(died_to); + village + .character_by_id_mut(character_id)? + .kill(died_to.clone()); + if let DiedTo::Militia { killer, .. } = died_to + && let Some(existing) = village + .character_by_id_mut(killer)? + .militia_mut()? + .replace(character_id) + { + log::error!("militia kill after already recording a kill on {existing}"); + return Err(GameError::MilitiaSpent); + } + Ok(()) } KillOutcome::Guarding { diff --git a/werewolves-proto/src/game_test/role/militia.rs b/werewolves-proto/src/game_test/role/militia.rs new file mode 100644 index 0000000..c2bd59a --- /dev/null +++ b/werewolves-proto/src/game_test/role/militia.rs @@ -0,0 +1,77 @@ +// Copyright (C) 2025 Emilis Bliūdžius +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +use core::{num::NonZeroU8, ops::Deref}; +#[allow(unused)] +use pretty_assertions::{assert_eq, assert_ne, assert_str_eq}; + +use crate::{ + diedto::DiedTo, + game::{Game, GameSettings, SetupRole}, + game_test::{ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players}, + message::night::ActionPromptTitle, +}; + +#[test] +fn spent_shot() { + let players = gen_players(1..10); + let militia = players[0].player_id; + let target_wolf = players[1].player_id; + let other_wolf = players[2].player_id; + + let mut settings = GameSettings::empty(); + settings.add_and_assign(SetupRole::Militia, militia); + settings.add_and_assign(SetupRole::Werewolf, target_wolf); + settings.add_and_assign(SetupRole::Werewolf, other_wolf); + + settings.fill_remaining_slots_with_villagers(9); + 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(); + game.mark(game.living_villager().character_id()); + game.r#continue().sleep(); + + game.next().title().militia(); + game.mark(game.character_by_player_id(target_wolf).character_id()); + game.r#continue().sleep(); + + game.next_expect_day(); + + assert_eq!( + game.character_by_player_id(militia) + .militia() + .unwrap() + .deref() + .clone(), + Some(game.character_by_player_id(target_wolf).character_id()) + ); + + assert_eq!( + game.character_by_player_id(target_wolf).died_to().cloned(), + Some(DiedTo::Militia { + killer: game.character_by_player_id(militia).character_id(), + night: NonZeroU8::new(1).unwrap() + }) + ); + + game.execute().title().wolf_pack_kill(); + game.mark(game.living_villager().character_id()); + game.r#continue().sleep(); + + game.next_expect_day(); +} diff --git a/werewolves-proto/src/game_test/role/mod.rs b/werewolves-proto/src/game_test/role/mod.rs index fff05e1..15f65af 100644 --- a/werewolves-proto/src/game_test/role/mod.rs +++ b/werewolves-proto/src/game_test/role/mod.rs @@ -23,6 +23,7 @@ mod hunter; mod insomniac; mod lone_wolf; mod mason; +mod militia; mod mortician; mod pyremaster; mod scapegoat;