Compare commits

..

177 commits

Author SHA1 Message Date
Sorrel
08745443ca
Create LICENSE 2021-04-14 20:31:26 -04:00
Sorrel
177d3a4c42
Merge pull request #14 from sorrelbri/dependabot/npm_and_yarn/lodash-4.17.19
Bump lodash from 4.17.15 to 4.17.19
2020-07-20 13:13:50 -04:00
dependabot[bot]
1d804694b7
Bump lodash from 4.17.15 to 4.17.19
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-20 17:07:39 +00:00
Sorrel
a40253a4fd
Merge pull request #13 from sorrelbri/dependabot/npm_and_yarn/packages/server/lodash-4.17.19
Bump lodash from 4.17.15 to 4.17.19 in /packages/server
2020-07-20 13:05:21 -04:00
Sorrel
1c470ffec0
Merge pull request #15 from sorrelbri/patch/game-service
patch Game service
2020-07-20 12:35:15 -04:00
Sorrel
27cf281670
patch Game service
Bug where player passes were submitted before `game` object was assigned within `Game.makeMove` patched
2020-07-20 12:20:47 -04:00
dependabot[bot]
0fad98a1d0
Bump lodash from 4.17.15 to 4.17.19 in /packages/server
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-19 12:20:31 +00:00
Sorrel
14a94be59e
Merge pull request #12 from sorrelbri/game_record_tree
Game add checkMove hook
2020-07-07 17:30:20 -07:00
sorrelbri
0f3e7942ef refactor Game.makeMove to check legality only if checkMove has not yet been called 2020-07-07 17:08:02 -07:00
sorrelbri
cfec49845b add checkMove hook to Game 2020-07-07 16:52:45 -07:00
Sorrel
2a84644e12
Merge pull request #11 from sorrelbri/game_record_tree
Game Service plays through and ends finished games
2020-07-01 15:06:06 -07:00
Sorrel
e1a6cd9a44
patch rewrite
original written with optional chaining, changed to `&&` operations
2020-07-01 15:01:12 -07:00
Sorrel
a3420c1152
patch to fix gameData is undefined 2020-07-01 14:56:07 -07:00
sorrelbri
c4192f2e33 add support for ending game from game record to game service 2020-07-01 14:32:03 -07:00
sorrelbri
cbf9d461ba adjust endGame query to modify record with win information 2020-07-01 11:42:16 -07:00
sorrelbri
5eca128110 add placement boolean to Move migration, add placement and prior_move selects to move query 2020-07-01 10:54:32 -07:00
Sorrel
dd0289439c
Merge pull request #9 from sorrelbri/guest_account
Guest account
2020-06-27 13:21:27 -07:00
sorrelbri
9cae5ff185 style Continue as Guest button, patch account display on NavBar 2020-06-27 13:05:38 -07:00
sorrelbri
0ec92dd5e9 add random password to guest 2020-06-26 23:33:56 -07:00
sorrelbri
ff3d6c2c24 add service to generate guest account on server 2020-06-26 23:22:17 -07:00
sorrelbri
1e6050a486 add FE service to post to auth/guest 2020-06-26 17:55:52 -07:00
sorrelbri
2f1174c21b stub guest creation on server 2020-06-26 17:35:13 -07:00
Sorrel
ccbeddfa40
Merge pull request #8 from sorrelbri/patch_game_end_bug
patch game end bug
2020-06-26 16:33:33 -07:00
sorrelbri
f8779ae887 patch game end bug, update_board socket message always returns data in consistent format 2020-06-26 16:24:55 -07:00
Sorrel
a9be49c38e
Merge pull request #7 from sorrelbri/game_record
add game record view to game page menu
2020-06-26 13:31:27 -07:00
sorrelbri
5e33e6439e add print game record option to menu, prints only canvas elements 2020-06-26 13:15:07 -07:00
sorrelbri
ea951bee66 add game record section to readme 2020-06-26 11:50:26 -07:00
Sorrel
d7c53e7c7f
Merge pull request #6 from sorrelbri/game_record
add menu component to game page that displays game record
2020-06-25 22:52:55 -07:00
sorrelbri
a290ec532d add game record display overflow canvas for moves made at previously played points 2020-06-25 19:44:51 -07:00
sorrelbri
9ed4fc0a39 draw game record on canvas when menu is opened 2020-06-24 16:53:31 -07:00
sorrelbri
62fe6dfcb4 add click handler and basic style to menu on Game 2020-06-21 18:47:00 -07:00
sorrelbri
3955f940e2 stub Menu, add showMenu value to Game 2020-06-21 18:20:03 -07:00
sorrelbri
f14c0ce087 style Kifu to hover 'show menu' text 2020-06-21 17:34:03 -07:00
Sorrel
5eedd6b2e9
Merge pull request #5 from sorrelbri/game_record
Add Kifu to Game UI
2020-06-21 16:53:26 -07:00
sorrelbri
d803f55f5b remove props from Kifu 2020-06-21 16:33:53 -07:00
sorrelbri
35c51c567f style kifu 2020-06-21 16:32:24 -07:00
sorrelbri
8414d13949 pass Kifu to appropriate PlayerArea 2020-06-21 16:02:45 -07:00
sorrelbri
aed378c021 stub Kifu component 2020-06-21 15:00:11 -07:00
sorrelbri
4b507cce96 patch game meta to include score in tests 2020-06-20 17:23:13 -07:00
sorrelbri
a8119bb194 remove dot data from game end state 2020-06-20 17:05:39 -07:00
sorrelbri
692a8b1400 add endGame function to FE 2020-06-20 16:56:03 -07:00
sorrelbri
52762475f7 add socket send game_end message from FE 2020-06-20 16:25:09 -07:00
sorrelbri
0db13d2913 add socket message for toggling territory 2020-06-17 11:59:21 -07:00
sorrelbri
b1d43b5b02 display end game territory dot 2020-06-16 22:29:52 -07:00
sorrelbri
e40fa63274 update readme with game state details 2020-06-13 16:02:55 -07:00
sorrelbri
a3aecfeadc revert https patch 2020-06-08 14:13:32 -07:00
sorrelbri
0b2bfad12f add server side https redirect 2020-06-08 14:00:45 -07:00
sorrelbri
72b93f8ff1 patch secure protocol redirect 2020-06-08 13:13:36 -07:00
sorrelbri
18027ec1ef patch gameServices tests 2020-06-07 17:27:16 -07:00
sorrelbri
be8cb70431 connect FE submit pass to BE game service 2020-06-07 17:06:26 -07:00
sorrelbri
249789944b patch game_resign in games reducer 2020-06-07 15:10:18 -07:00
sorrelbri
17b2e2c31f connect FE submit resign to BE game service 2020-06-07 13:47:09 -07:00
sorrelbri
77ced54c6f patch useContext dependencies in Game component 2020-06-06 17:08:40 -07:00
sorrelbri
97242a2235 add render for hoshi points 2020-06-06 16:54:48 -07:00
sorrelbri
f010d5d9ee add highlight for current turn to PlayerArea bowl 2020-06-06 16:22:14 -07:00
sorrelbri
6359089e91 display player data in PlayerArea component 2020-06-06 16:04:32 -07:00
sorrelbri
49873aa706 patch endGame scoring bug 2020-06-05 22:54:19 -07:00
sorrelbri
12f9847a0e add toggleTerritory logic to Game 2020-06-04 23:47:25 -07:00
sorrelbri
09b346064f Merge branch 'master' into game_logic 2020-06-04 22:42:56 -07:00
sorrelbri
84085dc8b2 add screenshots and stub new sections of readme 2020-06-02 23:49:57 -07:00
sorrelbri
934b1b7b2d add endGame to count territory 2020-06-02 00:23:21 -07:00
sorrelbri
7c2bf6416b stub toggleTerritory and endGame methods on Game 2020-06-01 22:53:02 -07:00
sorrelbri
d1f3459516 add determineLife to Game.endGame, refactor joinEmptyPoints to avoid removing empty points from adjoining groups' liberties 2020-06-01 17:12:28 -07:00
sorrelbri
1b3dbd870a add determineTerritory to Game.endGame for empty points 2020-06-01 15:58:43 -07:00
sorrelbri
19a5282b73 add joinEmptyPoints to endGame method on Game 2020-05-31 23:27:38 -07:00
sorrelbri
55a282b5c8 update seed for honinbo game 2020-05-30 22:37:51 -07:00
sorrelbri
796bb7aad9 patch neighboring liberty bug 2020-05-30 21:44:52 -07:00
sorrelbri
8fa6e207ed add styling for ko and legal move on hover 2020-05-30 17:07:27 -07:00
sorrelbri
d54d43c42c add cache confirmation check to Game Service 2020-05-30 16:43:41 -07:00
sorrelbri
181554124b decouple gameService from db 2020-05-30 16:01:13 -07:00
Sorrel Bri
3eabf4191a stub endGame function, add pass counter on Game 2020-05-15 22:00:08 -07:00
Sorrel Bri
9b419a416b stub submitPass on Game 2020-05-14 23:21:19 -07:00
Sorrel Bri
e41d889177 patch failing react tests 2020-05-14 21:04:29 -07:00
Sorrel Bri
2922be86e1 Merge branch 'game_logic' 2020-05-14 21:01:54 -07:00
Sorrel Bri
7eb592a397 patch lerna bootstrap 2020-05-14 21:01:43 -07:00
Sorrel Bri
572a1dcf19 patch App and ActionError components to cleanup effect 2020-05-14 21:01:04 -07:00
Sorrel Bri
ef0e53756e add submitResign to Game 2020-05-14 20:59:49 -07:00
Sorrel Bri
9f18c01839 patch missing node-sass version 2020-05-14 19:30:58 -07:00
Sorrel Bri
8cdefa98c1 add seed example game 2020-05-14 19:26:41 -07:00
Sorrel Bri
475db4e812 patch trace capture bug in Game 2020-05-12 16:47:17 -07:00
Sorrel Bri
73ba2bb237 patch trace capture bug in Game 2020-05-12 16:34:11 -07:00
Sorrel Bri
a324ece0af add move persistence to gameService 2020-05-11 18:18:42 -07:00
Sorrel Bri
2e613daa1b patch seeds without hardcoded ids to ensure auto-increment 2020-05-10 17:08:13 -07:00
Sorrel Bri
8e25106be6 update knex and pg 2020-05-10 13:29:14 -07:00
Sorrel Bri
c43df2cd5f update node-sass 2020-05-10 12:48:35 -07:00
Sorrel Bri
72d31cca5b update bcrypt version 2020-05-10 12:04:58 -07:00
Sorrel Bri
54fd676656 patch implicit meta prop passed to Point object 2020-05-04 23:47:46 -07:00
Sorrel Bri
155cca9110 patch useEffect dependencies on Game page 2020-05-04 23:44:38 -07:00
Sorrel Bri
7aed5b7bf9 connect new Game module to service and update frontend connections 2020-05-04 23:22:50 -07:00
Sorrel Bri
a02576532d refactor server side socket for connection with new Game service 2020-05-02 23:22:57 -07:00
Sorrel Bri
b8eb3770d9 deprecate Game.v1 2020-05-02 23:00:20 -07:00
Sorrel Bri
a99426e03f Game.returnToMove(x) where x >= 0 returns game state at move x 2020-05-02 22:04:22 -07:00
Sorrel Bri
d51e3f72f4 init Game.returnToMove(x) where x < 0 rewinds x number of moves 2020-05-02 21:25:54 -07:00
Sorrel Bri
bdeb9c9d86 init Game.returnToMove 2020-05-02 21:11:54 -07:00
Sorrel Bri
88e51fdbf9 add clearKo to Game called after makeMove is verified as legal 2020-05-02 20:30:28 -07:00
Sorrel Bri
2237e344c1 patch ko bug preventing snapback 2020-05-02 20:18:21 -07:00
Sorrel Bri
5ad997d276 add ko check to makeCaptures 2020-05-02 01:35:41 -07:00
Sorrel Bri
9646656f7a patch checkLegal logic for capturing Set to support snapback 2020-05-01 22:58:59 -07:00
Sorrel Bri
1142c1d448 refactor Point.capturing to Set to handle adjascent stones in same group 2020-04-30 23:45:19 -07:00
Sorrel Bri
e9d94d1fad green all capture tests 2020-04-30 23:12:11 -07:00
Sorrel Bri
1a1e5121d7 refactor Game.v2 to remove side effects 2020-04-30 23:01:30 -07:00
Sorrel Bri
a5fbeea929 add makeCaptures to Point 2020-04-29 22:24:27 -07:00
Sorrel Bri
13a882d212 add checkCaptures to Point, called during makeMove 2020-04-29 21:02:01 -07:00
Sorrel Bri
96af52823d add logic to prevent move at group's final liberty 2020-04-26 17:45:35 -07:00
Sorrel Bri
9164ee5987 add support for group joining moves at points with no liberties 2020-04-26 00:13:25 -07:00
Sorrel Bri
4a90b933a7 add logic for legal check that prevents moves at points with no liberties 2020-04-24 00:16:31 -07:00
Sorrel Bri
089783c82d add setLiberties to Point and refactor groups object to include liberties for each group 2020-04-23 00:24:12 -07:00
Sorrel Bri
e553601af7 add join group logic to Point, triggered by Game.makeMove 2020-04-22 23:17:46 -07:00
Sorrel Bri
b1f29f3d2d refactor getNeighbors in initBoard for array index lookup instead of boardState.find(...) 2020-04-20 17:39:52 -07:00
Sorrel Bri
bdfb6ebe85 add getNeighbors to initBoard() allowing Points to directly reference neighbors 2020-04-20 17:34:49 -07:00
Sorrel Bri
a54d4bf7e4 stub makeMove in Game v2 API 2020-04-19 22:58:26 -07:00
Sorrel Bri
37db281d08 add getMeta to Game API 2020-04-19 22:26:04 -07:00
Sorrel Bri
41e6e662e9 initialize board for all board sizes and handicap amounts 2020-04-19 15:02:09 -07:00
Sorrel Bri
8fb1b80cb6 add initGame to v2 Game logic service 2020-04-18 20:04:00 -07:00
Sorrel Bri
e53a8f4f2a Merge branch 'game_logic' 2020-04-17 17:17:18 -07:00
Sorrel Bri
7408e409aa patch userValidator middleware to remove deprecated sanitize function 2020-04-17 17:14:31 -07:00
Sorrel Bri
c533c7837c patch failing frontend tests 2020-04-17 00:01:04 -07:00
Sorrel Bri
aa2c9084e5 readd react and react-dom to ci pipeline 2020-04-16 23:39:29 -07:00
Sorrel Bri
dde97e2a1f patch lerna bootstrap command 2020-04-16 23:19:39 -07:00
Sorrel Bri
2c39fe1156 patch Game logic neighboring stone bug 2020-04-16 20:28:40 -07:00
Sorrel Bri
a1f7c45536 update readme with setup 2020-04-15 23:16:00 -07:00
Sorrel Bri
f2c0ce62fe patch link underline styles 2020-04-13 18:58:55 -07:00
Sorrel Bri
176c087599 style RoomDetail component 2020-04-12 22:54:20 -07:00
Sorrel Bri
0dbf0ed844 patch to Game button style 2020-04-12 14:50:03 -07:00
Sorrel Bri
a35cad24df patch link style 2020-04-12 14:22:10 -07:00
Sorrel Bri
c03ee1020d stub RoomDetail component 2020-04-12 00:19:28 -07:00
Sorrel Bri
c54dcb41a8 stub RoomDetail component 2020-04-12 00:19:21 -07:00
sorrelbri
a9243840a6 refactor Room button styles to remove cruft and clean up 2020-04-11 21:43:48 -07:00
sorrelbri
a9025aca8f style small Room button 2020-04-11 21:39:09 -07:00
sorrelbri
0368325199 refactor to remove crufty renderOpenGame function from Game Button 2020-04-10 20:25:11 -07:00
sorrelbri
bf836d50d7 refactor to remove crufty renderOpenGame function from Game Button 2020-04-10 20:19:58 -07:00
sorrelbri
16868aea1a refactor to remove crufty renderOpenGame function from Game Button 2020-04-10 20:05:42 -07:00
sorrelbri
8a60d82832 patch styling of game room drop shadows, refactor GameButton for open games 2020-04-10 20:03:27 -07:00
sorrelbri
a69b48d238 clean unused package.json scripts 2020-04-10 19:10:34 -07:00
sorrelbri
f8451ce62d clean unused package.json scripts 2020-04-10 19:09:43 -07:00
sorrelbri
d628a8dd48 add react and react-dom to play-node-go package.json 2020-03-30 11:37:48 -07:00
sorrelbri
3768297f7f add react and react-dom to play-node-go package.json 2020-03-30 11:33:31 -07:00
sorrelbri
30f65b85b3 add react and react-dom to play-node-go package.json 2020-03-30 11:33:07 -07:00
sorrelbri
f1d480c203 push build in frontend deploy 2020-03-30 09:26:24 -07:00
sorrelbri
d5fe96e0e9 fix vulnerabilities in play-node-go dependencies 2020-03-30 00:24:00 -07:00
sorrelbri
69c59683e4 fix deploy bug in circleci 2020-03-29 23:32:12 -07:00
sorrelbri
46f82e658c add subtree to deploy commands in circleci 2020-03-29 23:30:14 -07:00
sorrelbri
6a17b6b872 collapse steps in circleci 2020-03-29 23:26:09 -07:00
sorrelbri
eb7adf37e1 add machine-enabled to deploy step 2020-03-29 23:23:51 -07:00
sorrelbri
c1a9fe7204 add circleci env variables to deploy step 2020-03-29 23:22:43 -07:00
sorrelbri
4d71c8fdaf add audit-level flag to ci to bypass low-level dev-dependency issues 2020-03-29 20:56:16 -07:00
sorrelbri
7dffc9dbc7 fix minimist version 2020-03-29 20:45:20 -07:00
sorrelbri
d6cfda9d8b add deployment to ci pipeline 2020-03-29 20:27:38 -07:00
sorrelbri
3789401918 raise react dependencies to root package.json 2020-02-10 22:23:44 -08:00
sorrelbri
45cbe56564 raise react dependencies to root package.json 2020-02-10 22:21:31 -08:00
sorrelbri
5bd9ca1de3 undo previous commit - readd npm i react & react-dom to circleci/config 2020-02-10 22:17:11 -08:00
sorrelbri
e42a300e36 remove npm i react & react-dom 2020-02-10 22:12:52 -08:00
sorrelbri
316396f218 remove psql references from circleci config and test suite 2020-02-10 22:09:07 -08:00
sorrelbri
8f18d0685c remove psql references from circleci config and test suite 2020-02-10 22:07:35 -08:00
sorrelbri
7b63a2f76d remove psql references from circleci config and test suite 2020-02-10 22:05:33 -08:00
sorrelbri
7481178abd config psql in circleci config 2020-02-08 17:26:10 -08:00
sorrelbri
fbea9767e3 config psql in circleci config 2020-02-08 17:24:16 -08:00
sorrelbri
3c34b6846a config psql in circleci config 2020-02-08 17:21:13 -08:00
sorrelbri
a9dbf359fc config psql in circleci config 2020-02-08 16:10:53 -08:00
sorrelbri
d29e488354 config psql in circleci config 2020-02-07 20:44:27 -08:00
sorrelbri
22907dc7c5 config psql in circleci config 2020-02-07 20:43:17 -08:00
sorrelbri
5589b1463e config psql in circleci config 2020-02-07 20:42:04 -08:00
sorrelbri
3d20606999 config psql in circleci config 2020-02-07 20:40:32 -08:00
sorrelbri
d79151c707 config psql in circleci config 2020-02-07 20:38:01 -08:00
sorrelbri
68f8f4bf50 config psql in circleci config 2020-02-07 19:21:19 -08:00
sorrelbri
b044bb57d2 add postgres image to circleci config 2020-02-07 18:35:43 -08:00
sorrelbri
8b1ef3dbff add react and react-dom install to circleci config 2020-02-07 18:22:32 -08:00
sorrelbri
6ce03c3449 patch reducer compile warnings 2020-02-07 18:15:22 -08:00
sorrelbri
3e2a956634 patch reducer compile warnings 2020-02-07 18:03:28 -08:00
sorrelbri
401076cd50 patch reducer compile warnings 2020-02-07 17:59:28 -08:00
sorrelbri
24a8a4e991 patch reducer compile warnings 2020-02-07 17:55:25 -08:00
sorrelbri
3f15e48eef remove flow from app 2020-02-07 17:51:09 -08:00
sorrelbri
bfdad6e3aa add build step to circleci config 2020-02-07 17:41:28 -08:00
sorrelbri
c3842a0589 move flow to dev dependencies 2020-02-07 16:23:58 -08:00
sorrelbri
bee61a1f31 add @babel/preset-flow config options all equals true 2020-02-07 16:16:47 -08:00
sorrelbri
2c5a0534d7 patch React compilation warnings and useEffect dependencies 2020-02-07 16:11:01 -08:00
sorrelbri
4c7fa1512d patch React compilation warnings and useEffect dependencies 2020-02-07 16:01:54 -08:00
93 changed files with 12137 additions and 6037 deletions

View file

@ -20,15 +20,13 @@ jobs:
# specify the version you desire here
- image: circleci/node:12.6
# Specify service dependencies here if necessary
# CircleCI maintains a library of pre-built images
# documented at https://circleci.com/docs/2.0/circleci-images/
# - image: circleci/mongo:3.4.4
working_directory: ~/node-go
steps:
- checkout
# - run: sudo apt-get install postgresql-client-11.4
# - run: pg_ctl -D /var/lib/postgresql/data -l logfile start
# - run: psql CREATE DATABASE circle_test
# Download and cache dependencies
- restore_cache:
@ -45,6 +43,13 @@ jobs:
- node_modules
key:
v1-dependencies-{{ checksum "package-lock.json" }}
# run tests!
- run: npm test
- run: npm test
- run: npm install react
- run: npm install react-dom
# ! temporary fix for deprecated package: minimist
- run: npm audit --audit-level=moderate
# DEPLOY
- run: git subtree push --prefix packages/server https://heroku:$HEROKU_API_KEY@git.heroku.com/$HK_API.git master
- run: git subtree push --prefix packages/play-node-go https://heroku:$HEROKU_API_KEY@git.heroku.com/$HK_PLAY.git master

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Sorrel
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

160
README.md
View file

@ -1,18 +1,170 @@
# Node Go
A browser application to play Go in real time.
## Development Demo
[The project in it's current state](https://play-node-go.herokuapp.com/)
[The project in it's current state](https://play-node-go.herokuapp.com/)
[Client only prototype](https://sorrelbri.github.io/browser-go-proto/)
![Screenshot of an in-progress game of Go.](public/game-in-progress.png)
[About Go](#the-game-of-go)
[Technical Challenges](#technical-challenges)
[Setup For Development](#setup)
[Known Bugs](#known-bugs)
[Roadmap](#roadmap)
[Features](#features)
[Tech](#built-with)
---
## The Game of Go
Go is a 2 player abstract strategy game of perfect information.
Players take turns placing playing pieces called stones on the intersections of a gridded board. This board is usually a square 19 points across. Stones remain on the points at which they are placed unless they are captured by the opposing player. Capture occurs when a stone or group of stones no longer has any adjascent empty points.
Play ends when both players agree that they have exhausted all advantageous moves. Scoring is determined by counting and comparing the area controlled by either player.
For a more detailed explanation of the rules, please see [my previous illustrated explanation of the game of go](https://github.com/sorrelbri/browser-go-proto#the-game-of-go) or the [American Go Association's Concise Rules of Go.](https://www.usgo.org/aga-concise-rules-go)
---
## Technical Challenges
### Modeling Game State
A go board typically consists of 361 points which can exist in a number of states. Points can influence the state of points that are orthogonal neighbors. This relationship can be thought of as an undirected graph, with each point being a vertex typically of degree 4. Special cases include 'edge' points, whose degree is 3 or, in the case of corner points, 2.
Many of the methods that manage the state of the game and of the board, make use of this graph representation. Groups are contiguous points with the same color stone which are important in determining the life or death of stones on the board. When a player makes a move, (provided that move is legal,) the point at which the move is made will utilize a breadth-first graph traversal calling a `joinGroup` method on each point with the same color stone.
Adjacent points without stones are very important to the state of a point as well. These are known as liberties, and so long as a group of stones has at least one point with at least one liberty, that group remains alive and on the board. Therefore, the `joinGroup` method also utilizes a depth first traversal to mark all of the liberties of a group. Both the stones and the liberties of the group are memoized on the Game object.
![Image of Game logic](public/game-logic.png)
### Game Records
For the moment, game records are modeled as a list where each move has the type `{player: <string: player color>, pos: {x: <integer>, y: <integer>}}`.
The database `move` table contains additional information `number`, `game_record`, and `prior_move` in addition to a foreign key for a `game` row. `number` represents the move number in a game record (for now this corresponds to list position), `game_record` is a boolean representing whether the move was 'official' or is an alternate move used in study, and `prior_move` is a reference to another `move` row allowing for the construction of a tree model for the game (see [Expanding this representation](#expanding-this-representation), below.)
There is a backend service that processes this list of moves into a current board state ([Modeling Game State](#modeling-game-state).) On the frontend, users have the option of expanding a menu to view the move order in the format below.
![Game Record: Black and white 19x19 grid with moves represented as circles filled in with stone color and marked with move number. Below the grid is an overflow area for moves made at previously played points in the format /[new move number and color at old move number and color/]](public/game-record.png)
This is a customary representation of game records printed in Go literature. A frontend service processes the list of moves and plots each move onto a `<canvas>` element drawn to resemble the grid lines on a board, with moves that are placed at prior points plotted on an additional `<canvas>` element below.
#### Expanding this representation
This representation will be expanded when support for alternate game paths is added. Alternate game paths will allow users to study completed games by playing out the consequences of moves not in the completed game. This feature will require a tree structure for the game record where moves on the main line are referred with a `main` property on each move node and alternate moves with any number of alternate references.
### Caching multiple in-progress games
![Image of Game module in context](public/game-module.png)
### Partitioning Game Rooms
Finding a game starts with joining a game room.
![Image of Home Screen with 'main' game room](public/home-screen.png)
Watch an in progress game, join a game, or study a historic game.
![Image of Room Screen with multiple games in various states](public/room-screen.png)
---
## Setup
### Local Repo
```sh
$ git clone https://github.com/sorrelbri/node-go.git
```
### Install Deps
```sh
$ npm run bootstrap
```
Runs lerna `bootstrap` command installing dependencies for project, development and package dependencies
### Initialize Database
#### Download PostgreSQL
To verify PostgreSQL installation:
```sh
$ psql -V
```
Node Go API was built with version 11.4.
[See documentation for Postgres download.](https://www.postgresql.org/download/)
#### Create Databases
```sh
$ psql
# psql(11.4)
```
```sql
CREATE DATABASE node-go;
CREATE DATABASE node-go-test; # testing database
```
### Configure Environment
```sh
$ touch packages/server/.env
```
```
# .env
NODE_ENV=development
PORT=# set development port for API
REACT_ADDRESS=http://localhost:3000 # default
PG_CONNECTION_STRING=postgresql://localhost:5432/node-go
PG_CONNECTION_STRING_TEST=postgresql://localhost:5432/node-go-test
JWT_SECRET=# generate a secret key for JWT signing algorithm
TEST_SECRET=# same as above, for test environment
SALT_ROUNDS=# set number of salt rounds for bcrypt hashing function
DOMAIN=localhost
USER_ONE_PASSWORD=# credentials for testing with
USER_ONE_EMAIL=# same as above
```
### Smoke test
```sh
$ lerna run test
```
### Run Database Migrations
```sh
$ cd packages/server; npm run migrate; npm run seed
```
### Running in development
```sh
$ cd packages/server
$ npm start # or if you have nodemon
$ nodemon
```
```sh
$ cd packages/play-node-go
$ npm start
```
---
## Known Bugs
- game end logic not implemented on front end yet
- no authorization for game moves
- websocket connections may remain open, pooling prevents runaway leaks, but tests may hang
---
## Roadmap
### 6/20
1. Frontend implementation of game end logic
2. Auth for games
3. Game request creation
### 7/20
1. Generate game records
2. Implement chat
3. Implement study mode
---
## Features
- [ ] Realtime communications
- [x] Realtime play
- [x] Account authentication
- [ ] Chat
- [ ] Study mode
- [ ] Multiple game settings
- [ ] Customizable board size
- [ ] Download games in .sgf format
---
## Built with
- [Express](https://expressjs.com)
- [React](https://reactjs.org)
- [PostgreSQL](https://postgresql.org)
- [Socket.io](https://socket.io)
- [Sass](https://sass-lang.com)
### Management & Deployment
- Lerna
- CircleCI

View file

@ -1,8 +0,0 @@
`hk-api=https://git.heroku.com/node-go-api.git`
`hk-play=https://git.heroku.com/play-node-go.git`
### Server
`$ git subtree push --prefix packages/server hk-api master`
### React
`$ git subtree push --prefix packages/play-node-go hk-play master`

2336
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -3,15 +3,16 @@
"private": true,
"scripts": {
"test": "lerna run test",
"bootstrap": "lerna bootstrap"
"bootstrap": "lerna bootstrap --hoist"
},
"devDependencies": {
"lerna": "^3.20.2",
"chai": "^4.2.0",
"chai-http": "^4.3.0",
"mocha": "^7.0.0",
"@babel/preset-flow": "^7.8.3",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.4.0",
"chai": "^4.2.0",
"chai-http": "^4.3.0",
"lerna": "^3.20.2",
"mocha": "^7.0.0",
"react-test-renderer": "^16.12.0"
}
}

View file

@ -1,29 +0,0 @@
[ignore]
.*/node_modules/.*
.*/src/serviceWorker\.js
.*/src/index\.js
.*\.test
.*/server/node_modules/.*
.*/server/test/.*
.*/server/knexfile\.js
.*/public/.*
.*\.scss
.*\.css
.+\.s?css
[include]
.*/src/components/
.*/src/pages/.*
.*/src/reducers/.*
.*/src/utils/.*
.*/App.js
[libs]
[lints]
[options]
[strict]
[untyped]

File diff suppressed because it is too large Load diff

View file

@ -1,22 +1,21 @@
{
"name": "react-boilerplate",
"name": "play-node-go",
"version": "0.1.0",
"private": true,
"dependencies": {
"@mars/heroku-js-runtime-env": "^3.0.2",
"@testing-library/user-event": "^7.1.2",
"flow-bin": "^0.114.0",
"node-sass": "^4.13.0",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"node-sass": "^4.14.1",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-router-dom": "^5.1.2",
"react-scripts": "3.3.0",
"react-scripts": "^3.4.0",
"socket.io-client": "^2.3.0"
},
"scripts": {
"start": "REACT_APP_ENVIRONMENT='development' react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"test": "CI=true react-scripts test",
"eject": "react-scripts eject",
"flow": "./node_modules/.bin/flow",
"predeploy": "REACT_APP_ENVIRONMENT=production npm run build",

View file

@ -1,3 +1,8 @@
a, a:link, a:visited, a:focus, a:active {
color: unset;
text-decoration: none;
}
/* http://meyerweb.com/eric/tools/css/reset/
v2.0 | 20110126
License: none (public domain)
@ -46,3 +51,8 @@ table {
border-collapse: collapse;
border-spacing: 0;
}
button {
background: none;
border: none;
}

View file

@ -14,4 +14,8 @@
color: rgb(255,240,230);
background-color: rgba(0,0,0,0.7);
padding: 0.25em;
}
@function place-below($grid-column, $grid-row) {
@return $grid-row + 1 + '/' + $grid-column + 2 + '/ span 1 / span 1';
}

View file

@ -28,4 +28,16 @@ $colors: (
$backgrounds: (
"game_room": radial-gradient(farthest-corner at 55% 40%, rgb(189, 131, 100) 0%, rgb(175, 113, 80) 65%, rgb(150, 90, 65) 90%, rgb(125, 65, 40) 100%),
)
);
/* Game Room Components */
$small-room: (
"roof": linear-gradient(rgb(69, 36, 19), rgb(31, 22, 18)),
"body": rgb(174, 169, 120),
"door": radial-gradient(15vh 20vh at top left, #662413,#992413 15%, #cc3423),
"door-handle": radial-gradient(circle at 40% 30%, rgb(247, 235, 183), rgb(213, 222, 41) 30%, rgb(121, 108, 7)),
"window-x": linear-gradient(rgba(67, 149, 159, 0.4), rgba(196, 249, 255, 0.4) 50%, rgba(67, 149, 159, 0.4) 50%, rgba(196, 249, 255, 0.4)),
"window-y": linear-gradient(to right, rgba(67, 149, 159, 0.5), rgba(196, 249, 255, 0.5) 50%, rgba(67, 149, 159, 0.5) 50%, rgba(196, 249, 255, 0.5)),
"frame": rgb(69, 36, 19),
"sign": rgb(188, 127, 95)
);

View file

@ -1,8 +0,0 @@
body {
padding: 50px;
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
}
a {
color: #00B7FF;
}

View file

@ -1,6 +1,5 @@
import React, {useState, useEffect, useReducer} from 'react';
import config from './config';
import { Switch, Route, BrowserRouter as Router, Redirect } from 'react-router-dom';
import React, { useEffect, useReducer } from 'react';
import { Switch, Route, BrowserRouter as Router } from 'react-router-dom';
import MainWrapper from './pages/Layout/MainWrapper/MainWrapper';
import { stateReducer } from './reducers/reducer';
import { initState } from './reducers/init/reducer.init';
@ -14,31 +13,32 @@ function App() {
{},
initState
);
const fetchIndexAPI = async () => {
const response = await indexServices.indexService();
if (response) {
const action = {
type: 'INDEX',
message: 'SET_USER',
body: response
useEffect(() => {
const fetchIndexAPI = async () => {
const response = await indexServices.indexService();
if (response) {
const action = {
type: 'INDEX',
message: 'SET_USER',
body: response
}
dispatch(action)
}
dispatch(action)
}
}
useEffect(() => {
fetchIndexAPI();
}, [])
const socketConnect = () => {
if (state.connect.type) return;
dispatch({type:'SOCKET', message: 'LAUNCH', body:{nsp:'', dispatch}});
}
}, [ ])
useEffect(() => {
const socketConnect = () => {
if (state.connect.type) return;
dispatch({type:'SOCKET', message: 'LAUNCH', body:{nsp:'', dispatch}});
}
socketConnect();
}, [])
return () => dispatch({type: 'SOCKET', message: 'DISCONNECT', body: {}});
}, [ state.connect ])
return (
<Router>

View file

@ -3,34 +3,48 @@ import { Link } from 'react-router-dom';
import './Game.scss';
const GameButton = (props) => {
const { game, dispatch, user } = props;
const { game, user } = props;
const requestJoinGame = () => {
console.log(`request to Join Game ${game.id}!`)
const requestAction = {
type: 'GAMES',
message: 'JOIN_REQUEST',
body: {id: game.id}
const setGameDisplayData = () => {
const gameData = {
playerBlack: game.playerBlack,
playerBlackRank: game.playerBlackRank,
gameId: game.id,
}
dispatch(requestAction);
}
const renderOpenGame = () => {
return (
<>
<a onClick={() => requestJoinGame()} >Request to Join Game</a>
<div className="GameButton__player-data GameButton__player-data--black">
<span className="GameButton__player-data__name GameButton__player-data__name--black">{game.playerBlack}</span>
<span className="GameButton__player-data__rank GameButton__player-data__rank--black">{game.playerBlackRank}</span>
</div>
</>
)
}
const renderInProgressGame = () => {
const gameLinkText = game.winType ? 'Study Game'
if (game.open) {
gameData.gameLinkText = 'Request to Join Game';
gameData.playerWhite = '';
gameData.playerWhiteRank = 'could be you!';
}
if (!game.open) {
gameData.playerWhite = game.playerWhite;
gameData.playerWhiteRank = game.playerWhiteRank;
gameData.gameLinkText = game.winType ? 'Study Game'
: user ? 'Rejoin Game' : 'Watch Game'
}
return gameData;
}
// TODO add to open game link
// const requestJoinGame = () => {
// console.log(`request to Join Game ${game.id}!`)
// const requestAction = {
// type: 'GAMES',
// message: 'JOIN_REQUEST',
// body: {id: game.id}
// }
// dispatch(requestAction);
// }
const renderGame = () => {
const {
gameLinkText,
playerBlack,
playerBlackRank,
gameId,
playerWhite,
playerWhiteRank
} = setGameDisplayData();
return (
<>
<div className="GameButton__seat GameButton__seat--black">
@ -44,26 +58,29 @@ const GameButton = (props) => {
>
<span
className="GameButton__player-data__name GameButton__player-data__name--black"
>{game.playerBlack}</span>
>{playerBlack}</span>
<span
className="GameButton__player-data__rank GameButton__player-data__rank"
>{game.playerBlackRank}</span>
>{playerBlackRank}</span>
</div>
<Link
className="GameButton__link"
to={`/games/${game.id}`}
to={`/games/${gameId}`}
>{gameLinkText}</Link>
<div
className="GameButton__player-data GameButton__player-data--white"
<div
className={`GameButton__player-data GameButton__player-data--white ${
playerWhite ? '' : 'GameButton__player-data--invisible'
}`
}
>
<span
className="GameButton__player-data__name GameButton__player-data__name--white"
>{game.playerWhite}</span>
<span
className="GameButton__player-data__rank GameButton__player-data__rank--white"
>{game.playerWhiteRank}</span>
<span
className="GameButton__player-data__name GameButton__player-data__name--white"
>{playerWhite}</span>
<span
className="GameButton__player-data__rank GameButton__player-data__rank--white"
>{playerWhiteRank}</span>
</div>
</div>
@ -90,7 +107,7 @@ const GameButton = (props) => {
return (
<div className="GameButton" data-testid="GameButton">
{game.open ? renderOpenGame() : renderInProgressGame()}
{renderGame()}
</div>
);
}

View file

@ -2,7 +2,6 @@
div.GameButton {
align-items: stretch;
box-shadow: -2vmin 4vmin 2vmin rgba(83, 53, 35, 0.81);
display: flex;
flex-flow: column nowrap;
height: 20vh;
@ -12,7 +11,7 @@ div.GameButton {
}
div.GameButton__seat {
background-color: red;
background: linear-gradient(190deg, rgb(232, 78, 78), rgb(172, 18, 18) 40%, rgb(100, 0, 0));
height: 10%;
margin: 0 auto;
width: 50%;
@ -30,6 +29,7 @@ div.GameButton__seat {
}
div.GameButton__table {
box-shadow: -2vmin 4vmin 2vmin rgba(83, 53, 35, 0.81);
height: 80%;
margin: 0;
width: 100%;
@ -67,15 +67,16 @@ div.GameButton__table__image {
z-index: 1;
div.table__game-board {
align-items: center;
background: radial-gradient(
farthest-corner at 55% 40%,
rgb(244, 230, 120) 0%, rgb(234, 178, 78) 65%, rgb(200, 160, 90) 90%, rgb(200, 140, 90) 100%
);
);
box-shadow: -0.5vh 1vh 2vh rgba(30,30,30, 0.7);
height: 60%;
justify-content: center;
width: 60%;
display: flex;
align-items: center;
justify-content: center;
div.table__game-board--grid {
background:
@ -99,6 +100,7 @@ div.table__player-area {
rgb(100, 40, 5) 80%, rgb(80, 20, 0) 90%
);
border-radius: 50%;
box-shadow: -0.5vh 1vh 2vh rgba(30,30,30, 0.7);
height: 100%;
&--white {
@ -117,6 +119,10 @@ div.GameButton__player-data {
display: flex;
justify-content: space-around;
max-width: 100%;
&.GameButton__player-data--invisible {
visibility: hidden;
}
&.GameButton__player-data--white {
margin: 0 5vw 1vw 0.5vw;
@ -127,11 +133,7 @@ div.GameButton__player-data {
}
}
.GameButton__link {
a.GameButton__link {
@include gameViewLabel;
margin: 0 auto;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}

View file

@ -0,0 +1,34 @@
import React from "react";
import authServices from "../../../services/authServices";
const Guest = ({ dispatch }) => {
const handleClick = async (e) => {
e.preventDefault();
// dispatch to guest endpoint
const guestResponse = await authServices.guestService();
if (guestResponse.errors) {
const authError = guestResponse.errors[0].auth;
return dispatch({
type: "ERR",
message: "AUTH_ERROR",
body: { authError },
});
}
return dispatch({
type: "AUTH",
message: "GUEST",
body: guestResponse,
});
};
return (
<>
<button className="nav__section__button" onClick={handleClick}>
Continue As Guest
</button>
</>
);
};
export default Guest;

View file

@ -1,14 +1,70 @@
import React from 'react';
import { Link } from 'react-router-dom';
import RoomDetail from '../../Display/RoomDetail/RoomDetail';
import './Room.scss';
const RoomButton = (props) => {
const roomData = props.room;
const RoomButton = ({room, roomDetail, showRoomDetail}) => {
const smallRoom = (
<div className="small-room">
<div className="small-room__roof">
<div className="small-room__roof-top small-room__roof-top--left"></div>
<div className="small-room__roof--left"></div>
<div className="small-room__roof-top small-room__roof-top--right"></div>
<div className="small-room__roof--right"></div>
</div>
<div className="small-room__body">
<div className="small-room__trim small-room__trim--left"></div>
<div className="small-room__sign">
<div className="small-room__sign__stone small-room__sign__stone--black"></div>
<div className="small-room__sign__stone small-room__sign__stone--white"></div>
<div className="small-room__sign__stone small-room__sign__stone--white"></div>
<div className="small-room__sign__stone small-room__sign__stone--black"></div>
</div>
<div className="small-room__window small-room__window--left">
<div className="small-room__window-frame small-room__window-frame--top"></div>
<div className="small-room__window-frame small-room__window-frame--left"></div>
<div className="small-room__window-frame small-room__window-frame--middle"></div>
<div className="small-room__window-frame small-room__window-frame--center"></div>
<div className="small-room__window-frame small-room__window-frame--right"></div>
<div className="small-room__window-frame small-room__window-frame--bottom"></div>
</div>
<div className="small-room__door">
<div className="small-room__door-frame small-room__door-frame--top"></div>
<div className="small-room__door-frame small-room__door-frame--left"></div>
<div className="small-room__door-handle"></div>
<div className="small-room__door-frame small-room__door-frame--right"></div>
</div>
<div className="small-room__window small-room__window--right">
<div className="small-room__window-frame small-room__window-frame--top"></div>
<div className="small-room__window-frame small-room__window-frame--left"></div>
<div className="small-room__window-frame small-room__window-frame--middle"></div>
<div className="small-room__window-frame small-room__window-frame--center"></div>
<div className="small-room__window-frame small-room__window-frame--right"></div>
<div className="small-room__window-frame small-room__window-frame--bottom"></div>
</div>
<div className="small-room__trim small-room__trim--bottom-left"></div>
<div className="small-room__trim small-room__trim--right"></div>
<div className="small-room__trim small-room__trim--bottom-right"></div>
</div>
</div>
)
return (
<div className="RoomButton" data-testid="RoomButton">
<h4 className="RoomButton__room-name">{roomData.name}</h4>
<Link to={`/rooms/${roomData.id}`}>Join Room</Link>
</div>
<>
<div className="RoomButton" data-testid="RoomButton">
<h4 className="RoomButton__room-link RoomButton__room-link--action">
<Link to={`/rooms/${room.id}`}>Join {room.name}</Link>
</h4>
<h4
className="RoomButton__room-link RoomButton__room-link--info --link"
onClick={e => showRoomDetail(room.id)}
>
?
</h4>
{smallRoom}
</div>
{roomDetail ? <RoomDetail room={room} /> : <></>}
</>
);
}

View file

@ -1,9 +1,227 @@
div.RoomButton {
display: block;
padding: 1vw;
@import '../../../../public/stylesheets/partials/mixins';
@import '../../../../public/stylesheets/partials/variables';
.RoomButton__room-name {
text-transform: capitalize;
div.RoomButton {
display: grid;
grid-template-columns: [left] 1fr [right];
grid-template-rows: [top] 1fr [middle] 4fr [bottom];
height: 20vh;
padding: 1vw;
width: 20vh;
.RoomButton__room-link {
@include gameViewLabel;
align-self: self-end;
grid-area: top/left/middle/right;
max-width: fit-content;
z-index: 1;
&--info {
justify-self: end;
}
}
div.small-room {
display: grid;
grid-area: top/left/bottom/right;
grid-template-rows: [sky-start] 4vh [roof-start] 5vh [body-start] 1fr [body-end];
height: 100%;
width: 100%;
.small-room__roof {
display: grid;
grid-row: roof-start / body-start;
grid-template-columns: [left] 10vh [middle] 10vh [right];
grid-template-rows: 1fr;
height: 100%;
max-width: 20vh;
.small-room__roof-top {
background: map-get($small-room, "roof");
box-shadow: 0 0.35vh 0.75vh map-get($colors, "home");
grid-row: 1 / 2;
height: 0.7vh;
position: relative;
top: 3vh;
width: 12vh;
&--left {
grid-column: left / middle;
left: -1.2vh;
transform: rotate(-26deg);
}
&--right {
grid-column: middle / right;
left: -0.8vh;
transform: rotate(26deg);
}
}
&--left {
border-right: map-get($small-room, "body") solid 10vh;
border-top: map-get($colors, "home") solid 5vh;
grid-column: left / middle;
height: 0;
}
&--right {
border-left: map-get($small-room, "body") solid 10vh;
border-top: map-get($colors, "home") solid 5vh;
grid-column: middle / right;
height: 0;
}
}
.small-room__body {
background-color: map-get($small-room, "body");
display: grid;
grid-row: body-start / body-end;
grid-template-areas:
"trim-left gap-left top-left gap-mid-left top-mid gap-mid-right top-right gap-right trim-right"
"trim-left gap-left top-left gap-mid-left top-mid gap-mid-right top-right gap-right trim-right"
"trim-left gap-left window--left gap-mid-left door gap-mid-right window--right gap-right trim-right"
"trim-left trim--bottom-left trim--bottom-left trim--bottom-left door trim--bottom-right trim--bottom-right trim--bottom-right trim-right";
grid-template-columns: 0.5vh 1vh 2fr 1vh 3fr 1vh 2fr 1vh 0.5vh;
grid-template-rows: 1fr 1vh 5fr 0.75vh;
justify-self: center;
width: 80%;
}
.small-room__trim {
background-color: map-get($small-room, "frame");
height: 100%;
width: 100%;
&--left {
grid-area: trim-left;
justify-self: start;
}
&--right {
grid-area: trim-right;
justify-self: end;
}
&--bottom-left {
grid-area: trim--bottom-left;
}
&--bottom-right {
grid-area: trim--bottom-right;
}
}
.small-room__sign {
background-color: map-get($small-room, "sign");
display: grid;
gap: 0.1vh;
grid-area: top-mid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
height: 2vh;
justify-self: center;
padding: 0.1vh;
width: 2vh;
.small-room__sign__stone {
border-radius: 100%;
&--white {
background-color: #FFF;
}
&--black {
background-color: #000;
}
}
}
.small-room__window {
align-self: center;
background: map-get($small-room, "window-x"), map-get($small-room, "window-y");
display: grid;
grid-template-columns: 1fr 5fr 1fr 5fr 1fr;
grid-template-rows: 1fr 5fr 1fr 5fr 1fr;
height: 60%;
width: 100%;
&--left {
grid-area: window--left;
}
&--right {
grid-area: window--right;
}
.small-room__window-frame {
background-color: map-get($small-room, "frame");
height: 100%;
width: 100%;
&--top {
grid-area: 1/1/2/6;
}
&--center {
grid-area: 3/1/4/6;
}
&--bottom {
grid-area: 5/1/6/6;
}
&--left {
grid-area: 1/1/6/2;
}
&--middle {
grid-area: 1/3/6/4;
}
&--right {
grid-area: 1/5/6/6;
}
}
}
.small-room__door {
grid-area: door;
background: map-get($small-room, "door");
display: grid;
grid-template-columns: 0.5vh 1fr 0.5vh;
grid-template-rows: 0.5vh 1fr;
height: 100%;
width: 100%;
.small-room__door-handle {
align-self: center;
background: map-get($small-room, "door-handle");
border-radius: 50%;
box-shadow: 0.1vh 0.25vh 0.4vh rgba(30, 30, 30, 0.7);
grid-area: 2/2/3/3;
height: 0.75vh;
justify-self: end;
margin: 0.5vh 0.5vh 0 0;
width: 0.75vh;
}
.small-room__door-frame {
background-color: map-get($small-room, "frame");
height: 100%;
width: 100%;
&--top {
grid-area: 1/1/2/3;
}
&--left {
grid-area: 1/1/3/2;
}
&--left {
grid-area: 1/3/3/4;
}
}
}
}
}

View file

@ -0,0 +1,25 @@
import React from 'react';
import './RoomDetail.scss';
const RoomDetail = ({room}) => {
return (
<div className="RoomDetail">
<div className="RoomDetail__arrow"></div>
<div className="RoomDetail__data">
<div className="RoomDetail__heading">
<h3 className="RoomDetail__room-name">{room.name}</h3>
<div className="RoomDetail__room-lang"></div>
<p className="RoomDetail__room-description">{room.description}</p>
</div>
<div className="RoomDetail__current">
<h4 className="RoomDetail__current-header">Current:</h4>
<p className="RoomDetail__current-players"></p>
<p className="RoomDetail__current-games"></p>
<p className="RoomDetail__current-rank"></p>
</div>
</div>
</div>
);
}
export default RoomDetail;

View file

@ -0,0 +1,32 @@
@import '../../../../public/stylesheets/partials/mixins';
@import '../../../../public/stylesheets/partials/variables';
div.RoomDetail {
display: grid;
grid-template-columns: [left] 1fr [right];
grid-template-rows: [top] 1fr [middle] 9fr [bottom];
height: 20vh;
padding: 1vw;
width: 20vh;
div.RoomDetail__arrow {
border-top: map-get($colors, 'home') solid 2vh;
border-left: white solid 1.5vh;
height: 0;
}
div.RoomDetail__data {
background-color: white;
height: 90%;
width: 100%;
padding: 1vh;
display: grid;
grid-template-rows: 2fr 3fr;
.RoomDetail__heading {
display: flex;
flex-flow: row wrap;
align-items: flex-start;
}
}
}

View file

@ -2,7 +2,7 @@ import React from 'react';
import './ActionError.scss';
const ActionError = (props) => {
const errorMessage = props.error;
const errorMessage = props.error || '';
return (
<span
className="error error--action"

View file

@ -3,7 +3,7 @@ import { render } from '@testing-library/react';
import FormError from './FormError';
test('renders FormError without crashing', () => {
const { getByTestId } = render(<FormError />);
const { getByTestId } = render(<FormError error={''}/>);
const FormErrorSpan = getByTestId('FormError');
expect(FormErrorSpan).toBeInTheDocument();
});

View file

@ -1,45 +1,49 @@
import React, { useState } from 'react';
import React, { useState } from "react";
import Login from '../Login/Login';
import Signup from '../Signup/Signup';
import Login from "../Login/Login";
import Signup from "../Signup/Signup";
import Guest from "../../Button/Guest/Guest";
const Auth = (props) => {
const [ showForm, setShowForm ] = useState('login')
const [showForm, setShowForm] = useState("login");
const { state, dispatch } = props;
return (
<>
<div
className="nav__section nav__section--auth"
onClick={()=>{setShowForm('login')}}
<div
className="nav__section nav__section--auth"
onClick={() => {
setShowForm("login");
}}
>
<p
className="nav__section__label"
>Login</p>
<p className="nav__section__label">Login</p>
{
showForm === 'login'
? <Login dispatch={dispatch} state={state}/>
: <></>
}
{showForm === "login" ? (
<Login dispatch={dispatch} state={state} />
) : (
<></>
)}
</div>
<div
className="nav__section nav__section--auth"
onClick={()=>{setShowForm('signup')}}
className="nav__section nav__section--auth"
onClick={() => {
setShowForm("signup");
}}
>
<p
className="nav__section__label"
>Signup</p>
<p className="nav__section__label">Signup</p>
{
showForm === 'signup'
? <Signup dispatch={dispatch} state={state}/>
: <></>
}
{showForm === "signup" ? (
<Signup dispatch={dispatch} state={state} />
) : (
<></>
)}
</div>
<div className="nav__section nav__section--auth">
<Guest dispatch={dispatch} />
</div>
</>
);
}
};
export default Auth;

View file

@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState } from 'react';
import './Signup.scss';
import authServices from '../../../services/authServices';
import FormError from '../../Error/FormError/FormError';
@ -22,7 +22,7 @@ const Signup = (props) => {
})
}
if (password.length < 8) {
if (password.length < minimumPasswordLength) {
return props.dispatch({
...errorDispatchAction,
body: { authError: 'Password must be at least 8 characters'}

View file

@ -1,37 +1,74 @@
import React from 'react';
import './Board.scss';
import Point from '../Point/Point';
import React from "react";
import "./Board.scss";
import Point from "../Point/Point";
const Board = (props) => {
const { game, record, user, dispatch, board } = props;
const sizeFlag = `Game__board--size-${ game.boardSize }`
const { game, user, dispatch, board, meta } = props;
const sizeFlag = `Game__board--size-${game.boardSize}`;
const hoshiPoints = {
9: { "3-3": true, "7-7": true, "3-7": true, "7-3": true },
13: {
"7-7": true,
"10-7": true,
"7-4": true,
"7-10": true,
"4-7": true,
"4-4": true,
"10-10": true,
"4-10": true,
"10-4": true,
},
19: {
"10-10": true,
"16-10": true,
"10-4": true,
"10-16": true,
"4-10": true,
"4-4": true,
"16-16": true,
"4-16": true,
"16-4": true,
},
};
const renderPoints = boardSize => {
let i = 0, boardPoints = [];
const isHoshi = (posX, posY) =>
hoshiPoints[game.boardSize][`${posX}-${posY}`];
const renderPoints = (boardSize) => {
let i = 0,
boardPoints = [];
while (i < boardSize * boardSize) {
const posX = Math.floor(i/boardSize) + 1;
const posY = i % boardSize + 1;
console.log(board[`${posX}-${posY}`])
const posX = Math.floor(i / boardSize) + 1;
const posY = (i % boardSize) + 1;
const pointData = board[`${posX}-${posY}`];
const dotData =
meta && meta.turn === 0 && !meta.winner && meta.territory
? meta.territory[`${posX}-${posY}`]
: game.turn || meta?.turn;
boardPoints.push(
<Point
key={`${posX}-${posY}`}
<Point
key={`${posX}-${posY}`}
posX={posX}
posY={posY}
pointData={board[`${posX}-${posY}`]}
// point={board[posX][posY]}
pointData={pointData}
dotData={dotData}
dispatch={dispatch}
user={user}
meta={meta}
hoshi={isHoshi(posX, posY)}
{...props}
/>
); i++;
);
i++;
}
return boardPoints;
}
};
return (
return (
<div className={`Game__board ${sizeFlag}`}>
{ game.id ? renderPoints(game.boardSize) : <></> }
{game.id ? renderPoints(game.boardSize) : <></>}
</div>
);
}
};
export default Board;
export default Board;

View file

@ -0,0 +1,15 @@
import React from "react";
import "./Kifu.scss";
const Kifu = ({ clickKifu }) => {
return (
<div className="Kifu">
<p className="Kifu__show-menu" onClick={clickKifu}>
Show Menu?
</p>
<div className="Kifu__board"></div>
</div>
);
};
export default Kifu;

View file

@ -0,0 +1,31 @@
@import '../../../../public/stylesheets/partials/mixins';
div.Kifu {
order: 0;
height: 10vh;
width: 8vh;
background-color: #FFF;
transform: rotate(-20deg);
p.Kifu__show-menu {
display: none;
margin: 3vh auto;
position: absolute;
transform: rotate(20deg);
width: fit-content;
}
&:hover {
p.Kifu__show-menu {
@include gameViewLabel;
cursor: pointer;
display: inline-block;
}
}
div.Kifu__board {
border: 0.25vh solid #444;
width: 5vh;
height: 5vh;
margin: 1vh 2vh 2vh auto;
}
}

View file

@ -0,0 +1,194 @@
import React, { useEffect, useRef } from "react";
import "./Menu.scss";
const Menu = ({ showMenu, clickClose, ...props }) => {
const { active, meta } = props.state; // active.game.boardSize, meta.gameRecord
const boardSize = active.game.boardSize;
const handleBackgroundClick = (e) => {
if (e.target.className === "Game__Menu-container") clickClose();
};
const canvasRef = useRef();
const overflowRef = useRef();
const drawGameRecord = () => {
return (
<>
<canvas ref={canvasRef} />
<canvas ref={overflowRef} />
</>
);
};
const clickPrint = () => {
const iframe = document.createElement("iframe");
const body = document.getElementsByTagName("body")[0];
const record = document.getElementById("record");
body.appendChild(iframe);
// move game record to iframe and print iframe
iframe.contentDocument.body.appendChild(canvasRef.current);
iframe.contentDocument.body.appendChild(overflowRef.current);
iframe.contentWindow.print();
// move game record back to menu and remove iframe
record.appendChild(canvasRef.current);
record.appendChild(overflowRef.current);
body.removeChild(iframe);
};
// draw canvases
useEffect(() => {
const canvas = canvasRef.current;
const ctx = canvas.getContext("2d");
const scale = Math.min(window.innerWidth * 0.75, 500);
canvas.height = scale;
canvas.width = scale;
const space = scale / boardSize;
const offset = space / 2;
for (let i = 0; i < boardSize; i++) {
const start = i * space + offset;
const end = scale - offset;
ctx.beginPath();
ctx.moveTo(start, offset);
ctx.lineTo(start, end);
ctx.closePath();
ctx.stroke();
ctx.beginPath();
ctx.moveTo(offset, start);
ctx.lineTo(end, start);
ctx.closePath();
ctx.stroke();
}
if (!meta?.gameRecord) return;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
const { overflow } = meta.gameRecord.reduce(
(dict, { player, pos }, index) => {
const past = dict[`${pos.x}-${pos.y}`];
if (past) {
// overflow: [ { move:#, player:'color', subsequentMoves: [ { move: #, player: 'color' } ] } ]
if (dict.overflow) {
const indexOfPrior = dict.overflow.findIndex(
({ move }) => move === past
);
if (indexOfPrior !== -1) {
// if multiple past moves at this point exist
dict.overflow[indexOfPrior].subsequentMoves.push({
move: index + 1,
player,
});
return dict;
}
// if a second move at this point has not yet been encountered
// prior move will be black if no active handicap and move is odd or if active handicap and move is even
const playerPrior =
(active.handicap && !(past % 2)) || past % 2 ? "black" : "white";
return {
...dict,
overflow: [
...dict.overflow,
{
move: past,
player: playerPrior,
subsequentMoves: [{ move: index + 1, player }],
},
],
};
}
// if no move has yet been encountered at a previously made move
return {
...dict,
overflow: [
{ move: past, subsequentMoves: [{ move: index + 1, player }] },
],
};
}
ctx.beginPath();
ctx.arc(
(pos.y - 1) * space + offset,
(pos.x - 1) * space + offset,
offset * 0.95,
0,
Math.PI * 2,
true
);
ctx.stroke();
ctx.fillStyle = player === "white" ? "#fff" : "#000";
ctx.fill();
ctx.fillStyle = player === "white" ? "#000" : "#fff";
ctx.fillText(
index + 1,
(pos.y - 1) * space + offset,
(pos.x - 1) * space + offset
);
return { ...dict, [`${pos.x}-${pos.y}`]: index + 1 };
},
{}
);
if (!overflow?.length) return;
// Draw Overflow Moves (moves made at prior points)
const canvas2 = overflowRef.current;
const ctx2 = canvas2.getContext("2d");
canvas2.width = scale;
canvas2.height = space * overflow.length;
ctx2.textAlign = "center";
ctx2.textBaseline = "middle";
overflow.forEach(({ move, subsequentMoves, player }, index) => {
subsequentMoves.forEach(({ player, move }, subIndex) => {
ctx2.beginPath();
ctx2.arc(
subIndex * space + offset,
index * space + offset,
offset * 0.95,
0,
Math.PI * 2,
true
);
ctx2.stroke();
ctx2.fillStyle = player === "white" ? "#fff" : "#000";
ctx2.fill();
ctx2.fillStyle = player === "white" ? "#000" : "#fff";
ctx2.fillText(move, subIndex * space + offset, index * space + offset);
});
ctx2.fillStyle = "#000";
ctx2.fillText(
"at",
subsequentMoves.length * space + offset,
index * space + offset
);
ctx2.fillStyle = player === "white" ? "#fff" : "#000";
ctx2.beginPath();
ctx2.arc(
(subsequentMoves.length + 1) * space + offset,
index * space + offset,
offset * 0.95,
0,
Math.PI * 2,
true
);
ctx2.fill();
ctx2.stroke();
ctx2.fillStyle = player === "white" ? "#000" : "#fff";
ctx2.fillText(
move,
(subsequentMoves.length + 1) * space + offset,
index * space + offset
);
});
}, [showMenu, meta, active.handicap, boardSize]);
return (
<div
className={`Game__Menu-container${showMenu ? "" : "--hidden"}`}
onClick={(e) => handleBackgroundClick(e)}
>
<div className="Game__Menu-container__Menu">
<button onClick={clickClose}>X</button>
<button onClick={clickPrint}>Print Record</button>
<div id="record" className="Game__Menu__game-record-container">
{drawGameRecord()}
</div>
</div>
</div>
);
};
export default Menu;

View file

@ -0,0 +1,29 @@
div.Game__Menu-container {
&--hidden {
display: none;
}
background: rgba(0,0,0,0.5);
display: flex;
flex-flow: column nowrap;
justify-content: center;
align-items: center;
height: 100vh;
position: absolute;
width: 100vw;
z-index: 2;
.Game__Menu-container__Menu {
background: #eef;
max-height: 90vh;
max-width: 80vw;
padding: 2vh;
overflow: scroll;
}
div.Game__Menu__game-record-container {
display: flex;
flex-flow: column nowrap;
align-items: center;
}
}

View file

@ -1,45 +1,66 @@
import React from 'react';
import './PlayerArea.scss';
import React from "react";
import "./PlayerArea.scss";
const PlayerArea = (props) => {
// const { user } = props
const user = {
stones: 'black',
username: 'Name',
captures: 0
}
const PlayerArea = ({
handleResignClick,
handlePassClick,
playerMeta,
turn,
Kifu,
}) => {
const { stones, player, rank, captures } = playerMeta;
const isTurn =
(stones === "black" && turn === 1) || (stones === "white" && turn === -1);
const bowlAttributes = () => {
if (isTurn || turn === 0)
return {
"data-turn": true,
onClick: () => handlePassClick(stones),
};
return null;
};
const bowlText = () => {
if (isTurn) return "Pass?";
if (turn === 0) return "End Game?";
};
return (
<div
className={`player-container player-container--${user.stones}`}
>
<div
className={`player-container__bowl player-container__bowl--${user.stones}`}
>
<p>Pass?</p>
</div>
<div className={`player-container player-container--${stones}`}>
<div
className={`player-container__name-space player-container__name-space--${user.stones}`}
className={`player-container__bowl player-container__bowl--${stones}`}
{...bowlAttributes()}
>
<h4>{user ? user.username : 'Waiting for player' }</h4>
<p>{bowlText()}</p>
</div>
{Kifu}
<div
className={`player-container__name-space player-container__name-space--${stones}`}
>
<h4>
{playerMeta
? `${player || stones} ${rank || "?"}`
: "Waiting for player"}
</h4>
<div
className={`player-container__caps-space player-container__caps-space__${user.stones}`}
className={`player-container__caps-space player-container__caps-space__${stones}`}
>
<p
className={`player-container__resign-message player-container__resign-message--${user.stones}`}
>Resign?</p>
<p
className={`player-container__caps-counter player-container__caps-counter--${user.stones}`}
>{user ? user.captures : 'Captures go here'}</p>
className={`player-container__resign-message player-container__resign-message--${stones}`}
{...(isTurn ? { onClick: () => handleResignClick(stones) } : null)}
>
Resign?
</p>
<p
className={`player-container__caps-counter player-container__caps-counter--${stones}`}
>
{playerMeta ? captures : "Captures go here"}
</p>
</div>
</div>
</div>
);
}
};
export default PlayerArea;
export default PlayerArea;

View file

@ -47,6 +47,7 @@ div.player-container {
padding: .5em;
}
&[data-turn] {
// highlight for turn
box-shadow: 0 0 3vh 3vh rgb(255, 175, 2);
& + .player-container__name-space .player-container__caps-space:hover :first-child {

View file

@ -1,58 +1,96 @@
import React from 'react';
import './Point.scss';
import React from "react";
import "./Point.scss";
const Point = (props) => {
const { posX, posY, user, game, record, dispatch, pointData } = props;
const turn = game.turn > 0 ? 'black' : 'white';
const stone = () => {
if (pointData === 1) return 'black'
if (pointData === -1) return 'white'
return 'none'
}
const {
posX,
posY,
user,
game,
meta,
dispatch,
pointData,
dotData,
hoshi,
} = props;
const turn =
meta && meta.turn
? meta.turn > 0
? "black"
: "white"
: game.turn > 0
? "black"
: "white";
const dot = () => {
if (pointData === 'l') return game.turn;
}
const stone = () => {
if (pointData === 1) return "black";
if (pointData === -1) return "white";
if (pointData === "k") return "ko";
return "none";
};
const xFlag = () => {
if ( posX === 1 ) return `board__point--top`
if ( posX === game.boardSize ) return `board__point--bottom`
return '';
}
if (posX === 1) return `board__point--top`;
if (posX === game.boardSize) return `board__point--bottom`;
return "";
};
const yFlag = () => {
if ( posY === 1 ) return `board__point--left`
if ( posY === game.boardSize ) return `board__point--right`
return '';
}
if (posY === 1) return `board__point--left`;
if (posY === game.boardSize) return `board__point--right`;
return "";
};
const clickHandle = (e) => {
if (meta?.turn === 0 && !meta?.winner) {
const action = {
type: "SOCKET",
message: "TOGGLE_TERRITORY",
body: { user, point: `${posX}-${posY}`, game, room: game.room },
};
return dispatch(action);
}
const action = {
type: 'SOCKET',
message: 'MAKE_MOVE',
type: "SOCKET",
message: "MAKE_MOVE",
body: {
user,
game,
room: game.room,
board: {},
move: { player: turn, pos: { x: posX, y: posY } }
move: { player: turn, pos: { x: posX, y: posY } },
},
};
dispatch(action);
};
const getDot = () => {
if (meta?.turn === 0) {
switch (dotData) {
case -1:
return "white";
case 1:
return "black";
case "d":
return "dame";
default:
return 0;
}
}
dispatch(action);
}
return dotData;
};
return (
<div
<div
className={`board__point ${xFlag()} ${yFlag()}`}
onClick={e => clickHandle(e)}
onClick={(e) => clickHandle(e)}
>
<div className="board__point__stone"
<div
className={`board__point__stone ${hoshi ? "hoshi" : ""}`}
data-stone={stone()}
>
<div className="board__point__dot" data-dot={dot()}></div>
<div className="board__point__dot" data-dot={getDot()}></div>
</div>
</div>
);
}
};
export default Point;
export default Point;

View file

@ -5,6 +5,9 @@ div.board__point {
margin: auto;
padding: 0;
vertical-align: middle;
display: flex;
justify-content: center;
align-items: center;
}
div.board__point--top {
@ -43,11 +46,14 @@ div.board__point__stone {
width: 85%;
height: 85%;
border-radius: 50%;
margin: auto;
vertical-align: middle;
display: flex;
flex-direction: column;
justify-content: center;
&.hoshi {
background: radial-gradient(circle farthest-corner at center, #000 0%, #000 14%, rgba(0,0,0,0) 15%);
z-index: 3;
}
}
div.board__point div.board__point__stone div.board__point__dot {
@ -58,13 +64,44 @@ div.board__point div.board__point__stone div.board__point__dot {
vertical-align: middle;
}
div.board__point__stone[data-stone="white"] {
background: radial-gradient(farthest-side at 55% 40%, white 0%, rgb(200,200,200) 65%, rgb(100,100,100) 90%, rgb(68, 50, 0) 100%);
box-shadow: -.25vmin .5vmin .5vmin rgba(145, 92, 23, 0.5);
div.board__point__stone{
&[data-stone="white"] {
background: radial-gradient(farthest-side at 55% 40%, white 0%, rgb(200,200,200) 65%, rgb(100,100,100) 90%, rgb(68, 50, 0) 100%);
box-shadow: -.25vmin .5vmin .5vmin rgba(145, 92, 23, 0.5);
}
&[data-stone="black"] {
background-color: black;
background: radial-gradient(farthest-side at 55% 40%, rgb(220,220,220) 0%, rgb(60,60,60) 45%, rgb(15,15,15) 90%, rgb(5, 5, 0) 100%);
box-shadow: -.25vmin .5vmin .5vmin rgba(145, 92, 23, 0.75);
}
&[data-stone="ko"] {
background-color: transparent;
border: .5vmin solid rgba(200,20,50,0.8);
border-radius: 0%;
height: 60%;
width: 60%;
}
}
div.board__point__stone[data-stone="black"] {
background-color: black;
background: radial-gradient(farthest-side at 55% 40%, rgb(220,220,220) 0%, rgb(60,60,60) 45%, rgb(15,15,15) 90%, rgb(5, 5, 0) 100%);
box-shadow: -.25vmin .5vmin .5vmin rgba(145, 92, 23, 0.75);
div.board__point {
div.board__point__dot[data-dot="dame"] {
background: purple;
}
div.board__point__dot[data-dot="black"] {
background: black;
}
div.board__point__dot[data-dot="white"] {
background: white;
}
&:hover {
div.board__point__dot[data-dot="1"] {
background: black;
}
div.board__point__dot[data-dot="-1"] {
background: white;
}
}
}

View file

@ -45,3 +45,24 @@ code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}
h3 {
font-size: 140%;
font-weight: 600;
}
h4 {
font-size: 110%;
}
a:hover, a:active, {
text-decoration: underline;
}
.--link {
cursor: pointer;
&:hover {
text-decoration: underline;
}
}

View file

@ -1,43 +1,56 @@
import socketIOClient from 'socket.io-client';
import config from './config';
import socketIOClient from "socket.io-client";
import config from "./config";
const launch = (nsp, dispatch) => {
const socket = socketIOClient(`${config.socketAddress}/${nsp}`);
socket.on('connected', () => {
dispatch({ type:'SOCKET', message:'CONNECTED', body:{nsp: socket.nsp} });
});
socket.on('connect_error', err => {
dispatch({ type: 'ERR', message:'SOCKET_ERROR', body: { socketError: err }});
});
socket.on('error', err => {
dispatch({ type: 'ERR', message:'SOCKET_ERROR', body: { socketError: err } });
});
socket.on('room_connected', (data) => {
dispatch({ type: 'ROOMS', message: 'CONNECT_ROOM', body: data });
});
socket.on('new_user', (data) => {
console.log('new_user received')
dispatch({ type: 'ROOMS', message: 'NEW_USER', body: data })
})
socket.on('game_connected', (data) => {
console.log(data)
console.log('game_connected received')
dispatch({ type: 'GAMES', message: 'UPDATE_BOARD', body: data })
})
socket.on('update_board', (data) => {
console.log(data)
console.log('update_board received')
dispatch({ type: 'GAMES', message: 'UPDATE_BOARD', body: data.board })
})
socket.on("connected", () => {
dispatch({
type: "SOCKET",
message: "CONNECTED",
body: { nsp: socket.nsp },
});
});
socket.on("connect_error", (err) => {
dispatch({
type: "ERR",
message: "SOCKET_ERROR",
body: { socketError: err },
});
});
socket.on("error", (err) => {
dispatch({
type: "ERR",
message: "SOCKET_ERROR",
body: { socketError: err },
});
});
socket.on("room_connected", (data) => {
dispatch({ type: "ROOMS", message: "CONNECT_ROOM", body: data });
});
socket.on("new_user", (data) => {
dispatch({ type: "ROOMS", message: "NEW_USER", body: data });
});
socket.on("game_connected", (data) => {
dispatch({ type: "GAMES", message: "UPDATE_BOARD", body: data });
});
socket.on("update_board", (data) => {
dispatch({ type: "GAMES", message: "UPDATE_BOARD", body: data });
});
socket.on("game_resign", (data) => {
dispatch({ type: "GAMES", message: "GAME_RESIGN", body: data });
});
socket.on("end_game", (data) => {
dispatch({ type: "GAMES", message: "GAME_END", body: data });
});
return socket;
}
};
export {
launch
}
export { launch };

View file

@ -1,7 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
<g fill="#61DAFB">
<path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
<circle cx="420.9" cy="296.5" r="45.7"/>
<path d="M520.5 78.1z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

View file

@ -1,71 +1,155 @@
import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import socketIOClient from 'socket.io-client';
import config from '../../config';
import gamesServices from '../../services/api/gamesServices';
import './Game.scss';
import Logo from '../../components/Display/Logo/Logo';
import Board from '../../components/GameUI/Board/Board';
import PlayerArea from '../../components/GameUI/PlayerArea/PlayerArea';
import React, { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import gamesServices from "../../services/api/gamesServices";
import "./Game.scss";
import Logo from "../../components/Display/Logo/Logo";
import Board from "../../components/GameUI/Board/Board";
import PlayerArea from "../../components/GameUI/PlayerArea/PlayerArea";
import Kifu from "../../components/GameUI/Kifu/Kifu";
import Menu from "../../components/GameUI/Menu/Menu";
const Game = (props) => {
const { state, dispatch } = props;
const gameId = parseInt(useParams().id) || 0;
const [playerBlackMeta, setPlayerBlackMeta] = useState({});
const [playerWhiteMeta, setPlayerWhiteMeta] = useState({});
const [showMenu, setShowMenu] = useState(false);
const playerState = state?.meta?.playerState;
const game = state.active?.game;
const fetchGameAPI = async () => {
const response = await gamesServices.getGameService(gameId);
if (response) {
const action = {
type: 'GAMES',
message: 'SET_ACTIVE',
body: response
useEffect(() => {
const fetchGameAPI = async () => {
const response = await gamesServices.getGameService(gameId);
if (response) {
const action = {
type: "GAMES",
message: "SET_ACTIVE",
body: response,
};
return dispatch(action);
}
};
fetchGameAPI();
}, [gameId, dispatch]);
useEffect(() => {
const roomSocketConnect = () => {
const game = state.active.game;
const user = state.user;
const action = {
type: "SOCKET",
message: "CONNECT_GAME",
body: { game, user, dispatch },
};
return dispatch(action);
};
roomSocketConnect();
}, [state.active.game, dispatch, state.user]);
useEffect(() => {
if (!game || !playerState) return;
const { playerBlack, playerBlackRank, playerWhite, playerWhiteRank } = game;
const { bCaptures, wCaptures } = playerState;
setPlayerBlackMeta({
player: playerBlack,
rank: playerBlackRank,
captures: bCaptures,
stones: "black",
});
setPlayerWhiteMeta({
player: playerWhite,
rank: playerWhiteRank,
captures: wCaptures,
stones: "white",
});
}, [playerState, game]);
const handleResignClick = (player) => {
const action = {
type: "SOCKET",
message: "RESIGN",
body: { game, player },
};
dispatch(action);
};
const handlePassClick = (player) => {
if (state?.meta && state?.meta?.winner) return;
if (state?.meta && state?.meta?.turn === 0) {
const action = {
type: "SOCKET",
message: "END_GAME",
body: { game, player },
};
return dispatch(action);
}
}
useEffect(() => {
fetchGameAPI();
}, [])
const roomSocketConnect = () => {
const game = state.active.game;
const user = state.user;
const action = {
type: 'SOCKET',
message: 'CONNECT_GAME',
body: { game, user, dispatch }
}
return dispatch(action);
}
type: "SOCKET",
message: "PASS",
body: { game, player },
};
dispatch(action);
};
useEffect(() => {
roomSocketConnect();
}, [state.active] )
return (
<div
className="Game"
data-testid="Game"
>
return (
<div className="Game" data-testid="Game">
<Menu
showMenu={showMenu}
clickClose={() => setShowMenu(false)}
{...props}
/>
<div className="Game__meta-container">
<span
className="Game__socket-flag"
>{state.socket ? '✓' : ' ⃠'}</span>
<span className="Game__socket-flag">{state.socket ? "✓" : " ⃠"}</span>
<Logo />
{state?.meta?.winner ? (
<p>
{`winner: ${
state.meta.winner === 1
? playerBlackMeta?.player
: playerWhiteMeta?.player
}
`}
</p>
) : (
<></>
)}
<p>Timer</p>
<p>? Game Tree</p>
</div>
<div className="Game__board-container">
<PlayerArea />
<Board
<PlayerArea
handleResignClick={handleResignClick}
handlePassClick={handlePassClick}
playerMeta={
state.user &&
playerBlackMeta.playerBlack &&
state.user === playerBlackMeta.playerBlack
? playerBlackMeta
: playerWhiteMeta
}
turn={state?.meta?.turn}
/>
<Board
dispatch={dispatch}
game={state.active.game}
record={state.active.record}
game={state.active.game}
meta={state.meta}
user={state.user}
board={state.board}
/>
<PlayerArea />
<PlayerArea
handleResignClick={handleResignClick}
handlePassClick={handlePassClick}
playerMeta={
state.user &&
playerBlackMeta.playerWhite &&
state.user === playerWhiteMeta.playerWhite
? playerWhiteMeta
: playerBlackMeta
}
Kifu={<Kifu clickKifu={() => setShowMenu(true)} />}
turn={state?.meta?.turn}
/>
</div>
<div className="Game__message-container">
@ -74,6 +158,6 @@ const Game = (props) => {
</div>
</div>
);
}
};
export default Game;
export default Game;

View file

@ -1,4 +1,4 @@
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import './Home.scss';
import roomsServices from '../../services/api/roomsServices';
import RoomButton from '../../components/Button/Room/Room';
@ -8,6 +8,9 @@ import Loading from '../../components/Display/Loading/Loading';
const Home = props => {
const state = props.state || {};
const dispatch = props.dispatch;
const [ roomDetail, setRoomDetail ] = useState(1);
const showRoomDetail = id => roomDetail === id ? setRoomDetail(0) : setRoomDetail(id);
const renderRooms = () => {
const rooms = state.rooms || [];
@ -16,6 +19,8 @@ const Home = props => {
<RoomButton
key={`room-${roomData.id}`}
room={roomData}
roomDetail={roomDetail === roomData.id}
showRoomDetail={showRoomDetail}
/>
))
}
@ -23,21 +28,21 @@ const Home = props => {
return <Loading />
}
const fetchRoomsAPI = async () => {
const response = await roomsServices.indexService();
if (response) {
const action = {
type: 'ROOMS',
message: 'SET_ROOMS',
body: response.rooms
}
return dispatch(action)
}
}
useEffect(() => {
const fetchRoomsAPI = async () => {
const response = await roomsServices.indexService();
if (response) {
const action = {
type: 'ROOMS',
message: 'SET_ROOMS',
body: response.rooms
}
return dispatch(action)
}
}
fetchRoomsAPI();
}, [])
}, [ dispatch ])
return (
<div className="Home" data-testid="Home">

View file

@ -3,10 +3,9 @@
div.Home {
@include fullspan;
display: flex;
flex-flow: row wrap;
align-items: flex-start;
justify-content: flex-start;
background-color: map-get($colors, "home");
display: grid;
grid-template-columns: repeat(auto-fill, 22vh);
grid-template-rows: repeat(auto-fill, minmax(22vh, 1fr));
overflow: scroll;
}

View file

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React from 'react';
import Auth from '../../components/Form/Auth/Auth';
import NewRoomButton from '../../components/Button/NewRoom/NewRoom';
@ -14,7 +14,7 @@ const HomeSidebar = (props) => {
<LibraryButton />
</>
const ifNoUser = <Auth state={state} dispatch={state} />
const ifNoUser = <Auth state={state} dispatch={dispatch} />
return (
<nav>

View file

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React from 'react';
import './MainWrapper.scss';
import NavBar from '../NavBar/NavBar';

View file

@ -1,4 +1,5 @@
@import '../../../../public/stylesheets/partials/mixins';
@import '../../../../public/stylesheets/partials/variables';
div.main-wrapper {
display: flex;
@ -33,4 +34,15 @@ div.main-wrapper {
height: 100%;
}
}
}
button {
color:map-get($colors, 'sidebar_link');
cursor: pointer;
font-family: inherit;
font-size: 110%;
font-weight: bold;
margin-bottom: .5em;
padding: 0;
text-decoration: none;
}

View file

@ -1,29 +1,36 @@
import React from 'react';
import { Link } from 'react-router-dom';
import './NavBar.scss';
import Logo from '../../../components/Display/Logo/Logo';
const NavBar = (props) => {
import React from "react";
import { Link } from "react-router-dom";
import "./NavBar.scss";
import Logo from "../../../components/Display/Logo/Logo";
const NavBar = ({ state }) => {
return (
<div className="NavBar" data-testid="NavBar">
<Link to="/home" >
<div className="NavBar__logo"><Logo /></div>
<Link to="/home">
<div className="NavBar__logo">
<Logo />
</div>
</Link>
<Link to="/home" >
<div className="NavBar__menu-item NavBar__home"><p>Find a Game</p></div>
<Link to="/home">
<div className="NavBar__menu-item NavBar__home">
<p className="--link">Find a Game</p>
</div>
</Link>
<Link to="/news">
<div className="NavBar__menu-item NavBar__news"><p>News</p></div>
<div className="NavBar__menu-item NavBar__news">
<p className="--link">News</p>
</div>
</Link>
<Link to="/account">
<div className="NavBar__menu-item NavBar__acount">{props.user ? props.user.username : <></>}</div>
<div className="NavBar__menu-item NavBar__account">
{state.user.username ? state.user.username : <></>}
</div>
</Link>
</div>
);
}
};
export default NavBar;
export default NavBar;

View file

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React from 'react';
import './Sidebar.scss';
import AccountSidebar from '../../Account/AccountSidebar';

View file

@ -1,8 +1,6 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect } from 'react';
import { useParams } from 'react-router-dom';
import './Room.scss';
import socketIOClient from 'socket.io-client';
import config from '../../config';
import roomsServices from '../../services/api/roomsServices';
import GameButton from '../../components/Button/Game/Game';
import Message from '../../components/Display/Message/Message';
@ -15,38 +13,33 @@ const Room = (props) => {
const { state, dispatch } = props;
const roomId = parseInt(useParams().id) || 0;
const fetchRoomAPI = async () => {
const response = await roomsServices.getRoomService(roomId);
if (response) {
const action = {
type: 'ROOMS',
message: 'JOIN_ROOM',
body: response
}
return dispatch(action);
}
}
useEffect(() => {
fetchRoomAPI();
}, [])
// ! [start] roomSocket
const roomSocketConnect = () => {
const action = {
type: 'SOCKET',
message: 'CONNECT_ROOM',
body: {user: state.user, room: roomId, dispatch}
const fetchRoomAPI = async () => {
const response = await roomsServices.getRoomService(roomId);
if (response) {
const action = {
type: 'ROOMS',
message: 'JOIN_ROOM',
body: response
}
return dispatch(action);
}
}
dispatch(action)
}
fetchRoomAPI();
}, [ roomId, dispatch ])
useEffect(() => {
const roomSocketConnect = () => {
const action = {
type: 'SOCKET',
message: 'CONNECT_ROOM',
body: {user: state.user, room: roomId, dispatch}
}
dispatch(action)
}
roomSocketConnect();
}, [])
// ! [end]
}, [ roomId, state.user, dispatch ])
const renderGames = () => {
const games = state.games || [];

View file

@ -1,23 +1,23 @@
// @flow
import type { state, action } from '../reducer';
export const authReducer = (state: state, action: action):state => {
export const authReducer = (state, action) => {
switch (action.message) {
case 'LOGIN':
return loginReducer(state, action);
case 'SIGNUP':
case "LOGIN":
return loginReducer(state, action);
case 'LOGOUT':
case "SIGNUP":
return loginReducer(state, action);
case "GUEST":
return loginReducer(state, action);
case "LOGOUT":
return state;
default:
return state;
}
}
}
};
function loginReducer(state: state, action: action): state {
function loginReducer(state, action) {
const newUser = action.body;
return {...state, user: newUser };
}
return { ...state, user: newUser };
}

View file

@ -1,7 +1,4 @@
// @flow
import type { state, action } from '../reducer';
export const errorReducer = (state: state, action: action):state => {
export const errorReducer = (state, action) => {
switch (action.message) {
case 'AUTH_ERROR':
return authErrorReducer(state, action);
@ -17,17 +14,17 @@ export const errorReducer = (state: state, action: action):state => {
}
}
function authErrorReducer(state: state, action: action): state {
function authErrorReducer(state, action) {
const auth = action.body.authError;
return {...state, errors: {auth} };
}
function joinRoomErrorReducer(state: state, action: action): state {
function joinRoomErrorReducer(state, action) {
const joinRoom = action.body.joinRoomError;
return { ...state, errors: {joinRoom} }
}
function joinGameErrorReducer(state: state, action: action): state {
function joinGameErrorReducer(state, action) {
const joinGame = action.body.joinGameError;
return { ...state, errors: {joinGame} }
}

View file

@ -1,62 +1,104 @@
// @flow
import type { state, action } from '../reducer';
import { stateReducer } from '../reducer';
import { stateReducer } from "../reducer";
export const gamesReducer = (state: state, action: action):state => {
switch(action.message) {
export const gamesReducer = (state, action) => {
switch (action.message) {
case "SET_GAMES": {
const games = formatGames(action);
return { ...state, games };
}
case 'SET_GAMES':
const games = formatGames(action);;
return {...state, games};
case 'JOIN_REQUEST':
if (!Object.entries(state.user).length) {
const errAction = {
type: 'ERR',
message: 'JOIN_GAME_ERROR',
body: {joinGameError: 'user not logged in'}
}
return stateReducer(state, errAction)
}
const id = action.body;
return {...state, joinGame: id};
case "JOIN_REQUEST": {
return joinRequest(state, action);
}
case 'UPDATE_BOARD':
console.log(action.body)
return {...state, board: action.body};
case "UPDATE_BOARD": {
return updateBoard(state, action);
}
case 'SET_ACTIVE':
return {...state, active: action.body};
default:
case "GAME_RESIGN": {
return gameResign(state, action);
}
case "SET_ACTIVE": {
return { ...state, active: action.body };
}
case "GAME_END": {
return gameEnd(state, action);
}
default: {
return state;
}
}
};
// parse ranks from db in K9 format to 9k format
function parseRank(rank) {
switch (rank[0]) {
// Dan ranks
case "D":
return `${rank.slice(1)}${rank[0].toLowerCase()}`;
// Kyu ranks
case "K":
return `${rank.slice(1)}${rank[0].toLowerCase()}`;
// Unranked
case "U":
return "?";
default:
return "?";
}
}
function parseRank(rank: string): string {
switch(rank[0]) {
case 'D':
return `${rank.slice(1)}${rank[0].toLowerCase()}`
case 'K':
return `${rank.slice(1)}${rank[0].toLowerCase()}`
case 'U':
return '?'
}
}
function formatGames(action: action): Array<{}> {
const games = [...action.body].map(game => {
function formatGames(action) {
const games = [...action.body].map((game) => {
if (game.playerBlackRank) {
game.playerBlackRank = parseRank(game.playerBlackRank)
game.playerBlackRank = parseRank(game.playerBlackRank);
}
if (game.playerWhiteRank) {
game.playerWhiteRank = parseRank(game.playerWhiteRank)
game.playerWhiteRank = parseRank(game.playerWhiteRank);
}
return game;
})
});
return games;
}
}
function joinRequest(state, action) {
if (!Object.entries(state.user).length) {
const errAction = {
type: "ERR",
message: "JOIN_GAME_ERROR",
body: { joinGameError: "user not logged in" },
};
return stateReducer(state, errAction);
}
const id = action.body;
return { ...state, joinGame: id };
}
function updateBoard(state, action) {
console.log(action.body);
const { gameRecord, pass, turn, winner, playerState } = action.body.meta;
const territory = action.body.territory;
return {
...state,
board: action.body.board,
meta: { gameRecord, pass, turn, winner, playerState, territory },
};
}
function gameResign(state, action) {
const { gameRecord, pass, turn, winner, playerState } = action.body;
return {
...state,
meta: { gameRecord, pass, turn, winner, playerState },
};
}
function gameEnd(state, action) {
const { winner, score } = action.body.meta;
return { ...state, meta: { ...state.meta, winner, score } };
}

View file

@ -1,7 +1,4 @@
// @flow
import type { state, action } from '../reducer';
export const indexReducer = (state: state, action: action):state => {
export const indexReducer = (state, action) => {
switch(action.message) {
case 'SET_USER':

View file

@ -1,9 +1,4 @@
//@ flow
import type { state } from '../reducer';
const socket = require('../../io');
export const initState = (): state => {
export const initState = () => {
return {
active: {
game: {

View file

@ -1,8 +1,4 @@
// @flow
import type { state, action } from '../reducer';
import { stateReducer } from '../reducer';
export const messagesReducer = (state: state, action: action):state => {
export const messagesReducer = (state, action) => {
switch(action.message) {
case 'SET_MESSAGES':

View file

@ -1,4 +1,3 @@
// @flow
import { initState } from './init/reducer.init';
import { authReducer } from './auth/reducer.auth';
import { errorReducer } from './err/reducer.err';
@ -8,20 +7,8 @@ import { messagesReducer } from './messages/reducer.messages';
import { gamesReducer } from './games/reducer.games';
import { socketReducer } from './socket/reducer.socket';
export type state = {
user: {},
errors: {},
messages: [],
state: {}
}
export type action = {
type: string,
message: ?string,
body: {} | Array<{}>,
}
export const stateReducer = (state: state, action: action): state => {
export const stateReducer = (state, action) => {
const errorStrippedState = stripErrors({...state});
switch (action.type) {
@ -52,6 +39,6 @@ export const stateReducer = (state: state, action: action): state => {
}
}
function stripErrors(state: state): state {
function stripErrors(state) {
return {...state, errors: {}}
}

View file

@ -1,4 +1,4 @@
import {stateReducer} from './reducer';
import { stateReducer } from './reducer';
it('default returns state unaltered', () => {
const state = {data: 'example'};

View file

@ -1,8 +1,6 @@
// @flow
import type { state, action } from '../reducer';
import { stateReducer } from '../reducer';
export const roomsReducer = (state: state, action: action):state => {
export const roomsReducer = (state, action) => {
switch(action.message) {
case 'SET_ROOMS':

View file

@ -1,71 +1,105 @@
// @flow
import type { state, action } from '../reducer';
import { stateReducer } from '../reducer';
const io = require('../../io');
import { stateReducer } from "../reducer";
const io = require("../../io");
export const socketReducer = (state: state, action: action):state => {
switch(action.message) {
export const socketReducer = (state, action) => {
switch (action.message) {
case "CONNECTED":
return { ...state, connect: { type: "home", location: action.body.nsp } };
case 'CONNECTED':
console.log(action.body.nsp)
return {...state, connect: { type: 'home', location: action.body.nsp } }
case 'LAUNCH': {
const {nsp, dispatch} = action.body;
case "LAUNCH": {
const { nsp, dispatch } = action.body;
const launchedSocket = io.launch(nsp, dispatch);
return {...state, socket: launchedSocket};
return { ...state, socket: launchedSocket };
}
case 'CONNECT_ROOM': {
const {user, room, dispatch} = action.body;
case "CONNECT_ROOM": {
const { user, room, dispatch } = action.body;
let priorSocket = state.socket;
if (!priorSocket.nsp) {
priorSocket = io.launch('', dispatch)
priorSocket = io.launch("", dispatch);
}
if (priorSocket.nsp !== `/${room}`) {
priorSocket.emit('connect_room', {user, room});
priorSocket.emit("connect_room", { user, room });
priorSocket.close();
}
const socket = io.launch(room, dispatch);
return {...state, socket}
return { ...state, socket };
}
case 'CONNECT_GAME': {
case "CONNECT_GAME": {
return connectGame(state, action);
}
case 'MAKE_MOVE': {
case "MAKE_MOVE": {
return makeMove(state, action);
}
case "RESIGN": {
return resign(state, action);
}
case "PASS": {
return pass(state, action);
}
case "TOGGLE_TERRITORY": {
return toggleTerritory(state, action);
}
case "END_GAME": {
return endGame(state, action);
}
default:
return state;
}
}
};
function connectGame (state, action) {
function connectGame(state, action) {
const { user, game, dispatch } = action.body;
const priorSocket = state.socket;
let updatedState;
if ( !priorSocket.nsp || priorSocket.nsp !== `/${game.room}` ) {
if (!priorSocket.nsp || priorSocket.nsp !== `/${game.room}`) {
const connectRoomAction = {
type: 'SOCKET',
message: 'CONNECT_ROOM',
body: { user, room: game.room, dispatch}
}
type: "SOCKET",
message: "CONNECT_ROOM",
body: { user, room: game.room, dispatch },
};
updatedState = stateReducer(state, connectRoomAction);
}
if (!updatedState) updatedState = {...state};
if (!updatedState) updatedState = { ...state };
const socket = updatedState.socket;
socket.emit('connect_game', {user, game});
return {...updatedState};
socket.emit("connect_game", { user, game });
return { ...updatedState };
}
function makeMove (state, action) {
const { user, game, room, board, move } = action.body;
function makeMove(state, action) {
const socket = state.socket;
console.log(action)
socket.emit('make_move', {...action.body});
socket.emit("make_move", { ...action.body });
return state;
}
}
function resign(state, action) {
const socket = state.socket;
socket.emit("resign", { ...action.body });
return state;
}
function pass(state, action) {
const socket = state.socket;
socket.emit("pass", { ...action.body });
return state;
}
function toggleTerritory(state, action) {
const socket = state.socket;
socket.emit("toggle_territory", { ...action.body });
return state;
}
function endGame(state, action) {
console.log("end game");
const socket = state.socket;
socket.emit("end_game", { ...action.body });
return state;
}

View file

@ -1,43 +1,58 @@
import config from '../config';
import config from "../config";
const authEndpoint = config.authAddress;
const signupEndpoint = `${authEndpoint}/signup`
const loginEndpoint = `${authEndpoint}/login`
const signupEndpoint = `${authEndpoint}/signup`;
const loginEndpoint = `${authEndpoint}/login`;
const guestEndpoint = `${authEndpoint}/guest`;
var headers = new Headers();
headers.append('Content-Type', 'application/json');
headers.append('Accept', 'application/json');
headers.append('Sec-Fetch-Site', 'cross-site')
headers.append("Content-Type", "application/json");
headers.append("Accept", "application/json");
headers.append("Sec-Fetch-Site", "cross-site");
const loginService = async(formData) => {
const loginService = async (formData) => {
const response = await fetch(loginEndpoint, {
method: 'POST',
credentials: 'include',
method: "POST",
credentials: "include",
body: JSON.stringify(formData),
headers: headers
headers: headers,
})
.then(res => res.text())
.then(text => JSON.parse(text))
.catch(err => err);
.then((res) => res.text())
.then((text) => JSON.parse(text))
.catch((err) => err);
return response;
}
};
const signupService = async (formData) => {
const response = await fetch(signupEndpoint, {
method: 'POST',
credentials: 'include',
method: "POST",
credentials: "include",
body: JSON.stringify(formData),
headers: headers
headers: headers,
})
.then(res => res.text())
.then(text => JSON.parse(text))
.catch(err => err);
.then((res) => res.text())
.then((text) => JSON.parse(text))
.catch((err) => err);
return response;
}
};
const guestService = async () => {
const response = await fetch(guestEndpoint, {
method: "POST",
credentials: "include",
headers,
})
.then((res) => res.text())
.then((text) => JSON.parse(text))
.catch((err) => err);
return response;
};
export default {
loginService,
signupService
}
signupService,
guestService,
};

View file

@ -10,8 +10,9 @@ const show = async (req, res, next) => {
// TODO Promise.all()
const game = await gameQueries.findGameById(gameId);
const record = await moveQueries.findGameRecord(gameId);
res.status(200).json({game, record})
// const record = await moveQueries.findGameRecord(gameId);
// console.log(record)
res.status(200).json({game})
}
catch (err) {
res.status(500).json(err);

View file

@ -10,8 +10,9 @@ const getAll = async (req, res, next) => {
res.status(200).json({rooms: [...publicRooms]})
}
catch (err) {
res.status(500).json(err);
catch (e) {
console.log(e)
res.status(500).json(e);
}
}
@ -28,9 +29,9 @@ const show = async (req, res, next) => {
const body = {currentRoom, messages, roomGames};
res.status(200).json(body);
}
catch (err) {
console.log(err)
res.status(500).json(err);
catch (e) {
console.log(e)
res.status(500).json(e);
}
}

View file

@ -1,15 +1,16 @@
const { validationResult } = require('express-validator');
const { validationResult } = require("express-validator");
const userQueries = require('../data/queries/user');
const { hashPassword, compareHash } = require('../services/bcrypt');
const signToken = require('../services/signToken');
const userQueries = require("../data/queries/user");
const { hashPassword, compareHash } = require("../services/bcrypt");
const signToken = require("../services/signToken");
const guestServices = require("../services/guestServices");
const checkValidationErrors = (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(422).json({ errors: errors.array() });
}
}
};
const signup = async (req, res, next) => {
checkValidationErrors(req, res);
@ -20,51 +21,68 @@ const signup = async (req, res, next) => {
const hashedPassword = await hashPassword(user.password);
const secureUser = { ...user, password: hashedPassword };
if (existingUser.length) {
return res.status(409).json({errors: [{auth: 'User already exists!'}]})
return res
.status(409)
.json({ errors: [{ auth: "User already exists!" }] });
}
const newUser = await userQueries.insertUser(secureUser);
signToken(res, newUser)
res.status(201).json({...newUser});
}
catch (err) {
signToken(res, newUser);
res.status(201).json({ ...newUser });
} catch (err) {
res.status(500).json(err);
}
}
};
const login = async (req, res, next) => {
checkValidationErrors(req, res);
const user = req.body;
try {
const queryResults = await userQueries.findUserByNameOrEmail(user);
const savedUser = queryResults[0] || null;
if (!savedUser) {
return res.status(401).send({errors: 'bad credentials'});
return res.status(401).send({ errors: "bad credentials" });
}
const hashedPassword = savedUser.password;
const passwordMatch = await compareHash(user.password, hashedPassword);
if (!passwordMatch) {
return res.status(401).send({errors: 'bad credentials'});
return res.status(401).send({ errors: "bad credentials" });
}
const authorizedUser = {...savedUser};
const authorizedUser = { ...savedUser };
delete authorizedUser.password;
signToken(res, authorizedUser);
res.send({...authorizedUser}).status(200);
res.send({ ...authorizedUser }).status(200);
} catch (e) {
res.status(500).send({ errors: e });
}
catch (err) {
res.status(500).send({errors: err});
};
const guest = async (req, res, next) => {
try {
// username generator returns `Guest-${num}`
const { username, password } = guestServices.generateGuest();
// generateGuestUser();
const email = null;
// id generator returns `
const id = null;
const user = { username, email, id, password };
signToken(res, user);
delete user.password;
res.send(user);
} catch (e) {
console.log(e);
res.status(500).send({ errors: e });
}
}
};
module.exports = {
signup,
login
}
login,
guest,
};

View file

@ -1,49 +1,81 @@
const winType = [
'B+R', 'B+', 'B+T',
'W+R', 'W+', 'W+T',
'0', 'Void', '?'
]
const winType = ["B+R", "B+", "B+T", "W+R", "W+", "W+T", "0", "Void", "?"];
const rankArray = [
'D9', 'D8', 'D7', 'D6', 'D5', 'D4', 'D3', 'D2', 'D1',
'K1', 'K2', 'K3', 'K4', 'K5', 'K6', 'K7', 'K8', 'K9', 'K10',
'K11', 'K12', 'K13', 'K14', 'K15', 'K16', 'K17', 'K18', 'K19', 'K20',
'K21', 'K22', 'K23', 'K24', 'K25', 'K26', 'K27', 'K28', 'K29', 'K30', 'UR'
]
"D9",
"D8",
"D7",
"D6",
"D5",
"D4",
"D3",
"D2",
"D1",
"K1",
"K2",
"K3",
"K4",
"K5",
"K6",
"K7",
"K8",
"K9",
"K10",
"K11",
"K12",
"K13",
"K14",
"K15",
"K16",
"K17",
"K18",
"K19",
"K20",
"K21",
"K22",
"K23",
"K24",
"K25",
"K26",
"K27",
"K28",
"K29",
"K30",
"UR",
];
exports.up = function(knex) {
return knex.schema.createTable("game", table => {
table.increments('id').primary();
table.datetime('date');
table.float('komi').default(6.5);
table.integer('handicap').default(0);
table.integer('board_size').default(19);
table.boolean('open').default(true);
exports.up = function (knex) {
return knex.schema.createTable("game", (table) => {
table.increments("id").primary();
table.datetime("date");
table.float("komi").default(6.5);
table.integer("handicap").default(0);
table.integer("board_size").default(19);
table.boolean("open").default(true);
table.string('application');
table.string('application_version');
table.string("application");
table.string("application_version");
table.timestamps(true, true);
table.string('player_black');
table.string('player_white');
table.enu('player_black_rank', rankArray).default('UR');
table.enu('player_white_rank', rankArray).default('UR');
table.string('event');
table.string('name');
table.string('description');
table.integer('round');
table.enu('win_type', winType);
table.float('score');
table.integer('captures_black');
table.integer('captures_white');
table.string("player_black");
table.string("player_white");
table.enu("player_black_rank", rankArray).default("UR");
table.enu("player_white_rank", rankArray).default("UR");
table.integer('user_black').references('id').inTable('user');
table.integer('user_white').references('id').inTable('user');
table.integer('room').references('id').inTable('room');
table.integer('time_setting').references('id').inTable('time_setting');
})
table.string("event");
table.string("name");
table.string("description");
table.integer("round");
table.enu("win_type", winType);
table.float("score");
table.integer("captures_black");
table.integer("captures_white");
table.integer("user_black").references("id").inTable("user");
table.integer("user_white").references("id").inTable("user");
table.integer("room").references("id").inTable("room");
table.integer("time_setting").references("id").inTable("time_setting");
});
};
exports.down = knex => knex.schema.dropTableIfExists("game");
exports.down = (knex) => knex.schema.dropTableIfExists("game");

View file

@ -1,17 +1,18 @@
const players = ['white', 'black']
const players = ["white", "black"];
exports.up = knex => {
return knex.schema.createTable("move", table => {
table.increments('id').primary();
table.enu('player', players).notNullable();
table.integer('point_x').notNullable();
table.integer('point_y').notNullable();
table.integer('number').notNullable();
table.boolean('game_record').notNullable().default(true);
exports.up = (knex) => {
return knex.schema.createTable("move", (table) => {
table.increments("id").primary();
table.enu("player", players).notNullable();
table.integer("point_x").notNullable();
table.integer("point_y").notNullable();
table.integer("number").notNullable();
table.boolean("game_record").notNullable().default(true);
table.boolean("placement").notNullable().default(false);
table.integer('game').references('id').inTable('game').notNullable();
table.integer('prior_move').references('id').inTable('move');
table.integer("game").references("id").inTable("game").notNullable();
table.integer("prior_move").references("id").inTable("move");
});
};
exports.down = knex => knex.schema.dropTableIfExists("move");
exports.down = (knex) => knex.schema.dropTableIfExists("move");

View file

@ -1,48 +1,97 @@
const knex = require('../db');
const knex = require("../db");
const gameDetailSelect = [
'game.id', 'application', 'application_version',
'board_size', 'komi', 'handicap', 'open', 'win_type',
'player_black', 'player_black_rank', 'player_white', 'player_white_rank',
'captures_black', 'captures_white', 'score', 'win_type',
'description', 'event', 'round', 'name', 'room'
]
"game.id",
"application",
"application_version",
"board_size",
"komi",
"handicap",
"open",
"win_type",
"player_black",
"player_black_rank",
"player_white",
"player_white_rank",
"captures_black",
"captures_white",
"score",
"win_type",
"description",
"event",
"round",
"name",
"room",
];
const timeSettingSelect = [
'main_time', 'time_period', 'period_length', 'overtime', 'overtime_period', 'overtime_length'
]
"main_time",
"time_period",
"period_length",
"overtime",
"overtime_period",
"overtime_length",
];
const gameOverviewSelect = [
'id', 'board_size', 'komi', 'handicap', 'open', 'win_type',
'player_black', 'player_black_rank', 'player_white', 'player_white_rank'
]
"id",
"board_size",
"komi",
"handicap",
"open",
"win_type",
"player_black",
"player_black_rank",
"player_white",
"player_white_rank",
];
const findGameById = async function (gameId) {
const selection = gameDetailSelect.concat(timeSettingSelect);
const game = await knex
.from('game')
.select(selection)
.where({'game.id': gameId})
.leftJoin('time_setting', function() { this.on('time_setting.id', '=', 'game.time_setting')})
.from("game")
.select(selection)
.where({ "game.id": gameId })
.leftJoin("time_setting", function () {
this.on("time_setting.id", "=", "game.time_setting");
});
return game[0];
}
};
const findGameByRoom = async (roomId) => {
const games = await knex('game')
.where({'room': roomId})
.select(gameOverviewSelect);
const games = await knex("game")
.where({ room: roomId })
.select(gameOverviewSelect);
return games;
}
};
const insertGame = async (game) => {
const insertGame = async (game) => {};
}
const endGame = async ({ id, winType, score, bCaptures, wCaptures }) => {
try {
const game = await knex
.from("game")
.returning(gameDetailSelect)
.where({ id: id })
.update({
win_type: winType,
score: score,
captures_black: bCaptures,
captures_white: wCaptures,
open: false,
});
return game;
} catch (e) {
return e;
}
};
module.exports = {
findGameById,
findGameByRoom,
insertGame
}
insertGame,
endGame,
};

View file

@ -1,11 +1,47 @@
const knex = require('../db');
const knex = require("../db");
const findGameRecord = async (gameId) => {
return await knex('move')
.where({'game': gameId, 'game_record': true})
.select('*');
}
return await knex("move")
.where({ game: gameId, game_record: true })
.select("player", "point_x", "point_y", "number", "prior_move", "placement")
.orderBy("number")
.then((record) =>
record.map(({ player, point_x, point_y }) => ({
player,
pos: { x: point_x, y: point_y },
}))
);
// .then(res => res)
};
// id: 1, player: 'black', point_x: 3, point_y: 3, number: 1, game_record: true, game: 1, prior_move: null
const addMove = async ({ gameId, player, x, y, gameRecord, priorMove }) => {
// ! priorMove must be FK not move number
const number = priorMove + 1;
let result;
try {
result = await knex("move")
.returning("*")
.insert({
game: gameId,
player,
point_x: x,
point_y: y,
number,
game_record: gameRecord,
prior_move: priorMove,
})
.then((res) => res);
} catch (e) {
result = e;
} finally {
console.log(result);
return result;
}
};
module.exports = {
findGameRecord
}
findGameRecord,
addMove,
};

View file

@ -5,8 +5,8 @@ exports.seed = function(knex) {
.then(function () {
// Inserts seed entries
return knex('room').insert([
{id: 1, name: 'main', description: 'A general place to play Go'},
{id: 2, name: 'private', description: 'A private place to play Go', private: true},
{name: 'main', description: 'A general place to play Go'},
{name: 'private', description: 'A private place to play Go', private: true},
]);
});
};

View file

@ -5,7 +5,7 @@ exports.seed = function(knex) {
.then(function () {
// Inserts seed entries
return knex('time_setting').insert([
{id: 1, main_time: 'untimed', time_period: 1, period_length: 0, overtime: 'untimed', overtime_period: 0, overtime_length: 0},
{main_time: 'untimed', time_period: 1, period_length: 0, overtime: 'untimed', overtime_period: 0, overtime_length: 0},
]);
});
};

View file

@ -10,9 +10,9 @@ exports.seed = async function(knex) {
.then(async function () {
const hashedPassword = await hashPassword(password);
// Inserts seed entries
return knex('user').insert([
{id: 2, username: 'user-one', email: email, password: hashedPassword, admin: true},
{id: 3, username: 'user-two', email: `2${email}`, password: hashedPassword, admin: true},
]);
return knex('user').returning('*').insert([
{username: 'user-one', email: email, password: hashedPassword, admin: true},
{username: 'user-two', email: `2${email}`, password: hashedPassword, admin: true},
]).then(entries => console.log({success: 'user', entries}));
});
};

View file

@ -1,33 +1,72 @@
exports.seed = function(knex) {
exports.seed = async function (knex) {
// Deletes ALL existing entries
return knex('game').del()
.then(function () {
return await knex("game")
.del()
.then(async function () {
// Inserts seed entries
return knex('game').insert([
{
id: 1, date: new Date(),
application: 'node-go', application_version: '0.1.0',
player_black: 'user-one', player_white: 'user-two',
player_black_rank: 'UR', player_white_rank: 'UR',
user_black: 2, user_white: 3,
room: 1, time_setting: 1, open: false
},
{
id: 2, date: new Date(),
application: 'node-go', application_version: '0.1.0',
player_black: 'user-one', player_black_rank: 'UR',
user_black: 2,
room: 1, time_setting: 1, open: true
},
{
id: 3, date: new Date('1971-05-06'),
application: 'node-go', application_version: '0.1.0',
player_black: 'Ishida Yoshio', player_black_rank: 'D7',
player_white: 'Rin Kaiho', player_white_rank: 'D9',
room: 1, time_setting: 1, open: false,
event: '', round: 2, win_type: 'B+', score: 1.5
}
]);
await knex("user")
.select("id")
.orderBy("id")
.whereIn("username", ["user-one", "user-two"])
.then(async ([userOne, userTwo]) => {
const res = await knex("room")
.select("id")
.where({ name: "main" })
.then(([room]) => {
console.log("inserting");
return knex("game")
.insert(
[
{
date: new Date(),
application: "node-go",
application_version: "0.1.0",
player_black: "user-one",
player_white: "user-two",
player_black_rank: "UR",
player_white_rank: "UR",
user_black: userOne.id,
user_white: userTwo.id,
room: room.id,
open: false,
},
{
date: new Date(),
application: "node-go",
application_version: "0.1.0",
player_black: "user-one",
player_black_rank: "UR",
user_black: userTwo.id,
room: room.id,
open: true,
},
{
date: new Date("1971-05-06"),
application: "node-go",
application_version: "0.1.0",
player_black: "Ishida Yoshio",
player_black_rank: "D7",
player_white: "Rin Kaiho",
player_white_rank: "D9",
room: room.id,
open: false,
event: "",
round: 2,
win_type: "B+",
score: 1.5,
},
],
["*"]
)
.then((res) => res)
.catch((e) => {
console.log("error");
console.log(e);
});
})
.then((entries) => {
console.log({ success: "game", entries });
});
});
});
};
};

View file

@ -1,11 +1,27 @@
exports.seed = function(knex) {
exports.seed = async function(knex) {
// Deletes ALL existing entries
return knex('message').del()
.then(function () {
return await knex('message').del()
.then(async function () {
// Inserts seed entries
return knex('message').insert([
{id: 1, content: 'Hey! Welcome to the general room!', room: 1, user: 2}
]);
});
await knex('room')
.where({name: 'main'})
.then(async ([room]) => await knex('user')
.where({username: 'user-two'})
.then(async ([user]) => {
const res = await knex('message')
.returning('*')
.insert(
[
{content: 'Hey! Welcome to the general room!', room: room.id, user: user.id}
]
)
.then(entries => {console.log({success: 'message', entries}); return res;})
.catch(e => e)
return res;
}
)
.then(() => {})
).then(() => {})
}).then(() => {})
};

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
const {check, sanitize, validationResult} = require('express-validator');
const {check, body, validationResult} = require('express-validator');
const signupValidationRules = () => {
return [
@ -7,7 +7,7 @@ const signupValidationRules = () => {
check('password', 'invalid password').isString().isLength({min: 8}),
check('confirmPassword', 'invalid password').isString()
.custom((confirmPassword, { req }) => confirmPassword === req.body.password),
sanitize('username').escape()
body('username').escape()
]
}

File diff suppressed because it is too large Load diff

View file

@ -13,7 +13,7 @@
"reset-db": "./node_modules/.bin/knex migrate:rollback true && npm run migrate && npm run seed"
},
"dependencies": {
"bcrypt": "^3.0.7",
"bcrypt": "^4.0.1",
"cookie-parser": "~1.4.4",
"cors": "^2.8.5",
"debug": "~2.6.9",
@ -22,9 +22,9 @@
"express-validator": "^6.3.1",
"http-errors": "~1.6.3",
"jsonwebtoken": "^8.5.1",
"knex": "^0.20.7",
"knex": "^0.21.1",
"morgan": "~1.9.1",
"pg": "^7.17.0",
"pg": "^8.1.0",
"socket.io": "^2.3.0"
}
}

View file

@ -1,10 +1,20 @@
const express = require('express');
const express = require("express");
const router = express.Router();
const app = require('../server');
const authController = require('../controllers/auth');
const { signupValidationRules, loginValidationRules, validate } = require('../middleware/userValidator');
const app = require("../server");
const authController = require("../controllers/auth");
const {
signupValidationRules,
loginValidationRules,
validate,
} = require("../middleware/userValidator");
router.post('/signup', signupValidationRules(), validate, authController.signup);
router.post('/login', loginValidationRules(), validate, authController.login);
router.post(
"/signup",
signupValidationRules(),
validate,
authController.signup
);
router.post("/login", loginValidationRules(), validate, authController.login);
router.post("/guest", authController.guest);
module.exports = router;

View file

@ -1,21 +1,21 @@
const createError = require('http-errors');
const express = require('express');
const createError = require("http-errors");
const express = require("express");
const cors = require('cors');
const cors = require("cors");
const path = require('path');
const cookieParser = require('cookie-parser');
const logger = require('morgan');
const path = require("path");
const cookieParser = require("cookie-parser");
const logger = require("morgan");
const db = require('./data/db');
const db = require("./data/db");
const dotenv = require('dotenv');
const dotenv = require("dotenv");
dotenv.config();
const indexRouter = require('./routes/index');
const usersRouter = require('./routes/users');
const authRouter = require('./routes/auth');
const apiRouter = require('./routes/api');
const indexRouter = require("./routes/index");
const usersRouter = require("./routes/users");
const authRouter = require("./routes/auth");
const apiRouter = require("./routes/api");
const app = express();
@ -23,40 +23,40 @@ const allowedOrigin = process.env.REACT_ADDRESS;
const corsOptions = {
origin: allowedOrigin,
credentials: true,
methods: "GET,PUT,POST,DELETE"
}
methods: "GET,PUT,POST,DELETE",
};
app.options('*', cors(corsOptions));
app.use('*', cors(corsOptions));
app.options("*", cors(corsOptions));
app.use("*", cors(corsOptions));
// disable logging for tests
if (process.env.NODE_ENV !== 'test') app.use(logger('dev'));
if (process.env.NODE_ENV !== "test") app.use(logger("dev"));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.static(path.join(__dirname, "public")));
app.use('/', indexRouter);
app.use('/users', usersRouter);
app.use('/auth', authRouter);
app.use("/", indexRouter);
app.use("/users", usersRouter);
app.use("/auth", authRouter);
// @auth
app.use('/api/v1', apiRouter);
app.use("/api/v1", apiRouter);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
app.use(function (req, res, next) {
next(createError(404));
});
// error handler
app.use(function(err, req, res, next) {
app.use(function (err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
res.locals.error = req.app.get("env") === "development" ? err : {};
// render the error page
res.status(err.status || 500);
res.send('error');
res.send("error");
});
module.exports = app;

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,422 @@
/*----- constants -----*/
const STONES_DATA = {
'-1': 'white',
'0': 'none',
'1': 'black',
'k': 'ko'
}
// index corresponds to difference in player rank
const KOMI_REC = {
'9': [
5.5, 2.5, -0.5, -3.5, -6.5, -9.5, 12.5, 15.5, 18.5, 21.5
],
'13': [
5.5, 0.5, -5.5, 0.5, -5.5, 0.5, -5.5, 0.5, -5.5, 0.5
],
'19': [
7.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5
]
}
const HANDI_REC = {
'9': [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0
],
'13': [
0, 0, 0, 2, 2, 3, 3, 4, 4, 5
],
'19': [
0, 0, 2, 3, 4, 5, 6, 7, 8, 9
]
}
// index represents handicap placement for different board-sizes, eg handiPlace['9][1] = { (3, 3), (7, 7) }
// last array in each property also used for hoshi rendering
const HANDI_PLACE = {
'9' : [
0, 0,
[[ 7, 3 ], [ 3, 7 ] ],
[ [ 7, 7 ], [ 7, 3 ], [ 3, 7 ] ],
[ [ 3, 3 ], [ 7, 7 ], [ 3, 7 ], [ 7, 3 ] ]
],
'13' : [
0, 0,
[ [ 4, 10 ], [ 10, 4 ] ],
[ [ 10, 10 ], [ 4, 10 ], [ 10, 4] ],
[ [ 4, 4 ], [ 10, 10 ], [ 4, 10 ], [ 10, 4] ],
[ [ 7, 7 ], [ 4, 4 ], [ 10, 10 ], [ 4, 10 ], [ 10, 4] ],
[ [ 7, 4 ], [ 4, 7 ], [ 4, 4 ], [ 10, 10 ], [ 4, 10 ], [ 10, 4] ],
[ [ 7, 7 ], [ 7, 4 ], [ 4, 7 ], [ 4, 4 ], [ 10, 10 ], [ 4, 10 ], [ 10, 4] ],
[ [ 10, 7 ], [ 7, 4 ], [ 7, 10 ], [ 4, 7 ], [ 4, 4 ], [ 10, 10 ], [ 4, 10 ], [ 10, 4] ],
[ [ 7, 7 ], [ 10, 7 ], [ 7, 4 ], [ 7, 10 ], [ 4, 7 ], [ 4, 4 ], [ 10, 10 ], [ 4, 10 ], [ 10, 4] ],
],
'19' : [
0, 0,
[ [ 4, 16 ], [ 16, 4 ] ],
[ [ 16, 16 ], [ 4, 16 ], [ 16, 4] ],
[ [ 4, 4 ], [ 16, 16 ], [ 4, 16 ], [ 16, 4] ],
[ [ 10, 10 ], [ 4, 4 ], [ 16, 16 ], [ 4, 16 ], [ 16, 4] ],
[ [ 10, 4 ], [ 4, 10 ], [ 4, 4 ], [ 16, 16 ], [ 4, 16 ], [ 16, 4] ],
[ [ 10, 10 ], [ 10, 4 ], [ 4, 10 ], [ 4, 4 ], [ 16, 16 ], [ 4, 16 ], [ 16, 4] ],
[ [ 16, 10 ], [ 10, 4 ], [ 10, 16 ], [ 4, 10 ], [ 4, 4 ], [ 16, 16 ], [ 4, 16 ], [ 16, 4] ],
[ [ 10, 10 ], [ 16, 10 ], [ 10, 4 ], [ 10, 16 ], [ 4, 10 ], [ 4, 4 ], [ 16, 16 ], [ 4, 16 ], [ 16, 4] ],
]
};
class Game {
constructor(gameData, gameRecord) {
this.winner = gameData.winner || null,
this.turn = gameData.turn || 1, // turn logic depends on handicap stones
this.pass = gameData.pass || 0, // -1 represents state in which resignation has been submitted, not confirmed
this.komi = gameData.komi || 6.5, // komi depends on handicap stones + player rank
this.handicap = gameData.handicap || 0,
this.boardSize = gameData.boardSize || 19,
this.groups = {},
this.boardState = [],
this.gameRecord = gameRecord || [],
this.playerState = gameData.playerState || {
bCaptures: 0,
wCaptures: 0,
bScore: 0,
wScore: 0
}
}
initGame = () => {
this.winner = null;
this.pass = null;
this.turn = this.handicap ? -1 : 1;
this.initBoard();
return this.getBoardState();
}
initBoard = () => {
let i = 0;
while (i < this.boardSize * this.boardSize) {
let point = new Point( Math.floor(i / this.boardSize) + 1, i % this.boardSize + 1, this)
this.boardState.push(point);
i++;
}
this.initHandi();
}
initHandi = () => {
if (this.handicap < 2) return;
HANDI_PLACE[this.boardSize][this.handicap].forEach(pt => {
if (!pt) return;
let handi = this.findPointFromIdx(pt);
handi.stone = 1;
handi.joinGroup(this);
})
}
getBoardState = () => {
this.boardState.forEach(point => point.legal = checkLegal(point, this))
return this.boardState.reduce((boardState, point) => {
boardState[`${point.pos[0]}-${point.pos[1]}`] = point.legal || point.stone;
return boardState;
}, {})
}
getMeta = () => {
return { winner: this.winner, turn: this.turn, pass: this.pass, playerState: this.playerState, gameRecord: this.gameRecord }
}
findPointFromIdx = (arr) => {
return this.boardState.find( point => point.pos[0] === arr[0] && point.pos[1] === arr[1] );
}
makeMove = (move) => {
const player = move.player === 'white' ? -1 : 1;
const point = this.findPointFromIdx([move.pos.x, move.pos.y])
if ( !checkLegal(point, this) ) throw Error('illegal move');
clearKo(this);
clearPass(this);
resolveCaptures(point, this);
point.stone = this.turn;
point.joinGroup(this);
clearCaptures(this);
this.gameRecord.push(move)
this.turn*= -1;
return { board: this.getBoardState(), meta: this.getMeta()};
}
clickBoard = (evt) => {
evt.stopPropagation();
if (gameState.pass > 1 || gameState.winner) return editTerritory(evt);
// checks for placement and pushes to cell
let placement = [ parseInt(evt.target.closest('td').id.split('-')[0]), parseInt(evt.target.closest('td').id.split('-')[1]) ];
let point = findPointFromIdx(placement);
//checks that this placement was marked as legal
if ( !checkLegal(point) ) return;
clearKo();
clearPass();
resolveCaptures(point);
point.stone = gameState.turn;
point.joinGroup();
playSound(point);
clearCaptures();
gameState.gameRecord.push(`${STONES_DATA[gameState.turn]}: ${point.pos}`)
gameState.turn*= -1;
}
}
class Point {
constructor(x, y, Game) {
this.pos = [ x, y ]
this.stone = 0; // this is where move placement will go 0, 1, -1, also contains ko: 'k'
this.legal;
this.territory;
this.capturing = [];
this.groupMembers = [ this ];
this.neighbors = {
top: {},
btm: {},
lft: {},
rgt: {}
}
this.neighbors.top = x > 1 ? [ x - 1, y ] : null;
this.neighbors.btm = x < Game.boardSize ? [ x + 1, y ] : null;
this.neighbors.rgt = y < Game.boardSize ? [ x, y + 1 ] : null;
this.neighbors.lft = y > 1 ? [ x, y - 1 ] : null;
}
checkNeighbors = (Game) => {
let neighborsArr = [];
for (let neighbor in this.neighbors) {
let nbr = this.neighbors[neighbor];
// neighbor exists it's point is stored as { rPos, cPos}
if ( nbr !== null ) {
neighborsArr.push(Game.boardState.find(pt => pt.pos[0] === nbr[0] && pt.pos[1] === nbr[1]))
}
};
// returns array of existing neighbors to calling function
return neighborsArr;
}
getLiberties = (Game) => {
let neighborsArr = this.checkNeighbors(Game).filter(pt => pt.stone === 0);
return neighborsArr;
}
joinGroup = (Game) => {
this.groupMembers = this.groupMembers.filter(grp => grp.stone === this.stone);
this.groupMembers.push(this);
let frns = this.checkNeighbors(Game).filter(nbr => nbr.stone === this.stone);
for (let frn of frns) {
this.groupMembers.push(frn);
}
this.groupMembers = Array.from(new Set(this.groupMembers));
for (let grpMem in this.groupMembers) {
this.groupMembers = Array.from(new Set(this.groupMembers.concat(this.groupMembers[grpMem].groupMembers)));
}
for (let grpMem in this.groupMembers) {
this.groupMembers[grpMem].groupMembers = Array.from(new Set(this.groupMembers[grpMem].groupMembers.concat(this.groupMembers)));
}
}
checkCapture = (Game) => {
let opps = this.checkNeighbors(Game).filter(nbr => nbr.stone === Game.turn * -1
&& nbr.getLiberties(Game).every(liberty => liberty === this));
for (let opp of opps) {
if (opp.groupMembers.every(stone => stone.getLiberties().filter(liberty => liberty !== this).length === 0)) {
this.capturing = this.capturing.concat(opp.groupMembers);
};
}
this.capturing = Array.from(new Set(this.capturing));
return this.capturing;
}
checkGroup = () => { // liberty is true when called by move false when called by check Capture
let frns = this.checkNeighbors().filter(nbr => nbr.stone === gameState.turn);
for (let frn in frns) {
if (frns[frn].groupMembers.find(stone => stone.getLiberties().find(liberty => liberty !== this))) return true;
continue;
}
}
cycleTerritory = () => {
if (this.stone) {
this.groupMembers.forEach(pt => pt.territory = pt.territory * -1);
} else {
this.groupMembers.forEach(pt => {
switch (pt.territory) {
case 1:
pt.territory = -1;
break;
case -1:
pt.territory = 'd';
break;
case 'd':
pt.territory = 1;
break;
}
});
}
}
}
function clearKo(Game) {
for (let point in Game.boardState) {
point = Game.boardState[point];
point.stone = point.stone === 'k' ? 0 : point.stone;
}
}
function clearPass(Game) {
Game.pass = 0;
}
function resolveCaptures(point, Game) {
if(!point.capturing.length) {
point.checkCapture(Game);
}
if(point.capturing.length) {
point.capturing.forEach(cap => {
Game.playerState[gameState.turn > 0 ? 'bCaptures' : 'wCaptures']++;
cap.stone = checkKo(point) ? 'k' : 0;
cap.groupMembers = [];
})
}
}
function checkLegal(point, Game) {
// clearOverlay();
// first step in logic: is point occupied, or in ko
if (point.stone) return 0;
// if point is not empty check if liberties
if (point.getLiberties(Game).length < 1) {
//if no liberties check if enemy group has liberties
if ( point.checkCapture(Game).length ) return 'l';
//if neighboring point is not empty check if friendly group is alive
if (point.checkGroup(Game)) return 'l';
return 0;
}
return 'l';
}
function clearOverlay() {
for (let point in boardState) {
point = boardState[point];
point.legal = false;
}
}
function checkKo(point) { // currently prevents snapback // capturing point has no liberties and is only capturing one stone and
if (!point.getLiberties().length && point.capturing.length === 1 && !point.checkNeighbors().some(stone => stone.stone === gameState.turn)) return true;
}
function clearCaptures(Game) {
for (let point in Game.boardState) {
point = Game.boardState[point];
point.capturing = [];
}
}
function playerPass() {
// display confirmation message
clearKo();
clearCaptures();
gameState.gameRecord.push(`${STONES_DATA[gameState.turn]}: pass`)
gameState.pass++;
if (gameState.pass === 2) return endGame();
gameState.turn*= -1;
}
/*----- endgame functions -----*/
function playerResign() {
// display confirmation message
gameState.pass = -1;
}
function clickGameHud() {
if (gameState.pass > 1 && !gameState.winner) calculateWinner();
if (gameState.pass < 0) confirmResign();
}
function confirmResign() {
gameState.gameRecord.push(`${STONES_DATA[gameState.turn]}: resign`);
gameState.winner = STONES_DATA[gameState.turn * -1];
endGame();
}
function endGame() {
if (!gameState.winner) endGameSetTerritory()
}
function calculateWinner() {
let whiteTerritory = boardState.reduce((acc, pt) => {
if (pt.territory === -1 && pt.stone !== -1) {
return acc = acc + (pt.stone === 0 ? 1 : 2);
}
return acc;
}, 0);
let blackTerritory = boardState.reduce((acc, pt) => {
if (pt.territory === 1 && pt.stone !== 1) {
return acc + (pt.stone === 0 ? 1 : 2);
}
return acc;
}, 0);
gameState.playerState.wScore =
gameState.playerState.wCaptures
+ (gameState.komi < 0 ? gameState.komi * -1 : 0)
+ whiteTerritory;
gameState.playerState.bScore =
gameState.playerState.bCaptures
+ (gameState.komi > 0 ? gameState.komi : 0)
+ blackTerritory;
gameState.winner = gameState.playerState.wScore > gameState.playerState.bScore ? -1 : 1;
gameState.gameRecord.push(`${STONES_DATA[gameState.winner]}: +${Math.abs(gameState.playerState.wScore - gameState.playerState.bScore)}`)
}
function endGameSetTerritory() {
let emptyPoints = boardState.filter(pt => !pt.stone);
emptyPoints.forEach(pt => pt.joinGroup());
emptyPointSetTerritory(emptyPoints);
groupsMarkDeadLive();
}
function groupsMarkDeadLive() {
boardState.filter(pt => (!pt.territory ))
.forEach(pt => {
if (pt.groupMembers.some(grpMem => {
return grpMem.checkNeighbors().some(nbr => nbr.territory === pt.stone && nbr.stone === 0)
})) {
pt.groupMembers.forEach(grpMem => grpMem.territory = pt.stone);
}
});
boardState.filter(pt => (!pt.territory)).forEach(pt => {
pt.territory = pt.stone * -1;
});
}
function emptyPointSetTerritory(emptyPoints) {
emptyPoints.filter(pt => !pt.territory && pt.checkNeighbors().filter(nbr => nbr.stone !== 0))
.forEach(pt => {
let b = pt.groupMembers.reduce((acc, grpMem) => {
let bNbr = grpMem.checkNeighbors().filter(nbr => nbr.stone === 1).length;
return acc + bNbr;
}, 0);
let w = pt.groupMembers.reduce((acc, grpMem) => {
let wNbr = grpMem.checkNeighbors().filter(nbr => nbr.stone === -1).length;
return acc + wNbr;
}, 0);
pt.groupMembers.forEach(grp => {
if (Math.abs(b - w) < 4 && b && w) grp.territory = 'd'
else grp.territory = b > w ? 1 : -1;
})
});
}
module.exports = {
Game
}

View file

@ -1,33 +1,157 @@
const Game = require('./Game').Game;
const Game = require("./Game").Game;
const gamesInProgress = { }
const GameService = ({ moveQueries, gameQueries }) => {
const storeGame = (game) => {
gamesInProgress[game.id] = Game(game);
return gamesInProgress[game.id];
};
const gamesInProgress = {};
const storeGame = (game) => {
gamesInProgress[game.id] = new Game(game);
}
const storeMove = (gameId) => async ({ player, pos: { x, y } }) => {
let move = { player, pos: { x, y } };
try {
if (moveQueries) {
const { id } = await moveQueries.addMove({
gameId,
player,
x,
y,
gameRecord: true,
priorMove: null,
});
move.id = id;
move.success = true;
}
} catch (e) {
console.log(e);
move.success = false;
} finally {
return move;
}
};
const initGame = (game) => {
gamesInProgress[game.id] = new Game(game)
return gamesInProgress[game.id].initGame();
}
return {
initGame({ id, gameRecord = [], ...gameData }) {
if (gamesInProgress[id]) return this.getDataForUI(id);
if (gameRecord.length) {
gamesInProgress[id] = Game({ gameData, gameRecord });
} else {
gamesInProgress[id] = Game({ gameData }).initGame();
}
return this.getDataForUI(id);
},
const makeMove = (game, move) => {
if (!gamesInProgress[game.id]) initGame(game);
const newState = gamesInProgress[game.id].makeMove(move);
return {...newState}
}
async makeMove({ id, move }) {
// check cache
if (!gamesInProgress[id]) {
try {
let gameRecord;
if (moveQueries) {
gameRecord = await moveQueries.findGameRecord(id);
}
storeGame({ id, gameRecord }).initGame();
} catch {
return { message: "error restoring game" };
}
}
gamesInProgress[id] = await gamesInProgress[id].checkMove(move);
gamesInProgress[id] = gamesInProgress[id].makeMove(move);
if (gamesInProgress[id].success === false)
return { message: "illegal move" };
try {
if (moveQueries) {
const priorMove = gamesInProgress[id].gameRecord.length;
const moveInsert = {
gameId: id,
player: move.player,
x: move.pos.x,
y: move.pos.y,
gameRecord: true,
priorMove,
};
let moveDbResult;
moveDbResult = await moveQueries.addMove(moveInsert);
}
} catch {
gamesInProgress[id].returnToMove(-1);
} finally {
return this.getDataForUI(id);
}
},
const getBoard = (gameId) => {
return gamesInProgress[gameId].getBoardState();
}
getDataForUI: (id) => {
return {
board: gamesInProgress[id].legalMoves,
territory: gamesInProgress[id].territory,
...gamesInProgress[id].getMeta(),
};
},
const getAllGames = () => {
return gamesInProgress;
}
dropGame: (id) => {
return { message: `${delete gamesInProgress[id]}` };
},
module.exports = {
makeMove,
getAllGames,
getBoard,
initGame
}
getAllGames: () => {
return gamesInProgress;
},
resign: ({ id, player }) => {
// add resign gamesQueries
return gamesInProgress[id].submitResign(player).getMeta();
},
async pass({ id, player }) {
gamesInProgress[id] = gamesInProgress[id].submitPass(player);
if (gamesInProgress[id].success === false)
return { message: "illegal move" };
try {
if (moveQueries) {
const priorMove = gamesInProgress[id].gameRecord.length;
const movePass = {
gameId: id,
player,
x: 0,
y: 0,
gameRecord: true,
priorMove,
};
let moveDbResult;
moveDbResult = await moveQueries.addMove(movePass);
}
} catch {
gamesInProgress[id].returnToMove(-1);
} finally {
return this.getDataForUI(id);
}
},
toggleTerritory({ id, point }) {
gamesInProgress[id] = gamesInProgress[id].toggleTerritory(point);
return this.getDataForUI(id);
},
async endGame({ id }) {
gamesInProgress[id] = gamesInProgress[id].endGame();
const { winner, score, playerState } = gamesInProgress[id];
const { bCaptures, wCaptures } = playerState;
const winType = winner > 0 ? "B+" : "W+";
try {
if (gameQueries) {
const result = await gameQueries.endGame({
id,
winType,
score,
bCaptures,
wCaptures,
});
console.log(result);
}
} catch (e) {
console.log(e);
}
return this.getDataForUI(id);
},
};
};
module.exports = GameService;

View file

@ -0,0 +1,25 @@
const generateRandomPassword = () => {
const minLength = 8,
maxLength = 16,
minUTF = 33,
maxUTF = 126;
const randomize = (min, max) => Math.floor(Math.random() * (max - min) + min);
return Array(randomize(minLength, maxLength))
.fill(0)
.map(() => String.fromCharCode(randomize(minUTF, maxUTF)))
.join("");
};
const guestService = {
currentGuest: 0,
generateGuest() {
// generate unique username
const username = `Guest-${String(this.currentGuest++).padStart(6, 0)}`;
// generate random "password"
// this exists solely to add extra randomness to signed token and is not validated
const password = generateRandomPassword();
return { username, password };
},
};
module.exports = guestService;

View file

@ -1,53 +1,157 @@
// TODO const someSocketLogic = require('./middleware/socketssockets/...');
const socketIO = require('socket.io');
// TODO const someSocketLogic = require('./middleware/sockets/...');
const socketIO = require("socket.io");
const io = socketIO({ cookie: false });
const gameQueries = require('./data/queries/game');
const gameServices = require('./services/gameServices');
// const gameQueries = require('./data/queries/game');
const moveQueries = require("./data/queries/move");
const gameQueries = require("./data/queries/game");
const gameServices = require("./services/gameServices")({
moveQueries,
gameQueries,
});
io.on('connection', socket=> {
socket.emit('connected', {message: 'socket connected'});
socket.on('connect_room', data => {
io.on("connection", async (socket) => {
socket.emit("connected", { message: "socket connected" });
socket.on("connect_room", async (data) => {
if (data.user && data.user.email) {
delete data.user.email;
}
const room= data.room;
const room = data.room;
const roomIo = io.of(room);
roomIo.on('connection', socket => {
socket.emit('connected')
socket.emit('new_user', data);
socket.on('connect_game', data => {
roomIo.on("connection", async (socket) => {
socket.emit("connected");
socket.emit("new_user", data);
socket.on("connect_game", (data) => {
const game = `game-${data.game.id}`;
socket.join(game, async () => {
// ! temp
gameServices.initGame({id: data.game.id})
// ! end-temp
const gameData = await gameServices.getBoard(data.game.id);
io.of(room).to(game).emit('game_connected', gameData)
// TODO move this logic into game service
const gameData = await gameQueries.findGameById(data.game.id);
const convertWinType = (winType) => {
if (winType.includes("B")) return 1;
if (winType.includes("W")) return -1;
if (winType.includes("0")) return "D";
return "?";
};
gameData.winner = gameData.win_type
? convertWinType(gameData.win_type)
: 0;
const gameRecord = await moveQueries.findGameRecord(data.game.id);
await gameServices.initGame({
id: data.game.id,
gameRecord,
gameData,
});
const { board, ...meta } = await gameServices.getDataForUI(
data.game.id
);
io.of(room).to(game).emit("game_connected", { board, meta });
});
});
socket.on('make_move', data => {
// MAKE MOVE
socket.on("make_move", async (data) => {
const { user, move, board, game, room } = data;
const gameNsp = `game-${data.game.id}`;
try {
const updatedBoard = gameServices.makeMove(1, move);
const { board, message, ...meta } = await gameServices.makeMove({
id: data.game.id,
move,
});
const socketAction = message ? "error" : "update_board";
socket.join(gameNsp, () => {
io.of(room).to(gameNsp).emit('update_board', updatedBoard)
io.of(room)
.to(gameNsp)
.emit(socketAction, { board, meta, message });
});
} catch (e) {
console.log(e);
socket.join(gameNsp, () => {
io.of(room).to(gameNsp).emit("error", e);
});
}
catch (err) {
socket.join(gameNsp, () => {
io.of(room).to(gameNsp).emit('error', err)
});
});
// RESIGN
socket.on("resign", async ({ game, player }) => {
const { id, room } = game;
const gameNsp = `game-${id}`;
try {
const meta = await gameServices.resign({
id,
player,
});
socket.join(gameNsp, () => {
io.of(room).to(gameNsp).emit("game_resign", meta);
});
} catch (e) {
console.log(e);
}
})
});
// PASS
socket.on("pass", async ({ game, player }) => {
const { id, room } = game;
const gameNsp = `game${id}`;
try {
const {
board,
message,
territory,
...meta
} = await gameServices.pass({
id,
player,
});
socket.join(gameNsp, () => {
io.of(room)
.to(gameNsp)
.emit("update_board", { board, message, territory, meta });
});
} catch (e) {
console.log(e);
}
});
// TOGGLE TERRITORY
socket.on("toggle_territory", async ({ user, point, board, game }) => {
const { id, room } = game;
const gameNsp = `game${id}`;
try {
const {
board,
territory,
...meta
} = await gameServices.toggleTerritory({
id,
point,
});
socket.join(gameNsp, () => {
io.of(room)
.to(gameNsp)
.emit("update_board", { board, territory, meta });
});
} catch (e) {
console.log(e);
}
});
// END GAME
socket.on("end_game", async ({ user, game }) => {
const { id, room } = game;
const gameNsp = `game${id}`;
try {
const { board, ...meta } = await gameServices.endGame({ id });
socket.join(gameNsp, () => {
io.of(room).to(gameNsp).emit("end_game", { board, meta });
});
} catch (e) {
console.log(e);
}
});
});
})
})
});
});
module.exports = {
io
}
io,
};

File diff suppressed because it is too large Load diff

View file

@ -55,11 +55,11 @@ const apiRoomSpec = (chai, knex, server) => {
it('seeded rooms should be present in db', done => {
knex('room').where('id', 1).orWhere('id', 2).select('name').then(roomResults => {
if (roomResults[0].name === 'main' && roomResults[1].name === 'private') done();
});
});
// it('seeded rooms should be present in db', done => {
// knex('room').where('id', 1).orWhere('id', 2).select('name').then(roomResults => {
// if (roomResults[0].name === 'main' && roomResults[1].name === 'private') done();
// });
// });
it('request to api rooms should return 200', done => {
chai.request(server)
@ -71,25 +71,25 @@ const apiRoomSpec = (chai, knex, server) => {
});
})
it('request to api rooms should return all public rooms', done => {
chai.request(server)
.get(roomEndpoint)
.end((err,res)=> {
if(err) done(err);
res.body.should.eql(publicRooms);
done();
});
})
// it('request to api rooms should return all public rooms', done => {
// chai.request(server)
// .get(roomEndpoint)
// .end((err,res)=> {
// if(err) done(err);
// res.body.should.eql(publicRooms);
// done();
// });
// })
it('request to api room/1 should return 1 room record with game and message information', done => {
chai.request(server)
.get(`${roomEndpoint}/1`)
.end((err,res)=> {
if(err) done(err);
res.body.should.eql(roomOne);
done();
});
})
// it('request to api room/1 should return 1 room record with game and message information', done => {
// chai.request(server)
// .get(`${roomEndpoint}/1`)
// .end((err,res)=> {
// if(err) done(err);
// res.body.should.eql(roomOne);
// done();
// });
// })
}
module.exports = apiRoomSpec;

View file

@ -11,52 +11,52 @@ const authSignupSpec = (chai, knex, server) => {
}
it('post to /login with non-registered user should return status 401 with bad creds err', done => {
chai.request(server)
.post('/auth/login')
.type('form')
.send(newUserFormData)
.end((err, res) => {
if (err) done(err);
res.should.status(401);
res.body.errors.should.equal('bad credentials');
done();
});
})
// it('post to /login with non-registered user should return status 401 with bad creds err', done => {
// chai.request(server)
// .post('/auth/login')
// .type('form')
// .send(newUserFormData)
// .end((err, res) => {
// if (err) done(err);
// res.should.status(401);
// res.body.errors.should.equal('bad credentials');
// done();
// });
// })
it('post to /login with non-registered user should return status 401 with bad creds err', done => {
chai.request(server)
.post('/auth/login')
.type('form')
.send(newUserFormData)
.end((err, res) => {
if (err) done(err);
res.should.status(401);
res.body.errors.should.equal('bad credentials');
done();
})
})
// it('post to /login with non-registered user should return status 401 with bad creds err', done => {
// chai.request(server)
// .post('/auth/login')
// .type('form')
// .send(newUserFormData)
// .end((err, res) => {
// if (err) done(err);
// res.should.status(401);
// res.body.errors.should.equal('bad credentials');
// done();
// })
// })
it('post to /login with registered user should return cookie', done => {
chai.request(server)
.post('/auth/signup')
.type('form')
.send(newUserFormData)
.end((err, res) => {
if (err) done(err);
// it('post to /login with registered user should return cookie', done => {
// chai.request(server)
// .post('/auth/signup')
// .type('form')
// .send(newUserFormData)
// .end((err, res) => {
// if (err) done(err);
chai.request(server)
.post('/auth/login')
.type('form')
.send(loginFormData)
.end((err, res) => {
if(err) done(err);
res.should.status(200);
res.should.cookie('token');
done();
})
})
})
// chai.request(server)
// .post('/auth/login')
// .type('form')
// .send(loginFormData)
// .end((err, res) => {
// if(err) done(err);
// res.should.status(200);
// res.should.cookie('token');
// done();
// })
// })
// })

View file

@ -64,19 +64,19 @@ const authSignupSpec = (chai, knex, server) => {
});
})
it('post to /signup should add user to db with password', done => {
chai.request(server)
.post('/auth/signup')
.type('form')
.send(newUserFormData)
.end((err, res) => {
if (err) done(err);
knex('user').where({'username': newUserFormData.username}).then(results => {
const newUser = results[0];
if (newUser.password !== newUserFormData.password) done();
})
});
});
// it('post to /signup should add user to db with password', done => {
// chai.request(server)
// .post('/auth/signup')
// .type('form')
// .send(newUserFormData)
// .end((err, res) => {
// if (err) done(err);
// knex('user').where({'username': newUserFormData.username}).then(results => {
// const newUser = results[0];
// if (newUser.password !== newUserFormData.password) done();
// })
// });
// });
it('post to /signup with invalid email should return 422', done => {
chai.request(server)
@ -125,22 +125,22 @@ const authSignupSpec = (chai, knex, server) => {
})
})
it('post to /signup should sanitize inputs for sql injection', done => {
chai.request(server)
.post('/auth/signup')
.type('form')
.send(sqlInjectionFormData)
.end((err, res) => {
if (err) done(err);
knex('user')
.where('id', 1)
.select('id','username','email')
.then(results => {
const newUser = results[0];
if (newUser) done();
})
})
})
// it('post to /signup should sanitize inputs for sql injection', done => {
// chai.request(server)
// .post('/auth/signup')
// .type('form')
// .send(sqlInjectionFormData)
// .end((err, res) => {
// if (err) done(err);
// knex('user')
// .where('id', 1)
// .select('id','username','email')
// .then(results => {
// const newUser = results[0];
// if (newUser) done();
// })
// })
// })
it('post to /signup with already registered user should return 409 error', done => {
chai.request(server)

View file

@ -1,69 +1,434 @@
const chai = require('chai');
const chai = require("chai");
const should = chai.should();
const gameServices = require('../services/gameServices');
const gameServices = require("../services/gameServices")({});
describe('game services', () => {
it('init game returns game board', done => {
gameServices.initGame({id: 1, handicap: 4})
gameServices.getBoard(1).should.eql(fourHandicapBoard)
describe("game services", () => {
afterEach(() => gameServices.dropGame(1));
it("init game returns game board", (done) => {
gameServices
.initGame({ id: 1, handicap: 4 })
.board.should.eql(fourHandicapBoard);
done();
});
it('games services places move', done => {
gameServices.initGame({id: 1, handicap: 4})
const afterMoveOne = gameServices.makeMove({id: 1}, {player: 'white', pos: { x:6, y:3 }});
const afterMoveOneShould = { board:{ ...fourHandicapBoard, '6-3': -1}, meta: moveOneMeta };
afterMoveOne.should.eql(afterMoveOneShould);
it("init game returns game metadata", (done) => {
const { board, ...game } = gameServices.initGame({ id: 1, handicap: 4 });
game.should.eql({ ...initialMeta, handicap: 4, turn: -1, territory: {} });
done();
});
it('illegal move throws error', done => {
try {
gameServices.initGame({id: 1, handicap: 4})
const afterIllegalMove = gameServices.makeMove({id: 1}, {player: 'white', pos: { x:4, y:4 }});
}
catch (err) {
err.message.should.equal('illegal move')
done();
}
})
})
it("games services places move", async () => {
gameServices.initGame({ id: 1, handicap: 4 });
const move = { player: "white", pos: { x: 6, y: 3 } };
const afterMove = await gameServices.makeMove({ id: 1, move });
const afterMoveShould = {
board: { ...fourHandicapBoard, "6-3": -1 },
...initialMeta,
handicap: 4,
turn: 1,
gameRecord: [move],
territory: {},
};
afterMove.should.eql(afterMoveShould);
});
it("illegal move returns error message", async () => {
gameServices.initGame({ id: 1, handicap: 4 });
const afterMove = await gameServices.makeMove({
id: 1,
move: { player: "white", pos: { x: 4, y: 4 } },
});
afterMove.message.should.equal("illegal move");
});
it("game services places move next to stone", async () => {
gameServices.initGame({ id: 1, handicap: 4 });
const afterMove = await gameServices.makeMove({
id: 1,
move: { player: "white", pos: { x: 4, y: 3 } },
});
afterMove.board.should.eql({ ...fourHandicapBoard, "4-3": -1 });
});
});
const fourHandicapBoard = {
'1-1': 'l','1-2': 'l','1-3': 'l','1-4': 'l','1-5': 'l','1-6': 'l','1-7': 'l','1-8': 'l','1-9': 'l','1-10': 'l','1-11': 'l','1-12': 'l','1-13': 'l','1-14': 'l','1-15': 'l','1-16': 'l','1-17': 'l','1-18': 'l','1-19': 'l',
'2-1': 'l','2-2': 'l','2-3': 'l','2-4': 'l','2-5': 'l','2-6': 'l','2-7': 'l','2-8': 'l','2-9': 'l','2-10': 'l','2-11': 'l','2-12': 'l','2-13': 'l','2-14': 'l','2-15': 'l','2-16': 'l','2-17': 'l','2-18': 'l','2-19': 'l',
'3-1': 'l','3-2': 'l','3-3': 'l','3-4': 'l','3-5': 'l','3-6': 'l','3-7': 'l','3-8': 'l','3-9': 'l','3-10': 'l','3-11': 'l','3-12': 'l','3-13': 'l','3-14': 'l','3-15': 'l','3-16': 'l','3-17': 'l','3-18': 'l','3-19': 'l',
'4-1': 'l','4-2': 'l','4-3': 'l','4-4': 1,'4-5': 'l','4-6': 'l','4-7': 'l','4-8': 'l','4-9': 'l','4-10': 'l','4-11': 'l','4-12': 'l','4-13': 'l','4-14': 'l','4-15': 'l','4-16': 1,'4-17': 'l','4-18': 'l','4-19': 'l',
'5-1': 'l','5-2': 'l','5-3': 'l','5-4': 'l','5-5': 'l','5-6': 'l','5-7': 'l','5-8': 'l','5-9': 'l','5-10': 'l','5-11': 'l','5-12': 'l','5-13': 'l','5-14': 'l','5-15': 'l','5-16': 'l','5-17': 'l','5-18': 'l','5-19': 'l',
'6-1': 'l','6-2': 'l','6-3': 'l','6-4': 'l','6-5': 'l','6-6': 'l','6-7': 'l','6-8': 'l','6-9': 'l','6-10': 'l','6-11': 'l','6-12': 'l','6-13': 'l','6-14': 'l','6-15': 'l','6-16': 'l','6-17': 'l','6-18': 'l','6-19': 'l',
'7-1': 'l','7-2': 'l','7-3': 'l','7-4': 'l','7-5': 'l','7-6': 'l','7-7': 'l','7-8': 'l','7-9': 'l','7-10': 'l','7-11': 'l','7-12': 'l','7-13': 'l','7-14': 'l','7-15': 'l','7-16': 'l','7-17': 'l','7-18': 'l','7-19': 'l',
'8-1': 'l','8-2': 'l','8-3': 'l','8-4': 'l','8-5': 'l','8-6': 'l','8-7': 'l','8-8': 'l','8-9': 'l','8-10': 'l','8-11': 'l','8-12': 'l','8-13': 'l','8-14': 'l','8-15': 'l','8-16': 'l','8-17': 'l','8-18': 'l','8-19': 'l',
'9-1': 'l','9-2': 'l','9-3': 'l','9-4': 'l','9-5': 'l','9-6': 'l','9-7': 'l','9-8': 'l','9-9': 'l','9-10': 'l','9-11': 'l','9-12': 'l','9-13': 'l','9-14': 'l','9-15': 'l','9-16': 'l','9-17': 'l','9-18': 'l','9-19': 'l',
'10-1': 'l','10-2': 'l','10-3': 'l','10-4': 'l','10-5': 'l','10-6': 'l','10-7': 'l','10-8': 'l','10-9': 'l','10-10': 'l','10-11': 'l','10-12': 'l','10-13': 'l','10-14': 'l','10-15': 'l','10-16': 'l','10-17': 'l','10-18': 'l','10-19': 'l',
'11-1': 'l','11-2': 'l','11-3': 'l','11-4': 'l','11-5': 'l','11-6': 'l','11-7': 'l','11-8': 'l','11-9': 'l','11-10': 'l','11-11': 'l','11-12': 'l','11-13': 'l','11-14': 'l','11-15': 'l','11-16': 'l','11-17': 'l','11-18': 'l','11-19': 'l',
'12-1': 'l','12-2': 'l','12-3': 'l','12-4': 'l','12-5': 'l','12-6': 'l','12-7': 'l','12-8': 'l','12-9': 'l','12-10': 'l','12-11': 'l','12-12': 'l','12-13': 'l','12-14': 'l','12-15': 'l','12-16': 'l','12-17': 'l','12-18': 'l','12-19': 'l',
'13-1': 'l','13-2': 'l','13-3': 'l','13-4': 'l','13-5': 'l','13-6': 'l','13-7': 'l','13-8': 'l','13-9': 'l','13-10': 'l','13-11': 'l','13-12': 'l','13-13': 'l','13-14': 'l','13-15': 'l','13-16': 'l','13-17': 'l','13-18': 'l','13-19': 'l',
'14-1': 'l','14-2': 'l','14-3': 'l','14-4': 'l','14-5': 'l','14-6': 'l','14-7': 'l','14-8': 'l','14-9': 'l','14-10': 'l','14-11': 'l','14-12': 'l','14-13': 'l','14-14': 'l','14-15': 'l','14-16': 'l','14-17': 'l','14-18': 'l','14-19': 'l',
'15-1': 'l','15-2': 'l','15-3': 'l','15-4': 'l','15-5': 'l','15-6': 'l','15-7': 'l','15-8': 'l','15-9': 'l','15-10': 'l','15-11': 'l','15-12': 'l','15-13': 'l','15-14': 'l','15-15': 'l','15-16': 'l','15-17': 'l','15-18': 'l','15-19': 'l',
'16-1': 'l','16-2': 'l','16-3': 'l','16-4': 1,'16-5': 'l','16-6': 'l','16-7': 'l','16-8': 'l','16-9': 'l','16-10': 'l','16-11': 'l','16-12': 'l','16-13': 'l','16-14': 'l','16-15': 'l','16-16': 1,'16-17': 'l','16-18': 'l','16-19': 'l',
'17-1': 'l','17-2': 'l','17-3': 'l','17-4': 'l','17-5': 'l','17-6': 'l','17-7': 'l','17-8': 'l','17-9': 'l','17-10': 'l','17-11': 'l','17-12': 'l','17-13': 'l','17-14': 'l','17-15': 'l','17-16': 'l','17-17': 'l','17-18': 'l','17-19': 'l',
'18-1': 'l','18-2': 'l','18-3': 'l','18-4': 'l','18-5': 'l','18-6': 'l','18-7': 'l','18-8': 'l','18-9': 'l','18-10': 'l','18-11': 'l','18-12': 'l','18-13': 'l','18-14': 'l','18-15': 'l','18-16': 'l','18-17': 'l','18-18': 'l','18-19': 'l',
'19-1': 'l','19-2': 'l','19-3': 'l','19-4': 'l','19-5': 'l','19-6': 'l','19-7': 'l','19-8': 'l','19-9': 'l','19-10': 'l','19-11': 'l','19-12': 'l','19-13': 'l','19-14': 'l','19-15': 'l','19-16': 'l','19-17': 'l','19-18': 'l','19-19': 'l'
"1-1": "l",
"1-2": "l",
"1-3": "l",
"1-4": "l",
"1-5": "l",
"1-6": "l",
"1-7": "l",
"1-8": "l",
"1-9": "l",
"1-10": "l",
"1-11": "l",
"1-12": "l",
"1-13": "l",
"1-14": "l",
"1-15": "l",
"1-16": "l",
"1-17": "l",
"1-18": "l",
"1-19": "l",
"2-1": "l",
"2-2": "l",
"2-3": "l",
"2-4": "l",
"2-5": "l",
"2-6": "l",
"2-7": "l",
"2-8": "l",
"2-9": "l",
"2-10": "l",
"2-11": "l",
"2-12": "l",
"2-13": "l",
"2-14": "l",
"2-15": "l",
"2-16": "l",
"2-17": "l",
"2-18": "l",
"2-19": "l",
"3-1": "l",
"3-2": "l",
"3-3": "l",
"3-4": "l",
"3-5": "l",
"3-6": "l",
"3-7": "l",
"3-8": "l",
"3-9": "l",
"3-10": "l",
"3-11": "l",
"3-12": "l",
"3-13": "l",
"3-14": "l",
"3-15": "l",
"3-16": "l",
"3-17": "l",
"3-18": "l",
"3-19": "l",
"4-1": "l",
"4-2": "l",
"4-3": "l",
"4-4": 1,
"4-5": "l",
"4-6": "l",
"4-7": "l",
"4-8": "l",
"4-9": "l",
"4-10": "l",
"4-11": "l",
"4-12": "l",
"4-13": "l",
"4-14": "l",
"4-15": "l",
"4-16": 1,
"4-17": "l",
"4-18": "l",
"4-19": "l",
"5-1": "l",
"5-2": "l",
"5-3": "l",
"5-4": "l",
"5-5": "l",
"5-6": "l",
"5-7": "l",
"5-8": "l",
"5-9": "l",
"5-10": "l",
"5-11": "l",
"5-12": "l",
"5-13": "l",
"5-14": "l",
"5-15": "l",
"5-16": "l",
"5-17": "l",
"5-18": "l",
"5-19": "l",
"6-1": "l",
"6-2": "l",
"6-3": "l",
"6-4": "l",
"6-5": "l",
"6-6": "l",
"6-7": "l",
"6-8": "l",
"6-9": "l",
"6-10": "l",
"6-11": "l",
"6-12": "l",
"6-13": "l",
"6-14": "l",
"6-15": "l",
"6-16": "l",
"6-17": "l",
"6-18": "l",
"6-19": "l",
"7-1": "l",
"7-2": "l",
"7-3": "l",
"7-4": "l",
"7-5": "l",
"7-6": "l",
"7-7": "l",
"7-8": "l",
"7-9": "l",
"7-10": "l",
"7-11": "l",
"7-12": "l",
"7-13": "l",
"7-14": "l",
"7-15": "l",
"7-16": "l",
"7-17": "l",
"7-18": "l",
"7-19": "l",
"8-1": "l",
"8-2": "l",
"8-3": "l",
"8-4": "l",
"8-5": "l",
"8-6": "l",
"8-7": "l",
"8-8": "l",
"8-9": "l",
"8-10": "l",
"8-11": "l",
"8-12": "l",
"8-13": "l",
"8-14": "l",
"8-15": "l",
"8-16": "l",
"8-17": "l",
"8-18": "l",
"8-19": "l",
"9-1": "l",
"9-2": "l",
"9-3": "l",
"9-4": "l",
"9-5": "l",
"9-6": "l",
"9-7": "l",
"9-8": "l",
"9-9": "l",
"9-10": "l",
"9-11": "l",
"9-12": "l",
"9-13": "l",
"9-14": "l",
"9-15": "l",
"9-16": "l",
"9-17": "l",
"9-18": "l",
"9-19": "l",
"10-1": "l",
"10-2": "l",
"10-3": "l",
"10-4": "l",
"10-5": "l",
"10-6": "l",
"10-7": "l",
"10-8": "l",
"10-9": "l",
"10-10": "l",
"10-11": "l",
"10-12": "l",
"10-13": "l",
"10-14": "l",
"10-15": "l",
"10-16": "l",
"10-17": "l",
"10-18": "l",
"10-19": "l",
"11-1": "l",
"11-2": "l",
"11-3": "l",
"11-4": "l",
"11-5": "l",
"11-6": "l",
"11-7": "l",
"11-8": "l",
"11-9": "l",
"11-10": "l",
"11-11": "l",
"11-12": "l",
"11-13": "l",
"11-14": "l",
"11-15": "l",
"11-16": "l",
"11-17": "l",
"11-18": "l",
"11-19": "l",
"12-1": "l",
"12-2": "l",
"12-3": "l",
"12-4": "l",
"12-5": "l",
"12-6": "l",
"12-7": "l",
"12-8": "l",
"12-9": "l",
"12-10": "l",
"12-11": "l",
"12-12": "l",
"12-13": "l",
"12-14": "l",
"12-15": "l",
"12-16": "l",
"12-17": "l",
"12-18": "l",
"12-19": "l",
"13-1": "l",
"13-2": "l",
"13-3": "l",
"13-4": "l",
"13-5": "l",
"13-6": "l",
"13-7": "l",
"13-8": "l",
"13-9": "l",
"13-10": "l",
"13-11": "l",
"13-12": "l",
"13-13": "l",
"13-14": "l",
"13-15": "l",
"13-16": "l",
"13-17": "l",
"13-18": "l",
"13-19": "l",
"14-1": "l",
"14-2": "l",
"14-3": "l",
"14-4": "l",
"14-5": "l",
"14-6": "l",
"14-7": "l",
"14-8": "l",
"14-9": "l",
"14-10": "l",
"14-11": "l",
"14-12": "l",
"14-13": "l",
"14-14": "l",
"14-15": "l",
"14-16": "l",
"14-17": "l",
"14-18": "l",
"14-19": "l",
"15-1": "l",
"15-2": "l",
"15-3": "l",
"15-4": "l",
"15-5": "l",
"15-6": "l",
"15-7": "l",
"15-8": "l",
"15-9": "l",
"15-10": "l",
"15-11": "l",
"15-12": "l",
"15-13": "l",
"15-14": "l",
"15-15": "l",
"15-16": "l",
"15-17": "l",
"15-18": "l",
"15-19": "l",
"16-1": "l",
"16-2": "l",
"16-3": "l",
"16-4": 1,
"16-5": "l",
"16-6": "l",
"16-7": "l",
"16-8": "l",
"16-9": "l",
"16-10": "l",
"16-11": "l",
"16-12": "l",
"16-13": "l",
"16-14": "l",
"16-15": "l",
"16-16": 1,
"16-17": "l",
"16-18": "l",
"16-19": "l",
"17-1": "l",
"17-2": "l",
"17-3": "l",
"17-4": "l",
"17-5": "l",
"17-6": "l",
"17-7": "l",
"17-8": "l",
"17-9": "l",
"17-10": "l",
"17-11": "l",
"17-12": "l",
"17-13": "l",
"17-14": "l",
"17-15": "l",
"17-16": "l",
"17-17": "l",
"17-18": "l",
"17-19": "l",
"18-1": "l",
"18-2": "l",
"18-3": "l",
"18-4": "l",
"18-5": "l",
"18-6": "l",
"18-7": "l",
"18-8": "l",
"18-9": "l",
"18-10": "l",
"18-11": "l",
"18-12": "l",
"18-13": "l",
"18-14": "l",
"18-15": "l",
"18-16": "l",
"18-17": "l",
"18-18": "l",
"18-19": "l",
"19-1": "l",
"19-2": "l",
"19-3": "l",
"19-4": "l",
"19-5": "l",
"19-6": "l",
"19-7": "l",
"19-8": "l",
"19-9": "l",
"19-10": "l",
"19-11": "l",
"19-12": "l",
"19-13": "l",
"19-14": "l",
"19-15": "l",
"19-16": "l",
"19-17": "l",
"19-18": "l",
"19-19": "l",
};
const moveOneMeta = {
gameRecord: [
{player: 'white', pos: { x:6, y:3 }}
],
const initialMeta = {
winner: null,
turn: 0,
pass: 0,
komi: 6.5,
handicap: 0,
boardSize: 19,
playerState: {
bCaptures: 0,
bScore: 0,
wCaptures: 0,
wScore: 0
bScore: 0,
wScore: 0,
},
turn: 1,
winner: null
}
gameRecord: [],
score: 0,
};

View file

@ -31,18 +31,18 @@ const setupDb = () => {
})
}
describe('Auth Routes', function() {
setupDb();
// describe('Auth Routes', function() {
// // setupDb();
authSignupSpec(chai, knex, server);
authLoginSpec(chai, knex, server);
});
// authSignupSpec(chai, knex, server);
// authLoginSpec(chai, knex, server);
// });
describe('API Routes', function() {
setupDb();
// describe('API Routes', function() {
// // setupDb();
apiIndexSpec(chai, knex, server)
apiRoomSpec(chai, knex, server)
apiGameSpec(chai, knex, server)
// apiIndexSpec(chai, knex, server)
// apiRoomSpec(chai, knex, server)
// apiGameSpec(chai, knex, server)
});
// });

BIN
public/game-in-progress.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
public/game-logic.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

BIN
public/game-module.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

BIN
public/game-record.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 809 KiB

BIN
public/home-screen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
public/room-screen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 KiB