diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8c3b883 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +config.json +built +node_modules diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..cb6c684 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,542 @@ +{ + "name": "ai", + "requires": true, + "lockfileVersion": 1, + "dependencies": { + "@types/events": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@types/events/-/events-1.2.0.tgz", + "integrity": "sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA==" + }, + "@types/node": { + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.0.5.tgz", + "integrity": "sha512-he3QlF+xnGlmsnL1H8/CiM6r25kk0STky6U5yIqNh4Nnp9KlJBSdMMIiCzDYtAFLw2rWnJ4XKc1xB2/u/anYow==" + }, + "@types/ws": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-5.1.2.tgz", + "integrity": "sha512-NkTXUKTYdXdnPE2aUUbGOXE1XfMK527SCvU/9bj86kyFF6kZ9ZnOQ3mK5jADn98Y2vEUD/7wKDgZa7Qst2wYOg==", + "requires": { + "@types/events": "*", + "@types/node": "*" + } + }, + "ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "requires": { + "co": "^4.6.0", + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0" + } + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=" + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "async-limiter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz", + "integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==" + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "optional": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "color-convert": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.2.tgz", + "integrity": "sha512-3NUJZdhMhcdPn8vJ9v2UQJoH0qqoGUkYTgFEPZaPjEtwmmKUfNV46zZmgB2M5M4DCEQHMaCfWHCxiBflLm04Tg==", + "requires": { + "color-name": "1.1.1" + } + }, + "color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha1-SxQVMEz1ACjqgWQ2Q72C6gWANok=" + }, + "combined-stream": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", + "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==" + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "optional": true, + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", + "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "1.0.6", + "mime-types": "^2.1.12" + } + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", + "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", + "requires": { + "ajv": "^5.1.0", + "har-schema": "^2.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "optional": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "lodash": { + "version": "4.17.10", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", + "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" + }, + "make-error": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.4.tgz", + "integrity": "sha512-0Dab5btKVPhibSalc9QGXb559ED7G7iLjFXBaj9Wq8O3vorueR5K5jaE3hkG6ZQINyhA/JgG6Qk4qdFQjsYV6g==" + }, + "mime-db": { + "version": "1.35.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.35.0.tgz", + "integrity": "sha512-JWT/IcCTsB0Io3AhWUMjRqucrHSPsSf2xKLaRldJVULioggvkJvggZ3VXNNSRkCddE6D+BUI4HEIZIA2OjwIvg==" + }, + "mime-types": { + "version": "2.1.19", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.19.tgz", + "integrity": "sha512-P1tKYHVSZ6uFo26mtnve4HQFE3koh1UWVkp8YUC+ESBHe945xWSoXuHHiGarDqcEZ+whpCDnlNw5LON0kLo+sw==", + "requires": { + "mime-db": "~1.35.0" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + }, + "misskey-reversi": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/misskey-reversi/-/misskey-reversi-0.0.5.tgz", + "integrity": "sha512-lALC8WxwQYU7wBUjbmcGc7b9AfNyzBqdoa8Q6sZfS8cfC+zEbi/Ln8m/fZKuM0qhnylBqTQiPQHHhBsSJxz/OQ==" + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + } + } + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "psl": { + "version": "1.1.29", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz", + "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==" + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "request": { + "version": "2.87.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz", + "integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.6.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.5", + "extend": "~3.0.1", + "forever-agent": "~0.6.1", + "form-data": "~2.3.1", + "har-validator": "~5.0.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.17", + "oauth-sign": "~0.8.2", + "performance-now": "^2.1.0", + "qs": "~6.5.1", + "safe-buffer": "^5.1.1", + "tough-cookie": "~2.3.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.1.0" + }, + "dependencies": { + "tough-cookie": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", + "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", + "requires": { + "punycode": "^1.4.1" + } + } + } + }, + "request-promise-core": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.1.tgz", + "integrity": "sha1-Pu4AssWqgyOc+wTFcA2jb4HNCLY=", + "requires": { + "lodash": "^4.13.1" + } + }, + "request-promise-native": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.5.tgz", + "integrity": "sha1-UoF3D2jgyXGeUWP9P6tIIhX0/aU=", + "requires": { + "request-promise-core": "1.1.1", + "stealthy-require": "^1.1.0", + "tough-cookie": ">=2.3.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "source-map-support": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.6.tgz", + "integrity": "sha512-N4KXEz7jcKqPf2b2vZF11lQIz9W5ZMuUcIOGj243lduidkf2fjkVKJS9vNxVWn3u/uxX38AcE8U9nnH9FPcq+g==", + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "sshpk": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz", + "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=", + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=" + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "requires": { + "psl": "^1.1.24", + "punycode": "^1.4.1" + } + }, + "ts-node": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-6.0.3.tgz", + "integrity": "sha512-ARaOMNFEPKg2ZuC1qJddFvHxHNFVckR0g9xLxMIoMqSSIkDc8iS4/LoV53EdDWWNq2FGwqcEf0bVVGJIWpsznw==", + "requires": { + "arrify": "^1.0.0", + "chalk": "^2.3.0", + "diff": "^3.1.0", + "make-error": "^1.1.1", + "minimist": "^1.2.0", + "mkdirp": "^0.5.1", + "source-map-support": "^0.5.3", + "yn": "^2.0.0" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "optional": true + }, + "typescript": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.8.3.tgz", + "integrity": "sha512-K7g15Bb6Ra4lKf7Iq2l/I5/En+hLIHmxWZGq3D4DIRNFxMNV6j2SHSvDOqs2tGd4UvD/fJvrwopzQXjLrT7Itw==" + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "ws": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.0.0.tgz", + "integrity": "sha512-c2UlYcAZp1VS8AORtpq6y4RJIkJ9dQz18W32SpR/qXGfLDZ2jU4y4wKvvZwqbi7U6gxFQTeE+urMbXU/tsDy4w==", + "requires": { + "async-limiter": "~1.0.0" + } + }, + "yn": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz", + "integrity": "sha1-5a2ryKz0CPY4X8dklWhMiOavaJo=" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..e37db18 --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "name": "ai", + "main": "./built/front.js", + "scripts": { + "build": "tsc" + }, + "dependencies": { + "@types/node": "10.0.5", + "@types/ws": "5.1.2", + "misskey-reversi": "0.0.5", + "request": "2.87.0", + "request-promise-native": "1.0.5", + "ts-node": "6.0.3", + "typescript": "2.8.3", + "ws": "6.0.0" + } +} diff --git a/src/back.ts b/src/back.ts new file mode 100644 index 0000000..9254598 --- /dev/null +++ b/src/back.ts @@ -0,0 +1,372 @@ +/** + * -AI- + * Botのバックエンド(思考を担当) + * + * 対話と思考を同じプロセスで行うと、思考時間が長引いたときにストリームから + * 切断されてしまうので、別々のプロセスで行うようにします + */ + +import * as request from 'request-promise-native'; +import Reversi, { Color } from 'misskey-reversi'; + +let game; +let form; + +const config = require('../config.json'); + +let note; + +function getUserName(user) { + return user.name || user.username; +} + +process.on('message', async msg => { + // 親プロセスからデータをもらう + if (msg.type == '_init_') { + game = msg.game; + form = msg.form; + } + + // フォームが更新されたとき + if (msg.type == 'update-form') { + form.find(i => i.id == msg.body.id).value = msg.body.value; + } + + // ゲームが始まったとき + if (msg.type == 'started') { + onGameStarted(msg.body); + + //#region TLに投稿する + const game = msg.body; + const url = `${config.host}/reversi/${game.id}`; + const user = game.user1Id == config.id ? game.user2 : game.user1; + const isSettai = form[0].value === 0; + const text = isSettai + ? `?[${getUserName(user)}](${config.host}/@${user.username})さんの接待を始めました!` + : `対局を?[${getUserName(user)}](${config.host}/@${user.username})さんと始めました! (強さ${form[0].value})`; + + const res = await request.post(`${config.host}/api/notes/create`, { + json: { + i: config.i, + text: `${text}\n→[観戦する](${url})` + } + }); + + note = res.createdNote; + //#endregion + } + + // ゲームが終了したとき + if (msg.type == 'ended') { + // ストリームから切断 + process.send({ + type: 'close' + }); + + //#region TLに投稿する + const user = game.user1Id == config.id ? game.user2 : game.user1; + const isSettai = form[0].value === 0; + const text = isSettai + ? msg.body.winnerId === null + ? `?[${getUserName(user)}](${config.host}/@${user.username})さんに接待で引き分けました...` + : msg.body.winnerId == config.id + ? `?[${getUserName(user)}](${config.host}/@${user.username})さんに接待で勝ってしまいました...` + : `?[${getUserName(user)}](${config.host}/@${user.username})さんに接待で負けてあげました♪` + : msg.body.winnerId === null + ? `?[${getUserName(user)}](${config.host}/@${user.username})さんと引き分けました~` + : msg.body.winnerId == config.id + ? `?[${getUserName(user)}](${config.host}/@${user.username})さんに勝ちました♪` + : `?[${getUserName(user)}](${config.host}/@${user.username})さんに負けました...`; + + await request.post(`${config.host}/api/notes/create`, { + json: { + i: config.i, + renoteId: note.id, + text: text + } + }); + //#endregion + process.exit(); + } + + // 打たれたとき + if (msg.type == 'set') { + onSet(msg.body); + } +}); + +let o: Reversi; +let botColor: Color; + +// 各マスの強さ +let cellWeights; + +/** + * ゲーム開始時 + * @param g ゲーム情報 + */ +function onGameStarted(g) { + game = g; + + // リバーシエンジン初期化 + o = new Reversi(game.settings.map, { + isLlotheo: game.settings.isLlotheo, + canPutEverywhere: game.settings.canPutEverywhere, + loopedBoard: game.settings.loopedBoard + }); + + // 各マスの価値を計算しておく + cellWeights = o.map.map((pix, i) => { + if (pix == 'null') return 0; + const [x, y] = o.transformPosToXy(i); + let count = 0; + const get = (x, y) => { + if (x < 0 || y < 0 || x >= o.mapWidth || y >= o.mapHeight) return 'null'; + return o.mapDataGet(o.transformXyToPos(x, y)); + }; + + if (get(x , y - 1) == 'null') count++; + if (get(x + 1, y - 1) == 'null') count++; + if (get(x + 1, y ) == 'null') count++; + if (get(x + 1, y + 1) == 'null') count++; + if (get(x , y + 1) == 'null') count++; + if (get(x - 1, y + 1) == 'null') count++; + if (get(x - 1, y ) == 'null') count++; + if (get(x - 1, y - 1) == 'null') count++; + //return Math.pow(count, 3); + return count >= 4 ? 1 : 0; + }); + + botColor = game.user1Id == config.id && game.black == 1 || game.user2Id == config.id && game.black == 2; + + if (botColor) { + think(); + } +} + +function onSet(x) { + o.put(x.color, x.pos); + + if (x.next === botColor) { + think(); + } +} + +const db = {}; + +function think() { + console.log('Thinking...'); + console.time('think'); + + const isSettai = form[0].value === 0; + + // 接待モードのときは、全力(5手先読みくらい)で負けるようにする + const maxDepth = isSettai ? 5 : form[0].value; + + /** + * Botにとってある局面がどれだけ有利か取得する + */ + function staticEval() { + let score = o.canPutSomewhere(botColor).length; + + cellWeights.forEach((weight, i) => { + // 係数 + const coefficient = 30; + weight = weight * coefficient; + + const stone = o.board[i]; + if (stone === botColor) { + // TODO: 価値のあるマスに設置されている自分の石に縦か横に接するマスは価値があると判断する + score += weight; + } else if (stone !== null) { + score -= weight; + } + }); + + // ロセオならスコアを反転 + if (game.settings.isLlotheo) score = -score; + + // 接待ならスコアを反転 + if (isSettai) score = -score; + + return score; + } + + /** + * αβ法での探索 + */ + const dive = (pos: number, alpha = -Infinity, beta = Infinity, depth = 0): number => { + // 試し打ち + o.put(o.turn, pos); + + const key = o.board.toString(); + let cache = db[key]; + if (cache) { + if (alpha >= cache.upper) { + o.undo(); + return cache.upper; + } + if (beta <= cache.lower) { + o.undo(); + return cache.lower; + } + alpha = Math.max(alpha, cache.lower); + beta = Math.min(beta, cache.upper); + } else { + cache = { + upper: Infinity, + lower: -Infinity + }; + } + + const isBotTurn = o.turn === botColor; + + // 勝った + if (o.turn === null) { + const winner = o.winner; + + // 勝つことによる基本スコア + const base = 10000; + + let score; + + if (game.settings.isLlotheo) { + // 勝ちは勝ちでも、より自分の石を少なくした方が美しい勝ちだと判定する + score = o.winner ? base - (o.blackCount * 100) : base - (o.whiteCount * 100); + } else { + // 勝ちは勝ちでも、より相手の石を少なくした方が美しい勝ちだと判定する + score = o.winner ? base + (o.blackCount * 100) : base + (o.whiteCount * 100); + } + + // 巻き戻し + o.undo(); + + // 接待なら自分が負けた方が高スコア + return isSettai + ? winner !== botColor ? score : -score + : winner === botColor ? score : -score; + } + + if (depth === maxDepth) { + // 静的に評価 + const score = staticEval(); + + // 巻き戻し + o.undo(); + + return score; + } else { + const cans = o.canPutSomewhere(o.turn); + + let value = isBotTurn ? -Infinity : Infinity; + let a = alpha; + let b = beta; + + // 次のターンのプレイヤーにとって最も良い手を取得 + for (const p of cans) { + if (isBotTurn) { + const score = dive(p, a, beta, depth + 1); + value = Math.max(value, score); + a = Math.max(a, value); + if (value >= beta) break; + } else { + const score = dive(p, alpha, b, depth + 1); + value = Math.min(value, score); + b = Math.min(b, value); + if (value <= alpha) break; + } + } + + // 巻き戻し + o.undo(); + + if (value <= alpha) { + cache.upper = value; + } else if (value >= beta) { + cache.lower = value; + } else { + cache.upper = value; + cache.lower = value; + } + + db[key] = cache; + + return value; + } + }; + + /** + * αβ法での探索(キャッシュ無し)(デバッグ用) + */ + const dive2 = (pos: number, alpha = -Infinity, beta = Infinity, depth = 0): number => { + // 試し打ち + o.put(o.turn, pos); + + const isBotTurn = o.turn === botColor; + + // 勝った + if (o.turn === null) { + const winner = o.winner; + + // 勝つことによる基本スコア + const base = 10000; + + let score; + + if (game.settings.isLlotheo) { + // 勝ちは勝ちでも、より自分の石を少なくした方が美しい勝ちだと判定する + score = o.winner ? base - (o.blackCount * 100) : base - (o.whiteCount * 100); + } else { + // 勝ちは勝ちでも、より相手の石を少なくした方が美しい勝ちだと判定する + score = o.winner ? base + (o.blackCount * 100) : base + (o.whiteCount * 100); + } + + // 巻き戻し + o.undo(); + + // 接待なら自分が負けた方が高スコア + return isSettai + ? winner !== botColor ? score : -score + : winner === botColor ? score : -score; + } + + if (depth === maxDepth) { + // 静的に評価 + const score = staticEval(); + + // 巻き戻し + o.undo(); + + return score; + } else { + const cans = o.canPutSomewhere(o.turn); + + // 次のターンのプレイヤーにとって最も良い手を取得 + for (const p of cans) { + if (isBotTurn) { + alpha = Math.max(alpha, dive2(p, alpha, beta, depth + 1)); + } else { + beta = Math.min(beta, dive2(p, alpha, beta, depth + 1)); + } + if (alpha >= beta) break; + } + + // 巻き戻し + o.undo(); + + return isBotTurn ? alpha : beta; + } + }; + + const cans = o.canPutSomewhere(botColor); + const scores = cans.map(p => dive(p)); + const pos = cans[scores.indexOf(Math.max(...scores))]; + + console.log('Thinked:', pos); + console.timeEnd('think'); + + process.send({ + type: 'put', + pos + }); +} diff --git a/src/front.ts b/src/front.ts new file mode 100644 index 0000000..71b30f6 --- /dev/null +++ b/src/front.ts @@ -0,0 +1,220 @@ +/** + * -AI- + * Botのフロントエンド(ストリームとの対話を担当) + * + * 対話と思考を同じプロセスで行うと、思考時間が長引いたときにストリームから + * 切断されてしまうので、別々のプロセスで行うようにします + */ + +import * as childProcess from 'child_process'; +import * as WebSocket from 'ws'; +import * as request from 'request-promise-native'; + +const config = require('../config.json'); + +const wsUrl = config.host.replace('http', 'ws'); +const apiUrl = config.host + '/api'; + +/** + * ホームストリーム + */ +const homeStream = new WebSocket(`${wsUrl}/?i=${config.i}`); + +homeStream.addEventListener('open', () => { + console.log('home stream opened'); +}); + +homeStream.addEventListener('close', () => { + console.log('home stream closed'); +}); + +homeStream.addEventListener('message', message => { + const msg = JSON.parse(message.data); + + // タイムライン上でなんか言われたまたは返信されたとき + if (msg.type == 'mention' || msg.type == 'reply') { + const note = msg.body; + + if (note.userId == config.id) return; + + // リアクションする + request.post(`${apiUrl}/notes/reactions/create`, { + json: { + i: config.i, + noteId: note.id, + reaction: 'love' + } + }); + + if (note.text) { + if (note.text.indexOf('リバーシ') > -1) { + request.post(`${apiUrl}/notes/create`, { + json: { + i: config.i, + replyId: note.id, + text: '良いですよ~' + } + }); + + invite(note.userId); + } + } + } + + // メッセージでなんか言われたとき + if (msg.type == 'messaging_message') { + const message = msg.body; + if (message.text) { + if (message.text.indexOf('リバーシ') > -1) { + request.post(`${apiUrl}/messaging/messages/create`, { + json: { + i: config.i, + userId: message.userId, + text: '良いですよ~' + } + }); + + invite(message.userId); + } + } + } +}); + +// ユーザーを対局に誘う +function invite(userId) { + request.post(`${apiUrl}/games/reversi/match`, { + json: { + i: config.i, + userId: userId + } + }); +} + +/** + * リバーシストリーム + */ +const reversiStream = new WebSocket(`${wsUrl}/games/reversi?i=${config.i}`); + +reversiStream.addEventListener('open', () => { + console.log('reversi stream opened'); +}); + +reversiStream.addEventListener('close', () => { + console.log('reversi stream closed'); +}); + +reversiStream.addEventListener('message', message => { + const msg = JSON.parse(message.data); + + // 招待されたとき + if (msg.type == 'invited') { + onInviteMe(msg.body.parent); + } + + // マッチしたとき + if (msg.type == 'matched') { + gameStart(msg.body); + } +}); + +/** + * ゲーム開始 + * @param game ゲーム情報 + */ +function gameStart(game) { + // ゲームストリームに接続 + const gw = new WebSocket(`${wsUrl}/games/reversi-game?i=${config.i}&game=${game.id}`); + + gw.addEventListener('open', () => { + console.log('reversi game stream opened'); + + // フォーム + const form = [{ + id: 'strength', + type: 'radio', + label: '強さ', + value: 2, + items: [{ + label: '接待', + value: 0 + }, { + label: '弱', + value: 1 + }, { + label: '中', + value: 2 + }, { + label: '強', + value: 3 + }, { + label: '最強', + value: 5 + }] + }]; + + //#region バックエンドプロセス開始 + const ai = childProcess.fork(__dirname + '/back.js'); + + // バックエンドプロセスに情報を渡す + ai.send({ + type: '_init_', + game, + form + }); + + ai.on('message', msg => { + if (msg.type == 'put') { + gw.send(JSON.stringify({ + type: 'set', + pos: msg.pos + })); + } else if (msg.type == 'close') { + gw.close(); + } + }); + + // ゲームストリームから情報が流れてきたらそのままバックエンドプロセスに伝える + gw.addEventListener('message', message => { + const msg = JSON.parse(message.data); + ai.send(msg); + }); + //#endregion + + // フォーム初期化 + setTimeout(() => { + gw.send(JSON.stringify({ + type: 'init-form', + body: form + })); + }, 1000); + + // どんな設定内容の対局でも受け入れる + setTimeout(() => { + gw.send(JSON.stringify({ + type: 'accept' + })); + }, 2000); + }); + + gw.addEventListener('close', () => { + console.log('reversi game stream closed'); + }); +} + +/** + * リバーシの対局に招待されたとき + * @param inviter 誘ってきたユーザー + */ +async function onInviteMe(inviter) { + console.log(`Someone invited me: @${inviter.username}`); + + // 承認 + const game = await request.post(`${apiUrl}/games/reversi/match`, { + json: { + i: config.i, + userId: inviter.id + } + }); + + gameStart(game); +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..30e53c4 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "noEmitOnError": true, + "noImplicitAny": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "experimentalDecorators": true, + "sourceMap": false, + "target": "es2017", + "module": "commonjs", + "removeComments": false, + "noLib": false, + "outDir": "built", + "rootDir": "src" + }, + "compileOnSave": false, + "include": [ + "./src/**/*.ts" + ] +}