├── .github └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md ├── js ├── App.tsx ├── components │ └── Greet.tsx ├── index.tsx └── stores │ ├── WasmEngine.ts │ └── use_engines.tsx ├── package.json ├── preview.png ├── src ├── game │ ├── logic │ │ ├── board.rs │ │ ├── gobblet.rs │ │ ├── hand.rs │ │ ├── mod.rs │ │ └── player.rs │ ├── manager.rs │ ├── mod.rs │ ├── ui │ │ ├── graphics.rs │ │ ├── interaction.rs │ │ ├── mod.rs │ │ └── shapes.rs │ └── utils │ │ ├── coord.rs │ │ └── mod.rs ├── lib.rs ├── macros.rs └── utils.rs ├── static ├── index.css └── index.html ├── tsconfig.json ├── webpack.config.js ├── yarn-error.log └── yarn.lock /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust + Webpack 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | env: 8 | CARGO_TERM_COLOR: always 9 | 10 | jobs: 11 | test: 12 | name: Test on node ${{ matrix.node_version }} and ${{ matrix.os }} 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | matrix: 16 | node_version: ['10', '12'] 17 | os: [ubuntu-latest, windows-latest, macOS-latest] 18 | 19 | steps: 20 | - uses: actions/checkout@v1 21 | - name: Use Node.js ${{ matrix.node_version }} 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: ${{ matrix.node_version }} 25 | 26 | - name: yarn install, build and test 27 | run: | 28 | yarn 29 | yarn test 30 | - name: Build game 31 | run: yarn build && cp static/* dist/ 32 | if: matrix.os == 'ubuntu-latest' && matrix.node_version == '12' 33 | - name: Deploy to GitHub Pages 34 | uses: peaceiris/actions-gh-pages@v3 35 | with: 36 | github_token: ${{ secrets.GITHUB_TOKEN }} 37 | publish_dir: ./dist 38 | if: github.ref == 'refs/heads/master' && matrix.os == 'ubuntu-latest' && matrix.node_version == '12' 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | pkg 3 | target 4 | dist -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "bumpalo" 5 | version = "3.4.0" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" 8 | 9 | [[package]] 10 | name = "cfg-if" 11 | version = "0.1.10" 12 | source = "registry+https://github.com/rust-lang/crates.io-index" 13 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 14 | 15 | [[package]] 16 | name = "console_error_panic_hook" 17 | version = "0.1.6" 18 | source = "registry+https://github.com/rust-lang/crates.io-index" 19 | checksum = "b8d976903543e0c48546a91908f21588a680a8c8f984df9a5d69feccb2b2a211" 20 | dependencies = [ 21 | "cfg-if", 22 | "wasm-bindgen", 23 | ] 24 | 25 | [[package]] 26 | name = "futures" 27 | version = "0.1.29" 28 | source = "registry+https://github.com/rust-lang/crates.io-index" 29 | checksum = "1b980f2816d6ee8673b6517b52cb0e808a180efc92e5c19d02cdda79066703ef" 30 | 31 | [[package]] 32 | name = "gobblet" 33 | version = "0.1.0" 34 | dependencies = [ 35 | "console_error_panic_hook", 36 | "futures", 37 | "js-sys", 38 | "wasm-bindgen", 39 | "wasm-bindgen-futures", 40 | "web-sys", 41 | "wee_alloc", 42 | ] 43 | 44 | [[package]] 45 | name = "js-sys" 46 | version = "0.3.40" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "ce10c23ad2ea25ceca0093bd3192229da4c5b3c0f2de499c1ecac0d98d452177" 49 | dependencies = [ 50 | "wasm-bindgen", 51 | ] 52 | 53 | [[package]] 54 | name = "lazy_static" 55 | version = "1.4.0" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 58 | 59 | [[package]] 60 | name = "libc" 61 | version = "0.2.71" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "9457b06509d27052635f90d6466700c65095fdf75409b3fbdd903e988b886f49" 64 | 65 | [[package]] 66 | name = "log" 67 | version = "0.4.8" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" 70 | dependencies = [ 71 | "cfg-if", 72 | ] 73 | 74 | [[package]] 75 | name = "memory_units" 76 | version = "0.4.0" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3" 79 | 80 | [[package]] 81 | name = "proc-macro2" 82 | version = "1.0.18" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "beae6331a816b1f65d04c45b078fd8e6c93e8071771f41b8163255bbd8d7c8fa" 85 | dependencies = [ 86 | "unicode-xid", 87 | ] 88 | 89 | [[package]] 90 | name = "quote" 91 | version = "1.0.7" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" 94 | dependencies = [ 95 | "proc-macro2", 96 | ] 97 | 98 | [[package]] 99 | name = "syn" 100 | version = "1.0.33" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "e8d5d96e8cbb005d6959f119f773bfaebb5684296108fb32600c00cde305b2cd" 103 | dependencies = [ 104 | "proc-macro2", 105 | "quote", 106 | "unicode-xid", 107 | ] 108 | 109 | [[package]] 110 | name = "unicode-xid" 111 | version = "0.2.0" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" 114 | 115 | [[package]] 116 | name = "wasm-bindgen" 117 | version = "0.2.64" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "6a634620115e4a229108b71bde263bb4220c483b3f07f5ba514ee8d15064c4c2" 120 | dependencies = [ 121 | "cfg-if", 122 | "wasm-bindgen-macro", 123 | ] 124 | 125 | [[package]] 126 | name = "wasm-bindgen-backend" 127 | version = "0.2.64" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "3e53963b583d18a5aa3aaae4b4c1cb535218246131ba22a71f05b518098571df" 130 | dependencies = [ 131 | "bumpalo", 132 | "lazy_static", 133 | "log", 134 | "proc-macro2", 135 | "quote", 136 | "syn", 137 | "wasm-bindgen-shared", 138 | ] 139 | 140 | [[package]] 141 | name = "wasm-bindgen-futures" 142 | version = "0.3.27" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "83420b37346c311b9ed822af41ec2e82839bfe99867ec6c54e2da43b7538771c" 145 | dependencies = [ 146 | "cfg-if", 147 | "futures", 148 | "js-sys", 149 | "wasm-bindgen", 150 | "web-sys", 151 | ] 152 | 153 | [[package]] 154 | name = "wasm-bindgen-macro" 155 | version = "0.2.64" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "3fcfd5ef6eec85623b4c6e844293d4516470d8f19cd72d0d12246017eb9060b8" 158 | dependencies = [ 159 | "quote", 160 | "wasm-bindgen-macro-support", 161 | ] 162 | 163 | [[package]] 164 | name = "wasm-bindgen-macro-support" 165 | version = "0.2.64" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "9adff9ee0e94b926ca81b57f57f86d5545cdcb1d259e21ec9bdd95b901754c75" 168 | dependencies = [ 169 | "proc-macro2", 170 | "quote", 171 | "syn", 172 | "wasm-bindgen-backend", 173 | "wasm-bindgen-shared", 174 | ] 175 | 176 | [[package]] 177 | name = "wasm-bindgen-shared" 178 | version = "0.2.64" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "7f7b90ea6c632dd06fd765d44542e234d5e63d9bb917ecd64d79778a13bd79ae" 181 | 182 | [[package]] 183 | name = "web-sys" 184 | version = "0.3.40" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "7b72fe77fd39e4bd3eaa4412fd299a0be6b3dfe9d2597e2f1c20beb968f41d17" 187 | dependencies = [ 188 | "js-sys", 189 | "wasm-bindgen", 190 | ] 191 | 192 | [[package]] 193 | name = "wee_alloc" 194 | version = "0.4.5" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "dbb3b5a6b2bb17cb6ad44a2e68a43e8d2722c997da10e928665c72ec6c0a0b8e" 197 | dependencies = [ 198 | "cfg-if", 199 | "libc", 200 | "memory_units", 201 | "winapi", 202 | ] 203 | 204 | [[package]] 205 | name = "winapi" 206 | version = "0.3.8" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" 209 | dependencies = [ 210 | "winapi-i686-pc-windows-gnu", 211 | "winapi-x86_64-pc-windows-gnu", 212 | ] 213 | 214 | [[package]] 215 | name = "winapi-i686-pc-windows-gnu" 216 | version = "0.4.0" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 219 | 220 | [[package]] 221 | name = "winapi-x86_64-pc-windows-gnu" 222 | version = "0.4.0" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 225 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | # You must change these to your own details. 2 | [package] 3 | authors = ["Alex Fallenstedt ", "Adam Michel "] 4 | categories = ["wasm"] 5 | description = "todo add desc" 6 | edition = "2018" 7 | name = "gobblet" 8 | readme = "README.md" 9 | version = "0.1.0" 10 | 11 | [lib] 12 | crate-type = ["cdylib"] 13 | 14 | [features] 15 | # If you uncomment this line, it will enable `wee_alloc`: 16 | #default = ["wee_alloc"] 17 | 18 | [dependencies] 19 | # The `wasm-bindgen` crate provides the bare minimum functionality needed 20 | # to interact with JavaScript. 21 | wasm-bindgen = "0.2.64" 22 | js-sys = "0.3.40" 23 | 24 | # `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size 25 | # compared to the default allocator's ~10K. However, it is slower than the default 26 | # allocator, so it's not enabled by default. 27 | wee_alloc = {version = "0.4.2", optional = true} 28 | 29 | [dependencies.web-sys] 30 | features = [ 31 | 'console', 32 | 'CanvasRenderingContext2d', 33 | 'Document', 34 | 'EventTarget', 35 | 'Element', 36 | 'HtmlCanvasElement', 37 | 'HtmlVideoElement', 38 | 'HtmlElement', 39 | 'ImageData', 40 | 'MediaStream', 41 | 'MessageEvent', 42 | 'MouseEvent', 43 | 'Path2d', 44 | 'Performance', 45 | 'RtcDataChannel', 46 | 'RtcDataChannelEvent', 47 | 'Window', 48 | ] 49 | version = "0.3.40" 50 | 51 | # The `console_error_panic_hook` crate provides better debugging of panics by 52 | # logging them with `console.error`. This is great for development, but requires 53 | # all the `std::fmt` and `std::panicking` infrastructure, so it's only enabled 54 | # in debug mode. 55 | [target."cfg(debug_assertions)".dependencies] 56 | console_error_panic_hook = "0.1.5" 57 | 58 | # These crates are used for running unit tests. 59 | [dev-dependencies] 60 | futures = "0.1.27" 61 | wasm-bindgen-futures = "0.3.22" 62 | 63 | [profile.release] 64 | lto = true 65 | opt-level = 3 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebAssembly Gobblet 2 | 3 | This is a simple game called [Gobblet](https://en.wikipedia.org/wiki/Gobblet) built using Rust and WebAssembly. 4 | 5 | ![preview](./preview.png) 6 | 7 | ## Setup 8 | 9 | A simple 'yarn' and 'yarn start' should do the trick. File an issue if it doesn't! 10 | 11 | Open up the console to see who wins the game. 12 | 13 | -------------------------------------------------------------------------------- /js/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { Observer } from 'mobx-react' 3 | import { Greet } from './components/Greet'; 4 | import { useEngines } from './stores/use_engines'; 5 | 6 | function App() { 7 | const { wasmEngine } = useEngines() 8 | 9 | useEffect(() => { 10 | async function loadWasm() { 11 | await wasmEngine.initialize() 12 | } 13 | loadWasm() 14 | }, []) 15 | 16 | return ( 17 | 18 | {() => wasmEngine.loading ?

Loading...

: } 19 |
20 | ) 21 | 22 | } 23 | 24 | export default App; 25 | -------------------------------------------------------------------------------- /js/components/Greet.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react'; 2 | import { useEngines } from '../stores/use_engines'; 3 | 4 | const styles = { 5 | container: { 6 | display: 'flex', 7 | justifyContent: 'center', 8 | margin: '0 auto' 9 | } 10 | } 11 | 12 | export function Greet() { 13 | const canvasRef = useRef(null) 14 | const { wasmEngine } = useEngines() 15 | 16 | useEffect(() => { 17 | let canvas: HTMLCanvasElement; 18 | if (wasmEngine.instance && canvasRef.current) { 19 | canvas = canvasRef.current; 20 | wasmEngine.instance.start_game(canvas, "Alex", "Angelica"); 21 | } 22 | }, [canvasRef, wasmEngine]) 23 | 24 | return ( 25 |
26 | 27 |
28 | ); 29 | } -------------------------------------------------------------------------------- /js/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | ReactDOM.render( 6 | 7 | 8 | , 9 | document.getElementById('root') 10 | ); 11 | -------------------------------------------------------------------------------- /js/stores/WasmEngine.ts: -------------------------------------------------------------------------------- 1 | import { observable, runInAction, action } from "mobx"; 2 | 3 | type WasmInstance = typeof import("../../pkg/index.js"); 4 | 5 | export class WasmEngine { 6 | @observable 7 | public instance: WasmInstance | undefined = undefined; 8 | 9 | @observable 10 | public loading: boolean = true; 11 | 12 | @observable 13 | public error: Error | undefined = undefined; 14 | 15 | @action 16 | public async initialize() { 17 | try { 18 | //@ts-ignore 19 | const wasm = await import("../../pkg/index.js"); 20 | runInAction(() => { 21 | this.loading = false; 22 | this.instance = wasm 23 | }) 24 | } catch (error) { 25 | runInAction(() => { 26 | this.loading = false; 27 | this.error = error 28 | }) 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /js/stores/use_engines.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { WasmEngine } from './WasmEngine'; 3 | 4 | const globalContext = React.createContext({ 5 | wasmEngine: new WasmEngine() 6 | }); 7 | 8 | export const useEngines = () => React.useContext(globalContext); 9 | 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "You ", 3 | "name": "rust-webpack-template", 4 | "version": "0.1.0", 5 | "scripts": { 6 | "build": "rimraf dist pkg && webpack", 7 | "start": "rimraf dist pkg && webpack-dev-server --open -d", 8 | "test": "cargo test" 9 | }, 10 | "devDependencies": { 11 | "@types/react": "^16.9.38", 12 | "@types/react-dom": "^16.9.8", 13 | "@wasm-tool/wasm-pack-plugin": "^1.1.0", 14 | "copy-webpack-plugin": "^5.0.3", 15 | "rimraf": "^3.0.0", 16 | "ts-loader": "^7.0.5", 17 | "typescript": "^3.9.5", 18 | "webpack": "^4.42.0", 19 | "webpack-cli": "^3.3.3", 20 | "webpack-dev-server": "^3.7.1" 21 | }, 22 | "dependencies": { 23 | "mobx": "^5.15.4", 24 | "mobx-react": "^6.2.2", 25 | "react": "^16.13.1", 26 | "react-dom": "^16.13.1" 27 | } 28 | } -------------------------------------------------------------------------------- /preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fallenstedt/rust-goblet/5153312b90b6a20629e3fa721957a2a646c20fc5/preview.png -------------------------------------------------------------------------------- /src/game/logic/board.rs: -------------------------------------------------------------------------------- 1 | use crate::game::utils::coord::Coord; 2 | use crate::game::utils::{PlayerNumber, player_number_match}; 3 | use crate::game::logic::gobblet::{Gobblet, GobbletSize}; 4 | 5 | #[derive(Debug)] 6 | pub struct Board { 7 | cells: Vec>, 8 | } 9 | 10 | impl Board { 11 | pub fn new() -> Board { 12 | Board { cells: Board::build_cells() } 13 | } 14 | 15 | pub fn add_piece_to_board(&mut self, coord: &Coord, gobblet: Gobblet) -> Option { 16 | let r = *coord.get_row() as usize; 17 | let c = *coord.get_column() as usize; 18 | let cell = &mut self.cells[r][c]; 19 | 20 | return if cell.can_add(&gobblet) { 21 | cell.add(gobblet); 22 | None 23 | } else { 24 | Some(gobblet) 25 | } 26 | } 27 | 28 | pub fn remove_piece_from_board(&mut self, coord: &Coord, player: &PlayerNumber) -> Option { 29 | let r = *coord.get_row() as usize; 30 | let c = *coord.get_column() as usize; 31 | let cell = &mut self.cells[r][c]; 32 | 33 | return if cell.can_remove(&player) { 34 | Some(cell.remove()) 35 | } else { 36 | None 37 | } 38 | } 39 | 40 | pub fn has_won(&self, number: PlayerNumber) -> bool { 41 | let mut rows: [u8; 4] = [0, 0, 0, 0]; 42 | let mut columns: [u8; 4] = [0, 0, 0, 0]; 43 | let mut diagonal: u8 = 0; 44 | let mut anti_diagonal: u8 = 0; 45 | for (r, row) in self.cells.iter().enumerate() { 46 | for (c, cell) in row.iter().enumerate() { 47 | if cell.is_empty() { 48 | continue; 49 | } 50 | // check rows, 51 | // check columns, 52 | if player_number_match(cell.get_top_piece().get_player_number(), &number) { 53 | rows[r] += 1; 54 | columns[c] += 1; 55 | } 56 | 57 | // check diagonal, 58 | if r == c && player_number_match(cell.get_top_piece().get_player_number(), &number) { 59 | diagonal += 1; 60 | } 61 | 62 | // check anti diagonal 63 | if r + c == 3 && player_number_match(cell.get_top_piece().get_player_number(), &number) { 64 | anti_diagonal += 1 65 | } 66 | } 67 | } 68 | 69 | return rows.contains(&4) || columns.contains(&4) || diagonal == 4 || anti_diagonal == 4 70 | } 71 | 72 | // Create 2 dimenson array of cells. 73 | // index in first vec represents row 74 | // index in second vec represent column 75 | // [ 76 | // [c, c, c, c], 77 | // [c, c, c, c], 78 | // [c, c, c, c], 79 | // [c, c, c, c] 80 | // ] 81 | fn build_cells() -> Vec> { 82 | vec![vec![Cell::new(); 4]; 4] 83 | } 84 | } 85 | 86 | #[derive(Debug, Clone)] 87 | pub struct Cell { 88 | state: Vec, 89 | } 90 | 91 | impl Cell { 92 | pub fn new() -> Cell { 93 | Cell { state: Vec::with_capacity(4) } 94 | } 95 | 96 | pub fn add(&mut self, gobblet: Gobblet) { 97 | &self.state.push(gobblet); 98 | } 99 | 100 | pub fn remove(&mut self) -> Gobblet { 101 | self.state.pop().unwrap() 102 | } 103 | 104 | pub fn can_add(&self, pending_gobblet: &Gobblet) -> bool { 105 | if &self.state.is_empty() == &true { 106 | return true; 107 | } 108 | let sizes = [GobbletSize::Tiny, GobbletSize::Small, GobbletSize::Medium, GobbletSize::Large]; 109 | let top_piece = &self.get_top_piece(); 110 | let index_top = &sizes.iter().position(|&g: &GobbletSize| &g == top_piece.get_size()).unwrap(); 111 | let index_pending = &sizes.iter().position(|&d: &GobbletSize| &d == pending_gobblet.get_size()).unwrap(); 112 | index_pending > index_top 113 | } 114 | 115 | pub fn can_remove(&self, player: &PlayerNumber) -> bool { 116 | if &self.state.is_empty() == &true { 117 | return false; 118 | } 119 | 120 | let top_piece = &self.get_top_piece(); 121 | player_number_match(top_piece.get_player_number(), player) 122 | } 123 | 124 | fn is_empty(&self) -> bool { 125 | &self.state.len() == &0 126 | } 127 | 128 | pub fn get_top_piece(&self) -> &Gobblet { 129 | &self.state[self.state.len() - 1] 130 | } 131 | 132 | } 133 | 134 | 135 | #[cfg(test)] 136 | mod board_tests { 137 | use super::{PlayerNumber, Board, Coord, Gobblet, GobbletSize}; 138 | 139 | #[test] 140 | fn add_piece_to_board_should_return_none_if_added_successfully() { 141 | // Given 142 | let mut b = Board::new(); 143 | b.add_piece_to_board(&Coord::new(1, 3), Gobblet::new(GobbletSize::Medium, PlayerNumber::One)); 144 | 145 | // When 146 | let r = b.add_piece_to_board(&Coord::new(1, 3), Gobblet::new(GobbletSize::Large, PlayerNumber::One)); 147 | 148 | match r { 149 | Some(_) => assert_eq!(false, true, "Piece was reutrned!"), 150 | None => assert_eq!(true, true) 151 | }; 152 | } 153 | 154 | #[test] 155 | fn has_won_should_return_false_if_no_one_has_won() { 156 | let b = Board::new(); 157 | let r = b.has_won(PlayerNumber::One); 158 | assert_eq!(r, false); 159 | } 160 | 161 | #[test] 162 | fn has_won_should_return_true_if_a_row_is_filled() { 163 | let mut b = Board::new(); 164 | let gobblet = Gobblet::new(GobbletSize::Large, PlayerNumber::One); 165 | for i in 0..4 { 166 | b.add_piece_to_board(&Coord::new(1, i), gobblet.clone()); 167 | } 168 | let r = b.has_won(PlayerNumber::One); 169 | assert_eq!(r, true); 170 | } 171 | 172 | #[test] 173 | fn has_won_should_return_true_if_a_column_is_filled() { 174 | let mut b = Board::new(); 175 | let gobblet = Gobblet::new(GobbletSize::Large, PlayerNumber::One); 176 | for i in 0..4 { 177 | b.add_piece_to_board(&Coord::new(i, 1), gobblet.clone()); 178 | } 179 | let r = b.has_won(PlayerNumber::One); 180 | assert_eq!(r, true); 181 | } 182 | 183 | #[test] 184 | fn has_won_should_return_true_if_diagonal_filled() { 185 | let mut b = Board::new(); 186 | let gobblet = Gobblet::new(GobbletSize::Large, PlayerNumber::One); 187 | for i in 0..4 { 188 | b.add_piece_to_board(&Coord::new(i, i), gobblet.clone()); 189 | } 190 | let r = b.has_won(PlayerNumber::One); 191 | assert_eq!(r, true); 192 | } 193 | 194 | #[test] 195 | fn has_won_should_return_true_if_anti_diagonal_filled() { 196 | let mut b = Board::new(); 197 | let gobblet = Gobblet::new(GobbletSize::Large, PlayerNumber::One); 198 | 199 | b.add_piece_to_board(&Coord::new(0, 3), gobblet.clone()); 200 | b.add_piece_to_board(&Coord::new(1, 2), gobblet.clone()); 201 | b.add_piece_to_board(&Coord::new(2, 1), gobblet.clone()); 202 | b.add_piece_to_board(&Coord::new(3, 0), gobblet.clone()); 203 | 204 | let r = b.has_won(PlayerNumber::One); 205 | assert_eq!(r, true); 206 | } 207 | 208 | #[test] 209 | fn has_won_should_return_false_if_anti_diagonal_not_filled() { 210 | let mut b = Board::new(); 211 | let gobblet = Gobblet::new(GobbletSize::Large, PlayerNumber::One); 212 | 213 | b.add_piece_to_board(&Coord::new(0, 3), gobblet.clone()); 214 | b.add_piece_to_board(&Coord::new(1, 2), gobblet.clone()); 215 | b.add_piece_to_board(&Coord::new(2, 2), gobblet.clone()); 216 | b.add_piece_to_board(&Coord::new(3, 0), gobblet.clone()); 217 | 218 | let r = b.has_won(PlayerNumber::One); 219 | assert_eq!(r, false); 220 | } 221 | } 222 | 223 | 224 | #[cfg(test)] 225 | mod cell_tests { 226 | use super::{Cell, Gobblet, GobbletSize, PlayerNumber}; 227 | 228 | #[test] 229 | fn can_add_should_return_true_if_cell_is_empty() { 230 | let c = Cell::new(); 231 | let r = c.can_add(&Gobblet::new(GobbletSize::Tiny, PlayerNumber::Two)); 232 | assert_eq!(r, true); 233 | } 234 | 235 | #[test] 236 | fn can_add_should_return_true_if_gobblet_is_larger_than_top_piece() { 237 | // Given Tiny Piece in cell 238 | let mut c = Cell::new(); 239 | c.add(Gobblet::new(GobbletSize::Tiny, PlayerNumber::Two)); 240 | 241 | // When Small, Medium, Large 242 | let s = c.can_add(&Gobblet::new(GobbletSize::Small, PlayerNumber::Two)); 243 | let m = c.can_add(&Gobblet::new(GobbletSize::Medium, PlayerNumber::Two)); 244 | let l = c.can_add(&Gobblet::new(GobbletSize::Large, PlayerNumber::Two)); 245 | 246 | assert_eq!(s, true); 247 | assert_eq!(m, true); 248 | assert_eq!(l, true); 249 | } 250 | 251 | 252 | #[test] 253 | fn can_add_should_return_false_if_gobblet_is_smaller_than_top_piece() { 254 | // Given Large Piece in cell 255 | let mut c = Cell::new(); 256 | c.add(Gobblet::new(GobbletSize::Large, PlayerNumber::Two)); 257 | 258 | // When Small, Medium, Large 259 | let s = c.can_add(&Gobblet::new(GobbletSize::Small, PlayerNumber::Two)); 260 | let m = c.can_add(&Gobblet::new(GobbletSize::Medium, PlayerNumber::Two)); 261 | let l = c.can_add(&Gobblet::new(GobbletSize::Large, PlayerNumber::Two)); 262 | 263 | assert_eq!(s, false); 264 | assert_eq!(m, false); 265 | assert_eq!(l, false); 266 | } 267 | 268 | #[test] 269 | fn can_add_should_return_false_if_gobblet_is_same_size() { 270 | // Given Large Piece in cell 271 | let mut c = Cell::new(); 272 | c.add(Gobblet::new(GobbletSize::Small, PlayerNumber::Two)); 273 | 274 | // When Small, Medium, Large 275 | let s = c.can_add(&Gobblet::new(GobbletSize::Small, PlayerNumber::Two)); 276 | 277 | assert_eq!(s, false); 278 | } 279 | } -------------------------------------------------------------------------------- /src/game/logic/gobblet.rs: -------------------------------------------------------------------------------- 1 | use crate::game::utils::PlayerNumber; 2 | 3 | #[derive(Debug, Clone, Copy, PartialEq)] 4 | pub enum GobbletSize { 5 | Tiny, 6 | Small, 7 | Medium, 8 | Large 9 | } 10 | 11 | 12 | #[derive(Debug, Clone)] 13 | pub struct Gobblet { 14 | size: GobbletSize, 15 | player_number: PlayerNumber, 16 | } 17 | 18 | impl Gobblet { 19 | pub fn new(size: GobbletSize, player_number: PlayerNumber) -> Gobblet { 20 | Gobblet{ size, player_number } 21 | } 22 | 23 | pub fn get_size(&self) -> &GobbletSize { 24 | return &self.size 25 | } 26 | 27 | pub fn get_player_number(&self) -> &PlayerNumber { 28 | &self.player_number 29 | } 30 | } 31 | 32 | // Player Tests 33 | #[cfg(test)] 34 | mod tests { 35 | use super::{Gobblet, GobbletSize}; 36 | use super::PlayerNumber; 37 | 38 | #[test] 39 | fn new_should_create_gobblet_with_size() { 40 | let p = Gobblet::new(GobbletSize::Tiny, PlayerNumber::One); 41 | 42 | match p.size { 43 | GobbletSize::Tiny => assert_eq!(true, true), 44 | _ => assert_eq!(false, true) 45 | }; 46 | } 47 | } -------------------------------------------------------------------------------- /src/game/logic/hand.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use crate::game::logic::gobblet::{Gobblet, GobbletSize}; 3 | use crate::game::utils::PlayerNumber; 4 | 5 | #[derive(Debug)] 6 | pub struct Hand { 7 | state: HashMap>, 8 | } 9 | 10 | impl Hand { 11 | pub fn new(number: PlayerNumber) -> Hand { 12 | let mut state = HashMap::new(); 13 | 14 | for i in 1..4 { 15 | let mut group = Vec::with_capacity(4); 16 | group.push(Gobblet::new(GobbletSize::Tiny, number)); 17 | group.push(Gobblet::new(GobbletSize::Small, number)); 18 | group.push(Gobblet::new(GobbletSize::Medium, number)); 19 | group.push(Gobblet::new(GobbletSize::Large, number)); 20 | 21 | state.insert(i, group); 22 | } 23 | Hand { state } 24 | } 25 | 26 | pub fn remove_piece(&mut self, section: u8) -> Option { 27 | match self.state.get_mut(§ion) { 28 | Some(pieces) => pieces.pop(), 29 | None => None 30 | } 31 | } 32 | 33 | pub fn add_piece(&mut self, gobblet: Gobblet, section: u8) { 34 | self.state.get_mut(§ion).unwrap().push(gobblet); 35 | } 36 | } -------------------------------------------------------------------------------- /src/game/logic/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod gobblet; 2 | pub mod hand; 3 | pub mod player; 4 | pub mod board; -------------------------------------------------------------------------------- /src/game/logic/player.rs: -------------------------------------------------------------------------------- 1 | use crate::game::logic::hand::Hand; 2 | use crate::game::logic::gobblet::Gobblet; 3 | use crate::game::utils::PlayerNumber; 4 | 5 | #[derive(Debug)] 6 | pub struct Player { 7 | name: String, 8 | hand: Hand, 9 | } 10 | 11 | impl Player { 12 | pub fn new(name: String, number: PlayerNumber) -> Player { 13 | let hand = Hand::new(number); 14 | Player{ name, hand } 15 | } 16 | 17 | pub fn remove_piece_from_hand(&mut self, hand_section: u8) -> Option { 18 | self.hand.remove_piece(hand_section) 19 | } 20 | 21 | pub fn add_piece_to_hand(&mut self, gobblet: Gobblet, hand_section: u8) { 22 | self.hand.add_piece(gobblet, hand_section); 23 | } 24 | } 25 | 26 | 27 | // Player Tests 28 | #[cfg(test)] 29 | mod tests { 30 | use super::Player; 31 | use crate::game::logic::gobblet::GobbletSize; 32 | use crate::game::utils::PlayerNumber; 33 | 34 | fn create_player(name: String, number: PlayerNumber) -> Player { 35 | Player::new(name, number) 36 | } 37 | 38 | #[test] 39 | fn remove_piece_from_hand_should_remove_four_pieces_in_order() { 40 | let mut p = create_player(String::from("Alex"), PlayerNumber::One); 41 | let mut count = 0u32; 42 | 43 | loop { 44 | count += 1; 45 | let piece = p.remove_piece_from_hand(1); 46 | 47 | let piece = match piece { 48 | Some(p) => p, 49 | None => { 50 | // There should only be 4 pieces, 51 | // which means 5th access to spot 1 in hand is None 52 | assert_eq!(count, 5); 53 | break; 54 | } 55 | }; 56 | 57 | match piece.get_size() { 58 | &GobbletSize::Large => assert_eq!(count, 1), 59 | &GobbletSize::Medium => assert_eq!(count, 2), 60 | &GobbletSize::Small => assert_eq!(count, 3), 61 | &GobbletSize::Tiny => assert_eq!(count, 4), 62 | } 63 | continue; 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /src/game/manager.rs: -------------------------------------------------------------------------------- 1 | 2 | use crate::game::utils::coord::Coord; 3 | use crate::game::utils::{PlayerNumber, player_number_match}; 4 | use super::logic::player::Player; 5 | use super::logic::gobblet::{Gobblet}; 6 | use super::logic::board::{Board}; 7 | 8 | use js_sys::Math; 9 | 10 | #[derive(Debug)] 11 | pub struct Manager { 12 | player1: Player, 13 | player2: Player, 14 | board: Board, 15 | turn: PlayerNumber, 16 | } 17 | 18 | impl Manager { 19 | pub fn new(name1: String, name2: String) -> Self { 20 | let board = Board::new(); 21 | let player1 = Player::new(name1, PlayerNumber::One); 22 | let player2 = Player::new(name2, PlayerNumber::Two); 23 | 24 | Manager{ player1, player2, board, turn: Manager::random_turn() } 25 | } 26 | 27 | pub fn move_gobblet_from_hand_to_board(&mut self, coord: &Coord, quadrant: u8) -> Option { 28 | match self.get_mut_player().remove_piece_from_hand(quadrant) { 29 | Some(gobblet) => self.board.add_piece_to_board(coord, gobblet), 30 | None => None, 31 | } 32 | } 33 | 34 | pub fn move_gobblet_on_board(&mut self, source: &Coord, destination: &Coord) -> Option { 35 | let gobblet = match self.board.remove_piece_from_board(source, &self.turn) { 36 | Some(g) => g, 37 | None => panic!("Expected piece to exist on board") 38 | }; 39 | 40 | self.board.add_piece_to_board(destination, gobblet) 41 | } 42 | 43 | pub fn return_gobblet_to_board(&mut self, coord: &Coord, gobblet: Gobblet) { 44 | match self.board.add_piece_to_board(coord, gobblet) { 45 | Some(_) => panic!("Failed to return piece to {:?}", coord), 46 | None => () 47 | } 48 | } 49 | 50 | pub fn return_gobblet_to_hand(&mut self, gobblet: Gobblet, section: u8) { 51 | self.get_mut_player().add_piece_to_hand(gobblet, section); 52 | } 53 | 54 | pub fn has_won(&self) -> Option { 55 | if self.board.has_won(PlayerNumber::One) { 56 | return Some(PlayerNumber::One) 57 | } else if self.board.has_won(PlayerNumber::Two) { 58 | return Some(PlayerNumber::Two) 59 | } 60 | return None 61 | } 62 | 63 | pub fn get_turn(&self) -> &PlayerNumber { 64 | &self.turn 65 | } 66 | 67 | pub fn change_turn(&mut self) { 68 | self.turn = match self.turn { 69 | PlayerNumber::One => PlayerNumber::Two, 70 | PlayerNumber::Two => PlayerNumber::One, 71 | }; 72 | } 73 | 74 | fn get_mut_player(&mut self) -> &mut Player { 75 | if player_number_match(&self.turn, &PlayerNumber::One) { 76 | &mut self.player1 77 | } else { 78 | &mut self.player2 79 | } 80 | } 81 | 82 | fn random_turn() -> PlayerNumber { 83 | return if Math::random() > 0.5 { 84 | PlayerNumber::One 85 | } else { 86 | PlayerNumber::Two 87 | } 88 | } 89 | } 90 | 91 | -------------------------------------------------------------------------------- /src/game/mod.rs: -------------------------------------------------------------------------------- 1 | 2 | pub mod utils; 3 | mod logic; 4 | pub mod ui; 5 | pub mod manager; -------------------------------------------------------------------------------- /src/game/ui/graphics.rs: -------------------------------------------------------------------------------- 1 | use crate::wasm_bindgen::{JsCast, JsValue}; 2 | 3 | use std::f64; 4 | 5 | 6 | use web_sys::{HtmlCanvasElement, CanvasRenderingContext2d, Path2d}; 7 | 8 | use super::shapes::{Rectangle, Circle}; 9 | use super::interaction::Interaction; 10 | use crate::game::utils::coord::Coord; 11 | use crate::game::utils::PlayerNumber; 12 | 13 | 14 | #[derive(Debug, Clone)] 15 | pub struct Graphics { 16 | pub interaction: Interaction, 17 | element: HtmlCanvasElement, 18 | context: CanvasRenderingContext2d, 19 | rectangles: Vec, 20 | circles: Vec, 21 | } 22 | 23 | impl Graphics { 24 | 25 | pub fn new(element: HtmlCanvasElement) -> Graphics { 26 | let context = element 27 | .get_context("2d") 28 | .unwrap() 29 | .unwrap() 30 | .dyn_into::() 31 | .unwrap(); 32 | 33 | let rectangles = Graphics::create_board(&context, &element); 34 | let circles = Graphics::create_hand(&context); 35 | let interaction = Interaction::new(); 36 | Graphics { 37 | interaction, 38 | element, 39 | context, 40 | rectangles, 41 | circles, 42 | } 43 | } 44 | 45 | pub fn get_clicked_rectangle_index(&self, x: f64, y: f64) -> isize { 46 | let mut index: isize = -1; 47 | for (i, r) in self.rectangles.iter().enumerate() { 48 | if self.context.is_point_in_path_with_path_2d_and_f64(r.get_path(), x, y) { 49 | index = i as isize; 50 | break; 51 | } 52 | } 53 | index 54 | } 55 | 56 | pub fn get_coord_for_rectangle(&self, index: usize) -> &Coord { 57 | self.rectangles.get(index).unwrap().get_coord() 58 | } 59 | 60 | pub fn update_circle_pos(&mut self, x: f64, y: f64) { 61 | let index = self.interaction.get_chosen_circle() as usize; 62 | let circle = self.circles.get_mut(index).unwrap(); 63 | circle.set_pos(x, y); 64 | } 65 | 66 | pub fn get_circle_quadrant(&self) -> u8 { 67 | let index = self.interaction.get_chosen_circle() as usize; 68 | self.circles.get(index).unwrap().get_quadrant() 69 | } 70 | 71 | pub fn get_circle(&self) -> &Circle { 72 | let index = self.interaction.get_chosen_circle(); 73 | return if index > -1 { 74 | self.circles.get(index as usize).unwrap() 75 | } else { 76 | panic!("Cannot get circle that is out of range"); 77 | } 78 | } 79 | 80 | pub fn set_largest_clicked_circle(&self, x: f64, y: f64) { 81 | let mut index: isize = -1; 82 | let mut clicked_circles = Vec::new(); 83 | 84 | for (i, c) in self.circles.iter().enumerate() { 85 | if self.context.is_point_in_path_with_path_2d_and_f64(c.get_path(), x, y) { 86 | clicked_circles.push((i, c)); 87 | index = i as isize; 88 | } 89 | } 90 | 91 | if clicked_circles.len() == 0 { 92 | self.interaction.set_chosen_circle(index); 93 | return; 94 | } 95 | 96 | // sort circles by largest -> smallest 97 | clicked_circles.sort_by(|a, b| b.1.get_size().partial_cmp(&a.1.get_size()).unwrap()); 98 | index = clicked_circles.get(0).unwrap().0 as isize; 99 | self.interaction.set_chosen_circle(index) 100 | } 101 | 102 | pub fn position_circle_center_of_rectangle(&mut self, rectange_index: usize) { 103 | let circle_index = self.interaction.get_chosen_circle() as usize; 104 | 105 | let rectangle = self.rectangles.get(rectange_index).unwrap(); 106 | let circle = self.circles.get_mut(circle_index).unwrap(); 107 | let (x, y) = rectangle.get_pos(); 108 | circle.set_pos(x, y); 109 | self.draw_circles(); 110 | } 111 | 112 | 113 | pub fn draw_circles(&mut self) { 114 | self.redraw_board(); 115 | 116 | let yellow = JsValue::from_str("#FFB85F"); 117 | let yellow_border = JsValue::from_str("#FFA433"); 118 | let red = JsValue::from_str("#F67280"); 119 | let red_border = JsValue::from_str("#C4421A"); 120 | 121 | for circle in &mut self.circles { 122 | let path = Path2d::new().unwrap(); 123 | let (x, y) = circle.get_pos(); 124 | let size = circle.get_size(); 125 | 126 | match circle.get_player() { 127 | PlayerNumber::One => { 128 | &self.context.set_fill_style(&yellow); 129 | &self.context.set_stroke_style(&yellow_border); 130 | }, 131 | PlayerNumber::Two => { 132 | &self.context.set_fill_style(&red); 133 | &self.context.set_stroke_style(&red_border); 134 | } 135 | }; 136 | 137 | path.arc(x, y, size, 0.0, 2.0 * f64::consts::PI).unwrap(); 138 | 139 | self.context.set_line_width(5.0); 140 | self.context.stroke_with_path(&path); 141 | self.context.fill_with_path_2d(&path); 142 | 143 | circle.set_path(path); 144 | } 145 | } 146 | 147 | fn redraw_board(&self) { 148 | let light_purple: JsValue = JsValue::from_str("#6C5B7B"); 149 | self.context.set_fill_style(&light_purple); 150 | self.context.fill_rect(0.0, 0.0, self.element.width() as f64, self.element.height() as f64); 151 | 152 | // board 153 | let w = 400.0; 154 | let h = 400.0; 155 | let n_row = 4.0; 156 | let n_col = 4.0; 157 | 158 | let w: f64 = w / n_row; // width of block 159 | let h: f64 = h / n_col; // height of block 160 | 161 | // colors 162 | let sea = JsValue::from_str("#5f506c"); 163 | let foam = JsValue::from_str("#867297"); 164 | 165 | let offset = (100.0, 200.0); 166 | for i in 0..n_row as u8 { // row 167 | for j in 0..(n_col as u8) { // column 168 | // cast as floats 169 | let j = j as f64; 170 | let i = i as f64; 171 | 172 | if i % 2.0 == 0.0 { 173 | if j % 2.0 == 0.0 { self.context.set_fill_style(&foam); } else { self.context.set_fill_style(&sea); }; 174 | } else { 175 | if j % 2.0 == 0.0 { self.context.set_fill_style(&sea); } else { self.context.set_fill_style(&foam); }; 176 | } 177 | 178 | let x = j * w + offset.0; 179 | let y = i * h + offset.1; 180 | 181 | let path = Path2d::new().unwrap(); 182 | path.rect(x, y, w, h); 183 | self.context.fill_with_path_2d(&path); 184 | } 185 | } 186 | } 187 | 188 | fn create_hand(context: &CanvasRenderingContext2d) -> Vec { 189 | fn piece_renderer(context: &CanvasRenderingContext2d, quadrant: usize, size: usize, player: PlayerNumber, y: f64) -> Circle { 190 | let coord = match quadrant { 191 | 1 => (200.0, y), 192 | 2 => (300.0, y), 193 | 3 => (400.0, y), 194 | _ => (0.0, 0.0), 195 | }; 196 | let size = (10 * size) as f64 ; 197 | 198 | let path = Path2d::new().unwrap(); 199 | path.arc(coord.0, coord.1, size, 0.0, 2.0 * f64::consts::PI).unwrap(); 200 | 201 | context.set_line_width(5.0); 202 | context.stroke_with_path(&path); 203 | context.fill_with_path_2d(&path); 204 | 205 | let circle = Circle::new(path, quadrant as u8, player, coord.0, coord.1, size); 206 | circle 207 | } 208 | 209 | let mut circles: Vec = Vec::with_capacity(12); 210 | let yellow = JsValue::from_str("#FFB85F"); 211 | let yellow_border = JsValue::from_str("#FFA433"); 212 | let red = JsValue::from_str("#F67280"); 213 | let red_border = JsValue::from_str("#C4421A"); 214 | 215 | for size in 1..5 { 216 | for player in 1..3 { 217 | 218 | let y: f64; 219 | let p: PlayerNumber; 220 | match player { 221 | 1 => { 222 | context.set_fill_style(&yellow); 223 | context.set_stroke_style(&yellow_border); 224 | y = 100.0; 225 | p = PlayerNumber::One; 226 | }, 227 | 2 => { 228 | context.set_fill_style(&red); 229 | context.set_stroke_style(&red_border); 230 | y = 700.0; 231 | p = PlayerNumber::Two; 232 | }, 233 | _ => panic!("Cannot have more than two players!") 234 | }; 235 | 236 | for quadrant in 1..4 { 237 | let circle = piece_renderer(context, quadrant, size, p, y); 238 | circles.push(circle); 239 | } 240 | } 241 | } 242 | circles 243 | } 244 | 245 | fn create_board(context: &CanvasRenderingContext2d, element: &HtmlCanvasElement) -> Vec { 246 | let light_purple: JsValue = JsValue::from_str("#6C5B7B"); 247 | context.set_fill_style(&light_purple); 248 | context.fill_rect(0.0, 0.0, element.width() as f64, element.height() as f64); 249 | 250 | // board 251 | let w = 400.0; 252 | let h = 400.0; 253 | let n_row = 4.0; 254 | let n_col = 4.0; 255 | 256 | let w: f64 = w / n_row; // width of block 257 | let h: f64 = h / n_col; // height of block 258 | 259 | // colors 260 | let sea = JsValue::from_str("#5f506c"); 261 | let foam = JsValue::from_str("#867297"); 262 | 263 | let offset = (100.0, 200.0); 264 | let mut rectangles: Vec = Vec::with_capacity(16); 265 | for i in 0..n_row as u8 { // row 266 | for j in 0..(n_col as u8) { // column 267 | // cast as floats 268 | let j = j as f64; 269 | let i = i as f64; 270 | 271 | if i % 2.0 == 0.0 { 272 | if j % 2.0 == 0.0 { context.set_fill_style(&foam); } else { context.set_fill_style(&sea); }; 273 | } else { 274 | if j % 2.0 == 0.0 { context.set_fill_style(&sea); } else { context.set_fill_style(&foam); }; 275 | } 276 | 277 | let x = j * w + offset.0; 278 | let y = i * h + offset.1; 279 | 280 | let coord = Coord::new(i as u8, j as u8); 281 | let path = Path2d::new().unwrap(); 282 | path.rect(x, y, w, h); 283 | context.fill_with_path_2d(&path); 284 | 285 | let rectangle = Rectangle::new(path, coord, x + (0.5 * w), y + (0.5 * h)); 286 | rectangles.push(rectangle); 287 | } 288 | } 289 | rectangles 290 | } 291 | } -------------------------------------------------------------------------------- /src/game/ui/interaction.rs: -------------------------------------------------------------------------------- 1 | use std::cell::{Cell}; 2 | use std::rc::Rc; 3 | 4 | #[derive(Debug, Clone)] 5 | pub struct Interaction { 6 | pressed: Rc>, 7 | chosen_circle: Rc>, 8 | initial_rectangle: Rc>, 9 | ending_rectangle: Rc> 10 | } 11 | 12 | impl Interaction { 13 | pub fn new() -> Interaction { 14 | let pressed = Rc::new(Cell::new(false)); 15 | let chosen_circle = Rc::new(Cell::new(-1)); 16 | let initial_rectangle = Rc::new(Cell::new(-1)); 17 | let ending_rectangle = Rc::new(Cell::new(-1)); 18 | 19 | Interaction { 20 | pressed, 21 | chosen_circle, 22 | initial_rectangle, 23 | ending_rectangle 24 | } 25 | } 26 | 27 | pub fn set_pressed(&mut self, state: bool) { 28 | &self.pressed.set(state); 29 | } 30 | 31 | pub fn is_pressed(&self) -> bool { 32 | self.pressed.get() 33 | } 34 | 35 | pub fn set_initial_rectangle(&self, index: isize) { 36 | self.initial_rectangle.set(index); 37 | } 38 | 39 | pub fn get_initial_rectangle(&self) -> isize { 40 | self.initial_rectangle.get() 41 | } 42 | 43 | pub fn get_chosen_circle(&self) -> isize { 44 | self.chosen_circle.get() 45 | } 46 | 47 | pub fn set_chosen_circle(&self, value: isize) { 48 | self.chosen_circle.set(value) 49 | } 50 | 51 | pub fn reset_state(&self) { 52 | self.pressed.set(false); 53 | self.chosen_circle.set(-1); 54 | self.initial_rectangle.set(-1); 55 | self.ending_rectangle.set(-1); 56 | } 57 | 58 | } -------------------------------------------------------------------------------- /src/game/ui/mod.rs: -------------------------------------------------------------------------------- 1 | mod interaction; 2 | pub mod shapes; 3 | pub mod graphics; 4 | -------------------------------------------------------------------------------- /src/game/ui/shapes.rs: -------------------------------------------------------------------------------- 1 | use web_sys::Path2d; 2 | use crate::game::utils::coord::Coord; 3 | use crate::game::utils::PlayerNumber; 4 | 5 | #[derive(Debug, Clone)] 6 | pub struct Rectangle { 7 | path: Path2d, 8 | coord: Coord, 9 | x: f64, 10 | y: f64, 11 | } 12 | 13 | impl Rectangle { 14 | pub fn new(path: Path2d, coord: Coord, x: f64, y: f64) -> Rectangle { 15 | Rectangle { path, coord, x, y } 16 | } 17 | 18 | pub fn get_path(&self) -> &Path2d { 19 | &self.path 20 | } 21 | 22 | pub fn get_coord(&self) -> &Coord { 23 | &self.coord 24 | } 25 | 26 | pub fn get_pos(&self) -> (f64, f64) { 27 | (self.x, self.y) 28 | } 29 | } 30 | 31 | #[derive(Debug, Clone)] 32 | pub struct Circle { 33 | path: Path2d, 34 | quadrant: u8, 35 | player: PlayerNumber, 36 | x: f64, 37 | y: f64, 38 | size: f64 39 | } 40 | 41 | impl Circle { 42 | pub fn new(path: Path2d, quadrant: u8, player: PlayerNumber, x: f64, y: f64, size: f64) -> Circle { 43 | Circle { path, quadrant, player, x, y, size } 44 | } 45 | 46 | pub fn get_path(&self) -> &Path2d { 47 | &self.path 48 | } 49 | 50 | pub fn set_path(&mut self, path: Path2d) { 51 | self.path = path; 52 | } 53 | 54 | pub fn get_quadrant(&self) -> u8 { 55 | self.quadrant 56 | } 57 | 58 | pub fn get_player(&self) -> &PlayerNumber { 59 | &self.player 60 | } 61 | 62 | pub fn get_pos(&self) -> (f64, f64) { 63 | (self.x, self.y) 64 | } 65 | 66 | pub fn set_pos(&mut self, x: f64, y: f64) { 67 | self.x = x; 68 | self.y = y; 69 | } 70 | 71 | pub fn get_size(&self) -> f64 { 72 | self.size 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/game/utils/coord.rs: -------------------------------------------------------------------------------- 1 | 2 | 3 | #[derive(Debug, Clone)] 4 | pub struct Coord(u8, u8); 5 | 6 | impl Coord { 7 | pub fn new(row: u8, column: u8) -> Coord { 8 | Coord(row, column) 9 | } 10 | pub fn get_row(&self) -> &u8 { 11 | &self.0 12 | } 13 | pub fn get_column(&self) -> &u8 { 14 | &self.1 15 | } 16 | } 17 | 18 | // Player Tests 19 | #[cfg(test)] 20 | mod tests { 21 | use super::Coord; 22 | 23 | #[test] 24 | fn new_should_create_coord_with_row_column() { 25 | let p = Coord::new(1, 2); 26 | 27 | assert_eq!(p.get_row(), &1); 28 | assert_eq!(p.get_column(), &2); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/game/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod coord; 2 | 3 | 4 | #[derive(Debug, Clone, Copy)] 5 | pub enum PlayerNumber { 6 | One, 7 | Two 8 | } 9 | 10 | pub fn player_number_match(shape_owner: &PlayerNumber, current_turn: &PlayerNumber) -> bool { 11 | return std::mem::discriminant(shape_owner) == std::mem::discriminant(current_turn) 12 | } -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate js_sys; 2 | extern crate wasm_bindgen; 3 | 4 | mod game; 5 | mod macros; 6 | mod utils; 7 | 8 | use std::cell::{Cell, RefCell}; 9 | use std::rc::Rc; 10 | use web_sys::HtmlCanvasElement; 11 | 12 | use crate::game::manager::Manager; 13 | use crate::game::ui::graphics::Graphics; 14 | use crate::game::utils::player_number_match; 15 | use crate::wasm_bindgen::prelude::*; 16 | use crate::wasm_bindgen::JsCast; 17 | 18 | // When the `wee_alloc` feature is enabled, this uses `wee_alloc` as the global 19 | // allocator. 20 | // 21 | // If you don't want to use `wee_alloc`, you can safely delete this. 22 | #[cfg(feature = "wee_alloc")] 23 | #[global_allocator] 24 | static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; 25 | 26 | #[wasm_bindgen(start)] 27 | pub fn main_js() -> Result<(), JsValue> { 28 | utils::set_panic_hook(); 29 | Ok(()) 30 | } 31 | 32 | // TODO move all the complexity of interaction into graphics. 33 | #[wasm_bindgen] 34 | pub fn start_game(canvas: HtmlCanvasElement, name1: String, name2: String) { 35 | let graphics = Rc::new(RefCell::new(Graphics::new(canvas.clone()))); 36 | let manager = Rc::new(RefCell::new(Manager::new(name1, name2))); 37 | 38 | let original_circle_x_y = Rc::new(Cell::new((0.0, 0.0))); 39 | 40 | // process mousedown 41 | { 42 | let graphics = graphics.clone(); 43 | let manager = manager.clone(); 44 | let original_circle_x_y = original_circle_x_y.clone(); 45 | 46 | let closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| { 47 | let x = event.offset_x() as f64; 48 | let y = event.offset_y() as f64; 49 | let mut graphics = graphics.borrow_mut(); 50 | let manager = manager.borrow(); 51 | 52 | graphics.set_largest_clicked_circle(x, y); 53 | if graphics.interaction.get_chosen_circle() > -1 { 54 | let current_turn = manager.get_turn(); 55 | let shape_owner = graphics.get_circle().get_player(); 56 | 57 | if player_number_match(shape_owner, current_turn) { 58 | graphics.interaction.set_pressed(true); 59 | 60 | let rectangle_index = graphics.get_clicked_rectangle_index(x, y); 61 | graphics.interaction.set_initial_rectangle(rectangle_index); 62 | 63 | original_circle_x_y.set(graphics.get_circle().get_pos()) 64 | } else { 65 | graphics.interaction.reset_state(); 66 | } 67 | } 68 | }) as Box); 69 | canvas 70 | .add_event_listener_with_callback("mousedown", closure.as_ref().unchecked_ref()) 71 | .unwrap(); 72 | closure.forget(); 73 | } 74 | 75 | // process mouse move 76 | { 77 | let graphics = graphics.clone(); 78 | 79 | let closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| { 80 | let mut graphics = graphics.borrow_mut(); 81 | 82 | if graphics.interaction.is_pressed() && graphics.interaction.get_chosen_circle() > -1 { 83 | let x = event.offset_x() as f64; 84 | let y = event.offset_y() as f64; 85 | graphics.update_circle_pos(x, y); 86 | graphics.draw_circles(); 87 | } 88 | }) as Box); 89 | canvas 90 | .add_event_listener_with_callback("mousemove", closure.as_ref().unchecked_ref()) 91 | .unwrap(); 92 | closure.forget(); 93 | } 94 | 95 | //process mouse up 96 | { 97 | let original_circle_x_y = original_circle_x_y.clone(); 98 | let graphics = graphics.clone(); 99 | let manager = manager.clone(); 100 | 101 | let closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| { 102 | let x = event.offset_x() as f64; 103 | let y = event.offset_y() as f64; 104 | let mut graphics = graphics.borrow_mut(); 105 | let mut manager = manager.borrow_mut(); 106 | let ending_rectangle = graphics.get_clicked_rectangle_index(x, y); 107 | // user didn't click on circle 108 | if graphics.interaction.get_chosen_circle() < 0 { 109 | graphics.interaction.set_pressed(false); 110 | graphics.interaction.set_initial_rectangle(-1); 111 | return; 112 | } 113 | 114 | // user didn't drop a circle on a rectangle 115 | if ending_rectangle < 0 { 116 | let (original_x, original_y) = original_circle_x_y.get(); 117 | graphics.update_circle_pos(original_x, original_y); 118 | graphics.draw_circles(); 119 | graphics.interaction.reset_state(); 120 | return; 121 | } 122 | 123 | // piece came from hand 124 | let ending_rectangle = ending_rectangle as usize; 125 | match graphics.interaction.get_initial_rectangle() < 0 { 126 | true => { 127 | let coord = graphics.get_coord_for_rectangle(ending_rectangle); 128 | let quadrant = graphics.get_circle_quadrant(); 129 | 130 | match manager.move_gobblet_from_hand_to_board(coord, quadrant) { 131 | Some(gobblet) => { 132 | // return piece to hand 133 | manager.return_gobblet_to_hand(gobblet, quadrant); 134 | // repaint it back at the hand 135 | let (original_x, original_y) = original_circle_x_y.get(); 136 | graphics.update_circle_pos( 137 | original_x, 138 | original_y, 139 | ); 140 | graphics.draw_circles(); 141 | // reset interaction state 142 | graphics.interaction.reset_state(); 143 | return; 144 | } 145 | None => graphics.position_circle_center_of_rectangle( 146 | ending_rectangle, 147 | ), 148 | }; 149 | } 150 | false => { 151 | // piece came from board 152 | let source = graphics.get_coord_for_rectangle(graphics.interaction.get_initial_rectangle() as usize); 153 | let destination = graphics.get_coord_for_rectangle(ending_rectangle); 154 | 155 | match manager.move_gobblet_on_board(source, destination) { 156 | None => graphics.position_circle_center_of_rectangle( 157 | ending_rectangle, 158 | ), 159 | Some(gobblet) => { 160 | // return the piece to source 161 | manager.return_gobblet_to_board(source, gobblet); 162 | 163 | // repaint it at source rectangle 164 | let (original_x, original_y) = original_circle_x_y.get(); 165 | graphics.update_circle_pos( 166 | original_x, 167 | original_y, 168 | ); 169 | graphics.draw_circles(); 170 | graphics.interaction.reset_state(); 171 | return; 172 | } 173 | }; 174 | } 175 | }; 176 | 177 | graphics.interaction.reset_state(); 178 | 179 | match manager.has_won() { 180 | Some(player) => { 181 | log!("Game Over! {:?} won", player); 182 | return; 183 | } 184 | None => manager.change_turn(), 185 | } 186 | }) as Box); 187 | canvas 188 | .add_event_listener_with_callback("mouseup", closure.as_ref().unchecked_ref()) 189 | .unwrap(); 190 | closure.forget(); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/macros.rs: -------------------------------------------------------------------------------- 1 | #![macro_use] 2 | // A macro to provide `println!(..)`-style syntax for `console.log` logging. 3 | #[macro_export] 4 | macro_rules! log { 5 | ( $( $t:tt )* ) => { 6 | web_sys::console::log_1(&format!( $( $t )* ).into()); 7 | } 8 | } 9 | 10 | // A macro to provide `println!(..)`-style syntax for `console.error` logging. 11 | #[macro_export] 12 | macro_rules! err { 13 | ( $( $t:tt )* ) => { 14 | web_sys::console::error_1(&format!( $( $t )* ).into()); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | #![macro_use] 2 | 3 | pub fn set_panic_hook() { 4 | // When the `console_error_panic_hook` feature is enabled, we can call the 5 | // `set_panic_hook` function at least once during initialization, and then 6 | // we will get better error messages if our code ever panics. 7 | // 8 | // For more details see 9 | // https://github.com/rustwasm/console_error_panic_hook#readme 10 | #[cfg(feature = "console_error_panic_hook")] 11 | console_error_panic_hook::set_once(); 12 | } 13 | -------------------------------------------------------------------------------- /static/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | My Rust + Webpack project! 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "jsx": "react", 5 | "lib": ["dom", "es2015"], 6 | "strict": true, 7 | "experimentalDecorators": true, 8 | "esModuleInterop": true, 9 | "module": "esNext", 10 | "target": "es5", 11 | "moduleResolution": "node" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const CopyPlugin = require("copy-webpack-plugin"); 3 | const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin"); 4 | 5 | const dist = path.resolve(__dirname, "dist"); 6 | 7 | module.exports = { 8 | mode: "production", 9 | entry: { 10 | index: "./js/index.tsx", 11 | }, 12 | output: { 13 | path: dist, 14 | filename: "[name].js", 15 | chunkFilename: "[name].chunk.js", 16 | }, 17 | devServer: { 18 | contentBase: dist, 19 | }, 20 | resolve: { 21 | // Add `.ts` and `.tsx` as a resolvable extension. 22 | extensions: [".ts", ".tsx", ".js"], 23 | }, 24 | module: { 25 | rules: [ 26 | // all files with a `.ts` or `.tsx` extension will be handled by `ts-loader` 27 | { test: /\.tsx?$/, loader: "ts-loader" }, 28 | ], 29 | }, 30 | plugins: [ 31 | new CopyPlugin([path.resolve(__dirname, "static")]), 32 | 33 | new WasmPackPlugin({ 34 | crateDirectory: __dirname, 35 | forceMode: "production", 36 | }), 37 | ], 38 | }; 39 | --------------------------------------------------------------------------------