Merge branch 'feature/leaderboard'
This commit is contained in:
commit
80c60ac69c
18 changed files with 429 additions and 112 deletions
|
@ -38,7 +38,6 @@ pub struct PlayerInfo {
|
|||
}
|
||||
|
||||
pub struct MatchPlayer {
|
||||
pub name: String,
|
||||
pub bot_spec: Box<dyn BotSpec>,
|
||||
}
|
||||
|
||||
|
@ -52,7 +51,11 @@ pub trait BotSpec: Send + Sync {
|
|||
) -> Box<dyn PlayerHandle>;
|
||||
}
|
||||
|
||||
pub async fn run_match(config: MatchConfig) {
|
||||
pub struct MatchOutcome {
|
||||
pub winner: Option<usize>,
|
||||
}
|
||||
|
||||
pub async fn run_match(config: MatchConfig) -> MatchOutcome {
|
||||
let pw_config = PwConfig {
|
||||
map_file: config.map_path,
|
||||
max_turns: 100,
|
||||
|
@ -103,8 +106,18 @@ pub async fn run_match(config: MatchConfig) {
|
|||
// )
|
||||
// .unwrap();
|
||||
|
||||
let match_state = pw_match::PwMatch::create(match_ctx, pw_config);
|
||||
let mut match_state = pw_match::PwMatch::create(match_ctx, pw_config);
|
||||
match_state.run().await;
|
||||
|
||||
let final_state = match_state.match_state.state();
|
||||
let survivors = final_state.living_players();
|
||||
let winner = if survivors.len() == 1 {
|
||||
Some(survivors[0])
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
MatchOutcome { winner }
|
||||
}
|
||||
|
||||
// writing this as a closure causes lifetime inference errors
|
||||
|
|
|
@ -24,8 +24,8 @@ pub struct MatchConfig {
|
|||
}
|
||||
|
||||
pub struct PwMatch {
|
||||
match_ctx: MatchCtx,
|
||||
match_state: PlanetWars,
|
||||
pub match_ctx: MatchCtx,
|
||||
pub match_state: PlanetWars,
|
||||
}
|
||||
|
||||
impl PwMatch {
|
||||
|
@ -39,7 +39,7 @@ impl PwMatch {
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn run(mut self) {
|
||||
pub async fn run(&mut self) {
|
||||
while !self.match_state.is_finished() {
|
||||
let player_messages = self.prompt_players().await;
|
||||
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
-- This file should undo anything in `up.sql`
|
||||
drop table ratings;
|
|
@ -0,0 +1,7 @@
|
|||
-- Your SQL goes here
|
||||
-- this table could later be expanded to include more information,
|
||||
-- such as rating state (eg. number of matches played) or scope (eg. map)
|
||||
create table ratings (
|
||||
bot_id integer PRIMARY KEY REFERENCES bots(id),
|
||||
rating float NOT NULL
|
||||
)
|
|
@ -11,7 +11,7 @@ pub struct NewBot<'a> {
|
|||
pub name: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Queryable, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[derive(Queryable, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Bot {
|
||||
pub id: i32,
|
||||
pub owner_id: Option<i32>,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
pub mod bots;
|
||||
pub mod matches;
|
||||
pub mod ratings;
|
||||
pub mod sessions;
|
||||
pub mod users;
|
||||
|
|
54
planetwars-server/src/db/ratings.rs
Normal file
54
planetwars-server/src/db/ratings.rs
Normal file
|
@ -0,0 +1,54 @@
|
|||
use diesel::{prelude::*, PgConnection, QueryResult};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::db::bots::Bot;
|
||||
use crate::schema::{bots, ratings, users};
|
||||
|
||||
#[derive(Queryable, Debug, Insertable, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Rating {
|
||||
pub bot_id: i32,
|
||||
pub rating: f64,
|
||||
}
|
||||
|
||||
pub fn get_rating(bot_id: i32, db_conn: &PgConnection) -> QueryResult<Option<f64>> {
|
||||
ratings::table
|
||||
.filter(ratings::bot_id.eq(bot_id))
|
||||
.select(ratings::rating)
|
||||
.first(db_conn)
|
||||
.optional()
|
||||
}
|
||||
|
||||
pub fn set_rating(bot_id: i32, rating: f64, db_conn: &PgConnection) -> QueryResult<usize> {
|
||||
diesel::insert_into(ratings::table)
|
||||
.values(Rating { bot_id, rating })
|
||||
.on_conflict(ratings::bot_id)
|
||||
.do_update()
|
||||
.set(ratings::rating.eq(rating))
|
||||
.execute(db_conn)
|
||||
}
|
||||
|
||||
#[derive(Queryable, Serialize, Deserialize)]
|
||||
pub struct Author {
|
||||
id: i32,
|
||||
username: String,
|
||||
}
|
||||
|
||||
#[derive(Queryable, Serialize, Deserialize)]
|
||||
pub struct RankedBot {
|
||||
pub bot: Bot,
|
||||
pub author: Option<Author>,
|
||||
pub rating: f64,
|
||||
}
|
||||
|
||||
pub fn get_bot_ranking(db_conn: &PgConnection) -> QueryResult<Vec<RankedBot>> {
|
||||
bots::table
|
||||
.left_join(users::table)
|
||||
.inner_join(ratings::table)
|
||||
.select((
|
||||
bots::all_columns,
|
||||
(users::id, users::username).nullable(),
|
||||
ratings::rating,
|
||||
))
|
||||
.order_by(ratings::rating.desc())
|
||||
.get_results(db_conn)
|
||||
}
|
|
@ -8,11 +8,14 @@ pub mod routes;
|
|||
pub mod schema;
|
||||
pub mod util;
|
||||
|
||||
use std::net::SocketAddr;
|
||||
use std::ops::Deref;
|
||||
|
||||
use bb8::{Pool, PooledConnection};
|
||||
use bb8_diesel::{self, DieselConnectionManager};
|
||||
use config::ConfigError;
|
||||
use diesel::{Connection, PgConnection};
|
||||
use modules::ranking::run_ranker;
|
||||
use serde::Deserialize;
|
||||
|
||||
use axum::{
|
||||
|
@ -55,16 +58,16 @@ pub async fn seed_simplebot(pool: &ConnectionPool) {
|
|||
});
|
||||
}
|
||||
|
||||
pub async fn prepare_db(database_url: &str) -> Pool<DieselConnectionManager<PgConnection>> {
|
||||
pub type DbPool = Pool<DieselConnectionManager<PgConnection>>;
|
||||
|
||||
pub async fn prepare_db(database_url: &str) -> DbPool {
|
||||
let manager = DieselConnectionManager::<PgConnection>::new(database_url);
|
||||
let pool = bb8::Pool::builder().build(manager).await.unwrap();
|
||||
seed_simplebot(&pool).await;
|
||||
pool
|
||||
}
|
||||
|
||||
pub async fn api(configuration: Configuration) -> Router {
|
||||
let db_pool = prepare_db(&configuration.database_url).await;
|
||||
|
||||
pub fn api() -> Router {
|
||||
Router::new()
|
||||
.route("/register", post(routes::users::register))
|
||||
.route("/login", post(routes::users::login))
|
||||
|
@ -88,21 +91,34 @@ pub async fn api(configuration: Configuration) -> Router {
|
|||
"/matches/:match_id/log",
|
||||
get(routes::matches::get_match_log),
|
||||
)
|
||||
.route("/leaderboard", get(routes::bots::get_ranking))
|
||||
.route("/submit_bot", post(routes::demo::submit_bot))
|
||||
.route("/save_bot", post(routes::bots::save_bot))
|
||||
.layer(AddExtensionLayer::new(db_pool))
|
||||
}
|
||||
|
||||
pub async fn app() -> Router {
|
||||
let configuration = config::Config::builder()
|
||||
pub fn get_config() -> Result<Configuration, ConfigError> {
|
||||
config::Config::builder()
|
||||
.add_source(config::File::with_name("configuration.toml"))
|
||||
.add_source(config::Environment::with_prefix("PLANETWARS"))
|
||||
.build()
|
||||
.unwrap()
|
||||
.build()?
|
||||
.try_deserialize()
|
||||
.unwrap();
|
||||
let api = api(configuration).await;
|
||||
Router::new().nest("/api", api)
|
||||
}
|
||||
|
||||
pub async fn run_app() {
|
||||
let configuration = get_config().unwrap();
|
||||
let db_pool = prepare_db(&configuration.database_url).await;
|
||||
|
||||
tokio::spawn(run_ranker(db_pool.clone()));
|
||||
|
||||
let api_service = Router::new()
|
||||
.nest("/api", api())
|
||||
.layer(AddExtensionLayer::new(db_pool))
|
||||
.into_make_service();
|
||||
|
||||
// TODO: put in config
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], 9000));
|
||||
|
||||
axum::Server::bind(&addr).serve(api_service).await.unwrap();
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
|
|
@ -1,16 +1,7 @@
|
|||
use std::net::SocketAddr;
|
||||
|
||||
extern crate planetwars_server;
|
||||
extern crate tokio;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let app = planetwars_server::app().await;
|
||||
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], 9000));
|
||||
|
||||
axum::Server::bind(&addr)
|
||||
.serve(app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
planetwars_server::run_app().await;
|
||||
}
|
||||
|
|
102
planetwars-server/src/modules/matches.rs
Normal file
102
planetwars-server/src/modules/matches.rs
Normal file
|
@ -0,0 +1,102 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use diesel::{PgConnection, QueryResult};
|
||||
use planetwars_matchrunner::{self as runner, docker_runner::DockerBotSpec, BotSpec, MatchConfig};
|
||||
use runner::MatchOutcome;
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
use crate::{
|
||||
db::{self, matches::MatchData},
|
||||
util::gen_alphanumeric,
|
||||
ConnectionPool, BOTS_DIR, MAPS_DIR, MATCHES_DIR,
|
||||
};
|
||||
|
||||
const PYTHON_IMAGE: &str = "python:3.10-slim-buster";
|
||||
|
||||
pub struct RunMatch<'a> {
|
||||
log_file_name: String,
|
||||
player_code_bundles: Vec<&'a db::bots::CodeBundle>,
|
||||
match_id: Option<i32>,
|
||||
}
|
||||
|
||||
impl<'a> RunMatch<'a> {
|
||||
pub fn from_players(player_code_bundles: Vec<&'a db::bots::CodeBundle>) -> Self {
|
||||
let log_file_name = format!("{}.log", gen_alphanumeric(16));
|
||||
RunMatch {
|
||||
log_file_name,
|
||||
player_code_bundles,
|
||||
match_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn runner_config(&self) -> runner::MatchConfig {
|
||||
runner::MatchConfig {
|
||||
map_path: PathBuf::from(MAPS_DIR).join("hex.json"),
|
||||
map_name: "hex".to_string(),
|
||||
log_path: PathBuf::from(MATCHES_DIR).join(&self.log_file_name),
|
||||
players: self
|
||||
.player_code_bundles
|
||||
.iter()
|
||||
.map(|b| runner::MatchPlayer {
|
||||
bot_spec: code_bundle_to_botspec(b),
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn store_in_database(&mut self, db_conn: &PgConnection) -> QueryResult<MatchData> {
|
||||
// don't store the same match twice
|
||||
assert!(self.match_id.is_none());
|
||||
|
||||
let new_match_data = db::matches::NewMatch {
|
||||
state: db::matches::MatchState::Playing,
|
||||
log_path: &self.log_file_name,
|
||||
};
|
||||
let new_match_players = self
|
||||
.player_code_bundles
|
||||
.iter()
|
||||
.map(|b| db::matches::MatchPlayerData {
|
||||
code_bundle_id: b.id,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let match_data = db::matches::create_match(&new_match_data, &new_match_players, &db_conn)?;
|
||||
self.match_id = Some(match_data.base.id);
|
||||
Ok(match_data)
|
||||
}
|
||||
|
||||
pub fn spawn(self, pool: ConnectionPool) -> JoinHandle<MatchOutcome> {
|
||||
let match_id = self.match_id.expect("match must be saved before running");
|
||||
let runner_config = self.runner_config();
|
||||
tokio::spawn(run_match_task(pool, runner_config, match_id))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn code_bundle_to_botspec(code_bundle: &db::bots::CodeBundle) -> Box<dyn BotSpec> {
|
||||
let bundle_path = PathBuf::from(BOTS_DIR).join(&code_bundle.path);
|
||||
|
||||
Box::new(DockerBotSpec {
|
||||
code_path: bundle_path,
|
||||
image: PYTHON_IMAGE.to_string(),
|
||||
argv: vec!["python".to_string(), "bot.py".to_string()],
|
||||
})
|
||||
}
|
||||
|
||||
async fn run_match_task(
|
||||
connection_pool: ConnectionPool,
|
||||
match_config: MatchConfig,
|
||||
match_id: i32,
|
||||
) -> MatchOutcome {
|
||||
let outcome = runner::run_match(match_config).await;
|
||||
|
||||
// update match state in database
|
||||
let conn = connection_pool
|
||||
.get()
|
||||
.await
|
||||
.expect("could not get database connection");
|
||||
|
||||
db::matches::set_match_state(match_id, db::matches::MatchState::Finished, &conn)
|
||||
.expect("could not update match state");
|
||||
|
||||
return outcome;
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
// This module implements general domain logic, not directly
|
||||
// tied to the database or API layers.
|
||||
pub mod bots;
|
||||
pub mod matches;
|
||||
pub mod ranking;
|
||||
|
|
87
planetwars-server/src/modules/ranking.rs
Normal file
87
planetwars-server/src/modules/ranking.rs
Normal file
|
@ -0,0 +1,87 @@
|
|||
use crate::{db::bots::Bot, DbPool};
|
||||
|
||||
use crate::db;
|
||||
use crate::modules::matches::RunMatch;
|
||||
use rand::seq::SliceRandom;
|
||||
use std::time::Duration;
|
||||
use tokio;
|
||||
|
||||
const RANKER_INTERVAL: u64 = 60;
|
||||
const START_RATING: f64 = 0.0;
|
||||
const SCALE: f64 = 100.0;
|
||||
const MAX_UPDATE: f64 = 10.0;
|
||||
|
||||
pub async fn run_ranker(db_pool: DbPool) {
|
||||
// TODO: make this configurable
|
||||
// play at most one match every n seconds
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(RANKER_INTERVAL));
|
||||
let db_conn = db_pool
|
||||
.get()
|
||||
.await
|
||||
.expect("could not get database connection");
|
||||
loop {
|
||||
interval.tick().await;
|
||||
let bots = db::bots::find_all_bots(&db_conn).unwrap();
|
||||
if bots.len() < 2 {
|
||||
// not enough bots to play a match
|
||||
continue;
|
||||
}
|
||||
let selected_bots: Vec<Bot> = {
|
||||
let mut rng = &mut rand::thread_rng();
|
||||
bots.choose_multiple(&mut rng, 2).cloned().collect()
|
||||
};
|
||||
play_ranking_match(selected_bots, db_pool.clone()).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn play_ranking_match(selected_bots: Vec<Bot>, db_pool: DbPool) {
|
||||
let db_conn = db_pool.get().await.expect("could not get db pool");
|
||||
let mut code_bundles = Vec::new();
|
||||
for bot in &selected_bots {
|
||||
let code_bundle = db::bots::active_code_bundle(bot.id, &db_conn)
|
||||
.expect("could not get active code bundle");
|
||||
code_bundles.push(code_bundle);
|
||||
}
|
||||
|
||||
let code_bundle_refs = code_bundles.iter().map(|b| b).collect::<Vec<_>>();
|
||||
|
||||
let mut run_match = RunMatch::from_players(code_bundle_refs);
|
||||
run_match
|
||||
.store_in_database(&db_conn)
|
||||
.expect("could not store match in db");
|
||||
let outcome = run_match
|
||||
.spawn(db_pool.clone())
|
||||
.await
|
||||
.expect("running match failed");
|
||||
|
||||
let mut ratings = Vec::new();
|
||||
for bot in &selected_bots {
|
||||
let rating = db::ratings::get_rating(bot.id, &db_conn)
|
||||
.expect("could not get bot rating")
|
||||
.unwrap_or(START_RATING);
|
||||
ratings.push(rating);
|
||||
}
|
||||
|
||||
// simple elo rating
|
||||
|
||||
let scores = match outcome.winner {
|
||||
None => vec![0.5; 2],
|
||||
Some(player_num) => {
|
||||
// TODO: please get rid of this offset
|
||||
let player_ix = player_num - 1;
|
||||
let mut scores = vec![0.0; 2];
|
||||
scores[player_ix] = 1.0;
|
||||
scores
|
||||
}
|
||||
};
|
||||
|
||||
for i in 0..2 {
|
||||
let j = 1 - i;
|
||||
|
||||
let scaled_difference = (ratings[j] - ratings[i]) / SCALE;
|
||||
let expected = 1.0 / (1.0 + 10f64.powf(scaled_difference));
|
||||
let new_rating = ratings[i] + MAX_UPDATE * (scores[i] - expected);
|
||||
db::ratings::set_rating(selected_bots[i].id, new_rating, &db_conn)
|
||||
.expect("could not update bot rating");
|
||||
}
|
||||
}
|
|
@ -12,6 +12,7 @@ use std::path::PathBuf;
|
|||
use thiserror;
|
||||
|
||||
use crate::db::bots::{self, CodeBundle};
|
||||
use crate::db::ratings::{RankedBot, self};
|
||||
use crate::db::users::User;
|
||||
use crate::modules::bots::save_code_bundle;
|
||||
use crate::{DatabaseConnection, BOTS_DIR};
|
||||
|
@ -170,6 +171,12 @@ pub async fn list_bots(conn: DatabaseConnection) -> Result<Json<Vec<Bot>>, Statu
|
|||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
|
||||
}
|
||||
|
||||
pub async fn get_ranking(conn: DatabaseConnection) -> Result<Json<Vec<RankedBot>>, StatusCode> {
|
||||
ratings::get_bot_ranking(&conn)
|
||||
.map(Json)
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
|
||||
}
|
||||
|
||||
// TODO: currently this only implements the happy flow
|
||||
pub async fn upload_code_multipart(
|
||||
conn: DatabaseConnection,
|
||||
|
|
|
@ -1,20 +1,16 @@
|
|||
use crate::db;
|
||||
use crate::db::matches::{FullMatchData, FullMatchPlayerData, MatchPlayerData, MatchState};
|
||||
use crate::db::matches::{FullMatchData, FullMatchPlayerData};
|
||||
use crate::modules::bots::save_code_bundle;
|
||||
use crate::util::gen_alphanumeric;
|
||||
use crate::{ConnectionPool, BOTS_DIR, MAPS_DIR, MATCHES_DIR};
|
||||
use crate::modules::matches::RunMatch;
|
||||
use crate::ConnectionPool;
|
||||
use axum::extract::Extension;
|
||||
use axum::Json;
|
||||
use hyper::StatusCode;
|
||||
use planetwars_matchrunner::BotSpec;
|
||||
use planetwars_matchrunner::{docker_runner::DockerBotSpec, run_match, MatchConfig, MatchPlayer};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::matches::ApiMatch;
|
||||
|
||||
const PYTHON_IMAGE: &str = "python:3.10-slim-buster";
|
||||
const OPPONENT_NAME: &str = "simplebot";
|
||||
const DEFAULT_OPPONENT_NAME: &str = "simplebot";
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct SubmitBotParams {
|
||||
|
@ -29,16 +25,6 @@ pub struct SubmitBotResponse {
|
|||
pub match_data: ApiMatch,
|
||||
}
|
||||
|
||||
fn code_bundle_to_botspec(code_bundle: &db::bots::CodeBundle) -> Box<dyn BotSpec> {
|
||||
let bundle_path = PathBuf::from(BOTS_DIR).join(&code_bundle.path);
|
||||
|
||||
Box::new(DockerBotSpec {
|
||||
code_path: bundle_path,
|
||||
image: PYTHON_IMAGE.to_string(),
|
||||
argv: vec!["python".to_string(), "bot.py".to_string()],
|
||||
})
|
||||
}
|
||||
|
||||
/// submit python code for a bot, which will face off
|
||||
/// with a demo bot. Return a played match.
|
||||
pub async fn submit_bot(
|
||||
|
@ -49,7 +35,7 @@ pub async fn submit_bot(
|
|||
|
||||
let opponent_name = params
|
||||
.opponent_name
|
||||
.unwrap_or_else(|| OPPONENT_NAME.to_string());
|
||||
.unwrap_or_else(|| DEFAULT_OPPONENT_NAME.to_string());
|
||||
|
||||
let opponent =
|
||||
db::bots::find_bot_by_name(&opponent_name, &conn).map_err(|_| StatusCode::BAD_REQUEST)?;
|
||||
|
@ -60,46 +46,11 @@ pub async fn submit_bot(
|
|||
// TODO: can we recover from this?
|
||||
.expect("could not save bot code");
|
||||
|
||||
let log_file_name = format!("{}.log", gen_alphanumeric(16));
|
||||
// play the match
|
||||
let match_config = MatchConfig {
|
||||
map_path: PathBuf::from(MAPS_DIR).join("hex.json"),
|
||||
map_name: "hex".to_string(),
|
||||
log_path: PathBuf::from(MATCHES_DIR).join(&log_file_name),
|
||||
players: vec![
|
||||
MatchPlayer {
|
||||
name: "player".to_string(),
|
||||
bot_spec: code_bundle_to_botspec(&player_code_bundle),
|
||||
},
|
||||
MatchPlayer {
|
||||
name: OPPONENT_NAME.to_string(),
|
||||
bot_spec: code_bundle_to_botspec(&opponent_code_bundle),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// store match in database
|
||||
let new_match_data = db::matches::NewMatch {
|
||||
state: MatchState::Playing,
|
||||
log_path: &log_file_name,
|
||||
};
|
||||
|
||||
let new_match_players = [
|
||||
MatchPlayerData {
|
||||
code_bundle_id: player_code_bundle.id,
|
||||
},
|
||||
MatchPlayerData {
|
||||
code_bundle_id: opponent_code_bundle.id,
|
||||
},
|
||||
];
|
||||
let match_data = db::matches::create_match(&new_match_data, &new_match_players, &conn)
|
||||
.expect("failed to create match");
|
||||
|
||||
tokio::spawn(run_match_task(
|
||||
match_data.base.id,
|
||||
match_config,
|
||||
pool.clone(),
|
||||
));
|
||||
let mut run_match = RunMatch::from_players(vec![&player_code_bundle, &opponent_code_bundle]);
|
||||
let match_data = run_match
|
||||
.store_in_database(&conn)
|
||||
.expect("failed to save match");
|
||||
run_match.spawn(pool.clone());
|
||||
|
||||
// TODO: avoid clones
|
||||
let full_match_data = FullMatchData {
|
||||
|
@ -123,13 +74,3 @@ pub async fn submit_bot(
|
|||
match_data: api_match,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn run_match_task(match_id: i32, match_config: MatchConfig, connection_pool: ConnectionPool) {
|
||||
run_match(match_config).await;
|
||||
let conn = connection_pool
|
||||
.get()
|
||||
.await
|
||||
.expect("could not get database connection");
|
||||
db::matches::set_match_state(match_id, MatchState::Finished, &conn)
|
||||
.expect("failed to update match state");
|
||||
}
|
||||
|
|
|
@ -52,7 +52,6 @@ pub async fn play_match(
|
|||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
players.push(MatchPlayer {
|
||||
name: bot.name.clone(),
|
||||
bot_spec: Box::new(DockerBotSpec {
|
||||
code_path: PathBuf::from(BOTS_DIR).join(code_bundle.path),
|
||||
image: "python:3.10-slim-buster".to_string(),
|
||||
|
|
|
@ -47,6 +47,16 @@ table! {
|
|||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
use crate::db_types::*;
|
||||
|
||||
ratings (bot_id) {
|
||||
bot_id -> Int4,
|
||||
rating -> Float8,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
use crate::db_types::*;
|
||||
|
@ -74,6 +84,15 @@ joinable!(bots -> users (owner_id));
|
|||
joinable!(code_bundles -> bots (bot_id));
|
||||
joinable!(match_players -> code_bundles (code_bundle_id));
|
||||
joinable!(match_players -> matches (match_id));
|
||||
joinable!(ratings -> bots (bot_id));
|
||||
joinable!(sessions -> users (user_id));
|
||||
|
||||
allow_tables_to_appear_in_same_query!(bots, code_bundles, match_players, matches, sessions, users,);
|
||||
allow_tables_to_appear_in_same_query!(
|
||||
bots,
|
||||
code_bundles,
|
||||
match_players,
|
||||
matches,
|
||||
ratings,
|
||||
sessions,
|
||||
users,
|
||||
);
|
||||
|
|
72
web/pw-server/src/lib/components/Leaderboard.svelte
Normal file
72
web/pw-server/src/lib/components/Leaderboard.svelte
Normal file
|
@ -0,0 +1,72 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
|
||||
let leaderboard = [];
|
||||
|
||||
onMount(async () => {
|
||||
const res = await fetch("/api/leaderboard", {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
leaderboard = await res.json();
|
||||
console.log(leaderboard);
|
||||
}
|
||||
});
|
||||
|
||||
function formatRating(entry: object): any {
|
||||
const rating = entry["rating"];
|
||||
if (rating != null) {
|
||||
return rating.toFixed(0);
|
||||
} else {
|
||||
// why does this happen?
|
||||
return "-inf";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<table class="leaderboard">
|
||||
<tr class="leaderboard-row leaderboard-header">
|
||||
<th class="leaderboard-rank" />
|
||||
<th class="leaderboard-rating">Rating</th>
|
||||
<th class="leaderboard-bot">Bot</th>
|
||||
<th class="leaderboard-author">Author</th>
|
||||
</tr>
|
||||
{#each leaderboard as entry, index}
|
||||
<tr class="leaderboard-row">
|
||||
<td class="leaderboard-rank">{index + 1}</td>
|
||||
<td class="leaderboard-rating">
|
||||
{formatRating(entry)}
|
||||
</td>
|
||||
<td class="leaderboard-bot">{entry["bot"]["name"]}</td>
|
||||
<td class="leaderboard-author">
|
||||
{#if entry["author"]}
|
||||
{entry["author"]["username"]}
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.container {
|
||||
overflow-y: scroll;
|
||||
height: 100%;
|
||||
}
|
||||
.leaderboard {
|
||||
margin: 18px auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.leaderboard th,
|
||||
.leaderboard td {
|
||||
padding: 8px 16px;
|
||||
}
|
||||
.leaderboard-rank {
|
||||
color: #333;
|
||||
}
|
||||
</style>
|
|
@ -13,11 +13,13 @@
|
|||
import SubmitPane from "$lib/components/SubmitPane.svelte";
|
||||
import OutputPane from "$lib/components/OutputPane.svelte";
|
||||
import RulesView from "$lib/components/RulesView.svelte";
|
||||
import Leaderboard from "$lib/components/Leaderboard.svelte";
|
||||
|
||||
enum ViewMode {
|
||||
Editor,
|
||||
MatchVisualizer,
|
||||
Rules,
|
||||
Leaderboard,
|
||||
}
|
||||
|
||||
let matches = [];
|
||||
|
@ -111,10 +113,10 @@
|
|||
return log;
|
||||
}
|
||||
|
||||
function selectEditor() {
|
||||
function setViewMode(viewMode_: ViewMode) {
|
||||
selectedMatchId = undefined;
|
||||
selectedMatchLog = undefined;
|
||||
viewMode = ViewMode.Editor;
|
||||
viewMode = viewMode_;
|
||||
}
|
||||
|
||||
function selectRules() {
|
||||
|
@ -140,17 +142,24 @@
|
|||
<div
|
||||
class="editor-button sidebar-item"
|
||||
class:selected={viewMode === ViewMode.Editor}
|
||||
on:click={selectEditor}
|
||||
on:click={() => setViewMode(ViewMode.Editor)}
|
||||
>
|
||||
Editor
|
||||
</div>
|
||||
<div
|
||||
class="rules-button sidebar-item"
|
||||
class:selected={viewMode === ViewMode.Rules}
|
||||
on:click={selectRules}
|
||||
on:click={() => setViewMode(ViewMode.Rules)}
|
||||
>
|
||||
Rules
|
||||
</div>
|
||||
<div
|
||||
class="sidebar-item"
|
||||
class:selected={viewMode === ViewMode.Leaderboard}
|
||||
on:click={() => setViewMode(ViewMode.Leaderboard)}
|
||||
>
|
||||
Leaderboard
|
||||
</div>
|
||||
<div class="sidebar-header">match history</div>
|
||||
<ul class="match-list">
|
||||
{#each matches as match}
|
||||
|
@ -175,6 +184,8 @@
|
|||
<EditorView {editSession} />
|
||||
{:else if viewMode === ViewMode.Rules}
|
||||
<RulesView />
|
||||
{:else if viewMode === ViewMode.Leaderboard}
|
||||
<Leaderboard />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="sidebar-right">
|
||||
|
@ -220,16 +231,9 @@
|
|||
height: 100%;
|
||||
}
|
||||
|
||||
.editor-button {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.rules-button {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.sidebar-item {
|
||||
color: #eee;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.sidebar-item:hover {
|
||||
|
|
Loading…
Reference in a new issue