werewolves/werewolves-proto/src/game/night/process.rs

595 lines
23 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 core::num::NonZeroU8;
use crate::{
aura::Aura,
bag::DrunkRoll,
diedto::DiedTo,
error::GameError,
2025-10-17 21:16:10 +01:00
game::night::{
ActionComplete, CurrentResult, Night, NightState, ResponseOutcome, changes::NightChange,
},
message::night::{ActionPrompt, ActionResponse, ActionResult},
player::Protection,
role::{Alignment, AlignmentEq, PreviousGuardianAction, RoleBlock, RoleTitle},
};
type Result<T> = core::result::Result<T, GameError>;
impl Night {
pub(super) fn process(&self, resp: ActionResponse) -> Result<ResponseOutcome> {
let current_prompt = match &self.night_state {
NightState::Active {
2025-10-17 21:16:10 +01:00
current_result: CurrentResult::GoBackToSleepAfterShown { .. },
..
}
| NightState::Active {
current_result: CurrentResult::Result(ActionResult::GoBackToSleep),
..
} => return Err(GameError::NightNeedsNext),
NightState::Active {
current_prompt,
2025-10-17 21:16:10 +01:00
current_result: CurrentResult::None,
..
}
| NightState::Active {
current_prompt,
current_result: CurrentResult::Result(_),
..
} => current_prompt,
NightState::Complete => return Err(GameError::NightOver),
};
match resp {
ActionResponse::MarkTarget(mark) => {
return Ok(ResponseOutcome::PromptUpdate(
current_prompt.with_mark(mark)?,
));
}
ActionResponse::Shapeshift => {
return match current_prompt {
ActionPrompt::Shapeshifter {
character_id: source,
} => Ok(ResponseOutcome::ActionComplete(ActionComplete {
result: ActionResult::GoBackToSleep,
change: Some(NightChange::Shapeshift {
source: source.character_id,
into: self
.changes_from_actions()
.into_iter()
.find_map(|c| match c {
NightChange::Kill {
target,
died_to: DiedTo::Wolfpack { .. },
} => Some(target),
_ => None,
})
.ok_or(GameError::InvalidTarget)?,
}),
})),
_ => Err(GameError::ShapeshiftingIsForShapeshifters),
};
}
ActionResponse::Continue => {
if let ActionPrompt::Insomniac { character_id } = current_prompt {
return Ok(ActionComplete {
result: ActionResult::Insomniac(
self.get_visits_for(character_id.character_id),
),
change: None,
}
.into());
}
if let ActionPrompt::RoleChange {
character_id,
new_role,
} = current_prompt
{
return Ok(ResponseOutcome::ActionComplete(ActionComplete {
result: ActionResult::GoBackToSleep,
change: Some(NightChange::RoleChange(
character_id.character_id,
*new_role,
)),
}));
}
}
ActionResponse::ContinueToResult => return self.process(ActionResponse::Continue),
};
match current_prompt {
ActionPrompt::TraitorIntro { .. } => Ok(ActionComplete {
result: ActionResult::GoBackToSleep,
change: None,
}
.into()),
ActionPrompt::Bloodletter {
character_id,
marked: Some(marked),
..
} => Ok(ActionComplete {
result: ActionResult::GoBackToSleep,
change: Some(NightChange::ApplyAura {
source: character_id.character_id,
aura: Aura::Bloodlet { night: self.night },
target: *marked,
}),
}
.into()),
ActionPrompt::LoneWolfKill {
character_id,
marked: Some(marked),
..
} => Ok(ActionComplete {
result: ActionResult::GoBackToSleep,
change: Some(NightChange::Kill {
target: *marked,
died_to: DiedTo::LoneWolf {
killer: character_id.character_id,
night: self.night,
},
}),
}
.into()),
ActionPrompt::RoleChange { .. }
| ActionPrompt::WolvesIntro { .. }
| ActionPrompt::CoverOfDarkness => {
Ok(ResponseOutcome::ActionComplete(ActionComplete {
result: ActionResult::GoBackToSleep,
change: None,
}))
}
ActionPrompt::ElderReveal { character_id } => {
Ok(ResponseOutcome::ActionComplete(ActionComplete {
result: ActionResult::GoBackToSleep,
change: Some(NightChange::ElderReveal {
elder: character_id.character_id,
}),
}))
}
ActionPrompt::Seer {
marked: Some(marked),
..
} => {
let alignment = self.character_with_current_auras(*marked)?.alignment();
Ok(ResponseOutcome::ActionComplete(ActionComplete {
result: ActionResult::Seer(alignment),
change: None,
}))
}
ActionPrompt::Protector {
marked: Some(marked),
character_id,
..
} => Ok(ResponseOutcome::ActionComplete(ActionComplete {
result: ActionResult::GoBackToSleep,
change: Some(NightChange::Protection {
target: *marked,
protection: Protection::Protector {
source: character_id.character_id,
},
}),
})),
ActionPrompt::Arcanist {
marked: (Some(marked1), Some(marked2)),
..
} => {
let same = self.character_with_current_auras(*marked1)?.alignment()
== self.character_with_current_auras(*marked2)?.alignment();
Ok(ResponseOutcome::ActionComplete(ActionComplete {
result: ActionResult::Arcanist(AlignmentEq::new(same)),
change: None,
}))
}
ActionPrompt::Gravedigger {
marked: Some(marked),
..
} => {
let dig_role = self
.character_with_current_auras(*marked)?
.gravedigger_dig();
Ok(ResponseOutcome::ActionComplete(ActionComplete {
result: ActionResult::GraveDigger(dig_role),
change: None,
}))
}
ActionPrompt::Hunter {
character_id,
marked: Some(marked),
..
} => Ok(ResponseOutcome::ActionComplete(ActionComplete {
result: ActionResult::GoBackToSleep,
change: Some(NightChange::HunterTarget {
source: character_id.character_id,
target: *marked,
}),
})),
ActionPrompt::Militia {
character_id,
marked: Some(marked),
..
} => Ok(ResponseOutcome::ActionComplete(ActionComplete {
result: ActionResult::GoBackToSleep,
change: Some(NightChange::Kill {
target: *marked,
died_to: DiedTo::Militia {
killer: character_id.character_id,
night: NonZeroU8::new(self.night)
.ok_or(GameError::CannotHappenOnNightZero)?,
},
}),
})),
ActionPrompt::Militia { marked: None, .. } => {
Ok(ResponseOutcome::ActionComplete(Default::default()))
}
ActionPrompt::MapleWolf {
character_id,
kill_or_die,
marked: Some(marked),
..
} => Ok(ResponseOutcome::ActionComplete(ActionComplete {
result: ActionResult::GoBackToSleep,
change: NonZeroU8::new(self.night).map(|night| NightChange::Kill {
target: *marked,
died_to: DiedTo::MapleWolf {
night,
source: character_id.character_id,
starves_if_fails: *kill_or_die,
},
}),
})),
ActionPrompt::MapleWolf { marked: None, .. } => {
Ok(ResponseOutcome::ActionComplete(Default::default()))
}
ActionPrompt::Guardian {
character_id,
previous: None,
marked: Some(marked),
..
} => Ok(ResponseOutcome::ActionComplete(ActionComplete {
result: ActionResult::GoBackToSleep,
change: Some(NightChange::Protection {
target: *marked,
protection: Protection::Guardian {
source: character_id.character_id,
guarding: false,
},
}),
})),
ActionPrompt::Guardian {
character_id,
previous: Some(PreviousGuardianAction::Guard(prev_target)),
marked: Some(marked),
..
} => {
if prev_target.character_id == *marked {
return Err(GameError::InvalidTarget);
}
Ok(ResponseOutcome::ActionComplete(ActionComplete {
result: ActionResult::GoBackToSleep,
change: Some(NightChange::Protection {
target: *marked,
protection: Protection::Guardian {
source: character_id.character_id,
guarding: false,
},
}),
}))
}
ActionPrompt::Guardian {
character_id,
previous: Some(PreviousGuardianAction::Protect(prev_protect)),
marked: Some(marked),
..
} => Ok(ResponseOutcome::ActionComplete(ActionComplete {
result: ActionResult::GoBackToSleep,
change: Some(NightChange::Protection {
target: *marked,
protection: Protection::Guardian {
source: character_id.character_id,
guarding: prev_protect.character_id == *marked,
},
}),
})),
ActionPrompt::WolfPackKill {
marked: Some(marked),
..
} => Ok(ResponseOutcome::ActionComplete(ActionComplete {
result: ActionResult::GoBackToSleep,
change: Some(NightChange::Kill {
target: *marked,
died_to: DiedTo::Wolfpack {
killing_wolf: self
.village
.killing_wolf()
.ok_or(GameError::NoWolves)?
.character_id(),
night: NonZeroU8::new(self.night)
.ok_or(GameError::CannotHappenOnNightZero)?,
},
}),
})),
ActionPrompt::Shapeshifter { character_id } => {
Ok(ResponseOutcome::ActionComplete(ActionComplete {
result: ActionResult::GoBackToSleep,
change: match &resp {
ActionResponse::Continue => None,
ActionResponse::Shapeshift => Some(NightChange::Shapeshift {
source: character_id.character_id,
into: self
.changes_from_actions()
.into_iter()
.find_map(|c| match c {
NightChange::Kill {
target,
died_to: DiedTo::Wolfpack { .. },
} => Some(target),
_ => None,
})
.ok_or(GameError::InvalidTarget)?,
}),
_ => return Err(GameError::ShapeshiftingIsForShapeshifters),
},
}))
}
ActionPrompt::AlphaWolf {
character_id,
marked: Some(marked),
..
} => Ok(ResponseOutcome::ActionComplete(ActionComplete {
result: ActionResult::GoBackToSleep,
change: Some(NightChange::Kill {
target: *marked,
died_to: DiedTo::AlphaWolf {
killer: character_id.character_id,
night: NonZeroU8::new(self.night)
.ok_or(GameError::CannotHappenOnNightZero)?,
},
}),
})),
ActionPrompt::AlphaWolf { marked: None, .. } => {
Ok(ResponseOutcome::ActionComplete(Default::default()))
}
ActionPrompt::DireWolf {
character_id,
marked: Some(marked),
..
} => Ok(ResponseOutcome::ActionComplete(ActionComplete {
result: ActionResult::GoBackToSleep,
change: Some(NightChange::RoleBlock {
source: character_id.character_id,
target: *marked,
block_type: RoleBlock::Direwolf,
}),
})),
ActionPrompt::Adjudicator {
marked: Some(marked),
..
} => Ok(ActionComplete {
result: ActionResult::Adjudicator {
killer: self.character_with_current_auras(*marked)?.killer(),
},
change: None,
}
.into()),
ActionPrompt::PowerSeer {
marked: Some(marked),
..
} => Ok(ActionComplete {
result: ActionResult::PowerSeer {
powerful: self.character_with_current_auras(*marked)?.powerful(),
},
change: None,
}
.into()),
ActionPrompt::Mortician {
marked: Some(marked),
..
} => Ok(ActionComplete {
result: ActionResult::Mortician(
self.village
.character_by_id(*marked)?
.died_to()
.ok_or(GameError::InvalidTarget)?
.title(),
),
change: None,
}
.into()),
ActionPrompt::Beholder {
marked: Some(marked),
..
} => {
if let Some(result) = self.used_actions.iter().find_map(|(prompt, result, _)| {
prompt.matches_beholding(*marked).then_some(result)
}) && self.died_to_tonight(*marked)?.is_some()
{
Ok(ActionComplete {
result: if matches!(result, ActionResult::RoleBlocked | ActionResult::Drunk)
{
ActionResult::BeholderSawNothing
} else {
result.clone()
},
change: None,
}
.into())
} else {
Ok(ActionComplete {
result: ActionResult::GoBackToSleep,
change: None,
}
.into())
}
}
ActionPrompt::MasonsWake { .. } => Ok(ActionComplete {
result: ActionResult::GoBackToSleep,
change: None,
}
.into()),
ActionPrompt::MasonLeaderRecruit {
character_id,
marked: Some(marked),
..
} => Ok(ActionComplete {
result: ActionResult::Continue,
change: Some(NightChange::MasonRecruit {
mason_leader: character_id.character_id,
recruiting: *marked,
}),
}
.into()),
ActionPrompt::Empath {
character_id,
marked: Some(marked),
..
} => {
let marked = self.village.character_by_id(*marked)?;
let scapegoat = marked.role_title() == RoleTitle::Scapegoat;
Ok(ActionComplete {
result: ActionResult::Empath { scapegoat },
change: scapegoat.then(|| NightChange::EmpathFoundScapegoat {
empath: character_id.character_id,
scapegoat: marked.character_id(),
}),
}
.into())
}
ActionPrompt::Vindicator {
character_id,
marked: Some(marked),
..
} => Ok(ActionComplete {
result: ActionResult::GoBackToSleep,
change: Some(NightChange::Protection {
target: *marked,
protection: Protection::Vindicator {
source: character_id.character_id,
},
}),
}
.into()),
ActionPrompt::Insomniac { .. } => Ok(ActionComplete {
result: ActionResult::GoBackToSleep,
change: None,
}
.into()),
ActionPrompt::PyreMaster {
character_id,
marked: Some(marked),
..
} => Ok(ActionComplete {
result: ActionResult::GoBackToSleep,
change: NonZeroU8::new(self.night).map(|night| NightChange::Kill {
target: *marked,
died_to: DiedTo::PyreMaster {
killer: character_id.character_id,
night,
},
}),
}
.into()),
ActionPrompt::PyreMaster { marked: None, .. }
| ActionPrompt::MasonLeaderRecruit { marked: None, .. } => Ok(ActionComplete {
result: ActionResult::GoBackToSleep,
change: None,
}
.into()),
ActionPrompt::Bloodletter { marked: None, .. }
| ActionPrompt::Adjudicator { marked: None, .. }
| ActionPrompt::PowerSeer { marked: None, .. }
| ActionPrompt::Mortician { marked: None, .. }
| ActionPrompt::Beholder { marked: None, .. }
| ActionPrompt::Empath { marked: None, .. }
| ActionPrompt::Vindicator { marked: None, .. }
| ActionPrompt::Protector { marked: None, .. }
| ActionPrompt::Arcanist {
marked: (None, None),
..
}
| ActionPrompt::Arcanist {
marked: (None, Some(_)),
..
}
| ActionPrompt::Arcanist {
marked: (Some(_), None),
..
}
| ActionPrompt::LoneWolfKill { marked: None, .. }
| ActionPrompt::Gravedigger { marked: None, .. }
| ActionPrompt::Hunter { marked: None, .. }
| ActionPrompt::Guardian { marked: None, .. }
| ActionPrompt::WolfPackKill { marked: None, .. }
| ActionPrompt::DireWolf { marked: None, .. }
| ActionPrompt::Seer { marked: None, .. } => Err(GameError::MustSelectTarget),
}
}
pub(super) fn received_response_with_auras(
&self,
resp: ActionResponse,
) -> Result<ResponseOutcome> {
let outcome = self.process(resp)?;
if matches!(
self.current_prompt(),
Some((ActionPrompt::TraitorIntro { .. }, _))
| Some((ActionPrompt::RoleChange { .. }, _))
| Some((ActionPrompt::ElderReveal { .. }, _))
) {
return Ok(outcome);
}
let mut act = match outcome {
ResponseOutcome::PromptUpdate(prompt) => {
return Ok(ResponseOutcome::PromptUpdate(prompt));
}
ResponseOutcome::ActionComplete(ActionComplete {
result: ActionResult::Drunk,
..
})
| ResponseOutcome::ActionComplete(ActionComplete {
result: ActionResult::RoleBlocked,
..
}) => return Ok(outcome),
ResponseOutcome::ActionComplete(act) => act,
};
let Some(char) = self.current_character() else {
return Ok(ResponseOutcome::ActionComplete(act));
};
for aura in char.auras() {
match aura {
Aura::Traitor | Aura::Bloodlet { .. } => continue,
Aura::Drunk(bag) => {
if bag.peek() == DrunkRoll::Drunk {
act.change = None;
act.result = ActionResult::Drunk;
return Ok(ResponseOutcome::ActionComplete(act));
}
}
Aura::Insane => {
if let Some(insane_result) = act.result.insane() {
act.result = insane_result;
}
}
}
}
Ok(ResponseOutcome::ActionComplete(act))
}
}