werewolves/werewolves-proto/src/game/story.rs

555 lines
18 KiB
Rust
Raw Normal View History

// 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/>.
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::{
character::CharacterId,
diedto::DiedToTitle,
error::GameError,
game::{GameTime, Village, night::changes::NightChange},
message::night::{ActionPrompt, ActionResult},
role::{Alignment, AlignmentEq, Killer, Powerful, PreviousGuardianAction, RoleTitle},
};
type Result<T> = core::result::Result<T, GameError>;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum GameActions {
DayDetails(Box<[DayDetail]>),
NightDetails(NightDetails),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum DayDetail {
Execute(CharacterId),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct NightDetails {
pub choices: Box<[NightChoice]>,
pub changes: Box<[NightChange]>,
}
impl NightDetails {
2026-01-09 02:03:48 +00:00
pub fn new(
choices: &[(ActionPrompt, ActionResult)],
changes: Box<[NightChange]>,
village: &Village,
) -> Self {
Self {
changes,
choices: choices
.iter()
.cloned()
2026-01-09 02:03:48 +00:00
.filter_map(|(prompt, result)| NightChoice::new(prompt, result, village))
.collect(),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct NightChoice {
pub prompt: StoryActionPrompt,
pub result: Option<StoryActionResult>,
}
impl NightChoice {
2026-01-09 02:03:48 +00:00
pub fn new(prompt: ActionPrompt, result: ActionResult, village: &Village) -> Option<Self> {
Some(Self {
2026-01-09 02:03:48 +00:00
prompt: StoryActionPrompt::new(prompt, village)?,
result: StoryActionResult::new(result),
})
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum StoryActionResult {
RoleBlocked,
Seer(Alignment),
PowerSeer { powerful: Powerful },
Adjudicator { killer: Killer },
Arcanist(AlignmentEq),
GraveDigger(Option<RoleTitle>),
Mortician(DiedToTitle),
Insomniac { visits: Box<[CharacterId]> },
Empath { scapegoat: bool },
BeholderSawNothing,
BeholderSawEverything,
Drunk,
ShiftFailed,
}
impl StoryActionResult {
pub fn new(result: ActionResult) -> Option<Self> {
Some(match result {
ActionResult::ShiftFailed => Self::ShiftFailed,
ActionResult::BeholderSawNothing => Self::BeholderSawNothing,
ActionResult::BeholderSawEverything => Self::BeholderSawEverything,
ActionResult::Drunk => Self::Drunk,
ActionResult::RoleBlocked => Self::RoleBlocked,
ActionResult::Seer(alignment) => Self::Seer(alignment),
ActionResult::PowerSeer { powerful } => Self::PowerSeer { powerful },
ActionResult::Adjudicator { killer } => Self::Adjudicator { killer },
ActionResult::Arcanist(same) => Self::Arcanist(same),
ActionResult::GraveDigger(role_title) => Self::GraveDigger(role_title),
ActionResult::Mortician(died_to) => Self::Mortician(died_to),
ActionResult::Insomniac(visits) => Self::Insomniac {
visits: visits.iter().map(|c| c.character_id).collect(),
},
ActionResult::Empath { scapegoat } => Self::Empath { scapegoat },
ActionResult::GoBackToSleep | ActionResult::Continue => return None,
})
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum StoryActionPrompt {
Seer {
character_id: CharacterId,
chosen: CharacterId,
},
Protector {
character_id: CharacterId,
chosen: CharacterId,
},
Arcanist {
character_id: CharacterId,
chosen: (CharacterId, CharacterId),
},
Gravedigger {
character_id: CharacterId,
chosen: CharacterId,
},
Hunter {
character_id: CharacterId,
chosen: CharacterId,
},
Militia {
character_id: CharacterId,
chosen: CharacterId,
},
MapleWolf {
character_id: CharacterId,
chosen: CharacterId,
},
Guardian {
character_id: CharacterId,
chosen: CharacterId,
guarding: bool,
},
Adjudicator {
character_id: CharacterId,
chosen: CharacterId,
},
PowerSeer {
character_id: CharacterId,
chosen: CharacterId,
},
Mortician {
character_id: CharacterId,
chosen: CharacterId,
},
Beholder {
character_id: CharacterId,
chosen: CharacterId,
},
MasonsWake {
leader: CharacterId,
masons: Box<[CharacterId]>,
},
MasonLeaderRecruit {
character_id: CharacterId,
chosen: CharacterId,
},
Empath {
character_id: CharacterId,
chosen: CharacterId,
},
Vindicator {
character_id: CharacterId,
chosen: CharacterId,
},
PyreMaster {
character_id: CharacterId,
chosen: CharacterId,
},
WolfPackKill {
2026-01-09 02:03:48 +00:00
killing_wolf: CharacterId,
chosen: CharacterId,
},
Shapeshifter {
character_id: CharacterId,
},
AlphaWolf {
character_id: CharacterId,
chosen: CharacterId,
},
DireWolf {
character_id: CharacterId,
chosen: CharacterId,
},
LoneWolfKill {
character_id: CharacterId,
chosen: CharacterId,
},
Insomniac {
character_id: CharacterId,
},
Bloodletter {
character_id: CharacterId,
chosen: CharacterId,
},
2026-01-06 00:50:51 +00:00
BeholderWakes {
character_id: CharacterId,
},
}
impl StoryActionPrompt {
2026-01-09 02:03:48 +00:00
pub fn new(prompt: ActionPrompt, village: &Village) -> Option<Self> {
Some(match prompt {
2026-01-06 00:50:51 +00:00
ActionPrompt::BeholderWakes { character_id } => Self::BeholderWakes {
character_id: character_id.character_id,
},
ActionPrompt::Bloodletter {
character_id,
marked: Some(marked),
..
} => Self::Bloodletter {
character_id: character_id.character_id,
chosen: marked,
},
ActionPrompt::Seer {
character_id,
marked: Some(marked),
..
} => Self::Seer {
character_id: character_id.character_id,
chosen: marked,
},
ActionPrompt::Arcanist {
character_id,
marked: (Some(marked1), Some(marked2)),
..
} => Self::Arcanist {
character_id: character_id.character_id,
chosen: (marked1, marked2),
},
ActionPrompt::Protector {
character_id,
marked: Some(marked),
..
} => Self::Protector {
character_id: character_id.character_id,
chosen: marked,
},
ActionPrompt::Gravedigger {
character_id,
marked: Some(marked),
..
} => Self::Gravedigger {
character_id: character_id.character_id,
chosen: marked,
},
ActionPrompt::Hunter {
character_id,
marked: Some(marked),
..
} => Self::Hunter {
character_id: character_id.character_id,
chosen: marked,
},
ActionPrompt::Militia {
character_id,
marked: Some(marked),
..
} => Self::Militia {
character_id: character_id.character_id,
chosen: marked,
},
ActionPrompt::MapleWolf {
character_id,
marked: Some(marked),
..
} => Self::MapleWolf {
character_id: character_id.character_id,
chosen: marked,
},
ActionPrompt::Guardian {
character_id,
previous,
marked: Some(marked),
..
} => Self::Guardian {
character_id: character_id.character_id,
chosen: marked,
guarding: previous
.map(|prev| match prev {
PreviousGuardianAction::Protect(id) => id.character_id == marked,
_ => false,
})
.unwrap_or_default(),
},
ActionPrompt::Adjudicator {
character_id,
marked: Some(marked),
..
} => Self::Adjudicator {
character_id: character_id.character_id,
chosen: marked,
},
ActionPrompt::PowerSeer {
character_id,
marked: Some(marked),
..
} => Self::PowerSeer {
character_id: character_id.character_id,
chosen: marked,
},
ActionPrompt::Mortician {
character_id,
marked: Some(marked),
..
} => Self::Mortician {
character_id: character_id.character_id,
chosen: marked,
},
ActionPrompt::BeholderChooses {
character_id,
marked: Some(marked),
..
} => Self::Beholder {
character_id: character_id.character_id,
chosen: marked,
},
ActionPrompt::MasonsWake { leader, masons } => Self::MasonsWake {
leader: leader.character_id,
masons: masons.into_iter().map(|c| c.character_id).collect(),
},
ActionPrompt::MasonLeaderRecruit {
character_id,
marked: Some(marked),
..
} => Self::MasonLeaderRecruit {
character_id: character_id.character_id,
chosen: marked,
},
ActionPrompt::Empath {
character_id,
marked: Some(marked),
..
} => Self::Empath {
character_id: character_id.character_id,
chosen: marked,
},
ActionPrompt::Vindicator {
character_id,
marked: Some(marked),
..
} => Self::Vindicator {
character_id: character_id.character_id,
chosen: marked,
},
ActionPrompt::PyreMaster {
character_id,
marked: Some(marked),
..
} => Self::PyreMaster {
character_id: character_id.character_id,
chosen: marked,
},
ActionPrompt::WolfPackKill {
marked: Some(marked),
..
2026-01-09 02:03:48 +00:00
} => Self::WolfPackKill {
chosen: marked,
killing_wolf: village.killing_wolf().map(|c| c.character_id())?,
},
ActionPrompt::Shapeshifter { character_id } => Self::Shapeshifter {
character_id: character_id.character_id,
},
ActionPrompt::AlphaWolf {
character_id,
marked: Some(marked),
..
} => Self::AlphaWolf {
character_id: character_id.character_id,
chosen: marked,
},
ActionPrompt::DireWolf {
character_id,
marked: Some(marked),
..
} => Self::DireWolf {
character_id: character_id.character_id,
chosen: marked,
},
ActionPrompt::LoneWolfKill {
character_id,
marked: Some(marked),
..
} => Self::LoneWolfKill {
character_id: character_id.character_id,
chosen: marked,
},
ActionPrompt::Insomniac { character_id } => Self::Insomniac {
character_id: character_id.character_id,
},
ActionPrompt::TraitorIntro { .. }
| ActionPrompt::Bloodletter { .. }
| ActionPrompt::Protector { .. }
| ActionPrompt::Gravedigger { .. }
| ActionPrompt::Hunter { .. }
| ActionPrompt::Militia { .. }
| ActionPrompt::MapleWolf { .. }
| ActionPrompt::Guardian { .. }
| ActionPrompt::Adjudicator { .. }
| ActionPrompt::PowerSeer { .. }
| ActionPrompt::Mortician { .. }
| ActionPrompt::BeholderChooses { .. }
| ActionPrompt::MasonLeaderRecruit { .. }
| ActionPrompt::Empath { .. }
| ActionPrompt::Vindicator { .. }
| ActionPrompt::PyreMaster { .. }
| ActionPrompt::WolfPackKill { .. }
| ActionPrompt::AlphaWolf { .. }
| ActionPrompt::DireWolf { .. }
| ActionPrompt::LoneWolfKill { .. }
| ActionPrompt::Seer { .. }
| ActionPrompt::Arcanist { .. }
| ActionPrompt::WolvesIntro { .. }
| ActionPrompt::RoleChange { .. }
| ActionPrompt::ElderReveal { .. }
| ActionPrompt::CoverOfDarkness => return None,
})
}
2026-01-06 00:50:51 +00:00
pub const fn character_id(&self) -> Option<CharacterId> {
match self {
2026-01-09 02:03:48 +00:00
StoryActionPrompt::MasonsWake { .. } => None,
StoryActionPrompt::WolfPackKill {
killing_wolf: character_id,
..
}
| StoryActionPrompt::Seer { character_id, .. }
2026-01-06 00:50:51 +00:00
| StoryActionPrompt::Protector { character_id, .. }
| StoryActionPrompt::Arcanist { character_id, .. }
| StoryActionPrompt::Gravedigger { character_id, .. }
| StoryActionPrompt::Hunter { character_id, .. }
| StoryActionPrompt::Militia { character_id, .. }
| StoryActionPrompt::MapleWolf { character_id, .. }
| StoryActionPrompt::Guardian { character_id, .. }
| StoryActionPrompt::Adjudicator { character_id, .. }
| StoryActionPrompt::PowerSeer { character_id, .. }
| StoryActionPrompt::Mortician { character_id, .. }
| StoryActionPrompt::Beholder { character_id, .. }
| StoryActionPrompt::MasonLeaderRecruit { character_id, .. }
| StoryActionPrompt::Empath { character_id, .. }
| StoryActionPrompt::Vindicator { character_id, .. }
| StoryActionPrompt::PyreMaster { character_id, .. }
| StoryActionPrompt::Shapeshifter { character_id, .. }
| StoryActionPrompt::AlphaWolf { character_id, .. }
| StoryActionPrompt::DireWolf { character_id, .. }
| StoryActionPrompt::LoneWolfKill { character_id, .. }
| StoryActionPrompt::Insomniac { character_id, .. }
| StoryActionPrompt::Bloodletter { character_id, .. }
| StoryActionPrompt::BeholderWakes { character_id, .. } => Some(*character_id),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GameStory {
pub starting_village: Village,
pub changes: HashMap<GameTime, GameActions>,
}
impl GameStory {
pub fn new(starting_village: Village) -> Self {
Self {
starting_village,
changes: HashMap::new(),
}
}
pub fn add(&mut self, time: GameTime, changes: GameActions) -> Result<()> {
if self.changes.contains_key(&time) {
return Err(GameError::ChangesAlreadySet(time));
}
self.changes.insert(time, changes);
Ok(())
}
pub fn final_village(&self) -> Result<Village> {
let mut village = self.starting_village.clone();
for (_, actions) in self.iter() {
village = match actions {
GameActions::DayDetails(day_details) => village.with_day_changes(day_details)?,
GameActions::NightDetails(night_details) => {
village.with_night_changes(&night_details.changes)?.0
}
};
}
Ok(village)
}
pub fn village_at(&self, at_time: GameTime) -> Result<Option<Village>> {
let mut village = self.starting_village.clone();
for (time, actions) in self.iter() {
village = match actions {
GameActions::DayDetails(day_details) => village.with_day_changes(day_details)?,
GameActions::NightDetails(night_details) => {
village.with_night_changes(&night_details.changes)?.0
}
};
if time == at_time {
return Ok(Some(village));
}
}
Ok(None)
}
pub fn iter<'a>(&'a self) -> StoryIterator<'a> {
StoryIterator {
story: self,
time: self.starting_village.time(),
}
}
}
pub struct StoryIterator<'a> {
story: &'a GameStory,
time: GameTime,
}
impl<'a> Iterator for StoryIterator<'a> {
type Item = (GameTime, &'a GameActions);
fn next(&mut self) -> Option<Self::Item> {
match self.story.changes.get(&self.time) {
Some(changes) => {
let changes_time = self.time;
self.time = self.time.next();
Some((changes_time, changes))
}
None => None,
}
}
}