fuzzyworld

dao-game / commit

Starting to think more about the HTML format of the site, so adding a landing page and separate pages for local and remote games.

5805825cf3b74303756a2dd97cd4e0cf20ce9a7a · Pavel Safronov · 2022-03-21 12:38:16 -0700 · parents 0f7e0e228403

Starting to think more about the HTML format of the site, so adding a landing page and separate pages for local and remote games.
Added the concept of player name, which is distinct from player id. Mostly used for reporting terminal conditions.
Altered how the mouse position information is calculated and used. Now a relative (ratio) position is sent to the other peer.
Using a test board for this work, will need to undo that before merging to main.
Localized all esbuild settings in a single spot, so now only 1 file needs to be updated, and then all build paths will be updated.
 .gitignore                       |   2 +-
 package-lock.json                | 693 ++++++++++++++++++++++++++++++++++++++-
 public/index.html                |  80 ++---
 public/local.html                |  61 ++++
 public/online.html               |  94 +++---
 public/styles.css                |   1 +
 src/game.ts                      |  27 +-
 src/local-game.ts                |  11 +-
 src/remote-game.ts               |  88 +++--
 src/types.ts                     |  36 +-
 tools/esbuild-build-and-serve.ts |  13 +-
 tools/esbuild-settings.ts        |  13 +
 tools/esbuild-watch.ts           |   8 +-
 tools/esbuild.ts                 |   7 +-
 14 files changed, 958 insertions(+), 176 deletions(-)

diff --git a/.gitignore b/.gitignore
index 9b60cca..6760a00 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,4 +5,4 @@
 /.git/*
 
 # artifacts
-/public/bundle.js
\ No newline at end of file
+/public/js
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 4ed5c54..5271b1e 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1,54 +1,44 @@
 <!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="online.js"></script>
-  </head>
 
-  <body>
-    <div id="placeholders" class="invisible">
-      <p class="piece p1-piece" id="default-p1-piece"></p>
-      <p class="piece p2-piece" id="default-p2-piece"></p>
-      <div id="other-mouse">
-        <img src="./cursor.png" alt="cursor" />
-      </div>
-    </div>
-    <main class="invisible">
-      <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>
-
-      <table id="table"></table>
-    </main>
+<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" />
+</head>
 
-    <hr />
-    <div id="shareLink">Share this link to play with an opponent.</div>
-    <hr />
+<body>
+  <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="title">
-      <p>Board game <a href="https://en.wikipedia.org/wiki/Dao_(game)" target="_blank" rel="noopener">Dao</a>.</p>
+  <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>
+    <div>
+      <a href='local.html'>Play local (pass-and-play) game of Dao.</a>
     </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>
+      <a href='online.html'>Play online game of Dao.</a>
     </div>
-    <hr />
+  </div>
 
-    <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>
+  <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>
+
+  <div id="snackbar" class="info"></div>
+</body>
 
-    <div id="snackbar"></div>
-  </body>
-</html>
+</html>
\ No newline at end of file
diff --git a/public/local.html b/public/local.html
new file mode 100644
index 0000000..f48f8c6
--- /dev/null
+++ b/public/local.html
@@ -0,0 +1,61 @@
+<!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="js/local.js"></script>
+</head>
+
+<body>
+    <div id="placeholders" 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>
+
+        <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>
+
+    <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>
+
+    <div id="snackbar"></div>
+</body>
+
+</html>
\ No newline at end of file
diff --git a/public/online.html b/public/online.html
index 91997b5..f7fefe2 100644
--- a/public/online.html
+++ b/public/online.html
@@ -1,50 +1,60 @@
 <!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="online.js"></script>
-  </head>
-
-  <body>
-    <div id="placeholders" class="invisible">
-      <p class="piece p1-piece" id="default-p1-piece"></p>
-      <p class="piece p2-piece" id="default-p2-piece"></p>
-      <div id="other-mouse">
-        <img src="./cursor.png" alt="cursor" />
-      </div>
+
+<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="js/online.js"></script>
+</head>
+
+<body>
+  <div id="placeholders" class="invisible">
+    <p class="piece p1-piece" id="default-p1-piece"></p>
+    <p class="piece p2-piece" id="default-p2-piece"></p>
+  </div>
+  <main class="invisible">
+    <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 class="invisible">
-      <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>
 
-      <table id="table"></table>
-    </main>
+    <table id="table"></table>
+  </main>
 
-    <hr />
+  <hr />
+  <div id="shareLink">Share this link to play with an opponent.</div>
+  <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="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">
+    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>
+  <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>
+
+  <div id="snackbar" class="info"></div>
+
+  <div id="other-mouse">
+    <img src="./cursor.png" alt="cursor" />
+  </div>
+
+</body>
+
+</html>
\ No newline at end of file
diff --git a/public/styles.css b/public/styles.css
index ccd4f8e..93b9ebe 100644
--- a/public/styles.css
+++ b/public/styles.css
@@ -33,6 +33,7 @@ div {
   position: absolute;
   z-index: 1000;
   pointer-events: none;
+  display: none;
 }
 
 .invisible {
diff --git a/src/game.ts b/src/game.ts
index f5a5881..55a2b96 100644
--- a/src/game.ts
+++ b/src/game.ts
@@ -4,7 +4,9 @@ export interface IGame {
   size: Coord;
   moveHistory: Move[];
   player1Id: string;
+  player1Name: string;
   player2Id: string;
+  player2Name: string;
   currentPlayerId: string;
   canUndoMove: boolean;
   getPlayerPositions: (playerId: string) => Coord[];
@@ -18,17 +20,20 @@ export interface IGame {
 export class Game implements IGame {
   private isGameOver: boolean;
   private readonly gameBoard: GameBoard;
-  private readonly _player1Id: string;
-  private readonly _player2Id: string;
   private readonly _moveHistory: Move[];
+  public readonly player1Id: string;
+  public readonly player2Id: string;
+  public readonly player1Name: string;
+  public readonly player2Name: string;
 
-  public constructor(player1Id: string, player2Id: string) {
+  public constructor(player1Id: string, player1Name: string, player2Id: string, player2Name: string) {
     this.gameBoard = new GameBoard(player1Id, player2Id);
-    this._player1Id = player1Id;
-    this._player2Id = player2Id;
+    this.player1Id = player1Id;
+    this.player2Id = player2Id;
+    this.player1Name = player1Name;
+    this.player2Name = player2Name;
     this.isGameOver = false;
     this._moveHistory = [];
-    // this.currentPlayerId = Math.random() > 0.5 ? player1Id : player2Id;
     this.currentPlayerId = player1Id;
   }
   getPiece(pos: Coord): string {
@@ -42,12 +47,6 @@ export class Game implements IGame {
   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;
   }
@@ -68,7 +67,7 @@ export class Game implements IGame {
         const isSquare = diff.row === 1 && diff.col === 1;
         return isSquare;
       };
-      const { gameBoard, player1Id, player2Id } = this;
+      const { gameBoard, player1Id, player2Id, player1Name, player2Name } = this;
       const playerId = isP1Turn ? player1Id : player2Id;
 
       const playerCoords = gameBoard.getCoords(playerId);
@@ -88,7 +87,7 @@ export class Game implements IGame {
       const allInCorners = playerCoords.every((pc) => squareCoords.findIndex((sc) => pc.equals(sc)) >= 0);
       const allInSquare = doCoordsFormSquare(playerCoords);
 
-      const winnerName = isP1Turn ? 'Player 1' : 'Player 2';
+      const winnerName = isP1Turn ? player1Name : player2Name;
       let winningMessage = undefined;
       if (allInRow) {
         winningMessage = `${winnerName} wins by forming a row`;
diff --git a/src/local-game.ts b/src/local-game.ts
index 078e0f3..48a317f 100644
--- a/src/local-game.ts
+++ b/src/local-game.ts
@@ -83,8 +83,8 @@ export class LocalGame extends Game {
     piece.parentNode.removeChild(piece);
   }
 
-  public constructor(player1Id: string, player2Id: string) {
-    super(player1Id, player2Id);
+  public constructor(player1Id: string, player1Name: string, player2Id: string, player2Name: string) {
+    super(player1Id, player1Name, player2Id, player2Name);
     this.init();
   }
 
@@ -175,7 +175,7 @@ export class LocalGame extends Game {
   }
 
   private init() {
-    const { dom, player1Id, player2Id, size } = this;
+    const { dom, player1Id, player1Name, player2Id, player2Name, size } = this;
 
     // create board and pieces
     let isWhite = true;
@@ -201,6 +201,9 @@ export class LocalGame extends Game {
     this.getPlayerPositions(player1Id).forEach((coord) => createPiece(true, player1Id, coord));
     this.getPlayerPositions(player2Id).forEach((coord) => createPiece(false, player2Id, coord));
 
+    dom.p1TurnText().textContent = `${player1Name}'s turn`
+    dom.p2TurnText().textContent = `${player2Name}'s turn`
+
     // hook up buttons
     let blockActions = false;
     if (dom.resetButton) {
@@ -238,7 +241,7 @@ export class LocalGame extends Game {
 
 // start!
 export const playGame = () => {
-  const lg = new LocalGame('Red', 'Blue');
+  const lg = new LocalGame('red-player', 'Red', 'blue-player', 'Blue');
   lg.resetUi();
   lg.setActiveUi();
 };
diff --git a/src/remote-game.ts b/src/remote-game.ts
index 35cf3b8..11ec495 100644
--- a/src/remote-game.ts
+++ b/src/remote-game.ts
@@ -16,9 +16,10 @@ interface PeerData {
     mousePos?: Pos;
     move?: Move;
 }
-const peerOptions: PeerJSOption = { debug: 4 };
+const peerOptions: PeerJSOption = { debug: 0 };
 const { getter: getData, setter: setData } = useSessionStorage<LocalData>('dao-game', { id: undefined, peerId: undefined });
 const otherMouseElement = document.getElementById('other-mouse');
+const shareLinkElement = document.getElementById('shareLink');
 const openInNewTab = (url: string) => {
     const openInSystemDefault = false;
     if (openInSystemDefault) {
@@ -45,19 +46,19 @@ const getFirstPlayerId = (playerId1: string, playerId2: string) => {
     const id = isEven ? minString(playerId1, playerId2) : maxString(playerId1, playerId2);
     return id;
 };
+const dropFileExtension = (path: string) => {
+    const lastSlashIndex = path.lastIndexOf('/');
+    const dirName = path.substring(0, lastSlashIndex);
+    return dirName;
+}
 
 export class PeerGame extends LocalGame {
     public conn: Peer.DataConnection;
     public selfId: string;
-    public constructor(player1Id: string, player2Id: string, selfId: string, conn: Peer.DataConnection) {
-        super(player1Id, player2Id);
+    public constructor(player1Id: string, player1Name: string, player2Id: string, player2Name: string, selfId: string, conn: Peer.DataConnection) {
+        super(player1Id, player1Name, player2Id, player2Name);
         this.selfId = selfId;
         this.conn = conn;
-
-        const { dom } = this;
-        const isLocalP1 = player1Id === selfId;
-        (dom.p1TurnText() as HTMLSpanElement).innerText = isLocalP1 ? `Local turn` : `Remote turn`;
-        (dom.p2TurnText() as HTMLSpanElement).innerText = isLocalP1 ? `Remote turn` : `Local turn`;
     }
     public override setActiveUi(): void {
         const { currentPlayerId, selfId } = this;
@@ -73,6 +74,18 @@ export class PeerGame extends LocalGame {
         this.conn.send(JSON.stringify(peerData));
         return result;
     }
+    public override async handleMoveResult(result: MoveResult): Promise<void> {
+        if (result.isTerminal) {
+            alert(result.message);
+            const { protocol, host, pathname } = location;
+            const dir = dropFileExtension(pathname);
+            const newUrl = `${protocol}//${host}${dir}`
+            document.location = newUrl;
+        } else {
+            this.resetUi();
+            this.setActiveUi();
+        }
+    }
 
     public static async createPeerGame(): Promise<PeerGame> {
         let { id, peerId } = getData();
@@ -85,28 +98,60 @@ export class PeerGame extends LocalGame {
                 return;
             }
 
+            shareLinkElement.classList.add('invisible');
+
             id = peer.id;
             peerId = conn.peer;
             setData({ id, peerId });
 
             const isLocalP1 = getFirstPlayerId(id, peerId) === id;
-            game = isLocalP1 ? new PeerGame(id, peerId, id, conn) : new PeerGame(peerId, id, id, conn);
+            game = isLocalP1 ?
+                new PeerGame(id, "Local", peerId, "Remote", id, conn) :
+                new PeerGame(peerId, "Remote", id, "Local", id, conn);
             game.resetUi();
             game.setActiveUi();
             main.classList.remove('invisible');
 
-            main.addEventListener('mousemove', (ev: MouseEvent) => {
-                const peerData: PeerData = { mousePos: { x: ev.x, y: ev.y } };
+            const table = document.getElementById('table');
+            table.addEventListener('mousemove', (ev: MouseEvent) => {
+                const xTable = ev.clientX - table.offsetLeft;
+                const yTable = ev.clientY - table.offsetTop;
+                const xRatio = xTable / table.clientWidth;
+                const yRatio = yTable / table.clientHeight;
+                const peerData: PeerData = { mousePos: { x: xRatio, y: yRatio } };
+                conn.send(JSON.stringify(peerData));
+            });
+            table.addEventListener('mouseleave', () => {
+                const peerData: PeerData = { mousePos: { x: -1, y: -1 } };
                 conn.send(JSON.stringify(peerData));
             });
 
+            let lastTimerId = undefined;
             conn.on('data', (data: string) => {
                 const peerData = JSON.parse(data) as PeerData;
                 const { mousePos, move } = peerData;
 
                 if (mousePos) {
-                    const style = `top:${mousePos.y}px;left:${mousePos.x - otherMouseElement.clientWidth / 2}px;`;
-                    otherMouseElement.setAttribute('style', style);
+                    const isValidPos = mousePos.x !== -1 && mousePos.y !== -1;
+                    if (isValidPos) {
+                        const xRatio = mousePos.x;
+                        const yRatio = mousePos.y;
+                        const xTable = xRatio * table.clientWidth;
+                        const yTable = yRatio * table.clientHeight;
+                        const x = table.offsetLeft + xTable;
+                        const y = table.offsetTop + yTable;
+                        const style = `left:${x - otherMouseElement.clientWidth / 2}px;top:${y}px;display:block;`;
+                        otherMouseElement.setAttribute('style', style);
+                        if (lastTimerId !== undefined) {
+                            clearTimeout(lastTimerId);
+                        }
+                        lastTimerId = setTimeout(() => {
+                            otherMouseElement.setAttribute('style', '');
+                            lastTimerId = undefined;
+                        }, 3000);
+                    } else if (lastTimerId !== undefined) {
+                        otherMouseElement.setAttribute('style', '');
+                    }
                 }
 
                 const isWaitingForRemoteMove = peerId === game.currentPlayerId;
@@ -140,16 +185,16 @@ export class PeerGame extends LocalGame {
             } else {
                 queryParams.set('peerId', id);
                 const newSearch = queryParams.toString();
-                const newUrl = `${protocol}//${host}${pathname}?${newSearch}`;
+                const newUrl = `microsoft-edge:${protocol}//${host}${pathname}?${newSearch}`;
                 const link = document.createElement('a');
                 link.innerText = newUrl;
-                // link.href = newUrl;
-                // link.target = '_blank';
-                link.onclick = () => {
-                    navigator.clipboard.writeText(newUrl);
-                    showSnackbar('Url copied to clipboard');
-                };
-                document.getElementById('shareLink').appendChild(link);
+                link.href = newUrl;
+                link.target = '_blank';
+                // link.onclick = () => {
+                //     navigator.clipboard.writeText(newUrl);
+                //     showSnackbar('Url copied to clipboard');
+                // };
+                shareLinkElement.appendChild(link);
                 // openInNewTab(newUrl);
             }
         });
@@ -163,6 +208,5 @@ export class PeerGame extends LocalGame {
 }
 
 export const playGame = async () => {
-    // document.getElementsByTagName('body')[0].style.setProperty('display', '');
     const game = await PeerGame.createPeerGame();
 };
diff --git a/src/types.ts b/src/types.ts
index ca8d96a..2af1ea2 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -69,31 +69,19 @@ export class GameBoard {
   public readonly player1Id: string;
   public readonly player2Id: string;
 
-  // testBoard: [
-  //     4, null, null, null,
-  //     null, 0, 1, 5,
-  //     null, 2, 3, null,
-  //     6, null, null, 7
-  // ],
   public constructor(player1Id: string, player2Id: string) {
-    this.board = [
-      player1Id,
-      null,
-      null,
-      player2Id,
-      null,
-      player1Id,
-      player2Id,
-      null,
-      null,
-      player2Id,
-      player1Id,
-      null,
-      player2Id,
-      null,
-      null,
-      player1Id,
-    ];
+    // default board
+    const defaultBoard = []
+      .concat([player1Id, null, null, player2Id])
+      .concat([null, player1Id, player2Id, null])
+      .concat([null, player2Id, player1Id, null])
+      .concat([player2Id, null, null, player1Id]);
+    const testBoard = []
+      .concat([player1Id, null, player2Id, player2Id])
+      .concat([player1Id, null, player2Id, null])
+      .concat([player1Id, null, null, null])
+      .concat([null, player1Id, null, player2Id]);
+    this.board = testBoard;
     this.size = new Coord(4, 4);
     this.player1Id = player1Id;
     this.player2Id = player2Id;
diff --git a/tools/esbuild-build-and-serve.ts b/tools/esbuild-build-and-serve.ts
index a936dfc..144ba95 100644
--- a/tools/esbuild-build-and-serve.ts
+++ b/tools/esbuild-build-and-serve.ts
@@ -1,18 +1,7 @@
-import childProcess from 'child_process';
 import esbuild from 'esbuild';
 import http from 'http';
+import { serveOptions, buildOptions, hostPort } from './esbuild-settings'
 
-const hostPort = 3000;
-
-const serveOptions: esbuild.ServeOptions = {
-  servedir: 'public',
-};
-const buildOptions: esbuild.BuildOptions = {
-  entryPoints: ['src/local.ts', 'src/online.ts'],
-  bundle: true,
-  outdir: 'public',
-  sourcemap: true,
-};
 esbuild.serve(serveOptions, buildOptions).then((result) => {
   const { host, port } = result;
   http
diff --git a/tools/esbuild-settings.ts b/tools/esbuild-settings.ts
new file mode 100644
index 0000000..d6c5afd
--- /dev/null
+++ b/tools/esbuild-settings.ts
@@ -0,0 +1,13 @@
+import esbuild from 'esbuild';
+
+export const hostPort = 3000;
+
+export const serveOptions: esbuild.ServeOptions = {
+    servedir: 'public',
+};
+export const buildOptions: esbuild.BuildOptions = {
+    entryPoints: ['src/local.ts', 'src/online.ts'],
+    bundle: true,
+    outdir: 'public/js',
+    sourcemap: true,
+};
diff --git a/tools/esbuild-watch.ts b/tools/esbuild-watch.ts
index 40d47f1..e37f236 100644
--- a/tools/esbuild-watch.ts
+++ b/tools/esbuild-watch.ts
@@ -1,8 +1,4 @@
 import esbuild from 'esbuild';
+import { buildOptions } from './esbuild-settings'
 
-esbuild.build({
-    entryPoints: ['src/local.ts', 'src/online.ts'],
-    bundle: true,
-    outdir: 'public',
-    watch: true,
-}).catch(() => process.exit(1))
\ No newline at end of file
+esbuild.build(buildOptions).catch(() => process.exit(1))
\ No newline at end of file
diff --git a/tools/esbuild.ts b/tools/esbuild.ts
index 7a0a8fc..e9dce4e 100644
--- a/tools/esbuild.ts
+++ b/tools/esbuild.ts
@@ -1,7 +1,4 @@
 import esbuild from 'esbuild';
+import { serveOptions, buildOptions, hostPort } from './esbuild-settings'
 
-esbuild.build({
-    entryPoints: ['src/local.ts', 'src/online.ts'],
-    bundle: true,
-    outdir: 'public'
-}).catch(() => process.exit(1))
\ No newline at end of file
+esbuild.build(buildOptions).catch(() => process.exit(1))
\ No newline at end of file