// 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 . mod apply; use core::num::NonZeroU8; use rand::Rng; use serde::{Deserialize, Serialize}; use super::Result; use crate::{ character::{Character, CharacterId}, diedto::DiedTo, error::GameError, game::{GameOver, GameSettings, GameTime}, message::{CharacterIdentity, Identification, night::ActionPrompt}, player::PlayerId, role::{Role, RoleTitle}, }; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Village { characters: Box<[Character]>, time: GameTime, settings: GameSettings, } impl Village { pub fn new(players: &[Identification], settings: GameSettings) -> Result { if settings.min_players_needed() > players.len() { return Err(GameError::TooManyRoles { players: players.len() as u8, roles: settings.min_players_needed() as u8, }); } let mut characters = settings.assign(players)?; assert_eq!(characters.len(), players.len()); characters.sort_by_key(|l| l.number()); Ok(Self { settings, characters, time: GameTime::Night { number: 0 }, }) } pub fn settings(&self) -> GameSettings { self.settings.clone() } pub fn killing_wolf(&self) -> Option<&Character> { let mut wolves = self .characters .iter() .filter(|c| c.alive() && c.is_wolf()) .collect::>(); wolves.sort_by_key(|w| w.killing_wolf_order()); wolves.first().copied() } pub fn wolf_revert_prompt(&self) -> Option { 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, }) } pub fn wolf_pack_kill(&self) -> Option { let night = match self.time { GameTime::Day { .. } => return None, GameTime::Night { number } => number, }; 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(), }) } pub const fn time(&self) -> GameTime { self.time } 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) } fn living_wolves_count(&self) -> usize { self.characters .iter() .filter(|c| c.is_wolf() && c.alive()) .count() } fn living_villager_count(&self) -> usize { self.characters .iter() .filter(|c| c.is_village() && c.alive()) .count() } pub fn is_game_over(&self) -> Option { let wolves = self.living_wolves_count(); let villagers = self.living_villager_count(); let weightlifters = self .living_characters_by_role(RoleTitle::Weightlifter) .len(); if weightlifters > 0 && wolves == 1 && villagers == 1 { return Some(GameOver::VillageWins); } if wolves == 0 { return Some(GameOver::VillageWins); } if wolves >= villagers { return Some(GameOver::WolvesWin); } None } pub fn execute(&mut self, characters: &[CharacterId]) -> Result> { let day = match self.time { GameTime::Day { number } => number, GameTime::Night { number: _ } => return Err(GameError::NoExecutionsAtNight), }; let targets = self .characters .iter_mut() .filter(|c| characters.contains(&c.character_id())) .collect::>(); for t in targets { t.execute(day)?; } if let Some(game_over) = self.is_game_over() { return Ok(Some(game_over)); } self.time = self.time.next(); Ok(None) } pub fn to_day(&mut self) -> Result { if self.time.is_day() { return Err(GameError::AlreadyDaytime); } self.time = self.time.next(); Ok(self.time) } pub fn living_wolf_pack_players(&self) -> Box<[Character]> { self.characters .iter() .filter(|c| c.is_wolf() && c.alive()) .cloned() .collect() } pub fn living_players(&self) -> Box<[CharacterIdentity]> { self.characters .iter() .filter(|c| c.alive()) .map(Character::identity) .collect() } pub fn target_by_id(&self, character_id: CharacterId) -> Result { self.character_by_id(character_id).map(Character::identity) } pub fn living_villagers(&self) -> Box<[CharacterIdentity]> { self.characters .iter() .filter(|c| c.alive() && c.is_village()) .map(Character::identity) .collect() } pub fn living_players_excluding(&self, exclude: CharacterId) -> Box<[CharacterIdentity]> { self.characters .iter() .filter(|c| c.alive() && c.character_id() != exclude) .map(Character::identity) .collect() } pub fn dead_targets(&self) -> Box<[CharacterIdentity]> { self.characters .iter() .filter(|c| !c.alive()) .map(Character::identity) .collect() } pub fn dead_characters(&self) -> Box<[&Character]> { self.characters.iter().filter(|c| !c.alive()).collect() } pub fn executed_known_elder(&self) -> bool { self.characters.iter().any(|d| { d.known_elder() && d.died_to() .map(|d| matches!(d, DiedTo::Execution { .. })) .unwrap_or_default() }) } 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() } pub fn living_characters_by_role(&self, role: RoleTitle) -> Box<[Character]> { self.characters .iter() .filter(|c| c.role_title() == role) .cloned() .collect() } 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() } pub fn characters(&self) -> Box<[Character]> { self.characters.iter().cloned().collect() } pub fn characters_mut(&mut self) -> Box<[&mut Character]> { self.characters.iter_mut().collect() } pub fn character_by_id_mut(&mut self, character_id: CharacterId) -> Result<&mut Character> { self.characters .iter_mut() .find(|c| c.character_id() == character_id) .ok_or(GameError::InvalidTarget) } pub fn character_by_id(&self, character_id: CharacterId) -> Result<&Character> { self.characters .iter() .find(|c| c.character_id() == character_id) .ok_or(GameError::InvalidTarget) } 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::Insomniac => Role::Insomniac, RoleTitle::LoneWolf => Role::LoneWolf, RoleTitle::Villager => Role::Villager, RoleTitle::Scapegoat => Role::Scapegoat { redeemed: rand::random(), }, RoleTitle::Seer => Role::Seer, RoleTitle::Arcanist => Role::Arcanist, RoleTitle::Elder => Role::Elder { knows_on_night: NonZeroU8::new(rand::rng().random_range(1u8..3)).unwrap(), woken_for_reveal: false, lost_protection_night: None, }, RoleTitle::Werewolf => Role::Werewolf, RoleTitle::AlphaWolf => Role::AlphaWolf { killed: None }, RoleTitle::DireWolf => Role::DireWolf { last_blocked: None }, 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, }, RoleTitle::Apprentice => Role::Villager, 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, RoleTitle::BlackKnight => Role::BlackKnight { attacked: None }, RoleTitle::Weightlifter => Role::Weightlifter, RoleTitle::PyreMaster => Role::PyreMaster { villagers_killed: 0, }, } } }