proto changes to accomodate leptos client

This commit is contained in:
emilis 2026-02-17 17:17:25 +00:00
parent 78ecb6c164
commit 314e113a46
No known key found for this signature in database
13 changed files with 214 additions and 35 deletions

View File

@ -47,6 +47,12 @@ impl CharacterId {
pub const fn from_u128(v: u128) -> Self {
Self(uuid::Uuid::from_u128(v))
}
pub const fn from_uuid(v: uuid::Uuid) -> Self {
Self(v)
}
pub const fn into_uuid(self) -> uuid::Uuid {
self.0
}
}
impl Display for CharacterId {
@ -66,7 +72,11 @@ pub struct Character {
}
impl Character {
pub fn new(
pub fn new(ident: Identification, role: Role, auras: Vec<Aura>) -> Option<Self> {
Self::new_with_character_id(ident, role, auras, CharacterId::new())
}
pub(crate) fn new_with_character_id(
Identification {
player_id,
public:
@ -78,6 +88,7 @@ impl Character {
}: Identification,
role: Role,
auras: Vec<Aura>,
character_id: CharacterId,
) -> Option<Self> {
Some(Self {
role,
@ -86,7 +97,7 @@ impl Character {
auras: Auras::new(auras),
role_changes: Vec::new(),
identity: CharacterIdentity {
character_id: CharacterId::new(),
character_id,
name,
pronouns,
number: number?,

View File

@ -109,4 +109,16 @@ pub enum GameError {
NoCurrentPromptForAura,
#[error("you're not dead")]
NotDead,
#[error("invalid character id assignment for player ID {for_player}")]
InvalidCharacterIdAssignment { for_player: PlayerId },
#[error("already joined")]
AlreadyJoined,
#[error("cannot join own game")]
CannotJoinOwnGame,
#[error("cannot leave a started game")]
CannotLeaveOnceStarted,
#[error("cannot join a started game")]
CannotJoinStartedGame,
#[error("game already started")]
GameAlreadyStarted,
}

View File

@ -61,6 +61,19 @@ pub struct Game {
}
impl Game {
pub fn new_with_assigned_character_ids(
players: &[(Identification, CharacterId)],
settings: GameSettings,
) -> Result<Self> {
let village = Village::new_with_assigned_character_ids(players, settings)?;
Ok(Self {
started: Utc::now(),
history: GameStory::new(village.clone()),
state: GameState::Night {
night: Night::new(village)?,
},
})
}
pub fn new(players: &[Identification], settings: GameSettings) -> Result<Self> {
let village = Village::new(players, settings)?;
Ok(Self {

View File

@ -829,7 +829,7 @@ impl Night {
NightChange::Protection {
target,
protection: _,
} => target == kill_target,
} => target == kill_target || target == *source,
_ => false,
}) {
// there is protection, so the kill doesn't happen -> no shapeshift

View File

@ -23,7 +23,12 @@ use super::Result;
use serde::{Deserialize, Serialize};
use crate::{character::Character, error::GameError, message::Identification, role::RoleTitle};
use crate::{
character::{Character, CharacterId},
error::GameError,
message::Identification,
role::RoleTitle,
};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GameSettings {
@ -115,8 +120,12 @@ impl GameSettings {
}
}
pub fn assign(&self, players: &[Identification]) -> Result<Box<[Character]>> {
self.check_with_player_list(players)?;
pub fn assign_with_set_character_ids(
&self,
players: &[(Identification, CharacterId)],
) -> Result<Box<[Character]>> {
let idents_only = players.iter().map(|i| i.0.clone()).collect::<Box<_>>();
self.check_with_player_list(&idents_only)?;
let roles_in_game = self
.roles
@ -131,7 +140,7 @@ impl GameSettings {
s.assign_to.as_ref().map(|assign_to| {
players
.iter()
.find(|pid| pid.player_id == *assign_to)
.find(|(pid, _)| pid.player_id == *assign_to)
.ok_or(GameError::AssignedPlayerMissing(*assign_to))
.map(|id| (id, s))
})
@ -140,10 +149,10 @@ impl GameSettings {
let mut random_assign_players = players
.iter()
.filter(|p| {
.filter(|(p, _)| {
!with_assigned_roles
.iter()
.any(|(r, _)| r.player_id == p.player_id)
.any(|((r, _), _)| r.player_id == p.player_id)
})
.collect::<Box<[_]>>();
@ -156,10 +165,21 @@ impl GameSettings {
.into_iter()
.zip(self.roles.iter().filter(|s| s.assign_to.is_none())),
)
.map(|(id, slot)| slot.clone().into_character(id.clone(), &roles_in_game))
.map(|((ident, char_id), slot)| {
slot.clone()
.into_character_with_id(ident.clone(), &roles_in_game, *char_id)
})
.collect::<Result<Box<[_]>>>()
}
pub fn assign(&self, players: &[Identification]) -> Result<Box<[Character]>> {
let with_cids = players
.iter()
.map(|ident| (ident.clone(), CharacterId::new()))
.collect::<Box<_>>();
self.assign_with_set_character_ids(&with_cids)
}
pub fn check_with_player_list(&self, players: &[Identification]) -> Result<()> {
self.check()?;
let (p_len, r_len) = (players.len(), self.roles.len());

View File

@ -24,7 +24,7 @@ use werewolves_macros::{All, ChecksAs, Titles};
use crate::{
aura::AuraTitle,
character::Character,
character::{Character, CharacterId},
error::GameError,
message::Identification,
player::PlayerId,
@ -435,6 +435,12 @@ impl SlotId {
}
}
impl Display for SlotId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Display::fmt(&self.0, f)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SetupSlot {
pub slot_id: SlotId,
@ -470,17 +476,22 @@ impl SetupSlot {
)
.ok_or(GameError::PlayerNotAssignedNumber(ident.to_string()))
}
}
impl Category {
pub const fn class(&self) -> &'static str {
match self {
Category::Wolves => "wolves",
Category::Villager => "village",
Category::Intel => "intel",
Category::Defensive => "defensive",
Category::Offensive => "offensive",
Category::StartsAsVillager => "starts-as-villager",
}
pub fn into_character_with_id(
self,
ident: Identification,
roles_in_game: &[RoleTitle],
id: CharacterId,
) -> Result<Character, GameError> {
Character::new_with_character_id(
ident.clone(),
self.role.into_role(roles_in_game)?,
self.auras
.into_iter()
.map(|aura| aura.into_aura())
.collect(),
id,
)
.ok_or(GameError::PlayerNotAssignedNumber(ident.to_string()))
}
}

View File

@ -43,6 +43,27 @@ pub struct Village {
}
impl Village {
pub fn new_with_assigned_character_ids(
players: &[(Identification, CharacterId)],
settings: GameSettings,
) -> Result<Self> {
if settings.min_players_needed() > players.len() {
return Err(GameError::TooManyRoles {
players: players.len() as u8,
roles: settings.min_players_needed() as u8,
});
}
let mut characters = settings.assign_with_set_character_ids(players)?;
assert_eq!(characters.len(), players.len());
characters.sort_by_key(|l| l.number());
Ok(Self {
settings,
characters,
time: GameTime::Night { number: 0 },
dead_chat: DeadChat::new(),
})
}
pub fn new(players: &[Identification], settings: GameSettings) -> Result<Self> {
if settings.min_players_needed() > players.len() {
return Err(GameError::TooManyRoles {

View File

@ -1119,7 +1119,7 @@ fn big_game_test_based_on_story_test() {
);
game.execute().title().vindicator();
game.mark(game.character_by_player_id(shapeshifter).character_id());
game.mark(game.character_by_player_id(insomniac).character_id());
game.r#continue().sleep();
game.next().title().wolf_pack_kill();
@ -1127,11 +1127,16 @@ fn big_game_test_based_on_story_test() {
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.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
ActionResponse::Shapeshift,
)))
.expect("shapeshift"),
ServerToHostMessage::ActionResult(
Some(game.character_by_player_id(shapeshifter).identity()),
ActionResult::Continue
)
);
assert_eq!(
game.next(),

View File

@ -444,3 +444,52 @@ fn shapeshift_removes_village_prompt_but_previous_can_bring_it_back() {
None
);
}
#[test]
fn shapeshifter_protected_when_shifting_prevents_shift() {
init_log();
let players = gen_players(1..21);
let mut player_ids = players.iter().map(|p| p.player_id);
let shapeshifter = player_ids.next().unwrap();
let wolf = player_ids.next().unwrap();
let protector = player_ids.next().unwrap();
let hunter = player_ids.next().unwrap();
let mut settings = GameSettings::empty();
settings.add_and_assign(SetupRole::Shapeshifter, shapeshifter);
settings.add_and_assign(SetupRole::Werewolf, wolf);
settings.add_and_assign(SetupRole::Protector, protector);
settings.add_and_assign(SetupRole::Hunter, hunter);
settings.fill_remaining_slots_with_villagers(players.len());
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().protector();
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(hunter).character_id());
game.r#continue().r#continue();
game.next().title().shapeshifter();
match game
.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
ActionResponse::Shapeshift,
)))
.unwrap()
{
ServerToHostMessage::ActionResult(_, res) => assert_eq!(res, ActionResult::ShiftFailed),
other => panic!("expected action result, got {other:?}"),
}
game.r#continue().sleep();
game.next().title().hunter();
game.mark_villager();
game.r#continue().sleep();
game.next_expect_day();
}

View File

@ -22,6 +22,7 @@ use core::num::NonZeroU8;
use chrono::{DateTime, Utc};
pub use ident::*;
use serde::{Deserialize, Serialize};
use werewolves_macros::Titles;
use crate::{
character::CharacterId,
@ -62,7 +63,7 @@ pub struct DayCharacter {
pub alive: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Titles)]
pub enum ServerToClientMessage {
Disconnect,
LobbyInfo {
@ -73,9 +74,6 @@ pub enum ServerToClientMessage {
GameStart {
role: RoleTitle,
},
InvalidMessageForGameState,
NoSuchTarget,
GameOver(GameOver),
Story(GameStory),
Update(PlayerUpdate),
DeadChat(Box<[DeadChatMessage]>),
@ -85,7 +83,7 @@ pub enum ServerToClientMessage {
Error(GameError),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum PlayerUpdate {
Number(NonZeroU8),
}

View File

@ -105,8 +105,8 @@ pub enum ServerToHostMessage {
Lobby {
players: Box<[PlayerState]>,
settings: GameSettings,
qr_mode: bool,
},
QrMode(bool),
Error(GameError),
GameOver(GameOver),
WaitingForRoleRevealAcks {

View File

@ -31,6 +31,15 @@ impl PlayerId {
pub const fn from_u128(v: u128) -> Self {
Self(uuid::Uuid::from_u128(v))
}
pub const fn from_uuid(uuid: uuid::Uuid) -> Self {
Self(uuid)
}
}
impl From<PlayerId> for uuid::Uuid {
fn from(value: PlayerId) -> Self {
value.0
}
}
impl Display for PlayerId {

View File

@ -20,7 +20,7 @@ use werewolves_macros::{All, ChecksAs, RefAndMut, Titles};
use crate::{
character::CharacterId,
diedto::DiedTo,
game::{GameTime, Village},
game::{Category, GameTime, Village},
message::CharacterIdentity,
};
@ -122,50 +122,59 @@ pub enum Role {
#[checks(Alignment::Village)]
#[checks(Killer::NotKiller)]
#[checks(Powerful::NotPowerful)]
#[checks(Category::Villager)]
Villager,
#[checks(Alignment::Wolves)]
#[checks(Killer::Killer)]
#[checks(Powerful::Powerful)]
#[checks(Category::Villager)]
Scapegoat { redeemed: bool },
#[checks(Alignment::Village)]
#[checks(Powerful::Powerful)]
#[checks(Killer::NotKiller)]
#[checks("is_mentor")]
#[checks("doesnt_wake_if_died_tonight")]
#[checks(Category::Intel)]
Seer,
#[checks(Alignment::Village)]
#[checks(Powerful::Powerful)]
#[checks(Killer::NotKiller)]
#[checks("is_mentor")]
#[checks("doesnt_wake_if_died_tonight")]
#[checks(Category::Intel)]
Arcanist,
#[checks(Alignment::Village)]
#[checks(Powerful::Powerful)]
#[checks(Killer::NotKiller)]
#[checks("is_mentor")]
#[checks("doesnt_wake_if_died_tonight")]
#[checks(Category::Intel)]
Adjudicator,
#[checks(Alignment::Village)]
#[checks(Powerful::Powerful)]
#[checks(Killer::NotKiller)]
#[checks("is_mentor")]
#[checks("doesnt_wake_if_died_tonight")]
#[checks(Category::Intel)]
PowerSeer,
#[checks(Alignment::Village)]
#[checks(Powerful::Powerful)]
#[checks(Killer::NotKiller)]
#[checks("is_mentor")]
#[checks("doesnt_wake_if_died_tonight")]
#[checks(Category::Intel)]
Mortician,
#[checks(Alignment::Village)]
#[checks(Powerful::Powerful)]
#[checks(Killer::NotKiller)]
#[checks("is_mentor")]
#[checks("doesnt_wake_if_died_tonight")]
#[checks(Category::Intel)]
Beholder,
#[checks(Alignment::Village)]
#[checks(Powerful::Powerful)]
#[checks(Killer::NotKiller)]
#[checks(Category::Intel)]
MasonLeader {
recruits_available: u8,
recruits: Box<[CharacterId]>,
@ -173,58 +182,69 @@ pub enum Role {
#[checks(Alignment::Village)]
#[checks(Powerful::Powerful)]
#[checks(Killer::NotKiller)]
#[checks(Category::Intel)]
Empath { cursed: bool },
#[checks(Alignment::Village)]
#[checks(Powerful::Powerful)]
#[checks(Killer::NotKiller)]
#[checks("is_mentor")]
#[checks(Category::Defensive)]
Vindicator,
#[checks(Alignment::Village)]
#[checks(Powerful::Powerful)]
#[checks(Killer::NotKiller)]
#[checks("is_mentor")]
#[checks(Category::Defensive)]
Diseased,
#[checks(Alignment::Village)]
#[checks(Powerful::Powerful)]
#[checks(Killer::NotKiller)]
#[checks("is_mentor")]
#[checks(Category::Defensive)]
BlackKnight { attacked: Option<DiedTo> },
#[checks(Alignment::Village)]
#[checks(Powerful::Powerful)]
#[checks(Killer::NotKiller)]
#[checks("is_mentor")]
#[checks(Category::Offensive)]
Weightlifter,
#[checks(Alignment::Village)]
#[checks(Powerful::Powerful)]
#[checks(Killer::Killer)]
#[checks("is_mentor")]
#[checks(Category::Offensive)]
PyreMaster { villagers_killed: u8 },
#[checks(Alignment::Village)]
#[checks(Powerful::Powerful)]
#[checks(Killer::NotKiller)]
#[checks("is_mentor")]
#[checks("doesnt_wake_if_died_tonight")]
#[checks(Category::Intel)]
Gravedigger,
#[checks(Alignment::Village)]
#[checks(Killer::Killer)]
#[checks(Powerful::Powerful)]
#[checks("is_mentor")]
#[checks]
#[checks(Category::Offensive)]
Hunter { target: Option<CharacterId> },
#[checks(Alignment::Village)]
#[checks(Killer::Killer)]
#[checks(Powerful::Powerful)]
#[checks("is_mentor")]
#[checks(Category::Offensive)]
Militia { targeted: Option<CharacterId> },
#[checks(Alignment::Wolves)]
#[checks(Killer::Killer)]
#[checks(Powerful::Powerful)]
#[checks("is_mentor")]
#[checks(Category::Offensive)]
MapleWolf { last_kill_on_night: u8 },
#[checks(Alignment::Village)]
#[checks(Powerful::Powerful)]
#[checks(Killer::Killer)]
#[checks("is_mentor")]
#[checks(Category::Defensive)]
Guardian {
last_protected: Option<PreviousGuardianAction>,
},
@ -232,14 +252,17 @@ pub enum Role {
#[checks(Powerful::Powerful)]
#[checks(Killer::NotKiller)]
#[checks("is_mentor")]
#[checks(Category::Defensive)]
Protector { last_protected: Option<CharacterId> },
#[checks(Alignment::Village)]
#[checks(Powerful::Powerful)]
#[checks(Killer::NotKiller)]
#[checks(Category::StartsAsVillager)]
Apprentice(RoleTitle),
#[checks(Alignment::Village)]
#[checks(Powerful::Powerful)]
#[checks(Killer::NotKiller)]
#[checks(Category::StartsAsVillager)]
Elder {
knows_on_night: NonZeroU8,
woken_for_reveal: bool,
@ -249,6 +272,7 @@ pub enum Role {
#[checks(Powerful::Powerful)]
#[checks(Killer::NotKiller)]
#[checks("doesnt_wake_if_died_tonight")]
#[checks(Category::Intel)]
Insomniac,
#[checks(Alignment::Wolves)]
@ -256,33 +280,39 @@ pub enum Role {
#[checks(Powerful::Powerful)]
#[checks("wolf")]
#[checks("killing_wolf")]
#[checks(Category::Wolves)]
Werewolf,
#[checks(Alignment::Wolves)]
#[checks(Killer::Killer)]
#[checks(Powerful::Powerful)]
#[checks("wolf")]
#[checks("killing_wolf")]
#[checks(Category::Wolves)]
AlphaWolf { killed: Option<CharacterId> },
#[checks(Alignment::Village)]
#[checks(Killer::Killer)]
#[checks(Powerful::Powerful)]
#[checks("wolf")]
#[checks(Category::Wolves)]
DireWolf { last_blocked: Option<CharacterId> },
#[checks(Alignment::Wolves)]
#[checks(Killer::Killer)]
#[checks(Powerful::Powerful)]
#[checks("wolf")]
#[checks("killing_wolf")]
#[checks(Category::Wolves)]
Shapeshifter { shifted_into: Option<CharacterId> },
#[checks(Alignment::Wolves)]
#[checks(Killer::Killer)]
#[checks(Powerful::Powerful)]
#[checks("wolf")]
#[checks(Category::Wolves)]
LoneWolf,
#[checks(Alignment::Wolves)]
#[checks(Killer::Killer)]
#[checks(Powerful::Powerful)]
#[checks("wolf")]
#[checks(Category::Wolves)]
Bloodletter,
}