diff --git a/.vscode/settings.json b/.vscode/settings.json index 307da5237..ca454612d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,6 +11,8 @@ "Doesnt", "dompurify", "entriy", + "guiltied", + "guilties", "Informant", "Informants", "inno", diff --git a/client/src/components/ChatMessage.tsx b/client/src/components/ChatMessage.tsx index ab970d4c2..852f99aaa 100644 --- a/client/src/components/ChatMessage.tsx +++ b/client/src/components/ChatMessage.tsx @@ -604,8 +604,8 @@ export function translateChatMessage( return translate("chatMessage.godfatherBackupKilled", playerNames[message.backup]); case "roleBlocked": return translate("chatMessage.roleBlocked" + (message.immune ? ".immune" : "")); - case "sheriffResult": - return translate("chatMessage.sheriffResult." + (message.suspicious ? "suspicious" : "innocent")); + case "detectiveResult": + return translate("chatMessage.detectiveResult." + (message.suspicious ? "suspicious" : "innocent")); case "snoopResult": return translate("chatMessage.snoopResult." + (message.townie ? "townie" : "inconclusive")); case "gossipResult": @@ -620,8 +620,8 @@ export function translateChatMessage( return translate("chatMessage.spyBug."+message.bug); case "trackerResult": return translate("chatMessage.trackerResult", playerListToString(message.players, playerNames)); - case "seerResult": - return translate("chatMessage.seerResult." + (message.enemies ? "enemies" : "friends")); + case "philosopherResult": + return translate("chatMessage.philosopherResult." + (message.enemies ? "enemies" : "friends")); case "psychicEvil": return translate( "chatMessage.psychicEvil", @@ -938,7 +938,7 @@ export type ChatMessageVariant = { { type: "wardblocked" } | { - type: "sheriffResult", + type: "detectiveResult", suspicious: boolean } | { type: "snoopResult", @@ -962,7 +962,7 @@ export type ChatMessageVariant = { type: "trackerResult", players: PlayerIndex[] } | { - type: "seerResult", + type: "philosopherResult", enemies: boolean } | { type: "psychicGood", diff --git a/client/src/resources/lang/en_us.json b/client/src/resources/lang/en_us.json index 99eec4d3b..1b69ef1dc 100644 --- a/client/src/resources/lang/en_us.json +++ b/client/src/resources/lang/en_us.json @@ -942,12 +942,12 @@ "chatMessage.youArePoisoned": "You are poisoned, you will be attacked tomorrow night.", - "chatMessage.sheriffResult.suspicious":"Your target is enemies with the town.", - "chatMessage.sheriffResult.innocent":"Your target is friends with the town.", + "chatMessage.detectiveResult.suspicious":"Your target is enemies with the town.", + "chatMessage.detectiveResult.innocent":"Your target is friends with the town.", "chatMessage.lookoutResult":"Your target was visited by \\0.", "chatMessage.trackerResult":"Your target visited \\0.", - "chatMessage.seerResult.friends":"Your targets are friends.", - "chatMessage.seerResult.enemies":"Your targets are enemies.", + "chatMessage.philosopherResultResult.friends":"Your targets are friends.", + "chatMessage.philosopherResultResult.enemies":"Your targets are enemies.", "chatMessage.spyMafiaVisit":"The syndicate visited \\0.", "chatMessage.spyBug.silenced":"Your target was silenced.", "chatMessage.spyBug.roleblocked":"Someone attempted to roleblock your target.", @@ -1251,7 +1251,9 @@ "wiki.article.standard.confused.title:var.0": "Confuses", "wiki.article.standard.confused.title:var.1": "Confusing", "wiki.article.standard.confused.title:var.2": "Confuse", - "wiki.article.standard.confused.text": "The role of a player who is confused might not function correctly. Confused affects each role differently. You might not be told that you are confused.\n How this affects every role:\n - Snoop: You are always told that you can't tell\n - Detective: You are always told innocent\n - Philosopher: You are always told friends\n - Gossip: You are always told that your target didn't visit an enemy\n - Psychic: You are told a random list of 3 players for your evil vision, You are told a random list of 2 players for your good vision.\n - Tally Clerk: You are told either 1 more or 1 less than the correct number you should be told. If it would exceed the number of players who voted guilty, it is that number instead. If it were to go under 0, it is 0 instead.\n - Auditor: You get random roles, they might be correct or incorrect. You are only told roles that could have come from that slot.", + "wiki.article.standard.confused.title:var.3": "red herring", + "wiki.article.standard.confused.title:var.4": "red herrings", + "wiki.article.standard.confused.text": "The role of a player who is confused might not function correctly. Confused affects each role differently. You might not be told that you are confused. All confused players are assigned a set of players they believe are evil, called red herrings. Not all confused roles are affected by red herrings. The number of red herrings for a confused player is the number of non-townie roles assigned at the start of the game before factoring in recruiter and reeducator.\n How this affects every role:\n - Snoop: You are always told that you can't tell\n - Detective: You are always told unless your target is framed or is one of your red herrings\n - Philosopher: your told enemies iff 1 one of the targets is framed or one of your red herrings, but the other isn't \n - Gossip: You are told your target visited an enemy if they visited a player that is framed or is one of your red herrings\n - Psychic: You are told a random list of 3 players for your evil vision, You are told a random list of 2 players for your good vision\n - Tally Clerk: You are told the number of players that are framed or are one of your red herrings that voted guilty\n - Auditor: You get random roles, they might be correct or incorrect. You are only told roles that could have come from that slot.", "wiki.article.standard.marionette.title": "String Up", "wiki.article.standard.marionette.title:var.0": "String", "wiki.article.standard.marionette.title:var.1": "Marionette", diff --git a/client/src/resources/roles.json b/client/src/resources/roles.json index 5e1295c3a..f53e3b965 100644 --- a/client/src/resources/roles.json +++ b/client/src/resources/roles.json @@ -62,8 +62,8 @@ "canWriteDeathNote": false, "canBeConvertedTo": [], "chatMessages":[ - {"type":"sheriffResult","suspicious":true}, - {"type":"sheriffResult","suspicious":false} + {"type": "detectiveResult","suspicious":true}, + {"type":"detectiveResult","suspicious":false} ] }, "lookout": { @@ -88,8 +88,8 @@ "canWriteDeathNote": false, "canBeConvertedTo": [], "chatMessages":[ - {"type": "seerResult", "enemies": true}, - {"type": "seerResult", "enemies": false} + {"type": "philosopherResult", "enemies": true}, + {"type": "philosopherResult", "enemies": false} ] }, "tracker": { @@ -499,7 +499,7 @@ "canBeConvertedTo": [], "chatMessages":[ {"type": "targetsMessage", "message": - {"type": "sheriffResult","suspicious":true} + {"type": "detectiveResult","suspicious":true} }, {"type": "youWerePossessed", "immune": false}, {"type": "youWerePossessed", "immune": true}, @@ -689,7 +689,7 @@ {"type": "framerResult", "mafiaMember": 1, "visitors": ["gossip", "detective"]}, {"type": "framerResult", "mafiaMember": 4, "visitors": ["doctor"]}, {"type": "framerResult", "mafiaMember": 20, "visitors": []}, - {"type": "sheriffResult","suspicious":true}, + {"type": "detectiveResult","suspicious":true}, {"type": "snoopResult", "townie":false}, {"type": "snoopResult", "townie":false}, {"type": "seerResult", "enemies": true} @@ -750,7 +750,7 @@ "canBeConvertedTo": ["godfather"], "chatMessages":[ {"type": "targetsMessage", "message": - {"type": "sheriffResult","suspicious":true} + {"type": "detectiveResult","suspicious":true} }, {"type": "targetHasRole", "role":"detective"}, {"type": "targetIsPossessionImmune"}, @@ -768,7 +768,7 @@ "canBeConvertedTo": ["godfather"], "chatMessages":[ {"type": "targetsMessage", "message": - {"type": "sheriffResult","suspicious":true} + {"type": "detectiveResult","suspicious":true} }, {"type": "targetIsPossessionImmune"}, {"type": "youWerePossessed", "immune": false}, @@ -969,7 +969,7 @@ "canBeConvertedTo": [], "chatMessages":[ {"type": "targetsMessage", "message": - {"type": "sheriffResult","suspicious":true} + {"type": "detectiveResult","suspicious":true} }, {"type": "targetHasRole", "role":"detective"}, {"type": "targetIsPossessionImmune"}, diff --git a/server/src/game/chat/chat_message_variant.rs b/server/src/game/chat/chat_message_variant.rs index af37abc67..6e4d06a0a 100644 --- a/server/src/game/chat/chat_message_variant.rs +++ b/server/src/game/chat/chat_message_variant.rs @@ -162,10 +162,10 @@ pub enum ChatMessageVariant { Wardblocked, - SheriffResult {suspicious: bool}, + DetectiveResult {suspicious: bool}, LookoutResult{players: Vec}, TrackerResult{players: Vec}, - SeerResult{enemies: bool}, + PhilosopherResult{enemies: bool}, SpyMafiaVisit{players: Vec}, SpyBug{bug: SpyBug}, PsychicGood{player: PlayerReference}, diff --git a/server/src/game/components/confused.rs b/server/src/game/components/confused.rs index ae43e2ff2..1afd077ff 100644 --- a/server/src/game/components/confused.rs +++ b/server/src/game/components/confused.rs @@ -1,32 +1,82 @@ -use std::collections::HashSet; +use rand::seq::IteratorRandom; -use crate::game::{player::PlayerReference, Game}; - - -#[derive(Default)] -pub struct Confused{ - players: HashSet +use crate::{game::{player::PlayerReference, Game}, vec_map::VecMap}; +/// Its not an enum that is different for every role because if you swap roles but stay confused, +/// the information you get should be consistent with your previous role's info if possible. +/// the reason why it stores whether your confused instead of removing you if your not, +/// is so if you become confused after stop being confused, you have the same confusion data. +#[derive(Default, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] +pub struct ConfusionData{ + pub red_herrings: Vec, + pub confused: bool, } -impl Confused{ - fn confused<'a>(game: &'a Game)->&'a Self{ - &game.confused +impl ConfusionData { + pub fn new(game: &Game, player: PlayerReference) -> ConfusionData { + ConfusionData { + red_herrings: Self::generate_red_herrings(game,player), + confused: true, + } } - fn confused_mut<'a>(game: &'a mut Game)->&'a mut Self{ - &mut game.confused + pub fn generate_red_herrings(game: &Game, player: PlayerReference) -> Vec { + let count = game.assignments.iter() + .filter(|a|a.2.is_evil()) + .count(); + PlayerReference::all_players(game) + .filter(|p|*p != player) + .choose_multiple(&mut rand::rng(), count) } +} - pub fn add_player(game: &mut Game, player: PlayerReference){ - let confused = Self::confused_mut(game); - confused.players.insert(player); +#[derive(Default, Clone, Debug)] +pub struct Confused(pub VecMap); + +impl Game { + fn confused(&self)->&Confused{ + &self.confused + } + fn confused_mut(&mut self)->&mut Confused{ + &mut self.confused + } +} + +impl Confused { + pub fn add_player(game: &mut Game, player: PlayerReference) { + match game.confused_mut().0.get_mut(&player) { + Some(data) => data.confused = true, + _=> { + let data = ConfusionData::new(game, player); + game.confused_mut().0.insert_unsized(player, data); + } + } } - pub fn remove_player(game: &mut Game, player: PlayerReference){ - let confused = Self::confused_mut(game); - confused.players.remove(&player); + + pub fn remove_player(game: &mut Game, player: PlayerReference) -> bool { + match game.confused_mut().0.get_mut(&player) { + Some(data) => { + let old = data.confused.clone(); + data.confused = false; + old + }, + None=>false, + } } pub fn is_confused(game: &Game, player: PlayerReference)->bool{ - let confused = Self::confused(game); - confused.players.contains(&player) + Self::get_confusion_data(game, player).is_some_and(|data|data.confused) + } + + pub fn is_red_herring(game: &Game, confused_player: PlayerReference, target: PlayerReference) -> bool{ + match game.confused().0.get(&confused_player) { + Some(data) => data.confused && data.red_herrings.contains(&target), + None => false, + } + } + + /// WARNING: JUST BECAUSE A PLAYER HAS CONFUSION DATA, IT DOESN'T MEAN THEY ARE CONFUSED
+ /// to check if player is confused, either check whether the data's confused flag is true + /// or use is_confused function + pub fn get_confusion_data(game: &Game, player: PlayerReference) -> Option<&ConfusionData>{ + game.confused().0.get(&player) } } \ No newline at end of file diff --git a/server/src/game/components/drunk_aura.rs b/server/src/game/components/drunk_aura.rs index c87a1868e..8ec365d75 100644 --- a/server/src/game/components/drunk_aura.rs +++ b/server/src/game/components/drunk_aura.rs @@ -1,36 +1,36 @@ -use std::collections::HashSet; - -use crate::game::{player::PlayerReference, Game}; +use crate::{game::{player::PlayerReference, Game}, vec_set::VecSet}; use super::confused::Confused; #[derive(Default, Clone)] pub struct DrunkAura { - pub players: HashSet, + pub players: VecSet, } -impl DrunkAura { - fn drunk_aura(game: &Game) -> &Self { - &game.drunk_aura +impl Game { + fn drunk_aura(&self) -> &DrunkAura { + &self.drunk_aura } - fn drunk_aura_mut(game: &mut Game) -> &mut Self { - &mut game.drunk_aura + fn drunk_aura_mut(&mut self) -> &mut DrunkAura { + &mut self.drunk_aura } +} - pub fn add_player(game: &mut Game, player: PlayerReference) { - Self::drunk_aura_mut(game).players.insert(player); +impl DrunkAura { + pub fn add_player(game: &mut Game, player: PlayerReference){ + game.drunk_aura_mut().players.insert(player); } - pub fn remove_player(game: &mut Game, player: PlayerReference) { - Self::drunk_aura_mut(game).players.remove(&player); + /// Returns true if the player was drunk + pub fn remove_player(game: &mut Game, player: PlayerReference) -> bool{ + game.drunk_aura_mut().players.remove(&player).is_some() } - + pub fn has_drunk_aura(game: &Game, player: PlayerReference) -> bool { - Self::drunk_aura(game).players.contains(&player) + game.drunk_aura().players.contains(&player) } pub fn on_role_switch(game: &mut Game, player: PlayerReference) { - if Self::has_drunk_aura(game, player) { - Self::remove_player(game, player); + if Self::remove_player(game, player) { Confused::remove_player(game, player); } } diff --git a/server/src/game/event/on_phase_start.rs b/server/src/game/event/on_phase_start.rs index fb1fce080..424369a8f 100644 --- a/server/src/game/event/on_phase_start.rs +++ b/server/src/game/event/on_phase_start.rs @@ -1,8 +1,6 @@ use crate::game::{ ability_input::saved_controllers_map::SavedControllersMap, components::{ - cult::Cult, detained::Detained, - mafia::Mafia, night_visits::NightVisits, - verdicts_today::VerdictsToday + cult::Cult, detained::Detained, mafia::Mafia, night_visits::NightVisits, verdicts_today::VerdictsToday }, modifiers::Modifiers, phase::PhaseState, player::PlayerReference, Game }; diff --git a/server/src/game/role/detective.rs b/server/src/game/role/detective.rs index 842c1e050..4d5637c6c 100644 --- a/server/src/game/role/detective.rs +++ b/server/src/game/role/detective.rs @@ -23,19 +23,19 @@ impl RoleStateImpl for Detective { if priority != Priority::Investigative {return;} let actor_visits = actor_ref.untagged_night_visits_cloned(game); - if let Some(visit) = actor_visits.first(){ - let suspicious = if Confused::is_confused(game, actor_ref) { - false - }else{ - Detective::player_is_suspicious(game, visit.target) - }; + let Some(visit) = actor_visits.first() else {return}; + let target_ref = visit.target; + let suspicious = if Confused::is_confused(game, actor_ref) { + Self::player_is_suspicious_confused(game, target_ref, actor_ref) + }else{ + Detective::player_is_suspicious(game, target_ref) + }; - let message = ChatMessageVariant::SheriffResult { - suspicious - }; - - actor_ref.push_night_message(game, message); - } + let message = ChatMessageVariant::DetectiveResult { + suspicious + }; + + actor_ref.push_night_message(game, message); } fn controller_parameters_map(self, game: &Game, actor_ref: PlayerReference) -> ControllerParametersMap { crate::game::role::common_role::controller_parameters_map_player_list_night_typical( @@ -68,4 +68,8 @@ impl Detective { !player_ref.win_condition(game).friends_with_resolution_state(GameConclusion::Town) } } + pub fn player_is_suspicious_confused(game: &Game, player_ref: PlayerReference, actor_ref: PlayerReference) -> bool { + player_ref.night_framed(game) || + Confused::is_red_herring(game, actor_ref, player_ref) + } } \ No newline at end of file diff --git a/server/src/game/role/gossip.rs b/server/src/game/role/gossip.rs index 8726f59a7..7fdb6a0a5 100644 --- a/server/src/game/role/gossip.rs +++ b/server/src/game/role/gossip.rs @@ -25,16 +25,11 @@ impl RoleStateImpl for Gossip { let actor_visits = actor_ref.untagged_night_visits_cloned(game); if let Some(visit) = actor_visits.first(){ - let enemies = if Confused::is_confused(game, actor_ref){ - false - }else{ - Gossip::enemies(game, visit.target) - }; - - let message = ChatMessageVariant::GossipResult{ enemies }; - + let enemies = Self::visited_enemies(game, visit.target, actor_ref); + let message: ChatMessageVariant = ChatMessageVariant::GossipResult{ enemies }; actor_ref.push_night_message(game, message); } + } fn controller_parameters_map(self, game: &Game, actor_ref: PlayerReference) -> ControllerParametersMap { crate::game::role::common_role::controller_parameters_map_player_list_night_typical( @@ -57,16 +52,18 @@ impl RoleStateImpl for Gossip { } impl Gossip { - pub fn enemies(game: &Game, player_ref: PlayerReference) -> bool { - + pub fn visited_enemies(game: &Game, player_ref: PlayerReference, actor_ref: PlayerReference) -> bool { match player_ref.night_appeared_visits(game) { Some(x) => x.clone(), None => player_ref.all_night_visits_cloned(game), } .iter() - .map(|v|v.target.clone()) - .any(|targets_target| - Detective::player_is_suspicious(game, targets_target) + .any(|visit: &Visit| + if Confused::is_confused(game, actor_ref) { + Detective::player_is_suspicious_confused(game, visit.target, actor_ref) + } else { + Detective::player_is_suspicious(game, visit.target) + } ) } } \ No newline at end of file diff --git a/server/src/game/role/philosopher.rs b/server/src/game/role/philosopher.rs index 849415b3c..8d6125f20 100644 --- a/server/src/game/role/philosopher.rs +++ b/server/src/game/role/philosopher.rs @@ -10,6 +10,7 @@ use crate::game::visit::Visit; use crate::game::Game; use crate::vec_set; +use super::detective::Detective; use super::{common_role, AvailableAbilitySelection, ControllerID, ControllerParametersMap, Priority, Role, RoleStateImpl}; #[derive(Clone, Debug, Serialize, Default)] @@ -24,19 +25,27 @@ impl RoleStateImpl for Philosopher { fn do_night_action(self, game: &mut Game, actor_ref: PlayerReference, priority: Priority) { if priority != Priority::Investigative {return;} + println!("phil do_night_action"); + let actor_visits = actor_ref.untagged_night_visits_cloned(game); let Some(first_visit) = actor_visits.get(0) else {return;}; let Some(second_visit) = actor_visits.get(1) else {return;}; - let enemies = if Confused::is_confused(game, actor_ref) { + let enemies = + if first_visit.target == second_visit.target { false + } else if Confused::is_confused(game, actor_ref) { + (first_visit.target.night_framed(game) || Confused::is_red_herring(game, actor_ref, first_visit.target)) ^ + (second_visit.target.night_framed(game) || Confused::is_red_herring(game, actor_ref, second_visit.target)) } else { Philosopher::players_are_enemies(game, first_visit.target, second_visit.target) }; - let message = ChatMessageVariant::SeerResult{ enemies }; + let message = ChatMessageVariant::PhilosopherResult{ enemies }; actor_ref.push_night_message(game, message); + + println!("enemies: {}", enemies); } fn controller_parameters_map(self, game: &Game, actor_ref: PlayerReference) -> ControllerParametersMap { @@ -81,4 +90,8 @@ impl Philosopher{ !WinCondition::are_friends(a.win_condition(game), b.win_condition(game)) } } + pub fn players_are_enemies_confused(game: &Game, a: PlayerReference, b: PlayerReference, actor_ref: PlayerReference) -> bool { + Detective::player_is_suspicious_confused(game, a, actor_ref) ^ + Detective::player_is_suspicious_confused(game, b, actor_ref) + } } \ No newline at end of file diff --git a/server/src/game/role/tally_clerk.rs b/server/src/game/role/tally_clerk.rs index ea7d0967e..138dad1a6 100644 --- a/server/src/game/role/tally_clerk.rs +++ b/server/src/game/role/tally_clerk.rs @@ -30,21 +30,14 @@ impl RoleStateImpl for TallyClerk { .filter(|player|player.alive(game)) .filter(|player|VerdictsToday::player_guiltied_today(game, player)) { - if TallyClerk::player_is_suspicious(game, player){ + if Confused::is_confused(game, actor_ref) { + if Self::player_is_suspicious_confused(game, player, actor_ref) { + evil_count += 1; + } + } else if Self::player_is_suspicious(game, player) { evil_count += 1; } } - - if Confused::is_confused(game, actor_ref){ - let total_guilties = VerdictsToday::guilties(game).len(); - //add or subtract 1 randomly from the count - if rand::random::(){ - evil_count = (evil_count.saturating_add(1u8)).min(total_guilties as u8); - }else{ - evil_count = (evil_count.saturating_sub(1u8)).max(0); - } - } - let message = ChatMessageVariant::TallyClerkResult{ evil_count }; actor_ref.push_night_message(game, message); @@ -53,7 +46,6 @@ impl RoleStateImpl for TallyClerk { impl TallyClerk { pub fn player_is_suspicious(game: &Game, player_ref: PlayerReference) -> bool { - if player_ref.has_suspicious_aura(game){ true }else if player_ref.has_innocent_aura(game){ @@ -62,4 +54,8 @@ impl TallyClerk { !player_ref.win_condition(game).is_loyalist_for(GameConclusion::Town) } } + pub fn player_is_suspicious_confused(game: &Game, player_ref: PlayerReference, actor_ref: PlayerReference) -> bool { + actor_ref.night_framed(game) || + Confused::is_red_herring(game, actor_ref, player_ref) + } } \ No newline at end of file diff --git a/server/src/game/role_list.rs b/server/src/game/role_list.rs index 7edcef004..6758906fc 100644 --- a/server/src/game/role_list.rs +++ b/server/src/game/role_list.rs @@ -7,7 +7,7 @@ use vec1::{ use crate::vec_set::VecSet; -use super::{components::insider_group::InsiderGroupID, game_conclusion::GameConclusion, role::Role}; +use super::{components::insider_group::InsiderGroupID, game_conclusion::GameConclusion, role::Role, win_condition::WinCondition}; #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct RoleList(pub Vec); @@ -43,7 +43,17 @@ pub struct RoleAssignment { pub win_condition: RoleOutlineOptionWinCondition } - +impl RoleAssignment { + pub fn is_evil(&self)-> bool{ + match self.win_condition.clone() { + RoleOutlineOptionWinCondition::GameConclusionReached { win_if_any } => + !WinCondition::GameConclusionReached{win_if_any: win_if_any.into()}.friends_with_resolution_state(GameConclusion::Town), + RoleOutlineOptionWinCondition::RoleDefault => { + !RoleSet::Town.get_roles().contains(&self.role) + }, + } + } +} #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct RoleOutline { diff --git a/server/src/vec_map.rs b/server/src/vec_map.rs index a67d5211c..9a732ff38 100644 --- a/server/src/vec_map.rs +++ b/server/src/vec_map.rs @@ -1,3 +1,5 @@ + + use serde::{Deserialize, Serialize}; #[derive(Clone, Debug)] @@ -21,10 +23,12 @@ impl VecMap where K: Eq { } out } + pub fn with_capacity(capacity: usize) -> Self{ + VecMap { vec: Vec::with_capacity(capacity) } + } /// returns the old value if the key already exists pub fn insert(&mut self, key: K, value: V) -> Option<(K, V)>{ - if let Some((old_key, old_val)) = self.vec.iter_mut().find(|(k, _)| *k == key) { Some(( std::mem::replace(old_key, key), @@ -36,6 +40,13 @@ impl VecMap where K: Eq { } } + pub fn insert_unsized(&mut self, key: K, value: V) { + if let Some(i) = self.vec.iter().position(|(k, _)| *k == key) { + self.vec.swap_remove(i); + } + self.vec.push((key, value)); + } + pub fn get(&self, key: &K) -> Option<&V> { self.get_kvp(key).map(|(_, v)| v) } @@ -66,18 +77,12 @@ impl VecMap where K: Eq { } pub fn remove(&mut self, key: &K) -> Option<(K, V)> { - let mut index = None; for (i, (k, _)) in self.vec.iter().enumerate() { if k == key { - index = Some(i); - break; + return Some(self.vec.swap_remove(i)); } } - if let Some(i) = index { - Some(self.vec.remove(i)) - } else { - None - } + return None; } pub fn iter(&self) -> impl Iterator { @@ -104,6 +109,10 @@ impl VecMap where K: Eq { self.vec.retain(|(k, v)| f(k, v)); } + pub fn retain_mut(&mut self, mut f: F) where F: FnMut(&K, &mut V) -> bool { + self.vec.retain_mut(|(k, v)| f(k, v)); + } + pub fn keys(&self) -> impl Iterator { self.vec.iter().map(|(k, _)| k) } @@ -120,6 +129,80 @@ impl VecMap where K: Eq { } } +impl VecMap where K: Eq, V: PartialOrd { + /// Returns the old value if the key already exists, the last value of the tuple is the true if the old value was replaced + /// If the old value is greater than value, then it is not replaced + pub fn keep_greater(&mut self, key: K, value: V) -> Option<(K, V, bool)>{ + if let Some((old_key, old_val)) = self.vec.iter_mut().find(|(k, _)| *k == key) { + if *old_val > value { + Some(( + key, + value, + false, + )) + } else { + Some(( + std::mem::replace(old_key, key), + std::mem::replace(old_val, value), + true, + )) + } + + }else{ + self.vec.push((key, value)); + None + } + } + + /// If the old value is greater than value, then it is not replaced + pub fn keep_greater_unsized(&mut self, key: K, value: V) { + if let Some(i) = self.vec.iter().position(|(k, _)| *k == key) { + if self.vec[i].1 < value { + self.vec.swap_remove(i); + self.vec.push((key, value)); + } + return; + } + self.vec.push((key, value)); + } + + /// Returns the old value if the key already exists, the last value of the tuple is the true if the old value was replaced + /// If the old value is lesser than value, then it is not replaced + pub fn keep_lesser(&mut self, key: K, value: V) -> Option<(K, V, bool)>{ + if let Some((old_key, old_val)) = self.vec.iter_mut().find(|(k, _)| *k == key) { + if *old_val < value { + Some(( + key, + value, + false, + )) + } else { + Some(( + std::mem::replace(old_key, key), + std::mem::replace(old_val, value), + true, + )) + } + + }else{ + self.vec.push((key, value)); + None + } + } + + /// If the old value is lesser than value, then it is not replaced + pub fn keep_lesser_unsized(&mut self, key: K, value: V) { + if let Some(i) = self.vec.iter().position(|(k, _)| *k == key) { + if self.vec[i].1 > value { + self.vec.swap_remove(i); + self.vec.push((key, value)); + } + return; + } + self.vec.push((key, value)); + } +} + impl PartialEq for VecMap where K: Eq, V: Eq { fn eq(&self, other: &Self) -> bool { self.vec.iter().all(|(k, v)| other.get(k) == Some(v)) diff --git a/server/src/vec_set.rs b/server/src/vec_set.rs index 85f4adb69..307a30182 100644 --- a/server/src/vec_set.rs +++ b/server/src/vec_set.rs @@ -1,3 +1,5 @@ +use std::{collections::HashSet, hash::Hash}; + use serde::{Deserialize, Serialize}; use crate::vec_map::VecMap; @@ -123,6 +125,16 @@ impl Ord for VecSet { } } +impl Into> for VecSet where K: Eq+Hash{ + fn into(self) -> HashSet { + let mut hash_set = HashSet::with_capacity(self.len()); + for key in self.vec { + hash_set.insert(key.0); + } + hash_set + } +} + pub use macros::vec_set; mod macros { #[macro_export] diff --git a/server/tests/kit/player.rs b/server/tests/kit/player.rs index d88610231..e9750a55a 100644 --- a/server/tests/kit/player.rs +++ b/server/tests/kit/player.rs @@ -170,4 +170,12 @@ impl From for Vec { fn from(value: TestPlayer) -> Self { vec![value] } -} \ No newline at end of file +} + +impl PartialEq for TestPlayer { + fn eq(&self, other: &TestPlayer) -> bool{ + return self.0 == other.0 + } +} + +impl Eq for TestPlayer {} \ No newline at end of file diff --git a/server/tests/role.rs b/server/tests/role.rs index 0182a0778..7a2a2ee60 100644 --- a/server/tests/role.rs +++ b/server/tests/role.rs @@ -1,10 +1,11 @@ mod kit; use std::{ops::Deref, vec}; - +use kit::player::TestPlayer; pub(crate) use kit::{assert_contains, assert_not_contains}; -use mafia_server::game::{ability_input::{ability_selection::AbilitySelection, ControllerID, RoleOptionSelection}, game_conclusion::GameConclusion, role::engineer::Trap}; +use mafia_server::game::{ability_input::{ability_selection::AbilitySelection, ControllerID, RoleOptionSelection}, components::{confused::Confused, drunk_aura::DrunkAura}, game_conclusion::GameConclusion, role::{engineer::Trap, reeducator::Reeducator}}; + pub use mafia_server::game::{ chat::{ChatMessageVariant, MessageSender, ChatGroup}, grave::*, @@ -152,7 +153,7 @@ fn detective_basic() { game.next_phase(); assert_contains!( sher.get_messages_after_night(1), - ChatMessageVariant::SheriffResult { suspicious: true } + ChatMessageVariant::DetectiveResult { suspicious: true } ); game.skip_to(Night, 2); @@ -160,7 +161,7 @@ fn detective_basic() { game.next_phase(); assert_contains!( sher.get_messages_after_night(2), - ChatMessageVariant::SheriffResult { suspicious: false } + ChatMessageVariant::DetectiveResult { suspicious: false } ); } @@ -178,7 +179,7 @@ fn detective_neutrals(){ game.next_phase(); assert_contains!( sher.get_messages_after_night(1), - ChatMessageVariant::SheriffResult { suspicious: true } + ChatMessageVariant::DetectiveResult { suspicious: true } ); game.skip_to(Night, 2); @@ -186,7 +187,7 @@ fn detective_neutrals(){ game.next_phase(); assert_contains!( sher.get_messages_after_night(2), - ChatMessageVariant::SheriffResult { suspicious: false } + ChatMessageVariant::DetectiveResult { suspicious: false } ); game.skip_to(Night, 3); @@ -194,7 +195,7 @@ fn detective_neutrals(){ game.next_phase(); assert_contains!( sher.get_messages_after_night(3), - ChatMessageVariant::SheriffResult { suspicious: true } + ChatMessageVariant::DetectiveResult { suspicious: true } ); } @@ -296,7 +297,7 @@ fn detective_godfather() { ); sher.send_ability_input_player_list_typical(mafia); game.next_phase(); - assert_contains!(sher.get_messages(), ChatMessageVariant::SheriffResult { suspicious: false }); + assert_contains!(sher.get_messages(), ChatMessageVariant::DetectiveResult { suspicious: false }); } #[test] @@ -314,7 +315,7 @@ fn philosopher_basic() { game.next_phase(); assert_contains!( philosopher.get_messages_after_night(1), - ChatMessageVariant::SeerResult { enemies: true } + ChatMessageVariant::PhilosopherResult { enemies: true } ); game.skip_to(Night, 2); @@ -323,7 +324,7 @@ fn philosopher_basic() { game.next_phase(); assert_contains!( philosopher.get_messages_after_night(2), - ChatMessageVariant::SeerResult { enemies: false } + ChatMessageVariant::PhilosopherResult { enemies: false } ); game.skip_to(Night, 3); @@ -332,7 +333,7 @@ fn philosopher_basic() { game.next_phase(); assert_contains!( philosopher.get_messages_after_night(3), - ChatMessageVariant::SeerResult { enemies: false } + ChatMessageVariant::PhilosopherResult { enemies: false } ); } @@ -352,7 +353,7 @@ fn philosopher_neutrals() { game.next_phase(); assert_contains!( philosopher.get_messages_after_night(3), - ChatMessageVariant::SeerResult { enemies: false } + ChatMessageVariant::PhilosopherResult { enemies: false } ); game.skip_to(Night, 4); @@ -361,7 +362,7 @@ fn philosopher_neutrals() { game.next_phase(); assert_contains!( philosopher.get_messages_after_night(4), - ChatMessageVariant::SeerResult { enemies: false } + ChatMessageVariant::PhilosopherResult { enemies: false } ); game.skip_to(Night, 6); @@ -370,7 +371,7 @@ fn philosopher_neutrals() { game.next_phase(); assert_contains!( philosopher.get_messages_after_night(6), - ChatMessageVariant::SeerResult { enemies: true } + ChatMessageVariant::PhilosopherResult { enemies: true } ); game.skip_to(Night, 7); @@ -379,7 +380,7 @@ fn philosopher_neutrals() { game.next_phase(); assert_contains!( philosopher.get_messages_after_night(7), - ChatMessageVariant::SeerResult { enemies: false } + ChatMessageVariant::PhilosopherResult { enemies: false } ); game.skip_to(Night, 8); @@ -388,7 +389,7 @@ fn philosopher_neutrals() { game.next_phase(); assert_contains!( philosopher.get_messages_after_night(8), - ChatMessageVariant::SeerResult { enemies: false } + ChatMessageVariant::PhilosopherResult { enemies: false } ); } @@ -628,15 +629,15 @@ fn transporter_basic_seer_sheriff_framer() { game.skip_to(Obituary, 2); assert_contains!( philosopher.get_messages_after_night(1), - ChatMessageVariant::SeerResult { enemies: true } + ChatMessageVariant::PhilosopherResult { enemies: true } ); assert_contains!( town1.get_messages_after_night(1), - ChatMessageVariant::SheriffResult { suspicious: false } + ChatMessageVariant::DetectiveResult { suspicious: false } ); assert_contains!( town2.get_messages_after_night(1), - ChatMessageVariant::SheriffResult { suspicious: true } + ChatMessageVariant::DetectiveResult { suspicious: true } ); } @@ -703,7 +704,7 @@ fn retributionist_basic(){ assert_contains!( ret.get_messages_after_night(4), ChatMessageVariant::TargetsMessage{message: Box::new( - ChatMessageVariant::SheriffResult{ suspicious: true } + ChatMessageVariant::DetectiveResult{ suspicious: true } )} ); @@ -713,7 +714,7 @@ fn retributionist_basic(){ assert_contains!( ret.get_messages_after_night(5), ChatMessageVariant::TargetsMessage{message: Box::new( - ChatMessageVariant::SheriffResult{ suspicious: true } + ChatMessageVariant::DetectiveResult{ suspicious: true } )} ); @@ -723,7 +724,7 @@ fn retributionist_basic(){ assert_not_contains!( ret.get_messages_after_night(6), ChatMessageVariant::TargetsMessage{message: Box::new( - ChatMessageVariant::SheriffResult{ suspicious: true } + ChatMessageVariant::DetectiveResult{ suspicious: true } )} ); } @@ -768,7 +769,7 @@ fn witch_basic(){ assert!(witch.send_ability_input_two_player_typical(sher, mafioso)); game.next_phase(); assert_contains!(witch.get_messages(), ChatMessageVariant::TargetsMessage{message: Box::new( - ChatMessageVariant::SheriffResult{ suspicious: true } + ChatMessageVariant::DetectiveResult{ suspicious: true } )}); game.skip_to(Night, 2); @@ -778,7 +779,7 @@ fn witch_basic(){ assert_contains!( witch.get_messages_after_night(2), ChatMessageVariant::TargetsMessage{message: Box::new( - ChatMessageVariant::SeerResult { enemies: false } + ChatMessageVariant::PhilosopherResult { enemies: false } )} ); } @@ -1262,10 +1263,65 @@ fn drunk_suspicious_aura() { assert_contains!( detective.get_messages(), - ChatMessageVariant::SheriffResult { suspicious: true } + ChatMessageVariant::DetectiveResult { suspicious: true } ); } +#[test] +fn drunk_confused_and_drunk_aura() { + for _ in 0..20 { + kit::scenario!(game in Night 2 where + drunk: Drunk, + reed: Reeducator, + townie: Deputy + ); + + let red_herring; + match Confused::get_confusion_data(&*game, drunk.player_ref()) { + Some(value) => { + assert!(value.red_herrings.len() == 1); + assert!(value.red_herrings.first().is_some_and(|rh| + (*rh == reed.player_ref()) ^ (*rh == townie.player_ref()) + )); + assert!(value.confused); + red_herring = value.red_herrings[0]; + }, + None => unreachable!() + } + + assert!(Confused::is_confused(&*game, drunk.player_ref())); + assert!(!Confused::is_confused(&*game, reed.player_ref())); + assert!(!Confused::is_confused(&*game, townie.player_ref())); + + assert!(DrunkAura::has_drunk_aura(&*game, drunk.player_ref())); + assert!(!DrunkAura::has_drunk_aura(&*game, reed.player_ref())); + assert!(!DrunkAura::has_drunk_aura(&*game, townie.player_ref())); + + reed.send_ability_input_player_list_typical(drunk); + game.next_phase(); + + match game.confused.0.get(&drunk.player_ref()) { + Some(value) => { + assert!(value.red_herrings.len() == 1); + assert!(value.red_herrings.first().is_some_and(|rh| + (*rh == reed.player_ref()) ^ (*rh == townie.player_ref()) + )); + assert!(!value.confused); + assert!(red_herring == value.red_herrings[0]); + }, + None => unreachable!() + } + + assert!(!Confused::is_confused(&*game, drunk.player_ref())); + assert!(!Confused::is_confused(&*game, reed.player_ref())); + assert!(!Confused::is_confused(&*game, townie.player_ref())); + + assert!(!DrunkAura::has_drunk_aura(&*game, drunk.player_ref())); + assert!(!DrunkAura::has_drunk_aura(&*game, reed.player_ref())); + assert!(!DrunkAura::has_drunk_aura(&*game, townie.player_ref())); + } +} + #[test] fn drunk_framer() { kit::scenario!(game in Night 2 where @@ -1328,6 +1384,203 @@ fn drunk_role_change() { ); } +#[test] +fn red_herrings() { + for _ in 0..20 { + kit::scenario!(game in Dusk 1 where + detective: Detective, + gossip: Gossip, + philosopher: Philosopher, + //Has to be a doctor so they can visit themself + tester: Doctor, + mafia: Informant + ); + + Confused::add_player(&mut game, detective.player_ref()); + Confused::add_player(&mut game, gossip.player_ref()); + Confused::add_player(&mut game, philosopher.player_ref()); + + assert!(Confused::is_confused(&*game, detective.player_ref())); + assert!(Confused::is_confused(&*game, gossip.player_ref())); + assert!(Confused::is_confused(&*game, philosopher.player_ref())); + + let mut found_detective_red_herring = false; + let mut found_gossip_red_herring = false; + let mut found_philosopher_red_herring = false; + let mut philosopher_red_herring_is_tester = false; + + for index in 0u8..5 { + let target; + //The unsafe part is the fact that the player index is next ensured to be a player. + //this is fine as only indices 0 to 4 would be allowed which is what is going on here. + unsafe { + target = TestPlayer::new(PlayerReference::new_unchecked(index), &*game); + } + + detective.send_ability_input_player_list_typical(target); + gossip.send_ability_input_player_list_typical(tester); + philosopher.send_ability_input_two_player_typical(target, tester); + tester.send_ability_input_player_list_typical(target); + mafia.send_ability_input_player_list_typical(target); + + game.skip_to(Obituary, index+2); + + assert!(detective.alive()); + assert!(gossip.alive()); + assert!(philosopher.alive()); + assert!(tester.alive()); + assert!(mafia.alive()); + + let message_before_ability_message = ChatMessageVariant::PhaseChange { phase: PhaseState::Night, day_number: index+1 }; + + /* Test Detective */ + let detective_messages = detective.get_messages_after_last_message(message_before_ability_message.clone()); + + if detective_messages.contains(&ChatMessageVariant::DetectiveResult { suspicious: true }) { + if found_detective_red_herring { + panic!("detective_messages: {:?}", detective_messages); + } else { + found_detective_red_herring = true; + assert!(Confused::is_red_herring(&*game, detective.player_ref(), target.player_ref())); + } + } else { + assert_eq!( + detective_messages.contains(&ChatMessageVariant::DetectiveResult { suspicious: false }), + target != detective + ); + assert!(!Confused::is_red_herring(&*game, detective.player_ref(), target.player_ref())); + } + + /* Test Gossip */ + let gossip_messages = gossip.get_messages_after_last_message(message_before_ability_message.clone()); + + if gossip_messages.contains(&ChatMessageVariant::GossipResult { enemies: true }){ + if found_gossip_red_herring { + panic!("gossip_messages: {:?}", gossip_messages); + } else { + found_gossip_red_herring = true; + assert!(Confused::is_red_herring(&*game, gossip.player_ref(), target.player_ref())) + } + } else { + assert!(gossip_messages.contains(&ChatMessageVariant::GossipResult { enemies: false })); + assert!(!Confused::is_red_herring(&*game, gossip.player_ref(), target.player_ref())); + } + + + /* Test Philosopher */ + let philosopher_messages = philosopher.get_messages_after_last_message(message_before_ability_message.clone()); + + if philosopher_messages.contains(&ChatMessageVariant::PhilosopherResult { enemies: true }){ + if found_philosopher_red_herring { + if !philosopher_red_herring_is_tester { + if game.day_number() > 3 { + panic!("philosopher_messages: {:?}", philosopher_messages); + } else { + philosopher_red_herring_is_tester = true; + } + } + } else { + found_philosopher_red_herring = true; + } + } else { + assert_eq!( + philosopher_messages.contains(&ChatMessageVariant::PhilosopherResult { enemies: false }), + target != philosopher && target != tester + ) + } + } + + assert!(found_detective_red_herring); + assert!(found_gossip_red_herring); + assert!(found_philosopher_red_herring); + } +} + +#[test] +fn red_herrings_framer() { + for _ in 0..20 { + kit::scenario!(game in Dusk 1 where + detective: Detective, + gossip: Gossip, + philosopher: Philosopher, + tester: Doctor, + mafia: Framer + ); + + Confused::add_player(&mut game, detective.player_ref()); + Confused::add_player(&mut game, gossip.player_ref()); + Confused::add_player(&mut game, philosopher.player_ref()); + + assert!(Confused::is_confused(&*game, detective.player_ref())); + assert!(Confused::is_confused(&*game, gossip.player_ref())); + assert!(Confused::is_confused(&*game, philosopher.player_ref())); + + let mut found_philosopher_red_herring = false; + + let philosopher_red_herring_is_tester = Confused::is_red_herring(&*game, philosopher.player_ref(), tester.player_ref()); + + for index in 0..4u8 { + let target; + //The unsafe part is the fact that the player index is next ensured to be a player. + //this is fine as only indices 0 to 4 would be allowed which is what is going on here. + unsafe { + target = TestPlayer::new(PlayerReference::new_unchecked(index), &*game); + } + + detective.send_ability_input_player_list_typical(target); + gossip.send_ability_input_player_list_typical(tester); + philosopher.send_ability_input_two_player_typical(target, tester); + tester.send_ability_input_player_list_typical(target); + mafia.send_ability_input_player_list_typical(target); + + game.skip_to(Obituary, index+2); + + assert!(detective.alive()); + assert!(gossip.alive()); + assert!(philosopher.alive()); + assert!(tester.alive()); + assert!(mafia.alive()); + + let message_before_ability_message = ChatMessageVariant::PhaseChange { phase: PhaseState::Night, day_number: index+1 }; + + /* Test Detective */ + let detective_messages = detective.get_messages_after_last_message(message_before_ability_message.clone()); + assert_eq!( + detective_messages.contains(&ChatMessageVariant::DetectiveResult { suspicious: true }), + target != detective + ); + + /* Test Gossip */ + let gossip_messages = gossip.get_messages_after_last_message(message_before_ability_message.clone()); + assert!(gossip_messages.contains(&ChatMessageVariant::GossipResult { enemies: true })); + + /* Test Philosopher */ + let philosopher_messages = philosopher.get_messages_after_last_message(message_before_ability_message.clone()); + if philosopher_red_herring_is_tester{ + assert_eq!( + philosopher_messages.contains(&ChatMessageVariant::PhilosopherResult { enemies: false }), + target != philosopher && target != tester + ); + assert!(!philosopher_messages.contains(&ChatMessageVariant::PhilosopherResult { enemies: true })); + } else if philosopher_messages.contains(&ChatMessageVariant::PhilosopherResult { enemies: false }) { + if found_philosopher_red_herring { + panic!("philosopher_messages: {}", kit::_format_messages_debug(philosopher_messages)); + } else { + assert!(target != tester); + assert!(target != tester); + found_philosopher_red_herring = true; + } + } else { + assert_eq!( + philosopher_messages.contains(&ChatMessageVariant::PhilosopherResult { enemies: true }), + target != philosopher && target != tester + ) + } + + } + } +} + #[test] fn vigilante_cant_select_night_one() { kit::scenario!(game in Night 1 where @@ -1627,7 +1880,7 @@ fn seer_cant_see_godfather() { philosopher.get_messages_after_last_message( ChatMessageVariant::PhaseChange { phase: PhaseState::Night, day_number: 1 } ), - ChatMessageVariant::SeerResult { enemies: false } + ChatMessageVariant::PhilosopherResult { enemies: false } ); game.skip_to(Night, 2); @@ -1637,7 +1890,7 @@ fn seer_cant_see_godfather() { philosopher.get_messages_after_last_message( ChatMessageVariant::PhaseChange { phase: PhaseState::Night, day_number: 2 } ), - ChatMessageVariant::SeerResult { enemies: false } + ChatMessageVariant::PhilosopherResult { enemies: false } ); } @@ -1861,7 +2114,7 @@ fn puppeteer_marionettes_philosopher(){ game.next_phase(); assert_contains!( philo.get_messages_after_night(1), - ChatMessageVariant::SeerResult{ enemies: true } + ChatMessageVariant::PhilosopherResult{ enemies: true } ); game.skip_to(Night, 3); @@ -1871,7 +2124,7 @@ fn puppeteer_marionettes_philosopher(){ game.next_phase(); assert_contains!( philo.get_messages_after_night(2), - ChatMessageVariant::SeerResult{ enemies: false } + ChatMessageVariant::PhilosopherResult{ enemies: false } ); } @@ -2071,7 +2324,7 @@ fn arsonist_ignites_and_aura(){ assert_contains!(sher.get_messages_after_last_message( ChatMessageVariant::PhaseChange{phase: PhaseState::Night, day_number: 1} - ), ChatMessageVariant::SheriffResult{ suspicious: true }); + ), ChatMessageVariant::DetectiveResult{ suspicious: true }); game.skip_to(Night, 2); @@ -2082,7 +2335,7 @@ fn arsonist_ignites_and_aura(){ assert_contains!(sher.get_messages_after_last_message( ChatMessageVariant::PhaseChange{phase: PhaseState::Night, day_number: 2} - ), ChatMessageVariant::SheriffResult{ suspicious: true }); + ), ChatMessageVariant::DetectiveResult{ suspicious: true }); game.skip_to(Nomination, 3); @@ -2102,7 +2355,7 @@ fn arsonist_ignites_and_aura(){ assert_contains!(sher.get_messages_after_last_message( ChatMessageVariant::PhaseChange{phase: PhaseState::Night, day_number: 3} - ), ChatMessageVariant::SheriffResult{ suspicious: false }); + ), ChatMessageVariant::DetectiveResult{ suspicious: false }); }