Progress on events

This commit is contained in:
Daan Vanoverloop 2022-08-30 13:15:57 +02:00
parent 01bfe925b6
commit 91ec035439
Signed by: Danacus
GPG Key ID: F2272B50E129FC5C
3 changed files with 424 additions and 39 deletions

View File

@ -1,11 +1,10 @@
use std::collections::HashMap; use std::{collections::HashMap, future, hash::Hash};
use futures::TryStreamExt; use futures::{StreamExt, TryStreamExt};
use rocket::serde::json::json; use rocket_db_pools::mongodb::bson::to_bson;
use rocket_db_pools::mongodb::{bson::to_bson, options::InsertOneOptions};
use serde_json::Value;
use super::{prelude::*, util::PartyError}; use super::{prelude::*, util::PartyError};
use paste::paste;
api_routes!( api_routes!(
create_event, create_event,
@ -22,6 +21,21 @@ pub struct EventOutcome {
points: HashMap<String, i64>, points: HashMap<String, i64>,
} }
impl EventOutcome {
pub async fn apply(&self, db: &Connection<Db>) -> Result<(), PartyError> {
for (player, reward) in self.points.iter() {
db.users()
.update_one(
doc! { "id": player },
doc! { "$inc": { "score": reward } },
None,
)
.await?;
}
Ok(())
}
}
/// # Event /// # Event
/// ///
/// An event in which participants can win or lose points /// An event in which participants can win or lose points
@ -32,53 +46,399 @@ pub struct Event {
#[serde(default = "uuid")] #[serde(default = "uuid")]
id: String, id: String,
/// Has this event concluded? /// Has this event concluded?
#[serde(default)]
concluded: bool, concluded: bool,
/// Name of the event
#[serde(default)]
name: String,
/// Description of the event
#[serde(default)]
description: String,
/// Event type /// Event type
event_type: EventType, event_type: EventType,
} }
impl Event { impl Event {
pub fn outcome(&self) -> EventOutcome { pub fn outcome(&self) -> EventOutcome {
match self.event_type { self.event_type.outcome()
_ => EventOutcome::default(),
}
} }
pub fn conclude(&self) {} pub fn conclude(&self) {}
pub fn apply_update(&mut self, update: EventUpdate) -> Result<(), PartyError> {
self.event_type.apply_update(update)
}
} }
fn uuid() -> String { fn uuid() -> String {
uuid::Uuid::new_v4().to_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 /// # EventType
/// ///
/// Enumeration of all different types of events /// An enumeration of event types
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(crate = "rocket::serde")] #[serde(crate = "rocket::serde")]
pub enum EventType { pub enum EventType {
FreeForAllGame {}, $($name($module::$name),)*
TeamGame {},
Test {},
} }
impl EventSpec {
pub fn create_event(self) -> Result<Event, PartyError> {
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<String, Vec<String>>,
#[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<String, Vec<String>>,
/// 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<i64>,
/// 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<i64>,
}
#[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<String> = 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<String>),
/// Score based ranking of players by user id
Scores(HashMap<String, i64>),
}
#[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<FreeForAllGameRanking>,
/// Specification of the game
#[serde(flatten)]
spec: FreeForAllGameSpec,
}
pub fn example_win_rewards() -> Vec<i64> {
vec![10, 7, 5, 3, 2, 1]
}
pub fn example_lose_rewards() -> Vec<i64> {
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<String>,
/// 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<i64>,
/// 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<i64>,
}
#[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<String, i64>),
}
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 /// # Create event
/// ///
/// If an `id` is supplied, it will be replaced by a randomly generated /// If an `id` is supplied, it will be replaced by a randomly generated
/// one. /// one.
/// ///
/// Returns the created event. #[openapi(tag = "Event")] /// Returns the created event.
#[openapi(tag = "Event")] #[openapi(tag = "Event")]
#[post("/", data = "<event>")] #[post("/", data = "<event>")]
pub async fn create_event( pub async fn create_event(
_api_key: ApiKey, _api_key: ApiKey,
db: Connection<Db>, db: Connection<Db>,
mut event: Json<Event>, event: Json<EventSpec>,
) -> Result<Json<Event>, PartyError> { ) -> Result<Json<Event>, PartyError> {
event.id = uuid(); let event = event.into_inner().create_event()?;
event.concluded = false; db.events().insert_one(&event, None).await?;
db.events().insert_one(&*event, None).await?; Ok(Json(event))
Ok(event)
} }
/// # Update 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 /// 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. /// changed and `concluded` will always be false.
#[openapi(tag = "Event")] #[openapi(tag = "Event")]
#[post("/<id>", data = "<event>")] #[post("/<id>", data = "<update>")]
pub async fn update_event( pub async fn update_event(
_api_key: ApiKey, _api_key: ApiKey,
db: Connection<Db>, db: Connection<Db>,
id: String, id: String,
mut event: Json<Value>, // Use serde_json::Value to allow a partial struct update: Json<EventUpdate>,
) -> Result<Status, PartyError> { ) -> Result<Status, PartyError> {
if let Some(i) = event.get_mut("id") { let mut event = db
*i = json! { &id }; .events()
} .find_one(doc! { "id": &id }, None)
if let Some(i) = event.get_mut("concluded") { .await?
*i = json! { false }; .ok_or(PartyError::EventNotFound(id.clone()))?;
}
db.users() event.apply_update(update.into_inner())?;
.update_one(doc! { "id": &id }, doc! { "$set": to_bson(&*event)? }, None)
db.events()
.update_one(doc! { "id": &id }, doc! { "$set": to_bson(&event)? }, None)
.await?; .await?;
Ok(Status::Ok) Ok(Status::Ok)
@ -126,21 +488,32 @@ pub async fn get_event(
/// # Get events /// # Get events
/// ///
/// Returns the event with the given id /// Returns all events
#[openapi(tag = "Event")] #[openapi(tag = "Event")]
#[get("/")] #[get("/?<concluded>")]
pub async fn get_all_events( pub async fn get_all_events(
_api_key: ApiKey, _api_key: ApiKey,
db: Connection<Db>, db: Connection<Db>,
concluded: Option<bool>,
) -> Result<Json<Vec<Event>>, PartyError> { ) -> Result<Json<Vec<Event>>, PartyError> {
let filter = if let Some(concluded) = concluded {
doc! { "concluded": concluded }
} else {
doc! {}
};
Ok(Json( 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 /// # Get outcome
/// ///
/// Returns the outcome of a concluded event. /// Returns the outcome of an event.
#[openapi(tag = "Event")] #[openapi(tag = "Event")]
#[get("/<id>/outcome")] #[get("/<id>/outcome")]
pub async fn event_outcome( pub async fn event_outcome(
@ -157,6 +530,8 @@ pub async fn event_outcome(
} }
/// # Stop event /// # Stop event
///
/// This will conclude the event, apply the outcome to the users and return the outcome.
#[openapi(tag = "Event")] #[openapi(tag = "Event")]
#[post("/<id>/stop")] #[post("/<id>/stop")]
pub async fn stop_event( pub async fn stop_event(
@ -170,8 +545,15 @@ pub async fn stop_event(
.await? .await?
.ok_or(PartyError::EventNotFound(id.clone()))?; .ok_or(PartyError::EventNotFound(id.clone()))?;
event.conclude(); event.conclude();
db.users() db.events()
.update_one(doc! { "id": &id }, doc! { "concluded": true }, None) .update_one(
doc! { "id": &id },
doc! { "$set": { "concluded": true } },
None,
)
.await?; .await?;
Ok(Json(event.outcome())) let outcome = event.outcome();
outcome.apply(&db).await?;
Ok(Json(outcome))
} }

View File

@ -70,6 +70,7 @@ impl OpenApiResponderInner for PartyError {
impl<'r, 'o: 'r> Responder<'r, 'o> for PartyError { impl<'r, 'o: 'r> Responder<'r, 'o> for PartyError {
fn respond_to(self, req: &'r Request<'_>) -> response::Result<'o> { fn respond_to(self, req: &'r Request<'_>) -> response::Result<'o> {
println!("{:#?}", &self);
match self { match self {
Self::UserNotFound(_) | Self::EventNotFound(_) => Status::NotFound, Self::UserNotFound(_) | Self::EventNotFound(_) => Status::NotFound,
_ => Status::InternalServerError, _ => Status::InternalServerError,

View File

@ -1,6 +1,8 @@
#!/bin/sh
podman run -d --rm --name lan_party_db \ podman run -d --rm --name lan_party_db \
-v lan-party-db:/data/db:z \ -v lan-party-db:/data/db:z \
-p 27017:27017 \ -p "27017:27017" \
-e MONGO_INITDB_ROOT_USERNAME=root \ -e MONGO_INITDB_ROOT_USERNAME=root \
-e MONGO_INITDB_ROOT_PASSWORD=example \ -e MONGO_INITDB_ROOT_PASSWORD=example \
docker.io/mongo:latest docker.io/mongo:latest