store matches in database
This commit is contained in:
parent
bdb77f97d6
commit
85dcf3ba2f
7 changed files with 194 additions and 9 deletions
|
@ -0,0 +1,3 @@
|
||||||
|
DROP TABLE match_players;
|
||||||
|
DROP INDEX match_created_at;
|
||||||
|
DROP TABLE matches;
|
|
@ -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)
|
||||||
|
);
|
97
planetwars-server/src/db/matches.rs
Normal file
97
planetwars-server/src/db/matches.rs
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
|
@ -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,);
|
||||||
|
|
Loading…
Reference in a new issue