GoToSleep stage for all results

This commit is contained in:
emilis 2025-10-17 21:16:10 +01:00
parent eaaf0ab2b7
commit 96ce57f9f3
No known key found for this signature in database
11 changed files with 208 additions and 21 deletions

View File

@ -23,6 +23,7 @@ use crate::{
message::{
CharacterState, Identification,
host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage},
night::ActionResponse,
},
};
@ -166,6 +167,19 @@ impl Game {
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)),

View File

@ -193,13 +193,39 @@ impl From<Unless> for ActionResult {
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) enum CurrentResult {
None,
Result(ActionResult),
/// Appears after [CurrentResult::Result] so that *all* results get a
/// consistent "go back to sleep" screen
GoBackToSleepAfterShown {
result_with_data: ActionResult,
},
}
impl From<ActionResult> for CurrentResult {
fn from(value: ActionResult) -> Self {
Self::Result(value)
}
}
impl From<Option<ActionResult>> for CurrentResult {
fn from(value: Option<ActionResult>) -> Self {
match value {
Some(res) => Self::Result(res),
None => Self::None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(clippy::large_enum_variant)]
enum NightState {
Active {
current_prompt: ActionPrompt,
current_result: Option<ActionResult>,
current_result: CurrentResult,
current_changes: Vec<NightChange>,
current_page: usize,
},
@ -279,7 +305,7 @@ impl Night {
let night_state = NightState::Active {
current_prompt: ActionPrompt::CoverOfDarkness,
current_changes: Vec::new(),
current_result: None,
current_result: CurrentResult::None,
current_page: 0,
};
@ -467,7 +493,7 @@ impl Night {
.map(ActionPrompt::title)
.collect::<Box<[_]>>()
);
*current_result = None;
*current_result = CurrentResult::None;
*current_changes = Vec::new();
Ok(())
} else {
@ -601,11 +627,35 @@ impl Night {
}
}
pub(crate) fn continue_result(&mut self) -> Result<ServerAction> {
match &mut self.night_state {
NightState::Active { current_result, .. } => match current_result {
CurrentResult::None => self.received_response(ActionResponse::Continue),
CurrentResult::Result(ActionResult::Continue)
| CurrentResult::GoBackToSleepAfterShown { .. }
| CurrentResult::Result(ActionResult::GoBackToSleep) => {
Err(GameError::NightNeedsNext)
}
CurrentResult::Result(action_result) => {
*current_result = CurrentResult::GoBackToSleepAfterShown {
result_with_data: action_result.clone(),
};
Ok(ServerAction::Result(ActionResult::GoBackToSleep))
}
},
NightState::Complete => Err(GameError::NightOver),
}
}
pub fn received_response(&mut self, resp: ActionResponse) -> Result<ServerAction> {
match self.received_response_with_role_blocks(resp)? {
BlockResolvedOutcome::PromptUpdate(prompt) => match &mut self.night_state {
NightState::Active {
current_result: Some(_),
current_result: CurrentResult::Result(_),
..
}
| NightState::Active {
current_result: CurrentResult::GoBackToSleepAfterShown { .. },
..
} => Err(GameError::AwaitingResponse),
NightState::Active { current_prompt, .. } => {
@ -620,7 +670,7 @@ impl Night {
current_prompt: _,
current_result,
..
} => current_result.replace(result.clone()),
} => *current_result = result.clone().into(),
NightState::Complete => return Err(GameError::NightOver),
};
if let NightChange::Shapeshift { source, .. } = &change {
@ -657,7 +707,7 @@ impl Night {
current_result,
..
} => {
current_result.replace(result.clone());
*current_result = result.clone().into();
}
NightState::Complete => return Err(GameError::NightOver),
};
@ -781,10 +831,17 @@ impl Night {
pub const fn current_result(&self) -> Option<&ActionResult> {
match &self.night_state {
NightState::Active {
current_prompt: _,
current_result,
current_result: CurrentResult::Result(current_result),
..
} => current_result.as_ref(),
} => Some(&current_result),
NightState::Active {
current_result: CurrentResult::GoBackToSleepAfterShown { .. },
..
} => Some(&ActionResult::GoBackToSleep),
NightState::Active {
current_result: CurrentResult::None,
..
} => None,
NightState::Complete => None,
}
}
@ -857,23 +914,55 @@ impl Night {
.collect()
}
#[allow(clippy::should_implement_trait)]
pub fn next(&mut self) -> Result<()> {
match &self.night_state {
NightState::Active {
current_prompt,
current_result: Some(result),
current_result: CurrentResult::Result(ActionResult::Continue),
current_changes,
..
} => {
self.used_actions.push((
current_prompt.clone(),
result.clone(),
ActionResult::Continue,
current_changes.clone(),
));
}
NightState::Active {
current_prompt,
current_result: CurrentResult::Result(ActionResult::GoBackToSleep),
current_changes,
..
} => {
self.used_actions.push((
current_prompt.clone(),
ActionResult::GoBackToSleep,
current_changes.clone(),
));
}
NightState::Active {
current_result: CurrentResult::Result(_),
..
} => {
// needs Continue, not Next
return Err(GameError::AwaitingResponse);
}
NightState::Active {
current_prompt,
current_result: CurrentResult::GoBackToSleepAfterShown { result_with_data },
current_changes,
..
} => {
self.used_actions.push((
current_prompt.clone(),
result_with_data.clone(),
current_changes.clone(),
));
}
NightState::Active {
current_prompt: _,
current_result: None,
current_result: CurrentResult::None,
..
} => return Err(GameError::AwaitingResponse),
NightState::Complete => return Err(GameError::NightOver),
@ -888,7 +977,7 @@ impl Night {
}
self.night_state = NightState::Active {
current_prompt: prompt,
current_result: None,
current_result: CurrentResult::None,
current_changes: Vec::new(),
current_page: 0,
};

View File

@ -3,7 +3,9 @@ use core::num::NonZeroU8;
use crate::{
diedto::DiedTo,
error::GameError,
game::night::{ActionComplete, Night, NightState, ResponseOutcome, changes::NightChange},
game::night::{
ActionComplete, CurrentResult, Night, NightState, ResponseOutcome, changes::NightChange,
},
message::night::{ActionPrompt, ActionResponse, ActionResult},
player::Protection,
role::{AlignmentEq, PreviousGuardianAction, RoleBlock, RoleTitle},
@ -15,13 +17,21 @@ impl Night {
pub(super) fn process(&self, resp: ActionResponse) -> Result<ResponseOutcome> {
let current_prompt = match &self.night_state {
NightState::Active {
current_prompt: _,
current_result: Some(_),
current_result: CurrentResult::GoBackToSleepAfterShown { .. },
..
}
| NightState::Active {
current_result: CurrentResult::Result(ActionResult::GoBackToSleep),
..
} => return Err(GameError::NightNeedsNext),
NightState::Active {
current_prompt,
current_result: None,
current_result: CurrentResult::None,
..
}
| NightState::Active {
current_prompt,
current_result: CurrentResult::Result(_),
..
} => current_prompt,
NightState::Complete => return Err(GameError::NightOver),

View File

@ -875,19 +875,23 @@ fn big_game_test_based_on_story_test() {
game.next().title().seer();
game.mark(game.character_by_player_id(werewolf).character_id());
game.r#continue().seer();
game.r#continue().sleep();
game.next().title().arcanist();
game.mark(game.character_by_player_id(seer).character_id());
game.mark(game.character_by_player_id(werewolf).character_id());
game.r#continue().role_blocked();
game.r#continue().sleep();
game.next().title().adjudicator();
game.mark(game.character_by_player_id(werewolf).character_id());
game.r#continue().adjudicator();
game.r#continue().sleep();
game.next().title().power_seer();
game.mark(game.character_by_player_id(werewolf).character_id());
game.r#continue().power_seer();
game.r#continue().sleep();
game.next_expect_day();
game.mark_for_execution(game.character_by_player_id(dire_wolf).character_id());
@ -908,31 +912,38 @@ fn big_game_test_based_on_story_test() {
game.next().title().seer();
game.mark(game.character_by_player_id(werewolf).character_id());
game.r#continue().seer();
game.r#continue().sleep();
game.next().title().arcanist();
game.mark(game.character_by_player_id(seer).character_id());
game.mark(game.character_by_player_id(werewolf).character_id());
game.r#continue().arcanist();
game.r#continue().sleep();
game.next().title().adjudicator();
game.mark(game.character_by_player_id(seer).character_id());
game.r#continue().adjudicator();
game.r#continue().sleep();
game.next().title().power_seer();
game.mark(game.living_villager().character_id());
game.r#continue().power_seer();
game.r#continue().sleep();
game.next().title().gravedigger();
game.mark(game.character_by_player_id(dire_wolf).character_id());
assert_eq!(game.r#continue().gravedigger(), Some(RoleTitle::DireWolf));
game.r#continue().sleep();
game.next().title().mortician();
game.mark(game.character_by_player_id(dire_wolf).character_id());
assert_eq!(game.r#continue().mortician(), DiedToTitle::Execution);
game.r#continue().sleep();
game.next().title().empath();
game.mark(game.living_villager().character_id());
assert!(!game.r#continue().empath());
game.r#continue().sleep();
game.next().title().maple_wolf();
game.mark(
@ -947,10 +958,12 @@ fn big_game_test_based_on_story_test() {
game.next().title().insomniac();
game.r#continue().insomniac();
game.r#continue().sleep();
game.next().title().beholder();
game.mark(game.character_by_player_id(power_seer).character_id());
game.r#continue().power_seer();
game.r#continue().sleep();
game.next_expect_day();
@ -984,31 +997,38 @@ fn big_game_test_based_on_story_test() {
game.next().title().seer();
game.mark(game.character_by_player_id(werewolf).character_id());
game.r#continue().seer();
game.r#continue().sleep();
game.next().title().arcanist();
game.mark(game.character_by_player_id(insomniac).character_id());
game.mark(game.character_by_player_id(werewolf).character_id());
game.r#continue().arcanist();
game.r#continue().sleep();
game.next().title().adjudicator();
game.mark(game.character_by_player_id(insomniac).character_id());
game.r#continue().adjudicator();
game.r#continue().sleep();
game.next().title().power_seer();
game.mark(game.living_villager().character_id());
game.r#continue().power_seer();
game.r#continue().sleep();
game.next().title().gravedigger();
game.mark(game.character_by_player_id(dire_wolf).character_id());
assert_eq!(game.r#continue().gravedigger(), Some(RoleTitle::DireWolf));
game.r#continue().sleep();
game.next().title().mortician();
game.mark(game.character_by_player_id(dire_wolf).character_id());
assert_eq!(game.r#continue().mortician(), DiedToTitle::Execution);
game.r#continue().sleep();
game.next().title().empath();
game.mark(game.character_by_player_id(scapegoat).character_id());
assert!(game.r#continue().empath());
game.r#continue().sleep();
game.next().title().maple_wolf();
game.r#continue().sleep();
@ -1019,10 +1039,12 @@ fn big_game_test_based_on_story_test() {
game.next().title().insomniac();
game.r#continue().insomniac();
game.r#continue().sleep();
game.next().title().beholder();
game.mark(game.character_by_player_id(power_seer).character_id());
game.r#continue().power_seer();
game.r#continue().sleep();
game.next_expect_day();
game.mark_for_execution(
@ -1049,27 +1071,33 @@ fn big_game_test_based_on_story_test() {
game.next().title().seer();
game.mark(game.character_by_player_id(shapeshifter).character_id());
game.r#continue().seer();
game.r#continue().sleep();
game.next().title().arcanist();
game.mark(game.character_by_player_id(insomniac).character_id());
game.mark(game.character_by_player_id(shapeshifter).character_id());
game.r#continue().arcanist();
game.r#continue().sleep();
game.next().title().adjudicator();
game.mark(game.character_by_player_id(insomniac).character_id());
game.r#continue().adjudicator();
game.r#continue().sleep();
game.next().title().power_seer();
game.mark(game.living_villager().character_id());
game.r#continue().power_seer();
game.r#continue().sleep();
game.next().title().gravedigger();
game.mark(game.character_by_player_id(guardian).character_id());
assert_eq!(game.r#continue().gravedigger(), Some(RoleTitle::Guardian));
game.r#continue().sleep();
game.next().title().mortician();
game.mark(game.character_by_player_id(guardian).character_id());
assert_eq!(game.r#continue().mortician(), DiedToTitle::Wolfpack);
game.r#continue().sleep();
game.next().title().maple_wolf();
game.r#continue().sleep();
@ -1080,10 +1108,12 @@ fn big_game_test_based_on_story_test() {
game.next().title().insomniac();
game.r#continue().insomniac();
game.r#continue().sleep();
game.next().title().beholder();
game.mark(game.character_by_player_id(gravedigger).character_id());
assert_eq!(game.r#continue().gravedigger(), Some(RoleTitle::Guardian));
game.r#continue().sleep();
game.next_expect_day();
game.mark_for_execution(game.character_by_player_id(vindicator).character_id());
@ -1094,23 +1124,28 @@ fn big_game_test_based_on_story_test() {
game.next().title().seer();
game.mark(game.character_by_player_id(insomniac).character_id());
game.r#continue().seer();
game.r#continue().sleep();
game.next().title().arcanist();
game.mark(game.character_by_player_id(insomniac).character_id());
game.mark(game.character_by_player_id(shapeshifter).character_id());
game.r#continue().arcanist();
game.r#continue().sleep();
game.next().title().adjudicator();
game.mark(game.character_by_player_id(shapeshifter).character_id());
game.r#continue().adjudicator();
game.r#continue().sleep();
game.next().title().power_seer();
game.mark(game.character_by_player_id(shapeshifter).character_id());
game.r#continue().power_seer();
game.r#continue().sleep();
game.next().title().gravedigger();
game.mark(game.character_by_player_id(empath).character_id());
assert_eq!(game.r#continue().gravedigger(), Some(RoleTitle::Empath));
game.r#continue().sleep();
game.next().title().mortician();
game.mark(game.character_by_player_id(werewolf).character_id());
@ -1118,6 +1153,7 @@ fn big_game_test_based_on_story_test() {
game.r#continue().mortician(),
DiedToTitle::GuardianProtecting
);
game.r#continue().sleep();
game.next().title().maple_wolf();
game.mark(game.character_by_player_id(hunter).character_id());
@ -1129,6 +1165,7 @@ fn big_game_test_based_on_story_test() {
game.next().title().insomniac();
game.r#continue().insomniac();
game.r#continue().sleep();
game.next().title().beholder();
game.mark(game.character_by_player_id(mortician).character_id());
@ -1136,6 +1173,7 @@ fn big_game_test_based_on_story_test() {
game.r#continue().mortician(),
DiedToTitle::GuardianProtecting
);
game.r#continue().sleep();
game.next_expect_game_over();
}

View File

@ -265,15 +265,18 @@ fn previous_prompt() {
game.next().title().seer();
game.mark(game.character_by_player_id(werewolf).character_id());
game.r#continue().seer();
game.r#continue().sleep();
game.next().title().arcanist();
game.mark(game.character_by_player_id(seer).character_id());
game.mark(game.character_by_player_id(werewolf).character_id());
game.r#continue().role_blocked();
game.r#continue().sleep();
game.next().title().adjudicator();
game.mark(game.character_by_player_id(werewolf).character_id());
game.r#continue().adjudicator();
game.r#continue().sleep();
game.next().title().power_seer();
match game.prev() {
@ -312,12 +315,15 @@ fn previous_prompt() {
resp => panic!("expected arcanist prompt, got {resp:?}"),
}
game.r#continue().role_blocked();
game.r#continue().sleep();
game.next().title().adjudicator();
game.r#continue().adjudicator();
game.r#continue().sleep();
game.next().title().power_seer();
game.mark(game.character_by_player_id(werewolf).character_id());
game.r#continue().power_seer();
game.r#continue().sleep();
game.next_expect_day();
game.mark_for_execution(game.character_by_player_id(dire_wolf).character_id());
@ -338,31 +344,38 @@ fn previous_prompt() {
game.next().title().seer();
game.mark(game.character_by_player_id(werewolf).character_id());
game.r#continue().seer();
game.r#continue().sleep();
game.next().title().arcanist();
game.mark(game.character_by_player_id(seer).character_id());
game.mark(game.character_by_player_id(werewolf).character_id());
game.r#continue().arcanist();
game.r#continue().sleep();
game.next().title().adjudicator();
game.mark(game.character_by_player_id(seer).character_id());
game.r#continue().adjudicator();
game.r#continue().sleep();
game.next().title().power_seer();
game.mark(game.living_villager().character_id());
game.r#continue().power_seer();
game.r#continue().sleep();
game.next().title().gravedigger();
game.mark(game.character_by_player_id(dire_wolf).character_id());
assert_eq!(game.r#continue().gravedigger(), Some(RoleTitle::DireWolf));
game.r#continue().sleep();
game.next().title().mortician();
game.mark(game.character_by_player_id(dire_wolf).character_id());
assert_eq!(game.r#continue().mortician(), DiedToTitle::Execution);
game.r#continue().sleep();
game.next().title().empath();
game.mark(game.living_villager().character_id());
assert!(!game.r#continue().empath());
game.r#continue().sleep();
game.next().title().maple_wolf();
game.mark(
@ -377,10 +390,12 @@ fn previous_prompt() {
game.next().title().insomniac();
game.r#continue().insomniac();
game.r#continue().sleep();
game.next().title().beholder();
game.mark(game.character_by_player_id(power_seer).character_id());
game.r#continue().power_seer();
game.r#continue().sleep();
game.next_expect_day();
}

View File

@ -31,6 +31,7 @@ fn beholding_seer() {
game.next().title().seer();
game.mark(game.character_by_player_id(wolf_player_id).character_id());
game.r#continue().seer().wolves();
game.r#continue().sleep();
game.next_expect_day();
game.execute().title().wolf_pack_kill();
@ -40,10 +41,12 @@ fn beholding_seer() {
game.next().title().seer();
game.mark(game.character_by_player_id(wolf_player_id).character_id());
game.r#continue().seer().wolves();
game.r#continue().sleep();
game.next().title().beholder();
game.mark(game.character_by_player_id(seer_player_id).character_id());
game.r#continue().seer().wolves();
game.r#continue().sleep();
game.next_expect_day();
}

View File

@ -149,6 +149,7 @@ fn executed_doesnt_know() {
game.next().title().seer();
game.mark(villagers.next().unwrap());
assert_eq!(game.r#continue().seer(), Alignment::Village);
game.r#continue().sleep();
game.next_expect_day();
let elder = game.character_by_player_id(elder_player_id);
@ -181,6 +182,7 @@ fn executed_doesnt_know() {
game.next().title().seer();
game.mark(villagers.next().unwrap());
assert_eq!(game.r#continue().seer(), Alignment::Village);
game.r#continue().sleep();
game.next_expect_day();
}
@ -216,6 +218,7 @@ fn elder_executed_knows_no_powers_incl_hunter_activation() {
game.next().title().seer();
game.mark(game.living_villager_excl(seer_player_id).character_id());
assert_eq!(game.r#continue().seer(), Alignment::Village);
game.r#continue().sleep();
game.next_expect_day();
@ -226,6 +229,7 @@ fn elder_executed_knows_no_powers_incl_hunter_activation() {
game.next().title().seer();
game.mark(game.living_villager_excl(seer_player_id).character_id());
assert_eq!(game.r#continue().seer(), Alignment::Village);
game.r#continue().sleep();
game.next().title().hunter();
game.mark(game.character_by_player_id(wolf_player_id).character_id());

View File

@ -34,6 +34,7 @@ fn nothing_on_wolf() {
game.next().title().empath();
game.mark(game.character_by_player_id(wolf_player_id).character_id());
assert_eq!(game.r#continue(), ActionResult::Empath { scapegoat: false });
game.r#continue().sleep();
game.next_expect_day();
@ -74,10 +75,12 @@ fn takes_on_scapegoats_curse() {
.character_id(),
);
game.r#continue().seer().wolves();
game.r#continue().sleep();
game.next().title().seer();
game.mark(game.character_by_player_id(empath_player_id).character_id());
game.r#continue().seer().village();
game.r#continue().sleep();
game.next_expect_day();
@ -91,6 +94,7 @@ fn takes_on_scapegoats_curse() {
.character_id(),
);
assert_eq!(game.r#continue(), ActionResult::Empath { scapegoat: true });
game.r#continue().sleep();
game.next().title().seer();
game.mark(
@ -98,10 +102,12 @@ fn takes_on_scapegoats_curse() {
.character_id(),
);
game.r#continue().seer().wolves();
game.r#continue().sleep();
game.next().title().seer();
game.mark(game.character_by_player_id(empath_player_id).character_id());
game.r#continue().seer().village();
game.r#continue().sleep();
game.next_expect_day();
@ -125,10 +131,12 @@ fn takes_on_scapegoats_curse() {
.character_id(),
);
game.r#continue().seer().village();
game.r#continue().sleep();
game.next().title().seer();
game.mark(game.character_by_player_id(empath_player_id).character_id());
game.r#continue().seer().wolves();
game.r#continue().sleep();
game.next_expect_day();
}

View File

@ -37,12 +37,14 @@ fn sees_visits() {
game.next().title().seer();
game.mark(game.living_villager().character_id());
game.r#continue().seer().village();
game.r#continue().sleep();
game.next().title().arcanist();
let mut villagers = game.villager_character_ids().into_iter();
game.mark(villagers.next().unwrap());
game.mark(villagers.next().unwrap());
assert_eq!(game.r#continue().arcanist(), true);
game.r#continue().sleep();
game.next_expect_day();
@ -56,6 +58,7 @@ fn sees_visits() {
.character_id(),
);
game.r#continue().seer().village();
game.r#continue().sleep();
game.next().title().arcanist();
game.mark(game.character_by_player_id(seer_player_id).character_id());
@ -64,6 +67,7 @@ fn sees_visits() {
.character_id(),
);
assert_eq!(game.r#continue().arcanist(), true);
game.r#continue().sleep();
game.next().title().insomniac();
assert_eq!(

View File

@ -71,6 +71,7 @@ fn redeemed_scapegoat_role_changes() {
.character_id();
game.mark_and_check(wolf_char_id);
assert_eq!(game.r#continue().seer(), Alignment::Wolves);
game.r#continue().sleep();
game.next_expect_day();
game.execute().title().wolf_pack_kill();
@ -88,6 +89,7 @@ fn redeemed_scapegoat_role_changes() {
assert_eq!(game.next().title(), ActionPromptTitle::Seer);
game.mark_and_check(wolf_char_id);
assert_eq!(game.r#continue().seer(), Alignment::Wolves);
game.r#continue().sleep();
game.next_expect_day();
assert_eq!(

View File

@ -4,7 +4,7 @@ use convert_case::{Case, Casing};
use werewolves_proto::message::{
PublicIdentity,
host::{HostGameMessage, HostMessage, HostNightMessage},
night::ActionResult,
night::{ActionResponse, ActionResult},
};
use yew::prelude::*;
@ -34,15 +34,15 @@ pub fn ActionResultView(props: &ActionResultProps) -> Html {
.then(|| html! {<Identity ident={ident.clone()}/>})
});
let on_complete = props.on_complete.clone();
let on_complete = Callback::from(move |_| {
let on_continue = Callback::from(move |_| {
on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
HostNightMessage::Next,
HostNightMessage::ActionResponse(ActionResponse::Continue),
)))
});
let cont = props
.big_screen
.not()
.then(|| html! {<Button on_click={on_complete}>{"continue"}</Button>});
.then(|| html! {<Button on_click={on_continue}>{"continue"}</Button>});
let body = match &props.result {
ActionResult::PowerSeer { powerful } => {
html! {