From 9d3b68f8ef17d4e83118ad6dcab3ae4785858afb Mon Sep 17 00:00:00 2001 From: TristanStreich Date: Fri, 12 Jan 2024 08:27:03 -0800 Subject: [PATCH 01/10] trying channels approach --- Cargo.lock | 85 ++++++++++++++++++++++++++++++++++++++++++-- Cargo.toml | 1 + client/template.lua | 2 +- src/chess_engine.rs | 14 +++++++- src/lib.rs | 1 + src/lua.rs | 78 ++++++++++++++++++++++++++++++++++++++++ src/player_vs_bot.rs | 5 ++- 7 files changed, 179 insertions(+), 7 deletions(-) create mode 100644 src/lua.rs diff --git a/Cargo.lock b/Cargo.lock index a92d319..6d0ee8d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -491,6 +491,16 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "bstr" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c48f0051a4b4c5e0b6d365cd04af53aeaa209e3cc15ec2cdb69e73cc87fbd0dc" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "btoi" version = "0.4.3" @@ -751,6 +761,12 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + [[package]] name = "encoding_rs" version = "0.8.33" @@ -1171,6 +1187,25 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +[[package]] +name = "lua-src" +version = "546.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da0daa7eee611a4c30c8f5ee31af55266e26e573971ba9336d2993e2da129b2" +dependencies = [ + "cc", +] + +[[package]] +name = "luajit-src" +version = "210.5.4+c525bcb" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a10ab4ed12d22cb50ef43ece4f6c5ca594b2d2480019e87facfd422225a9908" +dependencies = [ + "cc", + "which", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -1182,9 +1217,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.6.4" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "mime" @@ -1223,6 +1258,32 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "mlua" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "069264935e816c85884b99e88c8b408d6d92e40ae8760f726c983526a53546b5" +dependencies = [ + "bstr", + "mlua-sys", + "num-traits", + "once_cell", + "rustc-hash", +] + +[[package]] +name = "mlua-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4655631a02e3739d014951291ecfa08db49c4da3f7f8c6f3931ed236af5dd78e" +dependencies = [ + "cc", + "cfg-if", + "lua-src", + "luajit-src", + "pkg-config", +] + [[package]] name = "ndk-context" version = "0.1.1" @@ -1500,6 +1561,12 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc_version" version = "0.4.0" @@ -1671,6 +1738,7 @@ dependencies = [ "futures-util", "handlebars", "log", + "mlua", "rand", "serde", "serde_json", @@ -2109,6 +2177,19 @@ dependencies = [ "web-sys", ] +[[package]] +name = "which" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bf3ea8596f3a0dd5980b46430f2058dfe2c36a27ccfbb1845d6fbfcd9ba6e14" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", + "windows-sys 0.48.0", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 31c8027..1e730eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,3 +27,4 @@ env_logger = "0.10.1" futures-util = "0.3.30" clap = "3.1" webbrowser = "0.8.12" +mlua = { version = "0.9.4", features = ["vendored", "luajit52"] } diff --git a/client/template.lua b/client/template.lua index d4b46f9..78b4a44 100644 --- a/client/template.lua +++ b/client/template.lua @@ -10,5 +10,5 @@ function MyRandomBot:chooseMove(chess_game, legal_moves) return legal_moves[ math.random( #legal_moves ) ] end ----Return must return your bot from the script +---Must return your bot from the script return MyRandomBot \ No newline at end of file diff --git a/src/chess_engine.rs b/src/chess_engine.rs index 7390aaf..11af1df 100644 --- a/src/chess_engine.rs +++ b/src/chess_engine.rs @@ -1,5 +1,5 @@ use rand::Rng; -use shakmaty::{Move, MoveList, Position}; +use shakmaty::{Move, MoveList}; use std::sync::mpsc::Sender; use std::sync::{Arc, RwLock}; use std::thread; @@ -14,6 +14,18 @@ pub trait ChooseMove { fn choose_move(&self, fen: &str, legal_moves: &MoveList) -> Option; } +pub trait IntoChooseMove { + type Bot: ChooseMove; + fn into_choose_move(self) -> Self::Bot; +} + +impl IntoChooseMove for C { + type Bot = C; + fn into_choose_move(self) -> Self::Bot { + self + } +} + pub struct RandomEngine {} impl RandomEngine { diff --git a/src/lib.rs b/src/lib.rs index e5b731d..5d2d3ef 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,3 +5,4 @@ pub mod http_server; pub mod player_vs_bot; pub mod types; pub mod websocket; +pub mod lua; \ No newline at end of file diff --git a/src/lua.rs b/src/lua.rs new file mode 100644 index 0000000..461ae46 --- /dev/null +++ b/src/lua.rs @@ -0,0 +1,78 @@ +use mlua::{Lua, StdLib}; + +use anyhow::Result as AnyResult; + +use shakmaty::MoveList; + +use tokio::sync::mpsc; + + +pub struct LuaBot<'lua> { + vm: Lua, + bot: mlua::Table<'lua>, +} + +/* + * Ideally a LuaBot would look like this + * ```rust + * pub struct LuaBot<'lua> { + * vm: Lua, + * bot: mlua::Table<'lua>, + * } + * ``` + * + * Where we hold the initialized bot and the lua vm that it is running in. + * However this is a self referential struct which is impossible in safe rust. + * + * + * Instead, going to try a hack where we spawn the vm in a tokio task and then communicate with it via a channel + */ + + + +// impl<'lua> LuaBot<'lua> { +// pub fn try_new(script: &str) -> AnyResult{ +// let vm = init_vm(); +// let chunk = vm.load(script); +// let bot = chunk.eval()?; + +// Ok(Self { +// vm, bot +// }) +// } +// } + +fn spawn_vm(script: String, rcv: mpsc::Receiver<()>, sender: mpsc::Sender>) -> AnyResult<()> { + + tokio::spawn(async move { + let vm = init_vm(); + let chunk = vm.load(script); + let bot: mlua::Table = chunk.eval().unwrap(); + + //TODO: validate bot. + // 1. make sure it has chooseMove function and passing in a simple example works. + let vm = vm; + + while let Some(args) = rcv.recv().await { + let chosen_move = choose_move(bot.clone(), "TODO:", &[String::new()]); + + sender.send(chosen_move).await; + } + }); + + Ok(()) +} + +fn choose_move<'lua>(bot: mlua::Table<'lua>, chess_game: &str, legal_moves: &[String]) -> AnyResult { + let choose_move: mlua::Function = bot.get("chooseMove")?; + let chosen_move = choose_move.call((bot, chess_game, legal_moves))?; + Ok(chosen_move) +} + +fn init_vm() -> Lua { + // the Anti-Eamonn policy + let whitelisted_libs = StdLib::MATH & StdLib::TABLE & StdLib::STRING; + // SAFETY: This function only errors if we use StdLib::DEBUG or StdLib::FFI + let lua = Lua::new_with(whitelisted_libs, mlua::LuaOptions::default()).unwrap(); + lua +} \ No newline at end of file diff --git a/src/player_vs_bot.rs b/src/player_vs_bot.rs index 47f5544..c33b82c 100644 --- a/src/player_vs_bot.rs +++ b/src/player_vs_bot.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use log::{error, info}; +use log::error; use shakmaty::Move; use crate::{chess_engine::ChooseMove, chess_game::ChessGame}; @@ -30,8 +30,7 @@ impl PlayerGame { return Ok(None); } - // FIXME: remove unwrap. What does `None` mean for a choose move? it ran out of time? - let bot_move = match self.bot.choose_move(&self.game.fen(), &legal_moves) { + let bot_move = match self.bot.choose_move(&self.game.fen(), legal_moves) { Some(m) => m, None => { // not really sure what we are supposed to do here From ab13c7242f66f8218b758da0bc669ef119877aef Mon Sep 17 00:00:00 2001 From: TristanStreich Date: Sat, 13 Jan 2024 12:06:42 -0800 Subject: [PATCH 02/10] basic bot works --- client/game.html | 1 + client/index.html | 16 +++--- client/template.lua | 26 ++++++++- client/text_editor.html | 79 ++++++++++++++++++++++++-- src/chess_engine.rs | 65 ++++++++++++--------- src/http_server.rs | 121 ++++++++++++++++++++++++++++------------ src/lib.rs | 2 +- src/lua.rs | 116 +++++++++++++++++++------------------- src/player_vs_bot.rs | 36 +++--------- 9 files changed, 298 insertions(+), 164 deletions(-) diff --git a/client/game.html b/client/game.html index cffb98c..d1890a2 100644 --- a/client/game.html +++ b/client/game.html @@ -21,6 +21,7 @@ +

{{bot_name}}

- +
+ + + +
+ \ No newline at end of file diff --git a/src/chess_engine.rs b/src/chess_engine.rs index 11af1df..9f11ff8 100644 --- a/src/chess_engine.rs +++ b/src/chess_engine.rs @@ -5,27 +5,39 @@ use std::sync::{Arc, RwLock}; use std::thread; use std::time::Duration; +use anyhow::Result as AnyResult; + use log::{error, info}; use crate::chess_game::ChessGame; use crate::websocket::Notification; -pub trait ChooseMove { - fn choose_move(&self, fen: &str, legal_moves: &MoveList) -> Option; +pub trait ChooseMove: Send + Sync { + /// Choose move has the assumption that the bot can play a move. Handing it a game with no legal moves + /// is invalid. The user should check if the game is over before calling this + fn choose_move(&self, chess_game: &ChessGame, legal_moves: &MoveList) -> AnyResult; // TODO: use something other than anyhow here plz +} + +/// Allows us to distinguish between a saved bot and an active bot +/// In addition Clone is not allowed for dyn trait objects and this helps us get around that +pub trait ToChooseMove { + fn to_choose_move(&self) -> Box; } -pub trait IntoChooseMove { - type Bot: ChooseMove; - fn into_choose_move(self) -> Self::Bot; +impl ToChooseMove for C { + fn to_choose_move(&self) -> Box { + Box::new(self.clone()) + } } -impl IntoChooseMove for C { - type Bot = C; - fn into_choose_move(self) -> Self::Bot { - self +impl ChooseMove for Box { + fn choose_move(&self, chess_game: &ChessGame, legal_moves: &MoveList) -> AnyResult { + // need to make sure we call the choose_move of the dyn object not the choose_move of the box + (**self).choose_move(chess_game, legal_moves) } } +#[derive(Clone)] pub struct RandomEngine {} impl RandomEngine { @@ -35,15 +47,13 @@ impl RandomEngine { } impl ChooseMove for RandomEngine { - fn choose_move(&self, _chess_game: &str, legal_moves: &MoveList) -> Option { - if legal_moves.is_empty() { - None - } else { - thread::sleep(Duration::from_millis(250)); // Delay for 250 ms - let mut rng = rand::thread_rng(); - let random_index = rng.gen_range(0..legal_moves.len()); - legal_moves.get(random_index).cloned() - } + fn choose_move(&self, _chess_game: &ChessGame, legal_moves: &MoveList) -> AnyResult { + thread::sleep(Duration::from_millis(250)); // Delay for 250 ms + let mut rng = rand::thread_rng(); + let random_index = rng.gen_range(0..legal_moves.len()); + legal_moves.get(random_index).cloned().ok_or_else(|| { + anyhow::anyhow!("Random Bot could not make a move since there are no legal moves") + }) } } @@ -65,15 +75,18 @@ pub fn engine_vs_engine( // Alternate turns between Engine 1 and Engine 2 for engine in [&engine1, &engine2].iter() { - let (legal_moves, fen) = (game.get_legal_moves(), game.fen()); + let legal_moves = game.get_legal_moves(); - if let Some(m) = engine.choose_move(&fen, &legal_moves) { - game.make_move(&m); - send_notification(&sender_channel, game.fen()); - } else { - info!("Game over, other engine wins or stalemate"); - return; - } + let m = match engine.choose_move(&game, &legal_moves) { + Ok(m) => m, + Err(e) => { + error!("Bot threw error while choosing move: {e}"); + return; + } + }; + + game.make_move(&m); + send_notification(&sender_channel, game.fen()); if game.game_over() { return; diff --git a/src/http_server.rs b/src/http_server.rs index b5cd378..3f710b8 100644 --- a/src/http_server.rs +++ b/src/http_server.rs @@ -24,18 +24,24 @@ use uuid::Uuid; use shakmaty::uci::Uci; -use crate::chess_engine; +use crate::chess_engine::{self, RandomEngine, ToChooseMove}; use crate::player_vs_bot::PlayerGame; use crate::websocket::MyWebSocket; -use crate::{chess_engine::engine_vs_engine, chess_game::ChessGame}; +use crate::{chess_engine::engine_vs_engine, chess_game::ChessGame, lua::StatelessLuaBot}; pub type GameMap = DashMap>>; pub type Connection = Addr; pub type SharedState = Arc>>; +// TODO: this is actually incomplete. We will need to store saved bots as more like +// instructions to create a new bot. We need to distinguish between a serialized bot and +// and a deserialized bot. In case the bots are stateful +pub type SavedBots = DashMap>; #[derive(Deserialize, Debug)] struct NewGameArgs { mode: String, + #[serde(rename = "botName")] + bot_name: String, } #[get("/ping")] @@ -45,10 +51,7 @@ async fn ping() -> impl Responder { #[post("/new_game")] async fn new_game( - app_data: web::Data, - active_processes: web::Data>>>>, - active_player_games: web::Data>, - connections: web::Data>, + app_data: web::Data, req_body: Json, ) -> impl Responder { info!("recieved request!"); @@ -58,15 +61,19 @@ async fn new_game( match req_body.mode.as_str() { "playerVsBot" => { // TODO: allow bot id in request body to select bot to play here - let bot = chess_engine::RandomEngine::new(); - let game = PlayerGame::new(bot); + let Some(saved_bot) = app_data.saved_bots.get(&req_body.bot_name) else { + return HttpResponse::BadRequest() + .body(format!("No bot named {} found", &req_body.bot_name)); + }; + let game = PlayerGame::new(saved_bot.to_choose_move(), &req_body.bot_name); info!("Starting Player vs Bot Game: {new_game_id}"); - active_player_games.insert(new_game_id, game); + app_data.active_player_games.insert(new_game_id, game); } "botVsBot" => { let game = Arc::new(RwLock::new(ChessGame::new())); - let engine1 = Arc::new(crate::chess_engine::RandomEngine::new()); - let engine2 = Arc::new(crate::chess_engine::RandomEngine::new()); + // TODO: add option for second bot so player can select which 2 bots should play each other + let engine1 = Arc::new(RandomEngine::new()); + let engine2 = Arc::new(RandomEngine::new()); let game_clone = game.clone(); let engine1_clone = engine1.clone(); @@ -81,7 +88,9 @@ async fn new_game( }); let new_game_connections: SharedState = Arc::new(RwLock::new(Vec::new())); - connections.insert(new_game_id, new_game_connections.clone()); + app_data + .connections + .insert(new_game_id, new_game_connections.clone()); game_join_set.spawn_blocking(move || { loop { let result = match rx.recv() { @@ -101,11 +110,11 @@ async fn new_game( // not doing anything with set right now, but save in case in the future // we want to do some graceful shutdown logic - let mut active_tasks = active_processes.lock().unwrap(); + let mut active_tasks = app_data.active_processes.lock().unwrap(); active_tasks.insert(new_game_id, game_join_set); info!("inserted game {} into active tasks", new_game_id); - app_data.insert(new_game_id, game); + app_data.active_bot_bot_games.insert(new_game_id, game); } _ => { return HttpResponse::BadRequest().body("Invalid game mode"); @@ -119,14 +128,14 @@ async fn new_game( #[get("/spectate/{uuid}")] async fn spectate_game( - app_data: web::Data, + app_data: web::Data, hb: web::Data>, info: web::Path, ) -> impl Responder { let game_uuid = info.into_inner(); // Fetch the game data - let game_data = match app_data.get(&game_uuid) { + let game_data = match app_data.active_bot_bot_games.get(&game_uuid) { Some(game) => game, None => return HttpResponse::NotFound().body("Game not found"), }; @@ -161,10 +170,10 @@ pub async fn ws_index( req: HttpRequest, stream: web::Payload, uuid: web::Path, // Extract UUID from the path - connections: web::Data>, + app_data: web::Data, ) -> Result { info!("New Connection to Game: {}", &uuid); - match connections.get(&uuid) { + match app_data.connections.get(&uuid) { Some(game_conns) => { let game_conns: SharedState = game_conns.clone(); let ws = MyWebSocket::new(game_conns); @@ -202,11 +211,11 @@ struct PlayGameResponse { #[post("/play/{uuid}")] /// Play a given move against a bot pub async fn player_vs_bot( - active_player_games: web::Data>, + app_data: web::Data, req_body: Json, uuid: web::Path, ) -> actix_web::Result> { - let Some(mut game) = active_player_games.get_mut(&uuid) else { + let Some(mut game) = app_data.active_player_games.get_mut(&uuid) else { return Err(actix_web::error::ErrorBadRequest(format!( "No active game for {uuid}" ))); @@ -227,9 +236,9 @@ pub async fn player_vs_bot( match game.play_move(player_move) { Ok(_) => {} Err(e) => { - error!("Error playing move: {}", e); + error!("Error playing move: {e:?}"); return Err(actix_web::error::ErrorBadRequest(format!( - "Error Playing Move {}: {e}", + "Error Playing Move {}: {e:?}", req_body.player_move ))); } @@ -242,11 +251,11 @@ pub async fn player_vs_bot( #[get("/game/{uuid}")] async fn play_game_entry( - active_player_games: web::Data>, + app_data: web::Data, hb: web::Data>, uuid: web::Path, ) -> impl Responder { - let Some(game) = active_player_games.get(&uuid) else { + let Some(game) = app_data.active_player_games.get(&uuid) else { return Err(actix_web::error::ErrorBadRequest(format!( "No active game for {uuid}" ))); @@ -260,7 +269,8 @@ async fn play_game_entry( "game_id": uuid.to_string(), "position": game.fen(), "style": css_content, - "board_js":js_content + "board_js":js_content, + "bot_name": game.bot_name, }); // Render the template with the data @@ -272,16 +282,49 @@ async fn play_game_entry( Ok(HttpResponse::Ok().content_type("text/html").body(body)) } +#[derive(Deserialize)] +struct NewBotRequest { + script: String, + #[serde(rename = "botName")] + bot_name: String, +} + +#[derive(Serialize)] +struct NewBotResponse {} + +#[post("/newBot")] +async fn new_bot( + request: Json, + app_data: web::Data, +) -> actix_web::Result> { + let request = request.0; + let bot = StatelessLuaBot::try_new(request.script) + .map_err(|e| actix_web::error::ErrorBadRequest(format!("Failed to initialize Bot: {e}")))?; + + // TODO: for now this will overwrite other saved bots of the same name. + // I think this is what we want for now. But later we will need to block this and + // then add an ability to edit already saved bots + app_data.saved_bots.insert(request.bot_name, Box::new(bot)); + + Ok(Json(NewBotResponse {})) +} + +pub struct GlobalAppData { + active_processes: Arc>>>, + active_player_games: DashMap, + connections: DashMap, + active_bot_bot_games: GameMap, + saved_bots: SavedBots, +} + pub async fn start_server(hostname: String, port: u16) -> std::io::Result<()> { // Init an empty hashmap to store all the ongoing processes - let active = Arc::new(Mutex::new(HashMap::>::new())); - let active_tasks = web::Data::new(active); + let active_processes = Arc::new(Mutex::new(HashMap::>::new())); - let player_bot_games = web::Data::new(DashMap::::new()); + let active_player_games = DashMap::::new(); // Initialize an empty hashmap which maps UUID to ChessGame - let games: GameMap = DashMap::new(); - let games_data = web::Data::new(games); + let active_bot_bot_games: GameMap = DashMap::new(); let mut handlebars = Handlebars::new(); handlebars @@ -294,7 +337,17 @@ pub async fn start_server(hostname: String, port: u16) -> std::io::Result<()> { // Active Spectator connections let connections: DashMap = DashMap::new(); - let connections_data = web::Data::new(connections); + + let saved_bots: SavedBots = DashMap::new(); + saved_bots.insert(String::from("RustRandomBot"), Box::new(RandomEngine::new())); + + let app_data = web::Data::new(GlobalAppData { + connections, + active_processes, + active_bot_bot_games, + active_player_games, + saved_bots, + }); info!("Starting server on {}:{}", hostname, port); let allowed_origin = format!("http://{}:{}", &hostname, &port); @@ -314,16 +367,14 @@ pub async fn start_server(hostname: String, port: u16) -> std::io::Result<()> { // routes actually does matter here App::new() .wrap(cors) - .app_data(games_data.clone()) // Add the shared state to the app + .app_data(app_data.clone()) // Add the shared state to the app .app_data(handlebars_ref.clone()) - .app_data(active_tasks.clone()) - .app_data(connections_data.clone()) - .app_data(player_bot_games.clone()) .route("/ws/{uuid}", web::get().to(ws_index)) .service(spectate_game) .service(new_game) .service(player_vs_bot) .service(play_game_entry) + .service(new_bot) .service(fs::Files::new("/", "./client/").index_file("index.html")) // .service(fs::Files::new("/img", "./client/img")) .service( diff --git a/src/lib.rs b/src/lib.rs index 5d2d3ef..1bb0e5e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,7 @@ pub mod browser; pub mod chess_engine; pub mod chess_game; pub mod http_server; +pub mod lua; pub mod player_vs_bot; pub mod types; pub mod websocket; -pub mod lua; \ No newline at end of file diff --git a/src/lua.rs b/src/lua.rs index 461ae46..1d10caf 100644 --- a/src/lua.rs +++ b/src/lua.rs @@ -1,69 +1,61 @@ use mlua::{Lua, StdLib}; -use anyhow::Result as AnyResult; +use anyhow::{Context, Result as AnyResult}; -use shakmaty::MoveList; +use shakmaty::{uci::Uci, CastlingMode, MoveList}; -use tokio::sync::mpsc; +use crate::{chess_engine::ChooseMove, chess_game::ChessGame}; - -pub struct LuaBot<'lua> { - vm: Lua, - bot: mlua::Table<'lua>, +/// Simplest implentation of a lua bot is stateless. +/// Each time a move is chosen this script is evaluated by a new lua vm to generate the bot +/// +/// This means that any state in the bot will not be preserved on successive calls to choose_move +#[derive(Clone)] +pub struct StatelessLuaBot { + script: String, } -/* - * Ideally a LuaBot would look like this - * ```rust - * pub struct LuaBot<'lua> { - * vm: Lua, - * bot: mlua::Table<'lua>, - * } - * ``` - * - * Where we hold the initialized bot and the lua vm that it is running in. - * However this is a self referential struct which is impossible in safe rust. - * - * - * Instead, going to try a hack where we spawn the vm in a tokio task and then communicate with it via a channel - */ - - - -// impl<'lua> LuaBot<'lua> { -// pub fn try_new(script: &str) -> AnyResult{ -// let vm = init_vm(); -// let chunk = vm.load(script); -// let bot = chunk.eval()?; - -// Ok(Self { -// vm, bot -// }) -// } -// } +impl StatelessLuaBot { + pub fn try_new(script: String) -> AnyResult { + validate_script(&script)?; + Ok(Self { script }) + } +} -fn spawn_vm(script: String, rcv: mpsc::Receiver<()>, sender: mpsc::Sender>) -> AnyResult<()> { - - tokio::spawn(async move { +impl ChooseMove for StatelessLuaBot { + fn choose_move( + &self, + chess_game: &ChessGame, + legal_moves: &MoveList, + ) -> AnyResult { let vm = init_vm(); - let chunk = vm.load(script); - let bot: mlua::Table = chunk.eval().unwrap(); - - //TODO: validate bot. - // 1. make sure it has chooseMove function and passing in a simple example works. - let vm = vm; - - while let Some(args) = rcv.recv().await { - let chosen_move = choose_move(bot.clone(), "TODO:", &[String::new()]); - - sender.send(chosen_move).await; - } - }); - - Ok(()) + let chunk = vm.load(&self.script); + let bot = chunk + .eval() + .context("Error initializing bot while choosing move")?; + + let uci_legal_moves = legal_moves + .iter() + .map(|legal_move| legal_move.to_uci(CastlingMode::Standard).to_string()) + .collect::>(); + + let chosen_uci: Uci = invoke_bot(bot, &chess_game.fen(), &uci_legal_moves) + .context("Bot made an error while choosing its move")? + .parse() + // TODO: confirm that this error message will include the string the bot returned + .context("Error parsing bot response as uci")?; + + log::debug!("Bot trying to play move: {chosen_uci}"); + + let chosen_move = chosen_uci + .to_move(&chess_game.game) + .context("Bot returned illegal move")?; + + Ok(chosen_move) + } } -fn choose_move<'lua>(bot: mlua::Table<'lua>, chess_game: &str, legal_moves: &[String]) -> AnyResult { +fn invoke_bot(bot: mlua::Table<'_>, chess_game: &str, legal_moves: &[String]) -> AnyResult { let choose_move: mlua::Function = bot.get("chooseMove")?; let chosen_move = choose_move.call((bot, chess_game, legal_moves))?; Ok(chosen_move) @@ -71,8 +63,14 @@ fn choose_move<'lua>(bot: mlua::Table<'lua>, chess_game: &str, legal_moves: &[St fn init_vm() -> Lua { // the Anti-Eamonn policy - let whitelisted_libs = StdLib::MATH & StdLib::TABLE & StdLib::STRING; + let whitelisted_libs = StdLib::MATH | StdLib::TABLE | StdLib::STRING; // SAFETY: This function only errors if we use StdLib::DEBUG or StdLib::FFI - let lua = Lua::new_with(whitelisted_libs, mlua::LuaOptions::default()).unwrap(); - lua -} \ No newline at end of file + Lua::new_with(whitelisted_libs, mlua::LuaOptions::default()).unwrap() +} + +/// TODO: +/// will probably want to try to generate the bot and check that chooseMove exists +/// valide it is a function and try passing it a starting position as a simple dummy check +fn validate_script(_script: &str) -> AnyResult<()> { + Ok(()) +} diff --git a/src/player_vs_bot.rs b/src/player_vs_bot.rs index c33b82c..c3f2f88 100644 --- a/src/player_vs_bot.rs +++ b/src/player_vs_bot.rs @@ -1,19 +1,20 @@ use anyhow::Result; -use log::error; use shakmaty::Move; use crate::{chess_engine::ChooseMove, chess_game::ChessGame}; pub struct PlayerGame { - bot: Box, + bot: Box, pub game: ChessGame, + pub bot_name: String, } impl PlayerGame { - pub fn new(bot: C) -> Self { + pub fn new(bot: C, name: impl ToString) -> Self { Self { bot: Box::new(bot), game: ChessGame::new(), + bot_name: name.to_string(), } } @@ -30,32 +31,9 @@ impl PlayerGame { return Ok(None); } - let bot_move = match self.bot.choose_move(&self.game.fen(), legal_moves) { - Some(m) => m, - None => { - // not really sure what we are supposed to do here - // this is not a mistake by the player its a mistake by the bot - error!( - "Despite the game not being over, - the bot returned None for a move. Game FEN {}. - Defaulting to a random move", - self.game.fen() - ); - - // as mentioned above, the game not being over should - // guarantee that there are legal moves - if legal_moves.is_empty() { - let msg = format!( - "Despite the game not being over There are no legal moves. FEN {}", - self.game.fen() - ); - error!("{}", msg); - return Err(anyhow::anyhow!(msg)); - } - - legal_moves[0].clone() - } - }; + // Going to propagate error here from bot. For lua bots we have to assume that + // bugs are common and we want to propogate them to the client so bot creators can debug + let bot_move = self.bot.choose_move(&self.game, legal_moves)?; self.game.make_move(&bot_move); From 4d12a1072616185551746970a7f1a0e07b7de2ea Mon Sep 17 00:00:00 2001 From: TristanStreich Date: Sat, 13 Jan 2024 13:15:08 -0800 Subject: [PATCH 03/10] edit bot end point --- client/index.html | 2 +- client/text_editor.html | 5 ++-- src/http_server.rs | 54 ++++++++++++++++++++++++++++++++++++++++- src/lua.rs | 2 +- 4 files changed, 58 insertions(+), 5 deletions(-) diff --git a/client/index.html b/client/index.html index 07bedb4..a34bd7c 100644 --- a/client/index.html +++ b/client/index.html @@ -41,7 +41,7 @@ $(document).ready(function() { $("#createBot").click(function(event) { - window.location.href = "/text_editor.html"; + window.location.href = "/editBot"; }); }); diff --git a/client/text_editor.html b/client/text_editor.html index 6464720..3b53b71 100644 --- a/client/text_editor.html +++ b/client/text_editor.html @@ -24,18 +24,19 @@
- +
+

{{bot_name}}

+ + @@ -46,12 +112,12 @@

{{bot_name}}

// Called when you let go of the piece async function onDrop( - draggedPieceSource, - draggedPieceDest, - draggedPiece, - newPosition, - oldPosition, - currentOrientation + draggedPieceSource, + draggedPieceDest, + draggedPiece, + newPosition, + oldPosition, + currentOrientation ) { if (draggedPieceSource === draggedPieceDest) { return; @@ -88,6 +154,14 @@

{{bot_name}}

} else { console.error('Error Sending Request:', response.statusText); console.error('Full Response', response); + if (response.status == 400) { + // here the player made an invalide move. + // We will just return and reset the board to the original state + return; + } + // otherwise the bot made an error. + // We will give them a pop up and allow them to go fix the bot + errorPopUp(await response.text()) } } @@ -101,6 +175,32 @@

{{bot_name}}

board = Chessboard('board1', config); + + var modal = document.getElementById("errorPopup"); + + function errorPopUp(msg) { + document.getElementById("errorText").textContent = msg; + + // active popup + modal.style.display = "block"; + } + + // Get the element that closes the modal + var span = document.getElementsByClassName("close")[0]; + // When the user clicks on (x), close the modal + span.onclick = function () { + modal.style.display = "none"; + } + + // When the user clicks anywhere outside of the modal, close it + window.onclick = function (event) { + if (event.target == modal) { + modal.style.display = "none"; + } + } + + - + + \ No newline at end of file diff --git a/src/http_server.rs b/src/http_server.rs index 30d5331..cb56f50 100644 --- a/src/http_server.rs +++ b/src/http_server.rs @@ -238,7 +238,7 @@ pub async fn player_vs_bot( Ok(_) => {} Err(e) => { error!("Error playing move: {e:?}"); - return Err(actix_web::error::ErrorBadRequest(format!( + return Err(actix_web::error::ErrorInternalServerError(format!( "Error Playing Move {}: {e:?}", req_body.player_move ))); From 89d20240eaa72d598dfeba07e83d13c21a324c5b Mon Sep 17 00:00:00 2001 From: TristanStreich Date: Sat, 13 Jan 2024 13:47:36 -0800 Subject: [PATCH 05/10] center popup --- client/game.html | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/client/game.html b/client/game.html index 70f5f5e..1c2c1a7 100644 --- a/client/game.html +++ b/client/game.html @@ -33,6 +33,10 @@ padding: 20px; border: 1px solid #888; width: 80%; + display: flex; + flex-direction: column; + align-items: center; /* Aligns items horizontally in the center */ + justify-content: center; /* Could be more or less, depending on screen size */ } @@ -83,7 +87,6 @@

{{bot_name}}

@@ -185,13 +188,6 @@

{{bot_name}}

modal.style.display = "block"; } - // Get the element that closes the modal - var span = document.getElementsByClassName("close")[0]; - // When the user clicks on (x), close the modal - span.onclick = function () { - modal.style.display = "none"; - } - // When the user clicks anywhere outside of the modal, close it window.onclick = function (event) { if (event.target == modal) { From 096416b38f39bb678020a1f834bdbfb8ecbe02d7 Mon Sep 17 00:00:00 2001 From: TristanStreich Date: Sat, 13 Jan 2024 14:35:30 -0800 Subject: [PATCH 06/10] edit bot section --- client/choose_bot.html | 38 +++++++++++++ client/choose_bot_edit.html | 38 +++++++++++++ client/css/botButtons.css | 10 ++++ client/game.html | 106 ++++++++++++++++++------------------ client/index.html | 52 ++---------------- client/js/utils.js | 43 +++++++++++++++ client/spectate.html | 4 +- client/text_editor.html | 34 +----------- src/http_server.rs | 25 ++++++--- 9 files changed, 205 insertions(+), 145 deletions(-) create mode 100644 client/choose_bot.html create mode 100644 client/choose_bot_edit.html create mode 100644 client/css/botButtons.css create mode 100644 client/js/utils.js diff --git a/client/choose_bot.html b/client/choose_bot.html new file mode 100644 index 0000000..6552f39 --- /dev/null +++ b/client/choose_bot.html @@ -0,0 +1,38 @@ + + + + + + + Choose Which Bot To Play + + + +
+ + + + + \ No newline at end of file diff --git a/client/choose_bot_edit.html b/client/choose_bot_edit.html new file mode 100644 index 0000000..8c470f1 --- /dev/null +++ b/client/choose_bot_edit.html @@ -0,0 +1,38 @@ + + + + + + + Choose Which Bot To Edit + + + +
+ + + + \ No newline at end of file diff --git a/client/css/botButtons.css b/client/css/botButtons.css new file mode 100644 index 0000000..6eac668 --- /dev/null +++ b/client/css/botButtons.css @@ -0,0 +1,10 @@ +#botButtons { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; /* This makes the div take the full viewport height */ +} +button { + margin: 10px; /* Adds spacing between buttons */ +} \ No newline at end of file diff --git a/client/game.html b/client/game.html index 1c2c1a7..53b47d7 100644 --- a/client/game.html +++ b/client/game.html @@ -1,62 +1,62 @@ + - Chess + @@ -92,9 +93,6 @@

{{bot_name}}

- + diff --git a/client/js/utils.js b/client/js/utils.js new file mode 100644 index 0000000..c12b225 --- /dev/null +++ b/client/js/utils.js @@ -0,0 +1,43 @@ + +// Start a new game +// mode: 'playerVsBot' | 'botVsBot' +// botName string +async function startGame(mode, botName) { + var body = { + mode, + botName + }; + console.log("input", JSON.stringify(body)); + var response = await fetch('/new_game', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(body) + }); + + console.log("received response"); + + if (response.ok) { + const data = await response.json(); + const game_id = data.game_id; + + if (mode === 'playerVsBot') { + window.location.href = `/game/${game_id}`; + } else if (mode === 'botVsBot') { + console.log(`/spectate/${game_id}`); + window.location.href = `/spectate/${game_id}`; + } + } else { + console.error('Failed to fetch:', response.statusText); + } +} + + +function startBotGame(botName) { + startGame('botVsBot', botName) +} + +function startPlayerGame(botName) { + startGame('playerVsBot', botName) +} \ No newline at end of file diff --git a/client/spectate.html b/client/spectate.html index 604e97e..add44b4 100644 --- a/client/spectate.html +++ b/client/spectate.html @@ -19,12 +19,10 @@ } +
- +
@@ -69,39 +70,6 @@ }); }); - - // lifted this straight from index.html. What is the best way for these html files to share code? - // do we just write these in .js files and include them with diff --git a/src/http_server.rs b/src/http_server.rs index cb56f50..17b0024 100644 --- a/src/http_server.rs +++ b/src/http_server.rs @@ -24,7 +24,7 @@ use uuid::Uuid; use shakmaty::uci::Uci; -use crate::chess_engine::{self, RandomEngine, ToChooseMove}; +use crate::chess_engine::{RandomEngine, ToChooseMove}; use crate::player_vs_bot::PlayerGame; use crate::websocket::MyWebSocket; use crate::{chess_engine::engine_vs_engine, chess_game::ChessGame, lua::StatelessLuaBot}; @@ -32,9 +32,6 @@ use crate::{chess_engine::engine_vs_engine, chess_game::ChessGame, lua::Stateles pub type GameMap = DashMap>>; pub type Connection = Addr; pub type SharedState = Arc>>; -// TODO: this is actually incomplete. We will need to store saved bots as more like -// instructions to create a new bot. We need to distinguish between a serialized bot and -// and a deserialized bot. In case the bots are stateful pub type SavedBots = DashMap>; pub type SavedLuaBots = DashMap; @@ -148,14 +145,12 @@ async fn spectate_game( let position = gd_lock.fen(); let css_content = std::fs::read_to_string("./client/css/chessboard-1.0.0.min.css").unwrap(); - let js_content = std::fs::read_to_string("./client/js/chessboard-1.0.0.js").unwrap(); // Create data to fill the template let data = json!({ "game_id": game_uuid.to_string(), "position": position, "style": css_content, - "board_js":js_content }); // Render the template with the data @@ -263,14 +258,12 @@ async fn play_game_entry( }; let css_content = std::fs::read_to_string("./client/css/chessboard-1.0.0.min.css").unwrap(); - let js_content = std::fs::read_to_string("./client/js/chessboard-1.0.0.js").unwrap(); // Create data to fill the template let data = json!({ "game_id": uuid.to_string(), "position": game.fen(), "style": css_content, - "board_js":js_content, "bot_name": game.bot_name, }); @@ -352,6 +345,21 @@ async fn get_script( script } +#[derive(Serialize)] +struct BotNamesResponse { + bot_names: Vec, +} + +#[get("/botNames")] +async fn get_bots(app_data: web::Data) -> Json { + let bot_names = app_data + .saved_bots + .iter() + .map(|entry| entry.key().to_string()) + .collect(); + Json(BotNamesResponse { bot_names }) +} + pub struct GlobalAppData { active_processes: Arc>>>, active_player_games: DashMap, @@ -426,6 +434,7 @@ pub async fn start_server(hostname: String, port: u16) -> std::io::Result<()> { .service(play_game_entry) .service(new_bot) .service(get_script) + .service(get_bots) .service(web::resource(["/editBot", "/editBot/{bot_name}"]).to(edit_bot)) .service(fs::Files::new("/", "./client/").index_file("index.html")) // .service(fs::Files::new("/img", "./client/img")) From e60539bca663b523776a2d8d65e57fce6b60c3e1 Mon Sep 17 00:00:00 2001 From: TristanStreich Date: Sat, 13 Jan 2024 14:47:31 -0800 Subject: [PATCH 07/10] cleanup --- client/game.html | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/game.html b/client/game.html index 53b47d7..13b40de 100644 --- a/client/game.html +++ b/client/game.html @@ -1,8 +1,6 @@ - - + +