├── .gitignore
├── preview.png
├── src
├── game
│ ├── ui
│ │ ├── mod.rs
│ │ ├── interaction.rs
│ │ ├── shapes.rs
│ │ └── graphics.rs
│ ├── mod.rs
│ ├── logic
│ │ ├── mod.rs
│ │ ├── gobblet.rs
│ │ ├── hand.rs
│ │ ├── player.rs
│ │ └── board.rs
│ ├── utils
│ │ ├── mod.rs
│ │ └── coord.rs
│ └── manager.rs
├── macros.rs
├── utils.rs
└── lib.rs
├── js
├── index.tsx
├── stores
│ ├── use_engines.tsx
│ └── WasmEngine.ts
├── App.tsx
└── components
│ └── Greet.tsx
├── static
├── index.html
└── index.css
├── tsconfig.json
├── README.md
├── package.json
├── webpack.config.js
├── .github
└── workflows
│ └── rust.yml
├── Cargo.toml
└── Cargo.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | pkg
3 | target
4 | dist
--------------------------------------------------------------------------------
/preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Fallenstedt/rust-goblet/HEAD/preview.png
--------------------------------------------------------------------------------
/src/game/ui/mod.rs:
--------------------------------------------------------------------------------
1 | mod interaction;
2 | pub mod shapes;
3 | pub mod graphics;
4 |
--------------------------------------------------------------------------------
/src/game/mod.rs:
--------------------------------------------------------------------------------
1 |
2 | pub mod utils;
3 | mod logic;
4 | pub mod ui;
5 | pub mod manager;
--------------------------------------------------------------------------------
/src/game/logic/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod gobblet;
2 | pub mod hand;
3 | pub mod player;
4 | pub mod board;
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/static/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | My Rust + Webpack project!
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | 
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 | }
--------------------------------------------------------------------------------
| | | |