dao-game / commit
Moved most of the game logic into its own class.
package-lock.json | 693 ++++++++++++++++++++++++++++++++++++++- public/index.html | 121 ++++--- public/online.html | 8 +- public/styles.css | 10 +- src/app.ts | 3 + src/game.ts | 76 +++-- src/listeners.ts | 24 ++ src/local-game.ts | 462 +++++++------------------- src/peering.ts | 51 --- src/peering_.ts | 119 ------- src/piano.ts | 21 -- src/types.ts | 39 ++- tools/esbuild-build-and-serve.ts | 2 +- tools/esbuild-watch.ts | 2 +- tools/esbuild.ts | 2 +- 15 files changed, 968 insertions(+), 665 deletions(-) diff --git a/package-lock.json b/package-lock.json index edb445e..59eac04 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,8 +1,699 @@ { "name": "dao-game", "version": "1.0.0", - "lockfileVersion": 1, + "lockfileVersion": 2, "requires": true, + "packages": { + "": { + "name": "dao-game", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "esbuild": "^0.14.25", + "peerjs": "^1.3.2" + }, + "devDependencies": { + "@types/node": "^17.0.21", + "rimraf": "^3.0.2", + "ts-node": "^10.6.0", + "typescript": "^4.6.2" + } + }, + "node_modules/@cspotcode/source-map-consumer": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz", + "integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz", + "integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-consumer": "0.8.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz", + "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz", + "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz", + "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz", + "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "17.0.21", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.21.tgz", + "integrity": "sha512-DBZCJbhII3r90XbQxI8Y9IjjiiOGlZ0Hr32omXIZvwwZ7p4DMMXGrKXVyPfuoBOri9XNtL0UK69jYIBIsRX3QQ==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", + "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/esbuild": { + "version": "0.14.25", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.25.tgz", + "integrity": "sha512-4JHEIOMNFvK09ziiL+iVmldIhLbn49V4NAVo888tcGFKedEZY/Y8YapfStJ6zSE23tzYPKxqKwQBnQoIO0BI/Q==", + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "esbuild-android-64": "0.14.25", + "esbuild-android-arm64": "0.14.25", + "esbuild-darwin-64": "0.14.25", + "esbuild-darwin-arm64": "0.14.25", + "esbuild-freebsd-64": "0.14.25", + "esbuild-freebsd-arm64": "0.14.25", + "esbuild-linux-32": "0.14.25", + "esbuild-linux-64": "0.14.25", + "esbuild-linux-arm": "0.14.25", + "esbuild-linux-arm64": "0.14.25", + "esbuild-linux-mips64le": "0.14.25", + "esbuild-linux-ppc64le": "0.14.25", + "esbuild-linux-riscv64": "0.14.25", + "esbuild-linux-s390x": "0.14.25", + "esbuild-netbsd-64": "0.14.25", + "esbuild-openbsd-64": "0.14.25", + "esbuild-sunos-64": "0.14.25", + "esbuild-windows-32": "0.14.25", + "esbuild-windows-64": "0.14.25", + "esbuild-windows-arm64": "0.14.25" + } + }, + "node_modules/esbuild-android-64": { + "version": "0.14.25", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.25.tgz", + "integrity": "sha512-L5vCUk7TzFbBnoESNoXjU3x9+/+7TDIE/1mTfy/erAfvZAqC+S3sp/Qa9wkypFMcFvN9FzvESkTlpeQDolREtQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-android-arm64": { + "version": "0.14.25", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.25.tgz", + "integrity": "sha512-4jv5xPjM/qNm27T5j3ZEck0PvjgQtoMHnz4FzwF5zNP56PvY2CT0WStcAIl6jNlsuDdN63rk2HRBIsO6xFbcFw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-64": { + "version": "0.14.25", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.25.tgz", + "integrity": "sha512-TGp8tuudIxOyWd1+8aYPxQmC1ZQyvij/AfNBa35RubixD0zJ1vkKHVAzo0Zao1zcG6pNqiSyzfPto8vmg0s7oA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-arm64": { + "version": "0.14.25", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.25.tgz", + "integrity": "sha512-oTcDgdm0MDVEmw2DWu8BV68pYuImpFgvWREPErBZmNA4MYKGuBRaCiJqq6jZmBR1x+3y1DWCjez+5uLtuAm6mw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-64": { + "version": "0.14.25", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.25.tgz", + "integrity": "sha512-ueAqbnMZ8arnuLH8tHwTCQYeptnHOUV7vA6px6j4zjjQwDx7TdP7kACPf3TLZLdJQ3CAD1XCvQ2sPhX+8tacvQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-arm64": { + "version": "0.14.25", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.25.tgz", + "integrity": "sha512-+ZVWud2HKh+Ob6k/qiJWjBtUg4KmJGGmbvEXXW1SNKS7hW7HU+Zq2ZCcE1akFxOPkVB+EhOty/sSek30tkCYug==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-32": { + "version": "0.14.25", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.25.tgz", + "integrity": "sha512-3OP/lwV3kCzEz45tobH9nj+uE4ubhGsfx+tn0L26WAGtUbmmcRpqy7XRG/qK7h1mClZ+eguIANcQntYMdYklfw==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-64": { + "version": "0.14.25", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.25.tgz", + "integrity": "sha512-+aKHdHZmX9qwVlQmu5xYXh7GsBFf4TWrePgeJTalhXHOG7NNuUwoHmketGiZEoNsWyyqwH9rE5BC+iwcLY30Ug==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm": { + "version": "0.14.25", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.25.tgz", + "integrity": "sha512-aTLcE2VBoLydL943REcAcgnDi3bHtmULSXWLbjtBdtykRatJVSxKMjK9YlBXUZC4/YcNQfH7AxwVeQr9fNxPhw==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm64": { + "version": "0.14.25", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.25.tgz", + "integrity": "sha512-UxfenPx/wSZx55gScCImPtXekvZQLI2GW3qe5dtlmU7luiqhp5GWPzGeQEbD3yN3xg/pHc671m5bma5Ns7lBHw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-mips64le": { + "version": "0.14.25", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.25.tgz", + "integrity": "sha512-wLWYyqVfYx9Ur6eU5RT92yJVsaBGi5RdkoWqRHOqcJ38Kn60QMlcghsKeWfe9jcYut8LangYZ98xO1LxIoSXrQ==", + "cpu": [ + "mips64el" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-ppc64le": { + "version": "0.14.25", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.25.tgz", + "integrity": "sha512-0dR6Csl6Zas3g4p9ULckEl8Mo8IInJh33VCJ3eaV1hj9+MHGdmDOakYMN8MZP9/5nl+NU/0ygpd14cWgy8uqRw==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-riscv64": { + "version": "0.14.25", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.25.tgz", + "integrity": "sha512-J4d20HDmTrgvhR0bdkDhvvJGaikH3LzXQnNaseo8rcw9Yqby9A90gKUmWpfwqLVNRILvNnAmKLfBjCKU9ajg8w==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-s390x": { + "version": "0.14.25", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.25.tgz", + "integrity": "sha512-YI2d5V6nTE73ZnhEKQD7MtsPs1EtUZJ3obS21oxQxGbbRw1G+PtJKjNyur+3t6nzHP9oTg6GHQ3S3hOLLmbDIQ==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-netbsd-64": { + "version": "0.14.25", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.25.tgz", + "integrity": "sha512-TKIVgNWLUOkr+Exrye70XTEE1lJjdQXdM4tAXRzfHE9iBA7LXWcNtVIuSnphTqpanPzTDFarF0yqq4kpbC6miA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-openbsd-64": { + "version": "0.14.25", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.25.tgz", + "integrity": "sha512-QgFJ37A15D7NIXBTYEqz29+uw3nNBOIyog+3kFidANn6kjw0GHZ0lEYQn+cwjyzu94WobR+fes7cTl/ZYlHb1A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-sunos-64": { + "version": "0.14.25", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.25.tgz", + "integrity": "sha512-rmWfjUItYIVlqr5EnTH1+GCxXiBOC42WBZ3w++qh7n2cS9Xo0lO5pGSG2N+huOU2fX5L+6YUuJ78/vOYvefeFw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-32": { + "version": "0.14.25", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.25.tgz", + "integrity": "sha512-HGAxVUofl3iUIz9W10Y9XKtD0bNsK9fBXv1D55N/ljNvkrAYcGB8YCm0v7DjlwtyS6ws3dkdQyXadbxkbzaKOA==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-64": { + "version": "0.14.25", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.25.tgz", + "integrity": "sha512-TirEohRkfWU9hXLgoDxzhMQD1g8I2mOqvdQF2RS9E/wbkORTAqJHyh7wqGRCQAwNzdNXdg3JAyhQ9/177AadWA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-arm64": { + "version": "0.14.25", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.25.tgz", + "integrity": "sha512-4ype9ERiI45rSh+R8qUoBtaj6kJvUOI7oVLhKqPEpcF4Pa5PpT3hm/mXAyotJHREkHpM87PAJcA442mLnbtlNA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/eventemitter3": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", + "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/peerjs": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/peerjs/-/peerjs-1.3.2.tgz", + "integrity": "sha512-+PHfmsC7QGUU8Ye3OLi6tKQZGPCNy7QatUVNw4JtE8alkguF3+DdO5W0bzepqP2OtE9FqH/ltXt37qyvHw2CqA==", + "dependencies": { + "@types/node": "^10.14.33", + "eventemitter3": "^3.1.2", + "peerjs-js-binarypack": "1.0.1", + "webrtc-adapter": "^7.7.1" + } + }, + "node_modules/peerjs-js-binarypack": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/peerjs-js-binarypack/-/peerjs-js-binarypack-1.0.1.tgz", + "integrity": "sha512-N6aeia3NhdpV7kiGxJV5xQiZZCVEEVjRz2T2C6UZQiBkHWHzUv/oWA4myQLcwBwO8LUoR1KWW5oStvwVesmfCg==" + }, + "node_modules/peerjs/node_modules/@types/node": { + "version": "10.17.60", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", + "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==" + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rtcpeerconnection-shim": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/rtcpeerconnection-shim/-/rtcpeerconnection-shim-1.2.15.tgz", + "integrity": "sha512-C6DxhXt7bssQ1nHb154lqeL0SXz5Dx4RczXZu2Aa/L1NJFnEVDxFwCBo3fqtuljhHIGceg5JKBV4XJ0gW5JKyw==", + "dependencies": { + "sdp": "^2.6.0" + }, + "engines": { + "node": ">=6.0.0", + "npm": ">=3.10.0" + } + }, + "node_modules/sdp": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/sdp/-/sdp-2.12.0.tgz", + "integrity": "sha512-jhXqQAQVM+8Xj5EjJGVweuEzgtGWb3tmEEpl3CLP3cStInSbVHSg0QWOGQzNq8pSID4JkpeV2mPqlMDLrm0/Vw==" + }, + "node_modules/ts-node": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.6.0.tgz", + "integrity": "sha512-CJen6+dfOXolxudBQXnVjRVvYTmTWbyz7cn+xq2XTsvnaXbHqr4gXSCNbS2Jj8yTZMuGwUoBESLaOkLascVVvg==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "0.7.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.0", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.2.tgz", + "integrity": "sha512-HM/hFigTBHZhLXshn9sN37H085+hQGeJHJ/X7LpBWLID/fbc2acUMfU+lGD98X81sKP+pFa9f0DZmCwB9GnbAg==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.0.tgz", + "integrity": "sha512-mpSYqfsFvASnSn5qMiwrr4VKfumbPyONLCOPmsR3A6pTY/r0+tSaVbgPWSAIuzbk3lCTa+FForeTiO+wBQGkjA==", + "dev": true + }, + "node_modules/webrtc-adapter": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-7.7.1.tgz", + "integrity": "sha512-TbrbBmiQBL9n0/5bvDdORc6ZfRY/Z7JnEj+EYOD1ghseZdpJ+nF2yx14k3LgQKc7JZnG7HAcL+zHnY25So9d7A==", + "dependencies": { + "rtcpeerconnection-shim": "^1.2.15", + "sdp": "^2.12.0" + }, + "engines": { + "node": ">=6.0.0", + "npm": ">=3.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + } + }, "dependencies": { "@cspotcode/source-map-consumer": { "version": "0.8.0", diff --git a/public/index.html b/public/index.html index ae1c898..5eb09b2 100644 --- a/public/index.html +++ b/public/index.html @@ -1,73 +1,62 @@ <!DOCTYPE html> <html lang="en"> - <head> - <meta charset="utf-8" /> - <title>Dao Game</title> - <meta name="viewport" content="width=device-width, initial-scale=1" /> - <link rel="icon" href="yin-yang.svg" type="image/svg+xml" /> - <link rel="stylesheet" href="styles.css" /> - <script defer src="bundle.js"></script> - </head> - <body> - <div class="invisible"> - <p class="piece red-piece" id="default-red-piece"></p> - <p class="piece blue-piece" id="default-blue-piece"></p> +<head> + <meta charset="utf-8" /> + <title>Dao Game</title> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <link rel="icon" href="yin-yang.svg" type="image/svg+xml" /> + <link rel="stylesheet" href="styles.css" /> + <script defer src="bundle.js"></script> +</head> + +<body> + <div class="invisible"> + <p class="piece p1-piece" id="default-p1-piece"></p> + <p class="piece p2-piece" id="default-p2-piece"></p> + </div> + <main> + <div class="controls"> + <span class="p1-turn-text"> Red's turn! </span> + <span class="divider">|</span> + <span class="p2-turn-text"> Blue's turn </span> </div> - <main> - <div class="controls"> - <span class="red-turn-text"> Red's turn! </span> - <span class="divider">|</span> + <table id="table"></table> - <span class="blue-turn-text"> Blue's turn </span> - </div> - - <table id="table"></table> - - <div class="controls"> - <button type="button" class="button" id="undo-button"> - Undo - </button> - <span class="divider">|</span> - <button type="button" class="button" id="reset-button"> - Reset - </button> - </div> - </main> - <hr /> - - <div class="title"> - <p> - Board game - <a - href="https://en.wikipedia.org/wiki/Dao_(game)" - target="_blank" - rel="noopener" - >Dao</a - >. - </p> - </div> - - <div class="info"> - The game consists of only one simple rule and three ways to win. - Pieces move in a straight line, including diagonally, as far as - possible with no jumps. The three ways to win are arranging your - pieces in a straight line on the board (not diagonally), occupying - the four corners, or forming a 2x2 square anywhere on the board. - </div> - <hr /> - - <div class="info"> - <p> - Here is the - <a - href="https://gitlab.com/fuzzy-rp/dao-game" - target="_blank" - rel="noopener" - >source code</a - > - </p> + <div class="controls"> + <button type="button" class="button" id="undo-button"> + Undo + </button> + <span class="divider">|</span> + <button type="button" class="button" id="reset-button"> + Reset + </button> </div> - </body> -</html> + </main> + <hr /> + + <div class="title"> + <p>Board game + <a href="https://en.wikipedia.org/wiki/Dao_(game)" target="_blank" rel="noopener">Dao</a>. + </p> + </div> + + <div class="info"> + The game consists of only one simple rule and three ways to win. + Pieces move in a straight line, including diagonally, as far as + possible with no jumps. The three ways to win are arranging your + pieces in a straight line on the board (not diagonally), occupying + the four corners, or forming a 2x2 square anywhere on the board. + </div> + <hr /> + + <div class="info"> + <p> + Here is the + <a href="https://gitlab.com/fuzzy-rp/dao-game" target="_blank" rel="noopener">source code</a> + </p> + </div> +</body> + +</html> \ No newline at end of file diff --git a/public/online.html b/public/online.html index 1d4f8ac..88997d3 100644 --- a/public/online.html +++ b/public/online.html @@ -14,16 +14,16 @@ <body> <div class="invisible"> - <p class="piece red-piece" id="default-red-piece"></p> - <p class="piece blue-piece" id="default-blue-piece"></p> + <p class="piece p1-piece" id="default-p1-piece"></p> + <p class="piece p2-piece" id="default-p2-piece"></p> </div> <main> <div class="controls"> - <span class="red-turn-text"> Red's turn! </span> + <span class="p1-turn-text"> Red's turn! </span> <span class="divider">|</span> - <span class="blue-turn-text"> Blue's turn </span> + <span class="p2-turn-text"> Blue's turn </span> </div> <table id="table"></table> diff --git a/public/styles.css b/public/styles.css index cb59d42..021b0df 100644 --- a/public/styles.css +++ b/public/styles.css @@ -65,7 +65,7 @@ td { margin-right: 15px; } -.red-turn-text, .blue-turn-text { +.p1-turn-text, .p2-turn-text { text-align: center; color: lightgray; background-color: white; @@ -77,11 +77,11 @@ td { user-select: none; } -.red-turn-text.selected-player-text { +.p1-turn-text.selected-player-text { background-color: red; } -.blue-turn-text.selected-player-text { +.p2-turn-text.selected-player-text { background-color: blue; } @@ -120,11 +120,11 @@ td { /* background-image: url(yin-yang.svg); */ } -.red-piece { +.p1-piece { background-color: crimson; /* background-image: url(yin-yang.svg); */ } -.blue-piece { +.p2-piece { background-color: mediumblue; /* background-image: url(yin-yang.svg); */ } diff --git a/src/app.ts b/src/app.ts index e69de29..ceec005 100644 --- a/src/app.ts +++ b/src/app.ts @@ -0,0 +1,3 @@ +import { playGame } from './local-game' + +playGame(); \ No newline at end of file diff --git a/src/game.ts b/src/game.ts index b4233ca..5c5a68d 100644 --- a/src/game.ts +++ b/src/game.ts @@ -1,22 +1,41 @@ -import { Coord, GameBoard, IGame, Move, MoveResult } from "./types"; +import { Coord, GameBoard, Move, MoveResult } from "./types"; + +export interface IGame { + size: Coord; + moveHistory: Move[]; + player1Id: string; + player2Id: string; + currentPlayerId: string; + canUndoMove: boolean; + getPlayerPositions: (isP1: boolean) => Coord[]; + coordToBoardIndex: (coord: Coord) => number; + getValidMoves: (start: Coord) => Coord[]; + makeMove: (source: Coord, dest: Coord) => MoveResult; + undoMove: () => Move; +} export class Game implements IGame { - private readonly gameBoard: GameBoard; - private readonly player1Id: string; - private readonly player2Id: string; private isGameOver: boolean; + private readonly gameBoard: GameBoard; + private readonly _player1Id: string; + private readonly _player2Id: string; private readonly _moveHistory: Move[]; public constructor(player1Id: string, player2Id: string) { this.gameBoard = new GameBoard(player1Id, player2Id); - this.player1Id = player1Id; - this.player2Id = player2Id; + this._player1Id = player1Id; + this._player2Id = player2Id; this.isGameOver = false; this._moveHistory = []; + this.currentPlayerId = player1Id; } - - public get moveHistory(): Move[] { return this._moveHistory; } public currentPlayerId: string; + public get size(): Coord { return this.gameBoard.size; } + public get player1Id(): string { return this._player1Id; } + public get player2Id(): string { return this._player2Id; } + public get moveHistory(): Move[] { return this._moveHistory; } + public getPlayerPositions(isP1: boolean): Coord[] { return this.gameBoard.getCoords(isP1 ? this._player1Id : this.player2Id); } + public coordToBoardIndex(coord: Coord): number { return this.gameBoard.coordToBoardIndex(coord); } public makeMove(source: Coord, dest: Coord): MoveResult { const checkForWin = (isP1Turn: boolean): MoveResult => { const doCoordsFormSquare = (coords: Coord[]): boolean => { @@ -86,14 +105,14 @@ export class Game implements IGame { const isP1Turn = (this.currentPlayerId === player1Id); const playerId = isP1Turn ? player1Id : player2Id; + this._moveHistory.push(new Move(playerId, source, dest)); gameBoard.move(source, dest); const result = checkForWin(isP1Turn); - if (!result.isTerminal) { - this.currentPlayerId = isP1Turn ? this.player2Id : this.player1Id; + if (result.isTerminal) { this.isGameOver = true; } else { - this._moveHistory.push(new Move(playerId, source, dest)); + this.currentPlayerId = isP1Turn ? this.player2Id : this.player1Id; } return result; } @@ -106,23 +125,19 @@ export class Game implements IGame { let farthestPos = undefined; let testPos = startCoord; while (true) { - const pos = new Coord( - testPos.row + direction.row, - testPos.col + direction.col - ); - const withinBounds = - pos.row >= 0 && - pos.row < gameBoard.rows && - pos.col >= 0 && - pos.col < gameBoard.cols; + const pos = new Coord(testPos.row + direction.row, testPos.col + direction.col); + const withinBounds = gameBoard.size.contains(pos); + if (!withinBounds) { + break; + } const isOccupied = gameBoard.get(pos) !== null; - if (isOccupied || !withinBounds) { + if (isOccupied) { break; - } else { - farthestPos = pos; - testPos = pos; } + + farthestPos = pos; + testPos = pos; } return farthestPos; }; @@ -133,20 +148,19 @@ export class Game implements IGame { return farthestCoords; } - public undoMove(): void { - if (!this.canUndoMove) { - throw "Newp! Can't undo!" - } + public undoMove(): Move { + if (!this.canUndoMove) { throw "Newp! Can't undo!" } const { gameBoard } = this; - const { source, dest, playerId } = this._moveHistory.pop(); + const move = this._moveHistory.pop(); + const { source, dest, playerId } = move; this.currentPlayerId = playerId; gameBoard.move(dest, source); + return move; } public get canUndoMove(): boolean { if (this._moveHistory.length === 0 || this.isGameOver) { return false; } - return true; + return true; } - } diff --git a/src/listeners.ts b/src/listeners.ts new file mode 100644 index 0000000..2ffe56e --- /dev/null +++ b/src/listeners.ts @@ -0,0 +1,24 @@ +export interface Listener { + element: Element; + type: string; + listener: (event: Event) => void; +} + +export class ListenerManager { + private listeners: Listener[] = []; + + public addListener(element: Element, type: string, listener: (event: Event) => void): void { + const { listeners } = this; + element.addEventListener(type, listener); + listeners.push({ element, type, listener }); + } + + public removeListeners() { + const { listeners } = this; + listeners.forEach(value => { + const { element, type, listener } = value; + element.removeEventListener(type, listener); + }); + listeners.length = 0; + } +} diff --git a/src/local-game.ts b/src/local-game.ts index bcc0870..531ffb3 100644 --- a/src/local-game.ts +++ b/src/local-game.ts @@ -1,102 +1,71 @@ -import { PeerComm } from "./peering"; -import { Coord, GameBoard, Move } from "./types"; - -const constants = { - selectedPieceClassName: "selected-piece", - selectedPlayerTextClassName: "selected-player-text", - activePlayerPieceClassName: "active-player-piece", - possibleMoveClassName: "possible-move", +import { Game, IGame } from "./game"; +import { Coord } from "./types"; +import { ListenerManager } from "./listeners" + +const classNames = { + selectedPiece: "selected-piece", + selectedPlayerText: "selected-player-text", + activePlayerPiece: "active-player-piece", + possibleMove: "possible-move", }; -interface Listener { - element: Element; - type: string; - listener: (event: Event) => void; -} - -export class ListenerManager { - private listeners: Listener[] = []; - - public addListener(element: Element, type: string, listener: (event: Event) => void): void { - const { listeners } = this; - element.addEventListener(type, listener); - listeners.push({ element, type, listener }); - } - public removeListeners() { - const { listeners } = this; - listeners.forEach(value => { - const { element, type, listener } = value; - element.removeEventListener(type, listener); - }); - listeners.length = 0; - } -} - -const gameBoard = new GameBoard(); +let selectedPiece: Element | null = null; +const game: IGame = new Game('Red', 'Blue'); const listenerManager: ListenerManager = new ListenerManager(); -const player: { - isPlayerRed: boolean; - isRedTurn: boolean; - selectedPieceId: number | undefined; - moveHistory: Move[]; -} = { - isPlayerRed: false, - isRedTurn: false, - selectedPieceId: undefined, - moveHistory: [], -}; - // DOM references const dom = { - defaultRedPiece: document.getElementById("default-red-piece"), - defaultBluePiece: document.getElementById("default-blue-piece"), + defaultP1Piece: document.getElementById("default-p1-piece"), + defaultP2Piece: document.getElementById("default-p2-piece"), cells: () => Array.from(document.querySelectorAll("td")), allPieces: () => Array.from(document.querySelectorAll(".piece")), - playerPieces: (isRedTurn: boolean) => - isRedTurn - ? Array.from( - document.querySelectorAll(".red-piece:not(#default-red-piece") - ) - : Array.from( - document.querySelectorAll( - ".blue-piece:not(#default-blue-piece" - ) - ), - redTurnText: () => Array.from(document.querySelectorAll(".red-turn-text")), - blueTurnText: () => - Array.from(document.querySelectorAll(".blue-turn-text")), + playerPieces: (isP1Turn: boolean) => isP1Turn + ? Array.from(document.querySelectorAll(".p1-piece:not(#default-p1-piece)")) + : Array.from(document.querySelectorAll(".p2-piece:not(#default-p2-piece)")), + p1TurnText: () => Array.from(document.querySelectorAll(".p1-turn-text")), + p2TurnText: () => Array.from(document.querySelectorAll(".p2-turn-text")), resetButton: document.getElementById("reset-button") as HTMLInputElement, undoButton: document.getElementById("undo-button") as HTMLInputElement, }; -const resetBoard = () => { - // resetCells - dom.cells().forEach((cell) => { - cell.classList.remove(constants.possibleMoveClassName); - }); +const getPieceAtIndex = (index: number): Element => { + const cell = dom.cells()[index]; + const piece = cell.children[0]; + return piece; +} +const getPieceAtCoord = (coord: Coord): Element => { + const index = game.coordToBoardIndex(coord); + const piece = getPieceAtIndex(index); + return piece; +} +const resetBoard = () => { + // reset Cells + dom.cells().forEach(cell => cell.classList.remove(classNames.possibleMove)); // reset pieces - dom.allPieces().forEach((piece) => { - if (piece.classList.contains(constants.activePlayerPieceClassName)) { - piece.classList.toggle(constants.activePlayerPieceClassName); - } - if (piece.classList.contains(constants.selectedPieceClassName)) { - piece.classList.toggle(constants.selectedPieceClassName); - } + dom.allPieces().forEach(piece => { + piece.classList.remove(classNames.activePlayerPiece) + piece.classList.remove(classNames.selectedPiece) }); - // remove all listeners listenerManager.removeListeners(); }; -const movePiece = async (sourceIndex: number, destIndex: number) => { - const pieceId = gameBoard.board[sourceIndex]; +/** + * Animates the motion of piece moving from source to dest + * @param source Starting Coord + * @param dest Ending Coord + * @param playerId Player id + */ +const movePiece = async (source: Coord, dest: Coord) => { + const sourceIndex = game.coordToBoardIndex(source); + const destIndex = game.coordToBoardIndex(dest); + // find current piece - const piece = document.getElementById(pieceId.toString()); + const piece = dom.cells()[sourceIndex].children[0]; // unselect piece - piece.classList.remove(constants.selectedPieceClassName); + piece.classList.remove(classNames.selectedPiece); // create clone for new position const destPiece = piece.cloneNode(true); @@ -104,25 +73,19 @@ const movePiece = async (sourceIndex: number, destIndex: number) => { dom.cells()[destIndex].appendChild(destPiece); // start animations - const destAnimation = dom - .cells() - [destIndex].animate([{ opacity: 0 }, { opacity: 1 }], { - duration: 1000, - iterations: 1, - }); + const destAnimation = dom.cells()[destIndex].animate( + [{ opacity: 0 }, { opacity: 1 }], + { duration: 1000, iterations: 1, }); const sourceAnimation = piece.animate( - [ - { - opacity: 1, - width: "45px", - height: "45px", - }, - { - opacity: 0, - width: "5px", - height: "5px", - }, - ], + [{ + opacity: 1, + width: "45px", + height: "45px", + }, { + opacity: 0, + width: "5px", + height: "5px", + }], { duration: 1000, iterations: 1 } ); @@ -131,142 +94,74 @@ const movePiece = async (sourceIndex: number, destIndex: number) => { // remove old piece piece.parentNode.removeChild(piece); - gameBoard.board[sourceIndex] = null; - // set new piece - gameBoard.board[destIndex] = pieceId; }; const nextMove = () => { const setActivePlayerPieces = () => { - const selectPiece = (event: Event) => { - const getAllowedMoveIndicesForIndex = ( - boardIndex: number - ): number[] => { - const getFarthestCoord = ( - startCoord: Coord, - direction: Coord - ): Coord => { - let farthestPos = undefined; - let testPos = startCoord; - while (true) { - const pos = new Coord( - testPos.row + direction.row, - testPos.col + direction.col - ); - const index = gameBoard.coordToBoardIndex(pos); - const withinBounds = - pos.row >= 0 && - pos.row < gameBoard.rows && - pos.col >= 0 && - pos.col < gameBoard.cols; - const isOccupied = Number.isInteger( - gameBoard.board[index] - ); - if (isOccupied || !withinBounds) { - break; - } else { - farthestPos = pos; - testPos = pos; - } - } - return farthestPos; - }; - - const coord = gameBoard.boardIndexToCoord(boardIndex); - - const farthestCoords = Coord.allDirections - .map((direction) => getFarthestCoord(coord, direction)) - .filter((farCoord) => farCoord !== undefined); - - const farthestIndices = farthestCoords.map((c) => - gameBoard.coordToBoardIndex(c) - ); - return farthestIndices; - }; - - const previousSelectedPieceId = player.selectedPieceId; - if (previousSelectedPieceId !== undefined) { - const previousPiece = document.getElementById( - previousSelectedPieceId.toString() - ); - previousPiece.classList.remove( - constants.selectedPieceClassName - ); - dom.cells().forEach((cell) => { - cell.classList.remove(constants.possibleMoveClassName); - }); + const selectPiece = (piece: Element, source: Coord) => { + // clear current selection + if (selectedPiece !== null) { + selectedPiece.classList.remove(classNames.selectedPiece); + dom.cells().forEach(cell => cell.classList.remove(classNames.possibleMove)); } - // read event data - const piece = event.target as Element; - const pieceId = parseInt(piece.id); - // set as selected - player.selectedPieceId = pieceId; - piece.classList.add(constants.selectedPieceClassName); + piece.classList.add(classNames.selectedPiece); + selectedPiece = piece; // hook up allowed moves for: // 1. blink // 2. complete the move - const pieceIndex = gameBoard.board.indexOf(pieceId); - const allowedMoveIndices = - getAllowedMoveIndicesForIndex(pieceIndex); - allowedMoveIndices.forEach((i) => { - const cell = dom.cells()[i]; - listenerManager.addListener( - cell, - "click", - async () => await makeMove(i) - ); - cell.classList.add(constants.possibleMoveClassName); + const allowedMoves = game.getValidMoves(source); + allowedMoves.map((dest: Coord) => { + const cell = dom.cells()[game.coordToBoardIndex(dest)]; + cell.classList.add(classNames.possibleMove); + listenerManager.addListener(cell, "click", async () => await makeMove(source, dest)); }); }; - const makeMove = async (destIndex) => { + // complete the move from source to dest + const makeMove = async (source: Coord, dest: Coord) => { listenerManager.removeListeners(); - const sourceIndex = gameBoard.board.indexOf(player.selectedPieceId); - player.moveHistory.push(new Move(sourceIndex, destIndex)); + const result = game.makeMove(source, dest); + await movePiece(source, dest); - await movePiece(sourceIndex, destIndex); - - checkForWin(); - nextMove(); + if (result.isTerminal) { + alert(result.message); + document.location.reload(); + } else { + nextMove(); + } }; // mark active pieces - const playerPieces = dom.playerPieces(player.isRedTurn); - playerPieces.forEach((pp) => { - pp.classList.add(constants.activePlayerPieceClassName); - listenerManager.addListener(pp, "click", selectPiece); - }); + const playerPositions = game.getPlayerPositions(game.currentPlayerId === game.player1Id); + const piecesAndPositions = playerPositions.map(coord => ({ coord, piece: getPieceAtCoord(coord) })); + piecesAndPositions + .forEach(({ coord, piece }) => { + piece.classList.add(classNames.activePlayerPiece); + listenerManager.addListener(piece, "click", event => selectPiece(piece, coord)); + }); }; // reset text - dom.redTurnText().forEach((t) => - t.classList.remove(constants.selectedPlayerTextClassName) - ); - dom.blueTurnText().forEach((t) => - t.classList.remove(constants.selectedPlayerTextClassName) - ); + dom.p1TurnText().forEach((t) => t.classList.remove(classNames.selectedPlayerText)); + dom.p2TurnText().forEach((t) => t.classList.remove(classNames.selectedPlayerText)); // changePlayer // 1. flip flag // 2. set text // 3. set click callbacks - if (player.isRedTurn) { - player.isRedTurn = false; - dom.blueTurnText().forEach((t) => - t.classList.add(constants.selectedPlayerTextClassName) - ); + const isP1Turn = game.currentPlayerId === game.player1Id; + if (isP1Turn) { + dom.p1TurnText().forEach((t) => t.classList.add(classNames.selectedPlayerText)); } else { - player.isRedTurn = true; - dom.redTurnText().forEach((t) => - t.classList.add(constants.selectedPlayerTextClassName) - ); + dom.p2TurnText().forEach((t) => t.classList.add(classNames.selectedPlayerText)); } - if (player.moveHistory.length === 0) { + dom.undoButton.disabled = game.canUndoMove; + if (game.moveHistory.length === 0) { dom.undoButton.disabled = true; dom.resetButton.disabled = true; } else { @@ -274,92 +169,20 @@ const nextMove = () => { dom.resetButton.disabled = false; } - player.selectedPieceId = undefined; + selectedPiece = null; + resetBoard(); setActivePlayerPieces(); }; -const checkForWin = () => { - const elementsToCoords = (elements: Element[]): Coord[] => { - const elementsToBoardIndexes = (elements: Element[]): number[] => { - const indices = elements.map((pp) => { - const pieceId = Number.parseInt(pp.id); - const pieceIndex = gameBoard.board.indexOf(pieceId); - return pieceIndex; - }); - return indices; - }; - - const indices = elementsToBoardIndexes(elements); - const coords = indices.map((pieceIndex) => - gameBoard.boardIndexToCoord(pieceIndex) - ); - return coords; - }; - const doCoordsFormSquare = (coords: Coord[]): boolean => { - const allRows = coords.map((c) => c.row); - const allCols = coords.map((c) => c.col); - const min = new Coord(Math.min(...allRows), Math.min(...allCols)); - const max = new Coord(Math.max(...allRows), Math.max(...allCols)); - const diff = max.sub(min); - const isSquare = diff.row === 1 && diff.col === 1; - return isSquare; - }; - - const playerCoords = elementsToCoords(dom.playerPieces(player.isRedTurn)); - const otherCoords = elementsToCoords(dom.playerPieces(!player.isRedTurn)); - const squareCoords: Coord[] = [ - new Coord(0, 0), - new Coord(3, 0), - new Coord(3, 3), - new Coord(0, 3), - ]; - - // winning conditions: - // 1. all in one row - // 2. all in one column - // 3. all in corners - // 4. all in square - // losing conditions: - // 1. on three sides of an opponent - - const firstPiece = playerCoords[0]; - const allInRow = playerCoords.every((pc) => pc.row === firstPiece.row); - const allInCol = playerCoords.every((pc) => pc.col === firstPiece.col); - const allInCorners = playerCoords.every( - (pc) => squareCoords.findIndex((sc) => pc.equals(sc)) >= 0 - ); - const allInSquare = doCoordsFormSquare(playerCoords); - - const winnerName = player.isRedTurn ? "Red" : "Blue"; - let winningMessage = undefined; - if (allInRow) { - winningMessage = `${winnerName} wins by forming a row`; - } - if (allInCol) { - winningMessage = `${winnerName} wins by forming a column`; - } - if (allInCorners) { - winningMessage = `${winnerName} wins by putting pieces in corners`; - } - if (allInSquare) { - winningMessage = `${winnerName} wins by forming a square`; - } - - if (winningMessage !== undefined) { - alert(winningMessage); - document.location.reload(); - } -}; - const init = () => { // create board and pieces let isWhite = true; const table = document.getElementById("table"); - for (let r = 0; r < gameBoard.rows; r++) { + for (let r = 0; r < game.size.row; r++) { const tr = document.createElement("tr"); table.appendChild(tr); - for (let c = 0; c < gameBoard.cols; c++) { + for (let c = 0; c < game.size.col; c++) { const td = document.createElement("td"); tr.appendChild(td); td.className = isWhite ? "white" : "black"; @@ -367,19 +190,19 @@ const init = () => { } isWhite = !isWhite; } - gameBoard.board.forEach((value, index) => { - if (value !== null) { - const isRed = value < 4; - const defaultPiece = isRed - ? dom.defaultRedPiece - : dom.defaultBluePiece; - const newPiece = defaultPiece.cloneNode( - true - ) as HTMLParagraphElement; - newPiece.id = value.toString(); - dom.cells()[index].appendChild(newPiece); - } - }); + const createPiece = (isP1Turn: boolean, pieceId: string, coord: Coord) => { + const defaultPiece = isP1Turn + ? dom.defaultP1Piece + : dom.defaultP2Piece; + const newPiece = defaultPiece.cloneNode( + true + ) as HTMLParagraphElement; + newPiece.id = pieceId; + const index = game.coordToBoardIndex(coord); + dom.cells()[index].appendChild(newPiece); + }; + game.getPlayerPositions(true).forEach(coord => createPiece(true, game.player1Id, coord)); + game.getPlayerPositions(false).forEach(coord => createPiece(false, game.player2Id, coord)); // hook up buttons let blockActions = false; @@ -387,9 +210,6 @@ const init = () => { if (blockActions) { return; } - if (player.moveHistory.length === 0) { - return; - } blockActions = true; if (confirm("Reset?")) { @@ -401,69 +221,19 @@ const init = () => { if (blockActions) { return; } - if (player.moveHistory.length === 0) { - return; - } blockActions = true; - player.selectedPieceId = undefined; - resetBoard(); - - const { destIndex, sourceIndex } = player.moveHistory.pop(); - await movePiece(destIndex, sourceIndex); + const move = game.undoMove(); + await movePiece(move.dest, move.source); nextMove(); + blockActions = false; }); }; // start! -const playGame = () => { +export const playGame = () => { init(); nextMove(); }; - -// const isOnlineGame = (window as any).daoIsOnline; -// if (isOnlineGame) { -// const receiver = (data) => -// const peer = new PeerComm() -// } else { -// playGame(); - -// } - -playGame(); - -function delay(ms: number) { - return new Promise(resolve => setTimeout(resolve, ms)); -} -const doStuff = async () => { - const peer = new PeerComm((data) => { - console.log(`TO: ${peer.id})`); - console.log(`FROM: ?)`); - console.log(`DATA: ${data}`); - }); - prompt('Self id:', peer.id); - const otherId = prompt('Peer id to connect to:', ''); - if (!otherId) { - while (!peer.isConnected) { - await delay(500); - } - } else { - peer.connect(otherId); - } - - alert('Connected!'); - alert('Starting messaging server'); - alert('Respond with empty text to stop'); - - while (true) { - const message = prompt('Message to send:', ''); - if (!message) { - break; - } - peer.send(message); - delay(1000); - } -} -// doStuff(); diff --git a/src/peering.ts b/src/peering.ts deleted file mode 100644 index ed11a3f..0000000 --- a/src/peering.ts +++ /dev/null @@ -1,51 +0,0 @@ -import Peer from "peerjs"; - -const daoPeerIdKey = "dao-game-peer-id"; -export class PeerComm<T> { - public constructor(receiver: (data: T) => void) { - this.receiver = receiver; - - // const peerId = localStorage.getItem(daoPeerIdKey) ?? `dao-game-peer-id-${Date.now()}`; - const peerId = `dao-game-peer-id-${Date.now()}`; - const peer = new Peer(peerId); - // localStorage.setItem(daoPeerIdKey, peerId); - this.id = peerId; - this.peer = peer; - - peer.on('connection', (connection) => { - connection.on('data', data => { - receiver(JSON.parse(data)); - }); - connection.on('open', () => { - connection.send('hello'); - this.conn = connection; - }) - }); - } - - public get isConnected() { - return this.conn !== null; - } - public connect(otherPeerId: string): void { - if (this.conn !== null) { - throw "Already connected"; - } - const conn = this.peer.connect(otherPeerId); - conn.on('open', () => { - conn.send('hi'); - this.conn = conn; - }); - } - - public readonly id: string; - public receiver: (data: T) => void; - public send(data: T): void { - if (!this.isConnected) { - throw "Not connected"; - } - this.conn.send(JSON.stringify(data)); - } - - private peer: Peer; - private conn: Peer.DataConnection | null = null; -} diff --git a/src/peering_.ts b/src/peering_.ts deleted file mode 100644 index e89878a..0000000 --- a/src/peering_.ts +++ /dev/null @@ -1,119 +0,0 @@ -let pc; -let localStream; - -const signaling = new BroadcastChannel('webrtc'); -signaling.onmessage = e => { - if (!localStream) { - console.log('not ready yet'); - return; - } - switch (e.data.type) { - case 'offer': - handleOffer(e.data); - break; - case 'answer': - handleAnswer(e.data); - break; - case 'candidate': - handleCandidate(e.data); - break; - case 'ready': - // A second tab joined. This tab will initiate a call unless in a call already. - if (pc) { - console.log('already in call, ignoring'); - return; - } - makeCall(); - break; - case 'bye': - if (pc) { - hangup(); - } - break; - default: - console.log('unhandled', e); - break; - } -}; - -export const start = async () => { - localStream = await navigator.mediaDevices.getUserMedia({audio: true, video: true}); - localVideo.srcObject = localStream; - - signaling.postMessage({type: 'ready'}); -}; - -hangupButton.onclick = async () => { - hangup(); - signaling.postMessage({type: 'bye'}); -}; - -async function hangup() { - if (pc) { - pc.close(); - pc = null; - } - localStream.getTracks().forEach(track => track.stop()); - localStream = null; - startButton.disabled = false; - hangupButton.disabled = true; -}; - -function createPeerConnection() { - pc = new RTCPeerConnection(); - pc.onicecandidate = e => { - const message = { - type: 'candidate', - candidate: null, - }; - if (e.candidate) { - message.candidate = e.candidate.candidate; - message.sdpMid = e.candidate.sdpMid; - message.sdpMLineIndex = e.candidate.sdpMLineIndex; - } - signaling.postMessage(message); - }; - pc.ontrack = e => remoteVideo.srcObject = e.streams[0]; - localStream.getTracks().forEach(track => pc.addTrack(track, localStream)); -} - -async function makeCall() { - await createPeerConnection(); - - const offer = await pc.createOffer(); - signaling.postMessage({type: 'offer', sdp: offer.sdp}); - await pc.setLocalDescription(offer); -} - -async function handleOffer(offer) { - if (pc) { - console.error('existing peerconnection'); - return; - } - await createPeerConnection(); - await pc.setRemoteDescription(offer); - - const answer = await pc.createAnswer(); - signaling.postMessage({type: 'answer', sdp: answer.sdp}); - await pc.setLocalDescription(answer); -} - -async function handleAnswer(answer) { - if (!pc) { - console.error('no peerconnection'); - return; - } - await pc.setRemoteDescription(answer); -} - -async function handleCandidate(candidate) { - if (!pc) { - console.error('no peerconnection'); - return; - } - if (!candidate.candidate) { - await pc.addIceCandidate(null); - } else { - await pc.addIceCandidate(candidate); - } -} \ No newline at end of file diff --git a/src/piano.ts b/src/piano.ts deleted file mode 100644 index fb1a7ac..0000000 --- a/src/piano.ts +++ /dev/null @@ -1,21 +0,0 @@ -// import { PeerComm } from "./peering"; -// import { Move } from "./types"; - -// interface PeerData { -// action: 'move'; -// sourceIndex: number; -// destIndex: number; -// } - -// export class Piano { -// public game: IGame; -// private peerComm: PeerComm<PeerData>; -// public constructor(game: IGame) { -// this.game = game; -// this.receiver = (data: PeerData) => { -// const isGameStarting = game.moveHistory.length === 0; -// const isLocalRed = data.redId === this.peerComm.id; -// }; -// this.peerComm = new PeerComm<PeerData>(this.receiver) -// } -// } \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index a680fdd..ff0e77f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -15,6 +15,17 @@ export class Coord { return new Coord(this.row - coord.row, this.col - coord.col); } + /** + * Finds if otherCoord is contained within a rectangle that sits between origin and this Coord + * @param otherCoord + */ + contains(otherCoord: Coord): boolean { + const xRatio = otherCoord.col / this.col; + const yRatio = otherCoord.row / this.row; + const xyInsideSquare = xRatio >= 0 && xRatio < 1 && yRatio >= 0 && yRatio < 1; + return xyInsideSquare; + } + equals(otherCoord: Coord): boolean { const areEqual = (this.col === otherCoord.col && this.row === otherCoord.row); return areEqual; @@ -53,19 +64,9 @@ export interface MoveResult { message?: string; } -export interface IGame { - moveHistory: Move[]; - currentPlayerId: string; - getValidMoves: (start: Coord) => Coord[]; - makeMove: (source: Coord, dest: Coord) => MoveResult; - undoMove: () => void; - get canUndoMove(): boolean; -} - export class GameBoard { private board: (string | null)[]; - public readonly rows: number; - public readonly cols: number; + public readonly size: Coord; public readonly player1Id: string; public readonly player2Id: string @@ -82,8 +83,7 @@ export class GameBoard { null, player2Id, player1Id, null, player2Id, null, null, player1Id ]; - this.rows = 4; - this.cols = 4; + this.size = new Coord(4, 4); this.player1Id = player1Id; this.player2Id = player2Id; } @@ -112,15 +112,18 @@ export class GameBoard { private set(coord: Coord, isP1: boolean | null): void { const index = this.coordToBoardIndex(coord); if (!Number.isSafeInteger(index) || index < 0 || index >= this.board.length) throw `Index ${index} is out of bounds`; - this.board[index] = isP1 ? this.player1Id : this.player2Id; + this.board[index] = isP1 === null ? + null : + (isP1 ? this.player1Id : this.player2Id); } - private coordToBoardIndex({ row, col }: Coord): number { - const index = row * 4 + col; + public coordToBoardIndex(coord: Coord): number { + const { row, col } = coord; + const index = row * this.size.col + col; return index; } private boardIndexToCoord(index: number): Coord { - const col = index % 4; - const row = Math.floor(index / 4); + const col = index % this.size.col; + const row = Math.floor(index / this.size.col); return new Coord(row, col); } } diff --git a/tools/esbuild-build-and-serve.ts b/tools/esbuild-build-and-serve.ts index 3be8fb1..1f39f91 100644 --- a/tools/esbuild-build-and-serve.ts +++ b/tools/esbuild-build-and-serve.ts @@ -8,7 +8,7 @@ const serveOptions: esbuild.ServeOptions = { servedir: 'public' }; const buildOptions: esbuild.BuildOptions = { - entryPoints: ['src/game.ts'], + entryPoints: ['src/app.ts'], bundle: true, outfile: 'public/bundle.js', sourcemap: true diff --git a/tools/esbuild-watch.ts b/tools/esbuild-watch.ts index 602f1c3..6843d69 100644 --- a/tools/esbuild-watch.ts +++ b/tools/esbuild-watch.ts @@ -1,7 +1,7 @@ import esbuild from 'esbuild'; esbuild.build({ - entryPoints: ['src/game.ts'], + entryPoints: ['src/app.ts'], bundle: true, outfile: 'public/bundle.js', watch: true, diff --git a/tools/esbuild.ts b/tools/esbuild.ts index 24ef6db..dc7e306 100644 --- a/tools/esbuild.ts +++ b/tools/esbuild.ts @@ -1,7 +1,7 @@ import esbuild from 'esbuild'; esbuild.build({ - entryPoints: ['src/game.ts'], + entryPoints: ['src/app.ts'], bundle: true, outfile: 'public/bundle.js', }).catch(() => process.exit(1)) \ No newline at end of file