fix tests for new setup + role change fix

This commit is contained in:
emilis 2025-10-05 01:22:55 +01:00
parent d664ff281d
commit 97e1ca8a39
No known key found for this signature in database
7 changed files with 274 additions and 16 deletions

View File

@ -1,5 +1,5 @@
mod kill; mod kill;
mod night; pub(crate) mod night;
mod settings; mod settings;
mod village; mod village;
@ -21,6 +21,7 @@ use crate::{
}, },
player::CharacterId, player::CharacterId,
}; };
pub use { pub use {
settings::{Category, GameSettings, OrRandom, SetupRole, SetupSlot, SlotId}, settings::{Category, GameSettings, OrRandom, SetupRole, SetupSlot, SlotId},
village::Village, village::Village,

View File

@ -565,7 +565,22 @@ impl Night {
_ => Err(GameError::InvalidMessageForGameState), _ => Err(GameError::InvalidMessageForGameState),
}; };
} }
ActionResponse::Continue => {} ActionResponse::Continue => {
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.clone(),
*new_role,
)),
unless: None,
}));
}
}
}; };
match current_prompt { match current_prompt {

View File

@ -1,18 +1,23 @@
mod night_order; mod night_order;
use crate::{ use crate::{
diedto::DiedTo,
error::GameError, error::GameError,
game::{Game, GameSettings, SetupRole}, game::{Game, GameSettings, OrRandom, SetupRole, night::NightChange},
message::{ message::{
CharacterState, Identification, PublicIdentity, CharacterState, Identification, PublicIdentity,
host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage}, host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage},
night::{ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult}, night::{ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult},
}, },
player::{CharacterId, PlayerId}, player::{CharacterId, PlayerId},
role::RoleTitle, role::{Alignment, Role, RoleTitle},
}; };
use colored::Colorize; use colored::Colorize;
use core::{num::NonZeroU8, ops::Range}; use core::{
char,
num::{NonZero, NonZeroU8},
ops::Range,
};
#[allow(unused)] #[allow(unused)]
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq}; use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
use std::io::Write; use std::io::Write;
@ -20,6 +25,7 @@ use std::io::Write;
trait ActionResultExt { trait ActionResultExt {
fn sleep(&self); fn sleep(&self);
fn r#continue(&self); fn r#continue(&self);
fn seer(&self) -> Alignment;
} }
impl ActionResultExt for ActionResult { impl ActionResultExt for ActionResult {
@ -30,6 +36,13 @@ impl ActionResultExt for ActionResult {
fn r#continue(&self) { fn r#continue(&self) {
assert_eq!(*self, ActionResult::Continue) assert_eq!(*self, ActionResult::Continue)
} }
fn seer(&self) -> Alignment {
match self {
ActionResult::Seer(a) => a.clone(),
_ => panic!("expected a seer result"),
}
}
} }
trait ServerToHostMessageExt { trait ServerToHostMessageExt {
@ -210,7 +223,10 @@ fn gen_players(range: Range<u8>) -> Box<[Identification]> {
#[test] #[test]
fn starts_with_wolf_intro() { fn starts_with_wolf_intro() {
let players = gen_players(1..10); let players = gen_players(1..10);
let settings = GameSettings::default(); let mut settings = GameSettings::default();
for _ in 0..8 {
settings.new_slot(RoleTitle::Villager);
}
let mut game = Game::new(&players, settings).unwrap(); let mut game = Game::new(&players, settings).unwrap();
let resp = game.process(HostGameMessage::GetState).unwrap(); let resp = game.process(HostGameMessage::GetState).unwrap();
assert_eq!( assert_eq!(
@ -225,6 +241,9 @@ fn no_wolf_kill_n1() {
let mut settings = GameSettings::default(); let mut settings = GameSettings::default();
settings.new_slot(RoleTitle::Shapeshifter); settings.new_slot(RoleTitle::Shapeshifter);
settings.new_slot(RoleTitle::Protector); settings.new_slot(RoleTitle::Protector);
for _ in 0..7 {
settings.new_slot(RoleTitle::Villager);
}
if let Some(slot) = settings if let Some(slot) = settings
.slots() .slots()
.iter() .iter()
@ -266,7 +285,10 @@ fn no_wolf_kill_n1() {
#[test] #[test]
fn yes_wolf_kill_n2() { fn yes_wolf_kill_n2() {
let players = gen_players(1..10); let players = gen_players(1..10);
let settings = GameSettings::default(); let mut settings = GameSettings::default();
for _ in 0..8 {
settings.new_slot(RoleTitle::Villager);
}
let mut game = Game::new(&players, settings).unwrap(); let mut game = Game::new(&players, settings).unwrap();
assert_eq!( assert_eq!(
game.process(HostGameMessage::Night(HostNightMessage::ActionResponse( game.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
@ -350,6 +372,9 @@ fn protect_stops_shapeshift() {
let mut settings = GameSettings::default(); let mut settings = GameSettings::default();
settings.new_slot(RoleTitle::Shapeshifter); settings.new_slot(RoleTitle::Shapeshifter);
settings.new_slot(RoleTitle::Protector); settings.new_slot(RoleTitle::Protector);
for _ in 0..7 {
settings.new_slot(RoleTitle::Villager);
}
if let Some(slot) = settings if let Some(slot) = settings
.slots() .slots()
.iter() .iter()
@ -496,6 +521,9 @@ fn wolfpack_kill_all_targets_valid() {
init_log(); init_log();
let players = gen_players(1..10); let players = gen_players(1..10);
let mut settings = GameSettings::default(); let mut settings = GameSettings::default();
for _ in 0..8 {
settings.new_slot(RoleTitle::Villager);
}
settings.new_slot(RoleTitle::Shapeshifter); settings.new_slot(RoleTitle::Shapeshifter);
if let Some(slot) = settings if let Some(slot) = settings
.slots() .slots()
@ -569,6 +597,9 @@ fn only_1_shapeshift_prompt_if_first_shifts() {
let mut settings = GameSettings::default(); let mut settings = GameSettings::default();
settings.new_slot(RoleTitle::Shapeshifter); settings.new_slot(RoleTitle::Shapeshifter);
settings.new_slot(RoleTitle::Shapeshifter); settings.new_slot(RoleTitle::Shapeshifter);
for _ in 0..7 {
settings.new_slot(RoleTitle::Villager);
}
if let Some(slot) = settings if let Some(slot) = settings
.slots() .slots()
.iter() .iter()
@ -614,3 +645,173 @@ fn only_1_shapeshift_prompt_if_first_shifts() {
game.next_expect_day(); game.next_expect_day();
} }
#[test]
fn redeemed_scapegoat_role_changes() {
let players = gen_players(1..10);
let scapegoat_player_id = players[0].player_id.clone();
let seer_player_id = players[1].player_id.clone();
let wolf_player_id = players[2].player_id.clone();
let wolf_target_2_player_id = players[3].player_id.clone();
let mut settings = GameSettings::default();
{
let scapegoat_slot = settings.new_slot(RoleTitle::Scapegoat);
let mut scapegoat_slot = settings
.slots()
.iter()
.find(|s| s.slot_id == scapegoat_slot)
.unwrap()
.clone();
scapegoat_slot.role = SetupRole::Scapegoat {
redeemed: OrRandom::Determined(true),
};
scapegoat_slot.assign_to = Some(scapegoat_player_id.clone());
settings.update_slot(scapegoat_slot);
}
{
let mut slot = settings
.slots()
.iter()
.find(|s| matches!(s.role, SetupRole::Werewolf))
.unwrap()
.clone();
slot.assign_to = Some(wolf_player_id.clone());
settings.update_slot(slot);
}
{
let slot = settings.new_slot(RoleTitle::Seer);
let mut slot = settings
.slots()
.iter()
.find(|s| s.slot_id == slot)
.unwrap()
.clone();
slot.assign_to = Some(seer_player_id.clone());
settings.update_slot(slot);
}
for _ in 0..6 {
settings.new_slot(RoleTitle::Villager);
}
let mut game = Game::new(&players, settings).unwrap();
game.r#continue().r#continue();
assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro);
game.r#continue().sleep();
assert_eq!(game.next().title(), ActionPromptTitle::Seer);
let wolf_char_id = game
.village()
.characters()
.into_iter()
.find(|c| c.player_id() == &wolf_player_id)
.unwrap()
.character_id()
.clone();
game.mark_and_check(&wolf_char_id, |p| match p {
ActionPrompt::Seer {
marked: Some(marked),
..
} => marked == &wolf_char_id,
_ => false,
});
assert_eq!(game.r#continue().seer(), Alignment::Wolves);
game.next_expect_day();
assert_eq!(game.execute().title(), ActionPromptTitle::CoverOfDarkness);
game.r#continue().r#continue();
assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill);
let seer = game
.village()
.characters()
.into_iter()
.find(|c| c.player_id() == &seer_player_id)
.unwrap()
.character_id()
.clone();
game.mark_and_check(&seer, |p| match p {
ActionPrompt::WolfPackKill {
marked: Some(t), ..
} => *t == seer,
_ => false,
});
game.r#continue().sleep();
assert_eq!(game.next().title(), ActionPromptTitle::Seer);
game.mark_and_check(&wolf_char_id, |p| match p {
ActionPrompt::Seer {
marked: Some(marked),
..
} => marked == &wolf_char_id,
_ => false,
});
assert_eq!(game.r#continue().seer(), Alignment::Wolves);
game.next_expect_day();
assert_eq!(
*game
.village()
.character_by_id(&seer)
.unwrap()
.died_to()
.unwrap(),
DiedTo::Wolfpack {
killing_wolf: wolf_char_id.clone(),
night: NonZero::new(1).unwrap()
}
);
assert_eq!(game.execute().title(), ActionPromptTitle::CoverOfDarkness);
game.r#continue().r#continue();
assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill);
let wolf_target_2 = game
.village()
.characters()
.iter()
.find(|c| c.player_id() == &wolf_target_2_player_id)
.unwrap()
.character_id()
.clone();
game.mark_and_check(&wolf_target_2, |r| match r {
ActionPrompt::WolfPackKill {
marked: Some(marked),
..
} => marked == &wolf_target_2,
_ => false,
});
game.r#continue().sleep();
let scapegoat = game
.village()
.characters()
.into_iter()
.find(|c| c.player_id() == &scapegoat_player_id)
.unwrap()
.clone();
assert_eq!(
game.next(),
ActionPrompt::RoleChange {
character_id: scapegoat.identity(),
new_role: RoleTitle::Seer
}
);
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);
}

View File

@ -121,7 +121,7 @@ pub enum ActionPrompt {
} }
impl ActionPrompt { impl ActionPrompt {
pub fn with_mark(&self, mark: CharacterId) -> Result<ActionPrompt> { pub(crate) fn with_mark(&self, mark: CharacterId) -> Result<ActionPrompt> {
let mut prompt = self.clone(); let mut prompt = self.clone();
match &mut prompt { match &mut prompt {
ActionPrompt::WolvesIntro { .. } ActionPrompt::WolvesIntro { .. }

View File

@ -1075,4 +1075,6 @@ input {
flex-wrap: wrap; flex-wrap: wrap;
justify-content: space-around; justify-content: space-around;
row-gap: 10px; row-gap: 10px;
font-size: 2em;
} }

View File

@ -473,10 +473,36 @@ impl Component for Host {
HostMessage::InGame(HostGameMessage::PreviousState), HostMessage::InGame(HostGameMessage::PreviousState),
self.send.clone(), self.send.clone(),
); );
let client_click = Callback::from(|_| {
if let Some(loc) = gloo::utils::document().location() {
let _ = loc.replace("/");
}
});
let screen = if self.big_screen {
let to_normal = Callback::from(|_| {
if let Some(loc) = gloo::utils::document().location() {
let _ = loc.replace("/host");
}
});
html! {
<Button on_click={to_normal}>{"small screen"}</Button>
}
} else {
let to_big = Callback::from(|_| {
if let Some(loc) = gloo::utils::document().location() {
let _ = loc.replace("/host/big");
}
});
html! {
<Button on_click={to_big}>{"big screen 📺"}</Button>
}
};
html! { html! {
<nav class="debug-nav" style="z-index: 3;"> <nav class="debug-nav" style="z-index: 3;">
<Button on_click={on_error_click}>{"error"}</Button> <Button on_click={on_error_click}>{"error"}</Button>
<Button on_click={on_prev_click}>{"previous"}</Button> <Button on_click={on_prev_click}>{"previous"}</Button>
{screen}
<Button on_click={client_click}>{"client"}</Button>
</nav> </nav>
} }
}); });

View File

@ -140,6 +140,7 @@ pub fn ClientNav(ClientNavProps { message_callback }: &ClientNavProps) -> Html {
</ClickableTextEdit> </ClickableTextEdit>
} }
}; };
let debug = option_env!("DEBUG").map(|_| {
let cb = message_callback.clone(); let cb = message_callback.clone();
let forgor = move |_| { let forgor = move |_| {
PlayerId::delete(); PlayerId::delete();
@ -147,12 +148,24 @@ pub fn ClientNav(ClientNavProps { message_callback }: &ClientNavProps) -> Html {
cb.emit(ClientMessage::Goodbye); cb.emit(ClientMessage::Goodbye);
let _ = gloo::utils::window().location().reload(); let _ = gloo::utils::window().location().reload();
}; };
let host_click = Callback::from(|_| {
if let Some(loc) = gloo::utils::document().location() {
let _ = loc.replace("/host");
}
});
html! {
<>
<Button on_click={host_click}>{"host"}</Button>
<Button on_click={forgor}>{"forgor 💀"}</Button>
</>
}
});
html! { html! {
<nav class="client-nav"> <nav class="client-nav">
{number} {number}
{name} {name}
{pronouns} {pronouns}
<Button on_click={forgor}>{"forgor 💀"}</Button> {debug}
</nav> </nav>
} }
} }