store matches in database

This commit is contained in:
Ilion Beyst 2022-01-02 16:14:03 +01:00
parent bdb77f97d6
commit 85dcf3ba2f
7 changed files with 194 additions and 9 deletions

View file

@ -0,0 +1,3 @@
DROP TABLE match_players;
DROP INDEX match_created_at;
DROP TABLE matches;

View file

@ -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)
);

View file

@ -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<i32> {
conn.transaction(|| {
let match_base = diesel::insert_into(matches::table)
.values(match_data)
.get_result::<MatchBase>(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::<Vec<_>>();
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<MatchPlayer>,
}
pub fn list_matches(conn: &PgConnection) -> QueryResult<Vec<MatchData>> {
conn.transaction(|| {
let matches = matches::table.get_results::<MatchBase>(conn)?;
let match_players = MatchPlayer::belonging_to(&matches)
.load::<MatchPlayer>(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)
})
}

View file

@ -1,3 +1,4 @@
pub mod bots; pub mod bots;
pub mod matches;
pub mod sessions; pub mod sessions;
pub mod users; pub mod users;

View file

@ -48,7 +48,10 @@ pub async fn api() -> Router {
"/bots/:bot_id/upload", "/bots/:bot_id/upload",
post(routes::bots::upload_code_multipart), 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)); .layer(AddExtensionLayer::new(pool));
api api
} }

View file

@ -1,14 +1,14 @@
use std::path::PathBuf; use std::path::PathBuf;
use axum::Json; use axum::{extract::Extension, Json};
use hyper::StatusCode; use hyper::StatusCode;
use planetwars_matchrunner::{run_match, MatchConfig, MatchPlayer}; use planetwars_matchrunner::{run_match, MatchConfig, MatchPlayer};
use rand::{distributions::Alphanumeric, Rng}; use rand::{distributions::Alphanumeric, Rng};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
db::{bots, users::User}, db::{bots, matches, users::User},
DatabaseConnection, BOTS_DIR, MAPS_DIR, MATCHES_DIR, ConnectionPool, DatabaseConnection, BOTS_DIR, MAPS_DIR, MATCHES_DIR,
}; };
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
@ -18,10 +18,11 @@ pub struct MatchParams {
} }
pub async fn play_match( pub async fn play_match(
user: User, _user: User,
conn: DatabaseConnection, Extension(pool): Extension<ConnectionPool>,
Json(params): Json<MatchParams>, Json(params): Json<MatchParams>,
) -> Result<(), StatusCode> { ) -> Result<(), StatusCode> {
let conn = pool.get().await.expect("could not get database connection");
let map_path = PathBuf::from(MAPS_DIR).join("hex.json"); let map_path = PathBuf::from(MAPS_DIR).join("hex.json");
let slug: String = rand::thread_rng() 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 log_path = PathBuf::from(MATCHES_DIR).join(&format!("{}.log", slug));
let mut players = Vec::new(); let mut players = Vec::new();
let mut bot_ids = Vec::new();
for bot_name in params.players { for bot_name in params.players {
let bot = bots::find_bot(bot_name, &conn).map_err(|_| StatusCode::BAD_REQUEST)?; let bot = bots::find_bot(bot_name, &conn).map_err(|_| StatusCode::BAD_REQUEST)?;
let code_bundle = 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 // TODO: this is an user error, should ideally be handled before we get here
.ok_or_else(|| StatusCode::INTERNAL_SERVER_ERROR)?, .ok_or_else(|| StatusCode::INTERNAL_SERVER_ERROR)?,
}); });
bot_ids.push(matches::MatchPlayerData { bot_id: bot.id });
} }
let match_config = MatchConfig { let match_config = MatchConfig {
map_name: "hex".to_string(), map_name: "hex".to_string(),
map_path, map_path,
log_path: log_path.clone(), 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(()) Ok(())
} }
async fn run_match_task(
config: MatchConfig,
match_players: Vec<matches::MatchPlayerData>,
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<ApiMatchPlayer>,
}
#[derive(Serialize, Deserialize)]
pub struct ApiMatchPlayer {
bot_id: i32,
}
pub async fn list_matches(conn: DatabaseConnection) -> Result<Json<Vec<ApiMatch>>, 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 // TODO: this is duplicated from planetwars-cli
// clean this up and move to matchrunner crate // clean this up and move to matchrunner crate
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]

View file

@ -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! { table! {
sessions (id) { sessions (id) {
id -> Int4, id -> Int4,
@ -34,6 +50,8 @@ table! {
joinable!(bots -> users (owner_id)); joinable!(bots -> users (owner_id));
joinable!(code_bundles -> bots (bot_id)); joinable!(code_bundles -> bots (bot_id));
joinable!(match_players -> bots (bot_id));
joinable!(match_players -> matches (match_id));
joinable!(sessions -> users (user_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,);