use std::{collections::HashMap, future, hash::Hash}; use futures::{StreamExt, TryStreamExt}; use rocket_db_pools::mongodb::bson::to_bson; use super::{prelude::*, util::PartyError}; use paste::paste; api_routes!( create_event, stop_event, get_event, update_event, get_all_events, event_outcome, ); #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] #[serde(crate = "rocket::serde")] pub struct EventOutcome { points: HashMap, } impl EventOutcome { pub async fn apply(&self, db: &Connection) -> Result<(), PartyError> { for (player, reward) in self.points.iter() { db.users() .update_one( doc! { "id": player }, doc! { "$inc": { "score": reward } }, None, ) .await?; } Ok(()) } } /// # Event /// /// An event in which participants can win or lose points #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] #[serde(crate = "rocket::serde")] pub struct Event { /// Unique identifier of the event. This will be randomly generated and cannot be modified. #[serde(default = "uuid")] id: String, /// Has this event concluded? #[serde(default)] concluded: bool, /// Name of the event #[serde(default)] name: String, /// Description of the event #[serde(default)] description: String, /// Event type 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) } } fn uuid() -> String { uuid::Uuid::new_v4().to_string() } /// # EventSpec /// /// A specification of an event #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] #[serde(crate = "rocket::serde")] pub struct EventSpec { name: String, description: String, event_type: EventTypeSpec, } macro_rules! events { ($($module:ident => $name:ident),* $(,)?) => { paste! { /// # EventTypeSpec /// /// A specification of an event type #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] #[serde(crate = "rocket::serde")] pub enum EventTypeSpec { $($name($module::[<$name Spec>]),)* } /// # EventUpdate /// /// An update that can be applied to an event #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] #[serde(crate = "rocket::serde")] pub enum EventUpdate { $($name($module::[<$name Update>]),)* } /// # EventType /// /// An enumeration of event types #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] #[serde(crate = "rocket::serde")] 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 { id: uuid(), 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() } } mod test { use super::*; #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] #[serde(crate = "rocket::serde")] pub struct Test { num_players: i64, } #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] #[serde(crate = "rocket::serde")] pub struct TestSpec { num_players: i64, } #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] #[serde(crate = "rocket::serde")] pub struct TestUpdate { 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 } } } } mod team_game { use super::{ free_for_all_game::{ FreeForAllGame, FreeForAllGameSpec, FreeForAllGameUpdate, FreeForAllGameUpdateParticipants, FreeForAllGameUpdateRanking, FreeForAllGameUpdateRewards, }, *, }; #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] #[serde(crate = "rocket::serde")] pub struct TeamGame { /// Map of teams with a name as key and an array of players as value teams: HashMap>, #[serde(flatten)] ffa_game: FreeForAllGame, } #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] #[serde(crate = "rocket::serde")] pub struct TeamGameSpec { /// Map of teams with a name as key and an array of players as value teams: HashMap>, /// Rewards for winning the game (first element for first place, second element for second /// place, etc.) #[schemars(example = "super::free_for_all_game::example_win_rewards")] win_rewards: Vec, /// Rewards for losing the game (first element for last place, second element for second to /// last place, etc.) #[schemars(example = "super::free_for_all_game::example_lose_rewards")] lose_rewards: Vec, } #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] #[serde(crate = "rocket::serde")] pub enum TeamGameUpdateInner { /// Add or replace a team with the given name and array of members SetTeam { team: String, members: Vec }, /// Remove team with given name RemoveTeam(String), } #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] #[serde(crate = "rocket::serde")] #[serde(untagged)] pub enum TeamGameFfaInheritedUpdate { /// Change the ranking and scores Ranking(FreeForAllGameUpdateRanking), /// Update rewards Rewards(FreeForAllGameUpdateRewards), } #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] #[serde(crate = "rocket::serde")] #[serde(untagged)] pub enum TeamGameUpdate { /// Team specific updates Team(TeamGameUpdateInner), /// Inherited from FreeForAllGame Ffa(TeamGameFfaInheritedUpdate), } 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 { team, members } => { self.ffa_game .apply_update(FreeForAllGameUpdate::Participants( FreeForAllGameUpdateParticipants::AddParticipant(team.clone()), ))?; self.teams.insert(team, 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 } } } } mod free_for_all_game { use std::collections::HashSet; use super::*; #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] #[serde(crate = "rocket::serde")] pub enum FreeForAllGameRanking { /// Ranking of participants by user id 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 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, Serialize, Deserialize, JsonSchema)] #[serde(crate = "rocket::serde")] pub struct FreeForAllGame { /// Ranking of participants by user id or team name (first element is first place, second element is second /// place, etc.) ranking: Option, /// Specification of the game #[serde(flatten)] 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, Serialize, Deserialize, JsonSchema)] #[serde(crate = "rocket::serde")] pub struct FreeForAllGameSpec { /// Array of user ids that participate in the game pub(crate) participants: HashSet, /// Rewards for winning the game (first element for first place, second element for second /// place, etc.) #[schemars(example = "example_win_rewards")] pub(crate) win_rewards: Vec, /// Rewards for losing the game (first element for last place, second element for second to /// last place, etc.) #[schemars(example = "example_lose_rewards")] pub(crate) lose_rewards: Vec, } #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] #[serde(crate = "rocket::serde")] 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), } #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] #[serde(crate = "rocket::serde")] pub enum FreeForAllGameUpdateRewards { /// Set rewards for winning the game SetWinRewards(Vec), /// Set rewards for losing the game SetLoseRewards(Vec), } #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] #[serde(crate = "rocket::serde")] pub enum FreeForAllGameUpdateParticipants { /// Set list of participants participating in the game SetParticipants(HashSet), /// Add participant by id AddParticipant(String), /// Remove participant by id RemoveParticipant(String), } #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] #[serde(crate = "rocket::serde")] #[serde(untagged)] pub enum FreeForAllGameUpdate { /// Change the ranking and scores Ranking(FreeForAllGameUpdateRanking), /// Update rewards Rewards(FreeForAllGameUpdateRewards), /// Update participants Participants(FreeForAllGameUpdateParticipants), } 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(id) => { self.spec.participants.insert(id); } FreeForAllGameUpdateParticipants::RemoveParticipant(id) => { self.spec.participants.remove(&id); if !self .ranking .as_ref() .map(|r| r.is_valid(&self.spec.participants)) .unwrap_or(true) { self.spec.participants.insert(id); 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); /// # Create event /// /// If an `id` is supplied, it will be replaced by a randomly generated /// one. /// /// Returns the created event. #[openapi(tag = "Event")] #[post("/", data = "")] pub async fn create_event( _api_key: ApiKey, db: Connection, event: Json, ) -> Result, PartyError> { let event = event.into_inner().create_event()?; db.events().insert_one(&event, None).await?; Ok(Json(event)) } /// # Update event /// /// Update the supplied values in the event with the given id. The `id` of an event cannot be /// changed and `concluded` will always be false. #[openapi(tag = "Event")] #[post("/", data = "")] pub async fn update_event( _api_key: ApiKey, db: Connection, id: String, update: Json, ) -> Result { let mut event = db .events() .find_one(doc! { "id": &id }, None) .await? .ok_or(PartyError::EventNotFound(id.clone()))?; event.apply_update(update.into_inner())?; db.events() .update_one(doc! { "id": &id }, doc! { "$set": to_bson(&event)? }, None) .await?; Ok(Status::Ok) } /// # Get event /// /// Returns the event with the given id #[openapi(tag = "Event")] #[get("/")] pub async fn get_event( _api_key: ApiKey, db: Connection, id: String, ) -> Result, PartyError> { Ok(Json( db.events() .find_one(doc! { "id": &id }, None) .await? .ok_or(PartyError::EventNotFound(id))?, )) } /// # Get events /// /// Returns all events #[openapi(tag = "Event")] #[get("/?")] pub async fn get_all_events( _api_key: ApiKey, db: Connection, concluded: Option, ) -> Result>, PartyError> { let filter = if let Some(concluded) = concluded { doc! { "concluded": concluded } } else { doc! {} }; Ok(Json( db.events() .find(filter, None) .await? .filter(|e| future::ready(e.is_ok())) .try_collect() .await?, )) } /// # Get outcome /// /// Returns the outcome of an event. #[openapi(tag = "Event")] #[get("//outcome")] pub async fn event_outcome( _api_key: ApiKey, db: Connection, id: String, ) -> Result, PartyError> { let event = db .events() .find_one(doc! { "id": &id }, None) .await? .ok_or(PartyError::EventNotFound(id.clone()))?; Ok(Json(event.outcome())) } /// # Stop event /// /// This will conclude the event, apply the outcome to the users and return the outcome. #[openapi(tag = "Event")] #[post("//stop")] pub async fn stop_event( _api_key: ApiKey, db: Connection, id: String, ) -> Result, PartyError> { let event = db .events() .find_one(doc! { "id": &id }, None) .await? .ok_or(PartyError::EventNotFound(id.clone()))?; event.conclude(); db.events() .update_one( doc! { "id": &id }, doc! { "$set": { "concluded": true } }, None, ) .await?; let outcome = event.outcome(); outcome.apply(&db).await?; Ok(Json(outcome)) }