fuzzyworld

dao-game / commit

In-progress code, doesn't compile, etc.

2909d32401d1465f964c32c71fa17d362fe383f8 · Pavel Safronov · 2022-03-16 11:55:32 -0700 · parents cda3e92d1219

In-progress code, doesn't compile, etc.
 .vscode/settings.json |   1 +
 src/app.ts            |   0
 src/game.ts           | 558 ++++++++++++--------------------------------------
 src/local-game.ts     | 469 ++++++++++++++++++++++++++++++++++++++++++
 src/peering_.ts       | 119 +++++++++++
 src/piano.ts          |  53 ++---
 src/types.ts          |  99 +++++----
 7 files changed, 801 insertions(+), 498 deletions(-)

diff --git a/.vscode/settings.json b/.vscode/settings.json
index 1e2d4fa..03ad85b 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,6 +1,7 @@
 {
     "cSpell.words": [
         "esbuild",
+        "Newp",
         "outfile",
         "servedir",
         "Todos"
diff --git a/src/app.ts b/src/app.ts
new file mode 100644
index 0000000..e69de29
diff --git a/src/game.ts b/src/game.ts
index 19371a7..b4233ca 100644
--- a/src/game.ts
+++ b/src/game.ts
@@ -1,444 +1,152 @@
-import { PeerComm } from "./peering";
-import { Coord, GameBoard, ListenerManager, Move } from "./types";
-
-const constants = {
-    selectedPieceClassName: "selected-piece",
-    selectedPlayerTextClassName: "selected-player-text",
-    activePlayerPieceClassName: "active-player-piece",
-    possibleMoveClassName: "possible-move",
-};
-
-const gameBoard = new GameBoard();
-const listenerManager: ListenerManager = new ListenerManager();
-
-const player: {
-    isPlayerRed: boolean;
-    isRedTurn: boolean;
-    selectedPieceId: number | undefined;
-    moveHistory: Move[];
-} = {
-    isPlayerRed: false,
-    isRedTurn: false,
-    selectedPieceId: undefined,
-    moveHistory: [],
-};
-
-// DOM references
-const dom = {
-    defaultRedPiece: document.getElementById("default-red-piece"),
-    defaultBluePiece: document.getElementById("default-blue-piece"),
-    cells: () => Array.from(document.querySelectorAll("td")),
-    allPieces: () => Array.from(document.querySelectorAll(".piece")),
-    playerPieces: (isRedTurn: boolean) =>
-        isRedTurn
-            ? Array.from(
-                document.querySelectorAll(".red-piece:not(#default-red-piece")
-            )
-            : Array.from(
-                document.querySelectorAll(
-                    ".blue-piece:not(#default-blue-piece"
-                )
-            ),
-    redTurnText: () => Array.from(document.querySelectorAll(".red-turn-text")),
-    blueTurnText: () =>
-        Array.from(document.querySelectorAll(".blue-turn-text")),
-    resetButton: document.getElementById("reset-button") as HTMLInputElement,
-    undoButton: document.getElementById("undo-button") as HTMLInputElement,
-};
-
-const resetBoard = () => {
-    // resetCells
-    dom.cells().forEach((cell) => {
-        cell.classList.remove(constants.possibleMoveClassName);
-    });
-
-    // reset pieces
-    dom.allPieces().forEach((piece) => {
-        if (piece.classList.contains(constants.activePlayerPieceClassName)) {
-            piece.classList.toggle(constants.activePlayerPieceClassName);
-        }
-        if (piece.classList.contains(constants.selectedPieceClassName)) {
-            piece.classList.toggle(constants.selectedPieceClassName);
-        }
-    });
-
-    // remove all listeners
-    listenerManager.removeListeners();
-};
-
-const movePiece = async (sourceIndex: number, destIndex: number) => {
-    const pieceId = gameBoard.board[sourceIndex];
-    // find current piece
-    const piece = document.getElementById(pieceId.toString());
-    // unselect piece
-    piece.classList.remove(constants.selectedPieceClassName);
-
-    // 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);
-    gameBoard.board[sourceIndex] = null;
-    // set new piece
-    gameBoard.board[destIndex] = pieceId;
-};
-
-const nextMove = () => {
-    const setActivePlayerPieces = () => {
-        const selectPiece = (event: Event) => {
-            const getAllowedMoveIndicesForIndex = (
-                boardIndex: number
-            ): number[] => {
-                const getFarthestCoord = (
-                    startCoord: Coord,
-                    direction: Coord
-                ): Coord => {
-                    let farthestPos = undefined;
-                    let testPos = startCoord;
-                    while (true) {
-                        const pos = new Coord(
-                            testPos.row + direction.row,
-                            testPos.col + direction.col
-                        );
-                        const index = gameBoard.coordToBoardIndex(pos);
-                        const withinBounds =
-                            pos.row >= 0 &&
-                            pos.row < gameBoard.rows &&
-                            pos.col >= 0 &&
-                            pos.col < gameBoard.cols;
-                        const isOccupied = Number.isInteger(
-                            gameBoard.board[index]
-                        );
-                        if (isOccupied || !withinBounds) {
-                            break;
-                        } else {
-                            farthestPos = pos;
-                            testPos = pos;
-                        }
-                    }
-                    return farthestPos;
-                };
-
-                const coord = gameBoard.boardIndexToCoord(boardIndex);
-
-                const farthestCoords = Coord.allDirections
-                    .map((direction) => getFarthestCoord(coord, direction))
-                    .filter((farCoord) => farCoord !== undefined);
+import { Coord, GameBoard, IGame, Move, MoveResult } from "./types";
+
+export class Game implements IGame {
+    private readonly gameBoard: GameBoard;
+    private readonly player1Id: string;
+    private readonly player2Id: string;
+    private isGameOver: boolean;
+    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 = [];
+    }
 
-                const farthestIndices = farthestCoords.map((c) =>
-                    gameBoard.coordToBoardIndex(c)
-                );
-                return farthestIndices;
+    public get moveHistory(): Move[] { return this._moveHistory; }
+    public currentPlayerId: string;
+    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 previousSelectedPieceId = player.selectedPieceId;
-            if (previousSelectedPieceId !== undefined) {
-                const previousPiece = document.getElementById(
-                    previousSelectedPieceId.toString()
-                );
-                previousPiece.classList.remove(
-                    constants.selectedPieceClassName
-                );
-                dom.cells().forEach((cell) => {
-                    cell.classList.remove(constants.possibleMoveClassName);
-                });
+            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),
+            ];
+
+            // 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 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
+                };
             }
 
-            // read event data
-            const piece = event.target as Element;
-            const pieceId = parseInt(piece.id);
-
-            // set as selected
-            player.selectedPieceId = pieceId;
-            piece.classList.add(constants.selectedPieceClassName);
-
-            // hook up allowed moves for:
-            // 1. blink
-            // 2. complete the move
-            const pieceIndex = gameBoard.board.indexOf(pieceId);
-            const allowedMoveIndices =
-                getAllowedMoveIndicesForIndex(pieceIndex);
-            allowedMoveIndices.forEach((i) => {
-                const cell = dom.cells()[i];
-                listenerManager.addListener(
-                    cell,
-                    "click",
-                    async () => await makeMove(i)
-                );
-                cell.classList.add(constants.possibleMoveClassName);
-            });
-        };
-
-        const makeMove = async (destIndex) => {
-            listenerManager.removeListeners();
-
-            const sourceIndex = gameBoard.board.indexOf(player.selectedPieceId);
-            player.moveHistory.push(new Move(sourceIndex, destIndex));
-
-            await movePiece(sourceIndex, destIndex);
-
-            checkForWin();
-            nextMove();
         };
 
-        // mark active pieces
-        const playerPieces = dom.playerPieces(player.isRedTurn);
-        playerPieces.forEach((pp) => {
-            pp.classList.add(constants.activePlayerPieceClassName);
-            listenerManager.addListener(pp, "click", selectPiece);
-        });
-    };
+        const { gameBoard, player1Id, player2Id } = this;
+        const isP1Turn = (this.currentPlayerId === player1Id);
+        const playerId = isP1Turn ? player1Id : player2Id;
 
-    // reset text
-    dom.redTurnText().forEach((t) =>
-        t.classList.remove(constants.selectedPlayerTextClassName)
-    );
-    dom.blueTurnText().forEach((t) =>
-        t.classList.remove(constants.selectedPlayerTextClassName)
-    );
+        gameBoard.move(source, dest);
 
-    // changePlayer
-    // 1. flip flag
-    // 2. set text
-    // 3. set click callbacks
-    if (player.isRedTurn) {
-        player.isRedTurn = false;
-        dom.blueTurnText().forEach((t) =>
-            t.classList.add(constants.selectedPlayerTextClassName)
-        );
-    } else {
-        player.isRedTurn = true;
-        dom.redTurnText().forEach((t) =>
-            t.classList.add(constants.selectedPlayerTextClassName)
-        );
-    }
-
-    if (player.moveHistory.length === 0) {
-        dom.undoButton.disabled = true;
-        dom.resetButton.disabled = true;
-    } else {
-        dom.undoButton.disabled = false;
-        dom.resetButton.disabled = false;
+        const result = checkForWin(isP1Turn);
+        if (!result.isTerminal) {
+            this.currentPlayerId = isP1Turn ? this.player2Id : this.player1Id;
+            this.isGameOver = true;
+        } else {
+            this._moveHistory.push(new Move(playerId, source, dest));
+        }
+        return result;
     }
-
-    player.selectedPieceId = undefined;
-    resetBoard();
-    setActivePlayerPieces();
-};
-
-const checkForWin = () => {
-    const elementsToCoords = (elements: Element[]): Coord[] => {
-        const elementsToBoardIndexes = (elements: Element[]): number[] => {
-            const indices = elements.map((pp) => {
-                const pieceId = Number.parseInt(pp.id);
-                const pieceIndex = gameBoard.board.indexOf(pieceId);
-                return pieceIndex;
-            });
-            return indices;
+    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 =
+                    pos.row >= 0 &&
+                    pos.row < gameBoard.rows &&
+                    pos.col >= 0 &&
+                    pos.col < gameBoard.cols;
+
+                const isOccupied = gameBoard.get(pos) !== null;
+                if (isOccupied || !withinBounds) {
+                    break;
+                } else {
+                    farthestPos = pos;
+                    testPos = pos;
+                }
+            }
+            return farthestPos;
         };
 
-        const indices = elementsToBoardIndexes(elements);
-        const coords = indices.map((pieceIndex) =>
-            gameBoard.boardIndexToCoord(pieceIndex)
-        );
-        return coords;
-    };
-    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 playerCoords = elementsToCoords(dom.playerPieces(player.isRedTurn));
-    const otherCoords = elementsToCoords(dom.playerPieces(!player.isRedTurn));
-    const squareCoords: Coord[] = [
-        new Coord(0, 0),
-        new Coord(3, 0),
-        new Coord(3, 3),
-        new Coord(0, 3),
-    ];
-
-    // 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 farthestCoords = Coord.allDirections
+            .map((direction) => getFarthestCoord(start, direction))
+            .filter((farCoord) => farCoord !== undefined);
 
-    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 = player.isRedTurn ? "Red" : "Blue";
-    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`;
+        return farthestCoords;
     }
-    if (allInSquare) {
-        winningMessage = `${winnerName} wins by forming a square`;
-    }
-
-    if (winningMessage !== undefined) {
-        alert(winningMessage);
-        document.location.reload();
-    }
-};
-
-const init = () => {
-    // create board and pieces
-    let isWhite = true;
-    const table = document.getElementById("table");
-    for (let r = 0; r < gameBoard.rows; r++) {
-        const tr = document.createElement("tr");
-        table.appendChild(tr);
-        for (let c = 0; c < gameBoard.cols; c++) {
-            const td = document.createElement("td");
-            tr.appendChild(td);
-            td.className = isWhite ? "white" : "black";
-            isWhite = !isWhite;
+    public undoMove(): void {
+        if (!this.canUndoMove) {
+            throw "Newp! Can't undo!"
         }
-        isWhite = !isWhite;
+        const { gameBoard } = this;
+        const { source, dest, playerId } = this._moveHistory.pop();
+        this.currentPlayerId = playerId;
+        gameBoard.move(dest, source);
     }
-    gameBoard.board.forEach((value, index) => {
-        if (value !== null) {
-            const isRed = value < 4;
-            const defaultPiece = isRed
-                ? dom.defaultRedPiece
-                : dom.defaultBluePiece;
-            const newPiece = defaultPiece.cloneNode(
-                true
-            ) as HTMLParagraphElement;
-            newPiece.id = value.toString();
-            dom.cells()[index].appendChild(newPiece);
-        }
-    });
-
-    // hook up buttons
-    let blockActions = false;
-    dom.resetButton.addEventListener("click", () => {
-        if (blockActions) {
-            return;
+    public get canUndoMove(): boolean {
+        if (this._moveHistory.length === 0 || this.isGameOver) {
+            return false;
         }
-        if (player.moveHistory.length === 0) {
-            return;
-        }
-
-        blockActions = true;
-        if (confirm("Reset?")) {
-            window.location.reload();
-        }
-        blockActions = false;
-    });
-    dom.undoButton.addEventListener("click", async () => {
-        if (blockActions) {
-            return;
-        }
-        if (player.moveHistory.length === 0) {
-            return;
-        }
-
-        blockActions = true;
-
-        player.selectedPieceId = undefined;
-        resetBoard();
-
-        const { destIndex, sourceIndex } = player.moveHistory.pop();
-        await movePiece(destIndex, sourceIndex);
-        nextMove();
-        blockActions = false;
-    });
-};
-
-// start!
-const playGame = () => {
-    init();
-    nextMove();
-};
-
-// const isOnlineGame = (window as any).daoIsOnline;
-// if (isOnlineGame) {
-//     const receiver = (data) => 
-//     const peer = new PeerComm()
-//     } else {
-//         playGame();
-
-// }
-
-playGame();
-
-function delay(ms: number) {
-    return new Promise(resolve => setTimeout(resolve, ms));
-}
-const doStuff = async () => {
-    const peer = new PeerComm((data) => {
-        console.log(`TO:    ${peer.id})`);
-        console.log(`FROM:  ?)`);
-        console.log(`DATA:  ${data}`);
-    });
-    prompt('Self id:', peer.id);
-    const otherId = prompt('Peer id to connect to:', '');
-    if (!otherId) {
-        while (!peer.isConnected) {
-            await delay(500);
-        }
-    } else {
-        peer.connect(otherId);
+        return true;        
     }
 
-    alert('Connected!');
-    alert('Starting messaging server');
-    alert('Respond with empty text to stop');
-
-    while (true) {
-        const message = prompt('Message to send:', '');
-        if (!message) {
-            break;
-        }
-        peer.send(message);
-        delay(1000);
-    }
 }
-// doStuff();
diff --git a/src/local-game.ts b/src/local-game.ts
new file mode 100644
index 0000000..bcc0870
--- /dev/null
+++ b/src/local-game.ts
@@ -0,0 +1,469 @@
+import { PeerComm } from "./peering";
+import { Coord, GameBoard, Move } from "./types";
+
+const constants = {
+    selectedPieceClassName: "selected-piece",
+    selectedPlayerTextClassName: "selected-player-text",
+    activePlayerPieceClassName: "active-player-piece",
+    possibleMoveClassName: "possible-move",
+};
+
+interface Listener {
+    element: Element;
+    type: string;
+    listener: (event: Event) => void;
+}
+
+export class ListenerManager {
+    private listeners: Listener[] = [];
+
+    public addListener(element: Element, type: string, listener: (event: Event) => void): void {
+        const { listeners } = this;
+        element.addEventListener(type, listener);
+        listeners.push({ element, type, listener });
+    }
+
+    public removeListeners() {
+        const { listeners } = this;
+        listeners.forEach(value => {
+            const { element, type, listener } = value;
+            element.removeEventListener(type, listener);
+        });
+        listeners.length = 0;
+    }
+}
+
+const gameBoard = new GameBoard();
+const listenerManager: ListenerManager = new ListenerManager();
+
+const player: {
+    isPlayerRed: boolean;
+    isRedTurn: boolean;
+    selectedPieceId: number | undefined;
+    moveHistory: Move[];
+} = {
+    isPlayerRed: false,
+    isRedTurn: false,
+    selectedPieceId: undefined,
+    moveHistory: [],
+};
+
+// DOM references
+const dom = {
+    defaultRedPiece: document.getElementById("default-red-piece"),
+    defaultBluePiece: document.getElementById("default-blue-piece"),
+    cells: () => Array.from(document.querySelectorAll("td")),
+    allPieces: () => Array.from(document.querySelectorAll(".piece")),
+    playerPieces: (isRedTurn: boolean) =>
+        isRedTurn
+            ? Array.from(
+                document.querySelectorAll(".red-piece:not(#default-red-piece")
+            )
+            : Array.from(
+                document.querySelectorAll(
+                    ".blue-piece:not(#default-blue-piece"
+                )
+            ),
+    redTurnText: () => Array.from(document.querySelectorAll(".red-turn-text")),
+    blueTurnText: () =>
+        Array.from(document.querySelectorAll(".blue-turn-text")),
+    resetButton: document.getElementById("reset-button") as HTMLInputElement,
+    undoButton: document.getElementById("undo-button") as HTMLInputElement,
+};
+
+const resetBoard = () => {
+    // resetCells
+    dom.cells().forEach((cell) => {
+        cell.classList.remove(constants.possibleMoveClassName);
+    });
+
+    // reset pieces
+    dom.allPieces().forEach((piece) => {
+        if (piece.classList.contains(constants.activePlayerPieceClassName)) {
+            piece.classList.toggle(constants.activePlayerPieceClassName);
+        }
+        if (piece.classList.contains(constants.selectedPieceClassName)) {
+            piece.classList.toggle(constants.selectedPieceClassName);
+        }
+    });
+
+    // remove all listeners
+    listenerManager.removeListeners();
+};
+
+const movePiece = async (sourceIndex: number, destIndex: number) => {
+    const pieceId = gameBoard.board[sourceIndex];
+    // find current piece
+    const piece = document.getElementById(pieceId.toString());
+    // unselect piece
+    piece.classList.remove(constants.selectedPieceClassName);
+
+    // 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);
+    gameBoard.board[sourceIndex] = null;
+    // set new piece
+    gameBoard.board[destIndex] = pieceId;
+};
+
+const nextMove = () => {
+    const setActivePlayerPieces = () => {
+        const selectPiece = (event: Event) => {
+            const getAllowedMoveIndicesForIndex = (
+                boardIndex: number
+            ): number[] => {
+                const getFarthestCoord = (
+                    startCoord: Coord,
+                    direction: Coord
+                ): Coord => {
+                    let farthestPos = undefined;
+                    let testPos = startCoord;
+                    while (true) {
+                        const pos = new Coord(
+                            testPos.row + direction.row,
+                            testPos.col + direction.col
+                        );
+                        const index = gameBoard.coordToBoardIndex(pos);
+                        const withinBounds =
+                            pos.row >= 0 &&
+                            pos.row < gameBoard.rows &&
+                            pos.col >= 0 &&
+                            pos.col < gameBoard.cols;
+                        const isOccupied = Number.isInteger(
+                            gameBoard.board[index]
+                        );
+                        if (isOccupied || !withinBounds) {
+                            break;
+                        } else {
+                            farthestPos = pos;
+                            testPos = pos;
+                        }
+                    }
+                    return farthestPos;
+                };
+
+                const coord = gameBoard.boardIndexToCoord(boardIndex);
+
+                const farthestCoords = Coord.allDirections
+                    .map((direction) => getFarthestCoord(coord, direction))
+                    .filter((farCoord) => farCoord !== undefined);
+
+                const farthestIndices = farthestCoords.map((c) =>
+                    gameBoard.coordToBoardIndex(c)
+                );
+                return farthestIndices;
+            };
+
+            const previousSelectedPieceId = player.selectedPieceId;
+            if (previousSelectedPieceId !== undefined) {
+                const previousPiece = document.getElementById(
+                    previousSelectedPieceId.toString()
+                );
+                previousPiece.classList.remove(
+                    constants.selectedPieceClassName
+                );
+                dom.cells().forEach((cell) => {
+                    cell.classList.remove(constants.possibleMoveClassName);
+                });
+            }
+
+            // read event data
+            const piece = event.target as Element;
+            const pieceId = parseInt(piece.id);
+
+            // set as selected
+            player.selectedPieceId = pieceId;
+            piece.classList.add(constants.selectedPieceClassName);
+
+            // hook up allowed moves for:
+            // 1. blink
+            // 2. complete the move
+            const pieceIndex = gameBoard.board.indexOf(pieceId);
+            const allowedMoveIndices =
+                getAllowedMoveIndicesForIndex(pieceIndex);
+            allowedMoveIndices.forEach((i) => {
+                const cell = dom.cells()[i];
+                listenerManager.addListener(
+                    cell,
+                    "click",
+                    async () => await makeMove(i)
+                );
+                cell.classList.add(constants.possibleMoveClassName);
+            });
+        };
+
+        const makeMove = async (destIndex) => {
+            listenerManager.removeListeners();
+
+            const sourceIndex = gameBoard.board.indexOf(player.selectedPieceId);
+            player.moveHistory.push(new Move(sourceIndex, destIndex));
+
+            await movePiece(sourceIndex, destIndex);
+
+            checkForWin();
+            nextMove();
+        };
+
+        // mark active pieces
+        const playerPieces = dom.playerPieces(player.isRedTurn);
+        playerPieces.forEach((pp) => {
+            pp.classList.add(constants.activePlayerPieceClassName);
+            listenerManager.addListener(pp, "click", selectPiece);
+        });
+    };
+
+    // reset text
+    dom.redTurnText().forEach((t) =>
+        t.classList.remove(constants.selectedPlayerTextClassName)
+    );
+    dom.blueTurnText().forEach((t) =>
+        t.classList.remove(constants.selectedPlayerTextClassName)
+    );
+
+    // changePlayer
+    // 1. flip flag
+    // 2. set text
+    // 3. set click callbacks
+    if (player.isRedTurn) {
+        player.isRedTurn = false;
+        dom.blueTurnText().forEach((t) =>
+            t.classList.add(constants.selectedPlayerTextClassName)
+        );
+    } else {
+        player.isRedTurn = true;
+        dom.redTurnText().forEach((t) =>
+            t.classList.add(constants.selectedPlayerTextClassName)
+        );
+    }
+
+    if (player.moveHistory.length === 0) {
+        dom.undoButton.disabled = true;
+        dom.resetButton.disabled = true;
+    } else {
+        dom.undoButton.disabled = false;
+        dom.resetButton.disabled = false;
+    }
+
+    player.selectedPieceId = undefined;
+    resetBoard();
+    setActivePlayerPieces();
+};
+
+const checkForWin = () => {
+    const elementsToCoords = (elements: Element[]): Coord[] => {
+        const elementsToBoardIndexes = (elements: Element[]): number[] => {
+            const indices = elements.map((pp) => {
+                const pieceId = Number.parseInt(pp.id);
+                const pieceIndex = gameBoard.board.indexOf(pieceId);
+                return pieceIndex;
+            });
+            return indices;
+        };
+
+        const indices = elementsToBoardIndexes(elements);
+        const coords = indices.map((pieceIndex) =>
+            gameBoard.boardIndexToCoord(pieceIndex)
+        );
+        return coords;
+    };
+    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 playerCoords = elementsToCoords(dom.playerPieces(player.isRedTurn));
+    const otherCoords = elementsToCoords(dom.playerPieces(!player.isRedTurn));
+    const squareCoords: Coord[] = [
+        new Coord(0, 0),
+        new Coord(3, 0),
+        new Coord(3, 3),
+        new Coord(0, 3),
+    ];
+
+    // 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 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 = player.isRedTurn ? "Red" : "Blue";
+    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 !== undefined) {
+        alert(winningMessage);
+        document.location.reload();
+    }
+};
+
+const init = () => {
+    // create board and pieces
+    let isWhite = true;
+    const table = document.getElementById("table");
+    for (let r = 0; r < gameBoard.rows; r++) {
+        const tr = document.createElement("tr");
+        table.appendChild(tr);
+        for (let c = 0; c < gameBoard.cols; c++) {
+            const td = document.createElement("td");
+            tr.appendChild(td);
+            td.className = isWhite ? "white" : "black";
+            isWhite = !isWhite;
+        }
+        isWhite = !isWhite;
+    }
+    gameBoard.board.forEach((value, index) => {
+        if (value !== null) {
+            const isRed = value < 4;
+            const defaultPiece = isRed
+                ? dom.defaultRedPiece
+                : dom.defaultBluePiece;
+            const newPiece = defaultPiece.cloneNode(
+                true
+            ) as HTMLParagraphElement;
+            newPiece.id = value.toString();
+            dom.cells()[index].appendChild(newPiece);
+        }
+    });
+
+    // hook up buttons
+    let blockActions = false;
+    dom.resetButton.addEventListener("click", () => {
+        if (blockActions) {
+            return;
+        }
+        if (player.moveHistory.length === 0) {
+            return;
+        }
+
+        blockActions = true;
+        if (confirm("Reset?")) {
+            window.location.reload();
+        }
+        blockActions = false;
+    });
+    dom.undoButton.addEventListener("click", async () => {
+        if (blockActions) {
+            return;
+        }
+        if (player.moveHistory.length === 0) {
+            return;
+        }
+
+        blockActions = true;
+
+        player.selectedPieceId = undefined;
+        resetBoard();
+
+        const { destIndex, sourceIndex } = player.moveHistory.pop();
+        await movePiece(destIndex, sourceIndex);
+        nextMove();
+        blockActions = false;
+    });
+};
+
+// start!
+const playGame = () => {
+    init();
+    nextMove();
+};
+
+// const isOnlineGame = (window as any).daoIsOnline;
+// if (isOnlineGame) {
+//     const receiver = (data) => 
+//     const peer = new PeerComm()
+//     } else {
+//         playGame();
+
+// }
+
+playGame();
+
+function delay(ms: number) {
+    return new Promise(resolve => setTimeout(resolve, ms));
+}
+const doStuff = async () => {
+    const peer = new PeerComm((data) => {
+        console.log(`TO:    ${peer.id})`);
+        console.log(`FROM:  ?)`);
+        console.log(`DATA:  ${data}`);
+    });
+    prompt('Self id:', peer.id);
+    const otherId = prompt('Peer id to connect to:', '');
+    if (!otherId) {
+        while (!peer.isConnected) {
+            await delay(500);
+        }
+    } else {
+        peer.connect(otherId);
+    }
+
+    alert('Connected!');
+    alert('Starting messaging server');
+    alert('Respond with empty text to stop');
+
+    while (true) {
+        const message = prompt('Message to send:', '');
+        if (!message) {
+            break;
+        }
+        peer.send(message);
+        delay(1000);
+    }
+}
+// doStuff();
diff --git a/src/peering_.ts b/src/peering_.ts
new file mode 100644
index 0000000..e89878a
--- /dev/null
+++ b/src/peering_.ts
@@ -0,0 +1,119 @@
+let pc;
+let localStream;
+
+const signaling = new BroadcastChannel('webrtc');
+signaling.onmessage = e => {
+  if (!localStream) {
+    console.log('not ready yet');
+    return;
+  }
+  switch (e.data.type) {
+    case 'offer':
+      handleOffer(e.data);
+      break;
+    case 'answer':
+      handleAnswer(e.data);
+      break;
+    case 'candidate':
+      handleCandidate(e.data);
+      break;
+    case 'ready':
+      // A second tab joined. This tab will initiate a call unless in a call already.
+      if (pc) {
+        console.log('already in call, ignoring');
+        return;
+      }
+      makeCall();
+      break;
+    case 'bye':
+      if (pc) {
+        hangup();
+      }
+      break;
+    default:
+      console.log('unhandled', e);
+      break;
+  }
+};
+
+export const start = async () => {
+  localStream = await navigator.mediaDevices.getUserMedia({audio: true, video: true});
+  localVideo.srcObject = localStream;
+
+  signaling.postMessage({type: 'ready'});
+};
+
+hangupButton.onclick = async () => {
+  hangup();
+  signaling.postMessage({type: 'bye'});
+};
+
+async function hangup() {
+  if (pc) {
+    pc.close();
+    pc = null;
+  }
+  localStream.getTracks().forEach(track => track.stop());
+  localStream = null;
+  startButton.disabled = false;
+  hangupButton.disabled = true;
+};
+
+function createPeerConnection() {
+  pc = new RTCPeerConnection();
+  pc.onicecandidate = e => {
+    const message = {
+      type: 'candidate',
+      candidate: null,
+    };
+    if (e.candidate) {
+      message.candidate = e.candidate.candidate;
+      message.sdpMid = e.candidate.sdpMid;
+      message.sdpMLineIndex = e.candidate.sdpMLineIndex;
+    }
+    signaling.postMessage(message);
+  };
+  pc.ontrack = e => remoteVideo.srcObject = e.streams[0];
+  localStream.getTracks().forEach(track => pc.addTrack(track, localStream));
+}
+
+async function makeCall() {
+  await createPeerConnection();
+
+  const offer = await pc.createOffer();
+  signaling.postMessage({type: 'offer', sdp: offer.sdp});
+  await pc.setLocalDescription(offer);
+}
+
+async function handleOffer(offer) {
+  if (pc) {
+    console.error('existing peerconnection');
+    return;
+  }
+  await createPeerConnection();
+  await pc.setRemoteDescription(offer);
+
+  const answer = await pc.createAnswer();
+  signaling.postMessage({type: 'answer', sdp: answer.sdp});
+  await pc.setLocalDescription(answer);
+}
+
+async function handleAnswer(answer) {
+  if (!pc) {
+    console.error('no peerconnection');
+    return;
+  }
+  await pc.setRemoteDescription(answer);
+}
+
+async function handleCandidate(candidate) {
+  if (!pc) {
+    console.error('no peerconnection');
+    return;
+  }
+  if (!candidate.candidate) {
+    await pc.addIceCandidate(null);
+  } else {
+    await pc.addIceCandidate(candidate);
+  }
+}
\ No newline at end of file
diff --git a/src/piano.ts b/src/piano.ts
index d9072ec..fb1a7ac 100644
--- a/src/piano.ts
+++ b/src/piano.ts
@@ -1,36 +1,21 @@
-import { PeerComm } from "./peering";
-import { Move } from "./types";
+// import { PeerComm } from "./peering";
+// import { Move } from "./types";
 
-export interface Game {
-  moveHistory: Move[];
-  isRedTurn: boolean;
-  isLocalRed: boolean;
-  isMoveLegal: (sourceIndex: number, destIndex: number) => boolean;
-  requestMove: () => ((sourceIndex: number, destIndex: number) => void);
-}
-enum State {
-  Starting,
-  RedTurn,
-  BlueTurn
-}
-interface PeerData {
-  senderId: string;
-  receiverId: string;
-  redId: string;
-  state: State;
-  move: Move | null;
-}
+// interface PeerData {
+//   action: 'move';
+//   sourceIndex: number;
+//   destIndex: number;
+// }
 
-export class Piano {
-  public game: Game;
-  private peerComm: PeerComm<PeerData>;
-  private receiver: (data: PeerData) => void;
-  public constructor(game: Game) {
-    this.game = game;
-    this.receiver = (data: PeerData) => {
-      const isGameStarting = game.moveHistory.length === 0;
-      const isLocalRed = data.redId === this.peerComm.id;
-    };
-    this.peerComm = new PeerComm<PeerData>(this.receiver)
-  }
-}
\ No newline at end of file
+// export class Piano {
+//   public game: IGame;
+//   private peerComm: PeerComm<PeerData>;
+//   public constructor(game: IGame) {
+//     this.game = game;
+//     this.receiver = (data: PeerData) => {
+//       const isGameStarting = game.moveHistory.length === 0;
+//       const isLocalRed = data.redId === this.peerComm.id;
+//     };
+//     this.peerComm = new PeerComm<PeerData>(this.receiver)
+//   }
+// }
\ No newline at end of file
diff --git a/src/types.ts b/src/types.ts
index 18cf62d..a680fdd 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -38,68 +38,89 @@ export class Coord {
 }
 
 export class Move {
-    public readonly sourceIndex: number;
-    public readonly destIndex: number;
-    public readonly isPieceRed: boolean;
-    constructor(isPieceRed: boolean, sourceIndex: number, destIndex: number) {
-        this.sourceIndex = sourceIndex;
-        this.destIndex = destIndex;
-        this.isPieceRed = isPieceRed;
+    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;
     }
 }
 
-interface Listener {
-    element: Element;
-    type: string;
-    listener: (event: Event) => void;
+export interface MoveResult {
+    isTerminal: boolean;
+    message?: string;
 }
 
-export class ListenerManager {
-    private listeners: Listener[] = [];
-
-    public addListener(element: Element, type: string, listener: (event: Event) => void): void {
-        const { listeners } = this;
-        element.addEventListener(type, listener);
-        listeners.push({ element, type, listener });
-    }
-
-    public removeListeners() {
-        const { listeners } = this;
-        listeners.forEach(value => {
-            const { element, type, listener } = value;
-            element.removeEventListener(type, listener);
-        });
-        listeners.length = 0;
-    }
+export interface IGame {
+    moveHistory: Move[];
+    currentPlayerId: string;
+    getValidMoves: (start: Coord) => Coord[];
+    makeMove: (source: Coord, dest: Coord) => MoveResult;
+    undoMove: () => void;
+    get canUndoMove(): boolean;
 }
 
 export class GameBoard {
-    public board: number[];
-    public rows: number;
-    public cols: number;
+    private board: (string | null)[];
+    public readonly rows: number;
+    public readonly cols: number;
+    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() {
+    public constructor(player1Id: string, player2Id: string) {
         this.board = [
-            0, null, null, 4,
-            null, 1, 5, null,
-            null, 6, 2, null,
-            7, null, null, 3
+            player1Id, null, null, player2Id,
+            null, player1Id, player2Id, null,
+            null, player2Id, player1Id, null,
+            player2Id, null, null, player1Id
         ];
         this.rows = 4;
         this.cols = 4;
+        this.player1Id = player1Id;
+        this.player2Id = player2Id;
     }
-    public coordToBoardIndex({ row, col }: Coord): number {
+    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 ? this.player1Id : this.player2Id;
+    }
+    private coordToBoardIndex({ row, col }: Coord): number {
         const index = row * 4 + col;
         return index;
     }
-    public boardIndexToCoord(index: number): Coord {
+    private boardIndexToCoord(index: number): Coord {
         const col = index % 4;
         const row = Math.floor(index / 4);
         return new Coord(row, col);
     }
-}
\ No newline at end of file
+}