fuzzyworld

dao-game / commit

Starting to work on remote React game. Initial thought is to properly implement Game subclass that automatically handles the peer connection stuff.

41db8b5863fc12587e5eab2f3b604b296340fa0f · Pavel Safronov · 2022-03-23 22:24:03 -0700 · parents 9d181b04a478

Starting to work on remote React game. Initial thought is to properly implement Game subclass that automatically handles the peer connection stuff.
 src/game/remote-game-2.ts | 232 ++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 232 insertions(+)

diff --git a/src/game/remote-game-2.ts b/src/game/remote-game-2.ts
new file mode 100644
index 0000000..f3aebf6
--- /dev/null
+++ b/src/game/remote-game-2.ts
@@ -0,0 +1,232 @@
+import Peer, { PeerJSOption } from 'peerjs';
+import { showModal } from '../utils/modal';
+import { useSessionStorage } from '../utils/storage';
+import { showSnackbar } from "../utils/snackbar";
+import { Game, Move, MoveResult } from './game';
+import { Coord } from './coord';
+import { generateHash } from '../utils/hashing';
+import { delay } from '../utils/delay';
+import { copyToClipboard } from '../utils/clipboard';
+import { detect } from 'detect-browser';
+
+interface LocalData {
+    id?: string;
+    peerId?: string;
+}
+interface Pos {
+    x: number;
+    y: number;
+}
+interface PeerData {
+    mousePos?: Pos;
+    move?: Move;
+}
+
+const peerOptions: PeerJSOption = { debug: 0 };
+const { getter: getData, setter: setData } = useSessionStorage<LocalData>('dao-game', { id: undefined, peerId: undefined });
+const otherMouseElement = document.getElementById('other-mouse')!;
+const shareContainer = document.getElementById('share-container')!;
+const shareLinkButton = document.getElementById('share-link')!;
+const minString = (a: string, b: string): string => (a < b ? a : b);
+const maxString = (a: string, b: string): string => (a > b ? a : b);
+const getFirstPlayerId = (playerId1: string, playerId2: string) => {
+    const hash1 = generateHash(playerId1);
+    const hash2 = generateHash(playerId2);
+    const prod = hash1 * hash2;
+    const isEven = prod % 2 == 0;
+    const id = isEven ? minString(playerId1, playerId2) : maxString(playerId1, playerId2);
+    return id;
+};
+const dropFileExtension = (path: string) => {
+    const lastSlashIndex = path.lastIndexOf('/');
+    const dirName = path.substring(0, lastSlashIndex);
+    return dirName;
+}
+const showMessageAndLeave = async (title: string, text: string) => {
+    await showModal({ title, text, willBackgroundCancel: false });
+    sessionStorage.clear();
+    const { protocol, host, pathname } = location;
+    const dir = dropFileExtension(pathname);
+    const newUrl = `${protocol}//${host}${dir}`
+    document.location = newUrl;
+}
+
+export class PeerGame extends Game {
+    public conn: Peer.DataConnection;
+    public selfId: string;
+    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> {
+        let { id, peerId } = getData();
+        const main = document.getElementsByTagName('main')[0];
+        let game: PeerGame | undefined = undefined;
+
+        const handleConn = async (conn: Peer.DataConnection) => {
+            shareContainer.classList.add('invisible');
+
+            await new Promise<void>((resolve) => {
+                conn.on('open', () => resolve());
+            });
+
+            id = peer.id;
+            peerId = conn.peer;
+            setData({ id, peerId });
+
+            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));
+            });
+
+            let lastTimerId: NodeJS.Timeout | undefined = undefined;
+            conn.on('data', (data: string) => {
+                if (!game) return;
+
+                const peerData = JSON.parse(data) as PeerData;
+                const { mousePos, move } = peerData;
+
+                if (mousePos) {
+                    const isValidPos = mousePos.x !== -1 && mousePos.y !== -1;
+                    if (isValidPos) {
+                        const xRatio = mousePos.x;
+                        const yRatio = mousePos.y;
+                        const xTable = xRatio * table.clientWidth;
+                        const yTable = yRatio * table.clientHeight;
+                        const x = table.offsetLeft + xTable;
+                        const y = table.offsetTop + yTable;
+                        const style = `left:${x - otherMouseElement.clientWidth / 2}px;top:${y}px;display:block;`;
+                        otherMouseElement.setAttribute('style', style);
+                        if (lastTimerId !== undefined) {
+                            clearTimeout(lastTimerId);
+                        }
+                        lastTimerId = setTimeout(() => {
+                            otherMouseElement.setAttribute('style', '');
+                            lastTimerId = undefined;
+                        }, 3000);
+                    } else if (lastTimerId !== undefined) {
+                        otherMouseElement.setAttribute('style', '');
+                    }
+                }
+
+                const isWaitingForRemoteMove = peerId === game.currentPlayerId;
+                if (isWaitingForRemoteMove && move) {
+                    const isMoveLegal = game.getPiece(move.source) === game.currentPlayerId && game.getPiece(move.dest) === null;
+                    if (isMoveLegal) {
+                        game.makeMove(move.source, move.dest);
+                    } else {
+                        console.log({ isMoveLegal, move });
+                    }
+                }
+            });
+            conn.on('error', (err) => {
+                showMessageAndLeave(`Connection error`, err);
+            });
+            conn.on('close', () => {
+                showMessageAndLeave(`Connection closed`, 'Connection closed for some reason')
+            });
+        };
+
+        const { host, protocol, pathname, search } = location;
+        const queryParams = new URLSearchParams(search);
+
+        const peer = id === undefined ? new Peer(peerOptions) : new Peer(id, peerOptions);
+        window.onunload = () => peer.destroy();
+        peer.on('connection', (conn) => handleConn(conn));
+        peer.on('error', async (err) => {
+            await showMessageAndLeave('Peer error encountered', err);
+        });
+        peer.on('open', (trueId) => {
+            id = trueId;
+            peerId = queryParams.get('peerId') ?? peerId;
+            setData({ id, peerId });
+
+            if (peerId) {
+                console.log(`${id} connecting to peer "${peerId}"`);
+                const conn = peer.connect(peerId);
+                handleConn(conn);
+            } else {
+                queryParams.set('peerId', id);
+                const newSearch = queryParams.toString();
+                const newUrl = `${protocol}//${host}${pathname}?${newSearch}`;
+                shareLinkButton.onclick = () => {
+                    showSnackbar('Url copied to clipboard');
+                    copyToClipboard(newUrl);
+                };
+                shareContainer.classList.remove('invisible');
+            }
+        });
+        peer.on('disconnected', async () => {
+            await showMessageAndLeave('Peer disconnected', 'For some reason')
+        });
+        while (game === undefined) {
+            console.log('Waiting to connect to peer');
+            await delay(500);
+        }
+        return game;
+    }
+}
+
+const isBrowserSupported = () => {
+    const browser = detect();
+    switch (browser && browser.name) {
+        case 'chrome':
+        case 'firefox':
+            return true;
+        default:
+            return false;
+    }
+}
+export const playGame = async () => {
+    if (isBrowserSupported()) {
+        await PeerGame.createPeerGame();
+    } else {
+        showMessageAndLeave(
+            'Unsupported browser',
+            '<p>Your current browser is not yet supported.</p>' +
+            '<p>This PeerJs issue is tracking support in Microsoft Edge and Safari: </p>' +
+            '<p><a href="https://github.com/peers/peerjs/issues/899" target="_blank">https://github.com/peers/peerjs/issues/899</a></p>')
+    }
+};