diff --git a/src/api/event.rs b/src/api/event.rs index 05bbb51..b0e7ec1 100644 --- a/src/api/event.rs +++ b/src/api/event.rs @@ -1,11 +1,10 @@ -use std::collections::HashMap; +use std::{collections::HashMap, future, hash::Hash}; -use futures::TryStreamExt; -use rocket::serde::json::json; -use rocket_db_pools::mongodb::{bson::to_bson, options::InsertOneOptions}; -use serde_json::Value; +use futures::{StreamExt, TryStreamExt}; +use rocket_db_pools::mongodb::bson::to_bson; use super::{prelude::*, util::PartyError}; +use paste::paste; api_routes!( create_event, @@ -22,6 +21,21 @@ 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 @@ -32,53 +46,399 @@ pub struct Event { #[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 { - match self.event_type { - _ => EventOutcome::default(), - } + 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() } -/// # EventType +/// # EventSpec /// -/// Enumeration of all different types of events +/// A specification of an event #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] #[serde(crate = "rocket::serde")] -pub enum EventType { - FreeForAllGame {}, - TeamGame {}, - Test {}, +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}, + *, + }; + + #[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 struct TeamGameUpdate { + #[serde(flatten)] + inner: FreeForAllGameUpdate, + } + + impl EventTrait for TeamGame { + type Spec = TeamGameSpec; + type Update = TeamGameUpdate; + + fn from_spec(spec: TeamGameSpec) -> Self { + let players: Vec = spec + .teams + .values() + .map(|v| v.iter().cloned()) + .flatten() + .collect(); + + let ffa_game_spec = FreeForAllGameSpec { + players, + 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> { + self.ffa_game.apply_update(update.inner) + } + + 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 super::*; + + #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] + #[serde(crate = "rocket::serde")] + pub enum FreeForAllGameRanking { + /// Ranking of players by user id (first element is first place, second element is second + /// place, etc.) + Ranking(Vec), + /// Score based ranking of players by user id + Scores(HashMap), + } + + #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] + #[serde(crate = "rocket::serde")] + pub struct FreeForAllGame { + /// Ranking of players by user id (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) players: Vec, + + /// 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 FreeForAllGameUpdate { + /// Replace the current ranking with the given ranking + Ranking(FreeForAllGameRanking), + + /// If the current ranking is of type `Scores`, apply the given score deltas + ScoreDelta(HashMap), + } + + 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(r) => self.ranking = Some(r), + FreeForAllGameUpdate::ScoreDelta(d) => match &mut self.ranking { + Some(FreeForAllGameRanking::Ranking(_)) | None => { + return Err(PartyError::Unknown("cannot apply score delta".into())) + } + Some(FreeForAllGameRanking::Scores(s)) => { + for (player, delta) in d.iter() { + if let Some(value) = s.get(player) { + s.insert(player.clone(), value + delta); + } + } + } + }, + } + 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 (player, reward) in ranking.iter().zip(self.spec.win_rewards.iter()) { + let score = points.get(player).unwrap_or(&0); + points.insert(player.clone(), score + reward); + } + + for (player, reward) in ranking.iter().rev().zip(self.spec.lose_rewards.iter()) { + let score = points.get(player).unwrap_or(&0); + points.insert(player.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")] +/// Returns the created event. #[openapi(tag = "Event")] #[post("/", data = "")] pub async fn create_event( _api_key: ApiKey, db: Connection, - mut event: Json, + event: Json, ) -> Result, PartyError> { - event.id = uuid(); - event.concluded = false; - db.events().insert_one(&*event, None).await?; - Ok(event) + let event = event.into_inner().create_event()?; + db.events().insert_one(&event, None).await?; + Ok(Json(event)) } /// # Update event @@ -86,21 +446,23 @@ pub async fn create_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 = "")] +#[post("/", data = "")] pub async fn update_event( _api_key: ApiKey, db: Connection, id: String, - mut event: Json, // Use serde_json::Value to allow a partial struct + update: Json, ) -> Result { - if let Some(i) = event.get_mut("id") { - *i = json! { &id }; - } - if let Some(i) = event.get_mut("concluded") { - *i = json! { false }; - } - db.users() - .update_one(doc! { "id": &id }, doc! { "$set": to_bson(&*event)? }, None) + 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) @@ -126,21 +488,32 @@ pub async fn get_event( /// # Get events /// -/// Returns the event with the given id +/// Returns all events #[openapi(tag = "Event")] -#[get("/")] +#[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(doc! {}, None).await?.try_collect().await?, + db.events() + .find(filter, None) + .await? + .filter(|e| future::ready(e.is_ok())) + .try_collect() + .await?, )) } /// # Get outcome /// -/// Returns the outcome of a concluded event. +/// Returns the outcome of an event. #[openapi(tag = "Event")] #[get("//outcome")] pub async fn event_outcome( @@ -157,6 +530,8 @@ pub async fn 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( @@ -170,8 +545,15 @@ pub async fn stop_event( .await? .ok_or(PartyError::EventNotFound(id.clone()))?; event.conclude(); - db.users() - .update_one(doc! { "id": &id }, doc! { "concluded": true }, None) + db.events() + .update_one( + doc! { "id": &id }, + doc! { "$set": { "concluded": true } }, + None, + ) .await?; - Ok(Json(event.outcome())) + let outcome = event.outcome(); + outcome.apply(&db).await?; + + Ok(Json(outcome)) } diff --git a/src/api/util.rs b/src/api/util.rs index a457e15..6b2b76d 100644 --- a/src/api/util.rs +++ b/src/api/util.rs @@ -70,6 +70,7 @@ impl OpenApiResponderInner for PartyError { impl<'r, 'o: 'r> Responder<'r, 'o> for PartyError { fn respond_to(self, req: &'r Request<'_>) -> response::Result<'o> { + println!("{:#?}", &self); match self { Self::UserNotFound(_) | Self::EventNotFound(_) => Status::NotFound, _ => Status::InternalServerError, diff --git a/start_mongodb.sh b/start_mongodb.sh index eddbe30..c3d7bc6 100755 --- a/start_mongodb.sh +++ b/start_mongodb.sh @@ -1,6 +1,8 @@ - podman run -d --rm --name lan_party_db \ + #!/bin/sh + +podman run -d --rm --name lan_party_db \ -v lan-party-db:/data/db:z \ - -p 27017:27017 \ + -p "27017:27017" \ -e MONGO_INITDB_ROOT_USERNAME=root \ -e MONGO_INITDB_ROOT_PASSWORD=example \ docker.io/mongo:latest