#[cfg(feature = "sycamore")] use crate::edit::prelude::*; #[cfg(feature = "sycamore")] use crate::view::prelude::*; use crate::{ state::Users, util::{PartyError, WithContext}, }; #[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, HashSet}, hash::Hash, }; type User = WithContext; type Team = String; #[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 Ranking { /// 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 Ranking { fn default() -> Self { Self::Ranking(Vec::default()) } } impl Ranking { 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)] #[cfg_attr(feature = "openapi", derive(JsonSchema))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct EventOutcome { pub points: HashMap, } impl Default for EventOutcome { fn default() -> Self { Self { points: HashMap::::default(), } } } /// # 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))] #[cfg_attr(feature = "sycamore", view(title = "name"))] 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 { if self.name.is_empty() { return Err(PartyError::Other("invalid name".into())) } 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::Other("invalid update: update type does not match event type".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".to_string().into(), self.num_players); EventOutcome { points } } } } pub mod team_game { use std::ops::Deref; use super::{ free_for_all_game::{FreeForAllGame, FreeForAllGameSpec, FreeForAllGameUpdate}, *, }; #[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: Team, pub members: Vec, } #[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 { /// Add or replace a team with the given name and array of members SetTeam(TeamGameUpdateSetTeam), /// Remove team with given name RemoveTeam(Team), /// Replace the current ranking with the given ranking SetRanking(Ranking), /// If the current ranking is of type `Scores`, apply the given score deltas ScoreDelta(HashMap), /// Set rewards for winning the game SetWinRewards(Vec), /// Set rewards for losing the game SetLoseRewards(Vec), } impl Default for TeamGameUpdate { fn default() -> Self { TeamGameUpdate::SetTeam(TeamGameUpdateSetTeam::default()) } } impl From> for Ranking { fn from(r: Ranking) -> Self { match r { Ranking::Scores(s) => { Ranking::Scores(s.into_iter().map(|(k, v)| (k.into(), v)).collect()) } Ranking::Ranking(s) => Ranking::Ranking(s.into_iter().map(|k| k.into()).collect()), } } } 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().map(|k| User::from(k.clone())).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::SetRanking(x) => self .ffa_game .apply_update(FreeForAllGameUpdate::SetRanking(x.into())), TeamGameUpdate::ScoreDelta(x) => { self.ffa_game.apply_update(FreeForAllGameUpdate::ScoreDelta( x.into_iter().map(|(k, v)| (k.into(), v)).collect(), )) } TeamGameUpdate::SetWinRewards(x) => self .ffa_game .apply_update(FreeForAllGameUpdate::SetWinRewards(x)), TeamGameUpdate::SetLoseRewards(x) => self .ffa_game .apply_update(FreeForAllGameUpdate::SetLoseRewards(x)), TeamGameUpdate::SetTeam(u) => { self.ffa_game .apply_update(FreeForAllGameUpdate::AddParticipant( u.team.clone().into(), ))?; self.teams.insert( u.team, u.members .into_iter() .map(|t| (t.clone()).deref().clone().into()) .collect(), ); Ok(()) } TeamGameUpdate::RemoveTeam(team) => { self.ffa_game .apply_update(FreeForAllGameUpdate::RemoveParticipant( team.clone().into(), ))?; 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.to_string()) { for player in team { let score = points.get(&player.clone().into()).unwrap_or(&0); points.insert(User::from(player.clone()), score + reward); } } } EventOutcome { points } } } } pub mod free_for_all_game { use std::collections::HashSet; use super::*; /* #[derive(Clone, Debug, Default, PartialEq, Eq, Hash)] #[cfg_attr(feature = "openapi", derive(JsonSchema))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))] pub struct User { name: String, } */ #[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 = "serde", serde(untagged))] #[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))] pub enum FreeForAllGameUpdate { /// Replace the current ranking with the given ranking SetRanking(Ranking), /// If the current ranking is of type `Scores`, apply the given score deltas ScoreDelta(HashMap), /// Set rewards for winning the game SetWinRewards(Vec), /// Set rewards for losing the game SetLoseRewards(Vec), /// Set list of participants participating in the game SetParticipants(HashSet), /// Add participant by name AddParticipant(User), /// Remove participant by name RemoveParticipant(User), } impl Default for FreeForAllGameUpdate { fn default() -> Self { Self::AddParticipant(String::new().into()) } } 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::SetRanking(r) => { if !r.is_valid(&self.spec.participants) { return Err(PartyError::Other("invalid ranking, all participants mentioned in ranking must be participating".into())); } self.ranking = Some(r) } FreeForAllGameUpdate::ScoreDelta(d) => match &mut self.ranking { Some(Ranking::Ranking(_)) => { return Err(PartyError::Other("cannot apply score delta".into())) } None => self.ranking = Some(Ranking::Scores(d)), Some(Ranking::Scores(s)) => { for (participant, delta) in d.iter() { let value = s.get(participant).unwrap_or(&0); s.insert(participant.clone(), value + delta); } } }, FreeForAllGameUpdate::AddParticipant(name) => { self.spec.participants.insert(name); } FreeForAllGameUpdate::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::Other("cannot remove participant, all participants mentioned in ranking must be participating".into())); } } FreeForAllGameUpdate::SetParticipants(participants) => { if !self .ranking .as_ref() .map(|r| r.is_valid(&participants)) .unwrap_or(true) { return Err(PartyError::Other("invalid list of participants, all participants mentioned in ranking must be participating".into())); } self.spec.participants = participants; } FreeForAllGameUpdate::SetWinRewards(rewards) => { self.spec.win_rewards = rewards; } FreeForAllGameUpdate::SetLoseRewards(rewards) => { self.spec.lose_rewards = rewards; } } Ok(()) } fn outcome(&self) -> EventOutcome { let ranking = match &self.ranking { Some(Ranking::Ranking(r)) => r.clone(), Some(Ranking::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()) } }