diff --git a/planetwars-server/migrations/2022-01-02-105610_matches/down.sql b/planetwars-server/migrations/2022-01-02-105610_matches/down.sql new file mode 100644 index 0000000..eadd0fa --- /dev/null +++ b/planetwars-server/migrations/2022-01-02-105610_matches/down.sql @@ -0,0 +1,3 @@ +DROP TABLE match_players; +DROP INDEX match_created_at; +DROP TABLE matches; \ No newline at end of file diff --git a/planetwars-server/migrations/2022-01-02-105610_matches/up.sql b/planetwars-server/migrations/2022-01-02-105610_matches/up.sql new file mode 100644 index 0000000..88c8c9e --- /dev/null +++ b/planetwars-server/migrations/2022-01-02-105610_matches/up.sql @@ -0,0 +1,14 @@ +CREATE TABLE matches ( + id SERIAL PRIMARY KEY, + log_path text NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX match_created_at ON matches(created_at); + +CREATE TABLE match_players ( + match_id integer REFERENCES matches(id) NOT NULL, + bot_id integer REFERENCES bots(id) NOT NULL, + player_id integer NOT NULL, + PRIMARY KEY (match_id, player_id) +); \ No newline at end of file diff --git a/planetwars-server/src/db/matches.rs b/planetwars-server/src/db/matches.rs new file mode 100644 index 0000000..85f0631 --- /dev/null +++ b/planetwars-server/src/db/matches.rs @@ -0,0 +1,97 @@ +use chrono::NaiveDateTime; +use diesel::{BelongingToDsl, RunQueryDsl}; +use diesel::{Connection, GroupedBy, PgConnection, QueryResult}; + +use crate::schema::{match_players, matches}; + +#[derive(Insertable)] +#[table_name = "matches"] +pub struct NewMatch<'a> { + pub log_path: &'a str, +} + +#[derive(Insertable)] +#[table_name = "match_players"] +pub struct NewMatchPlayer { + /// id of the match this player is in + pub match_id: i32, + /// id of the bot behind this player + pub bot_id: i32, + /// player id within the match + pub player_id: i32, +} + +#[derive(Queryable, Identifiable)] +#[table_name = "matches"] +pub struct MatchBase { + pub id: i32, + pub log_path: String, + pub created_at: NaiveDateTime, +} + +#[derive(Queryable, Identifiable, Associations)] +#[primary_key(match_id, player_id)] +#[belongs_to(MatchBase, foreign_key = "match_id")] +pub struct MatchPlayer { + pub match_id: i32, + pub bot_id: i32, + pub player_id: i32, +} + +pub struct MatchPlayerData { + pub bot_id: i32, +} + +pub fn create_match( + match_data: &NewMatch, + match_players: &[MatchPlayerData], + conn: &PgConnection, +) -> QueryResult { + conn.transaction(|| { + let match_base = diesel::insert_into(matches::table) + .values(match_data) + .get_result::(conn)?; + + let match_players = match_players + .iter() + .enumerate() + .map(|(num, player_data)| NewMatchPlayer { + match_id: match_base.id, + bot_id: player_data.bot_id, + player_id: num as i32, + }) + .collect::>(); + + diesel::insert_into(match_players::table) + .values(&match_players) + .execute(conn)?; + + Ok(match_base.id) + }) +} + +pub struct MatchData { + pub base: MatchBase, + pub match_players: Vec, +} + +pub fn list_matches(conn: &PgConnection) -> QueryResult> { + conn.transaction(|| { + let matches = matches::table.get_results::(conn)?; + + let match_players = MatchPlayer::belonging_to(&matches) + .load::(conn)? + .grouped_by(&matches); + + let res = matches + .into_iter() + .zip(match_players.into_iter()) + .map(|(base, players)| MatchData { + base, + match_players: players.into_iter().collect(), + }) + .collect(); + + Ok(res) + }) +} diff --git a/planetwars-server/src/db/mod.rs b/planetwars-server/src/db/mod.rs index 947b789..7a950c6 100644 --- a/planetwars-server/src/db/mod.rs +++ b/planetwars-server/src/db/mod.rs @@ -1,3 +1,4 @@ pub mod bots; +pub mod matches; pub mod sessions; pub mod users; diff --git a/planetwars-server/src/lib.rs b/planetwars-server/src/lib.rs index 1d08580..7eaa5a0 100644 --- a/planetwars-server/src/lib.rs +++ b/planetwars-server/src/lib.rs @@ -48,7 +48,10 @@ pub async fn api() -> Router { "/bots/:bot_id/upload", post(routes::bots::upload_code_multipart), ) - .route("/matches", post(routes::matches::play_match)) + .route( + "/matches", + get(routes::matches::list_matches).post(routes::matches::play_match), + ) .layer(AddExtensionLayer::new(pool)); api } diff --git a/planetwars-server/src/routes/matches.rs b/planetwars-server/src/routes/matches.rs index 4a556af..f2c5186 100644 --- a/planetwars-server/src/routes/matches.rs +++ b/planetwars-server/src/routes/matches.rs @@ -1,14 +1,14 @@ use std::path::PathBuf; -use axum::Json; +use axum::{extract::Extension, Json}; use hyper::StatusCode; use planetwars_matchrunner::{run_match, MatchConfig, MatchPlayer}; use rand::{distributions::Alphanumeric, Rng}; use serde::{Deserialize, Serialize}; use crate::{ - db::{bots, users::User}, - DatabaseConnection, BOTS_DIR, MAPS_DIR, MATCHES_DIR, + db::{bots, matches, users::User}, + ConnectionPool, DatabaseConnection, BOTS_DIR, MAPS_DIR, MATCHES_DIR, }; #[derive(Serialize, Deserialize, Debug)] @@ -18,10 +18,11 @@ pub struct MatchParams { } pub async fn play_match( - user: User, - conn: DatabaseConnection, + _user: User, + Extension(pool): Extension, Json(params): Json, ) -> Result<(), StatusCode> { + let conn = pool.get().await.expect("could not get database connection"); let map_path = PathBuf::from(MAPS_DIR).join("hex.json"); let slug: String = rand::thread_rng() @@ -32,6 +33,7 @@ pub async fn play_match( let log_path = PathBuf::from(MATCHES_DIR).join(&format!("{}.log", slug)); let mut players = Vec::new(); + let mut bot_ids = Vec::new(); for bot_name in params.players { let bot = bots::find_bot(bot_name, &conn).map_err(|_| StatusCode::BAD_REQUEST)?; let code_bundle = @@ -49,19 +51,66 @@ pub async fn play_match( // TODO: this is an user error, should ideally be handled before we get here .ok_or_else(|| StatusCode::INTERNAL_SERVER_ERROR)?, }); + + bot_ids.push(matches::MatchPlayerData { bot_id: bot.id }); } let match_config = MatchConfig { map_name: "hex".to_string(), map_path, log_path: log_path.clone(), - players, + players: players, }; - tokio::spawn(run_match(match_config)); + tokio::spawn(run_match_task(match_config, bot_ids, pool.clone())); Ok(()) } +async fn run_match_task( + config: MatchConfig, + match_players: Vec, + pool: ConnectionPool, +) { + let log_path = config.log_path.as_os_str().to_str().unwrap().to_string(); + let match_data = matches::NewMatch { + log_path: &log_path, + }; + + run_match(config).await; + let conn = pool.get().await.expect("could not get database connection"); + matches::create_match(&match_data, &match_players, &conn).expect("could not create match"); +} + +#[derive(Serialize, Deserialize)] +pub struct ApiMatch { + id: i32, + timestamp: chrono::NaiveDateTime, + players: Vec, +} + +#[derive(Serialize, Deserialize)] +pub struct ApiMatchPlayer { + bot_id: i32, +} + +pub async fn list_matches(conn: DatabaseConnection) -> Result>, StatusCode> { + matches::list_matches(&conn) + .map_err(|_| StatusCode::BAD_REQUEST) + .map(|matches| Json(matches.into_iter().map(match_data_to_api).collect())) +} + +fn match_data_to_api(data: matches::MatchData) -> ApiMatch { + ApiMatch { + id: data.base.id, + timestamp: data.base.created_at, + players: data + .match_players + .iter() + .map(|p| ApiMatchPlayer { bot_id: p.bot_id }) + .collect(), + } +} + // TODO: this is duplicated from planetwars-cli // clean this up and move to matchrunner crate #[derive(Serialize, Deserialize)] diff --git a/planetwars-server/src/schema.rs b/planetwars-server/src/schema.rs index bf58434..413c4d1 100644 --- a/planetwars-server/src/schema.rs +++ b/planetwars-server/src/schema.rs @@ -15,6 +15,22 @@ table! { } } +table! { + match_players (match_id, player_id) { + match_id -> Int4, + bot_id -> Int4, + player_id -> Int4, + } +} + +table! { + matches (id) { + id -> Int4, + log_path -> Text, + created_at -> Timestamp, + } +} + table! { sessions (id) { id -> Int4, @@ -34,6 +50,8 @@ table! { joinable!(bots -> users (owner_id)); joinable!(code_bundles -> bots (bot_id)); +joinable!(match_players -> bots (bot_id)); +joinable!(match_players -> matches (match_id)); joinable!(sessions -> users (user_id)); -allow_tables_to_appear_in_same_query!(bots, code_bundles, sessions, users,); +allow_tables_to_appear_in_same_query!(bots, code_bundles, match_players, matches, sessions, users,);