// Copyright (C) 2025-2026 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 kill;
pub mod night;
mod settings;
pub mod story;
mod village;
use core::{
cmp::Ordering,
fmt::{Debug, Display},
num::NonZeroU8,
ops::{Deref, Range, RangeBounds},
};
use chrono::{DateTime, Utc};
use rand::{Rng, seq::SliceRandom};
use serde::{
Deserialize, Serialize,
de::{Expected, Unexpected},
};
use uuid::Uuid;
use crate::{
character::CharacterId,
error::GameError,
game::{
night::{Night, ServerAction},
story::{DayDetail, GameActions, GameStory, NightDetails},
},
id_impl,
message::{
CharacterState, ClientDeadChat, Identification, ServerToClientMessage,
dead::{DeadChatContent, DeadChatMessage},
host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage},
night::ActionResponse,
},
player::PlayerId,
};
pub use {
settings::{Category, GameSettings, OrRandom, SetupRole, SetupRoleTitle, SetupSlot, SlotId},
village::Village,
};
type Result = core::result::Result;
id_impl!(GameId);
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Game {
started: DateTime,
history: GameStory,
state: GameState,
}
impl Game {
pub fn new_with_assigned_character_ids(
players: &[(Identification, CharacterId)],
settings: GameSettings,
) -> Result {
let village = Village::new_with_assigned_character_ids(players, settings)?;
Ok(Self {
started: Utc::now(),
history: GameStory::new(village.clone()),
state: GameState::Night {
night: Night::new(village)?,
},
})
}
pub fn new(players: &[Identification], settings: GameSettings) -> Result {
let village = Village::new(players, settings)?;
Ok(Self {
started: Utc::now(),
history: GameStory::new(village.clone()),
state: GameState::Night {
night: Night::new(village)?,
},
})
}
pub const fn started(&self) -> DateTime {
self.started
}
pub const fn village(&self) -> &Village {
match &self.state {
GameState::Day { village, marked: _ } => village,
GameState::Night { night } => night.village(),
}
}
pub fn dead_chats_since(&mut self, since: DateTime) -> Vec {
match &mut self.state {
GameState::Day { village, .. } => village.dead_chat_mut().host_get_since(since),
GameState::Night { night } => night.dead_chat_mut().host_get_since(since),
}
}
pub fn process_dead_chat_request(
&mut self,
player_id: PlayerId,
message: ClientDeadChat,
) -> Result {
let char = self
.village()
.character_by_player_id(player_id)
.ok_or(GameError::NoMatchingCharacterFound)?;
match message {
ClientDeadChat::Send(message) => {
let msg = DeadChatMessage {
id: Uuid::new_v4(),
timestamp: Utc::now(),
message: DeadChatContent::PlayerMessage {
message,
from: char.identity(),
},
};
match &mut self.state {
GameState::Day { village, .. } => {
village.send_dead_chat_message(msg.clone())?
}
GameState::Night { night } => {
let time = night.village().time();
night.dead_chat_mut().add(time, msg.clone())?
}
}
Ok(ServerToClientMessage::DeadChatMessage(msg))
}
ClientDeadChat::GetHistory => {
let messages = self
.village()
.dead_chat()
.get_since(self.started, char.character_id());
Ok(ServerToClientMessage::DeadChat(messages))
}
ClientDeadChat::GetSince(since) => {
let messages = self
.village()
.dead_chat()
.get_since(since, char.character_id());
Ok(ServerToClientMessage::DeadChat(messages))
}
}
}
#[cfg(test)]
#[doc(hidden)]
pub const fn village_mut(&mut self) -> &mut Village {
match &mut self.state {
GameState::Day { village, marked: _ } => village,
GameState::Night { night } => night.village_mut(),
}
}
pub fn process(&mut self, message: HostGameMessage) -> Result {
match (&mut self.state, message) {
(_, HostGameMessage::SendChatMessage(msg)) => {
let msg = match &mut self.state {
GameState::Day { village, .. } => {
let time = village.time();
village.dead_chat_mut().add_host_message(time, msg)
}
GameState::Night { night } => {
let time = night.village().time();
night.dead_chat_mut().add_host_message(time, msg)
}
};
log::info!("host sent dead chat message: {msg:?}");
Ok(ServerToHostMessage::DeadChatMessage(msg))
}
(_, HostGameMessage::GetDeadChatSince(since)) => Ok(ServerToHostMessage::DeadChat(
match &mut self.state {
GameState::Day { village, .. } => village.dead_chat(),
GameState::Night { night } => night.dead_chat(),
}
.host_get_since(since),
)),
(GameState::Night { night }, HostGameMessage::SeePlayersWithRoles) => {
Ok(ServerToHostMessage::PlayerStates(
night
.village()
.characters()
.into_iter()
.map(|c| CharacterState {
player_id: c.player_id(),
identity: c.identity(),
role: c.role_title(),
died_to: c.died_to().cloned(),
})
.collect(),
))
}
(GameState::Night { night }, HostGameMessage::Night(HostNightMessage::NextPage)) => {
night.next_page();
self.process(HostGameMessage::GetState)
}
(GameState::Night { night }, HostGameMessage::Night(HostNightMessage::Next)) => {
night.next()?;
self.process(HostGameMessage::GetState)
}
(
GameState::Day { village, marked },
HostGameMessage::Day(HostDayMessage::MarkForExecution(target)),
) => {
if village.character_by_id(target)?.died_to().is_some() {
return Err(GameError::CharacterAlreadyDead);
}
match marked
.iter()
.enumerate()
.find_map(|(idx, mark)| (mark == &target).then_some(idx))
{
Some(idx) => {
marked.swap_remove(idx);
}
None => marked.push(target),
}
self.process(HostGameMessage::GetState)
}
(GameState::Day { village, marked }, HostGameMessage::Day(HostDayMessage::Execute)) => {
let time = village.time();
if let Some(outcome) = village.execute(marked)? {
self.history.add(
village.time(),
GameActions::DayDetails(
marked.iter().map(|c| DayDetail::Execute(*c)).collect(),
),
)?;
return Ok(ServerToHostMessage::GameOver(outcome));
}
let night = Night::new(village.clone())?;
self.history.add(
time,
GameActions::DayDetails(
marked
.iter()
.map(|mark| DayDetail::Execute(*mark))
.collect(),
),
)?;
self.state = GameState::Night { night };
self.process(HostGameMessage::GetState)
}
(GameState::Day { village, marked }, HostGameMessage::GetState) => {
if let Some(outcome) = village.is_game_over() {
return Ok(ServerToHostMessage::GameOver(outcome));
}
Ok(ServerToHostMessage::Daytime {
marked: marked.clone().into_boxed_slice(),
characters: village
.characters()
.into_iter()
.map(|c| CharacterState {
player_id: c.player_id(),
identity: c.identity(),
role: c.role_title(),
died_to: c.died_to().cloned(),
})
.collect(),
day: match village.time() {
GameTime::Day { number } => number,
GameTime::Night { number: _ } => unreachable!(),
},
settings: village.settings(),
})
}
(GameState::Night { night }, HostGameMessage::Night(HostNightMessage::SkipAction)) => {
night.skip_action()?;
self.process(HostGameMessage::GetState)
}
(GameState::Night { night }, HostGameMessage::GetState) => {
if let Some(res) = night.current_result() {
return Ok(ServerToHostMessage::ActionResult(
night.current_character().map(|c| c.identity()),
res.clone(),
));
}
if let Some((prompt, page)) = night.current_prompt() {
return Ok(ServerToHostMessage::ActionPrompt(prompt.clone(), page));
}
match night.next() {
Ok(_) => self.process(HostGameMessage::GetState),
Err(GameError::NightOver) => {
let changes = night.collect_changes()?;
let (village, recorded_changes) =
night.village().with_night_changes(&changes)?;
self.history.add(
night.village().time(),
GameActions::NightDetails(NightDetails::new(
&night.used_actions(),
recorded_changes,
self.village(),
)),
)?;
self.state = GameState::Day {
village,
marked: Vec::new(),
};
self.process(HostGameMessage::GetState)
}
Err(err) => Err(err),
}
}
(
GameState::Night { night },
HostGameMessage::Night(HostNightMessage::ActionResponse(ActionResponse::Continue)),
) => match night.continue_result()? {
ServerAction::Prompt(prompt) => Ok(ServerToHostMessage::ActionPrompt(
prompt,
night.page().unwrap_or_default(),
)),
ServerAction::Result(result) => Ok(ServerToHostMessage::ActionResult(
night.current_character().map(|c| c.identity()),
result,
)),
},
(
GameState::Night { night },
HostGameMessage::Night(HostNightMessage::ActionResponse(resp)),
) => match night.received_response(resp.clone()) {
Ok(ServerAction::Prompt(prompt)) => Ok(ServerToHostMessage::ActionPrompt(
prompt,
night.page().unwrap_or_default(),
)),
Ok(ServerAction::Result(res)) => Ok(ServerToHostMessage::ActionResult(
night.current_character().map(|c| c.identity()),
res,
)),
Err(GameError::NightNeedsNext) => match night.next() {
Ok(_) => self.process(HostGameMessage::Night(
HostNightMessage::ActionResponse(resp),
)),
Err(GameError::NightOver) => {
// since the block handling HostGameMessage::GetState for night
// already manages the NightOver state, just invoke it
self.process(HostGameMessage::GetState)
}
Err(err) => Err(err),
},
Err(err) => Err(err),
},
(GameState::Night { night: _ }, HostGameMessage::Day(_))
| (
GameState::Day {
village: _,
marked: _,
},
HostGameMessage::Night(_),
) => Err(GameError::InvalidMessageForGameState),
(GameState::Day { .. }, HostGameMessage::PreviousState) => {
Err(GameError::NoPreviousDuringDay)
}
(GameState::Night { night }, HostGameMessage::PreviousState) => {
night.previous_state()?;
self.process(HostGameMessage::GetState)
}
(_, HostGameMessage::SeePlayersWithRoles) => self.process(HostGameMessage::GetState),
}
}
pub fn story(&self) -> GameStory {
self.history.clone()
}
pub fn game_over(&self) -> Option {
self.state.game_over()
}
pub fn game_state(&self) -> &GameState {
&self.state
}
#[cfg(test)]
pub fn game_state_mut(&mut self) -> &mut GameState {
&mut self.state
}
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum GameState {
Day {
village: Village,
marked: Vec,
},
Night {
night: Night,
},
}
impl GameState {
pub fn game_over(&self) -> Option {
match self {
GameState::Day { village, marked: _ } => village.is_game_over(),
GameState::Night { night: _ } => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum GameOver {
VillageWins,
WolvesWin,
}
impl Display for GameOver {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
GameOver::VillageWins => f.write_str("village wins"),
GameOver::WolvesWin => f.write_str("wolves win"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Pool
where
T: Debug + Clone,
{
pub pool: Vec,
pub range: Range,
}
impl Pool
where
T: Debug + Clone,
{
pub const fn new(pool: Vec, range: Range) -> Self {
Self { pool, range }
}
pub fn collapse(mut self, rng: &mut impl Rng, max: u8) -> Vec {
let range = match self.range.end_bound() {
core::ops::Bound::Included(end) => {
if max < *end {
self.range.start..max + 1
} else {
self.range.clone()
}
}
core::ops::Bound::Excluded(end) => {
if max <= *end {
self.range.start..max + 1
} else {
self.range.clone()
}
}
core::ops::Bound::Unbounded => self.range.start..max + 1,
};
let count = rng.random_range(range);
self.pool.shuffle(rng);
self.pool.truncate(count as _);
self.pool
}
}
impl Deref for Pool
where
T: Debug + Clone,
{
type Target = [T];
fn deref(&self) -> &Self::Target {
&self.pool
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Maybe {
Yes,
No,
Maybe,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Copy)]
pub enum GameTime {
Day { number: NonZeroU8 },
Night { number: u8 },
}
impl Serialize for GameTime {
fn serialize(&self, serializer: S) -> std::result::Result
where
S: serde::Serializer,
{
match self {
GameTime::Day { number } => format!("day_{number}"),
GameTime::Night { number } => format!("night_{number}"),
}
.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for GameTime {
fn deserialize(deserializer: D) -> std::result::Result
where
D: serde::Deserializer<'de>,
{
enum ExpectedGameTime {
Format,
NumberU8,
NonZeroU8,
}
impl Expected for ExpectedGameTime {
fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str(match self {
Self::Format => "expected day_{num_nz} or night_{num}, where {num_nz} is a nonzero u8, and {num} is a u8",
Self::NumberU8 => "expected a u8 after day_ or night_",
Self::NonZeroU8 => "expected a non-zero day number",
})
}
}
let s = crate::limited::ClampedString::<5, 9>::deserialize(deserializer)?;
let (day_or_night, number) = s.split_once('_').ok_or(serde::de::Error::invalid_value(
Unexpected::Str(s.as_str()),
&ExpectedGameTime::Format,
))?;
let parsed_number = number.parse::().map_err(|_| {
serde::de::Error::invalid_value(Unexpected::Str(number), &ExpectedGameTime::NumberU8)
})?;
match day_or_night {
"day" => NonZeroU8::new(parsed_number)
.map(|number| GameTime::Day { number })
.ok_or(serde::de::Error::invalid_value(
Unexpected::Str(number),
&ExpectedGameTime::NonZeroU8,
)),
"night" => Ok(GameTime::Night {
number: parsed_number,
}),
_ => Err(serde::de::Error::invalid_value(
Unexpected::Str(day_or_night),
&ExpectedGameTime::Format,
)),
}
}
}
impl PartialOrd for GameTime {
fn partial_cmp(&self, other: &Self) -> Option {
Some(self.cmp(other))
}
}
impl Ord for GameTime {
fn cmp(&self, other: &Self) -> Ordering {
match (self, other) {
(GameTime::Day { number: l }, GameTime::Day { number: r }) => l.cmp(r),
(GameTime::Day { number: l }, GameTime::Night { number: r }) => {
if *r >= l.get() {
Ordering::Less
} else {
Ordering::Greater
}
}
(GameTime::Night { number: l }, GameTime::Day { number: r }) => {
if *l >= r.get() {
Ordering::Greater
} else {
Ordering::Less
}
}
(GameTime::Night { number: l }, GameTime::Night { number: r }) => l.cmp(r),
}
}
}
impl Display for GameTime {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
GameTime::Day { number } => write!(f, "Day {number}"),
GameTime::Night { number } => write!(f, "Night {number}"),
}
}
}
impl Default for GameTime {
fn default() -> Self {
GameTime::Day {
number: NonZeroU8::new(1).unwrap(),
}
}
}
impl GameTime {
pub const fn is_day(&self) -> bool {
matches!(self, GameTime::Day { number: _ })
}
pub const fn is_night(&self) -> bool {
matches!(self, GameTime::Night { number: _ })
}
pub const fn next(self) -> Self {
match self {
GameTime::Day { number } => GameTime::Night {
number: number.get(),
},
GameTime::Night { number } => GameTime::Day {
number: NonZeroU8::new(number + 1).unwrap(),
},
}
}
pub const fn previous(self) -> Option {
match self {
GameTime::Day { number } => Some(GameTime::Night {
number: number.get() - 1,
}),
GameTime::Night { number } => match NonZeroU8::new(number) {
Some(number) => Some(GameTime::Day { number }),
None => None,
},
}
}
}