2025-11-05 20:24:51 +00:00
|
|
|
// 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/>.
|
2025-10-12 23:48:52 +01:00
|
|
|
mod apply;
|
2025-06-23 09:48:28 +01:00
|
|
|
use core::num::NonZeroU8;
|
|
|
|
|
|
2025-10-05 10:52:37 +01:00
|
|
|
use rand::Rng;
|
2025-06-23 09:48:28 +01:00
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
|
|
|
|
|
use super::Result;
|
|
|
|
|
use crate::{
|
2025-10-06 20:45:15 +01:00
|
|
|
character::{Character, CharacterId},
|
2025-10-05 10:52:37 +01:00
|
|
|
diedto::DiedTo,
|
2025-06-23 09:48:28 +01:00
|
|
|
error::GameError,
|
2025-10-12 23:48:52 +01:00
|
|
|
game::{GameOver, GameSettings, GameTime},
|
2025-10-06 21:59:44 +01:00
|
|
|
message::{CharacterIdentity, Identification, night::ActionPrompt},
|
2025-10-06 20:45:15 +01:00
|
|
|
player::PlayerId,
|
2025-06-23 09:48:28 +01:00
|
|
|
role::{Role, RoleTitle},
|
|
|
|
|
};
|
|
|
|
|
|
2025-10-12 23:48:52 +01:00
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
2025-06-23 09:48:28 +01:00
|
|
|
pub struct Village {
|
2025-10-02 17:52:12 +01:00
|
|
|
characters: Box<[Character]>,
|
2025-10-12 23:48:52 +01:00
|
|
|
time: GameTime,
|
2025-10-09 22:27:21 +01:00
|
|
|
settings: GameSettings,
|
2025-06-23 09:48:28 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Village {
|
|
|
|
|
pub fn new(players: &[Identification], settings: GameSettings) -> Result<Self> {
|
|
|
|
|
if settings.min_players_needed() > players.len() {
|
|
|
|
|
return Err(GameError::TooManyRoles {
|
|
|
|
|
players: players.len() as u8,
|
|
|
|
|
roles: settings.min_players_needed() as u8,
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-10-04 17:50:29 +01:00
|
|
|
let mut characters = settings.assign(players)?;
|
|
|
|
|
assert_eq!(characters.len(), players.len());
|
|
|
|
|
characters.sort_by_key(|l| l.number());
|
2025-06-23 09:48:28 +01:00
|
|
|
|
|
|
|
|
Ok(Self {
|
2025-10-09 22:27:21 +01:00
|
|
|
settings,
|
2025-10-04 17:50:29 +01:00
|
|
|
characters,
|
2025-10-12 23:48:52 +01:00
|
|
|
time: GameTime::Night { number: 0 },
|
2025-06-23 09:48:28 +01:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-09 22:27:21 +01:00
|
|
|
pub fn settings(&self) -> GameSettings {
|
|
|
|
|
self.settings.clone()
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-03 22:47:38 +01:00
|
|
|
pub fn killing_wolf(&self) -> Option<&Character> {
|
2025-10-12 23:48:52 +01:00
|
|
|
let mut wolves = self
|
|
|
|
|
.characters
|
|
|
|
|
.iter()
|
2025-10-13 23:16:20 +01:00
|
|
|
.filter(|c| c.alive() && c.is_wolf())
|
2025-10-12 23:48:52 +01:00
|
|
|
.collect::<Box<[_]>>();
|
|
|
|
|
wolves.sort_by_key(|w| w.killing_wolf_order());
|
|
|
|
|
wolves.first().copied()
|
2025-10-03 22:47:38 +01:00
|
|
|
}
|
2025-10-13 23:16:20 +01:00
|
|
|
|
|
|
|
|
pub fn wolf_revert_prompt(&self) -> Option<ActionPrompt> {
|
|
|
|
|
self.killing_wolf()
|
|
|
|
|
.filter(|killing_wolf| RoleTitle::Werewolf != killing_wolf.role_title())
|
|
|
|
|
.map(|killing_wolf| ActionPrompt::RoleChange {
|
|
|
|
|
character_id: killing_wolf.identity(),
|
|
|
|
|
new_role: RoleTitle::Werewolf,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-06 21:59:44 +01:00
|
|
|
pub fn wolf_pack_kill(&self) -> Option<ActionPrompt> {
|
2025-10-12 23:48:52 +01:00
|
|
|
let night = match self.time {
|
|
|
|
|
GameTime::Day { .. } => return None,
|
|
|
|
|
GameTime::Night { number } => number,
|
2025-10-06 21:59:44 +01:00
|
|
|
};
|
|
|
|
|
let no_kill_due_to_disease = self
|
|
|
|
|
.characters
|
|
|
|
|
.iter()
|
|
|
|
|
.filter(|d| matches!(d.role_title(), RoleTitle::Diseased))
|
|
|
|
|
.any(|d| match d.died_to() {
|
|
|
|
|
Some(DiedTo::Wolfpack {
|
|
|
|
|
night: diseased_death_night,
|
|
|
|
|
..
|
|
|
|
|
}) => (diseased_death_night.get() + 1) == night,
|
|
|
|
|
_ => false,
|
|
|
|
|
});
|
|
|
|
|
(night > 0 && !no_kill_due_to_disease).then_some(ActionPrompt::WolfPackKill {
|
|
|
|
|
marked: None,
|
|
|
|
|
living_villagers: self.living_villagers(),
|
|
|
|
|
})
|
|
|
|
|
}
|
2025-10-03 22:47:38 +01:00
|
|
|
|
2025-10-12 23:48:52 +01:00
|
|
|
pub const fn time(&self) -> GameTime {
|
|
|
|
|
self.time
|
2025-06-23 09:48:28 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn find_by_character_id_mut(
|
|
|
|
|
&mut self,
|
2025-10-05 10:52:37 +01:00
|
|
|
character_id: CharacterId,
|
2025-06-23 09:48:28 +01:00
|
|
|
) -> Option<&mut Character> {
|
|
|
|
|
self.characters
|
|
|
|
|
.iter_mut()
|
|
|
|
|
.find(|c| c.character_id() == character_id)
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-26 21:15:52 +01:00
|
|
|
fn living_wolves_count(&self) -> usize {
|
|
|
|
|
self.characters
|
|
|
|
|
.iter()
|
|
|
|
|
.filter(|c| c.is_wolf() && c.alive())
|
|
|
|
|
.count()
|
2025-06-23 09:48:28 +01:00
|
|
|
}
|
|
|
|
|
|
2025-09-26 21:15:52 +01:00
|
|
|
fn living_villager_count(&self) -> usize {
|
|
|
|
|
self.characters
|
|
|
|
|
.iter()
|
|
|
|
|
.filter(|c| c.is_village() && c.alive())
|
|
|
|
|
.count()
|
2025-06-23 09:48:28 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn is_game_over(&self) -> Option<GameOver> {
|
2025-09-26 21:15:52 +01:00
|
|
|
let wolves = self.living_wolves_count();
|
|
|
|
|
let villagers = self.living_villager_count();
|
2025-10-06 22:36:58 +01:00
|
|
|
let weightlifters = self
|
|
|
|
|
.living_characters_by_role(RoleTitle::Weightlifter)
|
|
|
|
|
.len();
|
|
|
|
|
if weightlifters > 0 && wolves == 1 && villagers == 1 {
|
|
|
|
|
return Some(GameOver::VillageWins);
|
|
|
|
|
}
|
2025-06-23 09:48:28 +01:00
|
|
|
|
|
|
|
|
if wolves == 0 {
|
|
|
|
|
return Some(GameOver::VillageWins);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if wolves >= villagers {
|
|
|
|
|
return Some(GameOver::WolvesWin);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn execute(&mut self, characters: &[CharacterId]) -> Result<Option<GameOver>> {
|
2025-10-12 23:48:52 +01:00
|
|
|
let day = match self.time {
|
|
|
|
|
GameTime::Day { number } => number,
|
|
|
|
|
GameTime::Night { number: _ } => return Err(GameError::NoExecutionsAtNight),
|
2025-06-23 09:48:28 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let targets = self
|
|
|
|
|
.characters
|
|
|
|
|
.iter_mut()
|
2025-10-05 10:52:37 +01:00
|
|
|
.filter(|c| characters.contains(&c.character_id()))
|
2025-06-23 09:48:28 +01:00
|
|
|
.collect::<Box<[_]>>();
|
|
|
|
|
for t in targets {
|
|
|
|
|
t.execute(day)?;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-12 23:48:52 +01:00
|
|
|
if let Some(game_over) = self.is_game_over() {
|
|
|
|
|
return Ok(Some(game_over));
|
|
|
|
|
}
|
|
|
|
|
self.time = self.time.next();
|
|
|
|
|
Ok(None)
|
2025-06-23 09:48:28 +01:00
|
|
|
}
|
|
|
|
|
|
2025-10-12 23:48:52 +01:00
|
|
|
pub fn to_day(&mut self) -> Result<GameTime> {
|
|
|
|
|
if self.time.is_day() {
|
2025-06-23 09:48:28 +01:00
|
|
|
return Err(GameError::AlreadyDaytime);
|
|
|
|
|
}
|
2025-10-12 23:48:52 +01:00
|
|
|
self.time = self.time.next();
|
|
|
|
|
Ok(self.time)
|
2025-06-23 09:48:28 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn living_wolf_pack_players(&self) -> Box<[Character]> {
|
|
|
|
|
self.characters
|
|
|
|
|
.iter()
|
2025-10-06 20:45:15 +01:00
|
|
|
.filter(|c| c.is_wolf() && c.alive())
|
2025-06-23 09:48:28 +01:00
|
|
|
.cloned()
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-02 17:52:12 +01:00
|
|
|
pub fn living_players(&self) -> Box<[CharacterIdentity]> {
|
2025-06-23 09:48:28 +01:00
|
|
|
self.characters
|
|
|
|
|
.iter()
|
|
|
|
|
.filter(|c| c.alive())
|
2025-10-02 17:52:12 +01:00
|
|
|
.map(Character::identity)
|
2025-06-23 09:48:28 +01:00
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-06 20:45:15 +01:00
|
|
|
pub fn target_by_id(&self, character_id: CharacterId) -> Result<CharacterIdentity> {
|
2025-10-02 17:52:12 +01:00
|
|
|
self.character_by_id(character_id).map(Character::identity)
|
2025-06-23 09:48:28 +01:00
|
|
|
}
|
|
|
|
|
|
2025-10-02 17:52:12 +01:00
|
|
|
pub fn living_villagers(&self) -> Box<[CharacterIdentity]> {
|
2025-06-23 09:48:28 +01:00
|
|
|
self.characters
|
|
|
|
|
.iter()
|
|
|
|
|
.filter(|c| c.alive() && c.is_village())
|
2025-10-02 17:52:12 +01:00
|
|
|
.map(Character::identity)
|
2025-06-23 09:48:28 +01:00
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-05 10:52:37 +01:00
|
|
|
pub fn living_players_excluding(&self, exclude: CharacterId) -> Box<[CharacterIdentity]> {
|
2025-06-23 09:48:28 +01:00
|
|
|
self.characters
|
|
|
|
|
.iter()
|
|
|
|
|
.filter(|c| c.alive() && c.character_id() != exclude)
|
2025-10-02 17:52:12 +01:00
|
|
|
.map(Character::identity)
|
2025-06-23 09:48:28 +01:00
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-02 17:52:12 +01:00
|
|
|
pub fn dead_targets(&self) -> Box<[CharacterIdentity]> {
|
2025-06-23 09:48:28 +01:00
|
|
|
self.characters
|
|
|
|
|
.iter()
|
|
|
|
|
.filter(|c| !c.alive())
|
2025-10-02 17:52:12 +01:00
|
|
|
.map(Character::identity)
|
2025-06-23 09:48:28 +01:00
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn dead_characters(&self) -> Box<[&Character]> {
|
|
|
|
|
self.characters.iter().filter(|c| !c.alive()).collect()
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-05 10:52:37 +01:00
|
|
|
pub fn executed_known_elder(&self) -> bool {
|
|
|
|
|
self.characters.iter().any(|d| {
|
2025-10-06 20:45:15 +01:00
|
|
|
d.known_elder()
|
|
|
|
|
&& d.died_to()
|
|
|
|
|
.map(|d| matches!(d, DiedTo::Execution { .. }))
|
|
|
|
|
.unwrap_or_default()
|
2025-10-05 10:52:37 +01:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-06 20:45:15 +01:00
|
|
|
pub fn executions_on_day(&self, on_day: NonZeroU8) -> Box<[Character]> {
|
|
|
|
|
self.characters
|
|
|
|
|
.iter()
|
|
|
|
|
.filter(|c| match c.died_to() {
|
|
|
|
|
Some(DiedTo::Execution { day }) => day.get() == on_day.get(),
|
|
|
|
|
_ => false,
|
|
|
|
|
})
|
|
|
|
|
.cloned()
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-23 09:48:28 +01:00
|
|
|
pub fn living_characters_by_role(&self, role: RoleTitle) -> Box<[Character]> {
|
|
|
|
|
self.characters
|
|
|
|
|
.iter()
|
2025-10-06 20:45:15 +01:00
|
|
|
.filter(|c| c.role_title() == role)
|
2025-06-23 09:48:28 +01:00
|
|
|
.cloned()
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-06 21:59:44 +01:00
|
|
|
pub fn living_characters_by_role_mut(&mut self, role: RoleTitle) -> Box<[&mut Character]> {
|
|
|
|
|
self.characters
|
|
|
|
|
.iter_mut()
|
|
|
|
|
.filter(|c| c.role_title() == role)
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-23 09:48:28 +01:00
|
|
|
pub fn characters(&self) -> Box<[Character]> {
|
|
|
|
|
self.characters.iter().cloned().collect()
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-06 21:59:44 +01:00
|
|
|
pub fn characters_mut(&mut self) -> Box<[&mut Character]> {
|
|
|
|
|
self.characters.iter_mut().collect()
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-06 20:45:15 +01:00
|
|
|
pub fn character_by_id_mut(&mut self, character_id: CharacterId) -> Result<&mut Character> {
|
2025-06-23 09:48:28 +01:00
|
|
|
self.characters
|
|
|
|
|
.iter_mut()
|
|
|
|
|
.find(|c| c.character_id() == character_id)
|
2025-10-06 20:45:15 +01:00
|
|
|
.ok_or(GameError::InvalidTarget)
|
2025-06-23 09:48:28 +01:00
|
|
|
}
|
|
|
|
|
|
2025-10-06 20:45:15 +01:00
|
|
|
pub fn character_by_id(&self, character_id: CharacterId) -> Result<&Character> {
|
2025-06-23 09:48:28 +01:00
|
|
|
self.characters
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|c| c.character_id() == character_id)
|
2025-10-06 20:45:15 +01:00
|
|
|
.ok_or(GameError::InvalidTarget)
|
2025-06-23 09:48:28 +01:00
|
|
|
}
|
|
|
|
|
|
2025-10-05 10:52:37 +01:00
|
|
|
pub fn character_by_player_id(&self, player_id: PlayerId) -> Option<&Character> {
|
2025-06-23 09:48:28 +01:00
|
|
|
self.characters.iter().find(|c| c.player_id() == player_id)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl RoleTitle {
|
|
|
|
|
pub fn title_to_role_excl_apprentice(self) -> Role {
|
|
|
|
|
match self {
|
2025-10-07 17:45:21 +01:00
|
|
|
RoleTitle::Insomniac => Role::Insomniac,
|
2025-10-07 02:52:06 +01:00
|
|
|
RoleTitle::LoneWolf => Role::LoneWolf,
|
2025-06-23 09:48:28 +01:00
|
|
|
RoleTitle::Villager => Role::Villager,
|
2025-10-04 17:50:29 +01:00
|
|
|
RoleTitle::Scapegoat => Role::Scapegoat {
|
|
|
|
|
redeemed: rand::random(),
|
|
|
|
|
},
|
2025-06-23 09:48:28 +01:00
|
|
|
RoleTitle::Seer => Role::Seer,
|
|
|
|
|
RoleTitle::Arcanist => Role::Arcanist,
|
|
|
|
|
RoleTitle::Elder => Role::Elder {
|
|
|
|
|
knows_on_night: NonZeroU8::new(rand::rng().random_range(1u8..3)).unwrap(),
|
2025-10-05 10:52:37 +01:00
|
|
|
woken_for_reveal: false,
|
|
|
|
|
lost_protection_night: None,
|
2025-06-23 09:48:28 +01:00
|
|
|
},
|
|
|
|
|
RoleTitle::Werewolf => Role::Werewolf,
|
|
|
|
|
RoleTitle::AlphaWolf => Role::AlphaWolf { killed: None },
|
2025-10-13 23:29:10 +01:00
|
|
|
RoleTitle::DireWolf => Role::DireWolf { last_blocked: None },
|
2025-06-23 09:48:28 +01:00
|
|
|
RoleTitle::Shapeshifter => Role::Shapeshifter { shifted_into: None },
|
|
|
|
|
RoleTitle::Protector => Role::Protector {
|
|
|
|
|
last_protected: None,
|
|
|
|
|
},
|
|
|
|
|
RoleTitle::Gravedigger => Role::Gravedigger,
|
|
|
|
|
RoleTitle::Hunter => Role::Hunter { target: None },
|
|
|
|
|
RoleTitle::Militia => Role::Militia { targeted: None },
|
|
|
|
|
RoleTitle::MapleWolf => Role::MapleWolf {
|
|
|
|
|
last_kill_on_night: 0,
|
|
|
|
|
},
|
|
|
|
|
RoleTitle::Guardian => Role::Guardian {
|
|
|
|
|
last_protected: None,
|
|
|
|
|
},
|
2025-10-04 17:50:29 +01:00
|
|
|
RoleTitle::Apprentice => Role::Villager,
|
2025-10-06 20:45:15 +01:00
|
|
|
RoleTitle::Adjudicator => Role::Adjudicator,
|
|
|
|
|
RoleTitle::PowerSeer => Role::PowerSeer,
|
|
|
|
|
RoleTitle::Mortician => Role::Mortician,
|
|
|
|
|
RoleTitle::Beholder => Role::Beholder,
|
|
|
|
|
RoleTitle::MasonLeader => Role::MasonLeader {
|
|
|
|
|
recruits_available: 1,
|
|
|
|
|
recruits: Box::new([]),
|
|
|
|
|
},
|
|
|
|
|
RoleTitle::Empath => Role::Empath { cursed: false },
|
|
|
|
|
RoleTitle::Vindicator => Role::Vindicator,
|
|
|
|
|
RoleTitle::Diseased => Role::Diseased,
|
2025-10-06 21:59:44 +01:00
|
|
|
RoleTitle::BlackKnight => Role::BlackKnight { attacked: None },
|
2025-10-06 20:45:15 +01:00
|
|
|
RoleTitle::Weightlifter => Role::Weightlifter,
|
|
|
|
|
RoleTitle::PyreMaster => Role::PyreMaster {
|
|
|
|
|
villagers_killed: 0,
|
|
|
|
|
},
|
2025-06-23 09:48:28 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|