initial commit with board skeleton

This commit is contained in:
andy 2021-06-25 18:02:45 +01:00
commit 98abdcea45
13 changed files with 670 additions and 0 deletions

44
.github/workflows/test.yml vendored Normal file
View File

@ -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

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
/target
**/*.rs.bk
Cargo.lock
bin/
pkg/
wasm-pack.log

55
Cargo.toml Normal file
View File

@ -0,0 +1,55 @@
[package]
name = "draught"
version = "0.1.0"
authors = ["aj <andrewjpack@gmail.com>"]
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

22
README.md Normal file
View File

@ -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`

280
src/lib.rs Normal file
View File

@ -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<Piece>,
state: SquareState
}
impl Square {
fn new(state: SquareState, occupant: Option<Piece>) -> 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<Square>,
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<Vec<usize>> {
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<Square> = 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<Board>
}

10
src/utils.rs Normal file
View File

@ -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();
}

131
tests/board.rs Normal file
View File

@ -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)));
}

2
www/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules
dist

5
www/bootstrap.js vendored Normal file
View File

@ -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));

49
www/index.html Normal file
View File

@ -0,0 +1,49 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x" crossorigin="anonymous">
<title>game of life</title>
<style>
body {
/* position: absolute; */
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #f0f0f0;
}
h1 {
font-family: monospace;
}
</style>
</head>
<body>
<div class="card container text-center p-4 m-3">
<div class="card-header">
<h1>Draught 🚀</h1>
</div>
<div class="card-body">
</div>
</div>
<!-- <pre id="game-of-life-canvas"></pre> -->
<canvas id="game-canvas" class="pb-2"></canvas>
<script src="./bootstrap.js"></script>
<a href="https://github.com/sarsoo"><img src="https://storage.googleapis.com/sarsooxyzstatic/andy.png" class=" pb-2" style="width: 150px" /></a>
</body>
</html>

9
www/index.js Normal file
View File

@ -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');

34
www/package.json Normal file
View File

@ -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"
}
}

23
www/webpack.config.js Normal file
View File

@ -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")
}
]
})
],
};