1
0
mirror of https://git.ngram.ca/OpenJam/rc-servers synced 2026-08-23 23:08:52 +00:00

Factor in platoons during team assignment #88

This commit is contained in:
NG (Graham)
2026-03-21 16:06:57 -04:00
parent 18e3f1f6f7
commit 289827ec3b
10 changed files with 392 additions and 112 deletions

View File

@@ -2,25 +2,6 @@ use serde::{Serialize, Deserialize};
use super::account_json::UserData;
pub enum TeamChooser {
/// Alternating between team 0 and team 1
Alternating,
/// All players will be put on the specified team
AllOn(u8),
/// Each player will be put on their own team (like in Pit mode)
OnePer,
}
impl TeamChooser {
pub fn team(&self, index: usize) -> i32 {
match self {
Self::Alternating => (index % 2) as i32,
Self::AllOn(team) => *team as i32,
Self::OnePer => index as i32,
}
}
}
fn fake_impl_to_db(client_emu: &crate::persist::config::ClientEmulator) -> oj_rc_database::schema::multiplayer_game_player::ClientType {
match client_emu {
crate::persist::config::ClientEmulator::Experiment => oj_rc_database::schema::multiplayer_game_player::ClientType::ServerExperimental,
@@ -45,10 +26,10 @@ impl super::LobbyUser for UserData {
})
}
async fn team_chooser(&self, game: &super::GameDescriptor) -> TeamChooser {
async fn team_chooser(&self, game: &super::GameDescriptor) -> super::StandardTeamChooser {
match game.mode {
crate::data::game_mode::GameMode::Pit => TeamChooser::OnePer,
_ => TeamChooser::Alternating,
crate::data::game_mode::GameMode::Pit => super::StandardTeamChooser::OnePer,
_ => super::StandardTeamChooser::alternating(),
}
}
@@ -59,7 +40,7 @@ impl super::LobbyUser for UserData {
factory: &dyn oj_rc_factory::VehicleFactoryAdapter,
cpu_counter: &crate::cubes::CpuListParser,
weapon_lister: &crate::cubes::WeaponListParser,
chooser: &TeamChooser,
chooser: &dyn super::TeamChooser,
missing_players: usize,
) -> Result<super::FakePlayers, polariton_server::operations::SimpleOpError> {
let now = chrono::Utc::now().timestamp();
@@ -76,8 +57,8 @@ impl super::LobbyUser for UserData {
oj_rc_database::schema::multiplayer_game::GameType::Standard
};
let forced_fake_players = self.generate_forced_fake_players_data(guid, &players, factory, cpu_counter, weapon_lister, chooser).await?;
let filler_players = self.generate_filler_players_data(guid, &players, factory, cpu_counter, weapon_lister, chooser, missing_players).await?;
let forced_fake_players = self.generate_forced_fake_players_data(&game.guid, &players, factory, cpu_counter, weapon_lister, chooser).await?;
let filler_players = self.generate_filler_players_data(&game.guid, factory, cpu_counter, weapon_lister, chooser, missing_players, players.len() + forced_fake_players.len()).await?;
let game_dbo = oj_rc_database::schema::multiplayer_game::ActiveModel {
id: oj_rc_database::sea_orm::ActiveValue::NotSet,
@@ -102,6 +83,17 @@ impl super::LobbyUser for UserData {
let players_len = players.len();
let forced_fake_players_len = forced_fake_players.len();
let mut group_map = std::collections::HashMap::new();
let mut group_num = 1;
for player in players.iter() {
if let Some(group) = &player.group {
if !group_map.contains_key(group) {
group_map.insert(group.to_owned(), group_num);
group_num += 1;
}
}
}
let players: Vec<oj_rc_database::schema::multiplayer_game_player::ActiveModel> = players.into_iter()
.enumerate()
.map(|(i, player)| {
@@ -112,7 +104,7 @@ impl super::LobbyUser for UserData {
creation_time: oj_rc_database::sea_orm::ActiveValue::Set(now),
player_id: oj_rc_database::sea_orm::ActiveValue::Set((i as u8) as _),
team: oj_rc_database::sea_orm::ActiveValue::Set(player.team),
group: oj_rc_database::sea_orm::ActiveValue::Set(player.group),
group: oj_rc_database::sea_orm::ActiveValue::Set(player.group.as_ref().and_then(|x| group_map.get(x)).copied()),
is_claimed: oj_rc_database::sea_orm::ActiveValue::Set(false),
public_id: oj_rc_database::sea_orm::ActiveValue::Set(player.public_id),
display_name: oj_rc_database::sea_orm::ActiveValue::Set(player.display_name),
@@ -220,7 +212,7 @@ impl super::LobbyUser for UserData {
creation_time: oj_rc_database::sea_orm::ActiveValue::Set(now),
player_id: oj_rc_database::sea_orm::ActiveValue::Set((i as u8) as _),
team: oj_rc_database::sea_orm::ActiveValue::Set(player.team),
group: oj_rc_database::sea_orm::ActiveValue::Set(player.group),
group: oj_rc_database::sea_orm::ActiveValue::Set(None),
is_claimed: oj_rc_database::sea_orm::ActiveValue::Set(false),
public_id: oj_rc_database::sea_orm::ActiveValue::Set(player.public_id),
display_name: oj_rc_database::sea_orm::ActiveValue::Set(player.display_name),

View File

@@ -18,13 +18,14 @@ pub use intercom::generate_token as generate_intercom_token;
mod multiplayer;
mod lobby;
pub use lobby::TeamChooser;
mod common;
mod chat;
mod social;
mod singleplayer;
mod factory;
mod userless;
mod team;
pub use team::{TeamChooser, StandardTeamChooser};
pub const TOKEN_SECRET_FILENAME: &str = "token_secret.key";

View File

@@ -12,18 +12,24 @@ impl UserData {
#[allow(clippy::too_many_arguments)]
pub(super) async fn generate_fake_players_data(
&self,
_guid: i64,
real_players: &[super::PlayerLobbyDescriptor],
guid_str: &str,
factory: &dyn oj_rc_factory::VehicleFactoryAdapter,
cpu_counter: &crate::cubes::CpuListParser,
weapon_lister: &crate::cubes::WeaponListParser,
chooser: &super::TeamChooser,
chooser: &dyn super::TeamChooser,
fake_players: &[crate::persist::config::FakePlayer],
total_offset: usize,
) -> Result<Vec<(crate::data::player_data::PlayerData, crate::persist::config::ClientEmulator)>, polariton_server::operations::SimpleOpError> {
let mut fakes = Vec::with_capacity(fake_players.len());
let mut fake_i = real_players.len();
for fake in fake_players.iter() {
for (i, fake) in fake_players.iter().enumerate() {
let vehicle = self.resolve_vehicle(&fake.vehicle, factory, weapon_lister, cpu_counter).await?;
let fake_lobby_desc = super::PlayerLobbyDescriptor {
user_id: -1,
team: -1,
public_id: fake.vehicle.username.clone(),
display_name: fake.vehicle.username.clone(),
group: None,
};
let out = (
crate::data::player_data::PlayerData {
name: fake.vehicle.username.clone(),
@@ -33,12 +39,7 @@ impl UserData {
robot_name: vehicle.robot_name,
robot_map: vehicle.robot_map,
group: None,
team: fake.team.map(|t| t as i32)
.unwrap_or_else(|| {
let assigned_team = chooser.team(fake_i);
fake_i += 1;
assigned_team
}),
team: chooser.choose_team(guid_str, total_offset + i, &fake_lobby_desc),
has_premium: true,
robot_uuid: vehicle.robot_uuid,
cpu: vehicle.cpu,
@@ -61,43 +62,44 @@ impl UserData {
#[allow(clippy::too_many_arguments)]
pub(super) async fn generate_forced_fake_players_data(
&self,
guid: i64,
guid_str: &str,
real_players: &[super::PlayerLobbyDescriptor],
factory: &dyn oj_rc_factory::VehicleFactoryAdapter,
cpu_counter: &crate::cubes::CpuListParser,
weapon_lister: &crate::cubes::WeaponListParser,
chooser: &super::TeamChooser,
chooser: &dyn super::TeamChooser,
) -> Result<Vec<(crate::data::player_data::PlayerData, crate::persist::config::ClientEmulator)>, polariton_server::operations::SimpleOpError> {
let offset = real_players.len();
self.generate_fake_players_data(
guid,
real_players,
guid_str,
factory,
cpu_counter,
weapon_lister,
chooser,
&self.fake_players,
offset,
).await
}
#[allow(clippy::too_many_arguments)]
pub(super) async fn generate_filler_players_data(
&self,
guid: i64,
real_players: &[super::PlayerLobbyDescriptor],
guid_str: &str,
factory: &dyn oj_rc_factory::VehicleFactoryAdapter,
cpu_counter: &crate::cubes::CpuListParser,
weapon_lister: &crate::cubes::WeaponListParser,
chooser: &super::TeamChooser,
chooser: &dyn super::TeamChooser,
count: usize,
total_offset: usize,
) -> Result<Vec<(crate::data::player_data::PlayerData, crate::persist::config::ClientEmulator)>, polariton_server::operations::SimpleOpError> {
self.generate_fake_players_data(
guid,
real_players,
guid_str,
factory,
cpu_counter,
weapon_lister,
chooser,
&self.filler_players[0..count],
total_offset
).await
}
}

View File

@@ -0,0 +1,116 @@
pub trait TeamChooser: Send + Sync {
fn choose_team(&self, game: &str, index: usize, player: &super::PlayerLobbyDescriptor) -> i32;
}
pub enum StandardTeamChooser {
/// Alternating between team 0 and team 1
Alternating(TwoTeamPartyAware),
/// All players will be put on the specified team
AllOn(u8),
/// Each player will be put on their own team (like in Pit mode)
OnePer,
//Custom(Box<dyn TeamChooser>),
}
impl StandardTeamChooser {
pub fn alternating() -> Self {
Self::Alternating(TwoTeamPartyAware::new())
}
}
impl TeamChooser for StandardTeamChooser {
fn choose_team(&self, game: &str, index: usize, player: &super::PlayerLobbyDescriptor) -> i32 {
match self {
Self::Alternating(t) => t.choose_team(game, index, player),
Self::AllOn(team) => *team as i32,
Self::OnePer => index as i32,
//Self::Custom(t) => t.choose_team(game, index, player),
}
}
}
pub struct TwoTeamPartyAware {
tracker: std::sync::Mutex<std::collections::HashMap<String, BalanceInfo>>,
}
struct BalanceInfo {
overall: i64,
/// platoon ID -> team
platoons: std::collections::HashMap<String, i8>,
created_at: chrono::DateTime<chrono::Utc>,
}
impl TwoTeamPartyAware {
pub fn new() -> Self {
Self {
tracker: std::sync::Mutex::new(std::collections::HashMap::new()),
}
}
/// Manually remove stale team infos
fn do_cleanup(tracker: &mut std::collections::HashMap<String, BalanceInfo>) {
let max_ttl = chrono::TimeDelta::from_std(std::time::Duration::from_secs(10)).unwrap();
let now = chrono::Utc::now();
tracker.retain(|_, bal_info| (now - bal_info.created_at) < max_ttl);
}
}
impl TeamChooser for TwoTeamPartyAware {
fn choose_team(&self, game: &str, _index: usize, player: &super::PlayerLobbyDescriptor) -> i32 {
let mut lock = self.tracker.lock().unwrap();
// the algorithm is basically:
// if player is in platoon AND platoon is known platoon:
// use platoon's team assignment
// else if overall > 0:
// choose team 1 for player, remember team assignment for all future platoon members
// overall -= 1
// else:
// choose team 0 for player, remember team assignment for all future platoon members
// overall += 1
//
// this roughly balances player count between the two teams
// while ensuring players on the same platoon are put on the same team
if let Some(bal_info) = lock.get_mut(game) {
if let Some(platoon_id) = &player.group {
if let Some(&platoon_team) = bal_info.platoons.get(platoon_id) {
if platoon_team == 0 {
bal_info.overall += 1;
} else {
bal_info.overall -= 1;
}
return platoon_team as i32;
}
}
let chosen_team = if bal_info.overall > 0 {
bal_info.overall -= 1;
1
} else {
bal_info.overall += 1;
0
};
if let Some(platoon_id) = &player.group {
bal_info.platoons.insert(platoon_id.to_owned(), chosen_team);
}
return chosen_team as i32;
} else {
Self::do_cleanup(&mut lock);
let init_info = if let Some(platoon) = &player.group {
let mut platoon_teams = std::collections::HashMap::new();
platoon_teams.insert(platoon.to_owned(), 0);
BalanceInfo {
overall: 1,
platoons: platoon_teams,
created_at: chrono::Utc::now(),
}
} else {
BalanceInfo {
overall: 1,
platoons: std::collections::HashMap::new(),
created_at: chrono::Utc::now(),
}
};
lock.insert(game.to_owned(), init_info);
0
}
}
}

View File

@@ -279,9 +279,9 @@ pub enum UserRole {
pub trait LobbyUser {
fn user_id(&self) -> i32;
async fn player_data(&self, cpu_counter: &crate::cubes::CpuListParser) -> Result<crate::data::player_data::PlayerData, polariton_server::operations::SimpleOpError>;
async fn team_chooser(&self, game: &GameDescriptor) -> super::TeamChooser;
async fn team_chooser(&self, game: &GameDescriptor) -> super::StandardTeamChooser;
#[allow(clippy::too_many_arguments)]
async fn start_game(&self, game: GameDescriptor, players: Vec<PlayerLobbyDescriptor>, factory: &dyn oj_rc_factory::VehicleFactoryAdapter, cpu_counter: &crate::cubes::CpuListParser, weapon_lister: &crate::cubes::WeaponListParser, team_chooser: &super::TeamChooser, missing_players: usize) -> Result<FakePlayers, polariton_server::operations::SimpleOpError>;
async fn start_game(&self, game: GameDescriptor, players: Vec<PlayerLobbyDescriptor>, factory: &dyn oj_rc_factory::VehicleFactoryAdapter, cpu_counter: &crate::cubes::CpuListParser, weapon_lister: &crate::cubes::WeaponListParser, team_chooser: &dyn super::TeamChooser, missing_players: usize) -> Result<FakePlayers, polariton_server::operations::SimpleOpError>;
#[allow(clippy::too_many_arguments)]
async fn start_custom_game(&self, game: GameDescriptor, players: Vec<PlayerLobbyDescriptor>) -> Result<(), polariton_server::operations::SimpleOpError>;
}
@@ -337,7 +337,7 @@ pub struct PlayerLobbyDescriptor {
pub team: i32,
pub public_id: String,
pub display_name: String,
pub group: Option<i32>,
pub group: Option<String>,
}
#[derive(Clone)]