#[cfg(feature = "sycamore")] use crate::edit::prelude::*; use crate::util::PartyError; #[cfg(feature = "sycamore")] use crate::view::prelude::*; #[cfg(feature = "sycamore")] use lan_party_macros::{WebEdit, WebView}; use paste::paste; #[cfg(feature = "openapi")] use schemars::JsonSchema; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use std::collections::HashMap; #[derive(Clone, Debug, Default)] #[cfg_attr(feature = "openapi", derive(JsonSchema))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct EventOutcome { pub points: HashMap, } /// # Event /// /// An event in which participants can win or lose points #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "openapi", derive(JsonSchema))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "sycamore", derive(WebView))] pub struct Event { /// Has this event concluded? #[cfg_attr(feature = "serde", serde(default))] pub concluded: bool, /// Name of the event #[cfg_attr(feature = "serde", serde(default))] pub name: String, /// Description of the event #[cfg_attr(feature = "serde", serde(default))] pub description: String, pub event_type: EventType, } impl Event { pub fn outcome(&self) -> EventOutcome { self.event_type.outcome() } pub fn conclude(&self) {} pub fn apply_update(&mut self, update: EventUpdate) -> Result<(), PartyError> { self.event_type.apply_update(update) } } /// # EventSpec /// /// A specification of an event #[derive(Clone, Debug, Default, PartialEq)] #[cfg_attr(feature = "openapi", derive(JsonSchema))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))] pub struct EventSpec { /// Name of the event pub name: String, /// Description of the event pub description: String, pub event_type: EventTypeSpec, } macro_rules! events { ($($module:ident => $name:ident),* $(,)?) => { paste! { /// # EventTypeSpec /// /// A specification of an event type #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "openapi", derive(JsonSchema))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))] pub enum EventTypeSpec { $($name($module::[<$name Spec>]),)* } /// # EventUpdate /// /// An update that can be applied to an event #[derive(Clone, Debug)] #[cfg_attr(feature = "openapi", derive(JsonSchema))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))] pub enum EventUpdate { $($name($module::[<$name Update>]),)* } /// # EventType /// /// An enumeration of event types #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "openapi", derive(JsonSchema))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))] pub enum EventType { $($name($module::$name),)* } impl EventSpec { pub fn create_event(self) -> Result { let event_type = match self.event_type { $(EventTypeSpec::$name(s) => { EventType::$name($module::$name::from_spec(s)) })* }; let event = Event { name: self.name, description: self.description, concluded: false, event_type, }; Ok(event) } } impl EventType { pub fn apply_update(&mut self, update: EventUpdate) -> Result<(), PartyError> { match (self, update) { $((EventType::$name(s), EventUpdate::$name(u)) => { s.apply_update(u) })* _ => Err(PartyError::Unknown("invalid update".into())), } } pub fn outcome(&self) -> EventOutcome { match self { $(EventType::$name(s) => { s.outcome() })* } } } } }; } pub trait EventTrait { type Spec; type Update; fn from_spec(spec: Self::Spec) -> Self; fn apply_update(&mut self, _update: Self::Update) -> Result<(), PartyError> { Ok(()) } fn outcome(&self) -> EventOutcome { EventOutcome::default() } } pub mod test { use super::*; #[derive(Clone, Debug, Default, PartialEq)] #[cfg_attr(feature = "openapi", derive(JsonSchema))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))] pub struct Test { pub num_players: i64, } #[derive(Clone, Debug, Default, PartialEq)] #[cfg_attr(feature = "openapi", derive(JsonSchema))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))] pub struct TestSpec { pub num_players: i64, } #[derive(Clone, Debug, Default)] #[cfg_attr(feature = "openapi", derive(JsonSchema))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))] pub struct TestUpdate { pub win_game: bool, } impl EventTrait for Test { type Spec = TestSpec; type Update = TestUpdate; fn from_spec(spec: TestSpec) -> Self { Test { num_players: spec.num_players, } } fn apply_update(&mut self, update: TestUpdate) -> Result<(), PartyError> { if update.win_game { self.num_players += 1; } Ok(()) } fn outcome(&self) -> EventOutcome { let mut points = HashMap::new(); points.insert("420".into(), self.num_players); EventOutcome { points } } } } pub mod team_game { use super::{ free_for_all_game::{ FreeForAllGame, FreeForAllGameSpec, FreeForAllGameUpdate, FreeForAllGameUpdateParticipants, FreeForAllGameUpdateRanking, FreeForAllGameUpdateRewards, }, *, }; #[derive(Clone, Debug, Default, PartialEq)] #[cfg_attr(feature = "openapi", derive(JsonSchema))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))] pub struct TeamGame { /// Map of teams with a name as key and an array of players as value pub teams: HashMap>, #[cfg_attr(feature = "serde", serde(flatten))] pub ffa_game: FreeForAllGame, } #[derive(Clone, Debug, PartialEq, Default)] #[cfg_attr(feature = "openapi", derive(JsonSchema))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))] pub struct TeamGameSpec { /// Map of teams with a name as key and an array of players as value pub teams: HashMap>, /// Rewards for winning the game (first element for first place, second element for second /// place, etc.) #[cfg_attr( feature = "openapi", schemars(example = "super::free_for_all_game::example_win_rewards") )] pub win_rewards: Vec, /// Rewards for losing the game (first element for last place, second element for second to /// last place, etc.) #[cfg_attr( feature = "openapi", schemars(example = "super::free_for_all_game::example_lose_rewards") )] pub lose_rewards: Vec, } #[derive(Clone, Debug, Default)] #[cfg_attr(feature = "openapi", derive(JsonSchema))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))] pub struct TeamGameUpdateSetTeam { pub team: String, pub members: Vec, } #[derive(Clone, Debug)] #[cfg_attr(feature = "openapi", derive(JsonSchema))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))] pub enum TeamGameUpdateInner { /// Add or replace a team with the given name and array of members SetTeam(TeamGameUpdateSetTeam), /// Remove team with given name RemoveTeam(String), } impl Default for TeamGameUpdateInner { fn default() -> Self { Self::SetTeam(TeamGameUpdateSetTeam::default()) } } #[derive(Clone, Debug)] #[cfg_attr(feature = "openapi", derive(JsonSchema))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", serde(untagged))] #[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))] pub enum TeamGameFfaInheritedUpdate { /// Change the ranking and scores Ranking(FreeForAllGameUpdateRanking), /// Update rewards Rewards(FreeForAllGameUpdateRewards), } impl Default for TeamGameFfaInheritedUpdate { fn default() -> Self { Self::Ranking(FreeForAllGameUpdateRanking::default()) } } #[derive(Clone, Debug)] #[cfg_attr(feature = "openapi", derive(JsonSchema))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", serde(untagged))] #[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))] pub enum TeamGameUpdate { /// # Team /// /// Team specific updates Team(TeamGameUpdateInner), /// # Other /// /// Inherited from FreeForAllGame Ffa(TeamGameFfaInheritedUpdate), } impl Default for TeamGameUpdate { fn default() -> Self { TeamGameUpdate::Team(TeamGameUpdateInner::default()) } } impl EventTrait for TeamGame { type Spec = TeamGameSpec; type Update = TeamGameUpdate; fn from_spec(spec: TeamGameSpec) -> Self { let ffa_game_spec = FreeForAllGameSpec { participants: spec.teams.keys().cloned().collect(), win_rewards: spec.win_rewards, lose_rewards: spec.lose_rewards, }; TeamGame { teams: spec.teams, ffa_game: FreeForAllGame::from_spec(ffa_game_spec), } } fn apply_update(&mut self, update: Self::Update) -> Result<(), PartyError> { match update { TeamGameUpdate::Ffa(update) => match update { TeamGameFfaInheritedUpdate::Ranking(u) => { self.ffa_game.apply_update(FreeForAllGameUpdate::Ranking(u)) } TeamGameFfaInheritedUpdate::Rewards(u) => { self.ffa_game.apply_update(FreeForAllGameUpdate::Rewards(u)) } }, TeamGameUpdate::Team(update) => match update { TeamGameUpdateInner::SetTeam(u) => { self.ffa_game .apply_update(FreeForAllGameUpdate::Participants( FreeForAllGameUpdateParticipants::AddParticipant(u.team.clone()), ))?; self.teams.insert(u.team, u.members); Ok(()) } TeamGameUpdateInner::RemoveTeam(team) => { self.ffa_game .apply_update(FreeForAllGameUpdate::Participants( FreeForAllGameUpdateParticipants::RemoveParticipant(team.clone()), ))?; self.teams.remove(&team); Ok(()) } }, } } fn outcome(&self) -> EventOutcome { let ffa_outcome = self.ffa_game.outcome(); let mut points = HashMap::new(); for (team, reward) in ffa_outcome.points.iter() { if let Some(team) = self.teams.get(team) { for player in team { let score = points.get(player).unwrap_or(&0); points.insert(player.clone(), score + reward); } } } EventOutcome { points } } } } pub mod free_for_all_game { use std::collections::HashSet; use super::*; #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "openapi", derive(JsonSchema))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))] pub enum FreeForAllGameRanking { /// Ranking of participants by user name or team name (first element is first place, second element is second /// place, etc.) Ranking(Vec), /// Score based ranking of participants/teams Scores(HashMap), } impl Default for FreeForAllGameRanking { fn default() -> Self { Self::Ranking(Vec::default()) } } impl FreeForAllGameRanking { pub fn is_valid(&self, participants: &HashSet) -> bool { match self { Self::Ranking(v) => v.iter().all(|p| participants.contains(p)), Self::Scores(m) => m.keys().all(|p| participants.contains(p)), } } } #[derive(Clone, Debug, Default, PartialEq)] #[cfg_attr(feature = "openapi", derive(JsonSchema))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))] pub struct FreeForAllGame { /// Ranking of participants by user name or team name (first element is first place, second element is second /// place, etc.) pub ranking: Option, /// Specification of the game #[cfg_attr(feature = "serde", serde(flatten))] pub spec: FreeForAllGameSpec, } pub fn example_win_rewards() -> Vec { vec![10, 7, 5, 3, 2, 1] } pub fn example_lose_rewards() -> Vec { vec![-3, -2, -1] } #[derive(Clone, Debug, PartialEq, Default)] #[cfg_attr(feature = "openapi", derive(JsonSchema))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))] pub struct FreeForAllGameSpec { /// Array of user ids that participate in the game pub participants: HashSet, /// Rewards for winning the game (first element for first place, second element for second /// place, etc.) #[cfg_attr(feature = "schemars", schemars(example = "example_win_rewards"))] pub win_rewards: Vec, /// Rewards for losing the game (first element for last place, second element for second to /// last place, etc.) #[cfg_attr(feature = "schemars", schemars(example = "example_lose_rewards"))] pub lose_rewards: Vec, } #[derive(Clone, Debug)] #[cfg_attr(feature = "openapi", derive(JsonSchema))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))] pub enum FreeForAllGameUpdateRanking { /// Replace the current ranking with the given ranking SetRanking(FreeForAllGameRanking), /// If the current ranking is of type `Scores`, apply the given score deltas ScoreDelta(HashMap), } impl Default for FreeForAllGameUpdateRanking { fn default() -> Self { Self::SetRanking(FreeForAllGameRanking::default()) } } #[derive(Clone, Debug)] #[cfg_attr(feature = "openapi", derive(JsonSchema))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))] pub enum FreeForAllGameUpdateRewards { /// Set rewards for winning the game SetWinRewards(Vec), /// Set rewards for losing the game SetLoseRewards(Vec), } impl Default for FreeForAllGameUpdateRewards { fn default() -> Self { Self::SetWinRewards(Vec::default()) } } #[derive(Clone, Debug)] #[cfg_attr(feature = "openapi", derive(JsonSchema))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))] pub enum FreeForAllGameUpdateParticipants { /// Set list of participants participating in the game SetParticipants(HashSet), /// Add participant by name AddParticipant(String), /// Remove participant by name RemoveParticipant(String), } impl Default for FreeForAllGameUpdateParticipants { fn default() -> Self { Self::SetParticipants(HashSet::default()) } } #[derive(Clone, Debug)] #[cfg_attr(feature = "openapi", derive(JsonSchema))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", serde(untagged))] #[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))] pub enum FreeForAllGameUpdate { /// Change the ranking and scores Ranking(FreeForAllGameUpdateRanking), /// Update rewards Rewards(FreeForAllGameUpdateRewards), /// Update participants Participants(FreeForAllGameUpdateParticipants), } impl Default for FreeForAllGameUpdate { fn default() -> Self { Self::Ranking(FreeForAllGameUpdateRanking::default()) } } impl EventTrait for FreeForAllGame { type Spec = FreeForAllGameSpec; type Update = FreeForAllGameUpdate; fn from_spec(spec: Self::Spec) -> Self { FreeForAllGame { ranking: None, spec, } } fn apply_update(&mut self, update: Self::Update) -> Result<(), PartyError> { match update { FreeForAllGameUpdate::Ranking(update) => match update { FreeForAllGameUpdateRanking::SetRanking(r) => { if !r.is_valid(&self.spec.participants) { return Err(PartyError::Unknown("invalid ranking, all participants mentioned in ranking must be participating".into())); } self.ranking = Some(r) } FreeForAllGameUpdateRanking::ScoreDelta(d) => match &mut self.ranking { Some(FreeForAllGameRanking::Ranking(_)) | None => { return Err(PartyError::Unknown("cannot apply score delta".into())) } Some(FreeForAllGameRanking::Scores(s)) => { for (participant, delta) in d.iter() { if let Some(value) = s.get(participant) { s.insert(participant.clone(), value + delta); } } } }, }, FreeForAllGameUpdate::Participants(update) => match update { FreeForAllGameUpdateParticipants::AddParticipant(name) => { self.spec.participants.insert(name); } FreeForAllGameUpdateParticipants::RemoveParticipant(name) => { self.spec.participants.remove(&name); if !self .ranking .as_ref() .map(|r| r.is_valid(&self.spec.participants)) .unwrap_or(true) { self.spec.participants.insert(name); return Err(PartyError::Unknown("cannot remove participant, all participants mentioned in ranking must be participating".into())); } } FreeForAllGameUpdateParticipants::SetParticipants(participants) => { if !self .ranking .as_ref() .map(|r| r.is_valid(&participants)) .unwrap_or(true) { return Err(PartyError::Unknown("invalid list of participants, all participants mentioned in ranking must be participating".into())); } self.spec.participants = participants; } }, FreeForAllGameUpdate::Rewards(update) => match update { FreeForAllGameUpdateRewards::SetWinRewards(rewards) => { self.spec.win_rewards = rewards; } FreeForAllGameUpdateRewards::SetLoseRewards(rewards) => { self.spec.lose_rewards = rewards; } }, } Ok(()) } fn outcome(&self) -> EventOutcome { let ranking = match &self.ranking { Some(FreeForAllGameRanking::Ranking(r)) => r.clone(), Some(FreeForAllGameRanking::Scores(s)) => { let mut results: Vec<(_, _)> = s.iter().collect(); results.sort_by(|a, b| b.1.cmp(a.1)); results.into_iter().map(|(k, _)| k.clone()).collect() } None => return EventOutcome::default(), }; let mut points = HashMap::new(); for (participant, reward) in ranking.iter().zip(self.spec.win_rewards.iter()) { let score = points.get(participant).unwrap_or(&0); points.insert(participant.clone(), score + reward); } for (participant, reward) in ranking.iter().rev().zip(self.spec.lose_rewards.iter()) { let score = points.get(participant).unwrap_or(&0); points.insert(participant.clone(), score + reward); } EventOutcome { points } } } } events!(test => Test, team_game => TeamGame, free_for_all_game => FreeForAllGame); impl Default for EventTypeSpec { fn default() -> Self { Self::Test(test::TestSpec::default()) } } impl Default for EventUpdate { fn default() -> Self { Self::Test(test::TestUpdate::default()) } }