aboutsummaryrefslogtreecommitdiff
path: root/planetwars-server/src
diff options
context:
space:
mode:
Diffstat (limited to 'planetwars-server/src')
-rw-r--r--planetwars-server/src/cli.rs54
-rw-r--r--planetwars-server/src/db/maps.rs35
-rw-r--r--planetwars-server/src/db/matches.rs152
-rw-r--r--planetwars-server/src/db/mod.rs1
-rw-r--r--planetwars-server/src/db/users.rs28
-rw-r--r--planetwars-server/src/lib.rs12
-rw-r--r--planetwars-server/src/modules/client_api.rs11
-rw-r--r--planetwars-server/src/modules/matches.rs22
-rw-r--r--planetwars-server/src/modules/ranking.rs22
-rw-r--r--planetwars-server/src/modules/registry.rs4
-rw-r--r--planetwars-server/src/routes/demo.rs14
-rw-r--r--planetwars-server/src/routes/maps.rs19
-rw-r--r--planetwars-server/src/routes/matches.rs75
-rw-r--r--planetwars-server/src/routes/mod.rs1
-rw-r--r--planetwars-server/src/schema.rs15
15 files changed, 414 insertions, 51 deletions
diff --git a/planetwars-server/src/cli.rs b/planetwars-server/src/cli.rs
new file mode 100644
index 0000000..f33506e
--- /dev/null
+++ b/planetwars-server/src/cli.rs
@@ -0,0 +1,54 @@
+extern crate planetwars_server;
+extern crate tokio;
+
+use clap::Parser;
+use planetwars_server::db;
+use planetwars_server::{create_db_pool, get_config};
+
+#[derive(clap::Parser)]
+struct Args {
+ #[clap(subcommand)]
+ action: Action,
+}
+
+#[derive(clap::Subcommand)]
+enum Action {
+ SetPassword(SetPassword),
+}
+
+impl Action {
+ async fn run(self) {
+ match self {
+ Action::SetPassword(set_password) => set_password.run().await,
+ }
+ }
+}
+
+#[derive(clap::Parser)]
+struct SetPassword {
+ #[clap(value_parser)]
+ username: String,
+
+ #[clap(value_parser)]
+ new_password: String,
+}
+
+impl SetPassword {
+ async fn run(self) {
+ let global_config = get_config().unwrap();
+ let pool = create_db_pool(&global_config).await;
+
+ let conn = pool.get().await.expect("could not get database connection");
+ let credentials = db::users::Credentials {
+ username: &self.username,
+ password: &self.new_password,
+ };
+ db::users::set_user_password(credentials, &conn).expect("could not set password");
+ }
+}
+
+#[tokio::main]
+pub async fn main() {
+ let args = Args::parse();
+ args.action.run().await;
+}
diff --git a/planetwars-server/src/db/maps.rs b/planetwars-server/src/db/maps.rs
new file mode 100644
index 0000000..dffe4fd
--- /dev/null
+++ b/planetwars-server/src/db/maps.rs
@@ -0,0 +1,35 @@
+use diesel::prelude::*;
+
+use crate::schema::maps;
+
+#[derive(Insertable)]
+#[table_name = "maps"]
+pub struct NewMap<'a> {
+ pub name: &'a str,
+ pub file_path: &'a str,
+}
+
+#[derive(Queryable, Clone, Debug)]
+pub struct Map {
+ pub id: i32,
+ pub name: String,
+ pub file_path: String,
+}
+
+pub fn create_map(new_map: NewMap, conn: &PgConnection) -> QueryResult<Map> {
+ diesel::insert_into(maps::table)
+ .values(new_map)
+ .get_result(conn)
+}
+
+pub fn find_map(id: i32, conn: &PgConnection) -> QueryResult<Map> {
+ maps::table.find(id).get_result(conn)
+}
+
+pub fn find_map_by_name(name: &str, conn: &PgConnection) -> QueryResult<Map> {
+ maps::table.filter(maps::name.eq(name)).first(conn)
+}
+
+pub fn list_maps(conn: &PgConnection) -> QueryResult<Vec<Map>> {
+ maps::table.get_results(conn)
+}
diff --git a/planetwars-server/src/db/matches.rs b/planetwars-server/src/db/matches.rs
index 061e2ea..2041296 100644
--- a/planetwars-server/src/db/matches.rs
+++ b/planetwars-server/src/db/matches.rs
@@ -1,20 +1,27 @@
pub use crate::db_types::MatchState;
use chrono::NaiveDateTime;
use diesel::associations::BelongsTo;
+use diesel::pg::Pg;
+use diesel::query_builder::BoxedSelectStatement;
+use diesel::query_source::{AppearsInFromClause, Once};
use diesel::{
BelongingToDsl, ExpressionMethods, JoinOnDsl, NullableExpressionMethods, QueryDsl, RunQueryDsl,
};
use diesel::{Connection, GroupedBy, PgConnection, QueryResult};
+use std::collections::{HashMap, HashSet};
-use crate::schema::{bot_versions, bots, match_players, matches};
+use crate::schema::{bot_versions, bots, maps, match_players, matches};
use super::bots::{Bot, BotVersion};
+use super::maps::Map;
#[derive(Insertable)]
#[table_name = "matches"]
pub struct NewMatch<'a> {
pub state: MatchState,
pub log_path: &'a str,
+ pub is_public: bool,
+ pub map_id: Option<i32>,
}
#[derive(Insertable)]
@@ -36,6 +43,8 @@ pub struct MatchBase {
pub log_path: String,
pub created_at: NaiveDateTime,
pub winner: Option<i32>,
+ pub is_public: bool,
+ pub map_id: Option<i32>,
}
#[derive(Queryable, Identifiable, Associations, Clone)]
@@ -87,42 +96,133 @@ pub struct MatchData {
pub match_players: Vec<MatchPlayer>,
}
+/// Add player information to MatchBase instances
+fn fetch_full_match_data(
+ matches: Vec<MatchBase>,
+ conn: &PgConnection,
+) -> QueryResult<Vec<FullMatchData>> {
+ let map_ids: HashSet<i32> = matches.iter().filter_map(|m| m.map_id).collect();
+
+ let maps_by_id: HashMap<i32, Map> = maps::table
+ .filter(maps::id.eq_any(map_ids))
+ .load::<Map>(conn)?
+ .into_iter()
+ .map(|m| (m.id, m))
+ .collect();
+
+ let match_players = MatchPlayer::belonging_to(&matches)
+ .left_join(
+ bot_versions::table.on(match_players::bot_version_id.eq(bot_versions::id.nullable())),
+ )
+ .left_join(bots::table.on(bot_versions::bot_id.eq(bots::id.nullable())))
+ .order_by((
+ match_players::match_id.asc(),
+ match_players::player_id.asc(),
+ ))
+ .load::<FullMatchPlayerData>(conn)?
+ .grouped_by(&matches);
+
+ let res = matches
+ .into_iter()
+ .zip(match_players.into_iter())
+ .map(|(base, players)| FullMatchData {
+ match_players: players.into_iter().collect(),
+ map: base
+ .map_id
+ .and_then(|map_id| maps_by_id.get(&map_id).cloned()),
+ base,
+ })
+ .collect();
+
+ Ok(res)
+}
+
+// TODO: this method should disappear
pub fn list_matches(amount: i64, conn: &PgConnection) -> QueryResult<Vec<FullMatchData>> {
conn.transaction(|| {
let matches = matches::table
+ .filter(matches::state.eq(MatchState::Finished))
.order_by(matches::created_at.desc())
.limit(amount)
.get_results::<MatchBase>(conn)?;
- let match_players = MatchPlayer::belonging_to(&matches)
- .left_join(
- bot_versions::table
- .on(match_players::bot_version_id.eq(bot_versions::id.nullable())),
- )
- .left_join(bots::table.on(bot_versions::bot_id.eq(bots::id.nullable())))
- .order_by((
- match_players::match_id.asc(),
- match_players::player_id.asc(),
- ))
- .load::<FullMatchPlayerData>(conn)?
- .grouped_by(&matches);
-
- let res = matches
- .into_iter()
- .zip(match_players.into_iter())
- .map(|(base, players)| FullMatchData {
- base,
- match_players: players.into_iter().collect(),
- })
- .collect();
+ fetch_full_match_data(matches, conn)
+ })
+}
- Ok(res)
+pub fn list_public_matches(
+ amount: i64,
+ before: Option<NaiveDateTime>,
+ after: Option<NaiveDateTime>,
+ conn: &PgConnection,
+) -> QueryResult<Vec<FullMatchData>> {
+ conn.transaction(|| {
+ // TODO: how can this common logic be abstracted?
+ let query = matches::table
+ .filter(matches::state.eq(MatchState::Finished))
+ .filter(matches::is_public.eq(true))
+ .into_boxed();
+
+ let matches =
+ select_matches_page(query, amount, before, after).get_results::<MatchBase>(conn)?;
+ fetch_full_match_data(matches, conn)
})
}
+pub fn list_bot_matches(
+ bot_id: i32,
+ amount: i64,
+ before: Option<NaiveDateTime>,
+ after: Option<NaiveDateTime>,
+ conn: &PgConnection,
+) -> QueryResult<Vec<FullMatchData>> {
+ let query = matches::table
+ .filter(matches::state.eq(MatchState::Finished))
+ .filter(matches::is_public.eq(true))
+ .order_by(matches::created_at.desc())
+ .inner_join(match_players::table)
+ .inner_join(
+ bot_versions::table.on(match_players::bot_version_id.eq(bot_versions::id.nullable())),
+ )
+ .filter(bot_versions::bot_id.eq(bot_id))
+ .select(matches::all_columns)
+ .into_boxed();
+
+ let matches =
+ select_matches_page(query, amount, before, after).get_results::<MatchBase>(conn)?;
+ fetch_full_match_data(matches, conn)
+}
+
+fn select_matches_page<QS>(
+ query: BoxedSelectStatement<'static, matches::SqlType, QS, Pg>,
+ amount: i64,
+ before: Option<NaiveDateTime>,
+ after: Option<NaiveDateTime>,
+) -> BoxedSelectStatement<'static, matches::SqlType, QS, Pg>
+where
+ QS: AppearsInFromClause<matches::table, Count = Once>,
+{
+ // TODO: this is not nice. Replace this with proper cursor logic.
+ match (before, after) {
+ (None, None) => query.order_by(matches::created_at.desc()),
+ (Some(before), None) => query
+ .filter(matches::created_at.lt(before))
+ .order_by(matches::created_at.desc()),
+ (None, Some(after)) => query
+ .filter(matches::created_at.gt(after))
+ .order_by(matches::created_at.asc()),
+ (Some(before), Some(after)) => query
+ .filter(matches::created_at.lt(before))
+ .filter(matches::created_at.gt(after))
+ .order_by(matches::created_at.desc()),
+ }
+ .limit(amount)
+}
+
// TODO: maybe unify this with matchdata?
pub struct FullMatchData {
pub base: MatchBase,
+ pub map: Option<Map>,
pub match_players: Vec<FullMatchPlayerData>,
}
@@ -151,6 +251,11 @@ pub fn find_match(id: i32, conn: &PgConnection) -> QueryResult<FullMatchData> {
conn.transaction(|| {
let match_base = matches::table.find(id).get_result::<MatchBase>(conn)?;
+ let map = match match_base.map_id {
+ None => None,
+ Some(map_id) => Some(super::maps::find_map(map_id, conn)?),
+ };
+
let match_players = MatchPlayer::belonging_to(&match_base)
.left_join(
bot_versions::table
@@ -163,6 +268,7 @@ pub fn find_match(id: i32, conn: &PgConnection) -> QueryResult<FullMatchData> {
let res = FullMatchData {
base: match_base,
match_players,
+ map,
};
Ok(res)
diff --git a/planetwars-server/src/db/mod.rs b/planetwars-server/src/db/mod.rs
index 84ed2a6..f014cea 100644
--- a/planetwars-server/src/db/mod.rs
+++ b/planetwars-server/src/db/mod.rs
@@ -1,4 +1,5 @@
pub mod bots;
+pub mod maps;
pub mod matches;
pub mod ratings;
pub mod sessions;
diff --git a/planetwars-server/src/db/users.rs b/planetwars-server/src/db/users.rs
index ebb2268..9676dae 100644
--- a/planetwars-server/src/db/users.rs
+++ b/planetwars-server/src/db/users.rs
@@ -42,11 +42,17 @@ fn argon2_config() -> argon2::Config<'static> {
}
}
-pub fn create_user(credentials: &Credentials, conn: &PgConnection) -> QueryResult<User> {
+pub fn hash_password(password: &str) -> (Vec<u8>, [u8; 32]) {
let argon_config = argon2_config();
-
let salt: [u8; 32] = rand::thread_rng().gen();
- let hash = argon2::hash_raw(credentials.password.as_bytes(), &salt, &argon_config).unwrap();
+ let hash = argon2::hash_raw(password.as_bytes(), &salt, &argon_config).unwrap();
+
+ (hash, salt)
+}
+
+pub fn create_user(credentials: &Credentials, conn: &PgConnection) -> QueryResult<User> {
+ let (hash, salt) = hash_password(&credentials.password);
+
let new_user = NewUser {
username: credentials.username,
password_salt: &salt,
@@ -69,6 +75,22 @@ pub fn find_user_by_name(username: &str, db_conn: &PgConnection) -> QueryResult<
.first::<User>(db_conn)
}
+pub fn set_user_password(credentials: Credentials, db_conn: &PgConnection) -> QueryResult<()> {
+ let (hash, salt) = hash_password(&credentials.password);
+
+ let n_changes = diesel::update(users::table.filter(users::username.eq(&credentials.username)))
+ .set((
+ users::password_salt.eq(salt.as_slice()),
+ users::password_hash.eq(hash.as_slice()),
+ ))
+ .execute(db_conn)?;
+ if n_changes == 0 {
+ Err(diesel::result::Error::NotFound)
+ } else {
+ Ok(())
+ }
+}
+
pub fn authenticate_user(credentials: &Credentials, db_conn: &PgConnection) -> Option<User> {
find_user_by_name(credentials.username, db_conn)
.optional()
diff --git a/planetwars-server/src/lib.rs b/planetwars-server/src/lib.rs
index e16b232..fa1af8d 100644
--- a/planetwars-server/src/lib.rs
+++ b/planetwars-server/src/lib.rs
@@ -94,11 +94,9 @@ pub async fn seed_simplebot(config: &GlobalConfig, pool: &ConnectionPool) {
pub type DbPool = Pool<DieselConnectionManager<PgConnection>>;
-pub async fn prepare_db(config: &GlobalConfig) -> DbPool {
+pub async fn create_db_pool(config: &GlobalConfig) -> DbPool {
let manager = DieselConnectionManager::<PgConnection>::new(&config.database_url);
- let pool = bb8::Pool::builder().build(manager).await.unwrap();
- seed_simplebot(config, &pool).await;
- pool
+ bb8::Pool::builder().build(manager).await.unwrap()
}
// create all directories required for further operation
@@ -129,12 +127,13 @@ pub fn api() -> Router {
"/bots/:bot_name/upload",
post(routes::bots::upload_code_multipart),
)
- .route("/matches", get(routes::matches::list_matches))
+ .route("/matches", get(routes::matches::list_recent_matches))
.route("/matches/:match_id", get(routes::matches::get_match_data))
.route(
"/matches/:match_id/log",
get(routes::matches::get_match_log),
)
+ .route("/maps", get(routes::maps::list_maps))
.route("/leaderboard", get(routes::bots::get_ranking))
.route("/submit_bot", post(routes::demo::submit_bot))
.route("/save_bot", post(routes::bots::save_bot))
@@ -165,7 +164,8 @@ async fn run_registry(config: Arc<GlobalConfig>, db_pool: DbPool) {
pub async fn run_app() {
let global_config = Arc::new(get_config().unwrap());
- let db_pool = prepare_db(&global_config).await;
+ let db_pool = create_db_pool(&global_config).await;
+ seed_simplebot(&global_config, &db_pool).await;
init_directories(&global_config).unwrap();
if global_config.ranker_enabled {
diff --git a/planetwars-server/src/modules/client_api.rs b/planetwars-server/src/modules/client_api.rs
index 7026671..3402964 100644
--- a/planetwars-server/src/modules/client_api.rs
+++ b/planetwars-server/src/modules/client_api.rs
@@ -111,14 +111,23 @@ impl pb::client_api_service_server::ClientApiService for ClientApiServer {
db::bots::find_bot_with_version_by_name(&match_request.opponent_name, &conn)
.map_err(|_| Status::not_found("opponent not found"))?;
+ let map_name = match match_request.map_name.as_str() {
+ "" => "hex",
+ name => name,
+ };
+ let map = db::maps::find_map_by_name(map_name, &conn)
+ .map_err(|_| Status::not_found("map not found"))?;
+
let player_key = gen_alphanumeric(32);
let remote_bot_spec = Box::new(RemoteBotSpec {
player_key: player_key.clone(),
router: self.router.clone(),
});
- let run_match = RunMatch::from_players(
+ let run_match = RunMatch::new(
self.runner_config.clone(),
+ false,
+ map,
vec![
MatchPlayer::BotSpec {
spec: remote_bot_spec,
diff --git a/planetwars-server/src/modules/matches.rs b/planetwars-server/src/modules/matches.rs
index 4f538ed..ecc7976 100644
--- a/planetwars-server/src/modules/matches.rs
+++ b/planetwars-server/src/modules/matches.rs
@@ -7,6 +7,7 @@ use tokio::task::JoinHandle;
use crate::{
db::{
self,
+ maps::Map,
matches::{MatchData, MatchResult},
},
util::gen_alphanumeric,
@@ -17,6 +18,11 @@ pub struct RunMatch {
log_file_name: String,
players: Vec<MatchPlayer>,
config: Arc<GlobalConfig>,
+ is_public: bool,
+ // Map is mandatory for now.
+ // It would be nice to allow "anonymous" (eg. randomly generated) maps
+ // in the future, too.
+ map: Map,
}
pub enum MatchPlayer {
@@ -30,19 +36,27 @@ pub enum MatchPlayer {
}
impl RunMatch {
- pub fn from_players(config: Arc<GlobalConfig>, players: Vec<MatchPlayer>) -> Self {
+ // TODO: create a MatchParams struct
+ pub fn new(
+ config: Arc<GlobalConfig>,
+ is_public: bool,
+ map: Map,
+ players: Vec<MatchPlayer>,
+ ) -> Self {
let log_file_name = format!("{}.log", gen_alphanumeric(16));
RunMatch {
config,
log_file_name,
players,
+ is_public,
+ map,
}
}
fn into_runner_config(self) -> runner::MatchConfig {
runner::MatchConfig {
- map_path: PathBuf::from(&self.config.maps_directory).join("hex.json"),
- map_name: "hex".to_string(),
+ map_path: PathBuf::from(&self.config.maps_directory).join(self.map.file_path),
+ map_name: self.map.name,
log_path: PathBuf::from(&self.config.match_logs_directory).join(&self.log_file_name),
players: self
.players
@@ -80,6 +94,8 @@ impl RunMatch {
let new_match_data = db::matches::NewMatch {
state: db::matches::MatchState::Playing,
log_path: &self.log_file_name,
+ is_public: self.is_public,
+ map_id: Some(self.map.id),
};
let new_match_players = self
.players
diff --git a/planetwars-server/src/modules/ranking.rs b/planetwars-server/src/modules/ranking.rs
index cb699fe..90c4a56 100644
--- a/planetwars-server/src/modules/ranking.rs
+++ b/planetwars-server/src/modules/ranking.rs
@@ -1,4 +1,5 @@
use crate::db::bots::BotVersion;
+use crate::db::maps::Map;
use crate::{db::bots::Bot, DbPool, GlobalConfig};
use crate::db;
@@ -25,22 +26,31 @@ pub async fn run_ranker(config: Arc<GlobalConfig>, db_pool: DbPool) {
.expect("could not get database connection");
loop {
interval.tick().await;
- let bots = db::bots::all_active_bots_with_version(&db_conn).unwrap();
+ let bots = db::bots::all_active_bots_with_version(&db_conn).expect("could not load bots");
if bots.len() < 2 {
// not enough bots to play a match
continue;
}
- let selected_bots: Vec<(Bot, BotVersion)> = {
- let mut rng = &mut rand::thread_rng();
- bots.choose_multiple(&mut rng, 2).cloned().collect()
+
+ let selected_bots: Vec<(Bot, BotVersion)> = bots
+ .choose_multiple(&mut rand::thread_rng(), 2)
+ .cloned()
+ .collect();
+
+ let maps = db::maps::list_maps(&db_conn).expect("could not load map");
+ let map = match maps.choose(&mut rand::thread_rng()).cloned() {
+ None => continue, // no maps available
+ Some(map) => map,
};
- play_ranking_match(config.clone(), selected_bots, db_pool.clone()).await;
+
+ play_ranking_match(config.clone(), map, selected_bots, db_pool.clone()).await;
recalculate_ratings(&db_conn).expect("could not recalculate ratings");
}
}
async fn play_ranking_match(
config: Arc<GlobalConfig>,
+ map: Map,
selected_bots: Vec<(Bot, BotVersion)>,
db_pool: DbPool,
) {
@@ -53,7 +63,7 @@ async fn play_ranking_match(
players.push(player);
}
- let (_, handle) = RunMatch::from_players(config, players)
+ let (_, handle) = RunMatch::new(config, true, map, players)
.run(db_pool.clone())
.await
.expect("failed to run match");
diff --git a/planetwars-server/src/modules/registry.rs b/planetwars-server/src/modules/registry.rs
index 297404a..4a79d59 100644
--- a/planetwars-server/src/modules/registry.rs
+++ b/planetwars-server/src/modules/registry.rs
@@ -300,8 +300,10 @@ async fn put_upload(
while let Some(Ok(chunk)) = stream.next().await {
file.write_all(&chunk).await.unwrap();
}
- file.flush().await.unwrap();
let range_end = last_byte_pos(&file).await.unwrap();
+ // Close the file to ensure all data has been flushed to the kernel.
+ // If we don't do this, calculating the checksum can fail.
+ std::mem::drop(file);
let expected_digest = params.digest.strip_prefix("sha256:").unwrap();
let digest = file_sha256_digest(&upload_path).unwrap();
diff --git a/planetwars-server/src/routes/demo.rs b/planetwars-server/src/routes/demo.rs
index dad9453..1ec8825 100644
--- a/planetwars-server/src/routes/demo.rs
+++ b/planetwars-server/src/routes/demo.rs
@@ -14,12 +14,13 @@ use serde::{Deserialize, Serialize};
use super::matches::ApiMatch;
const DEFAULT_OPPONENT_NAME: &str = "simplebot";
+const DEFAULT_MAP_NAME: &str = "hex";
#[derive(Serialize, Deserialize, Debug)]
pub struct SubmitBotParams {
pub code: String,
- // TODO: would it be better to pass an ID here?
pub opponent_name: Option<String>,
+ pub map_name: Option<String>,
}
#[derive(Serialize, Deserialize)]
@@ -40,16 +41,24 @@ pub async fn submit_bot(
.opponent_name
.unwrap_or_else(|| DEFAULT_OPPONENT_NAME.to_string());
+ let map_name = params
+ .map_name
+ .unwrap_or_else(|| DEFAULT_MAP_NAME.to_string());
+
let (opponent_bot, opponent_bot_version) =
db::bots::find_bot_with_version_by_name(&opponent_name, &conn)
.map_err(|_| StatusCode::BAD_REQUEST)?;
+ let map = db::maps::find_map_by_name(&map_name, &conn).map_err(|_| StatusCode::BAD_REQUEST)?;
+
let player_bot_version = save_code_string(&params.code, None, &conn, &config)
// TODO: can we recover from this?
.expect("could not save bot code");
- let run_match = RunMatch::from_players(
+ let run_match = RunMatch::new(
config,
+ false,
+ map.clone(),
vec![
MatchPlayer::BotVersion {
bot: None,
@@ -81,6 +90,7 @@ pub async fn submit_bot(
bot: Some(opponent_bot),
},
],
+ map: Some(map),
};
let api_match = super::matches::match_data_to_api(full_match_data);
diff --git a/planetwars-server/src/routes/maps.rs b/planetwars-server/src/routes/maps.rs
new file mode 100644
index 0000000..689b11e
--- /dev/null
+++ b/planetwars-server/src/routes/maps.rs
@@ -0,0 +1,19 @@
+use crate::{db, DatabaseConnection};
+use axum::Json;
+use hyper::StatusCode;
+use serde::{Deserialize, Serialize};
+
+#[derive(Serialize, Deserialize)]
+pub struct ApiMap {
+ pub name: String,
+}
+
+pub async fn list_maps(conn: DatabaseConnection) -> Result<Json<Vec<ApiMap>>, StatusCode> {
+ let maps = db::maps::list_maps(&conn).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
+
+ let api_maps = maps
+ .into_iter()
+ .map(|map| ApiMap { name: map.name })
+ .collect();
+ Ok(Json(api_maps))
+}
diff --git a/planetwars-server/src/routes/matches.rs b/planetwars-server/src/routes/matches.rs
index 58ca478..10b4507 100644
--- a/planetwars-server/src/routes/matches.rs
+++ b/planetwars-server/src/routes/matches.rs
@@ -1,19 +1,30 @@
-use axum::{extract::Path, Extension, Json};
+use axum::{
+ extract::{Path, Query},
+ Extension, Json,
+};
+use chrono::NaiveDateTime;
use hyper::StatusCode;
use serde::{Deserialize, Serialize};
use std::{path::PathBuf, sync::Arc};
use crate::{
- db::matches::{self, MatchState},
+ db::{
+ self,
+ matches::{self, MatchState},
+ },
DatabaseConnection, GlobalConfig,
};
+use super::maps::ApiMap;
+
#[derive(Serialize, Deserialize)]
pub struct ApiMatch {
id: i32,
timestamp: chrono::NaiveDateTime,
state: MatchState,
players: Vec<ApiMatchPlayer>,
+ winner: Option<i32>,
+ map: Option<ApiMap>,
}
#[derive(Serialize, Deserialize)]
@@ -23,10 +34,60 @@ pub struct ApiMatchPlayer {
bot_name: Option<String>,
}
-pub async fn list_matches(conn: DatabaseConnection) -> Result<Json<Vec<ApiMatch>>, StatusCode> {
- matches::list_matches(100, &conn)
- .map_err(|_| StatusCode::BAD_REQUEST)
- .map(|matches| Json(matches.into_iter().map(match_data_to_api).collect()))
+#[derive(Serialize, Deserialize)]
+pub struct ListRecentMatchesParams {
+ count: Option<usize>,
+ // TODO: should timezone be specified here?
+ before: Option<NaiveDateTime>,
+ after: Option<NaiveDateTime>,
+
+ bot: Option<String>,
+}
+
+const MAX_NUM_RETURNED_MATCHES: usize = 100;
+const DEFAULT_NUM_RETURNED_MATCHES: usize = 50;
+
+#[derive(Serialize, Deserialize)]
+pub struct ListMatchesResponse {
+ matches: Vec<ApiMatch>,
+ has_next: bool,
+}
+
+pub async fn list_recent_matches(
+ Query(params): Query<ListRecentMatchesParams>,
+ conn: DatabaseConnection,
+) -> Result<Json<ListMatchesResponse>, StatusCode> {
+ let requested_count = std::cmp::min(
+ params.count.unwrap_or(DEFAULT_NUM_RETURNED_MATCHES),
+ MAX_NUM_RETURNED_MATCHES,
+ );
+
+ // fetch one additional record to check whether a next page exists
+ let count = (requested_count + 1) as i64;
+
+ let matches_result = match params.bot {
+ Some(bot_name) => {
+ let bot = db::bots::find_bot_by_name(&bot_name, &conn)
+ .map_err(|_| StatusCode::BAD_REQUEST)?;
+ matches::list_bot_matches(bot.id, count, params.before, params.after, &conn)
+ }
+ None => matches::list_public_matches(count, params.before, params.after, &conn),
+ };
+
+ let mut matches = matches_result.map_err(|_| StatusCode::BAD_REQUEST)?;
+
+ let mut has_next = false;
+ if matches.len() > requested_count {
+ has_next = true;
+ matches.truncate(requested_count);
+ }
+
+ let api_matches = matches.into_iter().map(match_data_to_api).collect();
+
+ Ok(Json(ListMatchesResponse {
+ matches: api_matches,
+ has_next,
+ }))
}
pub fn match_data_to_api(data: matches::FullMatchData) -> ApiMatch {
@@ -43,6 +104,8 @@ pub fn match_data_to_api(data: matches::FullMatchData) -> ApiMatch {
bot_name: _p.bot.as_ref().map(|b| b.name.clone()),
})
.collect(),
+ winner: data.base.winner,
+ map: data.map.map(|m| ApiMap { name: m.name }),
}
}
diff --git a/planetwars-server/src/routes/mod.rs b/planetwars-server/src/routes/mod.rs
index b3decb8..9510fd4 100644
--- a/planetwars-server/src/routes/mod.rs
+++ b/planetwars-server/src/routes/mod.rs
@@ -1,4 +1,5 @@
pub mod bots;
pub mod demo;
+pub mod maps;
pub mod matches;
pub mod users;
diff --git a/planetwars-server/src/schema.rs b/planetwars-server/src/schema.rs
index 5993115..adc6555 100644
--- a/planetwars-server/src/schema.rs
+++ b/planetwars-server/src/schema.rs
@@ -30,6 +30,17 @@ table! {
use diesel::sql_types::*;
use crate::db_types::*;
+ maps (id) {
+ id -> Int4,
+ name -> Text,
+ file_path -> Text,
+ }
+}
+
+table! {
+ use diesel::sql_types::*;
+ use crate::db_types::*;
+
match_players (match_id, player_id) {
match_id -> Int4,
player_id -> Int4,
@@ -47,6 +58,8 @@ table! {
log_path -> Text,
created_at -> Timestamp,
winner -> Nullable<Int4>,
+ is_public -> Bool,
+ map_id -> Nullable<Int4>,
}
}
@@ -86,12 +99,14 @@ table! {
joinable!(bots -> users (owner_id));
joinable!(match_players -> bot_versions (bot_version_id));
joinable!(match_players -> matches (match_id));
+joinable!(matches -> maps (map_id));
joinable!(ratings -> bots (bot_id));
joinable!(sessions -> users (user_id));
allow_tables_to_appear_in_same_query!(
bot_versions,
bots,
+ maps,
match_players,
matches,
ratings,