Compare commits

..

No commits in common. "master" and "green_build" have entirely different histories.

96 changed files with 6017 additions and 11181 deletions

View file

@ -43,13 +43,7 @@ jobs:
- node_modules
key:
v1-dependencies-{{ checksum "package-lock.json" }}
# - run: npm install react
# - run: npm install react-dom
# run tests!
- 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
View file

@ -1,21 +0,0 @@
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.

158
README.md
View file

@ -1,170 +1,18 @@
# 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/)
[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
- [x] Realtime play
- [x] Account authentication
- [ ] Chat
- [ ] Study mode
- [ ] Realtime communications
- [ ] 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

8
deployment.md Normal file
View file

@ -0,0 +1,8 @@
`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`

1741
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -3,7 +3,11 @@
"private": true,
"scripts": {
"test": "lerna run test",
"bootstrap": "lerna bootstrap --hoist"
"bootstrap": "lerna bootstrap"
},
"dependencies": {
"react": "^16.12.0",
"react-dom": "^16.12.0"
},
"devDependencies": {
"@babel/preset-flow": "^7.8.3",

View file

View file

@ -0,0 +1,22 @@
{
"files": {
"main.css": "/static/css/main.1d648088.chunk.css",
"main.js": "/static/js/main.164728a0.chunk.js",
"main.js.map": "/static/js/main.164728a0.chunk.js.map",
"runtime-main.js": "/static/js/runtime-main.7289240f.js",
"runtime-main.js.map": "/static/js/runtime-main.7289240f.js.map",
"static/js/2.26d4f1c3.chunk.js": "/static/js/2.26d4f1c3.chunk.js",
"static/js/2.26d4f1c3.chunk.js.map": "/static/js/2.26d4f1c3.chunk.js.map",
"index.html": "/index.html",
"precache-manifest.1de5f97d46b517940634a416af030f9a.js": "/precache-manifest.1de5f97d46b517940634a416af030f9a.js",
"service-worker.js": "/service-worker.js",
"static/css/main.1d648088.chunk.css.map": "/static/css/main.1d648088.chunk.css.map",
"static/js/2.26d4f1c3.chunk.js.LICENSE": "/static/js/2.26d4f1c3.chunk.js.LICENSE"
},
"entrypoints": [
"static/js/runtime-main.7289240f.js",
"static/js/2.26d4f1c3.chunk.js",
"static/css/main.1d648088.chunk.css",
"static/js/main.164728a0.chunk.js"
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 B

View file

@ -0,0 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site created using create-react-app"/><link rel="manifest" href="/manifest.json"/><link rel="stylesheet" href="/reset.css"><link href="https://fonts.googleapis.com/css?family=Oswald&display=swap" rel="stylesheet"><title>Node Go</title><link href="/static/css/main.1d648088.chunk.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script>!function(i){function e(e){for(var r,t,n=e[0],o=e[1],u=e[2],l=0,a=[];l<n.length;l++)t=n[l],Object.prototype.hasOwnProperty.call(f,t)&&f[t]&&a.push(f[t][0]),f[t]=0;for(r in o)Object.prototype.hasOwnProperty.call(o,r)&&(i[r]=o[r]);for(s&&s(e);a.length;)a.shift()();return c.push.apply(c,u||[]),p()}function p(){for(var e,r=0;r<c.length;r++){for(var t=c[r],n=!0,o=1;o<t.length;o++){var u=t[o];0!==f[u]&&(n=!1)}n&&(c.splice(r--,1),e=l(l.s=t[0]))}return e}var t={},f={1:0},c=[];function l(e){if(t[e])return t[e].exports;var r=t[e]={i:e,l:!1,exports:{}};return i[e].call(r.exports,r,r.exports,l),r.l=!0,r.exports}l.m=i,l.c=t,l.d=function(e,r,t){l.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},l.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},l.t=function(r,e){if(1&e&&(r=l(r)),8&e)return r;if(4&e&&"object"==typeof r&&r&&r.__esModule)return r;var t=Object.create(null);if(l.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:r}),2&e&&"string"!=typeof r)for(var n in r)l.d(t,n,function(e){return r[e]}.bind(null,n));return t},l.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return l.d(r,"a",r),r},l.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},l.p="/";var r=this["webpackJsonpreact-boilerplate"]=this["webpackJsonpreact-boilerplate"]||[],n=r.push.bind(r);r.push=e,r=r.slice();for(var o=0;o<r.length;o++)e(r[o]);var s=n;p()}([])</script><script src="/static/js/2.26d4f1c3.chunk.js"></script><script src="/static/js/main.164728a0.chunk.js"></script></body></html>

View file

@ -0,0 +1,15 @@
{
"short_name": "Node Go",
"name": "Node Go",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View file

@ -0,0 +1,26 @@
self.__precacheManifest = (self.__precacheManifest || []).concat([
{
"revision": "2cdbae043b55430098550a4182e1f532",
"url": "/index.html"
},
{
"revision": "a0ff352995c458284a59",
"url": "/static/css/main.1d648088.chunk.css"
},
{
"revision": "59e50fddef74efce5aef",
"url": "/static/js/2.26d4f1c3.chunk.js"
},
{
"revision": "138a6d51240ab070e8d1b20c54be6394",
"url": "/static/js/2.26d4f1c3.chunk.js.LICENSE"
},
{
"revision": "a0ff352995c458284a59",
"url": "/static/js/main.164728a0.chunk.js"
},
{
"revision": "75a6732926f560c9e1f3",
"url": "/static/js/runtime-main.7289240f.js"
}
]);

View file

@ -0,0 +1,48 @@
/* http://meyerweb.com/eric/tools/css/reset/
v2.0 | 20110126
License: none (public domain)
*/
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}

View file

@ -0,0 +1,2 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *

View file

@ -0,0 +1,39 @@
/**
* Welcome to your Workbox-powered service worker!
*
* You'll need to register this file in your web app and you should
* disable HTTP caching for this file too.
* See https://goo.gl/nhQhGp
*
* The rest of the code is auto-generated. Please don't update this file
* directly; instead, make changes to your Workbox build configuration
* and re-run your build process.
* See https://goo.gl/2aRDsh
*/
importScripts("https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js");
importScripts(
"/precache-manifest.1de5f97d46b517940634a416af030f9a.js"
);
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});
workbox.core.clientsClaim();
/**
* The workboxSW.precacheAndRoute() method efficiently caches and responds to
* requests for URLs in the manifest.
* See https://goo.gl/S9QRab
*/
self.__precacheManifest = [].concat(self.__precacheManifest || []);
workbox.precaching.precacheAndRoute(self.__precacheManifest, {});
workbox.routing.registerNavigationRoute(workbox.precaching.getCacheKeyForURL("/index.html"), {
blacklist: [/^\/_/,/\/[^\/?]+\.[^\/]+$/],
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,50 @@
/*!
* The buffer module from node.js, for the browser.
*
* @author Feross Aboukhadijeh <http://feross.org>
* @license MIT
*/
/*
object-assign
(c) Sindre Sorhus
@license MIT
*/
/** @license React v16.12.0
* react.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v16.12.0
* react-dom.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v0.18.0
* scheduler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v16.12.0
* react-is.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/*! https://mths.be/utf8js v2.1.2 by @mathias */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,2 @@
!function(e){function r(r){for(var n,l,a=r[0],i=r[1],p=r[2],c=0,s=[];c<a.length;c++)l=a[c],Object.prototype.hasOwnProperty.call(o,l)&&o[l]&&s.push(o[l][0]),o[l]=0;for(n in i)Object.prototype.hasOwnProperty.call(i,n)&&(e[n]=i[n]);for(f&&f(r);s.length;)s.shift()();return u.push.apply(u,p||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,a=1;a<t.length;a++){var i=t[a];0!==o[i]&&(n=!1)}n&&(u.splice(r--,1),e=l(l.s=t[0]))}return e}var n={},o={1:0},u=[];function l(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,l),t.l=!0,t.exports}l.m=e,l.c=n,l.d=function(e,r,t){l.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},l.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},l.t=function(e,r){if(1&r&&(e=l(e)),8&r)return e;if(4&r&&"object"===typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(l.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)l.d(t,n,function(r){return e[r]}.bind(null,n));return t},l.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return l.d(r,"a",r),r},l.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},l.p="/";var a=this["webpackJsonpreact-boilerplate"]=this["webpackJsonpreact-boilerplate"]||[],i=a.push.bind(a);a.push=r,a=a.slice();for(var p=0;p<a.length;p++)r(a[p]);var f=i;t()}([]);
//# sourceMappingURL=runtime-main.7289240f.js.map

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,17 @@
@mixin fullspan {
height: 100%;
width: 100%;
}
@mixin flexAround($direction) {
display: flex;
flex-direction: $direction;
align-items: center;
justify-content: space-around;
}
@mixin gameViewLabel {
color: rgb(255,240,230);
background-color: rgba(0,0,0,0.7);
padding: 0.25em;
}

View file

@ -0,0 +1,31 @@
$dev-colors: (
"a": #34dc90,
"b": #9034dc,
"c": #dc9034,
"d": #dc34dc,
"e": #34dcdc,
"f": #dcdc34
);
/* Responsive Design */
$break-points: (
"500": "only screen and (min-width: 500px)",
"570": "only screen and (min-width: 570px)",
"590": "only screen and (min-width: 590px)",
"700": "only screen and (min-width: 700px)",
"900": "only screen and (min-width: 900px)"
);
$colors: (
"error": #aa3333,
"nav_bar": #6f3e68,
"nav_link": #dae1b7,
"sidebar": #3c013f,
"sidebar_link": #f2ce3d,
"main": #95acae,
"home": #444444
);
$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%),
)

View file

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

File diff suppressed because it is too large Load diff

View file

@ -5,17 +5,15 @@
"dependencies": {
"@mars/heroku-js-runtime-env": "^3.0.2",
"@testing-library/user-event": "^7.1.2",
"node-sass": "^4.14.1",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"node-sass": "^4.13.0",
"react-router-dom": "^5.1.2",
"react-scripts": "^3.4.0",
"react-scripts": "3.3.0",
"socket.io-client": "^2.3.0"
},
"scripts": {
"start": "REACT_APP_ENVIRONMENT='development' react-scripts start",
"build": "react-scripts build",
"test": "CI=true react-scripts test",
"test": "react-scripts test",
"eject": "react-scripts eject",
"flow": "./node_modules/.bin/flow",
"predeploy": "REACT_APP_ENVIRONMENT=production npm run build",

View file

@ -1,8 +1,3 @@
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)
@ -51,8 +46,3 @@ table {
border-collapse: collapse;
border-spacing: 0;
}
button {
background: none;
border: none;
}

View file

@ -15,7 +15,3 @@
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,16 +28,4 @@ $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

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

View file

@ -36,8 +36,6 @@ function App() {
dispatch({type:'SOCKET', message: 'LAUNCH', body:{nsp:'', dispatch}});
}
socketConnect();
return () => dispatch({type: 'SOCKET', message: 'DISCONNECT', body: {}});
}, [ state.connect ])
return (

View file

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

View file

@ -2,6 +2,7 @@
div.GameButton {
align-items: stretch;
box-shadow: -2vmin 4vmin 2vmin rgba(83, 53, 35, 0.81);
display: flex;
flex-flow: column nowrap;
height: 20vh;
@ -11,7 +12,7 @@ div.GameButton {
}
div.GameButton__seat {
background: linear-gradient(190deg, rgb(232, 78, 78), rgb(172, 18, 18) 40%, rgb(100, 0, 0));
background-color: red;
height: 10%;
margin: 0 auto;
width: 50%;
@ -29,7 +30,6 @@ div.GameButton__seat {
}
div.GameButton__table {
box-shadow: -2vmin 4vmin 2vmin rgba(83, 53, 35, 0.81);
height: 80%;
margin: 0;
width: 100%;
@ -67,16 +67,15 @@ 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:
@ -100,7 +99,6 @@ 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 {
@ -120,10 +118,6 @@ div.GameButton__player-data {
justify-content: space-around;
max-width: 100%;
&.GameButton__player-data--invisible {
visibility: hidden;
}
&.GameButton__player-data--white {
margin: 0 5vw 1vw 0.5vw;
}
@ -133,7 +127,11 @@ div.GameButton__player-data {
}
}
a.GameButton__link {
.GameButton__link {
@include gameViewLabel;
margin: 0 auto;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}

View file

@ -1,34 +0,0 @@
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,70 +1,14 @@
import React from 'react';
import { Link } from 'react-router-dom';
import RoomDetail from '../../Display/RoomDetail/RoomDetail';
import './Room.scss';
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>
)
const RoomButton = (props) => {
const roomData = props.room;
return (
<>
<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}
<h4 className="RoomButton__room-name">{roomData.name}</h4>
<Link to={`/rooms/${roomData.id}`}>Join Room</Link>
</div>
{roomDetail ? <RoomDetail room={room} /> : <></>}
</>
);
}

View file

@ -1,227 +1,9 @@
@import '../../../../public/stylesheets/partials/mixins';
@import '../../../../public/stylesheets/partials/variables';
div.RoomButton {
display: grid;
grid-template-columns: [left] 1fr [right];
grid-template-rows: [top] 1fr [middle] 4fr [bottom];
height: 20vh;
display: block;
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;
}
.RoomButton__room-name {
text-transform: capitalize;
}
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

@ -1,25 +0,0 @@
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

@ -1,32 +0,0 @@
@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 error={''}/>);
const { getByTestId } = render(<FormError />);
const FormErrorSpan = getByTestId('FormError');
expect(FormErrorSpan).toBeInTheDocument();
});

View file

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

View file

@ -1,74 +1,38 @@
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, 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 { game, user, dispatch, board } = props;
const sizeFlag = `Game__board--size-${ game.boardSize }`
const isHoshi = (posX, posY) =>
hoshiPoints[game.boardSize][`${posX}-${posY}`];
const renderPoints = (boardSize) => {
let i = 0,
boardPoints = [];
const renderPoints = boardSize => {
let i = 0, boardPoints = [];
while (i < boardSize * boardSize) {
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;
const posY = i % boardSize + 1;
console.log(board[`${posX}-${posY}`])
boardPoints.push(
<Point
key={`${posX}-${posY}`}
posX={posX}
posY={posY}
pointData={pointData}
dotData={dotData}
pointData={board[`${posX}-${posY}`]}
// point={board[posX][posY]}
dispatch={dispatch}
user={user}
meta={meta}
hoshi={isHoshi(posX, posY)}
{...props}
/>
);
i++;
); i++;
}
return boardPoints;
};
}
return (
<div className={`Game__board ${sizeFlag}`}>
{ game.id ? renderPoints(game.boardSize) : <></> }
</div>
);
};
}
export default Board;

View file

@ -1,15 +0,0 @@
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

@ -1,31 +0,0 @@
@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

@ -1,194 +0,0 @@
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

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

View file

@ -47,7 +47,6 @@ 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,96 +1,58 @@
import React from "react";
import "./Point.scss";
import React from 'react';
import './Point.scss';
const Point = (props) => {
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 { posX, posY, user, game, dispatch, pointData } = props;
const turn = game.turn > 0 ? 'black' : 'white';
const stone = () => {
if (pointData === 1) return "black";
if (pointData === -1) return "white";
if (pointData === "k") return "ko";
return "none";
};
if (pointData === 1) return 'black'
if (pointData === -1) return 'white'
return 'none'
}
const dot = () => {
if (pointData === 'l') return game.turn;
}
const xFlag = () => {
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 "";
};
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);
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 '';
}
const clickHandle = (e) => {
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;
}
}
return dotData;
};
return (
<div
className={`board__point ${xFlag()} ${yFlag()}`}
onClick={(e) => clickHandle(e)}
onClick={e => clickHandle(e)}
>
<div
className={`board__point__stone ${hoshi ? "hoshi" : ""}`}
<div className="board__point__stone"
data-stone={stone()}
>
<div className="board__point__dot" data-dot={getDot()}></div>
<div className="board__point__dot" data-dot={dot()}></div>
</div>
</div>
);
};
}
export default Point;

View file

@ -5,9 +5,6 @@ div.board__point {
margin: auto;
padding: 0;
vertical-align: middle;
display: flex;
justify-content: center;
align-items: center;
}
div.board__point--top {
@ -46,14 +43,11 @@ 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 {
@ -64,44 +58,13 @@ div.board__point div.board__point__stone div.board__point__dot {
vertical-align: middle;
}
div.board__point__stone{
&[data-stone="white"] {
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"] {
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);
}
&[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 {
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,24 +45,3 @@ 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,56 +1,43 @@
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('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("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('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('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('new_user', (data) => {
console.log('new_user received')
dispatch({ type: 'ROOMS', message: 'NEW_USER', body: data })
})
socket.on("game_connected", (data) => {
dispatch({ type: "GAMES", message: "UPDATE_BOARD", 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) => {
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 });
});
socket.on('update_board', (data) => {
console.log(data)
console.log('update_board received')
dispatch({ type: 'GAMES', message: 'UPDATE_BOARD', body: data.board })
})
return socket;
};
}
export { launch };
export {
launch
}

View file

@ -0,0 +1,7 @@
<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>

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

@ -1,155 +1,70 @@
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";
import React, { useEffect } 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';
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;
useEffect(() => {
const fetchGameAPI = async () => {
const response = await gamesServices.getGameService(gameId);
if (response) {
const action = {
type: "GAMES",
message: "SET_ACTIVE",
body: response,
};
type: 'GAMES',
message: 'SET_ACTIVE',
body: response
}
return dispatch(action);
}
};
}
fetchGameAPI();
}, [gameId, dispatch]);
}, [ 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 },
};
type: 'SOCKET',
message: 'CONNECT_GAME',
body: { game, user, dispatch }
}
return dispatch(action);
}
const action = {
type: "SOCKET",
message: "PASS",
body: { game, player },
};
dispatch(action);
};
roomSocketConnect();
}, [ state.active , dispatch, state.user ] )
return (
<div className="Game" data-testid="Game">
<Menu
showMenu={showMenu}
clickClose={() => setShowMenu(false)}
{...props}
/>
<div
className="Game"
data-testid="Game"
>
<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
handleResignClick={handleResignClick}
handlePassClick={handlePassClick}
playerMeta={
state.user &&
playerBlackMeta.playerBlack &&
state.user === playerBlackMeta.playerBlack
? playerBlackMeta
: playerWhiteMeta
}
turn={state?.meta?.turn}
/>
<PlayerArea />
<Board
dispatch={dispatch}
game={state.active.game}
meta={state.meta}
record={state.active.record}
user={state.user}
board={state.board}
/>
<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}
/>
<PlayerArea />
</div>
<div className="Game__message-container">
@ -158,6 +73,6 @@ const Game = (props) => {
</div>
</div>
);
};
}
export default Game;

View file

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect } from 'react';
import './Home.scss';
import roomsServices from '../../services/api/roomsServices';
import RoomButton from '../../components/Button/Room/Room';
@ -8,9 +8,6 @@ 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 || [];
@ -19,8 +16,6 @@ const Home = props => {
<RoomButton
key={`room-${roomData.id}`}
room={roomData}
roomDetail={roomDetail === roomData.id}
showRoomDetail={showRoomDetail}
/>
))
}

View file

@ -3,9 +3,10 @@
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,5 +1,4 @@
@import '../../../../public/stylesheets/partials/mixins';
@import '../../../../public/stylesheets/partials/variables';
div.main-wrapper {
display: flex;
@ -35,14 +34,3 @@ div.main-wrapper {
}
}
}
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,36 +1,29 @@
import React from "react";
import { Link } from "react-router-dom";
import "./NavBar.scss";
import Logo from "../../../components/Display/Logo/Logo";
import React from 'react';
import { Link } from 'react-router-dom';
import './NavBar.scss';
import Logo from '../../../components/Display/Logo/Logo';
const NavBar = (props) => {
const NavBar = ({ state }) => {
return (
<div className="NavBar" data-testid="NavBar">
<Link to="/home" >
<div className="NavBar__logo">
<Logo />
</div>
<div className="NavBar__logo"><Logo /></div>
</Link>
<Link to="/home" >
<div className="NavBar__menu-item NavBar__home">
<p className="--link">Find a Game</p>
</div>
<div className="NavBar__menu-item NavBar__home"><p>Find a Game</p></div>
</Link>
<Link to="/news">
<div className="NavBar__menu-item NavBar__news">
<p className="--link">News</p>
</div>
<div className="NavBar__menu-item NavBar__news"><p>News</p></div>
</Link>
<Link to="/account">
<div className="NavBar__menu-item NavBar__account">
{state.user.username ? state.user.username : <></>}
</div>
<div className="NavBar__menu-item NavBar__acount">{props.user ? props.user.username : <></>}</div>
</Link>
</div>
);
};
}
export default NavBar;

View file

@ -1,21 +1,18 @@
export const authReducer = (state, action) => {
switch (action.message) {
case "LOGIN":
case 'LOGIN':
return loginReducer(state, action);
case "SIGNUP":
case 'SIGNUP':
return loginReducer(state, action);
case "GUEST":
return loginReducer(state, action);
case "LOGOUT":
case 'LOGOUT':
return state;
default:
return state;
}
};
}
function loginReducer(state, action) {
const newUser = action.body;

View file

@ -1,104 +1,62 @@
import { stateReducer } from "../reducer";
import { stateReducer } from '../reducer';
export const gamesReducer = (state, action) => {
switch(action.message) {
case "SET_GAMES": {
const games = formatGames(action);
case 'SET_GAMES':
const games = formatGames(action);;
return {...state, games};
}
case "JOIN_REQUEST": {
return joinRequest(state, action);
case 'JOIN_REQUEST':
if (!Object.entries(state.user).length) {
const errAction = {
type: 'ERR',
message: 'JOIN_GAME_ERROR',
body: {joinGameError: 'user not logged in'}
}
case "UPDATE_BOARD": {
return updateBoard(state, action);
return stateReducer(state, errAction)
}
const id = action.body;
return {...state, joinGame: id};
case "GAME_RESIGN": {
return gameResign(state, action);
}
case 'UPDATE_BOARD':
console.log(action.body)
return {...state, board: action.body};
case "SET_ACTIVE": {
case 'SET_ACTIVE':
return {...state, active: action.body};
}
case "GAME_END": {
return gameEnd(state, action);
}
default: {
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 "?";
case 'D':
return `${rank.slice(1)}${rank[0].toLowerCase()}`
case 'K':
return `${rank.slice(1)}${rank[0].toLowerCase()}`
case 'U':
return '?'
default:
return "?";
return '?'
}
}
function formatGames(action) {
const games = [...action.body].map((game) => {
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,59 +1,45 @@
import { stateReducer } from "../reducer";
const io = require("../../io");
import { stateReducer } from '../reducer';
const io = require('../../io');
export const socketReducer = (state, action) => {
switch(action.message) {
case "CONNECTED":
return { ...state, connect: { type: "home", location: action.body.nsp } };
case "LAUNCH": {
case 'CONNECTED':
console.log(action.body.nsp)
return {...state, connect: { type: 'home', location: action.body.nsp } }
case 'LAUNCH': {
const {nsp, dispatch} = action.body;
const launchedSocket = io.launch(nsp, dispatch);
return {...state, socket: launchedSocket};
}
case "CONNECT_ROOM": {
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) {
const { user, game, dispatch } = action.body;
@ -61,45 +47,23 @@ function connectGame(state, action) {
let updatedState;
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};
const socket = updatedState.socket;
socket.emit("connect_game", { user, game });
socket.emit('connect_game', {user, game});
return {...updatedState};
}
function makeMove (state, action) {
// const { user, game, room, board, move } = action.body;
const socket = state.socket;
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 });
console.log(action)
socket.emit('make_move', {...action.body});
return state;
}

View file

@ -1,58 +1,43 @@
import config from "../config";
import config from '../config';
const authEndpoint = config.authAddress;
const signupEndpoint = `${authEndpoint}/signup`;
const loginEndpoint = `${authEndpoint}/login`;
const guestEndpoint = `${authEndpoint}/guest`;
const signupEndpoint = `${authEndpoint}/signup`
const loginEndpoint = `${authEndpoint}/login`
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 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,
guestService,
};
signupService
}

View file

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

View file

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

View file

@ -1,16 +1,15 @@
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 guestServices = require("../services/guestServices");
const userQueries = require('../data/queries/user');
const { hashPassword, compareHash } = require('../services/bcrypt');
const signToken = require('../services/signToken');
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);
@ -21,18 +20,18 @@ 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);
signToken(res, newUser)
res.status(201).json({...newUser});
} catch (err) {
}
catch (err) {
res.status(500).json(err);
}
};
}
const login = async (req, res, next) => {
checkValidationErrors(req, res);
@ -43,14 +42,14 @@ const login = async (req, res, next) => {
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};
@ -58,31 +57,14 @@ const login = async (req, res, next) => {
signToken(res, authorizedUser);
res.send({...authorizedUser}).status(200);
} catch (e) {
res.status(500).send({ errors: e });
}
};
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 });
catch (err) {
res.status(500).send({errors: err});
}
}
};
module.exports = {
signup,
login,
guest,
};
login
}

View file

@ -1,81 +1,49 @@
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);
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('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.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.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");
});
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,18 +1,17 @@
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);
table.boolean("placement").notNullable().default(false);
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.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,97 +1,48 @@
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")
.from('game')
.select(selection)
.where({ "game.id": gameId })
.leftJoin("time_setting", function () {
this.on("time_setting.id", "=", "game.time_setting");
});
.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 })
const games = await knex('game')
.where({'room': roomId})
.select(gameOverviewSelect);
return games;
};
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;
}
};
const insertGame = async (game) => {
}
module.exports = {
findGameById,
findGameByRoom,
insertGame,
endGame,
};
insertGame
}

View file

@ -1,47 +1,11 @@
const knex = require("../db");
const knex = require('../db');
const findGameRecord = async (gameId) => {
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;
return await knex('move')
.where({'game': gameId, 'game_record': true})
.select('*');
}
};
module.exports = {
findGameRecord,
addMove,
};
findGameRecord
}

View file

@ -5,8 +5,8 @@ exports.seed = function(knex) {
.then(function () {
// Inserts seed entries
return knex('room').insert([
{name: 'main', description: 'A general place to play Go'},
{name: 'private', description: 'A private place to play Go', private: true},
{id: 1, name: 'main', description: 'A general place to play Go'},
{id: 2, 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([
{main_time: 'untimed', time_period: 1, period_length: 0, overtime: 'untimed', overtime_period: 0, overtime_length: 0},
{id: 1, 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').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}));
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},
]);
});
};

View file

@ -1,72 +1,33 @@
exports.seed = async function (knex) {
exports.seed = function(knex) {
// Deletes ALL existing entries
return await knex("game")
.del()
.then(async function () {
return knex('game').del()
.then(function () {
// Inserts seed entries
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(
[
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,
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
},
{
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,
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
},
{
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 });
});
});
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
}
]);
});
};

View file

@ -1,27 +1,11 @@
exports.seed = async function(knex) {
exports.seed = function(knex) {
// Deletes ALL existing entries
return await knex('message').del()
.then(async function () {
return knex('message').del()
.then(function () {
// Inserts seed entries
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(() => {})
return knex('message').insert([
{id: 1, content: 'Hey! Welcome to the general room!', room: 1, user: 2}
]);
});
};

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
const {check, body, validationResult} = require('express-validator');
const {check, sanitize, 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),
body('username').escape()
sanitize('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": "^4.0.1",
"bcrypt": "^3.0.7",
"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.21.1",
"knex": "^0.20.7",
"morgan": "~1.9.1",
"pg": "^8.1.0",
"pg": "^7.17.0",
"socket.io": "^2.3.0"
}
}

View file

@ -1,20 +1,10 @@
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("/guest", authController.guest);
router.post('/signup', signupValidationRules(), validate, authController.signup);
router.post('/login', loginValidationRules(), validate, authController.login);
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,25 +23,25 @@ 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) {
@ -52,11 +52,11 @@ app.use(function (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

@ -1,422 +0,0 @@
/*----- 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,157 +1,33 @@
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 = {};
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;
const initGame = (game) => {
gamesInProgress[game.id] = new Game(game)
return gamesInProgress[game.id].initGame();
}
} catch (e) {
console.log(e);
move.success = false;
} finally {
return move;
}
};
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();
const makeMove = (game, move) => {
if (!gamesInProgress[game.id]) initGame(game);
const newState = gamesInProgress[game.id].makeMove(move);
return {...newState}
}
return this.getDataForUI(id);
},
async makeMove({ id, move }) {
// check cache
if (!gamesInProgress[id]) {
try {
let gameRecord;
if (moveQueries) {
gameRecord = await moveQueries.findGameRecord(id);
const getBoard = (gameId) => {
return gamesInProgress[gameId].getBoardState();
}
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);
}
},
getDataForUI: (id) => {
return {
board: gamesInProgress[id].legalMoves,
territory: gamesInProgress[id].territory,
...gamesInProgress[id].getMeta(),
};
},
dropGame: (id) => {
return { message: `${delete gamesInProgress[id]}` };
},
getAllGames: () => {
const 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);
module.exports = {
makeMove,
getAllGames,
getBoard,
initGame
}
} catch (e) {
console.log(e);
}
return this.getDataForUI(id);
},
};
};
module.exports = GameService;

View file

@ -1,25 +0,0 @@
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,157 +1,53 @@
// TODO const someSocketLogic = require('./middleware/sockets/...');
const socketIO = require("socket.io");
// TODO const someSocketLogic = require('./middleware/socketssockets/...');
const socketIO = require('socket.io');
const io = socketIO({ cookie: false });
// 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,
});
const gameQueries = require('./data/queries/game');
const gameServices = require('./services/gameServices');
io.on("connection", async (socket) => {
socket.emit("connected", { message: "socket connected" });
socket.on("connect_room", async (data) => {
io.on('connection', socket=> {
socket.emit('connected', {message: 'socket connected'});
socket.on('connect_room', data => {
if (data.user && data.user.email) {
delete data.user.email;
}
const room= data.room;
const roomIo = io.of(room);
roomIo.on("connection", async (socket) => {
socket.emit("connected");
socket.emit("new_user", data);
socket.on("connect_game", (data) => {
roomIo.on('connection', socket => {
socket.emit('connected')
socket.emit('new_user', data);
socket.on('connect_game', data => {
const game = `game-${data.game.id}`;
socket.join(game, async () => {
// 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 });
// ! 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)
});
});
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 { board, message, ...meta } = await gameServices.makeMove({
id: data.game.id,
move,
});
const socketAction = message ? "error" : "update_board";
const updatedBoard = gameServices.makeMove(1, move);
socket.join(gameNsp, () => {
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);
io.of(room).to(gameNsp).emit('update_board', updatedBoard)
});
}
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

@ -1,434 +1,69 @@
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", () => {
afterEach(() => gameServices.dropGame(1));
it("init game returns game board", (done) => {
gameServices
.initGame({ id: 1, handicap: 4 })
.board.should.eql(fourHandicapBoard);
describe('game services', () => {
it('init game returns game board', done => {
gameServices.initGame({id: 1, handicap: 4})
gameServices.getBoard(1).should.eql(fourHandicapBoard)
done();
});
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: {} });
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);
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 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("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 initialMeta = {
winner: null,
turn: 0,
const moveOneMeta = {
gameRecord: [
{player: 'white', pos: { x:6, y:3 }}
],
pass: 0,
komi: 6.5,
handicap: 0,
boardSize: 19,
playerState: {
bCaptures: 0,
wCaptures: 0,
bScore: 0,
wScore: 0,
wCaptures: 0,
wScore: 0
},
gameRecord: [],
score: 0,
};
turn: 1,
winner: null
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 809 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 359 KiB