diff --git a/build.sbt b/build.sbt index 082ad11532..f608292152 100644 --- a/build.sbt +++ b/build.sbt @@ -61,8 +61,6 @@ lazy val `vm-hello-world2-2018` = (project in file("vm/examples/hello-world2/app ) lazy val `vm-hello-world2-runner` = (project in file("vm/examples/hello-world2/runner")) - .configs(IntegrationTest) - .settings(inConfig(IntegrationTest)(Defaults.itSettings): _*) .settings( commons, libraryDependencies ++= Seq( @@ -82,6 +80,25 @@ lazy val `vm-llamadb` = (project in file("vm/examples/llamadb")) rustVmExample("llamadb") ) +lazy val `tic-tac-toe` = (project in file("vm/examples/tic-tac-toe/app")) + .settings( + rustVmExample("tic-tac-toe/app") + ) + +lazy val `tic-tac-toe-runner` = (project in file("vm/examples/tic-tac-toe/runner")) + .settings( + commons, + libraryDependencies ++= Seq( + asmble, + cats, + catsEffect, + pureConfig, + cryptoHashing, + ) + ) + .dependsOn(vm, `tic-tac-toe`) + .enablePlugins(AutomateHeaderPlugin) + lazy val `statemachine-control` = (project in file("statemachine/control")) .settings( commons, diff --git a/vm/examples/tic-tac-toe/app/Cargo.lock b/vm/examples/tic-tac-toe/app/Cargo.lock new file mode 100644 index 0000000000..60b958eae0 --- /dev/null +++ b/vm/examples/tic-tac-toe/app/Cargo.lock @@ -0,0 +1,237 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "arraydeque" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "boolinator" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "cfg-if" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "chrono" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "fluence" +version = "0.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "fluence-sdk-macro 0.0.10 (registry+https://github.com/rust-lang/crates.io-index)", + "fluence-sdk-main 0.0.10 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.15.26 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "fluence-sdk-macro" +version = "0.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.15.26 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "fluence-sdk-main" +version = "0.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.15.26 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "itoa" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "libc" +version = "0.2.48" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "log" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-integer" +version = "0.1.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-traits" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "proc-macro2" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "quote" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "redox_syscall" +version = "0.1.51" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "ryu" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "serde" +version = "1.0.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "serde_derive 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "serde_derive" +version = "1.0.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.15.26 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "serde_json" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", + "ryu 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "simple_logger" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "syn" +version = "0.15.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tic-tac-toe" +version = "0.1.0" +dependencies = [ + "arraydeque 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", + "boolinator 2.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "fluence 0.0.10 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)", + "simple_logger 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "time" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_syscall 0.1.51 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "unicode-xid" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[metadata] +"checksum arraydeque 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e300327073b806ffc81fccb228b2d4131ac7ef1b1a015f7b0c399c7f886cacc6" +"checksum boolinator 2.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cfa8873f51c92e232f9bac4065cddef41b714152812bfc5f7672ba16d6ef8cd9" +"checksum cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "082bb9b28e00d3c9d39cc03e64ce4cea0f1bb9b3fde493f0cbc008472d22bdf4" +"checksum chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "45912881121cb26fad7c38c17ba7daa18764771836b34fab7d3fbd93ed633878" +"checksum fluence 0.0.10 (registry+https://github.com/rust-lang/crates.io-index)" = "274f5f35e26995b091ebe9cfb0325136cc73ef1a7e1ba5db804ff949bcc7bbdc" +"checksum fluence-sdk-macro 0.0.10 (registry+https://github.com/rust-lang/crates.io-index)" = "fd54fe59b655bb070a8359f7711db3387634202e4102baa7e5af6c5df725905e" +"checksum fluence-sdk-main 0.0.10 (registry+https://github.com/rust-lang/crates.io-index)" = "a0d407ba6335165eab7cf5422fd0e9e1d28d5a7ce907b1f7bc62f4f7cd313eaa" +"checksum itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "1306f3464951f30e30d12373d31c79fbd52d236e5e896fd92f96ec7babbbe60b" +"checksum libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)" = "e962c7641008ac010fa60a7dfdc1712449f29c44ef2d4702394aea943ee75047" +"checksum log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c84ec4b527950aa83a329754b01dbe3f58361d1c5efacd1f6d68c494d08a17c6" +"checksum num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)" = "e83d528d2677f0518c570baf2b7abdcf0cd2d248860b68507bdcb3e91d4c0cea" +"checksum num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0b3a5d7cc97d6d30d8b9bc8fa19bf45349ffe46241e8816f50f62f6d6aaabee1" +"checksum proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)" = "4d317f9caece796be1980837fd5cb3dfec5613ebdb04ad0956deea83ce168915" +"checksum quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)" = "cdd8e04bd9c52e0342b406469d494fcb033be4bdbe5c606016defbb1681411e1" +"checksum redox_syscall 0.1.51 (registry+https://github.com/rust-lang/crates.io-index)" = "423e376fffca3dfa06c9e9790a9ccd282fafb3cc6e6397d01dbf64f9bacc6b85" +"checksum ryu 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "eb9e9b8cde282a9fe6a42dd4681319bfb63f121b8a8ee9439c6f4107e58a46f7" +"checksum serde 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)" = "9f301d728f2b94c9a7691c90f07b0b4e8a4517181d9461be94c04bddeb4bd850" +"checksum serde_derive 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)" = "beed18e6f5175aef3ba670e57c60ef3b1b74d250d962a26604bff4c80e970dd4" +"checksum serde_json 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)" = "27dce848e7467aa0e2fcaf0a413641499c0b745452aaca1194d24dedde9e13c9" +"checksum simple_logger 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "25111f1d77db1ac3ee11b62ba4b7a162e6bb3be43e28273f0d3935cc8d3ff7fb" +"checksum syn 0.15.26 (registry+https://github.com/rust-lang/crates.io-index)" = "f92e629aa1d9c827b2bb8297046c1ccffc57c99b947a680d3ccff1f136a3bee9" +"checksum time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f" +"checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" +"checksum winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "92c1eb33641e276cfa214a0522acad57be5c56b10cb348b3c5117db75f3ac4b0" +"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/vm/examples/tic-tac-toe/app/Cargo.toml b/vm/examples/tic-tac-toe/app/Cargo.toml new file mode 100644 index 0000000000..ebcd11e439 --- /dev/null +++ b/vm/examples/tic-tac-toe/app/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "tic-tac-toe" +version = "0.1.0" +authors = ["Fluence Labs"] +publish = false +description = "A tic-tac-toe example for the Fluence network" +edition = "2018" + +[lib] +name = "tic_tac_toe" +path = "src/lib.rs" +crate-type = ["cdylib"] + +[profile.release] +debug = false +lto = true +opt-level = "z" +panic = "abort" + +[dependencies] +log = "0.4" +arraydeque = "0.4.3" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0.38" +boolinator = "2.4.0" +fluence = { version = "0.0.10", features = ["export_allocator", "wasm_logger"] } + +#[dev-dependencies] +simple_logger = "1.0.1" diff --git a/vm/examples/tic-tac-toe/app/rust-toolchain b/vm/examples/tic-tac-toe/app/rust-toolchain new file mode 100644 index 0000000000..bf867e0ae5 --- /dev/null +++ b/vm/examples/tic-tac-toe/app/rust-toolchain @@ -0,0 +1 @@ +nightly diff --git a/vm/examples/tic-tac-toe/app/src/error_type.rs b/vm/examples/tic-tac-toe/app/src/error_type.rs new file mode 100644 index 0000000000..c5067d2c8f --- /dev/null +++ b/vm/examples/tic-tac-toe/app/src/error_type.rs @@ -0,0 +1,19 @@ +/* + * Copyright 2018 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use std::error::Error; + +pub type AppResult = ::std::result::Result>; diff --git a/vm/examples/tic-tac-toe/app/src/game.rs b/vm/examples/tic-tac-toe/app/src/game.rs new file mode 100644 index 0000000000..8728c619bb --- /dev/null +++ b/vm/examples/tic-tac-toe/app/src/game.rs @@ -0,0 +1,217 @@ +/* + * Copyright 2018 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use boolinator::Boolinator; +use std::convert::From; +use std::{fmt, result::Result}; + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum Tile { + X, + O, +} + +impl Tile { + pub fn from_char(ch: char) -> Option { + match ch { + 'X' => Some(Tile::X), + 'O' => Some(Tile::O), + _ => None, + } + } + + pub fn to_char(self) -> char { + match self { + Tile::X => 'X', + Tile::O => 'O', + } + } + + // returns tile type of opposite player + pub fn other(self) -> Self { + match self { + Tile::X => Tile::O, + Tile::O => Tile::X, + } + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum Winner { + X, + O, + Draw, +} + +impl fmt::Display for Winner { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + let winner_as_str = match self { + Winner::X => "X", + Winner::O => "O", + Winner::Draw => "Draw", + }; + fmt.write_str(winner_as_str)?; + Ok(()) + } +} + +impl From for Winner { + fn from(tile: Tile) -> Self { + match tile { + Tile::X => Winner::X, + Tile::O => Winner::O, + } + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct GameMove { + pub x: usize, + pub y: usize, +} + +impl GameMove { + pub fn new(x: usize, y: usize) -> Option { + fn is_valid(x: usize, y: usize) -> bool { + x <= 2 || y <= 2 + } + + is_valid(x, y).as_some(GameMove { x, y }) + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct Game { + board: [[Option; 3]; 3], + player_tile: Tile, +} + +impl Game { + pub fn new(player_tile: Tile) -> Self { + Game { + board: [[None; 3]; 3], + player_tile, + } + } + + /// Returns Some(Winner) if there is some and None otherwise. + pub fn get_winner(&self) -> Option { + fn same_row(game: &Game) -> Option { + for col in 0..2 { + if game.board[0][col].is_some() + && (game.board[0][col] == game.board[1][col]) + && (game.board[1][col] == game.board[2][col]) + { + return game.board[0][col].map(|tile| tile.into()); + } + } + None + } + + fn same_col(game: &Game) -> Option { + for row in 0..2 { + if game.board[row][0].is_some() + && (game.board[row][0] == game.board[row][1]) + && (game.board[row][1] == game.board[row][2]) + { + return game.board[row][0].map(|tile| tile.into()); + } + } + None + } + + // checks the left-right diagonal + fn same_main_diag(game: &Game) -> Option { + (game.board[0][0].is_some() + && (game.board[0][0] == game.board[1][1]) + && (game.board[1][1] == game.board[2][2])) + .and_option(game.board[0][0].map(|tile| tile.into())) + } + + // checks the right-left diagonal + fn same_anti_diag(game: &Game) -> Option { + (game.board[0][2].is_some() + && (game.board[0][2] == game.board[1][1]) + && (game.board[1][1] == game.board[2][0])) + .and_option(game.board[0][2].map(|tile| tile.into())) + } + + // checks that all tiles are empty (a draw condition) + fn no_empty(game: &Game) -> Option { + game.board + .iter() + .all(|row| row.iter().all(|cell| cell.is_some())) + .as_some(Winner::Draw) + } + + same_row(self) + .or_else(|| same_col(self)) + .or_else(|| same_main_diag(self)) + .or_else(|| same_anti_diag(self)) + .or_else(|| no_empty(self)) + } + + /// Makes player and application moves successively. Returns Some() of with coords of app move + /// if it was successfull and None otherwise. None result means a draw or win of the player. + pub fn player_move(&mut self, game_move: GameMove) -> Result, String> { + if let Some(player) = self.get_winner() { + return Err(format!("Player {} has already won this game", player)); + } + + self.board[game_move.x][game_move.y] + .is_none() + .ok_or_else(|| "Please choose a free position".to_owned())?; + + self.board[game_move.x][game_move.y].replace(self.player_tile); + + Ok(self.app_move()) + } + + /// Returns current game state as a tuple with players tile and board. + pub fn get_state(&self) -> (Tile, Vec) { + let mut board: Vec = Vec::new(); + + for tile in self.board.iter().flat_map(|r| r.iter()) { + match tile { + Some(tile) => board.push(tile.to_char()), + None => board.push('_'), + } + } + + (self.player_tile, board) + } + + /// Makes application move. Returns Some() of with coords of app move if it was successfull and + /// None otherwise. None result means a draw or win of the app. + pub fn app_move(&mut self) -> Option { + if self.get_winner().is_some() { + return None; + } + + // TODO: use more complicated strategy + for (x, row) in self.board.iter_mut().enumerate() { + for (y, tile) in row.iter_mut().enumerate() { + if tile.is_some() { + continue; + } + tile.replace(self.player_tile.other()); + return Some(GameMove::new(x, y).unwrap()); + } + } + + None + } +} diff --git a/vm/examples/tic-tac-toe/app/src/game_manager.rs b/vm/examples/tic-tac-toe/app/src/game_manager.rs new file mode 100644 index 0000000000..ec7a644136 --- /dev/null +++ b/vm/examples/tic-tac-toe/app/src/game_manager.rs @@ -0,0 +1,213 @@ +/* + * Copyright 2018 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use crate::error_type::AppResult; +use crate::game::{Game, GameMove, Tile}; +use crate::json_parser::{ + CreateGameResponse, CreatePlayerResponse, GetGameStateResponse, GetStatisticsResponse, + MoveResponse, +}; +use crate::player::Player; + +use crate::settings::{GAMES_MAX_COUNT, PLAYERS_MAX_COUNT}; +use arraydeque::{ArrayDeque, Wrapping}; +use serde_json::Value; +use std::{cell::RefCell, collections::HashMap, ops::AddAssign, rc::Rc, rc::Weak}; + +pub struct GameStatistics { + // overall players count that has been registered + pub players_created: u64, + // overall players count that has been created + pub games_created: u64, + // overall move count that has been made + pub moves_count: i64, +} + +pub struct GameManager { + players: ArrayDeque<[Rc>; PLAYERS_MAX_COUNT], Wrapping>, + games: ArrayDeque<[Rc>; GAMES_MAX_COUNT], Wrapping>, + // TODO: String key should be replaced with Cow<'a, str>. After that signatures of all public + // functions also should be changed similar to https://jwilm.io/blog/from-str-to-cow/. + players_by_name: HashMap>>, + game_statistics: RefCell, +} + +impl GameManager { + pub fn new() -> Self { + GameManager { + players: ArrayDeque::new(), + games: ArrayDeque::new(), + players_by_name: HashMap::new(), + game_statistics: RefCell::new(GameStatistics { + players_created: 0, + games_created: 0, + moves_count: 0, + }), + } + } + + /// Marks an empty position on the board by user's tile type. Returns MoveResponse structure + /// as a serde_json Value. + pub fn make_move(&self, player_name: String, coords: (usize, usize)) -> AppResult { + let game = self.get_player_game(&player_name)?; + let mut game = game.borrow_mut(); + let game_move = GameMove::new(coords.0, coords.1) + .ok_or_else(|| format!("Invalid coordinates: x = {} y = {}", coords.0, coords.1))?; + + let response = match game.player_move(game_move)? { + Some(app_move) => { + // checks did the app win in this turn? + let winner = match game.get_winner() { + Some(winner) => winner.to_string(), + None => "None".to_owned(), + }; + MoveResponse { + player_name, + winner, + coords: (app_move.x, app_move.y), + } + } + // none means a win of the player or a draw + None => MoveResponse { + player_name, + winner: game.get_winner().unwrap().to_string(), + coords: (std::usize::MAX, std::usize::MAX), + }, + }; + + self.game_statistics.borrow_mut().moves_count.add_assign(1); + + serde_json::to_value(response).map_err(Into::into) + } + + /// Creates a new player with given player name. + pub fn create_player(&mut self, player_name: String) -> AppResult { + let new_player = Rc::new(RefCell::new(Player::new(player_name.clone()))); + if let Some(_) = self.players_by_name.get(&player_name) { + return Err( + "User with this name is already registered, please choose another one".to_owned(), + ) + .map_err(Into::into); + } + + self.players_by_name + .insert(new_player.borrow().name.clone(), Rc::downgrade(&new_player)); + + if let Some(prev) = self.players.push_back(new_player) { + // if some elements poped from the deque, delete a corresponding weak link from + // names_to_players + self.players_by_name.remove(&prev.borrow().name); + } + + let response = CreatePlayerResponse { + player_name, + result: "A new player has been successfully created".to_owned(), + }; + + self.game_statistics + .borrow_mut() + .players_created + .add_assign(1); + + serde_json::to_value(response).map_err(Into::into) + } + + /// Creates a new game for provided player. Note that the previous one is deleted (if it + /// present) and won't be accessed anymore. Returns CreateGameResponse as a serde_json Value if + /// 'X' tile type has been chosen and MoveResponse otherwise. + pub fn create_game(&mut self, player_name: String, player_tile: Tile) -> AppResult { + let player = self.get_player(&player_name)?; + + let game_state = Rc::new(RefCell::new(Game::new(player_tile))); + player.borrow_mut().game = Rc::downgrade(&game_state); + + let response = match player_tile { + Tile::X => { + let response = CreateGameResponse { + player_name, + result: "A new game has been successfully created".to_owned(), + }; + serde_json::to_value(response) + } + // if the user chose 'O' tile the app should move first + Tile::O => { + let app_move = game_state.borrow_mut().app_move().unwrap(); + let response = MoveResponse { + player_name, + winner: "None".to_owned(), + coords: (app_move.x, app_move.y), + }; + serde_json::to_value(response) + } + }; + + self.game_statistics + .borrow_mut() + .games_created + .add_assign(1); + self.games.push_back(game_state); + + response.map_err(Into::into) + } + + /// Returns current game state for provided user as a GetGameStateResponse serde_json Value. + pub fn get_game_state(&self, player_name: String) -> AppResult { + let game = self.get_player_game(&player_name)?; + let (chosen_tile, board) = game.borrow().get_state(); + + let response = GetGameStateResponse { + player_name, + player_tile: chosen_tile.to_char(), + board, + }; + serde_json::to_value(response).map_err(Into::into) + } + + /// Returns statistics of application usage. + pub fn get_statistics(&self) -> AppResult { + let response = GetStatisticsResponse { + players_created: self.game_statistics.borrow().players_created, + games_created: self.game_statistics.borrow().games_created, + moves_count: self.game_statistics.borrow().moves_count, + }; + serde_json::to_value(response).map_err(Into::into) + } + + fn get_player(&self, player_name: &str) -> AppResult>> { + // try to find player by name in players_by_name and then convert Weak to Rc + match self.players_by_name.get(&player_name.to_owned()) { + Some(player) => player.upgrade().ok_or_else(|| { + "Internal error occurred - player has been already removed".to_owned() + }), + None => Err(format!("Player with name {} wasn't found", player_name)), + } + .map_err(Into::into) + } + + fn get_player_game(&self, player_name: &str) -> AppResult>> { + self + // returns Rc> if success + .get_player(player_name)? + // borrows a mutable link to Player from RefCell + .borrow_mut() + // gets Weak> from Player + .game + // tries to upgrade Weak> to Rc> + .upgrade() + .ok_or_else(|| "Sorry! Your game was deleted, but you can start a new one".to_owned()) + .map_err(Into::into) + } +} diff --git a/vm/examples/tic-tac-toe/app/src/json_parser.rs b/vm/examples/tic-tac-toe/app/src/json_parser.rs new file mode 100644 index 0000000000..932241fffa --- /dev/null +++ b/vm/examples/tic-tac-toe/app/src/json_parser.rs @@ -0,0 +1,72 @@ +/* + * Copyright 2018 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +pub struct Request { + pub action: String, + pub player_name: String, +} + +#[derive(Serialize, Deserialize)] +pub struct PlayerTile { + pub tile: char, +} + +#[derive(Serialize, Deserialize)] +pub struct PlayerMove { + pub coords: (usize, usize), +} + +#[derive(Serialize, Deserialize)] +pub struct ErrorResponse { + pub player_name: String, + pub error: String, +} + +#[derive(Serialize, Deserialize)] +pub struct MoveResponse { + pub player_name: String, + pub winner: String, + pub coords: (usize, usize), +} + +#[derive(Serialize, Deserialize)] +pub struct CreatePlayerResponse { + pub player_name: String, + pub result: String, +} + +#[derive(Serialize, Deserialize)] +pub struct CreateGameResponse { + pub player_name: String, + pub result: String, +} + +#[derive(Serialize, Deserialize)] +pub struct GetGameStateResponse { + pub player_name: String, + pub player_tile: char, + pub board: Vec, +} + +#[derive(Serialize, Deserialize)] +pub struct GetStatisticsResponse { + pub players_created: u64, + pub games_created: u64, + pub moves_count: i64, +} diff --git a/vm/examples/tic-tac-toe/app/src/lib.rs b/vm/examples/tic-tac-toe/app/src/lib.rs new file mode 100644 index 0000000000..cb13fc7dc4 --- /dev/null +++ b/vm/examples/tic-tac-toe/app/src/lib.rs @@ -0,0 +1,89 @@ +/* + * Copyright 2018 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#[cfg(test)] +mod tests; + +mod error_type; +mod game; +mod game_manager; +mod json_parser; +mod player; + +use crate::error_type::AppResult; +use crate::game_manager::GameManager; +use crate::json_parser::*; + +use fluence::sdk::*; +use serde_json::{json, Value}; +use std::cell::RefCell; + +mod settings { + pub const PLAYERS_MAX_COUNT: usize = 1024; + pub const GAMES_MAX_COUNT: usize = 1024; +} + +thread_local! { + static GAME_MANAGER: RefCell = RefCell::new(GameManager::new()); +} + +fn do_request(req: String) -> AppResult { + let raw_request: Value = serde_json::from_str(req.as_str())?; + let request: Request = serde_json::from_value(raw_request.clone())?; + + match request.action.as_str() { + "move" => { + let player_move: PlayerMove = serde_json::from_value(raw_request)?; + GAME_MANAGER.with(|gm| { + gm.borrow() + .make_move(request.player_name, player_move.coords) + }) + } + + "create_player" => { + GAME_MANAGER.with(|gm| gm.borrow_mut().create_player(request.player_name)) + } + + "create_game" => { + let player_tile: PlayerTile = serde_json::from_value(raw_request)?; + let player_tile = game::Tile::from_char(player_tile.tile).ok_or_else(|| { + "incorrect tile type, please choose it from {'X', 'O'} set".to_owned() + })?; + + GAME_MANAGER.with(|gm| { + gm.borrow_mut() + .create_game(request.player_name, player_tile) + }) + } + + "get_game_state" => GAME_MANAGER.with(|gm| gm.borrow().get_game_state(request.player_name)), + + "get_statistics" => GAME_MANAGER.with(|gm| gm.borrow().get_statistics()), + + _ => Err(format!("{} action key is unsupported", request.action)).map_err(Into::into), + } +} + +#[invocation_handler] +fn main(req: String) -> String { + match do_request(req) { + Ok(req) => req.to_string(), + Err(err) => json!({ + "error": err.to_string() + }) + .to_string(), + } +} diff --git a/vm/examples/tic-tac-toe/app/src/player.rs b/vm/examples/tic-tac-toe/app/src/player.rs new file mode 100644 index 0000000000..7f9255fc4c --- /dev/null +++ b/vm/examples/tic-tac-toe/app/src/player.rs @@ -0,0 +1,36 @@ +/* + * Copyright 2018 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use crate::game::Game; +use std::{cell::RefCell, rc::Weak}; + +/// Represents player with name and a link to Game. +pub struct Player { + pub name: String, + pub game: Weak>, +} + +impl Player { + pub fn new(name: S) -> Self + where + S: Into, + { + Player { + name: name.into(), + game: Weak::new(), + } + } +} diff --git a/vm/examples/tic-tac-toe/app/src/tests.rs b/vm/examples/tic-tac-toe/app/src/tests.rs new file mode 100644 index 0000000000..5a96c69bec --- /dev/null +++ b/vm/examples/tic-tac-toe/app/src/tests.rs @@ -0,0 +1,175 @@ +/* + * Copyright 2018 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +use crate::main; + +// TODO: add more tests + +#[test] +fn x_tile_win() { + let create_player = create_player_json("John".to_owned(), "so_secret_key".to_owned()); + assert_eq!( + main(create_player), + r#"{"player_name":"John","result":"A new player has been successfully created"}"# + ); + + let create_game = create_game_json("John".to_owned(), "so_secret_key".to_owned(), 'X'); + assert_eq!( + main(create_game), + r#"{"player_name":"John","result":"A new game has been successfully created"}"# + ); + + let player_move = create_move_json("John".to_owned(), "so_secret_key".to_owned(), 0, 0); + assert_eq!( + main(player_move), + r#"{"coords":[0,1],"player_name":"John","winner":"None"}"# + ); + + let player_move = create_move_json("John".to_owned(), "so_secret_key".to_owned(), 1, 0); + assert_eq!( + main(player_move), + r#"{"coords":[0,2],"player_name":"John","winner":"None"}"# + ); + + let player_move = create_move_json("John".to_owned(), "so_secret_key".to_owned(), 2, 0); + assert_eq!(main(player_move), r#"{"coords":[18446744073709551615,18446744073709551615],"player_name":"John","winner":"X"}"#); + + let get_state = get_state_json("John".to_owned(), "so_secret_key".to_owned()); + assert_eq!( + main(get_state), + r#"{"board":["X","O","O","X","_","_","X","_","_"],"player_name":"John","player_tile":"X"}"# + ); +} + +#[test] +fn o_tile_win() { + let create_player = create_player_json("John2".to_owned(), "so_secret_key2".to_owned()); + assert_eq!( + main(create_player), + r#"{"player_name":"John2","result":"A new player has been successfully created"}"# + ); + + let create_game = create_game_json("John2".to_owned(), "so_secret_key2".to_owned(), 'O'); + assert_eq!( + main(create_game), + r#"{"coords":[0,0],"player_name":"John2","winner":"None"}"# + ); + + let player_move = create_move_json("John2".to_owned(), "so_secret_key2".to_owned(), 0, 2); + assert_eq!( + main(player_move), + r#"{"coords":[0,1],"player_name":"John2","winner":"None"}"# + ); + + let player_move = create_move_json("John2".to_owned(), "so_secret_key2".to_owned(), 1, 2); + assert_eq!( + main(player_move), + r#"{"coords":[1,0],"player_name":"John2","winner":"None"}"# + ); + + let player_move = create_move_json("John2".to_owned(), "so_secret_key2".to_owned(), 2, 2); + assert_eq!( + main(player_move), + r#"{"coords":[1,1],"player_name":"John2","winner":"None"}"# + ); + + let get_state = get_state_json("John2".to_owned(), "so_secret_key2".to_owned()); + assert_eq!(main(get_state), r#"{"board":["X","X","O","X","X","O","_","_","O"],"player_name":"John2","player_tile":"O"}"#); +} + +#[test] +fn app_win() { + let create_player = create_player_json("John3".to_owned(), "so_secret_key3".to_owned()); + assert_eq!( + main(create_player), + r#"{"player_name":"John3","result":"A new player has been successfully created"}"# + ); + + let create_game = create_game_json("John3".to_owned(), "so_secret_key3".to_owned(), 'O'); + assert_eq!( + main(create_game), + r#"{"coords":[0,0],"player_name":"John3","winner":"None"}"# + ); + + let player_move = create_move_json("John3".to_owned(), "so_secret_key3".to_owned(), 2, 0); + assert_eq!( + main(player_move), + r#"{"coords":[0,1],"player_name":"John3","winner":"None"}"# + ); + + let player_move = create_move_json("John3".to_owned(), "so_secret_key3".to_owned(), 2, 1); + assert_eq!( + main(player_move), + r#"{"coords":[0,2],"player_name":"John3","winner":"X"}"# + ); + + let player_move = create_move_json("John3".to_owned(), "so_secret_key3".to_owned(), 2, 2); + assert_eq!( + main(player_move), + r#"{"error":"Player X has already won this game"}"# + ); + + let get_state = get_state_json("John3".to_owned(), "so_secret_key3".to_owned()); + assert_eq!(main(get_state), r#"{"board":["X","X","X","_","_","_","O","O","_"],"player_name":"John3","player_tile":"O"}"#); +} + +fn create_move_json(player_name: String, player_sign: String, x: usize, y: usize) -> String { + generate_json( + "move".to_owned(), + player_name, + player_sign, + format!(r#", "coords": [{}, {}]"#, x, y), + ) +} + +fn create_player_json(player_name: String, player_sign: String) -> String { + generate_json( + "create_player".to_owned(), + player_name, + player_sign, + "".to_owned(), + ) +} + +fn create_game_json(player_name: String, player_sign: String, tile: char) -> String { + generate_json( + "create_game".to_owned(), + player_name, + player_sign, + format!(r#", "tile": "{}""#, tile), + ) +} + +fn get_state_json(player_name: String, player_sign: String) -> String { + generate_json( + "get_game_state".to_owned(), + player_name, + player_sign, + "".to_owned(), + ) +} + +fn generate_json( + action: String, + player_name: String, + player_sign: String, + additional_fields: String, +) -> String { + // TODO: move to json! macro + format!( + r#"{{ "action": "{}", "player_name": "{}", "player_sign": "{}" {} }}"#, + action, player_name, player_sign, additional_fields + ) +} diff --git a/vm/examples/tic-tac-toe/runner/src/main/resources/reference.conf b/vm/examples/tic-tac-toe/runner/src/main/resources/reference.conf new file mode 100644 index 0000000000..e54b942453 --- /dev/null +++ b/vm/examples/tic-tac-toe/runner/src/main/resources/reference.conf @@ -0,0 +1,20 @@ +# +# These settings describe the reasonable settings for debugging of backend application. +# + +fluence.vm.debugger { + + # 65536*16384 = 1GB + defaultMaxMemPages: 16384 + + specTestRegister: false + + loggerRegister: 2 + + allocateFunctionName: "allocate" + + deallocateFunctionName: "deallocate" + + invokeFunctionName: "invoke" + +} diff --git a/vm/examples/tic-tac-toe/runner/src/main/scala/TicTacToeRunner.scala b/vm/examples/tic-tac-toe/runner/src/main/scala/TicTacToeRunner.scala new file mode 100644 index 0000000000..ee9e37d3fb --- /dev/null +++ b/vm/examples/tic-tac-toe/runner/src/main/scala/TicTacToeRunner.scala @@ -0,0 +1,132 @@ +/* + * Copyright 2018 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import cats.data.{EitherT, NonEmptyList} +import cats.effect.{ExitCode, IO, IOApp} +import fluence.vm.VmError.InternalVmError +import fluence.vm.{VmError, WasmVm} + +import scala.language.higherKinds + +/** + * A tic-tac-toe example runner that is an example of possible debugger of `tic-tac-toe` backend application. + * Internally it creates WasmVm and invokes the application with some parameters. Also can be used as a template for + * debugging other backend applications. + */ +object TicTacToeRunner extends IOApp { + + override def run(args: List[String]): IO[ExitCode] = { + + val program: EitherT[IO, VmError, String] = for { + inputFile <- EitherT(getWasmFilePath(args).attempt) + .leftMap(e => InternalVmError(e.getMessage, Some(e))) + vm ← WasmVm[IO](NonEmptyList.one(inputFile), "fluence.vm.debugger") + initState ← vm.getVmState[IO] + + createPlayer = createPlayerJson("John", "secret_key") + createPlayer2 = createPlayerJson("John2", "secret_key") + createGame = createGameJson("John", "secret_key", 'X') + createGame2 = createGameJson("John2", "secret_key", 'X') + getGameState = getStateJson("John", "secret_key") + makeMove = moveJson("John", "secret_key", 0, 0) + makeMove2 = moveJson("John", "secret_key", 1, 0) + makeMove3 = moveJson("John", "secret_key", 2, 0) + + result1 ← vm.invoke[IO](None, createPlayer.getBytes()) + result2 ← vm.invoke[IO](None, createPlayer2.getBytes()) + result3 ← vm.invoke[IO](None, createGame.getBytes()) + result4 ← vm.invoke[IO](None, createGame2.getBytes()) + result5 ← vm.invoke[IO](None, makeMove.getBytes()) + result6 ← vm.invoke[IO](None, getGameState.getBytes()) + result7 ← vm.invoke[IO](None, makeMove.getBytes()) + result8 ← vm.invoke[IO](None, makeMove2.getBytes()) + result9 ← vm.invoke[IO](None, makeMove3.getBytes()) + result10 ← vm.invoke[IO](None, getGameState.getBytes()) + + finishState <- vm.getVmState[IO].toVmError + } yield { + + /* + The console output should be like this: + + [SUCCESS] Execution Results. + initState=ByteVector(32 bytes, 0x1a9d7358e0fbd33fe14df1f8b72e6e846f35cd8040dd42ed818f6c356516b18c) + result1={"player_name":"John","result":"A new player has been successfully created"} + result2={"player_name":"John2","result":"A new player has been successfully created"} + result3={"player_name":"John","result":"A new game has been successfully created"} + result4={"player_name":"John2","result":"A new game has been successfully created"} + result5={"coords":[0,1],"player_name":"John","winner":"None"} + result6={"board":["X","O","_","_","_","_","_","_","_"],"player_name":"John","player_tile":"X"} + result7={"error":"Please choose a free position"} + result8={"coords":[0,2],"player_name":"John","winner":"None"} + result9={"coords":[4294967295,4294967295],"player_name":"John","winner":"X"} + result10={"board":["X","O","O","X","_","_","X","_","_"],"player_name":"John","player_tile":"X"} + finishState=ByteVector(32 bytes, 0x3b25d096464433263766bb6a2da70c59fc81fdb0a46080bbb993b5aed110bf6f) + */ + + s"[SUCCESS] Execution Results.\n" + + s"initState=$initState \n" + + s"result1=${new String(result1)} \n" + + s"result2=${new String(result2)} \n" + + s"result3=${new String(result3)} \n" + + s"result4=${new String(result4)} \n" + + s"result5=${new String(result5)} \n" + + s"result6=${new String(result6)} \n" + + s"result7=${new String(result7)} \n" + + s"result8=${new String(result8)} \n" + + s"result9=${new String(result9)} \n" + + s"result10=${new String(result10)} \n" + + s"finishState=$finishState" + } + + program.value.map { + case Left(err) ⇒ + println(s"[Error]: $err cause=${err.getCause}") + ExitCode.Error + case Right(value) ⇒ + println(value) + ExitCode.Success + } + } + + private def moveJson(playerName: String, playerSign: String, x: Int, y: Int): String = + generateJson("move", playerName, playerSign, ", \"coords\": " + s"[$x, $y]") + + private def createPlayerJson(playerName: String, playerSign: String): String = + generateJson("create_player", playerName, playerSign) + + private def createGameJson(playerName: String, playerSign: String, tile: Char): String = + generateJson("create_game", playerName, playerSign, ", \"tile\": \"" + tile + "\"") + + private def getStateJson(playerName: String, playerSign: String): String = + generateJson("get_game_state", playerName, playerSign) + + private def generateJson(action: String, playerName: String, playerSign: String, additionalFields: String="") = + s"""{ + "action": "$action", "player_name": "$playerName", "player_sign": "$playerSign" $additionalFields + }""".stripMargin + + private def getWasmFilePath(args: List[String]): IO[String] = IO { + args.headOption match { + case Some(value) ⇒ + println(s"Starts for input file $value") + value + case None ⇒ + throw new IllegalArgumentException("Please provide a full path for wasm file as the first CLI argument.") + } + } + +}