reimplement previous

This commit is contained in:
emilis 2025-10-07 21:18:31 +01:00
parent 17f583539d
commit 62d650fb34
No known key found for this signature in database
10 changed files with 327 additions and 102 deletions

View File

@ -198,6 +198,10 @@ impl Character {
Ok(())
}
#[cfg(test)]
pub fn role_changes(&self) -> &[RoleChange] {
&self.role_changes
}
pub const fn is_wolf(&self) -> bool {
self.role.wolf()

View File

@ -220,7 +220,7 @@ impl<'a> ChangesLookup<'a> {
.contains(&idx)
.not()
.then_some(match c {
NightChange::Shapeshift { source } => Some(source),
NightChange::Shapeshift { source, .. } => Some(source),
_ => None,
})
.flatten()

View File

@ -192,6 +192,11 @@ impl Game {
&self.state
}
#[cfg(test)]
pub fn game_state_mut(&mut self) -> &mut GameState {
&mut self.state
}
pub fn previous_game_states(&self) -> &[GameState] {
&self.previous
}

View File

@ -21,7 +21,7 @@ use crate::{
role::{PYREMASTER_VILLAGER_KILLS_TO_DIE, PreviousGuardianAction, RoleBlock, RoleTitle},
};
#[derive(Debug, Clone, Serialize, Deserialize, Extract)]
#[derive(Debug, Clone, Serialize, PartialEq, Deserialize, Extract)]
pub enum NightChange {
RoleChange(CharacterId, RoleTitle),
HunterTarget {
@ -39,6 +39,7 @@ pub enum NightChange {
},
Shapeshift {
source: CharacterId,
into: CharacterId,
},
Protection {
target: CharacterId,
@ -234,6 +235,7 @@ enum NightState {
Active {
current_prompt: ActionPrompt,
current_result: Option<ActionResult>,
current_changes: Vec<NightChange>,
},
Complete,
}
@ -243,8 +245,7 @@ pub struct Night {
village: Village,
night: u8,
action_queue: VecDeque<ActionPrompt>,
used_actions: Vec<(ActionPrompt, ActionResult)>,
changes: Vec<NightChange>,
used_actions: Vec<(ActionPrompt, ActionResult, Vec<NightChange>)>,
night_state: NightState,
}
@ -291,14 +292,12 @@ impl Night {
// let current_prompt = action_queue.pop_front().ok_or(GameError::NoNightActions)?;
let night_state = NightState::Active {
current_prompt: ActionPrompt::CoverOfDarkness,
current_changes: Vec::new(),
current_result: None,
};
let changes = Self::automatic_changes(&village, night);
Ok(Self {
night,
changes,
village,
night_state,
action_queue,
@ -337,39 +336,40 @@ impl Night {
}
pub fn previous_state(&mut self) -> Result<()> {
return Err(GameError::NoPreviousState);
let (prev_act, prev_result) = self.used_actions.pop().ok_or(GameError::NoPreviousState)?;
log::info!("loading previous prompt: {prev_act:?}");
match &self.night_state {
let (current_prompt, current_result, current_changes) = match &mut self.night_state {
NightState::Active {
current_prompt,
current_result: Some(current_result),
} => {
log::info!("removing current result: {current_result:?}");
self.night_state = NightState::Active {
current_prompt: current_prompt.clone(),
current_result: None,
current_result,
current_changes,
} => (current_prompt, current_result, current_changes),
NightState::Complete => return Err(GameError::NightOver),
};
if let Some((prompt, _, changes)) = self.used_actions.pop() {
// Remove the shapeshifter role change from the queue
if let ActionPrompt::Shapeshifter {
character_id: ss_char,
} = &prompt
&& let Some(change) = changes.first()
&& let NightChange::Shapeshift { source, into } = change
&& ss_char.character_id == *source
&& let Some(next) = self.action_queue.pop_front()
&& let ActionPrompt::RoleChange {
character_id: role_change_char,
..
} = &next
&& role_change_char.character_id != *into
{
// put it back in
self.action_queue.push_front(next);
}
NightState::Active {
current_prompt,
current_result: None,
} => {
log::info!("pushing current prompt to front of action queue: {current_prompt:?}");
self.action_queue.push_front(current_prompt.clone());
self.night_state = NightState::Active {
current_prompt: prev_act,
current_result: None,
}
}
NightState::Complete => {
self.night_state = NightState::Active {
current_prompt: prev_act,
current_result: None,
};
}
}
// panic!("{:#?}", self.action_queue.pop_front());
*current_prompt = prompt;
*current_result = None;
*current_changes = Vec::new();
Ok(())
} else {
Err(GameError::NoPreviousState)
}
}
#[cfg(test)]
@ -382,8 +382,10 @@ impl Night {
return Err(GameError::NotEndOfNight);
}
let mut new_village = self.village.clone();
let mut changes = ChangesLookup::new(&self.changes);
for change in self.changes.iter() {
let mut all_changes = Self::automatic_changes(&self.village, self.night);
all_changes.append(&mut self.changes_from_actions().into_vec());
let mut changes = ChangesLookup::new(&all_changes);
for change in all_changes.iter() {
match change {
NightChange::ElderReveal { elder } => {
new_village.character_by_id_mut(*elder)?.elder_reveal()
@ -418,10 +420,14 @@ impl Night {
kill.apply_to_village(&mut new_village)?;
}
}
NightChange::Shapeshift { source } => {
NightChange::Shapeshift { source, into } => {
if let Some(target) = changes.wolf_pack_kill_target()
&& changes.protected(target).is_none()
{
if *target != *into {
log::error!("shapeshift into({into}) != target({target})");
continue;
}
let ss = new_village.character_by_id_mut(*source).unwrap();
ss.shapeshifter_mut().unwrap().replace(*target);
ss.kill(DiedTo::Shapeshift {
@ -538,7 +544,10 @@ impl Night {
}
fn apply_shapeshift(&mut self, source: &CharacterId) -> Result<()> {
if let Some(kill_target) = self.changes.iter().find_map(|c| match c {
if let Some(kill_target) = self
.changes_from_actions()
.into_iter()
.find_map(|c| match c {
NightChange::Kill {
target,
died_to:
@ -546,21 +555,22 @@ impl Night {
night: _,
killing_wolf: _,
},
} => Some(*target),
} => Some(target),
_ => None,
}) {
if self.changes.iter().any(|c| match c {
})
{
if self.changes_from_actions().into_iter().any(|c| match c {
NightChange::Protection {
target,
protection: _,
} => target == &kill_target,
} => target == kill_target,
_ => false,
}) {
// there is protection, so the kill doesn't happen -> no shapeshift
return Ok(());
}
if self.changes.iter_mut().any(|c| {
if self.changes_from_actions().into_iter().any(|c| {
matches!(
c,
NightChange::Kill {
@ -572,16 +582,28 @@ impl Night {
}
)
}) {
self.changes.push(NightChange::Kill {
match &mut self.night_state {
NightState::Active {
current_changes, ..
} => current_changes.push(NightChange::Kill {
target: *source,
died_to: DiedTo::Shapeshift {
into: kill_target,
night: NonZeroU8::new(self.night).unwrap(),
},
});
}),
_ => return Err(GameError::InvalidMessageForGameState),
}
}
match &mut self.night_state {
NightState::Active {
current_changes, ..
} => current_changes.push(NightChange::Shapeshift {
source: *source,
into: kill_target,
}),
_ => return Err(GameError::InvalidMessageForGameState),
}
self.changes
.push(NightChange::Shapeshift { source: *source });
self.action_queue.push_front(ActionPrompt::RoleChange {
new_role: RoleTitle::Werewolf,
character_id: self.village.character_by_id(kill_target)?.identity(),
@ -617,10 +639,11 @@ impl Night {
NightState::Active {
current_prompt: _,
current_result,
..
} => current_result.replace(result.clone()),
NightState::Complete => return Err(GameError::NightOver),
};
if let NightChange::Shapeshift { source } = &change {
if let NightChange::Shapeshift { source, into } = &change {
// needs to be resolved _now_ so that the target can be woken
// for the role change with the wolves
self.apply_shapeshift(source)?;
@ -639,7 +662,12 @@ impl Night {
{
result = self.apply_mason_recruit(*mason_leader, *recruiting)?;
}
self.changes.push(change);
match &mut self.night_state {
NightState::Active {
current_changes, ..
} => current_changes.push(change),
NightState::Complete => return Err(GameError::InvalidMessageForGameState),
}
Ok(ServerAction::Result(result))
}
BlockResolvedOutcome::ActionComplete(result, None) => {
@ -647,6 +675,7 @@ impl Night {
NightState::Active {
current_prompt: _,
current_result,
..
} => {
current_result.replace(result.clone());
}
@ -693,13 +722,13 @@ impl Night {
(
ResponseOutcome::ActionComplete(ActionComplete {
result: ActionResult::GoBackToSleep,
change: Some(NightChange::Shapeshift { source }),
change: Some(NightChange::Shapeshift { source, into }),
}),
true,
_,
) => Ok(ResponseOutcome::ActionComplete(ActionComplete {
result: ActionResult::Continue,
change: Some(NightChange::Shapeshift { source }),
change: Some(NightChange::Shapeshift { source, into }),
})),
(
ResponseOutcome::ActionComplete(ActionComplete {
@ -725,12 +754,12 @@ impl Night {
ResponseOutcome::ActionComplete(ActionComplete { result, change }) => {
match self.current_prompt().ok_or(GameError::NightOver)?.unless() {
Some(Unless::TargetBlocked(unless_blocked)) => {
if self.changes.iter().any(|c| match c {
if self.changes_from_actions().into_iter().any(|c| match c {
NightChange::RoleBlock {
source: _,
target,
block_type: _,
} => target == &unless_blocked,
} => target == unless_blocked,
_ => false,
}) {
Ok(BlockResolvedOutcome::ActionComplete(
@ -742,12 +771,12 @@ impl Night {
}
}
Some(Unless::TargetsBlocked(unless_blocked1, unless_blocked2)) => {
if self.changes.iter().any(|c| match c {
if self.changes_from_actions().into_iter().any(|c| match c {
NightChange::RoleBlock {
source: _,
target,
block_type: _,
} => target == &unless_blocked1 || target == &unless_blocked2,
} => target == unless_blocked1 || target == unless_blocked2,
_ => false,
}) {
Ok(BlockResolvedOutcome::ActionComplete(
@ -769,10 +798,12 @@ impl Night {
NightState::Active {
current_prompt: _,
current_result: Some(_),
..
} => return Err(GameError::NightNeedsNext),
NightState::Active {
current_prompt,
current_result: None,
..
} => current_prompt,
NightState::Complete => return Err(GameError::NightOver),
};
@ -791,6 +822,17 @@ impl Night {
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::InvalidMessageForGameState),
@ -1019,9 +1061,24 @@ impl Night {
ActionPrompt::Shapeshifter { character_id } => {
Ok(ResponseOutcome::ActionComplete(ActionComplete {
result: ActionResult::GoBackToSleep,
change: Some(NightChange::Shapeshift {
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::InvalidMessageForGameState),
},
}))
}
ActionPrompt::AlphaWolf {
@ -1092,7 +1149,7 @@ impl Night {
marked: Some(marked),
..
} => {
if let Some(result) = self.used_actions.iter().find_map(|(prompt, result)| {
if let Some(result) = self.used_actions.iter().find_map(|(prompt, result, _)| {
prompt.matches_beholding(*marked).then_some(result)
}) {
Ok(ActionComplete {
@ -1222,6 +1279,7 @@ impl Night {
NightState::Active {
current_prompt: _,
current_result,
..
} => current_result.as_ref(),
NightState::Complete => None,
}
@ -1232,6 +1290,7 @@ impl Night {
NightState::Active {
current_prompt,
current_result: _,
..
} => Some(current_prompt),
NightState::Complete => None,
}
@ -1242,6 +1301,7 @@ impl Night {
NightState::Active {
current_prompt,
current_result: _,
..
} => match current_prompt {
ActionPrompt::Insomniac { character_id, .. }
| ActionPrompt::LoneWolfKill { character_id, .. }
@ -1290,13 +1350,18 @@ impl Night {
NightState::Active {
current_prompt,
current_result: Some(result),
current_changes,
} => {
self.used_actions
.push((current_prompt.clone(), result.clone()));
self.used_actions.push((
current_prompt.clone(),
result.clone(),
current_changes.clone(),
));
}
NightState::Active {
current_prompt: _,
current_result: None,
..
} => return Err(GameError::AwaitingResponse),
NightState::Complete => return Err(GameError::NightOver),
}
@ -1311,6 +1376,7 @@ impl Night {
self.night_state = NightState::Active {
current_prompt: prompt,
current_result: None,
current_changes: Vec::new(),
};
} else {
self.night_state = NightState::Complete;
@ -1319,15 +1385,20 @@ impl Night {
Ok(())
}
pub const fn changes(&self) -> &[NightChange] {
self.changes.as_slice()
fn changes_from_actions(&self) -> Box<[NightChange]> {
self.used_actions
.iter()
.map(|(_, _, act)| act.into_iter())
.flatten()
.cloned()
.collect()
}
pub fn get_visits_for(&self, visit_char: CharacterId) -> Visits {
Visits::new(
self.used_actions
.iter()
.filter_map(|(prompt, _)| match prompt {
.filter_map(|(prompt, _, _)| match prompt {
ActionPrompt::Arcanist {
character_id,
marked: (Some(marked1), Some(marked2)),

View File

@ -1,4 +1,5 @@
mod night_order;
mod previous;
mod role;
use crate::{

View File

@ -0,0 +1,121 @@
#[allow(unused)]
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
use crate::{
game::{Game, GameSettings, GameState, SetupRole},
game_test::{ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players},
message::{
host::ServerToHostMessage,
night::{ActionPrompt, ActionPromptTitle, ActionResponse},
},
role::RoleTitle,
};
#[test]
fn previous_shapeshifter_undone_redone() {
let players = gen_players(1..21);
let shapeshifter_player_id = players[0].player_id;
let wolf_player_id = players[1].player_id;
let mut settings = GameSettings::empty();
settings.add_and_assign(SetupRole::Shapeshifter, shapeshifter_player_id);
settings.add_and_assign(SetupRole::Werewolf, wolf_player_id);
settings.fill_remaining_slots_with_villagers(20);
let mut game = Game::new(&players, settings).unwrap();
game.r#continue().r#continue();
assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro);
game.r#continue().sleep();
game.next_expect_day();
game.execute().title().wolf_pack_kill();
let ss_target = game.living_villager();
game.mark(ss_target.character_id());
game.r#continue().r#continue();
game.next().title().shapeshifter();
game.response(ActionResponse::Shapeshift).r#continue();
assert_eq!(
game.next(),
ActionPrompt::RoleChange {
character_id: ss_target.identity(),
new_role: RoleTitle::Werewolf
}
);
match game.game_state_mut() {
GameState::Night { night } => night.previous_state().unwrap(),
GameState::Day { .. } => unreachable!(),
}
assert_eq!(
game.get_state(),
ServerToHostMessage::ActionPrompt(ActionPrompt::Shapeshifter {
character_id: game
.character_by_player_id(shapeshifter_player_id)
.identity()
})
);
game.response(ActionResponse::Shapeshift).r#continue();
assert_eq!(
game.next(),
ActionPrompt::RoleChange {
character_id: ss_target.identity(),
new_role: RoleTitle::Werewolf
}
);
game.r#continue().sleep();
game.next_expect_day();
}
#[test]
fn previous_shapeshifter_undone_and_changed_to_no() {
let players = gen_players(1..21);
let shapeshifter_player_id = players[0].player_id;
let wolf_player_id = players[1].player_id;
let mut settings = GameSettings::empty();
settings.add_and_assign(SetupRole::Shapeshifter, shapeshifter_player_id);
settings.add_and_assign(SetupRole::Werewolf, wolf_player_id);
settings.fill_remaining_slots_with_villagers(20);
let mut game = Game::new(&players, settings).unwrap();
game.r#continue().r#continue();
assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro);
game.r#continue().sleep();
game.next_expect_day();
game.execute().title().wolf_pack_kill();
let ss_target = game.living_villager();
game.mark(ss_target.character_id());
game.r#continue().r#continue();
game.next().title().shapeshifter();
game.response(ActionResponse::Shapeshift).r#continue();
assert_eq!(
game.next(),
ActionPrompt::RoleChange {
character_id: ss_target.identity(),
new_role: RoleTitle::Werewolf
}
);
match game.game_state_mut() {
GameState::Night { night } => night.previous_state().unwrap(),
GameState::Day { .. } => unreachable!(),
}
assert_eq!(
game.get_state(),
ServerToHostMessage::ActionPrompt(ActionPrompt::Shapeshifter {
character_id: game
.character_by_player_id(shapeshifter_player_id)
.identity()
})
);
game.r#continue().sleep();
game.next_expect_day();
assert_eq!(
game.character_by_player_id(ss_target.player_id())
.role_changes(),
&[]
);
}

View File

@ -5,7 +5,8 @@ use crate::{
game::{Game, GameSettings, OrRandom, SetupRole, night::NightChange},
game_test::{ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players},
message::night::{ActionPrompt, ActionPromptTitle},
role::{Alignment, RoleTitle},
player::RoleChange,
role::{Alignment, Role, RoleTitle},
};
#[allow(unused)]
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
@ -128,26 +129,18 @@ fn redeemed_scapegoat_role_changes() {
);
game.r#continue().sleep();
match game.game_state() {
crate::game::GameState::Night { night } => night
.changes()
.iter()
.find(|c| match c {
NightChange::RoleChange(char, role) => {
char == &scapegoat.character_id() && role == &RoleTitle::Seer
}
_ => false,
})
.expect("no role change"),
_ => unreachable!(),
};
game.next_expect_day();
let day_scapegoat = game
.village()
.character_by_id(scapegoat.character_id())
.unwrap();
assert_eq!(day_scapegoat.role().title(), RoleTitle::Seer);
let scapegoat = game.character_by_player_id(scapegoat_player_id);
assert_eq!(*scapegoat.role(), Role::Seer);
assert_eq!(
scapegoat.role_changes(),
&[RoleChange {
role: Role::Scapegoat { redeemed: true },
new_role: RoleTitle::Seer,
changed_on_night: 2,
}]
);
}
#[test]

View File

@ -4,7 +4,10 @@ use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
use crate::{
character::CharacterId,
game::{Game, GameSettings, SetupRole},
game_test::{ActionResultExt, GameExt, ServerToHostMessageExt, gen_players, init_log},
game_test::{
ActionPromptTitleExt, ActionResultExt, GameExt, ServerToHostMessageExt, SettingsExt,
gen_players, init_log,
},
message::{
host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage},
night::{ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult},
@ -204,3 +207,30 @@ fn only_1_shapeshift_prompt_if_first_shifts() {
game.next_expect_day();
}
#[test]
fn i_would_simply_refuse() {
let players = gen_players(1..21);
let shapeshifter_player_id = players[0].player_id;
let wolf_player_id = players[1].player_id;
let mut settings = GameSettings::empty();
settings.add_and_assign(SetupRole::Shapeshifter, shapeshifter_player_id);
settings.add_and_assign(SetupRole::Werewolf, wolf_player_id);
settings.fill_remaining_slots_with_villagers(20);
let mut game = Game::new(&players, settings).unwrap();
game.r#continue().r#continue();
assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro);
game.r#continue().sleep();
game.next_expect_day();
game.execute().title().wolf_pack_kill();
let ss_target = game.living_villager();
game.mark(ss_target.character_id());
game.r#continue().r#continue();
game.next().title().shapeshifter();
game.r#continue().sleep();
game.next_expect_day();
}

View File

@ -53,7 +53,7 @@ pub enum KillOutcome {
Failed,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RoleChange {
pub role: Role,
pub new_role: RoleTitle,

View File

@ -273,7 +273,7 @@ pub enum ArcanistCheck {
pub const MAPLE_WOLF_ABSTAIN_LIMIT: NonZeroU8 = NonZeroU8::new(3).unwrap();
pub const PYREMASTER_VILLAGER_KILLS_TO_DIE: NonZeroU8 = NonZeroU8::new(2).unwrap();
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum RoleBlock {
Direwolf,
}