use std::{collections::HashMap, fmt::Display, str::FromStr, sync::Arc, time::Duration};
use tracing::{error, info, instrument, trace, warn};
use crate::agent::Agent;
use crate::client_handler::ClientHandler;
use crate::configuration::Configuration;
use crate::constraints::Constraints;
use crate::game_interface::Game;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MatchSettings {
pub ordered_player: Vec<Arc<Agent>>,
pub resources: Constraints,
}
impl Display for MatchSettings {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = self
.ordered_player
.iter()
.map(|a| &a.name[..])
.collect::<Vec<_>>()
.join(" VS ");
write!(f, "[{s}]")
}
}
pub type MatchResult<S> = Vec<(Arc<Agent>, S)>;
#[derive(Debug, Clone)]
pub struct RunnerResult<S>
where
S: PartialOrd,
{
pub results: MatchResult<S>,
pub resources_freed: Constraints,
pub errors: String,
}
#[instrument(skip_all,fields(%settings,cpus=?settings.resources.cpus))]
pub fn run_match<G: Game>(
settings: MatchSettings,
config: Configuration,
mut game: G,
) -> RunnerResult<G::Score> {
trace!("game started");
let MatchSettings {
ordered_player,
resources,
} = settings;
let mut errors_string = String::new();
let max_turn_duration = resources.action_timeout;
const MAX_BUFFER_SIZE: usize = 4096;
let mut clients: HashMap<usize, ClientHandler> = HashMap::new();
{
let num_cpus = resources.cpus_per_agent;
let ram = resources.agent_ram;
let mut avail_res = resources.clone();
for (i, agent) in ordered_player.iter().enumerate() {
match ClientHandler::init(
agent.clone(),
&avail_res.take(num_cpus, ram),
config.allow_uncontained,
config.debug_agent_stderr,
) {
Ok(client) => {
clients.insert(i, client);
}
Err(e) => {
errors_string += &format!("{} startup failed ({e}), ", agent.name);
warn!("Failed to start client for agent {}: {e}", agent.name);
}
}
}
}
let mut time_budgets = vec![resources.time_budget; ordered_player.len()];
let mut turn = 0;
while !game.is_finished() && !clients.is_empty() {
turn += 1;
let current = game.get_current_player_number();
let state_str = game.get_state().to_string();
let action = if let Some(client) = clients.get_mut(¤t) {
let mut buf = [0; MAX_BUFFER_SIZE];
let time_budget = time_budgets[current];
let mut max_duration = Duration::min(max_turn_duration, time_budget);
if !max_duration.is_zero() {
max_duration += resources.time_margin;
}
let timer_start = std::time::Instant::now();
let response = client.send_and_recv(state_str.as_bytes(), &mut buf, max_duration);
let elapsed = timer_start.elapsed();
time_budgets[current] = time_budgets[current]
.checked_sub(elapsed)
.unwrap_or(Duration::ZERO);
match response {
Ok(received) => {
let response = std::str::from_utf8(&buf[..received]);
match response {
Ok(text) => match G::Action::from_str(text.trim()) {
Ok(action) => Some(action),
Err(_) => {
info!(
"Agent {} sent invalid action: '{text}' {}",
ordered_player[current].name,
if received == 0 {
"(probably crashed)"
} else {
""
}
);
if received == 0 {
errors_string += &format!(
"{} empty string received (player probably crashed), ",
ordered_player[current].name
);
} else {
errors_string += &format!(
"{} not an action: '{text}', ",
ordered_player[current].name
);
}
clients.remove(¤t);
None
}
},
Err(_) => {
error!(
"Agent {} sent non-UTF8 response",
ordered_player[current].name
);
errors_string +=
&format!("{} non-utf8 response, ", ordered_player[current].name);
clients.remove(¤t);
None
}
}
}
Err(e) => {
if max_duration >= resources.action_timeout
|| max_duration >= (resources.time_budget / 10)
{
errors_string += &format!(
"{}: {e} response timeout ({}ms) (turn {turn}), ",
ordered_player[current].name,
max_duration.as_millis()
);
warn!(
"Agent {} did not respond in time (min(action_timeout, time_budget) + margin = {}ms): state={state_str}, error={e}",
ordered_player[current].name,
max_duration.as_millis()
);
} else {
info!(
"Agent {} did not have enough time (small timeout: {}ms)",
ordered_player[current].name,
max_duration.as_millis()
);
}
clients.remove(¤t);
None
}
}
} else {
None
};
if let Err(e) = game.apply_action(&action) {
if action.is_some() {
warn!(
"player {current}'s action ({}) rejected by Game (State={state_str})",
action.as_ref().unwrap().to_string(),
);
errors_string += &format!(
"{}'s action '{}' was rejected: {e}, ",
ordered_player[current].name,
action.unwrap().to_string()
);
clients.remove(¤t);
}
}
}
drop(clients);
let mut result_str = vec![];
let mut results = vec![];
for (i, agent) in ordered_player.iter().enumerate() {
let score = game.get_player_score(i as u32);
result_str.push(score.to_string());
results.push((agent.clone(), score));
}
let result_str = result_str.join("-");
trace!("match end: {result_str}, {errors_string}");
RunnerResult {
results,
resources_freed: resources,
errors: errors_string,
}
}