2025-06-23 09:48:28 +01:00
|
|
|
use core::num::NonZeroU8;
|
|
|
|
|
|
|
|
|
|
use rand::{Rng, seq::SliceRandom};
|
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
|
|
|
|
|
use super::Result;
|
|
|
|
|
use crate::{
|
|
|
|
|
error::GameError,
|
|
|
|
|
game::{DateTime, GameOver, GameSettings},
|
|
|
|
|
message::{Identification, Target},
|
|
|
|
|
player::{Character, CharacterId, PlayerId},
|
|
|
|
|
role::{Role, RoleTitle},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct Village {
|
|
|
|
|
characters: Vec<Character>,
|
|
|
|
|
date_time: DateTime,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
settings.check()?;
|
|
|
|
|
|
|
|
|
|
let roles_spread = settings.spread();
|
|
|
|
|
let potential_apprentice_havers = roles_spread
|
|
|
|
|
.iter()
|
|
|
|
|
.filter(|r| r.is_mentor())
|
|
|
|
|
.map(|r| r.title_to_role_excl_apprentice())
|
|
|
|
|
.collect::<Box<[_]>>();
|
|
|
|
|
|
|
|
|
|
let mut roles = roles_spread
|
|
|
|
|
.into_iter()
|
|
|
|
|
.chain(
|
|
|
|
|
(0..settings.villagers_needed_for_player_count(players.len())?)
|
|
|
|
|
.map(|_| RoleTitle::Villager),
|
|
|
|
|
)
|
|
|
|
|
.map(|title| match title {
|
|
|
|
|
RoleTitle::Apprentice => Role::Apprentice(Box::new(
|
|
|
|
|
potential_apprentice_havers
|
|
|
|
|
[rand::rng().random_range(0..potential_apprentice_havers.len())]
|
|
|
|
|
.clone(),
|
|
|
|
|
)),
|
|
|
|
|
_ => title.title_to_role_excl_apprentice(),
|
|
|
|
|
})
|
|
|
|
|
.collect::<Box<[_]>>();
|
|
|
|
|
|
|
|
|
|
assert_eq!(players.len(), roles.len());
|
|
|
|
|
roles.shuffle(&mut rand::rng());
|
|
|
|
|
Ok(Self {
|
|
|
|
|
characters: players
|
|
|
|
|
.iter()
|
|
|
|
|
.cloned()
|
|
|
|
|
.zip(roles)
|
|
|
|
|
.map(|(player, role)| Character::new(player, role))
|
|
|
|
|
.collect(),
|
|
|
|
|
date_time: DateTime::Night { number: 0 },
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub const fn date_time(&self) -> DateTime {
|
|
|
|
|
self.date_time
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn find_by_character_id(&self, character_id: &CharacterId) -> Option<&Character> {
|
|
|
|
|
self.characters
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|c| c.character_id() == character_id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn find_by_character_id_mut(
|
|
|
|
|
&mut self,
|
|
|
|
|
character_id: &CharacterId,
|
|
|
|
|
) -> 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-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>> {
|
|
|
|
|
let day = match self.date_time {
|
|
|
|
|
DateTime::Day { number } => number,
|
|
|
|
|
DateTime::Night { number: _ } => return Err(GameError::NoExecutionsAtNight),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if characters.is_empty() {
|
|
|
|
|
return Err(GameError::NoTrialNotAllowed);
|
|
|
|
|
}
|
|
|
|
|
let targets = self
|
|
|
|
|
.characters
|
|
|
|
|
.iter_mut()
|
|
|
|
|
.filter(|c| characters.contains(c.character_id()))
|
|
|
|
|
.collect::<Box<[_]>>();
|
|
|
|
|
if targets.len() != characters.len() {
|
|
|
|
|
return Err(GameError::CannotFindTargetButShouldBeThere);
|
|
|
|
|
}
|
|
|
|
|
for t in targets {
|
|
|
|
|
t.execute(day)?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.date_time = self.date_time.next();
|
|
|
|
|
Ok(self.is_game_over())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn to_day(&mut self) -> Result<DateTime> {
|
|
|
|
|
if self.date_time.is_day() {
|
|
|
|
|
return Err(GameError::AlreadyDaytime);
|
|
|
|
|
}
|
|
|
|
|
self.date_time = self.date_time.next();
|
|
|
|
|
Ok(self.date_time)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn living_wolf_pack_players(&self) -> Box<[Character]> {
|
|
|
|
|
self.characters
|
|
|
|
|
.iter()
|
|
|
|
|
.filter(|c| c.role().wolf() && c.alive())
|
|
|
|
|
.cloned()
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-28 02:13:34 +01:00
|
|
|
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()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-23 09:48:28 +01:00
|
|
|
pub fn living_players(&self) -> Box<[Target]> {
|
|
|
|
|
self.characters
|
|
|
|
|
.iter()
|
|
|
|
|
.filter(|c| c.alive())
|
|
|
|
|
.map(Character::target)
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn target_by_id(&self, character_id: &CharacterId) -> Option<Target> {
|
|
|
|
|
self.character_by_id(character_id).map(Character::target)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn living_villagers(&self) -> Box<[Target]> {
|
|
|
|
|
self.characters
|
|
|
|
|
.iter()
|
|
|
|
|
.filter(|c| c.alive() && c.is_village())
|
|
|
|
|
.map(Character::target)
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn living_players_excluding(&self, exclude: &CharacterId) -> Box<[Target]> {
|
|
|
|
|
self.characters
|
|
|
|
|
.iter()
|
|
|
|
|
.filter(|c| c.alive() && c.character_id() != exclude)
|
|
|
|
|
.map(Character::target)
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn dead_targets(&self) -> Box<[Target]> {
|
|
|
|
|
self.characters
|
|
|
|
|
.iter()
|
|
|
|
|
.filter(|c| !c.alive())
|
|
|
|
|
.map(Character::target)
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn dead_characters(&self) -> Box<[&Character]> {
|
|
|
|
|
self.characters.iter().filter(|c| !c.alive()).collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn living_characters_by_role(&self, role: RoleTitle) -> Box<[Character]> {
|
|
|
|
|
self.characters
|
|
|
|
|
.iter()
|
|
|
|
|
.filter(|c| c.role().title() == role)
|
|
|
|
|
.cloned()
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn characters(&self) -> Box<[Character]> {
|
|
|
|
|
self.characters.iter().cloned().collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn character_by_id_mut(&mut self, character_id: &CharacterId) -> Option<&mut Character> {
|
|
|
|
|
self.characters
|
|
|
|
|
.iter_mut()
|
|
|
|
|
.find(|c| c.character_id() == character_id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn character_by_id(&self, character_id: &CharacterId) -> Option<&Character> {
|
|
|
|
|
self.characters
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|c| c.character_id() == character_id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn character_by_player_id(&self, player_id: &PlayerId) -> Option<&Character> {
|
|
|
|
|
self.characters.iter().find(|c| c.player_id() == player_id)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl RoleTitle {
|
|
|
|
|
pub fn title_to_role_excl_apprentice(self) -> Role {
|
|
|
|
|
match self {
|
|
|
|
|
RoleTitle::Villager => Role::Villager,
|
|
|
|
|
RoleTitle::Scapegoat => Role::Scapegoat,
|
|
|
|
|
RoleTitle::Seer => Role::Seer,
|
|
|
|
|
RoleTitle::Arcanist => Role::Arcanist,
|
|
|
|
|
RoleTitle::Elder => Role::Elder {
|
|
|
|
|
knows_on_night: NonZeroU8::new(rand::rng().random_range(1u8..3)).unwrap(),
|
|
|
|
|
},
|
|
|
|
|
RoleTitle::Werewolf => Role::Werewolf,
|
|
|
|
|
RoleTitle::AlphaWolf => Role::AlphaWolf { killed: None },
|
|
|
|
|
RoleTitle::DireWolf => Role::DireWolf,
|
|
|
|
|
RoleTitle::Shapeshifter => Role::Shapeshifter { shifted_into: None },
|
|
|
|
|
RoleTitle::Apprentice => panic!("title_to_role_excl_apprentice got an apprentice role"),
|
|
|
|
|
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,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|