588 lines
20 KiB
Rust
588 lines
20 KiB
Rust
#[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::{web_view_attr, 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,
|
|
};
|
|
|
|
#[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<T: Hash + PartialEq + Eq + Clone + Default> {
|
|
/// Ranking of participants by user name or team name (first element is first place, second element is second
|
|
/// place, etc.)
|
|
Ranking(Vec<T>),
|
|
/// Score based ranking of participants/teams
|
|
Scores(HashMap<T, i64>),
|
|
}
|
|
|
|
impl<T: Hash + PartialEq + Eq + Clone + Default> Default for Ranking<T> {
|
|
fn default() -> Self {
|
|
Self::Ranking(Vec::default())
|
|
}
|
|
}
|
|
|
|
impl<T: Hash + PartialEq + Eq + Clone + Default> Ranking<T> {
|
|
pub fn is_valid(&self, participants: &HashSet<T>) -> 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)]
|
|
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
|
pub struct EventOutcome {
|
|
pub points: HashMap<String, i64>,
|
|
}
|
|
|
|
/// # 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", web_view_attr(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<Event, PartyError> {
|
|
if self.name.is_empty() {
|
|
return Err(PartyError::Unknown("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::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},
|
|
*,
|
|
};
|
|
|
|
#[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<String, Vec<String>>,
|
|
|
|
#[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<String, Vec<String>>,
|
|
|
|
/// 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<i64>,
|
|
/// 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<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 TeamGameUpdateSetTeam {
|
|
pub team: String,
|
|
pub members: Vec<String>,
|
|
}
|
|
|
|
#[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 Team {
|
|
name: String,
|
|
}
|
|
|
|
#[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(String),
|
|
/// Replace the current ranking with the given ranking
|
|
SetRanking(Ranking<String>),
|
|
/// If the current ranking is of type `Scores`, apply the given score deltas
|
|
ScoreDelta(HashMap<String, i64>),
|
|
/// Set rewards for winning the game
|
|
SetWinRewards(Vec<i64>),
|
|
/// Set rewards for losing the game
|
|
SetLoseRewards(Vec<i64>),
|
|
}
|
|
|
|
impl Default for TeamGameUpdate {
|
|
fn default() -> Self {
|
|
TeamGameUpdate::SetTeam(TeamGameUpdateSetTeam::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::SetRanking(x) => self
|
|
.ffa_game
|
|
.apply_update(FreeForAllGameUpdate::SetRanking(x)),
|
|
TeamGameUpdate::ScoreDelta(x) => self
|
|
.ffa_game
|
|
.apply_update(FreeForAllGameUpdate::ScoreDelta(x)),
|
|
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()))?;
|
|
self.teams.insert(u.team, u.members);
|
|
Ok(())
|
|
}
|
|
TeamGameUpdate::RemoveTeam(team) => {
|
|
self.ffa_game
|
|
.apply_update(FreeForAllGameUpdate::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, 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<Ranking<String>>,
|
|
|
|
/// Specification of the game
|
|
#[cfg_attr(feature = "serde", serde(flatten))]
|
|
pub 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, 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<String>,
|
|
|
|
/// 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<i64>,
|
|
/// 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<i64>,
|
|
}
|
|
|
|
#[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<String>),
|
|
/// If the current ranking is of type `Scores`, apply the given score deltas
|
|
ScoreDelta(HashMap<String, i64>),
|
|
/// Set rewards for winning the game
|
|
SetWinRewards(Vec<i64>),
|
|
/// Set rewards for losing the game
|
|
SetLoseRewards(Vec<i64>),
|
|
/// Set list of participants participating in the game
|
|
SetParticipants(HashSet<String>),
|
|
/// Add participant by name
|
|
AddParticipant(String),
|
|
/// Remove participant by name
|
|
RemoveParticipant(String),
|
|
}
|
|
|
|
impl Default for FreeForAllGameUpdate {
|
|
fn default() -> Self {
|
|
Self::AddParticipant(String::new())
|
|
}
|
|
}
|
|
|
|
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::Unknown("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(_)) | None => {
|
|
return Err(PartyError::Unknown("cannot apply score delta".into()))
|
|
}
|
|
Some(Ranking::Scores(s)) => {
|
|
for (participant, delta) in d.iter() {
|
|
if let Some(value) = s.get(participant) {
|
|
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::Unknown("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::Unknown("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())
|
|
}
|
|
}
|