From 1ece1cd042ec22363757dd39d6f4f16c18e64e99 Mon Sep 17 00:00:00 2001 From: Lionarius Date: Wed, 5 Apr 2023 11:25:49 +0300 Subject: [PATCH] what the hell, now i can mix stuff, and it seems to work --- src/bot/commands/lobby.rs | 29 +++- src/bot/commands/preference.rs | 9 +- src/bot/commands/rank.rs | 2 +- src/database/mod.rs | 18 +-- src/mixer/mixer.rs | 272 +++++++++------------------------ src/mixer/mod.rs | 2 + src/mixer/player.rs | 55 +++++++ src/mixer/role.rs | 96 +++++------- src/mixer/team.rs | 79 ++++++++++ 9 files changed, 282 insertions(+), 280 deletions(-) create mode 100644 src/mixer/player.rs create mode 100644 src/mixer/team.rs diff --git a/src/bot/commands/lobby.rs b/src/bot/commands/lobby.rs index b0c710f..13eb26e 100644 --- a/src/bot/commands/lobby.rs +++ b/src/bot/commands/lobby.rs @@ -12,6 +12,10 @@ use serenity::model::id::{ChannelId, RoleId, UserId}; use serenity::model::Permissions; use crate::bot::commands::MixerCommand; use crate::database::DatabaseContainer; +use crate::mixer::mixer; +use crate::mixer::player::Player; +use crate::mixer::role::Role; + #[derive(Clone)] pub struct LobbyCommand; @@ -146,11 +150,28 @@ impl LobbyCommand { member.move_to_voice_channel(ctx.http(), main_channel.id).await?; } - let members = main_channel.members(ctx.cache().unwrap()).await?; - let users = members.iter().map(|m| m.user.id).collect::>(); + // TODO: uncomment this + // let members = main_channel.members(ctx.cache().unwrap()).await?; + // let users = members.iter().map(|m| m.user.id).collect::>(); + let users = (0..10).map(|id| UserId::from(id)).collect::>(); let players = db.get_players(users).await; - - println!("{:?}", players); + let players = players.into_iter().map(|p| Player::new(p)).collect::>(); + 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()); + println!("Average rank tank {}", team1.average_rank_role(&Role::Tank)); + println!("Average rank dps {}", team1.average_rank_role(&Role::Dps)); + println!("Average rank support {}", team1.average_rank_role(&Role::Support)); + println!("Average rank {}", team2.average_rank()); + println!("Average rank tank {}", team2.average_rank_role(&Role::Tank)); + println!("Average rank dps {}", team2.average_rank_role(&Role::Dps)); + 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::>()); + println!("Team 2: {:?}", team2.players.iter().map(|p| (p.0.clone().0, p.1.clone().unwrap().name.clone())).collect::>()); + } + else { + println!("Fair lobby could not be mixed") + } interaction.create_interaction_response(ctx.http(), |response| { response.kind(InteractionResponseType::ChannelMessageWithSource) diff --git a/src/bot/commands/preference.rs b/src/bot/commands/preference.rs index a238db6..894d003 100644 --- a/src/bot/commands/preference.rs +++ b/src/bot/commands/preference.rs @@ -6,7 +6,6 @@ use serenity::model::application::interaction::{ InteractionResponseType }; use serenity::async_trait; -use serenity::http::CacheHttp; use serenity::model::Permissions; use serenity::model::prelude::command::CommandOptionType; use serenity::model::prelude::interaction::application_command::CommandDataOptionValue::User; @@ -99,12 +98,12 @@ impl MixerCommand for PreferenceCommand { match interaction.data.options.get(0).unwrap().options.get(0).unwrap().name.as_str() { "flex" => { - db.update_player_preference(user.id, true, Role::None, Role::None, Role::None).await; + db.update_player_preference(user.id, true, None, None, None).await; }, "complex" => { - let role1 = Role::from_str(interaction.data.options.get(0).unwrap().options.get(0).unwrap().options.get(1).unwrap().value.as_ref().unwrap().as_str().unwrap()).unwrap(); - let role2 = Role::from_str(interaction.data.options.get(0).unwrap().options.get(0).unwrap().options.get(2).unwrap().value.as_ref().unwrap().as_str().unwrap()).unwrap(); - let role3 = Role::from_str(interaction.data.options.get(0).unwrap().options.get(0).unwrap().options.get(3).unwrap().value.as_ref().unwrap().as_str().unwrap()).unwrap(); + let role1 = Role::from_str(interaction.data.options.get(0).unwrap().options.get(0).unwrap().options.get(1).unwrap().value.as_ref().unwrap().as_str().unwrap()).ok(); + let role2 = Role::from_str(interaction.data.options.get(0).unwrap().options.get(0).unwrap().options.get(2).unwrap().value.as_ref().unwrap().as_str().unwrap()).ok(); + let role3 = Role::from_str(interaction.data.options.get(0).unwrap().options.get(0).unwrap().options.get(3).unwrap().value.as_ref().unwrap().as_str().unwrap()).ok(); db.update_player_preference(user.id, false, role1, role2, role3).await; }, diff --git a/src/bot/commands/rank.rs b/src/bot/commands/rank.rs index 7b5f664..35564f8 100644 --- a/src/bot/commands/rank.rs +++ b/src/bot/commands/rank.rs @@ -67,7 +67,7 @@ impl MixerCommand for RankCommand { } }; - let role = Role::from_str(interaction.data.options.get(0).unwrap().options.get(1).unwrap().value.as_ref().unwrap().as_str().unwrap()).unwrap(); + let role = Role::from_str(interaction.data.options.get(0).unwrap().options.get(1).unwrap().value.as_ref().unwrap().as_str().unwrap()).ok(); let rank = interaction.data.options.get(0).unwrap().options.get(2).unwrap().value.as_ref().unwrap().as_u64().unwrap(); if rank < 1 || rank > 5000 { diff --git a/src/database/mod.rs b/src/database/mod.rs index 8cad696..c94cdae 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -54,7 +54,7 @@ impl MixerDatabase { .expect("Could not insert player into database"); } - pub async fn update_player_rank(&self, id: UserId, role: Role, rank: f32) { + pub async fn update_player_rank(&self, id: UserId, role: Option, rank: f32) { if !self.has_player(id).await { self.insert_player(id).await; } @@ -62,10 +62,10 @@ impl MixerDatabase { let mut player = self.get_player(id).await.unwrap().into_active_model(); match role { - Role::Tank => player.tank = Set(rank), - Role::Dps => player.dps = Set(rank), - Role::Support => player.support = Set(rank), - Role::None => return + Some(Role::Tank) => player.tank = Set(rank), + Some(Role::Dps) => player.dps = Set(rank), + Some(Role::Support) => player.support = Set(rank), + None => return } player.update(&self.connection) @@ -73,7 +73,7 @@ impl MixerDatabase { .expect("Could not update player rank in database"); } - pub async fn update_player_preference(&self, id: UserId, flex: bool, primary: Role, secondary: Role, tertiary: Role) { + pub async fn update_player_preference(&self, id: UserId, flex: bool, primary: Option, secondary: Option, tertiary: Option) { if !self.has_player(id).await { self.insert_player(id).await; } @@ -81,9 +81,9 @@ impl MixerDatabase { let mut player = self.get_player(id).await.unwrap().into_active_model(); player.flex = Set(flex); - player.primary_role = Set(primary.into()); - player.secondary_role = Set(secondary.into()); - player.tertiary_role = Set(tertiary.into()); + player.primary_role = Set(Role::option_to_i32(primary)); + player.secondary_role = Set(Role::option_to_i32(secondary)); + player.tertiary_role = Set(Role::option_to_i32(tertiary)); player.update(&self.connection) .await diff --git a/src/mixer/mixer.rs b/src/mixer/mixer.rs index d82cf9d..0125a20 100644 --- a/src/mixer/mixer.rs +++ b/src/mixer/mixer.rs @@ -1,206 +1,76 @@ -use std::collections::HashMap; use crate::mixer::role::Role; -use crate::database::models::player::{Model as Player, Model}; -use crate::mixer::role; +use crate::mixer::player::Player; +use crate::mixer::team::Team; -pub struct Mixer { - players: Vec, + +pub fn mix_players(players: &Vec, slots: Vec) -> Option<(Team, Team)> { + let mut players = players.clone(); + let mut team1 = Team::new(slots.clone()); + let mut team2 = Team::new(slots.clone()); + + for _ in 0..(slots.len()*2) { + let mut priorities = calculate_priorities(&players, &team1, &team2); + priorities.sort_by(|(_, _, p1), (_, _, p2)| p2.partial_cmp(p1).unwrap()); + + for (player, role, _) in priorities { + if team1.full_rank() > team2.full_rank() { + if team2.has_slot(&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)) } -#[derive(Debug, Clone)] -pub struct MixedPlayer { - pub role: Role, - pub player: Player, + +fn calculate_priorities(players: &Vec, team1: &Team, team2: &Team) -> Vec<(Player, Role, f32)> { + let mut priorities = Vec::new(); + + for player in players { + for (role, priority) in player.base_priority() { + priorities.push((player.clone(), role, priority)); + } + } + + for item in &mut priorities { + let (player, role, _) = item; + let empty_teams = team1.count_role(role) == 0 && team2.count_role(role) == 0; + + let role_diff_rank = 1.0 + { + if empty_teams { + 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); + let role_diff_avg_rank = 1.0 + { + if empty_teams { + let filtered_players = players.iter().filter(|player| player.priority_roles.contains(&Some(role.clone())) || player.flex).collect::>(); + (player.ranks[role] - filtered_players.iter().map(|player| player.ranks[role]).sum::() / filtered_players.len() as f32).abs() + } else { + (player.ranks[role] - sum_average_rank).abs() + } + } as f32; + + let complex_coefficient = role_diff_rank * role_diff_avg_rank; + item.2 *= player.ranks[role] / complex_coefficient; + } + + priorities } - -impl Mixer { - pub fn new(players: Vec) -> Self { - Self { - players - } - } - - pub fn select_teams(self) -> Option<(Vec, Vec)> { - let mut players = self.players.clone(); - - // TODO! pass in the original priorities - // let original_priorities = players.iter().cloned().map(|player| (player.id, 1)).collect::>(); - - let mut team1 = Vec::new(); - let mut team2 = Vec::new(); - - for i in 0..10 { - let priorities = self.get_players_priority(&players, &team1, &team2); - - if let Some(player) = self.get_highest_priority_player(&players, &priorities) { - if i % 2 == 0 { - team1.push(player.clone()); - } - else { - team2.push(player.clone()); - } - - players.retain(|p| p.id != player.player.id); - } - else { - return None; - } - } - - Some((team1, team2)) - } - - fn get_players_priority(&self, players: &Vec, team1: &Vec, team2: &Vec) -> HashMap<(Role, i32), f32> { - let mut priorities = HashMap::new(); - - if Self::calculate_priorities(players, team1, team2, Role::Tank, &mut priorities) { - return priorities; - } - - if Self::calculate_priorities(players, team1, team2, Role::Dps, &mut priorities) { - return priorities; - } - - if Self::calculate_priorities(players, team1, team2, Role::Support, &mut priorities) { - return priorities; - } - - priorities - } - - fn average_rank(team1: &Vec, team2: &Vec, role: Role) -> f32 { - let selected_players = team1.iter().filter(|player| player.role == role) - .chain(team2.iter().filter(|player| player.role == role)).clone().collect::>(); - - selected_players.iter().map(|player| { - match player.role { - Role::Tank => player.player.tank, - Role::Dps => player.player.dps, - Role::Support => player.player.support, - _ => 0.0 - } - }).sum::() / selected_players.len() as f32 - } - - fn calculate_priorities(players: &Vec, team1: &Vec, team2: &Vec, expected: Role, priorities: &mut HashMap<(Role, i32), f32>) -> bool { - let group_coefficients = vec![10.0, 50.0, 100.0, 150.0, 125.0]; - - let team1_roles = team1.iter().map(|player| player.role).collect::>(); - let team2_roles = team2.iter().map(|player| player.role).collect::>(); - - let team1_role_count = team1_roles.iter().filter(|role| **role == expected).count(); - let team2_role_count = team2_roles.iter().filter(|role| **role == expected).count(); - let prioritize_role = team1_role_count == 0 || team2_role_count == 0; - - if !prioritize_role { - return false; - } - - - let otp = players.iter().filter(|player| - player.primary_role == expected && player.secondary_role == Role::None && player.tertiary_role == Role::None - ).cloned().collect::>(); - - let primary = players.iter().filter(|player| - player.primary_role == expected && (player.secondary_role != Role::None || player.tertiary_role == Role::None) - ).cloned().collect::>(); - - let secondary = players.iter().filter(|player| - player.secondary_role == expected - ).cloned().collect::>(); - - let flex = players.iter().filter(|player| - player.flex - ).cloned().collect::>(); - - let tertiary = players.iter().filter(|player| - player.tertiary_role == expected - ).cloned().collect::>(); - - - let average_tank_skill = Self::average_rank(team1, team2, expected); - - for player in otp { - let rank = match expected { - Role::Tank => player.tank, - Role::Dps => player.dps, - Role::Support => player.support, - _ => 0.0 - }; - let skill_difference = (rank - average_tank_skill).abs(); - - priorities.insert((expected, player.id), rank / group_coefficients[0] / (skill_difference + 1.0)); - } - - for player in primary { - let rank = match expected { - Role::Tank => player.tank, - Role::Dps => player.dps, - Role::Support => player.support, - _ => 0.0 - }; - let skill_difference = (rank - average_tank_skill).abs(); - - priorities.insert((expected, player.id), rank / group_coefficients[1] / (skill_difference + 1.0)); - } - - for player in secondary { - let rank = match expected { - Role::Tank => player.tank, - Role::Dps => player.dps, - Role::Support => player.support, - _ => 0.0 - }; - let skill_difference = (rank - average_tank_skill).abs(); - - priorities.insert((expected, player.id), rank / group_coefficients[2] / (skill_difference + 1.0)); - } - - for player in tertiary { - let rank = match expected { - Role::Tank => player.tank, - Role::Dps => player.dps, - Role::Support => player.support, - _ => 0.0 - }; - let skill_difference = (rank - average_tank_skill).abs(); - - priorities.insert((expected, player.id), rank / group_coefficients[3] / (skill_difference + 1.0)); - } - - for player in flex { - let rank = match expected { - Role::Tank => player.tank, - Role::Dps => player.dps, - Role::Support => player.support, - _ => 0.0 - }; - let skill_difference = (rank - average_tank_skill).abs(); - - priorities.insert((expected, player.id), rank / group_coefficients[4] / (skill_difference + 1.0)); - } - - true - } - - fn get_highest_priority_player(&self, players: &Vec, priorities: &HashMap<(Role, i32), f32>) -> Option { - let mut highest_priority = 0.0; - let mut highest_priority_player = None; - - for role in vec![Role::Tank, Role::Dps, Role::Support] { - for player in players { - if let Some(priority) = priorities.get(&(role, player.id)) { - if *priority > highest_priority { - highest_priority = *priority; - highest_priority_player = Some(MixedPlayer { - role, - player: player.clone(), - }); - } - } - } - } - - highest_priority_player - } -} \ No newline at end of file diff --git a/src/mixer/mod.rs b/src/mixer/mod.rs index 8914eaf..09e5550 100644 --- a/src/mixer/mod.rs +++ b/src/mixer/mod.rs @@ -1,2 +1,4 @@ pub mod mixer; pub mod role; +pub mod player; +pub mod team; diff --git a/src/mixer/player.rs b/src/mixer/player.rs new file mode 100644 index 0000000..93ce83e --- /dev/null +++ b/src/mixer/player.rs @@ -0,0 +1,55 @@ +use std::collections::HashMap; +use crate::database::models::player::Model; +use crate::mixer::role::Role; + +#[derive(Debug, Clone)] +pub struct Player { + pub(crate) id: i32, + pub(crate) name: String, + + pub(crate) ranks: HashMap, + pub(crate) flex: bool, + pub(crate) priority_roles: Vec>, +} + + +impl Player { + pub fn new(model: Model) -> Self { + Self { + id: model.id, + name: model.bn_name, + ranks: vec![(Role::Tank, model.tank), (Role::Dps, model.dps), (Role::Support, model.support)].into_iter().collect(), + flex: model.flex, + priority_roles: vec![model.primary_role, model.secondary_role, model.tertiary_role].into_iter().map(|role| { + match role { + -1 => None, + _ => Some(Role::from(role)) + } + }).collect() + } + } + + pub fn base_priority(&self) -> HashMap { + let mut priorities = HashMap::new(); + + if self.flex { + for role in Role::iter() { + priorities.insert(role, (self.priority_roles.len() / 2) as f32); + } + + 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; + let priority_points = 100.0; + + for (i, role) in self.priority_roles.iter().enumerate() { + if let Some(role) = role { + priorities.insert(role.clone(), priority_points * (count - i as f32)*(count - i as f32) / denominator as f32); + } + } + + priorities + } +} \ No newline at end of file diff --git a/src/mixer/role.rs b/src/mixer/role.rs index c5c4792..42f2df4 100644 --- a/src/mixer/role.rs +++ b/src/mixer/role.rs @@ -1,67 +1,11 @@ -use std::hash::{Hash, Hasher}; +use std::hash::Hash; use std::str::FromStr; #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] pub enum Role { Tank, Dps, - Support, - None -} - -impl PartialEq for Role { - fn eq(&self, other: &i32) -> bool { - match self { - Role::Tank => *other == 0, - Role::Dps => *other == 1, - Role::Support => *other == 2, - Role::None => *other == -1 - } - } -} - -impl PartialEq for i32 { - fn eq(&self, other: &Role) -> bool { - match other { - Role::Tank => *self == 0, - Role::Dps => *self == 1, - Role::Support => *self == 2, - Role::None => *self == -1 - } - } -} - -impl From<&Role> for i32 { - fn from(role: &Role) -> Self { - match role { - Role::Tank => 0, - Role::Dps => 1, - Role::Support => 2, - Role::None => -1 - } - } -} - -impl From for Role { - fn from(i: i32) -> Self { - match i { - 0 => Role::Tank, - 1 => Role::Dps, - 2 => Role::Support, - _ => Role::None - } - } -} - -impl Into for Role { - fn into(self) -> i32 { - match self { - Role::Tank => 0, - Role::Dps => 1, - Role::Support => 2, - Role::None => -1 - } - } + Support } impl FromStr for Role { @@ -72,9 +16,41 @@ impl FromStr for Role { "tank" => Ok(Role::Tank), "dps" => Ok(Role::Dps), "support" => Ok(Role::Support), - "none" => Ok(Role::None), - _ => Ok(Role::None) + _ => Err(()) } } } +impl From for Role { + fn from(i: i32) -> Self { + match i { + 0 => Role::Tank, + 1 => Role::Dps, + 2 => Role::Support, + _ => panic!("Invalid role number") + } + } +} + +impl PartialEq for Role { + fn eq(&self, other: &i32) -> bool { + match self { + Role::Tank => *other == 0, + Role::Dps => *other == 1, + Role::Support => *other == 2 + } + } +} + +impl Role { + pub fn iter() -> impl Iterator { + vec![Role::Tank, Role::Dps, Role::Support].into_iter() + } + + pub fn option_to_i32(role: Option) -> i32 { + match role { + Some(role) => role as i32, + None => -1 + } + } +} \ No newline at end of file diff --git a/src/mixer/team.rs b/src/mixer/team.rs new file mode 100644 index 0000000..50c551c --- /dev/null +++ b/src/mixer/team.rs @@ -0,0 +1,79 @@ +use std::collections::HashMap; +use crate::mixer::player::Player; +use crate::mixer::role::Role; + +#[derive(Debug, Clone)] +pub struct Team { + pub players: HashMap<(Role, i32), Option> +} + + +impl Team { + pub fn new(slots: Vec) -> Self { + Self { + players: { + let mut players = HashMap::new(); + for role in Role::iter() { + for i in 0..slots.iter().filter(|slot| **slot == role).count() { + players.insert((role, i as i32), None); + } + } + players + } + } + } + + pub fn count(&self) -> usize { + self.players.iter().filter(|(_, player)| player.is_some()).count() + } + + pub fn count_role(&self, role: &Role) -> usize { + self.players.iter().filter(|((r, _), player)| r == role && player.is_some()).count() + } + + pub fn full_rank(&self) -> f32 { + self.players.iter().map(|((role, _), player)| { + if let Some(player) = player { + player.ranks.get(role).unwrap().clone() + } else { + 0.0 + } + }).sum::() + } + + pub fn average_rank(&self) -> f32 { + if self.players.len() == 0 { + return 0.0; + } + + self.full_rank() / self.players.len() as f32 + } + + pub fn full_rank_role(&self, role: &Role) -> f32 { + self.players.iter().filter(|((r, _), _)| r == role).map(|((_, _), player)| { + if let Some(player) = player { + player.ranks.get(&role).unwrap().clone() + } else { + 0.0 + } + }).sum::() + } + + pub fn average_rank_role(&self, role: &Role) -> f32 { + let count = self.players.iter().filter(|((r, _), _)| r == role).count(); + if count == 0 { + return 0.0; + } + + self.full_rank_role(role) / count as f32 + } + + pub fn has_slot(&self, role: &Role) -> bool { + self.players.iter().filter(|((r, _), _)| r == role).any(|(_, player)| player.is_none()) + } + + pub fn add_player(&mut self, player: &Player, role: &Role) { + let slot = self.players.iter().filter(|((r, _), _)| r == role).find(|(_, player)| player.is_none()).unwrap().0.clone(); + self.players.insert(slot, Some(player.clone())); + } +} \ No newline at end of file