i want to die
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -4,7 +4,5 @@ mixer_discord_bot.iml
|
|||||||
|
|
||||||
# Rust
|
# Rust
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
|
Secrets.toml
|
||||||
target/
|
target/
|
||||||
|
|
||||||
# Database
|
|
||||||
data.db
|
|
||||||
|
|||||||
12
Cargo.toml
12
Cargo.toml
@@ -1,9 +1,15 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "mixer_discord_bot"
|
name = "mixer-discord-bot"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tokio = { version = "*", features = ["full"] }
|
tokio = { version = "*", features = ["full"] }
|
||||||
serenity = { version = "*", default-features = false, features = ["client", "gateway", "rustls_backend", "model", "cache", "utils"] }
|
serenity = {version = "*", default-features = false, features = ["rustls_backend", "client", "gateway", "model", "cache", "collector", "utils"] }
|
||||||
sea-orm = { version = "*", features = ["sqlx-sqlite", "runtime-tokio-rustls", "macros"] }
|
sqlx = { version = "0.6.2", features = ["runtime-tokio-native-tls", "postgres"] }
|
||||||
|
sea-orm = { version = "*", features = ["sqlx-postgres", "runtime-tokio-native-tls", "macros"] }
|
||||||
|
itertools = "*"
|
||||||
|
shuttle-secrets = "*"
|
||||||
|
shuttle-serenity = "*"
|
||||||
|
shuttle-runtime = "*"
|
||||||
|
shuttle-shared-db = { version = "*", features = ["postgres"] }
|
||||||
1
Shuttle.toml
Normal file
1
Shuttle.toml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
name = "mixer-discord-bot"
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
CREATE TABLE IF NOT EXISTS players (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
discord_id INTEGER NOT NULL,
|
|
||||||
bn_name TEXT,
|
|
||||||
bn_tag TEXT,
|
|
||||||
tank REAL NOT NULL DEFAULT 2500.0,
|
|
||||||
dps REAL NOT NULL DEFAULT 2500.0,
|
|
||||||
support REAL NOT NULL DEFAULT 2500.0,
|
|
||||||
flex INTEGER NOT NULL DEFAULT true,
|
|
||||||
primary_role INTEGER NOT NULL DEFAULT -1,
|
|
||||||
secondary_role INTEGER NOT NULL DEFAULT -1,
|
|
||||||
tertiary_role INTEGER NOT NULL DEFAULT -1,
|
|
||||||
UNIQUE (discord_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS lobbies (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
guild_id INTEGER NOT NULL,
|
|
||||||
main_voice_id INTEGER NOT NULL,
|
|
||||||
red_team_voice_id INTEGER NOT NULL,
|
|
||||||
blue_team_voice_id INTEGER NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS players_discord_id_idx ON players (discord_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS lobbies_guild_id_idx ON lobbies (guild_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS lobbies_main_voice_id_idx ON lobbies (main_voice_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS lobbies_red_team_voice_id_idx ON lobbies (red_team_voice_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS lobbies_blue_team_voice_id_idx ON lobbies (blue_team_voice_id);
|
|
||||||
95
schema.sql
Normal file
95
schema.sql
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
-- DROP TABLE IF EXISTS players_test;
|
||||||
|
-- DROP FUNCTION IF EXISTS rating_not_null;
|
||||||
|
-- DROP TYPE rating;
|
||||||
|
-- DROP TYPE IF EXISTS role;
|
||||||
|
|
||||||
|
|
||||||
|
-- CREATE TYPE rating AS (
|
||||||
|
-- rating REAL,
|
||||||
|
-- rd REAL,
|
||||||
|
-- volatility REAL
|
||||||
|
-- );
|
||||||
|
|
||||||
|
-- CREATE TYPE role AS ENUM ('tank', 'dps', 'support');
|
||||||
|
|
||||||
|
-- CREATE OR REPLACE FUNCTION rating_not_null(r rating) RETURNS BOOLEAN AS $$
|
||||||
|
-- BEGIN
|
||||||
|
-- RETURN (r.rating IS NOT NULL) AND (r.rd IS NOT NULL) AND (r.volatility IS NOT NULL);
|
||||||
|
-- END;
|
||||||
|
-- $$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
-- CREATE TABLE players_test (
|
||||||
|
-- id SERIAL PRIMARY KEY,
|
||||||
|
-- discord_id BIGINT NOT NULL,
|
||||||
|
-- bn_name TEXT DEFAULT '',
|
||||||
|
-- bn_tag TEXT DEFAULT '',
|
||||||
|
|
||||||
|
-- tank_rating rating NOT NULL DEFAULT (2500.0, 300.0, 0.06),
|
||||||
|
-- dps_rating rating NOT NULL DEFAULT (2500.0, 300.0, 0.06),
|
||||||
|
-- support_rating rating NOT NULL DEFAULT (2500.0, 300.0, 0.06),
|
||||||
|
|
||||||
|
-- flex BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
-- primary_role role,
|
||||||
|
-- secondary_role role,
|
||||||
|
-- tertiary_role role,
|
||||||
|
-- UNIQUE (discord_id),
|
||||||
|
-- CHECK (
|
||||||
|
-- rating_not_null(tank_rating) AND
|
||||||
|
-- rating_not_null(dps_rating) AND
|
||||||
|
-- rating_not_null(support_rating)
|
||||||
|
-- )
|
||||||
|
-- );
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS players (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
discord_id BIGINT NOT NULL,
|
||||||
|
bn_name TEXT,
|
||||||
|
bn_tag TEXT,
|
||||||
|
tank_rating REAL NOT NULL DEFAULT 2500.0,
|
||||||
|
tank_rd REAL NOT NULL DEFAULT 300,
|
||||||
|
tank_volatility REAL NOT NULL DEFAULT 0.06,
|
||||||
|
dps_rating REAL NOT NULL DEFAULT 2500.0,
|
||||||
|
dps_rd REAL NOT NULL DEFAULT 300,
|
||||||
|
dps_volatility REAL NOT NULL DEFAULT 0.06,
|
||||||
|
support_rating REAL NOT NULL DEFAULT 2500.0,
|
||||||
|
support_rd REAL NOT NULL DEFAULT 300,
|
||||||
|
support_volatility REAL NOT NULL DEFAULT 0.06,
|
||||||
|
flex BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
primary_role INTEGER NOT NULL DEFAULT -1,
|
||||||
|
secondary_role INTEGER NOT NULL DEFAULT -1,
|
||||||
|
tertiary_role INTEGER NOT NULL DEFAULT -1,
|
||||||
|
UNIQUE (discord_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS lobbies (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
guild_id BIGINT NOT NULL,
|
||||||
|
main_voice_id BIGINT NOT NULL,
|
||||||
|
red_team_voice_id BIGINT NOT NULL,
|
||||||
|
blue_team_voice_id BIGINT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CREATE TABLE IF NOT EXISTS guilds (
|
||||||
|
-- id SERIAL PRIMARY KEY,
|
||||||
|
-- guild_id BIGINT NOT NULL,
|
||||||
|
-- separate_roles BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
-- grandmaster_name TEXT,
|
||||||
|
-- master_name TEXT,
|
||||||
|
-- diamond_name TEXT,
|
||||||
|
-- platinum_name TEXT,
|
||||||
|
-- gold_name TEXT,
|
||||||
|
-- silver_name TEXT,
|
||||||
|
-- bronze_name TEXT,
|
||||||
|
--
|
||||||
|
-- tank_name TEXT,
|
||||||
|
-- dps_name TEXT,
|
||||||
|
-- support_name TEXT,
|
||||||
|
--
|
||||||
|
-- UNIQUE (guild_id)
|
||||||
|
-- );
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS players_discord_id_idx ON players (discord_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS lobbies_guild_id_idx ON lobbies (guild_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS lobbies_main_voice_id_idx ON lobbies (main_voice_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS lobbies_red_team_voice_id_idx ON lobbies (red_team_voice_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS lobbies_blue_team_voice_id_idx ON lobbies (blue_team_voice_id);
|
||||||
67
src/algorithm/glicko2/mod.rs
Normal file
67
src/algorithm/glicko2/mod.rs
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
pub fn update(r: f32, rd: f32, volatility: f32, r_opponent: f32, rd_opponent: f32, scale: f32, score: f32) -> (f32, f32, f32) {
|
||||||
|
let tau = 0.2f32;
|
||||||
|
|
||||||
|
let mu = (r / scale - 1500.0) / 173.7178;
|
||||||
|
let phi = rd / 173.7178;
|
||||||
|
let mu_opponent = (r_opponent / scale - 1500.0) / 173.7178;
|
||||||
|
let phi_opponent = rd_opponent / 173.7178;
|
||||||
|
|
||||||
|
let g = 1.0 / (1.0 + 3.0 * phi_opponent.powi(2) / std::f32::consts::PI.powi(2)).sqrt();
|
||||||
|
|
||||||
|
let e = 1.0 / (1.0 + (-g * (mu - mu_opponent)).exp());
|
||||||
|
|
||||||
|
let v = 1.0 / (g.powi(2) * e * (1.0 - e));
|
||||||
|
|
||||||
|
let delta = v * g * (score - e);
|
||||||
|
|
||||||
|
|
||||||
|
let a_init = volatility.powi(2).ln();
|
||||||
|
|
||||||
|
let f = |x: f32| -> f32 {
|
||||||
|
((x.exp() * (delta.powi(2) - phi.powi(2) - v - x.exp()))
|
||||||
|
/ (2.0 * (phi.powi(2) + v + x.exp()).powi(2))) - ((x - a_init) / (tau.powi(2)))
|
||||||
|
};
|
||||||
|
|
||||||
|
let epsilon = 0.000001;
|
||||||
|
|
||||||
|
let mut a = a_init;
|
||||||
|
let mut b = if delta.powi(2) > phi.powi(2) + v {
|
||||||
|
(delta.powi(2) - phi.powi(2) - v).ln()
|
||||||
|
} else {
|
||||||
|
let mut k = 1.0;
|
||||||
|
while f(a_init - k * tau) < 0.0 {
|
||||||
|
k += 1.0;
|
||||||
|
}
|
||||||
|
a_init - k * tau
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut f_a = f(a);
|
||||||
|
let mut f_b = f(b);
|
||||||
|
|
||||||
|
while (b - a).abs() > epsilon {
|
||||||
|
let c = a + (a - b) * f_a / (f_b - f_a);
|
||||||
|
let f_c = f(c);
|
||||||
|
|
||||||
|
if f_c * f_b <= 0.0 {
|
||||||
|
a = b;
|
||||||
|
f_a = f_b;
|
||||||
|
} else {
|
||||||
|
f_a /= 2.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
b = c;
|
||||||
|
f_b = f_c;
|
||||||
|
}
|
||||||
|
|
||||||
|
let sigma_prime = (a / 2.0).exp();
|
||||||
|
|
||||||
|
|
||||||
|
let phi_star = (phi.powi(2) + sigma_prime.powi(2)).sqrt();
|
||||||
|
let phi_prime = 1.0 / (1.0 / phi_star.powi(2) + 1.0 / v).sqrt();
|
||||||
|
let mu_prime = mu + phi_prime.powi(2) * g * (score - e);
|
||||||
|
|
||||||
|
let r_prime = (173.7178 * mu_prime + 1500.0) * scale;
|
||||||
|
let rd_prime = 173.7178 * phi_prime;
|
||||||
|
|
||||||
|
(r_prime, rd_prime, sigma_prime)
|
||||||
|
}
|
||||||
1
src/algorithm/mod.rs
Normal file
1
src/algorithm/mod.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod glicko2;
|
||||||
@@ -1,20 +1,28 @@
|
|||||||
use serenity::builder::CreateApplicationCommand;
|
use std::time::Duration;
|
||||||
|
use itertools::Itertools;
|
||||||
|
use serenity::builder::{CreateApplicationCommand, CreateEmbed};
|
||||||
use serenity::client::Context;
|
use serenity::client::Context;
|
||||||
use serenity::model::application::interaction::{
|
use serenity::model::application::interaction::{
|
||||||
application_command::ApplicationCommandInteraction,
|
application_command::ApplicationCommandInteraction,
|
||||||
InteractionResponseType
|
InteractionResponseType
|
||||||
};
|
};
|
||||||
use serenity::async_trait;
|
use serenity::async_trait;
|
||||||
use serenity::http::CacheHttp;
|
use serenity::futures::future::join_all;
|
||||||
|
use serenity::futures::StreamExt;
|
||||||
use serenity::model::application::command::CommandOptionType;
|
use serenity::model::application::command::CommandOptionType;
|
||||||
|
use serenity::model::application::component::ButtonStyle;
|
||||||
|
use serenity::model::application::interaction::message_component::MessageComponentInteraction;
|
||||||
use serenity::model::channel::{ChannelType, PermissionOverwrite, PermissionOverwriteType};
|
use serenity::model::channel::{ChannelType, PermissionOverwrite, PermissionOverwriteType};
|
||||||
use serenity::model::id::{ChannelId, RoleId, UserId};
|
use serenity::model::id::{ChannelId, RoleId, UserId};
|
||||||
use serenity::model::Permissions;
|
use serenity::model::Permissions;
|
||||||
|
use serenity::utils::Colour;
|
||||||
use crate::bot::commands::MixerCommand;
|
use crate::bot::commands::MixerCommand;
|
||||||
use crate::database::DatabaseContainer;
|
use crate::database::DatabaseContainer;
|
||||||
|
use crate::database::models::lobby::Model;
|
||||||
use crate::mixer::mixer;
|
use crate::mixer::mixer;
|
||||||
use crate::mixer::player::Player;
|
use crate::mixer::player::Player;
|
||||||
use crate::mixer::role::Role;
|
use crate::mixer::role::Role;
|
||||||
|
use crate::mixer::team::Team;
|
||||||
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -54,16 +62,21 @@ impl MixerCommand for LobbyCommand {
|
|||||||
impl LobbyCommand {
|
impl LobbyCommand {
|
||||||
async fn create_lobby(&self, ctx: &Context, interaction: ApplicationCommandInteraction) -> serenity::Result<()> {
|
async fn create_lobby(&self, ctx: &Context, interaction: ApplicationCommandInteraction) -> serenity::Result<()> {
|
||||||
let guild_id = interaction.guild_id.unwrap();
|
let guild_id = interaction.guild_id.unwrap();
|
||||||
let member = guild_id.member(ctx.http(), interaction.user.id).await?;
|
let guild = guild_id.to_partial_guild(ctx).await?;
|
||||||
|
let member = guild.member(ctx, interaction.user.id).await?;
|
||||||
|
|
||||||
let mut has_permission = false;
|
let mut has_permission = false;
|
||||||
if let Ok(perms) = member.permissions(&ctx.cache) {
|
if guild.owner_id == member.user.id {
|
||||||
if perms.manage_channels() {
|
has_permission = true;
|
||||||
|
}
|
||||||
|
if let Ok(perms) = member.permissions(ctx) {
|
||||||
|
if perms.administrator() {
|
||||||
has_permission = true;
|
has_permission = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !has_permission {
|
if !has_permission {
|
||||||
interaction.create_interaction_response(&ctx.http, |response| {
|
interaction.create_interaction_response(ctx, |response| {
|
||||||
response.kind(InteractionResponseType::ChannelMessageWithSource)
|
response.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||||
.interaction_response_data(|message| {
|
.interaction_response_data(|message| {
|
||||||
message.content("You don't have permission to create a lobby!")
|
message.content("You don't have permission to create a lobby!")
|
||||||
@@ -75,7 +88,7 @@ impl LobbyCommand {
|
|||||||
let data = ctx.data.read().await;
|
let data = ctx.data.read().await;
|
||||||
let db = data.get::<DatabaseContainer>().unwrap().read().await;
|
let db = data.get::<DatabaseContainer>().unwrap().read().await;
|
||||||
|
|
||||||
let main_voice = interaction.guild_id.unwrap().create_channel(&ctx.http, |c| {
|
let main_voice = interaction.guild_id.unwrap().create_channel(ctx, |c| {
|
||||||
c.name("Mix Lobby").kind(ChannelType::Voice)
|
c.name("Mix Lobby").kind(ChannelType::Voice)
|
||||||
}).await?;
|
}).await?;
|
||||||
|
|
||||||
@@ -86,16 +99,18 @@ impl LobbyCommand {
|
|||||||
kind: PermissionOverwriteType::Role(RoleId::from(interaction.guild_id.unwrap().0))
|
kind: PermissionOverwriteType::Role(RoleId::from(interaction.guild_id.unwrap().0))
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
let red_voice = interaction.guild_id.unwrap().create_channel(&ctx.http, |c| {
|
let red_voice = interaction.guild_id.unwrap().create_channel(ctx, |c| {
|
||||||
c.name("Red").kind(ChannelType::Voice).user_limit(5).permissions(permissions.clone())
|
c.name("Red").kind(ChannelType::Voice).user_limit(5).permissions(permissions.clone())
|
||||||
}).await?;
|
}).await?;
|
||||||
let blue_voice = interaction.guild_id.unwrap().create_channel(&ctx.http, |c| {
|
let blue_voice = interaction.guild_id.unwrap().create_channel(ctx, |c| {
|
||||||
c.name("Blue").kind(ChannelType::Voice).user_limit(5).permissions(permissions)
|
c.name("Blue").kind(ChannelType::Voice).user_limit(5).permissions(permissions)
|
||||||
}).await?;
|
}).await?;
|
||||||
|
|
||||||
db.insert_guild_lobby(interaction.guild_id.unwrap(), main_voice.id, red_voice.id, blue_voice.id).await;
|
db.insert_guild_lobby(interaction.guild_id.unwrap(), main_voice.id, red_voice.id, blue_voice.id).await;
|
||||||
|
drop(db);
|
||||||
|
drop(data);
|
||||||
|
|
||||||
interaction.create_interaction_response(&ctx.http, |response| {
|
interaction.create_interaction_response(ctx, |response| {
|
||||||
response.kind(InteractionResponseType::ChannelMessageWithSource)
|
response.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||||
.interaction_response_data(|message| {
|
.interaction_response_data(|message| {
|
||||||
message.content("Successfully created a new mix lobby!")
|
message.content("Successfully created a new mix lobby!")
|
||||||
@@ -107,18 +122,20 @@ impl LobbyCommand {
|
|||||||
|
|
||||||
async fn start_lobby(&self, ctx: &Context, interaction: ApplicationCommandInteraction) -> serenity::Result<()> {
|
async fn start_lobby(&self, ctx: &Context, interaction: ApplicationCommandInteraction) -> serenity::Result<()> {
|
||||||
let data = ctx.data.read().await;
|
let data = ctx.data.read().await;
|
||||||
let db = data.get::<DatabaseContainer>().unwrap().write().await;
|
let db = data.get::<DatabaseContainer>().unwrap().read().await;
|
||||||
|
|
||||||
let guild_id = interaction.guild_id.unwrap();
|
let guild_id = interaction.guild_id.unwrap();
|
||||||
let member = guild_id.member(ctx.http(), interaction.user.id).await?;
|
let member = guild_id.member(ctx, interaction.user.id).await?;
|
||||||
|
|
||||||
|
let channels = guild_id.channels(ctx).await?;
|
||||||
|
|
||||||
let mut is_in_lobby = false;
|
let mut is_in_lobby = false;
|
||||||
let mut channel_id = None;
|
let mut channel_id = None;
|
||||||
for (id, channel) in guild_id.channels(ctx.http()).await? {
|
for (id, channel) in channels {
|
||||||
if channel.kind != ChannelType::Voice {
|
if channel.kind != ChannelType::Voice {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let members = channel.members(&ctx.cache).await?;
|
let members = channel.members(ctx).await?;
|
||||||
if members.iter().any(|m| m.user.id == member.user.id) && db.get_lobby_by_channel(guild_id, id).await.is_some() {
|
if members.iter().any(|m| m.user.id == member.user.id) && db.get_lobby_by_channel(guild_id, id).await.is_some() {
|
||||||
is_in_lobby = true;
|
is_in_lobby = true;
|
||||||
channel_id = Some(id);
|
channel_id = Some(id);
|
||||||
@@ -127,7 +144,7 @@ impl LobbyCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !is_in_lobby {
|
if !is_in_lobby {
|
||||||
interaction.create_interaction_response(ctx.http(), |response| {
|
interaction.create_interaction_response(ctx, |response| {
|
||||||
response.kind(InteractionResponseType::ChannelMessageWithSource)
|
response.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||||
.interaction_response_data(|message| {
|
.interaction_response_data(|message| {
|
||||||
message.content("You are not in the mix lobby!").ephemeral(true)
|
message.content("You are not in the mix lobby!").ephemeral(true)
|
||||||
@@ -139,47 +156,273 @@ impl LobbyCommand {
|
|||||||
|
|
||||||
let lobby = db.get_lobby_by_channel(guild_id, channel_id.unwrap()).await.unwrap();
|
let lobby = db.get_lobby_by_channel(guild_id, channel_id.unwrap()).await.unwrap();
|
||||||
|
|
||||||
let main_channel = ChannelId::from(lobby.main_voice_id as u64).to_channel(ctx.http()).await.unwrap().guild().unwrap();
|
let main_channel = ChannelId::from(lobby.main_voice_id as u64).to_channel(ctx).await.unwrap().guild().unwrap();
|
||||||
let red_channel = ChannelId::from(lobby.red_team_voice_id as u64).to_channel(ctx.http()).await.unwrap().guild().unwrap();
|
let red_channel = ChannelId::from(lobby.red_team_voice_id as u64).to_channel(ctx).await.unwrap().guild().unwrap();
|
||||||
let blue_channel = ChannelId::from(lobby.blue_team_voice_id as u64).to_channel(ctx.http()).await.unwrap().guild().unwrap();
|
let blue_channel = ChannelId::from(lobby.blue_team_voice_id as u64).to_channel(ctx).await.unwrap().guild().unwrap();
|
||||||
|
|
||||||
for member in red_channel.members(ctx.cache().unwrap()).await? {
|
interaction.create_interaction_response(ctx, |response| {
|
||||||
member.move_to_voice_channel(ctx.http(), main_channel.id).await?;
|
response.kind(InteractionResponseType::DeferredChannelMessageWithSource)
|
||||||
|
}).await?;
|
||||||
|
|
||||||
|
for member in red_channel.members(ctx).await? {
|
||||||
|
member.move_to_voice_channel(ctx, main_channel.id).await?;
|
||||||
}
|
}
|
||||||
for member in blue_channel.members(ctx.cache().unwrap()).await? {
|
for member in blue_channel.members(ctx).await? {
|
||||||
member.move_to_voice_channel(ctx.http(), main_channel.id).await?;
|
member.move_to_voice_channel(ctx, main_channel.id).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: uncomment this
|
// TODO: uncomment this
|
||||||
// let members = main_channel.members(ctx.cache().unwrap()).await?;
|
let members = main_channel.members(ctx).await?;
|
||||||
// let users = members.iter().map(|m| m.user.id).collect::<Vec<UserId>>();
|
let users = members.iter().map(|m| m.user.id).collect::<Vec<UserId>>();
|
||||||
let users = (0..10).map(|id| UserId::from(id)).collect::<Vec<UserId>>();
|
// let users = (0..11).map(|id| UserId::from(id)).chain(users.into_iter()).collect::<Vec<UserId>>();
|
||||||
let players = db.get_players(users).await;
|
let players = db.get_players(users).await;
|
||||||
let players = players.into_iter().map(|p| Player::new(p)).collect::<Vec<Player>>();
|
let players = players.into_iter().map(|p| Player::new(p)).collect::<Vec<Player>>();
|
||||||
let slots = vec![Role::Tank, Role::Dps, Role::Dps, Role::Support, Role::Support];
|
let slots = vec![Role::Tank, Role::Dps, Role::Dps, Role::Support, Role::Support];
|
||||||
if let Some((team1, team2)) = mixer::mix_players(&players, slots) {
|
|
||||||
println!("Average rank {}", team1.average_rank());
|
interaction.edit_original_interaction_response(ctx, |response| {
|
||||||
println!("Average rank tank {}", team1.average_rank_role(&Role::Tank));
|
response.content("Mixing teams...")
|
||||||
println!("Average rank dps {}", team1.average_rank_role(&Role::Dps));
|
}).await?;
|
||||||
println!("Average rank support {}", team1.average_rank_role(&Role::Support));
|
|
||||||
println!("Average rank {}", team2.average_rank());
|
if let Some(teams) = mixer::mix_players(&players, slots) {
|
||||||
println!("Average rank tank {}", team2.average_rank_role(&Role::Tank));
|
// let interaction = interaction.clone();
|
||||||
println!("Average rank dps {}", team2.average_rank_role(&Role::Dps));
|
self.process_valid_teams(ctx, interaction, lobby, teams, players).await?;
|
||||||
println!("Average rank support {}\n", team2.average_rank_role(&Role::Support));
|
|
||||||
println!("Team 1: {:?}\n\n", team1.players.iter().map(|p| (p.0.clone().0, p.1.clone().unwrap().name.clone())).collect::<Vec<(Role, String)>>());
|
|
||||||
println!("Team 2: {:?}", team2.players.iter().map(|p| (p.0.clone().0, p.1.clone().unwrap().name.clone())).collect::<Vec<(Role, String)>>());
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
println!("Fair lobby could not be mixed")
|
interaction.edit_original_interaction_response(ctx, |response| {
|
||||||
|
response.content("Fair teams' composition could not be found!")
|
||||||
|
}).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
interaction.create_interaction_response(ctx.http(), |response| {
|
Ok(())
|
||||||
response.kind(InteractionResponseType::ChannelMessageWithSource)
|
}
|
||||||
.interaction_response_data(|message| {
|
|
||||||
message.content("Successfully started the mix lobby!").ephemeral(true)
|
async fn process_valid_teams(&self, ctx: &Context, interaction: ApplicationCommandInteraction, lobby: Model, teams: (Team, Team), players: Vec<Player>) -> serenity::Result<()> {
|
||||||
|
let (team1, team2) = teams.clone();
|
||||||
|
|
||||||
|
let team1_names = team1.players.iter()
|
||||||
|
.sorted_by(|((a, _), _), ((b, _), _)| i32::from(*a).cmp(&i32::from(*b)))
|
||||||
|
.map(|(_, i)| async {
|
||||||
|
if let Some(user) = players[i.unwrap()].discord_id.to_user(ctx).await.ok() {
|
||||||
|
user.name
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
players[i.unwrap()].bn_name.clone()
|
||||||
|
}
|
||||||
|
}).collect_vec();
|
||||||
|
let team2_names = team2.players.iter()
|
||||||
|
.sorted_by(|((a, _), _), ((b, _), _)| i32::from(*a).cmp(&i32::from(*b)))
|
||||||
|
.map(|(_, i)| async {
|
||||||
|
if let Some(user) = players[i.unwrap()].discord_id.to_user(ctx).await.ok() {
|
||||||
|
user.name
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
players[i.unwrap()].bn_name.clone()
|
||||||
|
}
|
||||||
|
}).collect_vec();
|
||||||
|
|
||||||
|
let team1_names = join_all(team1_names).await;
|
||||||
|
let team2_names = join_all(team2_names).await;
|
||||||
|
|
||||||
|
interaction.edit_original_interaction_response(ctx, |response| {
|
||||||
|
response.content("").components(|components| {
|
||||||
|
components.create_action_row(|row| {
|
||||||
|
row.create_button(|button| {
|
||||||
|
button.custom_id("cancel").label("Cancel")
|
||||||
|
.style(ButtonStyle::Danger)
|
||||||
|
});
|
||||||
|
row.create_button(|button| {
|
||||||
|
button.custom_id("swap").label("Swap").disabled(true)
|
||||||
|
.style(ButtonStyle::Primary)
|
||||||
|
});
|
||||||
|
row.create_button(|button| {
|
||||||
|
button.custom_id("start").label("Start")
|
||||||
|
.style(ButtonStyle::Success)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
}).embed(|embed| {
|
||||||
|
embed.title("Teams").fields(vec![
|
||||||
|
("Team 1", "", false),
|
||||||
|
("Tank", &team1_names[0], true),
|
||||||
|
("Dps", &format!("{}\n{}", team1_names[1], team1_names[2]), true),
|
||||||
|
("Support", &format!("{}\n{}", team1_names[3], team1_names[4]), true),
|
||||||
|
("Team 2", "", false),
|
||||||
|
("Tank", &team2_names[0], true),
|
||||||
|
("Dps", &format!("{}\n{}", team2_names[1], team2_names[2]), true),
|
||||||
|
("Support", &format!("{}\n{}", team2_names[3], team2_names[4]), true),
|
||||||
|
]).colour(Colour::new(0xcfa22f))
|
||||||
|
})
|
||||||
|
}).await.unwrap();
|
||||||
|
|
||||||
|
let msg = interaction.get_interaction_response(ctx).await.unwrap();
|
||||||
|
let collector = msg.await_component_interactions(ctx)
|
||||||
|
.timeout(Duration::from_secs(2 * 60))
|
||||||
|
.guild_id(interaction.guild_id.unwrap())
|
||||||
|
.channel_id(interaction.channel_id)
|
||||||
|
.author_id(interaction.user.id)
|
||||||
|
.collect_limit(1)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let interactions = collector.collect::<Vec<_>>().await;
|
||||||
|
if let Some(interaction) = interactions.first() {
|
||||||
|
match interaction.data.custom_id.as_str() {
|
||||||
|
"start" => self.process_valid_teams_start(ctx, interaction, lobby, &team1, &team2, players).await?,
|
||||||
|
"cancel" => self.process_valid_teams_cancel(ctx, interaction, &team1, &team2).await?,
|
||||||
|
"swap" => self.process_valid_teams_swap(ctx, interaction, &team1, &team2).await?,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
interaction.delete_original_interaction_response(ctx).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn process_valid_teams_start(&self, ctx: &Context, interaction: &MessageComponentInteraction, lobby: Model, team1: &Team, team2: &Team, players: Vec<Player>) -> serenity::Result<()> {
|
||||||
|
let main_channel = ChannelId::from(lobby.main_voice_id as u64).to_channel(ctx).await.unwrap().guild().unwrap();
|
||||||
|
let red_channel = ChannelId::from(lobby.red_team_voice_id as u64).to_channel(ctx).await.unwrap().guild().unwrap();
|
||||||
|
let blue_channel = ChannelId::from(lobby.blue_team_voice_id as u64).to_channel(ctx).await.unwrap().guild().unwrap();
|
||||||
|
|
||||||
|
for member in main_channel.members(ctx).await? {
|
||||||
|
let index = players.iter().position(|p| p.discord_id == member.user.id);
|
||||||
|
|
||||||
|
if team1.players.iter().any(|(_, i)| *i == index && index.is_some()) {
|
||||||
|
member.move_to_voice_channel(ctx, red_channel.id).await?;
|
||||||
|
}
|
||||||
|
else if team2.players.iter().any(|(_, i)| *i == index && index.is_some()) {
|
||||||
|
member.move_to_voice_channel(ctx, blue_channel.id).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let embed = interaction.message.embeds.get(0).unwrap();
|
||||||
|
interaction.delete_original_interaction_response(ctx).await?;
|
||||||
|
|
||||||
|
// create msg without interaction
|
||||||
|
let msg = interaction.channel_id.send_message(ctx, |message| {
|
||||||
|
message.content(format!("<@{}>", interaction.user.id.0))
|
||||||
|
.set_embeds(vec![CreateEmbed::from(embed.clone())])
|
||||||
|
.components(|components| {
|
||||||
|
components.create_action_row(|row| {
|
||||||
|
row.create_button(|button| {
|
||||||
|
button.custom_id("win_team1")
|
||||||
|
.label("Team 1 win")
|
||||||
|
.style(ButtonStyle::Success)
|
||||||
|
}).create_button(|button| {
|
||||||
|
button.custom_id("draw")
|
||||||
|
.label("Draw")
|
||||||
|
.style(ButtonStyle::Secondary)
|
||||||
|
}).create_button(|button| {
|
||||||
|
button.custom_id("win_team2")
|
||||||
|
.label("Team 2 win")
|
||||||
|
.style(ButtonStyle::Success)
|
||||||
|
})
|
||||||
|
}).create_action_row(|row| {
|
||||||
|
row.create_button(|button| {
|
||||||
|
button.custom_id("cancel")
|
||||||
|
.label("Cancel game")
|
||||||
|
.style(ButtonStyle::Danger)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
}).await?;
|
}).await?;
|
||||||
|
|
||||||
|
|
||||||
|
// let msg = interaction.edit_original_interaction_response(ctx, |message| {
|
||||||
|
// message.components(|components| {
|
||||||
|
// components.create_action_row(|row| {
|
||||||
|
// row.create_button(|button| {
|
||||||
|
// button.custom_id("win_team1")
|
||||||
|
// .label("Team 1 win")
|
||||||
|
// .style(ButtonStyle::Success)
|
||||||
|
// }).create_button(|button| {
|
||||||
|
// button.custom_id("draw")
|
||||||
|
// .label("Draw")
|
||||||
|
// .style(ButtonStyle::Secondary)
|
||||||
|
// }).create_button(|button| {
|
||||||
|
// button.custom_id("win_team2")
|
||||||
|
// .label("Team 2 win")
|
||||||
|
// .style(ButtonStyle::Success)
|
||||||
|
// })
|
||||||
|
// }).create_action_row(|row| {
|
||||||
|
// row.create_button(|button| {
|
||||||
|
// button.custom_id("cancel")
|
||||||
|
// .label("Cancel game")
|
||||||
|
// .style(ButtonStyle::Danger)
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
// }).await?;
|
||||||
|
|
||||||
|
let collector = msg.await_component_interactions(ctx)
|
||||||
|
.timeout(Duration::from_secs(30 * 60))
|
||||||
|
.guild_id(interaction.guild_id.unwrap())
|
||||||
|
.channel_id(interaction.channel_id)
|
||||||
|
.author_id(interaction.user.id)
|
||||||
|
.collect_limit(1)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let interactions = collector.collect::<Vec<_>>().await;
|
||||||
|
if let Some(interaction) = interactions.first() {
|
||||||
|
let mut score = 0.5f32;
|
||||||
|
match interaction.data.custom_id.as_str() {
|
||||||
|
"win_team1" => score = 1.0,
|
||||||
|
"draw" => score = 0.5,
|
||||||
|
"win_team2" => score = 0.0,
|
||||||
|
"cancel" => {
|
||||||
|
return msg.delete(ctx).await;
|
||||||
|
// return interaction.delete_original_interaction_response(ctx).await;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let team1_average_rating = team1.average_rating(&players);
|
||||||
|
let team2_average_rating = team2.average_rating(&players);
|
||||||
|
|
||||||
|
let team1 = team1.players.clone().into_iter()
|
||||||
|
.filter_map(|((role, _), player)|
|
||||||
|
if player.is_some() {
|
||||||
|
Some((role, player.unwrap()))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
).collect::<Vec<_>>();
|
||||||
|
let team2 = team2.players.clone().into_iter()
|
||||||
|
.filter_map(|((role, _), player)|
|
||||||
|
if player.is_some() {
|
||||||
|
Some((role, player.unwrap()))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
).collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let data = ctx.data.read().await;
|
||||||
|
let db = data.get::<DatabaseContainer>().unwrap().read().await;
|
||||||
|
|
||||||
|
for (role, index) in team1 {
|
||||||
|
let mut rating = players[index].ranks[&role];
|
||||||
|
rating.update(&team2_average_rating, score);
|
||||||
|
db.update_player_rank(players[index].discord_id, Some(role), rating).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (role, index) in team2 {
|
||||||
|
let mut rating = players[index].ranks[&role];
|
||||||
|
rating.update(&team1_average_rating, 1.0 - score);
|
||||||
|
db.update_player_rank(players[index].discord_id, Some(role), rating).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
drop(db);
|
||||||
|
drop(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.delete(ctx).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn process_valid_teams_cancel(&self, ctx: &Context, interaction: &MessageComponentInteraction, team1: &Team, team2: &Team) -> serenity::Result<()> {
|
||||||
|
interaction.delete_original_interaction_response(ctx).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn process_valid_teams_swap(&self, ctx: &Context, interaction: &MessageComponentInteraction, team1: &Team, team2: &Team) -> serenity::Result<()> {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@ pub mod ping;
|
|||||||
pub mod lobby;
|
pub mod lobby;
|
||||||
pub mod rank;
|
pub mod rank;
|
||||||
pub mod preference;
|
pub mod preference;
|
||||||
|
pub mod settings;
|
||||||
|
|
||||||
use serenity::builder::CreateApplicationCommand;
|
use serenity::builder::CreateApplicationCommand;
|
||||||
use serenity::client::Context;
|
use serenity::client::Context;
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ use serenity::model::application::interaction::{
|
|||||||
InteractionResponseType
|
InteractionResponseType
|
||||||
};
|
};
|
||||||
use serenity::async_trait;
|
use serenity::async_trait;
|
||||||
use serenity::http::CacheHttp;
|
|
||||||
use crate::bot::commands::MixerCommand;
|
use crate::bot::commands::MixerCommand;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -23,27 +22,22 @@ impl MixerCommand for PingCommand {
|
|||||||
|
|
||||||
async fn execute(&self, ctx: &Context, interaction: ApplicationCommandInteraction) -> serenity::Result<()> {
|
async fn execute(&self, ctx: &Context, interaction: ApplicationCommandInteraction) -> serenity::Result<()> {
|
||||||
let content = "Pong!";
|
let content = "Pong!";
|
||||||
interaction.create_interaction_response(&ctx.http(), |response| {
|
interaction.create_interaction_response(ctx, |response| {
|
||||||
response.kind(InteractionResponseType::ChannelMessageWithSource)
|
response.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||||
.interaction_response_data(|message| {
|
.interaction_response_data(|message| {
|
||||||
message.content(content).ephemeral(true)
|
message.content(content).ephemeral(true)
|
||||||
})
|
})
|
||||||
}).await?;
|
}).await?;
|
||||||
|
|
||||||
let follow1 = interaction.create_followup_message(&ctx.http(), |followup| {
|
let follow1 = interaction.create_followup_message(ctx, |followup| {
|
||||||
followup.content("followup1").ephemeral(true)
|
followup.content("followup1").ephemeral(true)
|
||||||
}).await?;
|
}).await?;
|
||||||
let follow2 = interaction.create_followup_message(&ctx.http(), |followup| {
|
let follow2 = interaction.create_followup_message(ctx, |followup| {
|
||||||
followup.content("followup2").ephemeral(true)
|
followup.content("followup2").ephemeral(true)
|
||||||
}).await?;
|
}).await?;
|
||||||
|
|
||||||
interaction.delete_followup_message(&ctx.http(), follow1).await?;
|
interaction.delete_followup_message(ctx, follow1).await?;
|
||||||
interaction.delete_followup_message(&ctx.http(), follow2).await?;
|
interaction.delete_followup_message(ctx, follow2).await?;
|
||||||
|
|
||||||
println!("{:#?}", interaction.get_interaction_response(&ctx.http).await?);
|
|
||||||
// interaction.
|
|
||||||
|
|
||||||
println!("Interacted");
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ impl MixerCommand for PreferenceCommand {
|
|||||||
let user = match interaction.data.options.get(0).unwrap().options.get(0).unwrap().options.get(0).unwrap().resolved.as_ref().unwrap() {
|
let user = match interaction.data.options.get(0).unwrap().options.get(0).unwrap().options.get(0).unwrap().resolved.as_ref().unwrap() {
|
||||||
User(user, _) => user,
|
User(user, _) => user,
|
||||||
_ => {
|
_ => {
|
||||||
interaction.create_interaction_response(&ctx.http, |response| {
|
interaction.create_interaction_response(ctx, |response| {
|
||||||
response.kind(InteractionResponseType::ChannelMessageWithSource)
|
response.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||||
.interaction_response_data(|message| {
|
.interaction_response_data(|message| {
|
||||||
message.content(format!("User not found")).ephemeral(true)
|
message.content(format!("User not found")).ephemeral(true)
|
||||||
@@ -110,7 +110,7 @@ impl MixerCommand for PreferenceCommand {
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
interaction.create_interaction_response(&ctx.http, |response| {
|
interaction.create_interaction_response(ctx, |response| {
|
||||||
response.kind(InteractionResponseType::ChannelMessageWithSource)
|
response.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||||
.interaction_response_data(|message| {
|
.interaction_response_data(|message| {
|
||||||
message.content(format!("Preference set for {}", user.name)).ephemeral(true)
|
message.content(format!("Preference set for {}", user.name)).ephemeral(true)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use serenity::model::Permissions;
|
|||||||
use serenity::model::prelude::command::CommandOptionType;
|
use serenity::model::prelude::command::CommandOptionType;
|
||||||
use crate::bot::commands::MixerCommand;
|
use crate::bot::commands::MixerCommand;
|
||||||
use crate::database::DatabaseContainer;
|
use crate::database::DatabaseContainer;
|
||||||
|
use crate::mixer::rating::Rating;
|
||||||
use crate::mixer::role::Role;
|
use crate::mixer::role::Role;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -71,7 +72,7 @@ impl MixerCommand for RankCommand {
|
|||||||
let rank = interaction.data.options.get(0).unwrap().options.get(2).unwrap().value.as_ref().unwrap().as_u64().unwrap();
|
let rank = interaction.data.options.get(0).unwrap().options.get(2).unwrap().value.as_ref().unwrap().as_u64().unwrap();
|
||||||
|
|
||||||
if rank < 1 || rank > 5000 {
|
if rank < 1 || rank > 5000 {
|
||||||
interaction.create_interaction_response(&ctx.http, |response| {
|
interaction.create_interaction_response(ctx, |response| {
|
||||||
response.kind(InteractionResponseType::ChannelMessageWithSource)
|
response.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||||
.interaction_response_data(|message| {
|
.interaction_response_data(|message| {
|
||||||
message.content(format!("Rank must be between 1 and 5000")).ephemeral(true)
|
message.content(format!("Rank must be between 1 and 5000")).ephemeral(true)
|
||||||
@@ -83,10 +84,14 @@ impl MixerCommand for RankCommand {
|
|||||||
{
|
{
|
||||||
let data = ctx.data.read().await;
|
let data = ctx.data.read().await;
|
||||||
let db = data.get::<DatabaseContainer>().unwrap().read().await;
|
let db = data.get::<DatabaseContainer>().unwrap().read().await;
|
||||||
db.update_player_rank(user.id, role, rank as f32).await;
|
|
||||||
|
db.update_player_rank(user.id, role, Rating::new_no_sigma(
|
||||||
|
rank as f32,
|
||||||
|
300.0
|
||||||
|
)).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
interaction.create_interaction_response(&ctx.http, |response| {
|
interaction.create_interaction_response(ctx, |response| {
|
||||||
response.kind(InteractionResponseType::ChannelMessageWithSource)
|
response.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||||
.interaction_response_data(|message| {
|
.interaction_response_data(|message| {
|
||||||
message.content(format!("Setting rank for user {:?} to {:?} {:?}", user.id, role, rank))
|
message.content(format!("Setting rank for user {:?} to {:?} {:?}", user.id, role, rank))
|
||||||
|
|||||||
105
src/bot/commands/settings.rs
Normal file
105
src/bot/commands/settings.rs
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
use std::collections::hash_map::RandomState;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use serenity::builder::CreateApplicationCommand;
|
||||||
|
use serenity::client::Context;
|
||||||
|
use serenity::model::application::interaction::application_command::{ApplicationCommandInteraction, CommandDataOption};
|
||||||
|
use serenity::model::application::interaction::InteractionResponseType;
|
||||||
|
use serenity::async_trait;
|
||||||
|
use serenity::model::application::command::CommandOptionType;
|
||||||
|
use serenity::model::Permissions;
|
||||||
|
use crate::bot::commands::MixerCommand;
|
||||||
|
use crate::mixer::role::Role;
|
||||||
|
|
||||||
|
pub struct SettingsCommand;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl MixerCommand for SettingsCommand {
|
||||||
|
fn name(&self) -> String {
|
||||||
|
"settings".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create(&self, command: &mut CreateApplicationCommand) {
|
||||||
|
command.name(&self.name()).description("Change your server settings")
|
||||||
|
.create_option(|option| {
|
||||||
|
option.name("roles").kind(CommandOptionType::SubCommandGroup)
|
||||||
|
.description("Change your role settings")
|
||||||
|
.create_sub_option(|sub_option| {
|
||||||
|
sub_option.name("automatic").kind(CommandOptionType::SubCommand)
|
||||||
|
.description("Automatically assign roles based on your rank")
|
||||||
|
})
|
||||||
|
}).default_member_permissions(Permissions::ADMINISTRATOR)
|
||||||
|
.dm_permission(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute(&self, ctx: &Context, interaction: ApplicationCommandInteraction) -> serenity::Result<()> {
|
||||||
|
let data = interaction.data.options.get(0).unwrap().clone();
|
||||||
|
match data.name.as_str() {
|
||||||
|
"roles" => self.process_roles_subcommand(ctx, interaction.clone(), data).await?,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
interaction.create_interaction_response(ctx, |response| {
|
||||||
|
response.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||||
|
.interaction_response_data(|message| {
|
||||||
|
message.content("Settings updated")
|
||||||
|
})
|
||||||
|
}).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SettingsCommand {
|
||||||
|
async fn process_roles_subcommand(&self, ctx: &Context, interaction: ApplicationCommandInteraction, data: CommandDataOption) -> serenity::Result<()> {
|
||||||
|
match data.options.get(0).unwrap().name.as_str() {
|
||||||
|
"automatic" => {
|
||||||
|
let roles: HashMap<_, _, RandomState> = HashMap::from_iter([
|
||||||
|
("support", Role::Support),
|
||||||
|
("damage", Role::Dps),
|
||||||
|
("dps", Role::Dps),
|
||||||
|
("tank", Role::Tank)
|
||||||
|
].into_iter());
|
||||||
|
let ranks: HashMap<_, _, RandomState> = HashMap::from_iter([
|
||||||
|
("bronze", 0),
|
||||||
|
("silver", 1),
|
||||||
|
("gold", 2),
|
||||||
|
("platinum", 3),
|
||||||
|
("diamond", 4),
|
||||||
|
("master", 5),
|
||||||
|
("grandmaster", 6),
|
||||||
|
].into_iter());
|
||||||
|
|
||||||
|
let guild = interaction.guild_id.unwrap().to_partial_guild(ctx).await.unwrap();
|
||||||
|
// let guild_roles = guild.roles;
|
||||||
|
let mut guild_roles = HashMap::new();
|
||||||
|
for (id, guild_role) in guild.roles {
|
||||||
|
for (name, role) in roles.iter() {
|
||||||
|
if !(guild_role.name.to_lowercase().contains(name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (name, rank) in ranks.iter() {
|
||||||
|
if !(guild_role.name.to_lowercase().contains(name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if !guild_roles.contains_key(&id) {
|
||||||
|
guild_roles.insert(id, (role, rank));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let (_, old_rank) = guild_roles.get(&id).unwrap();
|
||||||
|
if rank > *old_rank {
|
||||||
|
guild_roles.insert(id, (role, rank));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("{:#?}", guild_roles);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
|
||||||
use serenity::builder::CreateApplicationCommands;
|
use serenity::builder::CreateApplicationCommands;
|
||||||
use serenity::client::Context;
|
use serenity::client::Context;
|
||||||
use serenity::model::application::interaction::application_command::ApplicationCommandInteraction;
|
use serenity::model::application::interaction::application_command::ApplicationCommandInteraction;
|
||||||
use serenity::prelude::TypeMapKey;
|
|
||||||
use crate::bot::commands::MixerCommand;
|
use crate::bot::commands::MixerCommand;
|
||||||
|
|
||||||
pub struct MixerCommandHandler {
|
pub struct MixerCommandHandler {
|
||||||
@@ -17,6 +15,10 @@ impl MixerCommandHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn add_command<T: MixerCommand + 'static>(&mut self, command: T) {
|
||||||
|
self.commands.insert(command.name(), Box::new(command));
|
||||||
|
}
|
||||||
|
|
||||||
pub fn create_all(&self, create_commands: &mut CreateApplicationCommands) {
|
pub fn create_all(&self, create_commands: &mut CreateApplicationCommands) {
|
||||||
self.commands.values().for_each(|command| {
|
self.commands.values().for_each(|command| {
|
||||||
create_commands.create_application_command(|create_command| {
|
create_commands.create_application_command(|create_command| {
|
||||||
@@ -35,9 +37,3 @@ impl MixerCommandHandler {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct MixerCommandHandlerContainer;
|
|
||||||
|
|
||||||
impl TypeMapKey for MixerCommandHandlerContainer {
|
|
||||||
type Value = Arc<MixerCommandHandler>;
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
use serenity::client::{Context, EventHandler};
|
|
||||||
use serenity::model::gateway::Ready;
|
|
||||||
use serenity::async_trait;
|
|
||||||
use serenity::model::application::command::Command;
|
|
||||||
use serenity::model::application::interaction::Interaction;
|
|
||||||
use serenity::model::id::ChannelId;
|
|
||||||
use serenity::model::prelude::{GuildId, VoiceState};
|
|
||||||
use crate::database::DatabaseContainer;
|
|
||||||
use crate::bot::handlers::command_handler::MixerCommandHandlerContainer;
|
|
||||||
|
|
||||||
pub struct Handler;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl EventHandler for Handler {
|
|
||||||
async fn ready(&self, ctx: Context, data_about_bot: Ready) {
|
|
||||||
println!("{} is connected!", data_about_bot.user.name);
|
|
||||||
let data = ctx.data.read().await;
|
|
||||||
let command_handler = data.get::<MixerCommandHandlerContainer>().unwrap();
|
|
||||||
|
|
||||||
Command::set_global_application_commands(&ctx.http, |commands| {
|
|
||||||
command_handler.create_all(commands);
|
|
||||||
commands
|
|
||||||
}).await.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn voice_state_update(&self, ctx: Context, old: Option<VoiceState>, new: VoiceState) {
|
|
||||||
let data = ctx.data.read().await;
|
|
||||||
let db = data.get::<DatabaseContainer>().unwrap().read().await;
|
|
||||||
|
|
||||||
let user_id = new.user_id;
|
|
||||||
let new_channel_id = new.channel_id.unwrap_or(ChannelId(0));
|
|
||||||
let guild_id = new.guild_id.unwrap_or(GuildId(0));
|
|
||||||
|
|
||||||
match db.get_lobby_by_channel(guild_id, new_channel_id).await {
|
|
||||||
Some(_) => {
|
|
||||||
if !db.has_player(user_id).await {
|
|
||||||
db.insert_player(user_id).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("{} joined lobby", user_id);
|
|
||||||
}
|
|
||||||
None => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn interaction_create(&self, ctx: Context, interaction: Interaction) {
|
|
||||||
let data = ctx.data.read().await;
|
|
||||||
let command_handler = data.get::<MixerCommandHandlerContainer>().unwrap();
|
|
||||||
|
|
||||||
match interaction {
|
|
||||||
Interaction::ApplicationCommand(command) => {
|
|
||||||
command_handler.handle_command(&ctx, command).await.unwrap();
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +1 @@
|
|||||||
pub mod event_handler;
|
|
||||||
pub mod command_handler;
|
pub mod command_handler;
|
||||||
117
src/bot/mod.rs
117
src/bot/mod.rs
@@ -3,79 +3,78 @@ pub mod interactions;
|
|||||||
mod handlers;
|
mod handlers;
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use serenity::client::{Context, EventHandler};
|
||||||
use serenity::{CacheAndHttp, Client};
|
use serenity::http::CacheHttp;
|
||||||
use serenity::prelude::{GatewayIntents, TypeMap, TypeMapKey};
|
use serenity::model::application::interaction::{Interaction, InteractionResponseType};
|
||||||
use tokio::sync::RwLock;
|
use serenity::model::gateway::Ready;
|
||||||
|
use serenity::async_trait;
|
||||||
|
use serenity::model::application::command::Command;
|
||||||
|
use serenity::model::prelude::VoiceState;
|
||||||
|
|
||||||
use crate::bot::commands::MixerCommand;
|
use crate::bot::commands::MixerCommand;
|
||||||
use crate::database::{MixerDatabase, DatabaseContainer};
|
use crate::bot::handlers::command_handler::MixerCommandHandler;
|
||||||
use crate::bot::handlers::command_handler::{MixerCommandHandler, MixerCommandHandlerContainer};
|
use crate::database::DatabaseContainer;
|
||||||
use crate::bot::handlers::event_handler::Handler;
|
|
||||||
|
|
||||||
pub struct MixerBot {
|
pub struct MixerBot {
|
||||||
token: String,
|
command_handler: MixerCommandHandler,
|
||||||
commands: Option<HashMap<String, Box<dyn MixerCommand>>>,
|
|
||||||
// lobbies: Vec<Lobby>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct MixerBotContainer;
|
|
||||||
|
|
||||||
|
|
||||||
impl TypeMapKey for MixerBotContainer {
|
|
||||||
type Value = Arc<RwLock<MixerBot>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
impl MixerBot {
|
impl MixerBot {
|
||||||
pub fn new(token: String) -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
token,
|
command_handler: MixerCommandHandler::new(HashMap::new())
|
||||||
commands: Some(HashMap::new()),
|
|
||||||
// lobbies: vec![]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn start(mut self) -> serenity::Result<()> {
|
|
||||||
let mut client = Client::builder(&self.token, GatewayIntents::all()).event_handler(Handler).await?;
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut data = client.data.write().await;
|
|
||||||
data.insert::<MixerCommandHandlerContainer>(Arc::new(MixerCommandHandler::new(self.commands.unwrap())));
|
|
||||||
self.commands = None;
|
|
||||||
|
|
||||||
data.insert::<MixerBotContainer>(Arc::new(RwLock::new(self)));
|
|
||||||
|
|
||||||
let db = MixerDatabase::new("sqlite://database/data.db?mode=rwc").await;
|
|
||||||
db.init("./database/script.sql").await;
|
|
||||||
data.insert::<DatabaseContainer>(Arc::new(RwLock::new(db)));
|
|
||||||
}
|
|
||||||
|
|
||||||
let shard_manager = client.shard_manager.clone();
|
|
||||||
let cache_and_http = client.cache_and_http.clone();
|
|
||||||
let data = client.data.clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
tokio::signal::ctrl_c().await.expect("Could not register ctrl+c handler");
|
|
||||||
|
|
||||||
let data_ = data.clone();
|
|
||||||
let data_ = data_.read().await;
|
|
||||||
let bot = data_.get::<MixerBotContainer>().unwrap();
|
|
||||||
bot.write().await.shutdown(data, cache_and_http).await;
|
|
||||||
|
|
||||||
shard_manager.lock().await.shutdown_all().await;
|
|
||||||
});
|
|
||||||
|
|
||||||
client.start().await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_command<T: MixerCommand + 'static>(&mut self, command: T) -> &mut Self {
|
pub fn add_command<T: MixerCommand + 'static>(&mut self, command: T) -> &mut Self {
|
||||||
self.commands.as_mut().unwrap().insert(command.name(), Box::new(command));
|
self.command_handler.add_command(command);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn shutdown(&mut self, data: Arc<RwLock<TypeMap>>, cache_and_http: Arc<CacheAndHttp>) {
|
#[async_trait]
|
||||||
|
impl EventHandler for MixerBot {
|
||||||
|
async fn ready(&self, ctx: Context, data_about_bot: Ready) {
|
||||||
|
println!("{} is connected!", data_about_bot.user.name);
|
||||||
|
|
||||||
println!("Bot has been shutdown.");
|
Command::set_global_application_commands(&ctx.http, |commands| {
|
||||||
|
self.command_handler.create_all(commands);
|
||||||
|
commands
|
||||||
|
}).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn voice_state_update(&self, ctx: Context, _old: Option<VoiceState>, new: VoiceState) {
|
||||||
|
if let Some(guild_id) = new.guild_id {
|
||||||
|
if let Some(channel_id) = new.channel_id {
|
||||||
|
let data = ctx.data.read().await;
|
||||||
|
let db = data.get::<DatabaseContainer>().unwrap().read().await;
|
||||||
|
|
||||||
|
if let Some(_) = db.get_lobby_by_channel(guild_id, channel_id).await {
|
||||||
|
if let Some(member) = new.member {
|
||||||
|
if member.user.bot {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if db.get_player(new.user_id).await.is_none() {
|
||||||
|
db.insert_player(new.user_id).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn interaction_create(&self, ctx: Context, interaction: Interaction) {
|
||||||
|
match interaction {
|
||||||
|
Interaction::ApplicationCommand(command) => {
|
||||||
|
self.command_handler.handle_command(&ctx, command).await.unwrap();
|
||||||
|
}
|
||||||
|
Interaction::MessageComponent(component) => {
|
||||||
|
component.create_interaction_response(ctx.http(), |response| {
|
||||||
|
response.kind(InteractionResponseType::DeferredUpdateMessage)
|
||||||
|
}).await.unwrap();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
pub mod models;
|
pub mod models;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use sea_orm::{ActiveModelTrait, ColumnTrait, ConnectionTrait, Database, DatabaseConnection, EntityTrait, IntoActiveModel, QueryFilter};
|
use sea_orm::{ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, IntoActiveModel, QueryFilter, SqlxPostgresConnector};
|
||||||
use sea_orm::ActiveValue::Set;
|
use sea_orm::ActiveValue::Set;
|
||||||
use serenity::model::id::{ChannelId, UserId};
|
use serenity::model::id::{ChannelId, UserId};
|
||||||
use serenity::model::prelude::GuildId;
|
use serenity::model::prelude::GuildId;
|
||||||
use serenity::prelude::TypeMapKey;
|
use serenity::prelude::TypeMapKey;
|
||||||
use tokio::fs;
|
use sqlx::PgPool;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
|
use crate::mixer::rating::Rating;
|
||||||
use crate::mixer::role::Role;
|
use crate::mixer::role::Role;
|
||||||
|
|
||||||
pub struct MixerDatabase {
|
pub struct MixerDatabase {
|
||||||
@@ -15,32 +16,12 @@ pub struct MixerDatabase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl MixerDatabase {
|
impl MixerDatabase {
|
||||||
pub async fn new(url: &str) -> Self {
|
pub fn new (pool: PgPool) -> Self {
|
||||||
Self {
|
Self {
|
||||||
connection: Database::connect(url).await.expect("Could not connect to database")
|
connection: SqlxPostgresConnector::from_sqlx_postgres_pool(pool)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn init(&self, script_path: &str) {
|
|
||||||
let script = fs::read_to_string(script_path).await.expect("Provided script path for creating database does not exist");
|
|
||||||
|
|
||||||
self.connection.execute_unprepared(&script).await.expect("Invalid database creating script provided");
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_connection(&self) -> DatabaseConnection {
|
|
||||||
self.connection.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
//CREATE TABLE IF NOT EXISTS players (
|
|
||||||
// id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
// discord_id INTEGER NOT NULL,
|
|
||||||
// bn_name TEXT,
|
|
||||||
// bn_tag TEXT,
|
|
||||||
// tank REAL NOT NULL,
|
|
||||||
// dps REAL NOT NULL,
|
|
||||||
// support REAL NOT NULL,
|
|
||||||
// UNIQUE (discord_id)
|
|
||||||
// );
|
|
||||||
pub async fn insert_player(&self, id: UserId) {
|
pub async fn insert_player(&self, id: UserId) {
|
||||||
let player = models::player::ActiveModel {
|
let player = models::player::ActiveModel {
|
||||||
discord_id: Set(id.0 as i64),
|
discord_id: Set(id.0 as i64),
|
||||||
@@ -54,17 +35,29 @@ impl MixerDatabase {
|
|||||||
.expect("Could not insert player into database");
|
.expect("Could not insert player into database");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update_player_rank(&self, id: UserId, role: Option<Role>, rank: f32) {
|
pub async fn update_player_rank(&self, id: UserId, role: Option<Role>, rating: Rating) {
|
||||||
if !self.has_player(id).await {
|
if self.get_player(id).await.is_none() {
|
||||||
self.insert_player(id).await;
|
self.insert_player(id).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut player = self.get_player(id).await.unwrap().into_active_model();
|
let mut player = self.get_player(id).await.unwrap().into_active_model();
|
||||||
|
|
||||||
match role {
|
match role {
|
||||||
Some(Role::Tank) => player.tank = Set(rank),
|
Some(Role::Tank) => {
|
||||||
Some(Role::Dps) => player.dps = Set(rank),
|
player.tank_rating = Set(rating.value);
|
||||||
Some(Role::Support) => player.support = Set(rank),
|
player.tank_rd = Set(rating.rd);
|
||||||
|
player.tank_volatility = Set(rating.volatility);
|
||||||
|
},
|
||||||
|
Some(Role::Dps) => {
|
||||||
|
player.dps_rating = Set(rating.value);
|
||||||
|
player.dps_rd = Set(rating.rd);
|
||||||
|
player.dps_volatility = Set(rating.volatility);
|
||||||
|
},
|
||||||
|
Some(Role::Support) => {
|
||||||
|
player.support_rating = Set(rating.value);
|
||||||
|
player.support_rd = Set(rating.rd);
|
||||||
|
player.support_volatility = Set(rating.volatility);
|
||||||
|
},
|
||||||
None => return
|
None => return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +67,7 @@ impl MixerDatabase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update_player_preference(&self, id: UserId, flex: bool, primary: Option<Role>, secondary: Option<Role>, tertiary: Option<Role>) {
|
pub async fn update_player_preference(&self, id: UserId, flex: bool, primary: Option<Role>, secondary: Option<Role>, tertiary: Option<Role>) {
|
||||||
if !self.has_player(id).await {
|
if self.get_player(id).await.is_none() {
|
||||||
self.insert_player(id).await;
|
self.insert_player(id).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,29 +99,20 @@ impl MixerDatabase {
|
|||||||
.expect("Could not get players from database")
|
.expect("Could not get players from database")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn has_player(&self, id: UserId) -> bool {
|
// pub async fn get_all_guild_lobbies(&self, guild_id: GuildId) -> Vec<models::lobby::Model> {
|
||||||
models::player::Entity::find()
|
// models::lobby::Entity::find()
|
||||||
.filter(models::player::Column::DiscordId.eq(id.0))
|
// .filter(models::lobby::Column::GuildId.eq(guild_id.0))
|
||||||
.one(&self.connection)
|
// .all(&self.connection)
|
||||||
.await
|
// .await
|
||||||
.expect("Could not get player from database")
|
// .expect("Could not get lobbies from database")
|
||||||
.is_some()
|
// }
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_all_guild_lobbies(&self, guild_id: GuildId) -> Vec<models::lobby::Model> {
|
// pub async fn get_guild_lobby(&self, lobby_id: i32) -> Option<models::lobby::Model> {
|
||||||
models::lobby::Entity::find()
|
// models::lobby::Entity::find_by_id(lobby_id)
|
||||||
.filter(models::lobby::Column::GuildId.eq(guild_id.0))
|
// .one(&self.connection)
|
||||||
.all(&self.connection)
|
// .await
|
||||||
.await
|
// .expect("Could not get lobby from database")
|
||||||
.expect("Could not get lobbies from database")
|
// }
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_guild_lobby(&self, lobby_id: i32) -> Option<models::lobby::Model> {
|
|
||||||
models::lobby::Entity::find_by_id(lobby_id)
|
|
||||||
.one(&self.connection)
|
|
||||||
.await
|
|
||||||
.expect("Could not get lobby from database")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn insert_guild_lobby(&self, guild_id: GuildId, main_voice_id: ChannelId, red_team_voice_id: ChannelId, blue_team_voice_id: ChannelId) {
|
pub async fn insert_guild_lobby(&self, guild_id: GuildId, main_voice_id: ChannelId, red_team_voice_id: ChannelId, blue_team_voice_id: ChannelId) {
|
||||||
let lobby = models::lobby::ActiveModel {
|
let lobby = models::lobby::ActiveModel {
|
||||||
|
|||||||
@@ -11,15 +11,31 @@ pub struct Model {
|
|||||||
pub bn_name: String,
|
pub bn_name: String,
|
||||||
#[sea_orm(column_type = "Text")]
|
#[sea_orm(column_type = "Text")]
|
||||||
pub bn_tag: String,
|
pub bn_tag: String,
|
||||||
#[sea_orm(default_value = 2500.0)]
|
|
||||||
pub tank: f32,
|
|
||||||
#[sea_orm(default_value = 2500.0)]
|
|
||||||
pub dps: f32,
|
|
||||||
#[sea_orm(default_value = 2500.0)]
|
|
||||||
pub support: f32,
|
|
||||||
|
|
||||||
#[sea_orm(default_value = true)]
|
#[sea_orm(default_value = true)]
|
||||||
pub flex: bool,
|
pub flex: bool,
|
||||||
|
|
||||||
|
#[sea_orm(default_value = 2500.0)]
|
||||||
|
pub tank_rating: f32,
|
||||||
|
#[sea_orm(default_value = 580.0)]
|
||||||
|
pub tank_rd: f32,
|
||||||
|
#[sea_orm(default_value = 0.06)]
|
||||||
|
pub tank_volatility: f32,
|
||||||
|
|
||||||
|
#[sea_orm(default_value = 2500.0)]
|
||||||
|
pub dps_rating: f32,
|
||||||
|
#[sea_orm(default_value = 580.0)]
|
||||||
|
pub dps_rd: f32,
|
||||||
|
#[sea_orm(default_value = 0.06)]
|
||||||
|
pub dps_volatility: f32,
|
||||||
|
|
||||||
|
#[sea_orm(default_value = 2500.0)]
|
||||||
|
pub support_rating: f32,
|
||||||
|
#[sea_orm(default_value = 580.0)]
|
||||||
|
pub support_rd: f32,
|
||||||
|
#[sea_orm(default_value = 0.06)]
|
||||||
|
pub support_volatility: f32,
|
||||||
|
|
||||||
#[sea_orm(default_value = -1)]
|
#[sea_orm(default_value = -1)]
|
||||||
pub primary_role: i32,
|
pub primary_role: i32,
|
||||||
#[sea_orm(default_value = -1)]
|
#[sea_orm(default_value = -1)]
|
||||||
|
|||||||
54
src/main.rs
54
src/main.rs
@@ -1,25 +1,65 @@
|
|||||||
mod bot;
|
mod bot;
|
||||||
mod mixer;
|
mod mixer;
|
||||||
mod database;
|
mod database;
|
||||||
|
mod algorithm;
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
use serenity::Client;
|
||||||
|
use serenity::prelude::GatewayIntents;
|
||||||
|
use shuttle_runtime::Context;
|
||||||
|
use shuttle_secrets::SecretStore;
|
||||||
|
use sqlx::{Executor, PgPool};
|
||||||
|
use tokio::sync::RwLock;
|
||||||
use crate::bot::commands::lobby::LobbyCommand;
|
use crate::bot::commands::lobby::LobbyCommand;
|
||||||
use crate::bot::commands::ping::PingCommand;
|
use crate::bot::commands::ping::PingCommand;
|
||||||
use crate::bot::commands::preference::PreferenceCommand;
|
use crate::bot::commands::preference::PreferenceCommand;
|
||||||
use crate::bot::commands::rank::RankCommand;
|
use crate::bot::commands::rank::RankCommand;
|
||||||
|
use crate::bot::commands::settings::SettingsCommand;
|
||||||
use crate::bot::MixerBot;
|
use crate::bot::MixerBot;
|
||||||
|
use crate::database::{DatabaseContainer, MixerDatabase};
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() -> serenity::Result<()> {
|
#[shuttle_runtime::main]
|
||||||
let mut bot = MixerBot::new(
|
async fn serenity(
|
||||||
"NTE2MzMyMzM2NzQ5NzQwMDUz.GiLPzQ.j5gIUGqx6vF6CFhJv8yizksDi-dOBqCvxR32EE".to_string()
|
#[shuttle_shared_db::Postgres(
|
||||||
);
|
local_uri = "postgres://postgres:{secrets.PASSWORD}@localhost:5432/postgres"
|
||||||
|
)] pool: PgPool,
|
||||||
|
#[shuttle_secrets::Secrets] secret_store: SecretStore
|
||||||
|
) -> shuttle_serenity::ShuttleSerenity {
|
||||||
|
let token = secret_store.get("DISCORD_TOKEN").context("'DISCORD_TOKEN' was not found")?;
|
||||||
|
let app_id = secret_store.get("DISCORD_APP_ID").context("'DISCORD_APP_ID' was not found")?;
|
||||||
|
|
||||||
|
pool.execute(include_str!("../schema.sql")).await.unwrap();
|
||||||
|
|
||||||
|
let mut bot = MixerBot::new();
|
||||||
|
|
||||||
bot.add_command(PingCommand);
|
bot.add_command(PingCommand);
|
||||||
bot.add_command(LobbyCommand);
|
bot.add_command(LobbyCommand);
|
||||||
bot.add_command(RankCommand);
|
bot.add_command(RankCommand);
|
||||||
bot.add_command(PreferenceCommand);
|
bot.add_command(PreferenceCommand);
|
||||||
|
bot.add_command(SettingsCommand);
|
||||||
|
|
||||||
bot.start().await?;
|
let client = Client::builder(&token, GatewayIntents::all())
|
||||||
|
.event_handler(bot)
|
||||||
|
.application_id(app_id.parse::<u64>().unwrap())
|
||||||
|
.await
|
||||||
|
.expect("Error creating client");
|
||||||
|
|
||||||
Ok(())
|
{
|
||||||
|
let mut data = client.data.write().await;
|
||||||
|
|
||||||
|
let db = MixerDatabase::new(pool);
|
||||||
|
data.insert::<DatabaseContainer>(Arc::new(RwLock::new(db)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let shard_manager = client.shard_manager.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
tokio::signal::ctrl_c().await.expect("Could not register ctrl+c handler");
|
||||||
|
|
||||||
|
println!("Bot has been shutdown.");
|
||||||
|
|
||||||
|
shard_manager.lock().await.shutdown_all().await;
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(client.into())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,78 +1,167 @@
|
|||||||
|
use std::cmp::Ordering;
|
||||||
|
use itertools::Itertools;
|
||||||
use crate::mixer::role::Role;
|
use crate::mixer::role::Role;
|
||||||
use crate::mixer::player::Player;
|
use crate::mixer::player::Player;
|
||||||
use crate::mixer::team::Team;
|
use crate::mixer::team::Team;
|
||||||
|
|
||||||
|
|
||||||
pub fn mix_players(players: &Vec<Player>, slots: Vec<Role>) -> Option<(Team, Team)> {
|
struct PlayerRoleEntry {
|
||||||
let mut players = players.clone();
|
pub index: usize,
|
||||||
let mut team1 = Team::new(slots.clone());
|
pub role: Role,
|
||||||
let mut team2 = Team::new(slots.clone());
|
pub priority: f32,
|
||||||
|
}
|
||||||
|
|
||||||
for _ in 0..(slots.len()*2) {
|
impl Eq for PlayerRoleEntry {}
|
||||||
let mut priorities = calculate_priorities(&players, &team1, &team2);
|
|
||||||
priorities.sort_by(|(_, _, p1), (_, _, p2)| p2.partial_cmp(p1).unwrap());
|
|
||||||
|
|
||||||
for (player, role, _) in priorities {
|
impl PartialEq for PlayerRoleEntry {
|
||||||
if team1.full_rank() > team2.full_rank() {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
if team2.has_slot(&role) {
|
self.index == other.index && self.role == other.role
|
||||||
team2.add_player(&player, &role);
|
|
||||||
players.retain(|p| p.id != player.id);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if team1.has_slot(&role) {
|
|
||||||
team1.add_player(&player, &role);
|
|
||||||
players.retain(|p| p.id != player.id);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if team1.count() < slots.len() || team2.count() < slots.len() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some((team1, team2))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn calculate_priorities(players: &Vec<Player>, team1: &Team, team2: &Team) -> Vec<(Player, Role, f32)> {
|
pub fn mix_players(players: &[Player], slots: Vec<Role>) -> Option<(Team, Team)> {
|
||||||
let mut priorities = Vec::new();
|
let players = players.to_vec();
|
||||||
|
|
||||||
for player in players {
|
let entries = calculate_priorities(&players);
|
||||||
for (role, priority) in player.base_priority() {
|
let tanks = entries.iter().filter(|e| e.role == Role::Tank).collect_vec();
|
||||||
priorities.push((player.clone(), role, priority));
|
let supports = entries.iter().filter(|e| e.role == Role::Support).collect_vec();
|
||||||
|
let dps = entries.iter().filter(|e| e.role == Role::Dps).collect_vec();
|
||||||
|
|
||||||
|
|
||||||
|
let tank_count = slots.iter().filter(|r| **r == Role::Tank).count();
|
||||||
|
let support_count = slots.iter().filter(|r| **r == Role::Support).count();
|
||||||
|
let dps_count = slots.iter().filter(|r| **r == Role::Dps).count();
|
||||||
|
|
||||||
|
let tank_combos = tanks.iter().combinations(tank_count)
|
||||||
|
.sorted_by(|a, b| {
|
||||||
|
let a = a.iter().map(|e| e.priority).sum::<f32>();
|
||||||
|
let b = b.iter().map(|e| e.priority).sum::<f32>();
|
||||||
|
a.partial_cmp(&b).unwrap_or(Ordering::Equal)
|
||||||
|
}).collect_vec();
|
||||||
|
let dps_combos = dps.iter().combinations(dps_count)
|
||||||
|
.sorted_by(|a, b| {
|
||||||
|
let a = a.iter().map(|e| e.priority).sum::<f32>();
|
||||||
|
let b = b.iter().map(|e| e.priority).sum::<f32>();
|
||||||
|
a.partial_cmp(&b).unwrap_or(Ordering::Equal)
|
||||||
|
}).collect_vec();
|
||||||
|
let support_combos = supports.iter().combinations(support_count)
|
||||||
|
.sorted_by(|a, b| {
|
||||||
|
let a = a.iter().map(|e| e.priority).sum::<f32>();
|
||||||
|
let b = b.iter().map(|e| e.priority).sum::<f32>();
|
||||||
|
a.partial_cmp(&b).unwrap_or(Ordering::Equal)
|
||||||
|
}).collect_vec();
|
||||||
|
|
||||||
|
let mut best_team1 = None;
|
||||||
|
let mut best_team2 = None;
|
||||||
|
let mut best_diff = None;
|
||||||
|
|
||||||
|
let threshold = 100.0;
|
||||||
|
|
||||||
|
// this is awful, but it works
|
||||||
|
for tank1_combo in &tank_combos {
|
||||||
|
for tank2_combo in &tank_combos {
|
||||||
|
if tank1_combo.iter().any(|e| tank2_combo.iter().any(|e2| e.index == e2.index)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for dps1_combo in &dps_combos {
|
||||||
|
if tank1_combo.iter().any(|e| dps1_combo.iter().any(|e2| e.index == e2.index)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if tank2_combo.iter().any(|e| dps1_combo.iter().any(|e2| e.index == e2.index)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for dps2_combo in &dps_combos {
|
||||||
|
if tank1_combo.iter().any(|e| dps2_combo.iter().any(|e2| e.index == e2.index)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if tank2_combo.iter().any(|e| dps2_combo.iter().any(|e2| e.index == e2.index)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if dps1_combo.iter().any(|e| dps2_combo.iter().any(|e2| e.index == e2.index)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for support1_combo in &support_combos {
|
||||||
|
if tank1_combo.iter().any(|e| support1_combo.iter().any(|e2| e.index == e2.index)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if tank2_combo.iter().any(|e| support1_combo.iter().any(|e2| e.index == e2.index)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if dps1_combo.iter().any(|e| support1_combo.iter().any(|e2| e.index == e2.index)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if dps2_combo.iter().any(|e| support1_combo.iter().any(|e2| e.index == e2.index)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for support2_combo in &support_combos {
|
||||||
|
if tank1_combo.iter().any(|e| support2_combo.iter().any(|e2| e.index == e2.index)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if tank2_combo.iter().any(|e| support2_combo.iter().any(|e2| e.index == e2.index)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if dps1_combo.iter().any(|e| support2_combo.iter().any(|e2| e.index == e2.index)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if dps2_combo.iter().any(|e| support2_combo.iter().any(|e2| e.index == e2.index)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if support1_combo.iter().any(|e| support2_combo.iter().any(|e2| e.index == e2.index)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut team1 = Team::new(slots.clone());
|
||||||
|
let mut team2 = Team::new(slots.clone());
|
||||||
|
|
||||||
|
for entry in tank1_combo.iter().chain(dps1_combo.iter()).chain(support1_combo.iter()) {
|
||||||
|
team1.add_player(entry.index, &entry.role);
|
||||||
|
}
|
||||||
|
|
||||||
|
for entry in tank2_combo.iter().chain(dps2_combo.iter()).chain(support2_combo.iter()) {
|
||||||
|
team2.add_player(entry.index, &entry.role);
|
||||||
|
}
|
||||||
|
|
||||||
|
let diff_rating = (team1.full_rating(&players).value - team2.full_rating(&players).value).abs();
|
||||||
|
let diff_rating_tank = (team1.full_rating_role(&Role::Tank, &players).value - team2.full_rating_role(&Role::Tank, &players).value).abs();
|
||||||
|
let diff_rating_dps = (team1.full_rating_role(&Role::Dps, &players).value - team2.full_rating_role(&Role::Dps, &players).value).abs();
|
||||||
|
let diff_rating_support = (team1.full_rating_role(&Role::Support, &players).value - team2.full_rating_role(&Role::Support, &players).value).abs();
|
||||||
|
let diff = diff_rating + (diff_rating_tank + diff_rating_dps + diff_rating_support);
|
||||||
|
|
||||||
|
if diff + threshold < best_diff.unwrap_or(f32::MAX) {
|
||||||
|
if diff < threshold {
|
||||||
|
return Some((team1, team2));
|
||||||
|
}
|
||||||
|
|
||||||
|
best_team1 = Some(team1);
|
||||||
|
best_team2 = Some(team2);
|
||||||
|
best_diff = Some(diff);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for item in &mut priorities {
|
if let (Some(team1), Some(team2)) = (best_team1, best_team2) {
|
||||||
let (player, role, _) = item;
|
Some((team1, team2))
|
||||||
let empty_teams = team1.count_role(role) == 0 && team2.count_role(role) == 0;
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let role_diff_rank = 1.0 + {
|
fn calculate_priorities(players: &[Player]) -> Vec<PlayerRoleEntry> {
|
||||||
if empty_teams {
|
let mut priorities = Vec::new();
|
||||||
0.0
|
|
||||||
} else {
|
|
||||||
(player.ranks[role] - (team1.full_rank_role(role) - team2.full_rank_role(role)).abs()).abs()
|
|
||||||
}
|
|
||||||
} as f32;
|
|
||||||
|
|
||||||
let sum_average_rank = team1.average_rank_role(role) + team2.average_rank_role(role);
|
for (i, player) in players.iter().enumerate() {
|
||||||
let role_diff_avg_rank = 1.0 + {
|
for (role, priority) in player.base_priority() {
|
||||||
if empty_teams {
|
priorities.push(PlayerRoleEntry { index: i, role, priority });
|
||||||
let filtered_players = players.iter().filter(|player| player.priority_roles.contains(&Some(role.clone())) || player.flex).collect::<Vec<&Player>>();
|
}
|
||||||
(player.ranks[role] - filtered_players.iter().map(|player| player.ranks[role]).sum::<f32>() / filtered_players.len() as f32).abs()
|
|
||||||
} else {
|
|
||||||
(player.ranks[role] - sum_average_rank).abs()
|
|
||||||
}
|
|
||||||
} as f32;
|
|
||||||
|
|
||||||
let team_rank_difference = (team1.average_rank_role(role) - team2.average_rank_role(role)).abs();
|
|
||||||
let rank_difference_weight = 1.0 + 1.5 * team_rank_difference;
|
|
||||||
|
|
||||||
let complex_coefficient = role_diff_rank * role_diff_avg_rank * rank_difference_weight;
|
|
||||||
item.2 *= player.ranks[role] / complex_coefficient;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
priorities
|
priorities
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ pub mod mixer;
|
|||||||
pub mod role;
|
pub mod role;
|
||||||
pub mod player;
|
pub mod player;
|
||||||
pub mod team;
|
pub mod team;
|
||||||
|
pub mod rating;
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use serenity::model::id::UserId;
|
||||||
use crate::database::models::player::Model;
|
use crate::database::models::player::Model;
|
||||||
|
use crate::mixer::rating::Rating;
|
||||||
use crate::mixer::role::Role;
|
use crate::mixer::role::Role;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct Player {
|
pub struct Player {
|
||||||
pub(crate) id: i32,
|
pub(crate) id: i32,
|
||||||
pub(crate) name: String,
|
pub(crate) discord_id: UserId,
|
||||||
|
pub(crate) bn_name: String,
|
||||||
|
pub(crate) bn_tag: String,
|
||||||
|
|
||||||
pub(crate) ranks: HashMap<Role, f32>,
|
pub(crate) ranks: HashMap<Role, Rating>,
|
||||||
pub(crate) flex: bool,
|
pub(crate) flex: bool,
|
||||||
pub(crate) priority_roles: Vec<Option<Role>>,
|
pub(crate) priority_roles: Vec<Option<Role>>,
|
||||||
}
|
}
|
||||||
@@ -17,10 +21,21 @@ impl Player {
|
|||||||
pub fn new(model: Model) -> Self {
|
pub fn new(model: Model) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: model.id,
|
id: model.id,
|
||||||
name: model.bn_name,
|
discord_id: UserId::from(model.discord_id as u64),
|
||||||
ranks: vec![(Role::Tank, model.tank), (Role::Dps, model.dps), (Role::Support, model.support)].into_iter().collect(),
|
bn_name: model.bn_name,
|
||||||
|
bn_tag: model.bn_tag,
|
||||||
|
|
||||||
|
ranks: vec![
|
||||||
|
(Role::Tank, Rating::new(model.tank_rating, model.tank_rd, model.tank_volatility)),
|
||||||
|
(Role::Dps, Rating::new(model.dps_rating, model.dps_rd, model.dps_volatility)),
|
||||||
|
(Role::Support, Rating::new(model.support_rating, model.support_rd, model.support_volatility))
|
||||||
|
].into_iter().collect(),
|
||||||
|
|
||||||
flex: model.flex,
|
flex: model.flex,
|
||||||
priority_roles: vec![model.primary_role, model.secondary_role, model.tertiary_role].into_iter().map(|role| {
|
priority_roles: vec![
|
||||||
|
model.primary_role,
|
||||||
|
model.secondary_role, model.tertiary_role
|
||||||
|
].into_iter().map(|role| {
|
||||||
match role {
|
match role {
|
||||||
-1 => None,
|
-1 => None,
|
||||||
_ => Some(Role::from(role))
|
_ => Some(Role::from(role))
|
||||||
@@ -29,24 +44,43 @@ impl Player {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn to_model(self) -> Model {
|
||||||
|
Model {
|
||||||
|
id: self.id,
|
||||||
|
discord_id: self.discord_id.0 as i64,
|
||||||
|
bn_name: self.bn_name,
|
||||||
|
bn_tag: self.bn_tag,
|
||||||
|
tank_rating: self.ranks.get(&Role::Tank).unwrap().value,
|
||||||
|
tank_rd: self.ranks.get(&Role::Tank).unwrap().rd,
|
||||||
|
tank_volatility: self.ranks.get(&Role::Tank).unwrap().volatility,
|
||||||
|
dps_rating: self.ranks.get(&Role::Dps).unwrap().value,
|
||||||
|
dps_rd: self.ranks.get(&Role::Dps).unwrap().rd,
|
||||||
|
dps_volatility: self.ranks.get(&Role::Dps).unwrap().volatility,
|
||||||
|
support_rating: self.ranks.get(&Role::Support).unwrap().value,
|
||||||
|
support_rd: self.ranks.get(&Role::Support).unwrap().rd,
|
||||||
|
support_volatility: self.ranks.get(&Role::Support).unwrap().volatility,
|
||||||
|
flex: self.flex,
|
||||||
|
primary_role: Role::option_to_i32(self.priority_roles.get(0).unwrap().clone()),
|
||||||
|
secondary_role: Role::option_to_i32(self.priority_roles.get(1).unwrap().clone()),
|
||||||
|
tertiary_role: Role::option_to_i32(self.priority_roles.get(2).unwrap().clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn base_priority(&self) -> HashMap<Role, f32> {
|
pub fn base_priority(&self) -> HashMap<Role, f32> {
|
||||||
let mut priorities = HashMap::new();
|
let mut priorities = HashMap::new();
|
||||||
let priority_points = 100.0;
|
let priority_points = 100.0;
|
||||||
|
|
||||||
if self.flex {
|
if self.flex {
|
||||||
for role in Role::iter() {
|
for role in Role::iter() {
|
||||||
priorities.insert(role, (priority_points / self.priority_roles.len() as f32) as f32);
|
priorities.insert(role, 1.5 * (priority_points / (Role::iter().count()) as f32));
|
||||||
}
|
}
|
||||||
|
|
||||||
return priorities;
|
return priorities;
|
||||||
}
|
}
|
||||||
|
|
||||||
let count = self.priority_roles.iter().filter(|role| role.is_some()).count() as f32;
|
|
||||||
let denominator = count * (count + 1.0) * (2.0 * count + 1.0) / 6.0;
|
|
||||||
|
|
||||||
for (i, role) in self.priority_roles.iter().enumerate() {
|
for (i, role) in self.priority_roles.iter().enumerate() {
|
||||||
if let Some(role) = role {
|
if let Some(role) = role {
|
||||||
priorities.insert(role.clone(), priority_points * (count - i as f32)*(count - i as f32) / denominator as f32);
|
priorities.insert(role.clone(), priority_points / (i + 1) as f32);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
102
src/mixer/rating.rs
Normal file
102
src/mixer/rating.rs
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
use std::cmp::Ordering;
|
||||||
|
use std::iter::Sum;
|
||||||
|
use std::ops::{Add, AddAssign, Div};
|
||||||
|
use crate::algorithm::glicko2;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct Rating {
|
||||||
|
pub value: f32,
|
||||||
|
pub rd: f32,
|
||||||
|
pub volatility: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for Rating {}
|
||||||
|
|
||||||
|
impl Ord for Rating {
|
||||||
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
|
self.value.partial_cmp(&other.value).unwrap_or(Ordering::Equal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for Rating {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Rating {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
value: 2500.0,
|
||||||
|
rd: 580.0,
|
||||||
|
volatility: 0.06,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Add for Rating {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn add(self, other: Self) -> Self::Output {
|
||||||
|
Self {
|
||||||
|
value: self.value + other.value,
|
||||||
|
rd: self.rd + other.rd,
|
||||||
|
volatility: self.volatility + other.volatility,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AddAssign for Rating {
|
||||||
|
fn add_assign(&mut self, other: Self) {
|
||||||
|
self.value += other.value;
|
||||||
|
self.rd += other.rd;
|
||||||
|
self.volatility += other.volatility;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Div<f32> for Rating {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn div(self, rhs: f32) -> Self::Output {
|
||||||
|
Self {
|
||||||
|
value: self.value / rhs,
|
||||||
|
rd: self.rd / rhs,
|
||||||
|
volatility: self.volatility / rhs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sum for Rating {
|
||||||
|
fn sum<I: Iterator<Item=Self>>(iter: I) -> Self {
|
||||||
|
let mut sum = Self::zero();
|
||||||
|
for rating in iter {
|
||||||
|
sum += rating;
|
||||||
|
}
|
||||||
|
sum
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Rating {
|
||||||
|
pub fn new(value: f32, rd: f32, sigma: f32) -> Self {
|
||||||
|
Self {
|
||||||
|
value,
|
||||||
|
rd,
|
||||||
|
volatility: sigma,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn zero() -> Self {
|
||||||
|
Self::new(0.0, 0.0, 0.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_no_sigma(value: f32, rd: f32) -> Self {
|
||||||
|
Self::new(value, rd, 0.06)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(&mut self, other: &Self, score: f32) {
|
||||||
|
let (value, rd, sigma) = glicko2::update(self.value, self.rd, self.volatility, other.value, other.rd, 5.0/3.0, score);
|
||||||
|
self.value = value;
|
||||||
|
self.rd = rd;
|
||||||
|
self.volatility = sigma;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,16 @@ impl FromStr for Role {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<Role> for i32 {
|
||||||
|
fn from(role: Role) -> Self {
|
||||||
|
match role {
|
||||||
|
Role::Tank => 0,
|
||||||
|
Role::Dps => 1,
|
||||||
|
Role::Support => 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<i32> for Role {
|
impl From<i32> for Role {
|
||||||
fn from(i: i32) -> Self {
|
fn from(i: i32) -> Self {
|
||||||
match i {
|
match i {
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use crate::mixer::player::Player;
|
use crate::mixer::player::Player;
|
||||||
|
use crate::mixer::rating::Rating;
|
||||||
use crate::mixer::role::Role;
|
use crate::mixer::role::Role;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Team {
|
pub struct Team {
|
||||||
pub players: HashMap<(Role, i32), Option<Player>>
|
pub players: HashMap<(Role, i32), Option<usize>>,
|
||||||
|
|
||||||
|
max_role: HashMap<Role, usize>,
|
||||||
|
count_role: HashMap<Role, usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -19,61 +23,80 @@ impl Team {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
players
|
players
|
||||||
}
|
},
|
||||||
|
max_role: {
|
||||||
|
let mut max_role = HashMap::new();
|
||||||
|
for role in Role::iter() {
|
||||||
|
max_role.insert(role, slots.iter().filter(|slot| **slot == role).count());
|
||||||
|
}
|
||||||
|
max_role
|
||||||
|
},
|
||||||
|
count_role: {
|
||||||
|
let mut count_role = HashMap::new();
|
||||||
|
for role in Role::iter() {
|
||||||
|
count_role.insert(role, 0);
|
||||||
|
}
|
||||||
|
count_role
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn count(&self) -> usize {
|
pub fn count(&self) -> usize {
|
||||||
self.players.iter().filter(|(_, player)| player.is_some()).count()
|
self.count_role.values().sum()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn count_role(&self, role: &Role) -> usize {
|
pub fn count_role(&self, role: &Role) -> usize {
|
||||||
self.players.iter().filter(|((r, _), player)| r == role && player.is_some()).count()
|
self.count_role.get(role).unwrap().clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn full_rank(&self) -> f32 {
|
pub fn full_rating(&self, players: &[Player]) -> Rating {
|
||||||
self.players.iter().map(|((role, _), player)| {
|
self.players.iter().map(|((role, _), index)| {
|
||||||
if let Some(player) = player {
|
if let Some(index) = index {
|
||||||
player.ranks.get(role).unwrap().clone()
|
players[*index].ranks.get(role).unwrap().clone()
|
||||||
} else {
|
} else {
|
||||||
0.0
|
Rating::zero()
|
||||||
}
|
}
|
||||||
}).sum::<f32>()
|
}).sum()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn average_rank(&self) -> f32 {
|
pub fn average_rating(&self, players: &[Player]) -> Rating {
|
||||||
if self.players.len() == 0 {
|
if self.players.len() == 0 {
|
||||||
return 0.0;
|
return Rating::zero();
|
||||||
}
|
}
|
||||||
|
|
||||||
self.full_rank() / self.players.len() as f32
|
self.full_rating(players) / self.players.len() as f32
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn full_rank_role(&self, role: &Role) -> f32 {
|
pub fn full_rating_role(&self, role: &Role, players: &[Player]) -> Rating {
|
||||||
self.players.iter().filter(|((r, _), _)| r == role).map(|((_, _), player)| {
|
self.players.iter().filter(|((r, _), _)| r == role).map(|((_, _), index)| {
|
||||||
if let Some(player) = player {
|
if let Some(index) = index {
|
||||||
player.ranks.get(&role).unwrap().clone()
|
players[*index].ranks.get(&role).unwrap().clone()
|
||||||
} else {
|
} else {
|
||||||
0.0
|
Rating::zero()
|
||||||
}
|
}
|
||||||
}).sum::<f32>()
|
}).sum()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn average_rank_role(&self, role: &Role) -> f32 {
|
pub fn average_rating_role(&self, role: &Role, players: &[Player]) -> Rating {
|
||||||
let count = self.players.iter().filter(|((r, _), _)| r == role).count();
|
let count = self.players.iter().filter(|((r, _), _)| r == role).count();
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
return 0.0;
|
return Rating::zero()
|
||||||
}
|
}
|
||||||
|
|
||||||
self.full_rank_role(role) / count as f32
|
self.full_rating_role(role, players) / count as f32
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn has_slot(&self, role: &Role) -> bool {
|
pub fn has_slot(&self, role: &Role) -> bool {
|
||||||
self.players.iter().filter(|((r, _), _)| r == role).any(|(_, player)| player.is_none())
|
self.count_role(role) < *self.max_role.get(role).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_player(&mut self, player: &Player, role: &Role) {
|
pub fn add_player(&mut self, index: usize, role: &Role) {
|
||||||
let slot = self.players.iter().filter(|((r, _), _)| r == role).find(|(_, player)| player.is_none()).unwrap().0.clone();
|
if !self.has_slot(role) {
|
||||||
self.players.insert(slot, Some(player.clone()));
|
panic!("No slot for role {:?}", role);
|
||||||
|
}
|
||||||
|
|
||||||
|
let count = self.count_role(role);
|
||||||
|
self.players.insert((*role, count as i32), Some(index));
|
||||||
|
self.count_role.insert(*role, count + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user