fuzzyworld

dao-game / commit

Multiplayer pretty much works.

47dfe48abb6056045f0469d690c5caf8748708ae · Pavel Safronov · 2022-03-20 01:06:08 -0700 · parents c3c52dafd09a

Multiplayer pretty much works.
 .prettierrc           |   3 +-
 .vscode/settings.json |   1 +
 public/cursor.png     | Bin 0 -> 504 bytes
 public/index.html     |   4 +-
 public/online.html    |   2 +-
 public/styles.css     |   8 +-
 readme.md             |   4 +-
 src/game.ts           | 300 ++++++++++++++++++----------------
 src/local-game.ts     | 443 ++++++++++++++++++++++++--------------------------
 src/remote-game.ts    | 149 ++++++++++-------
 src/storage.ts        |   2 +-
 src/types.ts          | 231 +++++++++++++-------------
 src/utils.ts          |  20 +++
 13 files changed, 613 insertions(+), 554 deletions(-)

diff --git a/.prettierrc b/.prettierrc
index 91ef31e..8350497 100644
--- a/.prettierrc
+++ b/.prettierrc
@@ -3,5 +3,6 @@
   "useTabs": false,
   "semi": true,
   "trailingComma": "es5",
-  "singleQuote": true
+  "singleQuote": true,
+  "printWidth": 140
 }
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 03ad85b..989167c 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -3,6 +3,7 @@
         "esbuild",
         "Newp",
         "outfile",
+        "peerjs",
         "servedir",
         "Todos"
     ]
diff --git a/public/cursor.png b/public/cursor.png
new file mode 100644
index 0000000..b34724b
Binary files /dev/null and b/public/cursor.png differ
diff --git a/public/index.html b/public/index.html
index 2bcc389..dc8de5a 100644
--- a/public/index.html
+++ b/public/index.html
@@ -60,7 +60,9 @@
             </p>
         </div>
 
-        <canvas id="back-canvas"></canvas>
+        <div id="other-mouse">
+            <img src="./cursor.png" alt="cursor" />
+        </div>
 
     </body>
 
diff --git a/public/online.html b/public/online.html
index ad554d7..5c4d114 100644
--- a/public/online.html
+++ b/public/online.html
@@ -12,7 +12,7 @@
         <script defer src="bundle.js"></script>
     </head>
 
-    <body>
+    <body style="display: none;">
         <div class="invisible">
             <p class="piece p1-piece" id="default-p1-piece"></p>
             <p class="piece p2-piece" id="default-p2-piece"></p>
diff --git a/public/styles.css b/public/styles.css
index 6f25d87..6f9a281 100644
--- a/public/styles.css
+++ b/public/styles.css
@@ -31,11 +31,11 @@ div {
     flex: 1; */
 }
 
-#back-canvas {
+#other-mouse {
   position: absolute;
-  top: 0px;
-  left: 0px;
-  z-index: -1000;
+  /* top: -16px;
+  left: -32px; */
+  z-index: 1000;
   pointer-events: none;
 }
 
diff --git a/readme.md b/readme.md
index 17faa1b..46f2a15 100644
--- a/readme.md
+++ b/readme.md
@@ -19,5 +19,5 @@ This project is using [esbuild](https://esbuild.github.io/getting-started/#build
 
 ## Todos
 
-1. Use esbuild and rewrite app in TypeScript
-2. Online multiplayer
+1. Online multiplayer
+2. There's a bug where opening a new tab results in the same session data, look into this and figure out why and how to prevent this
diff --git a/src/game.ts b/src/game.ts
index 4b58f6d..f5a5881 100644
--- a/src/game.ts
+++ b/src/game.ts
@@ -1,166 +1,178 @@
-import { Coord, GameBoard, Move, MoveResult } from "./types";
+import { Coord, GameBoard, Move, MoveResult } from './types';
 
 export interface IGame {
-    size: Coord;
-    moveHistory: Move[];
-    player1Id: string;
-    player2Id: string;
-    currentPlayerId: string;
-    canUndoMove: boolean;
-    getPlayerPositions: (playerId: string) => Coord[];
-    coordToBoardIndex: (coord: Coord) => number;
-    getValidMoves: (start: Coord) => Coord[];
-    makeMove: (source: Coord, dest: Coord) => MoveResult;
-    undoMove: () => Move;
+  size: Coord;
+  moveHistory: Move[];
+  player1Id: string;
+  player2Id: string;
+  currentPlayerId: string;
+  canUndoMove: boolean;
+  getPlayerPositions: (playerId: string) => Coord[];
+  coordToBoardIndex: (coord: Coord) => number;
+  getValidMoves: (start: Coord) => Coord[];
+  makeMove: (source: Coord, dest: Coord) => Promise<MoveResult>;
+  undoMove: () => Move;
+  getPiece(pos: Coord): string | undefined;
 }
 
 export class Game implements IGame {
-    private isGameOver: boolean;
-    private readonly gameBoard: GameBoard;
-    private readonly _player1Id: string;
-    private readonly _player2Id: string;
-    private readonly _moveHistory: Move[];
+  private isGameOver: boolean;
+  private readonly gameBoard: GameBoard;
+  private readonly _player1Id: string;
+  private readonly _player2Id: string;
+  private readonly _moveHistory: Move[];
 
-    public constructor(player1Id: string, player2Id: string) {
-        this.gameBoard = new GameBoard(player1Id, player2Id);
-        this._player1Id = player1Id;
-        this._player2Id = player2Id;
-        this.isGameOver = false;
-        this._moveHistory = [];
-        this.currentPlayerId = player1Id;
-    }
-    public currentPlayerId: string;
-    public get size(): Coord { return this.gameBoard.size; }
-    public get player1Id(): string { return this._player1Id; }
-    public get player2Id(): string { return this._player2Id; }
-    public get moveHistory(): Move[] { return this._moveHistory; }
-    public getPlayerPositions(playerId: string): Coord[] { return this.gameBoard.getCoords(playerId); }
-    public coordToBoardIndex(coord: Coord): number { return this.gameBoard.coordToBoardIndex(coord); }
-    public makeMove(source: Coord, dest: Coord): MoveResult {
-        const checkForWin = (isP1Turn: boolean): MoveResult => {
-            const doCoordsFormSquare = (coords: Coord[]): boolean => {
-                const allRows = coords.map((c) => c.row);
-                const allCols = coords.map((c) => c.col);
-                const min = new Coord(Math.min(...allRows), Math.min(...allCols));
-                const max = new Coord(Math.max(...allRows), Math.max(...allCols));
-                const diff = max.sub(min);
-                const isSquare = diff.row === 1 && diff.col === 1;
-                return isSquare;
-            };
-            const { gameBoard, player1Id, player2Id } = this;
-            const playerId = isP1Turn ? player1Id : player2Id;
-
-            const playerCoords = gameBoard.getCoords(playerId)
-            const squareCoords: Coord[] = [
-                new Coord(0, 0),
-                new Coord(3, 0),
-                new Coord(3, 3),
-                new Coord(0, 3),
-            ];
+  public constructor(player1Id: string, player2Id: string) {
+    this.gameBoard = new GameBoard(player1Id, player2Id);
+    this._player1Id = player1Id;
+    this._player2Id = player2Id;
+    this.isGameOver = false;
+    this._moveHistory = [];
+    // this.currentPlayerId = Math.random() > 0.5 ? player1Id : player2Id;
+    this.currentPlayerId = player1Id;
+  }
+  getPiece(pos: Coord): string {
+    const value = this.gameBoard.get(pos);
+    if (value === null) return null;
+    const b = value.valueOf();
+    const id = b ? this.player1Id : this.player2Id;
+    return id;
+  }
+  public currentPlayerId: string;
+  public get size(): Coord {
+    return this.gameBoard.size;
+  }
+  public get player1Id(): string {
+    return this._player1Id;
+  }
+  public get player2Id(): string {
+    return this._player2Id;
+  }
+  public get moveHistory(): Move[] {
+    return this._moveHistory;
+  }
+  public getPlayerPositions(playerId: string): Coord[] {
+    return this.gameBoard.getCoords(playerId);
+  }
+  public coordToBoardIndex(coord: Coord): number {
+    return this.gameBoard.coordToBoardIndex(coord);
+  }
+  public async makeMove(source: Coord, dest: Coord): Promise<MoveResult> {
+    const checkForWin = (isP1Turn: boolean): MoveResult => {
+      const doCoordsFormSquare = (coords: Coord[]): boolean => {
+        const allRows = coords.map((c) => c.row);
+        const allCols = coords.map((c) => c.col);
+        const min = new Coord(Math.min(...allRows), Math.min(...allCols));
+        const max = new Coord(Math.max(...allRows), Math.max(...allCols));
+        const diff = max.sub(min);
+        const isSquare = diff.row === 1 && diff.col === 1;
+        return isSquare;
+      };
+      const { gameBoard, player1Id, player2Id } = this;
+      const playerId = isP1Turn ? player1Id : player2Id;
 
-            // winning conditions:
-            // 1. all in one row
-            // 2. all in one column
-            // 3. all in corners
-            // 4. all in square
-            // losing conditions:
-            // 1. on three sides of an opponent
+      const playerCoords = gameBoard.getCoords(playerId);
+      const squareCoords: Coord[] = [new Coord(0, 0), new Coord(3, 0), new Coord(3, 3), new Coord(0, 3)];
 
-            const firstPiece = playerCoords[0];
-            const allInRow = playerCoords.every((pc) => pc.row === firstPiece.row);
-            const allInCol = playerCoords.every((pc) => pc.col === firstPiece.col);
-            const allInCorners = playerCoords.every(
-                (pc) => squareCoords.findIndex((sc) => pc.equals(sc)) >= 0
-            );
-            const allInSquare = doCoordsFormSquare(playerCoords);
+      // winning conditions:
+      // 1. all in one row
+      // 2. all in one column
+      // 3. all in corners
+      // 4. all in square
+      // losing conditions:
+      // 1. on three sides of an opponent
 
-            const winnerName = isP1Turn ? "Player 1" : "Player 2";
-            let winningMessage = undefined;
-            if (allInRow) {
-                winningMessage = `${winnerName} wins by forming a row`;
-            }
-            if (allInCol) {
-                winningMessage = `${winnerName} wins by forming a column`;
-            }
-            if (allInCorners) {
-                winningMessage = `${winnerName} wins by putting pieces in corners`;
-            }
-            if (allInSquare) {
-                winningMessage = `${winnerName} wins by forming a square`;
-            }
-            if (winningMessage) {
-                return {
-                    isTerminal: true,
-                    message: winningMessage
-                };
-            } else {
-                return {
-                    isTerminal: false
-                };
-            }
+      const firstPiece = playerCoords[0];
+      const allInRow = playerCoords.every((pc) => pc.row === firstPiece.row);
+      const allInCol = playerCoords.every((pc) => pc.col === firstPiece.col);
+      const allInCorners = playerCoords.every((pc) => squareCoords.findIndex((sc) => pc.equals(sc)) >= 0);
+      const allInSquare = doCoordsFormSquare(playerCoords);
 
+      const winnerName = isP1Turn ? 'Player 1' : 'Player 2';
+      let winningMessage = undefined;
+      if (allInRow) {
+        winningMessage = `${winnerName} wins by forming a row`;
+      }
+      if (allInCol) {
+        winningMessage = `${winnerName} wins by forming a column`;
+      }
+      if (allInCorners) {
+        winningMessage = `${winnerName} wins by putting pieces in corners`;
+      }
+      if (allInSquare) {
+        winningMessage = `${winnerName} wins by forming a square`;
+      }
+      if (winningMessage) {
+        return {
+          isTerminal: true,
+          message: winningMessage,
+        };
+      } else {
+        return {
+          isTerminal: false,
         };
+      }
+    };
 
-        const { gameBoard, player1Id, player2Id } = this;
-        const isP1Turn = (this.currentPlayerId === player1Id);
-        const playerId = isP1Turn ? player1Id : player2Id;
+    const { gameBoard, player1Id, player2Id } = this;
+    const isP1Turn = this.currentPlayerId === player1Id;
+    const playerId = isP1Turn ? player1Id : player2Id;
 
-        this._moveHistory.push(new Move(playerId, source, dest));
-        gameBoard.move(source, dest);
+    this._moveHistory.push(new Move(playerId, source, dest));
+    gameBoard.move(source, dest);
 
-        const result = checkForWin(isP1Turn);
-        if (result.isTerminal) {
-            this.isGameOver = true;
-        } else {
-            this.currentPlayerId = isP1Turn ? this.player2Id : this.player1Id;
-        }
-        return result;
+    const result = checkForWin(isP1Turn);
+    if (result.isTerminal) {
+      this.isGameOver = true;
+    } else {
+      this.currentPlayerId = isP1Turn ? this.player2Id : this.player1Id;
     }
-    public getValidMoves(start: Coord): Coord[] {
-        const getFarthestCoord = (
-            startCoord: Coord,
-            direction: Coord
-        ): Coord => {
-            const { gameBoard } = this;
-            let farthestPos = undefined;
-            let testPos = startCoord;
-            while (true) {
-                const pos = new Coord(testPos.row + direction.row, testPos.col + direction.col);
+    return result;
+  }
+  public getValidMoves(start: Coord): Coord[] {
+    const getFarthestCoord = (startCoord: Coord, direction: Coord): Coord => {
+      const { gameBoard } = this;
+      let farthestPos = undefined;
+      let testPos = startCoord;
+      while (true) {
+        const pos = new Coord(testPos.row + direction.row, testPos.col + direction.col);
 
-                const withinBounds = gameBoard.size.contains(pos);
-                if (!withinBounds) {
-                    break;
-                }
-                const isOccupied = gameBoard.get(pos) !== null;
-                if (isOccupied) {
-                    break;
-                }
+        const withinBounds = gameBoard.size.contains(pos);
+        if (!withinBounds) {
+          break;
+        }
+        const isOccupied = gameBoard.get(pos) !== null;
+        if (isOccupied) {
+          break;
+        }
 
-                farthestPos = pos;
-                testPos = pos;
-            }
-            return farthestPos;
-        };
+        farthestPos = pos;
+        testPos = pos;
+      }
+      return farthestPos;
+    };
 
-        const farthestCoords = Coord.allDirections
-            .map((direction) => getFarthestCoord(start, direction))
-            .filter((farCoord) => farCoord !== undefined);
+    const farthestCoords = Coord.allDirections
+      .map((direction) => getFarthestCoord(start, direction))
+      .filter((farCoord) => farCoord !== undefined);
 
-        return farthestCoords;
+    return farthestCoords;
+  }
+  public undoMove(): Move {
+    if (!this.canUndoMove) {
+      throw "Newp! Can't undo!";
     }
-    public undoMove(): Move {
-        if (!this.canUndoMove) { throw "Newp! Can't undo!" }
-        const { gameBoard } = this;
-        const move = this._moveHistory.pop();
-        const { source, dest, playerId } = move;
-        this.currentPlayerId = playerId;
-        gameBoard.move(dest, source);
-        return move;
-    }
-    public get canUndoMove(): boolean {
-        if (this._moveHistory.length === 0 || this.isGameOver) {
-            return false;
-        }
-        return true;
+    const { gameBoard } = this;
+    const move = this._moveHistory.pop();
+    const { source, dest, playerId } = move;
+    this.currentPlayerId = playerId;
+    gameBoard.move(dest, source);
+    return move;
+  }
+  public get canUndoMove(): boolean {
+    if (this._moveHistory.length === 0 || this.isGameOver) {
+      return false;
     }
+    return true;
+  }
 }
diff --git a/src/local-game.ts b/src/local-game.ts
index a8d9ffa..cfee804 100644
--- a/src/local-game.ts
+++ b/src/local-game.ts
@@ -1,248 +1,237 @@
-import { Game, IGame } from "./game";
-import { Coord } from "./types";
-import { ListenerManager } from "./listeners"
+import { Game, IGame } from './game';
+import { Coord, MoveResult } from './types';
+import { ListenerManager } from './listeners';
 
 const classNames = {
-    selectedPiece: "selected-piece",
-    selectedPlayerText: "selected-player-text",
-    activePlayerPiece: "active-player-piece",
-    possibleMove: "possible-move",
+  selectedPiece: 'selected-piece',
+  selectedPlayerText: 'selected-player-text',
+  activePlayerPiece: 'active-player-piece',
+  possibleMove: 'possible-move',
 };
 
 export class LocalGame extends Game {
-    selectedPiece: Element | null = null;
-    listenerManager: ListenerManager = new ListenerManager();
-
-    // DOM references
-    private readonly dom = {
-        defaultP1Piece: document.getElementById("default-p1-piece"),
-        defaultP2Piece: document.getElementById("default-p2-piece"),
-        resetButton: document.getElementById("reset-button") as HTMLInputElement,
-        undoButton: document.getElementById("undo-button") as HTMLInputElement,
-        cells: () => Array.from(document.querySelectorAll("td")),
-        allPieces: () => Array.from(document.querySelectorAll(".piece")),
-        p1TurnText: () => Array.from(document.querySelectorAll(".p1-turn-text")),
-        p2TurnText: () => Array.from(document.querySelectorAll(".p2-turn-text")),
+  selectedPiece: Element | null = null;
+  listenerManager: ListenerManager = new ListenerManager();
+
+  // DOM references
+  private readonly dom = {
+    defaultP1Piece: document.getElementById('default-p1-piece'),
+    defaultP2Piece: document.getElementById('default-p2-piece'),
+    resetButton: document.getElementById('reset-button') as HTMLInputElement,
+    undoButton: document.getElementById('undo-button') as HTMLInputElement,
+    cells: () => Array.from(document.querySelectorAll('td')),
+    allPieces: () => Array.from(document.querySelectorAll('.piece')),
+    p1TurnText: () => Array.from(document.querySelectorAll('.p1-turn-text')),
+    p2TurnText: () => Array.from(document.querySelectorAll('.p2-turn-text')),
+  };
+
+  private getPiecesAndCoords(playerId: string) {
+    const allCells = this.dom.cells();
+    const playerPositions = this.getPlayerPositions(playerId);
+    const piecesAndPositions = playerPositions.map((coord) => {
+      const index = this.coordToBoardIndex(coord);
+      const cell = allCells[index];
+      const piece = cell.children[0];
+      return { coord, piece };
+    });
+    return piecesAndPositions;
+  }
+
+  /**
+   * Animates the motion of piece moving from source to dest
+   * @param source Starting Coord
+   * @param dest Ending Coord
+   * @param playerId Player id
+   */
+  private async movePiece(source: Coord, dest: Coord) {
+    const { dom } = this;
+    const sourceIndex = this.coordToBoardIndex(source);
+    const destIndex = this.coordToBoardIndex(dest);
+
+    // find current piece
+    const piece = dom.cells()[sourceIndex].children[0];
+    // unselect piece
+    piece.classList.remove(classNames.selectedPiece);
+
+    // create clone for new position
+    const destPiece = piece.cloneNode(true);
+    // add to destination cell
+    dom.cells()[destIndex].appendChild(destPiece);
+
+    // start animations
+    const destAnimation = dom.cells()[destIndex].animate([{ opacity: 0 }, { opacity: 1 }], { duration: 1000, iterations: 1 });
+    const sourceAnimation = piece.animate(
+      [
+        {
+          opacity: 1,
+          width: '45px',
+          height: '45px',
+        },
+        {
+          opacity: 0,
+          width: '5px',
+          height: '5px',
+        },
+      ],
+      { duration: 1000, iterations: 1 }
+    );
+
+    // await animations
+    await Promise.all([sourceAnimation.finished, destAnimation.finished]);
+
+    // remove old piece
+    piece.parentNode.removeChild(piece);
+  }
+
+  public constructor(player1Id: string, player2Id: string) {
+    super(player1Id, player2Id);
+    this.init();
+  }
+
+  public override async makeMove(source: Coord, dest: Coord): Promise<MoveResult> {
+    this.listenerManager.removeListeners();
+    await this.movePiece(source, dest);
+    const result = await super.makeMove(source, dest);
+    this.handleMoveResult(result);
+    return result;
+  }
+
+  public async handleMoveResult(result: MoveResult) {
+    if (result.isTerminal) {
+      alert(result.message);
+      document.location.reload();
+    } else {
+      this.resetUi();
+      this.setActiveUi();
     }
+  }
 
-    private resetBoard() {
-        const { dom, listenerManager } = this;
-        // reset Cells
-        dom.cells().forEach(cell => cell.classList.remove(classNames.possibleMove));
-        // reset pieces
-        dom.allPieces().forEach(piece => {
-            piece.classList.remove(classNames.activePlayerPiece)
-            piece.classList.remove(classNames.selectedPiece)
-        });
-        // remove all listeners
-        listenerManager.removeListeners();
-    }
-
-    /**
-     * Animates the motion of piece moving from source to dest
-     * @param source Starting Coord
-     * @param dest Ending Coord
-     * @param playerId Player id
-     */
-    private async movePiece(source: Coord, dest: Coord) {
-        const { dom } = this;
-        const sourceIndex = this.coordToBoardIndex(source);
-        const destIndex = this.coordToBoardIndex(dest);
-
-        // find current piece
-        const piece = dom.cells()[sourceIndex].children[0];
-        // unselect piece
-        piece.classList.remove(classNames.selectedPiece);
-
-        // create clone for new position
-        const destPiece = piece.cloneNode(true);
-        // add to destination cell
-        dom.cells()[destIndex].appendChild(destPiece);
-
-        // start animations
-        const destAnimation = dom.cells()[destIndex].animate(
-            [{ opacity: 0 }, { opacity: 1 }],
-            { duration: 1000, iterations: 1, });
-        const sourceAnimation = piece.animate(
-            [{
-                opacity: 1,
-                width: "45px",
-                height: "45px",
-            }, {
-                opacity: 0,
-                width: "5px",
-                height: "5px",
-            }],
-            { duration: 1000, iterations: 1 }
-        );
-
-        // await animations
-        await Promise.all([sourceAnimation.finished, destAnimation.finished]);
-
-        // remove old piece
-        piece.parentNode.removeChild(piece);
-    }
+  public resetUi() {
+    const { dom, currentPlayerId, player1Id, canUndoMove, moveHistory, listenerManager, selectedPiece } = this;
 
-    public nextMove() {
-        const setActivePlayerPieces = () => {
-            const {
-                dom,
-                listenerManager,
-                selectedPiece,
-                currentPlayerId, player1Id, player2Id,
-            } = this;
-            const selectPiece = (piece: Element, source: Coord) => {
-                // clear current selection
-                if (selectedPiece !== null) {
-                    selectedPiece.classList.remove(classNames.selectedPiece);
-                    dom.cells().forEach(cell => cell.classList.remove(classNames.possibleMove));
-                }
-
-                // set as selected
-                piece.classList.add(classNames.selectedPiece);
-                this.selectedPiece = piece;
-
-                // hook up allowed moves for:
-                // 1. blink
-                // 2. complete the move
-                const allowedMoves = this.getValidMoves(source);
-                allowedMoves.map((dest: Coord) => {
-                    const cell = dom.cells()[this.coordToBoardIndex(dest)];
-                    cell.classList.add(classNames.possibleMove);
-                    listenerManager.addListener(cell, "click", async () => await finishMove(source, dest));
-                });
-            };
-
-            // complete the move from source to dest
-            const finishMove = async (source: Coord, dest: Coord) => {
-                listenerManager.removeListeners();
-
-                const result = this.makeMove(source, dest);
-                await this.movePiece(source, dest);
-
-                if (result.isTerminal) {
-                    alert(result.message);
-                    document.location.reload();
-                } else {
-                    this.nextMove();
-                }
-            };
-
-            // mark active pieces
-            const allCells = dom.cells();
-            const playerPositions = this.getPlayerPositions(currentPlayerId);
-            const piecesAndPositions = playerPositions.map(coord => {
-                const index = this.coordToBoardIndex(coord);
-                const cell = allCells[index];
-                const piece = cell.children[0];
-                return { coord, piece };
-            });
-            piecesAndPositions
-                .forEach(({ coord, piece }) => {
-                    piece.classList.add(classNames.activePlayerPiece);
-                    listenerManager.addListener(piece, "click", event => selectPiece(piece, coord));
-                });
-        };
-
-        const {
-            dom,
-            currentPlayerId, player1Id, player2Id,
-            canUndoMove, moveHistory,
-        } = this;
-
-        // reset text
-        dom.p1TurnText().forEach((t) => t.classList.remove(classNames.selectedPlayerText));
-        dom.p2TurnText().forEach((t) => t.classList.remove(classNames.selectedPlayerText));
-
-        // changePlayer
-        // 1. flip flag
-        // 2. set text
-        // 3. set click callbacks
-        const isP1Turn = currentPlayerId === player1Id;
-        if (isP1Turn) {
-            dom.p1TurnText().forEach((t) => t.classList.add(classNames.selectedPlayerText));
-        } else {
-            dom.p2TurnText().forEach((t) => t.classList.add(classNames.selectedPlayerText));
-        }
+    // reset text
+    dom.p1TurnText().forEach((t) => t.classList.remove(classNames.selectedPlayerText));
+    dom.p2TurnText().forEach((t) => t.classList.remove(classNames.selectedPlayerText));
 
-        dom.undoButton.disabled = canUndoMove;
-        if (moveHistory.length === 0) {
-            dom.undoButton.disabled = true;
-            dom.resetButton.disabled = true;
-        } else {
-            dom.undoButton.disabled = false;
-            dom.resetButton.disabled = false;
-        }
-
-        this.selectedPiece = null;
+    const isP1Turn = currentPlayerId === player1Id;
+    if (isP1Turn) {
+      dom.p1TurnText().forEach((t) => t.classList.add(classNames.selectedPlayerText));
+    } else {
+      dom.p2TurnText().forEach((t) => t.classList.add(classNames.selectedPlayerText));
+    }
 
-        this.resetBoard();
-        setActivePlayerPieces();
+    dom.undoButton.disabled = canUndoMove;
+    if (moveHistory.length === 0) {
+      dom.undoButton.disabled = true;
+      dom.resetButton.disabled = true;
+    } else {
+      dom.undoButton.disabled = false;
+      dom.resetButton.disabled = false;
     }
 
-    public init() {
-        const { dom, player1Id, player2Id, size } = this;
-
-        // create board and pieces
-        let isWhite = true;
-        const table = document.getElementById("table");
-        for (let r = 0; r < size.row; r++) {
-            const tr = document.createElement("tr");
-            table.appendChild(tr);
-            for (let c = 0; c < size.col; c++) {
-                const td = document.createElement("td");
-                tr.appendChild(td);
-                td.className = isWhite ? "white" : "black";
-                isWhite = !isWhite;
-            }
-            isWhite = !isWhite;
+    this.selectedPiece = null;
+
+    // reset Cells
+    dom.cells().forEach((cell) => cell.classList.remove(classNames.possibleMove));
+    // reset pieces
+    dom.allPieces().forEach((piece) => {
+      piece.classList.remove(classNames.activePlayerPiece);
+      piece.classList.remove(classNames.selectedPiece);
+    });
+    // remove all listeners
+    listenerManager.removeListeners();
+  }
+  public setActiveUi() {
+    const { dom, currentPlayerId, listenerManager, selectedPiece } = this;
+
+    // mark active pieces
+    const piecesAndPositions = this.getPiecesAndCoords(currentPlayerId);
+    piecesAndPositions.forEach(({ coord: source, piece }) => {
+      piece.classList.add(classNames.activePlayerPiece);
+      listenerManager.addListener(piece, 'click', () => {
+        // clear current selection
+        if (selectedPiece !== null) {
+          selectedPiece.classList.remove(classNames.selectedPiece);
+          dom.cells().forEach((cell) => cell.classList.remove(classNames.possibleMove));
         }
-        const createPiece = (isP1Turn: boolean, pieceId: string, coord: Coord) => {
-            const defaultPiece = isP1Turn
-                ? dom.defaultP1Piece
-                : dom.defaultP2Piece;
-            const newPiece = defaultPiece.cloneNode(
-                true
-            ) as HTMLParagraphElement;
-            newPiece.id = pieceId;
-            const index = this.coordToBoardIndex(coord);
-            dom.cells()[index].appendChild(newPiece);
-        };
-        this.getPlayerPositions(player1Id).forEach(coord => createPiece(true, player1Id, coord));
-        this.getPlayerPositions(player2Id).forEach(coord => createPiece(false, player2Id, coord));
-
-        // hook up buttons
-        let blockActions = false;
-        dom.resetButton.addEventListener("click", () => {
-            if (blockActions) {
-                return;
-            }
-
-            blockActions = true;
-            if (confirm("Reset?")) {
-                window.location.reload();
-            }
-            blockActions = false;
-        });
-        dom.undoButton.addEventListener("click", async () => {
-            if (blockActions) {
-                return;
-            }
-
-            blockActions = true;
-
-            const move = this.undoMove();
-            await this.movePiece(move.dest, move.source);
-            this.nextMove();
 
-            blockActions = false;
+        // set as selected
+        piece.classList.add(classNames.selectedPiece);
+        this.selectedPiece = piece;
+
+        // hook up allowed moves for:
+        // 1. blink
+        // 2. complete the move
+        const allowedMoves = this.getValidMoves(source);
+        allowedMoves.forEach((dest: Coord) => {
+          const cell = dom.cells()[this.coordToBoardIndex(dest)];
+          cell.classList.add(classNames.possibleMove);
+          listenerManager.addListener(cell, 'click', async () => await this.makeMove(source, dest));
         });
+      });
+    });
+  }
+
+  private init() {
+    const { dom, player1Id, player2Id, size } = this;
+
+    // create board and pieces
+    let isWhite = true;
+    const table = document.getElementById('table');
+    for (let r = 0; r < size.row; r++) {
+      const tr = document.createElement('tr');
+      table.appendChild(tr);
+      for (let c = 0; c < size.col; c++) {
+        const td = document.createElement('td');
+        tr.appendChild(td);
+        td.className = isWhite ? 'white' : 'black';
+        isWhite = !isWhite;
+      }
+      isWhite = !isWhite;
     }
+    const createPiece = (isP1Turn: boolean, pieceId: string, coord: Coord) => {
+      const defaultPiece = isP1Turn ? dom.defaultP1Piece : dom.defaultP2Piece;
+      const newPiece = defaultPiece.cloneNode(true) as HTMLParagraphElement;
+      newPiece.id = pieceId;
+      const index = this.coordToBoardIndex(coord);
+      dom.cells()[index].appendChild(newPiece);
+    };
+    this.getPlayerPositions(player1Id).forEach((coord) => createPiece(true, player1Id, coord));
+    this.getPlayerPositions(player2Id).forEach((coord) => createPiece(false, player2Id, coord));
+
+    // hook up buttons
+    let blockActions = false;
+    dom.resetButton.addEventListener('click', () => {
+      if (blockActions) {
+        return;
+      }
+
+      blockActions = true;
+      if (confirm('Reset?')) {
+        window.location.reload();
+      }
+      blockActions = false;
+    });
+    dom.undoButton.addEventListener('click', async () => {
+      if (blockActions) {
+        return;
+      }
+
+      blockActions = true;
+
+      const move = this.undoMove();
+      await this.movePiece(move.dest, move.source);
+      this.resetUi();
+      this.setActiveUi();
+
+      blockActions = false;
+    });
+  }
 }
 
 // start!
 export const playGame = () => {
-    const lg = new LocalGame('Red', 'Blue');
-    lg.init();
-    lg.nextMove();
+  const lg = new LocalGame('Red', 'Blue', 'Red');
+  lg.resetUi();
+  lg.setActiveUi();
 };
diff --git a/src/remote-game.ts b/src/remote-game.ts
index fdb631d..2923ead 100644
--- a/src/remote-game.ts
+++ b/src/remote-game.ts
@@ -1,6 +1,8 @@
 import Peer, { PeerJSOption } from 'peerjs';
 import { LocalGame } from './local-game';
-import { useLocalStorage } from './storage';
+import { useSessionStorage } from './storage';
+import { Coord, Move, MoveResult } from './types';
+import { delay, generateHash } from './utils';
 
 interface LocalData {
   id: string;
@@ -11,75 +13,113 @@ interface Pos {
   y: number;
 }
 interface PeerData {
-  mousePos: Pos;
+  mousePos?: Pos;
+  move?: Move;
 }
-
-// {
-//     selfPeerId: `dao-game-${prompt('Name?', 'Red') ?? 'Red'}`,
-//     otherPeerIds: []
-// }
-const { getter: getData, setter: setData } = useLocalStorage<LocalData>(
-  'dao-game-v2022-03-18-1412',
-  { id: undefined, peerId: undefined }
-);
+const { getter: getData, setter: setData } = useSessionStorage<LocalData>('dao-game', { id: undefined, peerId: undefined });
+const otherMouseElement = document.getElementById('other-mouse');
+const openInNewTab = (url: string) => {
+  const openInEdge = true;
+  if (openInEdge) {
+    //   opening in the same browser causes a copy of the session storage,
+    //   so we clear the session storage, open the url, and restore the session storage
+    const data = getData();
+    sessionStorage.clear();
+    try {
+      window.open(url, '_blank');
+    } finally {
+      setData(data);
+    }
+  } else {
+    window.open(url, '_blank');
+  }
+};
+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;
+};
 
 export class PeerGame extends LocalGame {
-  peerMousePos: Pos;
-
-  public constructor() {
-    super('Local', 'Remote');
+  public conn: Peer.DataConnection;
+  public selfId: string;
+  public constructor(player1Id: string, player2Id: string, selfId: string, conn: Peer.DataConnection) {
+    super(player1Id, player2Id);
+    this.selfId = selfId;
+    this.conn = conn;
+  }
+  public override setActiveUi(): void {
+    const isLocalTurn = this.currentPlayerId === this.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 static async createPeerGame(): Promise<PeerGame> {
     let { id, peerId } = getData();
     const main = document.getElementsByTagName('body')[0];
-    const backCanvas = document.getElementById(
-      'back-canvas'
-    ) as HTMLCanvasElement;
+    let game: PeerGame = undefined;
 
     const handleConn = (conn: Peer.DataConnection) => {
       id = peer.id;
       peerId = conn.peer;
       setData({ id, peerId });
 
-      console.log('Adding event listener to main');
+      const isLocalP1 = getFirstPlayerId(id, peerId) === id;
+      const game = isLocalP1 ? new PeerGame(id, peerId, id, conn) : new PeerGame(peerId, id, id, conn);
+      game.resetUi();
+      game.setActiveUi();
+
       main.addEventListener('mousemove', (ev: MouseEvent) => {
         const peerData: PeerData = { mousePos: { x: ev.x, y: ev.y } };
-        const text = JSON.stringify(peerData);
-        // console.log(`Sending data to ${conn.peer}: ${text}`);
-        conn.send(text);
+        conn.send(JSON.stringify(peerData));
       });
 
-      conn.on('open', () => {
-        console.log(`Connection to ${conn.peer} opened`);
-        conn.send('{}');
-      });
       conn.on('data', (data: string) => {
-        // console.log(`Receiving data from ${conn.peer}: ${data}`);
         const peerData = JSON.parse(data) as PeerData;
-        if (peerData && peerData.mousePos) {
-          this.peerMousePos = peerData.mousePos;
+        const { mousePos, move } = peerData;
+
+        if (mousePos) {
+          const style = `top:${mousePos.y}px;left:${mousePos.x - otherMouseElement.clientWidth / 2}px;`;
+          otherMouseElement.setAttribute('style', 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.send('hi!');
     };
 
     const { host, protocol, pathname, search } = location;
     const queryParams = new URLSearchParams(search);
 
-    console.log(`Creating peer connection for ${id}`);
     const peerOptions: PeerJSOption = { debug: 0 };
-    const peer =
-      id === undefined ? new Peer(peerOptions) : new Peer(id, peerOptions);
-    id = peer.id;
+    const peer = id === undefined ? new Peer(peerOptions) : new Peer(id, peerOptions);
     window.onunload = () => peer.destroy();
-    // document.addEventListener('close', () => peer.destroy());
-    peer.on('connection', (conn) => {
-      console.log(`Connection established to ${conn.peer}`);
-      handleConn(conn);
-    });
+    peer.on('connection', (conn) => handleConn(conn));
+    peer.on('error', (err) => console.error({ 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);
@@ -88,33 +128,18 @@ export class PeerGame extends LocalGame {
         queryParams.set('peerId', id);
         const newSearch = queryParams.toString();
         const newUrl = `microsoft-edge:${protocol}//${host}${pathname}?${newSearch}`;
-        const nw = window.open(newUrl, '_blank');
+        openInNewTab(newUrl);
       }
     });
-    peer.on('error', (err) => console.error({ err }));
 
-    const animate = () => {
-      const { peerMousePos } = this;
-      if (peerMousePos !== undefined) {
-        // console.log({ peerMousePos: this.peerMousePos });
-        // this.peerMousePos = undefined;
-        backCanvas.width = window.innerWidth;
-        backCanvas.height = window.innerHeight;
-        const ctx = backCanvas.getContext('2d');
-        ctx.fillStyle = 'green';
-        ctx.beginPath();
-        ctx.ellipse(peerMousePos.x, peerMousePos.y, 50, 50, 0, 0, Math.PI * 2);
-        ctx.closePath();
-        ctx.fill();
-      }
-      requestAnimationFrame(() => animate());
-    };
-    requestAnimationFrame(() => animate());
+    while (game === undefined) {
+      await delay(500);
+    }
+    return game;
   }
 }
 
-export const playGame = () => {
-  const pg = new PeerGame();
-  pg.init();
-  pg.nextMove();
+export const playGame = async () => {
+  document.getElementsByTagName('body')[0].style.setProperty('display', '');
+  const game = await PeerGame.createPeerGame();
 };
diff --git a/src/storage.ts b/src/storage.ts
index 7e48952..d05cd5c 100644
--- a/src/storage.ts
+++ b/src/storage.ts
@@ -2,7 +2,7 @@ export interface StorageInterface<T> {
   getter: () => T;
   setter: (data: T) => void;
 }
-export const useLocalStorage = <T>(
+export const useSessionStorage = <T>(
   key: string,
   defaultData?: T
 ): StorageInterface<T> => {
diff --git a/src/types.ts b/src/types.ts
index ff0e77f..ca8d96a 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,129 +1,138 @@
 export class Coord {
-    public row: number;
-    public col: number;
+  public row: number;
+  public col: number;
 
-    constructor(row: number, col: number) {
-        this.row = row;
-        this.col = col;
-    }
+  constructor(row: number, col: number) {
+    this.row = row;
+    this.col = col;
+  }
 
-    public add(coord: Coord): Coord {
-        return new Coord(this.row + coord.row, this.col + coord.col);
-    }
+  public add(coord: Coord): Coord {
+    return new Coord(this.row + coord.row, this.col + coord.col);
+  }
 
-    sub(coord: Coord): Coord {
-        return new Coord(this.row - coord.row, this.col - coord.col);
-    }
+  sub(coord: Coord): Coord {
+    return new Coord(this.row - coord.row, this.col - coord.col);
+  }
 
-    /**
-     * Finds if otherCoord is contained within a rectangle that sits between origin and this Coord
-     * @param otherCoord 
-     */
-    contains(otherCoord: Coord): boolean {
-        const xRatio = otherCoord.col / this.col;
-        const yRatio = otherCoord.row / this.row;
-        const xyInsideSquare = xRatio >= 0 && xRatio < 1 && yRatio >= 0 && yRatio < 1;
-        return xyInsideSquare;
-    }
+  /**
+   * Finds if otherCoord is contained within a rectangle that sits between origin and this Coord
+   * @param otherCoord
+   */
+  contains(otherCoord: Coord): boolean {
+    const xRatio = otherCoord.col / this.col;
+    const yRatio = otherCoord.row / this.row;
+    const xyInsideSquare = xRatio >= 0 && xRatio < 1 && yRatio >= 0 && yRatio < 1;
+    return xyInsideSquare;
+  }
 
-    equals(otherCoord: Coord): boolean {
-        const areEqual = (this.col === otherCoord.col && this.row === otherCoord.row);
-        return areEqual;
-    }
-
-    // clock face directions
-    static get allDirections(): Coord[] {
-        const allDirections = [
-            new Coord(1, 0), // up
-            new Coord(1, 1), // up-right
-            new Coord(0, 1), // right
-            new Coord(-1, 1), // right-down
-            new Coord(-1, 0), // down
-            new Coord(-1, -1), // down-left
-            new Coord(0, -1), // left
-            new Coord(1, -1), // left-up
-        ]
-        return allDirections;
-    }
+  equals(otherCoord: Coord): boolean {
+    const areEqual = this.col === otherCoord.col && this.row === otherCoord.row;
+    return areEqual;
+  }
 
+  // clock face directions
+  static get allDirections(): Coord[] {
+    const allDirections = [
+      new Coord(1, 0), // up
+      new Coord(1, 1), // up-right
+      new Coord(0, 1), // right
+      new Coord(-1, 1), // right-down
+      new Coord(-1, 0), // down
+      new Coord(-1, -1), // down-left
+      new Coord(0, -1), // left
+      new Coord(1, -1), // left-up
+    ];
+    return allDirections;
+  }
 }
 
 export class Move {
-    public readonly source: Coord;
-    public readonly dest: Coord;
-    public readonly playerId: string;
-    constructor(playerId: string, source: Coord, dest: Coord) {
-        this.source = source;
-        this.dest = dest;
-        this.playerId = playerId;
-    }
+  public source: Coord;
+  public dest: Coord;
+  public playerId: string;
+  constructor(playerId: string, source: Coord, dest: Coord) {
+    this.source = source;
+    this.dest = dest;
+    this.playerId = playerId;
+  }
 }
 
 export interface MoveResult {
-    isTerminal: boolean;
-    message?: string;
+  isTerminal: boolean;
+  message?: string;
 }
 
 export class GameBoard {
-    private board: (string | null)[];
-    public readonly size: Coord;
-    public readonly player1Id: string;
-    public readonly player2Id: string
+  private board: (string | null)[];
+  public readonly size: Coord;
+  public readonly player1Id: string;
+  public readonly player2Id: string;
 
-    // testBoard: [
-    //     4, null, null, null,
-    //     null, 0, 1, 5,
-    //     null, 2, 3, null,
-    //     6, null, null, 7
-    // ],
-    public constructor(player1Id: string, player2Id: string) {
-        this.board = [
-            player1Id, null, null, player2Id,
-            null, player1Id, player2Id, null,
-            null, player2Id, player1Id, null,
-            player2Id, null, null, player1Id
-        ];
-        this.size = new Coord(4, 4);
-        this.player1Id = player1Id;
-        this.player2Id = player2Id;
-    }
-    public move(source: Coord, dest: Coord): void {
-        const isP1 = this.get(source);
-        if (isP1 === null) throw `Source ${source} points to an empty space`;
-        this.set(source, null);
-        this.set(dest, isP1);
-    }
-    public get(coord: Coord): boolean | null {
-        const index = this.coordToBoardIndex(coord);
-        if (!Number.isSafeInteger(index) || index < 0 || index >= this.board.length) throw `Index ${index} is out of bounds`;
-        const id = this.board[index];
-        const isP1 = id === null ? null : (id === this.player1Id);
-        return isP1;
-    }
-    public getCoords(id: string): Coord[] {
-        const coords = this.board
-            .map((pieceId, index) => {
-                const isTarget = pieceId === id;
-                return isTarget ? this.boardIndexToCoord(index) : null
-            })
-            .filter(coord => coord !== null);
-        return coords;
-    }
-    private set(coord: Coord, isP1: boolean | null): void {
-        const index = this.coordToBoardIndex(coord);
-        if (!Number.isSafeInteger(index) || index < 0 || index >= this.board.length) throw `Index ${index} is out of bounds`;
-        this.board[index] = isP1 === null ?
-            null :
-            (isP1 ? this.player1Id : this.player2Id);
-    }
-    public coordToBoardIndex(coord: Coord): number {
-        const { row, col } = coord;
-        const index = row * this.size.col + col;
-        return index;
-    }
-    private boardIndexToCoord(index: number): Coord {
-        const col = index % this.size.col;
-        const row = Math.floor(index / this.size.col);
-        return new Coord(row, col);
-    }
+  // testBoard: [
+  //     4, null, null, null,
+  //     null, 0, 1, 5,
+  //     null, 2, 3, null,
+  //     6, null, null, 7
+  // ],
+  public constructor(player1Id: string, player2Id: string) {
+    this.board = [
+      player1Id,
+      null,
+      null,
+      player2Id,
+      null,
+      player1Id,
+      player2Id,
+      null,
+      null,
+      player2Id,
+      player1Id,
+      null,
+      player2Id,
+      null,
+      null,
+      player1Id,
+    ];
+    this.size = new Coord(4, 4);
+    this.player1Id = player1Id;
+    this.player2Id = player2Id;
+  }
+  public move(source: Coord, dest: Coord): void {
+    const isP1 = this.get(source);
+    if (isP1 === null) throw `Source ${source} points to an empty space`;
+    this.set(source, null);
+    this.set(dest, isP1);
+  }
+  public get(coord: Coord): boolean | null {
+    const index = this.coordToBoardIndex(coord);
+    if (!Number.isSafeInteger(index) || index < 0 || index >= this.board.length) throw `Index ${index} is out of bounds`;
+    const id = this.board[index];
+    const isP1 = id === null ? null : id === this.player1Id;
+    return isP1;
+  }
+  public getCoords(id: string): Coord[] {
+    const coords = this.board
+      .map((pieceId, index) => {
+        const isTarget = pieceId === id;
+        return isTarget ? this.boardIndexToCoord(index) : null;
+      })
+      .filter((coord) => coord !== null);
+    return coords;
+  }
+  private set(coord: Coord, isP1: boolean | null): void {
+    const index = this.coordToBoardIndex(coord);
+    if (!Number.isSafeInteger(index) || index < 0 || index >= this.board.length) throw `Index ${index} is out of bounds`;
+    this.board[index] = isP1 === null ? null : isP1 ? this.player1Id : this.player2Id;
+  }
+  public coordToBoardIndex(coord: Coord): number {
+    const { row, col } = coord;
+    const index = row * this.size.col + col;
+    return index;
+  }
+  private boardIndexToCoord(index: number): Coord {
+    const col = index % this.size.col;
+    const row = Math.floor(index / this.size.col);
+    return new Coord(row, col);
+  }
 }
diff --git a/src/utils.ts b/src/utils.ts
new file mode 100644
index 0000000..2ad21ab
--- /dev/null
+++ b/src/utils.ts
@@ -0,0 +1,20 @@
+export function generateHash(text: string) {
+  var hash = 0;
+  if (text.length == 0) {
+    return hash;
+  }
+  for (let i = 0; i < text.length; i++) {
+    var charCode = text.charCodeAt(i);
+    hash = (hash << 7) - hash + charCode;
+    hash = hash & hash;
+  }
+  return hash;
+}
+
+export async function delay(delayInMs: number) {
+  return new Promise((resolve) => {
+    setTimeout(() => {
+      resolve(2);
+    }, delayInMs);
+  });
+}