fuzzyworld

dao-game / commit

Starting work on React multiplayer implementation.

ecef404bb612b55fe918592fba17a1fa8fe65161 · Pavel Safronov · 2022-03-29 10:31:45 -0700 · parents 41db8b5863fc

Starting work on React multiplayer implementation.
 package-lock.json         |  14 ++---
 package.json              |   2 +-
 src/game/remote-game-2.ts |  51 ++++-------------
 src/react/App-Remote.css  |  39 +++++++++++++
 src/react/App-Remote.tsx  | 143 ++++++++++++++++++++++++++++++++++++++++++++++
 tools/settings.ts         |   3 +-
 6 files changed, 202 insertions(+), 50 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index e0567e3..6978cf2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -18,7 +18,7 @@
       },
       "devDependencies": {
         "@types/node": "^17.0.23",
-        "@types/react": "^17.0.42",
+        "@types/react": "^17.0.43",
         "@types/react-modal": "^3.13.1",
         "esbuild": "^0.14.27",
         "rimraf": "^3.0.2",
@@ -84,9 +84,9 @@
       "dev": true
     },
     "node_modules/@types/react": {
-      "version": "17.0.42",
-      "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.42.tgz",
-      "integrity": "sha512-nuab3x3CpJ7VFeNA+3HTUuEkvClYHXqWtWd7Ud6AZYW7Z3NH9WKtgU+tFB0ZLcHq+niB/HnzLcaZPqMJ95+k5Q==",
+      "version": "17.0.43",
+      "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.43.tgz",
+      "integrity": "sha512-8Q+LNpdxf057brvPu1lMtC5Vn7J119xrP1aq4qiaefNioQUYANF/CYeK4NsKorSZyUGJ66g0IM+4bbjwx45o2A==",
       "dev": true,
       "dependencies": {
         "@types/prop-types": "*",
@@ -932,9 +932,9 @@
       "dev": true
     },
     "@types/react": {
-      "version": "17.0.42",
-      "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.42.tgz",
-      "integrity": "sha512-nuab3x3CpJ7VFeNA+3HTUuEkvClYHXqWtWd7Ud6AZYW7Z3NH9WKtgU+tFB0ZLcHq+niB/HnzLcaZPqMJ95+k5Q==",
+      "version": "17.0.43",
+      "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.43.tgz",
+      "integrity": "sha512-8Q+LNpdxf057brvPu1lMtC5Vn7J119xrP1aq4qiaefNioQUYANF/CYeK4NsKorSZyUGJ66g0IM+4bbjwx45o2A==",
       "dev": true,
       "requires": {
         "@types/prop-types": "*",
diff --git a/package.json b/package.json
index 8f56050..6b0a5ba 100644
--- a/package.json
+++ b/package.json
@@ -29,7 +29,7 @@
   },
   "devDependencies": {
     "@types/node": "^17.0.23",
-    "@types/react": "^17.0.42",
+    "@types/react": "^17.0.43",
     "@types/react-modal": "^3.13.1",
     "esbuild": "^0.14.27",
     "rimraf": "^3.0.2",
diff --git a/src/game/remote-game-2.ts b/src/game/remote-game-2.ts
index f3aebf6..f570cad 100644
--- a/src/game/remote-game-2.ts
+++ b/src/game/remote-game-2.ts
@@ -23,7 +23,7 @@ interface PeerData {
 }
 
 const peerOptions: PeerJSOption = { debug: 0 };
-const { getter: getData, setter: setData } = useSessionStorage<LocalData>('dao-game', { id: undefined, peerId: undefined });
+const { getter: getData, setter: setData } = useSessionStorage<LocalData>('dao-game-2', { id: undefined, peerId: undefined });
 const otherMouseElement = document.getElementById('other-mouse')!;
 const shareContainer = document.getElementById('share-container')!;
 const shareLinkButton = document.getElementById('share-link')!;
@@ -51,42 +51,27 @@ const showMessageAndLeave = async (title: string, text: string) => {
     document.location = newUrl;
 }
 
-export class PeerGame extends Game {
+export class PeerGame2 extends Game {
     public conn: Peer.DataConnection;
     public selfId: string;
+    public onMouseMoveCallback: (pos: Coord) => void = () => { };
+    public peerMousePositionCallback: (pos: Coord) => void = () => { };
+    
     public constructor(player1Id: string, player1Name: string, player2Id: string, player2Name: string, selfId: string, conn: Peer.DataConnection) {
         super(player1Id, player1Name, player2Id, player2Name);
         this.selfId = selfId;
         this.conn = conn;
     }
-    public override setActiveUi(): void {
-        const { currentPlayerId, selfId } = this;
-
-        const isLocalTurn = currentPlayerId === selfId;
-        if (isLocalTurn) {
-            super.setActiveUi();
-        }
-    }
     public override makeMove(source: Coord, dest: Coord): Promise<MoveResult> {
         const result = super.makeMove(source, dest);
         const peerData: PeerData = { move: { source, dest, playerId: this.currentPlayerId } };
         this.conn.send(JSON.stringify(peerData));
         return result;
     }
-    public override async handleMoveResult(result: MoveResult): Promise<void> {
-        const { isTerminal, message } = result;
-        if (isTerminal) {
-            await showMessageAndLeave('Game Over', message!);
-        } else {
-            this.resetUi();
-            this.setActiveUi();
-        }
-    }
-
-    public static async createPeerGame(): Promise<PeerGame> {
+    public static async createPeerGame(): Promise<PeerGame2> {
         let { id, peerId } = getData();
         const main = document.getElementsByTagName('main')[0];
-        let game: PeerGame | undefined = undefined;
+        let game: PeerGame2 | undefined = undefined;
 
         const handleConn = async (conn: Peer.DataConnection) => {
             shareContainer.classList.add('invisible');
@@ -101,25 +86,9 @@ export class PeerGame extends Game {
 
             const isLocalP1 = getFirstPlayerId(id, peerId) === id;
             game = isLocalP1 ?
-                new PeerGame(id, "Local", peerId, "Remote", id, conn) :
-                new PeerGame(peerId, "Remote", id, "Local", id, conn);
-            game.resetUi();
-            game.setActiveUi();
-            main.classList.remove('invisible');
-
-            const table = document.getElementById('table')!;
-            table.addEventListener('mousemove', (ev: MouseEvent) => {
-                const xTable = ev.clientX - table.offsetLeft;
-                const yTable = ev.clientY - table.offsetTop;
-                const xRatio = xTable / table.clientWidth;
-                const yRatio = yTable / table.clientHeight;
-                const peerData: PeerData = { mousePos: { x: xRatio, y: yRatio } };
-                conn.send(JSON.stringify(peerData));
-            });
-            table.addEventListener('mouseleave', () => {
-                const peerData: PeerData = { mousePos: { x: -1, y: -1 } };
-                conn.send(JSON.stringify(peerData));
-            });
+                new PeerGame2(id, "Local", peerId, "Remote", id, conn) :
+                new PeerGame2(peerId, "Remote", id, "Local", id, conn);
+
 
             let lastTimerId: NodeJS.Timeout | undefined = undefined;
             conn.on('data', (data: string) => {
diff --git a/src/react/App-Remote.css b/src/react/App-Remote.css
new file mode 100644
index 0000000..738cea6
--- /dev/null
+++ b/src/react/App-Remote.css
@@ -0,0 +1,39 @@
+@import url('./Base.css');
+
+.grid-container {
+  display: grid;
+  grid-template-areas:
+    '0 1 2 3'
+    '4 5 6 7'
+    '8 9 10 11'
+    '12 13 14 15';
+  width: calc(var(--cell-size) * 4);
+  height: calc(var(--cell-size) * 4);
+  margin: auto;
+}
+
+.modal {
+  font-family: Arial, Helvetica, sans-serif;
+  font-size: xx-large;
+  /* margin: 100px 100px 100px 100px; */
+  margin: auto;
+  text-align: center;
+  background-color: white;
+  user-select: none;
+  border: 1px solid black;
+  padding: 20px;
+}
+
+.info {
+  font-family: var(--font-family);
+  font-size: 20px;
+  max-width: var(--max-width);
+  margin: auto;
+}
+
+.other-mouse {
+  position: absolute;
+  z-index: 1000;
+  pointer-events: none;
+  display: none;
+}
diff --git a/src/react/App-Remote.tsx b/src/react/App-Remote.tsx
new file mode 100644
index 0000000..9fd7395
--- /dev/null
+++ b/src/react/App-Remote.tsx
@@ -0,0 +1,143 @@
+import * as React from 'react';
+import { useEffect, useState } from 'react';
+import { Coord } from '../game/coord';
+import { IGame, MoveResult } from '../game/game';
+import Modal from 'react-modal';
+import './App.css';
+import { Tile } from './Tile';
+import { Controls } from './Controls';
+import { delay } from '../utils/delay';
+import { Status } from './Status';
+import { PeerGame2 } from '../game/remote-game-2';
+import cursorUrl from '../../public/cursor.png';
+
+export type Props = {
+    game: PeerGame2;
+};
+
+export const AppRemote = (props: Props): React.ReactElement => {
+    const { game } = props;
+
+    const [selectedPiecePos, setSelectedPiecePos] = useState<Coord>()
+    const [validMoves, setValidMoves] = useState<Coord[]>([]);
+    const [currentPlayerId, setCurrentPlayerId] = useState<string>(game.currentPlayerId);
+    const [moveResult, setMoveResult] = useState<MoveResult>();
+    const [isModalOpen, setIsModalOpen] = useState(false);
+    const [isControlsDisabled, setIsControlsDisabled] = useState(!game.canUndoMove);
+    const [peerMousePos, setPeerMousePos] = useState<Coord>();
+
+    game.peerMousePositionCallback = (pos) => {
+        setPeerMousePos(pos);
+    }
+
+    useEffect((): void => {
+        const slowUpdate = async () => {
+            if (moveResult && moveResult.isTerminal) {
+                await delay(750);
+                setIsModalOpen(true);
+            }
+        };
+        slowUpdate();
+    }, [moveResult]);
+
+    const makeMove = async (from: Coord, to: Coord) => {
+        const result = await game.makeMove(from, to);
+        setCurrentPlayerId(game.currentPlayerId);
+        setSelectedPiecePos(undefined);
+        setValidMoves([]);
+        setMoveResult(result);
+        setIsControlsDisabled(false);
+    };
+    const undoMove = () => {
+        const move = game.undoMove();
+        console.debug({ move });
+        setCurrentPlayerId(game.currentPlayerId);
+        setSelectedPiecePos(undefined);
+        setValidMoves([]);
+        setMoveResult(undefined);
+        setIsControlsDisabled(!game.canUndoMove);
+    };
+
+    const allTiles: React.ReactElement[] = [];
+    for (let r = 0; r < game.size.row; r++) {
+        for (let c = 0; c < game.size.col; c++) {
+            const pos = new Coord(r, c);
+            const pieceClickCb = () => {
+                const isUnselectClick = selectedPiecePos !== undefined && selectedPiecePos.equals(pos);
+                if (isUnselectClick) {
+                    setSelectedPiecePos(undefined);
+                    setValidMoves([]);
+                } else {
+                    setSelectedPiecePos(pos);
+                    setValidMoves([...game.getValidMoves(pos)]);
+                }
+            };
+            const validMoveClickCb = async () => {
+                await makeMove(selectedPiecePos!, pos)
+            };
+            const tile =
+                <Tile
+                    pos={pos}
+                    game={game}
+                    validMoves={validMoves}
+                    selectedPiecePos={selectedPiecePos}
+                    currentPlayerId={currentPlayerId}
+                    pieceClickCb={() => pieceClickCb()}
+                    validMoveClickCb={() => validMoveClickCb()}
+                />;
+            allTiles.push(tile);
+        }
+    }
+
+    const divRef = React.createRef<HTMLDivElement>();
+    return (<>
+        <div
+            ref={divRef}
+            onMouseMove={(ev) => {
+                const div = divRef.current!;
+                const xTable = ev.clientX - div.offsetLeft;
+                const yTable = ev.clientY - div.offsetTop;
+                const xRatio = xTable / div.clientWidth;
+                const yRatio = yTable / div.clientHeight;
+                game.onMouseMoveCallback(new Coord(xRatio, yRatio));
+            }}>
+            <Status
+                currentPlayerId={currentPlayerId}
+                player1Id={game.player1Id}
+                player1Name={game.player1Name}
+                player2Id={game.player2Id}
+                player2Name={game.player2Name}
+            />
+            <div className='grid-container'>
+                {allTiles}
+            </div>
+            <Controls onUndo={undoMove} isDisabled={isControlsDisabled} />
+            <div className='info'>
+                The game consists of only one simple rule and three ways to win. Rules:
+                <ul>
+                    <li>Pieces move in a straight line, including diagonally, as far as possible with no jumps.</li>
+                </ul>
+                Ways to win:
+                <ol>
+                    <li>Arrange your pieces in a straight line on the board (not diagonally)</li>
+                    <li>Occupy four corners of the board</li>
+                    <li>Form a 2x2 square anywhere on the board</li>
+                </ol>
+            </div>
+            <Modal isOpen={isModalOpen} className='modal'>
+                <h2>Game Over</h2>
+                <div>
+                    {moveResult?.message}
+                </div>
+                <div>
+                    <button onClick={() => location.reload()}>
+                        Restart game
+                    </button>
+                </div>
+            </Modal>
+        </div>
+        <div className='other-mouse'>
+            <img src={cursorUrl} />
+        </div>
+    </>);
+}
\ No newline at end of file
diff --git a/tools/settings.ts b/tools/settings.ts
index 57b3fd7..6f4d125 100644
--- a/tools/settings.ts
+++ b/tools/settings.ts
@@ -16,7 +16,8 @@ export const buildOptions: esbuild.BuildOptions = {
     target: [
         'es5',
         'es2016'
-    ]
+    ],
+    loader: { '.png': 'dataurl' },
 };
 export function clearOutDir() {
     fs.rmSync(outDir, { recursive: true, force: true });