├── .editorconfig
├── .gitignore
├── .vscode
└── settings.json
├── LICENSE
├── Makefile
├── README.md
├── deploy
├── dev
│ ├── .env
│ ├── Dockerfile
│ └── docker-compose.yml
├── nginx.conf
├── prod
│ ├── .env
│ ├── Dockerfile
│ └── docker-compose.yml
└── test
│ ├── .env
│ ├── Dockerfile
│ └── docker-compose.yml
├── docs
└── images
│ ├── grid_sphere.png
│ ├── quad_sphere.png
│ ├── uv_sphere_pole.jpg
│ └── uv_sphere_side.jpg
├── frontend
├── .env
├── .gitignore
├── .prettierrc
├── README.md
├── index.html
├── libs
│ └── index.ts
├── package.json
├── pnpm-lock.yaml
├── src
│ ├── App.module.css
│ ├── App.tsx
│ ├── api
│ │ ├── api_client.ts
│ │ ├── index.ts
│ │ ├── models.ts
│ │ └── ws_client.ts
│ ├── assets
│ │ ├── favicon.ico
│ │ └── gridMaterial.json
│ ├── auth.ts
│ ├── components
│ │ ├── ModeChooser.tsx
│ │ └── index.ts
│ ├── constants.ts
│ ├── gui
│ │ ├── alert.ts
│ │ ├── game_creation_form.ts
│ │ ├── gui_components.ts
│ │ ├── index.ts
│ │ ├── player_bar.ts
│ │ ├── select_menu.ts
│ │ └── text.ts
│ ├── index.css
│ ├── index.tsx
│ ├── logic
│ │ ├── fields
│ │ │ ├── enum.ts
│ │ │ ├── grid_sphere.ts
│ │ │ ├── interface.ts
│ │ │ └── regular.ts
│ │ ├── game_manager.ts
│ │ ├── games
│ │ │ ├── game.ts
│ │ │ ├── multiplayer_game.ts
│ │ │ └── singleplayer_game.ts
│ │ ├── scene.ts
│ │ └── stone_manager.ts
│ ├── pages
│ │ ├── 404.tsx
│ │ ├── Game.tsx
│ │ ├── Login.tsx
│ │ ├── Settings.tsx
│ │ └── rooms
│ │ │ ├── [id].tsx
│ │ │ └── index.tsx
│ └── router.ts
├── tsconfig.json
└── vite.config.ts
├── gamelib
├── Cargo.lock
├── Cargo.toml
├── rust-toolchain.toml
└── src
│ ├── aliases.rs
│ ├── errors.rs
│ ├── field
│ ├── cube_sphere_builder.rs
│ ├── field.rs
│ ├── field_builder.rs
│ ├── grid_sphere_builder.rs
│ ├── mod.rs
│ └── regular_field_builder.rs
│ ├── file_converters
│ ├── interfaces.rs
│ └── mod.rs
│ ├── game.rs
│ ├── group.rs
│ ├── history
│ ├── manager.rs
│ ├── mod.rs
│ └── models.rs
│ ├── ko_guard.rs
│ ├── lib.rs
│ ├── point.rs
│ ├── state.rs
│ └── tests
│ ├── fixtures
│ ├── game.rs
│ └── mod.rs
│ ├── mod.rs
│ ├── test_cubic_sphere_builder.rs
│ ├── test_grid_sphere_builder.rs
│ ├── test_groups_merge.rs
│ ├── test_ko_rule.rs
│ ├── test_regular_field_builder.rs
│ ├── test_stone_remover.rs
│ └── test_undo.rs
├── server
├── Cargo.lock
├── Cargo.toml
├── libs
│ ├── entity
│ │ ├── Cargo.toml
│ │ └── src
│ │ │ ├── games.rs
│ │ │ ├── histories.rs
│ │ │ ├── history.rs
│ │ │ ├── history_records.rs
│ │ │ ├── lib.rs
│ │ │ ├── rooms.rs
│ │ │ └── users.rs
│ └── migration
│ │ ├── Cargo.lock
│ │ ├── Cargo.toml
│ │ ├── README.md
│ │ └── src
│ │ ├── lib.rs
│ │ ├── m20220101_000001_create_table.rs
│ │ ├── m20230103_213731_games.rs
│ │ ├── m20230104_155530_rooms.rs
│ │ ├── m20230106_155413_histories.rs
│ │ ├── m20230108_204629_history_records.rs
│ │ └── main.rs
└── src
│ ├── app.rs
│ ├── apps
│ ├── games
│ │ ├── mod.rs
│ │ ├── repositories.rs
│ │ ├── routers.rs
│ │ ├── schemas.rs
│ │ └── services.rs
│ ├── histories
│ │ ├── mod.rs
│ │ ├── repositories.rs
│ │ └── schemas.rs
│ ├── mod.rs
│ ├── rooms
│ │ ├── mod.rs
│ │ ├── repositories.rs
│ │ ├── routers.rs
│ │ └── services.rs
│ └── users
│ │ ├── mod.rs
│ │ ├── repositories.rs
│ │ ├── routers.rs
│ │ ├── schemas.rs
│ │ └── services.rs
│ ├── common
│ ├── aliases.rs
│ ├── config.rs
│ ├── db
│ │ ├── connection.rs
│ │ └── mod.rs
│ ├── errors.rs
│ ├── mod.rs
│ └── routing
│ │ ├── app_state.rs
│ │ ├── auth.rs
│ │ └── mod.rs
│ ├── main.rs
│ └── tests
│ ├── fixtures
│ ├── create_room.rs
│ ├── create_user.rs
│ ├── mod.rs
│ └── test_tools.rs
│ ├── mod.rs
│ ├── test_games_crud.rs
│ ├── test_rooms_crud.rs
│ └── test_users_crud.rs
└── wasm
├── Cargo.lock
├── Cargo.toml
├── rust-toolchain.toml
└── src
└── lib.rs
/.editorconfig:
--------------------------------------------------------------------------------
1 | # https://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | end_of_line = lf
6 | insert_final_newline = true
7 |
8 | [*.{js,jsx,ts,tsx,html,css,json}]
9 | charset = utf-8
10 | indent_style = tab
11 | indent_size = 2
12 | trim_trailing_whitespace = true
13 |
14 | [*.rs]
15 | indent_size = 4
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | target/
2 |
3 | *.log
4 |
5 | frontend/node_modules/
6 | frontend/public/build/
7 |
8 | /.env
9 |
10 | .DS_Store
11 |
12 | /Dockerfile
13 | /docker-compose.yml
14 | /volumes/
15 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "rust-analyzer.cargo.extraEnv": {
3 | "RUSTUP_TOOLCHAIN": "nightly"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Kirill Gimranov
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | test_gamelib:
2 | cd ./gamelib && cargo test && cd -
3 |
4 | compile_gamelib_to_wasm:
5 | cd ./wasm &&\
6 | wasm-pack build --target web || true &&\
7 | rm -r ../frontend/src/pkg || true &&\
8 | mv pkg ../frontend/src &&\
9 | cd -
10 |
11 | pnpm_i:
12 | cd ./frontend && pnpm i || true && cd -
13 |
14 | run_dev:
15 | cd ./frontend && pnpm run dev || true && cd -
16 |
17 | front_build:
18 | cd ./frontend && pnpm run build || true && cd -
19 |
20 | test_server:
21 | rm .env Dockerfile docker-compose.yml || true &&\
22 | cp ./deploy/test/* . &&\
23 | cp ./deploy/test/.env . &&\
24 | docker compose down &&\
25 | docker compose build &&\
26 | docker compose run server
27 |
28 | run_prod:
29 | rm .env Dockerfile docker-compose.yml || true &&\
30 | cp ./deploy/prod/* . &&\
31 | cp ./deploy/prod/.env . &&\
32 | mkdir -p ./volumes/postgres_data &&\
33 | docker compose down --remove-orphans &&\
34 | docker compose up -d --build --force-recreate
35 |
36 | run_server:
37 | rm .env Dockerfile docker-compose.yml || true &&\
38 | cp ./deploy/dev/* . &&\
39 | cp ./deploy/dev/.env . &&\
40 | mkdir -p ./volumes/postgres_data &&\
41 | docker compose down --remove-orphans &&\
42 | docker compose up -d --build --force-recreate
43 |
44 | deploy_front:
45 | git branch -D gh-pages || true &&\
46 | git checkout -b gh-pages &&\
47 | make compile_gamelib_to_wasm &&\
48 | make pnpm_i &&\
49 | make front_build &&\
50 | mv ./frontend/dist/* ./ &&\
51 | mv ./frontend/404.html ./ &&\
52 | rm -rf ./frontend &&\
53 | find . -not \( -wholename './.git/*' -or -name 'index.html' -or -wholename './assets/*' -or -name '404.html' \) -delete || true &&\
54 | git add --all &&\
55 | git commit -m "lol" &&\
56 | git push -f -u origin gh-pages &&\
57 | git checkout - &&\
58 | make pnpm_i &&\
59 | make compile_gamelib_to_wasm
60 |
61 |
--------------------------------------------------------------------------------
/deploy/dev/.env:
--------------------------------------------------------------------------------
1 | DATABASE_URL=postgres://dgs:lmao@db/dgs
2 | PORT=8000
3 | ALLOWED_ORIGINS = "http://localhost:3000"
4 | MODE=DEV
5 |
--------------------------------------------------------------------------------
/deploy/dev/Dockerfile:
--------------------------------------------------------------------------------
1 | # Building
2 | FROM rustlang/rust:nightly AS builder
3 |
4 | WORKDIR /app
5 |
6 | # Creating dummy main.rs file
7 | RUN mkdir ./src
8 | RUN echo "fn main(){}" > ./src/main.rs
9 |
10 | # Removing local packages from Cargo.toml
11 | COPY ./server/Cargo.toml .
12 | RUN sed -i "s/^\(\(entity = {\)\|\(migration = {\)\|\(spherical_go_game_lib = {\)\\).*//g" ./Cargo.toml
13 |
14 | # Copying deps and downloading and pre-building them
15 | RUN cargo build
16 |
17 | # Copying all the logic
18 | COPY ../gamelib /gamelib
19 | COPY ./server .
20 |
21 | RUN cargo build
22 |
23 | # Running
24 | FROM debian:11-slim AS runtime
25 | COPY --from=builder /app/target/debug/server /dgs_server
26 | ENTRYPOINT [ "./dgs_server" ]
27 |
--------------------------------------------------------------------------------
/deploy/dev/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.9"
2 |
3 | services:
4 | server:
5 | container_name: "dgs-server"
6 | build: .
7 | depends_on:
8 | - db
9 | env_file:
10 | - .env
11 | ports:
12 | - "${PORT}:${PORT}"
13 |
14 | db:
15 | container_name: "dgs-db"
16 | image: postgres:15-alpine
17 | environment:
18 | POSTGRES_DB: "dgs"
19 | POSTGRES_USER: "dgs"
20 | POSTGRES_PASSWORD: "lmao"
21 | ports:
22 | - "5432:5432"
23 | volumes:
24 | - ./volumes/postgres_data:/var/lib/postgresql/data/
25 |
--------------------------------------------------------------------------------
/deploy/nginx.conf:
--------------------------------------------------------------------------------
1 | server {
2 | include mime.types;
3 | types {
4 | application/wasm wasm;
5 | text/javascript js;
6 | }
7 |
8 | server_name dgs.dominux.site;
9 |
10 | root /var/www/DGS_frontend;
11 |
12 | index index.html;
13 |
14 | # frontend
15 | location / {
16 | try_files $uri $uri/ /index.html;
17 | }
18 |
19 | # game websocket
20 | location /api/games/ws/ {
21 | proxy_pass http://127.0.0.1:8100/games/ws/;
22 | proxy_http_version 1.1;
23 | proxy_set_header Upgrade $http_upgrade;
24 | proxy_set_header Connection "Upgrade";
25 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
26 | }
27 |
28 | # backend
29 | location /api/ {
30 | proxy_pass http://127.0.0.1:8100/;
31 | proxy_set_header Host $host;
32 | proxy_redirect off;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/deploy/prod/.env:
--------------------------------------------------------------------------------
1 | DATABASE_URL=postgres://dgs:lmao@db/dgs
2 | PORT=8100
3 | ALLOWED_ORIGINS = "https://dominux.github.io"
4 | MODE=PROD
5 |
--------------------------------------------------------------------------------
/deploy/prod/Dockerfile:
--------------------------------------------------------------------------------
1 | # Building
2 | FROM rustlang/rust:nightly AS builder
3 |
4 | WORKDIR /app
5 |
6 | # Creating dummy main.rs file
7 | RUN mkdir ./src
8 | RUN echo "fn main(){}" > ./src/main.rs
9 |
10 | # Removing local packages from Cargo.toml
11 | COPY ./server/Cargo.toml .
12 | RUN sed -i "s/^\(\(entity = {\)\|\(migration = {\)\|\(spherical_go_game_lib = {\)\\).*//g" ./Cargo.toml
13 |
14 | # Copying deps and downloading and pre-building them
15 | RUN cargo build --release
16 |
17 | # Copying all the logic
18 | COPY ../gamelib /gamelib
19 | COPY ./server .
20 |
21 | RUN cargo build --release
22 |
23 | # Running
24 | FROM debian:11-slim AS runtime
25 | COPY --from=builder /app/target/release/server /dgs_server
26 | ENTRYPOINT [ "./dgs_server" ]
27 |
--------------------------------------------------------------------------------
/deploy/prod/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.9"
2 |
3 | services:
4 | server:
5 | container_name: "dgs-server"
6 | build: .
7 | depends_on:
8 | - db
9 | env_file:
10 | - .env
11 | ports:
12 | - "${PORT}:${PORT}"
13 |
14 | db:
15 | container_name: "dgs-db"
16 | image: postgres:15-alpine
17 | environment:
18 | POSTGRES_DB: "dgs"
19 | POSTGRES_USER: "dgs"
20 | POSTGRES_PASSWORD: "lmao"
21 | ports:
22 | - "5432:5432"
23 | volumes:
24 | - ./volumes/postgres_data:/var/lib/postgresql/data/
25 |
--------------------------------------------------------------------------------
/deploy/test/.env:
--------------------------------------------------------------------------------
1 | DATABASE_URL=postgres://dgs:lmao@db
2 | PORT=8000
3 | ALLOWED_ORIGINS = "http://localhost:3000"
4 | MODE=TEST
5 |
6 |
--------------------------------------------------------------------------------
/deploy/test/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM rustlang/rust:nightly
2 |
3 | WORKDIR /server
4 |
5 | # Creating dummy main.rs file
6 | RUN mkdir ./src
7 | RUN echo "fn main(){}" > ./src/main.rs
8 |
9 | # Removing local packages from Cargo.toml
10 | COPY ./server/Cargo.toml .
11 | RUN sed -i "s/^\(\(entity = {\)\|\(migration = {\)\|\(spherical_go_game_lib = {\)\\).*//g" ./Cargo.toml
12 |
13 | # Copying deps and downloading and pre-building them
14 | RUN cargo build --tests
15 |
16 | # Copying all the logic
17 | COPY ./server .
18 |
19 | COPY ../gamelib /gamelib
20 |
--------------------------------------------------------------------------------
/deploy/test/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.9"
2 |
3 | services:
4 | server:
5 | container_name: "dgs-app-test"
6 | build: .
7 | command: cargo test -- --test-threads=1
8 | depends_on:
9 | - db
10 | env_file:
11 | - .env
12 | tty: true
13 | stdin_open: true
14 |
15 | db:
16 | container_name: "dgs-db-test"
17 | image: postgres:15-alpine
18 | environment:
19 | POSTGRES_DB: "dgs"
20 | POSTGRES_USER: "dgs"
21 | POSTGRES_PASSWORD: "lmao"
22 | ports:
23 | - "5432:5432"
24 |
--------------------------------------------------------------------------------
/docs/images/grid_sphere.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dominux/DGS/a7fb63fc7d0a8a124a949dff74d7c12df1922d53/docs/images/grid_sphere.png
--------------------------------------------------------------------------------
/docs/images/quad_sphere.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dominux/DGS/a7fb63fc7d0a8a124a949dff74d7c12df1922d53/docs/images/quad_sphere.png
--------------------------------------------------------------------------------
/docs/images/uv_sphere_pole.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dominux/DGS/a7fb63fc7d0a8a124a949dff74d7c12df1922d53/docs/images/uv_sphere_pole.jpg
--------------------------------------------------------------------------------
/docs/images/uv_sphere_side.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dominux/DGS/a7fb63fc7d0a8a124a949dff74d7c12df1922d53/docs/images/uv_sphere_side.jpg
--------------------------------------------------------------------------------
/frontend/.env:
--------------------------------------------------------------------------------
1 | VITE_DEV_API = 'http://localhost:8000'
2 | VITE_PROD_API = 'https://dgs.dominux.site/api'
3 |
4 | VITE_DEV_WS = 'ws://localhost:8000'
5 | VITE_PROD_WS = 'wss://dgs.dominux.site/api'
6 |
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 |
--------------------------------------------------------------------------------
/frontend/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "singleQuote": true
4 | }
5 |
--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
1 | ## Usage
2 |
3 | Those templates dependencies are maintained via [pnpm](https://pnpm.io) via `pnpm up -Lri`.
4 |
5 | This is the reason you see a `pnpm-lock.yaml`. That being said, any package manager will work. This file can be safely be removed once you clone a template.
6 |
7 | ```bash
8 | $ npm install # or pnpm install or yarn install
9 | ```
10 |
11 | ### Learn more on the [Solid Website](https://solidjs.com) and come chat with us on our [Discord](https://discord.com/invite/solidjs)
12 |
13 | ## Available Scripts
14 |
15 | In the project directory, you can run:
16 |
17 | ### `npm dev` or `npm start`
18 |
19 | Runs the app in the development mode.
20 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
21 |
22 | The page will reload if you make edits.
23 |
24 | ### `npm run build`
25 |
26 | Builds the app for production to the `dist` folder.
27 | It correctly bundles Solid in production mode and optimizes the build for the best performance.
28 |
29 | The build is minified and the filenames include the hashes.
30 | Your app is ready to be deployed!
31 |
32 | ## Deployment
33 |
34 | You can deploy the `dist` folder to any static host provider (netlify, surge, now, etc.)
35 |
--------------------------------------------------------------------------------
/frontend/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | Dominux Go Server
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/frontend/libs/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | My modification of some package:
3 | https://www.npmjs.com/package/@solid-primitives/local-store
4 | */
5 |
6 | import { createSignal, getListener } from 'solid-js'
7 | /**
8 | * Create a new storage primitive that can retain any data type
9 | * with an interface compatible with the Web Storage API.
10 | *
11 | * @param prefix - Prefix to wrap all stored values with.
12 | * @param storage - Storage engine to use for recording the value
13 | * @return Returns a state reader, setter and clear function
14 | *
15 | * @example
16 | * ```ts
17 | * const [value, setValue] = createStorage('app');
18 | * setValue('My new value');
19 | * console.log(value());
20 | * ```
21 | */
22 | function createLocalStore(prefix = null, storage = localStorage) {
23 | const signals = new Map()
24 | const propPrefix = prefix === null ? '' : `${prefix}.`
25 | return [
26 | new Proxy(
27 | {},
28 | {
29 | get(_, key) {
30 | if (key === 'toJSON') {
31 | return storage.getAll ? () => storage.getAll() : () => storage
32 | }
33 | if (getListener()) {
34 | let node = signals.get(key)
35 | if (!node) {
36 | // @ts-ignore
37 | node = createSignal(undefined, { equals: false })
38 | signals.set(key, node)
39 | }
40 | node[0]()
41 | }
42 | const value = storage.getItem(`${propPrefix}${key}`)
43 | return JSON.parse(value)
44 | },
45 | }
46 | ),
47 | (key, value) => {
48 | storage.setItem(`${propPrefix}${key}`, JSON.stringify(value))
49 | const node = signals.get(key)
50 | node && node[1]()
51 | },
52 | (key) => {
53 | storage.removeItem(`${propPrefix}${key}`)
54 | const node = signals.get(key)
55 | node && node[1]()
56 | },
57 | () => {
58 | storage.clear()
59 | signals.clear()
60 | },
61 | ]
62 | }
63 | export default createLocalStore
64 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vite-template-solid",
3 | "version": "0.0.0",
4 | "description": "",
5 | "scripts": {
6 | "start": "vite",
7 | "dev": "vite",
8 | "build": "vite build",
9 | "serve": "vite preview"
10 | },
11 | "license": "MIT",
12 | "devDependencies": {
13 | "@suid/vite-plugin": "^0.1.2",
14 | "typescript": "^4.7.4",
15 | "vite": "^3.2.5",
16 | "vite-plugin-solid": "^2.3.0"
17 | },
18 | "dependencies": {
19 | "@solidjs/router": "^0.6.0",
20 | "@suid/icons-material": "^0.5.5",
21 | "@suid/material": "^0.9.1",
22 | "axios": "^1.2.2",
23 | "babylonjs": "^5.38.0",
24 | "babylonjs-gui": "^5.38.0",
25 | "solid-js": "^1.6.8"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/frontend/src/App.module.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .canvas {
6 | width: 100vw;
7 | height: 100vh;
8 | position: fixed;
9 | left: 0;
10 | }
11 |
12 | .mode_chooser {
13 | position: absolute;
14 | top: 50%;
15 | transform: translateY(-50%) translateX(-50%);
16 | left: 50%;
17 | }
18 |
--------------------------------------------------------------------------------
/frontend/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { Component } from 'solid-js'
2 | import { useNavigate, useRoutes } from '@solidjs/router'
3 | import { Card, CardActions, IconButton } from '@suid/material'
4 | import { Home } from '@suid/icons-material'
5 |
6 | import styles from './App.module.css'
7 | import routes, { fullLocation } from './router'
8 | import { BASENAME } from './constants'
9 |
10 | const App: Component = () => {
11 | const Routes = useRoutes(routes, BASENAME)
12 | const navigate = useNavigate()
13 |
14 | return (
15 |
16 |
26 |
27 | navigate(fullLocation('/'))}
30 | >
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | )
39 | }
40 |
41 | export default App
42 |
--------------------------------------------------------------------------------
/frontend/src/api/api_client.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 |
3 | import createLocalStore from '../../libs'
4 | import { API } from '../constants'
5 |
6 | /**
7 | * Api client to work with
8 | */
9 | export class ApiClient {
10 | constructor(readonly apiURI: string) {}
11 |
12 | protected buildAbsolutePath(path: string): string {
13 | return `${this.apiURI}${path}`
14 | }
15 |
16 | get headers() {
17 | const [store, _] = createLocalStore()
18 |
19 | if (store.user) {
20 | return { AUTHORIZATION: `${store.user.id}:${store.user.secure_id}` }
21 | }
22 | }
23 |
24 | async get(path: string, params?: Object) {
25 | return await axios
26 | .get(this.buildAbsolutePath(path), {
27 | params: params,
28 | headers: this.headers,
29 | })
30 | .catch((error) => {
31 | throw Error(error.response.data)
32 | })
33 | }
34 |
35 | async post(path: string, data?: Object, params?: Object) {
36 | return await axios
37 | .post(this.buildAbsolutePath(path), data, {
38 | params: params,
39 | headers: this.headers,
40 | })
41 | .catch((error) => {
42 | throw Error(error.response.data)
43 | })
44 | }
45 |
46 | async patch(path: string, data?: Object, params?: Object) {
47 | return await axios
48 | .patch(this.buildAbsolutePath(path), data, {
49 | params: params,
50 | headers: this.headers,
51 | })
52 | .catch((error) => {
53 | throw Error(error.response.data)
54 | })
55 | }
56 | }
57 |
58 | const apiClient = new ApiClient(API)
59 | export default apiClient
60 |
--------------------------------------------------------------------------------
/frontend/src/api/index.ts:
--------------------------------------------------------------------------------
1 | import FieldType from '../logic/fields/enum'
2 | import apiClient from './api_client'
3 | import {
4 | FetchedUser,
5 | GameWithHistory,
6 | GameWithLink,
7 | Room,
8 | User,
9 | } from './models'
10 |
11 | async function register(username: string): Promise {
12 | const res = await apiClient.post('/users', { username: username })
13 | return res.data
14 | }
15 |
16 | async function getUser(id: string): Promise {
17 | const res = await apiClient.get(`/users/${id}`)
18 | return res.data
19 | }
20 |
21 | async function createRoom(): Promise {
22 | const res = await apiClient.post('/rooms')
23 | return res.data
24 | }
25 |
26 | async function getRoom(id: string): Promise {
27 | const res = await apiClient.get(`/rooms/${id}`)
28 | return res.data
29 | }
30 |
31 | async function enterRoom(id: string): Promise {
32 | const res = await apiClient.patch(`/rooms/${id}/enter`)
33 | return res.data
34 | }
35 |
36 | async function startGame(
37 | room_id: string,
38 | field_type: FieldType,
39 | size: number
40 | ): Promise {
41 | const res = await apiClient.post('/games', { room_id, field_type, size })
42 | return res.data
43 | }
44 |
45 | async function getGameWithHistory(game_id: string): Promise {
46 | const res = await apiClient.get(`/games/${game_id}`)
47 | return res.data
48 | }
49 |
50 | const api = {
51 | register,
52 | getUser,
53 | createRoom,
54 | getRoom,
55 | enterRoom,
56 | startGame,
57 | getGameWithHistory,
58 | }
59 |
60 | export default api
61 |
--------------------------------------------------------------------------------
/frontend/src/api/models.ts:
--------------------------------------------------------------------------------
1 | import FieldType from '../logic/fields/enum'
2 |
3 | export type FetchedUser = {
4 | id: string
5 | username: string
6 | }
7 |
8 | export interface User extends FetchedUser {
9 | secure_id: string
10 | }
11 |
12 | export type Room = {
13 | id: string
14 | player1_id: string
15 | player2_id: string | null
16 | game_id: string | null
17 | }
18 |
19 | export type Game = {
20 | id: string
21 | is_ended: boolean
22 | }
23 |
24 | export type GameWithLink = {
25 | game: Game
26 | ws_link: string
27 | }
28 |
29 | export interface StoredGame extends Game {
30 | field_type: FieldType
31 | size: number
32 | }
33 |
34 | export type MoveSchema = {
35 | game_id: string
36 | point_id: number
37 | }
38 |
39 | export type MoveResult = {
40 | point_id: number
41 | died_stones_ids: Array
42 | }
43 |
44 | export type History = {
45 | id: string
46 | game_id: string
47 | size: number
48 | field_type: FieldType
49 | }
50 |
51 | export type HistoryRecord = {
52 | id: string
53 | history_id: string
54 | move_number: number
55 | point_id: number
56 | died_points_ids: Array
57 | }
58 |
59 | export type HistoryWithRecords = {
60 | history: History
61 | records: Array
62 | }
63 |
64 | export type GameWithHistory = {
65 | game: Game
66 | history: HistoryWithRecords
67 | }
68 |
--------------------------------------------------------------------------------
/frontend/src/api/ws_client.ts:
--------------------------------------------------------------------------------
1 | import {
2 | MOVE_RESULT_CHECK_INTERVAL_MS,
3 | RECONNECT_TIMEOUT_MS,
4 | } from '../constants'
5 | import { User } from './models'
6 |
7 | export default class WSClient {
8 | private socket: WebSocket
9 | messages: MessageQueue
10 |
11 | constructor(addr: string, user: User) {
12 | this.messages = new MessageQueue()
13 | this.socket = new WebSocket(addr)
14 |
15 | this.socket.onopen = (_) => {
16 | this.socket.send(`${user.id}:${user.secure_id}`)
17 | }
18 | this.socket.onerror = (e) => {
19 | console.log(e)
20 | }
21 | this.socket.onclose = (e) => {
22 | console.log(e)
23 | this.reconnect(addr, user)
24 | }
25 |
26 | this.socket.onmessage = (e) => {
27 | this.messages.push(e.data)
28 | }
29 | }
30 |
31 | private reconnect(addr: string, user: User) {
32 | const job = setInterval(() => {
33 | console.log('trying to reconnect')
34 | this.socket = new WSClient(addr, user).socket
35 | clearInterval(job)
36 | }, RECONNECT_TIMEOUT_MS)
37 | }
38 |
39 | public sendMsg(msg: string) {
40 | this.socket.send(msg)
41 | }
42 |
43 | public async waitForMsg(): Promise {
44 | let msg = undefined
45 | while (msg === undefined) {
46 | await new Promise((resolve) =>
47 | setTimeout(resolve, MOVE_RESULT_CHECK_INTERVAL_MS)
48 | )
49 |
50 | msg = this.messages.pop()
51 | }
52 |
53 | return msg
54 | }
55 | }
56 |
57 | class MessageQueue {
58 | protected queue: Array = []
59 |
60 | push(msg: T) {
61 | this.queue.push(msg)
62 | }
63 |
64 | pop(): T | undefined {
65 | return this.queue.pop()
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/frontend/src/assets/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dominux/DGS/a7fb63fc7d0a8a124a949dff74d7c12df1922d53/frontend/src/assets/favicon.ico
--------------------------------------------------------------------------------
/frontend/src/auth.ts:
--------------------------------------------------------------------------------
1 | import { useLocation, useNavigate } from '@solidjs/router'
2 |
3 | import createLocalStore from '../libs'
4 | import { fullLocation } from './router'
5 |
6 | export function checkAuth() {
7 | const [store, setStore] = createLocalStore()
8 | const navigate = useNavigate()
9 | const location = useLocation()
10 |
11 | if (!store.user) {
12 | setStore('redirect', location.pathname)
13 |
14 | navigate(fullLocation('/login'))
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/frontend/src/components/ModeChooser.tsx:
--------------------------------------------------------------------------------
1 | import { Component, For } from 'solid-js'
2 | import { Button, Card, CardActions, Stack, Typography } from '@suid/material'
3 |
4 | import styles from '../App.module.css'
5 |
6 | export type ModeCardProps = {
7 | label: string
8 | onClick: Function
9 | }
10 |
11 | const ModeCard: Component = (props) => {
12 | return (
13 |
14 |
15 |
18 |
19 |
20 | )
21 | }
22 |
23 | export type ModeChooserProps = {
24 | modes: Array
25 | }
26 |
27 | const ModeChooser: Component = (props) => {
28 | return (
29 |
30 |
31 |
32 | {(mode) => (
33 |
34 | )}
35 |
36 |
37 |
38 | )
39 | }
40 |
41 | export default ModeChooser
42 |
--------------------------------------------------------------------------------
/frontend/src/components/index.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dominux/DGS/a7fb63fc7d0a8a124a949dff74d7c12df1922d53/frontend/src/components/index.ts
--------------------------------------------------------------------------------
/frontend/src/constants.ts:
--------------------------------------------------------------------------------
1 | export const MIN_GRIDSIZE = 5
2 | export const MAX_GRIDSIZE = 255
3 | export const INPUTFIELD_LENGTH_IN_SYMBOLS = 12
4 |
5 | export const RECONNECT_TIMEOUT_MS = 1000
6 |
7 | export const ERROR_MSG_TIMEOUT = 3
8 |
9 | export const SPHERE_RADIUS = 0.5
10 | export const REGULAR_FIELD_PADDING_TO_CELL_FRACTION = 0.5
11 | export const ACCENT_COLOR = '#e3f2fd'
12 | export const ALERT_COLOR = '#fa9393'
13 |
14 | export const FIELD_Y = 1
15 |
16 | export const API = import.meta.env.DEV
17 | ? import.meta.env.VITE_DEV_API
18 | : import.meta.env.VITE_PROD_API
19 |
20 | export const WS_API = import.meta.env.DEV
21 | ? import.meta.env.VITE_DEV_WS
22 | : import.meta.env.VITE_PROD_WS
23 |
24 | export const BASENAME = ''
25 |
26 | export const MOVE_RESULT_CHECK_INTERVAL_MS = 200
27 |
28 | export type EnvTexture = {
29 | url: string
30 | res: number
31 | }
32 |
33 | export const defaultEnvTextures = {
34 | 'Rainforest trail': {
35 | '1K': {
36 | url: 'https://dl.polyhaven.org/file/ph-assets/HDRIs/hdr/1k/rainforest_trail_1k.hdr',
37 | res: 512,
38 | },
39 | '2K': {
40 | url: 'https://dl.polyhaven.org/file/ph-assets/HDRIs/hdr/2k/rainforest_trail_2k.hdr',
41 | res: 1024,
42 | },
43 | '4K': {
44 | url: 'https://dl.polyhaven.org/file/ph-assets/HDRIs/hdr/4k/rainforest_trail_4k.hdr',
45 | res: 2048,
46 | },
47 | '8K': {
48 | url: 'https://dl.polyhaven.org/file/ph-assets/HDRIs/hdr/8k/rainforest_trail_8k.hdr',
49 | res: 4096,
50 | },
51 | },
52 | 'Kiara Dawn': {
53 | '1K': {
54 | url: 'https://dl.polyhaven.org/file/ph-assets/HDRIs/hdr/1k/kiara_1_dawn_1k.hdr',
55 | res: 512,
56 | },
57 | '2K': {
58 | url: 'https://dl.polyhaven.org/file/ph-assets/HDRIs/hdr/2k/kiara_1_dawn_2k.hdr',
59 | res: 1024,
60 | },
61 | '4K': {
62 | url: 'https://dl.polyhaven.org/file/ph-assets/HDRIs/hdr/4k/kiara_1_dawn_4k.hdr',
63 | res: 2048,
64 | },
65 | '8K': {
66 | url: 'https://dl.polyhaven.org/file/ph-assets/HDRIs/hdr/8k/kiara_1_dawn_8k.hdr',
67 | res: 4096,
68 | },
69 | },
70 | 'Chinese garden': {
71 | '1K': {
72 | url: 'https://dl.polyhaven.org/file/ph-assets/HDRIs/hdr/1k/chinese_garden_1k.hdr',
73 | res: 512,
74 | },
75 | '2K': {
76 | url: 'https://dl.polyhaven.org/file/ph-assets/HDRIs/hdr/2k/chinese_garden_2k.hdr',
77 | res: 1024,
78 | },
79 | '4K': {
80 | url: 'https://dl.polyhaven.org/file/ph-assets/HDRIs/hdr/4k/chinese_garden_4k.hdr',
81 | res: 2048,
82 | },
83 | '8K': {
84 | url: 'https://dl.polyhaven.org/file/ph-assets/HDRIs/hdr/8k/chinese_garden_8k.hdr',
85 | res: 4096,
86 | },
87 | },
88 | 'Misty pines': {
89 | '1K': {
90 | url: 'https://dl.polyhaven.org/file/ph-assets/HDRIs/hdr/1k/misty_pines_1k.hdr',
91 | res: 512,
92 | },
93 | '2K': {
94 | url: 'https://dl.polyhaven.org/file/ph-assets/HDRIs/hdr/2k/misty_pines_2k.hdr',
95 | res: 1024,
96 | },
97 | '4K': {
98 | url: 'https://dl.polyhaven.org/file/ph-assets/HDRIs/hdr/4k/misty_pines_4k.hdr',
99 | res: 2048,
100 | },
101 | '8K': {
102 | url: 'https://dl.polyhaven.org/file/ph-assets/HDRIs/hdr/8k/misty_pines_8k.hdr',
103 | res: 4096,
104 | },
105 | },
106 | 'Phalzer forest': {
107 | '1K': {
108 | url: 'https://dl.polyhaven.org/file/ph-assets/HDRIs/hdr/1k/phalzer_forest_01_1k.hdr',
109 | res: 512,
110 | },
111 | '2K': {
112 | url: 'https://dl.polyhaven.org/file/ph-assets/HDRIs/hdr/2k/phalzer_forest_01_2k.hdr',
113 | res: 1024,
114 | },
115 | '4K': {
116 | url: 'https://dl.polyhaven.org/file/ph-assets/HDRIs/hdr/4k/phalzer_forest_01_4k.hdr',
117 | res: 2048,
118 | },
119 | '8K': {
120 | url: 'https://dl.polyhaven.org/file/ph-assets/HDRIs/hdr/8k/phalzer_forest_01_8k.hdr',
121 | res: 4096,
122 | },
123 | },
124 | }
125 | export const defaultTextureName = Object.keys(defaultEnvTextures)[0]
126 | export const defaultResolution = '1K'
127 |
128 | export const GRID_MATERIAL =
129 | 'https://raw.githubusercontent.com/Dominux/spherical-go/main/frontend/src/assets/gridMaterial.json'
130 |
--------------------------------------------------------------------------------
/frontend/src/gui/alert.ts:
--------------------------------------------------------------------------------
1 | import * as GUI from 'babylonjs-gui'
2 |
3 | import { ALERT_COLOR, ERROR_MSG_TIMEOUT } from '../constants'
4 |
5 | export default class AlertComponent {
6 | readonly container: GUI.Container
7 | private textBlock: GUI.TextBlock
8 |
9 | constructor(advancedTexture: GUI.AdvancedDynamicTexture) {
10 | // Container
11 | this.container = new GUI.Container()
12 | this.container.width = '300px'
13 | this.container.height = '80px'
14 | this.container.background = ALERT_COLOR
15 | this.container.verticalAlignment = GUI.Control.VERTICAL_ALIGNMENT_TOP
16 | this.container.top = 10
17 | this.container.isVisible = false
18 |
19 | // Text
20 | this.textBlock = new GUI.TextBlock('alert', '')
21 | this.textBlock.color = 'white'
22 |
23 | // add controls
24 | this.container.addControl(this.textBlock)
25 | advancedTexture.addControl(this.container)
26 | }
27 |
28 | set errorMsg(newVal: string) {
29 | this.textBlock.text = newVal
30 | this.container.isVisible = true
31 |
32 | setTimeout(
33 | () => (this.container.isVisible = false),
34 | ERROR_MSG_TIMEOUT * 1000
35 | )
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/frontend/src/gui/game_creation_form.ts:
--------------------------------------------------------------------------------
1 | import * as GUI from 'babylonjs-gui'
2 |
3 | import {
4 | ACCENT_COLOR,
5 | ALERT_COLOR,
6 | MAX_GRIDSIZE,
7 | MIN_GRIDSIZE,
8 | } from '../constants'
9 | import FieldType from '../logic/fields/enum'
10 | import GUIComponent from './gui_components'
11 | import SelectComponent from './select_menu'
12 | import TextComponent from './text'
13 |
14 | const ODD_RANGE_STR = [MIN_GRIDSIZE, MIN_GRIDSIZE + 2, MIN_GRIDSIZE + 4].join(
15 | ','
16 | )
17 |
18 | export default class GameCreationFormGUI {
19 | protected fieldTypeSelect: GUIComponent
20 | protected gridSizeInput: GUIComponent
21 | protected virtualKeyboard: GUIComponent
22 | protected gridSizeHint: GUIComponent
23 | protected startButton: GUIComponent
24 | protected stackMesh: BABYLON.Mesh
25 |
26 | protected _isValid: boolean = true
27 |
28 | protected get isValid() {
29 | return this._isValid
30 | }
31 | protected set isValid(newVal: boolean) {
32 | this._isValid = newVal
33 |
34 | // Enabling/disabling start button
35 | this.startButton.component.isEnabled = this._isValid
36 | }
37 |
38 | protected get fieldType(): FieldType {
39 | return this.fieldTypeSelect.component.selectedValue
40 | }
41 | protected get gridSize(): number {
42 | return parseInt(this.gridSizeInput.component.text) || 0
43 | }
44 |
45 | protected set errorMessage(newVal: string) {
46 | if (newVal) {
47 | this.gridSizeHint.component.text = newVal
48 | this.gridSizeHint.component.container.isVisible = true
49 | } else {
50 | this.gridSizeHint.component.container.isVisible = false
51 | }
52 | }
53 |
54 | constructor(
55 | readonly camera: BABYLON.Camera,
56 | onChangeFieldType: Function,
57 | onChangeGridSize: Function,
58 | defaultFieldType: FieldType,
59 | defaultGridSize: number,
60 | onSumbit: Function
61 | ) {
62 | const innerOnChangeFieldType = (selectedValue: FieldType) => {
63 | this.validateGridSize(this.gridSize)
64 | onChangeFieldType(selectedValue)
65 | }
66 |
67 | const stack = this.createStackPanel()
68 | this.stackMesh = stack.advancedTextureMesh
69 |
70 | this.fieldTypeSelect = this.createFieldTypeSelect(stack, defaultFieldType)
71 | this.fieldTypeSelect.component.onSelectRegister(innerOnChangeFieldType)
72 |
73 | this.gridSizeInput = this.createGridSizeInput(
74 | defaultGridSize,
75 | stack,
76 | onChangeGridSize
77 | )
78 | this.gridSizeHint = this.createGridSizeHint(stack)
79 | this.virtualKeyboard = this.createVirtualKeyBoard(
80 | stack,
81 | this.gridSizeInput.component
82 | )
83 |
84 | this.startButton = this.createStartButton(stack, onSumbit)
85 |
86 | this.validateGridSize(parseInt(this.gridSizeInput.component.text))
87 |
88 | this.hide()
89 | }
90 |
91 | createStackPanel() {
92 | const stack = new GUI.StackPanel('game-creation-form')
93 | stack.spacing = 10
94 | stack.isHitTestVisible = false
95 |
96 | const plane = BABYLON.MeshBuilder.CreatePlane('field-type-plane', {
97 | size: 1,
98 | })
99 | // plane.parent = this.camera
100 | plane.position.z = -0.1
101 | plane.position.y = 1.2
102 | plane.position.x = 0.8
103 |
104 | const advancedTexture = GUI.AdvancedDynamicTexture.CreateForMesh(plane)
105 | advancedTexture.addControl(stack)
106 |
107 | return new GUIComponent(stack, plane)
108 | }
109 |
110 | createFieldTypeSelect(
111 | stack: GUIComponent,
112 | defaultFieldType?: FieldType
113 | ): GUIComponent {
114 | const fieldTypeSelect = new SelectComponent(
115 | '80px',
116 | '360px',
117 | defaultFieldType
118 | )
119 |
120 | fieldTypeSelect.shadowOffsetX = 10
121 | fieldTypeSelect.shadowOffsetY = 10
122 |
123 | Object.values(FieldType).forEach((fieldType) =>
124 | fieldTypeSelect.addOption(fieldType, fieldType)
125 | )
126 |
127 | stack.component.addControl(fieldTypeSelect.container)
128 |
129 | return new GUIComponent(fieldTypeSelect, stack.advancedTextureMesh)
130 | }
131 |
132 | createGridSizeInput(
133 | defaultGridSize: number = 0,
134 | stack: GUIComponent,
135 | onChangeGridSize: Function
136 | ): GUIComponent {
137 | const input = new GUI.InputText('grid-size', String(defaultGridSize))
138 | input.height = '80px'
139 | input.width = '360px'
140 | input.fontSize = 50
141 | input.color = 'black'
142 | input.background = 'white'
143 | input.focusedBackground = ACCENT_COLOR
144 | input.disableMobilePrompt = true
145 |
146 | // Enabling only digits
147 | input.onBeforeKeyAddObservable.add((input) => {
148 | let key = input.currentKey
149 |
150 | input.addKey = key >= '0' && key <= '9'
151 | })
152 |
153 | // Reactivity
154 | input.onTextChangedObservable.add((input) => {
155 | const value = parseInt(input.text) || 0
156 | onChangeGridSize(value)
157 | this.validateGridSize(value)
158 | })
159 |
160 | stack.component.addControl(input)
161 |
162 | return new GUIComponent(input, stack.advancedTextureMesh)
163 | }
164 |
165 | createGridSizeHint(stack: GUIComponent) {
166 | // Container
167 | const text = new TextComponent('50px', '500px', 'white', ALERT_COLOR, 34)
168 |
169 | stack.component.addControl(text.container)
170 |
171 | return new GUIComponent(text, stack.advancedTextureMesh)
172 | }
173 |
174 | validateGridSize(value: number): boolean {
175 | if (value < MIN_GRIDSIZE) {
176 | this.errorMessage = `Number must be >= ${MIN_GRIDSIZE}`
177 | this.isValid = false
178 | return false
179 | }
180 | if (value > MAX_GRIDSIZE) {
181 | this.errorMessage = `Number must be <= ${MAX_GRIDSIZE}`
182 | this.isValid = false
183 | return false
184 | }
185 | if (this.fieldType === FieldType.GridSphere && value % 2 == 0) {
186 | this.errorMessage = `Number must be odd (${ODD_RANGE_STR}, ...)`
187 | this.isValid = false
188 | return false
189 | }
190 |
191 | this.errorMessage = ''
192 | this.isValid = true
193 | return true
194 | }
195 |
196 | createVirtualKeyBoard(
197 | stack: GUIComponent,
198 | gridSizeInput: GUI.InputText
199 | ): GUIComponent {
200 | const keyboard = GUI.VirtualKeyboard.CreateDefaultLayout()
201 | keyboard.verticalAlignment = GUI.Control.VERTICAL_ALIGNMENT_TOP
202 | keyboard.fontSize = 24
203 |
204 | keyboard.connect(gridSizeInput)
205 |
206 | stack.component.addControl(keyboard)
207 |
208 | return new GUIComponent(keyboard, stack.advancedTextureMesh)
209 | }
210 |
211 | createStartButton(
212 | stack: GUIComponent,
213 | onSumbit: Function
214 | ): GUIComponent {
215 | const button = GUI.Button.CreateSimpleButton(
216 | 'start-button',
217 | ' Start game '
218 | )
219 | button.height = '80px'
220 | button.width = '360px'
221 | button.fontSize = 44
222 | button.background = ACCENT_COLOR
223 |
224 | button.onPointerUpObservable.add(async () => {
225 | this.delete()
226 |
227 | await onSumbit()
228 | })
229 |
230 | stack.component.addControl(button)
231 |
232 | return new GUIComponent(button, stack.advancedTextureMesh)
233 | }
234 |
235 | show() {
236 | this.stackMesh.isVisible = true
237 | }
238 | hide() {
239 | this.stackMesh.isVisible = false
240 | }
241 |
242 | delete() {
243 | this.stackMesh.dispose()
244 | }
245 | }
246 |
--------------------------------------------------------------------------------
/frontend/src/gui/gui_components.ts:
--------------------------------------------------------------------------------
1 | import * as GUI from 'babylonjs-gui'
2 |
3 | export default class GUIComponent {
4 | constructor(
5 | readonly component: Type,
6 | readonly advancedTextureMesh: BABYLON.Mesh
7 | ) {}
8 |
9 | initialize() {
10 | this.advancedTextureMesh.isVisible = true
11 | const advancedTexture = GUI.AdvancedDynamicTexture.CreateForMesh(
12 | this.advancedTextureMesh
13 | )
14 | advancedTexture.addControl(
15 | this.component.container ? this.component.container : this.component
16 | )
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/frontend/src/gui/index.ts:
--------------------------------------------------------------------------------
1 | import * as GUI from 'babylonjs-gui'
2 |
3 | import FieldType from '../logic/fields/enum'
4 | import AlertComponent from './alert'
5 | import GameCreationFormGUI from './game_creation_form'
6 | import PlayerBarGUI from './player_bar'
7 |
8 | export default class GameGUI {
9 | protected gameCreationForm: GameCreationFormGUI
10 | protected playerBar: PlayerBarGUI
11 | protected globalTexture: GUI.AdvancedDynamicTexture
12 |
13 | protected alert: AlertComponent
14 |
15 | set isUndoButtonHidden(newVal: boolean) {
16 | this.playerBar.isUndoButtonHidden = newVal
17 | }
18 |
19 | constructor(
20 | readonly camera: BABYLON.Camera,
21 | onChangeFieldType: Function,
22 | onChangeGridSize: Function,
23 | defaultFieldType: FieldType,
24 | defaultGridSize: number,
25 | onSubmit: Function,
26 | onUndo: Function
27 | ) {
28 | this.globalTexture = GUI.AdvancedDynamicTexture.CreateFullscreenUI('gui')
29 |
30 | this.gameCreationForm = new GameCreationFormGUI(
31 | camera,
32 | onChangeFieldType,
33 | onChangeGridSize,
34 | defaultFieldType,
35 | defaultGridSize,
36 | onSubmit
37 | )
38 |
39 | this.playerBar = new PlayerBarGUI(this.camera, onUndo)
40 |
41 | this.alert = new AlertComponent(this.globalTexture)
42 | }
43 |
44 | show() {
45 | this.gameCreationForm.show()
46 | }
47 | hide() {
48 | this.gameCreationForm.hide()
49 | }
50 |
51 | onStart() {
52 | this.playerBar.initialize()
53 | }
54 |
55 | onError(errorMsg: string) {
56 | this.alert.errorMsg = errorMsg
57 | }
58 |
59 | setBlackScore(value: number) {
60 | this.playerBar.setBlackScore(value)
61 | }
62 |
63 | setWhiteScore(value: number) {
64 | this.playerBar.setWhiteScore(value)
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/frontend/src/gui/player_bar.ts:
--------------------------------------------------------------------------------
1 | import * as GUI from 'babylonjs-gui'
2 |
3 | import GUIComponent from './gui_components'
4 | import TextComponent from './text'
5 |
6 | export default class PlayerBarGUI {
7 | protected stack: GUIComponent
8 | protected blackScore: GUIComponent
9 | protected whiteScore: GUIComponent
10 | protected undoButton: GUIComponent
11 |
12 | set isUndoButtonHidden(newVal: boolean) {
13 | this.undoButton.component.isVisible = !newVal
14 | }
15 |
16 | constructor(readonly camera: BABYLON.Camera, onUndo: Function) {
17 | this.stack = this.createStackPanel()
18 | this.blackScore = this.createScoreComponent('black')
19 | this.whiteScore = this.createScoreComponent('white')
20 | this.undoButton = this.createUndoButton(onUndo)
21 | }
22 |
23 | createStackPanel() {
24 | const stack = new GUI.StackPanel('player-bar')
25 | stack.spacing = 10
26 | stack.isHitTestVisible = false
27 |
28 | const plane = BABYLON.MeshBuilder.CreatePlane('stack-plane', {
29 | size: 0.5,
30 | })
31 | // plane.parent = this.camera
32 | plane.position.z = -0.1
33 | plane.position.y = 1.3
34 | plane.position.x = 0.8
35 | plane.isVisible = false
36 |
37 | const advancedTexture = GUI.AdvancedDynamicTexture.CreateForMesh(plane)
38 | advancedTexture.addControl(stack)
39 |
40 | return new GUIComponent(stack, plane)
41 | }
42 |
43 | createScoreComponent(color: string): GUIComponent {
44 | const background = color
45 | color = color === 'black' ? 'white' : 'black'
46 | const scoreBlock = new TextComponent(
47 | '200px',
48 | '760px',
49 | color,
50 | background,
51 | 120
52 | )
53 | scoreBlock.container.isVisible = false
54 |
55 | this.stack.component.addControl(scoreBlock.container)
56 |
57 | return new GUIComponent(scoreBlock, this.stack.advancedTextureMesh)
58 | }
59 |
60 | createUndoButton(onUndo: Function): GUIComponent {
61 | const button = GUI.Button.CreateSimpleButton('undo-button', 'Undo move')
62 | button.background = 'white'
63 | button.height = '160px'
64 | button.width = '450px'
65 | button.fontSize = 60
66 | button.isVisible = false
67 |
68 | button.onPointerClickObservable.add(() => {
69 | onUndo()
70 | })
71 |
72 | this.stack.component.addControl(button)
73 |
74 | return new GUIComponent(button, this.stack.advancedTextureMesh)
75 | }
76 |
77 | initialize() {
78 | this.stack.advancedTextureMesh.isVisible = true
79 | this.blackScore.component.container.isVisible = true
80 | this.whiteScore.component.container.isVisible = true
81 | this.undoButton.component.isVisible = true
82 | this.isUndoButtonHidden = true
83 | }
84 |
85 | setBlackScore(value: number) {
86 | this.blackScore.component.text = String(value)
87 | }
88 |
89 | setWhiteScore(value: number) {
90 | this.whiteScore.component.text = String(value)
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/frontend/src/gui/select_menu.ts:
--------------------------------------------------------------------------------
1 | import * as GUI from 'babylonjs-gui'
2 |
3 | import { ACCENT_COLOR } from '../constants'
4 |
5 | // https://github.com/BabylonJS/Babylon.js/issues/3910
6 | export default class SelectComponent {
7 | readonly container: GUI.Container
8 | readonly options: GUI.StackPanel
9 | readonly button: GUI.Button
10 | public selected?
11 | public selectedValue?
12 | protected stackSize = 0
13 | protected onSelect?: Function
14 |
15 | constructor(
16 | protected height: string,
17 | protected width: string,
18 | defaultValue: any,
19 | readonly color: string = 'black',
20 | readonly fontSize: number = 32,
21 | readonly background: string = 'white',
22 | readonly accentColor: string = ACCENT_COLOR
23 | ) {
24 | // Container
25 | this.container = new GUI.Container()
26 | this.container.height = this.height
27 | this.container.width = this.width
28 | this.container.verticalAlignment = GUI.Control.VERTICAL_ALIGNMENT_TOP
29 | this.container.isHitTestVisible = false
30 |
31 | // Primary button
32 | this.button = GUI.Button.CreateSimpleButton(
33 | null,
34 | defaultValue || 'Please Select'
35 | )
36 | this.button.height = this.height
37 | this.button.background = this.background
38 | this.button.color = this.color
39 | this.button.fontSize = fontSize
40 | this.button.verticalAlignment = GUI.Control.VERTICAL_ALIGNMENT_TOP
41 |
42 | // Options panel
43 | this.options = new GUI.StackPanel()
44 | this.options.verticalAlignment = GUI.Control.VERTICAL_ALIGNMENT_TOP
45 | this.options.top = this.height
46 | this.options.isVisible = false
47 | this.options.isVertical = true
48 |
49 | this.button.onPointerUpObservable.add(() => {
50 | this.options.isVisible = !this.options.isVisible
51 |
52 | // Adjusting container size
53 | this.options.isVisible ? this.onOpenMenu() : this.onCloseMenu()
54 | })
55 |
56 | // add controls
57 | this.container.addControl(this.button)
58 | this.container.addControl(this.options)
59 |
60 | // Selection
61 | this.selected = null
62 | this.selectedValue = defaultValue
63 | }
64 |
65 | addOption(value, text, color?, background?) {
66 | const button = GUI.Button.CreateSimpleButton(text, text)
67 |
68 | button.height = this.height
69 | button.paddingTop = '-1px'
70 | button.background = background || this.accentColor
71 | button.color = color || this.color
72 | button.fontSize = this.fontSize
73 |
74 | button.onPointerUpObservable.add(() => {
75 | this.options.isVisible = false
76 | this.button.children[0].text = text
77 | this.selected = button
78 | this.selectedValue = value
79 |
80 | this.onCloseMenu()
81 |
82 | if (this.onSelect !== undefined) this.onSelect(this.selectedValue)
83 | })
84 |
85 | this.stackSize += parseFloat(this.height)
86 |
87 | this.options.addControl(button)
88 | }
89 |
90 | onSelectRegister(func: Function) {
91 | this.onSelect = func
92 | }
93 |
94 | onOpenMenu() {
95 | this.container.height = `${
96 | parseFloat(this.container.height) + this.stackSize
97 | }px`
98 | }
99 | onCloseMenu() {
100 | this.container.height = `${
101 | parseFloat(this.container.height) - this.stackSize
102 | }px`
103 | }
104 |
105 | /////////////////////////////////////////
106 | //// Descriptors
107 | /////////////////////////////////////////
108 |
109 | get top() {
110 | return this.container.top
111 | }
112 | set top(value) {
113 | this.container.top = value
114 | }
115 |
116 | get left() {
117 | return this.container.left
118 | }
119 | set left(value) {
120 | this.container.left = value
121 | }
122 |
123 | get shadowOffsetX() {
124 | return this.container.shadowOffsetX
125 | }
126 | set shadowOffsetX(value) {
127 | this.container.shadowOffsetX = value
128 | }
129 |
130 | get shadowOffsetY() {
131 | return this.container.shadowOffsetY
132 | }
133 | set shadowOffsetY(value) {
134 | this.container.shadowOffsetY = value
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/frontend/src/gui/text.ts:
--------------------------------------------------------------------------------
1 | import * as GUI from 'babylonjs-gui'
2 |
3 | export default class TextComponent {
4 | readonly container: GUI.Container
5 | private textBlock: GUI.TextBlock
6 |
7 | constructor(
8 | protected height: string,
9 | protected width: string,
10 | readonly color: string,
11 | readonly background: string,
12 | fontSize: number = 32
13 | ) {
14 | // Container
15 | this.container = new GUI.Container()
16 | this.container.width = this.width
17 | this.container.height = this.height
18 | this.container.isHitTestVisible = false
19 | this.container.background = this.background
20 |
21 | // Text
22 | this.textBlock = new GUI.TextBlock()
23 | this.textBlock.fontSize = fontSize
24 | this.textBlock.color = this.color
25 |
26 | // add controls
27 | this.container.addControl(this.textBlock)
28 | }
29 |
30 | get text() {
31 | return this.textBlock.text
32 | }
33 | set text(value: string) {
34 | this.textBlock.text = value
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/frontend/src/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 |
--------------------------------------------------------------------------------
/frontend/src/index.tsx:
--------------------------------------------------------------------------------
1 | /* @refresh reload */
2 | import { render } from 'solid-js/web'
3 | import { Router } from '@solidjs/router'
4 |
5 | import './index.css'
6 | import App from './App'
7 |
8 | render(
9 | () => (
10 |
11 |
12 |
13 | ),
14 | document.getElementById('root') as HTMLElement
15 | )
16 |
--------------------------------------------------------------------------------
/frontend/src/logic/fields/enum.ts:
--------------------------------------------------------------------------------
1 | import GridSphere from './grid_sphere'
2 | import { Field } from './interface'
3 | import RegularField from './regular'
4 |
5 | enum FieldType {
6 | GridSphere = 'GridSphere',
7 | Regular = 'Regular',
8 | }
9 |
10 | export default FieldType
11 |
12 | export function getFieldFromType(fieldType: FieldType): Field {
13 | switch (fieldType) {
14 | case FieldType.Regular:
15 | return RegularField
16 | break
17 |
18 | default:
19 | return GridSphere
20 | break
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/frontend/src/logic/fields/interface.ts:
--------------------------------------------------------------------------------
1 | import { MoveResult } from '../../api/models'
2 | import Game from '../games/game'
3 | import StoneManager, { CreateStoneScheme } from '../stone_manager'
4 |
5 | export interface Field {
6 | game: Game | undefined
7 |
8 | init(scene: BABYLON.Scene, gridSize: number): Promise
9 |
10 | start(
11 | game: Game,
12 | onEndMove: Function,
13 | onDeath: Function,
14 | onError: Function
15 | ): Promise
16 |
17 | get playerTurn(): string
18 | get blackScore(): number
19 | get whiteScore(): number
20 |
21 | makeMoveProgramatically(moveResult: MoveResult): void
22 | putStoneProgramatically(
23 | moveResult: MoveResult,
24 | color: 'Black' | 'White'
25 | ): void
26 | undoMove(): void
27 | getCreateStoneSchema(id: number, color: BABYLON.Color3): CreateStoneScheme
28 | delete(): void
29 | }
30 |
31 | export function returnStonesBack(
32 | stoneManager: StoneManager,
33 | stones: Array
34 | ): void {
35 | stoneManager.clear()
36 |
37 | for (const stone of stones) {
38 | stoneManager.create(stone)
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/frontend/src/logic/game_manager.ts:
--------------------------------------------------------------------------------
1 | import createLocalStore from '../../libs'
2 | import { EnvTexture } from '../constants'
3 | import GameGUI from '../gui'
4 | import FieldType, { getFieldFromType } from './fields/enum'
5 | import { Field } from './fields/interface'
6 | import Game from './games/game'
7 | import Scene from './scene'
8 |
9 | export default class GameManager {
10 | readonly _scene: Scene
11 | readonly _GUI: GameGUI
12 |
13 | fieldType: FieldType = FieldType.GridSphere
14 | gridSize: number = 9
15 |
16 | protected field?: Field
17 | protected isStarted: boolean = false
18 | protected isMultiplayer: boolean
19 |
20 | constructor(
21 | canvas: HTMLCanvasElement,
22 | sphereRadius: number,
23 | readonly gameKlass: { new (): Game }
24 | ) {
25 | this._scene = new Scene(canvas, sphereRadius)
26 |
27 | const onChangeFieldType = async (newVal: FieldType) => {
28 | this.fieldType = newVal
29 | await this.setField()
30 | }
31 | const onChangeGridSize = async (newVal: number) => {
32 | this.gridSize = newVal
33 | await this.setField()
34 | }
35 | const onSumbit = async () => await this.gameStart()
36 | const onUndo = () => this.undo()
37 |
38 | this._GUI = new GameGUI(
39 | this._scene._camera,
40 | onChangeFieldType,
41 | onChangeGridSize,
42 | this.fieldType,
43 | this.gridSize,
44 | onSumbit,
45 | onUndo
46 | )
47 | }
48 |
49 | showGUI() {
50 | this._GUI.show()
51 | }
52 | hideGUI() {
53 | this._GUI.hide()
54 | }
55 |
56 | async setField() {
57 | const klass = getFieldFromType(this.fieldType)
58 |
59 | // Deleting old field
60 | this.field?.delete()
61 |
62 | // Creating a new one
63 | this.field = await klass.init(this._scene._scene, this.gridSize)
64 | }
65 |
66 | async gameStart() {
67 | const [store, _setStore] = createLocalStore()
68 |
69 | this.isStarted = true
70 |
71 | // Starting game
72 | const game = new this.gameKlass(
73 | this.fieldType,
74 | this.gridSize,
75 | (blackStones, whiteStones) =>
76 | this.onRecreateGame(blackStones, whiteStones)
77 | )
78 |
79 | await this.field?.start(
80 | game,
81 | () => this.onEndMove(),
82 | () => this.onDeath(),
83 | (errorMsg: string) => this._GUI.onError(errorMsg)
84 | )
85 |
86 | this.isMultiplayer = game.wsClient
87 |
88 | this._GUI.onStart()
89 |
90 | // Setting initial score
91 | this.setBlackScore(game.blackScore)
92 | this.setWhiteScore(game.whiteScore)
93 |
94 | // if it's multiplayer and it's not player's turn
95 | if (
96 | this.isMultiplayer &&
97 | ((store.room.player2_id === store.user.id &&
98 | store.game?.history?.records.length % 2 === 0) ||
99 | (store.room.player1_id === store.user.id &&
100 | store.game?.history?.records.length % 2 === 1))
101 | ) {
102 | this.field.canMove = false
103 |
104 | const moveResult = await game.waitForOpponentMove()
105 | this.field?.makeMoveProgramatically(moveResult)
106 |
107 | this.field.canMove = true
108 | }
109 | }
110 |
111 | undo() {
112 | this.field?.undoMove()
113 |
114 | this.setBlackScore(this.field?.blackScore)
115 | this.setWhiteScore(this.field?.whiteScore)
116 | this.setIsUndoMoveDisabled()
117 | }
118 |
119 | onRecreateGame(blackStones: Array, whiteStones: Array) {
120 | // Putting stones
121 | for (const [stones, color] of [
122 | [blackStones, 'Black'],
123 | [whiteStones, 'White'],
124 | ]) {
125 | for (const stone of stones) {
126 | const moveResult = {
127 | point_id: stone,
128 | died_stones_ids: [],
129 | }
130 | this.field?.putStoneProgramatically(moveResult, color)
131 | }
132 | }
133 | }
134 |
135 | setIsUndoMoveDisabled() {
136 | this._GUI.isUndoButtonHidden =
137 | this.isMultiplayer || this.field?.game?.moveNumber <= 1
138 | }
139 |
140 | setEnvTexture(envTexture: EnvTexture) {
141 | this._scene.setEnv(envTexture)
142 | }
143 |
144 | setBlackScore(value: number) {
145 | this._GUI.setBlackScore(value)
146 | }
147 |
148 | setWhiteScore(value: number) {
149 | this._GUI.setWhiteScore(value)
150 | }
151 |
152 | onEndMove() {
153 | this.setIsUndoMoveDisabled()
154 | }
155 |
156 | onDeath() {
157 | if (
158 | this.field.game.playerTurn.toLowerCase() ===
159 | (this.isMultiplayer ? 'black' : 'white')
160 | ) {
161 | this.setBlackScore(this.field?.blackScore)
162 | } else {
163 | this.setWhiteScore(this.field?.whiteScore)
164 | }
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/frontend/src/logic/games/game.ts:
--------------------------------------------------------------------------------
1 | export default interface Game {
2 | start(): Promise
3 |
4 | makeMove(pointID: number): Promise>
5 |
6 | undoMove(): void
7 |
8 | get playerTurn(): any
9 |
10 | get whiteScore(): any
11 |
12 | get blackScore(): any
13 |
14 | get moveNumber(): any
15 |
16 | get blackStones(): any
17 |
18 | get whiteStones(): any
19 | }
20 |
--------------------------------------------------------------------------------
/frontend/src/logic/games/multiplayer_game.ts:
--------------------------------------------------------------------------------
1 | import createLocalStore from '../../../libs'
2 | import api from '../../api/index'
3 | import WSClient from '../../api/ws_client'
4 | import FieldType from '../fields/enum'
5 | import Game from './game'
6 | import { MoveResult, MoveSchema, GameWithHistory } from '../../api/models'
7 | import { WS_API } from '../../constants'
8 |
9 | export default class MultiplayerGame implements Game {
10 | private wsClient: WSClient | null = null
11 | private _moveNumber: number = 1
12 | private _blackStones: Array = []
13 | private _whiteStones: Array = []
14 | private _blackScore = 0
15 | private _whiteScore = 0
16 |
17 | constructor(
18 | public fieldType: FieldType,
19 | public size: number,
20 | readonly onRecreateGame: Function
21 | ) {}
22 |
23 | async start() {
24 | const [store, setStore] = createLocalStore()
25 |
26 | if (store.room.game_id === null) {
27 | let room = store.room
28 |
29 | // Starting game
30 | try {
31 | const game_with_link = await api.startGame(
32 | store.room?.id,
33 | this.fieldType,
34 | this.size
35 | )
36 | room.game_id = game_with_link.game.id
37 | } catch (_) {
38 | room = await api.getRoom(store.room?.id)
39 | }
40 |
41 | setStore('room', room)
42 | }
43 |
44 | // Fetching game
45 | const game = await api.getGameWithHistory(store.room.game_id)
46 | setStore('game', game)
47 |
48 | // Recreating game
49 | this.recreateGame(game)
50 |
51 | // Opening ws
52 | this.wsClient = new WSClient(
53 | `${WS_API}/games/ws/${store.room.id}`,
54 | store.user
55 | )
56 | }
57 |
58 | async makeMove(pointID: number): Promise {
59 | const [store, _setStore] = createLocalStore()
60 |
61 | // Sending message
62 | const move_schema: MoveSchema = {
63 | game_id: store.room.game_id,
64 | point_id: pointID,
65 | }
66 | this.wsClient?.sendMsg(JSON.stringify(move_schema))
67 |
68 | // Waiting for response from server
69 | const msg = await this.wsClient?.waitForMsg()
70 |
71 | const move_result = JSON.parse(msg)
72 |
73 | if (move_result.error !== undefined) {
74 | throw new Error(move_result.error)
75 | }
76 |
77 | // Making post move actions
78 | this.postMoveActions(move_result)
79 |
80 | return move_result.died_stones_ids
81 | }
82 |
83 | async waitForOpponentMove(): Promise {
84 | const msg = await this.wsClient?.waitForMsg()
85 | const move_result: MoveResult = JSON.parse(msg)
86 |
87 | // Post move actions
88 | this.postMoveActions(move_result)
89 |
90 | return move_result
91 | }
92 |
93 | private postMoveActions(move_result: MoveResult) {
94 | this._moveNumber++
95 |
96 | if (this.playerTurn == 'Black') {
97 | this._blackStones.push(move_result.point_id)
98 | this._whiteStones = this._whiteStones.filter((p) =>
99 | move_result.died_stones_ids.includes(p)
100 | )
101 | this._blackScore += move_result.died_stones_ids.length
102 | } else {
103 | this._whiteStones.push(move_result.point_id)
104 | this._blackStones = this._whiteStones.filter((p) =>
105 | move_result.died_stones_ids.includes(p)
106 | )
107 | this._whiteScore += move_result.died_stones_ids.length
108 | }
109 | }
110 |
111 | recreateGame(game: GameWithHistory) {
112 | if (!game.history.records.length) return
113 |
114 | let blackStones: Array = []
115 | let whiteStones: Array = []
116 |
117 | // Recreating moves
118 | let isBlackTurn = true
119 | for (const record of game.history.records) {
120 | if (isBlackTurn) {
121 | blackStones.push(record.point_id)
122 |
123 | if (record.died_points_ids) {
124 | whiteStones = whiteStones.filter(
125 | (pid) => !record.died_points_ids.includes(pid)
126 | )
127 |
128 | this._blackScore += record.died_points_ids.length
129 | }
130 | } else {
131 | whiteStones.push(record.point_id)
132 |
133 | if (record.died_points_ids) {
134 | blackStones = blackStones.filter(
135 | (pid) => !record.died_points_ids.includes(pid)
136 | )
137 |
138 | this._whiteScore += record.died_points_ids.length
139 | }
140 | }
141 |
142 | // Changing turn
143 | isBlackTurn = !isBlackTurn
144 | }
145 |
146 | // Setting right moveNumber
147 | this._moveNumber += game.history.records.length
148 |
149 | // put stones and scores
150 | this.onRecreateGame(blackStones, whiteStones)
151 | }
152 |
153 | undoMove(): void {
154 | throw new Error('Method not implemented.')
155 | }
156 |
157 | get playerTurn() {
158 | return this._moveNumber % 2 === 0 ? 'Black' : 'White'
159 | }
160 | get whiteScore(): any {
161 | return this._whiteScore
162 | }
163 | get blackScore(): any {
164 | return this._blackScore
165 | }
166 | get moveNumber(): any {
167 | return this._moveNumber
168 | }
169 | get blackStones(): any {
170 | return this._blackStones
171 | }
172 | get whiteStones(): any {
173 | return this._whiteStones
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/frontend/src/logic/games/singleplayer_game.ts:
--------------------------------------------------------------------------------
1 | import Game from './game'
2 | import init, { Game as GameLib } from '../../pkg/wasm_gamelib'
3 | import FieldType from '../fields/enum'
4 |
5 | await init()
6 |
7 | export default class SingleplayerGame implements Game {
8 | protected inner: GameLib
9 |
10 | constructor(fieldType: FieldType, size: number, _onRecreateGame: Function) {
11 | this.inner = new GameLib(fieldType, size, true)
12 | }
13 |
14 | async start() {
15 | this.inner.start()
16 | }
17 |
18 | async makeMove(pointID: number): Promise> {
19 | return [...this.inner.make_move(pointID)]
20 | }
21 |
22 | undoMove() {
23 | return this.inner.undo_move()
24 | }
25 |
26 | get playerTurn() {
27 | return this.inner.player_turn()
28 | }
29 |
30 | get whiteScore() {
31 | return this.inner.get_white_score()
32 | }
33 |
34 | get blackScore() {
35 | return this.inner.get_black_score()
36 | }
37 |
38 | get moveNumber() {
39 | return this.inner.get_move_number()
40 | }
41 |
42 | get blackStones() {
43 | return [...this.inner.get_black_stones()]
44 | }
45 |
46 | get whiteStones() {
47 | return [...this.inner.get_white_stones()]
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/frontend/src/logic/scene.ts:
--------------------------------------------------------------------------------
1 | import * as BABYLON from 'babylonjs'
2 | import {
3 | defaultEnvTextures,
4 | defaultResolution,
5 | defaultTextureName,
6 | EnvTexture,
7 | } from '../constants'
8 |
9 | export default class Scene {
10 | readonly _scene: BABYLON.Scene
11 | readonly _camera: BABYLON.ArcRotateCamera
12 | readonly _ground: BABYLON.GroundMesh
13 | protected _XRHelper: BABYLON.WebXRDefaultExperience | undefined
14 |
15 | constructor(canvas: HTMLCanvasElement, sphereRadius: number) {
16 | const engine = new BABYLON.Engine(canvas, true)
17 |
18 | this._scene = new BABYLON.Scene(engine)
19 |
20 | // Creating ground
21 | this._ground = BABYLON.MeshBuilder.CreateGround('ground', {
22 | height: 30,
23 | width: 30,
24 | })
25 | this._ground.setEnabled(false)
26 |
27 | // Loading environment
28 | this.setEnv(defaultEnvTextures[defaultTextureName][defaultResolution])
29 |
30 | this._camera = new BABYLON.ArcRotateCamera(
31 | 'camera',
32 | -Math.PI / 2,
33 | Math.PI / 2,
34 | sphereRadius * 4,
35 | new BABYLON.Vector3(0, 1, 0),
36 | this._scene
37 | )
38 | this._camera.attachControl(canvas, true)
39 | this._camera.lowerRadiusLimit = sphereRadius * 3.1
40 | this._camera.upperRadiusLimit = sphereRadius * 10
41 | this._camera.wheelPrecision = 100
42 |
43 | // Loading VR
44 | this.loadVR()
45 |
46 | engine.runRenderLoop(() => {
47 | this._scene.render()
48 | })
49 |
50 | window.addEventListener('resize', () => {
51 | engine.resize()
52 | })
53 | }
54 |
55 | public setEnv(envTexture: EnvTexture) {
56 | setTimeout(() => {
57 | const hdrTexture = new BABYLON.HDRCubeTexture(
58 | envTexture.url,
59 | this._scene,
60 | envTexture.res
61 | )
62 |
63 | this._scene.createDefaultSkybox(hdrTexture)
64 | }, 0)
65 | }
66 |
67 | protected async loadVR() {
68 | this._XRHelper = await this._scene.createDefaultXRExperienceAsync({
69 | floorMeshes: [this._ground],
70 | })
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/frontend/src/logic/stone_manager.ts:
--------------------------------------------------------------------------------
1 | import * as BABYLON from 'babylonjs'
2 | import { FIELD_Y } from '../constants'
3 |
4 | export type CreateStoneScheme = {
5 | id: number
6 | position: BABYLON.Vector3
7 | color: BABYLON.Color3
8 | rotation: BABYLON.Vector3
9 | }
10 |
11 | type Stone = {
12 | id: number
13 | stone: BABYLON.Mesh
14 | }
15 |
16 | export default class StoneManager {
17 | protected stones: Array = []
18 | readonly stoneSize = 1.6
19 | readonly height: number
20 | readonly multiplier: number
21 |
22 | constructor(readonly scene: BABYLON.Scene, gridSize: number, k: number) {
23 | this.height = (0.2 * this.stoneSize * k) / gridSize
24 | this.multiplier = 1 + this.height * 0.95
25 | }
26 |
27 | create(stoneSchema: CreateStoneScheme) {
28 | const id = stoneSchema.id
29 |
30 | const diameter = this.height * 3
31 |
32 | // Creating stone
33 | const stone = BABYLON.MeshBuilder.CreateSphere(`sphere_${id}`, {
34 | diameterX: diameter,
35 | diameterY: this.height,
36 | diameterZ: diameter,
37 | })
38 |
39 | // Setting right position
40 | stoneSchema.position.y -= FIELD_Y
41 | stone.position = stoneSchema.position.scale(this.multiplier)
42 | stone.position.y += FIELD_Y
43 |
44 | // Creating stone's material
45 | const material = new BABYLON.PBRMetallicRoughnessMaterial('stone')
46 | material._albedoColor = stoneSchema.color
47 | material.roughness = 0.9
48 | material.metallic = 0.15
49 | stone.material = material
50 |
51 | // Setting stone's rotation
52 | stone.rotation = stoneSchema.rotation
53 |
54 | this.stones.push({ id, stone })
55 | }
56 |
57 | delete(indexes: Array) {
58 | const newStones = []
59 |
60 | for (const stone of this.stones) {
61 | if (indexes.includes(stone.id)) {
62 | stone.stone.dispose()
63 | } else {
64 | newStones.push(stone)
65 | }
66 | }
67 |
68 | this.stones = newStones
69 | }
70 |
71 | clear() {
72 | this.stones.forEach((stone) => stone.stone.dispose())
73 | this.stones = []
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/frontend/src/pages/404.tsx:
--------------------------------------------------------------------------------
1 | export default function NotFound() {
2 | return <>Not Found>
3 | }
4 |
--------------------------------------------------------------------------------
/frontend/src/pages/Game.tsx:
--------------------------------------------------------------------------------
1 | import { Component, onMount, createSignal, Show } from 'solid-js'
2 | import { useLocation, useNavigate } from '@solidjs/router'
3 |
4 | import styles from '../App.module.css'
5 | import { SPHERE_RADIUS } from '../constants'
6 | import GameManager from '../logic/game_manager'
7 | import ModeChooser from '../components/ModeChooser'
8 | import MultiplayerGame from '../logic/games/multiplayer_game'
9 | import SingleplayerGame from '../logic/games/singleplayer_game'
10 | import createLocalStore from '../../libs'
11 | import api from '../api'
12 | import { fullLocation } from '../router'
13 | import { Settings } from './Settings'
14 |
15 | const GamePage: Component = () => {
16 | const [gameManager, setGameManager] = createSignal()
17 | const navigate = useNavigate()
18 | const location = useLocation()
19 | const [store, setStore] = createLocalStore()
20 |
21 | const modes = [
22 | {
23 | label: 'SinglePlayer',
24 | onClick: () => {
25 | {
26 | navigate(fullLocation('/singleplayer'))
27 | startGUI()
28 | }
29 | },
30 | },
31 | {
32 | label: 'MultiPlayer',
33 | onClick: () => {
34 | navigate(fullLocation('/rooms'))
35 | },
36 | },
37 | ]
38 |
39 | onMount(() => {
40 | // Creating game
41 | setTimeout(async () => {
42 | let innerGame = isMultiplayer() ? MultiplayerGame : SingleplayerGame
43 | let gm = new GameManager(canvas, SPHERE_RADIUS, innerGame)
44 | await gm.setField()
45 | setGameManager(gm)
46 |
47 | if (!isRoot()) await startGUI()
48 | }, 0)
49 | })
50 |
51 | let canvas: HTMLCanvasElement
52 |
53 | const isRoot = () =>
54 | location.pathname === fullLocation('/') ||
55 | location.pathname === fullLocation('')
56 | const isMultiplayer = () => location.pathname === fullLocation('/multiplayer')
57 |
58 | async function startGUI() {
59 | if (isMultiplayer()) {
60 | const room = await api.getRoom(store.room.id)
61 | setStore('room', room)
62 | await joinStartedGame()
63 | }
64 |
65 | if (isMultiplayer() && store.user.id === store.room.player2_id) return
66 |
67 | // Show GUI
68 | if (!isMultiplayer() || !store.room.game_id) {
69 | let gm = gameManager()
70 | gm?.showGUI()
71 | }
72 | }
73 |
74 | async function joinStartedGame() {
75 | // Fetching
76 | if (!store.room.game_id) return
77 |
78 | const game = await api.getGameWithHistory(store.room.game_id)
79 |
80 | // Storing
81 | setStore('game', game)
82 |
83 | // Setting
84 | let gm = gameManager()
85 |
86 | gm.fieldType = game.history.history.field_type
87 | gm.gridSize = game.history.history.size
88 |
89 | await gm?.setField()
90 |
91 | // Starting game
92 | await gm?.gameStart()
93 | }
94 |
95 | return (
96 | <>
97 | {/* Game Canvas */}
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 | >
106 | )
107 | }
108 |
109 | export default GamePage
110 |
--------------------------------------------------------------------------------
/frontend/src/pages/Login.tsx:
--------------------------------------------------------------------------------
1 | import Avatar from '@suid/material/Avatar'
2 | import Button from '@suid/material/Button'
3 | import CssBaseline from '@suid/material/CssBaseline'
4 | import TextField from '@suid/material/TextField'
5 | import Box from '@suid/material/Box'
6 | import LockOutlinedIcon from '@suid/icons-material/LockOutlined'
7 | import Typography from '@suid/material/Typography'
8 | import Container from '@suid/material/Container'
9 | import { createTheme, ThemeProvider } from '@suid/material/styles'
10 | import { createSignal, onMount } from 'solid-js'
11 | import { useNavigate } from '@solidjs/router'
12 |
13 | import createLocalStore from '../../libs'
14 | import api from '../api'
15 | import { fullLocation } from '../router'
16 |
17 | const theme = createTheme()
18 |
19 | export default function Login() {
20 | const [usernameError, setUsernameError] = createSignal('')
21 | const [store, setStore] = createLocalStore()
22 | const navigate = useNavigate()
23 |
24 | onMount(() => {
25 | // If user is registered => moving him to rooms
26 | if (store.user) {
27 | navigate(fullLocation('/rooms'))
28 | }
29 | })
30 |
31 | const handleSubmit = async (event: SubmitEvent) => {
32 | event.preventDefault()
33 | const data = new FormData(event.currentTarget)
34 |
35 | // Creating user
36 | const user = await api.register(data.get('username'))
37 |
38 | // Setting it to store
39 | setStore('user', user)
40 |
41 | // Redirecting back
42 | const redirect_url = store.redirect || fullLocation('/rooms')
43 | navigate(redirect_url)
44 | }
45 |
46 | const validateUsername = (e: InputEvent) => {
47 | const username: string = e.target?.value
48 |
49 | const errMsg = username.length === 0 ? 'Field is required' : ''
50 | setUsernameError(errMsg)
51 | }
52 |
53 | return (
54 |
55 |
56 |
57 |
65 |
66 |
67 |
68 |
69 | Registration
70 |
71 |
77 |
90 | {/* */}
100 |
109 |
110 |
111 |
112 |
113 | )
114 | }
115 |
--------------------------------------------------------------------------------
/frontend/src/pages/Settings.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | GMobiledata,
3 | Settings as SettingsIcon,
4 | Warning,
5 | } from '@suid/icons-material'
6 | import {
7 | Button,
8 | Card,
9 | CardActions,
10 | Dialog,
11 | DialogActions,
12 | DialogContent,
13 | DialogContentText,
14 | DialogTitle,
15 | IconButton,
16 | NativeSelect,
17 | Stack,
18 | } from '@suid/material'
19 | import { createSignal, For } from 'solid-js'
20 |
21 | import {
22 | defaultEnvTextures,
23 | defaultResolution,
24 | defaultTextureName,
25 | } from '../constants'
26 | import GameManager from '../logic/game_manager'
27 |
28 | export type SettingsProps = {
29 | gm: GameManager
30 | }
31 |
32 | export function Settings(props: SettingsProps) {
33 | const [isOpened, setIsOpened] = createSignal(false)
34 | const [textureName, setTextureName] = createSignal(defaultTextureName)
35 | const [resolution, setResolution] = createSignal(defaultResolution)
36 |
37 | function updateTextureName(e: any) {
38 | const newVal = e.target.value
39 | setTextureName(newVal)
40 |
41 | updateTexture()
42 | }
43 | function updateResolution(e: any) {
44 | const newVal = e.target.value
45 | setResolution(newVal)
46 |
47 | updateTexture()
48 | }
49 |
50 | function updateTexture() {
51 | const envTexture = defaultEnvTextures[textureName()][resolution()]
52 |
53 | props.gm.setEnvTexture(envTexture)
54 | }
55 |
56 | return (
57 | <>
58 |
68 |
69 | setIsOpened(true)}>
70 |
71 |
72 |
73 |
74 |
75 |
123 | >
124 | )
125 | }
126 |
--------------------------------------------------------------------------------
/frontend/src/pages/rooms/[id].tsx:
--------------------------------------------------------------------------------
1 | import CssBaseline from '@suid/material/CssBaseline'
2 | import Box from '@suid/material/Box'
3 | import Container from '@suid/material/Container'
4 | import { createTheme, ThemeProvider } from '@suid/material/styles'
5 | import { Component, createSignal, onMount, Show } from 'solid-js'
6 | import { useLocation, useNavigate, useParams } from '@solidjs/router'
7 | import { Button, Card, CardContent, Stack, Typography } from '@suid/material'
8 | import { grey } from '@suid/material/colors'
9 |
10 | import createLocalStore from '../../../libs'
11 | import api from '../../api'
12 | import { checkAuth } from '../../auth'
13 | import { FetchedUser } from '../../api/models'
14 | import { fullLocation } from '../../router'
15 |
16 | const theme = createTheme()
17 |
18 | export default function RoomPage() {
19 | const [store, setStore] = createLocalStore()
20 | const params = useParams()
21 | const navigate = useNavigate()
22 | const location = useLocation()
23 | const [anotherPlayer, setAnotherPlayer] = createSignal(
24 | null
25 | )
26 |
27 | onMount(async () => {
28 | checkAuth()
29 |
30 | // Getting room
31 | try {
32 | const room = await api.getRoom(params.id)
33 |
34 | // Inserting/Updating room in store
35 | setStore('room', room)
36 | } catch (e) {
37 | navigate(fullLocation('/404'))
38 | return
39 | }
40 |
41 | // If the user is the player 1 -> another player is (or will be) the player 2
42 | if (isUserPlayer1()) {
43 | /// Trying to fetch player 2
44 | if (store.room.player2_id !== null) {
45 | const player_2 = await api.getUser(store.room.player2_id)
46 | setAnotherPlayer(player_2)
47 | }
48 | } else {
49 | // user can be the player 2, so it's up to him to decide whether to be or not
50 | const player_1 = await api.getUser(store.room.player1_id)
51 | setAnotherPlayer(player_1)
52 | }
53 | })
54 |
55 | const isUserPlayer1 = () => store.room?.player1_id === store.user?.id
56 | const isWaitingForGameToStart = () => !isUserPlayer1() && !store.room?.game_id
57 |
58 | const copyLinkToClipboard = async () =>
59 | navigator.clipboard.writeText(window.location.href)
60 |
61 | async function enterRoom() {
62 | const room = await api.enterRoom(store.room.id)
63 | setStore('room', room)
64 | }
65 |
66 | return (
67 |
68 |
69 |
70 |
78 |
79 |
88 |
89 |
95 |
96 |
97 |
98 |
101 |
102 |
103 |
112 |
113 |
114 |
115 |
121 |
132 |
133 |
134 |
135 |
136 | )
137 | }
138 |
139 | type PlayerCardProps = {
140 | bg: string
141 | color: string
142 | player_name: string
143 | }
144 |
145 | const PlayerCard: Component = (props) => {
146 | return (
147 |
148 |
149 | {props.player_name}
150 |
151 |
152 | )
153 | }
154 |
--------------------------------------------------------------------------------
/frontend/src/pages/rooms/index.tsx:
--------------------------------------------------------------------------------
1 | import Button from '@suid/material/Button'
2 | import CssBaseline from '@suid/material/CssBaseline'
3 | import Box from '@suid/material/Box'
4 | import Container from '@suid/material/Container'
5 | import { createTheme, ThemeProvider } from '@suid/material/styles'
6 | import { onMount } from 'solid-js'
7 |
8 | import createLocalStore from '../../../libs'
9 | import api from '../../api'
10 | import { useNavigate } from '@solidjs/router'
11 | import { checkAuth } from '../../auth'
12 | import { fullLocation } from '../../router'
13 |
14 | const theme = createTheme()
15 |
16 | export default function RoomsPage() {
17 | const [_store, setStore] = createLocalStore()
18 | const navigate = useNavigate()
19 |
20 | onMount(() => {
21 | checkAuth()
22 | })
23 |
24 | const createRoom = async () => {
25 | // Creating room
26 | const room = await api.createRoom()
27 |
28 | // Storing it
29 | setStore('room', room)
30 |
31 | // Moving to the room
32 | navigate(fullLocation(`/rooms/${room.id}`))
33 | }
34 |
35 | return (
36 |
37 |
38 |
39 |
47 |
56 |
57 |
58 |
59 | )
60 | }
61 |
--------------------------------------------------------------------------------
/frontend/src/router.ts:
--------------------------------------------------------------------------------
1 | import { BASENAME } from './constants'
2 | import NotFound from './pages/404'
3 | import GamePage from './pages/Game'
4 | import LoginPage from './pages/Login'
5 | import RoomsPage from './pages/rooms'
6 | import RoomPage from './pages/rooms/[id]'
7 |
8 | export function fullLocation(location: string): string {
9 | return `${BASENAME}${location}`
10 | }
11 |
12 | const routes = [
13 | {
14 | path: ['/', '/singleplayer', '/multiplayer'],
15 | component: GamePage,
16 | },
17 | {
18 | path: '/login',
19 | component: LoginPage,
20 | },
21 | {
22 | path: '/rooms',
23 | children: [
24 | {
25 | path: '/',
26 | component: RoomsPage,
27 | },
28 | {
29 | path: '/:id',
30 | component: RoomPage,
31 | },
32 | ],
33 | },
34 | {
35 | path: '/404',
36 | component: NotFound,
37 | },
38 | ]
39 |
40 | export default routes
41 |
--------------------------------------------------------------------------------
/frontend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true,
4 | "target": "ESNext",
5 | "module": "ESNext",
6 | "moduleResolution": "node",
7 | "allowSyntheticDefaultImports": true,
8 | "esModuleInterop": true,
9 | "jsx": "preserve",
10 | "jsxImportSource": "solid-js",
11 | "types": ["vite/client", "babylonjs", "babylonjs-gui"],
12 | "noEmit": true,
13 | "isolatedModules": true
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/frontend/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import solidPlugin from 'vite-plugin-solid'
3 | import suidPlugin from '@suid/vite-plugin'
4 |
5 | export default defineConfig({
6 | plugins: [suidPlugin(), solidPlugin()],
7 | server: {
8 | port: 3000,
9 | },
10 | build: {
11 | target: 'esnext',
12 | },
13 | })
14 |
--------------------------------------------------------------------------------
/gamelib/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | version = 3
4 |
5 | [[package]]
6 | name = "either"
7 | version = "1.7.0"
8 | source = "registry+https://github.com/rust-lang/crates.io-index"
9 | checksum = "3f107b87b6afc2a64fd13cac55fe06d6c8859f12d4b14cbcdd2c67d0976781be"
10 |
11 | [[package]]
12 | name = "itertools"
13 | version = "0.10.3"
14 | source = "registry+https://github.com/rust-lang/crates.io-index"
15 | checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3"
16 | dependencies = [
17 | "either",
18 | ]
19 |
20 | [[package]]
21 | name = "itoa"
22 | version = "1.0.2"
23 | source = "registry+https://github.com/rust-lang/crates.io-index"
24 | checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d"
25 |
26 | [[package]]
27 | name = "proc-macro2"
28 | version = "1.0.40"
29 | source = "registry+https://github.com/rust-lang/crates.io-index"
30 | checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7"
31 | dependencies = [
32 | "unicode-ident",
33 | ]
34 |
35 | [[package]]
36 | name = "quote"
37 | version = "1.0.20"
38 | source = "registry+https://github.com/rust-lang/crates.io-index"
39 | checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804"
40 | dependencies = [
41 | "proc-macro2",
42 | ]
43 |
44 | [[package]]
45 | name = "ryu"
46 | version = "1.0.10"
47 | source = "registry+https://github.com/rust-lang/crates.io-index"
48 | checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695"
49 |
50 | [[package]]
51 | name = "serde"
52 | version = "1.0.137"
53 | source = "registry+https://github.com/rust-lang/crates.io-index"
54 | checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1"
55 | dependencies = [
56 | "serde_derive",
57 | ]
58 |
59 | [[package]]
60 | name = "serde_derive"
61 | version = "1.0.137"
62 | source = "registry+https://github.com/rust-lang/crates.io-index"
63 | checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be"
64 | dependencies = [
65 | "proc-macro2",
66 | "quote",
67 | "syn",
68 | ]
69 |
70 | [[package]]
71 | name = "serde_json"
72 | version = "1.0.81"
73 | source = "registry+https://github.com/rust-lang/crates.io-index"
74 | checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c"
75 | dependencies = [
76 | "itoa",
77 | "ryu",
78 | "serde",
79 | ]
80 |
81 | [[package]]
82 | name = "spherical_go_game_lib"
83 | version = "0.1.0"
84 | dependencies = [
85 | "itertools",
86 | "serde",
87 | "serde_json",
88 | "thiserror",
89 | ]
90 |
91 | [[package]]
92 | name = "syn"
93 | version = "1.0.98"
94 | source = "registry+https://github.com/rust-lang/crates.io-index"
95 | checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd"
96 | dependencies = [
97 | "proc-macro2",
98 | "quote",
99 | "unicode-ident",
100 | ]
101 |
102 | [[package]]
103 | name = "thiserror"
104 | version = "1.0.31"
105 | source = "registry+https://github.com/rust-lang/crates.io-index"
106 | checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a"
107 | dependencies = [
108 | "thiserror-impl",
109 | ]
110 |
111 | [[package]]
112 | name = "thiserror-impl"
113 | version = "1.0.31"
114 | source = "registry+https://github.com/rust-lang/crates.io-index"
115 | checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a"
116 | dependencies = [
117 | "proc-macro2",
118 | "quote",
119 | "syn",
120 | ]
121 |
122 | [[package]]
123 | name = "unicode-ident"
124 | version = "1.0.1"
125 | source = "registry+https://github.com/rust-lang/crates.io-index"
126 | checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c"
127 |
--------------------------------------------------------------------------------
/gamelib/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "spherical_go_game_lib"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 |
8 | [dependencies]
9 | thiserror = "1.0.30"
10 | itertools = "0.10.3"
11 |
12 | serde = { version = "1.0.137", features = ["derive"]}
13 | serde_json = { version = "1.0.81"}
14 |
--------------------------------------------------------------------------------
/gamelib/rust-toolchain.toml:
--------------------------------------------------------------------------------
1 | [toolchain]
2 | channel = "nightly"
3 | components = []
4 | targets = []
5 | profile = "default"
6 |
--------------------------------------------------------------------------------
/gamelib/src/aliases.rs:
--------------------------------------------------------------------------------
1 | pub type SizeType = u8;
2 |
3 | pub type PointID = usize;
4 |
--------------------------------------------------------------------------------
/gamelib/src/errors.rs:
--------------------------------------------------------------------------------
1 | use std::io;
2 |
3 | use crate::{aliases::PointID, state::GameState};
4 |
5 | #[derive(thiserror::Error, Debug)]
6 | pub enum GameLoadingError {
7 | #[error("file does not exist")]
8 | FileNotFound(#[from] io::Error),
9 | }
10 |
11 | pub type GameLoadingResult = Result;
12 |
13 | /// All common errors
14 | #[derive(thiserror::Error, Debug)]
15 | pub enum GameError {
16 | #[error("{0}")]
17 | ValidationError(String),
18 | #[error("{action} is not possible at {current:?} game state")]
19 | GameStateError { current: GameState, action: String },
20 | #[error("Point with id \"{0}\" is blocked")]
21 | PointBlocked(PointID),
22 | #[error("Point with id \"{0}\" is not empty")]
23 | PointOccupied(PointID),
24 | #[error("Suicide move is not permitted")]
25 | SuicideMoveIsNotPermitted,
26 |
27 | #[error("Error during game loading: \"{0}\"")]
28 | GameLoadingError(String),
29 | #[error("Undo move is not possible because moves history is not used in this game")]
30 | UndoIsImpossible,
31 | #[error("Game history is clear, you have nothing to undo")]
32 | UndoOnClearHistory,
33 | }
34 |
35 | pub type GameResult = Result;
36 |
--------------------------------------------------------------------------------
/gamelib/src/field/field.rs:
--------------------------------------------------------------------------------
1 | use std::{cell::RefCell, rc::Rc};
2 |
3 | use serde::{Deserialize, Serialize};
4 |
5 | use crate::{aliases::PointID, point::PointWrapper};
6 |
7 | pub type PointOwner = Rc>;
8 |
9 | #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
10 | pub enum FieldType {
11 | Regular,
12 | CubicSphere,
13 | GridSphere,
14 | }
15 |
16 | #[derive(Debug, PartialEq)]
17 | pub struct Field {
18 | points: Vec,
19 | pub field_type: FieldType,
20 | }
21 |
22 | impl Field {
23 | pub fn new(points: Vec, field_type: FieldType) -> Self {
24 | Self { points, field_type }
25 | }
26 |
27 | #[allow(dead_code)]
28 | #[inline]
29 | pub fn len(&self) -> usize {
30 | self.points.len()
31 | }
32 |
33 | pub fn get_point(&self, point_id: &PointID) -> Rc> {
34 | self.points[*point_id].clone()
35 | }
36 |
37 | pub fn get_neighbor_points(
38 | &self,
39 | point_id: &PointID,
40 | ) -> [Option>>; 4] {
41 | let point = self.get_point(point_id);
42 | let p = point.borrow();
43 | [p.top, p.right, p.bottom, p.left].map(|id| id.map(|id| self.get_point(&id)))
44 | }
45 | }
46 |
47 | // Custom implementation cause Rc> does not create completely new object in memory on .clone()
48 | // (IMAO just a bruh moment)
49 | impl Clone for Field {
50 | fn clone(&self) -> Self {
51 | let points = self
52 | .points
53 | .iter()
54 | .map(|point| Rc::new(RefCell::new(point.borrow().clone())))
55 | .collect();
56 | Self {
57 | points,
58 | field_type: self.field_type,
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/gamelib/src/field/field_builder.rs:
--------------------------------------------------------------------------------
1 | use crate::{errors, FieldType, SizeType};
2 |
3 | use super::{CubicSphereFieldBuilder, Field, GridSphereFieldBuilder, RegularFieldBuilder};
4 |
5 | /// Build field accordingly to given size and field type
6 | #[inline]
7 | pub(crate) fn build_field(size: &SizeType, field_type: FieldType) -> errors::GameResult {
8 | let field = match field_type {
9 | FieldType::CubicSphere => CubicSphereFieldBuilder::default().with_size(size)?,
10 | FieldType::GridSphere => GridSphereFieldBuilder::default().with_size(size),
11 | FieldType::Regular => RegularFieldBuilder::default().with_size(size),
12 | };
13 | Ok(field)
14 | }
15 |
--------------------------------------------------------------------------------
/gamelib/src/field/mod.rs:
--------------------------------------------------------------------------------
1 | mod cube_sphere_builder;
2 | mod field;
3 | mod field_builder;
4 | mod grid_sphere_builder;
5 | mod regular_field_builder;
6 |
7 | pub use cube_sphere_builder::CubicSphereFieldBuilder;
8 | pub use field::{Field, FieldType, PointOwner};
9 | pub(crate) use field_builder::build_field;
10 | pub use grid_sphere_builder::GridSphereFieldBuilder;
11 | pub use regular_field_builder::RegularFieldBuilder;
12 |
--------------------------------------------------------------------------------
/gamelib/src/field/regular_field_builder.rs:
--------------------------------------------------------------------------------
1 | use std::{cell::RefCell, rc::Rc};
2 |
3 | use crate::{
4 | point::{Point, PointWrapper},
5 | SizeType,
6 | };
7 |
8 | use super::{field::FieldType, Field};
9 |
10 | /// Struct to build RegularField
11 | pub struct RegularFieldBuilder;
12 |
13 | impl Default for RegularFieldBuilder {
14 | fn default() -> Self {
15 | Self {}
16 | }
17 | }
18 |
19 | impl RegularFieldBuilder {
20 | #[allow(dead_code)]
21 | pub fn with_size(&self, size: &SizeType) -> Field {
22 | self.construct(size)
23 | }
24 |
25 | #[allow(dead_code)]
26 | fn construct(&self, size: &SizeType) -> Field {
27 | let size = *size as usize;
28 |
29 | // Creating points
30 | let points: Vec<_> = {
31 | let points_count = size.pow(2);
32 |
33 | (0..points_count)
34 | .map(|i| {
35 | let top = if i < size { None } else { Some(i - size) };
36 | let bottom = if i >= (points_count - size) {
37 | None
38 | } else {
39 | Some(i + size)
40 | };
41 | let left = if i % size == 0 { None } else { Some(i - 1) };
42 | let right = if i % size == size - 1 {
43 | None
44 | } else {
45 | Some(i + 1)
46 | };
47 |
48 | Rc::new(RefCell::new(PointWrapper::new(
49 | Point::new(i),
50 | top,
51 | left,
52 | right,
53 | bottom,
54 | )))
55 | })
56 | .collect()
57 | };
58 |
59 | Field::new(points, FieldType::Regular)
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/gamelib/src/file_converters/interfaces.rs:
--------------------------------------------------------------------------------
1 | use std::fs::File;
2 | use std::io::prelude::*;
3 |
4 | use serde::de::DeserializeOwned;
5 | use serde::Serialize;
6 |
7 | use crate::errors::GameLoadingResult;
8 |
9 | /// This logic just reads/writes to/from file
10 | pub trait FileConverter {
11 | fn load(filepath: &str) -> GameLoadingResult {
12 | let mut file = File::open(filepath)?;
13 | let mut content = String::new();
14 | file.read_to_string(&mut content)?;
15 | Ok(content)
16 | }
17 |
18 | fn save(filepath: &str, content: String) -> GameLoadingResult<()> {
19 | let mut file = File::open(filepath)?;
20 | file.write(content.as_bytes())?;
21 | Ok(())
22 | }
23 | }
24 |
25 | pub trait JSONizer
26 | where
27 | T: Serialize + DeserializeOwned,
28 | {
29 | fn deserialize(json: &str) -> serde_json::Result {
30 | serde_json::from_str(json)
31 | }
32 |
33 | fn serialize(game: &T) -> serde_json::Result {
34 | serde_json::to_string(game)
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/gamelib/src/file_converters/mod.rs:
--------------------------------------------------------------------------------
1 | /// Mod for converting the game to/from files
2 | pub mod interfaces;
3 |
--------------------------------------------------------------------------------
/gamelib/src/group.rs:
--------------------------------------------------------------------------------
1 | use std::{collections::HashSet, ops::BitOrAssign};
2 |
3 | use crate::{
4 | aliases::PointID,
5 | field::Field,
6 | point::{PlayerColor, PointStatus},
7 | };
8 |
9 | #[derive(Debug, Clone, PartialEq)]
10 | pub struct Group {
11 | pub(crate) points_ids: HashSet,
12 | pub(crate) liberties: HashSet,
13 | }
14 |
15 | impl Group {
16 | /// Consider making any new point a group
17 | pub fn new(point_id: &PointID, field: &Field, color: &PlayerColor) -> Self {
18 | let mut points_ids = HashSet::new();
19 | points_ids.insert(*point_id);
20 | Self {
21 | points_ids,
22 | liberties: field
23 | .get_neighbor_points(point_id)
24 | .into_iter()
25 | .filter_map(|point| {
26 | let enemy = color.different_color();
27 | match point {
28 | Some(point) if point.borrow().inner.has_color(enemy) => None,
29 | Some(point) => Some(*point.borrow().id()),
30 | _ => None,
31 | }
32 | })
33 | .collect(),
34 | }
35 | }
36 |
37 | pub fn new_from_points(
38 | points_ids: HashSet,
39 | field: &Field,
40 | color: &PlayerColor,
41 | ) -> Vec {
42 | let mut groups: Vec<_> = points_ids
43 | .iter()
44 | .map(|point_id| Self::new(point_id, field, color))
45 | .collect();
46 |
47 | // Going through all the groups until we have only unmergeable ones
48 | let mut i = 0;
49 | while i < groups.len() {
50 | // Poping group
51 | let mut group = groups.remove(i);
52 |
53 | // Going through other groups to find if they have same liberties
54 | let joints_groups = groups
55 | .drain_filter(|other| {
56 | group
57 | .points_ids
58 | .iter()
59 | .any(|point_id| other.has_liberty(point_id))
60 | })
61 | .collect::>();
62 |
63 | // Don't increment if we have groups to merge, cause in that case
64 | // we miss checking liberties of those groups after merging them cause
65 | // we won't check them anymore
66 | let to_increment = joints_groups.is_empty();
67 |
68 | // Merging joint groups
69 | for other in joints_groups {
70 | group |= other
71 | }
72 |
73 | // Inserting group back
74 | groups.insert(i, group);
75 |
76 | if to_increment {
77 | i += 1
78 | }
79 | }
80 |
81 | groups
82 | }
83 |
84 | /// Merge another group into current
85 | pub fn merge(&mut self, mut other: Group) {
86 | // Removing intersections between them
87 | self.liberties = &self.liberties - &other.points_ids;
88 | other.liberties = &other.liberties - &self.points_ids;
89 |
90 | // Merging them
91 | self.points_ids = &self.points_ids | &other.points_ids;
92 | self.liberties = &self.liberties | &other.liberties;
93 | }
94 |
95 | pub fn refresh_liberties(&mut self, field: &Field) {
96 | self.liberties = self
97 | .points_ids
98 | .iter()
99 | .map(|id| {
100 | field
101 | .get_neighbor_points(id)
102 | .into_iter()
103 | .filter_map(|point| match point {
104 | Some(p) if p.borrow().inner.is_occupied() => None,
105 | Some(p) => Some(*p.borrow().id()),
106 | None => None,
107 | })
108 | .collect::>()
109 | })
110 | .flatten()
111 | .collect()
112 | }
113 |
114 | pub fn delete(self, field: &Field) {
115 | for id in self.points_ids {
116 | field.get_point(&id).borrow_mut().inner.status = PointStatus::Empty;
117 | }
118 | }
119 |
120 | /// Defines if the group has a liberty with the given point id
121 | #[inline]
122 | pub fn has_liberty(&self, point_id: &PointID) -> bool {
123 | self.liberties.contains(point_id)
124 | }
125 |
126 | #[inline]
127 | pub fn liberties_amount(&self) -> usize {
128 | self.liberties.len()
129 | }
130 |
131 | #[inline]
132 | pub fn points_amount(&self) -> usize {
133 | self.points_ids.len()
134 | }
135 | }
136 |
137 | impl BitOrAssign for Group {
138 | fn bitor_assign(&mut self, rhs: Self) {
139 | self.merge(rhs)
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/gamelib/src/history/manager.rs:
--------------------------------------------------------------------------------
1 | use std::collections::HashSet;
2 |
3 | use crate::{
4 | errors::{GameError, GameResult},
5 | field::build_field,
6 | game::Game,
7 | group::Group,
8 | ko_guard::KoGuard,
9 | point::PointStatus,
10 | state::GameState,
11 | PlayerColor,
12 | };
13 |
14 | use super::{StoredGame, StoredGameMove, StoredGameMoveType};
15 |
16 | /// Manager to convert game to history and vice versa
17 | pub(crate) struct HistoryManager {
18 | history: StoredGame,
19 | }
20 |
21 | impl HistoryManager {
22 | pub(crate) fn new(history: StoredGame) -> Self {
23 | Self { history }
24 | }
25 |
26 | pub(crate) fn load(&self) -> GameResult {
27 | // Creating a field
28 | let field = build_field(&self.history.meta.size, self.history.meta.field_type)?;
29 | let mut black_stones = HashSet::new();
30 | let mut white_stones = HashSet::new();
31 | let mut black_score = 0;
32 | let mut white_score = 0;
33 |
34 | let mut is_game_finished = false;
35 | let mut move_number = 0;
36 |
37 | let mut ko_guard = KoGuard::default();
38 |
39 | // Going through the history
40 | for (i, record) in self.history.moves.iter().enumerate() {
41 | move_number += 1;
42 |
43 | match record.move_type {
44 | StoredGameMoveType::Move => (),
45 | StoredGameMoveType::Pass => continue,
46 | StoredGameMoveType::Surrender => {
47 | is_game_finished = true;
48 | break;
49 | }
50 | }
51 |
52 | // Converting to players/enemies context
53 | let reminder = move_number % 2;
54 | let (mut players_stones, mut enemies_stones, mut players_score, color) = match reminder
55 | {
56 | 0 => (white_stones, black_stones, white_score, PlayerColor::White),
57 | _ => (black_stones, white_stones, black_score, PlayerColor::Black),
58 | };
59 |
60 | // Main move processing
61 | {
62 | let point_id = record.point_id.ok_or(GameError::GameLoadingError(
63 | format!("expected point ID in move with number {move_number}").to_string(),
64 | ))?;
65 | field.get_point(&point_id).borrow_mut().inner.status = PointStatus::Occupied(color);
66 | players_stones.insert(point_id);
67 |
68 | if !record.died.is_empty() {
69 | for dead_stone in &record.died {
70 | field.get_point(dead_stone).borrow_mut().inner.status = PointStatus::Empty;
71 | }
72 |
73 | enemies_stones = &enemies_stones - &record.died;
74 | players_score += record.died.len();
75 | }
76 | }
77 |
78 | // Converting back
79 | if reminder == 0 {
80 | white_stones = players_stones;
81 | black_stones = enemies_stones;
82 | white_score = players_score;
83 | } else {
84 | black_stones = players_stones;
85 | white_stones = enemies_stones;
86 | black_score = players_score;
87 | }
88 |
89 | // If it's prelast iteration - setting ko guard
90 | if i + 2 == self.history.moves.len() {
91 | ko_guard = KoGuard::new(black_stones.clone(), white_stones.clone());
92 | }
93 | }
94 |
95 | let state = if is_game_finished {
96 | GameState::Ended
97 | } else {
98 | GameState::Started
99 | };
100 |
101 | // Creating groups
102 | let mut black_groups = Group::new_from_points(black_stones, &field, &PlayerColor::Black);
103 | let mut white_groups = Group::new_from_points(white_stones, &field, &PlayerColor::White);
104 |
105 | // Refreshing their liberties
106 | black_groups
107 | .iter_mut()
108 | .for_each(|g| g.refresh_liberties(&field));
109 | white_groups
110 | .iter_mut()
111 | .for_each(|g| g.refresh_liberties(&field));
112 |
113 | // Setting actually used (next) move_number
114 | move_number += 1;
115 |
116 | Ok(Game::new_with_all_fields(
117 | state,
118 | field,
119 | black_groups,
120 | white_groups,
121 | Some(move_number),
122 | Some(black_score),
123 | Some(white_score),
124 | ko_guard,
125 | ))
126 | }
127 |
128 | pub(crate) fn append_record(&mut self, record: StoredGameMove) {
129 | self.history.moves.push(record)
130 | }
131 |
132 | pub(crate) fn pop_record(&mut self) -> GameResult<()> {
133 | self.history
134 | .moves
135 | .pop()
136 | .ok_or(GameError::UndoOnClearHistory)
137 | .map(|_| ())
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/gamelib/src/history/mod.rs:
--------------------------------------------------------------------------------
1 | mod manager;
2 | mod models;
3 |
4 | pub(crate) use manager::HistoryManager;
5 | pub use models::{StoredGame, StoredGameMeta, StoredGameMove, StoredGameMoveType};
6 |
--------------------------------------------------------------------------------
/gamelib/src/history/models.rs:
--------------------------------------------------------------------------------
1 | use std::collections::HashSet;
2 |
3 | use serde::{Deserialize, Serialize};
4 |
5 | use crate::{aliases::PointID, FieldType, SizeType};
6 |
7 | ///
8 | /// Consider a stored game to be like this:
9 | ///
10 | /// ```jsonc
11 | /// {
12 | /// "meta": {
13 | /// "field_type": "GridSphere",
14 | /// "size": 9
15 | /// },
16 | /// "moves": [
17 | /// {
18 | /// "moveType": "Move",
19 | /// "pointID": 228,
20 | /// "died": [5, 78]
21 | /// },
22 | /// {
23 | /// "moveType": "Move",
24 | /// "pointID": 1337,
25 | /// },
26 | /// {
27 | /// "moveType": "Pass",
28 | /// },
29 | /// ...
30 | /// ]
31 | /// }
32 | /// ```
33 | ///
34 | #[derive(Debug, Clone, Deserialize, Serialize)]
35 | pub struct StoredGame {
36 | pub meta: StoredGameMeta,
37 | pub moves: Vec,
38 | }
39 |
40 | #[derive(Debug, Clone, Deserialize, Serialize)]
41 | pub struct StoredGameMeta {
42 | pub field_type: FieldType,
43 | pub size: SizeType,
44 | }
45 |
46 | #[derive(Debug, Clone, Deserialize, Serialize)]
47 | #[serde(rename_all = "camelCase")]
48 | pub struct StoredGameMove {
49 | pub move_type: StoredGameMoveType,
50 |
51 | #[serde(default = "Option::default")]
52 | pub point_id: Option,
53 |
54 | #[serde(default = "HashSet::default")]
55 | pub died: HashSet,
56 | }
57 |
58 | #[derive(Debug, Clone, Deserialize, Serialize)]
59 | pub enum StoredGameMoveType {
60 | Move,
61 | Pass,
62 | Surrender,
63 | }
64 |
--------------------------------------------------------------------------------
/gamelib/src/ko_guard.rs:
--------------------------------------------------------------------------------
1 | use std::collections::HashSet;
2 |
3 | use crate::PointID;
4 |
5 | #[derive(Debug, Clone, PartialEq)]
6 | pub struct KoGuard {
7 | black_points: HashSet,
8 | white_points: HashSet,
9 | }
10 |
11 | impl KoGuard {
12 | pub fn new(black_points: HashSet, white_points: HashSet) -> Self {
13 | Self {
14 | black_points,
15 | white_points,
16 | }
17 | }
18 |
19 | pub fn update(
20 | &mut self,
21 | new_black_points: HashSet,
22 | new_white_points: HashSet,
23 | ) {
24 | self.black_points = new_black_points;
25 | self.white_points = new_white_points;
26 | }
27 |
28 | /// Checks if the ko rule was violated
29 | pub fn check(
30 | &self,
31 | next_black_points: HashSet,
32 | next_white_points: HashSet,
33 | ) -> bool {
34 | self.black_points == next_black_points && self.white_points == next_white_points
35 | }
36 | }
37 |
38 | impl Default for KoGuard {
39 | fn default() -> Self {
40 | Self {
41 | black_points: HashSet::default(),
42 | white_points: HashSet::default(),
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/gamelib/src/lib.rs:
--------------------------------------------------------------------------------
1 | #![feature(drain_filter)]
2 | #![feature(hash_drain_filter)]
3 | #![feature(slice_flatten)]
4 |
5 | use std::collections::HashSet;
6 |
7 | pub use aliases::{PointID, SizeType};
8 | use errors::GameResult;
9 | use field::build_field;
10 | pub use field::FieldType;
11 | use group::Group;
12 | use history::HistoryManager;
13 | pub use history::{StoredGame, StoredGameMeta, StoredGameMove, StoredGameMoveType};
14 | pub use point::PlayerColor;
15 |
16 | mod aliases;
17 | pub mod errors;
18 | mod field;
19 | mod file_converters;
20 | mod game;
21 | mod group;
22 | mod history;
23 | mod ko_guard;
24 | mod point;
25 | mod state;
26 |
27 | pub struct Game {
28 | pub(crate) inner: game::Game,
29 | pub(crate) history_manager: Option,
30 | }
31 |
32 | impl Game {
33 | /// Create the game
34 | pub fn new(
35 | field_type: FieldType,
36 | size: &SizeType,
37 | use_history: bool,
38 | ) -> errors::GameResult {
39 | // Creating a field by it's field_type
40 | let field = build_field(size, field_type)?;
41 |
42 | // Creating a history manager
43 | let history_manager = if use_history {
44 | let meta = StoredGameMeta {
45 | field_type,
46 | size: *size,
47 | };
48 | let stored_game = StoredGame {
49 | meta,
50 | moves: Vec::new(),
51 | };
52 | let history_manager = HistoryManager::new(stored_game);
53 | Some(history_manager)
54 | } else {
55 | None
56 | };
57 |
58 | let game = Self {
59 | inner: game::Game::new(field),
60 | history_manager,
61 | };
62 | Ok(game)
63 | }
64 |
65 | /// Load game from history
66 | pub fn new_from_history(history: StoredGame) -> GameResult {
67 | let history_manager = HistoryManager::new(history);
68 | let inner = history_manager.load()?;
69 |
70 | Ok(Self {
71 | inner,
72 | history_manager: Some(history_manager),
73 | })
74 | }
75 |
76 | /// Make a move
77 | ///
78 | /// Returns list of stoned became dead by this move
79 | pub fn make_move(&mut self, point_id: &PointID) -> errors::GameResult> {
80 | let died_stones = self.inner.make_move(point_id)?;
81 |
82 | // Filling history if we have it
83 | self.history_manager.as_mut().map(|history| {
84 | // TODO: add other types of moves (like pass and surrender)
85 | let record = StoredGameMove {
86 | move_type: StoredGameMoveType::Move,
87 | point_id: Some(*point_id),
88 | died: died_stones.clone(),
89 | };
90 | history.append_record(record)
91 | });
92 |
93 | Ok(died_stones)
94 | }
95 |
96 | /// Undo previous move
97 | pub fn undo_move(&mut self) -> errors::GameResult<()> {
98 | if self.history_manager.is_none() {
99 | return Err(errors::GameError::UndoIsImpossible);
100 | }
101 | let hm = self.history_manager.as_mut().unwrap();
102 |
103 | // Undoing move
104 | hm.pop_record()?;
105 |
106 | // Recreating game
107 | self.inner = hm.load()?;
108 |
109 | Ok(())
110 | }
111 |
112 | /// Start game
113 | pub fn start(&mut self) -> errors::GameResult<()> {
114 | self.inner.start()
115 | }
116 |
117 | /// End game
118 | pub fn end(&mut self) -> errors::GameResult<()> {
119 | self.inner.end()
120 | }
121 |
122 | #[inline]
123 | pub fn is_not_started(&self) -> bool {
124 | self.inner.is_not_started()
125 | }
126 |
127 | #[inline]
128 | pub fn is_started(&self) -> bool {
129 | self.inner.is_started()
130 | }
131 |
132 | #[inline]
133 | pub fn is_ended(&self) -> bool {
134 | self.inner.is_ended()
135 | }
136 |
137 | #[inline]
138 | pub fn get_black_stones(&self) -> Vec {
139 | Self::get_points_ids_from_groups(&self.inner.black_groups)
140 | }
141 |
142 | #[inline]
143 | pub fn get_white_stones(&self) -> Vec {
144 | Self::get_points_ids_from_groups(&self.inner.white_groups)
145 | }
146 |
147 | #[inline]
148 | fn get_points_ids_from_groups(groups: &Vec) -> Vec {
149 | groups.iter().flat_map(|g| g.points_ids.clone()).collect()
150 | }
151 |
152 | #[inline]
153 | pub fn get_black_score(&self) -> Option {
154 | self.inner.get_black_score()
155 | }
156 |
157 | #[inline]
158 | pub fn get_white_score(&self) -> Option {
159 | self.inner.get_white_score()
160 | }
161 |
162 | #[inline]
163 | pub fn player_turn(&self) -> Option {
164 | self.inner.player_turn()
165 | }
166 |
167 | #[inline]
168 | pub fn field_type(&self) -> FieldType {
169 | self.inner.field.field_type
170 | }
171 |
172 | #[inline]
173 | pub fn get_move_number(&self) -> Option {
174 | self.inner.get_move_number()
175 | }
176 | }
177 |
178 | #[cfg(test)]
179 | mod tests;
180 |
--------------------------------------------------------------------------------
/gamelib/src/point.rs:
--------------------------------------------------------------------------------
1 | use crate::aliases::PointID;
2 |
3 | /// Represents a single point in a game field
4 | #[derive(Clone, Debug, PartialEq)]
5 | pub struct Point {
6 | pub id: PointID,
7 | pub(crate) status: PointStatus,
8 | }
9 |
10 | impl Point {
11 | pub fn new(id: PointID) -> Self {
12 | Self {
13 | id,
14 | status: PointStatus::default(),
15 | }
16 | }
17 |
18 | // #[inline]
19 | // pub fn is_empty(&self) -> bool {
20 | // matches!(self.status, PointStatus::Empty)
21 | // }
22 |
23 | // #[inline]
24 | // pub fn is_blocked(&self) -> bool {
25 | // matches!(self.status, PointStatus::Blocked)
26 | // }
27 |
28 | #[inline]
29 | pub fn is_occupied(&self) -> bool {
30 | matches!(self.status, PointStatus::Occupied(_))
31 | }
32 |
33 | #[inline]
34 | pub fn has_color(&self, color: PlayerColor) -> bool {
35 | match &self.status {
36 | PointStatus::Occupied(c) if *c == color => true,
37 | _ => false,
38 | }
39 | }
40 | }
41 |
42 | #[derive(Clone, Debug, PartialEq)]
43 | pub enum PointStatus {
44 | Empty,
45 | Occupied(PlayerColor),
46 | // Blocked,
47 | }
48 |
49 | impl Default for PointStatus {
50 | fn default() -> Self {
51 | Self::Empty
52 | }
53 | }
54 |
55 | /// Represents a player for the game.
56 | /// For now we need only color
57 | #[derive(Clone, Debug, PartialEq, Copy)]
58 | pub enum PlayerColor {
59 | Black,
60 | White,
61 | }
62 |
63 | impl PlayerColor {
64 | pub fn different_color(&self) -> Self {
65 | match self {
66 | Self::Black => Self::White,
67 | Self::White => Self::Black,
68 | }
69 | }
70 | }
71 |
72 | #[derive(Debug, Clone, PartialEq)]
73 | pub struct PointWrapper {
74 | pub inner: Point,
75 | pub top: Option,
76 | pub left: Option,
77 | pub right: Option,
78 | pub bottom: Option,
79 | }
80 |
81 | impl PointWrapper {
82 | pub fn new(
83 | inner: Point,
84 | top: Option,
85 | left: Option,
86 | right: Option,
87 | bottom: Option,
88 | ) -> Self {
89 | Self {
90 | inner,
91 | top,
92 | left,
93 | right,
94 | bottom,
95 | }
96 | }
97 |
98 | #[inline]
99 | pub fn id(&self) -> &PointID {
100 | &self.inner.id
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/gamelib/src/state.rs:
--------------------------------------------------------------------------------
1 | #[derive(Debug, Clone, Copy, PartialEq)]
2 | pub enum GameState {
3 | NotStarted,
4 | Started,
5 | Ended,
6 | }
7 |
8 | impl Default for GameState {
9 | fn default() -> Self {
10 | Self::NotStarted
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/gamelib/src/tests/fixtures/game.rs:
--------------------------------------------------------------------------------
1 | use crate::{FieldType, Game, SizeType};
2 |
3 | pub fn create_and_start_game(field_type: FieldType, size: &SizeType, use_history: bool) -> Game {
4 | let mut game = Game::new(field_type, size, use_history).unwrap();
5 | game.start().unwrap();
6 | game
7 | }
8 |
--------------------------------------------------------------------------------
/gamelib/src/tests/fixtures/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod game;
2 |
--------------------------------------------------------------------------------
/gamelib/src/tests/mod.rs:
--------------------------------------------------------------------------------
1 | mod fixtures;
2 |
3 | mod test_cubic_sphere_builder;
4 | mod test_grid_sphere_builder;
5 | mod test_groups_merge;
6 | mod test_ko_rule;
7 | mod test_regular_field_builder;
8 | mod test_stone_remover;
9 | mod test_undo;
10 |
--------------------------------------------------------------------------------
/gamelib/src/tests/test_grid_sphere_builder.rs:
--------------------------------------------------------------------------------
1 | use std::cell::RefCell;
2 | use std::rc::Rc;
3 |
4 | use crate::{
5 | field::{Field, FieldType, GridSphereFieldBuilder},
6 | point::{Point, PointWrapper},
7 | };
8 |
9 | const COMPRESSED_FIELD: [[usize; 4]; 54] = [
10 | [20, 9, 1, 3],
11 | [19, 0, 2, 4],
12 | [18, 1, 17, 5],
13 | [0, 10, 4, 6],
14 | [1, 3, 5, 7],
15 | [2, 4, 16, 8],
16 | [3, 11, 7, 12],
17 | [4, 6, 8, 13],
18 | [5, 7, 15, 14],
19 | [0, 20, 10, 21],
20 | [3, 9, 11, 22],
21 | [6, 10, 12, 23],
22 | [6, 11, 13, 24],
23 | [7, 12, 14, 25],
24 | [8, 13, 15, 26],
25 | [8, 14, 16, 27],
26 | [5, 15, 17, 28],
27 | [2, 16, 18, 29],
28 | [2, 17, 19, 30],
29 | [1, 18, 20, 31],
30 | [0, 19, 9, 32],
31 | [9, 31, 22, 33],
32 | [10, 21, 23, 34],
33 | [11, 22, 24, 35],
34 | [12, 23, 25, 36],
35 | [13, 24, 26, 37],
36 | [14, 25, 27, 38],
37 | [15, 26, 28, 39],
38 | [16, 27, 29, 40],
39 | [17, 28, 30, 41],
40 | [18, 29, 31, 42],
41 | [19, 30, 32, 43],
42 | [20, 31, 21, 44],
43 | [21, 34, 44, 53],
44 | [22, 35, 33, 52],
45 | [23, 36, 34, 51],
46 | [24, 37, 35, 51],
47 | [25, 38, 36, 48],
48 | [26, 39, 37, 45],
49 | [27, 40, 38, 45],
50 | [28, 41, 39, 46],
51 | [29, 42, 40, 47],
52 | [30, 43, 41, 47],
53 | [31, 44, 42, 50],
54 | [32, 33, 43, 53],
55 | [39, 46, 38, 48],
56 | [40, 47, 45, 49],
57 | [41, 42, 46, 50],
58 | [45, 49, 37, 51],
59 | [46, 50, 48, 52],
60 | [47, 43, 49, 53],
61 | [48, 52, 36, 35],
62 | [49, 53, 51, 34],
63 | [50, 44, 52, 33],
64 | ];
65 |
66 | #[test]
67 | fn test_grid_sphere_builder_with_size_3() {
68 | let expected_field = {
69 | let points = COMPRESSED_FIELD
70 | .iter()
71 | .enumerate()
72 | .map(|(id, p)| {
73 | Rc::new(RefCell::new(PointWrapper::new(
74 | Point::new(id),
75 | Some(p[0]),
76 | Some(p[1]),
77 | Some(p[2]),
78 | Some(p[3]),
79 | )))
80 | })
81 | .collect();
82 | Field::new(points, FieldType::GridSphere)
83 | };
84 |
85 | let real = GridSphereFieldBuilder::default().with_size(&3);
86 |
87 | for id in 0..real.len() {
88 | assert_eq!(
89 | *expected_field.get_point(&id).borrow(),
90 | *real.get_point(&id).borrow()
91 | )
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/gamelib/src/tests/test_groups_merge.rs:
--------------------------------------------------------------------------------
1 | use crate::{
2 | aliases::PointID,
3 | field::{CubicSphereFieldBuilder, Field},
4 | group::Group,
5 | point::{PlayerColor, PointStatus},
6 | };
7 |
8 | #[test]
9 | fn test_merge_groups() {
10 | // Creating a game field
11 | let field = CubicSphereFieldBuilder::default().with_size(&5).unwrap();
12 |
13 | // Making a couple of moves
14 | let point_id_1 = 1;
15 | let mut group_1 = make_move(&field, &point_id_1, PlayerColor::Black);
16 | let point_id_2 = field
17 | .get_neighbor_points(&point_id_1)
18 | .into_iter()
19 | .filter_map(|point| point.map(|p| *p.borrow().id()))
20 | .next()
21 | .unwrap();
22 | let mut group_2 = make_move(&field, &point_id_2, PlayerColor::Black);
23 |
24 | let expected_group = {
25 | let mut group_1 = group_1.clone();
26 |
27 | // Removing intersections between them
28 | group_1.liberties = &group_1.liberties - &group_2.points_ids;
29 | group_2.liberties = &group_2.liberties - &group_1.points_ids;
30 |
31 | // Merging them
32 | group_1.points_ids = &group_1.points_ids | &group_2.points_ids;
33 | group_1.liberties = &group_1.liberties | &group_2.liberties;
34 |
35 | group_1
36 | };
37 |
38 | // Merging groups
39 | group_1 |= group_2.clone();
40 |
41 | assert_eq!(group_1.points_ids, expected_group.points_ids);
42 | assert_eq!(group_1.liberties, expected_group.liberties);
43 | }
44 |
45 | /// Mock function to make moves without validations at all
46 | fn make_move(field: &Field, point_id: &PointID, color: PlayerColor) -> Group {
47 | field.get_point(point_id).borrow_mut().inner.status = PointStatus::Occupied(color.clone());
48 | Group::new(point_id, field, &color)
49 | }
50 |
--------------------------------------------------------------------------------
/gamelib/src/tests/test_ko_rule.rs:
--------------------------------------------------------------------------------
1 | use crate::{errors::GameError, FieldType};
2 |
3 | use super::fixtures::game::create_and_start_game;
4 |
5 | #[test]
6 | fn test_get_blocking_error() {
7 | // Creating and starting game
8 | let mut game = create_and_start_game(FieldType::GridSphere, &5, false);
9 |
10 | /*
11 | Starting creating
12 |
13 | Reminder:
14 | 0 1 2 3 4
15 | 5 6 7 8 9
16 | 10 11 12 13 14
17 | 15 16 17 18 19
18 | 20 21 22 23 24
19 | */
20 | {
21 | // Creating a typical case
22 | let moves = [7, 8, 11, 14, 17, 18, 13];
23 | for id in moves {
24 | game.make_move(&id).unwrap();
25 | }
26 |
27 | // Making first killing move
28 | let result = game.make_move(&12);
29 | assert!(result.is_ok());
30 |
31 | // Trying to kill blocked stone
32 | // We must get blocking error here
33 | let result = game.make_move(&13);
34 | assert!(matches!(result, Err(GameError::PointBlocked(13))));
35 |
36 | // Trying to put stone at another points by both players
37 | // like a case when one player had made a Ko-threat and another didn't miss it
38 | for id in [68, 69] {
39 | assert!(game.make_move(&id).is_ok())
40 | }
41 |
42 | // Trying to put by the player that previously got the error a stone at the same point
43 | let result = game.make_move(&13);
44 | assert!(result.is_ok(), "{}", result.unwrap_err());
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/gamelib/src/tests/test_regular_field_builder.rs:
--------------------------------------------------------------------------------
1 | use std::cell::RefCell;
2 | use std::rc::Rc;
3 |
4 | use crate::{
5 | field::{Field, FieldType, RegularFieldBuilder},
6 | point::{Point, PointWrapper},
7 | };
8 |
9 | const COMPRESSED_FIELD: [[Option; 4]; 25] = [
10 | [None, None, Some(1), Some(5)],
11 | [None, Some(0), Some(2), Some(6)],
12 | [None, Some(1), Some(3), Some(7)],
13 | [None, Some(2), Some(4), Some(8)],
14 | [None, Some(3), None, Some(9)],
15 | [Some(0), None, Some(6), Some(10)],
16 | [Some(1), Some(5), Some(7), Some(11)],
17 | [Some(2), Some(6), Some(8), Some(12)],
18 | [Some(3), Some(7), Some(9), Some(13)],
19 | [Some(4), Some(8), None, Some(14)],
20 | [Some(5), None, Some(11), Some(15)],
21 | [Some(6), Some(10), Some(12), Some(16)],
22 | [Some(7), Some(11), Some(13), Some(17)],
23 | [Some(8), Some(12), Some(14), Some(18)],
24 | [Some(9), Some(13), None, Some(19)],
25 | [Some(10), None, Some(16), Some(20)],
26 | [Some(11), Some(15), Some(17), Some(21)],
27 | [Some(12), Some(16), Some(18), Some(22)],
28 | [Some(13), Some(17), Some(19), Some(23)],
29 | [Some(14), Some(18), None, Some(24)],
30 | [Some(15), None, Some(21), None],
31 | [Some(16), Some(20), Some(22), None],
32 | [Some(17), Some(21), Some(23), None],
33 | [Some(18), Some(22), Some(24), None],
34 | [Some(19), Some(23), None, None],
35 | ];
36 |
37 | #[test]
38 | fn test_regular_field_builder_with_size_5() {
39 | let expected_field = {
40 | let points = COMPRESSED_FIELD
41 | .iter()
42 | .enumerate()
43 | .map(|(id, p)| {
44 | Rc::new(RefCell::new(PointWrapper::new(
45 | Point::new(id),
46 | p[0],
47 | p[1],
48 | p[2],
49 | p[3],
50 | )))
51 | })
52 | .collect();
53 | Field::new(points, FieldType::Regular)
54 | };
55 |
56 | let real = RegularFieldBuilder::default().with_size(&5);
57 |
58 | for id in 0..real.len() {
59 | assert_eq!(
60 | *expected_field.get_point(&id).borrow(),
61 | *real.get_point(&id).borrow()
62 | )
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/gamelib/src/tests/test_stone_remover.rs:
--------------------------------------------------------------------------------
1 | use crate::{point::PointStatus, FieldType};
2 |
3 | use super::fixtures::game::create_and_start_game;
4 |
5 | #[test]
6 | fn test_remove_one_stone() {
7 | // Creating and starting game
8 | let mut game = create_and_start_game(FieldType::GridSphere, &10, false);
9 |
10 | /*
11 | Starting creating
12 |
13 | Considering the white is to be useless
14 | and the black is to surround the first white stone till it's dead
15 | */
16 | {
17 | let white_stone_to_die = 25;
18 | let moves = [15, white_stone_to_die, 24, 100, 26, 101];
19 | for id in moves {
20 | let deadlist = game.make_move(&id).unwrap();
21 | assert_eq!(deadlist.len(), 0)
22 | }
23 |
24 | let white_groups_count = game.inner.white_groups.len();
25 |
26 | // Making killing move
27 | let deadlist = game.make_move(&35).unwrap();
28 |
29 | assert_eq!(deadlist.len(), 1);
30 | assert_eq!(game.inner.white_groups.len() + 1, white_groups_count);
31 | assert!(matches!(
32 | game.inner
33 | .field
34 | .get_point(&white_stone_to_die)
35 | .borrow()
36 | .inner
37 | .status,
38 | PointStatus::Empty
39 | ))
40 | }
41 | }
42 |
43 | #[test]
44 | fn test_remove_one_stone_at_border() {
45 | // Creating and starting game
46 | let mut game = create_and_start_game(FieldType::GridSphere, &5, false);
47 |
48 | /*
49 | Starting creating
50 |
51 | Considering the white is to be useless
52 | and the black is to surround the first white stone till it's dead
53 | */
54 | {
55 | let moves = [139, 122, 121, 134, 123, 144];
56 | for id in moves {
57 | let deadlist = game.make_move(&id).unwrap();
58 | assert_eq!(deadlist.len(), 0)
59 | }
60 |
61 | // Making killing move
62 | let deadlist = game.make_move(&102).unwrap();
63 | assert_eq!(deadlist.len(), 1)
64 | }
65 |
66 | // for id in 0..(5_usize.pow(2) * 6) {
67 | // println!("{:?}", game.inner.field.get_point(&id).borrow());
68 | // }
69 | // assert!(false)
70 | }
71 |
72 | #[test]
73 | fn test_refreshing_enemies_liberties_after_losing_group() {
74 | // Creating and starting game
75 | let mut game = create_and_start_game(FieldType::GridSphere, &5, false);
76 |
77 | // Making moves
78 | let moves = [
79 | 42, 81, 82, 60, 63, 41, 61, 62, 101, 83, 80, 100, 61, 79, 40, 81, 3, 102, 80, 59, 120, 81,
80 | 121,
81 | ];
82 | for id in moves {
83 | game.make_move(&id).unwrap();
84 | }
85 |
86 | // Making a killing move
87 | let result = game.make_move(&62);
88 | assert!(result.is_ok(), "{}", result.unwrap_err());
89 | let deadlist = result.unwrap();
90 | assert_eq!(deadlist.len(), 1);
91 | }
92 |
--------------------------------------------------------------------------------
/gamelib/src/tests/test_undo.rs:
--------------------------------------------------------------------------------
1 | use super::fixtures::game::create_and_start_game;
2 | use crate::FieldType;
3 |
4 | #[test]
5 | fn test_undo() {
6 | // Creating and starting game
7 | let mut game = create_and_start_game(FieldType::Regular, &5, true);
8 |
9 | /*
10 | Reminder:
11 | 0 1 2 3 4
12 | 5 6 7 8 9
13 | 10 11 12 13 14
14 | 15 16 17 18 19
15 | 20 21 22 23 24
16 | */
17 | {
18 | // Creating a typical case
19 | let moves = [7, 8, 11, 14, 12, 18, 13, 3];
20 | for id in moves {
21 | game.make_move(&id).unwrap();
22 | }
23 |
24 | // Freezing a game
25 | let freezed_game = game.inner.clone();
26 |
27 | // Making one more move
28 | game.make_move(&15).unwrap();
29 |
30 | // Making undo
31 | assert!(game.undo_move().is_ok());
32 |
33 | // Comparing games
34 | assert_eq!(game.inner, freezed_game)
35 | }
36 | }
37 |
38 | #[test]
39 | fn test_undo_with_clear_history() {
40 | // Creating and starting game
41 | let mut game = create_and_start_game(FieldType::GridSphere, &5, true);
42 |
43 | // Trying to undo move
44 | assert!(game.undo_move().is_err());
45 | }
46 |
--------------------------------------------------------------------------------
/server/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "server"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 |
8 | [dependencies]
9 | tokio = { version = "1.23.0", features = ["full"] }
10 | axum = { version = "0.6.2", features = ["ws"] }
11 | tower-http = { version = "0.3.5", features = ["cors", "trace"], default_features = false }
12 | tracing = "0.1.37"
13 | tracing-subscriber = { version = "0.3.16", features = ["env-filter"]}
14 | futures = "0.3.25"
15 |
16 | serde = { version = "1.0.152", features = [ "derive" ] }
17 | serde_json = "1.0.91"
18 |
19 | sea-orm = { version = "^0", features = [ "sqlx-postgres", "runtime-tokio-rustls", "macros", "postgres-array" ] }
20 |
21 | thiserror = "1.0.38"
22 |
23 | uuid = { version = "1.2.2", features = ["serde", "v4"] }
24 |
25 | entity = {path="./libs/entity"}
26 | migration = {path="./libs/migration"}
27 | spherical_go_game_lib = {path="../gamelib"}
28 |
29 | [dev-dependencies]
30 | axum-test-helper = "0.2.0"
31 | serde_json = "1.0.91"
32 |
--------------------------------------------------------------------------------
/server/libs/entity/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "entity"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 |
8 | [dependencies]
9 | sea-orm = { version = "^0", features = [ "sqlx-postgres", "runtime-tokio-rustls", "macros", "postgres-array" ] }
10 |
11 | serde = { version = "1.0.152", features = [ "derive" ] }
12 |
13 | migration = {path="../migration"}
14 | spherical_go_game_lib = {path="../../../gamelib"}
15 |
--------------------------------------------------------------------------------
/server/libs/entity/src/games.rs:
--------------------------------------------------------------------------------
1 | //! `SeaORM` Entity. Generated by sea-orm-codegen 0.10.6
2 |
3 | use sea_orm::entity::prelude::*;
4 | use serde::{Deserialize, Serialize};
5 |
6 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
7 | #[sea_orm(table_name = "games")]
8 | pub struct Model {
9 | #[sea_orm(primary_key, auto_increment = false)]
10 | pub id: Uuid,
11 | pub is_ended: bool,
12 | }
13 |
14 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
15 | pub enum Relation {
16 | #[sea_orm(has_many = "super::histories::Entity")]
17 | Histories,
18 | #[sea_orm(has_many = "super::rooms::Entity")]
19 | Rooms,
20 | }
21 |
22 | impl Related for Entity {
23 | fn to() -> RelationDef {
24 | Relation::Histories.def()
25 | }
26 | }
27 |
28 | impl Related for Entity {
29 | fn to() -> RelationDef {
30 | Relation::Rooms.def()
31 | }
32 | }
33 |
34 | impl ActiveModelBehavior for ActiveModel {}
35 |
--------------------------------------------------------------------------------
/server/libs/entity/src/histories.rs:
--------------------------------------------------------------------------------
1 | //! `SeaORM` Entity. Generated by sea-orm-codegen 0.10.6
2 |
3 | use migration::FieldType;
4 | use sea_orm::entity::prelude::*;
5 | use serde::{Deserialize, Serialize};
6 | use spherical_go_game_lib::{SizeType, StoredGameMeta};
7 |
8 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
9 | #[sea_orm(table_name = "histories")]
10 | pub struct Model {
11 | #[sea_orm(primary_key, auto_increment = false)]
12 | pub id: Uuid,
13 | pub game_id: Uuid,
14 | pub size: i16,
15 | pub field_type: FieldType,
16 | }
17 |
18 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
19 | pub enum Relation {
20 | #[sea_orm(
21 | belongs_to = "super::games::Entity",
22 | from = "Column::GameId",
23 | to = "super::games::Column::Id",
24 | on_update = "NoAction",
25 | on_delete = "Cascade"
26 | )]
27 | Games,
28 | #[sea_orm(has_many = "super::history_records::Entity")]
29 | HistoryRecords,
30 | }
31 |
32 | impl Related for Entity {
33 | fn to() -> RelationDef {
34 | Relation::Games.def()
35 | }
36 | }
37 |
38 | impl Related for Entity {
39 | fn to() -> RelationDef {
40 | Relation::HistoryRecords.def()
41 | }
42 | }
43 |
44 | impl ActiveModelBehavior for ActiveModel {}
45 |
46 | impl Into for Model {
47 | fn into(self) -> StoredGameMeta {
48 | StoredGameMeta {
49 | field_type: self.field_type.into(),
50 | size: self.size as SizeType,
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/server/libs/entity/src/history.rs:
--------------------------------------------------------------------------------
1 | //! `SeaORM` Entity. Generated by sea-orm-codegen 0.10.6
2 |
3 | use migration::FieldType;
4 | use sea_orm::entity::prelude::*;
5 | use serde::{Deserialize, Serialize};
6 |
7 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
8 | #[sea_orm(table_name = "history")]
9 | pub struct Model {
10 | #[sea_orm(primary_key, auto_increment = false)]
11 | pub id: Uuid,
12 | pub game_id: Uuid,
13 | pub size: i16,
14 | pub field_type: FieldType,
15 | }
16 |
17 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
18 | pub enum Relation {
19 | #[sea_orm(
20 | belongs_to = "super::games::Entity",
21 | from = "Column::GameId",
22 | to = "super::games::Column::Id",
23 | on_update = "NoAction",
24 | on_delete = "NoAction"
25 | )]
26 | Games,
27 | }
28 |
29 | impl Related for Entity {
30 | fn to() -> RelationDef {
31 | Relation::Games.def()
32 | }
33 | }
34 |
35 | impl ActiveModelBehavior for ActiveModel {}
36 |
--------------------------------------------------------------------------------
/server/libs/entity/src/history_records.rs:
--------------------------------------------------------------------------------
1 | //! `SeaORM` Entity. Generated by sea-orm-codegen 0.10.6
2 |
3 | use std::collections::HashSet;
4 |
5 | use sea_orm::entity::prelude::*;
6 | use serde::{Deserialize, Serialize};
7 | use spherical_go_game_lib::{PointID, StoredGameMove, StoredGameMoveType};
8 |
9 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
10 | #[sea_orm(table_name = "history_records")]
11 | pub struct Model {
12 | #[sea_orm(primary_key, auto_increment = false)]
13 | pub id: Uuid,
14 | pub history_id: Uuid,
15 | pub move_number: i32,
16 | pub point_id: i32,
17 | pub died_points_ids: Vec,
18 | }
19 |
20 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
21 | pub enum Relation {
22 | #[sea_orm(
23 | belongs_to = "super::histories::Entity",
24 | from = "Column::HistoryId",
25 | to = "super::histories::Column::Id",
26 | on_update = "NoAction",
27 | on_delete = "Cascade"
28 | )]
29 | Histories,
30 | }
31 |
32 | impl Related for Entity {
33 | fn to() -> RelationDef {
34 | Relation::Histories.def()
35 | }
36 | }
37 |
38 | impl ActiveModelBehavior for ActiveModel {}
39 |
40 | impl Into for Model {
41 | fn into(self) -> StoredGameMove {
42 | // TODO: update logic once you add other move types to the db table
43 | StoredGameMove {
44 | move_type: StoredGameMoveType::Move,
45 | point_id: Some(self.point_id as PointID),
46 | died: HashSet::from_iter(self.died_points_ids.into_iter().map(|p| p as PointID)),
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/server/libs/entity/src/lib.rs:
--------------------------------------------------------------------------------
1 | pub mod games;
2 | pub mod histories;
3 | pub mod history_records;
4 | pub mod rooms;
5 | pub mod users;
6 |
--------------------------------------------------------------------------------
/server/libs/entity/src/rooms.rs:
--------------------------------------------------------------------------------
1 | //! `SeaORM` Entity. Generated by sea-orm-codegen 0.10.6
2 |
3 | use sea_orm::entity::prelude::*;
4 | use serde::{Deserialize, Serialize};
5 |
6 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
7 | #[sea_orm(table_name = "rooms")]
8 | pub struct Model {
9 | #[sea_orm(primary_key, auto_increment = false)]
10 | pub id: Uuid,
11 | pub player1_id: Uuid,
12 | pub player2_id: Option,
13 | pub game_id: Option,
14 | }
15 |
16 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
17 | pub enum Relation {
18 | #[sea_orm(
19 | belongs_to = "super::games::Entity",
20 | from = "Column::GameId",
21 | to = "super::games::Column::Id",
22 | on_update = "NoAction",
23 | on_delete = "NoAction"
24 | )]
25 | Games,
26 | #[sea_orm(
27 | belongs_to = "super::users::Entity",
28 | from = "Column::Player1Id",
29 | to = "super::users::Column::Id",
30 | on_update = "NoAction",
31 | on_delete = "NoAction"
32 | )]
33 | Users2,
34 | #[sea_orm(
35 | belongs_to = "super::users::Entity",
36 | from = "Column::Player2Id",
37 | to = "super::users::Column::Id",
38 | on_update = "NoAction",
39 | on_delete = "NoAction"
40 | )]
41 | Users1,
42 | }
43 |
44 | impl Related for Entity {
45 | fn to() -> RelationDef {
46 | Relation::Games.def()
47 | }
48 | }
49 |
50 | impl ActiveModelBehavior for ActiveModel {}
51 |
--------------------------------------------------------------------------------
/server/libs/entity/src/users.rs:
--------------------------------------------------------------------------------
1 | //! `SeaORM` Entity. Generated by sea-orm-codegen 0.10.6
2 |
3 | use sea_orm::entity::prelude::*;
4 | use serde::{Deserialize, Serialize};
5 |
6 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
7 | #[sea_orm(table_name = "users")]
8 | pub struct Model {
9 | #[sea_orm(primary_key, auto_increment = false)]
10 | pub id: Uuid,
11 | pub username: String,
12 | pub secure_id: Uuid,
13 | }
14 |
15 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
16 | pub enum Relation {}
17 |
18 | impl ActiveModelBehavior for ActiveModel {}
19 |
--------------------------------------------------------------------------------
/server/libs/migration/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "migration"
3 | version = "0.1.0"
4 | edition = "2021"
5 | publish = false
6 |
7 | [lib]
8 | name = "migration"
9 | path = "src/lib.rs"
10 |
11 | [dependencies]
12 | async-std = { version = "^1", features = ["attributes", "tokio1"] }
13 |
14 | serde = { version = "1.0.152", features = [ "derive" ] }
15 |
16 | spherical_go_game_lib = {path="../../../gamelib"}
17 |
18 | [dependencies.sea-orm-migration]
19 | version = "^0.10.0"
20 | features = [ "runtime-tokio-rustls", "sqlx-postgres" ]
21 |
--------------------------------------------------------------------------------
/server/libs/migration/README.md:
--------------------------------------------------------------------------------
1 | # Running Migrator CLI
2 |
3 | - Generate a new migration file
4 | ```sh
5 | cargo run -- migrate generate MIGRATION_NAME
6 | ```
7 | - Apply all pending migrations
8 | ```sh
9 | cargo run
10 | ```
11 | ```sh
12 | cargo run -- up
13 | ```
14 | - Apply first 10 pending migrations
15 | ```sh
16 | cargo run -- up -n 10
17 | ```
18 | - Rollback last applied migrations
19 | ```sh
20 | cargo run -- down
21 | ```
22 | - Rollback last 10 applied migrations
23 | ```sh
24 | cargo run -- down -n 10
25 | ```
26 | - Drop all tables from the database, then reapply all migrations
27 | ```sh
28 | cargo run -- fresh
29 | ```
30 | - Rollback all applied migrations, then reapply all migrations
31 | ```sh
32 | cargo run -- refresh
33 | ```
34 | - Rollback all applied migrations
35 | ```sh
36 | cargo run -- reset
37 | ```
38 | - Check the status of all migrations
39 | ```sh
40 | cargo run -- status
41 | ```
42 |
--------------------------------------------------------------------------------
/server/libs/migration/src/lib.rs:
--------------------------------------------------------------------------------
1 | pub use sea_orm_migration::prelude::*;
2 |
3 | mod m20220101_000001_create_table;
4 | mod m20230103_213731_games;
5 | mod m20230104_155530_rooms;
6 | mod m20230106_155413_histories;
7 | mod m20230108_204629_history_records;
8 |
9 | pub use m20230106_155413_histories::FieldType;
10 |
11 | pub struct Migrator;
12 |
13 | #[async_trait::async_trait]
14 | impl MigratorTrait for Migrator {
15 | fn migrations() -> Vec> {
16 | vec![
17 | Box::new(m20220101_000001_create_table::Migration),
18 | Box::new(m20230103_213731_games::Migration),
19 | Box::new(m20230104_155530_rooms::Migration),
20 | Box::new(m20230106_155413_histories::Migration),
21 | Box::new(m20230108_204629_history_records::Migration),
22 | ]
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/server/libs/migration/src/m20220101_000001_create_table.rs:
--------------------------------------------------------------------------------
1 | use sea_orm_migration::prelude::*;
2 |
3 | #[derive(DeriveMigrationName)]
4 | pub struct Migration;
5 |
6 | #[async_trait::async_trait]
7 | impl MigrationTrait for Migration {
8 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
9 | manager
10 | .create_table(
11 | Table::create()
12 | .table(User::Table)
13 | .if_not_exists()
14 | .col(ColumnDef::new(User::Id).uuid().not_null().primary_key())
15 | .col(ColumnDef::new(User::Username).string_len(15).not_null())
16 | .col(ColumnDef::new(User::SecureId).uuid().not_null())
17 | .to_owned(),
18 | )
19 | .await
20 | }
21 |
22 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
23 | manager
24 | .drop_table(Table::drop().table(User::Table).to_owned())
25 | .await
26 | }
27 | }
28 |
29 | /// Learn more at https://docs.rs/sea-query#iden
30 | #[derive(Iden)]
31 | #[iden = "users"]
32 | pub(crate) enum User {
33 | Table,
34 | Id,
35 | Username,
36 | #[iden = "secure_id"]
37 | SecureId,
38 | }
39 |
--------------------------------------------------------------------------------
/server/libs/migration/src/m20230103_213731_games.rs:
--------------------------------------------------------------------------------
1 | use sea_orm_migration::prelude::*;
2 |
3 | #[derive(DeriveMigrationName)]
4 | pub struct Migration;
5 |
6 | #[async_trait::async_trait]
7 | impl MigrationTrait for Migration {
8 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
9 | manager
10 | .create_table(
11 | Table::create()
12 | .table(Game::Table)
13 | .if_not_exists()
14 | .col(ColumnDef::new(Game::Id).uuid().not_null().primary_key())
15 | .col(ColumnDef::new(Game::IsEnded).boolean().not_null())
16 | .to_owned(),
17 | )
18 | .await
19 | }
20 |
21 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
22 | manager
23 | .drop_table(Table::drop().table(Game::Table).to_owned())
24 | .await
25 | }
26 | }
27 |
28 | /// Learn more at https://docs.rs/sea-query#iden
29 | #[derive(Iden)]
30 | #[iden = "games"]
31 | pub(crate) enum Game {
32 | Table,
33 | Id,
34 | IsEnded,
35 | }
36 |
--------------------------------------------------------------------------------
/server/libs/migration/src/m20230104_155530_rooms.rs:
--------------------------------------------------------------------------------
1 | use sea_orm_migration::prelude::*;
2 |
3 | use crate::{m20220101_000001_create_table::User, m20230103_213731_games::Game};
4 |
5 | #[derive(DeriveMigrationName)]
6 | pub struct Migration;
7 |
8 | #[async_trait::async_trait]
9 | impl MigrationTrait for Migration {
10 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
11 | manager
12 | .create_table(
13 | Table::create()
14 | .table(Room::Table)
15 | .if_not_exists()
16 | .col(ColumnDef::new(Room::Id).uuid().not_null().primary_key())
17 | .col(ColumnDef::new(Room::Player1Id).uuid().not_null())
18 | .foreign_key(
19 | ForeignKey::create()
20 | .name("fk-room-player1-id")
21 | .from_col(Room::Player1Id)
22 | .to(User::Table, User::Id),
23 | )
24 | .col(ColumnDef::new(Room::Player2Id).uuid())
25 | .foreign_key(
26 | ForeignKey::create()
27 | .name("fk-room-player2-id")
28 | .from_col(Room::Player2Id)
29 | .to(User::Table, User::Id),
30 | )
31 | .col(ColumnDef::new(Room::GameId).uuid())
32 | .foreign_key(
33 | ForeignKey::create()
34 | .name("fk-room-game-id")
35 | .from_col(Room::GameId)
36 | .to(Game::Table, Game::Id),
37 | )
38 | .to_owned(),
39 | )
40 | .await
41 | }
42 |
43 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
44 | manager
45 | .drop_table(Table::drop().table(Room::Table).to_owned())
46 | .await
47 | }
48 | }
49 |
50 | /// Learn more at https://docs.rs/sea-query#iden
51 | #[derive(Iden)]
52 | #[iden = "rooms"]
53 | enum Room {
54 | Table,
55 | Id,
56 |
57 | #[iden = "player1_id"]
58 | Player1Id,
59 |
60 | #[iden = "player2_id"]
61 | Player2Id,
62 |
63 | #[iden = "game_id"]
64 | GameId,
65 | }
66 |
--------------------------------------------------------------------------------
/server/libs/migration/src/m20230106_155413_histories.rs:
--------------------------------------------------------------------------------
1 | use sea_orm_migration::{
2 | prelude::*,
3 | sea_orm::{DeriveActiveEnum, EnumIter},
4 | };
5 | use serde::{Deserialize, Serialize};
6 | use spherical_go_game_lib::FieldType as GameLibFieldType;
7 |
8 | use crate::m20230103_213731_games::Game;
9 |
10 | #[derive(DeriveMigrationName)]
11 | pub struct Migration;
12 |
13 | #[async_trait::async_trait]
14 | impl MigrationTrait for Migration {
15 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
16 | manager
17 | .create_table(
18 | Table::create()
19 | .table(History::Table)
20 | .if_not_exists()
21 | .col(ColumnDef::new(History::Id).uuid().not_null().primary_key())
22 | .col(ColumnDef::new(History::GameId).uuid().not_null())
23 | .foreign_key(
24 | ForeignKey::create()
25 | .name("fk-history-game-id")
26 | .on_delete(ForeignKeyAction::Cascade)
27 | .from_col(History::GameId)
28 | .to(Game::Table, Game::Id),
29 | )
30 | .col(ColumnDef::new(History::Size).small_integer().not_null())
31 | .col(
32 | ColumnDef::new(History::FieldType)
33 | .small_integer()
34 | .not_null(),
35 | )
36 | .to_owned(),
37 | )
38 | .await
39 | }
40 |
41 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
42 | manager
43 | .drop_table(Table::drop().table(History::Table).to_owned())
44 | .await
45 | }
46 | }
47 |
48 | /// Learn more at https://docs.rs/sea-query#iden
49 | #[derive(Iden)]
50 | #[iden = "histories"]
51 | pub enum History {
52 | Table,
53 | Id,
54 | #[iden = "game_id"]
55 | GameId,
56 | Size,
57 | #[iden = "field_type"]
58 | FieldType,
59 | }
60 |
61 | #[derive(Debug, Clone, PartialEq, EnumIter, DeriveActiveEnum, Eq, Serialize, Deserialize)]
62 | #[sea_orm(rs_type = "i16", db_type = "SmallInteger")]
63 | pub enum FieldType {
64 | #[sea_orm(num_value = 0)]
65 | Regular,
66 | #[sea_orm(num_value = 1)]
67 | GridSphere,
68 | }
69 |
70 | impl Into for FieldType {
71 | fn into(self) -> GameLibFieldType {
72 | match self {
73 | Self::Regular => GameLibFieldType::Regular,
74 | Self::GridSphere => GameLibFieldType::GridSphere,
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/server/libs/migration/src/m20230108_204629_history_records.rs:
--------------------------------------------------------------------------------
1 | use sea_orm_migration::prelude::*;
2 |
3 | use crate::m20230106_155413_histories::History;
4 |
5 | #[derive(DeriveMigrationName)]
6 | pub struct Migration;
7 |
8 | #[async_trait::async_trait]
9 | impl MigrationTrait for Migration {
10 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
11 | manager
12 | .create_table(
13 | Table::create()
14 | .table(HistoryRecord::Table)
15 | .if_not_exists()
16 | .col(
17 | ColumnDef::new(HistoryRecord::Id)
18 | .uuid()
19 | .not_null()
20 | .primary_key(),
21 | )
22 | .col(ColumnDef::new(HistoryRecord::HistoryID).uuid().not_null())
23 | .foreign_key(
24 | ForeignKey::create()
25 | .name("fk-history_record-history-id")
26 | .on_delete(ForeignKeyAction::Cascade)
27 | .from_col(HistoryRecord::HistoryID)
28 | .to(History::Table, History::Id),
29 | )
30 | .col(
31 | ColumnDef::new(HistoryRecord::MoveNumber)
32 | .integer()
33 | .not_null(),
34 | )
35 | .col(ColumnDef::new(HistoryRecord::PointID).integer().not_null())
36 | .col(
37 | ColumnDef::new(HistoryRecord::DiedPointsIds)
38 | .array(ColumnType::Integer(None))
39 | .not_null(),
40 | )
41 | .to_owned(),
42 | )
43 | .await
44 | }
45 |
46 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
47 | manager
48 | .drop_table(Table::drop().table(HistoryRecord::Table).to_owned())
49 | .await
50 | }
51 | }
52 |
53 | /// Learn more at https://docs.rs/sea-query#iden
54 | #[derive(Iden)]
55 | #[iden = "history_records"]
56 | enum HistoryRecord {
57 | Table,
58 | Id,
59 | #[iden = "history_id"]
60 | HistoryID,
61 | #[iden = "move_number"]
62 | MoveNumber,
63 | #[iden = "point_id"]
64 | PointID,
65 | #[iden = "died_points_ids"]
66 | DiedPointsIds,
67 | }
68 |
--------------------------------------------------------------------------------
/server/libs/migration/src/main.rs:
--------------------------------------------------------------------------------
1 | use sea_orm_migration::prelude::*;
2 |
3 | #[async_std::main]
4 | async fn main() {
5 | cli::run_cli(migration::Migrator).await;
6 | }
7 |
--------------------------------------------------------------------------------
/server/src/app.rs:
--------------------------------------------------------------------------------
1 | use std::sync::Arc;
2 |
3 | use axum::Router;
4 | use tower_http::{
5 | cors,
6 | trace::{self, TraceLayer},
7 | };
8 | use tracing::Level;
9 |
10 | use crate::{
11 | apps::{games::routers::GamesRouter, rooms::routers::RoomsRouter, users::routers::UsersRouter},
12 | common::{config::Mode, routing::app_state::AppState},
13 | };
14 |
15 | pub fn create_app(app_state: Arc) -> Router {
16 | let dgs_cors = cors::CorsLayer::new()
17 | .allow_methods(cors::Any)
18 | .allow_headers(cors::Any)
19 | .allow_origin(
20 | app_state
21 | .config
22 | .allowed_origins
23 | .iter()
24 | .map(|origin| origin.parse().unwrap())
25 | .collect::>(),
26 | );
27 |
28 | let mode = app_state.config.mode.clone();
29 |
30 | let router = Router::new()
31 | .nest("/users", UsersRouter::get_router(app_state.clone()))
32 | .nest("/games", GamesRouter::get_router(app_state.clone()))
33 | .nest("/rooms", RoomsRouter::get_router(app_state))
34 | .layer(
35 | TraceLayer::new_for_http()
36 | .make_span_with(trace::DefaultMakeSpan::new().level(Level::INFO))
37 | .on_response(trace::DefaultOnResponse::new().level(Level::INFO)),
38 | );
39 |
40 | if matches!(mode, Mode::Dev) {
41 | router.layer(dgs_cors)
42 | } else {
43 | router
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/server/src/apps/games/mod.rs:
--------------------------------------------------------------------------------
1 | mod repositories;
2 | pub mod routers;
3 | pub mod schemas;
4 | mod services;
5 |
--------------------------------------------------------------------------------
/server/src/apps/games/repositories.rs:
--------------------------------------------------------------------------------
1 | use entity::games;
2 | use sea_orm::{ActiveModelTrait, ActiveValue, DbConn, EntityTrait};
3 | use uuid;
4 |
5 | use crate::common::errors::{DGSError, DGSResult};
6 |
7 | pub struct GamesRepository<'a> {
8 | db: &'a DbConn,
9 | }
10 |
11 | impl<'a> GamesRepository<'a> {
12 | pub fn new(db: &'a DbConn) -> Self {
13 | Self { db }
14 | }
15 |
16 | pub async fn create(&self) -> DGSResult {
17 | let game = games::ActiveModel {
18 | id: ActiveValue::Set(uuid::Uuid::new_v4()),
19 | is_ended: ActiveValue::Set(false),
20 | };
21 | let game = game.insert(self.db).await?;
22 |
23 | Ok(game)
24 | }
25 |
26 | pub async fn list(&self) -> DGSResult> {
27 | Ok(games::Entity::find().all(self.db).await?)
28 | }
29 |
30 | pub async fn get(&self, game_id: uuid::Uuid) -> DGSResult {
31 | Ok(games::Entity::find_by_id(game_id)
32 | .one(self.db)
33 | .await?
34 | .ok_or(DGSError::NotFound(format!("game with id {game_id}")))?
35 | .into())
36 | }
37 |
38 | pub async fn delete(&self, game_id: uuid::Uuid) -> DGSResult<()> {
39 | let res = games::Entity::delete_by_id(game_id).exec(self.db).await?;
40 |
41 | if res.rows_affected == 1 {
42 | Ok(())
43 | } else {
44 | Err(DGSError::NotFound(format!("game with id {game_id}")))
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/server/src/apps/games/schemas.rs:
--------------------------------------------------------------------------------
1 | use std::collections::HashSet;
2 |
3 | use entity::games::Model as Game;
4 | use migration::FieldType;
5 | use serde::{Deserialize, Serialize};
6 | use spherical_go_game_lib::{PointID, SizeType};
7 | use tokio::sync::broadcast;
8 |
9 | use crate::apps::{histories::schemas::HistoryWithRecords, users::schemas::OutUserSchema};
10 |
11 | #[derive(Debug, Deserialize)]
12 | pub struct CreateGameSchema {
13 | pub room_id: uuid::Uuid,
14 | pub field_type: FieldType,
15 | pub size: SizeType,
16 | }
17 |
18 | #[derive(Debug, Serialize)]
19 | pub struct GameWithHistorySchema {
20 | pub game: Game,
21 | pub history: HistoryWithRecords,
22 | }
23 |
24 | #[derive(Debug, Serialize, Deserialize)]
25 | pub struct GameWithWSLink {
26 | pub game: Game,
27 | pub ws_link: String,
28 | }
29 |
30 | impl GameWithWSLink {
31 | pub fn get_ws_link(game: &Game) -> String {
32 | format!("ws://{}", game.id)
33 | }
34 | }
35 |
36 | impl From for GameWithWSLink {
37 | fn from(game: Game) -> Self {
38 | let ws_link = Self::get_ws_link(&game);
39 | Self { game, ws_link }
40 | }
41 | }
42 |
43 | #[derive(Debug, Serialize, Deserialize, Clone)]
44 | pub struct MoveSchema {
45 | pub game_id: uuid::Uuid,
46 | pub point_id: PointID,
47 | }
48 |
49 | /// Internal message between receiving and sending tasks
50 | ///
51 | /// It defines whether to send msg to all the room users or to a single one only
52 | #[derive(Debug, Clone)]
53 | pub struct InternalMsg {
54 | pub receiver_id: Option,
55 | msg: String,
56 | }
57 |
58 | impl InternalMsg {
59 | pub fn new(receiver_id: Option, msg: String) -> Self {
60 | Self { receiver_id, msg }
61 | }
62 |
63 | pub fn get_msg(&self) -> String {
64 | self.msg.clone()
65 | }
66 | }
67 |
68 | #[derive(Debug, Clone)]
69 | pub struct RoomPlayer {
70 | pub player: OutUserSchema,
71 | pub is_connected: bool,
72 | }
73 |
74 | impl RoomPlayer {
75 | pub fn new(player: OutUserSchema, is_connected: bool) -> Self {
76 | Self {
77 | player,
78 | is_connected,
79 | }
80 | }
81 | }
82 |
83 | #[derive(Debug, Clone)]
84 | pub struct RoomState {
85 | pub room_id: uuid::Uuid,
86 | pub black_player: RoomPlayer,
87 | pub white_player: RoomPlayer,
88 | pub tx: broadcast::Sender,
89 | }
90 |
91 | #[derive(Debug, Serialize, Deserialize)]
92 | pub struct MoveWithResult {
93 | pub point_id: PointID,
94 | pub died_stones_ids: HashSet,
95 | }
96 |
97 | impl MoveWithResult {
98 | pub fn new(point_id: PointID, died_stones_ids: HashSet) -> Self {
99 | Self {
100 | point_id,
101 | died_stones_ids,
102 | }
103 | }
104 | }
105 |
106 | #[derive(Debug, Serialize, Deserialize)]
107 | pub struct WSError {
108 | pub error: String,
109 | }
110 |
111 | impl WSError {
112 | pub fn new(e: String) -> Self {
113 | Self { error: e }
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/server/src/apps/games/services.rs:
--------------------------------------------------------------------------------
1 | use sea_orm::DbConn;
2 | use spherical_go_game_lib::Game as Gamelib;
3 | use tokio::sync::broadcast;
4 |
5 | use super::schemas::{CreateGameSchema, GameWithHistorySchema, MoveSchema, RoomPlayer, RoomState};
6 | use crate::{
7 | apps::{
8 | games::{repositories::GamesRepository, schemas::GameWithWSLink},
9 | histories::{
10 | repositories::HistoriesRepository,
11 | schemas::{CreateHistoryRecordSchema, CreateHistorySchema, MoveResult},
12 | },
13 | rooms::repositories::RoomsRepository,
14 | users::repositories::UsersRepository,
15 | },
16 | common::{
17 | errors::{DGSError, DGSResult},
18 | routing::auth::AuthenticatedUser,
19 | },
20 | };
21 |
22 | /// Main game process service
23 | pub struct GameService<'a> {
24 | repo: GamesRepository<'a>,
25 | rooms_repo: RoomsRepository<'a>,
26 | histories_repo: HistoriesRepository<'a>,
27 | users_repo: UsersRepository<'a>,
28 | }
29 |
30 | impl<'a> GameService<'a> {
31 | pub fn new(db: &'a DbConn) -> Self {
32 | let repo = GamesRepository::new(db);
33 | let rooms_repo = RoomsRepository::new(db);
34 | let histories_repo = HistoriesRepository::new(db);
35 | let users_repo = UsersRepository::new(db);
36 | Self {
37 | repo,
38 | rooms_repo,
39 | histories_repo,
40 | users_repo,
41 | }
42 | }
43 |
44 | pub async fn start_game(
45 | &self,
46 | schema: CreateGameSchema,
47 | user: AuthenticatedUser,
48 | ) -> DGSResult {
49 | // TODO: turn this into atomic transaction
50 |
51 | let room = self.rooms_repo.get(schema.room_id).await?;
52 |
53 | // Validation
54 | {
55 | // Checking if a user is a first player so he has permissions to start game
56 | if user.user_id != room.player1_id {
57 | return Err(DGSError::UserIsNotPlayer1);
58 | }
59 |
60 | // Checking if there're all the players in a room
61 | if matches!(room.player2_id, None) {
62 | return Err(DGSError::Player2IsNone);
63 | }
64 |
65 | // Checking if game is already started
66 | if matches!(room.game_id, Some(_)) {
67 | return Err(DGSError::GameAlreadyStarted);
68 | }
69 | }
70 |
71 | // Creating a game
72 | let game = self.repo.create().await?;
73 |
74 | // Attaching it to the room
75 | self.rooms_repo.attach_game(room, game.id).await?;
76 |
77 | // Creating a game history
78 | {
79 | let history = CreateHistorySchema::new(game.id, schema.field_type, schema.size);
80 | self.histories_repo.create(&history).await?;
81 | }
82 |
83 | Ok(game.into())
84 | }
85 |
86 | pub async fn make_move(
87 | &self,
88 | move_schema: &MoveSchema,
89 | user: AuthenticatedUser,
90 | ) -> DGSResult {
91 | // Getting room
92 | let room = self.rooms_repo.get_by_game_id(move_schema.game_id).await?;
93 |
94 | // Checking if user is one of room players
95 | if user.user_id != room.player1_id && Some(user.user_id) != room.player2_id {
96 | return Err(DGSError::UserIsNotRoomPlayer);
97 | }
98 |
99 | // Getting game and history
100 | let game = self.repo.get(move_schema.game_id).await?;
101 | let history = self
102 | .histories_repo
103 | .get_by_game_id(move_schema.game_id)
104 | .await?;
105 | let history_id = history.history.id;
106 | let history_len = history.records.len();
107 |
108 | // Validating
109 | {
110 | // Checking if game is ended
111 | if game.is_ended {
112 | return Err(DGSError::GameEnded);
113 | }
114 |
115 | // Checking if player can make a move
116 | match history_len % 2 {
117 | 0 if user.user_id != room.player1_id => return Err(DGSError::NotPlayerTurn),
118 | 1 if user.user_id == room.player1_id => return Err(DGSError::NotPlayerTurn),
119 | _ => (),
120 | };
121 | }
122 |
123 | // Finally making move
124 | let died_stones = {
125 | let mut game = Gamelib::new_from_history(history.into())?;
126 | game.make_move(&move_schema.point_id)?
127 | };
128 |
129 | // Saving result as a history record
130 | {
131 | let move_number = history_len + 1;
132 | let record = &CreateHistoryRecordSchema::new(
133 | history_id,
134 | move_number,
135 | move_schema.point_id,
136 | died_stones.clone(),
137 | );
138 | self.histories_repo.create_record(record).await?
139 | };
140 |
141 | Ok(MoveResult::new(died_stones))
142 | }
143 |
144 | pub async fn undo_move(&self, user: AuthenticatedUser) -> DGSResult<()> {
145 | unimplemented!("undo move is currently unemplemented")
146 | }
147 |
148 | pub async fn get_room_state(&self, room_id: uuid::Uuid) -> DGSResult {
149 | // Fetching from db
150 | let room = self.rooms_repo.get(room_id).await?;
151 | let black_user = self.users_repo.get_out_user(room.player1_id).await?;
152 | let white_user = self
153 | .users_repo
154 | .get_out_user(room.player2_id.ok_or(DGSError::Unknown)?)
155 | .await?;
156 |
157 | // Creating a channel
158 | let (tx, _rx) = broadcast::channel(2);
159 |
160 | let black_user = RoomPlayer::new(black_user, true);
161 | let white_user = RoomPlayer::new(white_user, false);
162 |
163 | let room_state = RoomState {
164 | room_id: room.id,
165 | black_player: black_user,
166 | white_player: white_user,
167 | tx,
168 | };
169 |
170 | Ok(room_state)
171 | }
172 |
173 | pub async fn get_game_with_history(
174 | &self,
175 | game_id: uuid::Uuid,
176 | ) -> DGSResult {
177 | let game = self.repo.get(game_id).await?;
178 | let history = self.histories_repo.get_by_game_id(game_id).await?;
179 | Ok(GameWithHistorySchema { game, history })
180 | }
181 | }
182 |
--------------------------------------------------------------------------------
/server/src/apps/histories/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod repositories;
2 | pub mod schemas;
3 |
--------------------------------------------------------------------------------
/server/src/apps/histories/repositories.rs:
--------------------------------------------------------------------------------
1 | use entity::{histories, history_records};
2 | use sea_orm::{
3 | ActiveModelTrait, ActiveValue, ColumnTrait, DbConn, EntityTrait, QueryFilter, QueryOrder,
4 | };
5 |
6 | use super::schemas::{CreateHistoryRecordSchema, CreateHistorySchema, HistoryWithRecords};
7 | use crate::common::errors::{DGSError, DGSResult};
8 |
9 | pub struct HistoriesRepository<'a> {
10 | db: &'a DbConn,
11 | }
12 |
13 | impl<'a> HistoriesRepository<'a> {
14 | pub fn new(db: &'a DbConn) -> Self {
15 | Self { db }
16 | }
17 |
18 | pub async fn create(&self, history: &CreateHistorySchema) -> DGSResult {
19 | let history = histories::ActiveModel {
20 | id: ActiveValue::Set(uuid::Uuid::new_v4()),
21 | game_id: ActiveValue::Set(history.game_id),
22 | field_type: ActiveValue::Set(history.field_type.clone()),
23 | size: ActiveValue::Set(history.size.into()),
24 | };
25 |
26 | let history = history.insert(self.db).await?;
27 |
28 | Ok(history)
29 | }
30 |
31 | pub async fn create_record(
32 | &self,
33 | record: &CreateHistoryRecordSchema,
34 | ) -> DGSResult {
35 | let record = history_records::ActiveModel {
36 | id: ActiveValue::Set(uuid::Uuid::new_v4()),
37 | history_id: ActiveValue::Set(record.history_id),
38 | move_number: ActiveValue::Set(record.move_number as i32),
39 | point_id: ActiveValue::Set(record.point_id as i32),
40 | died_points_ids: ActiveValue::Set(
41 | record
42 | .died_stones_ids
43 | .iter()
44 | .map(|rec| *rec as i32)
45 | .collect(),
46 | ),
47 | };
48 |
49 | let record = record.insert(self.db).await?;
50 |
51 | Ok(record)
52 | }
53 |
54 | pub async fn get_by_game_id(&self, game_id: uuid::Uuid) -> DGSResult {
55 | // Getting a history itself
56 | let history = histories::Entity::find()
57 | .filter(histories::Column::GameId.eq(game_id))
58 | .one(self.db)
59 | .await?
60 | .ok_or(DGSError::NotFound(format!(
61 | "history with game id {game_id}"
62 | )))?;
63 |
64 | // Getting its records
65 | let records = history_records::Entity::find()
66 | .filter(history_records::Column::HistoryId.eq(history.id))
67 | .order_by_asc(history_records::Column::MoveNumber)
68 | .all(self.db)
69 | .await?;
70 |
71 | Ok(HistoryWithRecords { history, records })
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/server/src/apps/histories/schemas.rs:
--------------------------------------------------------------------------------
1 | use std::collections::HashSet;
2 |
3 | use entity::{histories, history_records};
4 | use migration::FieldType;
5 | use serde::Serialize;
6 | use spherical_go_game_lib::{PointID, SizeType, StoredGame};
7 |
8 | #[derive(Debug)]
9 | pub struct CreateHistorySchema {
10 | pub game_id: uuid::Uuid,
11 | pub field_type: FieldType,
12 | pub size: SizeType,
13 | }
14 |
15 | impl CreateHistorySchema {
16 | pub fn new(game_id: uuid::Uuid, field_type: FieldType, size: SizeType) -> Self {
17 | Self {
18 | game_id,
19 | field_type,
20 | size,
21 | }
22 | }
23 | }
24 |
25 | #[derive(Debug)]
26 | pub struct CreateHistoryRecordSchema {
27 | pub history_id: uuid::Uuid,
28 | pub move_number: usize,
29 | pub point_id: PointID,
30 | pub died_stones_ids: HashSet,
31 | }
32 |
33 | impl CreateHistoryRecordSchema {
34 | pub fn new(
35 | history_id: uuid::Uuid,
36 | move_number: usize,
37 | point_id: PointID,
38 | died_stones_ids: HashSet,
39 | ) -> Self {
40 | Self {
41 | history_id,
42 | move_number,
43 | point_id,
44 | died_stones_ids,
45 | }
46 | }
47 | }
48 |
49 | #[derive(Debug, Serialize)]
50 | pub struct HistoryWithRecords {
51 | pub history: histories::Model,
52 | pub records: Vec,
53 | }
54 |
55 | impl Into for HistoryWithRecords {
56 | fn into(self) -> StoredGame {
57 | StoredGame {
58 | meta: self.history.into(),
59 | moves: self.records.into_iter().map(|rec| rec.into()).collect(),
60 | }
61 | }
62 | }
63 |
64 | #[derive(Debug, Serialize)]
65 | pub struct MoveResult {
66 | pub died_stones_ids: HashSet,
67 | }
68 |
69 | impl MoveResult {
70 | pub fn new(died_stones_ids: HashSet) -> Self {
71 | Self { died_stones_ids }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/server/src/apps/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod games;
2 | pub mod histories;
3 | pub mod rooms;
4 | pub mod users;
5 |
--------------------------------------------------------------------------------
/server/src/apps/rooms/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod repositories;
2 | pub mod routers;
3 | mod services;
4 |
--------------------------------------------------------------------------------
/server/src/apps/rooms/repositories.rs:
--------------------------------------------------------------------------------
1 | use entity::rooms;
2 | use sea_orm::{ActiveModelTrait, ActiveValue, ColumnTrait, DbConn, EntityTrait, QueryFilter};
3 | use uuid;
4 |
5 | use crate::common::{
6 | errors::{DGSError, DGSResult},
7 | routing::auth::AuthenticatedUser,
8 | };
9 |
10 | pub struct RoomsRepository<'a> {
11 | db: &'a DbConn,
12 | }
13 |
14 | impl<'a> RoomsRepository<'a> {
15 | pub fn new(db: &'a DbConn) -> Self {
16 | Self { db }
17 | }
18 |
19 | pub async fn create(&self, user_id: uuid::Uuid) -> DGSResult {
20 | let room = rooms::ActiveModel {
21 | id: ActiveValue::Set(uuid::Uuid::new_v4()),
22 | player1_id: ActiveValue::Set(user_id),
23 | ..Default::default()
24 | };
25 | let room = room.insert(self.db).await?;
26 |
27 | Ok(room)
28 | }
29 |
30 | pub async fn list(&self) -> DGSResult> {
31 | Ok(rooms::Entity::find().all(self.db).await?)
32 | }
33 |
34 | pub async fn get(&self, room_id: uuid::Uuid) -> DGSResult {
35 | Ok(rooms::Entity::find_by_id(room_id)
36 | .one(self.db)
37 | .await?
38 | .ok_or(DGSError::NotFound(format!("room with id {room_id}")))?
39 | .into())
40 | }
41 |
42 | pub async fn get_by_game_id(&self, game_id: uuid::Uuid) -> DGSResult {
43 | Ok(rooms::Entity::find()
44 | .filter(rooms::Column::GameId.eq(game_id))
45 | .one(self.db)
46 | .await?
47 | .ok_or(DGSError::NotFound(format!("room with game id {game_id}")))?
48 | .into())
49 | }
50 |
51 | pub async fn delete(&self, room_id: uuid::Uuid) -> DGSResult<()> {
52 | let res = rooms::Entity::delete_by_id(room_id).exec(self.db).await?;
53 |
54 | if res.rows_affected == 1 {
55 | Ok(())
56 | } else {
57 | Err(DGSError::NotFound(format!("room with id {room_id}")))
58 | }
59 | }
60 |
61 | pub async fn add_player2(
62 | &self,
63 | room_id: uuid::Uuid,
64 | user: AuthenticatedUser,
65 | ) -> DGSResult {
66 | // Checking if the game already has a second player
67 | let game = self.get(room_id).await?;
68 | if matches!(game.player2_id, Some(_)) {
69 | return Err(DGSError::CannotAddPlayer);
70 | }
71 |
72 | // Adding a player
73 | let mut game: rooms::ActiveModel = game.into();
74 | game.player2_id = ActiveValue::Set(Some(user.user_id));
75 |
76 | // Updating it
77 | Ok(game.update(self.db).await?)
78 | }
79 |
80 | pub async fn attach_game(
81 | &self,
82 | room: rooms::Model,
83 | game_id: uuid::Uuid,
84 | ) -> DGSResult {
85 | let mut room: rooms::ActiveModel = room.into();
86 | room.game_id = ActiveValue::Set(Some(game_id));
87 |
88 | Ok(room.update(self.db).await?)
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/server/src/apps/rooms/routers.rs:
--------------------------------------------------------------------------------
1 | use std::sync::Arc;
2 |
3 | use axum::{
4 | extract::{Path, State},
5 | http::StatusCode,
6 | response::IntoResponse,
7 | routing::{get, patch, post},
8 | Json, Router,
9 | };
10 |
11 | use super::services::RoomService;
12 | use crate::common::routing::{app_state::AppState, auth::AuthenticatedUser};
13 |
14 | pub struct RoomsRouter;
15 |
16 | impl RoomsRouter {
17 | pub fn get_router(state: Arc) -> Router {
18 | Router::new()
19 | .route("/", post(Self::create_room))
20 | .route("/:room_id", get(Self::get_room))
21 | .route("/:room_id/enter", patch(Self::accept_invitation))
22 | .with_state(state)
23 | }
24 |
25 | pub async fn create_room(
26 | State(state): State>,
27 | user: AuthenticatedUser,
28 | ) -> impl IntoResponse {
29 | let room = RoomService::new(&state.db).create(user.user_id).await?;
30 | Ok::<_, (StatusCode, String)>((StatusCode::CREATED, Json(room)))
31 | }
32 |
33 | #[allow(dead_code)]
34 | pub async fn list_rooms(State(state): State>) -> impl IntoResponse {
35 | let rooms = RoomService::new(&state.db).list().await?;
36 | Ok::<_, (StatusCode, String)>((StatusCode::OK, Json(rooms)))
37 | }
38 |
39 | pub async fn get_room(
40 | State(state): State>,
41 | Path(room_id): Path,
42 | ) -> impl IntoResponse {
43 | let room = RoomService::new(&state.db).get(room_id).await?;
44 | Ok::<_, (StatusCode, String)>((StatusCode::OK, Json(room)))
45 | }
46 |
47 | pub async fn accept_invitation(
48 | State(state): State>,
49 | Path(room_id): Path,
50 | user: AuthenticatedUser,
51 | ) -> impl IntoResponse {
52 | let room = RoomService::new(&state.db)
53 | .accept_invitation(room_id, user)
54 | .await?;
55 | Ok::<_, (StatusCode, String)>((StatusCode::ACCEPTED, Json(room)))
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/server/src/apps/rooms/services.rs:
--------------------------------------------------------------------------------
1 | use entity::rooms;
2 | use sea_orm::DbConn;
3 |
4 | use super::repositories::RoomsRepository;
5 | use crate::common::{errors::DGSResult, routing::auth::AuthenticatedUser};
6 |
7 | pub struct RoomService<'a> {
8 | repo: RoomsRepository<'a>,
9 | }
10 |
11 | impl<'a> RoomService<'a> {
12 | pub fn new(db: &'a DbConn) -> Self {
13 | let repo = RoomsRepository::new(db);
14 | Self { repo }
15 | }
16 |
17 | pub async fn create(&self, user_id: uuid::Uuid) -> DGSResult {
18 | self.repo.create(user_id).await
19 | }
20 |
21 | pub async fn list(&self) -> DGSResult> {
22 | self.repo.list().await
23 | }
24 |
25 | pub async fn get(&self, room_id: uuid::Uuid) -> DGSResult {
26 | self.repo.get(room_id).await
27 | }
28 |
29 | #[allow(dead_code)]
30 | pub async fn delete(&self, room_id: uuid::Uuid) -> DGSResult<()> {
31 | self.repo.delete(room_id).await
32 | }
33 |
34 | pub async fn accept_invitation(
35 | &self,
36 | room_id: uuid::Uuid,
37 | user: AuthenticatedUser,
38 | ) -> DGSResult {
39 | self.repo.add_player2(room_id, user).await
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/server/src/apps/users/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod repositories;
2 | pub mod routers;
3 | pub mod schemas;
4 | pub mod services;
5 |
--------------------------------------------------------------------------------
/server/src/apps/users/repositories.rs:
--------------------------------------------------------------------------------
1 | use entity::users;
2 | use sea_orm::{ActiveModelTrait, ActiveValue, ColumnTrait, DbConn, EntityTrait, QueryFilter};
3 | use uuid;
4 |
5 | use super::schemas::OutUserSchema;
6 | use crate::apps::users::schemas::CreateUserSchema;
7 | use crate::common::errors::DGSError;
8 | use crate::common::errors::DGSResult;
9 |
10 | pub struct UsersRepository<'a> {
11 | db: &'a DbConn,
12 | }
13 |
14 | impl<'a> UsersRepository<'a> {
15 | pub fn new(db: &'a DbConn) -> Self {
16 | Self { db }
17 | }
18 |
19 | pub async fn create(&self, user: &CreateUserSchema) -> DGSResult {
20 | let user = users::ActiveModel {
21 | id: ActiveValue::Set(uuid::Uuid::new_v4()),
22 | username: ActiveValue::Set(user.username.clone()),
23 | secure_id: ActiveValue::Set(uuid::Uuid::new_v4()),
24 | };
25 |
26 | let user_from_db = user.insert(self.db).await?;
27 |
28 | Ok(user_from_db)
29 | }
30 |
31 | pub async fn list(&self) -> DGSResult> {
32 | let db_users = users::Entity::find().all(self.db).await?;
33 | Ok(db_users.into_iter().map(|u| u.into()).collect())
34 | }
35 |
36 | pub async fn get(&self, user_id: uuid::Uuid) -> DGSResult {
37 | users::Entity::find_by_id(user_id)
38 | .one(self.db)
39 | .await?
40 | .ok_or(DGSError::NotFound(format!("user with id {user_id}")))
41 | }
42 |
43 | pub async fn get_out_user(&self, user_id: uuid::Uuid) -> DGSResult {
44 | self.get(user_id).await.map(|user| user.into())
45 | }
46 |
47 | pub async fn delete(&self, user_id: uuid::Uuid, secure_id: uuid::Uuid) -> DGSResult<()> {
48 | let res = users::Entity::delete_by_id(user_id)
49 | .filter(users::Column::SecureId.eq(secure_id))
50 | .exec(self.db)
51 | .await?;
52 |
53 | if res.rows_affected == 1 {
54 | Ok(())
55 | } else {
56 | Err(DGSError::NotFound(format!(
57 | "user with id {user_id} and secure_id {secure_id}"
58 | )))
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/server/src/apps/users/routers.rs:
--------------------------------------------------------------------------------
1 | use std::sync::Arc;
2 |
3 | use axum::{
4 | extract::{Path, State},
5 | http::StatusCode,
6 | response::IntoResponse,
7 | routing::get,
8 | Json, Router,
9 | };
10 |
11 | use super::{
12 | schemas::{CreateUserSchema, DeleteUserSchema},
13 | services::UserService,
14 | };
15 | use crate::common::routing::app_state::AppState;
16 |
17 | pub struct UsersRouter;
18 |
19 | impl UsersRouter {
20 | pub fn get_router(state: Arc) -> Router {
21 | Router::new()
22 | .route("/", get(Self::list_users).post(Self::create_user))
23 | .route("/:user_id", get(Self::get_user).delete(Self::delete_user))
24 | .with_state(state)
25 | }
26 |
27 | pub async fn create_user(
28 | State(state): State>,
29 | Json(user): Json,
30 | ) -> impl IntoResponse {
31 | let user = UserService::new(&state.db).create(&user).await?;
32 | Ok::<_, (StatusCode, String)>((StatusCode::CREATED, Json(user)))
33 | }
34 |
35 | pub async fn list_users(State(state): State>) -> impl IntoResponse {
36 | let users = UserService::new(&state.db).list().await?;
37 | Ok::<_, (StatusCode, String)>((StatusCode::OK, Json(users)))
38 | }
39 |
40 | pub async fn get_user(
41 | State(state): State>,
42 | Path(user_id): Path,
43 | ) -> impl IntoResponse {
44 | let user = UserService::new(&state.db).get(user_id).await?;
45 | Ok::<_, (StatusCode, String)>((StatusCode::OK, Json(user)))
46 | }
47 |
48 | pub async fn delete_user(
49 | State(state): State>,
50 | Path(user_id): Path,
51 | Json(delete_schema): Json,
52 | ) -> impl IntoResponse {
53 | UserService::new(&state.db)
54 | .delete(user_id, delete_schema.secure_id)
55 | .await?;
56 | Ok::<_, (StatusCode, String)>(StatusCode::NO_CONTENT)
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/server/src/apps/users/schemas.rs:
--------------------------------------------------------------------------------
1 | use entity::users::Model as User;
2 | use serde::{Deserialize, Serialize};
3 |
4 | #[derive(Debug, Deserialize)]
5 | pub struct CreateUserSchema {
6 | pub username: String,
7 | }
8 |
9 | #[derive(Debug, Deserialize)]
10 | pub struct DeleteUserSchema {
11 | pub secure_id: uuid::Uuid,
12 | }
13 |
14 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
15 | pub struct OutUserSchema {
16 | pub id: uuid::Uuid,
17 | pub username: String,
18 | }
19 |
20 | impl From for OutUserSchema {
21 | fn from(value: User) -> Self {
22 | Self {
23 | id: value.id,
24 | username: value.username,
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/server/src/apps/users/services.rs:
--------------------------------------------------------------------------------
1 | use entity::users;
2 | use sea_orm::DbConn;
3 |
4 | use super::{
5 | repositories::UsersRepository,
6 | schemas::{CreateUserSchema, OutUserSchema},
7 | };
8 | use crate::common::{
9 | errors::{DGSError, DGSResult},
10 | routing::auth::AuthenticatedUser,
11 | };
12 |
13 | pub struct UserService<'a> {
14 | repo: UsersRepository<'a>,
15 | }
16 |
17 | impl<'a> UserService<'a> {
18 | pub fn new(db: &'a DbConn) -> Self {
19 | let repo = UsersRepository::new(db);
20 | Self { repo }
21 | }
22 |
23 | pub async fn create(&self, user: &CreateUserSchema) -> DGSResult {
24 | self.repo.create(user).await
25 | }
26 |
27 | pub async fn list(&self) -> DGSResult> {
28 | self.repo.list().await
29 | }
30 |
31 | pub async fn get(&self, user_id: uuid::Uuid) -> DGSResult {
32 | self.repo.get_out_user(user_id).await
33 | }
34 |
35 | pub async fn delete(&self, user_id: uuid::Uuid, secure_id: uuid::Uuid) -> DGSResult<()> {
36 | self.repo.delete(user_id, secure_id).await
37 | }
38 |
39 | pub async fn authenticate(&self, token: String) -> DGSResult {
40 | // Getting user token
41 | let token_user = AuthenticatedUser::decode_token(&token)?;
42 |
43 | // Getting db user
44 | let db_user = self.repo.get(token_user.user_id).await?;
45 |
46 | // Authenticating
47 | if token_user.secure_id == db_user.secure_id {
48 | Ok(token_user)
49 | } else {
50 | Err(DGSError::AuthenticationError)
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/server/src/common/aliases.rs:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/server/src/common/config.rs:
--------------------------------------------------------------------------------
1 | use std::{env, str::FromStr};
2 |
3 | use super::errors::{DGSError, DGSResult};
4 | #[derive(Debug, Clone)]
5 | pub struct Config {
6 | pub db_uri: String,
7 | pub port: u16,
8 | pub allowed_origins: Vec,
9 | pub mode: Mode,
10 | }
11 |
12 | impl Config {
13 | pub fn new() -> DGSResult {
14 | let db_uri = Self::get_env_var("DATABASE_URL")?;
15 | let port = Self::get_env_var("PORT")?;
16 | let allowed_origins = Self::get_env_var::("ALLOWED_ORIGINS")?
17 | .split(" ")
18 | .map(|origin| origin.to_string())
19 | .collect();
20 | let mode = Self::get_env_var::("MODE")?.as_str().parse()?;
21 |
22 | Ok(Self {
23 | db_uri,
24 | port,
25 | allowed_origins,
26 | mode,
27 | })
28 | }
29 |
30 | #[inline]
31 | fn get_env_var(env_var: &str) -> DGSResult {
32 | env::var(env_var)
33 | .map_err(|_| DGSError::EnvConfigLoadingError(env_var.to_owned()))?
34 | .parse::()
35 | .map_err(|_| DGSError::EnvVarParsingError(env_var.to_owned()))
36 | }
37 | }
38 |
39 | #[derive(Debug, Clone)]
40 | pub enum Mode {
41 | Test,
42 | Dev,
43 | Prod,
44 | }
45 |
46 | impl FromStr for Mode {
47 | type Err = DGSError;
48 |
49 | fn from_str(s: &str) -> Result {
50 | match s.to_lowercase().as_str() {
51 | "test" => Ok(Self::Test),
52 | "dev" => Ok(Self::Dev),
53 | "prod" => Ok(Self::Prod),
54 | _ => Err(DGSError::EnvVarParsingError(s.to_string())),
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/server/src/common/db/connection.rs:
--------------------------------------------------------------------------------
1 | use sea_orm::{Database, DbConn};
2 |
3 | use crate::common::errors::DGSResult;
4 |
5 | pub async fn get_db(db_uri: &str) -> DGSResult {
6 | Ok(Database::connect(db_uri).await?)
7 | }
8 |
--------------------------------------------------------------------------------
/server/src/common/db/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod connection;
2 |
--------------------------------------------------------------------------------
/server/src/common/errors.rs:
--------------------------------------------------------------------------------
1 | use axum::http::StatusCode;
2 | use sea_orm::error::DbErr;
3 | use serde::Serialize;
4 | use spherical_go_game_lib::errors::GameError;
5 | use thiserror::Error;
6 |
7 | #[derive(Debug, Error, Serialize)]
8 | pub enum DGSError {
9 | #[error("environment variable `{0}` is not set")]
10 | EnvConfigLoadingError(String),
11 | #[error("environment variable `{0}` cannot be parsed")]
12 | EnvVarParsingError(String),
13 | #[error("cannot establish connection with db")]
14 | DBConnectionError,
15 | #[error("not found: `{0}`")]
16 | NotFound(String),
17 | #[error("cannot decode token")]
18 | TokenDecodingError,
19 | #[error("wrong credentials")]
20 | AuthenticationError,
21 | #[error("game is already full of players")]
22 | CannotAddPlayer,
23 | #[error("only first player can start game")]
24 | UserIsNotPlayer1,
25 | #[error("you need to add a second player to start game")]
26 | Player2IsNone,
27 | #[error("game already started")]
28 | GameAlreadyStarted,
29 | #[error("user is not one of players")]
30 | UserIsNotRoomPlayer,
31 | #[error("it is not the player's turn now")]
32 | NotPlayerTurn,
33 | #[error("game is already ended")]
34 | GameEnded,
35 |
36 | #[error("{0}")]
37 | GameInternalError(String),
38 |
39 | #[error("unknown error")]
40 | Unknown,
41 | }
42 |
43 | impl From for DGSError {
44 | fn from(e: DbErr) -> Self {
45 | match e {
46 | DbErr::ConnectionAcquire => Self::DBConnectionError,
47 | DbErr::RecordNotFound(s) => Self::NotFound(s),
48 | _ => {
49 | println!("[DB Error] {e}");
50 | Self::Unknown
51 | }
52 | }
53 | }
54 | }
55 |
56 | impl From for (StatusCode, String) {
57 | fn from(e: DGSError) -> Self {
58 | match &e {
59 | DGSError::NotFound(_) => (StatusCode::NOT_FOUND, e.to_string()),
60 | DGSError::TokenDecodingError => (StatusCode::UNAUTHORIZED, e.to_string()),
61 | DGSError::CannotAddPlayer => (StatusCode::CONFLICT, e.to_string()),
62 | DGSError::UserIsNotPlayer1 => (StatusCode::FORBIDDEN, e.to_string()),
63 | DGSError::GameAlreadyStarted => (StatusCode::CONFLICT, e.to_string()),
64 | DGSError::Player2IsNone => (StatusCode::CONFLICT, e.to_string()),
65 | _ => (
66 | StatusCode::INTERNAL_SERVER_ERROR,
67 | "Something went wrong".to_owned(),
68 | ),
69 | }
70 | }
71 | }
72 |
73 | impl From for DGSError {
74 | fn from(e: GameError) -> Self {
75 | Self::GameInternalError(e.to_string())
76 | }
77 | }
78 |
79 | pub type DGSResult = Result;
80 |
--------------------------------------------------------------------------------
/server/src/common/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod aliases;
2 | pub mod config;
3 | pub mod db;
4 | pub mod errors;
5 | pub mod routing;
6 |
--------------------------------------------------------------------------------
/server/src/common/routing/app_state.rs:
--------------------------------------------------------------------------------
1 | use sea_orm::DbConn;
2 |
3 | use crate::common::config::Config;
4 |
5 | #[derive(Debug, Clone)]
6 | pub struct AppState {
7 | pub db: DbConn,
8 | pub config: Config,
9 | }
10 |
11 | impl AppState {
12 | pub fn new(db: DbConn, config: Config) -> Self {
13 | Self { db, config }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/server/src/common/routing/auth.rs:
--------------------------------------------------------------------------------
1 | use axum::http::{request::Parts, StatusCode};
2 | use axum::{async_trait, extract::FromRequestParts};
3 |
4 | use crate::common::errors::{DGSError, DGSResult};
5 |
6 | #[derive(Debug, Clone)]
7 | pub struct AuthenticatedUser {
8 | pub user_id: uuid::Uuid,
9 | pub secure_id: uuid::Uuid,
10 | }
11 |
12 | impl AuthenticatedUser {
13 | fn decode_request_parts(req: &mut Parts) -> DGSResult {
14 | // Get authorization header
15 | let authorization = Self::get_header(req)?;
16 | Self::decode_token(authorization)
17 | }
18 |
19 | pub fn decode_token(token: &str) -> DGSResult {
20 | let (user_id, secure_id) = token.split_once(':').ok_or(DGSError::TokenDecodingError)?;
21 | let user_id = uuid::Uuid::parse_str(user_id).map_err(|_| DGSError::TokenDecodingError)?;
22 | let secure_id =
23 | uuid::Uuid::parse_str(secure_id).map_err(|_| DGSError::TokenDecodingError)?;
24 |
25 | Ok(Self { user_id, secure_id })
26 | }
27 |
28 | fn get_header(parts: &mut Parts) -> DGSResult<&str> {
29 | Ok(parts
30 | .headers
31 | .get("AUTHORIZATION")
32 | .ok_or(DGSError::TokenDecodingError)?
33 | .to_str()
34 | .map_err(|_| DGSError::TokenDecodingError)?)
35 | }
36 | }
37 |
38 | #[async_trait]
39 | impl FromRequestParts for AuthenticatedUser
40 | where
41 | S: Send + Sync,
42 | {
43 | type Rejection = (StatusCode, String);
44 |
45 | async fn from_request_parts(parts: &mut Parts, _: &S) -> Result {
46 | Ok(Self::decode_request_parts(parts)?)
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/server/src/common/routing/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod app_state;
2 | pub mod auth;
3 |
--------------------------------------------------------------------------------
/server/src/main.rs:
--------------------------------------------------------------------------------
1 | use std::{net::SocketAddr, sync::Arc};
2 |
3 | use migration::{Migrator, MigratorTrait};
4 |
5 | use app::create_app;
6 | use common::{config::Config, db::connection::get_db, routing::app_state::AppState};
7 |
8 | mod app;
9 | mod apps;
10 | mod common;
11 |
12 | #[tokio::main]
13 | async fn main() {
14 | tracing_subscriber::fmt()
15 | .with_target(false)
16 | .compact()
17 | .init();
18 |
19 | // Creating config
20 | let config = Config::new().unwrap();
21 |
22 | // Creating a db connection
23 | let db = get_db(&config.db_uri).await.unwrap();
24 |
25 | // Running migrations
26 | Migrator::up(&db, None).await.unwrap();
27 |
28 | // Getting url
29 | let addr = SocketAddr::from(([0, 0, 0, 0], config.port));
30 |
31 | // Creating app_state
32 | let shared_state = {
33 | let app_state = AppState::new(db, config);
34 | Arc::new(app_state)
35 | };
36 |
37 | // build our application with a single route
38 | let app = create_app(shared_state);
39 |
40 | // Logging about successful start
41 | tracing::info!("listening on {addr}");
42 |
43 | // run it with hyper on localhost:3000
44 | axum::Server::bind(&addr)
45 | .serve(app.into_make_service())
46 | .await
47 | .unwrap();
48 | }
49 |
50 | #[cfg(test)]
51 | mod tests;
52 |
--------------------------------------------------------------------------------
/server/src/tests/fixtures/create_room.rs:
--------------------------------------------------------------------------------
1 | use axum::http::StatusCode;
2 | use axum_test_helper::TestClient;
3 | use entity::rooms::Model as Room;
4 |
5 | pub async fn create_room(
6 | user_id: uuid::Uuid,
7 | user_secure_id: uuid::Uuid,
8 | client: &TestClient,
9 | ) -> Room {
10 | let auth_headers = format!("{user_id}:{user_secure_id}");
11 | let res = client
12 | .post("/rooms")
13 | .header("AUTHORIZATION", auth_headers)
14 | .send()
15 | .await;
16 | assert_eq!(res.status(), StatusCode::CREATED);
17 |
18 | res.json().await
19 | }
20 |
--------------------------------------------------------------------------------
/server/src/tests/fixtures/create_user.rs:
--------------------------------------------------------------------------------
1 | use axum::http::StatusCode;
2 | use axum_test_helper::TestClient;
3 | use entity::users::Model as User;
4 | use serde_json::json;
5 |
6 | pub async fn create_user(username: &str, client: &TestClient) -> User {
7 | let user_schema = json!({
8 | "username": username,
9 | });
10 |
11 | let res = client.post("/users").json(&user_schema).send().await;
12 | assert_eq!(res.status(), StatusCode::CREATED);
13 |
14 | // By deserializing response to a model that has a secure_id field
15 | // we ensure it has this field
16 | let user: User = res.json().await;
17 | assert_eq!(user.username, username);
18 |
19 | user
20 | }
21 |
--------------------------------------------------------------------------------
/server/src/tests/fixtures/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod create_room;
2 | pub mod create_user;
3 | pub mod test_tools;
4 |
--------------------------------------------------------------------------------
/server/src/tests/fixtures/test_tools.rs:
--------------------------------------------------------------------------------
1 | use std::sync::Arc;
2 |
3 | use axum_test_helper::TestClient;
4 | use entity::{
5 | games::Entity as Game, histories::Entity as History, history_records::Entity as HistoryRecord,
6 | rooms::Entity as Room, users::Entity as User,
7 | };
8 | use migration::TableCreateStatement;
9 | use sea_orm::{ConnectionTrait, DbBackend, DbConn, Schema, Statement};
10 |
11 | use crate::{
12 | app::create_app,
13 | common::{config::Config, db::connection::get_db, routing::app_state::AppState},
14 | };
15 |
16 | const DBNAME: &'static str = "test";
17 |
18 | pub struct TestTools {
19 | pub db: TestDB,
20 | pub client: TestClient,
21 | }
22 |
23 | impl TestTools {
24 | pub async fn new() -> Self {
25 | // Creating config
26 | let config = Config::new().unwrap();
27 |
28 | // Creating test db
29 | let db = TestDB::new(&config.db_uri).await;
30 |
31 | // Running db schema setup
32 | setup_schema(&db.db).await;
33 |
34 | // Getting test client
35 | let client = create_test_client(db.db.clone(), config).await;
36 |
37 | Self { db, client }
38 | }
39 | }
40 |
41 | async fn create_test_client(db: DbConn, config: Config) -> TestClient {
42 | let app_state = AppState::new(db, config);
43 |
44 | let app = create_app(Arc::new(app_state));
45 | TestClient::new(app)
46 | }
47 |
48 | async fn setup_schema(db: &DbConn) {
49 | // Setup Schema helper
50 | let schema = Schema::new(DbBackend::Postgres);
51 |
52 | // Creating tables
53 | let stmt: TableCreateStatement = schema.create_table_from_entity(User);
54 | db.execute(db.get_database_backend().build(&stmt))
55 | .await
56 | .unwrap();
57 | let stmt: TableCreateStatement = schema.create_table_from_entity(Game);
58 | db.execute(db.get_database_backend().build(&stmt))
59 | .await
60 | .unwrap();
61 | let stmt: TableCreateStatement = schema.create_table_from_entity(Room);
62 | db.execute(db.get_database_backend().build(&stmt))
63 | .await
64 | .unwrap();
65 | let stmt: TableCreateStatement = schema.create_table_from_entity(History);
66 | db.execute(db.get_database_backend().build(&stmt))
67 | .await
68 | .unwrap();
69 | let stmt: TableCreateStatement = schema.create_table_from_entity(HistoryRecord);
70 | db.execute(db.get_database_backend().build(&stmt))
71 | .await
72 | .unwrap();
73 | }
74 |
75 | // Creates DB on object cretion and deletes it on object drop
76 | pub struct TestDB {
77 | pub db_uri: String,
78 | pub db: DbConn,
79 | }
80 |
81 | impl TestDB {
82 | async fn new(db_uri: &str) -> Self {
83 | let db = get_db(db_uri).await.unwrap();
84 |
85 | // Deleting and recreating test db
86 | for command in [
87 | format!("DROP DATABASE IF EXISTS {DBNAME};"),
88 | format!("CREATE DATABASE {DBNAME};"),
89 | ] {
90 | db.execute(Statement::from_string(DbBackend::Postgres, command))
91 | .await
92 | .unwrap();
93 | }
94 |
95 | let new_db_uri = format!("{db_uri}/{DBNAME}");
96 |
97 | // Getting new db_conn
98 | let db = get_db(&new_db_uri).await.unwrap();
99 |
100 | Self {
101 | db_uri: new_db_uri,
102 | db,
103 | }
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/server/src/tests/mod.rs:
--------------------------------------------------------------------------------
1 | mod fixtures;
2 | mod test_games_crud;
3 | mod test_rooms_crud;
4 | mod test_users_crud;
5 |
--------------------------------------------------------------------------------
/server/src/tests/test_games_crud.rs:
--------------------------------------------------------------------------------
1 | use axum::http::StatusCode;
2 | use entity::rooms::Model as Room;
3 | use migration::FieldType;
4 | use serde_json::json;
5 |
6 | use crate::{
7 | apps::{games::schemas::GameWithWSLink, histories::repositories::HistoriesRepository},
8 | tests::fixtures::{create_room::create_room, create_user::create_user, test_tools::TestTools},
9 | };
10 |
11 | #[tokio::test]
12 | async fn test_rooms_crud() {
13 | let test_tools = TestTools::new().await;
14 |
15 | // Creating 2 players and room
16 | let user_1 = create_user("motherfucker", &test_tools.client).await;
17 | let user_2 = create_user("dumba", &test_tools.client).await;
18 | let room = create_room(user_1.id, user_1.secure_id, &test_tools.client).await;
19 |
20 | let starting_game_json =
21 | json!({"room_id": room.id, "field_type": FieldType::Regular, "size": 9});
22 |
23 | // Trying to start game without 2 players
24 | {
25 | let auth_header = format!("{}:{}", user_1.id, user_1.secure_id);
26 | let res = test_tools
27 | .client
28 | .post("/games")
29 | .json(&starting_game_json)
30 | .header("AUTHORIZATION", auth_header)
31 | .send()
32 | .await;
33 | assert_eq!(res.status(), StatusCode::CONFLICT);
34 | }
35 |
36 | // Adding a second player
37 | {
38 | let url = format!("/rooms/{}/enter", room.id);
39 | let auth_header = format!("{}:{}", user_2.id, user_2.secure_id);
40 | let res = test_tools
41 | .client
42 | .patch(&url)
43 | .header("AUTHORIZATION", auth_header)
44 | .send()
45 | .await;
46 | assert_eq!(res.status(), StatusCode::ACCEPTED);
47 |
48 | let res_room: Room = res.json().await;
49 | assert_eq!(res_room.id, room.id);
50 | assert_eq!(res_room.player1_id, user_1.id);
51 | assert_eq!(res_room.player2_id, Some(user_2.id));
52 | }
53 |
54 | // Trying to start a game by a player 2
55 | {
56 | let auth_header = format!("{}:{}", user_2.id, user_2.secure_id);
57 | let res = test_tools
58 | .client
59 | .post("/games")
60 | .json(&starting_game_json)
61 | .header("AUTHORIZATION", auth_header)
62 | .send()
63 | .await;
64 | assert_eq!(res.status(), StatusCode::FORBIDDEN);
65 | }
66 |
67 | // Trying to start a game by a player 1
68 | {
69 | let auth_header = format!("{}:{}", user_1.id, user_1.secure_id);
70 | let res = test_tools
71 | .client
72 | .post("/games")
73 | .json(&starting_game_json)
74 | .header("AUTHORIZATION", auth_header)
75 | .send()
76 | .await;
77 | assert_eq!(res.status(), StatusCode::CREATED);
78 |
79 | // Checking if a game history was created too
80 | let game: GameWithWSLink = res.json().await;
81 | let history = HistoriesRepository::new(&test_tools.db.db)
82 | .get_by_game_id(game.game.id)
83 | .await;
84 | assert!(matches!(history, Result::Ok(_)))
85 | }
86 |
87 | // Trying to start a game again
88 | {
89 | let auth_header = format!("{}:{}", user_1.id, user_1.secure_id);
90 | let res = test_tools
91 | .client
92 | .post("/games")
93 | .json(&starting_game_json)
94 | .header("AUTHORIZATION", auth_header)
95 | .send()
96 | .await;
97 | assert_eq!(res.status(), StatusCode::CONFLICT);
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/server/src/tests/test_rooms_crud.rs:
--------------------------------------------------------------------------------
1 | use axum::http::StatusCode;
2 | use entity::rooms::Model as Room;
3 |
4 | use crate::tests::fixtures::{
5 | create_room::create_room, create_user::create_user, test_tools::TestTools,
6 | };
7 |
8 | #[tokio::test]
9 | async fn test_rooms_crud() {
10 | let test_tools = TestTools::new().await;
11 |
12 | // Creating 2 users and 2 rooms to each one
13 | let room_1 = {
14 | let user = create_user("motherfucker", &test_tools.client).await;
15 |
16 | let room = create_room(user.id, user.secure_id, &test_tools.client).await;
17 | assert_eq!(room.player1_id, user.id);
18 | assert_eq!(room.player2_id, None);
19 | room
20 | };
21 | {
22 | let user = create_user("lmao", &test_tools.client).await;
23 |
24 | let room = create_room(user.id, user.secure_id, &test_tools.client).await;
25 | assert_eq!(room.player1_id, user.id);
26 | assert_eq!(room.player2_id, None);
27 | };
28 |
29 | // Getting one of them
30 | {
31 | let url = format!("/rooms/{}", room_1.id);
32 | let res = test_tools.client.get(&url).send().await;
33 | assert_eq!(res.status(), StatusCode::OK);
34 |
35 | let room: Room = res.json().await;
36 | assert_eq!(room.id, room_1.id)
37 | }
38 | }
39 |
40 | #[tokio::test]
41 | async fn test_invite() {
42 | let test_tools = TestTools::new().await;
43 |
44 | // Creating 2 users and 1 room
45 | let user_1 = create_user("motherfucker", &test_tools.client).await;
46 | let user_2 = create_user("motherfucker", &test_tools.client).await;
47 |
48 | let room = create_room(user_1.id, user_1.secure_id, &test_tools.client).await;
49 |
50 | // Accepting invitation
51 | {
52 | let url = format!("/rooms/{}/enter", room.id);
53 | let auth_header = format!("{}:{}", user_2.id, user_2.secure_id);
54 | let res = test_tools
55 | .client
56 | .patch(&url)
57 | .header("AUTHORIZATION", auth_header)
58 | .send()
59 | .await;
60 | assert_eq!(res.status(), StatusCode::ACCEPTED);
61 |
62 | let res_room: Room = res.json().await;
63 | assert_eq!(res_room.id, room.id);
64 | assert_eq!(res_room.player1_id, user_1.id);
65 | assert_eq!(res_room.player2_id, Some(user_2.id));
66 | }
67 |
68 | // Trying to do it again and we must get an error
69 | {
70 | let url = format!("/rooms/{}/enter", room.id);
71 | let auth_header = format!("{}:{}", user_2.id, user_2.secure_id);
72 | let res = test_tools
73 | .client
74 | .patch(&url)
75 | .header("AUTHORIZATION", auth_header)
76 | .send()
77 | .await;
78 | assert_eq!(res.status(), StatusCode::CONFLICT);
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/server/src/tests/test_users_crud.rs:
--------------------------------------------------------------------------------
1 | use axum::http::StatusCode;
2 | use serde_json::json;
3 |
4 | use super::fixtures::{create_user::create_user, test_tools::TestTools};
5 | use crate::apps::users::schemas::OutUserSchema;
6 |
7 | #[tokio::test]
8 | async fn test_create_and_get_user() {
9 | let test_tools = TestTools::new().await;
10 |
11 | // Creating a user
12 | let user = create_user("test_user", &test_tools.client).await;
13 |
14 | // Getting him
15 | {
16 | let url = format!("/users/{}", &user.id);
17 | let res = test_tools.client.get(&url).send().await;
18 | assert_eq!(res.status(), StatusCode::OK);
19 |
20 | // By deserializing response to a schema that doesn't have a secure_id field
21 | // we ensure it doesn't have it
22 | let out_user: OutUserSchema = res.json().await;
23 | assert_eq!(out_user.id, user.id);
24 | assert_eq!(out_user.username, user.username);
25 | }
26 | }
27 |
28 | #[tokio::test]
29 | async fn test_get_and_list_users() {
30 | let test_tools = TestTools::new().await;
31 |
32 | // Creating users
33 | let user_1 = create_user("test_user_1", &test_tools.client).await;
34 | let user_2 = create_user("test_user_2", &test_tools.client).await;
35 |
36 | // Getting 1 user
37 | {
38 | let url = format!("/users/{}", &user_1.id);
39 | let res = test_tools.client.get(&url).send().await;
40 | assert_eq!(res.status(), StatusCode::OK);
41 |
42 | // By deserializing response to a schema that doesn't have a secure_id field
43 | // we ensure it doesn't have it
44 | let out_user: OutUserSchema = res.json().await;
45 | assert_eq!(out_user.id, user_1.id);
46 | assert_eq!(out_user.username, user_1.username);
47 | }
48 |
49 | // Getting both
50 | {
51 | let res = test_tools.client.get("/users").send().await;
52 | assert_eq!(res.status(), StatusCode::OK);
53 |
54 | // By deserializing response to a schema that doesn't have a secure_id field
55 | // we ensure it doesn't have it
56 | let out_users: [OutUserSchema; 2] = res.json().await;
57 | let real_users = [user_1, user_2].map(|u| u.into());
58 | assert_eq!(out_users, real_users);
59 | }
60 |
61 | // Trying to get a user that doesn't exist
62 | {
63 | let doesnt_exist_user_id = uuid::Uuid::new_v4();
64 | let url = format!("/users/{}", &doesnt_exist_user_id);
65 | let res = test_tools.client.get(&url).send().await;
66 | assert_eq!(res.status(), StatusCode::NOT_FOUND);
67 | }
68 | }
69 |
70 | #[tokio::test]
71 | async fn test_delete_user() {
72 | let test_tools = TestTools::new().await;
73 |
74 | // Creating users
75 | let user_1 = create_user("test_user_1", &test_tools.client).await;
76 | let user_2 = create_user("test_user_2", &test_tools.client).await;
77 |
78 | // Deleting 1 user
79 | {
80 | // Deleting the user
81 | let delete_schema = json!({"secure_id": user_1.secure_id});
82 | let url = format!("/users/{}", &user_1.id);
83 | let res = test_tools
84 | .client
85 | .delete(&url)
86 | .json(&delete_schema)
87 | .send()
88 | .await;
89 | assert_eq!(res.status(), StatusCode::NO_CONTENT);
90 |
91 | // Ensuring we don't have the user anymore
92 | let res = test_tools.client.get("/users").send().await;
93 | assert_eq!(res.status(), StatusCode::OK);
94 |
95 | let out_users: [OutUserSchema; 1] = res.json().await;
96 | let real_users = [user_2].map(|u| u.into());
97 | assert_eq!(out_users, real_users);
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/wasm/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | version = 3
4 |
5 | [[package]]
6 | name = "bumpalo"
7 | version = "3.10.0"
8 | source = "registry+https://github.com/rust-lang/crates.io-index"
9 | checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3"
10 |
11 | [[package]]
12 | name = "cfg-if"
13 | version = "1.0.0"
14 | source = "registry+https://github.com/rust-lang/crates.io-index"
15 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
16 |
17 | [[package]]
18 | name = "either"
19 | version = "1.7.0"
20 | source = "registry+https://github.com/rust-lang/crates.io-index"
21 | checksum = "3f107b87b6afc2a64fd13cac55fe06d6c8859f12d4b14cbcdd2c67d0976781be"
22 |
23 | [[package]]
24 | name = "itertools"
25 | version = "0.10.3"
26 | source = "registry+https://github.com/rust-lang/crates.io-index"
27 | checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3"
28 | dependencies = [
29 | "either",
30 | ]
31 |
32 | [[package]]
33 | name = "itoa"
34 | version = "1.0.4"
35 | source = "registry+https://github.com/rust-lang/crates.io-index"
36 | checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc"
37 |
38 | [[package]]
39 | name = "js-sys"
40 | version = "0.3.59"
41 | source = "registry+https://github.com/rust-lang/crates.io-index"
42 | checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2"
43 | dependencies = [
44 | "wasm-bindgen",
45 | ]
46 |
47 | [[package]]
48 | name = "log"
49 | version = "0.4.17"
50 | source = "registry+https://github.com/rust-lang/crates.io-index"
51 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
52 | dependencies = [
53 | "cfg-if",
54 | ]
55 |
56 | [[package]]
57 | name = "once_cell"
58 | version = "1.13.0"
59 | source = "registry+https://github.com/rust-lang/crates.io-index"
60 | checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1"
61 |
62 | [[package]]
63 | name = "proc-macro2"
64 | version = "1.0.47"
65 | source = "registry+https://github.com/rust-lang/crates.io-index"
66 | checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725"
67 | dependencies = [
68 | "unicode-ident",
69 | ]
70 |
71 | [[package]]
72 | name = "quote"
73 | version = "1.0.20"
74 | source = "registry+https://github.com/rust-lang/crates.io-index"
75 | checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804"
76 | dependencies = [
77 | "proc-macro2",
78 | ]
79 |
80 | [[package]]
81 | name = "ryu"
82 | version = "1.0.11"
83 | source = "registry+https://github.com/rust-lang/crates.io-index"
84 | checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
85 |
86 | [[package]]
87 | name = "serde"
88 | version = "1.0.150"
89 | source = "registry+https://github.com/rust-lang/crates.io-index"
90 | checksum = "e326c9ec8042f1b5da33252c8a37e9ffbd2c9bef0155215b6e6c80c790e05f91"
91 | dependencies = [
92 | "serde_derive",
93 | ]
94 |
95 | [[package]]
96 | name = "serde_derive"
97 | version = "1.0.150"
98 | source = "registry+https://github.com/rust-lang/crates.io-index"
99 | checksum = "42a3df25b0713732468deadad63ab9da1f1fd75a48a15024b50363f128db627e"
100 | dependencies = [
101 | "proc-macro2",
102 | "quote",
103 | "syn",
104 | ]
105 |
106 | [[package]]
107 | name = "serde_json"
108 | version = "1.0.89"
109 | source = "registry+https://github.com/rust-lang/crates.io-index"
110 | checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db"
111 | dependencies = [
112 | "itoa",
113 | "ryu",
114 | "serde",
115 | ]
116 |
117 | [[package]]
118 | name = "spherical_go_game_lib"
119 | version = "0.1.0"
120 | dependencies = [
121 | "itertools",
122 | "serde",
123 | "serde_json",
124 | "thiserror",
125 | ]
126 |
127 | [[package]]
128 | name = "syn"
129 | version = "1.0.105"
130 | source = "registry+https://github.com/rust-lang/crates.io-index"
131 | checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908"
132 | dependencies = [
133 | "proc-macro2",
134 | "quote",
135 | "unicode-ident",
136 | ]
137 |
138 | [[package]]
139 | name = "thiserror"
140 | version = "1.0.31"
141 | source = "registry+https://github.com/rust-lang/crates.io-index"
142 | checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a"
143 | dependencies = [
144 | "thiserror-impl",
145 | ]
146 |
147 | [[package]]
148 | name = "thiserror-impl"
149 | version = "1.0.31"
150 | source = "registry+https://github.com/rust-lang/crates.io-index"
151 | checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a"
152 | dependencies = [
153 | "proc-macro2",
154 | "quote",
155 | "syn",
156 | ]
157 |
158 | [[package]]
159 | name = "unicode-ident"
160 | version = "1.0.2"
161 | source = "registry+https://github.com/rust-lang/crates.io-index"
162 | checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7"
163 |
164 | [[package]]
165 | name = "wasm-bindgen"
166 | version = "0.2.82"
167 | source = "registry+https://github.com/rust-lang/crates.io-index"
168 | checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d"
169 | dependencies = [
170 | "cfg-if",
171 | "wasm-bindgen-macro",
172 | ]
173 |
174 | [[package]]
175 | name = "wasm-bindgen-backend"
176 | version = "0.2.82"
177 | source = "registry+https://github.com/rust-lang/crates.io-index"
178 | checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f"
179 | dependencies = [
180 | "bumpalo",
181 | "log",
182 | "once_cell",
183 | "proc-macro2",
184 | "quote",
185 | "syn",
186 | "wasm-bindgen-shared",
187 | ]
188 |
189 | [[package]]
190 | name = "wasm-bindgen-macro"
191 | version = "0.2.82"
192 | source = "registry+https://github.com/rust-lang/crates.io-index"
193 | checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602"
194 | dependencies = [
195 | "quote",
196 | "wasm-bindgen-macro-support",
197 | ]
198 |
199 | [[package]]
200 | name = "wasm-bindgen-macro-support"
201 | version = "0.2.82"
202 | source = "registry+https://github.com/rust-lang/crates.io-index"
203 | checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da"
204 | dependencies = [
205 | "proc-macro2",
206 | "quote",
207 | "syn",
208 | "wasm-bindgen-backend",
209 | "wasm-bindgen-shared",
210 | ]
211 |
212 | [[package]]
213 | name = "wasm-bindgen-shared"
214 | version = "0.2.82"
215 | source = "registry+https://github.com/rust-lang/crates.io-index"
216 | checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a"
217 |
218 | [[package]]
219 | name = "wasm_gamelib"
220 | version = "0.1.0"
221 | dependencies = [
222 | "js-sys",
223 | "spherical_go_game_lib",
224 | "wasm-bindgen",
225 | ]
226 |
--------------------------------------------------------------------------------
/wasm/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "wasm_gamelib"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | [lib]
7 | crate-type = ["cdylib"]
8 |
9 | [dependencies]
10 | wasm-bindgen = "0.2.82"
11 | js-sys = "0.3.59"
12 |
13 | spherical_go_game_lib = {path = "../gamelib"}
14 |
--------------------------------------------------------------------------------
/wasm/rust-toolchain.toml:
--------------------------------------------------------------------------------
1 | [toolchain]
2 | channel = "nightly"
3 | components = []
4 | targets = []
5 | profile = "default"
6 |
--------------------------------------------------------------------------------
/wasm/src/lib.rs:
--------------------------------------------------------------------------------
1 | use wasm_bindgen::prelude::*;
2 |
3 | use spherical_go_game_lib::{
4 | FieldType as InnerFieldType, Game as InnerGame, PlayerColor, PointID, SizeType,
5 | };
6 |
7 | type GameResult = Result;
8 |
9 | #[wasm_bindgen]
10 | #[derive(Debug, Clone, Copy)]
11 | pub enum Player {
12 | Black = "Black",
13 | White = "White",
14 | }
15 |
16 | impl From for Player {
17 | fn from(p: PlayerColor) -> Self {
18 | match p {
19 | PlayerColor::Black => Self::Black,
20 | PlayerColor::White => Self::White,
21 | }
22 | }
23 | }
24 |
25 | #[wasm_bindgen]
26 | #[derive(Debug, Clone, Copy)]
27 | pub enum FieldType {
28 | Regular = "Regular",
29 | CubicSphere = "CubicSphere ",
30 | GridSphere = "GridSphere",
31 | }
32 |
33 | impl From for InnerFieldType {
34 | fn from(f: FieldType) -> Self {
35 | match f {
36 | FieldType::Regular => Self::Regular,
37 | FieldType::GridSphere => Self::GridSphere,
38 | FieldType::CubicSphere => Self::CubicSphere,
39 | _ => panic!("Wrong variant for a field"),
40 | }
41 | }
42 | }
43 |
44 | impl From for FieldType {
45 | fn from(f: InnerFieldType) -> Self {
46 | match f {
47 | InnerFieldType::Regular => Self::Regular,
48 | InnerFieldType::GridSphere => Self::GridSphere,
49 | InnerFieldType::CubicSphere => Self::CubicSphere,
50 | }
51 | }
52 | }
53 |
54 | #[wasm_bindgen]
55 | pub struct Game {
56 | inner: InnerGame,
57 | }
58 |
59 | #[wasm_bindgen]
60 | impl Game {
61 | /// Create the game
62 | #[wasm_bindgen(constructor)]
63 | pub fn new(field_type: FieldType, size: SizeType, use_history: bool) -> GameResult {
64 | let inner = InnerGame::new(field_type.into(), &size, use_history)?;
65 | Ok(Self { inner })
66 | }
67 |
68 | /// Make a move
69 | pub fn make_move(&mut self, point_id: PointID) -> GameResult> {
70 | let deadlist = self.inner.make_move(&point_id)?;
71 | Ok(deadlist.into_iter().collect())
72 | }
73 |
74 | // Undo previous move
75 | pub fn undo_move(&mut self) -> GameResult<()> {
76 | self.inner.undo_move()?;
77 | Ok(())
78 | }
79 |
80 | /// Start game
81 | pub fn start(&mut self) -> GameResult<()> {
82 | self.inner.start()?;
83 | Ok(())
84 | }
85 |
86 | /// End game
87 | pub fn end(&mut self) -> GameResult<()> {
88 | self.inner.end()?;
89 | Ok(())
90 | }
91 |
92 | pub fn is_not_started(&self) -> bool {
93 | self.inner.is_not_started()
94 | }
95 |
96 | pub fn is_started(&self) -> bool {
97 | self.inner.is_started()
98 | }
99 |
100 | pub fn is_ended(&self) -> bool {
101 | self.inner.is_ended()
102 | }
103 |
104 | pub fn get_black_stones(&self) -> Vec {
105 | self.inner.get_black_stones()
106 | }
107 |
108 | pub fn get_white_stones(&self) -> Vec {
109 | self.inner.get_white_stones()
110 | }
111 |
112 | pub fn get_black_score(&self) -> Option {
113 | self.inner.get_black_score()
114 | }
115 |
116 | pub fn get_white_score(&self) -> Option {
117 | self.inner.get_white_score()
118 | }
119 |
120 | pub fn player_turn(&self) -> Option {
121 | self.inner.player_turn().map(|p| Player::from(p))
122 | }
123 |
124 | pub fn field_type(&self) -> FieldType {
125 | self.inner.field_type().into()
126 | }
127 |
128 | pub fn get_move_number(&self) -> Option {
129 | self.inner.get_move_number()
130 | }
131 | }
132 |
--------------------------------------------------------------------------------