// 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 kill;
pub mod night;
mod settings;
pub mod story;
mod village;
use core::{
fmt::{Debug, Display},
num::NonZeroU8,
ops::{Deref, Range, RangeBounds},
};
use rand::{Rng, seq::SliceRandom};
use serde::{Deserialize, Serialize};
use crate::{
character::CharacterId,
error::GameError,
game::{
night::{Night, ServerAction},
story::{DayDetail, GameActions, GameStory, NightDetails},
},
message::{
CharacterState, Identification,
host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage},
night::ActionResponse,
},
};
pub use {
settings::{Category, GameSettings, OrRandom, SetupRole, SetupRoleTitle, SetupSlot, SlotId},
village::Village,
};
type Result = core::result::Result;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Game {
history: GameStory,
state: GameState,
}
impl Game {
pub fn new(players: &[Identification], settings: GameSettings) -> Result {
let village = Village::new(players, settings)?;
Ok(Self {
history: GameStory::new(village.clone()),
state: GameState::Night {
night: Night::new(village)?,
},
})
}
pub const fn village(&self) -> &Village {
match &self.state {
GameState::Day { village, marked: _ } => village,
GameState::Night { night } => night.village(),
}
}
pub fn process(&mut self, message: HostGameMessage) -> Result {
match (&mut self.state, message) {
(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)),
) => {
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)? {
log::warn!("adding to history for {}", village.time());
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())?;
log::warn!("adding to history for {time}");
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::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 = night.village().with_night_changes(&changes)?;
log::warn!("adding to history for {}", night.village().time());
self.history.add(
night.village().time(),
GameActions::NightDetails(NightDetails::new(
&night.used_actions(),
changes,
)),
)?;
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)
}
}
}
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, Serialize, Deserialize)]
pub enum GameTime {
Day { number: NonZeroU8 },
Night { number: u8 },
}
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,
},
}
}
}