lone wolf role + tests
This commit is contained in:
parent
98493d34be
commit
ae71ea4eb0
|
|
@ -482,6 +482,11 @@ impl Character {
|
|||
living_players: village.living_players_excluding(self.character_id()),
|
||||
marked: None,
|
||||
},
|
||||
Role::LoneWolf => ActionPrompt::LoneWolfKill {
|
||||
character_id: self.identity(),
|
||||
living_players: village.living_players_excluding(self.character_id()),
|
||||
marked: None,
|
||||
},
|
||||
}]))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -57,6 +57,10 @@ pub enum DiedTo {
|
|||
tried_recruiting: CharacterId,
|
||||
night: u8,
|
||||
},
|
||||
LoneWolf {
|
||||
killer: CharacterId,
|
||||
night: u8,
|
||||
},
|
||||
}
|
||||
|
||||
impl DiedTo {
|
||||
|
|
@ -64,6 +68,7 @@ impl DiedTo {
|
|||
let mut next = self.clone();
|
||||
match &mut next {
|
||||
DiedTo::Execution { .. } => return None,
|
||||
|
||||
DiedTo::MapleWolf { night, .. }
|
||||
| DiedTo::MapleWolfStarved { night }
|
||||
| DiedTo::Militia { night, .. }
|
||||
|
|
@ -74,7 +79,10 @@ impl DiedTo {
|
|||
| DiedTo::GuardianProtecting { night, .. }
|
||||
| DiedTo::PyreMasterLynchMob { night, .. }
|
||||
| DiedTo::PyreMaster { night, .. } => *night = NonZeroU8::new(night.get() + 1)?,
|
||||
DiedTo::MasonLeaderRecruitFail { night, .. } => *night = *night + 1,
|
||||
|
||||
DiedTo::LoneWolf { night, .. } | DiedTo::MasonLeaderRecruitFail { night, .. } => {
|
||||
*night = *night + 1
|
||||
}
|
||||
}
|
||||
|
||||
Some(next)
|
||||
|
|
@ -101,7 +109,8 @@ impl DiedTo {
|
|||
..
|
||||
}
|
||||
| DiedTo::PyreMasterLynchMob { source: killer, .. }
|
||||
| DiedTo::PyreMaster { killer, .. } => Some(*killer),
|
||||
| DiedTo::PyreMaster { killer, .. }
|
||||
| DiedTo::LoneWolf { killer, .. } => Some(*killer),
|
||||
}
|
||||
}
|
||||
pub const fn date_time(&self) -> DateTime {
|
||||
|
|
@ -132,7 +141,9 @@ impl DiedTo {
|
|||
| DiedTo::Hunter { killer: _, night } => DateTime::Night {
|
||||
number: night.get(),
|
||||
},
|
||||
DiedTo::MasonLeaderRecruitFail { night, .. } => DateTime::Night { number: *night },
|
||||
DiedTo::LoneWolf { night, .. } | DiedTo::MasonLeaderRecruitFail { night, .. } => {
|
||||
DateTime::Night { number: *night }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,7 +90,11 @@ impl ActionPrompt {
|
|||
..
|
||||
} => Some(Unless::TargetsBlocked(*marked1, *marked2)),
|
||||
|
||||
ActionPrompt::Seer {
|
||||
ActionPrompt::LoneWolfKill {
|
||||
marked: Some(marked),
|
||||
..
|
||||
}
|
||||
| ActionPrompt::Seer {
|
||||
marked: Some(marked),
|
||||
..
|
||||
}
|
||||
|
|
@ -163,7 +167,8 @@ impl ActionPrompt {
|
|||
..
|
||||
} => Some(Unless::TargetBlocked(*marked)),
|
||||
|
||||
ActionPrompt::Seer { marked: None, .. }
|
||||
ActionPrompt::LoneWolfKill { marked: None, .. }
|
||||
| ActionPrompt::Seer { marked: None, .. }
|
||||
| ActionPrompt::Protector { marked: None, .. }
|
||||
| ActionPrompt::Gravedigger { marked: None, .. }
|
||||
| ActionPrompt::Hunter { marked: None, .. }
|
||||
|
|
@ -806,6 +811,21 @@ impl Night {
|
|||
};
|
||||
|
||||
match current_prompt {
|
||||
ActionPrompt::LoneWolfKill {
|
||||
character_id,
|
||||
living_players,
|
||||
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 => {
|
||||
|
|
@ -1166,6 +1186,7 @@ impl Night {
|
|||
marked: (Some(_), None),
|
||||
..
|
||||
}
|
||||
| ActionPrompt::LoneWolfKill { marked: None, .. }
|
||||
| ActionPrompt::Gravedigger { marked: None, .. }
|
||||
| ActionPrompt::Hunter { marked: None, .. }
|
||||
| ActionPrompt::Guardian { marked: None, .. }
|
||||
|
|
@ -1205,7 +1226,8 @@ impl Night {
|
|||
current_prompt,
|
||||
current_result: _,
|
||||
} => match current_prompt {
|
||||
ActionPrompt::ElderReveal { character_id }
|
||||
ActionPrompt::LoneWolfKill { character_id, .. }
|
||||
| ActionPrompt::ElderReveal { character_id }
|
||||
| ActionPrompt::RoleChange { character_id, .. }
|
||||
| ActionPrompt::Seer { character_id, .. }
|
||||
| ActionPrompt::Protector { character_id, .. }
|
||||
|
|
|
|||
|
|
@ -111,6 +111,8 @@ pub enum SetupRole {
|
|||
DireWolf,
|
||||
#[checks(Category::Wolves)]
|
||||
Shapeshifter,
|
||||
#[checks(Category::Wolves)]
|
||||
LoneWolf,
|
||||
|
||||
#[checks(Category::Intel)]
|
||||
Adjudicator,
|
||||
|
|
@ -139,6 +141,7 @@ pub enum SetupRole {
|
|||
impl SetupRoleTitle {
|
||||
pub fn into_role(self) -> Role {
|
||||
match self {
|
||||
SetupRoleTitle::LoneWolf => Role::LoneWolf,
|
||||
SetupRoleTitle::Villager => Role::Villager,
|
||||
SetupRoleTitle::Scapegoat => Role::Scapegoat { redeemed: false },
|
||||
SetupRoleTitle::Seer => Role::Seer,
|
||||
|
|
@ -188,6 +191,7 @@ impl SetupRoleTitle {
|
|||
impl Display for SetupRole {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(match self {
|
||||
SetupRole::LoneWolf => "Lone Wolf",
|
||||
SetupRole::Villager => "Villager",
|
||||
SetupRole::Scapegoat { .. } => "Scapegoat",
|
||||
SetupRole::Seer => "Seer",
|
||||
|
|
@ -222,6 +226,7 @@ impl Display for SetupRole {
|
|||
impl SetupRole {
|
||||
pub fn into_role(self, roles_in_game: &[RoleTitle]) -> Result<Role, GameError> {
|
||||
Ok(match self {
|
||||
SetupRole::LoneWolf => Role::LoneWolf,
|
||||
SetupRole::Villager => Role::Villager,
|
||||
SetupRole::Scapegoat { redeemed } => Role::Scapegoat {
|
||||
redeemed: redeemed.into_concrete(),
|
||||
|
|
@ -284,6 +289,7 @@ impl SetupRole {
|
|||
impl From<SetupRole> for RoleTitle {
|
||||
fn from(value: SetupRole) -> Self {
|
||||
match value {
|
||||
SetupRole::LoneWolf => RoleTitle::LoneWolf,
|
||||
SetupRole::Villager => RoleTitle::Villager,
|
||||
SetupRole::Scapegoat { .. } => RoleTitle::Scapegoat,
|
||||
SetupRole::Seer => RoleTitle::Seer,
|
||||
|
|
@ -318,6 +324,7 @@ impl From<SetupRole> for RoleTitle {
|
|||
impl From<RoleTitle> for SetupRole {
|
||||
fn from(value: RoleTitle) -> Self {
|
||||
match value {
|
||||
RoleTitle::LoneWolf => SetupRole::LoneWolf,
|
||||
RoleTitle::Villager => SetupRole::Villager,
|
||||
RoleTitle::Scapegoat => SetupRole::Scapegoat {
|
||||
redeemed: Default::default(),
|
||||
|
|
|
|||
|
|
@ -286,6 +286,7 @@ impl Village {
|
|||
impl RoleTitle {
|
||||
pub fn title_to_role_excl_apprentice(self) -> Role {
|
||||
match self {
|
||||
RoleTitle::LoneWolf => Role::LoneWolf,
|
||||
RoleTitle::Villager => Role::Villager,
|
||||
RoleTitle::Scapegoat => Role::Scapegoat {
|
||||
redeemed: rand::random(),
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ mod role;
|
|||
use crate::{
|
||||
character::{Character, CharacterId},
|
||||
error::GameError,
|
||||
game::{Game, GameSettings, SetupRole, SetupSlot},
|
||||
game::{Game, GameOver, GameSettings, SetupRole, SetupSlot},
|
||||
message::{
|
||||
CharacterState, Identification, PublicIdentity,
|
||||
host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage},
|
||||
|
|
@ -62,6 +62,7 @@ pub trait ActionPromptTitleExt {
|
|||
fn pyremaster(&self);
|
||||
fn empath(&self);
|
||||
fn adjudicator(&self);
|
||||
fn lone_wolf(&self);
|
||||
}
|
||||
|
||||
impl ActionPromptTitleExt for ActionPromptTitle {
|
||||
|
|
@ -131,6 +132,9 @@ impl ActionPromptTitleExt for ActionPromptTitle {
|
|||
fn empath(&self) {
|
||||
assert_eq!(*self, ActionPromptTitle::Empath)
|
||||
}
|
||||
fn lone_wolf(&self) {
|
||||
assert_eq!(*self, ActionPromptTitle::LoneWolfKill)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ActionResultExt {
|
||||
|
|
@ -227,9 +231,21 @@ pub trait GameExt {
|
|||
fn living_villager(&self) -> Character;
|
||||
#[allow(unused)]
|
||||
fn get_state(&mut self) -> ServerToHostMessage;
|
||||
fn next_expect_game_over(&mut self) -> GameOver;
|
||||
}
|
||||
|
||||
impl GameExt for Game {
|
||||
fn next_expect_game_over(&mut self) -> GameOver {
|
||||
match self
|
||||
.process(HostGameMessage::Night(
|
||||
crate::message::host::HostNightMessage::Next,
|
||||
))
|
||||
.unwrap()
|
||||
{
|
||||
ServerToHostMessage::GameOver(outcome) => outcome,
|
||||
resp => panic!("expected game to be over, got: {resp:?}"),
|
||||
}
|
||||
}
|
||||
fn get_state(&mut self) -> ServerToHostMessage {
|
||||
self.process(HostGameMessage::GetState).unwrap()
|
||||
}
|
||||
|
|
@ -295,7 +311,12 @@ impl GameExt for Game {
|
|||
| ActionPrompt::RoleChange { .. }
|
||||
| ActionPrompt::Shapeshifter { .. } => panic!("expected a prompt with a mark"),
|
||||
ActionPrompt::Arcanist { .. } => panic!("wrong call for arcanist"),
|
||||
ActionPrompt::Seer {
|
||||
|
||||
ActionPrompt::LoneWolfKill {
|
||||
marked: Some(marked),
|
||||
..
|
||||
}
|
||||
| ActionPrompt::Seer {
|
||||
marked: Some(marked),
|
||||
..
|
||||
}
|
||||
|
|
@ -384,7 +405,8 @@ impl GameExt for Game {
|
|||
| ActionPrompt::Guardian { marked: None, .. }
|
||||
| ActionPrompt::WolfPackKill { marked: None, .. }
|
||||
| ActionPrompt::AlphaWolf { marked: None, .. }
|
||||
| ActionPrompt::DireWolf { marked: None, .. } => panic!("no mark"),
|
||||
| ActionPrompt::DireWolf { marked: None, .. }
|
||||
| ActionPrompt::LoneWolfKill { marked: None, .. } => panic!("no mark"),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,87 @@
|
|||
#[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, GameExt, SettingsExt, gen_players},
|
||||
message::night::ActionPromptTitle,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn no_kill_on_executing_wolf_aligned_villager() {
|
||||
let players = gen_players(1..21);
|
||||
let lone_wolf_player_id = players[0].player_id;
|
||||
let wolf_player_id = players[1].player_id;
|
||||
let scapegoat_player_id = players[2].player_id;
|
||||
let mut settings = GameSettings::empty();
|
||||
settings.add_and_assign(SetupRole::LoneWolf, lone_wolf_player_id);
|
||||
settings.add_and_assign(SetupRole::Werewolf, wolf_player_id);
|
||||
settings.add_and_assign(
|
||||
SetupRole::Scapegoat {
|
||||
redeemed: OrRandom::Determined(false),
|
||||
},
|
||||
scapegoat_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.mark_for_execution(
|
||||
game.character_by_player_id(scapegoat_player_id)
|
||||
.character_id(),
|
||||
);
|
||||
|
||||
game.execute().title().wolf_pack_kill();
|
||||
game.mark(game.living_villager().character_id());
|
||||
game.r#continue().sleep();
|
||||
|
||||
game.next_expect_day();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gets_a_kill_after_wolf_execution_and_can_kill_a_wolf() {
|
||||
let players = gen_players(1..21);
|
||||
let lone_wolf_player_id = players[0].player_id;
|
||||
let wolf_player_id = players[1].player_id;
|
||||
let sacrificial_wolf_player_id = players[2].player_id;
|
||||
let mut settings = GameSettings::empty();
|
||||
settings.add_and_assign(SetupRole::LoneWolf, lone_wolf_player_id);
|
||||
settings.add_and_assign(SetupRole::Werewolf, wolf_player_id);
|
||||
settings.add_and_assign(SetupRole::Werewolf, sacrificial_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.mark_for_execution(
|
||||
game.character_by_player_id(sacrificial_wolf_player_id)
|
||||
.character_id(),
|
||||
);
|
||||
|
||||
game.execute().title().wolf_pack_kill();
|
||||
game.mark(game.living_villager().character_id());
|
||||
game.r#continue().sleep();
|
||||
|
||||
game.next().title().lone_wolf();
|
||||
game.mark(game.character_by_player_id(wolf_player_id).character_id());
|
||||
game.r#continue().sleep();
|
||||
|
||||
game.next_expect_day();
|
||||
assert_eq!(
|
||||
game.character_by_player_id(wolf_player_id)
|
||||
.died_to()
|
||||
.cloned(),
|
||||
Some(DiedTo::LoneWolf {
|
||||
killer: game
|
||||
.character_by_player_id(lone_wolf_player_id)
|
||||
.character_id(),
|
||||
night: 1
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ mod black_knight;
|
|||
mod diseased;
|
||||
mod elder;
|
||||
mod empath;
|
||||
mod lone_wolf;
|
||||
mod mason;
|
||||
mod mortician;
|
||||
mod pyremaster;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
|||
|
||||
use crate::{
|
||||
diedto::DiedTo,
|
||||
game::{Game, GameSettings, OrRandom, SetupRole},
|
||||
game::{Game, GameOver, GameSettings, OrRandom, SetupRole},
|
||||
game_test::{
|
||||
ActionPromptTitleExt, ActionResultExt, AlignmentExt, GameExt, ServerToHostMessageExt,
|
||||
SettingsExt, gen_players,
|
||||
|
|
@ -37,11 +37,5 @@ fn mayor_win() {
|
|||
game.mark(game.living_villager().character_id());
|
||||
game.r#continue().sleep();
|
||||
|
||||
assert_eq!(
|
||||
game.process(HostGameMessage::Night(
|
||||
crate::message::host::HostNightMessage::Next
|
||||
))
|
||||
.unwrap(),
|
||||
ServerToHostMessage::GameOver(crate::game::GameOver::VillageWins)
|
||||
);
|
||||
assert_eq!(game.next_expect_game_over(), GameOver::VillageWins);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ pub enum ActionType {
|
|||
WolfPackKill,
|
||||
Direwolf,
|
||||
OtherWolf,
|
||||
LoneWolfKill,
|
||||
Block,
|
||||
Intel,
|
||||
Other,
|
||||
|
|
@ -32,6 +33,7 @@ pub enum ActionType {
|
|||
|
||||
impl ActionType {
|
||||
const fn is_wolfy(&self) -> bool {
|
||||
// note: Lone Wolf isn't wolfy, as they don't wake with wolves
|
||||
matches!(
|
||||
self,
|
||||
ActionType::Direwolf
|
||||
|
|
@ -182,6 +184,12 @@ pub enum ActionPrompt {
|
|||
living_players: Box<[CharacterIdentity]>,
|
||||
marked: Option<CharacterId>,
|
||||
},
|
||||
#[checks(ActionType::LoneWolfKill)]
|
||||
LoneWolfKill {
|
||||
character_id: CharacterIdentity,
|
||||
living_players: Box<[CharacterIdentity]>,
|
||||
marked: Option<CharacterId>,
|
||||
},
|
||||
}
|
||||
|
||||
impl ActionPrompt {
|
||||
|
|
@ -212,7 +220,8 @@ impl ActionPrompt {
|
|||
| ActionPrompt::Empath { .. }
|
||||
| ActionPrompt::MasonsWake { .. }
|
||||
| ActionPrompt::MasonLeaderRecruit { .. }
|
||||
| ActionPrompt::WolfPackKill { .. } => false,
|
||||
| ActionPrompt::WolfPackKill { .. }
|
||||
| ActionPrompt::LoneWolfKill { .. } => false,
|
||||
}
|
||||
}
|
||||
pub(crate) fn with_mark(&self, mark: CharacterId) -> Result<ActionPrompt> {
|
||||
|
|
@ -288,7 +297,12 @@ impl ActionPrompt {
|
|||
Ok(prompt)
|
||||
}
|
||||
|
||||
ActionPrompt::Adjudicator {
|
||||
ActionPrompt::LoneWolfKill {
|
||||
living_players: targets,
|
||||
marked,
|
||||
..
|
||||
}
|
||||
| ActionPrompt::Adjudicator {
|
||||
living_players: targets,
|
||||
marked,
|
||||
..
|
||||
|
|
|
|||
|
|
@ -136,6 +136,11 @@ pub enum Role {
|
|||
#[checks("powerful")]
|
||||
#[checks("wolf")]
|
||||
Shapeshifter { shifted_into: Option<CharacterId> },
|
||||
#[checks(Alignment::Wolves)]
|
||||
#[checks("killer")]
|
||||
#[checks("powerful")]
|
||||
#[checks("wolf")]
|
||||
LoneWolf,
|
||||
}
|
||||
|
||||
impl Role {
|
||||
|
|
@ -156,7 +161,8 @@ impl Role {
|
|||
| Role::Arcanist
|
||||
| Role::Seer => true,
|
||||
|
||||
Role::Shapeshifter { .. }
|
||||
Role::LoneWolf
|
||||
| Role::Shapeshifter { .. }
|
||||
| Role::Werewolf
|
||||
| Role::AlphaWolf { .. }
|
||||
| Role::Elder { .. }
|
||||
|
|
@ -197,6 +203,14 @@ impl Role {
|
|||
| Role::BlackKnight { .. }
|
||||
| Role::Villager => false,
|
||||
|
||||
Role::LoneWolf => match village.date_time() {
|
||||
DateTime::Day { number: _ } => return false,
|
||||
DateTime::Night { number } => NonZeroU8::new(number),
|
||||
}
|
||||
.map(|night| village.executions_on_day(night))
|
||||
.map(|execs| execs.iter().any(|e| e.is_wolf()))
|
||||
.unwrap_or_default(),
|
||||
|
||||
Role::PowerSeer
|
||||
| Role::Mortician
|
||||
| Role::Beholder
|
||||
|
|
|
|||
|
|
@ -166,6 +166,16 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
|
|||
};
|
||||
}
|
||||
|
||||
ActionPrompt::LoneWolfKill {
|
||||
character_id,
|
||||
living_players,
|
||||
marked,
|
||||
} => (
|
||||
Some(character_id),
|
||||
living_players,
|
||||
marked.iter().cloned().collect::<Box<[CharacterId]>>(),
|
||||
html! {{"lone wolf kill"}},
|
||||
),
|
||||
ActionPrompt::Adjudicator {
|
||||
character_id,
|
||||
living_players,
|
||||
|
|
|
|||
Loading…
Reference in New Issue