commit 98abdcea45d03b89936e4341ca97c09e883c3c1f Author: andy Date: Fri Jun 25 18:02:45 2021 +0100 initial commit with board skeleton diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..6f7a293 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,44 @@ +name: test and deploy + +on: + push: + branches: [ master ] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Install wasm-pack + uses: jetli/wasm-pack-action@v0.3.0 + + - name: Build Rust for WASM + run: wasm-pack build + + - name: Test WASM in-browser + run: wasm-pack test --firefox --chrome --headless + + # - name: Install Node + # uses: actions/setup-node@v2 + # with: + # node-version: 16 + + # - name: Install Node Modules + # run: npm ci + # working-directory: ./www + + # - name: Build Js + # run: npm run build --if-present + # working-directory: ./www + + # - name: Deploy To Pages + # uses: peaceiris/actions-gh-pages@v3 + # with: + # github_token: ${{ secrets.GITHUB_TOKEN }} + # publish_dir: ./www/dist diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4e30131 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/target +**/*.rs.bk +Cargo.lock +bin/ +pkg/ +wasm-pack.log diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..48156c7 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,55 @@ +[package] +name = "draught" +version = "0.1.0" +authors = ["aj "] +edition = "2018" +repository = "https://github.com/Sarsoo/checkers" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +default = ["console_error_panic_hook", + "random_init" + ] + +random_init = ["rand", "rand_pcg"] + +[dependencies] +wasm-bindgen = "0.2.74" +rand = {version = "0.8.4", optional = true } +rand_pcg = {version = "0.3.1", optional = true } +indextree = "4.3.1" + +# The `console_error_panic_hook` crate provides better debugging of panics by +# logging them with `console.error`. This is great for development, but requires +# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for +# code size when deploying. +console_error_panic_hook = { version = "0.1.6", optional = true } + +# `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size +# compared to the default allocator's ~10K. It is slower than the default +# allocator, however. +# +# Unfortunately, `wee_alloc` requires nightly Rust when targeting wasm for now. +wee_alloc = { version = "0.4.5", optional = true } + +[dependencies.web-sys] +version = "0.3.45" +features = [ + "console", +] + +[dependencies.getrandom] +features = ["js"] + +[dev-dependencies] +wasm-bindgen-test = "0.3.24" + +[profile.release] +# Tell `rustc` to optimize for small code size. +opt-level = "s" + +[package.metadata.wasm-pack.profile.release] +wasm-opt = false + diff --git a/README.md b/README.md new file mode 100644 index 0000000..0fb8d7e --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +Draught +=============== + +![gof-ci](https://github.com/sarsoo/draught/actions/workflows/test.yml/badge.svg) + +## [Try it Out!](https://sarsoo.github.io/draught/) + +WASM-based checkers game. Looking to implement a minimax based AI. + +Rust WASM module for game logic with a JS frontend for rendering and processing user input. + +## Building + +1. Setup a Rust + wasm-pack environment and a Node environment +2. Build the Rust library into a WASM module + - `wasm-pack build` +3. Move to the Js workspace + - `cd www` +4. Install the Js dependencies + - `npm install` +5. Build the Js frontend with Rust WASM module + - `npm run build` \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..f35f22c --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,280 @@ +mod utils; + +use std::fmt::{Display, Write}; +use std::option::Option; + +extern crate wasm_bindgen; +use wasm_bindgen::prelude::*; + +extern crate indextree; +use indextree::Arena; + +pub const STD_WIDTH: usize = 8; +pub const STD_HEIGHT: usize = 8; + +// use rand::prelude::*; + +// use rand_pcg::Pcg64Mcg; + +// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global +// allocator. +// #[cfg(feature = "wee_alloc")] +// #[global_allocator] +// static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; + +macro_rules! log { + ( $( $t:tt )* ) => { + web_sys::console::log_1(&format!( $( $t )* ).into()); + } +} + +#[wasm_bindgen] +pub fn init_game() { + log!("initialising wasm"); + utils::set_panic_hook(); + + #[cfg(feature = "random_init")] + log!("random layout enabled"); +} + +#[wasm_bindgen] +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Team { + Black = 0, + White = 1, +} + +#[wasm_bindgen] +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Strength { + Man = 0, + King = 1 +} + +#[wasm_bindgen] +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SquareState { + Empty = 0, + Occupied = 1, + Unplayable = 2 +} + +impl Display for SquareState { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + SquareState::Empty => {write!(f, "{}", 'E')}, + SquareState::Occupied => {write!(f, "{}", 'O')}, + SquareState::Unplayable => {write!(f, "{}", 'U')}, + } + } +} + +#[wasm_bindgen] +#[derive(Clone)] +pub struct Piece { + id: u8, + team: Team, + strength: Strength +} + +#[wasm_bindgen] +#[derive(Clone)] +pub struct Square { + occupant: Option, + state: SquareState +} + +impl Square { + fn new(state: SquareState, occupant: Option) -> Square{ + Square { + occupant, + state + } + } +} + + +#[wasm_bindgen] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct BrdIdx { + row: usize, + col: usize +} + +#[wasm_bindgen] +impl BrdIdx { + pub fn from(row: usize, col: usize) -> BrdIdx { + BrdIdx{ + row, col + } + } +} + +/////////////// +// BOARD +/////////////// + +#[wasm_bindgen] +#[derive(Clone)] +pub struct Board { + cells: Vec, + width: usize, + height: usize, + + current_turn: Team +} + +impl Board { + pub fn cell(&self, idx: usize) -> &Square { + &self.cells[idx] + } + + pub fn grid_cell(&self, idx: BrdIdx) -> &Square { + self.cell(self.cell_idx(idx)) + } + + pub fn cell_mut(&mut self, idx: usize) -> &mut Square { + &mut self.cells[idx] + } +} + +#[wasm_bindgen] +impl Board { + pub fn cell_index(&self, row: usize, col: usize) -> usize { + (row * self.width) + col + } + + pub fn cell_idx(&self, idx: BrdIdx) -> usize { + self.cell_index(idx.row, idx.col) + } + + pub fn board_index(&self, idx: usize) -> BrdIdx { + let row = idx / self.width; + let col = idx - (row * self.width); + BrdIdx::from(row, col) + } + + pub fn diagonal_indices(&self, idx: BrdIdx) -> Option> { + if self.cell_state(self.cell_idx(idx)) == SquareState::Unplayable { + return None; + } + + let height_idx = self.height - 1; + let width_idx = self.width - 1; + + let mut cells = Vec::with_capacity(4); + + if idx.row > 0 { + if idx.col > 0 { + cells.push( + self.cell_index(idx.row - 1, idx.col - 1) + ); + } + + if idx.col < width_idx { + cells.push( + self.cell_index(idx.row - 1, idx.col + 1) + ); + } + } + + if idx.row < height_idx { + if idx.col > 0 { + cells.push( + self.cell_index(idx.row + 1, idx.col - 1) + ); + } + + if idx.col < width_idx { + cells.push( + self.cell_index(idx.row + 1, idx.col + 1) + ); + } + } + + cells.shrink_to_fit(); + Some(cells) + } + + // pub fn can_move(&self, from: BrdIdx, to: BrdIdx) -> bool { + // let diagonals = self.diagonal_indices(from); + // } + + pub fn new(width: usize, height: usize) -> Board { + let total_cells = width * height; + + let mut cells: Vec = Vec::with_capacity(total_cells); + let mut playable = false; + + for i in 0..height { + for _ in 0..width { + if playable { + cells.push(Square::new(SquareState::Empty, None)); + } + else { + cells.push(Square::new(SquareState::Unplayable, None)); + } + playable = !playable; + } + playable = i % 2 == 0; + } + + Board { + cells, + width, + height, + + current_turn: Team::Black + } + } + + pub fn current_turn(&self) -> Team { + self.current_turn + } + + pub fn cells(&self) -> *const Square { + self.cells.as_ptr() + } + + pub fn num_cells(&self) -> usize { + self.cells.len() + } + + pub fn cell_state(&self, idx: usize) -> SquareState { + self.cell(idx).state + } +} + +impl Display for Board { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result{ + let mut string = String::new(); + + for i in 0..self.height { + for j in 0..self.width { + write!(string, "{}", + self.cell_state( // empty, unocc, unplayable + self.cell_index( // 1d vec idx from 2d board idx + i, j // 2d board idx + ) + ) + ); + } + string.push('\n'); + } + + write!(f, "{}", string) + } +} + +/////////////// +// GAME +/////////////// + +#[wasm_bindgen] +pub struct Game { + current: Board, + tree: Arena +} \ No newline at end of file diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..b1d7929 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,10 @@ +pub fn set_panic_hook() { + // When the `console_error_panic_hook` feature is enabled, we can call the + // `set_panic_hook` function at least once during initialization, and then + // we will get better error messages if our code ever panics. + // + // For more details see + // https://github.com/rustwasm/console_error_panic_hook#readme + #[cfg(feature = "console_error_panic_hook")] + console_error_panic_hook::set_once(); +} diff --git a/tests/board.rs b/tests/board.rs new file mode 100644 index 0000000..98956c1 --- /dev/null +++ b/tests/board.rs @@ -0,0 +1,131 @@ +//! Test suite for the Web and headless browsers. + +#![cfg(target_arch = "wasm32")] + +extern crate wasm_bindgen_test; +// use wasm_bindgen_test::*; +use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; + +wasm_bindgen_test_configure!(run_in_browser); + +extern crate web_sys; +use web_sys::console; + +extern crate draught; +use draught::{Board, BrdIdx, SquareState, STD_WIDTH, STD_HEIGHT}; + +macro_rules! log { + ( $( $t:tt )* ) => { + web_sys::console::log_1(&format!( $( $t )* ).into()); + } +} + +#[wasm_bindgen_test] +fn create() { + let board = Board::new(STD_WIDTH, STD_HEIGHT); + assert!(true); +} + +#[wasm_bindgen_test] +fn std_num_cells() { + let board = Board::new(8, 8); + assert_eq!(64, board.num_cells()); +} + +////////////// +// INDEXING +////////////// + +#[wasm_bindgen_test] +fn cell_index_top_left() { + let board = Board::new(8, 8); + assert_eq!(0, board.cell_index(0, 0)); +} + +#[wasm_bindgen_test] +fn cell_index_central() { + let board = Board::new(8, 8); + assert_eq!(9, board.cell_index(1, 1)); +} + +#[wasm_bindgen_test] +fn cell_index_central_2() { + let board = Board::new(8, 8); + assert_eq!(17, board.cell_index(2, 1)); +} + +#[wasm_bindgen_test] +fn board_index() { + let board = Board::new(8, 8); + + // first row + assert_eq!(BrdIdx::from(0, 5), board.board_index(5)); + // second row + assert_eq!(BrdIdx::from(1, 6), board.board_index(14)); +} + +/////////////////// +// SQUARE STATE +/////////////////// + +#[wasm_bindgen_test] +fn first_square_unplayable() { + let board = Board::new(8, 8); + assert_eq!(SquareState::Unplayable, board.cell_state(board.cell_index(0, 0))); +} + +#[wasm_bindgen_test] +fn first_square_row_5_unplayable() { + let board = Board::new(8, 8); + assert_eq!(SquareState::Empty, board.cell_state(board.cell_index(5, 0))); +} + +#[wasm_bindgen_test] +fn moveable_indices_unplayable() { + let board = Board::new(8, 8); + assert_eq!(None, board.diagonal_indices(BrdIdx::from(7, 7))); + assert_eq!(None, board.diagonal_indices(BrdIdx::from(0, 0))); + assert_eq!(None, board.diagonal_indices(BrdIdx::from(1, 1))); +} + +#[wasm_bindgen_test] +fn moveable_indices_central() { + let board = Board::new(8, 8); + assert_eq!(Some(vec![1, 3, 17, 19]), board.diagonal_indices(BrdIdx::from(1, 2))); +} + +#[wasm_bindgen_test] +fn moveable_indices_top_row() { + let board = Board::new(8, 8); + assert_eq!(Some(vec![8, 10]), board.diagonal_indices(BrdIdx::from(0, 1))); +} + +#[wasm_bindgen_test] +fn moveable_indices_left_column() { + let board = Board::new(8, 8); + assert_eq!(Some(vec![1, 17]), board.diagonal_indices(BrdIdx::from(1, 0))); +} + +#[wasm_bindgen_test] +fn moveable_indices_bottom_row() { + let board = Board::new(8, 8); + assert_eq!(Some(vec![49, 51]), board.diagonal_indices(BrdIdx::from(7, 2))); +} + +#[wasm_bindgen_test] +fn moveable_indices_right_column() { + let board = Board::new(8, 8); + assert_eq!(Some(vec![14, 30]), board.diagonal_indices(BrdIdx::from(2, 7))); +} + +#[wasm_bindgen_test] +fn moveable_indices_top_right() { + let board = Board::new(8, 8); + assert_eq!(Some(vec![14]), board.diagonal_indices(BrdIdx::from(0, 7))); +} + +#[wasm_bindgen_test] +fn moveable_indices_bottom_left() { + let board = Board::new(8, 8); + assert_eq!(Some(vec![49]), board.diagonal_indices(BrdIdx::from(7, 0))); +} diff --git a/www/.gitignore b/www/.gitignore new file mode 100644 index 0000000..f06235c --- /dev/null +++ b/www/.gitignore @@ -0,0 +1,2 @@ +node_modules +dist diff --git a/www/bootstrap.js b/www/bootstrap.js new file mode 100644 index 0000000..7934d62 --- /dev/null +++ b/www/bootstrap.js @@ -0,0 +1,5 @@ +// A dependency graph that contains any wasm must all be imported +// asynchronously. This `bootstrap.js` file does the single async import, so +// that no one else needs to worry about it again. +import("./index.js") + .catch(e => console.error("Error importing `index.js`:", e)); diff --git a/www/index.html b/www/index.html new file mode 100644 index 0000000..a44b7b1 --- /dev/null +++ b/www/index.html @@ -0,0 +1,49 @@ + + + + + + + + game of life + + + + +
+
+

Draught 🚀

+
+
+ +
+
+ + + + + + + + + + \ No newline at end of file diff --git a/www/index.js b/www/index.js new file mode 100644 index 0000000..6d825df --- /dev/null +++ b/www/index.js @@ -0,0 +1,9 @@ +import { init_game } from "draught"; +import { memory } from "draught/draught_bg.wasm"; + +// let PLAY = true; +// let PLAY = false; +init_game(); + +const canvas = document.getElementById("game-canvas"); +const ctx = canvas.getContext('2d'); diff --git a/www/package.json b/www/package.json new file mode 100644 index 0000000..b51bc47 --- /dev/null +++ b/www/package.json @@ -0,0 +1,34 @@ +{ + "name": "draught", + "version": "0.1.0", + "description": "Rust wasm-based checkers game", + "main": "index.js", + "scripts": { + "build": "webpack --config webpack.config.js", + "start": "webpack serve --config webpack.config.js --progress" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Sarsoo/draught.git" + }, + "keywords": [ + "webassembly", + "wasm", + "rust", + "webpack" + ], + "author": "sarsoo", + "bugs": { + "url": "https://github.com/Sarsoo/draught/issues" + }, + "homepage": "https://github.com/Sarsoo/draught#readme", + "dependencies": { + "gameoflife": "file:../pkg" + }, + "devDependencies": { + "copy-webpack-plugin": "^9.0.0", + "webpack": "^5.40.0", + "webpack-cli": "^4.7.2", + "webpack-dev-server": "^3.11.2" + } +} diff --git a/www/webpack.config.js b/www/webpack.config.js new file mode 100644 index 0000000..e0a4eee --- /dev/null +++ b/www/webpack.config.js @@ -0,0 +1,23 @@ +const CopyWebpackPlugin = require("copy-webpack-plugin"); +const path = require('path'); + +module.exports = { + entry: "./bootstrap.js", + output: { + path: path.resolve(__dirname, "dist"), + filename: "bootstrap.js", + }, + mode: "development", + experiments: { + asyncWebAssembly: true + }, + plugins: [ + new CopyWebpackPlugin({ + patterns: [ + { + from: path.resolve(__dirname, "index.html") + } + ] + }) + ], +};