dao-game / commit
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 });