view roles during nighttime

This commit is contained in:
emilis 2025-11-18 10:47:38 +00:00
parent 7a50c3320a
commit ec29c66e4b
No known key found for this signature in database
7 changed files with 117 additions and 1067 deletions

View File

@ -83,6 +83,21 @@ impl Game {
pub fn process(&mut self, message: HostGameMessage) -> Result<ServerToHostMessage> {
match (&mut self.state, message) {
(GameState::Night { night }, HostGameMessage::SeePlayersWithRoles) => {
Ok(ServerToHostMessage::PlayerStates(
night
.village()
.characters()
.into_iter()
.map(|c| CharacterState {
player_id: c.player_id(),
identity: c.identity(),
role: c.role_title(),
died_to: c.died_to().cloned(),
})
.collect(),
))
}
(GameState::Night { night }, HostGameMessage::Night(HostNightMessage::NextPage)) => {
night.next_page();
self.process(HostGameMessage::GetState)
@ -241,6 +256,7 @@ impl Game {
night.previous_state()?;
self.process(HostGameMessage::GetState)
}
(_, HostGameMessage::SeePlayersWithRoles) => self.process(HostGameMessage::GetState),
}
}

View File

@ -51,6 +51,7 @@ pub enum HostGameMessage {
Day(HostDayMessage),
Night(HostNightMessage),
PreviousState,
SeePlayersWithRoles,
GetState,
}
@ -94,6 +95,7 @@ pub enum ServerToHostMessage {
day: NonZeroU8,
settings: GameSettings,
},
PlayerStates(Box<[CharacterState]>),
ActionPrompt(ActionPrompt, usize),
ActionResult(Option<CharacterIdentity>, ActionResult),
Lobby(Box<[PlayerState]>),

View File

@ -43,7 +43,7 @@ use crate::{
components::{
Button, Lobby, LobbyPlayerAction, RoleReveal, Settings, Story, Victory,
action::{ActionResultView, Prompt},
host::{DaytimePlayerList, Setup},
host::{CharacterStatesReadOnly, DaytimePlayerList, Setup},
},
pages::RolePage,
storage::StorageKey,
@ -197,6 +197,7 @@ pub enum HostEvent {
SetState(HostState),
Continue,
PlayerList(Box<[PlayerState]>),
CharacterList(Box<[CharacterState]>),
Settings(GameSettings),
Error(GameError),
QrMode(bool),
@ -228,11 +229,13 @@ pub enum HostState {
#[allow(unused)]
page: usize,
},
CharacterStates(Box<[CharacterState]>),
}
impl From<ServerToHostMessage> for HostEvent {
fn from(msg: ServerToHostMessage) -> Self {
match msg {
ServerToHostMessage::PlayerStates(states) => HostEvent::CharacterList(states),
ServerToHostMessage::QrMode(mode) => HostEvent::QrMode(mode),
ServerToHostMessage::Disconnect => HostEvent::SetState(HostState::Disconnected),
ServerToHostMessage::Daytime {
@ -313,6 +316,11 @@ impl Component for Host {
fn view(&self, _ctx: &Context<Self>) -> Html {
log::trace!("state: {:?}", self.state);
let content = match self.state.clone() {
HostState::CharacterStates(chars) => {
html! {
<CharacterStatesReadOnly states={chars}/>
}
}
HostState::Story { story, .. } => {
if let Some(outcome) = story
.final_village()
@ -459,55 +467,38 @@ impl Component for Host {
}
};
let debug_nav = self.debug.then(|| {
let on_error_click = callback::send_message(
HostMessage::Echo(ServerToHostMessage::Error(GameError::NoApprenticeMentor)),
self.send.clone(),
);
let client_click = Callback::from(|_| {
if let Some(loc) = gloo::utils::document().location() {
let _ = loc.replace("/");
}
});
let screen = self.big_screen.then_some({
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>
}
});
let story_on_click = if let HostState::Story { .. } = &self.state {
crate::callback::send_message(HostMessage::GetState, self.send.clone())
} else {
let s = _ctx.link().clone();
Callback::from(move |_| {
s.send_message(HostEvent::SetState(HostState::Story {
story: crate::clients::host::story_test::test_story(),
page: 0,
}));
})
};
html! {
<>
<Button on_click={on_error_click}>{"error"}</Button>
{screen}
<Button on_click={client_click}>{"client"}</Button>
<a href="/host/test"><Button on_click={|_|()}>{"test screens"}</Button></a>
<Button on_click={story_on_click}>{"story"}</Button>
</>
<a href="/host/test"><Button on_click={|_|()}>{"test screens"}</Button></a>
}
});
let on_prev_click = callback::send_message(
HostMessage::InGame(HostGameMessage::PreviousState),
self.send.clone(),
);
let view_roles_btn = match &self.state {
HostState::Prompt(_, _) | HostState::Result(_, _) => {
let on_view_click = crate::callback::send_message(
HostMessage::InGame(HostGameMessage::SeePlayersWithRoles),
self.send.clone(),
);
Some(html! {
<Button on_click={on_view_click}>{"view players"}</Button>
})
}
HostState::CharacterStates(_) => {
let back = crate::callback::send_message(HostMessage::GetState, self.send.clone());
Some(html! {
<Button on_click={back}>{"back"}</Button>
})
}
_ => None,
};
let nav = self.big_screen.not().then(|| {
html! {
<nav class="host-nav" style="z-index: 3;">
<Button on_click={on_prev_click}>{"previous"}</Button>
{view_roles_btn}
{debug_nav}
</nav>
}
@ -525,6 +516,13 @@ impl Component for Host {
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
log::debug!("update: {msg:?}, current: {:?}", self.state);
match msg {
HostEvent::CharacterList(char) => {
if self.big_screen {
return false;
}
self.state = HostState::CharacterStates(char);
true
}
HostEvent::QrMode(mode) => {
self.qr_mode = mode;
true
@ -563,7 +561,8 @@ impl Component for Host {
});
}
HostState::Prompt(_, _)
HostState::CharacterStates(_)
| HostState::Prompt(_, _)
| HostState::Result(_, _)
| HostState::RoleReveal { .. }
| HostState::Day { .. } => {
@ -588,7 +587,8 @@ impl Component for Host {
*s = settings;
true
}
HostState::Story { .. }
HostState::CharacterStates(_)
| HostState::Story { .. }
| HostState::Prompt(_, _)
| HostState::Result(_, _)
| HostState::Disconnected

View File

@ -1,999 +0,0 @@
// 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 <https://www.gnu.org/licenses/>.
use core::num::NonZeroU8;
use werewolves_proto::{
character::{Character, CharacterId},
diedto::DiedToTitle,
game::{self, Game, GameOver, GameSettings, OrRandom, SetupRole, story::GameStory},
message::{
CharacterState, Identification, PublicIdentity,
host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage},
night::{ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult, Visits},
},
player::PlayerId,
role::{Alignment, AlignmentEq, Killer, Powerful, RoleTitle},
};
trait GameSettingsExt {
fn add_and_assign(&mut self, role: SetupRole, player_id: PlayerId);
}
impl GameSettingsExt for GameSettings {
fn add_and_assign(&mut self, role: SetupRole, player_id: PlayerId) {
let slot_id = self.new_slot(role.clone().into());
let mut slot = self.get_slot_by_id(slot_id).unwrap().clone();
slot.role = role;
slot.assign_to.replace(player_id);
self.update_slot(slot);
}
}
pub fn test_story() -> GameStory {
let players = (1..32u8)
.filter_map(NonZeroU8::new)
.map(|n| Identification {
player_id: PlayerId::from_u128(n.get() as _),
public: PublicIdentity {
name: format!("Player {n}"),
pronouns: Some("he/him".into()),
number: Some(n),
},
})
.collect::<Box<[_]>>();
let mut players_iter = players.iter().map(|p| p.player_id);
let (
werewolf,
dire_wolf,
shapeshifter,
alpha_wolf,
seer,
arcanist,
maple_wolf,
guardian,
vindicator,
adjudicator,
power_seer,
beholder,
gravedigger,
mortician,
insomniac,
empath,
scapegoat,
hunter,
diseased,
) = (
(SetupRole::Werewolf, players_iter.next().unwrap()),
(SetupRole::DireWolf, players_iter.next().unwrap()),
(SetupRole::Shapeshifter, players_iter.next().unwrap()),
(SetupRole::AlphaWolf, players_iter.next().unwrap()),
(SetupRole::Seer, players_iter.next().unwrap()),
(SetupRole::Arcanist, players_iter.next().unwrap()),
(SetupRole::MapleWolf, players_iter.next().unwrap()),
(SetupRole::Guardian, players_iter.next().unwrap()),
(SetupRole::Vindicator, players_iter.next().unwrap()),
(SetupRole::Adjudicator, players_iter.next().unwrap()),
(SetupRole::PowerSeer, players_iter.next().unwrap()),
(SetupRole::Beholder, players_iter.next().unwrap()),
(SetupRole::Gravedigger, players_iter.next().unwrap()),
(SetupRole::Mortician, players_iter.next().unwrap()),
(SetupRole::Insomniac, players_iter.next().unwrap()),
(SetupRole::Empath, players_iter.next().unwrap()),
(
SetupRole::Scapegoat {
redeemed: OrRandom::Determined(false),
},
players_iter.next().unwrap(),
),
(SetupRole::Hunter, players_iter.next().unwrap()),
(SetupRole::Diseased, players_iter.next().unwrap()),
);
let mut settings = GameSettings::empty();
settings.add_and_assign(werewolf.0, werewolf.1);
settings.add_and_assign(dire_wolf.0, dire_wolf.1);
settings.add_and_assign(shapeshifter.0, shapeshifter.1);
settings.add_and_assign(alpha_wolf.0, alpha_wolf.1);
settings.add_and_assign(seer.0, seer.1);
settings.add_and_assign(arcanist.0, arcanist.1);
settings.add_and_assign(maple_wolf.0, maple_wolf.1);
settings.add_and_assign(guardian.0, guardian.1);
settings.add_and_assign(vindicator.0, vindicator.1);
settings.add_and_assign(adjudicator.0, adjudicator.1);
settings.add_and_assign(power_seer.0, power_seer.1);
settings.add_and_assign(beholder.0, beholder.1);
settings.add_and_assign(gravedigger.0, gravedigger.1);
settings.add_and_assign(mortician.0, mortician.1);
settings.add_and_assign(insomniac.0, insomniac.1);
settings.add_and_assign(empath.0, empath.1);
settings.add_and_assign(scapegoat.0, scapegoat.1);
settings.add_and_assign(hunter.0, hunter.1);
settings.add_and_assign(diseased.0, diseased.1);
settings.fill_remaining_slots_with_villagers(players.len());
#[allow(unused)]
let (
werewolf,
dire_wolf,
shapeshifter,
alpha_wolf,
seer,
arcanist,
maple_wolf,
guardian,
vindicator,
adjudicator,
power_seer,
beholder,
gravedigger,
mortician,
insomniac,
empath,
scapegoat,
hunter,
) = (
werewolf.1,
dire_wolf.1,
shapeshifter.1,
alpha_wolf.1,
seer.1,
arcanist.1,
maple_wolf.1,
guardian.1,
vindicator.1,
adjudicator.1,
power_seer.1,
beholder.1,
gravedigger.1,
mortician.1,
insomniac.1,
empath.1,
scapegoat.1,
hunter.1,
);
// let village = Village::new(&players, settings).unwrap();
let mut game = game::Game::new(&players, settings).unwrap();
game.r#continue().r#continue();
game.next().title().wolves_intro();
game.r#continue().r#continue();
game.next().title().direwolf();
game.mark(game.character_by_player_id(seer).character_id());
game.r#continue().sleep();
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());
game.mark_for_execution(game.character_by_player_id(alpha_wolf).character_id());
game.execute().title().guardian();
let protect = game.living_villager();
game.mark(protect.character_id());
game.r#continue().sleep();
game.next().title().wolf_pack_kill();
game.mark(protect.character_id());
game.r#continue().r#continue();
game.next().title().shapeshifter();
game.response(ActionResponse::Shapeshift).sleep();
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(
game.living_villager_excl(protect.player_id())
.character_id(),
);
game.r#continue().sleep();
game.next().title().hunter();
game.mark(game.character_by_player_id(insomniac).character_id());
game.r#continue().sleep();
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();
assert_eq!(
game.character_by_player_id(protect.player_id()).died_to(),
None
);
game.mark_for_execution(
game.living_villager_excl(protect.player_id())
.character_id(),
);
game.execute().title().guardian();
game.mark(protect.character_id());
game.r#continue().sleep();
game.next().title().vindicator();
game.mark(
game.living_villager_excl(protect.player_id())
.character_id(),
);
game.r#continue().sleep();
game.next().title().wolf_pack_kill();
game.mark(protect.character_id());
game.r#continue().r#continue();
game.next().title().shapeshifter();
game.r#continue().sleep();
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();
game.next().title().hunter();
game.mark(game.character_by_player_id(insomniac).character_id());
game.r#continue().sleep();
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(
game.living_villager_excl(protect.player_id())
.character_id(),
);
game.execute().title().vindicator();
game.mark(game.character_by_player_id(shapeshifter).character_id());
game.r#continue().sleep();
game.next().title().wolf_pack_kill();
game.mark(game.character_by_player_id(empath).character_id());
game.r#continue().r#continue();
game.next().title().shapeshifter();
game.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
ActionResponse::Shapeshift,
)))
.expect("shapeshift");
// game.r#continue().r#continue();
assert_eq!(
game.next(),
ActionPrompt::RoleChange {
character_id: game.character_by_player_id(empath).identity(),
new_role: RoleTitle::Werewolf
}
);
game.r#continue().sleep();
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();
game.next().title().hunter();
game.mark(game.character_by_player_id(insomniac).character_id());
game.r#continue().sleep();
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());
game.execute().title().wolf_pack_kill();
game.mark(game.living_villager().character_id());
game.r#continue().sleep();
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(empath).character_id());
game.r#continue().arcanist();
game.r#continue().sleep();
game.next().title().adjudicator();
game.mark(game.character_by_player_id(empath).character_id());
game.r#continue().adjudicator();
game.r#continue().sleep();
game.next().title().power_seer();
game.mark(game.character_by_player_id(empath).character_id());
game.r#continue().power_seer();
game.r#continue().sleep();
game.next().title().gravedigger();
game.mark(game.character_by_player_id(shapeshifter).character_id());
assert_eq!(game.r#continue().gravedigger(), None);
game.r#continue().sleep();
game.next().title().mortician();
game.mark(game.character_by_player_id(werewolf).character_id());
assert_eq!(
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());
game.r#continue().sleep();
game.next().title().hunter();
game.mark(game.character_by_player_id(empath).character_id());
game.r#continue().sleep();
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());
assert_eq!(
game.r#continue().mortician(),
DiedToTitle::GuardianProtecting
);
game.r#continue().sleep();
game.story()
}
#[allow(unused)]
pub trait ActionPromptTitleExt {
fn wolf_pack_kill(&self);
fn cover_of_darkness(&self);
fn wolves_intro(&self);
fn role_change(&self);
fn seer(&self);
fn protector(&self);
fn arcanist(&self);
fn gravedigger(&self);
fn hunter(&self);
fn militia(&self);
fn maple_wolf(&self);
fn guardian(&self);
fn shapeshifter(&self);
fn alphawolf(&self);
fn direwolf(&self);
fn masons_wake(&self);
fn masons_leader_recruit(&self);
fn beholder(&self);
fn vindicator(&self);
fn pyremaster(&self);
fn empath(&self);
fn adjudicator(&self);
fn lone_wolf(&self);
fn insomniac(&self);
fn power_seer(&self);
fn mortician(&self);
}
impl ActionPromptTitleExt for ActionPromptTitle {
fn mortician(&self) {
assert_eq!(*self, ActionPromptTitle::Mortician);
}
fn cover_of_darkness(&self) {
assert_eq!(*self, ActionPromptTitle::CoverOfDarkness);
}
fn wolves_intro(&self) {
assert_eq!(*self, ActionPromptTitle::WolvesIntro);
}
fn role_change(&self) {
assert_eq!(*self, ActionPromptTitle::RoleChange);
}
fn seer(&self) {
assert_eq!(*self, ActionPromptTitle::Seer);
}
fn protector(&self) {
assert_eq!(*self, ActionPromptTitle::Protector);
}
fn arcanist(&self) {
assert_eq!(*self, ActionPromptTitle::Arcanist);
}
fn gravedigger(&self) {
assert_eq!(*self, ActionPromptTitle::Gravedigger);
}
fn hunter(&self) {
assert_eq!(*self, ActionPromptTitle::Hunter);
}
fn militia(&self) {
assert_eq!(*self, ActionPromptTitle::Militia);
}
fn maple_wolf(&self) {
assert_eq!(*self, ActionPromptTitle::MapleWolf);
}
fn guardian(&self) {
assert_eq!(*self, ActionPromptTitle::Guardian);
}
fn shapeshifter(&self) {
assert_eq!(*self, ActionPromptTitle::Shapeshifter);
}
fn alphawolf(&self) {
assert_eq!(*self, ActionPromptTitle::AlphaWolf);
}
fn direwolf(&self) {
assert_eq!(*self, ActionPromptTitle::DireWolf);
}
fn wolf_pack_kill(&self) {
assert_eq!(*self, ActionPromptTitle::WolfPackKill);
}
fn masons_wake(&self) {
assert_eq!(*self, ActionPromptTitle::MasonsWake)
}
fn masons_leader_recruit(&self) {
assert_eq!(*self, ActionPromptTitle::MasonLeaderRecruit)
}
fn beholder(&self) {
assert_eq!(*self, ActionPromptTitle::Beholder)
}
fn vindicator(&self) {
assert_eq!(*self, ActionPromptTitle::Vindicator)
}
fn pyremaster(&self) {
assert_eq!(*self, ActionPromptTitle::PyreMaster)
}
fn adjudicator(&self) {
assert_eq!(*self, ActionPromptTitle::Adjudicator)
}
fn empath(&self) {
assert_eq!(*self, ActionPromptTitle::Empath)
}
fn lone_wolf(&self) {
assert_eq!(*self, ActionPromptTitle::LoneWolfKill)
}
fn insomniac(&self) {
assert_eq!(*self, ActionPromptTitle::Insomniac)
}
fn power_seer(&self) {
assert_eq!(*self, ActionPromptTitle::PowerSeer)
}
}
#[allow(unused)]
pub trait ActionResultExt {
fn sleep(&self);
fn r#continue(&self);
fn seer(&self) -> Alignment;
fn insomniac(&self) -> Visits;
fn arcanist(&self) -> AlignmentEq;
fn adjudicator(&self) -> Killer;
fn role_blocked(&self);
fn gravedigger(&self) -> Option<RoleTitle>;
fn power_seer(&self) -> Powerful;
fn mortician(&self) -> DiedToTitle;
fn empath(&self) -> bool;
}
impl ActionResultExt for ActionResult {
fn empath(&self) -> bool {
match self {
Self::Empath { scapegoat } => *scapegoat,
resp => panic!("expected empath, got {resp:?}"),
}
}
fn mortician(&self) -> DiedToTitle {
match self {
Self::Mortician(role) => *role,
resp => panic!("expected mortician, got {resp:?}"),
}
}
fn gravedigger(&self) -> Option<RoleTitle> {
match self {
Self::GraveDigger(role) => *role,
resp => panic!("expected gravedigger, got {resp:?}"),
}
}
fn adjudicator(&self) -> Killer {
match self {
Self::Adjudicator { killer } => *killer,
resp => panic!("expected adjudicator, got {resp:?}"),
}
}
fn power_seer(&self) -> Powerful {
match self {
Self::PowerSeer { powerful } => *powerful,
resp => panic!("expected power seer, got {resp:?}"),
}
}
fn sleep(&self) {
assert_eq!(*self, ActionResult::GoBackToSleep)
}
fn role_blocked(&self) {
assert_eq!(*self, ActionResult::RoleBlocked)
}
fn r#continue(&self) {
assert_eq!(*self, ActionResult::Continue)
}
fn seer(&self) -> Alignment {
match self {
ActionResult::Seer(a) => *a,
_ => panic!("expected a seer result"),
}
}
fn arcanist(&self) -> AlignmentEq {
match self {
ActionResult::Arcanist(same) => *same,
_ => panic!("expected an arcanist result"),
}
}
fn insomniac(&self) -> Visits {
match self {
ActionResult::Insomniac(v) => v.clone(),
_ => panic!("expected an insomniac result"),
}
}
}
#[allow(unused)]
pub trait AlignmentExt {
fn village(&self);
fn wolves(&self);
}
impl AlignmentExt for Alignment {
fn village(&self) {
assert_eq!(*self, Alignment::Village)
}
fn wolves(&self) {
assert_eq!(*self, Alignment::Wolves)
}
}
#[allow(unused)]
pub trait ServerToHostMessageExt {
fn prompt(self) -> ActionPrompt;
fn result(self) -> ActionResult;
fn daytime(self) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8);
}
impl ServerToHostMessageExt for ServerToHostMessage {
fn daytime(self) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8) {
match self {
Self::Daytime {
characters,
marked,
day,
..
} => (characters, marked, day),
resp => panic!("expected daytime, got {resp:?}"),
}
}
fn prompt(self) -> ActionPrompt {
match self {
Self::ActionPrompt(prompt, _) => prompt,
Self::Daytime { .. } => panic!("{}", "[got daytime]"),
msg => panic!("expected server message <<{msg:?}>> to be an ActionPrompt"),
}
}
fn result(self) -> ActionResult {
match self {
Self::ActionResult(_, res) => res,
msg => panic!("expected server message <<{msg:?}>> to be an ActionResult"),
}
}
}
#[allow(unused)]
pub trait GameExt {
fn villager_character_ids(&self) -> Box<[CharacterId]>;
fn character_by_player_id(&self, player_id: PlayerId) -> Character;
fn character_by_character_id(&self, character_id: CharacterId) -> Character;
fn next(&mut self) -> ActionPrompt;
fn r#continue(&mut self) -> ActionResult;
fn next_expect_day(&mut self) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8);
fn mark(&mut self, mark: CharacterId) -> ActionPrompt;
fn mark_and_check(&mut self, mark: CharacterId);
fn response(&mut self, resp: ActionResponse) -> ActionResult;
fn execute(&mut self) -> ActionPrompt;
fn mark_for_execution(
&mut self,
target: CharacterId,
) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8);
fn living_villager_excl(&self, excl: PlayerId) -> Character;
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(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()
}
fn living_villager(&self) -> Character {
self.village()
.characters()
.into_iter()
.find(|c| c.alive() && matches!(c.role_title(), RoleTitle::Villager))
.unwrap()
}
fn living_villager_excl(&self, excl: PlayerId) -> Character {
self.village()
.characters()
.into_iter()
.find(|c| {
c.alive() && matches!(c.role_title(), RoleTitle::Villager) && c.player_id() != excl
})
.unwrap()
}
fn villager_character_ids(&self) -> Box<[CharacterId]> {
self.village()
.characters()
.into_iter()
.filter_map(|c| {
(c.alive() && matches!(c.role_title(), RoleTitle::Villager))
.then_some(c.character_id())
})
.collect()
}
fn character_by_player_id(&self, player_id: PlayerId) -> Character {
self.village()
.character_by_player_id(player_id)
.unwrap()
.clone()
}
fn character_by_character_id(&self, character_id: CharacterId) -> Character {
self.village()
.character_by_id(character_id)
.unwrap()
.clone()
}
fn r#continue(&mut self) -> ActionResult {
self.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
ActionResponse::Continue,
)))
.unwrap()
.result()
}
fn mark(&mut self, mark: CharacterId) -> ActionPrompt {
self.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
ActionResponse::MarkTarget(mark),
)))
.unwrap()
.prompt()
}
fn mark_and_check(&mut self, mark: CharacterId) {
let prompt = self.mark(mark);
match prompt {
ActionPrompt::TraitorIntro { .. }
| ActionPrompt::Insomniac { .. }
| ActionPrompt::MasonsWake { .. }
| ActionPrompt::ElderReveal { .. }
| ActionPrompt::CoverOfDarkness
| ActionPrompt::WolvesIntro { .. }
| ActionPrompt::RoleChange { .. }
| ActionPrompt::Shapeshifter { .. } => panic!("expected a prompt with a mark"),
ActionPrompt::Arcanist { .. } => panic!("wrong call for arcanist"),
ActionPrompt::Bloodletter {
marked: Some(marked),
..
}
| ActionPrompt::LoneWolfKill {
marked: Some(marked),
..
}
| ActionPrompt::Seer {
marked: Some(marked),
..
}
| ActionPrompt::Adjudicator {
marked: Some(marked),
..
}
| ActionPrompt::PowerSeer {
marked: Some(marked),
..
}
| ActionPrompt::Mortician {
marked: Some(marked),
..
}
| ActionPrompt::Beholder {
marked: Some(marked),
..
}
| ActionPrompt::MasonLeaderRecruit {
marked: Some(marked),
..
}
| ActionPrompt::Empath {
marked: Some(marked),
..
}
| ActionPrompt::Vindicator {
marked: Some(marked),
..
}
| ActionPrompt::PyreMaster {
marked: Some(marked),
..
}
| ActionPrompt::Protector {
marked: Some(marked),
..
}
| ActionPrompt::Gravedigger {
marked: Some(marked),
..
}
| ActionPrompt::Hunter {
marked: Some(marked),
..
}
| ActionPrompt::Militia {
marked: Some(marked),
..
}
| ActionPrompt::MapleWolf {
marked: Some(marked),
..
}
| ActionPrompt::Guardian {
marked: Some(marked),
..
}
| ActionPrompt::WolfPackKill {
marked: Some(marked),
..
}
| ActionPrompt::AlphaWolf {
marked: Some(marked),
..
}
| ActionPrompt::DireWolf {
marked: Some(marked),
..
} => assert_eq!(marked, mark, "marked character"),
ActionPrompt::Bloodletter { marked: None, .. }
| ActionPrompt::Seer { marked: None, .. }
| ActionPrompt::Adjudicator { marked: None, .. }
| ActionPrompt::PowerSeer { marked: None, .. }
| ActionPrompt::Mortician { marked: None, .. }
| ActionPrompt::Beholder { marked: None, .. }
| ActionPrompt::MasonLeaderRecruit { marked: None, .. }
| ActionPrompt::Empath { marked: None, .. }
| ActionPrompt::Vindicator { marked: None, .. }
| ActionPrompt::PyreMaster { marked: None, .. }
| ActionPrompt::Protector { marked: None, .. }
| ActionPrompt::Gravedigger { marked: None, .. }
| ActionPrompt::Hunter { marked: None, .. }
| ActionPrompt::Militia { marked: None, .. }
| ActionPrompt::MapleWolf { marked: None, .. }
| ActionPrompt::Guardian { marked: None, .. }
| ActionPrompt::WolfPackKill { marked: None, .. }
| ActionPrompt::AlphaWolf { marked: None, .. }
| ActionPrompt::DireWolf { marked: None, .. }
| ActionPrompt::LoneWolfKill { marked: None, .. } => panic!("no mark"),
}
}
fn next(&mut self) -> ActionPrompt {
self.process(HostGameMessage::Night(HostNightMessage::Next))
.unwrap()
.prompt()
}
fn next_expect_day(&mut self) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8) {
match self
.process(HostGameMessage::Night(HostNightMessage::Next))
.unwrap()
{
ServerToHostMessage::Daytime {
characters,
marked,
day,
..
} => (characters, marked, day),
res => panic!("unexpected response to next_expect_day: {res:?}"),
}
}
fn response(&mut self, resp: ActionResponse) -> ActionResult {
self.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
resp,
)))
.unwrap()
.result()
}
fn mark_for_execution(
&mut self,
target: CharacterId,
) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8) {
match self
.process(HostGameMessage::Day(HostDayMessage::MarkForExecution(
target,
)))
.unwrap()
{
ServerToHostMessage::Daytime {
characters,
marked,
day,
..
} => (characters, marked, day),
res => panic!("unexpected response to mark_for_execution: {res:?}"),
}
}
fn execute(&mut self) -> ActionPrompt {
assert_eq!(
self.process(HostGameMessage::Day(HostDayMessage::Execute))
.unwrap()
.prompt(),
ActionPrompt::CoverOfDarkness
);
self.r#continue().r#continue();
self.next()
}
}

View File

@ -19,7 +19,6 @@ pub mod client {
}
pub mod host {
mod host;
pub mod story_test;
pub use host::*;
}
pub mod test_remote;
@ -27,18 +26,3 @@ pub mod test_remote;
const DEBUG_URL: &str = "ws://192.168.1.162:8080/connect/";
const LIVE_URL: &str = "wss://wolf.emilis.dev/connect/";
pub mod story_test {
use yew::prelude::*;
use crate::components::Story;
#[function_component]
pub fn StoryTest() -> Html {
let story = crate::clients::host::story_test::test_story();
html! {
<div class="content">
<Story story={story}/>
</div>
}
}
}

View File

@ -0,0 +1,36 @@
use werewolves_proto::message::CharacterState;
// 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 <https://www.gnu.org/licenses/>.
use yew::prelude::*;
use crate::components::host::DaytimePlayerList;
#[derive(Debug, Clone, PartialEq, Properties)]
pub struct CharacterStatesProps {
pub states: Box<[CharacterState]>,
}
#[function_component]
pub fn CharacterStatesReadOnly(CharacterStatesProps { states }: &CharacterStatesProps) -> Html {
let marked: Box<[_]> = Box::new([]);
html! {
<DaytimePlayerList
marked={marked}
characters={states.clone()}
on_mark={|_|()}
big_screen=false
/>
}
}

View File

@ -22,16 +22,16 @@ use werewolves_proto::{
};
use yew::prelude::*;
use crate::components::{
AssociatedIcon, Button, Icon, IconType, Identity, PartialAssociatedIcon,
};
use crate::components::{AssociatedIcon, Button, Icon, IconType, Identity, PartialAssociatedIcon};
#[derive(Debug, Clone, PartialEq, Properties)]
pub struct DaytimePlayerListProps {
pub day: NonZeroU8,
#[prop_or_default]
pub day: Option<NonZeroU8>,
pub characters: Box<[CharacterState]>,
pub marked: Box<[CharacterId]>,
pub on_execute: Callback<()>,
#[prop_or_default]
pub on_execute: Option<Callback<()>>,
pub on_mark: Callback<CharacterId>,
pub big_screen: bool,
}
@ -60,7 +60,9 @@ pub fn DaytimePlayerList(
Some(died_to) => match died_to.date_time() {
GameTime::Day { .. } => Some(MarkState::Dead),
GameTime::Night { number } => {
if number == day.get() - 1 {
if let Some(day) = day.as_ref()
&& number == day.get() - 1
{
Some(MarkState::DiedLastNight)
} else {
Some(MarkState::Dead)
@ -82,16 +84,25 @@ pub fn DaytimePlayerList(
} else {
"execute"
};
let button = big_screen.not().then(|| {
let button = big_screen
.not()
.then_some(())
.and_then(|_| on_execute.clone())
.map(|on_execute| {
html! {
<Button on_click={on_execute}>
{button_text}
</Button>
}
});
let day = day.as_ref().map(|day| {
html! {
<Button on_click={on_execute}>
{button_text}
</Button>
<h2>{"day "}{day.get()}</h2>
}
});
html! {
<div class="character-picker">
<h2>{"day "}{day.to_string()}</h2>
{day}
<div class="player-list">
{chars}
</div>