fuzzyworld

dao-game / commit

Experimenting with multiple entry points, getting remote play mostly working, trying to get pages working for all branches.

f2c940610b293a98adf1afc801d91e43ab412ad8 · Pavel Safronov · 2022-03-20 20:03:01 -0700 · parents 9e157243134f

Experimenting with multiple entry points, getting remote play mostly working, trying to get pages working for all branches.
 .gitlab-ci.yml                   |   3 +-
 .vscode/settings.json            |   1 +
 package-lock.json                | 693 +--------------------------------------
 public/index.html                | 127 +++----
 public/online.html               | 108 +++---
 public/styles.css                |  72 ++++
 src/app.ts                       |   4 -
 src/local-game.ts                |  63 ++--
 src/local.ts                     |   3 +
 src/online.ts                    |   3 +
 src/remote-game.ts               | 263 ++++++++-------
 src/utils.ts                     |  12 +
 tools/esbuild-build-and-serve.ts |   4 +-
 tools/esbuild-watch.ts           |   4 +-
 tools/esbuild.ts                 |   4 +-
 15 files changed, 369 insertions(+), 995 deletions(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 590999b..513e1b2 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -4,8 +4,7 @@ pages:
   script:
     - npm install
     - npm run build
+    - cp public/* public/$CI_COMMIT_BRANCH/ -R
   artifacts:
     paths:
       - public
-  only:
-    - main
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 989167c..48c5f7c 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -2,6 +2,7 @@
     "cSpell.words": [
         "esbuild",
         "Newp",
+        "outdir",
         "outfile",
         "peerjs",
         "servedir",
diff --git a/package-lock.json b/package-lock.json
index 59eac04..edb445e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,699 +1,8 @@
 {
   "name": "dao-game",
   "version": "1.0.0",
-  "lockfileVersion": 2,
+  "lockfileVersion": 1,
   "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 14baf73..4ed5c54 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1,77 +1,54 @@
 <!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 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>
-
-            <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>
-
-        <script type="module">
-            const bundle = import('./bundle.js');
-            debugger;
-            console.log('Hi!');
-            bundle.play();
-        </script>
-    </body>
-
-</html>
\ No newline at end of file
+  <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>
+
+    <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="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>
diff --git a/public/online.html b/public/online.html
index ad554d7..91997b5 100644
--- a/public/online.html
+++ b/public/online.html
@@ -1,64 +1,50 @@
 <!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>
-            window.daoIsOnline = true;
-        </script>
-        <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>
-
-            <table id="table"></table>
-        </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>
-
-        <canvas id="back-canvas"></canvas>
-    </body>
+  <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>
+
+    <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>
diff --git a/public/styles.css b/public/styles.css
index 12f0275..ccd4f8e 100644
--- a/public/styles.css
+++ b/public/styles.css
@@ -214,3 +214,75 @@ td {
     height: var(--active-size);
   }
 }
+
+/* Snackbar, from https://www.w3schools.com/howto/howto_js_snackbar.asp */
+
+/* The snackbar - position it at the bottom and in the middle of the screen */
+#snackbar {
+  visibility: hidden; /* Hidden by default. Visible on click */
+  min-width: 250px; /* Set a default minimum width */
+  margin-left: -125px; /* Divide value of min-width by 2 */
+  background-color: #333; /* Black background color */
+  color: #fff; /* White text color */
+  text-align: center; /* Centered text */
+  border-radius: 2px; /* Rounded borders */
+  padding: 16px; /* Padding */
+  position: fixed; /* Sit on top of the screen */
+  z-index: 1; /* Add a z-index if needed */
+  left: 50%; /* Center the snackbar */
+  bottom: 30px; /* 30px from the bottom */
+}
+
+/* Show the snackbar when clicking on a button (class added with JavaScript) */
+#snackbar.show {
+  visibility: visible; /* Show the snackbar */
+  /* Add animation: Take 0.5 seconds to fade in and out the snackbar.
+  However, delay the fade out process for 2.5 seconds */
+  -webkit-animation: fadein 0.5s, fadeout 0.5s 2.5s;
+  animation: fadein 0.5s, fadeout 0.5s 2.5s;
+}
+
+/* Animations to fade the snackbar in and out */
+@-webkit-keyframes fadein {
+  from {
+    bottom: 0;
+    opacity: 0;
+  }
+  to {
+    bottom: 30px;
+    opacity: 1;
+  }
+}
+
+@keyframes fadein {
+  from {
+    bottom: 0;
+    opacity: 0;
+  }
+  to {
+    bottom: 30px;
+    opacity: 1;
+  }
+}
+
+@-webkit-keyframes fadeout {
+  from {
+    bottom: 30px;
+    opacity: 1;
+  }
+  to {
+    bottom: 0;
+    opacity: 0;
+  }
+}
+
+@keyframes fadeout {
+  from {
+    bottom: 30px;
+    opacity: 1;
+  }
+  to {
+    bottom: 0;
+    opacity: 0;
+  }
+}
diff --git a/src/app.ts b/src/app.ts
deleted file mode 100644
index 515eb31..0000000
--- a/src/app.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-// import { playGame } from './local-game'
-import { playGame } from './remote-game';
-
-playGame();
\ No newline at end of file
diff --git a/src/local-game.ts b/src/local-game.ts
index 396921d..078e0f3 100644
--- a/src/local-game.ts
+++ b/src/local-game.ts
@@ -120,13 +120,15 @@ export class LocalGame extends Game {
       dom.p2TurnText().classList.add(classNames.selectedPlayerText);
     }
 
-    dom.undoButton.disabled = canUndoMove;
-    if (moveHistory.length === 0) {
-      dom.undoButton.disabled = true;
-      dom.resetButton.disabled = true;
-    } else {
-      dom.undoButton.disabled = false;
-      dom.resetButton.disabled = false;
+    if (dom.undoButton || dom.resetButton) {
+      dom.undoButton.disabled = canUndoMove;
+      if (moveHistory.length === 0) {
+        dom.undoButton.disabled = true;
+        dom.resetButton.disabled = true;
+      } else {
+        dom.undoButton.disabled = false;
+        dom.resetButton.disabled = false;
+      }
     }
 
     this.selectedPiece = null;
@@ -201,31 +203,36 @@ export class LocalGame extends Game {
 
     // hook up buttons
     let blockActions = false;
-    dom.resetButton.addEventListener('click', () => {
-      if (blockActions) {
-        return;
-      }
+    if (dom.resetButton) {
+      dom.resetButton.addEventListener('click', () => {
+        if (blockActions) {
+          return;
+        }
 
-      blockActions = true;
-      if (confirm('Reset?')) {
-        window.location.reload();
-      }
-      blockActions = false;
-    });
-    dom.undoButton.addEventListener('click', async () => {
-      if (blockActions) {
-        return;
-      }
+        blockActions = true;
+        if (confirm('Reset?')) {
+          window.location.reload();
+        }
+        blockActions = false;
+      });
+    }
 
-      blockActions = true;
+    if (dom.undoButton) {
+      dom.undoButton.addEventListener('click', async () => {
+        if (blockActions) {
+          return;
+        }
 
-      const move = this.undoMove();
-      await this.movePiece(move.dest, move.source);
-      this.resetUi();
-      this.setActiveUi();
+        blockActions = true;
 
-      blockActions = false;
-    });
+        const move = this.undoMove();
+        await this.movePiece(move.dest, move.source);
+        this.resetUi();
+        this.setActiveUi();
+
+        blockActions = false;
+      });
+    }
   }
 }
 
diff --git a/src/local.ts b/src/local.ts
new file mode 100644
index 0000000..22236bd
--- /dev/null
+++ b/src/local.ts
@@ -0,0 +1,3 @@
+import { playGame } from './local-game';
+
+playGame();
\ No newline at end of file
diff --git a/src/online.ts b/src/online.ts
new file mode 100644
index 0000000..b314e49
--- /dev/null
+++ b/src/online.ts
@@ -0,0 +1,3 @@
+import { playGame } from './remote-game';
+
+playGame();
\ No newline at end of file
diff --git a/src/remote-game.ts b/src/remote-game.ts
index 5f43230..35cf3b8 100644
--- a/src/remote-game.ts
+++ b/src/remote-game.ts
@@ -2,158 +2,167 @@ import Peer, { PeerJSOption } from 'peerjs';
 import { LocalGame } from './local-game';
 import { useSessionStorage } from './storage';
 import { Coord, Move, MoveResult } from './types';
-import { delay, generateHash } from './utils';
+import { delay, generateHash, showSnackbar } from './utils';
 
 interface LocalData {
-  id: string;
-  peerId: string;
+    id: string;
+    peerId: string;
 }
 interface Pos {
-  x: number;
-  y: number;
+    x: number;
+    y: number;
 }
 interface PeerData {
-  mousePos?: Pos;
-  move?: Move;
+    mousePos?: Pos;
+    move?: Move;
 }
 const peerOptions: PeerJSOption = { debug: 4 };
 const { getter: getData, setter: setData } = useSessionStorage<LocalData>('dao-game', { id: undefined, peerId: undefined });
 const otherMouseElement = document.getElementById('other-mouse');
 const openInNewTab = (url: string) => {
-  const openInEdge = true;
-  if (openInEdge) {
-    //   opening in the same browser causes a copy of the session storage,
-    //   so we clear the session storage, open the url, and restore the session storage
-    const data = getData();
-    sessionStorage.clear();
-    try {
-      window.open(url, '_blank');
-    } finally {
-      setData(data);
+    const openInSystemDefault = false;
+    if (openInSystemDefault) {
+        window.open(`microsoft-edge:${url}`, '_blank');
+    } else {
+        //   opening in the same browser causes a copy of the session storage,
+        //   so we clear the session storage, open the url, and restore the session storage
+        const data = getData();
+        sessionStorage.clear();
+        try {
+            window.open(url, '_blank');
+        } finally {
+            setData(data);
+        }
     }
-  } else {
-    window.open(url, '_blank');
-  }
 };
 const minString = (a: string, b: string): string => (a < b ? a : b);
 const maxString = (a: string, b: string): string => (a > b ? a : b);
 const getFirstPlayerId = (playerId1: string, playerId2: string) => {
-  const hash1 = generateHash(playerId1);
-  const hash2 = generateHash(playerId2);
-  const prod = hash1 * hash2;
-  const isEven = prod % 2 == 0;
-  const id = isEven ? minString(playerId1, playerId2) : maxString(playerId1, playerId2);
-  return id;
+    const hash1 = generateHash(playerId1);
+    const hash2 = generateHash(playerId2);
+    const prod = hash1 * hash2;
+    const isEven = prod % 2 == 0;
+    const id = isEven ? minString(playerId1, playerId2) : maxString(playerId1, playerId2);
+    return id;
 };
 
 export class PeerGame extends LocalGame {
-  public conn: Peer.DataConnection;
-  public selfId: string;
-  public constructor(player1Id: string, player2Id: string, selfId: string, conn: Peer.DataConnection) {
-    super(player1Id, player2Id);
-    this.selfId = selfId;
-    this.conn = conn;
-
-    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;
-
-    const isLocalTurn = currentPlayerId === selfId;
-    if (isLocalTurn) {
-      super.setActiveUi();
+    public conn: Peer.DataConnection;
+    public selfId: string;
+    public constructor(player1Id: string, player2Id: string, selfId: string, conn: Peer.DataConnection) {
+        super(player1Id, player2Id);
+        this.selfId = selfId;
+        this.conn = conn;
+
+        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 makeMove(source: Coord, dest: Coord): Promise<MoveResult> {
-    const result = super.makeMove(source, dest);
-    const peerData: PeerData = { move: { source, dest, playerId: this.currentPlayerId } };
-    this.conn.send(JSON.stringify(peerData));
-    return result;
-  }
-
-  public static async createPeerGame(): Promise<PeerGame> {
-    let { id, peerId } = getData();
-    const main = document.getElementsByTagName('main')[0];
-    let game: PeerGame = undefined;
-
-    const handleConn = (conn: Peer.DataConnection) => {
-      if (game !== undefined) {
-        conn.close();
-        return;
-      }
-
-      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.resetUi();
-      game.setActiveUi();
-      main.classList.remove('invisible');
-
-      main.addEventListener('mousemove', (ev: MouseEvent) => {
-        const peerData: PeerData = { mousePos: { x: ev.x, y: ev.y } };
-        conn.send(JSON.stringify(peerData));
-      });
-
-      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);
+    public override setActiveUi(): void {
+        const { currentPlayerId, selfId } = this;
+
+        const isLocalTurn = currentPlayerId === selfId;
+        if (isLocalTurn) {
+            super.setActiveUi();
         }
+    }
+    public override makeMove(source: Coord, dest: Coord): Promise<MoveResult> {
+        const result = super.makeMove(source, dest);
+        const peerData: PeerData = { move: { source, dest, playerId: this.currentPlayerId } };
+        this.conn.send(JSON.stringify(peerData));
+        return result;
+    }
 
-        const isWaitingForRemoteMove = peerId === game.currentPlayerId;
-        if (isWaitingForRemoteMove && move) {
-          const isMoveLegal = game.getPiece(move.source) === game.currentPlayerId && game.getPiece(move.dest) === null;
-          if (isMoveLegal) {
-            game.makeMove(move.source, move.dest);
-          } else {
-            console.log({ isMoveLegal, move });
-          }
+    public static async createPeerGame(): Promise<PeerGame> {
+        let { id, peerId } = getData();
+        const main = document.getElementsByTagName('main')[0];
+        let game: PeerGame = undefined;
+
+        const handleConn = (conn: Peer.DataConnection) => {
+            if (game !== undefined) {
+                conn.close();
+                return;
+            }
+
+            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.resetUi();
+            game.setActiveUi();
+            main.classList.remove('invisible');
+
+            main.addEventListener('mousemove', (ev: MouseEvent) => {
+                const peerData: PeerData = { mousePos: { x: ev.x, y: ev.y } };
+                conn.send(JSON.stringify(peerData));
+            });
+
+            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 isWaitingForRemoteMove = peerId === game.currentPlayerId;
+                if (isWaitingForRemoteMove && move) {
+                    const isMoveLegal = game.getPiece(move.source) === game.currentPlayerId && game.getPiece(move.dest) === null;
+                    if (isMoveLegal) {
+                        game.makeMove(move.source, move.dest);
+                    } else {
+                        console.log({ isMoveLegal, move });
+                    }
+                }
+            });
+        };
+
+        const { host, protocol, pathname, search } = location;
+        const queryParams = new URLSearchParams(search);
+
+        const peer = id === undefined ? new Peer(peerOptions) : new Peer(id, peerOptions);
+        window.onunload = () => peer.destroy();
+        peer.on('connection', (conn) => handleConn(conn));
+        peer.on('error', (err) => console.error({ err }));
+        peer.on('open', (trueId) => {
+            id = trueId;
+            peerId = queryParams.get('peerId') ?? peerId;
+            setData({ id, peerId });
+
+            if (peerId) {
+                console.log(`${id} connecting to peer "${peerId}"`);
+                const conn = peer.connect(peerId);
+                handleConn(conn);
+            } else {
+                queryParams.set('peerId', id);
+                const newSearch = queryParams.toString();
+                const newUrl = `${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);
+                // openInNewTab(newUrl);
+            }
+        });
+
+        while (game === undefined) {
+            console.log('Waiting to connect to peer');
+            await delay(500);
         }
-      });
-    };
-
-    const { host, protocol, pathname, search } = location;
-    const queryParams = new URLSearchParams(search);
-
-    const peer = id === undefined ? new Peer(peerOptions) : new Peer(id, peerOptions);
-    window.onunload = () => peer.destroy();
-    peer.on('connection', (conn) => handleConn(conn));
-    peer.on('error', (err) => console.error({ err }));
-    peer.on('open', (trueId) => {
-      id = trueId;
-      peerId = queryParams.get('peerId') ?? peerId;
-      setData({ id, peerId });
-
-      if (peerId) {
-        console.log(`${id} connecting to peer "${peerId}"`);
-        const conn = peer.connect(peerId);
-        handleConn(conn);
-      } else {
-        queryParams.set('peerId', id);
-        const newSearch = queryParams.toString();
-        const newUrl = `microsoft-edge:${protocol}//${host}${pathname}?${newSearch}`;
-        openInNewTab(newUrl);
-      }
-    });
-
-    while (game === undefined) {
-      console.log('Waiting to connect to peer');
-      await delay(500);
+        return game;
     }
-    return game;
-  }
 }
 
 export const playGame = async () => {
-  document.getElementsByTagName('body')[0].style.setProperty('display', '');
-  const game = await PeerGame.createPeerGame();
+    // document.getElementsByTagName('body')[0].style.setProperty('display', '');
+    const game = await PeerGame.createPeerGame();
 };
diff --git a/src/utils.ts b/src/utils.ts
index 2ad21ab..b97d40e 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -18,3 +18,15 @@ export async function delay(delayInMs: number) {
     }, delayInMs);
   });
 }
+
+export function showSnackbar(text) {
+  // Get the snackbar DIV
+  var x = document.getElementById("snackbar");
+
+  // Add the "show" class to DIV
+  x.className = "show";
+  x.innerText = text;
+
+  // After 3 seconds, remove the show class from DIV
+  setTimeout(function () { x.className = x.className.replace("show", ""); }, 3000);
+}
\ No newline at end of file
diff --git a/tools/esbuild-build-and-serve.ts b/tools/esbuild-build-and-serve.ts
index 4632dc5..a936dfc 100644
--- a/tools/esbuild-build-and-serve.ts
+++ b/tools/esbuild-build-and-serve.ts
@@ -8,9 +8,9 @@ const serveOptions: esbuild.ServeOptions = {
   servedir: 'public',
 };
 const buildOptions: esbuild.BuildOptions = {
-  entryPoints: ['src/app.ts'],
+  entryPoints: ['src/local.ts', 'src/online.ts'],
   bundle: true,
-  outfile: 'public/bundle.js',
+  outdir: 'public',
   sourcemap: true,
 };
 esbuild.serve(serveOptions, buildOptions).then((result) => {
diff --git a/tools/esbuild-watch.ts b/tools/esbuild-watch.ts
index 6843d69..40d47f1 100644
--- a/tools/esbuild-watch.ts
+++ b/tools/esbuild-watch.ts
@@ -1,8 +1,8 @@
 import esbuild from 'esbuild';
 
 esbuild.build({
-    entryPoints: ['src/app.ts'],
+    entryPoints: ['src/local.ts', 'src/online.ts'],
     bundle: true,
-    outfile: 'public/bundle.js',
+    outdir: 'public',
     watch: true,
 }).catch(() => process.exit(1))
\ No newline at end of file
diff --git a/tools/esbuild.ts b/tools/esbuild.ts
index dc7e306..7a0a8fc 100644
--- a/tools/esbuild.ts
+++ b/tools/esbuild.ts
@@ -1,7 +1,7 @@
 import esbuild from 'esbuild';
 
 esbuild.build({
-    entryPoints: ['src/app.ts'],
+    entryPoints: ['src/local.ts', 'src/online.ts'],
     bundle: true,
-    outfile: 'public/bundle.js',
+    outdir: 'public'
 }).catch(() => process.exit(1))
\ No newline at end of file