Merge branch 'bot-api' into next
This commit is contained in:
commit
268e080ec1
18 changed files with 493 additions and 32 deletions
|
@ -3,6 +3,6 @@
|
||||||
members = [
|
members = [
|
||||||
"planetwars-rules",
|
"planetwars-rules",
|
||||||
"planetwars-matchrunner",
|
"planetwars-matchrunner",
|
||||||
"planetwars-cli",
|
|
||||||
"planetwars-server",
|
"planetwars-server",
|
||||||
|
"planetwars-client",
|
||||||
]
|
]
|
||||||
|
|
18
planetwars-client/Cargo.toml
Normal file
18
planetwars-client/Cargo.toml
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
[package]
|
||||||
|
name = "planetwars-client"
|
||||||
|
version = "0.0.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tokio = { version = "1.15", features = ["full"] }
|
||||||
|
tokio-stream = "0.1.9"
|
||||||
|
prost = "0.10"
|
||||||
|
tonic = "0.7.2"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
toml = "0.5"
|
||||||
|
planetwars-matchrunner = { path = "../planetwars-matchrunner" }
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tonic-build = "0.7.2"
|
9
planetwars-client/build.rs
Normal file
9
planetwars-client/build.rs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
extern crate tonic_build;
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
tonic_build::configure()
|
||||||
|
.build_server(false)
|
||||||
|
.build_client(true)
|
||||||
|
.compile(&["../proto/bot_api.proto"], &["../proto"])?;
|
||||||
|
Ok(())
|
||||||
|
}
|
2
planetwars-client/simplebot.toml
Normal file
2
planetwars-client/simplebot.toml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
name = "simplebot"
|
||||||
|
command = ["python", "../simplebot/simplebot.py"]
|
72
planetwars-client/src/main.rs
Normal file
72
planetwars-client/src/main.rs
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
pub mod pb {
|
||||||
|
tonic::include_proto!("grpc.planetwars.bot_api");
|
||||||
|
}
|
||||||
|
|
||||||
|
use pb::bot_api_service_client::BotApiServiceClient;
|
||||||
|
use planetwars_matchrunner::bot_runner::Bot;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::{path::PathBuf, time::Duration};
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||||
|
use tonic::{metadata::MetadataValue, transport::Channel, Request, Status};
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct BotConfig {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
name: String,
|
||||||
|
command: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let content = std::fs::read_to_string("simplebot.toml").unwrap();
|
||||||
|
let bot_config: BotConfig = toml::from_str(&content).unwrap();
|
||||||
|
|
||||||
|
let channel = Channel::from_static("http://localhost:50051")
|
||||||
|
.connect()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let created_match = create_match(channel.clone()).await.unwrap();
|
||||||
|
run_player(bot_config, created_match.player_key, channel).await;
|
||||||
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_match(channel: Channel) -> Result<pb::CreatedMatch, Status> {
|
||||||
|
let mut client = BotApiServiceClient::new(channel);
|
||||||
|
let res = client
|
||||||
|
.create_match(Request::new(pb::MatchRequest {
|
||||||
|
opponent_name: "simplebot".to_string(),
|
||||||
|
}))
|
||||||
|
.await;
|
||||||
|
res.map(|response| response.into_inner())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_player(bot_config: BotConfig, player_key: String, channel: Channel) {
|
||||||
|
let mut client = BotApiServiceClient::with_interceptor(channel, |mut req: Request<()>| {
|
||||||
|
let player_key: MetadataValue<_> = player_key.parse().unwrap();
|
||||||
|
req.metadata_mut().insert("player_key", player_key);
|
||||||
|
Ok(req)
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut bot_process = Bot {
|
||||||
|
working_dir: PathBuf::from("."),
|
||||||
|
argv: bot_config.command,
|
||||||
|
}
|
||||||
|
.spawn_process();
|
||||||
|
|
||||||
|
let (tx, rx) = mpsc::unbounded_channel();
|
||||||
|
let mut stream = client
|
||||||
|
.connect_bot(UnboundedReceiverStream::new(rx))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.into_inner();
|
||||||
|
while let Some(message) = stream.message().await.unwrap() {
|
||||||
|
let moves = bot_process.communicate(&message.content).await.unwrap();
|
||||||
|
tx.send(pb::PlayerRequestResponse {
|
||||||
|
request_id: message.request_id,
|
||||||
|
content: moves.as_bytes().to_vec(),
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ edition = "2021"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
tokio = { version = "1.15", features = ["full"] }
|
tokio = { version = "1.15", features = ["full"] }
|
||||||
|
tokio-stream = "0.1.9"
|
||||||
hyper = "0.14"
|
hyper = "0.14"
|
||||||
axum = { version = "0.5", features = ["json", "headers", "multipart"] }
|
axum = { version = "0.5", features = ["json", "headers", "multipart"] }
|
||||||
diesel = { version = "1.4.4", features = ["postgres", "chrono"] }
|
diesel = { version = "1.4.4", features = ["postgres", "chrono"] }
|
||||||
|
@ -29,9 +30,14 @@ config = { version = "0.12", features = ["toml"] }
|
||||||
thiserror = "1.0.31"
|
thiserror = "1.0.31"
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
tokio-util = { version="0.7.3", features=["io"] }
|
tokio-util = { version="0.7.3", features=["io"] }
|
||||||
|
prost = "0.10"
|
||||||
|
tonic = "0.7.2"
|
||||||
|
|
||||||
# TODO: remove me
|
# TODO: remove me
|
||||||
shlex = "1.1"
|
shlex = "1.1"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tonic-build = "0.7.2"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
parking_lot = "0.11"
|
parking_lot = "0.11"
|
||||||
|
|
9
planetwars-server/build.rs
Normal file
9
planetwars-server/build.rs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
extern crate tonic_build;
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
tonic_build::configure()
|
||||||
|
.build_server(true)
|
||||||
|
.build_client(false)
|
||||||
|
.compile(&["../proto/bot_api.proto"], &["../proto"])?;
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE match_players ALTER COLUMN code_bundle_id SET NOT NULL;
|
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE match_players ALTER COLUMN code_bundle_id DROP NOT NULL;
|
|
@ -25,7 +25,7 @@ pub struct NewMatchPlayer {
|
||||||
/// player id within the match
|
/// player id within the match
|
||||||
pub player_id: i32,
|
pub player_id: i32,
|
||||||
/// id of the bot behind this player
|
/// id of the bot behind this player
|
||||||
pub code_bundle_id: i32,
|
pub code_bundle_id: Option<i32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Queryable, Identifiable)]
|
#[derive(Queryable, Identifiable)]
|
||||||
|
@ -44,11 +44,11 @@ pub struct MatchBase {
|
||||||
pub struct MatchPlayer {
|
pub struct MatchPlayer {
|
||||||
pub match_id: i32,
|
pub match_id: i32,
|
||||||
pub player_id: i32,
|
pub player_id: i32,
|
||||||
pub code_bundle_id: i32,
|
pub code_bundle_id: Option<i32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct MatchPlayerData {
|
pub struct MatchPlayerData {
|
||||||
pub code_bundle_id: i32,
|
pub code_bundle_id: Option<i32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_match(
|
pub fn create_match(
|
||||||
|
@ -92,7 +92,10 @@ pub fn list_matches(conn: &PgConnection) -> QueryResult<Vec<FullMatchData>> {
|
||||||
let matches = matches::table.get_results::<MatchBase>(conn)?;
|
let matches = matches::table.get_results::<MatchBase>(conn)?;
|
||||||
|
|
||||||
let match_players = MatchPlayer::belonging_to(&matches)
|
let match_players = MatchPlayer::belonging_to(&matches)
|
||||||
.inner_join(code_bundles::table)
|
.left_join(
|
||||||
|
code_bundles::table
|
||||||
|
.on(match_players::code_bundle_id.eq(code_bundles::id.nullable())),
|
||||||
|
)
|
||||||
.left_join(bots::table.on(code_bundles::bot_id.eq(bots::id.nullable())))
|
.left_join(bots::table.on(code_bundles::bot_id.eq(bots::id.nullable())))
|
||||||
.load::<FullMatchPlayerData>(conn)?
|
.load::<FullMatchPlayerData>(conn)?
|
||||||
.grouped_by(&matches);
|
.grouped_by(&matches);
|
||||||
|
@ -120,7 +123,7 @@ pub struct FullMatchData {
|
||||||
// #[primary_key(base.match_id, base::player_id)]
|
// #[primary_key(base.match_id, base::player_id)]
|
||||||
pub struct FullMatchPlayerData {
|
pub struct FullMatchPlayerData {
|
||||||
pub base: MatchPlayer,
|
pub base: MatchPlayer,
|
||||||
pub code_bundle: CodeBundle,
|
pub code_bundle: Option<CodeBundle>,
|
||||||
pub bot: Option<Bot>,
|
pub bot: Option<Bot>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,7 +145,10 @@ pub fn find_match(id: i32, conn: &PgConnection) -> QueryResult<FullMatchData> {
|
||||||
let match_base = matches::table.find(id).get_result::<MatchBase>(conn)?;
|
let match_base = matches::table.find(id).get_result::<MatchBase>(conn)?;
|
||||||
|
|
||||||
let match_players = MatchPlayer::belonging_to(&match_base)
|
let match_players = MatchPlayer::belonging_to(&match_base)
|
||||||
.inner_join(code_bundles::table)
|
.left_join(
|
||||||
|
code_bundles::table
|
||||||
|
.on(match_players::code_bundle_id.eq(code_bundles::id.nullable())),
|
||||||
|
)
|
||||||
.left_join(bots::table.on(code_bundles::bot_id.eq(bots::id.nullable())))
|
.left_join(bots::table.on(code_bundles::bot_id.eq(bots::id.nullable())))
|
||||||
.load::<FullMatchPlayerData>(conn)?;
|
.load::<FullMatchPlayerData>(conn)?;
|
||||||
|
|
||||||
|
|
272
planetwars-server/src/modules/bot_api.rs
Normal file
272
planetwars-server/src/modules/bot_api.rs
Normal file
|
@ -0,0 +1,272 @@
|
||||||
|
pub mod pb {
|
||||||
|
tonic::include_proto!("grpc.planetwars.bot_api");
|
||||||
|
}
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use runner::match_context::{EventBus, PlayerHandle, RequestError, RequestMessage};
|
||||||
|
use runner::match_log::MatchLogger;
|
||||||
|
use tokio::sync::{mpsc, oneshot};
|
||||||
|
use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||||
|
use tonic;
|
||||||
|
use tonic::transport::Server;
|
||||||
|
use tonic::{Request, Response, Status, Streaming};
|
||||||
|
|
||||||
|
use planetwars_matchrunner as runner;
|
||||||
|
|
||||||
|
use crate::db;
|
||||||
|
use crate::util::gen_alphanumeric;
|
||||||
|
use crate::ConnectionPool;
|
||||||
|
|
||||||
|
use super::matches::{MatchPlayer, RunMatch};
|
||||||
|
|
||||||
|
pub struct BotApiServer {
|
||||||
|
conn_pool: ConnectionPool,
|
||||||
|
router: PlayerRouter,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Routes players to their handler
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct PlayerRouter {
|
||||||
|
routing_table: Arc<Mutex<HashMap<String, SyncThingData>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlayerRouter {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
PlayerRouter {
|
||||||
|
routing_table: Arc::new(Mutex::new(HashMap::new())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PlayerRouter {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: implement a way to expire entries
|
||||||
|
impl PlayerRouter {
|
||||||
|
fn put(&self, player_key: String, entry: SyncThingData) {
|
||||||
|
let mut routing_table = self.routing_table.lock().unwrap();
|
||||||
|
routing_table.insert(player_key, entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn take(&self, player_key: &str) -> Option<SyncThingData> {
|
||||||
|
// TODO: this design does not allow for reconnects. Is this desired?
|
||||||
|
let mut routing_table = self.routing_table.lock().unwrap();
|
||||||
|
routing_table.remove(player_key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tonic::async_trait]
|
||||||
|
impl pb::bot_api_service_server::BotApiService for BotApiServer {
|
||||||
|
type ConnectBotStream = UnboundedReceiverStream<Result<pb::PlayerRequest, Status>>;
|
||||||
|
|
||||||
|
async fn connect_bot(
|
||||||
|
&self,
|
||||||
|
req: Request<Streaming<pb::PlayerRequestResponse>>,
|
||||||
|
) -> Result<Response<Self::ConnectBotStream>, Status> {
|
||||||
|
// TODO: clean up errors
|
||||||
|
let player_key = req
|
||||||
|
.metadata()
|
||||||
|
.get("player_key")
|
||||||
|
.ok_or_else(|| Status::unauthenticated("no player_key provided"))?;
|
||||||
|
|
||||||
|
let player_key_str = player_key
|
||||||
|
.to_str()
|
||||||
|
.map_err(|_| Status::invalid_argument("unreadable string"))?;
|
||||||
|
|
||||||
|
let sync_data = self
|
||||||
|
.router
|
||||||
|
.take(player_key_str)
|
||||||
|
.ok_or_else(|| Status::not_found("player_key not found"))?;
|
||||||
|
|
||||||
|
let stream = req.into_inner();
|
||||||
|
|
||||||
|
sync_data.tx.send(stream).unwrap();
|
||||||
|
Ok(Response::new(UnboundedReceiverStream::new(
|
||||||
|
sync_data.server_messages,
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_match(
|
||||||
|
&self,
|
||||||
|
req: Request<pb::MatchRequest>,
|
||||||
|
) -> Result<Response<pb::CreatedMatch>, Status> {
|
||||||
|
// TODO: unify with matchrunner module
|
||||||
|
let conn = self.conn_pool.get().await.unwrap();
|
||||||
|
|
||||||
|
let match_request = req.get_ref();
|
||||||
|
|
||||||
|
let opponent = db::bots::find_bot_by_name(&match_request.opponent_name, &conn)
|
||||||
|
.map_err(|_| Status::not_found("opponent not found"))?;
|
||||||
|
let opponent_code_bundle = db::bots::active_code_bundle(opponent.id, &conn)
|
||||||
|
.map_err(|_| Status::not_found("opponent has no code"))?;
|
||||||
|
|
||||||
|
let player_key = gen_alphanumeric(32);
|
||||||
|
|
||||||
|
let remote_bot_spec = Box::new(RemoteBotSpec {
|
||||||
|
player_key: player_key.clone(),
|
||||||
|
router: self.router.clone(),
|
||||||
|
});
|
||||||
|
let mut run_match = RunMatch::from_players(vec![
|
||||||
|
MatchPlayer::from_bot_spec(remote_bot_spec),
|
||||||
|
MatchPlayer::from_code_bundle(&opponent_code_bundle),
|
||||||
|
]);
|
||||||
|
let created_match = run_match
|
||||||
|
.store_in_database(&conn)
|
||||||
|
.expect("failed to save match");
|
||||||
|
run_match.spawn(self.conn_pool.clone());
|
||||||
|
|
||||||
|
Ok(Response::new(pb::CreatedMatch {
|
||||||
|
match_id: created_match.base.id,
|
||||||
|
player_key,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: please rename me
|
||||||
|
struct SyncThingData {
|
||||||
|
tx: oneshot::Sender<Streaming<pb::PlayerRequestResponse>>,
|
||||||
|
server_messages: mpsc::UnboundedReceiver<Result<pb::PlayerRequest, Status>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RemoteBotSpec {
|
||||||
|
player_key: String,
|
||||||
|
router: PlayerRouter,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tonic::async_trait]
|
||||||
|
impl runner::BotSpec for RemoteBotSpec {
|
||||||
|
async fn run_bot(
|
||||||
|
&self,
|
||||||
|
player_id: u32,
|
||||||
|
event_bus: Arc<Mutex<EventBus>>,
|
||||||
|
_match_logger: MatchLogger,
|
||||||
|
) -> Box<dyn PlayerHandle> {
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
let (server_msg_snd, server_msg_recv) = mpsc::unbounded_channel();
|
||||||
|
self.router.put(
|
||||||
|
self.player_key.clone(),
|
||||||
|
SyncThingData {
|
||||||
|
tx,
|
||||||
|
server_messages: server_msg_recv,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let fut = tokio::time::timeout(Duration::from_secs(10), rx);
|
||||||
|
match fut.await {
|
||||||
|
Ok(Ok(client_messages)) => {
|
||||||
|
// let client_messages = rx.await.unwrap();
|
||||||
|
tokio::spawn(handle_bot_messages(
|
||||||
|
player_id,
|
||||||
|
event_bus.clone(),
|
||||||
|
client_messages,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// ensure router cleanup
|
||||||
|
self.router.take(&self.player_key);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// If the player did not connect, the receiving half of `sender`
|
||||||
|
// will be dropped here, resulting in a time-out for every turn.
|
||||||
|
// This is fine for now, but
|
||||||
|
// TODO: provide a formal mechanism for player startup failure
|
||||||
|
Box::new(RemoteBotHandle {
|
||||||
|
sender: server_msg_snd,
|
||||||
|
player_id,
|
||||||
|
event_bus,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_bot_messages(
|
||||||
|
player_id: u32,
|
||||||
|
event_bus: Arc<Mutex<EventBus>>,
|
||||||
|
mut messages: Streaming<pb::PlayerRequestResponse>,
|
||||||
|
) {
|
||||||
|
while let Some(message) = messages.message().await.unwrap() {
|
||||||
|
let request_id = (player_id, message.request_id as u32);
|
||||||
|
event_bus
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.resolve_request(request_id, Ok(message.content));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RemoteBotHandle {
|
||||||
|
sender: mpsc::UnboundedSender<Result<pb::PlayerRequest, Status>>,
|
||||||
|
player_id: u32,
|
||||||
|
event_bus: Arc<Mutex<EventBus>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlayerHandle for RemoteBotHandle {
|
||||||
|
fn send_request(&mut self, r: RequestMessage) {
|
||||||
|
let res = self.sender.send(Ok(pb::PlayerRequest {
|
||||||
|
request_id: r.request_id as i32,
|
||||||
|
content: r.content,
|
||||||
|
}));
|
||||||
|
match res {
|
||||||
|
Ok(()) => {
|
||||||
|
// schedule a timeout. See comments at method implementation
|
||||||
|
tokio::spawn(schedule_timeout(
|
||||||
|
(self.player_id, r.request_id),
|
||||||
|
r.timeout,
|
||||||
|
self.event_bus.clone(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Err(_send_error) => {
|
||||||
|
// cannot contact the remote bot anymore;
|
||||||
|
// directly mark all requests as timed out.
|
||||||
|
// TODO: create a dedicated error type for this.
|
||||||
|
// should it be logged?
|
||||||
|
println!("send error: {:?}", _send_error);
|
||||||
|
self.event_bus
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.resolve_request((self.player_id, r.request_id), Err(RequestError::Timeout));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: this will spawn a task for every request, which might not be ideal.
|
||||||
|
// Some alternatives:
|
||||||
|
// - create a single task that manages all time-outs.
|
||||||
|
// - intersperse timeouts with incoming client messages
|
||||||
|
// - push timeouts upwards, into the matchrunner logic (before we hit the playerhandle).
|
||||||
|
// This was initially not done to allow timer start to be delayed until the message actually arrived
|
||||||
|
// with the player. Is this still needed, or is there a different way to do this?
|
||||||
|
//
|
||||||
|
async fn schedule_timeout(
|
||||||
|
request_id: (u32, u32),
|
||||||
|
duration: Duration,
|
||||||
|
event_bus: Arc<Mutex<EventBus>>,
|
||||||
|
) {
|
||||||
|
tokio::time::sleep(duration).await;
|
||||||
|
event_bus
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.resolve_request(request_id, Err(RequestError::Timeout));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run_bot_api(pool: ConnectionPool) {
|
||||||
|
let router = PlayerRouter::new();
|
||||||
|
let server = BotApiServer {
|
||||||
|
router,
|
||||||
|
conn_pool: pool.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let addr = SocketAddr::from(([127, 0, 0, 1], 50051));
|
||||||
|
Server::builder()
|
||||||
|
.add_service(pb::bot_api_service_server::BotApiServiceServer::new(server))
|
||||||
|
.serve(addr)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
}
|
|
@ -16,32 +16,54 @@ use crate::{
|
||||||
|
|
||||||
const PYTHON_IMAGE: &str = "python:3.10-slim-buster";
|
const PYTHON_IMAGE: &str = "python:3.10-slim-buster";
|
||||||
|
|
||||||
pub struct RunMatch<'a> {
|
pub struct RunMatch {
|
||||||
log_file_name: String,
|
log_file_name: String,
|
||||||
player_code_bundles: Vec<&'a db::bots::CodeBundle>,
|
players: Vec<MatchPlayer>,
|
||||||
match_id: Option<i32>,
|
match_id: Option<i32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> RunMatch<'a> {
|
pub struct MatchPlayer {
|
||||||
pub fn from_players(player_code_bundles: Vec<&'a db::bots::CodeBundle>) -> Self {
|
bot_spec: Box<dyn BotSpec>,
|
||||||
|
// meta that will be passed on to database
|
||||||
|
code_bundle_id: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MatchPlayer {
|
||||||
|
pub fn from_code_bundle(code_bundle: &db::bots::CodeBundle) -> Self {
|
||||||
|
MatchPlayer {
|
||||||
|
bot_spec: code_bundle_to_botspec(code_bundle),
|
||||||
|
code_bundle_id: Some(code_bundle.id),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_bot_spec(bot_spec: Box<dyn BotSpec>) -> Self {
|
||||||
|
MatchPlayer {
|
||||||
|
bot_spec,
|
||||||
|
code_bundle_id: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RunMatch {
|
||||||
|
pub fn from_players(players: Vec<MatchPlayer>) -> Self {
|
||||||
let log_file_name = format!("{}.log", gen_alphanumeric(16));
|
let log_file_name = format!("{}.log", gen_alphanumeric(16));
|
||||||
RunMatch {
|
RunMatch {
|
||||||
log_file_name,
|
log_file_name,
|
||||||
player_code_bundles,
|
players,
|
||||||
match_id: None,
|
match_id: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn runner_config(&self) -> runner::MatchConfig {
|
pub fn into_runner_config(self) -> runner::MatchConfig {
|
||||||
runner::MatchConfig {
|
runner::MatchConfig {
|
||||||
map_path: PathBuf::from(MAPS_DIR).join("hex.json"),
|
map_path: PathBuf::from(MAPS_DIR).join("hex.json"),
|
||||||
map_name: "hex".to_string(),
|
map_name: "hex".to_string(),
|
||||||
log_path: PathBuf::from(MATCHES_DIR).join(&self.log_file_name),
|
log_path: PathBuf::from(MATCHES_DIR).join(&self.log_file_name),
|
||||||
players: self
|
players: self
|
||||||
.player_code_bundles
|
.players
|
||||||
.iter()
|
.into_iter()
|
||||||
.map(|b| runner::MatchPlayer {
|
.map(|player| runner::MatchPlayer {
|
||||||
bot_spec: code_bundle_to_botspec(b),
|
bot_spec: player.bot_spec,
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
}
|
}
|
||||||
|
@ -56,10 +78,10 @@ impl<'a> RunMatch<'a> {
|
||||||
log_path: &self.log_file_name,
|
log_path: &self.log_file_name,
|
||||||
};
|
};
|
||||||
let new_match_players = self
|
let new_match_players = self
|
||||||
.player_code_bundles
|
.players
|
||||||
.iter()
|
.iter()
|
||||||
.map(|b| db::matches::MatchPlayerData {
|
.map(|p| db::matches::MatchPlayerData {
|
||||||
code_bundle_id: b.id,
|
code_bundle_id: p.code_bundle_id,
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
@ -70,7 +92,7 @@ impl<'a> RunMatch<'a> {
|
||||||
|
|
||||||
pub fn spawn(self, pool: ConnectionPool) -> JoinHandle<MatchOutcome> {
|
pub fn spawn(self, pool: ConnectionPool) -> JoinHandle<MatchOutcome> {
|
||||||
let match_id = self.match_id.expect("match must be saved before running");
|
let match_id = self.match_id.expect("match must be saved before running");
|
||||||
let runner_config = self.runner_config();
|
let runner_config = self.into_runner_config();
|
||||||
tokio::spawn(run_match_task(pool, runner_config, match_id))
|
tokio::spawn(run_match_task(pool, runner_config, match_id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
// This module implements general domain logic, not directly
|
// This module implements general domain logic, not directly
|
||||||
// tied to the database or API layers.
|
// tied to the database or API layers.
|
||||||
|
pub mod bot_api;
|
||||||
pub mod bots;
|
pub mod bots;
|
||||||
pub mod matches;
|
pub mod matches;
|
||||||
pub mod ranking;
|
pub mod ranking;
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
use crate::{db::bots::Bot, DbPool};
|
use crate::{db::bots::Bot, DbPool};
|
||||||
|
|
||||||
use crate::db;
|
use crate::db;
|
||||||
use crate::modules::matches::RunMatch;
|
|
||||||
use diesel::{PgConnection, QueryResult};
|
use diesel::{PgConnection, QueryResult};
|
||||||
|
use crate::modules::matches::{MatchPlayer, RunMatch};
|
||||||
use rand::seq::SliceRandom;
|
use rand::seq::SliceRandom;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::mem;
|
use std::mem;
|
||||||
|
@ -44,9 +44,12 @@ async fn play_ranking_match(selected_bots: Vec<Bot>, db_pool: DbPool) {
|
||||||
code_bundles.push(code_bundle);
|
code_bundles.push(code_bundle);
|
||||||
}
|
}
|
||||||
|
|
||||||
let code_bundle_refs = code_bundles.iter().collect::<Vec<_>>();
|
let players = code_bundles
|
||||||
|
.iter()
|
||||||
|
.map(MatchPlayer::from_code_bundle)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let mut run_match = RunMatch::from_players(code_bundle_refs);
|
let mut run_match = RunMatch::from_players(players);
|
||||||
run_match
|
run_match
|
||||||
.store_in_database(&db_conn)
|
.store_in_database(&db_conn)
|
||||||
.expect("could not store match in db");
|
.expect("could not store match in db");
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::db;
|
use crate::db;
|
||||||
use crate::db::matches::{FullMatchData, FullMatchPlayerData};
|
use crate::db::matches::{FullMatchData, FullMatchPlayerData};
|
||||||
use crate::modules::bots::save_code_bundle;
|
use crate::modules::bots::save_code_bundle;
|
||||||
use crate::modules::matches::RunMatch;
|
use crate::modules::matches::{MatchPlayer, RunMatch};
|
||||||
use crate::ConnectionPool;
|
use crate::ConnectionPool;
|
||||||
use axum::extract::Extension;
|
use axum::extract::Extension;
|
||||||
use axum::Json;
|
use axum::Json;
|
||||||
|
@ -46,7 +46,10 @@ pub async fn submit_bot(
|
||||||
// TODO: can we recover from this?
|
// TODO: can we recover from this?
|
||||||
.expect("could not save bot code");
|
.expect("could not save bot code");
|
||||||
|
|
||||||
let mut run_match = RunMatch::from_players(vec![&player_code_bundle, &opponent_code_bundle]);
|
let mut run_match = RunMatch::from_players(vec![
|
||||||
|
MatchPlayer::from_code_bundle(&player_code_bundle),
|
||||||
|
MatchPlayer::from_code_bundle(&opponent_code_bundle),
|
||||||
|
]);
|
||||||
let match_data = run_match
|
let match_data = run_match
|
||||||
.store_in_database(&conn)
|
.store_in_database(&conn)
|
||||||
.expect("failed to save match");
|
.expect("failed to save match");
|
||||||
|
@ -58,12 +61,12 @@ pub async fn submit_bot(
|
||||||
match_players: vec![
|
match_players: vec![
|
||||||
FullMatchPlayerData {
|
FullMatchPlayerData {
|
||||||
base: match_data.match_players[0].clone(),
|
base: match_data.match_players[0].clone(),
|
||||||
code_bundle: player_code_bundle,
|
code_bundle: Some(player_code_bundle),
|
||||||
bot: None,
|
bot: None,
|
||||||
},
|
},
|
||||||
FullMatchPlayerData {
|
FullMatchPlayerData {
|
||||||
base: match_data.match_players[1].clone(),
|
base: match_data.match_players[1].clone(),
|
||||||
code_bundle: opponent_code_bundle,
|
code_bundle: Some(opponent_code_bundle),
|
||||||
bot: Some(opponent),
|
bot: Some(opponent),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -61,7 +61,7 @@ pub async fn play_match(
|
||||||
});
|
});
|
||||||
|
|
||||||
bot_ids.push(matches::MatchPlayerData {
|
bot_ids.push(matches::MatchPlayerData {
|
||||||
code_bundle_id: code_bundle.id,
|
code_bundle_id: Some(code_bundle.id),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,7 +107,7 @@ pub struct ApiMatch {
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct ApiMatchPlayer {
|
pub struct ApiMatchPlayer {
|
||||||
code_bundle_id: i32,
|
code_bundle_id: Option<i32>,
|
||||||
bot_id: Option<i32>,
|
bot_id: Option<i32>,
|
||||||
bot_name: Option<String>,
|
bot_name: Option<String>,
|
||||||
}
|
}
|
||||||
|
@ -127,7 +127,7 @@ pub fn match_data_to_api(data: matches::FullMatchData) -> ApiMatch {
|
||||||
.match_players
|
.match_players
|
||||||
.iter()
|
.iter()
|
||||||
.map(|_p| ApiMatchPlayer {
|
.map(|_p| ApiMatchPlayer {
|
||||||
code_bundle_id: _p.code_bundle.id,
|
code_bundle_id: _p.code_bundle.as_ref().map(|cb| cb.id),
|
||||||
bot_id: _p.bot.as_ref().map(|b| b.id),
|
bot_id: _p.bot.as_ref().map(|b| b.id),
|
||||||
bot_name: _p.bot.as_ref().map(|b| b.name.clone()),
|
bot_name: _p.bot.as_ref().map(|b| b.name.clone()),
|
||||||
})
|
})
|
||||||
|
|
|
@ -31,7 +31,7 @@ table! {
|
||||||
match_players (match_id, player_id) {
|
match_players (match_id, player_id) {
|
||||||
match_id -> Int4,
|
match_id -> Int4,
|
||||||
player_id -> Int4,
|
player_id -> Int4,
|
||||||
code_bundle_id -> Int4,
|
code_bundle_id -> Nullable<Int4>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
36
proto/bot_api.proto
Normal file
36
proto/bot_api.proto
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package grpc.planetwars.bot_api;
|
||||||
|
|
||||||
|
message Hello {
|
||||||
|
string hello_message = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message HelloResponse {
|
||||||
|
string response = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PlayerRequest {
|
||||||
|
int32 request_id = 1;
|
||||||
|
bytes content = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PlayerRequestResponse {
|
||||||
|
int32 request_id = 1;
|
||||||
|
bytes content = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message MatchRequest {
|
||||||
|
string opponent_name = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreatedMatch {
|
||||||
|
int32 match_id = 1;
|
||||||
|
string player_key = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
service BotApiService {
|
||||||
|
rpc CreateMatch(MatchRequest) returns (CreatedMatch);
|
||||||
|
// server sends requests to the player, player responds
|
||||||
|
rpc ConnectBot(stream PlayerRequestResponse) returns (stream PlayerRequest);
|
||||||
|
}
|
Loading…
Reference in a new issue