fix: militia never being spent

This commit is contained in:
emilis 2025-11-07 21:10:17 +00:00
parent 8b894b4c8c
commit fc4da3bcc5
No known key found for this signature in database
5 changed files with 116 additions and 1 deletions

View File

@ -777,6 +777,28 @@ impl Character {
} }
} }
pub const fn militia<'a>(&'a self) -> Result<Militia<'a>> {
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<MilitiaMut<'a>> {
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 { pub const fn initial_shown_role(&self) -> RoleTitle {
self.role.initial_shown_role() self.role.initial_shown_role()
} }
@ -818,6 +840,7 @@ decl_ref_and_mut!(
BlackKnight, BlackKnightMut: Option<DiedTo>; BlackKnight, BlackKnightMut: Option<DiedTo>;
Guardian, GuardianMut: Option<PreviousGuardianAction>; Guardian, GuardianMut: Option<PreviousGuardianAction>;
Direwolf, DirewolfMut: Option<CharacterId>; Direwolf, DirewolfMut: Option<CharacterId>;
Militia, MilitiaMut: Option<CharacterId>;
); );
pub struct BlackKnightKill<'a> { pub struct BlackKnightKill<'a> {

View File

@ -95,4 +95,6 @@ pub enum GameError {
MissingTime(GameTime), MissingTime(GameTime),
#[error("no previous during day")] #[error("no previous during day")]
NoPreviousDuringDay, NoPreviousDuringDay,
#[error("militia already spent")]
MilitiaSpent,
} }

View File

@ -39,7 +39,19 @@ impl KillOutcome {
pub fn apply_to_village(self, village: &mut Village) -> Result<()> { pub fn apply_to_village(self, village: &mut Village) -> Result<()> {
match self { match self {
KillOutcome::Single(character_id, died_to) => { 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(()) Ok(())
} }
KillOutcome::Guarding { KillOutcome::Guarding {

View File

@ -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 <https://www.gnu.org/licenses/>.
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();
}

View File

@ -23,6 +23,7 @@ mod hunter;
mod insomniac; mod insomniac;
mod lone_wolf; mod lone_wolf;
mod mason; mod mason;
mod militia;
mod mortician; mod mortician;
mod pyremaster; mod pyremaster;
mod scapegoat; mod scapegoat;