From 8b894b4c8c005ecaffa5f7b29463e78832614214 Mon Sep 17 00:00:00 2001 From: emilis Date: Fri, 7 Nov 2025 20:51:40 +0000 Subject: [PATCH] apprentice now wakes in role-appropriate night order --- werewolves-proto/src/game/night.rs | 47 +++++++++-- werewolves-proto/src/game_test/mod.rs | 13 +-- .../src/game_test/role/apprentice.rs | 79 +++++++++++++++++++ werewolves-proto/src/game_test/role/mod.rs | 1 + .../src/game_test/role/scapegoat.rs | 25 +++--- 5 files changed, 133 insertions(+), 32 deletions(-) create mode 100644 werewolves-proto/src/game_test/role/apprentice.rs diff --git a/werewolves-proto/src/game/night.rs b/werewolves-proto/src/game/night.rs index ebe8a69..166eafa 100644 --- a/werewolves-proto/src/game/night.rs +++ b/werewolves-proto/src/game/night.rs @@ -195,6 +195,15 @@ impl Default for ActionComplete { } } +fn night_sort_order( + left_prompt: &ActionPrompt, + right_prompt: &ActionPrompt, +) -> core::cmp::Ordering { + left_prompt + .partial_cmp(right_prompt) + .unwrap_or(core::cmp::Ordering::Equal) +} + enum Unless { TargetBlocked(CharacterId), TargetsBlocked(CharacterId, CharacterId), @@ -279,15 +288,14 @@ impl Night { .flatten() .chain(village.wolf_pack_kill()) .collect::>(); - action_queue.sort_by(|left_prompt, right_prompt| { - left_prompt - .partial_cmp(right_prompt) - .unwrap_or(core::cmp::Ordering::Equal) - }); + action_queue.sort_by(night_sort_order); let mut action_queue = VecDeque::from({ // insert actions for role-changed roles let mut expanded_queue = Vec::new(); + let mut role_changes = Vec::new(); + // here we replace the role change prompts with the prompt they *would* have gotten + // (if any). if they wouldn't get a prompt, just add the role change prompt back in for action in action_queue { match &action { ActionPrompt::RoleChange { @@ -296,8 +304,15 @@ impl Night { } => { let char = village.character_by_id(character_id.character_id)?; let as_role = char.as_role(new_role.title_to_role_excl_apprentice()); - expanded_queue.push(action); - for prompt in as_role.night_action_prompts(&village)? { + let prompts = as_role.night_action_prompts(&village)?; + if prompts.is_empty() { + // they wouldn't get a prompt alongside the role change, so just add + // the role change prompt back in + expanded_queue.push(action); + continue; + } + role_changes.push((character_id.character_id, *new_role)); + for prompt in prompts { expanded_queue.push(prompt); } } @@ -307,7 +322,23 @@ impl Night { } } } - expanded_queue + expanded_queue.sort_by(night_sort_order); + let mut expanded_queue_with_role_changes = + Vec::with_capacity(expanded_queue.len() + role_changes.len()); + for prompt in expanded_queue { + if let Some(char) = prompt.character_id() + && let Some((_, role)) = role_changes.iter().find(|(c, _)| *c == char) + { + expanded_queue_with_role_changes.push(ActionPrompt::RoleChange { + character_id: village.character_by_id(char)?.identity(), + new_role: *role, + }); + expanded_queue_with_role_changes.push(prompt); + } else { + expanded_queue_with_role_changes.push(prompt); + } + } + expanded_queue_with_role_changes }); if night == 0 { diff --git a/werewolves-proto/src/game_test/mod.rs b/werewolves-proto/src/game_test/mod.rs index ed88f24..ee99528 100644 --- a/werewolves-proto/src/game_test/mod.rs +++ b/werewolves-proto/src/game_test/mod.rs @@ -699,18 +699,7 @@ fn yes_wolf_kill_n2() { .unwrap(), ServerToHostMessage::ActionResult(None, ActionResult::Continue) ); - - assert!(matches!( - game.process(HostGameMessage::Night(HostNightMessage::Next)) - .unwrap(), - ServerToHostMessage::ActionPrompt( - ActionPrompt::WolfPackKill { - living_villagers: _, - marked: _, - }, - 0 - ) - )); + game.next().title().wolf_pack_kill(); } #[test] diff --git a/werewolves-proto/src/game_test/role/apprentice.rs b/werewolves-proto/src/game_test/role/apprentice.rs new file mode 100644 index 0000000..c00c296 --- /dev/null +++ b/werewolves-proto/src/game_test/role/apprentice.rs @@ -0,0 +1,79 @@ +// 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 . +use core::num::NonZeroU8; +#[allow(unused)] +use pretty_assertions::{assert_eq, assert_ne, assert_str_eq}; + +use crate::{ + diedto::DiedTo, + game::{Game, GameSettings, OrRandom, SetupRole}, + game_test::{ + ActionPromptTitleExt, ActionResultExt, AlignmentExt, GameExt, ServerToHostMessageExt, + SettingsExt, gen_players, + }, + message::{ + host::{HostDayMessage, HostGameMessage}, + night::{ActionPrompt, ActionPromptTitle, ActionResult}, + }, + role::{Role, RoleTitle}, +}; + +#[test] +fn beholder_appropriate_prompt_position() { + let players = gen_players(1..10); + let apprentice = players[0].player_id; + let beholder = players[1].player_id; + let wolf_player_id = players[2].player_id; + + let mut settings = GameSettings::empty(); + settings.add_and_assign( + SetupRole::Apprentice { + to: Some(RoleTitle::Beholder), + }, + apprentice, + ); + settings.add_and_assign(SetupRole::Beholder, beholder); + settings.add_and_assign(SetupRole::Werewolf, wolf_player_id); + + settings.fill_remaining_slots_with_villagers(9); + 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(); + game.mark(game.character_by_player_id(beholder).character_id()); + game.r#continue().sleep(); + + game.next().title().beholder(); + game.mark(game.character_by_player_id(wolf_player_id).character_id()); + game.r#continue().sleep(); + + game.next_expect_day(); + game.execute().title().wolf_pack_kill(); + game.mark(game.living_villager().character_id()); + game.r#continue().sleep(); + + assert_eq!( + game.next(), + ActionPrompt::RoleChange { + character_id: game.character_by_player_id(apprentice).identity(), + new_role: RoleTitle::Beholder + } + ); + game.r#continue().r#continue(); + game.next().title().beholder(); +} diff --git a/werewolves-proto/src/game_test/role/mod.rs b/werewolves-proto/src/game_test/role/mod.rs index 6b928f2..fff05e1 100644 --- a/werewolves-proto/src/game_test/role/mod.rs +++ b/werewolves-proto/src/game_test/role/mod.rs @@ -12,6 +12,7 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +mod apprentice; mod beholder; mod black_knight; mod diseased; diff --git a/werewolves-proto/src/game_test/role/scapegoat.rs b/werewolves-proto/src/game_test/role/scapegoat.rs index ca1a616..3409e77 100644 --- a/werewolves-proto/src/game_test/role/scapegoat.rs +++ b/werewolves-proto/src/game_test/role/scapegoat.rs @@ -118,8 +118,20 @@ fn redeemed_scapegoat_role_changes() { night: NonZero::new(1).unwrap() } ); + + assert_eq!(game.execute().title(), ActionPromptTitle::WolfPackKill); + let wolf_target_2 = game + .village() + .characters() + .iter() + .find(|c| c.player_id() == wolf_target_2_player_id) + .unwrap() + .character_id(); + game.mark_and_check(wolf_target_2); + game.r#continue().sleep(); + assert_eq!( - game.execute(), + game.next(), ActionPrompt::RoleChange { character_id: game.character_by_player_id(scapegoat_player_id).identity(), new_role: RoleTitle::Seer @@ -131,17 +143,6 @@ fn redeemed_scapegoat_role_changes() { assert_eq!(game.r#continue().seer(), Alignment::Wolves); game.r#continue().sleep(); - 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(); - game.mark_and_check(wolf_target_2); - game.r#continue().sleep(); - game.next_expect_day(); let scapegoat = game.character_by_player_id(scapegoat_player_id);