fuzzyworld

dao-game / commit

Moved most of the game logic into its own class.

4918da41205ce456f112f772b4db066ffe6220d4 · Pavel Safronov · 2022-03-16 22:47:06 -0700 · parents 2909d32401d1

Moved most of the game logic into its own class.
Implemented basic online game as an extension of the base game.
Seems to work.
Next step: add peer support.
 package-lock.json                | 693 ++++++++++++++++++++++++++++++++++++++-
 public/index.html                | 121 ++++---
 public/online.html               |   8 +-
 public/styles.css                |  10 +-
 src/app.ts                       |   3 +
 src/game.ts                      |  76 +++--
 src/listeners.ts                 |  24 ++
 src/local-game.ts                | 462 +++++++-------------------
 src/peering.ts                   |  51 ---
 src/peering_.ts                  | 119 -------
 src/piano.ts                     |  21 --
 src/types.ts                     |  39 ++-
 tools/esbuild-build-and-serve.ts |   2 +-
 tools/esbuild-watch.ts           |   2 +-
 tools/esbuild.ts                 |   2 +-
 15 files changed, 968 insertions(+), 665 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index edb445e..59eac04 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,8 +1,699 @@
 {
   "name": "dao-game",
   "version": "1.0.0",
-  "lockfileVersion": 1,
+  "lockfileVersion": 2,
   "requires": true,
+  "packages": {
+    "": {
+      "name": "dao-game",
+      "version": "1.0.0",
+      "license": "ISC",
+      "dependencies": {
+        "esbuild": "^0.14.25",
+        "peerjs": "^1.3.2"
+      },
+      "devDependencies": {
+        "@types/node": "^17.0.21",
+        "rimraf": "^3.0.2",
+        "ts-node": "^10.6.0",
+        "typescript": "^4.6.2"
+      }
+    },
+    "node_modules/@cspotcode/source-map-consumer": {
+      "version": "0.8.0",
+      "resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz",
+      "integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==",
+      "dev": true,
+      "engines": {
+        "node": ">= 12"
+      }
+    },
+    "node_modules/@cspotcode/source-map-support": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz",
+      "integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==",
+      "dev": true,
+      "dependencies": {
+        "@cspotcode/source-map-consumer": "0.8.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@tsconfig/node10": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz",
+      "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==",
+      "dev": true
+    },
+    "node_modules/@tsconfig/node12": {
+      "version": "1.0.9",
+      "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz",
+      "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==",
+      "dev": true
+    },
+    "node_modules/@tsconfig/node14": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz",
+      "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==",
+      "dev": true
+    },
+    "node_modules/@tsconfig/node16": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz",
+      "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==",
+      "dev": true
+    },
+    "node_modules/@types/node": {
+      "version": "17.0.21",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.21.tgz",
+      "integrity": "sha512-DBZCJbhII3r90XbQxI8Y9IjjiiOGlZ0Hr32omXIZvwwZ7p4DMMXGrKXVyPfuoBOri9XNtL0UK69jYIBIsRX3QQ==",
+      "dev": true
+    },
+    "node_modules/acorn": {
+      "version": "8.7.0",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
+      "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==",
+      "dev": true,
+      "bin": {
+        "acorn": "bin/acorn"
+      },
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/acorn-walk": {
+      "version": "8.2.0",
+      "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
+      "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/arg": {
+      "version": "4.1.3",
+      "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
+      "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
+      "dev": true
+    },
+    "node_modules/balanced-match": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+      "dev": true
+    },
+    "node_modules/brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "dev": true,
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/concat-map": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+      "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+      "dev": true
+    },
+    "node_modules/create-require": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
+      "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
+      "dev": true
+    },
+    "node_modules/diff": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
+      "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.3.1"
+      }
+    },
+    "node_modules/esbuild": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.25.tgz",
+      "integrity": "sha512-4JHEIOMNFvK09ziiL+iVmldIhLbn49V4NAVo888tcGFKedEZY/Y8YapfStJ6zSE23tzYPKxqKwQBnQoIO0BI/Q==",
+      "hasInstallScript": true,
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "optionalDependencies": {
+        "esbuild-android-64": "0.14.25",
+        "esbuild-android-arm64": "0.14.25",
+        "esbuild-darwin-64": "0.14.25",
+        "esbuild-darwin-arm64": "0.14.25",
+        "esbuild-freebsd-64": "0.14.25",
+        "esbuild-freebsd-arm64": "0.14.25",
+        "esbuild-linux-32": "0.14.25",
+        "esbuild-linux-64": "0.14.25",
+        "esbuild-linux-arm": "0.14.25",
+        "esbuild-linux-arm64": "0.14.25",
+        "esbuild-linux-mips64le": "0.14.25",
+        "esbuild-linux-ppc64le": "0.14.25",
+        "esbuild-linux-riscv64": "0.14.25",
+        "esbuild-linux-s390x": "0.14.25",
+        "esbuild-netbsd-64": "0.14.25",
+        "esbuild-openbsd-64": "0.14.25",
+        "esbuild-sunos-64": "0.14.25",
+        "esbuild-windows-32": "0.14.25",
+        "esbuild-windows-64": "0.14.25",
+        "esbuild-windows-arm64": "0.14.25"
+      }
+    },
+    "node_modules/esbuild-android-64": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.25.tgz",
+      "integrity": "sha512-L5vCUk7TzFbBnoESNoXjU3x9+/+7TDIE/1mTfy/erAfvZAqC+S3sp/Qa9wkypFMcFvN9FzvESkTlpeQDolREtQ==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-android-arm64": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.25.tgz",
+      "integrity": "sha512-4jv5xPjM/qNm27T5j3ZEck0PvjgQtoMHnz4FzwF5zNP56PvY2CT0WStcAIl6jNlsuDdN63rk2HRBIsO6xFbcFw==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-darwin-64": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.25.tgz",
+      "integrity": "sha512-TGp8tuudIxOyWd1+8aYPxQmC1ZQyvij/AfNBa35RubixD0zJ1vkKHVAzo0Zao1zcG6pNqiSyzfPto8vmg0s7oA==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-darwin-arm64": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.25.tgz",
+      "integrity": "sha512-oTcDgdm0MDVEmw2DWu8BV68pYuImpFgvWREPErBZmNA4MYKGuBRaCiJqq6jZmBR1x+3y1DWCjez+5uLtuAm6mw==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-freebsd-64": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.25.tgz",
+      "integrity": "sha512-ueAqbnMZ8arnuLH8tHwTCQYeptnHOUV7vA6px6j4zjjQwDx7TdP7kACPf3TLZLdJQ3CAD1XCvQ2sPhX+8tacvQ==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-freebsd-arm64": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.25.tgz",
+      "integrity": "sha512-+ZVWud2HKh+Ob6k/qiJWjBtUg4KmJGGmbvEXXW1SNKS7hW7HU+Zq2ZCcE1akFxOPkVB+EhOty/sSek30tkCYug==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-linux-32": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.25.tgz",
+      "integrity": "sha512-3OP/lwV3kCzEz45tobH9nj+uE4ubhGsfx+tn0L26WAGtUbmmcRpqy7XRG/qK7h1mClZ+eguIANcQntYMdYklfw==",
+      "cpu": [
+        "ia32"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-linux-64": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.25.tgz",
+      "integrity": "sha512-+aKHdHZmX9qwVlQmu5xYXh7GsBFf4TWrePgeJTalhXHOG7NNuUwoHmketGiZEoNsWyyqwH9rE5BC+iwcLY30Ug==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-linux-arm": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.25.tgz",
+      "integrity": "sha512-aTLcE2VBoLydL943REcAcgnDi3bHtmULSXWLbjtBdtykRatJVSxKMjK9YlBXUZC4/YcNQfH7AxwVeQr9fNxPhw==",
+      "cpu": [
+        "arm"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-linux-arm64": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.25.tgz",
+      "integrity": "sha512-UxfenPx/wSZx55gScCImPtXekvZQLI2GW3qe5dtlmU7luiqhp5GWPzGeQEbD3yN3xg/pHc671m5bma5Ns7lBHw==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-linux-mips64le": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.25.tgz",
+      "integrity": "sha512-wLWYyqVfYx9Ur6eU5RT92yJVsaBGi5RdkoWqRHOqcJ38Kn60QMlcghsKeWfe9jcYut8LangYZ98xO1LxIoSXrQ==",
+      "cpu": [
+        "mips64el"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-linux-ppc64le": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.25.tgz",
+      "integrity": "sha512-0dR6Csl6Zas3g4p9ULckEl8Mo8IInJh33VCJ3eaV1hj9+MHGdmDOakYMN8MZP9/5nl+NU/0ygpd14cWgy8uqRw==",
+      "cpu": [
+        "ppc64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-linux-riscv64": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.25.tgz",
+      "integrity": "sha512-J4d20HDmTrgvhR0bdkDhvvJGaikH3LzXQnNaseo8rcw9Yqby9A90gKUmWpfwqLVNRILvNnAmKLfBjCKU9ajg8w==",
+      "cpu": [
+        "riscv64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-linux-s390x": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.25.tgz",
+      "integrity": "sha512-YI2d5V6nTE73ZnhEKQD7MtsPs1EtUZJ3obS21oxQxGbbRw1G+PtJKjNyur+3t6nzHP9oTg6GHQ3S3hOLLmbDIQ==",
+      "cpu": [
+        "s390x"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-netbsd-64": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.25.tgz",
+      "integrity": "sha512-TKIVgNWLUOkr+Exrye70XTEE1lJjdQXdM4tAXRzfHE9iBA7LXWcNtVIuSnphTqpanPzTDFarF0yqq4kpbC6miA==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-openbsd-64": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.25.tgz",
+      "integrity": "sha512-QgFJ37A15D7NIXBTYEqz29+uw3nNBOIyog+3kFidANn6kjw0GHZ0lEYQn+cwjyzu94WobR+fes7cTl/ZYlHb1A==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-sunos-64": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.25.tgz",
+      "integrity": "sha512-rmWfjUItYIVlqr5EnTH1+GCxXiBOC42WBZ3w++qh7n2cS9Xo0lO5pGSG2N+huOU2fX5L+6YUuJ78/vOYvefeFw==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-windows-32": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.25.tgz",
+      "integrity": "sha512-HGAxVUofl3iUIz9W10Y9XKtD0bNsK9fBXv1D55N/ljNvkrAYcGB8YCm0v7DjlwtyS6ws3dkdQyXadbxkbzaKOA==",
+      "cpu": [
+        "ia32"
+      ],
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-windows-64": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.25.tgz",
+      "integrity": "sha512-TirEohRkfWU9hXLgoDxzhMQD1g8I2mOqvdQF2RS9E/wbkORTAqJHyh7wqGRCQAwNzdNXdg3JAyhQ9/177AadWA==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-windows-arm64": {
+      "version": "0.14.25",
+      "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.25.tgz",
+      "integrity": "sha512-4ype9ERiI45rSh+R8qUoBtaj6kJvUOI7oVLhKqPEpcF4Pa5PpT3hm/mXAyotJHREkHpM87PAJcA442mLnbtlNA==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/eventemitter3": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz",
+      "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q=="
+    },
+    "node_modules/fs.realpath": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+      "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+      "dev": true
+    },
+    "node_modules/glob": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
+      "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
+      "dev": true,
+      "dependencies": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.0.4",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      },
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/inflight": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+      "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+      "dev": true,
+      "dependencies": {
+        "once": "^1.3.0",
+        "wrappy": "1"
+      }
+    },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+      "dev": true
+    },
+    "node_modules/make-error": {
+      "version": "1.3.6",
+      "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
+      "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
+      "dev": true
+    },
+    "node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dev": true,
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+      "dev": true,
+      "dependencies": {
+        "wrappy": "1"
+      }
+    },
+    "node_modules/path-is-absolute": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+      "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/peerjs": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/peerjs/-/peerjs-1.3.2.tgz",
+      "integrity": "sha512-+PHfmsC7QGUU8Ye3OLi6tKQZGPCNy7QatUVNw4JtE8alkguF3+DdO5W0bzepqP2OtE9FqH/ltXt37qyvHw2CqA==",
+      "dependencies": {
+        "@types/node": "^10.14.33",
+        "eventemitter3": "^3.1.2",
+        "peerjs-js-binarypack": "1.0.1",
+        "webrtc-adapter": "^7.7.1"
+      }
+    },
+    "node_modules/peerjs-js-binarypack": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/peerjs-js-binarypack/-/peerjs-js-binarypack-1.0.1.tgz",
+      "integrity": "sha512-N6aeia3NhdpV7kiGxJV5xQiZZCVEEVjRz2T2C6UZQiBkHWHzUv/oWA4myQLcwBwO8LUoR1KWW5oStvwVesmfCg=="
+    },
+    "node_modules/peerjs/node_modules/@types/node": {
+      "version": "10.17.60",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz",
+      "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw=="
+    },
+    "node_modules/rimraf": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+      "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+      "dev": true,
+      "dependencies": {
+        "glob": "^7.1.3"
+      },
+      "bin": {
+        "rimraf": "bin.js"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/rtcpeerconnection-shim": {
+      "version": "1.2.15",
+      "resolved": "https://registry.npmjs.org/rtcpeerconnection-shim/-/rtcpeerconnection-shim-1.2.15.tgz",
+      "integrity": "sha512-C6DxhXt7bssQ1nHb154lqeL0SXz5Dx4RczXZu2Aa/L1NJFnEVDxFwCBo3fqtuljhHIGceg5JKBV4XJ0gW5JKyw==",
+      "dependencies": {
+        "sdp": "^2.6.0"
+      },
+      "engines": {
+        "node": ">=6.0.0",
+        "npm": ">=3.10.0"
+      }
+    },
+    "node_modules/sdp": {
+      "version": "2.12.0",
+      "resolved": "https://registry.npmjs.org/sdp/-/sdp-2.12.0.tgz",
+      "integrity": "sha512-jhXqQAQVM+8Xj5EjJGVweuEzgtGWb3tmEEpl3CLP3cStInSbVHSg0QWOGQzNq8pSID4JkpeV2mPqlMDLrm0/Vw=="
+    },
+    "node_modules/ts-node": {
+      "version": "10.6.0",
+      "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.6.0.tgz",
+      "integrity": "sha512-CJen6+dfOXolxudBQXnVjRVvYTmTWbyz7cn+xq2XTsvnaXbHqr4gXSCNbS2Jj8yTZMuGwUoBESLaOkLascVVvg==",
+      "dev": true,
+      "dependencies": {
+        "@cspotcode/source-map-support": "0.7.0",
+        "@tsconfig/node10": "^1.0.7",
+        "@tsconfig/node12": "^1.0.7",
+        "@tsconfig/node14": "^1.0.0",
+        "@tsconfig/node16": "^1.0.2",
+        "acorn": "^8.4.1",
+        "acorn-walk": "^8.1.1",
+        "arg": "^4.1.0",
+        "create-require": "^1.1.0",
+        "diff": "^4.0.1",
+        "make-error": "^1.1.1",
+        "v8-compile-cache-lib": "^3.0.0",
+        "yn": "3.1.1"
+      },
+      "bin": {
+        "ts-node": "dist/bin.js",
+        "ts-node-cwd": "dist/bin-cwd.js",
+        "ts-node-script": "dist/bin-script.js",
+        "ts-node-transpile-only": "dist/bin-transpile.js",
+        "ts-script": "dist/bin-script-deprecated.js"
+      },
+      "peerDependencies": {
+        "@swc/core": ">=1.2.50",
+        "@swc/wasm": ">=1.2.50",
+        "@types/node": "*",
+        "typescript": ">=2.7"
+      },
+      "peerDependenciesMeta": {
+        "@swc/core": {
+          "optional": true
+        },
+        "@swc/wasm": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/typescript": {
+      "version": "4.6.2",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.2.tgz",
+      "integrity": "sha512-HM/hFigTBHZhLXshn9sN37H085+hQGeJHJ/X7LpBWLID/fbc2acUMfU+lGD98X81sKP+pFa9f0DZmCwB9GnbAg==",
+      "dev": true,
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=4.2.0"
+      }
+    },
+    "node_modules/v8-compile-cache-lib": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.0.tgz",
+      "integrity": "sha512-mpSYqfsFvASnSn5qMiwrr4VKfumbPyONLCOPmsR3A6pTY/r0+tSaVbgPWSAIuzbk3lCTa+FForeTiO+wBQGkjA==",
+      "dev": true
+    },
+    "node_modules/webrtc-adapter": {
+      "version": "7.7.1",
+      "resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-7.7.1.tgz",
+      "integrity": "sha512-TbrbBmiQBL9n0/5bvDdORc6ZfRY/Z7JnEj+EYOD1ghseZdpJ+nF2yx14k3LgQKc7JZnG7HAcL+zHnY25So9d7A==",
+      "dependencies": {
+        "rtcpeerconnection-shim": "^1.2.15",
+        "sdp": "^2.12.0"
+      },
+      "engines": {
+        "node": ">=6.0.0",
+        "npm": ">=3.10.0"
+      }
+    },
+    "node_modules/wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
+      "dev": true
+    },
+    "node_modules/yn": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
+      "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
+      "dev": true,
+      "engines": {
+        "node": ">=6"
+      }
+    }
+  },
   "dependencies": {
     "@cspotcode/source-map-consumer": {
       "version": "0.8.0",
diff --git a/public/index.html b/public/index.html
index ae1c898..5eb09b2 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1,73 +1,62 @@
 <!DOCTYPE html>
 <html lang="en">
-    <head>
-        <meta charset="utf-8" />
-        <title>Dao Game</title>
-        <meta name="viewport" content="width=device-width, initial-scale=1" />
-        <link rel="icon" href="yin-yang.svg" type="image/svg+xml" />
-        <link rel="stylesheet" href="styles.css" />
-        <script defer src="bundle.js"></script>
-    </head>
 
-    <body>
-        <div class="invisible">
-            <p class="piece red-piece" id="default-red-piece"></p>
-            <p class="piece blue-piece" id="default-blue-piece"></p>
+<head>
+    <meta charset="utf-8" />
+    <title>Dao Game</title>
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
+    <link rel="icon" href="yin-yang.svg" type="image/svg+xml" />
+    <link rel="stylesheet" href="styles.css" />
+    <script defer src="bundle.js"></script>
+</head>
+
+<body>
+    <div class="invisible">
+        <p class="piece p1-piece" id="default-p1-piece"></p>
+        <p class="piece p2-piece" id="default-p2-piece"></p>
+    </div>
+    <main>
+        <div class="controls">
+            <span class="p1-turn-text"> Red's turn! </span>
+            <span class="divider">|</span>
+            <span class="p2-turn-text"> Blue's turn </span>
         </div>
-        <main>
-            <div class="controls">
-                <span class="red-turn-text"> Red's turn! </span>
 
-                <span class="divider">|</span>
+        <table id="table"></table>
 
-                <span class="blue-turn-text"> Blue's turn </span>
-            </div>
-
-            <table id="table"></table>
-
-            <div class="controls">
-                <button type="button" class="button" id="undo-button">
-                    Undo
-                </button>
-                <span class="divider">|</span>
-                <button type="button" class="button" id="reset-button">
-                    Reset
-                </button>
-            </div>
-        </main>
-        <hr />
-
-        <div class="title">
-            <p>
-                Board game
-                <a
-                    href="https://en.wikipedia.org/wiki/Dao_(game)"
-                    target="_blank"
-                    rel="noopener"
-                    >Dao</a
-                >.
-            </p>
-        </div>
-
-        <div class="info">
-            The game consists of only one simple rule and three ways to win.
-            Pieces move in a straight line, including diagonally, as far as
-            possible with no jumps. The three ways to win are arranging your
-            pieces in a straight line on the board (not diagonally), occupying
-            the four corners, or forming a 2x2 square anywhere on the board.
-        </div>
-        <hr />
-
-        <div class="info">
-            <p>
-                Here is the
-                <a
-                    href="https://gitlab.com/fuzzy-rp/dao-game"
-                    target="_blank"
-                    rel="noopener"
-                    >source code</a
-                >
-            </p>
+        <div class="controls">
+            <button type="button" class="button" id="undo-button">
+                Undo
+            </button>
+            <span class="divider">|</span>
+            <button type="button" class="button" id="reset-button">
+                Reset
+            </button>
         </div>
-    </body>
-</html>
+    </main>
+    <hr />
+
+    <div class="title">
+        <p>Board game
+            <a href="https://en.wikipedia.org/wiki/Dao_(game)" target="_blank" rel="noopener">Dao</a>.
+        </p>
+    </div>
+
+    <div class="info">
+        The game consists of only one simple rule and three ways to win.
+        Pieces move in a straight line, including diagonally, as far as
+        possible with no jumps. The three ways to win are arranging your
+        pieces in a straight line on the board (not diagonally), occupying
+        the four corners, or forming a 2x2 square anywhere on the board.
+    </div>
+    <hr />
+
+    <div class="info">
+        <p>
+            Here is the
+            <a href="https://gitlab.com/fuzzy-rp/dao-game" target="_blank" rel="noopener">source code</a>
+        </p>
+    </div>
+</body>
+
+</html>
\ No newline at end of file
diff --git a/public/online.html b/public/online.html
index 1d4f8ac..88997d3 100644
--- a/public/online.html
+++ b/public/online.html
@@ -14,16 +14,16 @@
 
     <body>
         <div class="invisible">
-            <p class="piece red-piece" id="default-red-piece"></p>
-            <p class="piece blue-piece" id="default-blue-piece"></p>
+            <p class="piece p1-piece" id="default-p1-piece"></p>
+            <p class="piece p2-piece" id="default-p2-piece"></p>
         </div>
         <main>
             <div class="controls">
-                <span class="red-turn-text"> Red's turn! </span>
+                <span class="p1-turn-text"> Red's turn! </span>
 
                 <span class="divider">|</span>
 
-                <span class="blue-turn-text"> Blue's turn </span>
+                <span class="p2-turn-text"> Blue's turn </span>
             </div>
 
             <table id="table"></table>
diff --git a/public/styles.css b/public/styles.css
index cb59d42..021b0df 100644
--- a/public/styles.css
+++ b/public/styles.css
@@ -65,7 +65,7 @@ td {
     margin-right: 15px;
 }
 
-.red-turn-text, .blue-turn-text {
+.p1-turn-text, .p2-turn-text {
     text-align: center;
     color: lightgray;
     background-color: white;
@@ -77,11 +77,11 @@ td {
     user-select: none;
 }
 
-.red-turn-text.selected-player-text {
+.p1-turn-text.selected-player-text {
     background-color: red;
 }
 
-.blue-turn-text.selected-player-text {
+.p2-turn-text.selected-player-text {
     background-color: blue;
 }
 
@@ -120,11 +120,11 @@ td {
     /* background-image: url(yin-yang.svg); */
 }
 
-.red-piece {
+.p1-piece {
     background-color: crimson;
     /* background-image: url(yin-yang.svg); */
 }
-.blue-piece {
+.p2-piece {
     background-color: mediumblue;
     /* background-image: url(yin-yang.svg); */
 }
diff --git a/src/app.ts b/src/app.ts
index e69de29..ceec005 100644
--- a/src/app.ts
+++ b/src/app.ts
@@ -0,0 +1,3 @@
+import { playGame } from './local-game'
+
+playGame();
\ No newline at end of file
diff --git a/src/game.ts b/src/game.ts
index b4233ca..5c5a68d 100644
--- a/src/game.ts
+++ b/src/game.ts
@@ -1,22 +1,41 @@
-import { Coord, GameBoard, IGame, 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: (isP1: boolean) => Coord[];
+    coordToBoardIndex: (coord: Coord) => number;
+    getValidMoves: (start: Coord) => Coord[];
+    makeMove: (source: Coord, dest: Coord) => MoveResult;
+    undoMove: () => Move;
+}
 
 export class Game implements IGame {
-    private readonly gameBoard: GameBoard;
-    private readonly player1Id: string;
-    private readonly player2Id: string;
     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._player1Id = player1Id;
+        this._player2Id = player2Id;
         this.isGameOver = false;
         this._moveHistory = [];
+        this.currentPlayerId = player1Id;
     }
-
-    public get moveHistory(): Move[] { return this._moveHistory; }
     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(isP1: boolean): Coord[] { return this.gameBoard.getCoords(isP1 ? this._player1Id : this.player2Id); }
+    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 => {
@@ -86,14 +105,14 @@ export class Game implements IGame {
         const isP1Turn = (this.currentPlayerId === player1Id);
         const playerId = isP1Turn ? player1Id : player2Id;
 
+        this._moveHistory.push(new Move(playerId, source, dest));
         gameBoard.move(source, dest);
 
         const result = checkForWin(isP1Turn);
-        if (!result.isTerminal) {
-            this.currentPlayerId = isP1Turn ? this.player2Id : this.player1Id;
+        if (result.isTerminal) {
             this.isGameOver = true;
         } else {
-            this._moveHistory.push(new Move(playerId, source, dest));
+            this.currentPlayerId = isP1Turn ? this.player2Id : this.player1Id;
         }
         return result;
     }
@@ -106,23 +125,19 @@ export class Game implements IGame {
             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 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 || !withinBounds) {
+                if (isOccupied) {
                     break;
-                } else {
-                    farthestPos = pos;
-                    testPos = pos;
                 }
+
+                farthestPos = pos;
+                testPos = pos;
             }
             return farthestPos;
         };
@@ -133,20 +148,19 @@ export class Game implements IGame {
 
         return farthestCoords;
     }
-    public undoMove(): void {
-        if (!this.canUndoMove) {
-            throw "Newp! Can't undo!"
-        }
+    public undoMove(): Move {
+        if (!this.canUndoMove) { throw "Newp! Can't undo!" }
         const { gameBoard } = this;
-        const { source, dest, playerId } = this._moveHistory.pop();
+        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;        
+        return true;
     }
-
 }
diff --git a/src/listeners.ts b/src/listeners.ts
new file mode 100644
index 0000000..2ffe56e
--- /dev/null
+++ b/src/listeners.ts
@@ -0,0 +1,24 @@
+export 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;
+    }
+}
diff --git a/src/local-game.ts b/src/local-game.ts
index bcc0870..531ffb3 100644
--- a/src/local-game.ts
+++ b/src/local-game.ts
@@ -1,102 +1,71 @@
-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",
+import { Game, IGame } from "./game";
+import { Coord } from "./types";
+import { ListenerManager } from "./listeners"
+
+const classNames = {
+    selectedPiece: "selected-piece",
+    selectedPlayerText: "selected-player-text",
+    activePlayerPiece: "active-player-piece",
+    possibleMove: "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();
+let selectedPiece: Element | null = null;
+const game: IGame = new Game('Red', 'Blue');
 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"),
+    defaultP1Piece: document.getElementById("default-p1-piece"),
+    defaultP2Piece: document.getElementById("default-p2-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")),
+    playerPieces: (isP1Turn: boolean) => isP1Turn
+        ? Array.from(document.querySelectorAll(".p1-piece:not(#default-p1-piece)"))
+        : Array.from(document.querySelectorAll(".p2-piece:not(#default-p2-piece)")),
+    p1TurnText: () => Array.from(document.querySelectorAll(".p1-turn-text")),
+    p2TurnText: () => Array.from(document.querySelectorAll(".p2-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);
-    });
+const getPieceAtIndex = (index: number): Element => {
+    const cell = dom.cells()[index];
+    const piece = cell.children[0];
+    return piece;
+}
+const getPieceAtCoord = (coord: Coord): Element => {
+    const index = game.coordToBoardIndex(coord);
+    const piece = getPieceAtIndex(index);
+    return piece;
+}
 
+const resetBoard = () => {
+    // reset Cells
+    dom.cells().forEach(cell => cell.classList.remove(classNames.possibleMove));
     // 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);
-        }
+    dom.allPieces().forEach(piece => {
+        piece.classList.remove(classNames.activePlayerPiece)
+        piece.classList.remove(classNames.selectedPiece)
     });
-
     // remove all listeners
     listenerManager.removeListeners();
 };
 
-const movePiece = async (sourceIndex: number, destIndex: number) => {
-    const pieceId = gameBoard.board[sourceIndex];
+/**
+ * Animates the motion of piece moving from source to dest
+ * @param source Starting Coord
+ * @param dest Ending Coord
+ * @param playerId Player id
+ */
+const movePiece = async (source: Coord, dest: Coord) => {
+    const sourceIndex = game.coordToBoardIndex(source);
+    const destIndex = game.coordToBoardIndex(dest);
+
     // find current piece
-    const piece = document.getElementById(pieceId.toString());
+    const piece = dom.cells()[sourceIndex].children[0];
     // unselect piece
-    piece.classList.remove(constants.selectedPieceClassName);
+    piece.classList.remove(classNames.selectedPiece);
 
     // create clone for new position
     const destPiece = piece.cloneNode(true);
@@ -104,25 +73,19 @@ const movePiece = async (sourceIndex: number, destIndex: number) => {
     dom.cells()[destIndex].appendChild(destPiece);
 
     // start animations
-    const destAnimation = dom
-        .cells()
-    [destIndex].animate([{ opacity: 0 }, { opacity: 1 }], {
-        duration: 1000,
-        iterations: 1,
-    });
+    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",
-            },
-        ],
+        [{
+            opacity: 1,
+            width: "45px",
+            height: "45px",
+        }, {
+            opacity: 0,
+            width: "5px",
+            height: "5px",
+        }],
         { duration: 1000, iterations: 1 }
     );
 
@@ -131,142 +94,74 @@ const movePiece = async (sourceIndex: number, destIndex: number) => {
 
     // 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);
-                });
+        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));
             }
 
-            // 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);
+            piece.classList.add(classNames.selectedPiece);
+            selectedPiece = piece;
 
             // 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 allowedMoves = game.getValidMoves(source);
+            allowedMoves.map((dest: Coord) => {
+                const cell = dom.cells()[game.coordToBoardIndex(dest)];
+                cell.classList.add(classNames.possibleMove);
+                listenerManager.addListener(cell, "click", async () => await makeMove(source, dest));
             });
         };
 
-        const makeMove = async (destIndex) => {
+        // complete the move from source to dest
+        const makeMove = async (source: Coord, dest: Coord) => {
             listenerManager.removeListeners();
 
-            const sourceIndex = gameBoard.board.indexOf(player.selectedPieceId);
-            player.moveHistory.push(new Move(sourceIndex, destIndex));
+            const result = game.makeMove(source, dest);
+            await movePiece(source, dest);
 
-            await movePiece(sourceIndex, destIndex);
-
-            checkForWin();
-            nextMove();
+            if (result.isTerminal) {
+                alert(result.message);
+                document.location.reload();
+            } else {
+                nextMove();
+            }
         };
 
         // mark active pieces
-        const playerPieces = dom.playerPieces(player.isRedTurn);
-        playerPieces.forEach((pp) => {
-            pp.classList.add(constants.activePlayerPieceClassName);
-            listenerManager.addListener(pp, "click", selectPiece);
-        });
+        const playerPositions = game.getPlayerPositions(game.currentPlayerId === game.player1Id);
+        const piecesAndPositions = playerPositions.map(coord => ({ coord, piece: getPieceAtCoord(coord) }));
+        piecesAndPositions
+            .forEach(({ coord, piece }) => {
+                piece.classList.add(classNames.activePlayerPiece);
+                listenerManager.addListener(piece, "click", event => selectPiece(piece, coord));
+            });
     };
 
     // reset text
-    dom.redTurnText().forEach((t) =>
-        t.classList.remove(constants.selectedPlayerTextClassName)
-    );
-    dom.blueTurnText().forEach((t) =>
-        t.classList.remove(constants.selectedPlayerTextClassName)
-    );
+    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
-    if (player.isRedTurn) {
-        player.isRedTurn = false;
-        dom.blueTurnText().forEach((t) =>
-            t.classList.add(constants.selectedPlayerTextClassName)
-        );
+    const isP1Turn = game.currentPlayerId === game.player1Id;
+    if (isP1Turn) {
+        dom.p1TurnText().forEach((t) => t.classList.add(classNames.selectedPlayerText));
     } else {
-        player.isRedTurn = true;
-        dom.redTurnText().forEach((t) =>
-            t.classList.add(constants.selectedPlayerTextClassName)
-        );
+        dom.p2TurnText().forEach((t) => t.classList.add(classNames.selectedPlayerText));
     }
 
-    if (player.moveHistory.length === 0) {
+    dom.undoButton.disabled = game.canUndoMove;
+    if (game.moveHistory.length === 0) {
         dom.undoButton.disabled = true;
         dom.resetButton.disabled = true;
     } else {
@@ -274,92 +169,20 @@ const nextMove = () => {
         dom.resetButton.disabled = false;
     }
 
-    player.selectedPieceId = undefined;
+    selectedPiece = null;
+
     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++) {
+    for (let r = 0; r < game.size.row; r++) {
         const tr = document.createElement("tr");
         table.appendChild(tr);
-        for (let c = 0; c < gameBoard.cols; c++) {
+        for (let c = 0; c < game.size.col; c++) {
             const td = document.createElement("td");
             tr.appendChild(td);
             td.className = isWhite ? "white" : "black";
@@ -367,19 +190,19 @@ const init = () => {
         }
         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);
-        }
-    });
+    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 = game.coordToBoardIndex(coord);
+        dom.cells()[index].appendChild(newPiece);
+    };
+    game.getPlayerPositions(true).forEach(coord => createPiece(true, game.player1Id, coord));
+    game.getPlayerPositions(false).forEach(coord => createPiece(false, game.player2Id, coord));
 
     // hook up buttons
     let blockActions = false;
@@ -387,9 +210,6 @@ const init = () => {
         if (blockActions) {
             return;
         }
-        if (player.moveHistory.length === 0) {
-            return;
-        }
 
         blockActions = true;
         if (confirm("Reset?")) {
@@ -401,69 +221,19 @@ const init = () => {
         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);
+        const move = game.undoMove();
+        await movePiece(move.dest, move.source);
         nextMove();
+
         blockActions = false;
     });
 };
 
 // start!
-const playGame = () => {
+export 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
deleted file mode 100644
index ed11a3f..0000000
--- a/src/peering.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import Peer from "peerjs";
-
-const daoPeerIdKey = "dao-game-peer-id";
-export class PeerComm<T> {
-    public constructor(receiver: (data: T) => void) {
-        this.receiver = receiver;
-
-        // const peerId = localStorage.getItem(daoPeerIdKey) ?? `dao-game-peer-id-${Date.now()}`;
-        const peerId = `dao-game-peer-id-${Date.now()}`;
-        const peer = new Peer(peerId);
-        // localStorage.setItem(daoPeerIdKey, peerId);
-        this.id = peerId;
-        this.peer = peer;
-
-        peer.on('connection', (connection) => {
-            connection.on('data', data => {
-                receiver(JSON.parse(data));
-            });
-            connection.on('open', () => {
-                connection.send('hello');
-                this.conn = connection;
-            })
-        });
-    }
-
-    public get isConnected() {
-        return this.conn !== null;
-    }
-    public connect(otherPeerId: string): void {
-        if (this.conn !== null) {
-            throw "Already connected";
-        }
-        const conn = this.peer.connect(otherPeerId);
-        conn.on('open', () => {
-            conn.send('hi');
-            this.conn = conn;
-        });
-    }
-
-    public readonly id: string;
-    public receiver: (data: T) => void;
-    public send(data: T): void {
-        if (!this.isConnected) {
-            throw "Not connected";
-        }
-        this.conn.send(JSON.stringify(data));
-    }
-
-    private peer: Peer;
-    private conn: Peer.DataConnection | null = null;
-}
diff --git a/src/peering_.ts b/src/peering_.ts
deleted file mode 100644
index e89878a..0000000
--- a/src/peering_.ts
+++ /dev/null
@@ -1,119 +0,0 @@
-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
deleted file mode 100644
index fb1a7ac..0000000
--- a/src/piano.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-// import { PeerComm } from "./peering";
-// import { Move } from "./types";
-
-// interface PeerData {
-//   action: 'move';
-//   sourceIndex: number;
-//   destIndex: number;
-// }
-
-// 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 a680fdd..ff0e77f 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -15,6 +15,17 @@ export class 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;
+    }
+
     equals(otherCoord: Coord): boolean {
         const areEqual = (this.col === otherCoord.col && this.row === otherCoord.row);
         return areEqual;
@@ -53,19 +64,9 @@ export interface MoveResult {
     message?: string;
 }
 
-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 {
     private board: (string | null)[];
-    public readonly rows: number;
-    public readonly cols: number;
+    public readonly size: Coord;
     public readonly player1Id: string;
     public readonly player2Id: string
 
@@ -82,8 +83,7 @@ export class GameBoard {
             null, player2Id, player1Id, null,
             player2Id, null, null, player1Id
         ];
-        this.rows = 4;
-        this.cols = 4;
+        this.size = new Coord(4, 4);
         this.player1Id = player1Id;
         this.player2Id = player2Id;
     }
@@ -112,15 +112,18 @@ export class GameBoard {
     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;
+        this.board[index] = isP1 === null ?
+            null :
+            (isP1 ? this.player1Id : this.player2Id);
     }
-    private coordToBoardIndex({ row, col }: Coord): number {
-        const index = row * 4 + col;
+    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 % 4;
-        const row = Math.floor(index / 4);
+        const col = index % this.size.col;
+        const row = Math.floor(index / this.size.col);
         return new Coord(row, col);
     }
 }
diff --git a/tools/esbuild-build-and-serve.ts b/tools/esbuild-build-and-serve.ts
index 3be8fb1..1f39f91 100644
--- a/tools/esbuild-build-and-serve.ts
+++ b/tools/esbuild-build-and-serve.ts
@@ -8,7 +8,7 @@ const serveOptions: esbuild.ServeOptions = {
     servedir: 'public'
 };
 const buildOptions: esbuild.BuildOptions = {
-    entryPoints: ['src/game.ts'],
+    entryPoints: ['src/app.ts'],
     bundle: true,
     outfile: 'public/bundle.js',
     sourcemap: true
diff --git a/tools/esbuild-watch.ts b/tools/esbuild-watch.ts
index 602f1c3..6843d69 100644
--- a/tools/esbuild-watch.ts
+++ b/tools/esbuild-watch.ts
@@ -1,7 +1,7 @@
 import esbuild from 'esbuild';
 
 esbuild.build({
-    entryPoints: ['src/game.ts'],
+    entryPoints: ['src/app.ts'],
     bundle: true,
     outfile: 'public/bundle.js',
     watch: true,
diff --git a/tools/esbuild.ts b/tools/esbuild.ts
index 24ef6db..dc7e306 100644
--- a/tools/esbuild.ts
+++ b/tools/esbuild.ts
@@ -1,7 +1,7 @@
 import esbuild from 'esbuild';
 
 esbuild.build({
-    entryPoints: ['src/game.ts'],
+    entryPoints: ['src/app.ts'],
     bundle: true,
     outfile: 'public/bundle.js',
 }).catch(() => process.exit(1))
\ No newline at end of file