diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..3729ff0c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,25 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/.gitignore b/.gitignore index 5fdf4641..2b9dda27 100644 --- a/.gitignore +++ b/.gitignore @@ -1,34 +1,39 @@ -#ignore thumbnails created by windows -Thumbs.db -#Ignore files build by Visual Studio -AssemblyInfo.Version.cs -*.obj -*.exe -*.pdb -*.user -*.aps -*.pch -*.vspscc -*_i.c -*_p.c -*.ncb -*.suo -*.tlb -*.tlh -*.bak -*.cache -*.ilk -*.log -*.nupkg -[Bb]in -[Dd]ebug*/ -[Bb]uild*/ -[Nn]uget*/ -*.lib -*.sbr -obj/ -[Rr]elease*/ -_ReSharper*/ -[Tt]est[Rr]esult* -Output*/ -[Pp]ackages*/ \ No newline at end of file +#ignore thumbnails created by windows +Thumbs.db +#Ignore files build by Visual Studio +AssemblyInfo.Version.cs +*.obj +*.exe +*.pdb +*.user +*.aps +*.pch +*.vspscc +*_i.c +*_p.c +*.ncb +*.suo +*.tlb +*.tlh +*.bak +*.cache +*.ilk +*.log +*.nupkg +[Bb]in +[Dd]ebug*/ +[Bb]uild*/ +[Nn]uget*/ +*.lib +*.sbr +obj/ +[Rr]elease*/ +_ReSharper*/ +[Tt]est[Rr]esult* +Output*/ +[Pp]ackages*/ +/Jackal/Players/Personal/ +.vs/ + +# Rider +.idea/ \ No newline at end of file diff --git a/Front/.env.development b/Front/.env.development new file mode 100644 index 00000000..083c815f --- /dev/null +++ b/Front/.env.development @@ -0,0 +1 @@ +NODE_ENV=development \ No newline at end of file diff --git a/Front/.eslintrc.cjs b/Front/.eslintrc.cjs new file mode 100644 index 00000000..77a8f6a4 --- /dev/null +++ b/Front/.eslintrc.cjs @@ -0,0 +1,22 @@ +module.exports = { + root: true, + env: { browser: true, es2020: true }, + extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:react-hooks/recommended'], + ignorePatterns: ['dist', '.eslintrc.cjs'], + parser: '@typescript-eslint/parser', + plugins: ['react-refresh'], + rules: { + 'sort-imports': [ + 'warn', + { + ignoreCase: false, + ignoreDeclarationSort: true, + ignoreMemberSort: false, + memberSyntaxSortOrder: ['none', 'all', 'multiple', 'single'], + allowSeparatedGroups: false, + }, + ], + '@typescript-eslint/no-unused-vars': 'off', + 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], + }, +}; diff --git a/Front/.gitignore b/Front/.gitignore new file mode 100644 index 00000000..a56caf97 --- /dev/null +++ b/Front/.gitignore @@ -0,0 +1,25 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +dev +coverage +*.local + +# Editor directories and files +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/Front/.prettierrc b/Front/.prettierrc new file mode 100644 index 00000000..f7045c81 --- /dev/null +++ b/Front/.prettierrc @@ -0,0 +1,12 @@ +{ + "singleQuote": true, + "bracketSpacing": true, + "trailingComma": "all", + "tabWidth": 4, + "printWidth": 120, + "semi": true, + "importOrder": ["^@core/(.*)$", "^@server/(.*)$", "^@ui/(.*)$", "^[./]"], + "importOrderSeparation": true, + "importOrderSortSpecifiers": true, + "plugins": ["@trivago/prettier-plugin-sort-imports"] +} diff --git a/Front/.vscode/settings.json b/Front/.vscode/settings.json new file mode 100644 index 00000000..d3ae1cf1 --- /dev/null +++ b/Front/.vscode/settings.json @@ -0,0 +1,16 @@ +{ + "breadcrumbs.enabled": false, + "editor.formatOnSave": true, + "prettier.requireConfig": true, + "eslint.enable": true, + "eslint.validate": [], + "editor.defaultFormatter": "esbenp.prettier-vscode", + "eslint.lintTask.options": "", + "eslint.useFlatConfig": false, + "editor.showUnused": false, + "[typescriptreact]": { + "javascript.preferences.importModuleSpecifier": "project-relative", + "typescript.preferences.importModuleSpecifier": "non-relative" + }, + "prettier.tabWidth": 4 +} diff --git a/Front/README.md b/Front/README.md new file mode 100644 index 00000000..0d6babed --- /dev/null +++ b/Front/README.md @@ -0,0 +1,30 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: + +- Configure the top-level `parserOptions` property like this: + +```js +export default { + // other rules... + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + project: ['./tsconfig.json', './tsconfig.node.json'], + tsconfigRootDir: __dirname, + }, +} +``` + +- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` +- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` +- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list diff --git a/Front/index.html b/Front/index.html new file mode 100644 index 00000000..7f3d9bc3 --- /dev/null +++ b/Front/index.html @@ -0,0 +1,13 @@ + + + + + + + React Jackal + + +
+ + + diff --git a/Front/jest.config.ts b/Front/jest.config.ts new file mode 100644 index 00000000..139ef3fe --- /dev/null +++ b/Front/jest.config.ts @@ -0,0 +1,15 @@ +import { JestConfigWithTsJest } from 'ts-jest'; + +const config: JestConfigWithTsJest = { + roots: ['/src'], + testMatch: ['/src/**/*.test.ts'], + preset: 'ts-jest', + modulePaths: ['/src'], + moduleNameMapper: { + '/app/(.*)': ['/src/app/$1'], + '/common/(.*)': ['/src/common/$1'], + '/game/(.*)': ['/src/game/$1'], + }, +}; + +export default config; diff --git a/Front/package-lock.json b/Front/package-lock.json new file mode 100644 index 00000000..fabd1061 --- /dev/null +++ b/Front/package-lock.json @@ -0,0 +1,7453 @@ +{ + "name": "jackal-front", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "jackal-front", + "version": "1.0.0", + "dependencies": { + "@microsoft/signalr": "^8.0.7", + "@reduxjs/toolkit": "^2.2.5", + "axios": "^1.7.2", + "bootstrap": "^5.3.3", + "classnames": "^2.5.1", + "dayjs": "^1.11.13", + "proxy-memoize": "^3.0.1", + "react": "^18.2.0", + "react-bootstrap": "^2.10.2", + "react-dom": "^18.2.0", + "react-icons": "^5.3.0", + "react-move": "^6.5.0", + "react-redux": "^9.1.2", + "react-router-dom": "^6.25.1", + "react-tooltip": "^5.28.0", + "redux-saga": "^1.3.0" + }, + "devDependencies": { + "@trivago/prettier-plugin-sort-imports": "^5.2.2", + "@types/jest": "^29.5.12", + "@types/node": "^20.14.8", + "@types/react": "^18.2.66", + "@types/react-dom": "^18.2.22", + "@typescript-eslint/eslint-plugin": "^7.2.0", + "@typescript-eslint/parser": "^7.2.0", + "@vitejs/plugin-react-swc": "^3.5.0", + "eslint": "^8.57.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.6", + "jest": "^29.7.0", + "less": "^4.2.0", + "prettier": "^3.3.3", + "ts-jest": "^29.2.4", + "typescript": "^5.2.2", + "vite": "^5.2.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.2.tgz", + "integrity": "sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", + "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/helper-compilation-targets": "^7.25.2", + "@babel/helper-module-transforms": "^7.25.2", + "@babel/helpers": "^7.25.0", + "@babel/parser": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.2", + "@babel/types": "^7.25.2", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.10.tgz", + "integrity": "sha512-rRHT8siFIXQrAYOYqZQVsAr8vJ+cBNqcVAY6m5V8/4QqzaPl+zDBe6cLEPRDuNOUf3ww8RfJVlOyQMoSI+5Ang==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.26.10", + "@babel/types": "^7.26.10", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz", + "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.25.2", + "@babel/helper-validator-option": "^7.24.8", + "browserslist": "^4.23.1", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz", + "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", + "@babel/traverse": "^7.25.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", + "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", + "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.0.tgz", + "integrity": "sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.10.tgz", + "integrity": "sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.26.10" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz", + "integrity": "sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz", + "integrity": "sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.7.tgz", + "integrity": "sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.6.tgz", + "integrity": "sha512-Ja18XcETdEl5mzzACGd+DKgaGJzPTCow7EglgwTmHdwokzDFYh/MHua6lU6DV/hjF2IaOJ4oX2nqnjG7RElKOw==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", + "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.26.9", + "@babel/types": "^7.26.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.10.tgz", + "integrity": "sha512-k8NuDrxr0WrPH5Aupqb2LCVURP/S0vBEn5mK6iH+GIYob66U5EtoZvcdudR2jQ4cmTwhEwW1DLB+Yyas9zjF6A==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.10", + "@babel/parser": "^7.26.10", + "@babel/template": "^7.26.9", + "@babel/types": "^7.26.10", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz", + "integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/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/@eslint/eslintrc/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/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz", + "integrity": "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==", + "dependencies": { + "@floating-ui/utils": "^0.2.8" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.11", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.11.tgz", + "integrity": "sha512-qkMCxSR24v2vGkhYDo/UzxfJN3D4syqSjyuTFz6C7XcpU1pASPRieNI0Kj5VP3/503mOfYiGY891ugBX1GlABQ==", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.8" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz", + "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==" + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/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/@humanwhocodes/config-array/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/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "dev": true + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/core/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/core/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@microsoft/signalr": { + "version": "8.0.7", + "resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-8.0.7.tgz", + "integrity": "sha512-PHcdMv8v5hJlBkRHAuKG5trGViQEkPYee36LnJQx4xHOQ5LL4X0nEWIxOp5cCtZ7tu+30quz5V3k0b1YNuc6lw==", + "dependencies": { + "abort-controller": "^3.0.0", + "eventsource": "^2.0.2", + "fetch-cookie": "^2.0.3", + "node-fetch": "^2.6.7", + "ws": "^7.4.5" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@react-aria/ssr": { + "version": "3.9.4", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.4.tgz", + "integrity": "sha512-4jmAigVq409qcJvQyuorsmBR4+9r3+JEC60wC+Y0MZV0HCtTmm8D9guYXlJMdx0SSkgj0hHAyFm/HvPNFofCoQ==", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@redux-saga/core": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@redux-saga/core/-/core-1.3.0.tgz", + "integrity": "sha512-L+i+qIGuyWn7CIg7k1MteHGfttKPmxwZR5E7OsGikCL2LzYA0RERlaUY00Y3P3ZV2EYgrsYlBrGs6cJP5OKKqA==", + "dependencies": { + "@babel/runtime": "^7.6.3", + "@redux-saga/deferred": "^1.2.1", + "@redux-saga/delay-p": "^1.2.1", + "@redux-saga/is": "^1.1.3", + "@redux-saga/symbols": "^1.1.3", + "@redux-saga/types": "^1.2.1", + "typescript-tuple": "^2.2.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/redux-saga" + } + }, + "node_modules/@redux-saga/deferred": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@redux-saga/deferred/-/deferred-1.2.1.tgz", + "integrity": "sha512-cmin3IuuzMdfQjA0lG4B+jX+9HdTgHZZ+6u3jRAOwGUxy77GSlTi4Qp2d6PM1PUoTmQUR5aijlA39scWWPF31g==" + }, + "node_modules/@redux-saga/delay-p": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@redux-saga/delay-p/-/delay-p-1.2.1.tgz", + "integrity": "sha512-MdiDxZdvb1m+Y0s4/hgdcAXntpUytr9g0hpcOO1XFVyyzkrDu3SKPgBFOtHn7lhu7n24ZKIAT1qtKyQjHqRd+w==", + "dependencies": { + "@redux-saga/symbols": "^1.1.3" + } + }, + "node_modules/@redux-saga/is": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@redux-saga/is/-/is-1.1.3.tgz", + "integrity": "sha512-naXrkETG1jLRfVfhOx/ZdLj0EyAzHYbgJWkXbB3qFliPcHKiWbv/ULQryOAEKyjrhiclmr6AMdgsXFyx7/yE6Q==", + "dependencies": { + "@redux-saga/symbols": "^1.1.3", + "@redux-saga/types": "^1.2.1" + } + }, + "node_modules/@redux-saga/symbols": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@redux-saga/symbols/-/symbols-1.1.3.tgz", + "integrity": "sha512-hCx6ZvU4QAEUojETnX8EVg4ubNLBFl1Lps4j2tX7o45x/2qg37m3c6v+kSp8xjDJY+2tJw4QB3j8o8dsl1FDXg==" + }, + "node_modules/@redux-saga/types": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@redux-saga/types/-/types-1.2.1.tgz", + "integrity": "sha512-1dgmkh+3so0+LlBWRhGA33ua4MYr7tUOj+a9Si28vUi0IUFNbff1T3sgpeDJI/LaC75bBYnQ0A3wXjn0OrRNBA==" + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.2.5.tgz", + "integrity": "sha512-aeFA/s5NCG7NoJe/MhmwREJxRkDs0ZaSqt0MxhWUrwCf1UQXpwR87RROJEql0uAkLI6U7snBOYOcKw83ew3FPg==", + "dependencies": { + "immer": "^10.0.3", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@remix-run/router": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.18.0.tgz", + "integrity": "sha512-L3jkqmqoSVBVKHfpGZmLrex0lxR5SucGA0sUfFzGctehw+S/ggL9L/0NnC5mw6P8HUWpFZ3nQw3cRApjjWx9Sw==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@restart/hooks": { + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz", + "integrity": "sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w==", + "dependencies": { + "dequal": "^2.0.3" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@restart/ui": { + "version": "1.6.9", + "resolved": "https://registry.npmjs.org/@restart/ui/-/ui-1.6.9.tgz", + "integrity": "sha512-mUbygUsJcRurjZCt1f77gg4DpheD1D+Sc7J3JjAkysUj7t8m4EBJVOqWC9788Qtbc69cJ+HlJc6jBguKwS8Mcw==", + "dependencies": { + "@babel/runtime": "^7.21.0", + "@popperjs/core": "^2.11.6", + "@react-aria/ssr": "^3.5.0", + "@restart/hooks": "^0.4.9", + "@types/warning": "^3.0.0", + "dequal": "^2.0.3", + "dom-helpers": "^5.2.0", + "uncontrollable": "^8.0.1", + "warning": "^4.0.3" + }, + "peerDependencies": { + "react": ">=16.14.0", + "react-dom": ">=16.14.0" + } + }, + "node_modules/@restart/ui/node_modules/uncontrollable": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-8.0.4.tgz", + "integrity": "sha512-ulRWYWHvscPFc0QQXvyJjY6LIXU56f0h8pQFvhxiKk5V1fcI8gp9Ht9leVAhrVjzqMw0BgjspBINx9r6oyJUvQ==", + "peerDependencies": { + "react": ">=16.14.0" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.0.tgz", + "integrity": "sha512-wLJuPLT6grGZsy34g4N1yRfYeouklTgPhH1gWXCYspenKYD0s3cR99ZevOGw5BexMNywkbV3UkjADisozBmpPQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.28.0.tgz", + "integrity": "sha512-eiNkznlo0dLmVG/6wf+Ifi/v78G4d4QxRhuUl+s8EWZpDewgk7PX3ZyECUXU0Zq/Ca+8nU8cQpNC4Xgn2gFNDA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.28.0.tgz", + "integrity": "sha512-lmKx9yHsppblnLQZOGxdO66gT77bvdBtr/0P+TPOseowE7D9AJoBw8ZDULRasXRWf1Z86/gcOdpBrV6VDUY36Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.28.0.tgz", + "integrity": "sha512-8hxgfReVs7k9Js1uAIhS6zq3I+wKQETInnWQtgzt8JfGx51R1N6DRVy3F4o0lQwumbErRz52YqwjfvuwRxGv1w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.28.0.tgz", + "integrity": "sha512-lA1zZB3bFx5oxu9fYud4+g1mt+lYXCoch0M0V/xhqLoGatbzVse0wlSQ1UYOWKpuSu3gyN4qEc0Dxf/DII1bhQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.28.0.tgz", + "integrity": "sha512-aI2plavbUDjCQB/sRbeUZWX9qp12GfYkYSJOrdYTL/C5D53bsE2/nBPuoiJKoWp5SN78v2Vr8ZPnB+/VbQ2pFA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.28.0.tgz", + "integrity": "sha512-WXveUPKtfqtaNvpf0iOb0M6xC64GzUX/OowbqfiCSXTdi/jLlOmH0Ba94/OkiY2yTGTwteo4/dsHRfh5bDCZ+w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.28.0.tgz", + "integrity": "sha512-yLc3O2NtOQR67lI79zsSc7lk31xjwcaocvdD1twL64PK1yNaIqCeWI9L5B4MFPAVGEVjH5k1oWSGuYX1Wutxpg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.28.0.tgz", + "integrity": "sha512-+P9G9hjEpHucHRXqesY+3X9hD2wh0iNnJXX/QhS/J5vTdG6VhNYMxJ2rJkQOxRUd17u5mbMLHM7yWGZdAASfcg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.28.0.tgz", + "integrity": "sha512-1xsm2rCKSTpKzi5/ypT5wfc+4bOGa/9yI/eaOLW0oMs7qpC542APWhl4A37AENGZ6St6GBMWhCCMM6tXgTIplw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.28.0.tgz", + "integrity": "sha512-zgWxMq8neVQeXL+ouSf6S7DoNeo6EPgi1eeqHXVKQxqPy1B2NvTbaOUWPn/7CfMKL7xvhV0/+fq/Z/J69g1WAQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.28.0.tgz", + "integrity": "sha512-VEdVYacLniRxbRJLNtzwGt5vwS0ycYshofI7cWAfj7Vg5asqj+pt+Q6x4n+AONSZW/kVm+5nklde0qs2EUwU2g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.28.0.tgz", + "integrity": "sha512-LQlP5t2hcDJh8HV8RELD9/xlYtEzJkm/aWGsauvdO2ulfl3QYRjqrKW+mGAIWP5kdNCBheqqqYIGElSRCaXfpw==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.0.tgz", + "integrity": "sha512-Nl4KIzteVEKE9BdAvYoTkW19pa7LR/RBrT6F1dJCV/3pbjwDcaOq+edkP0LXuJ9kflW/xOK414X78r+K84+msw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.28.0.tgz", + "integrity": "sha512-eKpJr4vBDOi4goT75MvW+0dXcNUqisK4jvibY9vDdlgLx+yekxSm55StsHbxUsRxSTt3JEQvlr3cGDkzcSP8bw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.28.0.tgz", + "integrity": "sha512-Vi+WR62xWGsE/Oj+mD0FNAPY2MEox3cfyG0zLpotZdehPFXwz6lypkGs5y38Jd/NVSbOD02aVad6q6QYF7i8Bg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.28.0.tgz", + "integrity": "sha512-kN/Vpip8emMLn/eOza+4JwqDZBL6MPNpkdaEsgUtW1NYN3DZvZqSQrbKzJcTL6hd8YNmFTn7XGWMwccOcJBL0A==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.28.0.tgz", + "integrity": "sha512-Bvno2/aZT6usSa7lRDL2+hMjVAGjuqaymF1ApZm31JXzniR/hvr14jpU+/z4X6Gt5BPlzosscyJZGUvguXIqeQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@swc/core": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.5.7.tgz", + "integrity": "sha512-U4qJRBefIJNJDRCCiVtkfa/hpiZ7w0R6kASea+/KLp+vkus3zcLSB8Ub8SvKgTIxjWpwsKcZlPf5nrv4ls46SQ==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@swc/counter": "^0.1.2", + "@swc/types": "0.1.7" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.5.7", + "@swc/core-darwin-x64": "1.5.7", + "@swc/core-linux-arm-gnueabihf": "1.5.7", + "@swc/core-linux-arm64-gnu": "1.5.7", + "@swc/core-linux-arm64-musl": "1.5.7", + "@swc/core-linux-x64-gnu": "1.5.7", + "@swc/core-linux-x64-musl": "1.5.7", + "@swc/core-win32-arm64-msvc": "1.5.7", + "@swc/core-win32-ia32-msvc": "1.5.7", + "@swc/core-win32-x64-msvc": "1.5.7" + }, + "peerDependencies": { + "@swc/helpers": "^0.5.0" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.5.7.tgz", + "integrity": "sha512-bZLVHPTpH3h6yhwVl395k0Mtx8v6CGhq5r4KQdAoPbADU974Mauz1b6ViHAJ74O0IVE5vyy7tD3OpkQxL/vMDQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.5.7.tgz", + "integrity": "sha512-RpUyu2GsviwTc2qVajPL0l8nf2vKj5wzO3WkLSHAHEJbiUZk83NJrZd1RVbEknIMO7+Uyjh54hEh8R26jSByaw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.5.7.tgz", + "integrity": "sha512-cTZWTnCXLABOuvWiv6nQQM0hP6ZWEkzdgDvztgHI/+u/MvtzJBN5lBQ2lue/9sSFYLMqzqff5EHKlFtrJCA9dQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.5.7.tgz", + "integrity": "sha512-hoeTJFBiE/IJP30Be7djWF8Q5KVgkbDtjySmvYLg9P94bHg9TJPSQoC72tXx/oXOgXvElDe/GMybru0UxhKx4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.5.7.tgz", + "integrity": "sha512-+NDhK+IFTiVK1/o7EXdCeF2hEzCiaRSrb9zD7X2Z7inwWlxAntcSuzZW7Y6BRqGQH89KA91qYgwbnjgTQ22PiQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.5.7.tgz", + "integrity": "sha512-25GXpJmeFxKB+7pbY7YQLhWWjkYlR+kHz5I3j9WRl3Lp4v4UD67OGXwPe+DIcHqcouA1fhLhsgHJWtsaNOMBNg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.5.7.tgz", + "integrity": "sha512-0VN9Y5EAPBESmSPPsCJzplZHV26akC0sIgd3Hc/7S/1GkSMoeuVL+V9vt+F/cCuzr4VidzSkqftdP3qEIsXSpg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.5.7.tgz", + "integrity": "sha512-RtoNnstBwy5VloNCvmvYNApkTmuCe4sNcoYWpmY7C1+bPR+6SOo8im1G6/FpNem8AR5fcZCmXHWQ+EUmRWJyuA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.5.7.tgz", + "integrity": "sha512-Xm0TfvcmmspvQg1s4+USL3x8D+YPAfX2JHygvxAnCJ0EHun8cm2zvfNBcsTlnwYb0ybFWXXY129aq1wgFC9TpQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.5.7.tgz", + "integrity": "sha512-tp43WfJLCsKLQKBmjmY/0vv1slVywR5Q4qKjF5OIY8QijaEW7/8VwPyUyVoJZEnDgv9jKtUTG5PzqtIYPZGnyg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true + }, + "node_modules/@swc/helpers": { + "version": "0.5.11", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.11.tgz", + "integrity": "sha512-YNlnKRWF2sVojTpIyzwou9XoTNbzbzONwRhOoniEioF1AtaitTvVZblaQRrAzChWQ1bLYyYSWzM18y4WwgzJ+A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@swc/types": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.7.tgz", + "integrity": "sha512-scHWahbHF0eyj3JsxG9CFJgFdFNaVQCNAimBlT6PzS3n/HptxqREjsm4OH6AN3lYcffZYSPxXW8ua2BEHp0lJQ==", + "dev": true, + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@trivago/prettier-plugin-sort-imports": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-5.2.2.tgz", + "integrity": "sha512-fYDQA9e6yTNmA13TLVSA+WMQRc5Bn/c0EUBditUHNfMMxN7M82c38b1kEggVE3pLpZ0FwkwJkUEKMiOi52JXFA==", + "dev": true, + "dependencies": { + "@babel/generator": "^7.26.5", + "@babel/parser": "^7.26.7", + "@babel/traverse": "^7.26.7", + "@babel/types": "^7.26.7", + "javascript-natural-sort": "^0.7.1", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">18.12" + }, + "peerDependencies": { + "@vue/compiler-sfc": "3.x", + "prettier": "2.x - 3.x", + "prettier-plugin-svelte": "3.x", + "svelte": "4.x || 5.x" + }, + "peerDependenciesMeta": { + "@vue/compiler-sfc": { + "optional": true + }, + "prettier-plugin-svelte": { + "optional": true + }, + "svelte": { + "optional": true + } + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.12", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", + "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", + "dev": true, + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/jest/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@types/jest/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@types/jest/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.14.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.8.tgz", + "integrity": "sha512-DO+2/jZinXfROG7j7WKFn/3C6nFwxy2lLpgLjEXJz+0XKphZlTLJ14mo8Vfg8X5BWN6XjyESXq+LcYdT7tR3bA==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.12", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==" + }, + "node_modules/@types/react": { + "version": "18.3.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", + "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", + "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-transition-group": { + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", + "integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" + }, + "node_modules/@types/warning": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz", + "integrity": "sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==" + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.10.0.tgz", + "integrity": "sha512-PzCr+a/KAef5ZawX7nbyNwBDtM1HdLIT53aSA2DDlxmxMngZ43O8SIePOeX8H5S+FHXeI6t97mTt/dDdzY4Fyw==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.10.0", + "@typescript-eslint/type-utils": "7.10.0", + "@typescript-eslint/utils": "7.10.0", + "@typescript-eslint/visitor-keys": "7.10.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.10.0.tgz", + "integrity": "sha512-2EjZMA0LUW5V5tGQiaa2Gys+nKdfrn2xiTIBLR4fxmPmVSvgPcKNW+AE/ln9k0A4zDUti0J/GZXMDupQoI+e1w==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "7.10.0", + "@typescript-eslint/types": "7.10.0", + "@typescript-eslint/typescript-estree": "7.10.0", + "@typescript-eslint/visitor-keys": "7.10.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.10.0.tgz", + "integrity": "sha512-7L01/K8W/VGl7noe2mgH0K7BE29Sq6KAbVmxurj8GGaPDZXPr8EEQ2seOeAS+mEV9DnzxBQB6ax6qQQ5C6P4xg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.10.0", + "@typescript-eslint/visitor-keys": "7.10.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.10.0.tgz", + "integrity": "sha512-D7tS4WDkJWrVkuzgm90qYw9RdgBcrWmbbRkrLA4d7Pg3w0ttVGDsvYGV19SH8gPR5L7OtcN5J1hTtyenO9xE9g==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "7.10.0", + "@typescript-eslint/utils": "7.10.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.10.0.tgz", + "integrity": "sha512-7fNj+Ya35aNyhuqrA1E/VayQX9Elwr8NKZ4WueClR3KwJ7Xx9jcCdOrLW04h51de/+gNbyFMs+IDxh5xIwfbNg==", + "dev": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.10.0.tgz", + "integrity": "sha512-LXFnQJjL9XIcxeVfqmNj60YhatpRLt6UhdlFwAkjNc6jSUlK8zQOl1oktAP8PlWFzPQC1jny/8Bai3/HPuvN5g==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.10.0", + "@typescript-eslint/visitor-keys": "7.10.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.10.0.tgz", + "integrity": "sha512-olzif1Fuo8R8m/qKkzJqT7qwy16CzPRWBvERS0uvyc+DHd8AKbO4Jb7kpAvVzMmZm8TrHnI7hvjN4I05zow+tg==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.10.0", + "@typescript-eslint/types": "7.10.0", + "@typescript-eslint/typescript-estree": "7.10.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.10.0.tgz", + "integrity": "sha512-9ntIVgsi6gg6FIq9xjEO4VQJvwOqA3jaBFQJ/6TK5AvEup2+cECI6Fh7QiBxmfMHXU0V0J4RyPeOU1VDNzl9cg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.10.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/@vitejs/plugin-react-swc": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.7.0.tgz", + "integrity": "sha512-yrknSb3Dci6svCd/qhHqhFPDSw0QtjumcqdKMoNNzmOl5lMXTTiqzjWtG4Qask2HdvvzaNgSunbQGet8/GrKdA==", + "dev": true, + "dependencies": { + "@swc/core": "^1.5.7" + }, + "peerDependencies": { + "vite": "^4 || ^5" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.3.tgz", + "integrity": "sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.7.8", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.8.tgz", + "integrity": "sha512-Uu0wb7KNqK2t5K+YQyVCLM76prD5sRFjKHbJYCP1J7JFGEQ6nN7HWn9+04LAeiJ3ji54lgS/gZCH1oxyrf1SPw==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "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/bootstrap": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz", + "integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "peerDependencies": { + "@popperjs/core": "^2.11.8" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.23.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", + "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001646", + "electron-to-chromium": "^1.5.4", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001651", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz", + "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz", + "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==", + "dev": true + }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/copy-anything": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", + "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==", + "dev": true, + "dependencies": { + "is-what": "^3.14.1" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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, + "optional": true, + "peer": true + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/d3-timer": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.10.tgz", + "integrity": "sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==" + }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==" + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "dev": true, + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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, + "optional": true, + "peer": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.12", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.12.tgz", + "integrity": "sha512-tIhPkdlEoCL1Y+PToq3zRNehUaKp3wBX/sr7aclAWdIWjvqAe/Im/H0SiCM4c1Q8BLPHCdoJTol+ZblflydehA==", + "dev": true + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "optional": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.7.tgz", + "integrity": "sha512-yrj+KInFmwuQS2UQcg1SF83ha1tuHC1jMQbRNyuWtlEzzKRDgAl7L4Yp4NlDUZTZNlWvHEzOtJhMi40R7JxcSw==", + "dev": true, + "peerDependencies": { + "eslint": ">=7" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/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/eslint/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/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/eventsource": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", + "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fetch-cookie": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/fetch-cookie/-/fetch-cookie-2.2.0.tgz", + "integrity": "sha512-h9AgfjURuCgA2+2ISl8GbavpUdR+WGAM2McW/ovn4tVccegp8ZqCKWSBR8uRdM8dDNlx5WdKRWxBYUwteLDCNQ==", + "dependencies": { + "set-cookie-parser": "^2.4.8", + "tough-cookie": "^4.0.0" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/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/glob/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/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", + "dev": true, + "optional": true, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/immer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", + "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "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/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-core-module": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz", + "integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-what": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", + "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jake/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/jake/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/javascript-natural-sort": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", + "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==", + "dev": true + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-config/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-config/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-leak-detector/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-leak-detector/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-leak-detector/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-validate/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kapellmeister": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/kapellmeister/-/kapellmeister-3.0.1.tgz", + "integrity": "sha512-S7+gYcziMREv8RxG46138mb1O4Xf9II/bCxEJPYkhlZ7PgGWTlicgsyNad/DGc5oEAlWGLXE5ExLbTDVvJmgDA==", + "dependencies": { + "d3-timer": "^1.0.9" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/less": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/less/-/less-4.2.0.tgz", + "integrity": "sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA==", + "dev": true, + "dependencies": { + "copy-anything": "^2.0.1", + "parse-node-version": "^1.0.1", + "tslib": "^2.3.0" + }, + "bin": { + "lessc": "bin/lessc" + }, + "engines": { + "node": ">=6" + }, + "optionalDependencies": { + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "make-dir": "^2.1.0", + "mime": "^1.4.1", + "needle": "^3.1.0", + "source-map": "~0.6.0" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "optional": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, + "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/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/needle": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz", + "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==", + "dev": true, + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.3", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/postcss": { + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types-extra": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz", + "integrity": "sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==", + "dependencies": { + "react-is": "^16.3.2", + "warning": "^4.0.0" + }, + "peerDependencies": { + "react": ">=0.14.0" + } + }, + "node_modules/proxy-compare": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-3.0.1.tgz", + "integrity": "sha512-V9plBAt3qjMlS1+nC8771KNf6oJ12gExvaxnNzN/9yVRLdTv/lc+oJlnSzrdYDAvBfTStPCoiaCOTmTs0adv7Q==" + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/proxy-memoize": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/proxy-memoize/-/proxy-memoize-3.0.1.tgz", + "integrity": "sha512-VDdG/VYtOgdGkWJx7y0o7p+zArSf2383Isci8C+BP3YXgMYDoPd3cCBjw0JdWb6YBb9sFiOPbAADDVTPJnh+9g==", + "dependencies": { + "proxy-compare": "^3.0.0" + } + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true, + "optional": true + }, + "node_modules/psl": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.14.0.tgz", + "integrity": "sha512-Syk1bnf6fRZ9wQs03AtKJHcM12cKbOLo9L8JtCCdYj5/DTsHmTyXM4BK5ouWeG2P6kZ4nmFvuNTdtaqfobCOCg==", + "dependencies": { + "punycode": "^2.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-bootstrap": { + "version": "2.10.2", + "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.10.2.tgz", + "integrity": "sha512-UvB7mRqQjivdZNxJNEA2yOQRB7L9N43nBnKc33K47+cH90/ujmnMwatTCwQLu83gLhrzAl8fsa6Lqig/KLghaA==", + "dependencies": { + "@babel/runtime": "^7.22.5", + "@restart/hooks": "^0.4.9", + "@restart/ui": "^1.6.8", + "@types/react-transition-group": "^4.4.6", + "classnames": "^2.3.2", + "dom-helpers": "^5.2.1", + "invariant": "^2.2.4", + "prop-types": "^15.8.1", + "prop-types-extra": "^1.1.0", + "react-transition-group": "^4.4.5", + "uncontrollable": "^7.2.1", + "warning": "^4.0.3" + }, + "peerDependencies": { + "@types/react": ">=16.14.8", + "react": ">=16.14.0", + "react-dom": ">=16.14.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-icons": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.3.0.tgz", + "integrity": "sha512-DnUk8aFbTyQPSkCfF8dbX6kQjXA9DktMeJqfjrg6cK9vwQVMxmcA3BfP4QoiztVmEHtwlTgLFsPuH2NskKT6eg==", + "peerDependencies": { + "react": "*" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, + "node_modules/react-move": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/react-move/-/react-move-6.5.0.tgz", + "integrity": "sha512-tl8zwCqtXXWfmrUJGnkyPMNhx8DUTy1NugEuPW/JTMp2TGSEC819aMXGYMG8FWFzV9I6jy4kbgoZJnBpmZRktA==", + "dependencies": { + "@babel/runtime": "^7.14.5", + "kapellmeister": "^3.0.1", + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "react": ">=16.3.0" + } + }, + "node_modules/react-redux": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.2.tgz", + "integrity": "sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w==", + "dependencies": { + "@types/use-sync-external-store": "^0.0.3", + "use-sync-external-store": "^1.0.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25", + "react": "^18.0", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-router": { + "version": "6.25.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.25.1.tgz", + "integrity": "sha512-u8ELFr5Z6g02nUtpPAggP73Jigj1mRePSwhS/2nkTrlPU5yEkH1vYzWNyvSnSzeeE2DNqWdH+P8OhIh9wuXhTw==", + "dependencies": { + "@remix-run/router": "1.18.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.25.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.25.1.tgz", + "integrity": "sha512-0tUDpbFvk35iv+N89dWNrJp+afLgd+y4VtorJZuOCXK0kkCWjEvb3vTJM++SYvMEpbVwXKf3FjeVveVEb6JpDQ==", + "dependencies": { + "@remix-run/router": "1.18.0", + "react-router": "6.25.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/react-tooltip": { + "version": "5.28.0", + "resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-5.28.0.tgz", + "integrity": "sha512-R5cO3JPPXk6FRbBHMO0rI9nkUG/JKfalBSQfZedZYzmqaZQgq7GLzF8vcCWx6IhUCKg0yPqJhXIzmIO5ff15xg==", + "dependencies": { + "@floating-ui/dom": "^1.6.1", + "classnames": "^2.3.0" + }, + "peerDependencies": { + "react": ">=16.14.0", + "react-dom": ">=16.14.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" + }, + "node_modules/redux-saga": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-1.3.0.tgz", + "integrity": "sha512-J9RvCeAZXSTAibFY0kGw6Iy4EdyDNW7k6Q+liwX+bsck7QVsU78zz8vpBRweEfANxnnlG/xGGeOvf6r8UXzNJQ==", + "dependencies": { + "@redux-saga/core": "^1.3.0" + } + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==" + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.28.0.tgz", + "integrity": "sha512-G9GOrmgWHBma4YfCcX8PjH0qhXSdH8B4HDE2o4/jaxj93S4DPCIDoLcXz99eWMji4hB29UFCEd7B2gwGJDR9cQ==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.28.0", + "@rollup/rollup-android-arm64": "4.28.0", + "@rollup/rollup-darwin-arm64": "4.28.0", + "@rollup/rollup-darwin-x64": "4.28.0", + "@rollup/rollup-freebsd-arm64": "4.28.0", + "@rollup/rollup-freebsd-x64": "4.28.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.28.0", + "@rollup/rollup-linux-arm-musleabihf": "4.28.0", + "@rollup/rollup-linux-arm64-gnu": "4.28.0", + "@rollup/rollup-linux-arm64-musl": "4.28.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.28.0", + "@rollup/rollup-linux-riscv64-gnu": "4.28.0", + "@rollup/rollup-linux-s390x-gnu": "4.28.0", + "@rollup/rollup-linux-x64-gnu": "4.28.0", + "@rollup/rollup-linux-x64-musl": "4.28.0", + "@rollup/rollup-win32-arm64-msvc": "4.28.0", + "@rollup/rollup-win32-ia32-msvc": "4.28.0", + "@rollup/rollup-win32-x64-msvc": "4.28.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "optional": true + }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "dev": true, + "optional": true + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/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/test-exclude/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/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-jest": { + "version": "29.2.4", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.4.tgz", + "integrity": "sha512-3d6tgDyhCI29HlpwIq87sNuI+3Q6GLTTCeYRHCs7vDz+/3GCMwEtV9jezLyl4ZtnBgx00I7hm8PCP8cTksMGrw==", + "dev": true, + "dependencies": { + "bs-logger": "0.x", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "2.x", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "4.x", + "make-error": "1.x", + "semver": "^7.5.3", + "yargs-parser": "^21.0.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.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.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.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/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-compare": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/typescript-compare/-/typescript-compare-0.0.2.tgz", + "integrity": "sha512-8ja4j7pMHkfLJQO2/8tut7ub+J3Lw2S3061eJLFQcvs3tsmJKp8KG5NtpLn7KcY2w08edF74BSVN7qJS0U6oHA==", + "dependencies": { + "typescript-logic": "^0.0.0" + } + }, + "node_modules/typescript-logic": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/typescript-logic/-/typescript-logic-0.0.0.tgz", + "integrity": "sha512-zXFars5LUkI3zP492ls0VskH3TtdeHCqu0i7/duGt60i5IGPIpAHE/DWo5FqJ6EjQ15YKXrt+AETjv60Dat34Q==" + }, + "node_modules/typescript-tuple": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/typescript-tuple/-/typescript-tuple-2.2.1.tgz", + "integrity": "sha512-Zcr0lbt8z5ZdEzERHAMAniTiIKerFCMgd7yjq1fPnDJ43et/k9twIFQMUYff9k5oXcsQ0WpvFcgzK2ZKASoW6Q==", + "dependencies": { + "typescript-compare": "^0.0.2" + } + }, + "node_modules/uncontrollable": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz", + "integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==", + "dependencies": { + "@babel/runtime": "^7.6.3", + "@types/react": ">=16.9.11", + "invariant": "^2.2.4", + "react-lifecycles-compat": "^3.0.4" + }, + "peerDependencies": { + "react": ">=15.0.0" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", + "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/vite": { + "version": "5.4.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", + "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/Front/package.json b/Front/package.json new file mode 100644 index 00000000..62f1835f --- /dev/null +++ b/Front/package.json @@ -0,0 +1,51 @@ +{ + "name": "jackal-front", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "start": "vite", + "build-dev": "tsc && vite build --mode=development", + "build": "tsc && vite build", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview", + "test": "jest" + }, + "dependencies": { + "@microsoft/signalr": "^8.0.7", + "@reduxjs/toolkit": "^2.2.5", + "axios": "^1.7.2", + "bootstrap": "^5.3.3", + "classnames": "^2.5.1", + "dayjs": "^1.11.13", + "proxy-memoize": "^3.0.1", + "react": "^18.2.0", + "react-bootstrap": "^2.10.2", + "react-dom": "^18.2.0", + "react-icons": "^5.3.0", + "react-move": "^6.5.0", + "react-redux": "^9.1.2", + "react-router-dom": "^6.25.1", + "react-tooltip": "^5.28.0", + "redux-saga": "^1.3.0" + }, + "devDependencies": { + "@trivago/prettier-plugin-sort-imports": "^5.2.2", + "@types/jest": "^29.5.12", + "@types/node": "^20.14.8", + "@types/react": "^18.2.66", + "@types/react-dom": "^18.2.22", + "@typescript-eslint/eslint-plugin": "^7.2.0", + "@typescript-eslint/parser": "^7.2.0", + "@vitejs/plugin-react-swc": "^3.5.0", + "eslint": "^8.57.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.6", + "jest": "^29.7.0", + "less": "^4.2.0", + "prettier": "^3.3.3", + "ts-jest": "^29.2.4", + "typescript": "^5.2.2", + "vite": "^5.2.0" + } +} diff --git a/Front/public/fields/airplane.png b/Front/public/fields/airplane.png new file mode 100644 index 00000000..d9c8fe87 Binary files /dev/null and b/Front/public/fields/airplane.png differ diff --git a/Front/public/fields/arrow1.png b/Front/public/fields/arrow1.png new file mode 100644 index 00000000..3ebbf9f2 Binary files /dev/null and b/Front/public/fields/arrow1.png differ diff --git a/Front/public/fields/arrow2.png b/Front/public/fields/arrow2.png new file mode 100644 index 00000000..41c386a6 Binary files /dev/null and b/Front/public/fields/arrow2.png differ diff --git a/Front/public/fields/arrow3.png b/Front/public/fields/arrow3.png new file mode 100644 index 00000000..0c62272c Binary files /dev/null and b/Front/public/fields/arrow3.png differ diff --git a/Front/public/fields/arrow4.png b/Front/public/fields/arrow4.png new file mode 100644 index 00000000..7d6b2c85 Binary files /dev/null and b/Front/public/fields/arrow4.png differ diff --git a/Front/public/fields/arrow5.png b/Front/public/fields/arrow5.png new file mode 100644 index 00000000..47cbf58b Binary files /dev/null and b/Front/public/fields/arrow5.png differ diff --git a/Front/public/fields/arrow6.png b/Front/public/fields/arrow6.png new file mode 100644 index 00000000..5bd59970 Binary files /dev/null and b/Front/public/fields/arrow6.png differ diff --git a/Front/public/fields/arrow7.png b/Front/public/fields/arrow7.png new file mode 100644 index 00000000..45c68f2a Binary files /dev/null and b/Front/public/fields/arrow7.png differ diff --git a/Front/public/fields/back.png b/Front/public/fields/back.png new file mode 100644 index 00000000..e75ef783 Binary files /dev/null and b/Front/public/fields/back.png differ diff --git a/Front/public/fields/balloon.png b/Front/public/fields/balloon.png new file mode 100644 index 00000000..52dd697d Binary files /dev/null and b/Front/public/fields/balloon.png differ diff --git a/Front/public/fields/bengunn.png b/Front/public/fields/bengunn.png new file mode 100644 index 00000000..d0021625 Binary files /dev/null and b/Front/public/fields/bengunn.png differ diff --git a/Front/public/fields/canibal.png b/Front/public/fields/canibal.png new file mode 100644 index 00000000..bffa907f Binary files /dev/null and b/Front/public/fields/canibal.png differ diff --git a/Front/public/fields/cannabis.png b/Front/public/fields/cannabis.png new file mode 100644 index 00000000..fbcbcde1 Binary files /dev/null and b/Front/public/fields/cannabis.png differ diff --git a/Front/public/fields/cannon.png b/Front/public/fields/cannon.png new file mode 100644 index 00000000..0457c485 Binary files /dev/null and b/Front/public/fields/cannon.png differ diff --git a/Front/public/fields/caramba.png b/Front/public/fields/caramba.png new file mode 100644 index 00000000..6c016672 Binary files /dev/null and b/Front/public/fields/caramba.png differ diff --git a/Front/public/fields/chest.png b/Front/public/fields/chest.png new file mode 100644 index 00000000..56f0e4f2 Binary files /dev/null and b/Front/public/fields/chest.png differ diff --git a/Front/public/fields/croc.png b/Front/public/fields/croc.png new file mode 100644 index 00000000..f26faec4 Binary files /dev/null and b/Front/public/fields/croc.png differ diff --git a/Front/public/fields/desert.png b/Front/public/fields/desert.png new file mode 100644 index 00000000..6d7bd379 Binary files /dev/null and b/Front/public/fields/desert.png differ diff --git a/Front/public/fields/empty1.png b/Front/public/fields/empty1.png new file mode 100644 index 00000000..0306de5f Binary files /dev/null and b/Front/public/fields/empty1.png differ diff --git a/Front/public/fields/empty2.png b/Front/public/fields/empty2.png new file mode 100644 index 00000000..fabb4de7 Binary files /dev/null and b/Front/public/fields/empty2.png differ diff --git a/Front/public/fields/empty3.png b/Front/public/fields/empty3.png new file mode 100644 index 00000000..19155942 Binary files /dev/null and b/Front/public/fields/empty3.png differ diff --git a/Front/public/fields/empty4.png b/Front/public/fields/empty4.png new file mode 100644 index 00000000..5b84043f Binary files /dev/null and b/Front/public/fields/empty4.png differ diff --git a/Front/public/fields/forest.png b/Front/public/fields/forest.png new file mode 100644 index 00000000..fddf463a Binary files /dev/null and b/Front/public/fields/forest.png differ diff --git a/Front/public/fields/fort.png b/Front/public/fields/fort.png new file mode 100644 index 00000000..fc387126 Binary files /dev/null and b/Front/public/fields/fort.png differ diff --git a/Front/public/fields/hole.png b/Front/public/fields/hole.png new file mode 100644 index 00000000..065df72b Binary files /dev/null and b/Front/public/fields/hole.png differ diff --git a/Front/public/fields/horse.png b/Front/public/fields/horse.png new file mode 100644 index 00000000..83d0b91a Binary files /dev/null and b/Front/public/fields/horse.png differ diff --git a/Front/public/fields/ice.png b/Front/public/fields/ice.png new file mode 100644 index 00000000..424e328e Binary files /dev/null and b/Front/public/fields/ice.png differ diff --git a/Front/public/fields/jungle.png b/Front/public/fields/jungle.png new file mode 100644 index 00000000..ce422710 Binary files /dev/null and b/Front/public/fields/jungle.png differ diff --git a/Front/public/fields/lighthouse.png b/Front/public/fields/lighthouse.png new file mode 100644 index 00000000..6e61640e Binary files /dev/null and b/Front/public/fields/lighthouse.png differ diff --git a/Front/public/fields/missioner.png b/Front/public/fields/missioner.png new file mode 100644 index 00000000..4cbeba6d Binary files /dev/null and b/Front/public/fields/missioner.png differ diff --git a/Front/public/fields/mount.png b/Front/public/fields/mount.png new file mode 100644 index 00000000..05ed0b07 Binary files /dev/null and b/Front/public/fields/mount.png differ diff --git a/Front/public/fields/native.png b/Front/public/fields/native.png new file mode 100644 index 00000000..b037a89c Binary files /dev/null and b/Front/public/fields/native.png differ diff --git a/Front/public/fields/quake.png b/Front/public/fields/quake.png new file mode 100644 index 00000000..185df8c6 Binary files /dev/null and b/Front/public/fields/quake.png differ diff --git a/Front/public/fields/respawn.png b/Front/public/fields/respawn.png new file mode 100644 index 00000000..ac119b05 Binary files /dev/null and b/Front/public/fields/respawn.png differ diff --git a/Front/public/fields/rum1.png b/Front/public/fields/rum1.png new file mode 100644 index 00000000..89384324 Binary files /dev/null and b/Front/public/fields/rum1.png differ diff --git a/Front/public/fields/rum2.png b/Front/public/fields/rum2.png new file mode 100644 index 00000000..b3dcb4a3 Binary files /dev/null and b/Front/public/fields/rum2.png differ diff --git a/Front/public/fields/rum3.png b/Front/public/fields/rum3.png new file mode 100644 index 00000000..f385f397 Binary files /dev/null and b/Front/public/fields/rum3.png differ diff --git a/Front/public/fields/rumbar.png b/Front/public/fields/rumbar.png new file mode 100644 index 00000000..dbabc681 Binary files /dev/null and b/Front/public/fields/rumbar.png differ diff --git a/Front/public/fields/ship_1.png b/Front/public/fields/ship_1.png new file mode 100644 index 00000000..a4d14958 Binary files /dev/null and b/Front/public/fields/ship_1.png differ diff --git a/Front/public/fields/ship_2.png b/Front/public/fields/ship_2.png new file mode 100644 index 00000000..a8884a6a Binary files /dev/null and b/Front/public/fields/ship_2.png differ diff --git a/Front/public/fields/ship_3.png b/Front/public/fields/ship_3.png new file mode 100644 index 00000000..b38570ad Binary files /dev/null and b/Front/public/fields/ship_3.png differ diff --git a/Front/public/fields/ship_4.png b/Front/public/fields/ship_4.png new file mode 100644 index 00000000..4d0691b3 Binary files /dev/null and b/Front/public/fields/ship_4.png differ diff --git a/Front/public/fields/swamp.png b/Front/public/fields/swamp.png new file mode 100644 index 00000000..630d0f72 Binary files /dev/null and b/Front/public/fields/swamp.png differ diff --git a/Front/public/fields/trap.png b/Front/public/fields/trap.png new file mode 100644 index 00000000..34b5f731 Binary files /dev/null and b/Front/public/fields/trap.png differ diff --git a/Front/public/fields/used_airplane.png b/Front/public/fields/used_airplane.png new file mode 100644 index 00000000..d2d57903 Binary files /dev/null and b/Front/public/fields/used_airplane.png differ diff --git a/Front/public/fields/used_bengunn.png b/Front/public/fields/used_bengunn.png new file mode 100644 index 00000000..cd9f3dba Binary files /dev/null and b/Front/public/fields/used_bengunn.png differ diff --git a/Front/public/fields/used_rum1.png b/Front/public/fields/used_rum1.png new file mode 100644 index 00000000..d7f740c9 Binary files /dev/null and b/Front/public/fields/used_rum1.png differ diff --git a/Front/public/fields/used_rum2.png b/Front/public/fields/used_rum2.png new file mode 100644 index 00000000..3de34538 Binary files /dev/null and b/Front/public/fields/used_rum2.png differ diff --git a/Front/public/fields/used_rum3.png b/Front/public/fields/used_rum3.png new file mode 100644 index 00000000..082024b3 Binary files /dev/null and b/Front/public/fields/used_rum3.png differ diff --git a/Front/public/fields/water.png b/Front/public/fields/water.png new file mode 100644 index 00000000..0e63c9a6 Binary files /dev/null and b/Front/public/fields/water.png differ diff --git a/Front/public/pictures/add-pirate.png b/Front/public/pictures/add-pirate.png new file mode 100644 index 00000000..8c8983f8 Binary files /dev/null and b/Front/public/pictures/add-pirate.png differ diff --git a/Front/public/pictures/anime/logo.png b/Front/public/pictures/anime/logo.png new file mode 100644 index 00000000..701f07a8 Binary files /dev/null and b/Front/public/pictures/anime/logo.png differ diff --git a/Front/public/pictures/anime/pirate_1.jpg b/Front/public/pictures/anime/pirate_1.jpg new file mode 100644 index 00000000..daf243b3 Binary files /dev/null and b/Front/public/pictures/anime/pirate_1.jpg differ diff --git a/Front/public/pictures/anime/pirate_2.jpg b/Front/public/pictures/anime/pirate_2.jpg new file mode 100644 index 00000000..3b9fd89a Binary files /dev/null and b/Front/public/pictures/anime/pirate_2.jpg differ diff --git a/Front/public/pictures/anime/pirate_3.jpg b/Front/public/pictures/anime/pirate_3.jpg new file mode 100644 index 00000000..cfe11b6d Binary files /dev/null and b/Front/public/pictures/anime/pirate_3.jpg differ diff --git a/Front/public/pictures/anime/pirate_4.jpg b/Front/public/pictures/anime/pirate_4.jpg new file mode 100644 index 00000000..3fdc7d48 Binary files /dev/null and b/Front/public/pictures/anime/pirate_4.jpg differ diff --git a/Front/public/pictures/anime/pirate_5.jpg b/Front/public/pictures/anime/pirate_5.jpg new file mode 100644 index 00000000..e2b6ae61 Binary files /dev/null and b/Front/public/pictures/anime/pirate_5.jpg differ diff --git a/Front/public/pictures/army/logo.png b/Front/public/pictures/army/logo.png new file mode 100644 index 00000000..91784c6a Binary files /dev/null and b/Front/public/pictures/army/logo.png differ diff --git a/Front/public/pictures/army/pirate_1.jpg b/Front/public/pictures/army/pirate_1.jpg new file mode 100644 index 00000000..eb1822e8 Binary files /dev/null and b/Front/public/pictures/army/pirate_1.jpg differ diff --git a/Front/public/pictures/army/pirate_2.jpg b/Front/public/pictures/army/pirate_2.jpg new file mode 100644 index 00000000..1b7163fa Binary files /dev/null and b/Front/public/pictures/army/pirate_2.jpg differ diff --git a/Front/public/pictures/army/pirate_3.jpg b/Front/public/pictures/army/pirate_3.jpg new file mode 100644 index 00000000..79b73fe5 Binary files /dev/null and b/Front/public/pictures/army/pirate_3.jpg differ diff --git a/Front/public/pictures/army/pirate_4.jpg b/Front/public/pictures/army/pirate_4.jpg new file mode 100644 index 00000000..66e51d8b Binary files /dev/null and b/Front/public/pictures/army/pirate_4.jpg differ diff --git a/Front/public/pictures/caribian/logo.png b/Front/public/pictures/caribian/logo.png new file mode 100644 index 00000000..5b4a79ad Binary files /dev/null and b/Front/public/pictures/caribian/logo.png differ diff --git a/Front/public/pictures/caribian/pirate_1.png b/Front/public/pictures/caribian/pirate_1.png new file mode 100644 index 00000000..c9cc83ca Binary files /dev/null and b/Front/public/pictures/caribian/pirate_1.png differ diff --git a/Front/public/pictures/caribian/pirate_10.png b/Front/public/pictures/caribian/pirate_10.png new file mode 100644 index 00000000..7bab61ed Binary files /dev/null and b/Front/public/pictures/caribian/pirate_10.png differ diff --git a/Front/public/pictures/caribian/pirate_11.png b/Front/public/pictures/caribian/pirate_11.png new file mode 100644 index 00000000..afb058aa Binary files /dev/null and b/Front/public/pictures/caribian/pirate_11.png differ diff --git a/Front/public/pictures/caribian/pirate_2.png b/Front/public/pictures/caribian/pirate_2.png new file mode 100644 index 00000000..b87089e2 Binary files /dev/null and b/Front/public/pictures/caribian/pirate_2.png differ diff --git a/Front/public/pictures/caribian/pirate_3.png b/Front/public/pictures/caribian/pirate_3.png new file mode 100644 index 00000000..eadf1ae4 Binary files /dev/null and b/Front/public/pictures/caribian/pirate_3.png differ diff --git a/Front/public/pictures/caribian/pirate_4.png b/Front/public/pictures/caribian/pirate_4.png new file mode 100644 index 00000000..242d21a3 Binary files /dev/null and b/Front/public/pictures/caribian/pirate_4.png differ diff --git a/Front/public/pictures/caribian/pirate_5.png b/Front/public/pictures/caribian/pirate_5.png new file mode 100644 index 00000000..7c883518 Binary files /dev/null and b/Front/public/pictures/caribian/pirate_5.png differ diff --git a/Front/public/pictures/caribian/pirate_6.png b/Front/public/pictures/caribian/pirate_6.png new file mode 100644 index 00000000..dd797285 Binary files /dev/null and b/Front/public/pictures/caribian/pirate_6.png differ diff --git a/Front/public/pictures/caribian/pirate_7.png b/Front/public/pictures/caribian/pirate_7.png new file mode 100644 index 00000000..27265563 Binary files /dev/null and b/Front/public/pictures/caribian/pirate_7.png differ diff --git a/Front/public/pictures/caribian/pirate_8.png b/Front/public/pictures/caribian/pirate_8.png new file mode 100644 index 00000000..948402db Binary files /dev/null and b/Front/public/pictures/caribian/pirate_8.png differ diff --git a/Front/public/pictures/caribian/pirate_9.png b/Front/public/pictures/caribian/pirate_9.png new file mode 100644 index 00000000..beda4998 Binary files /dev/null and b/Front/public/pictures/caribian/pirate_9.png differ diff --git a/Front/public/pictures/clover/logo.png b/Front/public/pictures/clover/logo.png new file mode 100644 index 00000000..22fb2244 Binary files /dev/null and b/Front/public/pictures/clover/logo.png differ diff --git a/Front/public/pictures/clover/pirate_1.jpg b/Front/public/pictures/clover/pirate_1.jpg new file mode 100644 index 00000000..883625f8 Binary files /dev/null and b/Front/public/pictures/clover/pirate_1.jpg differ diff --git a/Front/public/pictures/clover/pirate_2.jpg b/Front/public/pictures/clover/pirate_2.jpg new file mode 100644 index 00000000..759151ab Binary files /dev/null and b/Front/public/pictures/clover/pirate_2.jpg differ diff --git a/Front/public/pictures/clover/pirate_3.jpg b/Front/public/pictures/clover/pirate_3.jpg new file mode 100644 index 00000000..1d0121a7 Binary files /dev/null and b/Front/public/pictures/clover/pirate_3.jpg differ diff --git a/Front/public/pictures/clover/pirate_4.jpg b/Front/public/pictures/clover/pirate_4.jpg new file mode 100644 index 00000000..8f67645b Binary files /dev/null and b/Front/public/pictures/clover/pirate_4.jpg differ diff --git a/Front/public/pictures/commonfridays/friday_1.png b/Front/public/pictures/commonfridays/friday_1.png new file mode 100644 index 00000000..3b70d1fb Binary files /dev/null and b/Front/public/pictures/commonfridays/friday_1.png differ diff --git a/Front/public/pictures/commonganns/gann_1.png b/Front/public/pictures/commonganns/gann_1.png new file mode 100644 index 00000000..ed075008 Binary files /dev/null and b/Front/public/pictures/commonganns/gann_1.png differ diff --git a/Front/public/pictures/commonganns/gann_2.png b/Front/public/pictures/commonganns/gann_2.png new file mode 100644 index 00000000..aee29738 Binary files /dev/null and b/Front/public/pictures/commonganns/gann_2.png differ diff --git a/Front/public/pictures/commonganns/gann_3.png b/Front/public/pictures/commonganns/gann_3.png new file mode 100644 index 00000000..5929e6ad Binary files /dev/null and b/Front/public/pictures/commonganns/gann_3.png differ diff --git a/Front/public/pictures/commonganns/gann_4.png b/Front/public/pictures/commonganns/gann_4.png new file mode 100644 index 00000000..2b03dc34 Binary files /dev/null and b/Front/public/pictures/commonganns/gann_4.png differ diff --git a/Front/public/pictures/cross-linear-icon.png b/Front/public/pictures/cross-linear-icon.png new file mode 100644 index 00000000..207eb8d8 Binary files /dev/null and b/Front/public/pictures/cross-linear-icon.png differ diff --git a/Front/public/pictures/girls/logo.png b/Front/public/pictures/girls/logo.png new file mode 100644 index 00000000..8b571e27 Binary files /dev/null and b/Front/public/pictures/girls/logo.png differ diff --git a/Front/public/pictures/girls/pirate_1.png b/Front/public/pictures/girls/pirate_1.png new file mode 100644 index 00000000..c2e77f50 Binary files /dev/null and b/Front/public/pictures/girls/pirate_1.png differ diff --git a/Front/public/pictures/girls/pirate_2.png b/Front/public/pictures/girls/pirate_2.png new file mode 100644 index 00000000..39cd6a12 Binary files /dev/null and b/Front/public/pictures/girls/pirate_2.png differ diff --git a/Front/public/pictures/girls/pirate_3.png b/Front/public/pictures/girls/pirate_3.png new file mode 100644 index 00000000..a5b6c2ce Binary files /dev/null and b/Front/public/pictures/girls/pirate_3.png differ diff --git a/Front/public/pictures/girls/pirate_4.png b/Front/public/pictures/girls/pirate_4.png new file mode 100644 index 00000000..71edd379 Binary files /dev/null and b/Front/public/pictures/girls/pirate_4.png differ diff --git a/Front/public/pictures/girls/pirate_5.png b/Front/public/pictures/girls/pirate_5.png new file mode 100644 index 00000000..727606dd Binary files /dev/null and b/Front/public/pictures/girls/pirate_5.png differ diff --git a/Front/public/pictures/girls/pirate_6.png b/Front/public/pictures/girls/pirate_6.png new file mode 100644 index 00000000..f19514be Binary files /dev/null and b/Front/public/pictures/girls/pirate_6.png differ diff --git a/Front/public/pictures/gold_ruble.png b/Front/public/pictures/gold_ruble.png new file mode 100644 index 00000000..61bc5a93 Binary files /dev/null and b/Front/public/pictures/gold_ruble.png differ diff --git a/Front/public/pictures/gold_ruble_empty.png b/Front/public/pictures/gold_ruble_empty.png new file mode 100644 index 00000000..521b1a21 Binary files /dev/null and b/Front/public/pictures/gold_ruble_empty.png differ diff --git a/Front/public/pictures/ground-hole.png b/Front/public/pictures/ground-hole.png new file mode 100644 index 00000000..cde4ac7e Binary files /dev/null and b/Front/public/pictures/ground-hole.png differ diff --git a/Front/public/pictures/hourglass.png b/Front/public/pictures/hourglass.png new file mode 100644 index 00000000..8e891d1b Binary files /dev/null and b/Front/public/pictures/hourglass.png differ diff --git a/Front/public/pictures/human.png b/Front/public/pictures/human.png new file mode 100644 index 00000000..1a341b6a Binary files /dev/null and b/Front/public/pictures/human.png differ diff --git a/Front/public/pictures/jump-into-water.png b/Front/public/pictures/jump-into-water.png new file mode 100644 index 00000000..8e43a9a2 Binary files /dev/null and b/Front/public/pictures/jump-into-water.png differ diff --git a/Front/public/pictures/orcs/logo.png b/Front/public/pictures/orcs/logo.png new file mode 100644 index 00000000..9d7c0173 Binary files /dev/null and b/Front/public/pictures/orcs/logo.png differ diff --git a/Front/public/pictures/orcs/pirate_1.jpg b/Front/public/pictures/orcs/pirate_1.jpg new file mode 100644 index 00000000..13cf663f Binary files /dev/null and b/Front/public/pictures/orcs/pirate_1.jpg differ diff --git a/Front/public/pictures/orcs/pirate_2.jpg b/Front/public/pictures/orcs/pirate_2.jpg new file mode 100644 index 00000000..f1ed0b87 Binary files /dev/null and b/Front/public/pictures/orcs/pirate_2.jpg differ diff --git a/Front/public/pictures/orcs/pirate_3.jpg b/Front/public/pictures/orcs/pirate_3.jpg new file mode 100644 index 00000000..3cd49e43 Binary files /dev/null and b/Front/public/pictures/orcs/pirate_3.jpg differ diff --git a/Front/public/pictures/orcs/pirate_4.jpg b/Front/public/pictures/orcs/pirate_4.jpg new file mode 100644 index 00000000..526fd8c1 Binary files /dev/null and b/Front/public/pictures/orcs/pirate_4.jpg differ diff --git a/Front/public/pictures/orcs/pirate_5.jpg b/Front/public/pictures/orcs/pirate_5.jpg new file mode 100644 index 00000000..35cea36c Binary files /dev/null and b/Front/public/pictures/orcs/pirate_5.jpg differ diff --git a/Front/public/pictures/orcs/pirate_6.jpg b/Front/public/pictures/orcs/pirate_6.jpg new file mode 100644 index 00000000..5e0b0d3c Binary files /dev/null and b/Front/public/pictures/orcs/pirate_6.jpg differ diff --git a/Front/public/pictures/redalert/logo.png b/Front/public/pictures/redalert/logo.png new file mode 100644 index 00000000..a465d7a8 Binary files /dev/null and b/Front/public/pictures/redalert/logo.png differ diff --git a/Front/public/pictures/redalert/pirate_1.jpg b/Front/public/pictures/redalert/pirate_1.jpg new file mode 100644 index 00000000..7eff5ff7 Binary files /dev/null and b/Front/public/pictures/redalert/pirate_1.jpg differ diff --git a/Front/public/pictures/redalert/pirate_2.jpg b/Front/public/pictures/redalert/pirate_2.jpg new file mode 100644 index 00000000..e6f67782 Binary files /dev/null and b/Front/public/pictures/redalert/pirate_2.jpg differ diff --git a/Front/public/pictures/redalert/pirate_3.jpg b/Front/public/pictures/redalert/pirate_3.jpg new file mode 100644 index 00000000..6ba2697f Binary files /dev/null and b/Front/public/pictures/redalert/pirate_3.jpg differ diff --git a/Front/public/pictures/redalert/pirate_4.jpg b/Front/public/pictures/redalert/pirate_4.jpg new file mode 100644 index 00000000..23bc6b02 Binary files /dev/null and b/Front/public/pictures/redalert/pirate_4.jpg differ diff --git a/Front/public/pictures/redalert/pirate_5.jpg b/Front/public/pictures/redalert/pirate_5.jpg new file mode 100644 index 00000000..354ad17d Binary files /dev/null and b/Front/public/pictures/redalert/pirate_5.jpg differ diff --git a/Front/public/pictures/robot.png b/Front/public/pictures/robot.png new file mode 100644 index 00000000..cf561eae Binary files /dev/null and b/Front/public/pictures/robot.png differ diff --git a/Front/public/pictures/robot2.png b/Front/public/pictures/robot2.png new file mode 100644 index 00000000..ae4e0cd4 Binary files /dev/null and b/Front/public/pictures/robot2.png differ diff --git a/Front/public/pictures/robot3.png b/Front/public/pictures/robot3.png new file mode 100644 index 00000000..828fb267 Binary files /dev/null and b/Front/public/pictures/robot3.png differ diff --git a/Front/public/pictures/ruble.png b/Front/public/pictures/ruble.png new file mode 100644 index 00000000..c6c31268 Binary files /dev/null and b/Front/public/pictures/ruble.png differ diff --git a/Front/public/pictures/ruble_empty.png b/Front/public/pictures/ruble_empty.png new file mode 100644 index 00000000..a66f4e98 Binary files /dev/null and b/Front/public/pictures/ruble_empty.png differ diff --git a/Front/public/pictures/rum-slim.png b/Front/public/pictures/rum-slim.png new file mode 100644 index 00000000..7ce3b65e Binary files /dev/null and b/Front/public/pictures/rum-slim.png differ diff --git a/Front/public/pictures/rum.png b/Front/public/pictures/rum.png new file mode 100644 index 00000000..8225dba6 Binary files /dev/null and b/Front/public/pictures/rum.png differ diff --git a/Front/public/pictures/skull.png b/Front/public/pictures/skull.png new file mode 100644 index 00000000..ece77001 Binary files /dev/null and b/Front/public/pictures/skull.png differ diff --git a/Front/public/pictures/skull_dark.png b/Front/public/pictures/skull_dark.png new file mode 100644 index 00000000..bc70b353 Binary files /dev/null and b/Front/public/pictures/skull_dark.png differ diff --git a/Front/public/pictures/skull_light.png b/Front/public/pictures/skull_light.png new file mode 100644 index 00000000..24ef6fff Binary files /dev/null and b/Front/public/pictures/skull_light.png differ diff --git a/Front/public/pictures/skulls/logo.png b/Front/public/pictures/skulls/logo.png new file mode 100644 index 00000000..79886c3c Binary files /dev/null and b/Front/public/pictures/skulls/logo.png differ diff --git a/Front/public/pictures/skulls/pirate_1.png b/Front/public/pictures/skulls/pirate_1.png new file mode 100644 index 00000000..224ee26b Binary files /dev/null and b/Front/public/pictures/skulls/pirate_1.png differ diff --git a/Front/public/pictures/skulls/pirate_2.png b/Front/public/pictures/skulls/pirate_2.png new file mode 100644 index 00000000..77bbde18 Binary files /dev/null and b/Front/public/pictures/skulls/pirate_2.png differ diff --git a/Front/public/pictures/skulls/pirate_3.png b/Front/public/pictures/skulls/pirate_3.png new file mode 100644 index 00000000..ed387047 Binary files /dev/null and b/Front/public/pictures/skulls/pirate_3.png differ diff --git a/Front/public/pictures/skulls/pirate_4.png b/Front/public/pictures/skulls/pirate_4.png new file mode 100644 index 00000000..5213c3ae Binary files /dev/null and b/Front/public/pictures/skulls/pirate_4.png differ diff --git a/Front/public/pictures/skulls/pirate_5.png b/Front/public/pictures/skulls/pirate_5.png new file mode 100644 index 00000000..7db4604b Binary files /dev/null and b/Front/public/pictures/skulls/pirate_5.png differ diff --git a/Front/public/pictures/somali/logo.png b/Front/public/pictures/somali/logo.png new file mode 100644 index 00000000..2c8783d2 Binary files /dev/null and b/Front/public/pictures/somali/logo.png differ diff --git a/Front/public/pictures/somali/pirate_1.png b/Front/public/pictures/somali/pirate_1.png new file mode 100644 index 00000000..fec5117a Binary files /dev/null and b/Front/public/pictures/somali/pirate_1.png differ diff --git a/Front/public/pictures/somali/pirate_2.png b/Front/public/pictures/somali/pirate_2.png new file mode 100644 index 00000000..30e77cce Binary files /dev/null and b/Front/public/pictures/somali/pirate_2.png differ diff --git a/Front/public/pictures/somali/pirate_3.png b/Front/public/pictures/somali/pirate_3.png new file mode 100644 index 00000000..b9f2e298 Binary files /dev/null and b/Front/public/pictures/somali/pirate_3.png differ diff --git a/Front/public/pictures/somali/pirate_4.png b/Front/public/pictures/somali/pirate_4.png new file mode 100644 index 00000000..95fbd75f Binary files /dev/null and b/Front/public/pictures/somali/pirate_4.png differ diff --git a/Front/public/pictures/somali/pirate_5.png b/Front/public/pictures/somali/pirate_5.png new file mode 100644 index 00000000..d399d06c Binary files /dev/null and b/Front/public/pictures/somali/pirate_5.png differ diff --git a/Front/public/pictures/somali/pirate_6.png b/Front/public/pictures/somali/pirate_6.png new file mode 100644 index 00000000..e6a02e73 Binary files /dev/null and b/Front/public/pictures/somali/pirate_6.png differ diff --git a/Front/public/pictures/somali/pirate_7.png b/Front/public/pictures/somali/pirate_7.png new file mode 100644 index 00000000..6293d072 Binary files /dev/null and b/Front/public/pictures/somali/pirate_7.png differ diff --git a/Front/public/pictures/somali/pirate_8.png b/Front/public/pictures/somali/pirate_8.png new file mode 100644 index 00000000..383dc088 Binary files /dev/null and b/Front/public/pictures/somali/pirate_8.png differ diff --git a/Front/public/pictures/treasure.png b/Front/public/pictures/treasure.png new file mode 100644 index 00000000..b07fe140 Binary files /dev/null and b/Front/public/pictures/treasure.png differ diff --git a/Front/public/ranks/Archer.webp b/Front/public/ranks/Archer.webp new file mode 100644 index 00000000..c96f6846 Binary files /dev/null and b/Front/public/ranks/Archer.webp differ diff --git a/Front/public/ranks/Archmage.webp b/Front/public/ranks/Archmage.webp new file mode 100644 index 00000000..e2c53340 Binary files /dev/null and b/Front/public/ranks/Archmage.webp differ diff --git a/Front/public/ranks/BattleDwarf.webp b/Front/public/ranks/BattleDwarf.webp new file mode 100644 index 00000000..745303e6 Binary files /dev/null and b/Front/public/ranks/BattleDwarf.webp differ diff --git a/Front/public/ranks/BlackDragon.webp b/Front/public/ranks/BlackDragon.webp new file mode 100644 index 00000000..026d31f4 Binary files /dev/null and b/Front/public/ranks/BlackDragon.webp differ diff --git a/Front/public/ranks/Boar.webp b/Front/public/ranks/Boar.webp new file mode 100644 index 00000000..e3a0deec Binary files /dev/null and b/Front/public/ranks/Boar.webp differ diff --git a/Front/public/ranks/BoneDragon.webp b/Front/public/ranks/BoneDragon.webp new file mode 100644 index 00000000..a86979ed Binary files /dev/null and b/Front/public/ranks/BoneDragon.webp differ diff --git a/Front/public/ranks/Cavalry.webp b/Front/public/ranks/Cavalry.webp new file mode 100644 index 00000000..5a82f9c2 Binary files /dev/null and b/Front/public/ranks/Cavalry.webp differ diff --git a/Front/public/ranks/Centaur.webp b/Front/public/ranks/Centaur.webp new file mode 100644 index 00000000..dcc48a8b Binary files /dev/null and b/Front/public/ranks/Centaur.webp differ diff --git a/Front/public/ranks/Champion.webp b/Front/public/ranks/Champion.webp new file mode 100644 index 00000000..863457eb Binary files /dev/null and b/Front/public/ranks/Champion.webp differ diff --git a/Front/public/ranks/Crusader.webp b/Front/public/ranks/Crusader.webp new file mode 100644 index 00000000..6dcbfa5c Binary files /dev/null and b/Front/public/ranks/Crusader.webp differ diff --git a/Front/public/ranks/Cyclops.webp b/Front/public/ranks/Cyclops.webp new file mode 100644 index 00000000..b55fa3d3 Binary files /dev/null and b/Front/public/ranks/Cyclops.webp differ diff --git a/Front/public/ranks/Druid.webp b/Front/public/ranks/Druid.webp new file mode 100644 index 00000000..0dfb0771 Binary files /dev/null and b/Front/public/ranks/Druid.webp differ diff --git a/Front/public/ranks/Dwarf.webp b/Front/public/ranks/Dwarf.webp new file mode 100644 index 00000000..6fad53f3 Binary files /dev/null and b/Front/public/ranks/Dwarf.webp differ diff --git a/Front/public/ranks/Elf.webp b/Front/public/ranks/Elf.webp new file mode 100644 index 00000000..fb55091d Binary files /dev/null and b/Front/public/ranks/Elf.webp differ diff --git a/Front/public/ranks/Gargoyle.webp b/Front/public/ranks/Gargoyle.webp new file mode 100644 index 00000000..52eb8ffe Binary files /dev/null and b/Front/public/ranks/Gargoyle.webp differ diff --git a/Front/public/ranks/Giant.webp b/Front/public/ranks/Giant.webp new file mode 100644 index 00000000..0f74581a Binary files /dev/null and b/Front/public/ranks/Giant.webp differ diff --git a/Front/public/ranks/Goblin.webp b/Front/public/ranks/Goblin.webp new file mode 100644 index 00000000..3685408b Binary files /dev/null and b/Front/public/ranks/Goblin.webp differ diff --git a/Front/public/ranks/GrandElf.webp b/Front/public/ranks/GrandElf.webp new file mode 100644 index 00000000..4555b2db Binary files /dev/null and b/Front/public/ranks/GrandElf.webp differ diff --git a/Front/public/ranks/GreaterDruid.webp b/Front/public/ranks/GreaterDruid.webp new file mode 100644 index 00000000..999a3223 Binary files /dev/null and b/Front/public/ranks/GreaterDruid.webp differ diff --git a/Front/public/ranks/GreenDragon.webp b/Front/public/ranks/GreenDragon.webp new file mode 100644 index 00000000..8865351d Binary files /dev/null and b/Front/public/ranks/GreenDragon.webp differ diff --git a/Front/public/ranks/Griffin.webp b/Front/public/ranks/Griffin.webp new file mode 100644 index 00000000..3e2aa572 Binary files /dev/null and b/Front/public/ranks/Griffin.webp differ diff --git a/Front/public/ranks/Halfling.webp b/Front/public/ranks/Halfling.webp new file mode 100644 index 00000000..8ea1a7c2 Binary files /dev/null and b/Front/public/ranks/Halfling.webp differ diff --git a/Front/public/ranks/Hydra.webp b/Front/public/ranks/Hydra.webp new file mode 100644 index 00000000..c2b933e2 Binary files /dev/null and b/Front/public/ranks/Hydra.webp differ diff --git a/Front/public/ranks/IronGolem.webp b/Front/public/ranks/IronGolem.webp new file mode 100644 index 00000000..fcba34e6 Binary files /dev/null and b/Front/public/ranks/IronGolem.webp differ diff --git a/Front/public/ranks/Lich.webp b/Front/public/ranks/Lich.webp new file mode 100644 index 00000000..914e6242 Binary files /dev/null and b/Front/public/ranks/Lich.webp differ diff --git a/Front/public/ranks/Mage.webp b/Front/public/ranks/Mage.webp new file mode 100644 index 00000000..72cd66a2 Binary files /dev/null and b/Front/public/ranks/Mage.webp differ diff --git a/Front/public/ranks/MasterSwordsman.webp b/Front/public/ranks/MasterSwordsman.webp new file mode 100644 index 00000000..a1e4c731 Binary files /dev/null and b/Front/public/ranks/MasterSwordsman.webp differ diff --git a/Front/public/ranks/Minotaur.webp b/Front/public/ranks/Minotaur.webp new file mode 100644 index 00000000..11ec6c1c Binary files /dev/null and b/Front/public/ranks/Minotaur.webp differ diff --git a/Front/public/ranks/MinotaurKing.webp b/Front/public/ranks/MinotaurKing.webp new file mode 100644 index 00000000..b5e69c9f Binary files /dev/null and b/Front/public/ranks/MinotaurKing.webp differ diff --git a/Front/public/ranks/Mummy.webp b/Front/public/ranks/Mummy.webp new file mode 100644 index 00000000..7cfb6cd1 Binary files /dev/null and b/Front/public/ranks/Mummy.webp differ diff --git a/Front/public/ranks/MutantZombie.webp b/Front/public/ranks/MutantZombie.webp new file mode 100644 index 00000000..fcaadd67 Binary files /dev/null and b/Front/public/ranks/MutantZombie.webp differ diff --git a/Front/public/ranks/Ogre.webp b/Front/public/ranks/Ogre.webp new file mode 100644 index 00000000..cf8308f1 Binary files /dev/null and b/Front/public/ranks/Ogre.webp differ diff --git a/Front/public/ranks/OgreLord.webp b/Front/public/ranks/OgreLord.webp new file mode 100644 index 00000000..00ba59d3 Binary files /dev/null and b/Front/public/ranks/OgreLord.webp differ diff --git a/Front/public/ranks/Orc.webp b/Front/public/ranks/Orc.webp new file mode 100644 index 00000000..019c0734 Binary files /dev/null and b/Front/public/ranks/Orc.webp differ diff --git a/Front/public/ranks/OrcChief.webp b/Front/public/ranks/OrcChief.webp new file mode 100644 index 00000000..72e18412 Binary files /dev/null and b/Front/public/ranks/OrcChief.webp differ diff --git a/Front/public/ranks/Paladin.webp b/Front/public/ranks/Paladin.webp new file mode 100644 index 00000000..ca792161 Binary files /dev/null and b/Front/public/ranks/Paladin.webp differ diff --git a/Front/public/ranks/Peasant.webp b/Front/public/ranks/Peasant.webp new file mode 100644 index 00000000..1b60ca17 Binary files /dev/null and b/Front/public/ranks/Peasant.webp differ diff --git a/Front/public/ranks/Phoenix.webp b/Front/public/ranks/Phoenix.webp new file mode 100644 index 00000000..b80bc480 Binary files /dev/null and b/Front/public/ranks/Phoenix.webp differ diff --git a/Front/public/ranks/Pikeman.webp b/Front/public/ranks/Pikeman.webp new file mode 100644 index 00000000..b365df68 Binary files /dev/null and b/Front/public/ranks/Pikeman.webp differ diff --git a/Front/public/ranks/PowerLich.webp b/Front/public/ranks/PowerLich.webp new file mode 100644 index 00000000..4b6c6d61 Binary files /dev/null and b/Front/public/ranks/PowerLich.webp differ diff --git a/Front/public/ranks/Ranger.webp b/Front/public/ranks/Ranger.webp new file mode 100644 index 00000000..f89b6b06 Binary files /dev/null and b/Front/public/ranks/Ranger.webp differ diff --git a/Front/public/ranks/RedDragon.webp b/Front/public/ranks/RedDragon.webp new file mode 100644 index 00000000..f6fe033e Binary files /dev/null and b/Front/public/ranks/RedDragon.webp differ diff --git a/Front/public/ranks/Roc.webp b/Front/public/ranks/Roc.webp new file mode 100644 index 00000000..415339b9 Binary files /dev/null and b/Front/public/ranks/Roc.webp differ diff --git a/Front/public/ranks/RoyalMummy.webp b/Front/public/ranks/RoyalMummy.webp new file mode 100644 index 00000000..4e732569 Binary files /dev/null and b/Front/public/ranks/RoyalMummy.webp differ diff --git a/Front/public/ranks/Skeleton.webp b/Front/public/ranks/Skeleton.webp new file mode 100644 index 00000000..45830a2b Binary files /dev/null and b/Front/public/ranks/Skeleton.webp differ diff --git a/Front/public/ranks/Sprite.webp b/Front/public/ranks/Sprite.webp new file mode 100644 index 00000000..19218011 Binary files /dev/null and b/Front/public/ranks/Sprite.webp differ diff --git a/Front/public/ranks/SteelGolem.webp b/Front/public/ranks/SteelGolem.webp new file mode 100644 index 00000000..3dd461a8 Binary files /dev/null and b/Front/public/ranks/SteelGolem.webp differ diff --git a/Front/public/ranks/Swordsman.webp b/Front/public/ranks/Swordsman.webp new file mode 100644 index 00000000..dc13d1b7 Binary files /dev/null and b/Front/public/ranks/Swordsman.webp differ diff --git a/Front/public/ranks/Titan.webp b/Front/public/ranks/Titan.webp new file mode 100644 index 00000000..0ab5d6d1 Binary files /dev/null and b/Front/public/ranks/Titan.webp differ diff --git a/Front/public/ranks/Troll.webp b/Front/public/ranks/Troll.webp new file mode 100644 index 00000000..69068088 Binary files /dev/null and b/Front/public/ranks/Troll.webp differ diff --git a/Front/public/ranks/Unicorn.webp b/Front/public/ranks/Unicorn.webp new file mode 100644 index 00000000..fce3a5a6 Binary files /dev/null and b/Front/public/ranks/Unicorn.webp differ diff --git a/Front/public/ranks/Vampire.webp b/Front/public/ranks/Vampire.webp new file mode 100644 index 00000000..f12f50d7 Binary files /dev/null and b/Front/public/ranks/Vampire.webp differ diff --git a/Front/public/ranks/VampireLord.webp b/Front/public/ranks/VampireLord.webp new file mode 100644 index 00000000..92678f97 Binary files /dev/null and b/Front/public/ranks/VampireLord.webp differ diff --git a/Front/public/ranks/VeteranPikeman.webp b/Front/public/ranks/VeteranPikeman.webp new file mode 100644 index 00000000..28351ccc Binary files /dev/null and b/Front/public/ranks/VeteranPikeman.webp differ diff --git a/Front/public/ranks/WarTroll.webp b/Front/public/ranks/WarTroll.webp new file mode 100644 index 00000000..6d40d795 Binary files /dev/null and b/Front/public/ranks/WarTroll.webp differ diff --git a/Front/public/ranks/Wolf.webp b/Front/public/ranks/Wolf.webp new file mode 100644 index 00000000..343658b2 Binary files /dev/null and b/Front/public/ranks/Wolf.webp differ diff --git a/Front/public/ranks/Zombie.webp b/Front/public/ranks/Zombie.webp new file mode 100644 index 00000000..d35f14f5 Binary files /dev/null and b/Front/public/ranks/Zombie.webp differ diff --git a/Front/public/vite.svg b/Front/public/vite.svg new file mode 100644 index 00000000..e7b8dfb1 --- /dev/null +++ b/Front/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Front/src/app/App.css b/Front/src/app/App.css new file mode 100644 index 00000000..25a86706 --- /dev/null +++ b/Front/src/app/App.css @@ -0,0 +1,41 @@ +#root { + width: 100%; + margin: 0 auto; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/Front/src/app/config.ts b/Front/src/app/config.ts new file mode 100644 index 00000000..52ef102a --- /dev/null +++ b/Front/src/app/config.ts @@ -0,0 +1,21 @@ +export interface ConfigAttributes { + BaseApi: string; + HubApi: string; + HasDebug: boolean; +} + +let config: ConfigAttributes = { + BaseApi: process.env.BASE_API || 'http://localhost:5130/api/', + HubApi: process.env.HUB_API || 'http://localhost:5130/gamehub', + HasDebug: true, +}; +if (process.env.NODE_ENV && process.env.NODE_ENV === 'production') { + // production code + config = { + BaseApi: window.location.origin + '/api', + HubApi: window.location.origin + '/gamehub', + HasDebug: false, + }; +} + +export default config; diff --git a/Front/src/app/constants.ts b/Front/src/app/constants.ts new file mode 100644 index 00000000..05a7f936 --- /dev/null +++ b/Front/src/app/constants.ts @@ -0,0 +1,71 @@ +const groupIds = { + girls: 'girls', + caribian: 'caribian', + somali: 'somali', + skulls: 'skulls', + redalert: 'redalert', + orcs: 'orcs', + anime: 'anime', + clover: 'clover', + army: 'army', +}; + +export const Constants = { + gameModeTypes: { + FreeForAll: 'FreeForAll', + TwoPlayersInTeam: 'TwoPlayersInTeam', + }, + teamColors: ['DarkRed', 'DarkBlue', 'DarkViolet', 'DarkOrange'], + pirateTypes: { + Usual: 'Usual', + BenGunn: 'BenGunn', + Friday: 'Friday', + }, + positions: ['Down', 'Left', 'Up', 'Right'], + commonGannMaxId: 4, + commonFridayMaxId: 1, + groupIds, + groups: [ + { + id: groupIds.girls, + photoMaxId: 6, + }, + { + id: groupIds.caribian, + photoMaxId: 11, + }, + { + id: groupIds.somali, + photoMaxId: 8, + }, + { + id: groupIds.redalert, + photoMaxId: 5, + extension: '.jpg', + }, + { + id: groupIds.orcs, + photoMaxId: 6, + extension: '.jpg', + }, + { + id: groupIds.skulls, + photoMaxId: 5, + }, + { + id: groupIds.anime, + photoMaxId: 5, + extension: '.jpg', + }, + { + id: groupIds.clover, + photoMaxId: 4, + extension: '.jpg', + }, + { + id: groupIds.army, + photoMaxId: 4, + extension: '.jpg', + }, + ], +}; diff --git a/Front/src/app/content/header/header.less b/Front/src/app/content/header/header.less new file mode 100644 index 00000000..aa62d464 --- /dev/null +++ b/Front/src/app/content/header/header.less @@ -0,0 +1,32 @@ +@import '/common/content/common.less'; + +.header { + margin-bottom: 20px; +} + +.menu-link { + @media (min-width: @screen-lg-min) { + display: block !important; + margin-right: auto !important; + margin-left: auto !important; + } + + @media (max-width: @screen-md-max) { + margin: 0.5rem 1rem !important; + } +} + +.login-wrapper { + height: 36px; + overflow: hidden; + + img { + margin-right: 5px; + } +} + +.login-text { + color: darkgoldenrod; + font-size: 18px; + vertical-align: middle; +} diff --git a/Front/src/app/content/header/index.tsx b/Front/src/app/content/header/index.tsx new file mode 100644 index 00000000..fa54cedf --- /dev/null +++ b/Front/src/app/content/header/index.tsx @@ -0,0 +1,149 @@ +import Container from 'react-bootstrap/Container'; +import Nav from 'react-bootstrap/Nav'; +import Navbar from 'react-bootstrap/Navbar'; +import Offcanvas from 'react-bootstrap/esm/Offcanvas'; +import { FaBookSkull, FaGamepad, FaNetworkWired } from 'react-icons/fa6'; +import { GiWingfoot } from 'react-icons/gi'; +import { HiLogin, HiLogout } from 'react-icons/hi'; +import { ImFire } from 'react-icons/im'; +import { MdWaterDrop } from 'react-icons/md'; +import { useDispatch, useSelector } from 'react-redux'; +import { Link } from 'react-router-dom'; + +import { activateSockets, getEnableSockets } from '../../../common/redux/commonSlice'; +import './header.less'; +import config from '/app/config'; +import { getAuth } from '/auth/redux/authSlice'; +import { sagaActions } from '/common/sagas'; + +const Header = () => { + const dispatch = useDispatch(); + + const authInfo = useSelector(getAuth); + const enableSockets = useSelector(getEnableSockets); + + const doLogout = () => + dispatch({ + type: sagaActions.AUTH_LOGOUT, + payload: {}, + }); + + const useSocketsToggle = () => dispatch(activateSockets(!enableSockets)); + + return ( + + + + + + + React-Jackal + + + + + + React-Jackal + + + + + + {process.env.NODE_ENV && process.env.NODE_ENV === 'development' && ( + + )} + + + + {authInfo.isAuthorised && ( + + )} + + + + + + + ); +}; + +export default Header; diff --git a/Front/src/app/content/layout/components/gameSettingsForm.tsx b/Front/src/app/content/layout/components/gameSettingsForm.tsx new file mode 100644 index 00000000..a31ce41c --- /dev/null +++ b/Front/src/app/content/layout/components/gameSettingsForm.tsx @@ -0,0 +1,201 @@ +import { useEffect } from 'react'; +import { Button, Form, InputGroup } from 'react-bootstrap'; +import { useDispatch, useSelector } from 'react-redux'; + +import classes from '../newgame.module.less'; +import Players from './players'; +import { PlayersInfo } from './types'; +import { sagaActions } from '/common/sagas'; +import { getGameSettings, getMapForecasts, setMapForecasts } from '/game/redux/gameSlice'; +import { GameSettingsFormData } from '/game/types/hubContracts'; + +export interface GameSettingsFormProps { + id?: number; + viewers?: UserInfo[]; + isPublic?: boolean; + isEditGroupsOnly?: boolean; + gameSettingsData: GameSettingsFormData; + setGameSettingsData: (data: GameSettingsFormData) => void; + children: React.ReactElement; +} + +const GameSettingsForm = ({ + id, + viewers, + isPublic, + isEditGroupsOnly, + gameSettingsData, + setGameSettingsData, + children, +}: GameSettingsFormProps) => { + const dispatch = useDispatch(); + + const { tilesPackNames } = useSelector(getGameSettings); + const mapForecasts = useSelector(getMapForecasts); + + if (!gameSettingsData.mapId) gameSettingsData.mapId = crypto.getRandomValues(new Int32Array(1))[0]; + + useEffect(() => { + dispatch({ + type: sagaActions.CHECK_MAP, + payload: { + mapId: gameSettingsData.mapId, + mapSize: gameSettingsData.mapSize, + tilesPackName: gameSettingsData.tilesPackName, + }, + }); + + return () => { + dispatch(setMapForecasts()); + }; + }, [dispatch, gameSettingsData.mapId, gameSettingsData.mapSize, gameSettingsData.tilesPackName]); + + const setPlayers = (data: PlayersInfo) => { + setGameSettingsData({ + ...gameSettingsData, + players: data, + }); + }; + + const setMapSize = (data: number) => { + setGameSettingsData({ + ...gameSettingsData, + mapSize: data, + }); + }; + + const setTilesPackName = (data: string) => { + setGameSettingsData({ + ...gameSettingsData, + tilesPackName: data, + }); + }; + + const setMapId = (data: number) => { + setGameSettingsData({ + ...gameSettingsData, + mapId: data, + }); + dispatch({ + type: sagaActions.CHECK_MAP, + payload: { + mapId: data, + mapSize: gameSettingsData.mapSize, + tilesPackName: gameSettingsData.tilesPackName, + }, + }); + }; + + const changeMapId = () => { + const newId = crypto.getRandomValues(new Int32Array(1)); + setMapId(newId[0]); + }; + + const storeMapId = (event: { target: { checked: boolean } }) => { + setGameSettingsData({ + ...gameSettingsData, + isStoredMap: event.target.checked, + }); + }; + + return ( +
event.preventDefault()}> + {isPublic && ( + <> +
+ + № публичной игры: {id} + +
+
+ + Участники:{' '} + {viewers && + viewers.map((it) => ( +
+ {it.login} +
{it.id}
+
+ ))} +
+
+ + )} + +
+ {isPublic ? ( +
Публичная игра
+ ) : ( +
Частная игра
+ )} +
+
+
+ Размер карты: {gameSettingsData.mapSize} + setMapSize(Number(e.target.value))} + className="custom-slider" + /> +
+
+ {tilesPackNames && tilesPackNames.length > 0 && ( + + Игровой набор + { + setTilesPackName(event.target.value); + }} + > + {tilesPackNames.map((it) => ( + + ))} + + + )} + + Код карты + + { + if (event.target.value) setMapId(Number(event.target.value)); + }} + /> + + + + + + + {children} + + ); +}; + +export default GameSettingsForm; diff --git a/Front/src/app/content/layout/components/players/index.tsx b/Front/src/app/content/layout/components/players/index.tsx new file mode 100644 index 00000000..675b1e44 --- /dev/null +++ b/Front/src/app/content/layout/components/players/index.tsx @@ -0,0 +1,105 @@ +import cn from 'classnames'; +import { useState } from 'react'; + +import { PlayerInfo, PlayersInfo } from '../types'; +import Player from './player'; +import classes from './players.module.less'; +import { Constants } from '/app/constants'; + +const convertGroups = (grps: string[]) => grps.map((gr) => Constants.groups.findIndex((it) => it.id == gr) || 0); +const deconvertGroups = (groups: number[]) => groups.map((num) => Constants.groups[num].id); + +export interface PlayersProps { + players: PlayersInfo; + gamers: PlayerInfo[]; + setPlayers: (data: PlayersInfo) => void; + mapInfo?: string[]; +} + +const Players = ({ players, gamers, setPlayers, mapInfo }: PlayersProps) => { + const [grps, setGrps] = useState(convertGroups(players.groups)); + + const changeGamer = (pos: number) => { + const clone = [...players.gamers]; + if (clone[pos].id + 1 >= gamers.length) clone[pos] = gamers[0]; + else clone[pos] = gamers.find((it) => it.id === clone[pos].id + 1) ?? gamers[0]; + setPlayers({ + ...players, + gamers: clone, + }); + }; + + const changeGroup = (pos: number) => { + const clone = [...grps]; + let current = clone[pos]; + while (clone.includes(current)) { + if (current + 1 >= Constants.groups.length) { + current = 0; + } else { + current += 1; + } + } + clone[pos] = current; + setGrps(clone); + setPlayers({ + ...players, + groups: deconvertGroups(clone), + }); + }; + + const calcNextMode = (prev: number) => { + if (prev == 8) return 1; + else if (prev == 1) return 2; + else if (prev == 2) return 4; + return 8; + }; + + return ( +
+ {players && + players.gamers && + players.gamers.map((gamer, index) => { + if (players.mode < 4 && (index == 1 || index == 3)) { + return null; + } + if (players.mode < 2 && index == 2) { + return null; + } + + return ( + 0 ? gamer.userName : undefined} + group={grps[index]} + posInfo={mapInfo && mapInfo[index]} + changePlayer={() => changeGamer(index)} + changeGroup={() => changeGroup(index)} + /> + ); + })} +
+ setPlayers({ + ...players, + mode: calcNextMode(players.mode), + }) + } + style={{ + cursor: 'pointer', + top: '110px', + left: players.mode == 8 ? '110px' : '130px', + fontSize: '48px', + lineHeight: '48px', + textAlign: 'center', + }} + > + {players.mode == 8 ? '2x2' : players.mode} +
+
+ ); +}; + +export default Players; diff --git a/Front/src/app/content/layout/components/players/player.tsx b/Front/src/app/content/layout/components/players/player.tsx new file mode 100644 index 00000000..332c5eb3 --- /dev/null +++ b/Front/src/app/content/layout/components/players/player.tsx @@ -0,0 +1,57 @@ +import Image from 'react-bootstrap/Image'; + +import classes from './players.module.less'; +import { Constants } from '/app/constants'; + +interface PlayerProps { + position: number; + type: string; + userName?: string; + group: number; + posInfo?: string; + changePlayer: () => void; + changeGroup: () => void; +} + +const Player = ({ position, type, userName, group, posInfo, changePlayer, changeGroup }: PlayerProps) => { + const getUrlByPlayer = (type: string) => { + if (type === 'human') return '/pictures/human.png'; + else if (type === 'robot') return '/pictures/robot.png'; + else if (type === 'robot2') return '/pictures/robot2.png'; + else if (type === 'robot3') return '/pictures/robot3.png'; + }; + + const getTopPosition = (pos: number) => { + if (pos === 0) return 200; + else if (pos === 2) return 0; + return 100; + }; + + const getLeftPosition = (pos: number) => { + if (pos === 1) return 0; + else if (pos === 3) return 200; + return 100; + }; + + return ( +
+ + {Constants.groups[group].id} + {userName &&
{userName}
} + {posInfo &&
{posInfo}
} +
+ ); +}; +export default Player; diff --git a/Front/src/app/content/layout/components/players/players.module.less b/Front/src/app/content/layout/components/players/players.module.less new file mode 100644 index 00000000..3f8e51d3 --- /dev/null +++ b/Front/src/app/content/layout/components/players/players.module.less @@ -0,0 +1,45 @@ +.settings { + width: 280px !important; + height: 280px; + position: relative; +} + +.player { + position: absolute; +} + +.userName { + position: absolute; + top: -20px; + left: -10px; + width: 100px; + height: 26px; + margin: 0; + padding: 0; + color: red; + font-weight: bold; + overflow: clip; + text-overflow: ellipsis; + white-space: nowrap; + text-align: center; +} + +.type { + cursor: pointer; + background-repeat: no-repeat; + width: 80px; + height: 80px; + margin: 0; + padding: 0; +} + +.group { + cursor: pointer; + position: absolute; + top: 50px; + left: 50px; + width: 50px; + height: 50px; + margin: 0; + padding: 0; +} diff --git a/Front/src/app/content/layout/components/types.d.ts b/Front/src/app/content/layout/components/types.d.ts new file mode 100644 index 00000000..b9fa3d2a --- /dev/null +++ b/Front/src/app/content/layout/components/types.d.ts @@ -0,0 +1,14 @@ +export interface PlayersInfo { + mode: number; + members: string[]; + users: number[]; + groups: string[]; + gamers: PlayerInfo[]; +} + +export interface PlayerInfo { + id: number; + type: string; + userId: number; + userName?: string; +} diff --git a/Front/src/app/content/layout/index.tsx b/Front/src/app/content/layout/index.tsx new file mode 100644 index 00000000..1674e5ac --- /dev/null +++ b/Front/src/app/content/layout/index.tsx @@ -0,0 +1,73 @@ +import { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { Route, Routes } from 'react-router-dom'; + +import GameDocuments from '../../../docs/content'; +import useClientMethod from '../../hubs/useClientMethod'; +import useHub from '../../hubs/useHub'; +import Logon from './logon'; +import MessageNotifier from './messNotifier'; +import Newgame from './newgame'; +import Quickstart from './quickstart'; +import { hubConnection } from '/app/global'; +import useClientMethods from '/app/hubs/useClientMethods'; +import Login from '/auth/content/login'; +import { getAuth } from '/auth/redux/authSlice'; +import { getEnableSockets, showMessage } from '/common/redux/commonSlice'; +import { sagaActions } from '/common/sagas'; +import Playground from '/game/content/playground'; +import gameHub from '/game/hub/gameHub'; +import { initMySettings } from '/game/redux/gameSlice'; +import { StorageState } from '/game/types'; +import NetGameCreate from '/lobby/content/gameCreate'; +import GameList from '/lobby/content/gameList'; + +const Layout = () => { + const dispatch = useDispatch(); + const enableSockets = useSelector(getEnableSockets); + const auth = useSelector(getAuth); + + useClientMethod(enableSockets, hubConnection, 'Notify', (text) => { + dispatch( + showMessage({ + isError: false, + errorCode: 'success', + messageText: JSON.stringify(text), + }), + ); + }); + useClientMethods(enableSockets, hubConnection, dispatch, gameHub.getEventHandlers); + useHub(enableSockets && auth.isAuthorised === true, hubConnection); + + useEffect(() => { + const myStateStr = localStorage.getItem('state'); + if (myStateStr) { + const myState: StorageState = JSON.parse(myStateStr); + if (myState) { + dispatch(initMySettings(myState)); + } + } + + dispatch({ type: sagaActions.GET_TILES_PACK_NAMES, payload: {} }); + dispatch({ type: sagaActions.LOBBY_GET_LEADERBOARD, payload: {} }); + dispatch({ type: sagaActions.AUTH_CHECK, payload: {} }); + }, [dispatch]); + + return ( + <> + + }> + }> + }> + }> + }> + }> + }> + + {auth.isAuthorised === false && } + + + ); +}; + +export default Layout; diff --git a/Front/src/app/content/layout/logon.tsx b/Front/src/app/content/layout/logon.tsx new file mode 100644 index 00000000..7a323415 --- /dev/null +++ b/Front/src/app/content/layout/logon.tsx @@ -0,0 +1,61 @@ +import { useState } from 'react'; +import { Button, Form, Modal } from 'react-bootstrap'; +import { useDispatch } from 'react-redux'; + +import { sagaActions } from '/common/sagas'; + +const Logon = () => { + const dispatch = useDispatch(); + + const [name, setName] = useState(); + + const enterLogin = (login: string) => { + dispatch({ + type: sagaActions.AUTH_LOGIN, + payload: { + login: login, + }, + }); + }; + + return ( + + + Авторизоваться + + +
+ + Ваше имя + setName(event.target.value)} + /> + +
+
+ + + +
+ ); +}; + +export default Logon; diff --git a/Front/src/app/content/layout/messNotifier.tsx b/Front/src/app/content/layout/messNotifier.tsx new file mode 100644 index 00000000..2076ec5c --- /dev/null +++ b/Front/src/app/content/layout/messNotifier.tsx @@ -0,0 +1,35 @@ +import { Toast, ToastContainer } from 'react-bootstrap'; +import { useDispatch, useSelector } from 'react-redux'; + +import { getMessage, hideError, processError } from '../../../common/redux/commonSlice'; + +const MessageNotifier = () => { + const dispatch = useDispatch(); + + const error = useSelector(getMessage); + + return ( + + { + dispatch(hideError()); + setTimeout(() => dispatch(processError()), 200); + }} + delay={3000} + autohide + > + + + Bootstrap + 11 mins ago + + {error?.messageText || 'неизвестная ошибка'} + + + ); +}; + +export default MessageNotifier; diff --git a/Front/src/app/content/layout/navigateSetter.tsx b/Front/src/app/content/layout/navigateSetter.tsx new file mode 100644 index 00000000..54b59e04 --- /dev/null +++ b/Front/src/app/content/layout/navigateSetter.tsx @@ -0,0 +1,10 @@ +import { useNavigate } from 'react-router-dom'; +import { history } from '/app/global'; + +const NavigateSetter = () => { + history.navigate = useNavigate(); + + return null; +}; + +export default NavigateSetter; diff --git a/Front/src/app/content/layout/newgame.module.less b/Front/src/app/content/layout/newgame.module.less new file mode 100644 index 00000000..83dbfbc0 --- /dev/null +++ b/Front/src/app/content/layout/newgame.module.less @@ -0,0 +1,23 @@ +@import '/common/content/common.less'; + +.lobbyList { + &:extend(.ui-panel all); + &:after { + content: 'Список лобби'; + } + max-width: 500px !important; +} + +.newgame { + &:extend(.ui-panel all); + &:after { + content: 'Новая игра'; + } + max-width: 500px !important; +} + +.settings { + width: 280px !important; + height: 280px; + position: relative; +} diff --git a/Front/src/app/content/layout/newgame.tsx b/Front/src/app/content/layout/newgame.tsx new file mode 100644 index 00000000..8bc67334 --- /dev/null +++ b/Front/src/app/content/layout/newgame.tsx @@ -0,0 +1,97 @@ +import { useState } from 'react'; +import { Button } from 'react-bootstrap'; +import Container from 'react-bootstrap/Container'; +import Row from 'react-bootstrap/Row'; +import { useDispatch, useSelector } from 'react-redux'; +import { useNavigate } from 'react-router-dom'; + +import { getUserSettings, saveMySettings } from '../../../game/redux/gameSlice'; +import GameSettingsForm from './components/gameSettingsForm'; +import { convertToSettings } from '/app/global'; +import { getAuth } from '/auth/redux/authSlice'; +import gameHub from '/game/hub/gameHub'; +import { GameSettingsFormData } from '/game/types/hubContracts'; + +const Newgame = () => { + const dispatch = useDispatch(); + const navigate = useNavigate(); + + const authInfo = useSelector(getAuth); + const userSettings = useSelector(getUserSettings); + + let counter = 0; + const gamers = [ + { id: counter++, type: 'human', userId: authInfo.user?.id ?? 0 }, + { id: counter++, type: 'robot', userId: 0 }, + { id: counter++, type: 'robot2', userId: 0 }, + { id: counter++, type: 'robot3', userId: 0 }, + ]; + + const [formData, setFormData] = useState({ + players: { + mode: userSettings.playersMode || 4, + users: [authInfo.user?.id ?? 0, authInfo.user?.id ?? 0, authInfo.user?.id ?? 0, authInfo.user?.id ?? 0], + members: userSettings.players || ['human', 'robot2', 'robot', 'robot2'], + gamers: (userSettings.players || ['human', 'robot2', 'robot', 'robot2']).map( + (it) => gamers.find((gm) => gm.type === it) ?? gamers[0], + ), + groups: userSettings.groups, + }, + gamers, + mapId: userSettings.mapId, + mapSize: userSettings.mapSize || 11, + tilesPackName: userSettings.tilesPackName, + isStoredMap: userSettings.mapId != undefined, + }); + + const newStart = () => { + saveToLocalStorage(); + gameHub.startGame(convertToSettings(formData)); + }; + + const createNetGame = () => { + navigate('/newpublic'); + + gameHub.netCreate(convertToSettings(formData)); + }; + + const saveToLocalStorage = () => { + dispatch( + saveMySettings({ + ...userSettings, + groups: formData.players.groups, + mapSize: formData.mapSize, + players: formData.players.members, + playersMode: formData.players.mode, + mapId: formData.isStoredMap ? formData.mapId : undefined, + tilesPackName: formData.tilesPackName, + }), + ); + }; + + const setGameFormData = (data: GameSettingsFormData) => { + if (formData.isStoredMap != data.isStoredMap) { + saveToLocalStorage(); + } + setFormData(data); + }; + + return ( + + + + <> + + + + + + + ); +}; + +export default Newgame; diff --git a/Front/src/app/content/layout/quickstart.tsx b/Front/src/app/content/layout/quickstart.tsx new file mode 100644 index 00000000..f75dcbbf --- /dev/null +++ b/Front/src/app/content/layout/quickstart.tsx @@ -0,0 +1,40 @@ +import { HubConnectionState } from '@microsoft/signalr'; +import { useEffect } from 'react'; +import { useSelector } from 'react-redux'; +import { useNavigate } from 'react-router-dom'; + +import { getUserSettings } from '../../../game/redux/gameSlice'; +import { Constants } from '/app/constants'; +import { hubConnection } from '/app/global'; +import { getAuth } from '/auth/redux/authSlice'; +import gameHub from '/game/hub/gameHub'; + +const Quickstart = () => { + const navigate = useNavigate(); + + const authInfo = useSelector(getAuth); + const userSettings = useSelector(getUserSettings); + + const speedStart = () => { + gameHub.startGame({ + players: [ + { userId: authInfo.user?.id ?? 0, type: 'human', position: Constants.positions[0] }, + { userId: 0, type: 'robot2', position: Constants.positions[2] }, + ], + mapId: userSettings.mapId, + mapSize: 11, + tilesPackName: userSettings.tilesPackName, + }); + }; + + useEffect(() => { + if (authInfo.isAuthorised && hubConnection?.state == HubConnectionState.Connected) { + navigate('/'); + speedStart(); + } + }, [authInfo.isAuthorised, hubConnection?.state]); + + return <>Ждите... Идёт загрузка...; +}; + +export default Quickstart; diff --git a/Front/src/app/global.ts b/Front/src/app/global.ts new file mode 100644 index 00000000..0438e3a8 --- /dev/null +++ b/Front/src/app/global.ts @@ -0,0 +1,128 @@ +import { HubConnectionBuilder, LogLevel } from '@microsoft/signalr'; +import dayjs from 'dayjs'; +import { NavigateFunction } from 'react-router-dom'; + +import config from './config'; +import { Constants } from './constants'; +import { PlayerInfo, PlayersInfo } from './content/layout/components/types'; +import { GamePlayer, GameSettings, GameSettingsFormData } from '/game/types/hubContracts'; + +export const uuidGen = () => { + return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, (c) => + (+c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (+c / 4)))).toString(16), + ); +}; + +export const getRandomValues = (min: number, max: number, count: number): number[] => { + if (max - min + 1 < count) return []; + + const arr = [] as number[]; + while (arr.length < count) { + const x = Math.floor(Math.random() * (max - min + 1) + min); + if (!arr.includes(x)) { + arr.push(x); + } + } + return arr; +}; + +export const getAnotherRandomValue = (min: number, max: number, except: number[]): number => { + if (max - min + 1 <= except.length) return min; + + let num = except.length > 0 ? except[0] : min; + while (except.includes(num)) { + num = Math.floor(Math.random() * (max - min + 1) + min); + } + return num; +}; + +export interface fromNowStruct { + value: number; + unit: string; + color: string; +} + +export const fromNow = (ts: number) => { + let inSec = Math.abs(dayjs(ts * 1000).diff(new Date(), 's')); + if (inSec < 60) { + return { + value: inSec, + unit: 'с', + color: 'green', + }; + } + inSec = Math.ceil(inSec / 60); + return { + value: inSec, + unit: 'м', + color: inSec < 3 ? 'orange' : 'red', + }; +}; + +export const debugLog = (message?: unknown, ...optionalParams: unknown[]) => { + if (config.HasDebug) console.log(message, optionalParams); +}; + +interface History { + navigate?: NavigateFunction; +} + +export const history: History = {}; + +export const hubConnection = new HubConnectionBuilder() + .withUrl(config.HubApi, { accessTokenFactory: () => localStorage.auth }) + .withAutomaticReconnect() + .configureLogging(LogLevel.Information) + .build(); + +const convertToPlayers = (data: PlayersInfo): GamePlayer[] => { + const { mode, gamers } = data; + + if (mode == 1) return [{ userId: gamers[0].userId, type: gamers[0].type, position: Constants.positions[0] }]; + else if (mode == 2) + return [ + { userId: gamers[0].userId, type: gamers[0].type, position: Constants.positions[0] }, + { userId: gamers[2].userId, type: gamers[2].type, position: Constants.positions[2] }, + ]; + else + return gamers.map((it, index) => ({ + userId: it.userId, + type: it.type, + position: Constants.positions[index], + })); +}; + +export const convertToSettings = (data: GameSettingsFormData): GameSettings => ({ + players: convertToPlayers(data.players), + mapId: data.mapId, + mapSize: data.mapSize, + tilesPackName: data.tilesPackName, + gameMode: data.players.mode == 8 ? Constants.gameModeTypes.TwoPlayersInTeam : Constants.gameModeTypes.FreeForAll, +}); + +export const convertToMembers = (data: GamePlayer[], defaults: string[]): string[] => { + if (data.length == 1) return data.map((it) => it.type.toLocaleLowerCase()).concat(defaults.slice(1)); + if (data.length == 2) { + return [data[0].type.toLocaleLowerCase(), defaults[1], data[1].type.toLocaleLowerCase(), defaults[3]]; + } else return data.map((it) => it.type.toLocaleLowerCase()); +}; + +export const convertToUsers = (data: GamePlayer[], defaults: number[]): number[] => { + if (data.length == 1) return data.map((it) => it.userId).concat(defaults.slice(1)); + if (data.length == 2) { + return [data[0].userId, defaults[1], data[1].userId, defaults[3]]; + } else return data.map((it) => it.userId); +}; + +export const convertToGamers = (data: GamePlayer[], gamers: PlayerInfo[], defaults: PlayerInfo[]): PlayerInfo[] => { + if (data.length == 1) return data.map((it) => getGamerByPlayer(it, gamers)).concat(defaults.slice(1)); + if (data.length == 2) { + return [getGamerByPlayer(data[0], gamers), defaults[1], getGamerByPlayer(data[1], gamers), defaults[3]]; + } else return data.map((it) => getGamerByPlayer(it, gamers)); +}; + +const getGamerByPlayer = (man: GamePlayer, gamers: PlayerInfo[]) => { + return man.userId > 0 + ? (gamers.find((gm) => gm.userId === man.userId) ?? gamers[0]) + : (gamers.find((gm) => gm.type === man.type.toLocaleLowerCase()) ?? gamers[0]); +}; diff --git a/Front/src/app/hubs/useClientMethod.tsx b/Front/src/app/hubs/useClientMethod.tsx new file mode 100644 index 00000000..78c67d70 --- /dev/null +++ b/Front/src/app/hubs/useClientMethod.tsx @@ -0,0 +1,31 @@ +import { HubConnection } from '@microsoft/signalr'; +import { useEffect } from 'react'; + +import { debugLog } from '/app/global'; + +const useClientMethod = ( + enableSockets: boolean, + hubConnection: HubConnection | undefined, + methodName: string, + method: (...args: unknown[]) => void, +) => { + useEffect(() => { + debugLog('useClientMethod', methodName, hubConnection); + + if (!hubConnection) { + return; + } + + if (enableSockets) { + hubConnection.on(methodName, method); + + return () => { + hubConnection.off(methodName, method); + }; + } else { + hubConnection.off(methodName, method); + } + }, [hubConnection, enableSockets, method, methodName]); +}; + +export default useClientMethod; diff --git a/Front/src/app/hubs/useClientMethods.tsx b/Front/src/app/hubs/useClientMethods.tsx new file mode 100644 index 00000000..c35f4dd3 --- /dev/null +++ b/Front/src/app/hubs/useClientMethods.tsx @@ -0,0 +1,51 @@ +import { HubConnection } from '@microsoft/signalr'; +import { UnknownAction } from '@reduxjs/toolkit'; +import { Dispatch, useEffect } from 'react'; + +import { debugLog } from '/app/global'; + +const useClientMethod = ( + enableSockets: boolean, + hubConnection: HubConnection | undefined, + dispatch: Dispatch, + methods: { + name: string; + sagaAction: string; + }[], +) => { + useEffect(() => { + for (const key in methods) { + debugLog('useClientMethod', key, hubConnection); + } + + if (!hubConnection) { + return; + } + + const handlers = methods.map((it) => ({ + name: it.name, + action: (data: unknown) => { + debugLog(data); + dispatch({ type: it.sagaAction, payload: data }); + }, + })); + + if (enableSockets) { + handlers.forEach((it) => { + hubConnection.on(it.name, it.action); + }); + + return () => { + handlers.forEach((it) => { + hubConnection.off(it.name, it.action); + }); + }; + } else { + handlers.forEach((it) => { + hubConnection.off(it.name, it.action); + }); + } + }, [hubConnection, enableSockets, dispatch, methods]); +}; + +export default useClientMethod; diff --git a/Front/src/app/hubs/useHub.tsx b/Front/src/app/hubs/useHub.tsx new file mode 100644 index 00000000..f50e58b7 --- /dev/null +++ b/Front/src/app/hubs/useHub.tsx @@ -0,0 +1,51 @@ +import { HubConnection, HubConnectionState } from '@microsoft/signalr'; +import { useEffect, useState } from 'react'; +import { useDispatch } from 'react-redux'; + +import { debugLog } from '/app/global'; +import { showMessage } from '/common/redux/commonSlice'; + +const useHub = (enableSockets: boolean, hubConnection?: HubConnection) => { + const [hubConnectionState, setHubConnectionState] = useState( + hubConnection?.state ?? HubConnectionState.Disconnected, + ); + const dispatch = useDispatch(); + + useEffect(() => { + debugLog('useHub', hubConnection, hubConnection?.state); + + if (!hubConnection) { + setHubConnectionState(HubConnectionState.Disconnected); + return; + } + if (!enableSockets) { + hubConnection.stop(); + return; + } + + if (hubConnection.state === HubConnectionState.Disconnected) { + debugLog('start connection', hubConnection.state); + hubConnection + .start() + .then(() => setHubConnectionState(hubConnection?.state)) + .catch((err) => { + debugLog('hubConnection error', err); + dispatch( + showMessage({ + isError: true, + errorCode: err.response?.statusText, + messageText: 'Соединение не установлено', + }), + ); + }); + } + + return () => { + hubConnection.stop(); + }; + }, [hubConnection, enableSockets, dispatch]); + + return { hubConnectionState }; +}; + +export default useHub; diff --git a/Front/src/app/index.tsx b/Front/src/app/index.tsx new file mode 100644 index 00000000..ea9fdd06 --- /dev/null +++ b/Front/src/app/index.tsx @@ -0,0 +1,24 @@ +import './App.css'; +import store from './store'; +import { Provider } from 'react-redux'; + +import Header from './content/header'; +import Layout from './content/layout'; +import NavigateSetter from './content/layout/navigateSetter'; + +import 'bootstrap/dist/css/bootstrap.min.css'; +import { BrowserRouter } from 'react-router-dom'; + +function App() { + return ( + + + +
+ + + + ); +} + +export default App; diff --git a/Front/src/app/sagas.ts b/Front/src/app/sagas.ts new file mode 100644 index 00000000..81cdece4 --- /dev/null +++ b/Front/src/app/sagas.ts @@ -0,0 +1,13 @@ +import { fork } from 'redux-saga/effects'; + +import authSaga from '/auth/sagas/authSaga'; +import gameFeaturesSaga from '/game/sagas/gameFeaturesSaga'; +import gameSaga from '/game/sagas/gameSaga'; +import lobbySaga from '/lobby/sagas/lobbySaga'; + +export default function* rootSaga() { + yield fork(authSaga); + yield fork(gameSaga); + yield fork(gameFeaturesSaga); + yield fork(lobbySaga); +} diff --git a/Front/src/app/store.ts b/Front/src/app/store.ts new file mode 100644 index 00000000..bce3e350 --- /dev/null +++ b/Front/src/app/store.ts @@ -0,0 +1,24 @@ +import { configureStore } from '@reduxjs/toolkit'; +import createSagaMiddleware from 'redux-saga'; + +import authReducer from '../auth/redux/authSlice'; +import commonReducer from '../common/redux/commonSlice'; +import gameReducer from '../game/redux/gameSlice'; +import saga from './sagas'; +import lobbyReducer from '/lobby/redux/lobbySlice'; + +const sagaMiddleware = createSagaMiddleware(); + +const store = configureStore({ + reducer: { + auth: authReducer, + common: commonReducer, + game: gameReducer, + lobby: lobbyReducer, + }, + middleware: (getDefaultMiddleware) => getDefaultMiddleware({ thunk: false }).concat(sagaMiddleware), +}); + +sagaMiddleware.run(saga); + +export default store; diff --git a/Front/src/auth/content/login.module.less b/Front/src/auth/content/login.module.less new file mode 100644 index 00000000..57dff9ab --- /dev/null +++ b/Front/src/auth/content/login.module.less @@ -0,0 +1,9 @@ +@import '/common/content/common.less'; + +.login { + &:extend(.ui-panel all); + &:after { + content: 'Авторизоваться'; + } + max-width: 500px !important; +} diff --git a/Front/src/auth/content/login.tsx b/Front/src/auth/content/login.tsx new file mode 100644 index 00000000..5de2dd4f --- /dev/null +++ b/Front/src/auth/content/login.tsx @@ -0,0 +1,55 @@ +import { useState } from 'react'; +import { Button, Form } from 'react-bootstrap'; +import Container from 'react-bootstrap/Container'; +import Row from 'react-bootstrap/Row'; +import { useDispatch } from 'react-redux'; + +import classes from './login.module.less'; +import { AuthLoginRequest } from '/auth/types/authSaga'; +import { sagaActions } from '/common/sagas'; + +function Login() { + const dispatch = useDispatch(); + + const [name, setName] = useState(); + + const enterLogin = (login: string) => { + dispatch({ + type: sagaActions.AUTH_LOGIN, + payload: { + login: login, + } as AuthLoginRequest, + }); + }; + + return ( + + +
event.preventDefault()}> + + Ваше имя + setName(event.target.value)} + /> + + +
+
+
+ ); +} + +export default Login; diff --git a/Front/src/auth/redux/authSlice.ts b/Front/src/auth/redux/authSlice.ts new file mode 100644 index 00000000..c48ce41e --- /dev/null +++ b/Front/src/auth/redux/authSlice.ts @@ -0,0 +1,36 @@ +import { PayloadAction, createSlice } from '@reduxjs/toolkit'; + +import { AuthState } from '../types/auth'; +import { AuthInfo } from '../types/authSlice'; +import { axiosInstance } from '/common/sagas'; + +export const authSlice = createSlice({ + name: 'auth', + initialState: {} satisfies AuthState as AuthState, + reducers: { + setAuth: (state, action: PayloadAction) => { + if (action.payload.token) { + localStorage.auth = action.payload.token; + axiosInstance.defaults.headers.common['Authorization'] = localStorage.auth; + } else { + localStorage.removeItem('auth'); + delete axiosInstance.defaults.headers.common['Authorization']; + } + + authSlice.caseReducers.checkAuth(state, checkAuth(action.payload)); + }, + checkAuth: (state, action: PayloadAction) => { + state.user = action.payload.user; + state.isAuthorised = action.payload.isAuthorised; + }, + }, + selectors: { + getAuth: (state): AuthState => state, + }, +}); + +export const { setAuth, checkAuth } = authSlice.actions; + +export const { getAuth } = authSlice.selectors; + +export default authSlice.reducer; diff --git a/Front/src/auth/sagas/authSaga.ts b/Front/src/auth/sagas/authSaga.ts new file mode 100644 index 00000000..1c20dff4 --- /dev/null +++ b/Front/src/auth/sagas/authSaga.ts @@ -0,0 +1,65 @@ +import { PayloadAction } from '@reduxjs/toolkit'; +import { call, put, takeEvery } from 'redux-saga/effects'; + +import { checkAuth, setAuth } from '../redux/authSlice'; +import { AuthLoginRequest, AuthResponse, CheckResponse } from '../types/authSaga'; +import { history } from '/app/global'; +import { axiosInstance, errorsWrapper, sagaActions } from '/common/sagas'; + +export function* authCheck(action: PayloadAction) { + const result: { data: CheckResponse } = yield call( + async () => + await axiosInstance({ + url: 'v1/auth/check', + method: 'post', + data: action.payload, + }), + ); + yield put( + checkAuth({ + user: result.data.user, + isAuthorised: !!result.data.user, + }), + ); +} + +export function* authLogin(action: { payload: AuthLoginRequest }) { + const result: { data: AuthResponse } = yield call( + async () => + await axiosInstance({ + url: 'v1/auth/register', + method: 'post', + data: action.payload, + }), + ); + history.navigate && history.navigate('/'); + yield put( + setAuth({ + token: result.data.token, + user: result.data.user, + isAuthorised: true, + }), + ); +} + +export function* authLogout(action: { payload: object }) { + yield call( + async () => + await axiosInstance({ + url: 'v1/auth/logout', + method: 'post', + data: action.payload, + }), + ); + yield put( + setAuth({ + isAuthorised: false, + }), + ); +} + +export default function* rootSaga() { + yield takeEvery(sagaActions.AUTH_CHECK, errorsWrapper(authCheck)); + yield takeEvery(sagaActions.AUTH_LOGIN, errorsWrapper(authLogin)); + yield takeEvery(sagaActions.AUTH_LOGOUT, errorsWrapper(authLogout)); +} diff --git a/Front/src/auth/types/auth.d.ts b/Front/src/auth/types/auth.d.ts new file mode 100644 index 00000000..1f32e794 --- /dev/null +++ b/Front/src/auth/types/auth.d.ts @@ -0,0 +1,4 @@ +export interface AuthState { + user?: UserInfo; + isAuthorised?: boolean; +} diff --git a/Front/src/auth/types/authSaga.d.ts b/Front/src/auth/types/authSaga.d.ts new file mode 100644 index 00000000..ff0784ff --- /dev/null +++ b/Front/src/auth/types/authSaga.d.ts @@ -0,0 +1,11 @@ +export interface CheckResponse { + user: UserInfo; +} + +export interface AuthResponse extends CheckResponse { + token: string; +} + +export interface AuthLoginRequest { + login: string; +} diff --git a/Front/src/auth/types/authSlice.d.ts b/Front/src/auth/types/authSlice.d.ts new file mode 100644 index 00000000..5c22fd38 --- /dev/null +++ b/Front/src/auth/types/authSlice.d.ts @@ -0,0 +1,5 @@ +export interface AuthInfo { + token?: string; + user?: UserInfo; + isAuthorised: boolean; +} diff --git a/Front/src/common/content/common.less b/Front/src/common/content/common.less new file mode 100644 index 00000000..b604a2ef --- /dev/null +++ b/Front/src/common/content/common.less @@ -0,0 +1,27 @@ +@screen-sm-max: 767.98px; +@screen-md-min: 768px; +@screen-md-max: 991.98px; +@screen-lg-min: 992px; +@screen-lg-max: 1200px; + +.ui-panel { + background-color: white; + border-color: gray; + border-style: solid; + border-width: 1px; + border-radius: 4px; + padding: 45px 15px 15px; + position: relative; + text-align: start; + + &:after { + color: #959595; + font-size: 12px; + font-weight: 700; + left: 15px; + letter-spacing: 1px; + position: absolute; + text-transform: uppercase; + top: 15px; + } +} diff --git a/Front/src/common/redux/commonSlice.ts b/Front/src/common/redux/commonSlice.ts new file mode 100644 index 00000000..65962179 --- /dev/null +++ b/Front/src/common/redux/commonSlice.ts @@ -0,0 +1,39 @@ +import { PayloadAction, createSlice } from '@reduxjs/toolkit'; + +import { CommonState, MessageInfo } from '../types/common'; + +export const commonSlice = createSlice({ + name: 'common', + initialState: { + enableSockets: true, + messageQueue: [], + } satisfies CommonState as CommonState, + reducers: { + activateSockets: (state, action: PayloadAction) => { + state.enableSockets = action.payload; + }, + showMessage: (state, action: PayloadAction) => { + if (state.message) { + state.messageQueue.push(action.payload); + } else { + state.message = action.payload; + } + }, + processError: (state) => { + state.message = state.messageQueue.shift(); + }, + hideError: (state) => { + state.message = undefined; + }, + }, + selectors: { + getMessage: (state) => state.message, + getEnableSockets: (state) => state.enableSockets, + }, +}); + +export const { activateSockets, showMessage, processError, hideError } = commonSlice.actions; + +export const { getMessage, getEnableSockets } = commonSlice.selectors; + +export default commonSlice.reducer; diff --git a/Front/src/common/sagas.ts b/Front/src/common/sagas.ts new file mode 100644 index 00000000..cee7f2cd --- /dev/null +++ b/Front/src/common/sagas.ts @@ -0,0 +1,84 @@ +import { PayloadAction } from '@reduxjs/toolkit'; +import axios from 'axios'; +import { call, put } from 'redux-saga/effects'; + +import { showMessage } from './redux/commonSlice'; +import { ErrorInfo } from './types/common'; +import config from '/app/config'; +import { debugLog } from '/app/global'; +import { setAuth } from '/auth/redux/authSlice'; + +export const sagaActions = { + GAME_START_APPLY_DATA: 'GAME_START_APPLY_DATA', + GAME_TURN_APPLY_DATA: 'GAME_TURN_APPLY_DATA', + GAME_START_LOOKING_DATA: 'GAME_START_LOOKING_DATA', + + START_ANIMATE: 'START_ANIMATE', + STOP_ANIMATE: 'STOP_ANIMATE', + GET_TILES_PACK_NAMES: 'GET_TILES_PACK_NAMES', + CHECK_MAP: 'CHECK_MAP', + + LOBBY_GET_LEADERBOARD: 'LOBBY_GET_LEADERBOARD', + + NET_GAME_CREATE: 'NET_GAME_CREATE', + NET_GAME_APPLY_DATA: 'NET_GAME_APPLY_DATA', + ACTIVE_GAMES_APPLY_DATA: 'ACTIVE_GAMES_APPLY_DATA', + NET_GAMES_APPLY_DATA: 'NET_GAMES_APPLY_DATA', + + AUTH_CHECK: 'AUTH_CHECK', + AUTH_LOGIN: 'AUTH_LOGIN', + AUTH_LOGOUT: 'AUTH_LOGOUT', +}; + +export const axiosInstance = axios.create({ + baseURL: config.BaseApi, + headers: { + Authorization: `Bearer ${localStorage.auth}`, + }, +}); + +export const errorsWrapper = (saga: (action: PayloadAction) => void) => + function* (action: PayloadAction) { + try { + yield call(saga, action); + } catch (err) { + if (axios.isAxiosError(err)) { + const error = err.response?.data as ErrorInfo; + + debugLog(error, err); + + if (error) { + yield put( + showMessage({ + isError: true, + errorCode: error.errorCode, + messageText: error.errorMessage, + }), + ); + } else if (err.response?.status == 401) { + yield put( + setAuth({ + isAuthorised: false, + }), + ); + yield put( + showMessage({ + isError: true, + errorCode: err.response?.statusText, + messageText: 'Не авторизован', + }), + ); + } else { + yield put( + showMessage({ + isError: true, + errorCode: 'InternalServerError', + messageText: 'Ошибка сервера', + }), + ); + } + } else { + debugLog(err); + } + } + }; diff --git a/Front/src/common/types/common.d.ts b/Front/src/common/types/common.d.ts new file mode 100644 index 00000000..67f8f83c --- /dev/null +++ b/Front/src/common/types/common.d.ts @@ -0,0 +1,17 @@ +export interface CommonState { + enableSockets: boolean; + message?: MessageInfo; + messageQueue: MessageInfo[]; +} + +export interface MessageInfo { + isError: boolean; + errorCode: string; + messageText: string; +} + +export interface ErrorInfo { + error: boolean; + errorCode: string; + errorMessage: string; +} diff --git a/Front/src/docs/content/gamedocuments.module.less b/Front/src/docs/content/gamedocuments.module.less new file mode 100644 index 00000000..3ee02d0b --- /dev/null +++ b/Front/src/docs/content/gamedocuments.module.less @@ -0,0 +1,10 @@ +@import '/common/content/common.less'; + +.gamedocuments { + &:extend(.ui-panel all); + // &:after { + // // content: 'Обучение'; + // } + padding-top: 15px; + overflow-x: auto; +} diff --git a/Front/src/docs/content/index.tsx b/Front/src/docs/content/index.tsx new file mode 100644 index 00000000..4ff500cc --- /dev/null +++ b/Front/src/docs/content/index.tsx @@ -0,0 +1,154 @@ +import { Col, Container, Row } from 'react-bootstrap'; + +import classes from './gamedocuments.module.less'; + +const GameDocuments = () => { + return ( + + + +
+

Цель игры

+

+ Цель игры ясна как день: найти и перетащить к себе на корабль как можно больше монет, + спрятанных на острове. Кто принёс на свой корабль наибольшее число монет, тот и победил. +

+

Как ходить

+

+ Первыми начинает команда в самом низу карты (на 6 часов). Далее участники ходят по очереди + по часовой стрелке. За один ход производится одно из следующих действий: +

+
    +
  1. + Корабль (хотя бы с одним пиратом) сдвигается вдоль берега на 1 клетку. Корабль может + плавать только вдоль своей стороны острова. Поворачивать за угол он не умеет. (делая ход + кораблём, если по пути попадётся ваш пират, улетевший в воду, он встаёт на корабль.) +
  2. +
  3. Пират сходит с корабля на берег — только на клетку прямо перед кораблём.
  4. +
  5. + Пират возвращается на корабль (с добычей или без) с клетки прямо перед кораблём или по + диагонали. Для возвращения на корабль также можно воспользоваться другими клетками поля: + стрелками, конём, воздушным шаром и др. (смотрите раздел Значение клеток поля). (Пират + может зайти только на свой или дружественный (при игре двое на двое) корабль: при + соприкосновении с вражеским кораблём пират умирает.) +
  6. +
  7. + По суше пират ходит на одну клетку по вертикали, горизонтали или диагонали. Если клетка + закрыта (перевернута рубашкой вверх), пират открывает её и выполняет действие, + предусмотренное рисунком (смотрите раздел Значение клеток поля). Переворачивать не + открытую клетку нужно наугад, не заглядывая под рубашку, чтобы например не повернуть + стрелку как удобнее. Открывать неизведанные земли пират может только с пустыми руками + (без монеты). Пират также может ходить по открытым ранее клеткам, выполняя все указанные + на них действия. +
  8. +
  9. + Пираты могут плавать и выбираться из воды на свой корабль. За ход пират может проплыть + одну клетку вдоль берега. Наткнувшись на вражеский корабль, он умирает (смотрите раздел + Смерть пирату!). Прыгать с берега в море пират не умеет. Выбираться на сушу из воды — + тоже. Зато пират может огибать остров вплавь. +
  10. +
+

За ход можно сходить только одним пиратом или кораблём.

+

Пропускать ход нельзя.

+

+ На одной клетке может находиться несколько пиратов с вашего или дружественного (при игре + двое на двое) корабля. +

+

+ Если за вас играет кто-то из обитателей острова (Пятница, миссионер или Бен Ганн), то за + один ход вы можете сходить либо пиратом, либо обитателем острова, либо кораблем. +

+

Как добывать золото

+

+ По проверенным данным на этом богом забытом острове спрятано 17 кладов разной ценности, + включая сокровище испанского галеона. Если, перевернув клетку поля, вы обнаружили сундук с + сокровищами, выложите на клетку столько монет, сколько указано на нём (римская цифра). Но не + радуйтесь раньше времени! Золото можно считать своим, только если вам удалось перетащить его + к себе на корабль (при этом монета убирается с игрового поля в вашу копилку). +

+

Каждый уважающий себя пират может тащить на себе только одну монету.

+

Перемещаться с монетой можно только по открытым клеткам.

+

+ Бить врага с золотишком в руках нельзя. Но если уж очень хочется, можно оставить монету на + месте и вперёд, на врага! +

+

+ Если вас, несущего монету, ударил соперник, вы отправляетесь на корабль с пустыми руками, а + поклажа остаётся на месте. +

+

+ Плавать с монетой нельзя. Если пират попал в море с монетой, она тонет (выбывает из игры). + Пират остаётся на плаву. +

+

Как бить врагов

+

Очень просто: для этого нужно переместиться на клетку, где стоит соперник-пират.

+

+ При этом последний улетает к себе на корабль, оставив на месте свою поклажу (если таковая + имелась), и продолжает игру оттуда. Если врагов на клетке было несколько, все они, побитые + переносятся на свои корабли. +

+

+ Бить врага можно только с пустыми руками. Если вы несли монету, можете оставить её на месте + и преспокойно стукнуть ничего не подозревающего соперница. +

+

Если враг окопался в крепости, то бить его нельзя (на то она и крепость).

+

+ Если соперник стоит на клетке-вертушке (джунгли, пустыня, болото, горы), ударить его можно + только если вы отстаёте на один ход. Например, он на цифре III, вы на цифре II. +

+

+ Вертушка — одна из немногих клеток, на которой одновременно могут находиться пираты + враждующих команд. +

+

+ Врагов может бить также живущий на острове пират Бен Ганн или миссионер (но предварительно + нужно напоить его ромом), если они играют за вас, конечно же. +

+

Смерть пирату!

+

+ Пират умирает (выбывает из игры) в следующих щекотливых ситуациях: +

    +
  1. + При соприкосновении с вражеским кораблём (если пират и корабль оказались на одной + клетке). +
  2. +
  3. Если в море его ударил противник (оказался с ним на одной клетке).
  4. +
  5. Если он попал в лапы людоеду.
  6. +
  7. + Если он попал в цикл — ситуацию, при которой пират застревает на одном месте и не + может двигаться куда либо дальше (например, смотрящие друг на друга стрелки). +
  8. +
+

+

+ Оживить умершего товарища можно, зайдя в крепость к симпатичной аборигенке. На возрождение + одного пирата потребуется один ход. Новорожденный пират начинает ход прямо из крепости. + Естественно, больше трёх пиратов каждого цвета на поле существовать не может. +

+

+ Если пирата ударил соперник, он не умирает, а переносится на свой корабль и продолжает игру + оттуда. +

+

Победа

+

+ В настольной драке побеждает тот, кто погрузил на свой корабль больше золота. Людские потери + не в счёт. Возникающие при игре споры решайте на месте кинжалами и пистолетами, не опускаясь + до примитивного мордобоя. +

+

Игра двое на двое

+

+ При игре двое на двое пираты с противоположных кораблей действуют сообща (белые и чёрные + против красных и жёлтых). Союзники могут спокойно стоять на одной клетке, пользоваться + дружественными кораблями как своими, в том числе носить на них честно украденное золото. + Однако, если пирата ударил соперник, он возвращается именно на свой корабль. При этом + очередность хода (по часовой стрелке) всегда соблюдается! В конце игры подсчитывается + награбленное сообща золото. +

+
+ +
+
+ ); +}; + +export default GameDocuments; diff --git a/Front/src/game/constants.ts b/Front/src/game/constants.ts new file mode 100644 index 00000000..2f48d807 --- /dev/null +++ b/Front/src/game/constants.ts @@ -0,0 +1,8 @@ +export const TooltipTypes = { + SkipMove: 'skipmove', + GroundHole: 'groundhole', + Respawn: 'respawn', + Seajump: 'seajump', + SomeFields: 'somefields', + NoTooltip: 'notooltip', +}; diff --git a/Front/src/game/content/components/controls/controls.module.less b/Front/src/game/content/components/controls/controls.module.less new file mode 100644 index 00000000..1013e069 --- /dev/null +++ b/Front/src/game/content/components/controls/controls.module.less @@ -0,0 +1,18 @@ +@import '/common/content/common.less'; + +.statistic { + &:extend(.ui-panel all); + &:after { + content: 'Статистика'; + } +} + +.teams { + margin: '15px 15px 0 15px'; + color: white; + + > div { + border-style: solid; + border-width: 3px; + } +} diff --git a/Front/src/game/content/components/controls/index.tsx b/Front/src/game/content/components/controls/index.tsx new file mode 100644 index 00000000..1b59313b --- /dev/null +++ b/Front/src/game/content/components/controls/index.tsx @@ -0,0 +1,68 @@ +import cn from 'classnames'; +import { Alert } from 'react-bootstrap'; +import { PiBeerBottleThin, PiCoinVerticalThin } from 'react-icons/pi'; +import { useSelector } from 'react-redux'; + +import classes from './controls.module.less'; +import { Constants } from '/app/constants'; +import { getGameSettings, getGameStatistics, getTeamScores } from '/game/redux/gameSlice'; + +function Controls() { + const { gameId, mapSize, mapId, tilesPackName, gameMode } = useSelector(getGameSettings); + const stat = useSelector(getGameStatistics); + const teamScores = useSelector(getTeamScores); + + return ( + <> +
+
+ ИД игры: {gameId} +
+
+ Код карты: {mapId} +
+
+ Игровой набор: {tilesPackName} +
+
+ Режим игры:{' '} + {gameMode == Constants.gameModeTypes.TwoPlayersInTeam ? '2x2' : 'каждый сам за себя'} +
+
+ Размер карты: {mapSize} +
+
+ Номер хода: {stat?.turnNumber} +
+
+ {teamScores?.map((it) => ( +
+
{it?.name}
+
+ + {it.bottles} + + {it.coins} +
+
+ ))} +
+
+ + {stat?.gameMessage != undefined && ( + + {stat?.gameMessage} + + )} + + ); +} + +export default Controls; diff --git a/Front/src/game/content/components/leftSidebar/index.tsx b/Front/src/game/content/components/leftSidebar/index.tsx new file mode 100644 index 00000000..b3622ac3 --- /dev/null +++ b/Front/src/game/content/components/leftSidebar/index.tsx @@ -0,0 +1,120 @@ +import cn from 'classnames'; +import { useState } from 'react'; +import { Button, Form, InputGroup } from 'react-bootstrap'; +import Image from 'react-bootstrap/Image'; +import { useDispatch, useSelector } from 'react-redux'; + +import { + chooseHumanPirate, + getCurrentPlayerPirates, + getIncludeMovesWithRum, + getPirateAutoChange, + getRumBottles, + getUserSettings, + saveMySettings, + setIncludeMovesWithRum, + setPirateAutoChange, +} from '../../../redux/gameSlice'; +import './leftSidebar.less'; +import PirateIcon from './pirateIcon'; + +function LeftSidebar() { + const dispatch = useDispatch(); + + const currentPlayerPirates = useSelector(getCurrentPlayerPirates); + const hasPirateAutoChange = useSelector(getPirateAutoChange); + const includeMovesWithRum = useSelector(getIncludeMovesWithRum); + const rumBottlesCount = useSelector(getRumBottles); + const userSettings = useSelector(getUserSettings); + + const [gameSpeed, setGameSpeed] = useState(userSettings.gameSpeed || 0); + + const onClick = (girl: GamePirate, withCoinAction: boolean) => () => + dispatch(chooseHumanPirate({ pirate: girl.id, withCoinAction })); + + const pirateAutoChangeToggle = (event: { target: { checked: boolean } }) => + dispatch(setPirateAutoChange(event.target.checked)); + const includeMovesWithRumToggle = (event: { target: { checked: boolean } }) => + dispatch(setIncludeMovesWithRum(event.target.checked)); + + const increaseSpeed = () => { + if (gameSpeed >= 10) return; + dispatch( + saveMySettings({ + ...userSettings, + gameSpeed: gameSpeed + 1, + }), + ); + setGameSpeed((prev) => prev + 1); + }; + + const decreaseSpeed = () => { + if (gameSpeed <= 0) return; + dispatch( + saveMySettings({ + ...userSettings, + gameSpeed: gameSpeed - 1, + }), + ); + setGameSpeed((prev) => prev - 1); + }; + + return ( + <> + {currentPlayerPirates && + currentPlayerPirates.map((girl, index) => ( + + ))} + + + + + x {rumBottlesCount} + + } + checked={includeMovesWithRum} + onChange={includeMovesWithRumToggle} + /> + Автовыбор пиратки} + checked={hasPirateAutoChange} + onChange={pirateAutoChangeToggle} + /> +
+ Задержка хода + + + + + +
+
+ + ); +} + +export default LeftSidebar; diff --git a/Front/src/game/content/components/leftSidebar/leftSidebar.less b/Front/src/game/content/components/leftSidebar/leftSidebar.less new file mode 100644 index 00000000..aa2c529a --- /dev/null +++ b/Front/src/game/content/components/leftSidebar/leftSidebar.less @@ -0,0 +1,21 @@ +.photo-position { + position: relative; + + & > input { + margin-left: 0 !important; + } +} + +.rum-bottle { + background: transparent; + width: 30px; + height: 67px; +} + +.bottles-count { + font-size: x-large; +} + +.rum-bottle-disabled { + filter: grayscale(100%); +} diff --git a/Front/src/game/content/components/leftSidebar/pirateIcon.less b/Front/src/game/content/components/leftSidebar/pirateIcon.less new file mode 100644 index 00000000..0f5e0d63 --- /dev/null +++ b/Front/src/game/content/components/leftSidebar/pirateIcon.less @@ -0,0 +1,33 @@ +.photo { + width: 100px; + height: 100px; + border: 3px gray solid; + margin-bottom: 10px; +} + +.photo-active { + border: 6px coral solid; +} + +.moneta { + &:extend(.basething); + top: 60px; + left: 60px; + width: 30px; + height: 30px; + border: 1px gray solid; + cursor: pointer; +} + +.rum { + &:extend(.basething); + top: 20px; + left: 40px; + width: 70px; + height: 70px; +} + +.basething { + position: absolute; + background: transparent; +} diff --git a/Front/src/game/content/components/leftSidebar/pirateIcon.tsx b/Front/src/game/content/components/leftSidebar/pirateIcon.tsx new file mode 100644 index 00000000..da8cfaaa --- /dev/null +++ b/Front/src/game/content/components/leftSidebar/pirateIcon.tsx @@ -0,0 +1,57 @@ +import cn from 'classnames'; +import Image from 'react-bootstrap/Image'; + +import './pirateIcon.less'; + +interface PirateProps { + pirate: GamePirate; + onClick: () => void; + onCoinClick: () => void; +} + +const PirateIcon = ({ pirate, onClick, onCoinClick }: PirateProps) => { + const isDisabled = pirate.isDrunk || pirate.isInTrap || pirate.isInHole; + let coinImg = '/pictures/ruble.png'; + if (pirate.isDrunk) coinImg = '/pictures/rum.png'; + if (pirate.withBigCoin) coinImg = '/pictures/gold_ruble.png'; + + return ( +
+ + {(pirate.withCoin !== undefined || pirate.withBigCoin !== undefined || pirate.isDrunk) && ( + <> + + {!pirate.withCoin && !pirate.withBigCoin && !pirate.isDrunk && ( + + )} + + )} +
+ ); +}; + +export default PirateIcon; diff --git a/Front/src/game/content/components/map/cell.less b/Front/src/game/content/components/map/cell.less new file mode 100644 index 00000000..d1978ddf --- /dev/null +++ b/Front/src/game/content/components/map/cell.less @@ -0,0 +1,14 @@ +.cell { + position: absolute; + background-size: cover; + border-width: 0px; + z-index: 0; +} + +.cell-active { + border: 3px coral solid; +} + +.cell-dark { + filter: brightness(60%); +} diff --git a/Front/src/game/content/components/map/cell.tsx b/Front/src/game/content/components/map/cell.tsx new file mode 100644 index 00000000..90dbda8f --- /dev/null +++ b/Front/src/game/content/components/map/cell.tsx @@ -0,0 +1,188 @@ +import cn from 'classnames'; +import { RefObject } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { TooltipRefProps } from 'react-tooltip'; + +import { TooltipTypes } from '../../../constants'; +import gameHub from '../../../hub/gameHub'; +import { CalcTooltipType } from '../../../logic/components/calcTooltipType'; +import { getGameField, getGameSettings } from '../../../redux/gameSlice'; +import { AvailableMove, FieldState, GameState } from '../../../types'; +import './cell.less'; +import Level from './level'; +import LevelZero from './levelZero'; +import store from '/app/store'; +import { showMessage } from '/common/redux/commonSlice'; +import { hasFreeMoney } from '/game/logic/gameLogic'; + +interface CellAvailableMove extends AvailableMove { + img?: string; + rotate?: number; +} + +interface CellProps { + row: number; + col: number; + tooltipRef: RefObject; +} + +function Cell({ row, col, tooltipRef }: CellProps) { + const field = useSelector<{ game: GameState }, FieldState>((state) => getGameField(state, row, col)); + const { gameId, cellSize, pirateSize } = useSelector(getGameSettings); + const hasMove = field.availableMoves.length > 0; + + const dispatch = useDispatch(); + + const onClick = () => { + if (gameId == undefined) { + dispatch( + showMessage({ + isError: true, + errorCode: 'HasNoGameId', + messageText: 'Не найден ключ игры', + }), + ); + + return; + } + + const makeMove = (move: AvailableMove) => { + gameHub.makeGameMove({ + gameId: gameId, + turnNum: move.num, + pirateId: move.pirateId, + }); + }; + const tooltipOnClick = (move: AvailableMove) => () => { + makeMove(move); + tooltipRef.current?.close(); + }; + + const gameState = store.getState().game as GameState; + const tooltipType = CalcTooltipType({ row, col, field, state: gameState }); + switch (tooltipType) { + case TooltipTypes.Respawn: + case TooltipTypes.GroundHole: + case TooltipTypes.SkipMove: + case TooltipTypes.Seajump: + tooltipRef.current?.open({ + anchorSelect: `#cell_${col}_${row}`, + content: ( +
+ ), + }); + break; + case TooltipTypes.SomeFields: { + const moves = [] as CellAvailableMove[]; + field.availableMoves.forEach((it) => { + const cell = gameState.fields[it.prev!.y][it.prev!.x]; + moves.push({ + ...it, + img: cell.image!, + rotate: cell.rotate, + }); + }); + + tooltipRef.current?.open({ + anchorSelect: `#cell_${col}_${row}`, + content: ( + <> + {moves.map((it) => ( + 0 ? `rotate(${it.rotate * 90}deg)` : 'none', + margin: '5px 3px', + width: pirateSize * 1.6, + height: pirateSize * 1.6, + cursor: 'pointer', + }} + src={it.img} + onClick={tooltipOnClick(it)} + /> + ))} + + ), + }); + break; + } + case TooltipTypes.NoTooltip: + default: + makeMove(field.availableMoves[0]); + break; + } + }; + + return ( + <> +
0 ? `rotate(${field.rotate * 90}deg)` : 'none', + opacity: hasMove ? '0.5' : '1', + cursor: hasMove ? 'pointer' : 'default', + }} + onClick={hasMove ? onClick : undefined} + >
+ {field.levels && + field.levels.length === 1 && + (hasFreeMoney(field.levels[0]) || + (field.levels[0].features && field.levels[0].features.length > 0)) && ( + + )} + {field.levels && + field.levels.length > 1 && + field.levels + .filter((it) => it.features && it.features.length > 0) + .map((it, idx) => ( + + ))} + {field.levels && + field.levels.length > 1 && + field.levels + .filter((it) => hasFreeMoney(it)) + .map((it, idx) => ( + + ))} + + ); +} + +export default Cell; diff --git a/Front/src/game/content/components/map/coinPhoto.tsx b/Front/src/game/content/components/map/coinPhoto.tsx new file mode 100644 index 00000000..4446b565 --- /dev/null +++ b/Front/src/game/content/components/map/coinPhoto.tsx @@ -0,0 +1,62 @@ +import { useDispatch } from 'react-redux'; + +import { chooseHumanPirate } from '../../../redux/gameSlice'; +import { GameLevel } from '/game/types/gameContent'; + +interface CoinPhotoCalcs { + imageClass: string; + count?: number; +} +interface CoinPhotoProps { + level: GameLevel; + pirateSize: number; +} + +const CoinPhoto = ({ level, pirateSize }: CoinPhotoProps) => { + const dispatch = useDispatch(); + + const data: CoinPhotoCalcs = { + imageClass: 'treasure', + }; + if (level.info.bigCoins === level.pirates.bigCoins) { + data.imageClass = 'coins'; + data.count = level.info.coins - level.pirates.coins; + } else if (level.info.coins === level.pirates.coins) { + data.imageClass = 'bigCoins'; + data.count = level.info.bigCoins - level.pirates.bigCoins; + } + + const coinSize = pirateSize * 0.6; + + const onClick = (girlId: string) => { + dispatch(chooseHumanPirate({ pirate: girlId, withCoinAction: true })); + }; + + return level.freeCoinGirlId ? ( +
onClick(level.freeCoinGirlId!)} + > + {data.count} +
+ ) : ( +
+ {data.count} +
+ ); +}; + +export default CoinPhoto; diff --git a/Front/src/game/content/components/map/featurePhoto.tsx b/Front/src/game/content/components/map/featurePhoto.tsx new file mode 100644 index 00000000..94c9cdc1 --- /dev/null +++ b/Front/src/game/content/components/map/featurePhoto.tsx @@ -0,0 +1,28 @@ +import cn from 'classnames'; +import Image from 'react-bootstrap/Image'; + +import './cell.less'; +import { GameLevelFeature } from '/game/types/gameContent'; + +interface FeaturePhotoProps { + feature: GameLevelFeature; + featureSize: number; + hasClick?: boolean; +} + +const FeaturePhoto = ({ feature, featureSize, hasClick }: FeaturePhotoProps) => { + return ( + <> + + + ); +}; +export default FeaturePhoto; diff --git a/Front/src/game/content/components/map/index.tsx b/Front/src/game/content/components/map/index.tsx new file mode 100644 index 00000000..c8e4b6d8 --- /dev/null +++ b/Front/src/game/content/components/map/index.tsx @@ -0,0 +1,56 @@ +import { useRef } from 'react'; +import { Tooltip, TooltipRefProps } from 'react-tooltip'; + +import MapPirates from '../mapPirates'; +import Cell from './cell'; +import './map.less'; + +interface MapProps { + mapSize: number; + cellSize: number; +} + +function Map({ mapSize, cellSize }: MapProps) { + const mapWidth = (cellSize + 1) * mapSize - 1; + const actionsTooltip = useRef(null); + + return ( + <> +
+
+ {Array(mapSize) + .fill(0) + .map((_, rIndex) => ( +
+ {Array(mapSize) + .fill(0) + .map((_, cIndex) => ( +
+ +
+ ))} +
+ ))} +
+ +
+ + + ); +} + +export default Map; diff --git a/Front/src/game/content/components/map/level.tsx b/Front/src/game/content/components/map/level.tsx new file mode 100644 index 00000000..c1279e15 --- /dev/null +++ b/Front/src/game/content/components/map/level.tsx @@ -0,0 +1,108 @@ +import CoinPhoto from './coinPhoto'; +import FeaturePhoto from './featurePhoto'; +import { hasFreeMoney } from '/game/logic/gameLogic'; +import { FieldState } from '/game/types'; +import { GameLevel } from '/game/types/gameContent'; + +interface LevelProps { + cellSize: number; + pirateSize: number; + field: FieldState; + data: GameLevel; + hasFeaturesOnly?: boolean; + onClick?: () => void; +} + +/// Для отображение монет или черепов на многоуровневых клетках +const Level = ({ cellSize, pirateSize, field, data, hasFeaturesOnly, onClick }: LevelProps) => { + const mul_x_times = cellSize / 50; + const addSize = (mul_x_times - 1) * 5; + const unitSize = cellSize - pirateSize / 2; + + const getMarginTop = (field: FieldState, level: number) => { + if (field.levels?.length === 3) { + if (level === 2) return unitSize * 0.7 + addSize; + else if (level == 1) return unitSize * 0.3 + addSize; + } else if (field.levels?.length === 2) { + if (level === 1) return unitSize * 0.7 + addSize; + } else if (field.levels?.length === 4) { + if (level === 3) return unitSize * 0.7 + addSize; + else if (level == 2) return unitSize * 0.5; + else if (level == 1) return unitSize * 0.2; + } else if (field.levels?.length === 5) { + if (level === 4) return addSize; + else if (level == 3) return addSize; + else if (level == 2) return unitSize * 0.3; + else if (level == 1) return unitSize * 0.7 - addSize; + else if (level == 0) return unitSize * 0.7; + } + return 0; + }; + + const getMarginLeft = (field: FieldState, level: number) => { + if (field.levels?.length === 3) { + if (level === 2) return unitSize * 0.7 + addSize; + else if (level == 1) return addSize * 3; + else if (level == 0) return unitSize * 0.7 + addSize; + } else if (field.levels?.length === 2) { + if (level === 0) return unitSize * 0.7 + addSize; + } else if (field.levels?.length === 4) { + if (level === 3) return unitSize * 0.7 - addSize; + else if (level == 2) return addSize * 2; + else if (level == 1) return unitSize * 0.5 + addSize; + else if (level == 0) return addSize * 2; + } else if (field.levels?.length === 5) { + if (level === 4) return unitSize * 0.7 + addSize; + else if (level === 3) return unitSize * 0.3 + addSize; + else if (level == 2) return addSize; + else if (level == 1) return addSize * 3; + else if (level == 0) return unitSize * 0.7; + } + + return 0; + }; + + const getWidth = (field: FieldState): number | undefined => { + if (field.levels?.length === 1) { + return cellSize; + } + return undefined; + }; + + if (hasFeaturesOnly) { + return ( +
+ {data.features && data.features.length > 0 && ( + + )} +
+ ); + } + + return ( +
+ {hasFreeMoney(data) && } +
+ ); +}; + +export default Level; diff --git a/Front/src/game/content/components/map/levelZero.tsx b/Front/src/game/content/components/map/levelZero.tsx new file mode 100644 index 00000000..40df9a1f --- /dev/null +++ b/Front/src/game/content/components/map/levelZero.tsx @@ -0,0 +1,67 @@ +import CoinPhoto from './coinPhoto'; +import FeaturePhoto from './featurePhoto'; +import { hasFreeMoney } from '/game/logic/gameLogic'; +import { GameLevel } from '/game/types/gameContent'; + +interface LevelZeroProps { + cellSize: number; + pirateSize: number; + data: GameLevel; + onClick?: () => void; +} + +/// Для отображение монет и черепов на одноуровневых клетках +const LevelZero = ({ cellSize, pirateSize, data, onClick }: LevelZeroProps) => { + const addSize = data.features && data.features.length > 3 ? cellSize / 10 : 0; + const unitSize = cellSize - pirateSize; + + const getMarginTop = (idx: number) => { + if (idx === 0) return -addSize; + if (idx === 1) return unitSize + addSize; + if (idx === 2) return unitSize + addSize; + if (idx === 3) return unitSize / 2; + return -addSize; + }; + + const getMarginLeft = (idx: number) => { + if (idx === 0) return -addSize; + if (idx === 1) return unitSize + addSize; + if (idx === 2) return -addSize; + if (idx === 3) return unitSize / 2; + return -addSize; + }; + + return ( + <> + {hasFreeMoney(data) && ( +
+ +
+ )} + {data.features?.map((feature, idx) => ( +
+ +
+ ))} + + ); +}; + +export default LevelZero; diff --git a/Front/src/game/content/components/map/map.less b/Front/src/game/content/components/map/map.less new file mode 100644 index 00000000..b9ea73bc --- /dev/null +++ b/Front/src/game/content/components/map/map.less @@ -0,0 +1,82 @@ +@import '/common/content/common.less'; + +.map-container { + position: relative; + + @media (max-width: @screen-md-max) { + overflow-x: auto; + } +} + +.map { + display: table; +} + +.map-cell { + display: table-cell; +} + +.map-row { + display: table-row; +} + +.level { + position: absolute; + z-index: 3; +} + +.feature { + position: absolute; + z-index: 1; +} + +.features { + position: relative; + background-size: cover; + float: left; + color: white; +} + +.treasure { + background-image: url('/pictures/treasure.png'); + &:extend(.baseicon); +} + +.coins { + background-image: url('/pictures/ruble_empty.png'); + &:extend(.baseicon); +} + +.bigCoins { + background-image: url('/pictures/gold_ruble_empty.png'); + &:extend(.baseicon); +} + +.respawn { + background-image: url('/pictures/add-pirate.png'); + &:extend(.baseicon); +} + +.skipmove { + background-image: url('/pictures/hourglass.png'); + &:extend(.baseicon); +} + +.groundhole { + background-image: url('/pictures/ground-hole.png'); + &:extend(.baseicon); +} + +.seajump { + background-image: url('/pictures/jump-into-water.png'); + &:extend(.baseicon); +} + +.baseicon { + background-repeat: no-repeat; + background-size: cover; + float: right; + color: black; + position: relative; + z-index: 5; +} diff --git a/Front/src/game/content/components/mapPirates/animatePirate.tsx b/Front/src/game/content/components/mapPirates/animatePirate.tsx new file mode 100644 index 00000000..65bad23b --- /dev/null +++ b/Front/src/game/content/components/mapPirates/animatePirate.tsx @@ -0,0 +1,66 @@ +import { Animate } from 'react-move'; + +import { girlsMap } from '../../../logic/gameLogic'; +import { PiratePhotoMemoized } from './piratePhotoMemoized'; + +interface AnimatePirateProps { + pirate: GamePirate; + pirateSize: number; + speed: number; + left: number; + top: number; + isCurrentPlayerGirl: boolean; + onTeamPirateClick: (girl: GamePirate, allowChoosing: boolean) => void; +} + +const AnimatePirate = ({ + pirate, + pirateSize, + speed, + left, + top, + isCurrentPlayerGirl, + onTeamPirateClick, +}: AnimatePirateProps) => { + const mapLevel = girlsMap.GetPosition(pirate); + + return ( + + {(state) => { + const { x, y } = state; + + return ( +
p.id == pirate.id)?.order || 0) + 3, + pointerEvents: isCurrentPlayerGirl ? 'auto' : 'none', + }} + > + +
+ ); + }} +
+ ); +}; +export default AnimatePirate; diff --git a/Front/src/game/content/components/mapPirates/index.tsx b/Front/src/game/content/components/mapPirates/index.tsx new file mode 100644 index 00000000..4ecdca6c --- /dev/null +++ b/Front/src/game/content/components/mapPirates/index.tsx @@ -0,0 +1,107 @@ +import { useSelector } from 'react-redux'; + +import { girlsMap } from '../../../logic/gameLogic'; +import { getGameSettings, getPiratesIds } from '../../../redux/gameSlice'; +import MapPirate from './mapPirate'; + +interface MapPiratesProps { + mapSize: number; + cellSize: number; +} + +const MapPirates = ({ mapSize, cellSize }: MapPiratesProps) => { + const piratesIds = useSelector(getPiratesIds); + const { pirateSize } = useSelector(getGameSettings); + + const unitSize = cellSize - pirateSize; + + const mul_x_times = cellSize / 50; + const xAddSize = (mul_x_times - 1) * 5; + const xUnitSize = cellSize - pirateSize / 2; + + const getMarginTop = (girl: GamePiratePosition): number => { + const position = girlsMap.GetPosition(girl); + const levelsCount = position!.levelsCountInCell; + const level = position!.level; + + if (levelsCount === 1) { + const addSize = position!.girls!.length > 3 ? cellSize / 10 : 0; + const idx = position!.girls!.find((it) => it.id == girl.id)?.order; + if (idx === 0) return -addSize; + if (idx === 1) return unitSize + addSize; + if (idx === 2) return unitSize + addSize; + if (idx === 3) return unitSize / 2; + return -addSize; + } else if (levelsCount === 3) { + if (level === 2) return xUnitSize * 0.7 + xAddSize; + else if (level == 1) return xUnitSize * 0.3 + xAddSize; + } else if (levelsCount === 2) { + if (level === 1) return xUnitSize * 0.7 + xAddSize; + } else if (levelsCount === 4) { + if (level === 3) return xUnitSize * 0.7 + xAddSize; + else if (level == 2) return xUnitSize * 0.5; + else if (level == 1) return xUnitSize * 0.2; + } else if (levelsCount === 5) { + if (level === 4) return xAddSize; + else if (level == 3) return xAddSize; + else if (level == 2) return xUnitSize * 0.3; + else if (level == 1) return xUnitSize * 0.7 - xAddSize; + else if (level == 0) return xUnitSize * 0.7; + } + return 0; + }; + + const getMarginLeft = (girl: GamePiratePosition): number => { + const position = girlsMap.GetPosition(girl); + const levelsCount = position!.levelsCountInCell; + const level = position!.level; + + if (levelsCount === 1) { + const addSize = position!.girls!.length > 3 ? cellSize / 10 : 0; + const idx = position!.girls!.find((it) => it.id == girl.id)?.order; + if (idx === 0) return -addSize; + if (idx === 1) return unitSize + addSize; + if (idx === 2) return -addSize; + if (idx === 3) return unitSize / 2; + return -addSize; + } else if (levelsCount === 3) { + if (level === 2) return xUnitSize * 0.7 + xAddSize; + else if (level == 1) return xAddSize * 3; + else if (level == 0) return xUnitSize * 0.7 + xAddSize; + } else if (levelsCount === 2) { + if (level === 0) return xUnitSize * 0.7 + xAddSize; + } else if (levelsCount === 4) { + if (level === 3) return xUnitSize * 0.7 - xAddSize; + else if (level == 2) return xAddSize * 2; + else if (level == 1) return xUnitSize * 0.5 + xAddSize; + else if (level == 0) return xAddSize * 2; + } else if (levelsCount === 5) { + if (level === 4) return xUnitSize * 0.7 + xAddSize; + else if (level === 3) return xUnitSize * 0.3 + xAddSize; + else if (level == 2) return xAddSize; + else if (level == 1) return xAddSize * 3; + else if (level == 0) return xUnitSize * 0.7; + } + + return 0; + }; + + return ( + <> + {piratesIds && + piratesIds.map((girlId) => ( + + ))} + + ); +}; + +export default MapPirates; diff --git a/Front/src/game/content/components/mapPirates/mapPirate.tsx b/Front/src/game/content/components/mapPirates/mapPirate.tsx new file mode 100644 index 00000000..63f32d6c --- /dev/null +++ b/Front/src/game/content/components/mapPirates/mapPirate.tsx @@ -0,0 +1,63 @@ +import { useCallback } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; + +import { girlsMap } from '../../../logic/gameLogic'; +import { chooseHumanPirate, getCurrentPlayerTeam, getPirateById, getUserSettings } from '../../../redux/gameSlice'; +import AnimatePirate from './animatePirate'; +import { GameState } from '/game/types'; + +interface MapPirateProps { + id: string; + pirateSize: number; + getMarginTop: (girl: GamePiratePosition) => number; + getMarginLeft: (girl: GamePiratePosition) => number; + mapSize: number; + cellSize: number; +} + +const MapPirate = ({ id, getMarginTop, getMarginLeft, cellSize, mapSize, pirateSize }: MapPirateProps) => { + const pirate = useSelector<{ game: GameState }, GamePirate | undefined>((state) => getPirateById(state, id)); + const currentHumanTeam = useSelector<{ game: GameState }, TeamState | undefined>((state) => + getCurrentPlayerTeam(state), + ); + + const { gameSpeed: speed } = useSelector(getUserSettings); + const dispatch = useDispatch(); + + const onTeamPirateClick = useCallback( + (girl: GamePirate, allowChoosing: boolean) => { + const mapLevel = girlsMap.GetPosition(girl); + if (!mapLevel || !mapLevel.girls) return; + + const willChangePirate = mapLevel.girls.length > 1 && girl.isActive && allowChoosing; + if (willChangePirate) { + girlsMap.ScrollGirls(mapLevel); + } + dispatch( + chooseHumanPirate({ + pirate: willChangePirate ? mapLevel.girls[mapLevel.girls.length - 1]?.id : girl.id, + withCoinAction: true, + }), + ); + }, + [dispatch], + ); + + if (!pirate) return <>; + + const leftOffset = pirate.position.x * (cellSize + 1) + getMarginLeft(pirate); + const topOffset = (mapSize - 1 - pirate.position.y) * (cellSize + 1) + getMarginTop(pirate); + + return ( + + ); +}; +export default MapPirate; diff --git a/Front/src/game/content/components/mapPirates/piratePhoto.less b/Front/src/game/content/components/mapPirates/piratePhoto.less new file mode 100644 index 00000000..6b9d7a7d --- /dev/null +++ b/Front/src/game/content/components/mapPirates/piratePhoto.less @@ -0,0 +1,17 @@ +.pirates { + position: relative; + background-size: cover; + float: left; + color: white; +} + +.cell-moneta { + position: absolute; + background: transparent; + border: 1px gray solid; +} + +.cell-rum { + position: absolute; + background: transparent; +} diff --git a/Front/src/game/content/components/mapPirates/piratePhoto.tsx b/Front/src/game/content/components/mapPirates/piratePhoto.tsx new file mode 100644 index 00000000..9c2db226 --- /dev/null +++ b/Front/src/game/content/components/mapPirates/piratePhoto.tsx @@ -0,0 +1,76 @@ +import cn from 'classnames'; +import Image from 'react-bootstrap/Image'; + +import { girlsMap } from '../../../logic/gameLogic'; +import './piratePhoto.less'; +import store from '/app/store'; +import { GameState } from '/game/types'; + +interface PiratePhotoProps { + pirate: GamePirate; + pirateSize: number; + isCurrentPlayerGirl: boolean; + onTeamPirateClick: (girl: GamePirate, allowChoosing: boolean) => void; +} + +const PiratePhoto = ({ pirate, pirateSize, isCurrentPlayerGirl, onTeamPirateClick }: PiratePhotoProps) => { + const mapLevel = girlsMap.GetPosition(pirate); + + const coinSize = pirateSize * 0.3 > 15 ? pirateSize * 0.3 : 15; + const addSize = (pirateSize - coinSize - 20) / 10; + const coinPos = pirateSize - coinSize - addSize; + const isDisabled = pirate.isDrunk || pirate.isInTrap || pirate.isInHole; + + let img = ''; + if (pirate.isDrunk) img = '/pictures/rum.png'; + if (pirate.withCoin) img = '/pictures/ruble.png'; + if (pirate.withBigCoin) img = '/pictures/gold_ruble.png'; + + return ( + <> + = 50 ? 4 : 3}px ${pirate.backgroundColor || 'transparent'} solid`, + // -webkit-filter: grayscale(100%); /* Safari 6.0 - 9.0 */ + filter: isDisabled ? 'grayscale(100%)' : undefined, + width: pirateSize, + height: pirateSize, + cursor: isDisabled && isCurrentPlayerGirl ? 'default' : 'pointer', + }} + onClick={(event) => { + if (isCurrentPlayerGirl) { + event.stopPropagation(); + const gameState = store.getState().game as GameState; + const level = + gameState.fields[pirate.position.y][pirate.position.x].levels[pirate.position.level]; + const allowChoosingPirate = + level.pirates.coins === level.info.coins && + level.pirates.bigCoins === level.info.bigCoins && + (mapLevel?.girls?.length || 0) > level.info.coins + level.info.bigCoins; + onTeamPirateClick(pirate, allowChoosingPirate); + } + }} + /> + {(pirate.withCoin || pirate.withBigCoin || pirate.isDrunk) && ( + + )} + + ); +}; +export default PiratePhoto; diff --git a/Front/src/game/content/components/mapPirates/piratePhotoMemoized.tsx b/Front/src/game/content/components/mapPirates/piratePhotoMemoized.tsx new file mode 100644 index 00000000..beb31cd2 --- /dev/null +++ b/Front/src/game/content/components/mapPirates/piratePhotoMemoized.tsx @@ -0,0 +1,5 @@ +import { memo } from 'react'; + +import PiratePhoto from './piratePhoto'; + +export const PiratePhotoMemoized = memo(PiratePhoto); diff --git a/Front/src/game/content/playground.module.less b/Front/src/game/content/playground.module.less new file mode 100644 index 00000000..cc394498 --- /dev/null +++ b/Front/src/game/content/playground.module.less @@ -0,0 +1,8 @@ +@import '/common/content/common.less'; + +.leftSidebar { + &:extend(.ui-panel all); + &:after { + content: 'Пиратки'; + } +} diff --git a/Front/src/game/content/playground.tsx b/Front/src/game/content/playground.tsx new file mode 100644 index 00000000..185b6673 --- /dev/null +++ b/Front/src/game/content/playground.tsx @@ -0,0 +1,33 @@ +import Col from 'react-bootstrap/Col'; +import Row from 'react-bootstrap/Row'; +import { useSelector } from 'react-redux'; + +import Controls from './components/controls'; +import LeftSidebar from './components/leftSidebar'; +import Map from './components/map'; +import classes from './playground.module.less'; +import { getGameSettings } from '/game/redux/gameSlice'; + +function Playground() { + const { cellSize, mapSize } = useSelector(getGameSettings); + + return ( + + {mapSize && cellSize && ( + <> + + + + + + + + + + + )} + + ); +} + +export default Playground; diff --git a/Front/src/game/hub/gameHub.ts b/Front/src/game/hub/gameHub.ts new file mode 100644 index 00000000..30935e05 --- /dev/null +++ b/Front/src/game/hub/gameHub.ts @@ -0,0 +1,43 @@ +import { GameSettings, makeGameMoveRequestProps } from '../types/hubContracts'; +import { debugLog, hubConnection } from '/app/global'; +import { sagaActions } from '/common/sagas'; + +const gameHub = { + startGame: (settings: GameSettings) => { + hubConnection.invoke('start', { settings }).catch((err) => { + debugLog(err); + }); + }, + startPublicGame: (id: number, settings: GameSettings) => { + hubConnection.invoke('startPublic', { id, settings }).catch((err) => { + debugLog(err); + }); + }, + loadGame: (gameId: number) => { + hubConnection.invoke('load', { gameId }).catch((err) => { + debugLog(err); + }); + }, + makeGameMove: (payload: makeGameMoveRequestProps) => { + hubConnection.send('move', payload); + }, + netCreate: (settings: GameSettings) => { + hubConnection.send('netStart', { settings }); + }, + netChange: (id: number, settings: GameSettings) => { + hubConnection.send('netUpdate', { id, settings }); + }, + netJoin: (id: number) => { + hubConnection.send('netJoin', { id }); + }, + getEventHandlers: [ + { name: 'GetStartData', sagaAction: sagaActions.GAME_START_APPLY_DATA }, + { name: 'LoadGameData', sagaAction: sagaActions.GAME_START_LOOKING_DATA }, + { name: 'GetMoveChanges', sagaAction: sagaActions.GAME_TURN_APPLY_DATA }, + { name: 'GetActiveGames', sagaAction: sagaActions.ACTIVE_GAMES_APPLY_DATA }, + { name: 'GetActiveNetGames', sagaAction: sagaActions.NET_GAMES_APPLY_DATA }, + { name: 'GetNetGameData', sagaAction: sagaActions.NET_GAME_APPLY_DATA }, + ], +}; + +export default gameHub; diff --git a/Front/src/game/logic/components/calcTooltipType.ts b/Front/src/game/logic/components/calcTooltipType.ts new file mode 100644 index 00000000..5ba29c00 --- /dev/null +++ b/Front/src/game/logic/components/calcTooltipType.ts @@ -0,0 +1,41 @@ +import { TooltipTypes } from '../../constants'; +import { FieldState, GameState } from '../../types'; + +export interface CalcTooltipTypeProps { + row: number; + col: number; + field: FieldState; + state: GameState; +} + +export const CalcTooltipType = ({ row, col, field, state }: CalcTooltipTypeProps): string => { + const team = state.teams.find((it) => it.id == state.currentHumanTeamId); + const activePirate = state.pirates?.find((it) => it.id == team?.activePirate); + const pirateField = activePirate && state.fields[activePirate.position.y][activePirate.position.x]; + + const move = field.availableMoves[0]; + + if (field.levels.length == 1 && activePirate?.position.y === row && activePirate?.position.x === col) { + if (move.isRespawn) return TooltipTypes.Respawn; + if (field.image?.includes('hole.png')) return TooltipTypes.GroundHole; + return TooltipTypes.SkipMove; + } + + if (field.availableMoves.length > 1 && !field.availableMoves.some((it) => !it.prev)) { + return TooltipTypes.SomeFields; + } + + if ( + state.lastMoves.length > 1 && + ((field.image?.includes('water.png') && + !pirateField?.image?.includes('ship_1.png') && + !pirateField?.image?.includes('ship_2.png') && + !pirateField?.image?.includes('ship_3.png') && + !pirateField?.image?.includes('ship_4.png') && + !pirateField?.image?.includes('water.png')) || // and jump from beach + (field.image?.includes('cannon.png') && !move.isQuake)) + ) { + return TooltipTypes.Seajump; + } + return TooltipTypes.NoTooltip; +}; diff --git a/Front/src/game/logic/components/girlsMap.ts b/Front/src/game/logic/components/girlsMap.ts new file mode 100644 index 00000000..0f19c7cb --- /dev/null +++ b/Front/src/game/logic/components/girlsMap.ts @@ -0,0 +1,51 @@ +import { GirlsLevel, GirlsPositions } from '../gameLogic.types'; + +// словарь, отслеживающий размещение нескольких пираток на одной клетке +// для корректного их смещения относительно друг друга +const girlsMap: GirlsPositions = { + Map: {}, + AddPosition: function (it: GamePiratePosition, levelsCount: number) { + const cachedId = it.position.y * 1000 + it.position.x * 10 + it.position.level; + const level = this.Map[cachedId]; + if (!level) { + this.Map[cachedId] = { + level: it.position.level, + levelsCountInCell: levelsCount, + girls: [{ id: it.id, order: 0 }], + }; + } else { + if (level.girls) { + let ords = level.girls.map((x) => x.order).sort(); + let ord = level.girls.length; + for (let i = 0; i < level.girls.length; ++i) { + if (i != ords[i]) { + ord = i; + break; + } + } + level.girls.push({ id: it.id, order: ord }); + } else { + level.girls = [{ id: it.id, order: 0 }]; + } + } + }, + RemovePosition: function (it: GamePiratePosition) { + const cachedId = it.position.y * 1000 + it.position.x * 10 + it.position.level; + const girlsLevel = this.Map[cachedId]; + if (girlsLevel?.girls != undefined) { + girlsLevel.girls = girlsLevel.girls.filter((girl) => girl.id != it.id); + if (girlsLevel.girls.length == 0) delete this.Map[cachedId]; + } + }, + GetPosition: function (it: GamePiratePosition): GirlsLevel | undefined { + const cachedId = it.position.y * 1000 + it.position.x * 10 + it.position.level; + return this.Map[cachedId]; + }, + ScrollGirls: function (pos: GirlsLevel) { + if (pos && pos.girls && pos.girls.length > 1) { + pos.girls.push(pos.girls.shift()!); + } + }, +}; + +export default girlsMap; diff --git a/Front/src/game/logic/gameLogic.test.ts b/Front/src/game/logic/gameLogic.test.ts new file mode 100644 index 00000000..f006e17f --- /dev/null +++ b/Front/src/game/logic/gameLogic.test.ts @@ -0,0 +1,208 @@ +import { TooltipTypes } from '../constants'; +import reducer, { + applyPirateChanges, + highlightHumanMoves, + initMap, + initPiratePositions, + initTeams, + setCurrentHumanTeam, +} from '../redux/gameSlice'; +import { getMapData } from '../redux/mapDataForTests'; +import { GameState } from '../types'; +import { GameTeamResponse } from '../types/gameSaga'; +import { CalcTooltipType } from './components/calcTooltipType'; +import { Constants } from '/app/constants'; + +const testTeamId = 12; + +const twoTeamsData: GameTeamResponse[] = [ + { + id: 1, + name: 'girls', + coins: 0, + userId: 0, + isHuman: false, + ship: { + x: 5, + y: 0, + }, + }, + { + id: testTeamId, + name: 'boys', + coins: 0, + userId: 2, + isHuman: true, + isCurrentUser: true, + ship: { + x: 5, + y: 10, + }, + }, +]; + +const testPirates: GamePirate[] = [ + { + id: '100', + teamId: testTeamId, + position: { + level: 0, + x: 2, + y: 0, + }, + groupId: '', + photo: '', + photoId: 0, + type: Constants.pirateTypes.Usual, + }, +]; + +const getState = (pirates: GamePirate[]): GameState => ({ + fields: [[]], + lastMoves: [], + gameSettings: { + mapSize: 5, + cellSize: 50, + pirateSize: 15, + tilesPackNames: [], + }, + userSettings: { + groups: [ + Constants.groupIds.girls, + Constants.groupIds.redalert, + Constants.groupIds.orcs, + Constants.groupIds.skulls, + ], + mapSize: 11, + players: ['human', 'robot2', 'robot', 'robot2'], + playersMode: 4, + gameSpeed: 0, + }, + stat: { + turnNumber: 1, + currentTeamId: testTeamId, + currentUserId: 2, + isCurrentUsersMove: true, + isGameOver: false, + gameMessage: '', + }, + teams: [], + pirates: pirates, + currentHumanTeamId: 0, + highlight_x: 0, + highlight_y: 0, + hasPirateAutoChange: true, + includeMovesWithRum: false, +}); + +describe('cell logic tests', () => { + let defaultState: GameState; + + beforeAll(() => { + defaultState = getState(testPirates); + defaultState = reducer(defaultState, initMap(getMapData)); + defaultState = reducer(defaultState, initTeams(twoTeamsData)); + defaultState = reducer(defaultState, initPiratePositions()); + defaultState = reducer(defaultState, setCurrentHumanTeam()); + defaultState = reducer( + defaultState, + highlightHumanMoves({ + moves: [ + { + moveNum: 1, + from: { pirateIds: ['100'], level: 0, x: 2, y: 0 }, + to: { pirateIds: ['100'], level: 0, x: 2, y: 0 }, + withCoin: false, + withBigCoin: false, + withRespawn: false, + withRumBottle: false, + withQuake: false, + }, + { + moveNum: 2, + from: { pirateIds: ['100'], level: 0, x: 2, y: 0 }, + to: { pirateIds: ['100'], level: 0, x: 2, y: 3 }, + withCoin: false, + withBigCoin: false, + withRespawn: false, + withRumBottle: false, + withQuake: false, + }, + { + moveNum: 3, + from: { pirateIds: ['100'], level: 0, x: 2, y: 0 }, + to: { pirateIds: ['100'], level: 0, x: 3, y: 3 }, + withCoin: false, + withBigCoin: false, + withRespawn: false, + withRumBottle: false, + withQuake: true, + }, + ], + }), + ); + }); + + test('Пропускаем ход', () => { + const row = 0; + const col = 2; + const result = CalcTooltipType({ + row, + col, + field: defaultState.fields[row][col], + state: defaultState, + }); + expect(result).toEqual(TooltipTypes.SkipMove); + }); + + test('Прыгаем в воду', () => { + const newState = reducer( + defaultState, + applyPirateChanges({ + changes: [ + { + id: '100', + type: Constants.pirateTypes.Usual, + teamId: testTeamId, + position: { level: 0, x: 2, y: 2 }, + }, + ], + moves: [], + }), + ); + + let row = 4; + let col = 2; + const result = CalcTooltipType({ + row, + col, + field: newState.fields[row][col], + state: newState, + }); + expect(result).toEqual(TooltipTypes.Seajump); + }); + + test('Прыгаем на пушку', () => { + let row = 3; + let col = 2; + const result = CalcTooltipType({ + row, + col, + field: defaultState.fields[row][col], + state: defaultState, + }); + expect(result).toEqual(TooltipTypes.Seajump); + }); + + test('Разыгрываем пушку при разломе', () => { + let row = 3; + let col = 3; + const result = CalcTooltipType({ + row, + col, + field: defaultState.fields[row][col], + state: defaultState, + }); + expect(result).toEqual(TooltipTypes.NoTooltip); + }); +}); diff --git a/Front/src/game/logic/gameLogic.ts b/Front/src/game/logic/gameLogic.ts new file mode 100644 index 00000000..d253189c --- /dev/null +++ b/Front/src/game/logic/gameLogic.ts @@ -0,0 +1,15 @@ +import { GameLevel } from '../types/gameContent'; +import { LevelInfoResponse } from '../types/gameSaga'; +import girlsMap from './components/girlsMap'; + +export { girlsMap }; +export const hasFreeMoney = (level: GameLevel) => { + return level.pirates.coins < level.info.coins || level.pirates.bigCoins < level.info.bigCoins; +}; +export const constructGameLevel = (info: LevelInfoResponse): GameLevel => ({ + info, + pirates: { + coins: 0, + bigCoins: 0, + }, +}); diff --git a/Front/src/game/logic/gameLogic.types.ts b/Front/src/game/logic/gameLogic.types.ts new file mode 100644 index 00000000..219b8e5c --- /dev/null +++ b/Front/src/game/logic/gameLogic.types.ts @@ -0,0 +1,18 @@ +export interface GirlsLevel { + level: number; + levelsCountInCell: number; + girls: GirlsLogicPosition[] | undefined; +} + +export interface GirlsPositions { + Map: { [id: number]: GirlsLevel }; + AddPosition: (it: GamePiratePosition, levelsCount: number) => void; + RemovePosition: (it: GamePiratePosition) => void; + GetPosition: (it: GamePiratePosition) => GirlsLevel | undefined; + ScrollGirls: (pos: GirlsLevel) => void; +} + +export interface GirlsLogicPosition { + id: string; + order: number; +} diff --git a/Front/src/game/redux/gameSlice.test.ts b/Front/src/game/redux/gameSlice.test.ts new file mode 100644 index 00000000..46629464 --- /dev/null +++ b/Front/src/game/redux/gameSlice.test.ts @@ -0,0 +1,708 @@ +import { girlsMap, hasFreeMoney } from '../logic/gameLogic'; +import { GameState } from '../types'; +import { GameTeamResponse } from '../types/gameSaga'; +import reducer, { + applyChanges, + applyPirateChanges, + chooseHumanPirate, + highlightHumanMoves, + initMap, + initPhotos, + initPiratePositions, + initSizes, + initTeams, + removeHumanMoves, + setCurrentHumanTeam, +} from './gameSlice'; +import { getMapData } from './mapDataForTests'; +import { Constants } from '/app/constants'; + +const testTeamId = 12; + +const twoTeamsData: GameTeamResponse[] = [ + { + id: 5, + name: 'girls', + coins: 0, + userId: 0, + isHuman: false, + ship: { + x: 5, + y: 0, + }, + }, + { + id: testTeamId, + isCurrentUser: true, + name: 'boys', + userId: 2, + isHuman: true, + coins: 0, + ship: { + x: 5, + y: 10, + }, + }, +]; + +const fourTeamsData: GameTeamResponse[] = [ + { + id: 5, + name: 'girls', + userId: 0, + isHuman: false, + coins: 0, + ship: { + x: 5, + y: 0, + }, + }, + { + id: testTeamId, + name: 'boys', + userId: 2, + isHuman: true, + coins: 0, + ship: { + x: 0, + y: 5, + }, + }, + { + id: 7, + name: 'cats', + userId: 0, + isHuman: false, + coins: 0, + ship: { + x: 5, + y: 10, + }, + }, + { + id: 8, + name: 'dogs', + userId: 0, + isHuman: false, + coins: 0, + ship: { + x: 10, + y: 5, + }, + }, +]; + +const getPirates = (data: GamePiratePosition[]): GamePirate[] => { + return data.map((it) => ({ + id: it.id, + teamId: testTeamId, + position: it.position, + groupId: '', + photo: '', + photoId: 0, + withCoin: false, + type: Constants.pirateTypes.Usual, + })); +}; + +const testPirates: GamePirate[] = [ + { + id: '100', + teamId: 5, + position: { + level: 0, + x: 2, + y: 0, + }, + groupId: '', + photo: '', + photoId: 0, + type: Constants.pirateTypes.Usual, + }, + { + id: '200', + teamId: testTeamId, + position: { + level: 0, + x: 2, + y: 4, + }, + groupId: '', + photo: '', + photoId: 0, + type: Constants.pirateTypes.Usual, + }, +]; + +const getState = (pirates: GamePirate[]): GameState => ({ + fields: [[]], + lastMoves: [], + gameSettings: { + mapSize: 5, + cellSize: 50, + pirateSize: 15, + tilesPackNames: [], + }, + userSettings: { + groups: [ + Constants.groupIds.girls, + Constants.groupIds.redalert, + Constants.groupIds.orcs, + Constants.groupIds.skulls, + ], + mapSize: 11, + players: ['human', 'robot2', 'robot', 'robot2'], + playersMode: 4, + gameSpeed: 0, + }, + stat: { + turnNumber: 1, + currentTeamId: testTeamId, + currentUserId: 2, + isCurrentUsersMove: true, + isGameOver: false, + gameMessage: '', + }, + teams: [], + pirates: pirates, + currentHumanTeamId: 0, + highlight_x: 0, + highlight_y: 0, + hasPirateAutoChange: true, + includeMovesWithRum: false, +}); + +describe('redux init tests', () => { + let defaultState: GameState; + + beforeAll(() => { + defaultState = getState(testPirates); + }); + + test('Инициализируем карту', () => { + const result: GameState = reducer(defaultState, initMap(getMapData)); + expect(result).toHaveProperty('fields'); + expect(result.fields).toHaveLength(5); + result.fields.forEach((it) => { + expect(it).toHaveLength(5); + }); + }); + + test('Инициализируем команды для игры 1х1', () => { + const result = reducer(defaultState, initTeams(twoTeamsData)); + expect(result.teams).toHaveLength(2); + expect(result.teams[0].group.id).toEqual(Constants.groupIds.girls); + expect(result.teams[1].group.id).toEqual(Constants.groupIds.orcs); + }); + + test('Инициализируем команды для игры 2х2', () => { + const result = reducer(defaultState, initTeams(fourTeamsData)); + expect(result.teams).toHaveLength(4); + expect(result.teams[0].group.id).toEqual(Constants.groupIds.girls); + expect(result.teams[1].group.id).toEqual(Constants.groupIds.redalert); + }); + + test('Определяем фотки пираток', () => { + const currentState = reducer(defaultState, initTeams(twoTeamsData)); + const result = reducer(currentState, initPhotos()); + expect(result.pirates).not.toBeUndefined(); + expect(result.pirates).not.toBeNull(); + expect(result.pirates).toHaveLength(2); + result.pirates!.forEach((it) => { + expect(it.photoId).toBeGreaterThan(0); + }); + expect(result.pirates![0].groupId).toEqual(Constants.groupIds.girls); + expect(result.pirates![0].photo).toContain(Constants.groupIds.girls + '/pirate_'); + expect(result.pirates![0].backgroundColor).toEqual('DarkRed'); + expect(result.pirates![1].groupId).toEqual(Constants.groupIds.orcs); + expect(result.pirates![1].photo).toContain(Constants.groupIds.orcs + '/pirate_'); + expect(result.pirates![1].backgroundColor).toEqual('DarkBlue'); + }); + + test('Определяем размеры объектов на карте', () => { + let result = reducer( + defaultState, + initSizes({ + width: 1000, + height: 500, + }), + ); + expect(result.gameSettings.cellSize).toEqual(50); + expect(result.gameSettings.pirateSize).toBeGreaterThanOrEqual(25); + expect(result.gameSettings.pirateSize).toBeLessThanOrEqual(30); + result = reducer( + defaultState, + initSizes({ + width: 720, + height: 680, + }), + ); + expect(result.gameSettings.cellSize).toEqual(120); + expect(result.gameSettings.pirateSize).toBeGreaterThanOrEqual(60); + expect(result.gameSettings.pirateSize).toBeLessThanOrEqual(70); + }); + + test('Инициализируем словарик с позициями пираток', () => { + reducer(defaultState, initPiratePositions()); + expect(girlsMap.Map).toEqual( + expect.objectContaining({ + '20': { girls: [{ id: '100', order: 0 }], level: 0, levelsCountInCell: 1 }, + '4020': { girls: [{ id: '200', order: 0 }], level: 0, levelsCountInCell: 1 }, + }), + ); + }); + + test('Инициализируем пираток на карте и словарик с позициями пираток', () => { + let currentState = reducer(defaultState, initMap(getMapData)); + currentState = reducer(currentState, initTeams(twoTeamsData)); + const result = reducer( + currentState, + applyPirateChanges({ + changes: testPirates.map((it) => ({ ...it })), + moves: [], + }), + ); + expect(result.pirates).toHaveLength(2); + expect(girlsMap.Map).toEqual( + expect.objectContaining({ + '20': { girls: [{ id: '100', order: 0 }], level: 0, levelsCountInCell: 1 }, + '4020': { girls: [{ id: '200', order: 0 }], level: 0, levelsCountInCell: 1 }, + }), + ); + }); + + test('Устанавливаем текущую команду', () => { + const result = reducer(defaultState, initTeams(twoTeamsData)); + expect(reducer(result, setCurrentHumanTeam())).toEqual( + expect.objectContaining({ + currentHumanTeamId: testTeamId, + }), + ); + }); +}); + +describe('redux basic tests', () => { + let previousState: GameState; + + beforeAll(() => { + const pirates = getPirates([ + { id: '100', position: { level: 0, x: 2, y: 0 } }, + { id: '200', position: { level: 0, x: 2, y: 4 } }, + { id: '300', position: { level: 0, x: 2, y: 4 } }, + ]); + + previousState = getState(pirates); + previousState = reducer(previousState, initMap(getMapData)); + previousState = reducer(previousState, initTeams(twoTeamsData)); + previousState = reducer( + previousState, + applyPirateChanges({ + changes: pirates, + moves: [], + }), + ); + previousState = reducer(previousState, setCurrentHumanTeam()); + }); + + test('Автовыбор пиратки, для которой возможны ходы, и подсвечивание её ходов', () => { + expect(previousState.fields[2][2].availableMoves).toHaveLength(0); + expect(previousState.highlight_x).toEqual(0); + expect(previousState.highlight_y).toEqual(0); + + const result = reducer( + previousState, + highlightHumanMoves({ + moves: [ + { + moveNum: 1, + from: { pirateIds: ['200'], level: 0, x: 2, y: 4 }, + to: { pirateIds: ['200'], level: 0, x: 2, y: 2 }, + withCoin: true, + withBigCoin: false, + withRespawn: false, + withRumBottle: false, + withQuake: false, + }, + ], + }), + ); + + expect(result.fields[2][2].availableMoves).toHaveLength(1); + expect(result.teams.find((it) => it.id === testTeamId)?.activePirate).toEqual('200'); + expect(result.highlight_x).toEqual(2); + expect(result.highlight_y).toEqual(4); + const girl = result.pirates?.find((it) => it.id == '200'); + expect(girl).not.toBeUndefined(); + expect(girl).not.toBeNull(); + expect(girl?.isActive).toBeTruthy(); + }); + + test('Убираем подсветку ходов', () => { + const currentState = reducer( + previousState, + highlightHumanMoves({ + moves: [ + { + moveNum: 1, + from: { pirateIds: ['200'], level: 0, x: 2, y: 4 }, + to: { pirateIds: ['200'], level: 0, x: 2, y: 2 }, + withCoin: true, + withBigCoin: false, + withRespawn: false, + withRumBottle: false, + withQuake: false, + }, + ], + }), + ); + + expect(currentState.fields[2][2].availableMoves).toHaveLength(1); + + const result = reducer(currentState, removeHumanMoves()); + + expect(result.fields[2][2].availableMoves).toHaveLength(0); + }); + + test('Выбираем активного пирата', () => { + const result = reducer(previousState, chooseHumanPirate({ pirate: '200', withCoinAction: true })); + expect(result.teams.find((it) => it.id === testTeamId)?.activePirate).toEqual('200'); + expect(result.highlight_x).toEqual(2); + expect(result.highlight_y).toEqual(4); + const boy = result.pirates?.find((it) => it.id == '200'); + expect(boy).not.toBeUndefined(); + expect(boy).not.toBeNull(); + expect(boy?.isActive).toBeTruthy(); + }); + + test('Меняем активного пирата', () => { + const currentState = reducer(previousState, chooseHumanPirate({ pirate: '200', withCoinAction: true })); + const result = reducer(currentState, chooseHumanPirate({ pirate: '300', withCoinAction: true })); + expect(result.teams).toContainEqual({ + activePirate: '300', + backColor: 'DarkBlue', + group: { + id: Constants.groupIds.orcs, + extension: '.jpg', + photoMaxId: 6, + }, + name: 'boys', + id: testTeamId, + isHuman: true, + isCurrentUser: true, + }); + expect(result.highlight_x).toEqual(2); + expect(result.highlight_y).toEqual(4); + const pboy = result.pirates?.find((it) => it.id == '200'); + expect(pboy?.isActive).toBeFalsy(); + const sboy = result.pirates?.find((it) => it.id == '300'); + expect(sboy?.isActive).toBeTruthy(); + }); + + test('Уходим с клетки, на клетке - никого', () => { + const currentState = reducer(previousState, chooseHumanPirate({ pirate: '100', withCoinAction: true })); + const result = reducer( + currentState, + applyPirateChanges({ + changes: [ + { + id: '100', + type: Constants.pirateTypes.Usual, + teamId: testTeamId, + position: { level: 0, x: 2, y: 2 }, + }, + ], + moves: [], + }), + ); + expect(result.highlight_x).toEqual(2); + expect(result.highlight_y).toEqual(2); + const boy = result.pirates?.find((it) => it.id == '100'); + expect(boy?.position).toEqual({ level: 0, x: 2, y: 2 }); + expect(girlsMap.Map).toEqual( + expect.objectContaining({ + '2020': { girls: [{ id: '100', order: 0 }], level: 0, levelsCountInCell: 1 }, + '4020': { + girls: [ + { id: '200', order: 0 }, + { id: '300', order: 1 }, + ], + level: 0, + levelsCountInCell: 1, + }, + }), + ); + }); +}); + +describe('redux money actions tests', () => { + let previousState: GameState; + + beforeAll(() => { + const pirates = getPirates([ + { id: '100', position: { level: 0, x: 2, y: 0 } }, + { id: '200', position: { level: 0, x: 2, y: 4 } }, + { id: '300', position: { level: 0, x: 2, y: 4 } }, + ]); + + previousState = getState(pirates); + previousState = reducer(previousState, initMap(getMapData)); + previousState = reducer(previousState, initTeams(twoTeamsData)); + previousState = reducer( + previousState, + applyPirateChanges({ + changes: pirates, + moves: [ + { + moveNum: 1, + from: { pirateIds: ['200'], level: 0, x: 2, y: 4 }, + to: { pirateIds: ['200'], level: 0, x: 2, y: 2 }, + withCoin: true, + withBigCoin: false, + withRespawn: false, + withRumBottle: false, + withQuake: false, + }, + { + moveNum: 2, + from: { pirateIds: ['200'], level: 0, x: 2, y: 4 }, + to: { pirateIds: ['200'], level: 0, x: 2, y: 2 }, + withCoin: false, + withBigCoin: false, + withRespawn: false, + withRumBottle: false, + withQuake: false, + }, + ], + }), + ); + }); + + test('Автоподнятие монеты по возможному действию', () => { + expect(previousState.pirates?.find((it) => it.id == '200')?.withCoin).toBeTruthy(); + const level = previousState.fields[4][2].levels[0]; + expect(level.info).toEqual({ + level: 0, + coins: 2, + bigCoins: 0, + }); + expect(level.pirates).toEqual({ + coins: 1, + bigCoins: 0, + }); + expect(hasFreeMoney(level)).toEqual(true); + }); + + test('Кладём монету', () => { + let currentState = reducer(previousState, setCurrentHumanTeam()); + currentState = reducer(currentState, chooseHumanPirate({ pirate: '200', withCoinAction: true })); + expect(currentState.highlight_x).toEqual(2); + expect(currentState.highlight_y).toEqual(4); + const preLevel = currentState.fields[4][2].levels[0]; + expect(preLevel.info).toEqual({ + level: 0, + coins: 2, + bigCoins: 0, + }); + expect(preLevel.pirates).toEqual({ + coins: 1, + bigCoins: 0, + }); + expect(hasFreeMoney(preLevel)).toEqual(true); + + const result = reducer(currentState, chooseHumanPirate({ pirate: '200', withCoinAction: true })); + + const boy = result.pirates?.find((it) => it.id == '200'); + expect(boy?.withCoin).toBeFalsy(); + expect(boy?.isActive).toBeTruthy(); + const postLevel = result.fields[4][2].levels[0]; + expect(postLevel.info).toEqual({ + level: 0, + coins: 2, + bigCoins: 0, + }); + expect(postLevel.pirates).toEqual({ + coins: 0, + bigCoins: 0, + }); + expect(hasFreeMoney(postLevel)).toEqual(true); + }); +}); + +describe('redux logic tests', () => { + let previousState: GameState; + + beforeAll(() => { + const pirates = getPirates([ + { id: '100', position: { level: 0, x: 2, y: 0 } }, + { id: '200', position: { level: 0, x: 2, y: 4 } }, + { id: '300', position: { level: 0, x: 2, y: 4 } }, + ]); + + previousState = getState(pirates); + previousState = reducer(previousState, initMap(getMapData)); + previousState = reducer(previousState, initTeams(twoTeamsData)); + previousState = reducer( + previousState, + applyPirateChanges({ + changes: pirates, + moves: [], + }), + ); + previousState = reducer(previousState, setCurrentHumanTeam()); + previousState = reducer(previousState, chooseHumanPirate({ pirate: '200', withCoinAction: true })); + }); + + test('Производим изменения на карте', () => { + const result = reducer( + previousState, + applyChanges([ + { + backgroundImageSrc: '/fields/forest.png', + rotate: 2, + levels: [ + { level: 0, coins: 0, bigCoins: 0 }, + { level: 1, coins: 0, bigCoins: 0 }, + { level: 2, coins: 0, bigCoins: 0 }, + ], + x: 2, + y: 4, + }, + ]), + ); + + const level = result.fields[4][2].levels[0]; + expect(level.info).toEqual({ + level: 0, + coins: 0, + bigCoins: 0, + }); + expect(level.pirates).toEqual({ + coins: 0, + bigCoins: 0, + }); + expect(hasFreeMoney(level)).toEqual(false); + }); + + test('Открываем Бен Ганна', () => { + expect(previousState.pirates).toHaveLength(3); + expect(previousState.highlight_x).toEqual(2); + expect(previousState.highlight_y).toEqual(4); + + const result = reducer( + previousState, + applyPirateChanges({ + changes: [ + { + id: '200', + type: Constants.pirateTypes.Usual, + teamId: testTeamId, + position: { + level: 0, + x: 2, + y: 3, + }, + }, + { + id: '400', + type: Constants.pirateTypes.BenGunn, + teamId: testTeamId, + position: { + level: 0, + x: 2, + y: 3, + }, + isAlive: true, + }, + ], + moves: [], + }), + ); + + expect(result.pirates).toHaveLength(4); + const boy = result.pirates?.find((it) => it.id == '200'); + expect(boy?.position).toEqual({ level: 0, x: 2, y: 3 }); + expect(boy?.isActive).toBeTruthy(); + const ben = result.pirates?.find((it) => it.id == '400'); + expect(ben?.position).toEqual({ level: 0, x: 2, y: 3 }); + expect(ben?.isActive).toBeFalsy(); + expect(ben?.backgroundColor).toEqual('DarkBlue'); + expect(result.highlight_x).toEqual(2); + expect(result.highlight_y).toEqual(3); + const level = result.fields[3][2].levels[0]; + expect(level.info).toEqual({ + level: 0, + coins: 0, + bigCoins: 0, + }); + expect(level.pirates).toEqual({ + coins: 0, + bigCoins: 0, + }); + expect(hasFreeMoney(level)).toEqual(false); + + expect(girlsMap.Map).toEqual( + expect.objectContaining({ + '20': { girls: [{ id: '100', order: 0 }], level: 0, levelsCountInCell: 1 }, + '3020': { + girls: [ + { id: '200', order: 0 }, + { id: '400', order: 1 }, + ], + level: 0, + levelsCountInCell: 1, + }, + '4020': { girls: [{ id: '300', order: 1 }], level: 0, levelsCountInCell: 1 }, + }), + ); + }); + + test('Открываем людоеда', () => { + const result = reducer( + previousState, + applyPirateChanges({ + changes: [ + { + id: '200', + type: Constants.pirateTypes.Usual, + teamId: 2, + position: { + level: 0, + x: 2, + y: 2, + }, + isAlive: false, + }, + ], + moves: [], + }), + ); + + expect(result.pirates).toHaveLength(2); + expect(result.highlight_x).toEqual(2); + expect(result.highlight_y).toEqual(4); + const level = result.fields[4][2].levels[0]; + expect(level.info).toEqual({ + level: 0, + coins: 2, + bigCoins: 0, + }); + expect(level.pirates).toEqual({ + coins: 0, + bigCoins: 0, + }); + expect(hasFreeMoney(level)).toEqual(true); + expect(level.features).toEqual([ + { + photo: 'skull_light.png', + backgroundColor: 'transparent', + }, + ]); + + expect(girlsMap.Map).toEqual( + expect.objectContaining({ + '20': { girls: [{ id: '100', order: 0 }], level: 0, levelsCountInCell: 1 }, + '4020': { girls: [{ id: '300', order: 1 }], level: 0, levelsCountInCell: 1 }, + }), + ); + }); +}); diff --git a/Front/src/game/redux/gameSlice.ts b/Front/src/game/redux/gameSlice.ts new file mode 100644 index 00000000..7a048c05 --- /dev/null +++ b/Front/src/game/redux/gameSlice.ts @@ -0,0 +1,580 @@ +import { PayloadAction, createSlice, current } from '@reduxjs/toolkit'; +import { memoize } from 'proxy-memoize'; + +import { constructGameLevel, girlsMap } from '../logic/gameLogic'; +import { + ChooseHumanPirateActionProps, + FieldState, + GamePlace, + GameState, + GameStateSettings, + HighlightHumanMovesActionProps, + StorageState, +} from '../types'; +import { GameLevel, GameLevelFeature } from '../types/gameContent'; +import { + CellDiffResponse, + GameMapResponse, + GamePirateChangesResponse, + GameStartResponse, + GameStatisticsResponse, + GameTeamResponse, +} from '../types/gameSaga'; +import { ScreenSizes, TeamScores } from './gameSlice.types'; +import { Constants } from '/app/constants'; +import { debugLog, getAnotherRandomValue } from '/app/global'; + +export const gameSlice = createSlice({ + name: 'game', + initialState: { + fields: [[]], + lastMoves: [], + gameSettings: { + cellSize: 50, + pirateSize: 15, + tilesPackNames: [], + }, + userSettings: { + groups: [ + Constants.groupIds.girls, + Constants.groupIds.redalert, + Constants.groupIds.orcs, + Constants.groupIds.skulls, + ], + mapSize: 11, + players: ['human', 'robot2', 'robot', 'robot2'], + playersMode: 4, + gameSpeed: 1, + }, + teams: [], + currentHumanTeamId: 0, + highlight_x: 0, + highlight_y: 0, + hasPirateAutoChange: true, + includeMovesWithRum: false, + } satisfies GameState as GameState, + reducers: { + initMySettings: (state, action: PayloadAction) => { + Object.assign(state.userSettings, action.payload); + }, + saveMySettings: (state, action: PayloadAction) => { + localStorage.state = JSON.stringify(action.payload, null, 2); + Object.assign(state.userSettings, action.payload); + }, + initGame: (state, action: PayloadAction) => { + state.gameSettings.gameId = action.payload.gameId; + state.gameSettings.gameMode = action.payload.gameMode; + state.gameSettings.tilesPackName = action.payload.tilesPackName; + state.gameSettings.mapId = action.payload.mapId; + state.pirates = action.payload.pirates; + state.lastMoves = []; + state.highlight_x = 0; + state.highlight_y = 0; + + gameSlice.caseReducers.initMap(state, initMap(action.payload.map)); + gameSlice.caseReducers.initTeams(state, initTeams(action.payload.teams)); + gameSlice.caseReducers.initPhotos(state); + gameSlice.caseReducers.initSizes( + state, + initSizes({ width: window.innerWidth, height: window.innerHeight }), + ); + gameSlice.caseReducers.initPiratePositions(state); + }, + initMap: (state, action: PayloadAction) => { + const map = []; + let j = 0; + for (let i = 0; i < action.payload.height; i++) { + const row: FieldState[] = []; + for (let col = 0; col < action.payload.width; col++) { + const change = action.payload.changes[j]; + row.push({ + image: change.backgroundImageSrc, + rotate: change.rotate, + levels: change.levels.map(constructGameLevel), + availableMoves: [], + }); + j++; + } + map.push(row); + } + state.gameSettings.mapSize = action.payload.width; + state.fields = map; + }, + initTeams: (state, action: PayloadAction) => { + state.teams = action.payload.map((it, idx, arr) => { + const grId = arr.length == 2 && idx == 1 ? 2 : idx; + return { + id: it.id, + isCurrentUser: it.isCurrentUser, + activePirate: '', + backColor: Constants.teamColors[idx] ?? '', + name: it.name, + isHuman: it.isHuman, + group: + Constants.groups.find((gr) => gr.id == state.userSettings.groups[grId]) || Constants.groups[0], + }; + }); + }, + initPhotos: (state) => { + state.teams.forEach((team) => { + state.pirates + ?.filter((it) => it.teamId == team.id) + .forEach((it) => { + let pname; + let pnumber; + let extension = '.png'; + + if (it.type == Constants.pirateTypes.BenGunn) { + pname = 'commonganns/gann'; + pnumber = getAnotherRandomValue( + 1, + Constants.commonGannMaxId, + state.pirates + ?.filter((pr) => pr.type == Constants.pirateTypes.BenGunn) + .map((pr) => pr.photoId ?? 0) ?? [], + ); + } else if (it.type == Constants.pirateTypes.Friday) { + pname = 'commonfridays/friday'; + pnumber = getAnotherRandomValue(1, Constants.commonFridayMaxId, []); + } else { + pname = `${team.group.id}/pirate`; + pnumber = getAnotherRandomValue( + 1, + team.group.photoMaxId, + state.pirates?.filter((pr) => pr.teamId == it.teamId).map((pr) => pr.photoId ?? 0) ?? + [], + ); + extension = team.group.extension || '.png'; + } + + it.photo = `${pname}_${pnumber}${extension}`; + it.photoId = pnumber; + it.groupId = team.group.id; + it.backgroundColor = team.backColor; + }); + }); + }, + initSizes: (state, action: PayloadAction) => { + const width = action.payload.width; + const height = action.payload.height - 56; + const mSize = width > height ? height : width; + + if (mSize > 560) { + state.gameSettings.cellSize = Math.floor(mSize / state.gameSettings.mapSize! / 10) * 10; + } + state.gameSettings.pirateSize = state.gameSettings.cellSize * 0.55; + }, + initPiratePositions: (state) => { + girlsMap.Map = {}; + state.pirates!.forEach((it: GamePirate) => { + girlsMap.AddPosition(it, 1); + }); + }, + setCurrentHumanTeam: (state) => { + if (state.stat?.isCurrentUsersMove && state.stat.currentTeamId !== state.currentHumanTeamId) { + state.currentHumanTeamId = state.stat.currentTeamId; + } + }, + setPirateAutoChange: (state, action: PayloadAction) => { + state.hasPirateAutoChange = action.payload; + }, + setIncludeMovesWithRum: (state, action: PayloadAction) => { + state.includeMovesWithRum = action.payload; + gameSlice.caseReducers.highlightHumanMoves(state, highlightHumanMoves({})); + }, + chooseHumanPirate: (state, action: PayloadAction) => { + const selectors = gameSlice.getSelectors(); + const pirate = selectors.getPirateById(state, action.payload.pirate)!; + const currentPlayerTeam = selectors.getCurrentPlayerTeam(state)!; + const hasPirateChanging = currentPlayerTeam.activePirate !== pirate.id; + if (hasPirateChanging) { + const prevPirate = selectors.getPirateById(state, currentPlayerTeam.activePirate); + if (prevPirate) prevPirate.isActive = false; + const nextPirate = selectors.getPirateById(state, pirate.id); + if (nextPirate) nextPirate.isActive = true; + + currentPlayerTeam.activePirate = pirate.id; + gameSlice.caseReducers.highlightHumanMoves(state, highlightHumanMoves({})); + return; + } + + const hasCoinChanging = + action.payload.withCoinAction && (pirate.withCoin !== undefined || pirate.withBigCoin !== undefined); + if (hasCoinChanging) { + const level = state.fields[pirate.position.y][pirate.position.x].levels[pirate.position.level]; + if (pirate.withBigCoin) { + pirate.withBigCoin = false; + if (level.pirates.coins < level.info.coins) { + pirate.withCoin = true; + } + } else if (pirate.withCoin) { + pirate.withCoin = false; + } else if (level.pirates.bigCoins < level.info.bigCoins) { + pirate.withBigCoin = true; + } else if (level.pirates.coins < level.info.coins) { + pirate.withCoin = true; + } + gameSlice.caseReducers.updateLevelCoinsData(state, updateLevelCoinsData(pirate)); + gameSlice.caseReducers.highlightHumanMoves(state, highlightHumanMoves({})); + } + }, + highlightHumanMoves: (state, action: PayloadAction) => { + const selectors = gameSlice.getSelectors(); + const currentTeam = state.teams.find((it) => it.id == state.stat?.currentTeamId)!; + + if (!state.stat?.isCurrentUsersMove) return; + + // undraw previous moves + state.lastMoves.forEach((move) => { + const cell = state.fields[move.to.y][move.to.x]; + cell.availableMoves = []; + }); + + if (action.payload.moves) { + state.lastMoves = action.payload.moves; + } + + if ( + state.hasPirateAutoChange && + state.lastMoves.length > 0 && + !state.lastMoves.some((move) => move.from.pirateIds.includes(currentTeam.activePirate)) + ) { + const prevPirate = selectors.getPirateById(state, currentTeam.activePirate); + if (prevPirate) prevPirate.isActive = false; + + currentTeam.activePirate = state.lastMoves[0].from.pirateIds[0]; + } + + const pirate = selectors.getPirateById(state, currentTeam.activePirate); + if (!pirate) return; + + pirate.isActive = true; + gameSlice.caseReducers.highlightPirate(state, highlightPirate(pirate)); + + // собственно подсвечиваем ходы + state.lastMoves + .filter( + (move) => + move.from.pirateIds.includes(currentTeam.activePirate) && + (!move.withRumBottle || (move.withRumBottle && state.includeMovesWithRum)) && + ((pirate?.withCoin && move.withCoin) || + (pirate?.withBigCoin && move.withBigCoin) || + (pirate?.withCoin === undefined && pirate?.withBigCoin === undefined) || + (!pirate?.withCoin && !pirate?.withBigCoin && !move.withCoin && !move.withBigCoin)), + ) + .forEach((move) => { + const cell = state.fields[move.to.y][move.to.x]; + cell.availableMoves.push({ + num: move.moveNum, + isRespawn: move.withRespawn, + isQuake: move.withQuake, + pirateId: pirate.id, + prev: move.prev, + }); + }); + }, + removeHumanMoves: (state) => { + // undraw previous moves + state.lastMoves.forEach((move) => { + const cell = state.fields[move.to.y][move.to.x]; + cell.availableMoves = []; + }); + }, + highlightPirate: (state, action: PayloadAction) => { + const pirate = action.payload; + if (!pirate) return; + + if (pirate?.position.x != state.highlight_x || pirate?.position.y != state.highlight_y) { + const prevCell = state.fields[state.highlight_y][state.highlight_x]; + prevCell.highlight = false; + } + + state.highlight_x = pirate?.position.x || 0; + state.highlight_y = pirate?.position.y || 0; + const curCell = state.fields[state.highlight_y][state.highlight_x]; + curCell.highlight = true; + }, + applyPirateChanges: (state, action: PayloadAction) => { + const cached = {} as { [id: number]: GameLevel }; + const selectors = gameSlice.getSelectors(); + + action.payload.changes.forEach((it) => { + const team = state.teams.find((tm) => tm.id == it.teamId)!; + if (it.isAlive === false) { + const place = selectors.getPirateCell(state, it.id); + if (place) { + const skull: GameLevelFeature = { + photo: place.cell.image?.includes('arrow') ? 'skull.png' : 'skull_light.png', + backgroundColor: 'transparent', + }; + if (place.level.features === undefined) place.level.features = [skull]; + else place.level.features.push(skull); + } + const pirate = state.pirates!.find((pr) => pr.id === it.id)!; + state.pirates = state.pirates?.filter((pr) => pr.id !== it.id); + + debugLog('dead pirate', current(pirate)); + girlsMap.RemovePosition(pirate); + gameSlice.caseReducers.updateLevelCoinsData(state, updateLevelCoinsData(pirate)); + } else if (it.isAlive === true) { + let pname; + let pnumber; + let extension = '.png'; + + if (it.type == Constants.pirateTypes.BenGunn) { + pname = 'commonganns/gann'; + pnumber = getAnotherRandomValue( + 1, + Constants.commonGannMaxId, + state.pirates + ?.filter((pr) => pr.type == Constants.pirateTypes.BenGunn) + .map((pr) => pr.photoId ?? 0) ?? [], + ); + } else if (it.type == Constants.pirateTypes.Friday) { + pname = 'commonfridays/friday'; + pnumber = getAnotherRandomValue(1, Constants.commonFridayMaxId, []); + } else { + pname = `${team.group.id}/pirate`; + pnumber = getAnotherRandomValue( + 1, + team.group.photoMaxId, + state.pirates?.filter((pr) => pr.teamId == it.teamId).map((pr) => pr.photoId ?? 0) ?? [], + ); + extension = team.group.extension || '.png'; + } + + state.pirates?.push({ + id: it.id, + teamId: it.teamId, + position: it.position, + groupId: team.group.id, + photo: `${pname}_${pnumber}${extension}`, + photoId: pnumber, + type: it.type, + isActive: it.id === team.activePirate, + backgroundColor: team.backColor, + }); + girlsMap.AddPosition(it, state.fields[it.position.y][it.position.x].levels.length); + } else { + const pirate = state.pirates!.find((pr) => pr.id === it.id)!; + + const cachedId = pirate.position.y * 1000 + pirate.position.x * 10 + pirate.position.level; + if (!Object.prototype.hasOwnProperty.call(cached, cachedId)) { + cached[cachedId] = + state.fields[pirate.position.y][pirate.position.x].levels[pirate.position.level]; + } + + girlsMap.RemovePosition(pirate); + gameSlice.caseReducers.updateLevelCoinsData(state, updateLevelCoinsData(pirate)); + + pirate.position = it.position; + pirate.isDrunk = it.isDrunk; + pirate.isInTrap = it.isInTrap; + pirate.isInHole = it.isInHole; + pirate.isActive = it.id === team.activePirate; + + girlsMap.AddPosition(pirate, state.fields[it.position.y][it.position.x].levels.length); + gameSlice.caseReducers.updateLevelCoinsData(state, updateLevelCoinsData(pirate)); + + debugLog('move pirate', current(pirate).position.x, current(pirate).position.y, current(pirate).id); + if (it.id === team.activePirate) { + gameSlice.caseReducers.highlightPirate(state, highlightPirate(pirate)); + } + } + }); + + debugLog(current(state.teams)); + // поднятие/опускание и автоподнятие монет + if (selectors.getGameStatistics(state)!.isCurrentUsersMove) { + const girlIds = new Set(); + action.payload.moves + .filter((move) => move.withCoin || move.withBigCoin) + .forEach((move) => { + move.from.pirateIds.forEach((it) => girlIds.add(it)); + }); + state.pirates?.forEach((it) => { + const changeCoin = girlIds.has(it.id) ? true : undefined; + if (changeCoin != it.withCoin || changeCoin != it.withBigCoin) { + const cachedId = it.position.y * 1000 + it.position.x * 10 + it.position.level; + if (!Object.prototype.hasOwnProperty.call(cached, cachedId)) { + cached[cachedId] = state.fields[it.position.y][it.position.x].levels[it.position.level]; + } + + const cell = girlsMap.GetPosition(it); + const level = cached[cachedId]; + const levelPirates = state.pirates?.filter((it) => cell?.girls?.some((x) => x.id == it.id)); + + let changeSmallCoin = changeCoin; + let changeBigCoin = changeCoin; + + if (changeCoin !== undefined) { + changeBigCoin = + levelPirates!.filter((pr) => pr.id != it.id && pr.withBigCoin).length < + level.info.bigCoins; + changeSmallCoin = changeBigCoin + ? false + : levelPirates!.filter((pr) => pr.id != it.id && pr.withCoin).length < level.info.coins; + } + it.withCoin = changeSmallCoin; + it.withBigCoin = changeBigCoin; + const prt = levelPirates?.find((pr) => pr.id == it.id); + if (prt) { + prt.withCoin = changeSmallCoin; + prt.withBigCoin = changeBigCoin; + } + + gameSlice.caseReducers.updateLevelCoinsData(state, updateLevelCoinsData(it)); + } + }); + } + }, + applyChanges: (state, action: PayloadAction) => { + action.payload.forEach((it) => { + const cell = state.fields[it.y][it.x]; + if (cell.image != it.backgroundImageSrc) { + cell.image = it.backgroundImageSrc; + cell.rotate = it.rotate; + } + if (state.stat?.isGameOver) { + cell.dark = true; + } + if (cell.levels.length !== it.levels.length) { + // открыли новую клетку или разлом + cell.levels = it.levels.map(constructGameLevel); + } else { + cell.levels.forEach((lev) => { + lev.info = it.levels[lev.info.level]; + }); + } + }); + }, + updateLevelCoinsData: (state, action: PayloadAction) => { + const field = state.fields[action.payload.position.y][action.payload.position.x]; + const level = field.levels[action.payload.position.level]; + const girlsLevel = girlsMap.GetPosition(action.payload); + const levelPirates = state.pirates?.filter((it) => girlsLevel?.girls?.some((x) => x.id == it.id)); + level.pirates = { + coins: levelPirates?.filter((it) => it.withCoin).length ?? 0, + bigCoins: levelPirates?.filter((it) => it.withBigCoin).length ?? 0, + }; + level.freeCoinGirlId = !field.image?.includes('ship') + ? levelPirates?.find( + (it) => + (!it.withBigCoin && level.pirates.bigCoins < level.info.bigCoins) || + (!it.withCoin && level.pirates.coins < level.info.coins), + )?.id + : undefined; + }, + checkBottles: (state, action: PayloadAction) => { + const curTeam = gameSlice.getSelectors().getCurrentPlayerTeam(state); + const curScores = state.teamScores?.find((it) => it.teamId === curTeam?.id); + const newScores = action.payload?.find((it) => it.teamId === curTeam?.id); + + if (curScores && newScores && curScores.rumBottles > newScores.rumBottles) { + state.includeMovesWithRum = false; + } + }, + applyStat: (state, action: PayloadAction) => { + state.stat = action.payload.stats; + state.teamScores = action.payload.teamScores; + + // вызываем после присваивания stat, т.к. именно от туда приходит stat.currentTeamId + gameSlice.caseReducers.setCurrentHumanTeam(state); + }, + setTilesPackNames: (state, action: PayloadAction) => { + state.gameSettings.tilesPackNames = action.payload; + }, + setMapForecasts: (state, action: PayloadAction) => { + state.mapForecasts = action.payload; + }, + }, + selectors: { + getTeamById: (state, teamId: number): TeamState | undefined => state.teams.find((it) => it.id == teamId), + getCurrentPlayerTeam: (state): TeamState | undefined => + state.teams.find((it) => it.id == state.currentHumanTeamId), + getCurrentPlayerPirates: (state): GamePirate[] | undefined => { + // const currentPlayerTeam = gameSlice.getSelectors().getCurrentPlayerTeam(state); + return state.pirates?.filter((it) => it.teamId == state.currentHumanTeamId); + }, + getPiratesIds: memoize((state): string[] | undefined => state.pirates?.map((it) => it.id)), + getPirateById: (state, pirateId: string): GamePirate | undefined => + state.pirates?.find((it) => it.id === pirateId), + getPirateCell: (state, pirateId: string): GamePlace | undefined => { + const gamePirate = gameSlice.getSelectors().getPirateById(state, pirateId); + if (!gamePirate) return undefined; + const cell = state.fields[gamePirate.position.y][gamePirate.position.x]; + const level = cell.levels[gamePirate.position.level]; + return { + cell, + level, + }; + }, + getUserSettings: (state): StorageState => state.userSettings, + getGameSettings: (state): GameStateSettings => state.gameSettings, + getGameField: (state, row: number, col: number): FieldState => state.fields[row][col], + getMapForecasts: (state): string[] | undefined => state.mapForecasts, + getPirateAutoChange: (state): boolean => state.hasPirateAutoChange, + getIncludeMovesWithRum: (state): boolean => state.includeMovesWithRum, + getGameStatistics: (state): GameStat | undefined => state.stat, + getTeamScores: (state): TeamScores[] | undefined => { + return state.teamScores?.map((it) => { + const team = gameSlice.getSelectors().getTeamById(state, it.teamId); + return { + teamId: team?.id, + name: team?.name, + backColor: team?.backColor, + coins: it.coins, + bottles: it.rumBottles, + } as TeamScores; + }); + }, + getRumBottles: (state): number => { + const curTeam = gameSlice.getSelectors().getCurrentPlayerTeam(state); + const curScores = state.teamScores?.find((it) => it.teamId === curTeam?.id); + return curScores?.rumBottles ?? 0; + }, + }, +}); + +export const { + initMySettings, + saveMySettings, + initMap, + initGame, + initTeams, + initPhotos, + initSizes, + initPiratePositions, + setCurrentHumanTeam, + setPirateAutoChange, + setIncludeMovesWithRum, + chooseHumanPirate, + highlightPirate, + highlightHumanMoves, + removeHumanMoves, + applyPirateChanges, + applyChanges, + updateLevelCoinsData, + checkBottles, + applyStat, + setTilesPackNames, + setMapForecasts, +} = gameSlice.actions; + +export const { + getCurrentPlayerTeam, + getCurrentPlayerPirates, + getPiratesIds, + getPirateById, + getGameField, + getGameSettings, + getUserSettings, + getMapForecasts, + getPirateAutoChange, + getIncludeMovesWithRum, + getGameStatistics, + getTeamScores, + getRumBottles, +} = gameSlice.selectors; + +export default gameSlice.reducer; diff --git a/Front/src/game/redux/gameSlice.types.ts b/Front/src/game/redux/gameSlice.types.ts new file mode 100644 index 00000000..352651cf --- /dev/null +++ b/Front/src/game/redux/gameSlice.types.ts @@ -0,0 +1,12 @@ +export interface ScreenSizes { + width: number; + height: number; +} + +export interface TeamScores { + teamId: number; + name: string; + backColor: string; + coins: number; + bottles: number; +} diff --git a/Front/src/game/redux/gameThunks.ts b/Front/src/game/redux/gameThunks.ts new file mode 100644 index 00000000..23eb873f --- /dev/null +++ b/Front/src/game/redux/gameThunks.ts @@ -0,0 +1,42 @@ +import { ThunkAction, UnknownAction } from '@reduxjs/toolkit'; + +import { girlsMap } from '../logic/gameLogic'; +import { GameState } from '../types'; +import { GameStartResponse, GameTurnResponse } from '../types/gameSaga'; +import { applyChanges, applyPirateChanges, applyStat, highlightHumanMoves, initGame, initMap } from './gameSlice'; + +export const applyStartData = + (data: GameStartResponse): ThunkAction => + async (dispatch) => { + dispatch(initMap(data.map)); + dispatch(initGame(data)); + dispatch(applyStat(data)); + data.pirates.forEach((girl) => { + girlsMap.AddPosition(girl, 1); + }); + dispatch( + applyPirateChanges({ + moves: data.moves, + changes: data.pirates, + }), + ); + dispatch(highlightHumanMoves({ moves: data.moves })); + }; + +export const applyMoveChanges = + (data: GameTurnResponse): ThunkAction => + async (dispatch) => { + dispatch(applyStat(data)); + dispatch(applyChanges(data.changes)); + dispatch( + applyPirateChanges({ + moves: data.moves, + changes: data.pirateChanges, + }), + ); + dispatch(highlightHumanMoves({ moves: data.moves })); + + if (data.stats.isGameOver) { + dispatch(highlightHumanMoves({ moves: [] })); + } + }; diff --git a/Front/src/game/redux/mapDataForTests.ts b/Front/src/game/redux/mapDataForTests.ts new file mode 100644 index 00000000..a9352019 --- /dev/null +++ b/Front/src/game/redux/mapDataForTests.ts @@ -0,0 +1,333 @@ +import { GameMapResponse } from '../types/gameSaga'; + +export const getMapData: GameMapResponse = { + height: 5, + width: 5, + changes: [ + { + backgroundImageSrc: '/fields/water.png', + rotate: 0, + levels: [ + { + level: 0, + coins: 0, + bigCoins: 0, + }, + ], + x: 0, + y: 0, + }, + { + backgroundImageSrc: '/fields/water.png', + rotate: 0, + levels: [ + { + level: 0, + coins: 0, + bigCoins: 0, + }, + ], + x: 1, + y: 0, + }, + { + backgroundImageSrc: '/fields/water.png', + rotate: 0, + levels: [ + { + level: 0, + coins: 0, + bigCoins: 0, + }, + ], + x: 2, + y: 0, + }, + { + backgroundImageSrc: '/fields/water.png', + rotate: 0, + levels: [ + { + level: 0, + coins: 0, + bigCoins: 0, + }, + ], + x: 3, + y: 0, + }, + { + backgroundImageSrc: '/fields/water.png', + rotate: 0, + levels: [ + { + level: 0, + coins: 0, + bigCoins: 0, + }, + ], + x: 4, + y: 0, + }, + { + backgroundImageSrc: '/fields/water.png', + rotate: 0, + levels: [ + { + level: 0, + coins: 0, + bigCoins: 0, + }, + ], + x: 0, + y: 1, + }, + { + backgroundImageSrc: '/fields/water.png', + rotate: 0, + levels: [ + { + level: 0, + coins: 0, + bigCoins: 0, + }, + ], + x: 1, + y: 1, + }, + { + backgroundImageSrc: '/fields/water.png', + rotate: 0, + levels: [ + { + level: 0, + coins: 0, + bigCoins: 0, + }, + ], + x: 2, + y: 1, + }, + { + backgroundImageSrc: '/fields/water.png', + rotate: 0, + levels: [ + { + level: 0, + coins: 0, + bigCoins: 0, + }, + ], + x: 3, + y: 1, + }, + { + backgroundImageSrc: '/fields/water.png', + rotate: 0, + levels: [ + { + level: 0, + coins: 0, + bigCoins: 0, + }, + ], + x: 4, + y: 1, + }, + { + backgroundImageSrc: '/fields/water.png', + rotate: 0, + levels: [ + { + level: 0, + coins: 0, + bigCoins: 0, + }, + ], + x: 0, + y: 2, + }, + { + backgroundImageSrc: '/fields/water.png', + rotate: 0, + levels: [ + { + level: 0, + coins: 0, + bigCoins: 0, + }, + ], + x: 1, + y: 2, + }, + { + backgroundImageSrc: '/fields/ground.png', + rotate: 0, + levels: [ + { + level: 0, + coins: 0, + bigCoins: 0, + }, + ], + x: 2, + y: 2, + }, + { + backgroundImageSrc: '/fields/water.png', + rotate: 0, + levels: [ + { + level: 0, + coins: 0, + bigCoins: 0, + }, + ], + x: 3, + y: 2, + }, + { + backgroundImageSrc: '/fields/water.png', + rotate: 0, + levels: [ + { + level: 0, + coins: 0, + bigCoins: 0, + }, + ], + x: 4, + y: 2, + }, + { + backgroundImageSrc: '/fields/water.png', + rotate: 0, + levels: [ + { + level: 0, + coins: 0, + bigCoins: 0, + }, + ], + x: 0, + y: 3, + }, + { + backgroundImageSrc: '/fields/water.png', + rotate: 0, + levels: [ + { + level: 0, + coins: 0, + bigCoins: 0, + }, + ], + x: 1, + y: 3, + }, + { + backgroundImageSrc: '/fields/cannon.png', + rotate: 0, + levels: [ + { + level: 0, + coins: 0, + bigCoins: 0, + }, + ], + x: 2, + y: 3, + }, + { + backgroundImageSrc: '/fields/cannon.png', + rotate: 0, + levels: [ + { + level: 0, + coins: 0, + bigCoins: 0, + }, + ], + x: 3, + y: 3, + }, + { + backgroundImageSrc: '/fields/water.png', + rotate: 0, + levels: [ + { + level: 0, + coins: 0, + bigCoins: 0, + }, + ], + x: 4, + y: 3, + }, + { + backgroundImageSrc: '/fields/water.png', + rotate: 0, + levels: [ + { + level: 0, + coins: 0, + bigCoins: 0, + }, + ], + x: 0, + y: 4, + }, + { + backgroundImageSrc: '/fields/water.png', + rotate: 0, + levels: [ + { + level: 0, + coins: 0, + bigCoins: 0, + }, + ], + x: 1, + y: 4, + }, + { + backgroundImageSrc: '/fields/water.png', + rotate: 0, + levels: [ + { + level: 0, + coins: 2, + bigCoins: 0, + }, + ], + x: 2, + y: 4, + }, + { + backgroundImageSrc: '/fields/water.png', + rotate: 0, + levels: [ + { + level: 0, + coins: 0, + bigCoins: 0, + }, + ], + x: 3, + y: 4, + }, + { + backgroundImageSrc: '/fields/water.png', + rotate: 0, + levels: [ + { + level: 0, + coins: 0, + bigCoins: 0, + }, + ], + x: 4, + y: 4, + }, + ], +}; diff --git a/Front/src/game/sagas/gameFeaturesSaga.ts b/Front/src/game/sagas/gameFeaturesSaga.ts new file mode 100644 index 00000000..a6687590 --- /dev/null +++ b/Front/src/game/sagas/gameFeaturesSaga.ts @@ -0,0 +1,34 @@ +import { PayloadAction } from '@reduxjs/toolkit'; +import { call, put, takeEvery } from 'redux-saga/effects'; + +import { setMapForecasts, setTilesPackNames } from '../redux/gameSlice'; +import { CheckMapRequest, CheckMapResponse } from '../types/gameFeaturesSaga'; +import { axiosInstance, errorsWrapper, sagaActions } from '/common/sagas'; + +export function* getTilesPackNames() { + const result: { data: string[] } = yield call( + async () => + await axiosInstance({ + url: 'v1/map/tiles-pack-names', + method: 'get', + }), + ); + yield put(setTilesPackNames(result.data)); +} + +export function* checkMap(action: PayloadAction) { + const result: { data: CheckMapResponse[] } = yield call( + async () => + await axiosInstance({ + url: 'v1/map/check-landing', + method: 'get', + params: action.payload, + }), + ); + yield put(setMapForecasts(result.data.map((it) => it.difficulty))); +} + +export default function* rootSaga() { + yield takeEvery(sagaActions.GET_TILES_PACK_NAMES, errorsWrapper(getTilesPackNames)); + yield takeEvery(sagaActions.CHECK_MAP, errorsWrapper(checkMap)); +} diff --git a/Front/src/game/sagas/gameSaga.ts b/Front/src/game/sagas/gameSaga.ts new file mode 100644 index 00000000..8ae7b3da --- /dev/null +++ b/Front/src/game/sagas/gameSaga.ts @@ -0,0 +1,127 @@ +import { PayloadAction } from '@reduxjs/toolkit'; +import { call, delay, fork, put, select, take, takeEvery } from 'redux-saga/effects'; + +import { + applyChanges, + applyPirateChanges, + applyStat, + checkBottles, + getGameStatistics, + getUserSettings, + highlightHumanMoves, + initGame, + removeHumanMoves, +} from '../redux/gameSlice'; +import { StorageState } from '../types'; +import { GameStartResponse, GameTurnResponse } from '../types/gameSaga'; +import { history } from '/app/global'; +import { getAuth } from '/auth/redux/authSlice'; +import { AuthState } from '/auth/types/auth'; +import { errorsWrapper, sagaActions } from '/common/sagas'; + +const animateQueue: GameTurnResponse[] = []; + +export function* applyStartData(action: { payload: GameStartResponse }) { + const data = action.payload; + const auth: AuthState = yield select(getAuth); + data.stats.isCurrentUsersMove = data.stats.currentUserId === auth.user?.id; + data.teams.forEach((it) => { + if (it.userId === auth.user?.id) it.isCurrentUser = true; + }); + yield put(initGame(data)); + yield put(applyStat(data)); + yield put( + applyPirateChanges({ + moves: data.moves, + changes: data.pirates, + }), + ); + yield put(highlightHumanMoves({ moves: data.moves })); + history.navigate && history.navigate('/'); +} + +export function* applyTurn(action: { payload: GameTurnResponse }) { + animateQueue.push(action.payload); + yield put({ + type: sagaActions.START_ANIMATE, + }); + return; +} + +function* watchAnimation() { + while (true) { + yield take(sagaActions.START_ANIMATE); + yield call(doAnimate); + } +} + +function* doAnimate() { + let elm = animateQueue.shift(); + while (elm) { + yield call(applyTurnData, { + type: sagaActions.GAME_TURN_APPLY_DATA, // любой тип + payload: elm, + }); + elm = animateQueue.shift(); + } + yield put({ + type: sagaActions.STOP_ANIMATE, + }); +} + +export function* applyTurnData(action: PayloadAction) { + const result = { data: action.payload }; + + const prevStat: GameStat | undefined = yield select(getGameStatistics); + const auth: AuthState = yield select(getAuth); + result.data.stats.isCurrentUsersMove = result.data.stats.currentUserId === auth.user?.id; + const { gameSpeed: speed }: StorageState = yield select(getUserSettings); + + yield put(removeHumanMoves()); + if (!prevStat?.isCurrentUsersMove) { + if (speed > 0) { + yield delay(speed * 100); + } + } + + yield put(checkBottles(result.data.teamScores)); + yield put(applyStat(result.data)); + yield put(applyChanges(result.data.changes)); + yield put( + applyPirateChanges({ + moves: result.data.moves, + changes: result.data.pirateChanges, + }), + ); + yield put(highlightHumanMoves({ moves: result.data.moves })); + + if (result.data.stats.isGameOver) { + yield put(removeHumanMoves()); + } +} + +export function* applyLookingData(action: { payload: GameStartResponse }) { + const data = action.payload; + const auth: AuthState = yield select(getAuth); + data.stats.isCurrentUsersMove = data.stats.currentUserId === auth.user?.id; + data.teams.forEach((it) => { + if (it.userId === auth.user?.id) it.isCurrentUser = true; + }); + yield put(initGame(data)); + yield put(applyStat(data)); + yield put( + applyPirateChanges({ + moves: data.moves, + changes: data.pirates, + }), + ); + yield put(highlightHumanMoves({ moves: data.moves })); + history.navigate && history.navigate('/'); +} + +export default function* rootSaga() { + yield takeEvery(sagaActions.GAME_START_APPLY_DATA, errorsWrapper(applyStartData)); + yield takeEvery(sagaActions.GAME_TURN_APPLY_DATA, errorsWrapper(applyTurn)); + yield takeEvery(sagaActions.GAME_START_LOOKING_DATA, errorsWrapper(applyLookingData)); + yield fork(watchAnimation); +} diff --git a/Front/src/game/types.d.ts b/Front/src/game/types.d.ts new file mode 100644 index 00000000..08f2f704 --- /dev/null +++ b/Front/src/game/types.d.ts @@ -0,0 +1,75 @@ +import { GameLevel } from '/game/types/gameContent'; + +export interface GameState { + stat?: GameStat; + teamScores?: GameScore[]; + mapForecasts?: string[]; + hasPirateAutoChange: boolean; + includeMovesWithRum: boolean; + + gameSettings: GameStateSettings; + userSettings: StorageState; + fields: FieldState[][]; + pirates?: GamePirate[]; + lastMoves: GameMove[]; + teams: TeamState[]; + currentHumanTeamId: number; + highlight_x: number; + highlight_y: number; +} + +export interface GameStateSettings { + gameId?: number; + gameMode?: string; + mapId?: number; + mapSize?: number; + cellSize: number; + pirateSize: number; + tilesPackName?: string; + + tilesPackNames: string[]; +} + +export interface StorageState { + groups: string[]; + players?: string[]; + playersMode?: number; + mapSize: number; + mapId?: number; + tilesPackName?: string; + gameSpeed: number; +} + +export interface GamePlace { + cell: FieldState; + level: GameLevel; +} + +export interface FieldState { + image?: string; + rotate?: number; + levels: GameLevel[]; + availableMoves: AvailableMove[]; + highlight?: boolean; + dark?: boolean; +} + +export interface AvailableMove { + num: number; + isRespawn: boolean; + isQuake: boolean; + pirateId: string; + prev?: { + x: number; + y: number; + }; +} + +export interface HighlightHumanMovesActionProps { + moves?: GameMove[]; +} + +export interface ChooseHumanPirateActionProps { + pirate: string; + withCoinAction: bool; +} diff --git a/Front/src/game/types/gameContent.d.ts b/Front/src/game/types/gameContent.d.ts new file mode 100644 index 00000000..d1e5623f --- /dev/null +++ b/Front/src/game/types/gameContent.d.ts @@ -0,0 +1,18 @@ +import { LevelInfoResponse } from './gameSaga'; + +export interface GameLevel { + info: LevelInfoResponse; + pirates: GameLevelPirates; + freeCoinGirlId?: string; + features?: GameLevelFeature[]; +} + +export interface GameLevelPirates { + coins: number; + bigCoins: number; +} + +export interface GameLevelFeature { + backgroundColor: string; + photo: string; +} diff --git a/Front/src/game/types/gameFeaturesSaga.d.ts b/Front/src/game/types/gameFeaturesSaga.d.ts new file mode 100644 index 00000000..1e9185ce --- /dev/null +++ b/Front/src/game/types/gameFeaturesSaga.d.ts @@ -0,0 +1,10 @@ +export interface CheckMapRequest { + mapId?: number; + mapSize: number; + tilesPackName?: string; +} + +export interface CheckMapResponse { + direction: string; + difficulty: string; +} diff --git a/Front/src/game/types/gameSaga.d.ts b/Front/src/game/types/gameSaga.d.ts new file mode 100644 index 00000000..9727f2c0 --- /dev/null +++ b/Front/src/game/types/gameSaga.d.ts @@ -0,0 +1,73 @@ +export interface GameStartResponse { + gameId: number; + gameMode?: string; + tilesPackName: string; + mapId: number; + map: GameMapResponse; + teams: GameTeamResponse[]; + pirates: GamePirate[]; + moves: GameMove[]; + stats: GameStat; + teamScores?: GameScore[]; +} + +export interface GameTurnResponse { + changes: CellDiffResponse[]; + pirates: GamePirate[]; + pirateChanges: PirateDiffResponse[]; + moves: GameMove[]; + stats: GameStat; + teamScores?: GameScore[]; +} + +export interface GameMapResponse { + changes: CellDiffResponse[]; + height: number; + width: number; +} + +interface CellDiffResponse { + backgroundImageSrc: string; + rotate: number; + levels: LevelInfoResponse[]; + x: number; + y: number; +} + +export interface LevelInfoResponse { + level: number; + coins: number; + bigCoins: number; +} + +export interface GameTeamResponse { + id: number; + isCurrentUser?: boolean; // TODO: это поле вычисляемое на фронте + name: string; + coins: number; + isHuman: boolean; + userId: number; + ship: { + x: number; + y: number; + }; +} + +export interface GameStatisticsResponse { + stats: GameStat; + teamScores?: GameScore[]; +} + +export interface GamePirateChangesResponse { + changes: PirateDiffResponse[]; + moves: GameMove[]; +} + +interface PirateDiffResponse extends GamePiratePosition { + type: string; + teamId: number; + isAlive?: boolean; + isDrunk?: boolean; + isInTrap?: boolean; + isInHole?: boolean; +} diff --git a/Front/src/game/types/hubContracts.d.ts b/Front/src/game/types/hubContracts.d.ts new file mode 100644 index 00000000..c6b6b9f4 --- /dev/null +++ b/Front/src/game/types/hubContracts.d.ts @@ -0,0 +1,30 @@ +import { PlayerInfo, PlayersInfo } from '/app/content/layout/components/types'; + +export interface makeGameMoveRequestProps { + gameId: number; + turnNum: number; + pirateId: string; +} + +export interface GameSettingsFormData { + players: PlayersInfo; + gamers: PlayerInfo[]; + mapId?: number; + mapSize: number; + tilesPackName?: string; + isStoredMap: boolean; +} + +export interface GameSettings { + players: GamePlayer[]; + mapId?: number; + mapSize: number; + tilesPackName?: string; + gameMode?: string; +} + +export interface GamePlayer { + userId: number; + type: string; + position: string; +} diff --git a/Front/src/lobby/content/gameCreate/index.tsx b/Front/src/lobby/content/gameCreate/index.tsx new file mode 100644 index 00000000..5b0819d9 --- /dev/null +++ b/Front/src/lobby/content/gameCreate/index.tsx @@ -0,0 +1,17 @@ +import { Container, Row } from 'react-bootstrap'; +import { useSelector } from 'react-redux'; + +import { getNetGame } from '../../redux/lobbySlice'; +import NetGameForm from './netGameForm'; + +const NetGameCreate = () => { + const netGame = useSelector(getNetGame); + + return ( + + {netGame && } + + ); +}; + +export default NetGameCreate; diff --git a/Front/src/lobby/content/gameCreate/netGameForm.tsx b/Front/src/lobby/content/gameCreate/netGameForm.tsx new file mode 100644 index 00000000..bfea51b1 --- /dev/null +++ b/Front/src/lobby/content/gameCreate/netGameForm.tsx @@ -0,0 +1,122 @@ +import { useState } from 'react'; +import { Button } from 'react-bootstrap'; +import { useDispatch, useSelector } from 'react-redux'; + +import { getNetGames } from '../../redux/lobbySlice'; +import { NetGameInfo } from '../../types/lobbySlice'; +import { Constants } from '/app/constants'; +import GameSettingsForm from '/app/content/layout/components/gameSettingsForm'; +import { PlayerInfo } from '/app/content/layout/components/types'; +import { convertToGamers, convertToMembers, convertToSettings, convertToUsers } from '/app/global'; +import { getAuth } from '/auth/redux/authSlice'; +import gameHub from '/game/hub/gameHub'; +import { getUserSettings, saveMySettings } from '/game/redux/gameSlice'; +import { GameSettingsFormData } from '/game/types/hubContracts'; + +const isEqualsLists = (sList: string[], rList: string[]): boolean => { + if (sList.length !== rList.length) return false; + for (let i = 0; i < sList.length; i++) { + if (sList[i] !== rList[i]) return false; + } + return true; +}; + +export interface NetGameFormProps { + netGame: NetGameInfo; +} + +const NetGameForm = ({ netGame }: NetGameFormProps) => { + const dispatch = useDispatch(); + + const authInfo = useSelector(getAuth); + const userSettings = useSelector(getUserSettings); + const netGames = useSelector(getNetGames); + + const [groups, setGroups] = useState(userSettings.groups); + + const newNetStart = () => { + gameHub.startPublicGame(netGame.id, convertToSettings(formData)); + }; + + let counter = 0; + const gamers: PlayerInfo[] = netGame.users + .map((it) => ({ id: counter++, type: 'human', userId: it.id, userName: it.login }) as PlayerInfo) + .concat([ + { id: counter++, type: 'robot', userId: 0 } as PlayerInfo, + { id: counter++, type: 'robot2', userId: 0 } as PlayerInfo, + { id: counter++, type: 'robot3', userId: 0 } as PlayerInfo, + ]); + + const formData: GameSettingsFormData = { + players: { + mode: + netGame.settings.gameMode === Constants.gameModeTypes.TwoPlayersInTeam + ? 8 + : netGame.settings.players.length, + members: convertToMembers( + netGame.settings.players, + userSettings.players || ['human', 'robot2', 'robot', 'robot2'], + ), + users: convertToUsers(netGame.settings.players, [ + authInfo.user?.id ?? 0, + authInfo.user?.id ?? 0, + authInfo.user?.id ?? 0, + authInfo.user?.id ?? 0, + ]), + gamers: convertToGamers( + netGame.settings.players, + gamers, + (userSettings.players || ['human', 'robot2', 'robot', 'robot2']).map( + (it) => gamers.find((gm) => gm.type === it) ?? gamers[0], + ), + ), + groups: groups, + }, + gamers, + mapId: netGame.settings.mapId, + mapSize: netGame.settings.mapSize, + tilesPackName: netGame.settings.tilesPackName, + isStoredMap: true, + }; + + const setFormData = (data: GameSettingsFormData) => { + if (!isEqualsLists(groups, data.players.groups)) { + saveToLocalStorage(data.players.groups); + setGroups(data.players.groups); + } + const curGame = netGames.find((it) => it.id === netGame?.id); + if (curGame?.isCreator) { + gameHub.netChange(netGame?.id, convertToSettings(data)); + } + }; + + const saveToLocalStorage = (grps: string[]) => { + dispatch( + saveMySettings({ + ...userSettings, + groups: grps, + }), + ); + }; + + return ( + + <> + {netGame.isCreator && ( + + )} + + + ); +}; + +export default NetGameForm; diff --git a/Front/src/lobby/content/gameList/components/gameListItem.tsx b/Front/src/lobby/content/gameList/components/gameListItem.tsx new file mode 100644 index 00000000..a7a5a682 --- /dev/null +++ b/Front/src/lobby/content/gameList/components/gameListItem.tsx @@ -0,0 +1,55 @@ +import { ListGroup } from 'react-bootstrap'; +import { BsArrowCounterclockwise } from 'react-icons/bs'; + +import classes from '../gamelist.module.less'; +import { fromNow } from '/app/global'; + +interface GameListItemProps { + key: string; + id?: number; + isPublic?: boolean; + creatorName: string; + timeStamp: number; + children: React.ReactElement; +} + +const GameListItem = ({ key, id, isPublic, creatorName, timeStamp, children }: GameListItemProps) => { + const timeData = fromNow(timeStamp); + + return ( + + {id && {id}} + {isPublic !== undefined && + (isPublic ? ( +
П
+ ) : ( +
Ч
+ ))} + + + + {timeData.value} + {timeData.unit} + + + + {creatorName} + {children} +
+ ); +}; + +export default GameListItem; diff --git a/Front/src/lobby/content/gameList/gamelist.module.less b/Front/src/lobby/content/gameList/gamelist.module.less new file mode 100644 index 00000000..3aac93df --- /dev/null +++ b/Front/src/lobby/content/gameList/gamelist.module.less @@ -0,0 +1,34 @@ +@import '/common/content/common.less'; + +.gameList { + &:extend(.ui-panel all); + &:after { + content: 'Активные игры'; + } + height: 100%; + // max-width: 500px !important; +} + +.netGameList { + &:extend(.ui-panel all); + &:after { + content: 'Формируемые публичные игры'; + } + height: 100%; + // max-width: 500px !important; +} + +.leaderboard { + &:extend(.ui-panel all); + &:after { + content: 'Рейтинг игроков'; + } + overflow-x: auto; + // max-width: 500px !important; +} + +.listIconsItem { + span { + padding-right: 10px; + } +} diff --git a/Front/src/lobby/content/gameList/index.tsx b/Front/src/lobby/content/gameList/index.tsx new file mode 100644 index 00000000..c6051280 --- /dev/null +++ b/Front/src/lobby/content/gameList/index.tsx @@ -0,0 +1,174 @@ +import cn from 'classnames'; +import { Button, Col, Container, ListGroup, Row, Table } from 'react-bootstrap'; +import { PiEyesThin } from 'react-icons/pi'; +import { TbArrowsJoin } from 'react-icons/tb'; +import { VscDebugContinueSmall } from 'react-icons/vsc'; +import { useSelector } from 'react-redux'; +import { useNavigate } from 'react-router-dom'; + +import { getGames, getLeaderBoard, getNetGames } from '../../redux/lobbySlice'; +import GameListItem from './components/gameListItem'; +import classes from './gamelist.module.less'; +import gameHub from '/game/hub/gameHub'; + +const GameList = () => { + const navigate = useNavigate(); + const list = useSelector(getGames); + const netList = useSelector(getNetGames); + const leaders = useSelector(getLeaderBoard); + + const continueNet = (gameId: number) => { + navigate('/newpublic'); + gameHub.netJoin(gameId); + }; + + const loadGame = (gameId: number) => { + gameHub.loadGame(gameId); + }; + + let ratingNumber = 1; + + return ( + + + +
+ + {list && + list.map((it) => ( + + + + ))} + +
+ + +
+ + {netList && + netList.map((it) => ( + + + + ))} + +
+ +
+ + +
+ + + + + + + + + + + + + + + {leaders && + leaders.map((it) => ( + + + + + + + + + + + ))} + +
#ЛогинРангИгры сегодняИгры неделиИгры месяцаПобеды - ИгрыМонеты
{ratingNumber++}{it.playerName} + {it.rank} + + {it.winCountToday} - {it.gamesCountToday} + + {it.winCountThisWeek} - {it.gamesCountThisWeek} + + {it.winCountThisMonth} - {it.gamesCountThisMonth} + + {it.totalWin} - {it.gamesCountTotal} + {it.totalCoins}
+
+ +
+
+ ); +}; + +export default GameList; diff --git a/Front/src/lobby/redux/lobbySlice.ts b/Front/src/lobby/redux/lobbySlice.ts new file mode 100644 index 00000000..2b5e8bf1 --- /dev/null +++ b/Front/src/lobby/redux/lobbySlice.ts @@ -0,0 +1,56 @@ +import { PayloadAction, createSlice } from '@reduxjs/toolkit'; + +import { LobbyState } from '../types/lobby'; +import { LeaderBoardItemResponse } from '../types/lobbySaga'; +import { GameInfo, LobbyGameInfo, LobbyGamesEntriesList, NetGameInfo } from '../types/lobbySlice'; + +export const lobbySlice = createSlice({ + name: 'lobby', + initialState: { + gamelist: [], + netgamelist: [], + } satisfies LobbyState as LobbyState, + reducers: { + applyLeaderBoard: (state, action: PayloadAction) => { + state.leaderBoard = action.payload; + }, + applyGamesList: (state, action: PayloadAction) => { + state.gamelist = action.payload.gamesEntries.map((it) => ({ + id: it.gameId, + creatorName: it.creator.name, + isCreator: it.creator.id === action.payload.currentUserId, + isPlayer: it.players.some((it) => it.id === action.payload.currentUserId), + isPublic: it.players.length > 1, + timeStamp: it.timeStamp, + })); + }, + applyNetGamesList: (state, action: PayloadAction) => { + state.netgamelist = action.payload.gamesEntries.map((it) => ({ + id: it.gameId, + creatorName: it.creator.name, + isCreator: it.creator.id === action.payload.currentUserId, + isPlayer: it.players.some((it) => it.id === action.payload.currentUserId), + isPublic: true, + timeStamp: it.timeStamp, + })); + }, + applyNetGame: (state, action: PayloadAction) => { + state.netGame = { + ...action.payload.gameInfo, + isCreator: action.payload.gameInfo.creatorId === action.payload.currentUserId, + }; + }, + }, + selectors: { + getLeaderBoard: (state): LeaderBoardItemResponse[] | undefined => state.leaderBoard, + getGames: (state): GameInfo[] => state.gamelist, + getNetGames: (state): GameInfo[] => state.netgamelist, + getNetGame: (state): NetGameInfo | undefined => state.netGame, + }, +}); + +export const { applyLeaderBoard, applyGamesList, applyNetGamesList, applyNetGame } = lobbySlice.actions; + +export const { getLeaderBoard, getGames, getNetGames, getNetGame } = lobbySlice.selectors; + +export default lobbySlice.reducer; diff --git a/Front/src/lobby/sagas/lobbySaga.ts b/Front/src/lobby/sagas/lobbySaga.ts new file mode 100644 index 00000000..25d39eca --- /dev/null +++ b/Front/src/lobby/sagas/lobbySaga.ts @@ -0,0 +1,48 @@ +import { PayloadAction } from '@reduxjs/toolkit'; +import { call, put, select, takeEvery } from 'redux-saga/effects'; + +import { applyGamesList, applyLeaderBoard, applyNetGame, applyNetGamesList } from '../redux/lobbySlice'; +import { LeaderBoardItemResponse, NetGameInfoResponse, NetGameListResponse } from '../types/lobbySaga'; +import { getAuth } from '/auth/redux/authSlice'; +import { AuthState } from '/auth/types/auth'; +import { axiosInstance, errorsWrapper, sagaActions } from '/common/sagas'; +import gameHub from '/game/hub/gameHub'; + +export function* applyActiveGamesData(action: PayloadAction) { + const auth: AuthState = yield select(getAuth); + const data = action.payload; + yield put(applyGamesList({ currentUserId: auth.user?.id, gamesEntries: data.gamesEntries })); +} + +export function* applyNetGamesData(action: PayloadAction) { + const auth: AuthState = yield select(getAuth); + const data = action.payload; + yield put(applyNetGamesList({ currentUserId: auth.user?.id, gamesEntries: data.gamesEntries })); +} + +export function* applyNetGameData(action: PayloadAction) { + const auth: AuthState = yield select(getAuth); + const data = action.payload; + yield put(applyNetGame({ currentUserId: auth.user?.id, gameInfo: data })); + if (data.gameId) { + gameHub.loadGame(data.gameId); + } +} + +export function* getLeaderBoardData() { + const result: { data: { leaderboard: LeaderBoardItemResponse[] } } = yield call( + async () => + await axiosInstance({ + url: 'v1/leaderboard', + method: 'get', + }), + ); + yield put(applyLeaderBoard(result.data.leaderboard)); +} + +export default function* rootSaga() { + yield takeEvery(sagaActions.ACTIVE_GAMES_APPLY_DATA, errorsWrapper(applyActiveGamesData)); + yield takeEvery(sagaActions.NET_GAMES_APPLY_DATA, errorsWrapper(applyNetGamesData)); + yield takeEvery(sagaActions.NET_GAME_APPLY_DATA, errorsWrapper(applyNetGameData)); + yield takeEvery(sagaActions.LOBBY_GET_LEADERBOARD, errorsWrapper(getLeaderBoardData)); +} diff --git a/Front/src/lobby/types/lobby.d.ts b/Front/src/lobby/types/lobby.d.ts new file mode 100644 index 00000000..5d2afdad --- /dev/null +++ b/Front/src/lobby/types/lobby.d.ts @@ -0,0 +1,9 @@ +import { LeaderBoardItemResponse } from './lobbySaga'; +import { GameInfo, NetGameInfo } from './lobbySlice'; + +export interface LobbyState { + gamelist: GameInfo[]; + netgamelist: GameInfo[]; + netGame?: NetGameInfo; + leaderBoard?: LeaderBoardItemResponse[]; +} diff --git a/Front/src/lobby/types/lobbySaga.d.ts b/Front/src/lobby/types/lobbySaga.d.ts new file mode 100644 index 00000000..24833967 --- /dev/null +++ b/Front/src/lobby/types/lobbySaga.d.ts @@ -0,0 +1,40 @@ +export interface LeaderBoardItemResponse { + playerName: string; + rank: string; + winCountToday: number; + winCountThisWeek: number; + winCountThisMonth: number; + totalWin: number; + gamesCountToday: number; + gamesCountThisWeek: number; + gamesCountThisMonth: number; + gamesCountTotal: number; + totalCoins: number; +} + +export interface NetGameInfoResponse { + id: number; + gameId?: number; + creatorId: number; + settings: GameSettings; + viewers: number[]; + users: UserInfo[]; +} + +export interface NetGameListResponse { + gamesEntries: NetGameEntryResponse[]; +} + +export interface NetGameEntryResponse { + gameId: number; + creator: { + id: number; + name: string; + }; + players: [ + { + id: number; + }, + ]; + timeStamp: number; +} diff --git a/Front/src/lobby/types/lobbySlice.d.ts b/Front/src/lobby/types/lobbySlice.d.ts new file mode 100644 index 00000000..9c1e1307 --- /dev/null +++ b/Front/src/lobby/types/lobbySlice.d.ts @@ -0,0 +1,30 @@ +import { NetGameEntryResponse, NetGameInfoResponse } from './lobbySaga'; +import { GameSettings } from '/game/types/hubContracts'; + +export interface LobbyGamesEntriesList { + currentUserId?: number; + gamesEntries: NetGameEntryResponse[]; +} + +export interface LobbyGameInfo { + currentUserId?: number; + gameInfo: NetGameInfoResponse; +} + +export interface NetGameInfo { + id: number; + gameId?: number; + isCreator: boolean; + settings: GameSettings; + viewers: number[]; + users: UserInfo[]; +} + +export interface GameInfo { + id: number; + creatorName: string; + isCreator: boolean; + isPlayer: boolean; + isPublic: boolean; + timeStamp: number; +} diff --git a/Front/src/main.css b/Front/src/main.css new file mode 100644 index 00000000..974b509c --- /dev/null +++ b/Front/src/main.css @@ -0,0 +1,70 @@ +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: normal; + background-color: lightgray; + min-width: 320px; + min-height: 100vh; + overflow-x: hidden; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/Front/src/main.tsx b/Front/src/main.tsx new file mode 100644 index 00000000..1a029571 --- /dev/null +++ b/Front/src/main.tsx @@ -0,0 +1,5 @@ +import ReactDOM from 'react-dom/client'; +import App from './app'; +import './main.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render(); diff --git a/Front/src/types/gameMove.d.ts b/Front/src/types/gameMove.d.ts new file mode 100644 index 00000000..db0575e0 --- /dev/null +++ b/Front/src/types/gameMove.d.ts @@ -0,0 +1,21 @@ +interface GameMove { + moveNum: number; + from: GameMovePosition; + to: GameMovePosition; + prev?: { + x: number; + y: number; + }; + withCoin: boolean; + withBigCoin: boolean; + withRespawn: boolean; + withRumBottle: boolean; + withQuake: boolean; +} + +interface GameMovePosition { + pirateIds: string[]; + level: number; + x: number; + y: number; +} diff --git a/Front/src/types/gamePirate.d.ts b/Front/src/types/gamePirate.d.ts new file mode 100644 index 00000000..c847eb0d --- /dev/null +++ b/Front/src/types/gamePirate.d.ts @@ -0,0 +1,23 @@ +interface GamePirate extends GamePiratePosition { + teamId: number; + withCoin?: boolean; + withBigCoin?: boolean; + isDrunk?: boolean; + isInTrap?: boolean; + isInHole?: boolean; + groupId: string; + photo: string; + photoId: number; + type: string; + isActive?: boolean; + backgroundColor?: string; +} + +interface GamePiratePosition { + id: string; + position: { + level: number; + x: number; + y: number; + }; +} diff --git a/Front/src/types/gameStat.d.ts b/Front/src/types/gameStat.d.ts new file mode 100644 index 00000000..b03d38e3 --- /dev/null +++ b/Front/src/types/gameStat.d.ts @@ -0,0 +1,14 @@ +interface GameStat { + turnNumber: number; + currentTeamId: number; + currentUserId: number; + isCurrentUsersMove?: boolean; // TODO: это поле вычисляемое на фронте + isGameOver: boolean; + gameMessage: string; +} + +interface GameScore { + teamId: number; + coins: number; + rumBottles: number; +} diff --git a/Front/src/types/gameTeam.d.ts b/Front/src/types/gameTeam.d.ts new file mode 100644 index 00000000..5a869a6d --- /dev/null +++ b/Front/src/types/gameTeam.d.ts @@ -0,0 +1,15 @@ +interface TeamState { + id: number; + isCurrentUser?: boolean; + activePirate: string; + name: string; + backColor: string; + group: TeamGroup; + isHuman: boolean; +} + +interface TeamGroup { + id: string; + photoMaxId: number; + extension?: string; +} diff --git a/Front/src/types/userInfo.d.ts b/Front/src/types/userInfo.d.ts new file mode 100644 index 00000000..bf8fa309 --- /dev/null +++ b/Front/src/types/userInfo.d.ts @@ -0,0 +1,5 @@ +interface UserInfo { + id: number; + login: string; + rank: string; +} diff --git a/Front/src/vite-env.d.ts b/Front/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/Front/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/Front/tsconfig.json b/Front/tsconfig.json new file mode 100644 index 00000000..c93b233f --- /dev/null +++ b/Front/tsconfig.json @@ -0,0 +1,33 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "esModuleInterop": true, + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + + "types": ["react", "react-dom", "node", "jest"], + + // all paths defined here must match the configured path in `vite.config.ts` + "paths": { + "/*": ["./src/*"] + } + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/Front/tsconfig.node.json b/Front/tsconfig.node.json new file mode 100644 index 00000000..6675f46d --- /dev/null +++ b/Front/tsconfig.node.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true + }, + "include": ["vite.config.ts"] +} diff --git a/Front/vite.config.ts b/Front/vite.config.ts new file mode 100644 index 00000000..831ef233 --- /dev/null +++ b/Front/vite.config.ts @@ -0,0 +1,36 @@ +import react from '@vitejs/plugin-react-swc'; +import { fileURLToPath } from 'node:url'; +import { defineConfig, loadEnv } from 'vite'; + +// https://vitejs.dev/config/ +export default defineConfig(({ mode }) => { + const isProd = mode === 'production'; + const env = loadEnv(mode, process.cwd(), ''); + + return { + plugins: [react()], + define: { + 'process.env': { + NODE_ENV: env.NODE_ENV, + BASE_API: env.BASE_API, + HUB_API: env.HUB_API, + }, + }, + build: { + outDir: isProd ? 'dist' : 'dev', + assetsDir: isProd ? 'dist' : 'dev', + }, + resolve: { + alias: { + '/app': fileURLToPath(new URL('./src/app', import.meta.url)), + '/content': fileURLToPath(new URL('./src/content', import.meta.url)), + '/hubs': fileURLToPath(new URL('./src/hubs', import.meta.url)), + '/redux': fileURLToPath(new URL('./src/redux', import.meta.url)), + '/auth': fileURLToPath(new URL('./src/auth', import.meta.url)), + '/common': fileURLToPath(new URL('./src/common', import.meta.url)), + '/game': fileURLToPath(new URL('./src/game', import.meta.url)), + '/lobby': fileURLToPath(new URL('./src/lobby', import.meta.url)), + }, + }, + }; +}); diff --git a/Jackal.BotArena/Jackal.BotArena.csproj b/Jackal.BotArena/Jackal.BotArena.csproj new file mode 100644 index 00000000..7662ae30 --- /dev/null +++ b/Jackal.BotArena/Jackal.BotArena.csproj @@ -0,0 +1,13 @@ + + + + Exe + net8.0 + enable + + + + + + + diff --git a/Jackal.BotArena/Program.cs b/Jackal.BotArena/Program.cs new file mode 100644 index 00000000..b206d2e2 --- /dev/null +++ b/Jackal.BotArena/Program.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Jackal.Core; +using Jackal.Core.MapGenerator; +using Jackal.Core.MapGenerator.TilesPack; +using Jackal.Core.Players; + +namespace Jackal.BotArena; + +/// +/// Консольное приложение для оценки силы игроков ботов +/// +internal static class Program +{ + /// + /// Количество запускаемых игр + /// + private const int ArenaGamesCount = 120; + + /// + /// Размер карты + /// + private const int MapSize = 13; + + /// + /// Комбинации игроков ботов и их позиций (зависит от порядка) + /// + private static readonly IPlayer[][] CombinationOfPlayers = + [ + [ + new EasyPlayer(), + new RandomPlayer() + ], + [ + new EasyPlayer(), + new OakioPlayer(), + ], + [ + new OakioPlayer(), + new RandomPlayer() + ] + ]; + + /// + /// Статистика по каждому игроку боту + /// + private static readonly Dictionary BotStat = new(); + + private static void Main() + { + int gameNumber = 0; + + var timeElapsed = StopwatchMeter.GetElapsed(() => + { + while (gameNumber < ArenaGamesCount) + { + var mapId = new Random().Next(); + var randomMap = new RandomMapGenerator(mapId, MapSize, TilesPackFactory.Extended); + + foreach (var players in CombinationOfPlayers) + { + var gameRequest = new GameRequest(MapSize, randomMap, players); + var game = new Game(gameRequest); + + while (game.IsGameOver == false) + { + game.Turn(); + } + + CalcStat(game); + + gameNumber++; + if (gameNumber == ArenaGamesCount) + { + break; + } + } + } + }); + + ShowStat(gameNumber, timeElapsed); + } + + private static void CalcStat(Game game) + { + var maxCoins = game.Board.Teams.Max(x => x.Coins); + foreach (var team in game.Board.Teams) + { + if (!BotStat.TryGetValue(team.Name, out var stat)) + { + stat = new GamePlayerStat { PlayerName = team.Name }; + BotStat.Add(team.Name, stat); + } + stat.TotalWin += team.Coins == maxCoins ? 1 : 0; + stat.TotalCoins += team.Coins; + stat.GamesCountTotal += 1; + } + } + + private static void ShowStat(int gamesCount, TimeSpan timeElapsed) + { + Console.WriteLine($"Arena games count = {gamesCount} | Time elapsed {timeElapsed}"); + var orderedBotStat = BotStat.OrderByDescending(p => p.Value.AverageWin); + foreach (var (_, gamePlayerStat) in orderedBotStat) + { + Console.WriteLine( + $"Player name = {gamePlayerStat.PlayerName} | " + + $"Average win = {gamePlayerStat.AverageWin:P} | " + + $"Total win = {gamePlayerStat.TotalWin} | " + + $"Games count = {gamePlayerStat.GamesCountTotal} | " + + $"Average coins = {gamePlayerStat.AverageCoins:F} | " + + $"Total coins = {gamePlayerStat.TotalCoins}" + ); + } + } +} \ No newline at end of file diff --git a/Jackal.BotArena/StopwatchMeter.cs b/Jackal.BotArena/StopwatchMeter.cs new file mode 100644 index 00000000..e96351f6 --- /dev/null +++ b/Jackal.BotArena/StopwatchMeter.cs @@ -0,0 +1,15 @@ +using System; +using System.Diagnostics; + +namespace Jackal.BotArena; + +public static class StopwatchMeter +{ + public static TimeSpan GetElapsed(Action act) { + var sw = new Stopwatch(); + sw.Start(); + act(); + sw.Stop(); + return sw.Elapsed; + } +} \ No newline at end of file diff --git a/Jackal.Core/Actions/DrinkRumBottleAction.cs b/Jackal.Core/Actions/DrinkRumBottleAction.cs new file mode 100644 index 00000000..ebeade16 --- /dev/null +++ b/Jackal.Core/Actions/DrinkRumBottleAction.cs @@ -0,0 +1,25 @@ +using System; +using Jackal.Core.Domain; + +namespace Jackal.Core.Actions; + +public class DrinkRumBottleAction : IGameAction +{ + public void Act(Game game, Pirate pirate) + { + Board board = game.Board; + Team ourTeam = board.Teams[pirate.TeamId]; + Team? allyTeam = ourTeam.AllyTeamId.HasValue + ? board.Teams[ourTeam.AllyTeamId.Value] + : null; + + if (ourTeam.RumBottles == 0) + throw new Exception("No rum bottles"); + + ourTeam.RumBottles -= 1; + if (allyTeam != null) + allyTeam.RumBottles -= 1; + + pirate.IsInTrap = false; + } +} \ No newline at end of file diff --git a/Jackal.Core/Actions/GameActionList.cs b/Jackal.Core/Actions/GameActionList.cs new file mode 100644 index 00000000..cbf6ca41 --- /dev/null +++ b/Jackal.Core/Actions/GameActionList.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using Jackal.Core.Domain; + +namespace Jackal.Core.Actions; + +public class GameActionList(params IGameAction[] actions) : IGameAction +{ + private readonly List _actions = [..actions]; + + public void AddFirstAction(IGameAction action) + { + _actions.Insert(0, action); + } + + public void Act(Game game,Pirate pirate) + { + foreach (var action in _actions) + { + action.Act(game, pirate); + } + } +} \ No newline at end of file diff --git a/Jackal.Core/Actions/IGameAction.cs b/Jackal.Core/Actions/IGameAction.cs new file mode 100644 index 00000000..8d178570 --- /dev/null +++ b/Jackal.Core/Actions/IGameAction.cs @@ -0,0 +1,8 @@ +using Jackal.Core.Domain; + +namespace Jackal.Core.Actions; + +public interface IGameAction +{ + void Act(Game game, Pirate pirate); +} \ No newline at end of file diff --git a/Jackal.Core/Actions/MovingAction.cs b/Jackal.Core/Actions/MovingAction.cs new file mode 100644 index 00000000..8dac2796 --- /dev/null +++ b/Jackal.Core/Actions/MovingAction.cs @@ -0,0 +1,324 @@ +using System.Collections.Generic; +using System.Linq; +using Jackal.Core.Domain; + +namespace Jackal.Core.Actions; + +internal class MovingAction(TilePosition from, TilePosition to, TilePosition prev) : IGameAction +{ + public TilePosition To = to; + + public void Act(Game game, Pirate pirate) + { + Board board = game.Board; + Map map = board.Map; + + Team ourTeam = board.Teams[pirate.TeamId]; + Team? allyTeam = ourTeam.AllyTeamId.HasValue + ? board.Teams[ourTeam.AllyTeamId.Value] + : null; + + Tile targetTile = map[To.Position]; + Tile sourceTile = map[from.Position]; + Tile prevTile = map[prev.Position]; + + // открываем закрытую клетку + bool newTile = false; + if (targetTile.Type == TileType.Unknown) + { + targetTile = board.OpenTile(To.Position); + game.LastActionTurnNumber = game.TurnNumber; + game.CoinsOnMap += targetTile.CoinsCount(); + game.CoinsOnMap += targetTile.BigCoinsCount() * Constants.BigCoinValue; + newTile = true; + } + + // воздушный шар переносит сразу на наш корабль + if (targetTile.Type == TileType.Balloon) + { + To = new TilePosition(ourTeam.ShipPosition); + } + + // пушка выстреливает сразу в воду + if (targetTile.Type == TileType.Cannon) + { + To = GetCannonFly(targetTile.Direction, To.Position, board.MapSize); + } + + // ходим по задерживающей клетке + if (newTile && targetTile.Type == TileType.Spinning) + { + To = new TilePosition(To.Position, targetTile.SpinningCount - 1); + } + + // нашли карамбу + if (targetTile is { Type: TileType.Caramba, Used: false }) + { + // проходим по всем командам и собираем пиратов на кораблях + foreach (var team in board.Teams) + { + foreach (var pirateOnMap in team.Pirates) + { + if (pirateOnMap.Position.Position == team.ShipPosition) + continue; + + // возвращаем пирата на его корабль + game.MovePirateToTheShip(pirateOnMap); + } + } + + To = new TilePosition(ourTeam.ShipPosition); + targetTile.Used = true; + } + + // нашли разлом + if (targetTile is { Type: TileType.Quake, Used: false }) + { + game.SubTurn.QuakePhase = 2; + game.NeedSubTurnPirate = pirate; + game.PrevSubTurnPosition = prev; + targetTile.Used = true; + } + + // нашли Бен Ганна не маяком + if (targetTile is { Type: TileType.BenGunn, Used: false } && + game.SubTurn.LighthouseViewCount == 0) + { + game.AddPirate(pirate.TeamId, To, PirateType.BenGunn); + targetTile.Used = true; + } + + // нашли бутылки с ромом не маяком + if (targetTile is { Type: TileType.RumBottle, Used: false } && + game.SubTurn.LighthouseViewCount == 0) + { + board.Teams[pirate.TeamId].RumBottles += targetTile.Code; + if (allyTeam != null) + { + allyTeam.RumBottles += targetTile.Code; + } + + targetTile.Used = true; + } + + // нашли хи-хи траву не маяком + if (targetTile is { Type: TileType.Cannabis, Used: false } && + game.SubTurn.LighthouseViewCount == 0) + { + game.SubTurn.CannabisTurnCount += board.Teams.Length + 1; + targetTile.Used = true; + } + + // просматриваем карту с маяка, + // перезатираем просматриваемую клетку текущей позицией пирата, + // важно вызвать после всех установок поля to + if (game.SubTurn.LighthouseViewCount > 0) + { + game.SubTurn.LighthouseViewCount--; + To = pirate.Position; + + if (targetTile.Type == TileType.Hole) + { + var holeTiles = board.AllTiles(x => x.Type == TileType.Hole).ToList(); + if (holeTiles.Count == 2) + { + // открыли вторую дыру - пираты меняются местами + game.SwapPiratePosition(holeTiles[0], holeTiles[1]); + } + } + } + + // нашли маяк + if (targetTile is { Type: TileType.Lighthouse, Used: false }) + { + var unknownTilesCount = game.Board.AllTiles(x => x.Type == TileType.Unknown).Count(); + var remainedTilesViewCount = unknownTilesCount - game.SubTurn.LighthouseViewCount; + game.SubTurn.LighthouseViewCount += remainedTilesViewCount < 4 ? remainedTilesViewCount : 4; + targetTile.Used = true; + } + + targetTile = map[To.Position]; + TileLevel targetTileLevel = map[To]; + TileLevel fromTileLevel = map[from]; + + if (from.Position == ourTeam.ShipPosition && + targetTile.Type == TileType.Water && + Board.GetPossibleShipMoves(ourTeam.ShipPosition, game.Board.MapSize).Contains(To.Position)) + { + // двигаем свой корабль + var pirateOnShips = map[ourTeam.ShipPosition].Pirates; + foreach (var pirateOnShip in pirateOnShips) + { + pirateOnShip.Position = To; + targetTileLevel.Pirates.Add(pirateOnShip); + } + ourTeam.ShipPosition = To.Position; + sourceTile.Pirates.Clear(); + } + else if (allyTeam != null && + from.Position == allyTeam.ShipPosition && + targetTile.Type == TileType.Water && + Board.GetPossibleShipMoves(allyTeam.ShipPosition, game.Board.MapSize).Contains(To.Position)) + { + // двигаем союзный корабль + var pirateOnShips = map[allyTeam.ShipPosition].Pirates; + foreach (var pirateOnShip in pirateOnShips) + { + pirateOnShip.Position = To; + targetTileLevel.Pirates.Add(pirateOnShip); + } + allyTeam.ShipPosition = To.Position; + sourceTile.Pirates.Clear(); + } + else + { + // двигаем своего пирата + fromTileLevel.Pirates.Remove(pirate); + + pirate.Position = To; + targetTileLevel.Pirates.Add(pirate); + } + + if (game.SubTurn.LighthouseViewCount > 0 || + (targetTile is { Used: false, Type: TileType.Airplane } && + from != To)) + { + game.NeedSubTurnPirate = pirate; + game.PrevSubTurnPosition = prev; + } + + // заход в дыру, не из дыры + if (targetTile.Type == TileType.Hole && !game.SubTurn.FallingInTheHole) + { + var holeTiles = board.AllTiles(x => x.Type == TileType.Hole).ToList(); + + var freeHoleTiles = holeTiles + .Where(x => x.Position != targetTile.Position && x.HasNoEnemy(ourTeam.EnemyTeamIds)) + .ToList(); + + if(holeTiles.Count == 1) + { + // пират застрял в единственной дыре + pirate.IsInHole = true; + } + else if (newTile && holeTiles.Count == 2) + { + // открыли вторую дыру - пираты меняются местами + game.SwapPiratePosition(holeTiles[0], holeTiles[1]); + } + else if (freeHoleTiles.Count >= 1) + { + // даем выбор куда идти, брать монету или нет: + // доступная одна свободная дыра, но на ней монета или идем с монетой + // доступно несколько свободных дыр + game.NeedSubTurnPirate = pirate; + game.PrevSubTurnPosition = prev; + game.SubTurn.FallingInTheHole = true; + } + } + + if (newTile && targetTile.Type is TileType.Arrow or TileType.Horse or TileType.Ice or TileType.Crocodile) + { + var airplaneFlying = targetTile.Type is TileType.Ice or TileType.Crocodile && + (prevTile is { Type: TileType.Airplane, Used: false } || + game.SubTurn.AirplaneFlying); + + AvailableMovesTask task = new AvailableMovesTask(pirate.TeamId, To, prev); + List moves = game.Board.GetAllAvailableMoves( + task, + task.Source, + task.Prev, + new SubTurnState() + ); + + if (moves.Count == 0 && + !airplaneFlying) + { + game.KillPirate(pirate); + return; + } + + game.NeedSubTurnPirate = pirate; + game.PrevSubTurnPosition = prev; + game.SubTurn.AirplaneFlying = airplaneFlying; + } + else + { + game.SubTurn.AirplaneFlying = false; + } + + // отмечаем, что мы использовали самолет + if (from != To) + { + if(sourceTile is { Type: TileType.Airplane, Used: false }) + sourceTile.Used = true; + + if(prevTile is { Type: TileType.Airplane, Used: false }) + prevTile.Used = true; + } + + // проверяем, не попадаем ли мы на чужой корабль - тогда мы погибли + IEnumerable enemyShips = game.Board.Teams + .Where(x => ourTeam.EnemyTeamIds.Contains(x.Id)) + .Select(x => x.ShipPosition); + + if (enemyShips.Contains(To.Position)) + { + game.KillPirate(pirate); + return; + } + + if (targetTileLevel.Pirates.Any(x => x.TeamId == pirate.TeamId)) + { + // убиваем чужих пиратов + var enemyPirates = targetTileLevel.Pirates + .Where(x => ourTeam.EnemyTeamIds.Contains(x.TeamId)) + .ToList(); + + foreach (var enemyPirate in enemyPirates) + { + if (targetTile.Type == TileType.Jungle) + continue; + + if (targetTile.Type == TileType.Water) + game.KillPirate(enemyPirate); + + game.MovePirateToTheShip(enemyPirate); + } + } + + switch (targetTile.Type) + { + case TileType.RumBarrel: + // проводим пьянку для пирата + pirate.DrunkSinceTurnNumber = game.TurnNumber; + pirate.IsDrunk = true; + break; + case TileType.Trap: + if (targetTile.Pirates.Count > 1) + { + foreach (Pirate pirateOnTile in targetTile.Pirates) + { + pirateOnTile.IsInTrap = false; + } + } + else + { + pirate.IsInTrap = true; + } + break; + case TileType.Cannibal: + game.KillPirate(pirate); + return; + } + } + + private static TilePosition GetCannonFly(DirectionType direction, Position pos, int mapSize) => + direction switch + { + DirectionType.Up => new TilePosition(pos.X, mapSize - 1), + DirectionType.Right => new TilePosition(mapSize - 1, pos.Y), + DirectionType.Down => new TilePosition(pos.X, 0), + _ => new TilePosition(0, pos.Y) + }; +} \ No newline at end of file diff --git a/Jackal.Core/Actions/MovingWithBigCoinAction.cs b/Jackal.Core/Actions/MovingWithBigCoinAction.cs new file mode 100644 index 00000000..f35b47f5 --- /dev/null +++ b/Jackal.Core/Actions/MovingWithBigCoinAction.cs @@ -0,0 +1,62 @@ +using System; +using Jackal.Core.Domain; + +namespace Jackal.Core.Actions; + +internal class MovingWithBigCoinAction(TilePosition from, TilePosition to, TilePosition prev) : IGameAction +{ + public void Act(Game game, Pirate pirate) + { + var movingAction = new MovingAction(from, to, prev); + movingAction.Act(game, pirate); + to = movingAction.To; + + Board board = game.Board; + Map map = board.Map; + + Team ourTeam = board.Teams[pirate.TeamId]; + Team? allyTeam = ourTeam.AllyTeamId.HasValue + ? board.Teams[ourTeam.AllyTeamId.Value] + : null; + + Tile targetTile = map[to.Position]; + TileLevel targetTileLevel = map[to]; + TileLevel fromTileLevel = map[from]; + + if (fromTileLevel.BigCoins == 0) + throw new Exception("No big coins"); + + fromTileLevel.BigCoins--; + + if (ourTeam.ShipPosition == to.Position || + (allyTeam != null && + allyTeam.ShipPosition == to.Position)) + { + // перенос монеты на корабль + ourTeam.Coins += Constants.BigCoinValue; + if (allyTeam != null) + allyTeam.Coins += Constants.BigCoinValue; + + game.CoinsOnMap -= Constants.BigCoinValue; + game.LastActionTurnNumber = game.TurnNumber; + } + else if (targetTile.Type == TileType.Water) + { + // монета в воде - тонет + game.CoinsOnMap -= Constants.BigCoinValue; + game.LostCoins += Constants.BigCoinValue; + game.LastActionTurnNumber = game.TurnNumber; + } + else if (targetTile.Type == TileType.Cannibal) + { + // монета на людоеде - пропадает т.к. Пятница не реализован + game.CoinsOnMap -= Constants.BigCoinValue; + game.LostCoins += Constants.BigCoinValue; + game.LastActionTurnNumber = game.TurnNumber; + } + else + { + targetTileLevel.BigCoins++; + } + } +} \ No newline at end of file diff --git a/Jackal.Core/Actions/MovingWithCoinAction.cs b/Jackal.Core/Actions/MovingWithCoinAction.cs new file mode 100644 index 00000000..34605f31 --- /dev/null +++ b/Jackal.Core/Actions/MovingWithCoinAction.cs @@ -0,0 +1,62 @@ +using System; +using Jackal.Core.Domain; + +namespace Jackal.Core.Actions; + +internal class MovingWithCoinAction(TilePosition from, TilePosition to, TilePosition prev) : IGameAction +{ + public void Act(Game game, Pirate pirate) + { + var movingAction = new MovingAction(from, to, prev); + movingAction.Act(game, pirate); + to = movingAction.To; + + Board board = game.Board; + Map map = board.Map; + + Team ourTeam = board.Teams[pirate.TeamId]; + Team? allyTeam = ourTeam.AllyTeamId.HasValue + ? board.Teams[ourTeam.AllyTeamId.Value] + : null; + + Tile targetTile = map[to.Position]; + TileLevel targetTileLevel = map[to]; + TileLevel fromTileLevel = map[from]; + + if (fromTileLevel.Coins == 0) + throw new Exception("No coins"); + + fromTileLevel.Coins--; + + if (ourTeam.ShipPosition == to.Position || + (allyTeam != null && + allyTeam.ShipPosition == to.Position)) + { + // перенос монеты на корабль + ourTeam.Coins++; + if (allyTeam != null) + allyTeam.Coins++; + + game.CoinsOnMap--; + game.LastActionTurnNumber = game.TurnNumber; + } + else if (targetTile.Type == TileType.Water) + { + // монета в воде - тонет + game.CoinsOnMap--; + game.LostCoins++; + game.LastActionTurnNumber = game.TurnNumber; + } + else if (targetTile.Type == TileType.Cannibal) + { + // монета на людоеде - пропадает т.к. Пятница не реализован + game.CoinsOnMap--; + game.LostCoins++; + game.LastActionTurnNumber = game.TurnNumber; + } + else + { + targetTileLevel.Coins++; + } + } +} \ No newline at end of file diff --git a/Jackal.Core/Actions/QuakeAction.cs b/Jackal.Core/Actions/QuakeAction.cs new file mode 100644 index 00000000..c2e5910a --- /dev/null +++ b/Jackal.Core/Actions/QuakeAction.cs @@ -0,0 +1,47 @@ +using Jackal.Core.Domain; + +namespace Jackal.Core.Actions; + +public class QuakeAction(TilePosition from, TilePosition to) : IGameAction +{ + public void Act(Game game, Pirate pirate) + { + var map = game.Board.Map; + + // выбираем вторую клетку для разлома + if (game.SubTurn.QuakePhase == 1) + { + game.SubTurn.QuakePhase = 0; + game.Board.Generator.Swap(from.Position, to.Position); + + // меняем клетки местами + var fromTile = map[from.Position]; + var toTile = map[to.Position]; + + map[from.Position] = new Tile(from.Position, toTile) + { + Used = toTile.Used + }; + + map[to.Position] = new Tile(to.Position, fromTile) + { + Used = fromTile.Used + }; + + // даем доиграть маяк, если разлом был открыт с маяка + if (game.SubTurn.LighthouseViewCount > 0) + { + game.NeedSubTurnPirate = pirate; + game.PrevSubTurnPosition = pirate.Position; + } + } + + // выбираем первую клетку для разлома + if (game.SubTurn.QuakePhase == 2) + { + game.SubTurn.QuakePhase = 1; + game.NeedSubTurnPirate = pirate; + game.PrevSubTurnPosition = to; + } + } +} \ No newline at end of file diff --git a/Jackal.Core/Actions/RespawnAction.cs b/Jackal.Core/Actions/RespawnAction.cs new file mode 100644 index 00000000..b9ac918c --- /dev/null +++ b/Jackal.Core/Actions/RespawnAction.cs @@ -0,0 +1,11 @@ +using Jackal.Core.Domain; + +namespace Jackal.Core.Actions; + +internal class RespawnAction : IGameAction +{ + public void Act(Game game, Pirate pirate) + { + game.AddPirate(pirate.TeamId, pirate.Position, PirateType.Usual); + } +} \ No newline at end of file diff --git a/Jackal.Core/ArrowsCodesHelper.cs b/Jackal.Core/ArrowsCodesHelper.cs new file mode 100644 index 00000000..86897ba8 --- /dev/null +++ b/Jackal.Core/ArrowsCodesHelper.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Jackal.Core.Domain; + +namespace Jackal.Core; + +public static class ArrowsCodesHelper +{ + /// + /// Одна стрелка перпендикулярно вверх + /// + public static int OneArrowUp => GetCodeFromString("10000000"); + + /// + /// Одна стрелка по диагонали правый верхний угол + /// + public static int OneArrowDiagonal => GetCodeFromString("01000000"); + + /// + /// Две стрелки по диагонали правый верхний - левый нижний угол + /// + public static int TwoArrowsDiagonal => GetCodeFromString("01000100"); + + /// + /// Две стрелки горизонтально на левую и правую стороны + /// + public static int TwoArrowsLeftRight => GetCodeFromString("00100010"); + + /// + /// Три стрелки одна по диагонали левый верхний угол, две перпендикулярно право и низ + /// + public static int ThreeArrows => GetCodeFromString("00101001"); + + /// + /// Четыре стрелки перпендикулярно на все стороны + /// + public static int FourArrowsPerpendicular => GetCodeFromString("10101010"); + + /// + /// Четыре стрелки по диагонали на все углы + /// + public static int FourArrowsDiagonal => GetCodeFromString("01010101"); + + /// + /// Последовательность типов стрелок не менять, + /// используется для выбора номера картинки + /// + private static readonly int[] ArrowsTypes = + [ + OneArrowUp, + TwoArrowsDiagonal, + OneArrowDiagonal, + FourArrowsDiagonal, + FourArrowsPerpendicular, + ThreeArrows, + TwoArrowsLeftRight + ]; + + public class ArrowSearchResult + { + public int ArrowType; + public int RotateCount; + } + + public static ArrowSearchResult Search(int code) + { + for (int arrowType = 0; arrowType < ArrowsTypes.Length; arrowType++) + { + int arrowsCode = ArrowsTypes[arrowType]; + for (int rotateCount = 0; rotateCount <= 3; rotateCount++) + { + if (arrowsCode == code) return new ArrowSearchResult {ArrowType = arrowType, RotateCount = rotateCount}; + arrowsCode = DoRotate(arrowsCode); + } + } + throw new Exception("Unknown arrow type"); + } + + public static int GetCodeFromString(string str) + { + if (str.Length != 8) throw new ArgumentException("str"); + str = new string(str.Reverse().ToArray()); + return Convert.ToInt32(str, 2); + } + + /// + /// Поворот по часовой стрелке + /// + public static int DoRotate(int code) + { + code &= 255; + //биты 6 и 7 + int bits67 = code >> 6; + int bits05 = code & 63; + int newBits = (bits05 << 2) | bits67; + return newBits; + } + + public static IEnumerable GetExitDeltas(int code) + { + code &= 255; + if ((code & 1) != 0) + yield return new Position(0, 1); + if ((code & 2) != 0) + yield return new Position(1, 1); + if ((code & 4) != 0) + yield return new Position(1, 0); + if ((code & 8) != 0) + yield return new Position(1, -1); + if ((code & 16) != 0) + yield return new Position(0, -1); + if ((code & 32) != 0) + yield return new Position(-1, -1); + if ((code & 64) != 0) + yield return new Position(-1, 0); + if ((code & 128) != 0) + yield return new Position(-1, 1); + } +} \ No newline at end of file diff --git a/Jackal.Core/AvailableMove.cs b/Jackal.Core/AvailableMove.cs new file mode 100644 index 00000000..884375c0 --- /dev/null +++ b/Jackal.Core/AvailableMove.cs @@ -0,0 +1,41 @@ +using Jackal.Core.Actions; +using Jackal.Core.Domain; + +namespace Jackal.Core; + +/// +/// Возможный ход +/// +public class AvailableMove(TilePosition from, TilePosition to, params IGameAction[] actions) +{ + /// + /// Список действий, которые надо выполнить + /// + public readonly GameActionList ActionList = new(actions); + + /// + /// Откуда идем + /// + public readonly TilePosition From = from; + + /// + /// Куда идем + /// + public readonly TilePosition To = to; + + /// + /// Предыдущая позиция - требуется для определения с какой кдетки идем на закрытую клетку, + /// это важно если открываем лед или крокодила, т.к. дальше эти клетки используют prev + /// + public Position? Prev; + + /// + /// Тип хода + /// + public MoveType MoveType = MoveType.Usual; + + /// + /// Ход + /// + public Move ToMove => new(From, To, Prev, MoveType); +} \ No newline at end of file diff --git a/Jackal.Core/AvailableMoveFactory.cs b/Jackal.Core/AvailableMoveFactory.cs new file mode 100644 index 00000000..16e76efc --- /dev/null +++ b/Jackal.Core/AvailableMoveFactory.cs @@ -0,0 +1,49 @@ +using Jackal.Core.Actions; +using Jackal.Core.Domain; + +namespace Jackal.Core; + +public static class AvailableMoveFactory +{ + public static AvailableMove RespawnMove(TilePosition from, TilePosition to) + { + var respawnAction = new RespawnAction(); + return new AvailableMove(from, to, respawnAction) + { + MoveType = MoveType.WithRespawn + }; + } + + public static AvailableMove QuakeMove(TilePosition from, TilePosition to, TilePosition prev) + { + var quakeAction = new QuakeAction(prev, to); + return new AvailableMove(from, to, quakeAction) + { + MoveType = MoveType.WithQuake + }; + } + + public static AvailableMove UsualMove(TilePosition from, TilePosition to, TilePosition prev) + { + var moving = new MovingAction(from, to, prev); + return new AvailableMove(from, to, moving); + } + + public static AvailableMove CoinMove(TilePosition from, TilePosition to, TilePosition prev) + { + var movingWithCoin = new MovingWithCoinAction(from, to, prev); + return new AvailableMove(from, to, movingWithCoin) + { + MoveType = MoveType.WithCoin + }; + } + + public static AvailableMove BigCoinMove(TilePosition from, TilePosition to, TilePosition prev) + { + var movingWithBigCoin = new MovingWithBigCoinAction(from, to, prev); + return new AvailableMove(from, to, movingWithBigCoin) + { + MoveType = MoveType.WithBigCoin + }; + } +} \ No newline at end of file diff --git a/Jackal.Core/AvailableMoveResult.cs b/Jackal.Core/AvailableMoveResult.cs new file mode 100644 index 00000000..eb3c577d --- /dev/null +++ b/Jackal.Core/AvailableMoveResult.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using Jackal.Core.Actions; +using Jackal.Core.Domain; + +namespace Jackal.Core; + +public class AvailableMoveResult +{ + public List AvailableMoves { get; } = new(); + + public List Actions { get; } = new(); +} \ No newline at end of file diff --git a/Jackal.Core/AvailableMovesTask.cs b/Jackal.Core/AvailableMovesTask.cs new file mode 100644 index 00000000..849ac7d1 --- /dev/null +++ b/Jackal.Core/AvailableMovesTask.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using Jackal.Core.Domain; + +namespace Jackal.Core; + +/// +/// Задача на поиск доступных ходов +/// +/// ИД команды +/// Текущая позиция +/// Предыдущая позиция +public record AvailableMovesTask(int TeamId, TilePosition Source, TilePosition Prev) +{ + /// + /// Просмотренные позиции + /// + public readonly List AlreadyCheckedList = []; +} \ No newline at end of file diff --git a/Jackal.Core/Board.cs b/Jackal.Core/Board.cs new file mode 100644 index 00000000..e49ef5a4 --- /dev/null +++ b/Jackal.Core/Board.cs @@ -0,0 +1,502 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Jackal.Core.Domain; +using Jackal.Core.MapGenerator; +using Newtonsoft.Json; + +namespace Jackal.Core; + +public class Board +{ + [JsonIgnore] + public readonly IMapGenerator Generator; + + /// + /// Размер стороны карты с учетом воды + /// + public readonly int MapSize; + + public readonly Map Map; + + public Team[] Teams; + + [JsonIgnore] + public List AllPirates + { + get + { + var allPirates = new List(); + foreach (var teamPirates in Teams.Select(item => item.Pirates.ToList())) + { + allPirates.AddRange(teamPirates); + } + + return allPirates; + } + } + + [JsonIgnore] + public List? DeadPirates { get; set; } + + public IEnumerable AllTiles(Predicate selector) + { + for (int i = 0; i < MapSize; i++) + { + for (int j = 0; j < MapSize; j++) + { + var tile = Map[i, j]; + if (selector(tile)) + yield return tile; + } + } + } + + [JsonConstructor] + public Board(int mapSize, Map map, Team[] teams) + { + MapSize = mapSize; + Map = map; + Teams = teams; + } + + public Board(GameRequest request) + { + Generator = request.MapGenerator; + MapSize = request.MapSize; + Teams = TeamsFactory.Create(request); + Map = MapFactory.Create(MapSize, Teams); + } + + /// + /// Возвращаем список всех полей, в которые можно попасть из исходного поля + /// + public List GetAllAvailableMoves( + AvailableMovesTask task, + TilePosition source, + TilePosition prev, + SubTurnState subTurn) + { + var sourceTile = Map[source.Position]; + + var ourTeamId = task.TeamId; + var ourTeam = Teams[ourTeamId]; + var allyTeam = ourTeam.AllyTeamId.HasValue + ? Teams[ourTeam.AllyTeamId.Value] + : null; + + if (sourceTile.Type is TileType.Arrow or TileType.Horse or TileType.Ice or TileType.Crocodile) + { + var prevMoveDelta = sourceTile.Type == TileType.Ice + ? Position.GetDelta(prev.Position, source.Position) + : null; + + // запоминаем, что в текущую клетку уже не надо возвращаться + task.AlreadyCheckedList.Add(new CheckedPosition(source, prevMoveDelta)); + } + + var goodTargets = new List(); + + // доступно воскрешение + if (sourceTile.Type == TileType.RespawnFort && + sourceTile.Pirates.Any(p => p.Type == PirateType.Usual) && + ourTeam.Pirates.Count(p => p.Type == PirateType.Usual) < 3) + { + var respawnMove = AvailableMoveFactory.RespawnMove(task.Source, source); + goodTargets.Add(respawnMove); + task.AlreadyCheckedList.Add(new CheckedPosition(source)); + } + + // места всех возможных ходов + IEnumerable positionsForCheck = GetAllTargetsForSubTurn( + source, prev, ourTeam, subTurn + ); + + foreach (var newPosition in positionsForCheck) + { + if (task.AlreadyCheckedList.Count > 0) + { + var incomeDelta = Position.GetDelta(source.Position, newPosition.Position); + var currentCheck = new CheckedPosition(newPosition, incomeDelta); + + if (WasCheckedBefore(task.AlreadyCheckedList, currentCheck)) + { + // попали по рекурсии в ранее просмотренную клетку + continue; + } + } + + if (subTurn.QuakePhase > 0) + { + // разыгрываем ход разлома + var quakeMove = AvailableMoveFactory.QuakeMove(task.Source, newPosition, prev); + goodTargets.Add(quakeMove); + continue; + } + + var usualMove = AvailableMoveFactory.UsualMove(task.Source, newPosition, source); + var coinMove = AvailableMoveFactory.CoinMove(task.Source, newPosition, source); + var bigCoinMove = AvailableMoveFactory.BigCoinMove(task.Source, newPosition, source); + + // проверяем, что на этой клетке + var newPositionTile = Map[newPosition.Position]; + + switch (newPositionTile.Type) + { + case TileType.Unknown: + usualMove.Prev = !subTurn.AirplaneFlying ? source.Position : null; + usualMove.MoveType = subTurn.LighthouseViewCount > 0 + ? MoveType.WithLighthouse + : MoveType.Usual; + + goodTargets.Add(usualMove); + break; + + case TileType.Water: + if (ourTeam.ShipPosition == newPosition.Position || + (allyTeam != null && allyTeam.ShipPosition == newPosition.Position)) + { + // заходим на свой корабль + goodTargets.Add(usualMove); + + if (Map[task.Source].Coins > 0) + goodTargets.Add(coinMove); + + if (Map[task.Source].BigCoins > 0) + goodTargets.Add(bigCoinMove); + } + else if (sourceTile.Type == TileType.Water) + { + // пират плавает на корабле или брасом + goodTargets.Add(usualMove); + } + else if (sourceTile.Type is TileType.Arrow or TileType.Cannon or TileType.Ice) + { + // ныряем с земли в воду + goodTargets.Add(usualMove); + + if (Map[task.Source].Coins > 0) + goodTargets.Add(coinMove); + + if (Map[task.Source].BigCoins > 0) + goodTargets.Add(bigCoinMove); + } + + break; + + case TileType.Fort: + case TileType.RespawnFort: + if (newPositionTile.HasNoEnemy(ourTeam.EnemyTeamIds)) + { + // форт не занят противником + goodTargets.Add(usualMove); + } + break; + + case TileType.Ice: + case TileType.Horse: + case TileType.Arrow: + case TileType.Crocodile: + goodTargets.AddRange( + GetAllAvailableMoves( + task, + newPosition, + source, + subTurn + ) + ); + break; + + case TileType.Jungle: + goodTargets.Add(usualMove); + break; + + default: + goodTargets.Add(usualMove); + + var newPositionTileLevel = Map[newPosition]; + if (Map[task.Source].Coins > 0 && + newPositionTileLevel.HasNoEnemy(ourTeam.EnemyTeamIds)) + { + goodTargets.Add(coinMove); + } + + if (Map[task.Source].BigCoins > 0 && + newPositionTileLevel.HasNoEnemy(ourTeam.EnemyTeamIds)) + { + goodTargets.Add(bigCoinMove); + } + break; + } + } + + return goodTargets; + } + + /// + /// Возвращаем все позиции, в которые в принципе достижимы из заданной клетки за один подход + /// (не проверяется, допустим ли такой ход) + /// + private List GetAllTargetsForSubTurn( + TilePosition source, + TilePosition prev, + Team ourTeam, + SubTurnState subTurn) + { + var allyTeam = ourTeam.AllyTeamId.HasValue + ? Teams[ourTeam.AllyTeamId.Value] + : null; + + var rez = GetNearDeltas(source.Position) + .Where(IsValidMapPosition) + .Where(x => + Map[x].Type != TileType.Water || + x == ourTeam.ShipPosition || + (allyTeam != null && x == allyTeam.ShipPosition) + ) + .Select(IncomeTilePosition); + + var sourceTile = Map[source.Position]; + switch (sourceTile.Type) + { + case TileType.Hole: + var holeTiles = AllTiles(x => x.Type == TileType.Hole).ToList(); + if (holeTiles.Count == 1) + { + rez = new List(); + } + else if (subTurn.FallingInTheHole) + { + var freeHoleTiles = holeTiles + .Where(x => x.Position != source.Position && x.HasNoEnemy(ourTeam.EnemyTeamIds)) + .ToList(); + + if (freeHoleTiles.Count > 0) + { + rez = freeHoleTiles.Select(x => IncomeTilePosition(x.Position)); + } + } + break; + case TileType.Horse: + rez = GetHorseDeltas(source.Position) + .Where(IsValidMapPosition) + .Where(x => + Map[x].Type != TileType.Water || Teams.Select(t => t.ShipPosition).Contains(x) + ) + .Select(IncomeTilePosition); + break; + case TileType.Arrow: + rez = GetArrowsDeltas(sourceTile.Code, source.Position) + .Select(IncomeTilePosition); + break; + case TileType.Airplane: + if (sourceTile.Used == false) + { + rez = GetAirplaneMoves(ourTeam.ShipPosition, allyTeam?.ShipPosition); + } + break; + case TileType.Crocodile: + if (subTurn.AirplaneFlying) + { + rez = GetAirplaneMoves(ourTeam.ShipPosition, allyTeam?.ShipPosition); + break; + } + + rez = new[] { IncomeTilePosition(prev.Position) }; + break; + case TileType.Ice: + if (subTurn.AirplaneFlying) + { + rez = GetAirplaneMoves(ourTeam.ShipPosition, allyTeam?.ShipPosition); + break; + } + + var prevDelta = Position.GetDelta(prev.Position, source.Position); + var target = Position.AddDelta(source.Position, prevDelta); + rez = new[] { IncomeTilePosition(target) }; + break; + case TileType.Spinning: + if (source.Level > 0 && !subTurn.DrinkRumBottle) + { + rez = new[] { new TilePosition(source.Position, source.Level - 1) }; + } + break; + case TileType.Water: + if (source.Position == ourTeam.ShipPosition || + (allyTeam != null && source.Position == allyTeam.ShipPosition)) + { + // со своего корабля + rez = GetPossibleShipMoves(source.Position, MapSize) + .Concat(new[] {GetShipLanding(source.Position)}) + .Select(IncomeTilePosition); + } + else + { + // пират плавает в воде + rez = GetPossibleSwimming(source.Position) + .Select(IncomeTilePosition); + } + break; + } + + if (subTurn.LighthouseViewCount > 0) + { + // просмотр карты с маяка + rez = AllTiles(x => x.Type == TileType.Unknown) + .Select(x => IncomeTilePosition(x.Position)); + } + + if (subTurn.QuakePhase > 0) + { + // перемещение клеток землетрясением + rez = AllTiles(x => + x.Coins == 0 && + x.BigCoins == 0 && + x.Type != TileType.Water && + !x.Levels.SelectMany(l => l.Pirates).Any() && + (x.Position != prev.Position || subTurn.QuakePhase == 2) + ) + .Select(x => IncomeTilePosition(x.Position)); + } + + return rez.Where(x => IsValidMapPosition(x.Position)).ToList(); + } + + public void ShowUnknownTiles() + { + var unknownTiles = AllTiles(x => x.Type == TileType.Unknown); + foreach (var tile in unknownTiles) + { + OpenTile(tile.Position); + } + } + + public Tile OpenTile(Position position) + { + var tile = Generator.GetNext(position); + Map[position] = tile; + + return tile; + } + + private IEnumerable GetAirplaneMoves(Position ourShipPosition, Position? allyShipPosition) => + AllTiles(x => + x.Type != TileType.Ice && + x.Type != TileType.Crocodile && + x.Type != TileType.Horse && + ( + x.Type != TileType.Water || + x.Position == ourShipPosition || + x.Position == allyShipPosition + ) + ) + .Select(x => IncomeTilePosition(x.Position)); + + private TilePosition IncomeTilePosition(Position pos) + { + return IsValidMapPosition(pos) && Map[pos].Type == TileType.Spinning + ? new TilePosition(pos, Map[pos].SpinningCount - 1) + : new TilePosition(pos); + } + + private static IEnumerable GetHorseDeltas(Position pos) + { + for (int x = -2; x <= 2; x++) + { + if (x == 0) continue; + int deltaY = (Math.Abs(x) == 2) ? 1 : 2; + yield return new Position(pos.X + x, pos.Y - deltaY); + yield return new Position(pos.X + x, pos.Y + deltaY); + } + } + + private static IEnumerable GetNearDeltas(Position pos) + { + for (int x = -1; x <= 1; x++) + { + for (int y = -1; y <= 1; y++) + { + if (x == 0 && y == 0) continue; + yield return new Position(pos.X + x, pos.Y + y); + } + } + } + + private bool IsValidMapPosition(Position pos) + { + return ( + pos.X >= 0 && pos.X < MapSize + && pos.Y >= 0 && pos.Y < MapSize //попадаем в карту + && Utils.InCorners(pos, 0, MapSize - 1) == false //не попадаем в углы карты + ); + } + + public static IEnumerable GetPossibleShipMoves(Position shipPosition, int mapSize) + { + if (shipPosition.X == 0 || shipPosition.X == mapSize - 1) + { + if (shipPosition.Y > 2) + yield return new Position(shipPosition.X, shipPosition.Y - 1); + if (shipPosition.Y < mapSize - 3) + yield return new Position(shipPosition.X, shipPosition.Y + 1); + } + else if (shipPosition.Y == 0 || shipPosition.Y == mapSize - 1) + { + if (shipPosition.X > 2) + yield return new Position(shipPosition.X - 1, shipPosition.Y); + if (shipPosition.X < mapSize - 3) + yield return new Position(shipPosition.X + 1, shipPosition.Y); + } + else + { + throw new Exception("Wrong ship position"); + } + } + + private Position GetShipLanding(Position pos) + { + if (pos.X == 0) + return new Position(1, pos.Y); + + if (pos.X == MapSize - 1) + return new Position(MapSize - 2, pos.Y); + + if (pos.Y == 0) + return new Position(pos.X, 1); + + if (pos.Y == MapSize - 1) + return new Position(pos.X, MapSize - 2); + + throw new Exception("Wrong ship position"); + } + + private static IEnumerable GetArrowsDeltas(int arrowsCode, Position source) + { + foreach (var delta in ArrowsCodesHelper.GetExitDeltas(arrowsCode)) + { + yield return new Position(source.X + delta.X, source.Y + delta.Y); + } + } + + /// + /// Все возможные цели для плавающего пирата + /// + private IEnumerable GetPossibleSwimming(Position pos) + { + return GetNearDeltas(pos).Where(IsValidMapPosition).Where(x => Map[x].Type == TileType.Water); + } + + private static bool WasCheckedBefore(List alreadyCheckedList, CheckedPosition currentCheck) + { + foreach (var info in alreadyCheckedList) + { + if (info.Position == currentCheck.Position) + { + if (info.IncomeDelta == null || info.IncomeDelta == currentCheck.IncomeDelta) + return true; + } + } + + return false; + } +} \ No newline at end of file diff --git a/Jackal.Core/CheckedPosition.cs b/Jackal.Core/CheckedPosition.cs new file mode 100644 index 00000000..81289be0 --- /dev/null +++ b/Jackal.Core/CheckedPosition.cs @@ -0,0 +1,15 @@ +using Jackal.Core.Domain; + +namespace Jackal.Core; + +public class CheckedPosition +{ + public TilePosition Position; + public Position? IncomeDelta; + + public CheckedPosition(TilePosition position, Position? incomeDelta = null) + { + Position = position; + IncomeDelta = incomeDelta; + } +} \ No newline at end of file diff --git a/Jackal.Core/Constants.cs b/Jackal.Core/Constants.cs new file mode 100644 index 00000000..6726616b --- /dev/null +++ b/Jackal.Core/Constants.cs @@ -0,0 +1,9 @@ +namespace Jackal.Core; + +public class Constants +{ + /// + /// Ценность большой монеты в монетах + /// + public const int BigCoinValue = 3; +} \ No newline at end of file diff --git a/Jackal.Core/Domain/DirectionType.cs b/Jackal.Core/Domain/DirectionType.cs new file mode 100644 index 00000000..bc05257d --- /dev/null +++ b/Jackal.Core/Domain/DirectionType.cs @@ -0,0 +1,12 @@ +namespace Jackal.Core.Domain; + +/// +/// Направление игровой клетки +/// +public enum DirectionType +{ + Up = 0, + Right = 1, + Down = 2, + Left = 3 +} \ No newline at end of file diff --git a/Jackal.Core/Domain/Map.cs b/Jackal.Core/Domain/Map.cs new file mode 100644 index 00000000..a43cd62e --- /dev/null +++ b/Jackal.Core/Domain/Map.cs @@ -0,0 +1,24 @@ +using Newtonsoft.Json; + +namespace Jackal.Core.Domain; + +public class Map(int mapSize) +{ + [JsonProperty] + private readonly Tile[,] Tiles = new Tile[mapSize, mapSize]; + + public Tile this[Position pos] + { + get => Tiles[pos.X, pos.Y]; + internal set => Tiles[pos.X, pos.Y] = value; + } + + public TileLevel this[TilePosition pos] => + Tiles[pos.Position.X, pos.Position.Y].Levels[pos.Level]; + + public Tile this[int x, int y] + { + get => Tiles[x, y]; + internal set => Tiles[x, y] = value; + } +} \ No newline at end of file diff --git a/Jackal.Core/Domain/MapFactory.cs b/Jackal.Core/Domain/MapFactory.cs new file mode 100644 index 00000000..a627d60c --- /dev/null +++ b/Jackal.Core/Domain/MapFactory.cs @@ -0,0 +1,55 @@ +namespace Jackal.Core.Domain; + +public static class MapFactory +{ + public static Map Create(int mapSize, Team[] teams) + { + var map = new Map(mapSize); + + for (int i = 0; i < mapSize; i++) + { + map.SetWater(i, 0); + map.SetWater(0, i); + map.SetWater(i, mapSize - 1); + map.SetWater(mapSize - 1, i); + } + + for (int x = 1; x < mapSize - 1; x++) + { + for (int y = 1; y < mapSize - 1; y++) + { + if ((x == 1 || x == mapSize - 2) && (y == 1 || y == mapSize - 2)) + map.SetWater(x, y); + else + map.SetUnknown(x, y); + } + } + + map.SetPiratesOnMap(teams); + + return map; + } + + private static void SetWater(this Map map, int x, int y) + { + var tile = new Tile(TileParams.Water(x, y)); + map[x, y] = tile; + } + + private static void SetUnknown(this Map map, int x, int y) + { + var tile = new Tile(TileParams.Unknown(x, y)); + map[x, y] = tile; + } + + private static void SetPiratesOnMap(this Map map, Team[] teams) + { + foreach (var team in teams) + { + foreach (var pirate in team.Pirates) + { + map[team.ShipPosition].Pirates.Add(pirate); + } + } + } +} \ No newline at end of file diff --git a/Jackal.Core/Domain/Move.cs b/Jackal.Core/Domain/Move.cs new file mode 100644 index 00000000..7d107e80 --- /dev/null +++ b/Jackal.Core/Domain/Move.cs @@ -0,0 +1,66 @@ +using System; +using Newtonsoft.Json; + +namespace Jackal.Core.Domain; + +/// +/// Ход +/// +[method: JsonConstructor] +public record Move(TilePosition From, TilePosition To, Position? Prev, MoveType Type = MoveType.Usual) +{ + /// + /// Откуда идем + /// + [JsonProperty] + public readonly TilePosition From = From ?? throw new ArgumentException(nameof(From)); + + /// + /// Куда идем + /// + [JsonProperty] + public readonly TilePosition To = To ?? throw new ArgumentException(nameof(To)); + + /// + /// Предыдущая позиция + /// + [JsonProperty] + public readonly Position? Prev = Prev; + + /// + /// Тип хода + /// + [JsonProperty] + public readonly MoveType Type = Type; + + /// + /// Использовать бутылку с ромом + /// + public bool WithRumBottle => + Type is MoveType.WithRumBottle or MoveType.WithRumBottleAndCoin or MoveType.WithRumBottleAndBigCoin; + + /// + /// Перенос монеты + /// + public bool WithCoin => Type is MoveType.WithCoin or MoveType.WithRumBottleAndCoin; + + /// + /// Перенос большой монеты + /// + public bool WithBigCoin => Type is MoveType.WithBigCoin or MoveType.WithRumBottleAndBigCoin; + + /// + /// Воскрешение пирата на бабе + /// + public bool WithRespawn => Type == MoveType.WithRespawn; + + /// + /// Открытие неизвестной клетки с маяка + /// + public bool WithLighthouse => Type == MoveType.WithLighthouse; + + /// + /// Выбор клетки для разлома + /// + public bool WithQuake => Type == MoveType.WithQuake; +} \ No newline at end of file diff --git a/Jackal.Core/Domain/MoveType.cs b/Jackal.Core/Domain/MoveType.cs new file mode 100644 index 00000000..2a424773 --- /dev/null +++ b/Jackal.Core/Domain/MoveType.cs @@ -0,0 +1,52 @@ +namespace Jackal.Core.Domain; + +/// +/// Тип хода +/// +public enum MoveType +{ + /// + /// Обычный + /// + Usual = 0, + + /// + /// Перенос монеты + /// + WithCoin = 1, + + /// + /// Воскрешение пирата на бабе + /// + WithRespawn = 2, + + /// + /// Открытие неизвестной клетки с маяка + /// + WithLighthouse = 3, + + /// + /// Выбор клетки для разлома + /// + WithQuake = 4, + + /// + /// Перенос большой монеты + /// + WithBigCoin = 5, + + /// + /// Выход за бутылку с ромом + /// + WithRumBottle = 6, + + /// + /// Выход за бутылку с ромом с монетой + /// + WithRumBottleAndCoin = 7, + + /// + /// Выход за бутылку с ромом с большой монетой + /// + WithRumBottleAndBigCoin = 8, +}; \ No newline at end of file diff --git a/Jackal.Core/Domain/Pirate.cs b/Jackal.Core/Domain/Pirate.cs new file mode 100644 index 00000000..fd7e9b04 --- /dev/null +++ b/Jackal.Core/Domain/Pirate.cs @@ -0,0 +1,80 @@ +using System; +using Newtonsoft.Json; + +namespace Jackal.Core.Domain; + +public record Pirate(int TeamId, TilePosition Position, PirateType Type) +{ + [JsonProperty] + public readonly Guid Id = Guid.NewGuid(); + + [JsonProperty] + public PirateType Type = Type; + + [JsonProperty] + public readonly int TeamId = TeamId; + + [JsonProperty] + public TilePosition Position = Position; + + /// + /// Напился на бочке с ромом + /// + [JsonProperty] + public bool IsDrunk; + + /// + /// Номер хода с которого пират поддался пьянству, + /// наша команда категорически не одобряет пьянство + /// + public int? DrunkSinceTurnNumber; + + /// + /// Попал в ловушку + /// + [JsonProperty] + public bool IsInTrap; + + /// + /// Застрял в дыре + /// + [JsonProperty] + public bool IsInHole; + + /// + /// Пират доступен для хода + /// + public bool IsActive => IsDrunk == false && IsInTrap == false && IsInHole == false; + + /// + /// Пират застрял + /// + public bool IsDisable => IsInTrap || IsInHole; + + /// + /// Сбросить все эффекты недоступности + /// + public void ResetEffects() + { + IsInTrap = false; + IsInHole = false; + IsDrunk = false; + DrunkSinceTurnNumber = null; + } + + public virtual bool Equals(Pirate? other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Id.Equals(other.Id) && + Position.Equals(other.Position) && + IsDrunk == other.IsDrunk && + IsInTrap == other.IsInTrap && + IsInHole == other.IsInHole; + } + + public override int GetHashCode() + { + return HashCode.Combine(Id); + } +} \ No newline at end of file diff --git a/Jackal.Core/Domain/PirateType.cs b/Jackal.Core/Domain/PirateType.cs new file mode 100644 index 00000000..9d50171b --- /dev/null +++ b/Jackal.Core/Domain/PirateType.cs @@ -0,0 +1,14 @@ +namespace Jackal.Core.Domain; + +public enum PirateType +{ + /// + /// Обычный + /// + Usual = 0, + + /// + /// Бен Ганн + /// + BenGunn = 1 +} \ No newline at end of file diff --git a/Jackal.Core/Domain/Position.cs b/Jackal.Core/Domain/Position.cs new file mode 100644 index 00000000..f939dd6b --- /dev/null +++ b/Jackal.Core/Domain/Position.cs @@ -0,0 +1,38 @@ +using Newtonsoft.Json; + +namespace Jackal.Core.Domain; + +public record Position +{ + [JsonProperty] + public readonly int X; + + [JsonProperty] + public readonly int Y; + + public Position() + { + } + + public Position(int x, int y) + { + X = x; + Y = y; + } + + public Position(Position position) + { + X = position.X; + Y = position.Y; + } + + public static Position GetDelta(Position from, Position to) + { + return new Position(to.X - from.X, to.Y - from.Y); + } + + public static Position AddDelta(Position pos, Position delta) + { + return new Position(pos.X + delta.X, pos.Y + delta.Y); + } +} \ No newline at end of file diff --git a/Jackal.Core/Domain/Team.cs b/Jackal.Core/Domain/Team.cs new file mode 100644 index 00000000..b66d2c57 --- /dev/null +++ b/Jackal.Core/Domain/Team.cs @@ -0,0 +1,88 @@ +using Jackal.Core.Players; +using Newtonsoft.Json; + +namespace Jackal.Core.Domain; + +/// +/// Команда пиратов +/// +public record Team +{ + /// + /// ИД команды + /// + public readonly int Id; + + /// + /// Имя команды + /// + public readonly string Name; + + /// + /// ИД пользователя + /// + public readonly long UserId; + + /// + /// Позиция корабля + /// + public Position ShipPosition; + + /// + /// Пираты - бравые ребята + /// + public Pirate[] Pirates; + + /// + /// ИД команд противников + /// + public int[] EnemyTeamIds; + + /// + /// ИД команды союзника + /// + public int? AllyTeamId; + + /// + /// Монеты на корабле + /// + public int Coins; + + /// + /// Бутылки с ромом + /// + public int RumBottles; + + [JsonConstructor] + public Team(int id, string name, long userId, Position shipPosition, Pirate[] pirates) + { + Id = id; + Name = name; + UserId = userId; + ShipPosition = shipPosition; + Pirates = pirates; + } + + public Team(int id, IPlayer player, int x, int y, int piratesPerPlayer) + { + Id = id; + ShipPosition = new Position(x, y); + + if (player is IHumanPlayer humanPlayer) + { + UserId = humanPlayer.UserId; + Name = humanPlayer.Name; + } + else + { + UserId = 0; + Name = player.GetType().Name; + } + + Pirates = new Pirate[piratesPerPlayer]; + for (int i = 0; i < Pirates.Length; i++) + { + Pirates[i] = new Pirate(id, new TilePosition(ShipPosition), PirateType.Usual); + } + } +} \ No newline at end of file diff --git a/Jackal.Core/Domain/TeamsFactory.cs b/Jackal.Core/Domain/TeamsFactory.cs new file mode 100644 index 00000000..e643c3d5 --- /dev/null +++ b/Jackal.Core/Domain/TeamsFactory.cs @@ -0,0 +1,60 @@ +using System; + +namespace Jackal.Core.Domain; + +public static class TeamsFactory +{ + public static Team[] Create(GameRequest request) + { + var players = request.Players; + var teams = new Team[players.Length]; + + switch (players.Length) + { + case 1: + teams[0] = new Team(0, players[0], (request.MapSize - 1) / 2, 0, request.PiratesPerPlayer); + teams[0].EnemyTeamIds = []; + break; + case 2: + teams[0] = new Team(0, players[0], (request.MapSize - 1) / 2, 0, request.PiratesPerPlayer); + teams[0].EnemyTeamIds = [1]; + + teams[1] = new Team(1, players[1], (request.MapSize - 1) / 2, (request.MapSize - 1), request.PiratesPerPlayer); + teams[1].EnemyTeamIds = [0]; + break; + case 4: + teams[0] = new Team(0, players[0], (request.MapSize - 1) / 2, 0, request.PiratesPerPlayer); + teams[1] = new Team(1, players[1], 0, (request.MapSize - 1) / 2, request.PiratesPerPlayer); + teams[2] = new Team(2, players[2], (request.MapSize - 1) / 2, (request.MapSize- 1), request.PiratesPerPlayer); + teams[3] = new Team(3, players[3], (request.MapSize - 1), (request.MapSize - 1) / 2, request.PiratesPerPlayer); + + if (request.GameMode == GameModeType.TwoPlayersInTeam) + { + teams[0].EnemyTeamIds = [1, 3]; + teams[0].AllyTeamId = 2; + + teams[1].EnemyTeamIds = [0, 2]; + teams[1].AllyTeamId = 3; + + teams[2].EnemyTeamIds = [1, 3]; + teams[2].AllyTeamId = 0; + + teams[3].EnemyTeamIds = [0, 2]; + teams[3].AllyTeamId = 1; + } + else + { + teams[0].EnemyTeamIds = [1, 2, 3]; + teams[1].EnemyTeamIds = [0, 2, 3]; + teams[2].EnemyTeamIds = [0, 1, 3]; + teams[3].EnemyTeamIds = [0, 1, 2]; + } + + break; + default: + throw new NotSupportedException("Only one player, two players or four"); + } + + return teams; + } +} \ No newline at end of file diff --git a/Jackal.Core/Domain/Tile.cs b/Jackal.Core/Domain/Tile.cs new file mode 100644 index 00000000..3fffa6fd --- /dev/null +++ b/Jackal.Core/Domain/Tile.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; + +namespace Jackal.Core.Domain; + +public record Tile +{ + /// + /// Позиция + /// + [JsonProperty] + public readonly Position Position; + + /// + /// Тип клетки + /// + [JsonProperty] + public readonly TileType Type; + + /// + /// Код клетки: + /// задаёт подтип клетки для стрелок и пустых клеток, + /// задаёт количество для клеток с монетами и бутылками + /// + [JsonProperty] + public readonly int Code; + + /// + /// Направление клетки (или количество поворотов) + /// + [JsonProperty] + public readonly DirectionType Direction; + + /// + /// Количество ходов на задерживающей клетке + /// + [JsonProperty] + public readonly int SpinningCount; + + /// + /// Уровни клетки (0 - обычный уровень/уровень выхода с клетки) + /// + [JsonProperty] + public readonly List Levels = []; + + /// + /// Использована (например самолет уже взлетал) + /// + [JsonProperty] + public bool Used; + + [JsonIgnore] + public int Coins => Levels[0].Coins; + + [JsonIgnore] + public int BigCoins => Levels[0].BigCoins; + + [JsonIgnore] + public int? OccupationTeamId => Levels[0].OccupationTeamId; + + /// + /// Предлагаю выкинуть пиратов из тайлов, + /// для отрисовки на задерживающих клетках ввести в + /// Team->Pirates->Position зачение z + /// + [JsonIgnore] + public HashSet Pirates => Levels[0].Pirates; + + public Tile() + { + } + + public Tile(Position position, Tile tile) + { + Position = position; + Type = tile.Type; + Code = tile.Code; + Direction = tile.Direction; + SpinningCount = tile.SpinningCount; + + InitLevels(); + } + + public Tile(TileParams tileParams) + { + Position = tileParams.Position; + Type = tileParams.Type; + Code = tileParams.Code; + Direction = tileParams.Direction; + SpinningCount = tileParams.SpinningCount; + + InitLevels(); + } + + private void InitLevels() + { + int levelsCount = Type == TileType.Spinning ? SpinningCount : 1; + for (int level = 0; level < levelsCount; level++) + { + var tileLevel = new TileLevel(new TilePosition(Position, level)); + Levels.Add(tileLevel); + } + } + + public bool HasNoEnemy(int[] enemyTeamIds) => + OccupationTeamId.HasValue == false || !enemyTeamIds.Contains(OccupationTeamId.Value); + + public virtual bool Equals(Tile? other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Position.Equals(other.Position) && + Type == other.Type && + Code == other.Code && + Direction == other.Direction && + SpinningCount == other.SpinningCount && + Levels.SequenceEqual(other.Levels) && + Used == other.Used; + } + + public override int GetHashCode() + { + return HashCode.Combine(Position, (int)Type, Code, (int)Direction, SpinningCount, Levels); + } +} \ No newline at end of file diff --git a/Jackal.Core/Domain/TileLevel.cs b/Jackal.Core/Domain/TileLevel.cs new file mode 100644 index 00000000..873befb9 --- /dev/null +++ b/Jackal.Core/Domain/TileLevel.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; + +namespace Jackal.Core.Domain; + +public record TileLevel(TilePosition Position) +{ + [JsonProperty] + public int Coins; + + [JsonProperty] + public int BigCoins; + + [JsonProperty] + public readonly TilePosition Position = Position; + + [JsonProperty] + public readonly HashSet Pirates = []; + + [JsonIgnore] + public int? OccupationTeamId => Pirates.Count > 0 ? Pirates.First().TeamId : null; + + public bool HasNoEnemy(int[] enemyTeamIds) => + OccupationTeamId.HasValue == false || !enemyTeamIds.Contains(OccupationTeamId.Value); + + public virtual bool Equals(TileLevel? other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Coins == other.Coins && + BigCoins == other.BigCoins && + Position.Equals(other.Position) && + Pirates.SequenceEqual(other.Pirates); + } + + public override int GetHashCode() + { + return HashCode.Combine(Position, Pirates); + } +} \ No newline at end of file diff --git a/Jackal.Core/Domain/TileParams.cs b/Jackal.Core/Domain/TileParams.cs new file mode 100644 index 00000000..ff5021f4 --- /dev/null +++ b/Jackal.Core/Domain/TileParams.cs @@ -0,0 +1,243 @@ +using System; + +namespace Jackal.Core.Domain; + +public class TileParams : IClonable +{ + /// + /// Позиция + /// + public Position Position; + + /// + /// Тип клетки + /// + public readonly TileType Type; + + /// + /// Код клетки: + /// задаёт подтип клетки для стрелок и пустых клеток, + /// задаёт количество для клеток с монетами и бутылками + /// + public int Code; + + /// + /// Направление клетки (или количество поворотов) + /// + public DirectionType Direction; + + /// + /// Количество ходов на задерживающей клетке + /// + public int SpinningCount; + + private TileParams(TileType type) + { + Type = type; + } + + private TileParams(TileType type, int code) + { + Type = type; + Code = code; + } + + public TileParams Clone() + { + return (TileParams)MemberwiseClone(); + } + + /// + /// Вода + /// + /// Позиция X на карте + /// Позиция У на карте + public static TileParams Water(int x, int y) => new(TileType.Water) { Position = new Position(x, y) }; + + /// + /// Неизвестная клетка суши + /// + /// Позиция X на карте + /// Позиция У на карте + public static TileParams Unknown(int x, int y) => new(TileType.Unknown) { Position = new Position(x, y) }; + + /// + /// Пустая клетка + /// + /// Номер изображения + public static TileParams Empty(int imageNumber = 1) + { + if (imageNumber < 1 || imageNumber > 4) + throw new ArgumentException( + "Номер изображения для TileType.Empty должен быть от 1 до 4 включительно", + nameof(imageNumber) + ); + + return new TileParams(TileType.Empty, imageNumber); + } + + /// + /// Монета + /// + /// Количество монет + public static TileParams Coin(int count = 1) => new(TileType.Coin, count); + + /// + /// Большая монета + /// + /// Количество больших монет + public static TileParams BigCoin(int count = 1) => new(TileType.BigCoin, count); + + /// + /// Бутылка с ромом + /// + /// Количество бутылок + public static TileParams RumBottle(int count = 1) => new(TileType.RumBottle, count); + + /// + /// Лес - требуется 2 хода для прохождения клетки + /// + public static TileParams SpinningForest() => new(TileType.Spinning) { SpinningCount = 2 }; + + /// + /// Пустыня - требуется 3 хода для прохождения клетки + /// + public static TileParams SpinningDesert() => new(TileType.Spinning) { SpinningCount = 3 }; + + /// + /// Болото - требуется 4 хода для прохождения клетки + /// + public static TileParams SpinningSwamp() => new(TileType.Spinning) { SpinningCount = 4 }; + + /// + /// Гора - требуется 5 ходов для прохождения клетки + /// + public static TileParams SpinningMount() => new(TileType.Spinning) { SpinningCount = 5 }; + + /// + /// Одна стрелка перпендикулярно вверх + /// + public static TileParams OneArrowUp() => new(TileType.Arrow, ArrowsCodesHelper.OneArrowUp); + + /// + /// Одна стрелка по диагонали правый верхний угол + /// + public static TileParams OneArrowDiagonal() => new(TileType.Arrow, ArrowsCodesHelper.OneArrowDiagonal); + + /// + /// Две стрелки по диагонали правый верхний - левый нижний угол + /// + public static TileParams TwoArrowsDiagonal() => new(TileType.Arrow, ArrowsCodesHelper.TwoArrowsDiagonal); + + /// + /// Две стрелки горизонтально на левую и правую стороны + /// + public static TileParams TwoArrowsLeftRight() => new(TileType.Arrow, ArrowsCodesHelper.TwoArrowsLeftRight); + + /// + /// Три стрелки одна по диагонали левый верхний угол, две перпендикулярно право и низ + /// + public static TileParams ThreeArrows() => new(TileType.Arrow, ArrowsCodesHelper.ThreeArrows); + + /// + /// Четыре стрелки перпендикулярно на все стороны + /// + public static TileParams FourArrowsPerpendicular() => new(TileType.Arrow, ArrowsCodesHelper.FourArrowsPerpendicular); + + /// + /// Четыре стрелки по диагонали на все углы + /// + public static TileParams FourArrowsDiagonal() => new(TileType.Arrow, ArrowsCodesHelper.FourArrowsDiagonal); + + /// + /// Дыра, задаём направление клетки чтобы пираты не закрывали изображение дыры + /// + public static TileParams Hole() => new(TileType.Hole) { Direction = DirectionType.Left }; + + /// + /// Форт + /// + public static TileParams Fort() => new(TileType.Fort); + + /// + /// Воскрешающий форт + /// + public static TileParams RespawnFort() => new(TileType.RespawnFort); + + /// + /// Людоед + /// + public static TileParams Cannibal() => new(TileType.Cannibal); + + /// + /// Бочка с ромом + /// + public static TileParams RumBarrel() => new(TileType.RumBarrel); + + /// + /// Лошадь + /// + public static TileParams Horse() => new(TileType.Horse); + + /// + /// Воздушный шар + /// + public static TileParams Balloon() => new(TileType.Balloon); + + /// + /// Самолёт + /// + public static TileParams Airplane() => new(TileType.Airplane); + + /// + /// Крокодил + /// + public static TileParams Crocodile() => new(TileType.Crocodile); + + /// + /// Лёд + /// + public static TileParams Ice() => new(TileType.Ice); + + /// + /// Пушка + /// + /// Направление задаётся для тестов, в игре выбирается рэндомом + public static TileParams Cannon(DirectionType direction = DirectionType.Up) => + new(TileType.Cannon) { Direction = direction }; + + /// + /// Ловушка + /// + public static TileParams Trap() => new(TileType.Trap); + + /// + /// Маяк + /// + public static TileParams Lighthouse() => new(TileType.Lighthouse); + + /// + /// Бен Ганн + /// + public static TileParams BenGunn() => new(TileType.BenGunn); + + /// + /// Карамба + /// + public static TileParams Caramba() => new(TileType.Caramba); + + /// + /// Джунгли + /// + public static TileParams Jungle() => new(TileType.Jungle); + + /// + /// Землетрясение + /// + public static TileParams Quake() => new(TileType.Quake); + + /// + /// Хи-хи трава + /// + public static TileParams Cannabis() => new(TileType.Cannabis); +} \ No newline at end of file diff --git a/Jackal.Core/Domain/TilePosition.cs b/Jackal.Core/Domain/TilePosition.cs new file mode 100644 index 00000000..ea2b571b --- /dev/null +++ b/Jackal.Core/Domain/TilePosition.cs @@ -0,0 +1,37 @@ +using System; +using Newtonsoft.Json; + +namespace Jackal.Core.Domain; + +public record TilePosition +{ + [JsonProperty] + public readonly Position Position; + + [JsonIgnore] + public int X => Position.X; + + [JsonIgnore] + public int Y => Position.Y; + + [JsonProperty] + public readonly int Level; + + public TilePosition(int x, int y, int level = 0) + : this(new Position(x, y), level) + { + } + + [JsonConstructor] + public TilePosition(Position position, int level = 0) + { + if (position == null) + throw new ArgumentNullException(nameof(position)); + + if (level < 0 || level > 4) + throw new ArgumentException(nameof(level)); + + Position = position; + Level = level; + } +} \ No newline at end of file diff --git a/Jackal.Core/Domain/TileType.cs b/Jackal.Core/Domain/TileType.cs new file mode 100644 index 00000000..8c3ebae5 --- /dev/null +++ b/Jackal.Core/Domain/TileType.cs @@ -0,0 +1,31 @@ +namespace Jackal.Core.Domain; + +public enum TileType +{ + Unknown, + Water, + Empty, + Coin, + BigCoin, + Fort, + RespawnFort, + RumBarrel, + RumBottle, + Horse, + Arrow, + Balloon, + Ice, + Airplane, + Crocodile, + Spinning, + Trap, + Cannibal, + Cannon, + Lighthouse, + BenGunn, + Caramba, + Jungle, + Hole, + Quake, + Cannabis +} \ No newline at end of file diff --git a/Jackal.Core/Game.cs b/Jackal.Core/Game.cs new file mode 100644 index 00000000..9bd6ea76 --- /dev/null +++ b/Jackal.Core/Game.cs @@ -0,0 +1,396 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Jackal.Core.Actions; +using Jackal.Core.Domain; +using Jackal.Core.MapGenerator; +using Jackal.Core.Players; +using Newtonsoft.Json; + +namespace Jackal.Core; + +public record GameRequest( + // данные по карте todo подумать об объединении + int MapSize, + IMapGenerator MapGenerator, + + // данные по игрокам + IPlayer[] Players, + GameModeType GameMode = GameModeType.FreeForAll, + int PiratesPerPlayer = 3 +); + +public class Game : ICompletable +{ + private readonly IPlayer[] _players; + + public readonly Board Board; + + /// + /// Открытое золото на карте + /// + public int CoinsOnMap; + + /// + /// Потерянные монеты + /// + public int LostCoins; + + /// + /// Режим игры + /// + public readonly GameModeType GameMode; + + /// + /// Рэндом для выбора игровых сообщений + /// + public readonly int MessagesKitRandom = new Random().Next(); + + /// + /// Индекс набора игровых сообщений + /// + [JsonIgnore] + private int MessagesKitIndex => Math.Abs(MessagesKitRandom % GameMessages.Kit.Length); + + /// + /// Игровое сообщение + /// + [JsonIgnore] + public string GameMessage { get; private set; } + + public Game(GameRequest request) + { + _players = request.Players; + GameMode = request.GameMode; + + foreach (var player in _players) + { + player.OnNewGame(); + } + + Board = new Board(request); + GameMessage = GameMessages.Kit[MessagesKitIndex][0]; + } + + public void Turn() + { + var result = GetAvailableMoves(CurrentTeamId); + + NeedSubTurnPirate = null; + PrevSubTurnPosition = null; + + if (result.AvailableMoves.Count > 0) + { + // есть возможные ходы + var gameState = new GameState + { + AvailableMoves = result.AvailableMoves.ToArray(), + Board = Board, + TeamId = CurrentTeamId, + UserId = Board.Teams[CurrentPlayerIndex].UserId + }; + var (moveNum, pirateId) = CurrentPlayer.OnMove(gameState); + + var move = result.AvailableMoves[moveNum]; + var currentTeamPirates = Board.Teams[CurrentTeamId].Pirates + .Where(x => !x.IsDrunk && !x.IsInHole && x.Position == move.From) + .Where(x => (!x.IsInTrap && !move.WithRumBottle) || move.WithRumBottle) + .ToList(); + + var pirate = currentTeamPirates.FirstOrDefault(x => x.Id == pirateId) ?? currentTeamPirates.First(); + IGameAction action = result.Actions[moveNum]; + action.Act(this, pirate); + } + + if (NeedSubTurnPirate == null) + { + //также протрезвляем всех пиратов, которые начали бухать раньше текущего хода + foreach (Pirate pirate in Board.Teams[CurrentTeamId].Pirates.Where(x => x.IsDrunk && x.DrunkSinceTurnNumber < TurnNumber)) + { + pirate.DrunkSinceTurnNumber = null; + pirate.IsDrunk = false; + } + + TurnNumber++; + SubTurn.Clear(); + } + + (IsGameOver, string gameOverMessage) = CheckGameOver(); + + if (IsGameOver) + { + var maxCoins = Board.Teams.Max(x => x.Coins); + var winners = Board.Teams.Length == 1 || Board.Teams.Any(x => x.Coins != maxCoins) + ? string.Join(" и ", Board.Teams.Where(x => x.Coins == maxCoins).Select(x => x.Name)) + : "дружбы"; + + GameMessage = $"Победа {winners} путём {gameOverMessage}!"; + } + else + { + var gameMessages = GameMessages.Kit[MessagesKitIndex]; + GameMessage = gameMessages[TurnNumber / _players.Length % gameMessages.Length]; + } + } + + /// + /// TODO-MAD является результатом действия - + /// Пират которому требуется дополнительный ход + /// + public Pirate? NeedSubTurnPirate { private get; set; } + + /// + /// TODO-MAD является результатом действия - + /// Предыдущая позиция пирата которому требуется дополнительный ход + /// + public TilePosition? PrevSubTurnPosition { get; set; } + + /// + /// TODO-MAD является результатом действия - + /// Состояние дополнительного хода + /// + public SubTurnState SubTurn { get; } = new(); + + public List GetAvailableMoves() + { + var result = GetAvailableMoves(CurrentTeamId); + return result.AvailableMoves; + } + + private AvailableMoveResult GetAvailableMoves(int teamId) + { + var result = new AvailableMoveResult(); + var targets = new List(); + + Team team = Board.Teams[teamId]; + if (SubTurn.CannabisTurnCount == 0 && + team.RumBottles > 0 && + NeedSubTurnPirate == null) + { + IEnumerable piratesWithRumBottles = team.Pirates.Where(x => x.IsInTrap || x.Position.Level > 0); + foreach (var pirate in piratesWithRumBottles) + { + AvailableMovesTask task = new AvailableMovesTask(teamId, pirate.Position, pirate.Position); + List moves = Board.GetAllAvailableMoves( + task, + task.Source, + task.Prev, + new SubTurnState { DrinkRumBottle = true } + ); + foreach (var move in moves) + { + var drinkRumBottleAction = new DrinkRumBottleAction(); + move.ActionList.AddFirstAction(drinkRumBottleAction); + move.MoveType = move.MoveType switch + { + MoveType.WithCoin => MoveType.WithRumBottleAndCoin, + MoveType.WithBigCoin => MoveType.WithRumBottleAndBigCoin, + _ => MoveType.WithRumBottle + }; + } + + targets.AddRange(moves); + } + } + + IEnumerable activePirates = NeedSubTurnPirate != null + ? new[] { NeedSubTurnPirate } + : team.Pirates.Where(x => x.IsActive); + + foreach (var pirate in activePirates) + { + TilePosition prev = PrevSubTurnPosition ?? pirate.Position; + AvailableMovesTask task = new AvailableMovesTask(teamId, pirate.Position, prev); + List moves = Board.GetAllAvailableMoves( + task, + task.Source, + task.Prev, + SubTurn + ); + targets.AddRange(moves); + } + + foreach (var availableMove in targets) + { + var move = availableMove.ToMove; + if (result.AvailableMoves.Exists(x => x == move)) + continue; + + result.AvailableMoves.Add(move); + result.Actions.Add(availableMove.ActionList); + } + + return result; + } + + /// + /// Конец игры + /// + public bool IsGameOver { get; private set; } + + public bool IsCompleted => IsGameOver; + + /// + /// Текущий ход - определяет какая команда ходит + /// + public int TurnNumber { get; private set; } + + /// + /// Последний ход когда производилось действие: + /// открытие новой клетки или перенос монеты + /// + public int LastActionTurnNumber { get; internal set; } + + /// + /// ИД команды пиратов чей ход + /// + public int CurrentTeamId => TurnNumber % _players.Length; + + /// + /// Индекс игрока чей ход, + /// отличается от ИД команды при розыгрыше хи-хи травы + /// + public int CurrentPlayerIndex => (TurnNumber + (SubTurn.CannabisTurnCount > 0 ? 1 : 0)) % _players.Length; + + /// + /// Игрок который ходит + /// + public IPlayer CurrentPlayer => _players[CurrentPlayerIndex]; + + /// + /// Убрать пирата с карты + /// + public void KillPirate(Pirate pirate) + { + int teamId = pirate.TeamId; + Board.Teams[teamId].Pirates = Board.Teams[teamId].Pirates.Where(x => x != pirate).ToArray(); + var tile = Board.Map[pirate.Position]; + tile.Pirates.Remove(pirate); + + Board.DeadPirates ??= []; + Board.DeadPirates.Add(pirate); + } + + /// + /// Добавить нового пирата на карту на открытую клетку + /// + public void AddPirate(int teamId, TilePosition position, PirateType type) + { + if (Board.Map[position.Position].Type == TileType.Unknown) + { + throw new Exception("Tile must not be Unknown"); + } + + var newPirate = new Pirate(teamId, position, type); + Board.Teams[teamId].Pirates = Board.Teams[teamId].Pirates.Concat(new[] { newPirate }).ToArray(); + + var tile = Board.Map[position]; + tile.Pirates.Add(newPirate); + } + + /// + /// Переместить пирата на его корабль + /// + public void MovePirateToTheShip(Pirate pirate) + { + var team = Board.Teams[pirate.TeamId]; + var pirateTileLevel = Board.Map[pirate.Position]; + + pirate.Position = new TilePosition(team.ShipPosition); + Board.Map[team.ShipPosition].Pirates.Add(pirate); + pirateTileLevel.Pirates.Remove(pirate); + + pirate.ResetEffects(); + } + + private void MovePirateToPosition(Pirate pirate, Position position) + { + var pirateTileLevel = Board.Map[pirate.Position]; + + pirate.Position = new TilePosition(position); + Board.Map[position].Pirates.Add(pirate); + pirateTileLevel.Pirates.Remove(pirate); + + pirate.ResetEffects(); + } + + public void SwapPiratePosition(Tile firstTile, Tile secondTile) + { + var pirates = new List(secondTile.Pirates); + foreach (var movedPirate in firstTile.Pirates) + { + MovePirateToPosition(movedPirate, secondTile.Position); + } + + foreach (var movedPirate in pirates) + { + MovePirateToPosition(movedPirate, firstTile.Position); + } + } + + private (bool GameOver, string Message) CheckGameOver() + { + var orderedTeamByCoins = Board.Teams + .OrderByDescending(x => x.Coins) + .ToList(); + + // игра на несколько игроков + if (orderedTeamByCoins.Count == 4 && + GameMode == GameModeType.TwoPlayersInTeam) + { + // свободное золото + int freeCoins = Board.Generator.TotalCoins - LostCoins - orderedTeamByCoins.Sum(x => x.Coins) / 2; + + // игрок затащил большую часть монет + int firstTeamCoins = orderedTeamByCoins[0].Coins; + int secondTeamCoins = orderedTeamByCoins[2].Coins; + var secondTeamPirates = orderedTeamByCoins[2].Pirates.Length + orderedTeamByCoins[3].Pirates.Length; + if (freeCoins == 0 || + firstTeamCoins > secondTeamCoins + freeCoins || + (firstTeamCoins > secondTeamCoins && secondTeamPirates == 0)) + { + return (true, "доминирования по золоту"); + } + } + else if (orderedTeamByCoins.Count > 1) + { + // свободное золото + int freeCoins = Board.Generator.TotalCoins - LostCoins - orderedTeamByCoins.Sum(x => x.Coins); + + // игрок затащил большую часть монет + int firstTeamCoins = orderedTeamByCoins[0].Coins; + int secondTeamCoins = orderedTeamByCoins[1].Coins; + var otherTeamsPirates = orderedTeamByCoins + .Where(x => x.Id != orderedTeamByCoins[0].Id) + .Sum(x => x.Pirates.Length); + + if (freeCoins == 0 || + firstTeamCoins > secondTeamCoins + freeCoins || + (firstTeamCoins > secondTeamCoins && otherTeamsPirates == 0)) + { + return (true, "доминирования по золоту"); + } + } + + // все клетки открыты и нет золота на карте + var allTilesOpen = !Board.AllTiles(x => x.Type == TileType.Unknown).Any(); + if (allTilesOpen && CoinsOnMap == 0) + { + return (true, "исследования карты"); + } + + // закончились пираты + if (Board.AllPirates.All(p => p.IsDisable)) + { + return (true, "конца всех пиратов"); + } + + // защита от яичинга (ходов без открытия клеток или переноса монет) + if (TurnNumber - 50 * _players.Length > LastActionTurnNumber) + { + return (true, "яичинга"); + } + + return (false, ""); + } +} \ No newline at end of file diff --git a/Jackal.Core/GameMessages.cs b/Jackal.Core/GameMessages.cs new file mode 100644 index 00000000..3c17229c --- /dev/null +++ b/Jackal.Core/GameMessages.cs @@ -0,0 +1,123 @@ +namespace Jackal.Core; + +public static class GameMessages +{ + /// + /// Наборы игровых сообщений + /// + public static readonly string[][] Kit = + [ + [ + "Дамы и господа!", + "Сейчас вы услышите трагическую и поучительную историю о мальчике Бобби.", + "Который любил... Да, любил деньги.", + "— Рассказывай!", + "С рождения Бобби пай-мальчиком был", + "— Молодец…", + "Имел Бобби хобби – он деньги любил", + "— Хороший мальчик…", + "Любил и копил.", + "— Что было дальше?", + "Все дети как дети — живут без забот", + "— Счастливое детство…", + "А Боб на диете — не ест и не пьёт", + "— Бедненький мальчик…", + "В копилку кладёт", + "Деньги-деньги, дребеденьги", + "Позабыв покой и лень.", + "Делай деньги, делай деньги", + "А остальное всё дребедень", + "А остальное всё дребебедень.", + "— Дальше что было?", + "Здесь пенни, там шиллинг а где-нибудь фунт", + "— Большие деньги…", + "Стал Бобби мошенник, мошенник и плут", + "— Почему мошенник и плут?", + "Скопил целый пуд.", + "— А-а-а, молодец…", + "Но в том-то и дело, что он не один", + "— Почему?..", + "Кто больше всех деньги на свете любил", + "— Человек наш…", + "Он это забыл ;)", + "Деньги-деньги, дребеденьги", + "Позабыв покой и лень.", + "Делай деньги, делай деньги", + "А остальное всё дребебедень.", + "Дамы и господа!", + "Подайте кто-нибудь.", + "Кто сколько может..." + ], + [ + "Лорды, сэры, пэры", + "Знайте чувство меры", + "Избегайте пьянства", + "Вы как западни.", + "Ждёт нас путь не близкий", + "И чем крепче виски", + "Тем короче, сэры", + "Будут ваши дни.", + "Каждый лишний градус будет вам не в радость.", + "Вашему здоровью вреден каждый тост!", + "— Простите, не цветёт, как роза, печень от цирроза?", + "— Да! И от склероза лишь тупеет мозг!", + "Пятнадцать человек на сундук мертвеца", + "Йо-хо-хо, и бутылка рома.", + "Пей, и дьявол тебя доведёт до конца.", + "Йо-хо-хо, и бутылка рома.", + "Пей, и дьявол тебя доведёт до конца.", + "Йо-хо-хо, и бутылка рома.", + "От похмелья, сэры, будете вы серы", + "И не мил вам будет утром белый свет.", + "Будет враг доволен - ты уже не воин", + "Если пляшут в пальцах шпага и мушкет.", + "Утром встал с постели, лучше взять гантели", + "И любая ноша будет вам как пух.", + "Нету лучше в мире полновесной гири.", + "И в здоровом теле - здоровее дух!", + "Пятнадцать человек на сундук мертвеца", + "Йо-хо-хо, и бутылка рома.", + "Пей, и дьявол тебя доведёт до конца.", + "Йо-хо-хо, и бутылка рома.", + "Пей, и дьявол тебя доведёт до конца.", + "Йо-хо-хо, и бутылка рома." + ], + [ + "Если хотите, поспорьте", + "Но я скажу, наконец", + "Кто себя выразил в спорте", + "Тот молодец, тот молодец.", + "Бегать, скакать, кувыркаться", + "Каждый обязан уметь.", + "Нужно лишь только собраться", + "И захотеть, и захотеть.", + "Соблюдает дня режим", + "Джим", + "Знает спорт необходим", + "Джим.", + "Даже опытный пират", + "Будет в схватке с ним не рад", + "Потому что пьет пират", + "Потому что пьет пират", + "Потому что пьет пират", + "Джин!", + "Тренинг хорош постоянством.", + "Ну и, конечно, пират", + "систематическим пьянством", + "Спорту не рад, спорту не рад.", + "В спорте основа успеха", + "Стойкость и трезвый расчет.", + "Тысячекратной эхо нам подпоет.", + "Соблюдает дня режим", + "Джим", + "Знает - спорт необходим", + "Джим.", + "Даже опытный пират", + "Будет в схватке с ним не рад", + "Потому что пьет пират", + "Потому что пьет пират", + "Потому что пьет пират", + "Джин!" + ] + ]; +} \ No newline at end of file diff --git a/Jackal.Core/GameModeType.cs b/Jackal.Core/GameModeType.cs new file mode 100644 index 00000000..3d4fa2b3 --- /dev/null +++ b/Jackal.Core/GameModeType.cs @@ -0,0 +1,14 @@ +namespace Jackal.Core; + +public enum GameModeType +{ + /// + /// Каждый игрок сам за себя + /// + FreeForAll = 0, + + /// + /// Командный режим 2x2 + /// + TwoPlayersInTeam = 1 +} \ No newline at end of file diff --git a/Jackal.Core/GameState.cs b/Jackal.Core/GameState.cs new file mode 100644 index 00000000..66411d65 --- /dev/null +++ b/Jackal.Core/GameState.cs @@ -0,0 +1,29 @@ +using Jackal.Core.Domain; + +namespace Jackal.Core; + +/// +/// Состояние игры +/// +public class GameState +{ + /// + /// Данные игры: карта + информация о командах + /// + public Board Board; + + /// + /// Доступные ходы + /// + public Move[] AvailableMoves; + + /// + /// ИД команды пиратов чей ход + /// + public int TeamId; + + /// + /// ИД пользователя чей ход + /// + public long UserId; +} \ No newline at end of file diff --git a/Jackal.Core/IClonable.cs b/Jackal.Core/IClonable.cs new file mode 100644 index 00000000..63804f77 --- /dev/null +++ b/Jackal.Core/IClonable.cs @@ -0,0 +1,6 @@ +namespace Jackal.Core; + +public interface IClonable where T : class +{ + T Clone(); +} \ No newline at end of file diff --git a/Jackal.Core/ICompletable.cs b/Jackal.Core/ICompletable.cs new file mode 100644 index 00000000..f6ef385b --- /dev/null +++ b/Jackal.Core/ICompletable.cs @@ -0,0 +1,7 @@ +namespace Jackal.Core +{ + public interface ICompletable + { + bool IsCompleted { get; } + } +} diff --git a/Jackal.Core/Jackal.Core.csproj b/Jackal.Core/Jackal.Core.csproj new file mode 100644 index 00000000..bdc3baef --- /dev/null +++ b/Jackal.Core/Jackal.Core.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + latest + enable + + + + + + + diff --git a/Jackal.Core/JsonHelper.cs b/Jackal.Core/JsonHelper.cs new file mode 100644 index 00000000..fbf5e4e6 --- /dev/null +++ b/Jackal.Core/JsonHelper.cs @@ -0,0 +1,18 @@ +using Newtonsoft.Json; + +namespace Jackal.Core; + +public static class JsonHelper +{ + private static readonly JsonSerializerSettings TypeNameSerializer = new() { TypeNameHandling = TypeNameHandling.Objects }; + + public static string SerializeWithType(T obj, Formatting formatting = Formatting.None) + { + return JsonConvert.SerializeObject(obj, formatting, TypeNameSerializer); + } + + public static T? DeserializeWithType(string str) + { + return JsonConvert.DeserializeObject(str, TypeNameSerializer); + } +} \ No newline at end of file diff --git a/Jackal.Core/MapGenerator/IMapGenerator.cs b/Jackal.Core/MapGenerator/IMapGenerator.cs new file mode 100644 index 00000000..c5626b80 --- /dev/null +++ b/Jackal.Core/MapGenerator/IMapGenerator.cs @@ -0,0 +1,34 @@ +using Jackal.Core.Domain; + +namespace Jackal.Core.MapGenerator; + +public interface IMapGenerator +{ + /// + /// ИД карты, по нему генерируется расположение клеток + /// + public int MapId { get; } + + /// + /// Название игрового набора клеток + /// + public string TilesPackName { get; } + + /// + /// Всего золота на карте + /// + int TotalCoins { get; } + + /// + /// Открыть закрытую клетку, + /// тип клетки строго привязан к позиции на карте, + /// сделано для возможности воспроизведения карты + /// + Tile GetNext(Position position); + + /// + /// Поменять клетки в генераторе местами, + /// требуется для Разлома, чтобы не множить одинаковые клетки + /// + void Swap(Position from, Position to); +} \ No newline at end of file diff --git a/Jackal.Core/MapGenerator/OneTileMapGenerator.cs b/Jackal.Core/MapGenerator/OneTileMapGenerator.cs new file mode 100644 index 00000000..094d13e6 --- /dev/null +++ b/Jackal.Core/MapGenerator/OneTileMapGenerator.cs @@ -0,0 +1,22 @@ +using Jackal.Core.Domain; + +namespace Jackal.Core.MapGenerator; + +/// +/// Все клетки oneTileParams +/// +public class OneTileMapGenerator(TileParams oneTileParams) : IMapGenerator +{ + private readonly ThreeTileMapGenerator _mapGenerator = + new(oneTileParams, oneTileParams, oneTileParams); + + public int MapId => _mapGenerator.MapId; + + public string TilesPackName => _mapGenerator.TilesPackName; + + public int TotalCoins => _mapGenerator.TotalCoins; + + public Tile GetNext(Position position) => _mapGenerator.GetNext(position); + + public void Swap(Position from, Position to) => _mapGenerator.Swap(from, to); +} \ No newline at end of file diff --git a/Jackal.Core/MapGenerator/RandomMapGenerator.cs b/Jackal.Core/MapGenerator/RandomMapGenerator.cs new file mode 100644 index 00000000..f1793ead --- /dev/null +++ b/Jackal.Core/MapGenerator/RandomMapGenerator.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Jackal.Core.Domain; +using Jackal.Core.MapGenerator.TilesPack; + +namespace Jackal.Core.MapGenerator; + +public class RandomMapGenerator : IMapGenerator +{ + private readonly Random _rand; + private readonly Dictionary _tiles; + + public int MapId { get; } + + public string TilesPackName { get; } + + public int TotalCoins { get; private set; } + + public RandomMapGenerator(int mapId, int mapSize, string? tilesPackName = null) + { + MapId = mapId; + _rand = new Random(mapId + 5000000); + + TilesPackName = TilesPackFactory.CheckName(tilesPackName); + var pack = TilesPackFactory.Create(tilesPackName); + + var selectedTiles = PullOut(_rand, mapSize, pack); + var shuffledTiles = Shuffle(selectedTiles); + + var positions = GetAllEarth(mapSize).ToList(); + + if (shuffledTiles.Count != positions.Count) + throw new Exception("Wrong tiles pack count"); + + _tiles = InitTiles(shuffledTiles, positions); + } + + private List PullOut(Random rand, int mapSize, ITilesPack pack) + { + var landSize = mapSize - 2; + var totalTiles = landSize * landSize - 4; + var list = new List(totalTiles); + + // выбираем обязательный сундук с 1 монетой + bool random = false; + int selectedIndex = 0; + + for (var i = 0; i < totalTiles; i++) + { + var index = random + ? rand.Next(0, pack.AllTiles.Length - i) + : selectedIndex; + + list.Add(pack.AllTiles[index]); + + switch (pack.AllTiles[index].Type) + { + case TileType.Cannibal: + // выбираем воскрешающий форт к людоеду + random = false; + selectedIndex = index > 0 ? index - 1 : 0; + break; + case TileType.RespawnFort: + // выбираем людоеда к воскрешающему форту + random = false; + selectedIndex = index + 1; + break; + default: + random = true; + break; + } + + var tileParam = pack.AllTiles[index]; + TotalCoins += tileParam.Type == TileType.Coin ? tileParam.Code : 0; + TotalCoins += tileParam.Type == TileType.BigCoin ? tileParam.Code * Constants.BigCoinValue : 0; + + // сдвигаем оставшиеся клетки в наборе, последнюю ставим на место выбранной + pack.AllTiles[index] = pack.AllTiles[pack.AllTiles.Length - 1 - i]; + } + + // если сгенерилась одна дыра на карту - то заменяем её на пустую клетку + var holeTilesCount = list.Count(x => x.Type == TileType.Hole); + if (holeTilesCount == 1) + { + var hole = list.First(x => x.Type == TileType.Hole); + list.Remove(hole); + list.Add(TileParams.Empty()); + } + + return list; + } + + private List Shuffle(IEnumerable defs) + { + return defs + .Select(x => new {Def = x, Number = _rand.NextDouble()}) + .OrderBy(x => x.Number) + .Select(x => x.Def) + .ToList(); + } + + private static IEnumerable GetAllEarth(int mapSize) + { + for (int x = 1; x <= mapSize - 2; x++) + { + for (int y = 1; y <= mapSize - 2; y++) + { + Position val = new Position(x, y); + if (Utils.InCorners(val, 1, mapSize - 2) == false) + { + yield return val; + } + } + } + } + + private Dictionary InitTiles(List pack, List positions) + { + var tiles = new Dictionary(); + + foreach (var info in pack.Zip(positions, (def, position) => new {Def = def, Position = position})) + { + var tempDef = info.Def.Clone(); + if (tempDef.Type != TileType.Spinning && + tempDef.Type != TileType.Caramba && + tempDef.Type != TileType.Coin && + tempDef.Type != TileType.BigCoin && + tempDef.Type != TileType.Ice && + tempDef.Type != TileType.Hole) + { + // клетки не указанные в условии - вращаем при отображении на карте + tempDef.Direction = (DirectionType)_rand.Next(4); + } + + if (tempDef.Type is TileType.Arrow) + { + for (var j = 1; j <= (int)tempDef.Direction; j++) + { + tempDef.Code = ArrowsCodesHelper.DoRotate(tempDef.Code); + } + } + + tempDef.Position = info.Position; + + var tile = new Tile(tempDef); + tile.Levels[0].Coins = tile.CoinsCount(); + tile.Levels[0].BigCoins = tile.BigCoinsCount(); + + tiles.Add(info.Position, tile); + } + + return tiles; + } + + public Tile GetNext(Position position) => _tiles[position]; + + public void Swap(Position from, Position to) + { + // меняем сгенеренные клетки местами + var fromTile = _tiles[from]; + var toTile = _tiles[to]; + + _tiles[from] = new Tile(from, toTile); + _tiles[from].Levels[0].Coins = toTile.Coins; + _tiles[from].Levels[0].BigCoins = toTile.BigCoins; + + _tiles[to] = new Tile(to, fromTile); + _tiles[to].Levels[0].Coins = fromTile.Coins; + _tiles[to].Levels[0].BigCoins = fromTile.BigCoins; + } +} \ No newline at end of file diff --git a/Jackal.Core/MapGenerator/ThreeTileMapGenerator.cs b/Jackal.Core/MapGenerator/ThreeTileMapGenerator.cs new file mode 100644 index 00000000..b3eece06 --- /dev/null +++ b/Jackal.Core/MapGenerator/ThreeTileMapGenerator.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using Jackal.Core.Domain; + +namespace Jackal.Core.MapGenerator; + +/// +/// Нижняя береговая линия firstTile, +/// следующая линия secondTile, +/// остальные все клетки thirdTile +/// +public class ThreeTileMapGenerator( + TileParams firstTileParams, + TileParams secondTileParams, + TileParams thirdTileParams, + int totalCoins = 1 +) : IMapGenerator +{ + private readonly Dictionary _tiles = new(); + + public int MapId => 777; + + public string TilesPackName => "unit-test"; + + public int TotalCoins => totalCoins; + + public Tile GetNext(Position position) + { + if (!_tiles.ContainsKey(position)) + { + var tileParams = position.Y switch + { + 1 => firstTileParams, + 2 => secondTileParams, + _ => thirdTileParams + }; + + tileParams.Position = position; + + var tile = new Tile(tileParams); + tile.Levels[0].Coins = tile.CoinsCount(); + tile.Levels[0].BigCoins = tile.BigCoinsCount(); + + _tiles[position] = tile; + } + + return _tiles[position]; + } + + public void Swap(Position from, Position to) + { + // инициализируем клетки, если их нет + GetNext(from); + GetNext(to); + + // меняем сгенеренные клетки местами + var fromTile = _tiles[from]; + var toTile = _tiles[to]; + + _tiles[from] = new Tile(from, toTile); + _tiles[from].Levels[0].Coins = toTile.Coins; + _tiles[from].Levels[0].BigCoins = toTile.BigCoins; + + _tiles[to] = new Tile(to, fromTile); + _tiles[to].Levels[0].Coins = fromTile.Coins; + _tiles[to].Levels[0].BigCoins = fromTile.BigCoins; + } +} \ No newline at end of file diff --git a/Jackal.Core/MapGenerator/TilesPack/AllGoldTilesPack.cs b/Jackal.Core/MapGenerator/TilesPack/AllGoldTilesPack.cs new file mode 100644 index 00000000..0b3ed45a --- /dev/null +++ b/Jackal.Core/MapGenerator/TilesPack/AllGoldTilesPack.cs @@ -0,0 +1,25 @@ +using Jackal.Core.Domain; + +namespace Jackal.Core.MapGenerator.TilesPack; + +/// +/// Набор только из клеток с монетами +/// +public class AllGoldTilesPack : ITilesPack +{ + /// + /// 117 клеток + /// + public TileParams[] AllTiles { get; } + + public AllGoldTilesPack() + { + AllTiles = new TileParams[117]; + for (var index = 0; index < AllTiles.Length; index++) + { + AllTiles[index] = index % 2 == 0 + ? TileParams.BigCoin() + : TileParams.Coin(); + } + } +} \ No newline at end of file diff --git a/Jackal.Core/MapGenerator/TilesPack/ClassicTilesPack.cs b/Jackal.Core/MapGenerator/TilesPack/ClassicTilesPack.cs new file mode 100644 index 00000000..9c638d64 --- /dev/null +++ b/Jackal.Core/MapGenerator/TilesPack/ClassicTilesPack.cs @@ -0,0 +1,135 @@ +using Jackal.Core.Domain; + +namespace Jackal.Core.MapGenerator.TilesPack; + +/// +/// Классический игровой набор +/// +public class ClassicTilesPack : ITilesPack +{ + /// + /// 117 клеток + /// + public TileParams[] AllTiles { get; } = + [ + // 77 значимых клеток + TileParams.Coin(), // 1 монета - первый сундук берем всегда + TileParams.Coin(), // 2 + TileParams.Coin(), // 3 + TileParams.Coin(), // 4 + TileParams.Coin(), // 5 + TileParams.Coin(2), // 7 + TileParams.Coin(2), // 9 + TileParams.Coin(2), // 11 + TileParams.Coin(2), // 13 + TileParams.Coin(2), // 15 + TileParams.Coin(3), // 18 + TileParams.Coin(3), // 21 + TileParams.Coin(3), // 24 + TileParams.Coin(4), // 28 + TileParams.Coin(4), // 32 + TileParams.Coin(5), // 37 + TileParams.Fort(), + TileParams.Fort(), + TileParams.RespawnFort(), // порядок RespawnFort и Cannibal важен для баланса + TileParams.Cannibal(), // берем воскрешающий форт вместе с людоедом + TileParams.RumBarrel(), + TileParams.RumBarrel(), + TileParams.RumBarrel(), + TileParams.RumBarrel(), + TileParams.Horse(), + TileParams.Horse(), + TileParams.Balloon(), + TileParams.Balloon(), + TileParams.Airplane(), + TileParams.Crocodile(), + TileParams.Crocodile(), + TileParams.Crocodile(), + TileParams.Crocodile(), + TileParams.Ice(), + TileParams.Ice(), + TileParams.Ice(), + TileParams.Ice(), + TileParams.Ice(), + TileParams.Ice(), + TileParams.Cannon(), + TileParams.Cannon(), + TileParams.OneArrowUp(), + TileParams.OneArrowUp(), + TileParams.OneArrowUp(), + TileParams.OneArrowDiagonal(), + TileParams.OneArrowDiagonal(), + TileParams.OneArrowDiagonal(), + TileParams.TwoArrowsDiagonal(), + TileParams.TwoArrowsDiagonal(), + TileParams.TwoArrowsDiagonal(), + TileParams.TwoArrowsLeftRight(), + TileParams.TwoArrowsLeftRight(), + TileParams.TwoArrowsLeftRight(), + TileParams.ThreeArrows(), + TileParams.ThreeArrows(), + TileParams.ThreeArrows(), + TileParams.FourArrowsPerpendicular(), + TileParams.FourArrowsPerpendicular(), + TileParams.FourArrowsPerpendicular(), + TileParams.FourArrowsDiagonal(), + TileParams.FourArrowsDiagonal(), + TileParams.FourArrowsDiagonal(), + TileParams.Trap(), + TileParams.Trap(), + TileParams.Trap(), + TileParams.SpinningForest(), + TileParams.SpinningForest(), + TileParams.SpinningForest(), + TileParams.SpinningForest(), + TileParams.SpinningForest(), + TileParams.SpinningDesert(), + TileParams.SpinningDesert(), + TileParams.SpinningDesert(), + TileParams.SpinningDesert(), + TileParams.SpinningSwamp(), + TileParams.SpinningSwamp(), + TileParams.SpinningMount(), + // 40 пустых клеток + TileParams.Empty(), + TileParams.Empty(), + TileParams.Empty(), + TileParams.Empty(), + TileParams.Empty(), + TileParams.Empty(), + TileParams.Empty(), + TileParams.Empty(), + TileParams.Empty(), + TileParams.Empty(), + TileParams.Empty(2), + TileParams.Empty(2), + TileParams.Empty(2), + TileParams.Empty(2), + TileParams.Empty(2), + TileParams.Empty(2), + TileParams.Empty(2), + TileParams.Empty(2), + TileParams.Empty(2), + TileParams.Empty(2), + TileParams.Empty(3), + TileParams.Empty(3), + TileParams.Empty(3), + TileParams.Empty(3), + TileParams.Empty(3), + TileParams.Empty(3), + TileParams.Empty(3), + TileParams.Empty(3), + TileParams.Empty(3), + TileParams.Empty(3), + TileParams.Empty(4), + TileParams.Empty(4), + TileParams.Empty(4), + TileParams.Empty(4), + TileParams.Empty(4), + TileParams.Empty(4), + TileParams.Empty(4), + TileParams.Empty(4), + TileParams.Empty(4), + TileParams.Empty(4) + ]; +} \ No newline at end of file diff --git a/Jackal.Core/MapGenerator/TilesPack/DifficultTilesPack.cs b/Jackal.Core/MapGenerator/TilesPack/DifficultTilesPack.cs new file mode 100644 index 00000000..b5a45f7e --- /dev/null +++ b/Jackal.Core/MapGenerator/TilesPack/DifficultTilesPack.cs @@ -0,0 +1,137 @@ +using Jackal.Core.Domain; + +namespace Jackal.Core.MapGenerator.TilesPack; + +/// +/// Безумный игровой набор от madddmax-а +/// +public class DifficultTilesPack : ITilesPack +{ + /// + /// 120 клеток + /// + public TileParams[] AllTiles { get; } = + [ + // 120 значимых клеток + TileParams.Coin(), // 1 монета - первый сундук берем всегда + TileParams.Coin(), // 2 + TileParams.Coin(), // 3 + TileParams.Coin(), // 4 + TileParams.Coin(), // 5 + TileParams.Coin(), // 6 + TileParams.Coin(), // 7 + TileParams.Coin(), // 8 + TileParams.Coin(2), // 10 + TileParams.Coin(2), // 12 + TileParams.Coin(2), // 14 + TileParams.Coin(2), // 16 + TileParams.Coin(3), // 19 + TileParams.Coin(3), // 21 + TileParams.Cannibal(), // берем сундук вместе с людоедом + TileParams.Cannibal(), + TileParams.Cannibal(), + TileParams.Cannibal(), + TileParams.Cannibal(), + TileParams.RumBarrel(), + TileParams.RumBarrel(), + TileParams.RumBarrel(), + TileParams.RumBarrel(), + TileParams.Horse(), + TileParams.Horse(), + TileParams.Horse(), + TileParams.Horse(), + TileParams.Balloon(), + TileParams.Balloon(), + TileParams.Balloon(), + TileParams.Balloon(), + TileParams.Balloon(), + TileParams.Balloon(), + TileParams.Balloon(), + TileParams.Balloon(), + TileParams.Balloon(), + TileParams.Balloon(), + TileParams.Balloon(), + TileParams.Balloon(), + TileParams.Balloon(), + TileParams.Airplane(), + TileParams.Airplane(), + TileParams.Airplane(), + TileParams.Airplane(), + TileParams.Airplane(), + TileParams.Airplane(), + TileParams.Airplane(), + TileParams.Airplane(), + TileParams.Airplane(), + TileParams.Airplane(), + TileParams.Airplane(), + TileParams.Airplane(), + TileParams.Airplane(), + TileParams.Crocodile(), + TileParams.Crocodile(), + TileParams.Crocodile(), + TileParams.Crocodile(), + TileParams.Ice(), + TileParams.Ice(), + TileParams.Ice(), + TileParams.Ice(), + TileParams.Ice(), + TileParams.Ice(), + TileParams.Cannon(), + TileParams.Cannon(), + TileParams.Cannon(), + TileParams.OneArrowUp(), + TileParams.OneArrowUp(), + TileParams.OneArrowUp(), + TileParams.OneArrowDiagonal(), + TileParams.OneArrowDiagonal(), + TileParams.OneArrowDiagonal(), + TileParams.TwoArrowsDiagonal(), + TileParams.TwoArrowsDiagonal(), + TileParams.TwoArrowsDiagonal(), + TileParams.TwoArrowsLeftRight(), + TileParams.TwoArrowsLeftRight(), + TileParams.TwoArrowsLeftRight(), + TileParams.ThreeArrows(), + TileParams.ThreeArrows(), + TileParams.ThreeArrows(), + TileParams.FourArrowsPerpendicular(), + TileParams.FourArrowsPerpendicular(), + TileParams.FourArrowsPerpendicular(), + TileParams.FourArrowsDiagonal(), + TileParams.FourArrowsDiagonal(), + TileParams.FourArrowsDiagonal(), + TileParams.BenGunn(), + TileParams.BenGunn(), + TileParams.BenGunn(), + TileParams.BenGunn(), + TileParams.BenGunn(), + TileParams.BenGunn(), + TileParams.BenGunn(), + TileParams.BenGunn(), + TileParams.BenGunn(), + TileParams.BenGunn(), + TileParams.Hole(), + TileParams.Hole(), + TileParams.Hole(), + TileParams.Hole(), + TileParams.Hole(), + TileParams.Hole(), + TileParams.Hole(), + TileParams.Hole(), + TileParams.Hole(), + TileParams.Hole(), + TileParams.Hole(), + TileParams.Hole(), + TileParams.Hole(), + TileParams.Quake(), + TileParams.Quake(), + TileParams.Quake(), + TileParams.Quake(), + TileParams.Quake(), + TileParams.Quake(), + TileParams.Quake(), + TileParams.Quake(), + TileParams.Quake(), + TileParams.Quake() + ]; +} \ No newline at end of file diff --git a/Jackal.Core/MapGenerator/TilesPack/ExtendedTilesPack.cs b/Jackal.Core/MapGenerator/TilesPack/ExtendedTilesPack.cs new file mode 100644 index 00000000..fe0cae41 --- /dev/null +++ b/Jackal.Core/MapGenerator/TilesPack/ExtendedTilesPack.cs @@ -0,0 +1,138 @@ +using Jackal.Core.Domain; + +namespace Jackal.Core.MapGenerator.TilesPack; + +/// +/// Расширенный игровой набор +/// +public class ExtendedTilesPack : ITilesPack +{ + /// + /// 120 клеток + /// + public TileParams[] AllTiles { get; } = + [ + // 108 значимых клеток + TileParams.Coin(), // 1 монета - первый сундук берем всегда + TileParams.Coin(), // 2 + TileParams.Coin(), // 3 + TileParams.Coin(), // 4 + TileParams.Coin(), // 5 + TileParams.Coin(2), // 7 + TileParams.Coin(2), // 9 + TileParams.Coin(2), // 11 + TileParams.Coin(2), // 13 + TileParams.Coin(3), // 16 + TileParams.Coin(3), // 19 + TileParams.Coin(3), // 21 + TileParams.Coin(4), // 25 + TileParams.Coin(4), // 29 + TileParams.Coin(5), // 34 + TileParams.BigCoin(), // 37 + TileParams.BigCoin(), // 40 + TileParams.Fort(), + TileParams.Fort(), + TileParams.RespawnFort(), // порядок RespawnFort и Cannibal важен для баланса + TileParams.Cannibal(), // берем воскрешающий форт вместе с людоедом + TileParams.RespawnFort(), + TileParams.Cannibal(), + TileParams.RumBarrel(), + TileParams.RumBarrel(), + TileParams.RumBarrel(), + TileParams.RumBarrel(), + TileParams.RumBottle(), + TileParams.RumBottle(), + TileParams.RumBottle(), + TileParams.RumBottle(2), + TileParams.RumBottle(2), + TileParams.RumBottle(3), + TileParams.Horse(), + TileParams.Horse(), + TileParams.Horse(), + TileParams.Balloon(), + TileParams.Balloon(), + TileParams.Airplane(), + TileParams.Airplane(), + TileParams.Crocodile(), + TileParams.Crocodile(), + TileParams.Crocodile(), + TileParams.Crocodile(), + TileParams.Ice(), + TileParams.Ice(), + TileParams.Ice(), + TileParams.Ice(), + TileParams.Ice(), + TileParams.Ice(), + TileParams.Cannon(), + TileParams.Cannon(), + TileParams.OneArrowUp(), + TileParams.OneArrowUp(), + TileParams.OneArrowUp(), + TileParams.OneArrowDiagonal(), + TileParams.OneArrowDiagonal(), + TileParams.OneArrowDiagonal(), + TileParams.TwoArrowsDiagonal(), + TileParams.TwoArrowsDiagonal(), + TileParams.TwoArrowsDiagonal(), + TileParams.TwoArrowsLeftRight(), + TileParams.TwoArrowsLeftRight(), + TileParams.TwoArrowsLeftRight(), + TileParams.ThreeArrows(), + TileParams.ThreeArrows(), + TileParams.ThreeArrows(), + TileParams.FourArrowsPerpendicular(), + TileParams.FourArrowsPerpendicular(), + TileParams.FourArrowsPerpendicular(), + TileParams.FourArrowsDiagonal(), + TileParams.FourArrowsDiagonal(), + TileParams.FourArrowsDiagonal(), + TileParams.Trap(), + TileParams.Trap(), + TileParams.Trap(), + TileParams.Lighthouse(), + TileParams.Lighthouse(), + TileParams.BenGunn(), + TileParams.BenGunn(), + TileParams.SpinningForest(), + TileParams.SpinningForest(), + TileParams.SpinningForest(), + TileParams.SpinningForest(), + TileParams.SpinningForest(), + TileParams.SpinningDesert(), + TileParams.SpinningDesert(), + TileParams.SpinningDesert(), + TileParams.SpinningDesert(), + TileParams.SpinningSwamp(), + TileParams.SpinningSwamp(), + TileParams.SpinningSwamp(), + TileParams.SpinningMount(), + TileParams.SpinningMount(), + TileParams.Caramba(), + TileParams.Jungle(), + TileParams.Jungle(), + TileParams.Hole(), + TileParams.Hole(), + TileParams.Hole(), + TileParams.Hole(), + TileParams.Hole(), + TileParams.Quake(), + TileParams.Quake(), + TileParams.Cannabis(), + TileParams.Cannabis(), + TileParams.Cannabis(), + TileParams.Cannabis(), + // 12 пустых клеток + TileParams.Empty(), + TileParams.Empty(), + TileParams.Empty(), + TileParams.Empty(2), + TileParams.Empty(2), + TileParams.Empty(2), + TileParams.Empty(3), + TileParams.Empty(3), + TileParams.Empty(3), + TileParams.Empty(4), + TileParams.Empty(4), + TileParams.Empty(4) + ]; +} \ No newline at end of file diff --git a/Jackal.Core/MapGenerator/TilesPack/ITilesPack.cs b/Jackal.Core/MapGenerator/TilesPack/ITilesPack.cs new file mode 100644 index 00000000..cd58c015 --- /dev/null +++ b/Jackal.Core/MapGenerator/TilesPack/ITilesPack.cs @@ -0,0 +1,15 @@ +using Jackal.Core.Domain; + +namespace Jackal.Core.MapGenerator.TilesPack; + +/// +/// Интерфейс игрового набора. Минимум 117 клеток, +/// самая большая карта 13x13 = 117 клеток +/// +public interface ITilesPack +{ + /// + /// Состав клеток в наборе + /// + TileParams[] AllTiles { get; } +} \ No newline at end of file diff --git a/Jackal.Core/MapGenerator/TilesPack/TilesPackFactory.cs b/Jackal.Core/MapGenerator/TilesPack/TilesPackFactory.cs new file mode 100644 index 00000000..99e531f6 --- /dev/null +++ b/Jackal.Core/MapGenerator/TilesPack/TilesPackFactory.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; + +namespace Jackal.Core.MapGenerator.TilesPack; + +public static class TilesPackFactory +{ + public const string Extended = "extended"; + public const string Classic = "classic"; + public const string Difficult = "difficult"; + public const string AllGold = "all-gold"; + + public static string CheckName(string? name) => + name switch + { + Classic => Classic, + Difficult => Difficult, + AllGold => AllGold, + _ => Extended + }; + + public static ITilesPack Create(string? name) => + name switch + { + Classic => new ClassicTilesPack(), + Difficult => new DifficultTilesPack(), + AllGold => new AllGoldTilesPack(), + _ => new ExtendedTilesPack() + }; + + public static List GetAll() => [Extended, Classic, Difficult]; +} \ No newline at end of file diff --git a/Jackal.Core/MapGenerator/TwoTileMapGenerator.cs b/Jackal.Core/MapGenerator/TwoTileMapGenerator.cs new file mode 100644 index 00000000..abcc5f6d --- /dev/null +++ b/Jackal.Core/MapGenerator/TwoTileMapGenerator.cs @@ -0,0 +1,27 @@ +using Jackal.Core.Domain; + +namespace Jackal.Core.MapGenerator; + +/// +/// Нижняя береговая линия firstTile, +/// остальные все клетки secondTile +/// +public class TwoTileMapGenerator( + TileParams firstTileParams, + TileParams secondTileParams, + int totalCoins = 1 +) : IMapGenerator +{ + private readonly ThreeTileMapGenerator _mapGenerator = + new(firstTileParams, secondTileParams, secondTileParams, totalCoins); + + public int MapId => _mapGenerator.MapId; + + public string TilesPackName => _mapGenerator.TilesPackName; + + public int TotalCoins => _mapGenerator.TotalCoins; + + public Tile GetNext(Position position) => _mapGenerator.GetNext(position); + + public void Swap(Position from, Position to) => _mapGenerator.Swap(from, to); +} \ No newline at end of file diff --git a/Jackal.Core/Players/EasyPlayer.cs b/Jackal.Core/Players/EasyPlayer.cs new file mode 100644 index 00000000..79fc4fcd --- /dev/null +++ b/Jackal.Core/Players/EasyPlayer.cs @@ -0,0 +1,315 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Jackal.Core.Domain; + +namespace Jackal.Core.Players; + +/// +/// Игрок простой бот - выбирает ход алгоритмом бей-неси, +/// рассчет дистанции упрощен через манхэттенское расстояние +/// +public class EasyPlayer : IPlayer +{ + private Random _rnd = new(); + + public void OnNewGame() + { + _rnd = new Random(1); + } + + public (int moveNum, Guid? pirateId) OnMove(GameState gameState) + { + int teamId = gameState.TeamId; + Board board = gameState.Board; + var shipPosition = board.Teams[teamId].ShipPosition; + + var enemyShipPositions = board.Teams + .Select(t => t.ShipPosition) + .Where(p => p != shipPosition) + .ToList(); + + var unknownPositions = board + .AllTiles(x => x.Type == TileType.Unknown) + .Select(x => x.Position) + .ToList(); + + var waterPositions = board + .AllTiles(x => x.Type == TileType.Water) + .Select(x => x.Position) + .Except(new[] { shipPosition }) + .ToList(); + + var goldPositions = board + .AllTiles(x => x.Type != TileType.Water && (x.Coins > 0 || x.BigCoins > 0)) + .Select(x => x.Position) + .ToList(); + + var cannibalPositions = board + .AllTiles(x => x.Type == TileType.Cannibal) + .Select(x => x.Position) + .ToList(); + + var trapPositions = board + .AllTiles(x => x.Type == TileType.Trap) + .Select(x => x.Position) + .ToList(); + + var respawnPositions = board + .AllTiles(x => x.Type == TileType.RespawnFort) + .Select(x => x.Position) + .ToList(); + + // воскрешаемся если можем + List goodMoves = gameState.AvailableMoves.Where(m => m.Type == MoveType.WithRespawn).ToList(); + if (CheckGoodMove(goodMoves, gameState.AvailableMoves, out var goodMoveNum)) + return (goodMoveNum, null); + + // освобождаем пирата из ловушек + foreach (var trapPosition in trapPositions) + { + if (gameState.AvailableMoves.Any(m => m.From.Position == trapPosition)) + { + continue; + } + + var piratesPosition = board.Teams[teamId].Pirates.Select(p => p.Position.Position); + goodMoves = gameState.AvailableMoves + .Where(m => trapPosition == m.To.Position && piratesPosition.Contains(trapPosition)) + .ToList(); + + if (CheckGoodMove(goodMoves, gameState.AvailableMoves, out goodMoveNum)) + return (goodMoveNum, null); + } + + // заносим золото на корабль + goodMoves = gameState.AvailableMoves.Where(move => move.WithCoin && TargetIsShip(board, teamId, move)).ToList(); + if (CheckGoodMove(goodMoves, gameState.AvailableMoves, out goodMoveNum)) + return (goodMoveNum, null); + + // не ходим по чужим кораблям, людоедам и держим бабу + Move[] safeAvailableMoves = gameState.AvailableMoves + .Where(x => x.To != x.From) + .Where(x => !enemyShipPositions.Contains(x.To.Position)) + .Where(x => !cannibalPositions.Contains(x.To.Position)) + .Where(x => !respawnPositions.Contains(x.From.Position)) + .ToArray(); + + bool hasMoveWithCoins = safeAvailableMoves.Any(m => m.WithCoin || m.WithBigCoin); + if (hasMoveWithCoins) + { + // перемещаем золото ближе к кораблю + var escapePositions = board.AllTiles(x => x.Type == TileType.Balloon) + .Select(x => x.Position) + .ToList(); + + escapePositions.Add(shipPosition); + + List> list = []; + foreach (Move move in safeAvailableMoves + .Where(x => x.WithCoin || x.WithBigCoin) + .Where(x => !waterPositions.Contains(x.To.Position)) + .Where(x => IsEnemyNear(x.To.Position, board, teamId) == false)) + { + // идем к самому ближнему выходу + var minDistance = escapePositions + .Select(p => Distance(p, move.To.Position) + move.To.Level) + .Min(); + + var escapePosition = escapePositions + .First(p => Distance(p, move.To.Position) + move.To.Level == minDistance); + + int currentDistance = Distance(escapePosition, move.From.Position) + move.From.Level; + minDistance = Distance(escapePosition, move.To.Position) + move.To.Level; + + if (currentDistance <= minDistance) + continue; + + list.Add(new Tuple(minDistance, move)); + } + + if (list.Count > 0) + { + int minDistance = list.Min(x => x.Item1); + goodMoves = list.Where(x => x.Item1 == minDistance) + .Select(x => x.Item2) + .ToList(); + } + } + + if (goodMoves.Count == 0) + { + // уничтожаем врага, если он рядом + goodMoves = gameState.AvailableMoves.Where(move => IsEnemyPosition(move.To.Position, board, teamId)).ToList(); + if (CheckGoodMove(goodMoves, gameState.AvailableMoves, out goodMoveNum)) + return (goodMoveNum, null); + } + + if (goodMoves.Count == 0 && goldPositions.Count > 0 && !hasMoveWithCoins) + { + // идем к самому ближнему золоту + goodMoves = safeAvailableMoves + .Where(x => x is { WithCoin: false, WithBigCoin: false }) + .Where(m => goldPositions.Contains(m.To.Position)) + .ToList(); + + if (CheckGoodMove(goodMoves, gameState.AvailableMoves, out goodMoveNum)) + return (goodMoveNum, null); + + List> list = []; + foreach (Move move in safeAvailableMoves + .Where(x => x.WithCoin == false) + .Where(x => !waterPositions.Contains(x.From.Position)) + .Where(x => IsEnemyNear(x.To.Position, board, teamId) == false)) + { + + var minDistance = goldPositions + .Select(p => Distance(p, move.To.Position) + move.To.Level) + .Min(); + + var goldPosition = goldPositions + .First(p => Distance(p, move.To.Position) + move.To.Level == minDistance); + + minDistance = Distance(goldPosition, move.To.Position) + move.To.Level; + list.Add(new Tuple(minDistance, move)); + } + + if (list.Count > 0) + { + int minDistance = list.Min(x => x.Item1); + goodMoves = list.Where(x => x.Item1 == minDistance) + .Select(x => x.Item2) + .ToList(); + } + } + + if (goodMoves.Count == 0 && unknownPositions.Count != 0) + { + // идем к самой ближней неизвестной клетке + List> list = new List>(); + foreach (Move move in safeAvailableMoves + .Where(x => x.WithCoin == false) + .Where(x => !waterPositions.Contains(x.From.Position)) + .Where(x => IsEnemyNear(x.To.Position, board, teamId) == false)) + { + var minDistance = MinDistance(unknownPositions, move.To.Position) + move.To.Level; + list.Add(new Tuple(minDistance, move)); + } + + if (list.Count > 0) + { + var minDistance = list.Min(x => x.Item1); + goodMoves = list.Where(x => x.Item1 == minDistance).Select(x => x.Item2).ToList(); + } + } + + if (goodMoves.Count == 0) + { + // залазим на свой корабль или бьемся об чужой + List> list = []; + foreach (Move move in gameState.AvailableMoves.Where(x => waterPositions.Contains(x.From.Position))) + { + int distance = WaterDistance(shipPosition, move.To.Position); + list.Add(new Tuple(distance, move)); + } + + if (list.Count > 0) + { + int minDistance = list.Min(x => x.Item1); + goodMoves = list.Where(x => x.Item1 == minDistance) + .Select(x => x.Item2) + .ToList(); + } + } + + if (goodMoves.Count == 0) + { + // выбираем любой доступный ход + goodMoves.AddRange(gameState.AvailableMoves); + } + + if (CheckGoodMove(goodMoves, gameState.AvailableMoves, out goodMoveNum)) + return (goodMoveNum, null); + + return (0, null); + } + + private bool CheckGoodMove(List moves, Move[] availableMoves, out int moveNum) + { + moveNum = 0; + if (moves.Count == 0) + return false; + + var resultMove = moves[_rnd.Next(moves.Count)]; + for (int i = 0; i < availableMoves.Length; i++) + { + if (availableMoves[i] == resultMove) + { + moveNum = i; + return true; + } + } + + return false; + } + + private static bool IsEnemyNear(Position to, Board board, int ourTeamId) + { + if (board.Map[to].Type == TileType.Water) return false; + + List enemyList = board.Teams[ourTeamId].EnemyTeamIds.ToList(); + for (int deltaX = -1; deltaX <= 1; deltaX++) + { + for (int deltaY = -1; deltaY <= 1; deltaY++) + { + if (deltaX == 0 && deltaY == 0) continue; + + var target = new Position(to.X + deltaX, to.Y + deltaY); + + var occupationTeamId = board.Map[target].OccupationTeamId; + if (occupationTeamId.HasValue && enemyList.Exists(x => x == occupationTeamId.Value)) + return true; + } + } + + return false; + } + + private static bool IsEnemyPosition(Position to, Board board, int teamId) + { + var occupationTeamId = board.Map[to].OccupationTeamId; + if (occupationTeamId.HasValue && + board.Teams[teamId].EnemyTeamIds.ToList().Exists(x => x == occupationTeamId.Value) && + to != board.Teams[occupationTeamId.Value].ShipPosition) + { + return true; + } + + return false; + } + + private static bool TargetIsShip(Board board, int teamId, Move move) + { + var shipPosition = board.Teams[teamId].ShipPosition; + return shipPosition == move.To.Position; + } + + private static int MinDistance(List positions, Position to) + { + return positions.ConvertAll(x => Distance(x, to)).Min(); + } + + private static int Distance(Position pos1, Position pos2) + { + int deltaX = Math.Abs(pos1.X - pos2.X); + int deltaY = Math.Abs(pos1.Y - pos2.Y); + return Math.Max(deltaX, deltaY); + } + + private static int WaterDistance(Position pos1, Position pos2) + { + int deltaX = Math.Abs(pos1.X - pos2.X); + int deltaY = Math.Abs(pos1.Y - pos2.Y); + return deltaX + deltaY; + } +} \ No newline at end of file diff --git a/Jackal.Core/Players/GamePlayerStat.cs b/Jackal.Core/Players/GamePlayerStat.cs new file mode 100644 index 00000000..305c4fe0 --- /dev/null +++ b/Jackal.Core/Players/GamePlayerStat.cs @@ -0,0 +1,73 @@ +namespace Jackal.Core.Players; + +/// +/// Статистика игрока +/// +public class GamePlayerStat +{ + /// + /// Имя пользователя или бота + /// + public string PlayerName { get; set; } + + /// + /// Количество побед за день + /// + public int WinCountToday { get; set; } + + /// + /// Количество побед за неделю + /// + public int WinCountThisWeek { get; set; } + + /// + /// Количество побед за месяц + /// + public int WinCountThisMonth { get; set; } + + /// + /// Количество побед, когда в конце игры золота оказалось больше. + /// В случае равенства по золоту, победа присуждается обеим командам. + /// + public int TotalWin { get; set; } + + /// + /// Ранг по шкале силы юнитов игры Heroes of Might and Magic II + /// + public string Rank => TotalWin.GetHeroes2Rank(); + + /// + /// Количество проведенных игр за день + /// + public int GamesCountToday { get; set; } + + /// + /// Количество проведенных игр за неделю + /// + public int GamesCountThisWeek { get; set; } + + /// + /// Количество проведенных игр за месяц + /// + public int GamesCountThisMonth { get; set; } + + /// + /// Количество проведенных игр за всё время + /// + public int GamesCountTotal { get; set; } + + /// + /// Суммарное количество добытых монет за все игры + /// + public int TotalCoins { get; set; } + + /// + /// Среднее количество побед за все игры + /// + public double AverageWin => (double)TotalWin / GamesCountTotal; + + /// + /// Среднее количество добытых монет за все игры + /// + public double AverageCoins => (double)TotalCoins / GamesCountTotal; +} \ No newline at end of file diff --git a/Jackal.Core/Players/HumanPlayer.cs b/Jackal.Core/Players/HumanPlayer.cs new file mode 100644 index 00000000..5e855a27 --- /dev/null +++ b/Jackal.Core/Players/HumanPlayer.cs @@ -0,0 +1,35 @@ +using System; + +namespace Jackal.Core.Players; + +/// +/// Игрок человек - выбирает ход через Web UI +/// +/// ИД пользователя +/// Имя игрока +public class HumanPlayer(long userId, string name) : IHumanPlayer, IPlayer +{ + private int _moveNum; + private Guid? _pirateId; + + public long UserId { get; } = userId; + + public string Name { get; } = name; + + public void OnNewGame() + { + _moveNum = 0; + _pirateId = null; + } + + public void SetMove(int moveNum, Guid? pirateId) + { + _moveNum = moveNum; + _pirateId = pirateId; + } + + public (int moveNum, Guid? pirateId) OnMove(GameState gameState) + { + return (_moveNum, _pirateId); + } +} \ No newline at end of file diff --git a/Jackal.Core/Players/IHumanPlayer.cs b/Jackal.Core/Players/IHumanPlayer.cs new file mode 100644 index 00000000..37c83ad8 --- /dev/null +++ b/Jackal.Core/Players/IHumanPlayer.cs @@ -0,0 +1,26 @@ +using System; + +namespace Jackal.Core.Players; + +/// +/// Интерфейс игрока человека +/// +public interface IHumanPlayer +{ + /// + /// ИД пользователя + /// + long UserId { get; } + + /// + /// Имя игрока + /// + string Name { get; } + + /// + /// Выбор хода для человека + /// + /// Номер хода из доступных ходов + /// Пират который ходит, можно не передавать + void SetMove(int moveNum, Guid? pirateId); +} \ No newline at end of file diff --git a/Jackal.Core/Players/IPlayer.cs b/Jackal.Core/Players/IPlayer.cs new file mode 100644 index 00000000..e022d994 --- /dev/null +++ b/Jackal.Core/Players/IPlayer.cs @@ -0,0 +1,24 @@ +using System; + +namespace Jackal.Core.Players; + +/// +/// Интерфейс игрока +/// +public interface IPlayer +{ + /// + /// Инициализация новой игры + /// + void OnNewGame(); + + /// + /// Игровой ход + /// + /// Состояние игры + /// + /// moveNum - номер хода из доступных ходов + /// pirateId - пират который ходит, можно не передавать + /// + (int moveNum, Guid? pirateId) OnMove(GameState gameState); +} \ No newline at end of file diff --git a/Jackal.Core/Players/OakioPlayer.cs b/Jackal.Core/Players/OakioPlayer.cs new file mode 100644 index 00000000..b231e821 --- /dev/null +++ b/Jackal.Core/Players/OakioPlayer.cs @@ -0,0 +1,19 @@ +using System; + +namespace Jackal.Core.Players; + +/// +/// Игрок бот реализованный фанатом игры oakio +/// +public class OakioPlayer : IPlayer +{ + public void OnNewGame() + { + } + + public (int moveNum, Guid? pirateId) OnMove(GameState gameState) + { + // выбирает первый ход из доступных ходов + return new ValueTuple(0, null); + } +} \ No newline at end of file diff --git a/Jackal.Core/Players/PlayerRankExtensions.cs b/Jackal.Core/Players/PlayerRankExtensions.cs new file mode 100644 index 00000000..e9f6f3d1 --- /dev/null +++ b/Jackal.Core/Players/PlayerRankExtensions.cs @@ -0,0 +1,207 @@ +namespace Jackal.Core.Players; + +public static class PlayerRankExtensions +{ + /// + /// Ранг по шкале силы юнитов игры Heroes of Might and Magic II + /// + public static string GetHeroes2Rank(this int totalWin) => + totalWin switch + { + // 1-ый уровень / +2 победы + < 2 => // Рыцари + // пехота / цена 20 + "Peasant", + < 4 => // Варвары + // пехота / цена 40 + "Goblin", + < 6 => // Колдуньи + // летуны / цена 50 + "Sprite", + < 8 => // Чародеи + // стрелки / цена 50 + "Halfling", + < 10 => // Некроманты + // пехота / цена 75 + "Skeleton", + < 12 => // Чернокнижники + // стрелки / цена 60 + "Centaur", + + // 2-ой уровень / +5 побед + < 15 => // Некроманты + // пехота / цена 150 + "Zombie", + < 20 => // Чародеи + // пехота / цена 150 + "Boar", + < 25 => // Колдуньи + // пехота / цена 200 + "Dwarf", + < 30 => // Варвары + // стрелки / цена 140 + "Orc", + < 35 => // Чернокнижники + // летуны / цена 200 + "Gargoyle", + < 40 => // Рыцари + // стрелки / цена 150 + "Archer", + + // 2-ой уровень - улучшение / +5 побед + < 45 => // Некроманты + // пехота / цена 200 + "MutantZombie", + < 50 => // Колдуньи + // пехота / цена 250 + "BattleDwarf", + < 55 => // Варвары + // стрелки / цена 175 + "OrcChief", + < 60 => // Рыцари + // стрелки / цена 200 + "Ranger", + + // 3-йй уровень / +10 побед + < 70 => // Рыцари + // пехота / цена 200 + "Pikeman", + < 80 => // Варвары + // пехота / цена 200 + "Wolf", + < 90 => // Некроманты + // пехота / цена 250 + "Mummy", + < 100 => // Чародеи + // пехота / цена 300 + "IronGolem", + < 110 => // Чернокнижники + // летуны / цена 300 + "Griffin", + < 120 => // Колдуньи + // стрелки / цена 250 + "Elf", + + // 3-йй уровень - улучшение / +10 побед + < 130 => // Рыцари + // пехота / цена 250 + "VeteranPikeman", + < 140 => // Некроманты + // пехота / цена 300 + "RoyalMummy", + < 150 => // Чародеи + // пехота / цена 350 + "SteelGolem", + < 160 => // Колдуньи + // стрелки / цена 300 + "GrandElf", + + // 4-ый уровень / +20 побед + < 180 => // Рыцари + // пехота / цена 250 + "Swordsman", + < 200 => // Варвары + // пехота / цена 300 + "Ogre", + < 220 => // Чернокнижники + // пехота / цена 400 + "Minotaur", + < 240 => // Чародеи + // летуны / цена 400 + "Roc", + < 260=> // Колдуньи + // стрелки / цена 350 + "Druid", + < 280 => // Некроманты + // летуны / цена 500 + "Vampire", + + // 4-ый уровень - улучшение / +20 побед + < 300 => // Рыцари + // пехота / цена 300 + "MasterSwordsman", + < 320 => // Варвары + // пехота / цена 500 + "OgreLord", + < 340 => // Чернокнижники + // пехота / цена 500 + "MinotaurKing", + < 360 => // Колдуньи + // стрелки / цена 400 + "GreaterDruid", + < 380 => // Некроманты + // летуны / цена 650 + "VampireLord", + + // 5-ый уровень / +30 побед + < 410 => // Рыцари + // пехота / цена 300 + "Cavalry", + < 440 => // Колдуньи + // пехота / цена 500 + "Unicorn", + < 470 => // Чернокнижники + // пехота / цена 800 + "Hydra", + < 500 => // Чародеи + // стрелки / цена 600 + "Mage", + < 530 => // Варвары + // стрелки / цена 600 + "Troll", + < 560 => // Некроманты + // стрелки / цена 750 + "Lich", + + // 5-ый уровень - улучшение / +30 побед + < 590 => // Рыцари + // пехота / цена 375 + "Champion", + < 620 => // Чародеи + // стрелки / цена 700 + "Archmage", + < 650 => // Варвары + // стрелки / цена 700 + "WarTroll", + < 680 => // Некроманты + // стрелки / цена 900 + "PowerLich", + + // 6-ой уровень / +40 побед + < 720 => // Рыцари + // пехота / цена 600 + "Paladin", + < 760 => // Варвары + // пехота / цена 750 +1 + "Cyclops", + < 800 => // Чародеи + // пехота / цена 2000 +1 + "Giant", + < 840 => // Некроманты + // летуны / цена 1500 + "BoneDragon", + < 880 => // Колдуньи + // летуны / цена 1500 +1 + "Phoenix", + < 920 => // Чернокнижники + // летуны / цена 3000 +1 + "GreenDragon", + + // 6-ой уровень - улучшение / +40 побед + < 960 => // Рыцари + // пехота / цена 1000 + "Crusader", + < 1000 => // Чернокнижники + // летуны / цена 3500 +1 + "RedDragon", + + // 6-ой уровень - мощь / +1000 побед + < 2000 => // Чародеи + // стрелки / цена 5000 +2 + "Titan", + + // Чернокнижники + // летуны / цена 4000 +2 + _ => "BlackDragon" + }; +} \ No newline at end of file diff --git a/Jackal.Core/Players/RandomPlayer.cs b/Jackal.Core/Players/RandomPlayer.cs new file mode 100644 index 00000000..d1ff8441 --- /dev/null +++ b/Jackal.Core/Players/RandomPlayer.cs @@ -0,0 +1,21 @@ +using System; + +namespace Jackal.Core.Players; + +/// +/// Игрок рэндом - выбирает ход случайным образом +/// +public class RandomPlayer : IPlayer +{ + private Random _rnd = new(); + + public void OnNewGame() + { + _rnd = new Random(42); + } + + public (int moveNum, Guid? pirateId) OnMove(GameState gameState) + { + return (_rnd.Next(gameState.AvailableMoves.Length), null); + } +} \ No newline at end of file diff --git a/Jackal.Core/SubTurnState.cs b/Jackal.Core/SubTurnState.cs new file mode 100644 index 00000000..40101e8f --- /dev/null +++ b/Jackal.Core/SubTurnState.cs @@ -0,0 +1,50 @@ +namespace Jackal.Core; + +/// +/// Состояние дополнительного хода +/// +public class SubTurnState +{ + /// + /// Полет на самолете + /// + public bool AirplaneFlying { get; set; } + + /// + /// Количество просмотров карты с маяка + /// + public int LighthouseViewCount { get; set; } + + /// + /// Падение в дыру + /// + public bool FallingInTheHole { get; set; } + + /// + /// Фаза разлома: + /// 2 - выбираем первую клетку для обмена, + /// 1 - выбираем вторую клетку + /// 0 - конец + /// + public int QuakePhase { get; set; } + + /// + /// Ход за бутылку с ромом + /// + public bool DrinkRumBottle { get; set; } + + /// + /// Ходы за другую команду при розыгрыше хи-хи травы + /// + public int CannabisTurnCount { get; set; } + + public void Clear() + { + AirplaneFlying = false; + LighthouseViewCount = 0; + FallingInTheHole = false; + QuakePhase = 0; + DrinkRumBottle = false; + CannabisTurnCount = CannabisTurnCount > 0 ? CannabisTurnCount - 1 : 0; + } +} \ No newline at end of file diff --git a/Jackal.Core/TileExtension.cs b/Jackal.Core/TileExtension.cs new file mode 100644 index 00000000..1ead629c --- /dev/null +++ b/Jackal.Core/TileExtension.cs @@ -0,0 +1,12 @@ +using Jackal.Core.Domain; + +namespace Jackal.Core; + +public static class TileExtension +{ + public static int CoinsCount(this Tile tile) => + tile.Type == TileType.Coin ? tile.Code : 0; + + public static int BigCoinsCount(this Tile tile) => + tile.Type == TileType.BigCoin ? tile.Code : 0; +} \ No newline at end of file diff --git a/Jackal.Core/Utils.cs b/Jackal.Core/Utils.cs new file mode 100644 index 00000000..481fe1f6 --- /dev/null +++ b/Jackal.Core/Utils.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Jackal.Core.Domain; + +namespace Jackal.Core; + +public static class Utils +{ + public static int Factorial(int n) + { + if (n < 0) + throw new ArgumentException("n"); + switch (n) + { + case 0: + case 1: + return 1; + case 2: + return 2; + case 3: + return 3*2; + case 4: + return 4*3*2; + default: + { + int rez = 4*3*2; + for (int i = 5; i <= n; i++) + { + checked + { + rez *= i; + } + } + return rez; + } + } + } + + /// + /// Попадание в углы участка + /// + /// + /// + /// + /// + public static bool InCorners(Position value, int min, int max) + { + return (value.X == min || value.X == max) && (value.Y == min || value.Y == max); + } + + public static IEnumerable GetPermutation(int index, T[] array) where T : class + { + int length = array.Length; + if (length == 1) + return array; + + int permutationsCount = Factorial(length); + index %= permutationsCount; + var t = array[index / (permutationsCount / length)]; + return new T[] { t }.Concat(GetPermutation( + index % (permutationsCount / length), + array.Where(x => !x.Equals(t)).ToArray()) + ); + } +} \ No newline at end of file diff --git a/Jackal.Tests2/GameTests/GameOverTests.cs b/Jackal.Tests2/GameTests/GameOverTests.cs new file mode 100644 index 00000000..433aab03 --- /dev/null +++ b/Jackal.Tests2/GameTests/GameOverTests.cs @@ -0,0 +1,179 @@ +using Jackal.Core.Domain; +using Jackal.Core.MapGenerator; +using Xunit; + +namespace Jackal.Tests2.GameTests; + +public class GameOverTests +{ + [Fact] + public void OnePlayerHole_MoveIntoTheHole_ReturnGameOverByAllPiratesEnd() + { + // Arrange + var holeOnlyMap = new OneTileMapGenerator( + TileParams.Hole() + ); + var game = new TestGame(holeOnlyMap); + + // Act - высадка с корабля на дыру + game.Turn(); + + // Assert - все пираты (один) застряли в дыре, карта не открыта = конец игры + Assert.True(game.IsGameOver); + Assert.Equal("Победа HumanPlayer путём конца всех пиратов!", game.GameMessage); + Assert.Equal(1, game.TurnNumber); + } + + [Fact] + public void OnePlayerLighthouse_TurnSearchAllMap_ReturnGameOverByMapExploration() + { + // Arrange + var lighthouseSpinningLineMap = new TwoTileMapGenerator( + TileParams.Lighthouse(), + TileParams.SpinningForest() + ); + var game = new TestGame(lighthouseSpinningLineMap); + + // Act - высадка с корабля на маяк + game.Turn(); + + // по очереди смотрим маяком неизвестные клетки + game.Turn(); + game.Turn(); + game.Turn(); + game.Turn(); + + // Assert - один игрок, вся карта открыта, золота нет = конец игры + Assert.True(game.IsGameOver); + Assert.Equal("Победа HumanPlayer путём исследования карты!", game.GameMessage); + Assert.Equal(1, game.TurnNumber); + } + + [Fact] + public void OnePlayerCoin1_MoveAllCoinsToTheShip_ReturnNoGameOver() + { + // Arrange + var totalCoins = 1; + var coin1EmptyLineMap = new TwoTileMapGenerator( + TileParams.Coin(), + TileParams.Empty(), + totalCoins + ); + var game = new TestGame(coin1EmptyLineMap); + + // Act - высадка с корабля на сундук с одной монетой + game.Turn(); + + // переносим монету на корабль + game.SetMoveAndTurn(2, 0, true); + + // Assert - один игрок, карта не открыта, перенесли большую часть золота (всё золото) <> конец игры + Assert.False(game.IsGameOver); + Assert.Equal(2, game.TurnNumber); + } + + [Fact] + public void TwoPlayersCoin1_MoveAllCoinsToTheShip_ReturnGameOverByGoldDomination() + { + // Arrange + var totalCoins = 1; + var coin1EmptyLineMap = new TwoTileMapGenerator( + TileParams.Coin(), + TileParams.Empty(), + totalCoins + ); + var game = new TestGame(coin1EmptyLineMap); + game.AddEnemyTeamAndPirate(new TilePosition(2, 4)); + + // Act - высадка с корабля на сундук с одной монетой + game.Turn(); + + // переносим монету на корабль + game.SetMoveAndTurn(2, 0, true); + + // Assert - два игрока, карта не открыта, перенесли большую часть золота (всё золото) = конец игры + Assert.True(game.IsGameOver); + Assert.Equal("Победа HumanPlayer путём доминирования по золоту!", game.GameMessage); + Assert.Equal(2, game.TurnNumber); + } + + [Fact] + public void TwoPlayersCoin1_EqualAmountOfCoinsThenSearchAllMap_ReturnGameOver() + { + // Arrange + var totalCoins = 2; + var enemyTeamCoins = 1; + var coin1LighthouseLineMap = new TwoTileMapGenerator( + TileParams.Coin(), + TileParams.Lighthouse(), + totalCoins + ); + var game = new TestGame(coin1LighthouseLineMap); + game.AddEnemyTeamAndPirate(new TilePosition(2, 4), enemyTeamCoins); + + // Act - высадка с корабля на сундук с одной монетой + game.Turn(); + + // переносим монету на корабль + game.SetMoveAndTurn(2, 0, true); + + // высадка с корабля на сундук с одной монетой + game.Turn(); + + // идем вперед на маяк + game.SetMoveAndTurn(2, 2); + + // открываем маяком оставшиеся 3 закрытых клетки + game.Turn(); + game.Turn(); + game.Turn(); + + // Assert - два игрока, карта открыта, перенесли равные части золота = конец игры + Assert.True(game.IsGameOver); + Assert.Equal("Победа дружбы путём исследования карты!", game.GameMessage); + Assert.Equal(4, game.TurnNumber); + } + + [Fact] + public void TwoPlayersCoin2_OneCoinMoveToTheShipOneCoinLost_ReturnGameOverByGoldDomination() + { + // Arrange + var totalCoins = 2; + var coin1EmptyLineMap = new TwoTileMapGenerator( + TileParams.Coin(2), + TileParams.FourArrowsDiagonal(), + totalCoins + ); + var game = new TestGame(coin1EmptyLineMap); + game.AddEnemyTeamAndPirate(new TilePosition(2, 4)); + + // Act - высадка с корабля на сундук с двумя монетами + game.Turn(); + + // открываем четыре стрелки по диагонали на все углы + game.SetMoveAndTurn(2, 2); + + // прыгаем в воду по стрелке рядом со своим кораблем + game.SetMoveAndTurn(1, 1); + + // залезаем из воды на свой корабль + game.SetMoveAndTurn(2, 0); + + // высадка с корабля на сундук с двумя монетами + game.Turn(); + + // переносим монету на корабль + game.SetMoveAndTurn(2, 0, true); + + // высадка с корабля на сундук с двумя монетами + game.Turn(); + + // топим вторую монету по стрелке рядом со своим кораблем + game.SetMoveAndTurn(1, 1, true); + + // Assert - два игрока, карта не открыта, перенесли большую часть золота = конец игры + Assert.True(game.IsGameOver); + Assert.Equal("Победа HumanPlayer путём доминирования по золоту!", game.GameMessage); + Assert.Equal(7, game.TurnNumber); + } +} \ No newline at end of file diff --git a/Jackal.Tests2/Jackal.Tests2.csproj b/Jackal.Tests2/Jackal.Tests2.csproj new file mode 100644 index 00000000..b1b47815 --- /dev/null +++ b/Jackal.Tests2/Jackal.Tests2.csproj @@ -0,0 +1,28 @@ + + + + net8.0 + latest + enable + false + true + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/Jackal.Tests2/JsonHelperTests.cs b/Jackal.Tests2/JsonHelperTests.cs new file mode 100644 index 00000000..3caa7f7c --- /dev/null +++ b/Jackal.Tests2/JsonHelperTests.cs @@ -0,0 +1,62 @@ +using Jackal.Core; +using Jackal.Core.Domain; +using Jackal.Core.MapGenerator; +using Jackal.Core.Players; +using Newtonsoft.Json; +using Xunit; + +namespace Jackal.Tests2; + +public class JsonHelperTests +{ + [Fact] + public void Board_SerializeAndDeserialize_Correct() + { + // Arrange + const int mapSize = 5; + IPlayer[] players = [new HumanPlayer(1, "HumanPlayer")]; + var randomMap = new RandomMapGenerator(1, mapSize); + var gameRequest = new GameRequest(mapSize, randomMap, players, GameModeType.TwoPlayersInTeam, 1); + var board = new Board(gameRequest); + + // Act + var json = JsonHelper.SerializeWithType(board, Formatting.Indented); + var board2 = JsonHelper.DeserializeWithType(json); + var json2 = JsonHelper.SerializeWithType(board2, Formatting.Indented); + + // Assert + Assert.True(json == json2); + } + + [Fact] + public void TilePosition_SerializeAndDeserialize_Correct() + { + // Arrange + const int level = 3; + var position = new Position(1, 2); + var tilePosition = new TilePosition(position, level); + + // Act + var json = JsonHelper.SerializeWithType(tilePosition, Formatting.Indented); + var tilePosition2 = JsonHelper.DeserializeWithType(json); + var json2 = JsonHelper.SerializeWithType(tilePosition2, Formatting.Indented); + + // Assert + Assert.True(json == json2); + } + + [Fact] + public void Position_SerializeAndDeserialize_Correct() + { + // Arrange + var position = new Position(1, 2); + + // Act + var json = JsonHelper.SerializeWithType(position, Formatting.Indented); + var position2 = JsonHelper.DeserializeWithType(json); + var json2 = JsonHelper.SerializeWithType(position2, Formatting.Indented); + + // Assert + Assert.True(json == json2); + } +} \ No newline at end of file diff --git a/Jackal.Tests2/TestGame.cs b/Jackal.Tests2/TestGame.cs new file mode 100644 index 00000000..de86dd90 --- /dev/null +++ b/Jackal.Tests2/TestGame.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Generic; +using Jackal.Core; +using Jackal.Core.Domain; +using Jackal.Core.MapGenerator; +using Jackal.Core.Players; + +namespace Jackal.Tests2; + +/// +/// Класс обертка для тестирования игры, +/// чтобы не плодить зависимости на Game +/// +public class TestGame +{ + /// + /// Тестовая игра + /// + private readonly Game _testGame; + + /// + /// Игровое поле + /// + public Board Board => _testGame.Board; + + /// + /// Текущий ход - определяет какая команда ходит + /// + public int TurnNumber => _testGame.TurnNumber; + + /// + /// Конец игры + /// + public bool IsGameOver => _testGame.IsGameOver; + + /// + /// Игровое сообщение + /// + public string GameMessage => _testGame.GameMessage; + + /// + /// Конструктор, всегда тестируем - производим ход только одной командой. + /// Пиратов команды противника добавляем отдельно, за них не ходим. + /// + /// Генератор карты + /// Размер карты вместе с морем, по умолчанию минимальный 5x5 (поле из 5 клеток) + /// Пиратов в команде, по умолчанию 1 + public TestGame(IMapGenerator generator, int mapSize = 5, int piratesPerPlayer = 1) + { + var gameRequest = new GameRequest( + mapSize, generator, [new HumanPlayer(1, "HumanPlayer")], GameModeType.FreeForAll, piratesPerPlayer + ); + _testGame = new Game(gameRequest); + } + + /// + /// Добавить монету в игру + /// + /// Позиция монеты + public void AddCoin(TilePosition coinPosition) + { + if (Board.Map[coinPosition.Position].Type == TileType.Unknown) + { + throw new Exception("Tile must not be Unknown"); + } + + _testGame.CoinsOnMap++; + Board.Map[coinPosition].Coins++; + } + + /// + /// Добавить вражескую команду и пирата в игру, + /// при этом не добавляем сущность игрока противника + /// следовательно ход не будет передаваться вражеской команде + /// + /// Позиция пирата противника + /// Количество затащенных монет + public void AddEnemyTeamAndPirate(TilePosition piratePosition, int coins = 0) + { + const int enemyTeamId = 1; + + // помещаем корабль противника сверху на противоположный берег + var shipPosition = new Position((Board.MapSize - 1) / 2, Board.MapSize - 1); + var enemyTeam = new Team(enemyTeamId, "Test enemy team", 0, shipPosition, []) + { + Coins = coins, + EnemyTeamIds = [0] + }; + + Board.Teams[0].EnemyTeamIds = [1]; + Board.Teams = [Board.Teams[0], enemyTeam]; + _testGame.AddPirate(enemyTeamId, piratePosition, PirateType.Usual); + } + + /// + /// Добавить своего пирата в игру + /// + /// Позиция своего пирата + /// Тип пирата + public void AddOwnTeamPirate(TilePosition piratePosition, PirateType type) + { + const int ownTeamId = 0; + + _testGame.AddPirate(ownTeamId, piratePosition, type); + } + + /// + /// Убрать пирата из игры + /// + /// Пират + public void KillPirate(Pirate pirate) => _testGame.KillPirate(pirate); + + /// + /// Получить возможные ходы + /// + public List GetAvailableMoves() + { + return _testGame.GetAvailableMoves(); + } + + /// + /// Сделать ход по умолчанию, + /// выбирает первый доступный ход, + /// если ход всего один то сделает его + /// + public void Turn(int moveNum = 0) + { + if (_testGame.CurrentPlayer is IHumanPlayer humanPlayer) + { + humanPlayer.SetMove(moveNum, null); + } + + _testGame.Turn(); + } + + /// + /// Сделать ход по координатам целевой клетки + /// + /// X координата куда делаем ход + /// Y координата куда делаем ход + /// С монетой + /// С большой монетой + public void SetMoveAndTurn(int x, int y, bool withCoin = false, bool withBigCoin = false) + { + var position = new TilePosition(x, y); + var moves = _testGame.GetAvailableMoves(); + var moveNum = moves.FindIndex(a => + a.To == position && a.WithCoin == withCoin && a.WithBigCoin == withBigCoin + ); + Turn(moveNum); + } + + /// + /// Сделать ход по позициям клеток, + /// используется если несколько пиратов + /// и надо выбрать с какой клетки делать ход + /// + /// Позиция откуда делаем ход + /// Позиция куда делаем ход + public void SetMoveAndTurn(TilePosition from, TilePosition to) + { + var moves = _testGame.GetAvailableMoves(); + var moveNum = moves.FindIndex(a => a.From == from && a.To == to); + Turn(moveNum); + } +} \ No newline at end of file diff --git a/Jackal.Tests2/TileTests/AirplaneTests.cs b/Jackal.Tests2/TileTests/AirplaneTests.cs new file mode 100644 index 00000000..18fe352b --- /dev/null +++ b/Jackal.Tests2/TileTests/AirplaneTests.cs @@ -0,0 +1,271 @@ +using System.Collections.Generic; +using System.Linq; +using Jackal.Core.Domain; +using Jackal.Core.MapGenerator; +using Xunit; + +namespace Jackal.Tests2.TileTests; + +public class AirplaneTests +{ + [Fact] + public void OneAirplane_GetAvailableMoves_ReturnWholeMapAndOwnShip() + { + // Arrange + var airplaneOnlyMap = new OneTileMapGenerator(TileParams.Airplane()); + var game = new TestGame(airplaneOnlyMap); + + // Act - высадка с корабля на самолет + game.Turn(); + + var moves = game.GetAvailableMoves(); + + // Assert - все поле 5 клеток + свой корабль + Assert.Equal(6, moves.Count); + Assert.Equal(new TilePosition(2, 1), moves.First().From); + Assert.Equivalent(new List + { + new(1, 2), + new(2, 0), // свой корабль + new(2, 1), // клетка с самолетом + new(2, 2), + new(2, 3), + new(3, 2) + }, + moves.Select(m => m.To) + ); + Assert.Equal(0, game.TurnNumber); + } + + [Fact] + public void AirplaneThenNextAirplane_GetAvailableMoves_ReturnWholeMapAndOwnShip() + { + // Arrange + var airplaneOnlyMap = new OneTileMapGenerator(TileParams.Airplane()); + var game = new TestGame(airplaneOnlyMap); + + // Act - высадка с корабля на самолет + game.Turn(); + + // выбираем ход - вперед на следующий самолет + game.SetMoveAndTurn(2, 2); + + var moves = game.GetAvailableMoves(); + + // Assert - все поле 5 клеток + свой корабль + Assert.Equal(6, moves.Count); + Assert.Equal(new TilePosition(2, 2), moves.First().From); + Assert.Equivalent(new List + { + new(1, 2), + new(2, 0), // свой корабль + new(2, 1), // клетка с самолетом + new(2, 2), + new(2, 3), + new(3, 2) + }, + moves.Select(m => m.To) + ); + Assert.Equal(0, game.TurnNumber); + } + + // TODO-TEST AirplaneThenNextAirplaneThenPrevAirplane_GetAvailableMoves_ReturnAroundFirstAirplaneTiles() + + [Fact] + public void AirplaneThenIce_GetAvailableMoves_ReturnWholeMapAndOwnShipExceptOpenIce() + { + // Arrange + var airplaneIceLineMap = new TwoTileMapGenerator( + TileParams.Airplane(), + TileParams.Ice() + ); + + var game = new TestGame(airplaneIceLineMap); + + // Act - высадка с корабля на самолет + game.Turn(); + + // выбираем ход - вперед на лед + game.SetMoveAndTurn(2, 2); + + var moves = game.GetAvailableMoves(); + + // Assert - все поле 5 клеток + свой корабль, кроме открытого льда + Assert.Equal(5, moves.Count); + Assert.Equal(new TilePosition(2, 2), moves.First().From); + Assert.Equivalent(new List + { + new(1, 2), + new(2, 0), // свой корабль + new(2, 1), // клетка с самолетом + new(2, 3), + new(3, 2) + }, + moves.Select(m => m.To) + ); + Assert.Equal(0, game.TurnNumber); + } + + [Fact] + public void AirplaneThenCrocodile_GetAvailableMoves_ReturnWholeMapAndOwnShipExceptOpenCrocodile() + { + // Arrange + var airplaneIceLineMap = new TwoTileMapGenerator( + TileParams.Airplane(), + TileParams.Crocodile() + ); + + var game = new TestGame(airplaneIceLineMap); + + // Act - высадка с корабля на самолет + game.Turn(); + + // выбираем ход - вперед на крокодила + game.SetMoveAndTurn(2, 2); + + var moves = game.GetAvailableMoves(); + + // Assert - все поле 5 клеток + свой корабль, кроме открытого крокодила + Assert.Equal(5, moves.Count); + Assert.Equal(new TilePosition(2, 2), moves.First().From); + Assert.Equivalent(new List + { + new(1, 2), + new(2, 0), // свой корабль + new(2, 1), // клетка с самолетом + new(2, 3), + new(3, 2) + }, + moves.Select(m => m.To) + ); + Assert.Equal(0, game.TurnNumber); + } + + [Fact] + public void AirplaneThenArrowUpOnCrocodile_Turn_ReturnDeadPirate() + { + // Arrange + var airplaneArrowUpOnCrocodileLineMap = new ThreeTileMapGenerator( + TileParams.Airplane(), + TileParams.OneArrowUp(), + TileParams.Crocodile() + ); + + var game = new TestGame(airplaneArrowUpOnCrocodileLineMap); + + // Act - высадка с корабля на самолет + game.Turn(); + + // выбираем ход - вперед на одинарную стрелку перпендикулярно вверх + game.SetMoveAndTurn(2, 2); + + // единственный ход - вперед по стрелке на крокодила + game.Turn(); + + // Assert - пират помер + Assert.Empty(game.Board.AllPirates); + Assert.NotNull(game.Board.DeadPirates); + Assert.Single(game.Board.DeadPirates); + Assert.Equal(1, game.TurnNumber); + } + + [Fact] + public void AirplaneThenCrocodileThenArrowUpOnCrocodile_Turn_ReturnDeadPirate() + { + // Arrange + var airplaneArrowUpOnCrocodileLineMap = new ThreeTileMapGenerator( + TileParams.Airplane(), + TileParams.OneArrowUp(), + TileParams.Crocodile() + ); + + var game = new TestGame(airplaneArrowUpOnCrocodileLineMap); + + // Act - высадка с корабля на самолет + game.Turn(); + + // выбираем ход - вперед через клетку на крокодила + game.SetMoveAndTurn(2, 3); + + // выбираем ход - на стрелку перед открытым крокодилом + game.SetMoveAndTurn(2, 2); + + // Assert - пират помер + Assert.Empty(game.Board.AllPirates); + Assert.NotNull(game.Board.DeadPirates); + Assert.Single(game.Board.DeadPirates); + Assert.Equal(1, game.TurnNumber); + } + + [Fact] + public void OneAirplaneWait_GetAvailableMoves_ReturnWholeMapAndOwnShip() + { + // Arrange + var airplaneOnlyMap = new OneTileMapGenerator(TileParams.Airplane()); + var game = new TestGame(airplaneOnlyMap); + + // Act - высадка с корабля на самолет + game.Turn(); + + // пропускаем ход самолета + game.SetMoveAndTurn(2, 1); + + var moves = game.GetAvailableMoves(); + + // Assert - следующий ход, доступен ход самолета + // все поле 5 клеток + свой корабль + Assert.Equal(6, moves.Count); + Assert.Equal(new TilePosition(2, 1), moves.First().From); + Assert.Equivalent(new List + { + new(1, 2), + new(2, 0), // свой корабль + new(2, 1), // клетка с самолетом + new(2, 2), + new(2, 3), + new(3, 2) + }, + moves.Select(m => m.To) + ); + Assert.Equal(1, game.TurnNumber); + } + + [Fact] + public void OneAirplaneWaitThenSameAirplaneMoveByNextPirate_GetAvailableMoves_ReturnWholeMapAndOwnShip() + { + // Arrange + const int piratesPerPlayer = 2; + var airplaneOnlyMap = new OneTileMapGenerator(TileParams.Airplane()); + var game = new TestGame(airplaneOnlyMap, 5, piratesPerPlayer); + + // Act - высадка с корабля на самолет + game.Turn(); + + // пропускаем ход самолета + game.SetMoveAndTurn(2, 1); + + // ходим вторым пиратом с корабля на самолет + var from = new TilePosition(2, 0); + var to = new TilePosition(2, 1); + game.SetMoveAndTurn(from, to); + + var moves = game.GetAvailableMoves(); + + // Assert - продолжается ход второго пирата, доступен ход самолета + // все поле 5 клеток + свой корабль + Assert.Equal(6, moves.Count); + Assert.Equal(new TilePosition(2, 1), moves.First().From); + Assert.Equivalent(new List + { + new(1, 2), + new(2, 0), // свой корабль + new(2, 1), // клетка с самолетом + new(2, 2), + new(2, 3), + new(3, 2) + }, + moves.Select(m => m.To) + ); + Assert.Equal(1, game.TurnNumber); + } +} \ No newline at end of file diff --git a/Jackal.Tests2/TileTests/BalloonTests.cs b/Jackal.Tests2/TileTests/BalloonTests.cs new file mode 100644 index 00000000..7737762e --- /dev/null +++ b/Jackal.Tests2/TileTests/BalloonTests.cs @@ -0,0 +1,43 @@ +using Jackal.Core.Domain; +using Jackal.Core.MapGenerator; +using Xunit; + +namespace Jackal.Tests2.TileTests; + +public class BalloonTests +{ + [Fact] + public void OneBalloon_Turn_ReturnToOurShip() + { + // Arrange + var balloonOnlyMap = new OneTileMapGenerator(TileParams.Balloon()); + var game = new TestGame(balloonOnlyMap); + + // Act - высадка с корабля на закрытый воздушный шар + game.Turn(); + + // Assert - пират находится на нашем корабле + Assert.Single(game.Board.AllPirates); + Assert.Equal(new TilePosition(2, 0), game.Board.AllPirates[0].Position); + Assert.Equal(1, game.TurnNumber); + } + + [Fact] + public void OneBalloon_2Turn_ReturnToOurShip() + { + // Arrange + var balloonOnlyMap = new OneTileMapGenerator(TileParams.Balloon()); + var game = new TestGame(balloonOnlyMap); + + // Act - высадка с корабля на закрытый воздушный шар + game.Turn(); + + // высадка с корабля на открытый воздушный шар + game.Turn(); + + // Assert - пират находится на нашем корабле + Assert.Single(game.Board.AllPirates); + Assert.Equal(new TilePosition(2, 0), game.Board.AllPirates[0].Position); + Assert.Equal(2, game.TurnNumber); + } +} \ No newline at end of file diff --git a/Jackal.Tests2/TileTests/BenGunnTests.cs b/Jackal.Tests2/TileTests/BenGunnTests.cs new file mode 100644 index 00000000..9b390e0d --- /dev/null +++ b/Jackal.Tests2/TileTests/BenGunnTests.cs @@ -0,0 +1,101 @@ +using System.Collections.Generic; +using System.Linq; +using Jackal.Core.Domain; +using Jackal.Core.MapGenerator; +using Xunit; + +namespace Jackal.Tests2.TileTests; + +public class BenGunnTests +{ + [Fact] + public void OneBenGunn_GetAvailableMoves_ReturnNearestMoves() + { + // Arrange + var benGunnOnlyMap = new OneTileMapGenerator(TileParams.BenGunn()); + var game = new TestGame(benGunnOnlyMap); + + // Act - высадка с корабля на Бен Ганна + game.Turn(); + var moves = game.GetAvailableMoves(); + + // Assert - доступно 4 хода на соседние клетки с Бен Ганна в месте высадки + Assert.Equal(4, moves.Count); + Assert.Equal(new TilePosition(2, 1), moves.First().From); + Assert.Equivalent(new List + { + new(1, 2), + new(2, 0), // свой корабль + new(2, 2), + new(3, 2) + }, + moves.Select(m => m.To) + ); + Assert.Equal(1, game.TurnNumber); + } + + [Fact] + public void OneBenGunn_Turn_ReturnNewPirate() + { + // Arrange + var benGunnOnlyMap = new OneTileMapGenerator(TileParams.BenGunn()); + var game = new TestGame(benGunnOnlyMap); + + // Act - высадка с корабля на Бен Ганна + game.Turn(); + + // Assert - пиратов стало больше: 1 обычный и 1 Бен Ганн + Assert.Equal(2, game.Board.AllPirates.Count); + Assert.Single(game.Board.AllPirates.Where(p => p.Type == PirateType.Usual)); + Assert.Single(game.Board.AllPirates.Where(p => p.Type == PirateType.BenGunn)); + Assert.Equal(1, game.TurnNumber); + } + + [Fact] + public void OneBenGunnThenShipThenSameBenGunnAgain_Turn_ReturnOneNewPirate() + { + // Arrange + var benGunnOnlyMap = new OneTileMapGenerator(TileParams.BenGunn()); + var game = new TestGame(benGunnOnlyMap); + + // Act - высадка с корабля на Бен Ганна + game.Turn(); + + // обратно на корабль + game.SetMoveAndTurn(2, 0); + + // высадка с корабля на уже открытого Бен Ганна + game.Turn(); + + // Assert - пиратов стало больше: 1 обычный и 1 Бен Ганн + Assert.Equal(2, game.Board.AllPirates.Count); + Assert.Single(game.Board.AllPirates.Where(p => p.Type == PirateType.Usual)); + Assert.Single(game.Board.AllPirates.Where(p => p.Type == PirateType.BenGunn)); + Assert.Equal(3, game.TurnNumber); + } + + [Fact] + public void LighthouseThenSearch4BenGunn_Turn_ReturnNoNewPirate() + { + // Arrange + var lighthouseBenGunnLineMap = new TwoTileMapGenerator( + TileParams.Lighthouse(), + TileParams.BenGunn() + ); + var game = new TestGame(lighthouseBenGunnLineMap); + + // Act - высадка с корабля на маяк + game.Turn(); + + // по очереди смотрим неизвестные клетки: все Бен Ганны + game.Turn(); + game.Turn(); + game.Turn(); + game.Turn(); + + // Assert - пиратов не прибавилось: 1 обычный + Assert.Single(game.Board.AllPirates); + Assert.Single(game.Board.AllPirates.Where(p => p.Type == PirateType.Usual)); + Assert.Equal(1, game.TurnNumber); + } +} \ No newline at end of file diff --git a/Jackal.Tests2/TileTests/BigCoinTests.cs b/Jackal.Tests2/TileTests/BigCoinTests.cs new file mode 100644 index 00000000..e3e76275 --- /dev/null +++ b/Jackal.Tests2/TileTests/BigCoinTests.cs @@ -0,0 +1,86 @@ +using System.Collections.Generic; +using System.Linq; +using Jackal.Core.Domain; +using Jackal.Core.MapGenerator; +using Xunit; + +namespace Jackal.Tests2.TileTests; + +public class BigCoinTests +{ + [Fact] + public void OneBigCoin_GetAvailableMoves_ReturnNearestMovesAndMoveWithBigCoin() + { + // Arrange + var bigCoinOnlyMap = new OneTileMapGenerator( + TileParams.BigCoin() + ); + var game = new TestGame(bigCoinOnlyMap); + + // Act - высадка с корабля на большую монету + game.Turn(); + var moves = game.GetAvailableMoves(); + + // Assert - доступно 5 ходов: 3 на соседние клетки в месте высадки + // + 2 на свой корабль с большой монетой и без неё + Assert.Equal(5, moves.Count); + Assert.Equal(new TilePosition(2, 1), moves.First().From); + Assert.Equivalent(new List + { + MoveType.Usual, + MoveType.WithBigCoin + }, + moves.Where(m => m.To == new TilePosition(2, 0)).Select(m => m.Type) + ); + Assert.Equal(1, game.TurnNumber); + } + + [Fact] + public void OneBigCoin_TakeBigCoinOnShip_AddThreeCoinsToTeamScore() + { + // Arrange + var bigCoinOnlyMap = new OneTileMapGenerator( + TileParams.BigCoin() + ); + var game = new TestGame(bigCoinOnlyMap); + + // Act - высадка с корабля на большую монету + game.Turn(); + + // обратно на корабль с большой монетой + game.SetMoveAndTurn(2, 0, withBigCoin: true); + var moves = game.GetAvailableMoves(); + + // Assert - доступен один ход - высадка с корабля + Assert.Single(moves); + Assert.Equal(new TilePosition(2, 0), moves.Single().From); + Assert.Equal(3, game.Board.Teams.Single().Coins); + Assert.Equal(2, game.TurnNumber); + } + + [Fact] + public void OneEmptyBigCoin_GetAvailableMoves_ReturnNearestMovesWithoutBigCoin() + { + // Arrange + var bigCoinOnlyMap = new OneTileMapGenerator( + TileParams.BigCoin() + ); + var game = new TestGame(bigCoinOnlyMap); + + // Act - высадка с корабля на большую монету + game.Turn(); + + // обратно на корабль с большой монетой + game.SetMoveAndTurn(2, 0, withBigCoin: true); + + // высадка с корабля + game.Turn(); + var moves = game.GetAvailableMoves(); + + // Assert - доступно 4 хода на соседние клетки в месте высадки + Assert.Equal(4, moves.Count); + Assert.Equal(new TilePosition(2, 1), moves.First().From); + Assert.Equivalent(new List { MoveType.Usual }, moves.Select(m => m.Type)); + Assert.Equal(3, game.TurnNumber); + } +} \ No newline at end of file diff --git a/Jackal.Tests2/TileTests/CannabisTests.cs b/Jackal.Tests2/TileTests/CannabisTests.cs new file mode 100644 index 00000000..59652f26 --- /dev/null +++ b/Jackal.Tests2/TileTests/CannabisTests.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using System.Linq; +using Jackal.Core.Domain; +using Jackal.Core.MapGenerator; +using Xunit; + +namespace Jackal.Tests2.TileTests; + +public class CannabisTests +{ + [Fact] + public void OneCannabis_GetAvailableMoves_ReturnNearestMoves() + { + // Arrange + var cannabisOnlyMap = new OneTileMapGenerator(TileParams.Cannabis()); + var game = new TestGame(cannabisOnlyMap); + + // Act - высадка с корабля на хи-хи траву + game.Turn(); + var moves = game.GetAvailableMoves(); + + // Assert - доступно 4 хода на соседние клетки с хи-хи травы в месте высадки + Assert.Equal(4, moves.Count); + Assert.Equal(new TilePosition(2, 1), moves.First().From); + Assert.Equivalent(new List + { + new(1, 2), + new(2, 0), // свой корабль + new(2, 2), + new(3, 2) + }, + moves.Select(m => m.To) + ); + Assert.Equal(1, game.TurnNumber); + } +} \ No newline at end of file diff --git a/Jackal.Tests2/TileTests/CannonTests.cs b/Jackal.Tests2/TileTests/CannonTests.cs new file mode 100644 index 00000000..56aabb60 --- /dev/null +++ b/Jackal.Tests2/TileTests/CannonTests.cs @@ -0,0 +1,177 @@ +using System.Collections.Generic; +using System.Linq; +using Jackal.Core.Domain; +using Jackal.Core.MapGenerator; +using Xunit; + +namespace Jackal.Tests2.TileTests; + +public class CannonTests +{ + [Fact] + public void OneCannonUp_GetAvailableMoves_ReturnOnlyWaterMoves() + { + // Arrange + var cannonOnlyMap = new OneTileMapGenerator(TileParams.Cannon()); + var game = new TestGame(cannonOnlyMap); + + // Act - высадка с корабля на пушку + game.Turn(); + + var moves = game.GetAvailableMoves(); + + // Assert - следующий ход, оказываемся на верху в воде + // доступно передвижение только по воде + Assert.Equal(4, moves.Count); + Assert.Equal(new TilePosition(2, 4), moves.First().From); + Assert.Equivalent(new List + { + new(1, 4), // влево + new(1, 3), // влево вниз + new(3, 4), // вправо + new(3, 3), // вправо вниз + }, + moves.Select(m => m.To) + ); + Assert.Equal(1, game.TurnNumber); + } + + [Fact] + public void OneCannonUp_MoveOnEnemyShipByCannon_ReturnDeadOwnPirate() + { + // Arrange + var cannonOnlyMap = new OneTileMapGenerator(TileParams.Cannon()); + var game = new TestGame(cannonOnlyMap); + + // добавляем пирата противника на корабль противника в место куда прилетает наш пират на пушке + game.AddEnemyTeamAndPirate(new TilePosition(2, 4)); + + // Act - высадка с корабля на пушку + game.Turn(); + + // Assert - наш пират помер + Assert.NotNull(game.Board.DeadPirates); + Assert.Single(game.Board.DeadPirates); + Assert.Equal(0, game.Board.DeadPirates.Single().TeamId); + Assert.Equal(1, game.TurnNumber); + } + + [Fact] + public void OneCannonRight_GetAvailableMoves_ReturnOnlyWaterMoves() + { + // Arrange + var cannonOnlyMap = new OneTileMapGenerator(TileParams.Cannon(DirectionType.Right)); + var game = new TestGame(cannonOnlyMap); + + // Act - высадка с корабля на пушку + game.Turn(); + + var moves = game.GetAvailableMoves(); + + // Assert - следующий ход, оказываемся справа в воде + // доступно передвижение только по воде + Assert.Equal(3, moves.Count); + Assert.Equal(new TilePosition(4, 1), moves.First().From); + Assert.Equivalent(new List + { + new(4, 2), // вверх + new(3, 1), // влево + new(3, 0), // влево вниз + + }, + moves.Select(m => m.To) + ); + Assert.Equal(1, game.TurnNumber); + } + + [Fact] + public void OneCannonRight_MoveOnEnemyPirateByCannon_ReturnDeadEnemyPirate() + { + // Arrange + var cannonOnlyMap = new OneTileMapGenerator(TileParams.Cannon(DirectionType.Right)); + var game = new TestGame(cannonOnlyMap); + + // добавляем пирата противника в воду в место куда прилетает наш пират на пушке + game.AddEnemyTeamAndPirate(new TilePosition(4, 1)); + + // Act - высадка с корабля на пушку + game.Turn(); + + // Assert - пират противника помер + Assert.NotNull(game.Board.DeadPirates); + Assert.Single(game.Board.DeadPirates); + Assert.Equal(1, game.Board.DeadPirates.Single().TeamId); + Assert.Equal(1, game.TurnNumber); + } + + [Fact] + public void OneCannonDown_GetAvailableMoves_ReturnSingleMoveFromShip() + { + // Arrange + var cannonOnlyMap = new OneTileMapGenerator(TileParams.Cannon(DirectionType.Down)); + var game = new TestGame(cannonOnlyMap); + + // Act - высадка с корабля на пушку + game.Turn(); + + var moves = game.GetAvailableMoves(); + + // Assert - следующий ход, оказываемся на своем корабле + // доступнен один ход - высадка на открытую пушку + Assert.Single(moves); + Assert.Equal(new TilePosition(2, 0), moves.Single().From); + Assert.Equal(new TilePosition(2, 1), moves.Single().To); + Assert.Equal(1, game.TurnNumber); + } + + [Fact] + public void OneCannonDownRepeatMove_GetAvailableMoves_ReturnSingleMoveFromShip() + { + // Arrange + var cannonOnlyMap = new OneTileMapGenerator(TileParams.Cannon(DirectionType.Down)); + var game = new TestGame(cannonOnlyMap); + + // Act - высадка с корабля на пушку + game.Turn(); + + // высадка с корабля на открытую пушку + game.Turn(); + + var moves = game.GetAvailableMoves(); + + // Assert - следующий ход, оказываемся на своем корабле + // доступнен один ход - высадка на открытую пушку + Assert.Single(moves); + Assert.Equal(new TilePosition(2, 0), moves.Single().From); + Assert.Equal(new TilePosition(2, 1), moves.Single().To); + Assert.Equal(2, game.TurnNumber); + } + + [Fact] + public void OneCannonLeft_GetAvailableMoves_ReturnOnlyWaterMoves() + { + // Arrange + var cannonOnlyMap = new OneTileMapGenerator(TileParams.Cannon(DirectionType.Left)); + + var game = new TestGame(cannonOnlyMap); + + // Act - высадка с корабля на пушку + game.Turn(); + + var moves = game.GetAvailableMoves(); + + // Assert - следующий ход, оказываемся слева в воде + // доступно передвижение только по воде + Assert.Equal(3, moves.Count); + Assert.Equal(new TilePosition(0, 1), moves.First().From); + Assert.Equivalent(new List + { + new(0, 2), // вверх + new(1, 1), // вправо + new(1, 0) // вправо вниз + }, + moves.Select(m => m.To) + ); + Assert.Equal(1, game.TurnNumber); + } +} \ No newline at end of file diff --git a/Jackal.Tests2/TileTests/CarambaTests.cs b/Jackal.Tests2/TileTests/CarambaTests.cs new file mode 100644 index 00000000..dba2ed9c --- /dev/null +++ b/Jackal.Tests2/TileTests/CarambaTests.cs @@ -0,0 +1,103 @@ +using System.Collections.Generic; +using System.Linq; +using Jackal.Core.Domain; +using Jackal.Core.MapGenerator; +using Xunit; + +namespace Jackal.Tests2.TileTests; + +public class CarambaTests +{ + [Fact] + public void OneCaramba_MoveOn_ReturnAllPiratesOnTheShips() + { + // Arrange + var carambaOnlyMap = new OneTileMapGenerator(TileParams.Caramba()); + var game = new TestGame(carambaOnlyMap); + + // добавляем пирата противника в воду, место выбрано случайно + game.AddEnemyTeamAndPirate(new TilePosition(4, 1)); + + // Act - высадка с корабля на карамбу + game.Turn(); + + // Assert - все пираты на своих кораблях + Assert.Equal(2, game.Board.Teams.Length); + Assert.Single(game.Board.Teams[0].Pirates); + Assert.Single(game.Board.Teams[1].Pirates); + + var ownPirate = game.Board.Teams[0].Pirates[0]; + Assert.Equal(game.Board.Teams[0].ShipPosition, ownPirate.Position.Position); + Assert.Equal(new Position(2, 0), ownPirate.Position.Position); + + var enemyPirate = game.Board.Teams[1].Pirates[0]; + Assert.Equal(game.Board.Teams[1].ShipPosition, enemyPirate.Position.Position); + Assert.Equal(new Position(2, 4), enemyPirate.Position.Position); + + Assert.Equal(1, game.TurnNumber); + } + + [Fact] + public void OneUsedCaramba_GetAvailableMoves_ReturnNearestMoves() + { + // Arrange + var carambaOnlyMap = new OneTileMapGenerator(TileParams.Caramba()); + var game = new TestGame(carambaOnlyMap); + + // Act - высадка с корабля на карамбу + game.Turn(); + + // высадка с корабля на использованную карамбу + game.Turn(); + + var moves = game.GetAvailableMoves(); + + // Assert - доступно 4 хода на соседние клетки с Бен Ганна в месте высадки + Assert.Equal(4, moves.Count); + Assert.Equal(new TilePosition(2, 1), moves.First().From); + Assert.Equivalent(new List + { + new(1, 2), + new(2, 0), // свой корабль + new(2, 2), + new(3, 2) + }, + moves.Select(m => m.To) + ); + Assert.Equal(2, game.TurnNumber); + } + + [Fact] + public void LighthouseThenSearchCaramba_Turn_ReturnAllPiratesOnTheShips() + { + // Arrange + var lighthouseCarambaLineMap = new TwoTileMapGenerator( + TileParams.Lighthouse(), TileParams.Caramba() + ); + var game = new TestGame(lighthouseCarambaLineMap); + + // добавляем пирата противника в воду, место выбрано случайно + game.AddEnemyTeamAndPirate(new TilePosition(4, 1)); + + // Act - высадка с корабля на маяк + game.Turn(); + + // открытие маяком карамбы + game.Turn(); + + // Assert - все пираты на своих кораблях + Assert.Equal(2, game.Board.Teams.Length); + Assert.Single(game.Board.Teams[0].Pirates); + Assert.Single(game.Board.Teams[1].Pirates); + + var ownPirate = game.Board.Teams[0].Pirates[0]; + Assert.Equal(game.Board.Teams[0].ShipPosition, ownPirate.Position.Position); + Assert.Equal(new Position(2, 0), ownPirate.Position.Position); + + var enemyPirate = game.Board.Teams[1].Pirates[0]; + Assert.Equal(game.Board.Teams[1].ShipPosition, enemyPirate.Position.Position); + Assert.Equal(new Position(2, 4), enemyPirate.Position.Position); + + Assert.Equal(0, game.TurnNumber); + } +} \ No newline at end of file diff --git a/Jackal.Tests2/TileTests/HoleTests.cs b/Jackal.Tests2/TileTests/HoleTests.cs new file mode 100644 index 00000000..daf3bad3 --- /dev/null +++ b/Jackal.Tests2/TileTests/HoleTests.cs @@ -0,0 +1,185 @@ +using System.Collections.Generic; +using System.Linq; +using Jackal.Core.Domain; +using Jackal.Core.MapGenerator; +using Xunit; + +namespace Jackal.Tests2.TileTests; + +public class HoleTests +{ + [Fact] + public void OneHole_GetAvailableMoves_ReturnNoAvailableMoves() + { + // Arrange + var holeOnlyMap = new OneTileMapGenerator( + TileParams.Hole() + ); + var game = new TestGame(holeOnlyMap); + + // Act - высадка с корабля на дыру + game.Turn(); + + var moves = game.GetAvailableMoves(); + + // Assert - не доступно ни одного хода, т.к. застряли в дыре + Assert.Empty(moves); + Assert.Equal(1, game.TurnNumber); + } + + [Fact] + public void EmptyThenHoleThenNextPirateEmptyAndSameHole_GetAvailableMoves_ReturnNoAvailableMoves() + { + // Arrange + var emptyHoleLineMap = new TwoTileMapGenerator( + TileParams.Empty(), + TileParams.Hole() + ); + const int mapSize = 5; + const int piratesPerPlayer = 2; + var game = new TestGame(emptyHoleLineMap, mapSize, piratesPerPlayer); + + // Act - высадка с корабля на пустое поле + game.Turn(); + + // выбираем ход - вперед на дыру + game.SetMoveAndTurn(2, 2); + + // высадка с корабля вторым пиратом на пустое поле + game.Turn(); + + // выбираем ход - вперед на дыру + game.SetMoveAndTurn(2, 2); + + var moves = game.GetAvailableMoves(); + + // Assert - не доступно ни одного хода, т.к. оба пирата застряли в дыре + Assert.Empty(moves); + Assert.Equal(4, game.TurnNumber); + } + + [Fact] + public void EmptyThenHoleThenNextPirateEmptyAndOtherHole_GetAvailableMoves_ReturnAvailableMoves() + { + // Arrange + var emptyHoleLineMap = new TwoTileMapGenerator( + TileParams.Empty(), + TileParams.Hole() + ); + const int mapSize = 5; + const int piratesPerPlayer = 2; + var game = new TestGame(emptyHoleLineMap, mapSize, piratesPerPlayer); + + // Act - высадка с корабля на пустое поле + game.Turn(); + + // выбираем ход - вперед на дыру + game.SetMoveAndTurn(2, 2); + + // высадка с корабля вторым пиратом на пустое поле + game.Turn(); + + // выбираем ход - вперед и вправо на другую дыру + game.SetMoveAndTurn(3, 2); + + var moves = game.GetAvailableMoves(); + + // Assert - доступны новые ходы + Assert.True(moves.Count > 0); + Assert.Equal(4, game.TurnNumber); + } + + [Fact] + public void MoveOnHoleWhenEnemyPirateInOtherHole_GetAvailableMoves_ReturnNearestMoves() + { + // Arrange + var emptyHoleLineMap = new TwoTileMapGenerator( + TileParams.Empty(), + TileParams.Hole() + ); + const int mapSize = 5; + const int piratesPerPlayer = 2; + var game = new TestGame(emptyHoleLineMap, mapSize, piratesPerPlayer); + + // Act - высадка с корабля на пустое поле + game.Turn(); + + // выбираем ход - вперед на дыру + game.SetMoveAndTurn(2, 2); + + // высадка с корабля вторым пиратом на пустое поле + game.Turn(); + + // выбираем ход - вперед и вправо на другую дыру + game.SetMoveAndTurn(3, 2); + + // убираем одного своего пирата + var ownPirate = game.Board.Teams[0].Pirates[0]; + game.KillPirate(ownPirate); + + // выбираем ход - назад на пустое поле + game.SetMoveAndTurn(2, 1); + + // добавляем пирата противника вперед и вправо на дыру + game.AddEnemyTeamAndPirate(new TilePosition(3, 2)); + + // выбираем ход - вперед на дыру + game.SetMoveAndTurn(2, 2); + + var moves = game.GetAvailableMoves(); + + // Assert - в дыру не провалились, доступны ходы на ближайшие клетки + Assert.Equal(4, moves.Count); + Assert.Equal(new TilePosition(2, 2), moves.First().From); + Assert.Equivalent(new List + { + new(1, 2), // влево + new(2, 1), // назад - пустая клетка + new(2, 3), // вперед + new(3, 2) // вправо + }, + moves.Select(m => m.To) + ); + Assert.Equal(6, game.TurnNumber); + } + + [Fact] + public void WaitMoveOnHole_Turn_ReturnAvailableMovesFromOtherHole() + { + // Arrange + var arrowHoleLineMap = new TwoTileMapGenerator( + TileParams.FourArrowsDiagonal(), + TileParams.Hole() + ); + const int mapSize = 5; + const int piratesPerPlayer = 2; + var game = new TestGame(arrowHoleLineMap, mapSize, piratesPerPlayer); + + // Act - высадка с корабля на пустое стрелку + game.Turn(); + + // выбираем ход - вперед и влево на дыру + game.SetMoveAndTurn(1, 2); + + // выбираем ход - вперед и вправо на другую дыру + game.SetMoveAndTurn(3, 2); + + // убираем одного своего пирата + var ownPirate = game.Board.Teams[0].Pirates[0]; + game.KillPirate(ownPirate); + + // ходим на ту же дыру где стоим + var moves = game.GetAvailableMoves(); + var waitMove = moves.Single(x => x.From == x.To); + game.SetMoveAndTurn(waitMove.From, waitMove.To); + + // выбираем единственный выход на другой дыре + game.Turn(); + + moves = game.GetAvailableMoves(); + + // Assert - оказываемся на другой дыре + Assert.NotEqual(waitMove.From, moves.First().From); + Assert.Equal(3, game.TurnNumber); + } +} \ No newline at end of file diff --git a/Jackal.Tests2/TileTests/IceTests.cs b/Jackal.Tests2/TileTests/IceTests.cs new file mode 100644 index 00000000..a6d55a6c --- /dev/null +++ b/Jackal.Tests2/TileTests/IceTests.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Jackal.Core.Domain; +using Jackal.Core.MapGenerator; +using Xunit; + +namespace Jackal.Tests2.TileTests; + +public class IceTests +{ + [Fact] + public void OneIce_MovingToWater() + { + // Arrange + var map = new TwoTileMapGenerator(TileParams.Ice(), TileParams.Empty()); + var game = new TestGame(map, 7); + + Assert.Single(game.Board.AllPirates); + + /* + Схема движения пирата: + P22 < P32 + v ^ + P21(лёд) P31(лёд) + v ^ + P20(вода) > P30(корабль) + */ + + var p30 = new Position(3, 0); + var p31 = new Position(3, 1); + var p32 = new Position(3, 2); + var p20 = new Position(2, 0); + var p21 = new Position(2, 1); + var p22 = new Position(2, 2); + + List GetMovesIndexesToPosition(Position position) + { + var moves = game.GetAvailableMoves(); + var movesIndexes = moves.Select((move, index) => new { move, index }) + .Where(x => x.move.To.Position == position) + .Select(x => x.index) + .ToList(); + return movesIndexes; + } + + int GetMoveIndexToPosition(Position position) + { + var indexes = GetMovesIndexesToPosition(position); + Assert.NotEmpty(indexes); + return indexes.First(); + } + + void AssertPiratePosition(Position position) + { + Assert.Equal(new TilePosition(position), game.Board.AllPirates[0].Position); + } + + // Высадка с корабля на лёд + game.Turn(GetMoveIndexToPosition(p31)); + AssertPiratePosition(p31); + + // Скольжение на льду вверх + game.Turn(); + AssertPiratePosition( p32); + + // Ход влево + game.Turn(GetMoveIndexToPosition(p22)); + AssertPiratePosition(p22); + + // Ход вниз, на лед + game.Turn(GetMoveIndexToPosition(p21)); + AssertPiratePosition(p21); + + // Скольжение на льду вниз, в воду + game.Turn(); + AssertPiratePosition(p20); + + // Ход вправо, на корабль + game.Turn(GetMoveIndexToPosition(p30)); + AssertPiratePosition(p30); + + // Высадка с корабля, через лёд + game.Turn(GetMoveIndexToPosition(p32)); + AssertPiratePosition(p32); + + // Ход влево + game.Turn(GetMoveIndexToPosition(p22)); + AssertPiratePosition(p22); + + // Теперь хода в воду не должно быть в спике доступных + var movesIndexes = GetMovesIndexesToPosition(p20); + Assert.Empty(movesIndexes); + } +} \ No newline at end of file diff --git a/Jackal.Tests2/TileTests/JungleTests.cs b/Jackal.Tests2/TileTests/JungleTests.cs new file mode 100644 index 00000000..4a2e9766 --- /dev/null +++ b/Jackal.Tests2/TileTests/JungleTests.cs @@ -0,0 +1,102 @@ +using System.Collections.Generic; +using System.Linq; +using Jackal.Core.Domain; +using Jackal.Core.MapGenerator; +using Xunit; + +namespace Jackal.Tests2.TileTests; + +public class JungleTests +{ + [Fact] + public void OneJungle_GetAvailableMoves_ReturnNearestMoves() + { + // Arrange + var jungleOnlyMap = new OneTileMapGenerator( + TileParams.Jungle() + ); + var game = new TestGame(jungleOnlyMap); + + // Act - высадка с корабля на джунгли + game.Turn(); + + var moves = game.GetAvailableMoves(); + + // Assert - доступно 4 хода на соседние клетки с клетки джунгли в месте высадки + Assert.Equal(4, moves.Count); + Assert.Equal(new TilePosition(2, 1), moves.First().From); + Assert.Equivalent(new List + { + new(1, 2), + new(2, 0), // свой корабль + new(2, 2), + new(3, 2) + }, + moves.Select(m => m.To) + ); + Assert.Equal(1, game.TurnNumber); + } + + [Fact] + public void JungleThenEmptyWithCoin_GetAvailableMoves_ReturnAllMoveWithoutCoin() + { + // Arrange + var jungleEmptyLineMap = new TwoTileMapGenerator( + TileParams.Jungle(), + TileParams.Empty() + ); + var game = new TestGame(jungleEmptyLineMap); + + // Act - высадка с корабля на джунгли + game.Turn(); + + // выбираем ход - вперед на пустую клетку + game.SetMoveAndTurn(2,2); + + // добавляем монету - на текущую позицию нашего пирата + game.AddCoin(new TilePosition(2, 2)); + + var moves = game.GetAvailableMoves(); + + // Assert - доступно 4 хода без монеты на соседние клетки из цента карты + Assert.Equal(4, moves.Count); + Assert.True(moves.All(m => !m.WithCoin)); + Assert.Equal(2, game.TurnNumber); + } + + [Fact] + public void JungleThenEmptyThenJungleAgainWithEnemy_MoveOnEnemyTurn_ReturnAllPiratesInOneJungle() + { + // Arrange + var jungleEmptyLineMap = new TwoTileMapGenerator( + TileParams.Jungle(), + TileParams.Empty() + ); + var game = new TestGame(jungleEmptyLineMap); + + // Act - высадка с корабля на джунгли + game.Turn(); + + // выбираем ход - вперед на пустую клетку + game.SetMoveAndTurn(2,2); + + // добавляем пирата противника - в месте высадки нашего пирата на джунгли + game.AddEnemyTeamAndPirate(new TilePosition(2, 1)); + + // выбираем ход - обратно в джунгли в место нашей высадки + game.SetMoveAndTurn(2,1); + + // Assert - все пираты стоят на одной клетке джунгли + Assert.Equal(2, game.Board.Teams.Length); + Assert.Single(game.Board.Teams[0].Pirates); + Assert.Single(game.Board.Teams[1].Pirates); + + var ownPirate = game.Board.Teams[0].Pirates[0]; + Assert.Equal(new TilePosition(2, 1), ownPirate.Position); + + var enemyPirate = game.Board.Teams[1].Pirates[0]; + Assert.Equal(new TilePosition(2, 1), enemyPirate.Position); + + Assert.Equal(3, game.TurnNumber); + } +} \ No newline at end of file diff --git a/Jackal.Tests2/TileTests/LighthouseTests.cs b/Jackal.Tests2/TileTests/LighthouseTests.cs new file mode 100644 index 00000000..a5df4635 --- /dev/null +++ b/Jackal.Tests2/TileTests/LighthouseTests.cs @@ -0,0 +1,153 @@ +using System.Collections.Generic; +using System.Linq; +using Jackal.Core.Domain; +using Jackal.Core.MapGenerator; +using Xunit; + +namespace Jackal.Tests2.TileTests; + +public class LighthouseTests +{ + [Fact] + public void OneLighthouse_GetAvailableMoves_ReturnAllUnknownTiles() + { + // Arrange + var lighthouseOnlyMap = new OneTileMapGenerator(TileParams.Lighthouse()); + var game = new TestGame(lighthouseOnlyMap); + + // Act - высадка с корабля на маяк + game.Turn(); + var moves = game.GetAvailableMoves(); + + // Assert - все оставшиеся неизвестные клетки: поле 5 клеток минус открытый маяк + Assert.Equal(4, moves.Count); + Assert.Equal(new TilePosition(2, 1), moves.First().From); + Assert.Equivalent(new List + { + new(1, 2), + new(2, 2), + new(2, 3), + new(3, 2) + }, + moves.Select(m => m.To) + ); + + // тип хода - открытие клетки с маяка + Assert.True(moves.All(m => m.WithLighthouse)); + Assert.Equal(0, game.TurnNumber); + } + + [Fact] + public void LighthouseThenSearch4Empty_Turn_ReturnGameOver() + { + // Arrange + var lighthouseEmptyLineMap = new TwoTileMapGenerator( + TileParams.Lighthouse(), + TileParams.Empty() + ); + var game = new TestGame(lighthouseEmptyLineMap); + + // Act - высадка с корабля на маяк + game.Turn(); + + // по очереди смотрим неизвестные клетки: все пустые поля + game.Turn(); + game.Turn(); + game.Turn(); + game.Turn(); + + // Assert - все поле открыто, золота нет = конец игры + Assert.True(game.IsGameOver); + Assert.Equal(1, game.TurnNumber); + } + + [Fact] + public void LighthouseThenSearch3LighthouseAndCoin_GetAvailableMoves_ReturnNearestMoves() + { + // Arrange + var lighthouseCoinLineMap = new ThreeTileMapGenerator( + TileParams.Lighthouse(), + TileParams.Lighthouse(), + TileParams.Coin() + ); + var game = new TestGame(lighthouseCoinLineMap); + + // Act - высадка с корабля на маяк + game.Turn(); + + // по очереди смотрим неизвестные клетки: 3 маяка и 1 сундук + game.Turn(); + game.Turn(); + game.Turn(); + game.Turn(); + + var moves = game.GetAvailableMoves(); + + // Assert - доступно 4 хода на соседние клетки с маяка в месте высадки + Assert.Equal(4, moves.Count); + Assert.Equal(new TilePosition(2, 1), moves.First().From); + Assert.Equivalent(new List + { + new(1, 2), // соседний маяк + new(2, 0), // свой корабль + new(2, 2), // соседний маяк + new(3, 2) // соседний маяк + }, + moves.Select(m => m.To) + ); + + // тип хода - обычный + Assert.True(moves.All(m => m.Type == MoveType.Usual)); + + // Все поле открыто, золото есть = игра продолжается + Assert.False(game.IsGameOver); + Assert.Equal(1, game.TurnNumber); + } + + [Fact] + public void LighthouseThenSearch2LighthouseThenSearch10Crocodile_GetAvailableMoves_ReturnNearestMoves() + { + // Arrange + var lighthouseCrocodileLineMap = new TwoTileMapGenerator( + TileParams.Lighthouse(), + TileParams.Crocodile() + ); + var mapSize = 7; // карта большая - возможно движение корабля + var game = new TestGame(lighthouseCrocodileLineMap, mapSize); + + // Act - высадка с корабля вперед на маяк + game.SetMoveAndTurn(3, 1); + + // выбираем ход просветку - влево на маяк 1 + game.SetMoveAndTurn(2, 1); + + // выбираем ход просветку - вправо на маяк 2 + game.SetMoveAndTurn(4, 1); + + // по очереди смотрим неизвестные клетки с крокодилами, т.к. только они остались + game.Turn(); // 1 + game.Turn(); // 2 + game.Turn(); // 3 + game.Turn(); // 4 + game.Turn(); // 5 + game.Turn(); // 6 + game.Turn(); // 7 + game.Turn(); // 8 + game.Turn(); // 9 + game.Turn(); // 10 + // в результате с 3-ех маяков посмотрели 12 клеток = 2 маяка + 10 крокодилов + + var moves = game.GetAvailableMoves(); + + // Assert - доступно >= 3 хода с маяка в месте высадки, т.к. впереди крокодилы или неизвестные клетки + Assert.True(moves.Count >= 3); + Assert.Equal(new TilePosition(3, 1), moves.First().From); + Assert.Contains(new TilePosition(2, 1), moves.Select(m => m.To)); // левый маяк + Assert.Contains(new TilePosition(3, 0), moves.Select(m => m.To)); // свой корабль + Assert.Contains(new TilePosition(4, 1), moves.Select(m => m.To)); // правый маяк + + // тип хода - обычный + Assert.True(moves.All(m => m.Type == MoveType.Usual)); + Assert.Equal(1, game.TurnNumber); + } +} \ No newline at end of file diff --git a/Jackal.Tests2/TileTests/RespawnFortTests.cs b/Jackal.Tests2/TileTests/RespawnFortTests.cs new file mode 100644 index 00000000..b05ee7dd --- /dev/null +++ b/Jackal.Tests2/TileTests/RespawnFortTests.cs @@ -0,0 +1,255 @@ +using System.Collections.Generic; +using System.Linq; +using Jackal.Core; +using Jackal.Core.Domain; +using Jackal.Core.MapGenerator; +using Xunit; + +namespace Jackal.Tests2.TileTests; + +public class RespawnFortTests +{ + [Fact] + public void OneRespawnFort_GetAvailableMoves_ReturnNearestMovesAndMoveWithRespawn() + { + // Arrange + var respawnFortOnlyMap = new OneTileMapGenerator(TileParams.RespawnFort()); + var game = new TestGame(respawnFortOnlyMap); + + // Act - высадка с корабля на воскрешающий форт + game.Turn(); + var moves = game.GetAvailableMoves(); + + // Assert - 3 поля рядом + воскрешающий ход на месте + свой корабль + Assert.Equal(5, moves.Count); + Assert.Equal(new TilePosition(2, 1), moves.First().From); + Assert.Equivalent(new List + { + new(1, 2), + new(2, 0), // свой корабль + new(2, 1), // воскрешающий ход на месте + new(2, 2), + new(3, 2) + }, + moves.Select(m => m.To) + ); + Assert.Equal(1, game.TurnNumber); + } + + [Fact] + public void OneRespawnFort_MoveWithRespawn_ReturnTwoOwnPirates() + { + // Arrange + var respawnFortOnlyMap = new OneTileMapGenerator(TileParams.RespawnFort()); + var game = new TestGame(respawnFortOnlyMap); + + // Act - высадка с корабля на воскрешающий форт + game.Turn(); + + // воскрешающий ход + game.SetMoveAndTurn(2, 1); + + // Assert - появилось 2 наших пирата + Assert.Equal(2, game.Board.AllPirates.Count); + Assert.Equal(0, game.Board.AllPirates[0].TeamId); + Assert.Equal(0, game.Board.AllPirates[1].TeamId); + Assert.Equal(2, game.TurnNumber); + } + + [Fact] + public void OneRespawnFort_DoubleMoveWithRespawn_ReturnThreeOwnPirates() + { + // Arrange + var respawnFortOnlyMap = new OneTileMapGenerator(TileParams.RespawnFort()); + var game = new TestGame(respawnFortOnlyMap); + + // Act - высадка с корабля на воскрешающий форт + game.Turn(); + + // воскрешающий ход + game.SetMoveAndTurn(2, 1); + + // воскрешающий ход + game.SetMoveAndTurn(2, 1); + + // Assert - появилось 2 наших пирата + Assert.Equal(3, game.Board.AllPirates.Count); + Assert.Equal(0, game.Board.AllPirates[0].TeamId); + Assert.Equal(0, game.Board.AllPirates[1].TeamId); + Assert.Equal(0, game.Board.AllPirates[2].TeamId); + Assert.Equal(3, game.TurnNumber); + } + + [Fact] + public void OneRespawnFort_DoubleMoveWithRespawn_ReturnNearestMovesAndNoMoveWithRespawn() + { + // Arrange + var respawnFortOnlyMap = new OneTileMapGenerator(TileParams.RespawnFort()); + var game = new TestGame(respawnFortOnlyMap); + + // Act - высадка с корабля на воскрешающий форт + game.Turn(); + + // воскрешающий ход + game.SetMoveAndTurn(2, 1); + + // воскрешающий ход + game.SetMoveAndTurn(2, 1); + + var moves = game.GetAvailableMoves(); + + // Assert - 3 поля рядом + свой корабль + Assert.Equal(4, moves.Count); + Assert.Equal(new TilePosition(2, 1), moves.First().From); + Assert.Equivalent(new List + { + new(1, 2), + new(2, 0), // свой корабль + new(2, 2), + new(3, 2) + }, + moves.Select(m => m.To) + ); + Assert.Equal(3, game.TurnNumber); + } + + [Fact] + public void OneRespawnFortWithBenGunn_GetAvailableMoves_ReturnNearestMovesAndNoMoveWithRespawn() + { + // Arrange + const int piratesPerPlayer = 0; + var respawnFortOnlyMap = new OneTileMapGenerator(TileParams.RespawnFort()); + var game = new TestGame(respawnFortOnlyMap, 5, piratesPerPlayer); + + // добавляем Бена Ганна на свой корабль + game.AddOwnTeamPirate(new TilePosition(2, 0), PirateType.BenGunn); + + // Act - высадка с корабля на воскрешающий форт + game.Turn(); + + var moves = game.GetAvailableMoves(); + + // Assert - 3 поля рядом + свой корабль + Assert.Equal(4, moves.Count); + Assert.Equal(new TilePosition(2, 1), moves.First().From); + Assert.Equivalent(new List + { + new(1, 2), + new(2, 0), // свой корабль + new(2, 2), + new(3, 2) + }, + moves.Select(m => m.To) + ); + Assert.Equal(1, game.TurnNumber); + } + + [Fact] + public void OneRespawnFortWithBenGunnAndUsualPirate_DoubleMoveWithRespawn_ReturnFourOwnPirates() + { + // Arrange + var respawnFortOnlyMap = new OneTileMapGenerator(TileParams.RespawnFort()); + var game = new TestGame(respawnFortOnlyMap); + + // добавляем Бена Ганна на свой корабль + game.AddOwnTeamPirate(new TilePosition(2, 0), PirateType.BenGunn); + + // Act - высадка с корабля на воскрешающий форт + game.Turn(); + + // воскрешающий ход или высадка с корабля Бена/Пирата + game.SetMoveAndTurn(2, 1); + + // воскрешающий ход или высадка с корабля Бена + game.SetMoveAndTurn(2, 1); + + // воскрешающий ход или высадка с корабля Бена + game.SetMoveAndTurn(2, 1); + + // Assert - появилось 3 наших пирата и Бен Ганн + Assert.Equal(4, game.Board.AllPirates.Count); + Assert.Equal(0, game.Board.AllPirates[0].TeamId); + Assert.Equal(0, game.Board.AllPirates[1].TeamId); + Assert.Equal(0, game.Board.AllPirates[2].TeamId); + Assert.Equal(0, game.Board.AllPirates[3].TeamId); + Assert.Equal(4, game.TurnNumber); + } + + [Fact] + public void ArrowUpOnRespawnFort_GetAvailableMoves_ReturnNearestMovesAndMoveWithRespawn() + { + // Arrange + var arrowUpOnRespawnFortLineMap = new TwoTileMapGenerator( + TileParams.OneArrowUp(), + TileParams.RespawnFort() + ); + var game = new TestGame(arrowUpOnRespawnFortLineMap); + + // Act - высадка с корабля на стрелку вперед + game.Turn(); + + // автоматом идем вперед на воскрешающий форт + game.Turn(); + + var moves = game.GetAvailableMoves(); + + // Assert - 3 поля рядом + воскрешающий ход на месте + Assert.Equal(4, moves.Count); + Assert.Equal(new TilePosition(2, 2), moves.First().From); + Assert.Equivalent(new List + { + new(1, 2), // влево + new(2, 2), // воскрешающий ход на месте + new(2, 3), // вперед + new(3, 2) // вправо + }, + moves.Select(m => m.To) + ); + Assert.Single(moves.Where(m => m.WithRespawn)); + Assert.Equal(1, game.TurnNumber); + } + + [Fact] + public void ArrowUpOnRespawnFort_GetAvailableMoves_ReturnNearestMovesAndWaitMove() + { + // Arrange + const int mapSize = 5; + const int piratesPerPlayer = 3; + + var arrowUpOnRespawnFortLineMap = new TwoTileMapGenerator( + TileParams.OneArrowUp(), + TileParams.RespawnFort() + ); + var game = new TestGame(arrowUpOnRespawnFortLineMap, mapSize, piratesPerPlayer); + + // Act - высадка с корабля на стрелку вперед + game.Turn(); + + // автоматом идем вперед на воскрешающий форт + game.Turn(); + + var moves = game.GetAvailableMoves(); + + // Assert - 3 поля рядом + ход на месте через стрелку + Assert.Equal(5, moves.Count); + Assert.Equivalent(new List + { + new (2, 0), // пираты на корабле + new(2, 2) // пират на форте + }, + moves.Select(m => m.From) + ); + Assert.Equivalent(new List + { + new (2, 2), // высадка с корабля + new(1, 2), // влево + new(2, 2), // ход на месте через стрелку + new(2, 3), // вперед + new(3, 2) // вправо + }, + moves.Select(m => m.To) + ); + Assert.Empty(moves.Where(m => m.WithRespawn)); + Assert.Equal(1, game.TurnNumber); + } +} \ No newline at end of file diff --git a/Jackal.Tests2/TileTests/RumBarrelTests.cs b/Jackal.Tests2/TileTests/RumBarrelTests.cs new file mode 100644 index 00000000..bfc674c6 --- /dev/null +++ b/Jackal.Tests2/TileTests/RumBarrelTests.cs @@ -0,0 +1,93 @@ +using System.Collections.Generic; +using System.Linq; +using Jackal.Core.Domain; +using Jackal.Core.MapGenerator; +using Xunit; + +namespace Jackal.Tests2.TileTests; + +public class RumBarrelTests +{ + [Fact] + public void OneRumBarrel_GetAvailableMoves_ReturnNoAvailableMoves() + { + // Arrange + var rumBarrelOnlyMap = new OneTileMapGenerator( + TileParams.RumBarrel() + ); + var game = new TestGame(rumBarrelOnlyMap); + + // Act - высадка с корабля на бочку с ромом + game.Turn(); + + var moves = game.GetAvailableMoves(); + + // Assert - не доступно ни одного хода, т.к. напились на бочке с ромом + Assert.Empty(moves); + Assert.Single(game.Board.Teams); + Assert.Single(game.Board.Teams[0].Pirates); + + var ownPirate = game.Board.Teams[0].Pirates[0]; + Assert.False(ownPirate.IsActive); + Assert.True(ownPirate.IsDrunk); + + Assert.Equal(1, game.TurnNumber); + } + + [Fact] + public void OneRumBarrelWaitTurn_GetAvailableMoves_ReturnNearestMoves() + { + // Arrange + var rumBarrelOnlyMap = new OneTileMapGenerator( + TileParams.RumBarrel() + ); + var game = new TestGame(rumBarrelOnlyMap); + + // Act - высадка с корабля на бочку с ромом + game.Turn(); + + // ждем следующий ход + game.Turn(); + + var moves = game.GetAvailableMoves(); + + // Assert - доступно 4 хода на соседние клетки с клетки бочки с ромом в месте высадки + Assert.Equal(4, moves.Count); + Assert.Equal(new TilePosition(2, 1), moves.First().From); + Assert.Equivalent(new List + { + new(1, 2), + new(2, 0), // свой корабль + new(2, 2), + new(3, 2) + }, + moves.Select(m => m.To) + ); + Assert.Equal(2, game.TurnNumber); + } + + [Fact] + public void RumBarrelWaitTurnThenNextRumBarrel_GetAvailableMoves_ReturnNoAvailableMoves() + { + // Arrange + var rumBarrelOnlyMap = new OneTileMapGenerator( + TileParams.RumBarrel() + ); + var game = new TestGame(rumBarrelOnlyMap); + + // Act - высадка с корабля на бочку с ромом + game.Turn(); + + // ждем следующий ход + game.Turn(); + + // выбираем ход - вперед на следующую бочку с ромом + game.SetMoveAndTurn(2, 2); + + var moves = game.GetAvailableMoves(); + + // Assert - не доступно ни одного хода, т.к. напились на бочке с ромом + Assert.Empty(moves); + Assert.Equal(3, game.TurnNumber); + } +} \ No newline at end of file diff --git a/Jackal.Tests2/TileTests/RumBottleTests.cs b/Jackal.Tests2/TileTests/RumBottleTests.cs new file mode 100644 index 00000000..073b48da --- /dev/null +++ b/Jackal.Tests2/TileTests/RumBottleTests.cs @@ -0,0 +1,165 @@ +using System.Collections.Generic; +using System.Linq; +using Jackal.Core.Domain; +using Jackal.Core.MapGenerator; +using Xunit; + +namespace Jackal.Tests2.TileTests; + +public class RumBottleTests +{ + [Fact] + public void OneRum_GetAvailableMoves_ReturnNearestMovesAndOneRumBottel() + { + // Arrange + var oneRumOnlyMap = new OneTileMapGenerator( + TileParams.RumBottle() + ); + var game = new TestGame(oneRumOnlyMap); + + // Act - высадка с корабля на бутылку с ромом + game.Turn(); + var moves = game.GetAvailableMoves(); + + // Assert - доступно 4 хода на соседние клетки в месте высадки и одна бутылка с ромом + Assert.Equal(4, moves.Count); + Assert.Equal(1, game.Board.Teams.Single().RumBottles); + Assert.Equal(1, game.TurnNumber); + } + + [Fact] + public void TwoRum_GetAvailableMoves_ReturnNearestMovesAndTwoRumBottels() + { + // Arrange + var twoRumOnlyMap = new OneTileMapGenerator( + TileParams.RumBottle(2) + ); + var game = new TestGame(twoRumOnlyMap); + + // Act - высадка с корабля на две бутылки с ромом + game.Turn(); + var moves = game.GetAvailableMoves(); + + // Assert - доступно 4 хода на соседние клетки в месте высадки и две бутылки с ромом + Assert.Equal(4, moves.Count); + Assert.Equal(2, game.Board.Teams.Single().RumBottles); + Assert.Equal(1, game.TurnNumber); + } + + [Fact] + public void ThreeRum_GetAvailableMoves_ReturnNearestMovesAndThreeRumBottels() + { + // Arrange + var threeRumOnlyMap = new OneTileMapGenerator( + TileParams.RumBottle(3) + ); + var game = new TestGame(threeRumOnlyMap); + + // Act - высадка с корабля на три бутылки с ромом + game.Turn(); + var moves = game.GetAvailableMoves(); + + // Assert - доступно 4 хода на соседние клетки в месте высадки и три бутылки с ромом + Assert.Equal(4, moves.Count); + Assert.Equal(3, game.Board.Teams.Single().RumBottles); + Assert.Equal(1, game.TurnNumber); + } + + [Fact] + public void RumBottleThenTrap_GetAvailableMoves_ReturnNearestMovesWithRumBottle() + { + // Arrange + var rumBottleTrapLineMap = new TwoTileMapGenerator( + TileParams.RumBottle(), + TileParams.Trap() + ); + var game = new TestGame(rumBottleTrapLineMap); + + // Act - высадка с корабля на бутылку с ромом + game.Turn(); + + // выбираем ход - вперед на ловушку + game.SetMoveAndTurn(2, 2); + + var moves = game.GetAvailableMoves(); + + // Assert - доступно 4 хода из ловушки за бутылку рома на соседние клетки + Assert.Equal(4, moves.Count); + Assert.Equal(new TilePosition(2, 2), moves.First().From); + Assert.Equivalent(new List + { + new(1, 2), + new(2, 1), + new(2, 3), + new(3, 2) + }, + moves.Select(m => m.To) + ); + Assert.Equal(4, moves.Count(m => m.WithRumBottle)); + Assert.Equal(2, game.TurnNumber); + } + + [Fact] + public void RumBottleThenSpinning_GetAvailableMoves_ReturnNearestMovesWithRumBottleAndOneSpinningMove() + { + // Arrange + var rumBottleSpinningLineMap = new TwoTileMapGenerator( + TileParams.RumBottle(), + TileParams.SpinningMount() + ); + var game = new TestGame(rumBottleSpinningLineMap); + + // Act - высадка с корабля на бутылку с ромом + game.Turn(); + + // выбираем ход - вперед на гору + game.SetMoveAndTurn(2, 2); + + var moves = game.GetAvailableMoves(); + + // Assert - доступно 5 ходов: 4 из горы за бутылку рома на соседние клетки и 1 ход по горе + Assert.Equal(5, moves.Count); + Assert.Equal(new TilePosition(2, 2, 4), moves.First().From); + Assert.Equivalent(new List + { + new(1, 2), + new(2, 1), + new(2, 3), + new(3, 2), + new(2,2, 3) + }, + moves.Select(m => m.To) + ); + Assert.Equal(4, moves.Count(m => m.WithRumBottle)); + Assert.Equal(1, moves.Count(m => !m.WithRumBottle)); + Assert.Equal(2, game.TurnNumber); + } + + [Fact] + public void LighthouseThenSearchRumBottles_GetAvailableMoves_ReturnNearestMovesAndNoRumBottles() + { + // Arrange + var rumBottleTrapLineMap = new TwoTileMapGenerator( + TileParams.Lighthouse(), + TileParams.RumBottle() + ); + var game = new TestGame(rumBottleTrapLineMap); + + // Act - высадка с корабля на маяк + game.Turn(); + + // по очереди смотрим неизвестные клетки с бутылками + game.Turn(); // 1 + game.Turn(); // 2 + game.Turn(); // 3 + game.Turn(); // 4 + + var moves = game.GetAvailableMoves(); + + // Assert - доступно 4 хода на соседние клетки в месте высадки и 0 бутылок с ромом + Assert.Equal(4, moves.Count); + Assert.Equal(new TilePosition(2, 1), moves.First().From); + Assert.Equal(0, game.Board.Teams.Single().RumBottles); + Assert.Equal(1, game.TurnNumber); + } +} \ No newline at end of file diff --git a/Jackal.Tests2/TileTests/SpinningTests.cs b/Jackal.Tests2/TileTests/SpinningTests.cs new file mode 100644 index 00000000..6618d2a3 --- /dev/null +++ b/Jackal.Tests2/TileTests/SpinningTests.cs @@ -0,0 +1,284 @@ +using System.Collections.Generic; +using System.Linq; +using Jackal.Core.Domain; +using Jackal.Core.MapGenerator; +using Xunit; + +namespace Jackal.Tests2.TileTests; + +public class SpinningTests +{ + [Fact] + public void OneSpinning_GetAvailableMoves_ReturnSingleSpinningMove() + { + // Arrange + var spinningOnlyMap = new OneTileMapGenerator( + TileParams.SpinningForest() + ); + var game = new TestGame(spinningOnlyMap); + + // Act - высадка с корабля на лес + game.Turn(); // 1 ход + + var moves = game.GetAvailableMoves(); + + // Assert - доступен единственный ход на следующую клетку леса + Assert.Single(moves); + Assert.Equal(new TilePosition(2, 1, 1), moves.First().From); + Assert.Equal(new TilePosition(2, 1, 0), moves.First().To); + Assert.Equal(1, game.TurnNumber); + } + + [Fact] + public void OneSpinningFinish_GetAvailableMoves_ReturnNearestMoves() + { + // Arrange + var spinningOnlyMap = new OneTileMapGenerator( + TileParams.SpinningForest() + ); + var game = new TestGame(spinningOnlyMap); + + // Act - высадка с корабля на лес + game.Turn(); // 1 ход + + // доходим до конца клетки лес + game.Turn(); // 2 ход + + var moves = game.GetAvailableMoves(); + + // Assert - доступно 4 хода на соседние клетки с клетки лес в месте высадки + Assert.Equal(4, moves.Count); + Assert.Equal(new TilePosition(2, 1), moves.First().From); + Assert.Equivalent(new List + { + new(1, 2), + new(2, 0), // свой корабль + new(2, 2), + new(3, 2) + }, + moves.Select(m => m.To) + ); + Assert.Equal(2, game.TurnNumber); + } + + [Fact] + public void OneSpinningMaxFinish_GetAvailableMoves_ReturnNearestMoves() + { + // Arrange + var spinningOnlyMap = new OneTileMapGenerator( + TileParams.SpinningMount() + ); + var game = new TestGame(spinningOnlyMap); + + // Act - высадка с корабля на гору + game.Turn(); // 1 ход + + // доходим до конца клетки гора + game.Turn(); // 2 ход + game.Turn(); // 3 ход + game.Turn(); // 4 ход + game.Turn(); // 5 ход + + var moves = game.GetAvailableMoves(); + + // Assert - доступно 4 хода на соседние клетки с клетки гора в месте высадки + Assert.Equal(4, moves.Count); + Assert.Equal(new TilePosition(2, 1), moves.First().From); + Assert.Equivalent(new List + { + new(1, 2), + new(2, 0), // свой корабль + new(2, 2), + new(3, 2) + }, + moves.Select(m => m.To) + ); + Assert.Equal(5, game.TurnNumber); + } + + [Fact] + public void SpinningFinishThenCrocodile_GetAvailableMoves_ReturnSpinningStart() + { + // Arrange + var spinningCrocodileLineMap = new TwoTileMapGenerator( + TileParams.SpinningForest(), + TileParams.Crocodile() + ); + var game = new TestGame(spinningCrocodileLineMap); + + // Act - высадка с корабля на лес + game.Turn(); // 1 ход + + // доходим до конца клетки лес + game.Turn(); // 2 ход + + // выбираем ход - вперед на крокодила + game.SetMoveAndTurn(2, 2); + + var moves = game.GetAvailableMoves(); + + // Assert - возврат на начало клетки лес + Assert.Single(moves); + Assert.Equal(new TilePosition(2, 2), moves.Single().From); + Assert.Equal(new TilePosition(2, 1, 1), moves.Single().To); + Assert.Equal(2, game.TurnNumber); + } + + [Fact] + public void OneSpinning_BeatTheEnemyTurn_ReturnEnemyOnHisShip() + { + // Arrange + var spinningOnlyMap = new OneTileMapGenerator( + TileParams.SpinningForest() + ); + var game = new TestGame(spinningOnlyMap); + + // Act - высадка с корабля на лес + game.Turn(); // 1 ход + + // добавляем пирата противника - в месте высадки нашего пирата на последнюю позицию леса + game.AddEnemyTeamAndPirate(new TilePosition(2, 1, 0)); + + // доходим до конца клетки лес - сбиваем вражеского пирата + game.Turn(); // 2 ход + + // Assert + Assert.Equal(2, game.Board.Teams.Length); + Assert.Single(game.Board.Teams[0].Pirates); + Assert.Single(game.Board.Teams[1].Pirates); + + // наш пират на последней позиции леса + var ownPirate = game.Board.Teams[0].Pirates[0]; + Assert.Equal(new TilePosition(2, 1, 0), ownPirate.Position); + + // пират противника на своем корабле + var enemyPirate = game.Board.Teams[1].Pirates[0]; + Assert.Equal(game.Board.Teams[1].ShipPosition, enemyPirate.Position.Position); + Assert.Equal(new Position(2, 4), enemyPirate.Position.Position); + + Assert.Equal(2, game.TurnNumber); + } + + [Fact] + public void OneSpinningWithCoin_GetAvailableMoves_ReturnTwoMovesWithAndWithoutCoin() + { + // Arrange + var spinningOnlyMap = new OneTileMapGenerator( + TileParams.SpinningForest() + ); + var game = new TestGame(spinningOnlyMap); + + // Act - высадка с корабля на лес + game.Turn(); // 1 ход + + // добавляем монету - в месте высадки нашего пирата на первую позицию леса + game.AddCoin(new TilePosition(2, 1, 1)); + + var moves = game.GetAvailableMoves(); + + // Assert - доступно 2 хода на следующую клетку леса с монетой и без монеты + Assert.Equal(2, moves.Count); + + var withCoin = moves.Single(m => m.WithCoin); + Assert.Equal(new TilePosition(2, 1, 1), withCoin.From); + Assert.Equal(new TilePosition(2, 1, 0), withCoin.To); + + var withoutCoin = moves.Single(m => !m.WithCoin); + Assert.Equal(new TilePosition(2, 1, 1), withoutCoin.From); + Assert.Equal(new TilePosition(2, 1, 0), withoutCoin.To); + + Assert.Equal(1, game.TurnNumber); + } + + [Fact] + public void OneSpinningWithCoinAndEnemyOnNextPosition_GetAvailableMoves_ReturnSingleMoveWithoutCoin() + { + // Arrange + var spinningOnlyMap = new OneTileMapGenerator( + TileParams.SpinningForest() + ); + var game = new TestGame(spinningOnlyMap); + + // Act - высадка с корабля на лес + game.Turn(); // 1 ход + + // добавляем монету - в месте высадки нашего пирата на первую позицию леса + game.AddCoin(new TilePosition(2, 1, 1)); + + // добавляем пирата противника - в месте высадки нашего пирата на последнюю позицию леса + game.AddEnemyTeamAndPirate(new TilePosition(2, 1, 0)); + + var moves = game.GetAvailableMoves(); + + // Assert - доступен единственный ход на следующую клетку леса без монеты + Assert.Single(moves); + + var withoutCoin = moves.Single(m => !m.WithCoin); + Assert.Equal(new TilePosition(2, 1, 1), withoutCoin.From); + Assert.Equal(new TilePosition(2, 1, 0), withoutCoin.To); + + Assert.Equal(1, game.TurnNumber); + } + + [Fact] + public void SpinningThenEmptyWithCoinThenSpinningAgainWithEnemyOnFirstPosition_GetAvailableMoves_ReturnAllMoveWithoutCoin() + { + // Arrange + var spinningEmptyLineMap = new TwoTileMapGenerator( + TileParams.SpinningForest(), + TileParams.Empty() + ); + var game = new TestGame(spinningEmptyLineMap); + + // Act - высадка с корабля на лес + game.Turn(); // 1 ход + game.Turn(); // 2 ход + + // выбираем ход - вперед на пустую клетку + game.SetMoveAndTurn(2,2); + + // добавляем монету - на текущую позицию нашего пирата + game.AddCoin(new TilePosition(2, 2)); + + // добавляем пирата противника - в месте высадки нашего пирата на первую позицию леса + game.AddEnemyTeamAndPirate(new TilePosition(2, 1, 1)); + + var moves = game.GetAvailableMoves(); + + // Assert - доступно 4 хода без монеты на соседние клетки из цента карты + Assert.Equal(4, moves.Count); + Assert.True(moves.All(m => !m.WithCoin)); + Assert.Equal(3, game.TurnNumber); + } + + [Fact] + public void SpinningThenEmptyWithCoinThenSpinningAgainWithEnemyOnLastPosition_GetAvailableMoves_ReturnSingleMoveWithCoin() + { + // Arrange + var spinningEmptyLineMap = new TwoTileMapGenerator( + TileParams.SpinningForest(), + TileParams.Empty() + ); + var game = new TestGame(spinningEmptyLineMap); + + // Act - высадка с корабля на лес + game.Turn(); // 1 ход + game.Turn(); // 2 ход + + // выбираем ход - вперед на пустую клетку + game.SetMoveAndTurn(2,2); + + // добавляем монету - на текущую позицию нашего пирата + game.AddCoin(new TilePosition(2, 2)); + + // добавляем пирата противника - в месте высадки нашего пирата на последнюю позицию леса + game.AddEnemyTeamAndPirate(new TilePosition(2, 1, 0)); + + var moves = game.GetAvailableMoves(); + + // Assert - доступно 4 хода без монеты на соседние клетки из цента карты и 1 ход с монетой на лес + Assert.Equal(5, moves.Count); + Assert.Single(moves, m => m.WithCoin); + Assert.Equal(3, game.TurnNumber); + } +} \ No newline at end of file diff --git a/Jackal.Tests2/UnitTests.cs b/Jackal.Tests2/UnitTests.cs new file mode 100644 index 00000000..ee73e92d --- /dev/null +++ b/Jackal.Tests2/UnitTests.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Jackal.Core; +using Xunit; + +namespace Jackal.Tests2; + +public class UnitTests +{ + [Fact] + public void TestPermutations() + { + Assert.True(Utils.Factorial(0) == 1); + Assert.True(Utils.Factorial(1) == 1); + Assert.True(Utils.Factorial(4) == 4 * 3 * 2 * 1); + + Console.WriteLine("Permutations:"); + HashSet hashSet=new HashSet(); + for (int i = 0; i < Utils.Factorial(4); i++) + { + var rec = Utils.GetPermutation(i, new[] {"1", "2", "3", "4"}); + var val = string.Join(",", rec.ToArray()); + Assert.True(hashSet.Contains(val) == false); + Console.WriteLine("{0}: {1}", i, val); + } + var set1 = Utils.GetPermutation(0, new[] {"1", "2", "3", "4"}); + var set2 = Utils.GetPermutation(Utils.Factorial(4), new[] {"1", "2", "3", "4"}); + Assert.True(set1.SequenceEqual(set2)); + } + + [Fact] + public void TestArrowsHelper() + { + var rez = ArrowsCodesHelper.DoRotate(255); + Assert.True(rez==255); + + for (int code = 0; code <= 255; code++) + { + int test = code; + for (int i = 1; i <= 4; i++) + test = ArrowsCodesHelper.DoRotate(test); + Assert.True(code == test); + } + } +} \ No newline at end of file diff --git a/Jackal.sln b/Jackal.sln index 35b159b1..d6cc016c 100644 --- a/Jackal.sln +++ b/Jackal.sln @@ -1,26 +1,43 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 2012 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jackal", "Jackal\Jackal.csproj", "{F09A6694-A08D-4C94-A6A2-9AF9FAA6AA6E}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JackalHost", "JackalHost\JackalHost.csproj", "{C5859774-9403-40CE-945F-8527398E7D3F}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {F09A6694-A08D-4C94-A6A2-9AF9FAA6AA6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F09A6694-A08D-4C94-A6A2-9AF9FAA6AA6E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F09A6694-A08D-4C94-A6A2-9AF9FAA6AA6E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F09A6694-A08D-4C94-A6A2-9AF9FAA6AA6E}.Release|Any CPU.Build.0 = Release|Any CPU - {C5859774-9403-40CE-945F-8527398E7D3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C5859774-9403-40CE-945F-8527398E7D3F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C5859774-9403-40CE-945F-8527398E7D3F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C5859774-9403-40CE-945F-8527398E7D3F}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.4.33110.190 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JackalWebHost2", "JackalWebHost2\JackalWebHost2.csproj", "{8C1A0D3A-02A3-4548-B6DF-6942D70D2039}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jackal.Tests2", "Jackal.Tests2\Jackal.Tests2.csproj", "{A8137676-EBDB-46C0-B7C1-A59FAF9CB7E9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jackal.Core", "Jackal.Core\Jackal.Core.csproj", "{84B19D12-3D5D-4B65-B11F-C57901096B2C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jackal.BotArena", "Jackal.BotArena\Jackal.BotArena.csproj", "{E9E0FFA8-253B-434A-B2A5-C28B27159885}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {8C1A0D3A-02A3-4548-B6DF-6942D70D2039}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8C1A0D3A-02A3-4548-B6DF-6942D70D2039}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8C1A0D3A-02A3-4548-B6DF-6942D70D2039}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8C1A0D3A-02A3-4548-B6DF-6942D70D2039}.Release|Any CPU.Build.0 = Release|Any CPU + {A8137676-EBDB-46C0-B7C1-A59FAF9CB7E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A8137676-EBDB-46C0-B7C1-A59FAF9CB7E9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A8137676-EBDB-46C0-B7C1-A59FAF9CB7E9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A8137676-EBDB-46C0-B7C1-A59FAF9CB7E9}.Release|Any CPU.Build.0 = Release|Any CPU + {84B19D12-3D5D-4B65-B11F-C57901096B2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {84B19D12-3D5D-4B65-B11F-C57901096B2C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {84B19D12-3D5D-4B65-B11F-C57901096B2C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {84B19D12-3D5D-4B65-B11F-C57901096B2C}.Release|Any CPU.Build.0 = Release|Any CPU + {E9E0FFA8-253B-434A-B2A5-C28B27159885}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E9E0FFA8-253B-434A-B2A5-C28B27159885}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E9E0FFA8-253B-434A-B2A5-C28B27159885}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E9E0FFA8-253B-434A-B2A5-C28B27159885}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {67BC37BE-A076-4A41-863B-A88C8459D125} + EndGlobalSection +EndGlobal diff --git a/Jackal/Board.cs b/Jackal/Board.cs deleted file mode 100644 index 9b70c0bf..00000000 --- a/Jackal/Board.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System.Collections.Generic; - -namespace Jackal -{ - public class Board - { - public const int Size = 13; - - public MapGenerator Generator; - public Tile[,] Map; - public Team[] Teams; - - public Board(int mapId) - { - Generator = new MapGenerator(mapId); - Map = new Tile[Size, Size]; - InitMap(); - - Teams = new Team[4]; - InitTeam(0, (Size - 1)/2, 0); - InitTeam(1, (Size - 1), (Size - 1)/2); - InitTeam(2, (Size - 1)/2, (Size - 1)); - InitTeam(3, 0, (Size - 1)/2); - - Teams[0].Enemies = new[] {Teams[1], Teams[2], Teams[3]}; - Teams[1].Enemies = new[] {Teams[0], Teams[2], Teams[3]}; - Teams[2].Enemies = new[] {Teams[0], Teams[1], Teams[3]}; - Teams[3].Enemies = new[] {Teams[0], Teams[1], Teams[2]}; - } - - private void InitMap() - { - for (int i = 0; i < Size; i++) - { - SetWater(i, 0); - SetWater(0, i); - SetWater(i, Size - 1); - SetWater(Size - 1, i); - } - - for (int x = 1; x < Size - 1; x++) - { - for (int y = 1; y < Size - 1; y++) - { - SetUnknown(x, y); - } - } - SetStone(0, 0); - SetStone(Size - 1, 0); - SetStone(Size - 1, Size - 1); - SetStone(0, Size - 1); - } - - void SetWater(int x, int y) - { - Map[x, y] = new Tile(TileType.Water); - } - void SetUnknown(int x, int y) - { - Map[x, y] = new Tile(TileType.Unknown); - } - void SetStone(int x, int y) - { - Map[x, y] = new Tile(TileType.Stone); - } - - private void InitTeam(int teamId, int x, int y) - { - var startPosition = new Position(x, y); - var pirates = new Pirate[3]; - for (int i = 0; i < pirates.Length; i++) - { - pirates[i] = new Pirate(teamId, startPosition); - } - var ship = new Ship(teamId, startPosition, new HashSet(pirates)); - Teams[teamId] = new Team(teamId, ship, pirates); - } - } -} \ No newline at end of file diff --git a/Jackal/IPlayer.cs b/Jackal/IPlayer.cs deleted file mode 100644 index 8bd65e4a..00000000 --- a/Jackal/IPlayer.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Jackal -{ - public interface IPlayer - { - int OnMove(Board board, Move[] availableMoves); - } -} \ No newline at end of file diff --git a/Jackal/Jackal.csproj b/Jackal/Jackal.csproj deleted file mode 100644 index e4de93c9..00000000 --- a/Jackal/Jackal.csproj +++ /dev/null @@ -1,62 +0,0 @@ - - - - - Debug - AnyCPU - {F09A6694-A08D-4C94-A6A2-9AF9FAA6AA6E} - Library - Properties - Jackal - Jackal - v4.5 - 512 - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Jackal/MapGenerator.cs b/Jackal/MapGenerator.cs deleted file mode 100644 index 519daa9f..00000000 --- a/Jackal/MapGenerator.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Jackal -{ - public class MapGenerator - { - public int Size = 11; - public int TotalCoins = 37; - - private readonly Random _rand; - private readonly List _tiles; - private int _nextTile; - - public MapGenerator(int mapId) - { - _rand = new Random(mapId); - - _tiles = new List(Size*Size); - - for (int i = 0; i < 5; i++) - { - AddTile(new Tile(TileType.Grass, 1)); - AddTile(new Tile(TileType.Grass, 2)); - } - - for (int i = 0; i < 3; i++) - { - AddTile(new Tile(TileType.Grass, 3)); - } - - for (int i = 0; i < 2; i++) - { - AddTile(new Tile(TileType.Grass, 4)); - } - - AddTile(new Tile(TileType.Grass, 5)); - - for (int i = 0; i < (Size*Size - 11); i++) - { - AddTile(new Tile(TileType.Grass)); - } - - _tiles = ShuffleTiles(); - } - - private void AddTile(Tile tile) - { - _tiles.Add(tile); - } - - private List ShuffleTiles() - { - return _tiles.OrderBy(x => _rand.Next()).ToList(); - } - - public Tile GetNext() - { - return _tiles[_nextTile++]; - } - } -} \ No newline at end of file diff --git a/Jackal/Move.cs b/Jackal/Move.cs deleted file mode 100644 index 27737470..00000000 --- a/Jackal/Move.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Jackal -{ - public struct Move - { - public Pirate Pirate; - - public Position To; - public bool WithCoins; - - public Move(Pirate pirate, Position to, bool withCoin) - { - Pirate = pirate; - To = to; - WithCoins = withCoin; - } - } -} \ No newline at end of file diff --git a/Jackal/Pirate.cs b/Jackal/Pirate.cs deleted file mode 100644 index efe3c678..00000000 --- a/Jackal/Pirate.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Jackal -{ - public class Pirate - { - public int TeamId; - public Position Position; - public int Coins; - - public Pirate(int teamId, Position position) - { - TeamId = teamId; - Position = position; - Coins = 0; - } - } -} \ No newline at end of file diff --git a/Jackal/Position.cs b/Jackal/Position.cs deleted file mode 100644 index 4a139720..00000000 --- a/Jackal/Position.cs +++ /dev/null @@ -1,48 +0,0 @@ -namespace Jackal -{ - public struct Position - { - public readonly int X; - public readonly int Y; - - public Position(int x, int y) - { - X = x; - Y = y; - } - - public bool Equals(Position other) - { - return X == other.X && Y == other.Y; - } - - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) return false; - return obj is Position && Equals((Position) obj); - } - - public static bool operator ==(Position left, Position right) - { - return left.Equals(right); - } - - public static bool operator !=(Position left, Position right) - { - return !left.Equals(right); - } - - public override int GetHashCode() - { - unchecked - { - return (X*397) ^ Y; - } - } - - public override string ToString() - { - return string.Format("({0},{1})", X, Y); - } - } -} \ No newline at end of file diff --git a/Jackal/Properties/AssemblyInfo.cs b/Jackal/Properties/AssemblyInfo.cs deleted file mode 100644 index dacd6ae5..00000000 --- a/Jackal/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("Jackal")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("Jackal")] -[assembly: AssemblyCopyright("Copyright © 2014")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("59119957-42ff-4b89-bd58-816320aa4046")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Jackal/Ship.cs b/Jackal/Ship.cs deleted file mode 100644 index d70bcd6a..00000000 --- a/Jackal/Ship.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Collections.Generic; - -namespace Jackal -{ - public class Ship - { - public int TeamId; - public Position Position; - public int Coins; - public HashSet Crew; - - public Ship(int teamId, Position position, HashSet crew) - { - TeamId = teamId; - Position = position; - Coins = 0; - Crew = crew; - } - } -} \ No newline at end of file diff --git a/Jackal/Team.cs b/Jackal/Team.cs deleted file mode 100644 index 73b86a44..00000000 --- a/Jackal/Team.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Jackal -{ - public class Team - { - public int Id; - public Ship Ship; - public Pirate[] Pirates; - public Team[] Enemies; - - public Team(int id, Ship ship, Pirate[] pirates) - { - Id = id; - Ship = ship; - Pirates = pirates; - } - } -} \ No newline at end of file diff --git a/Jackal/Tile.cs b/Jackal/Tile.cs deleted file mode 100644 index 9dfdd4b4..00000000 --- a/Jackal/Tile.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Collections.Generic; - -namespace Jackal -{ - public class Tile - { - public TileType Type; - public int Coins; - - public int? OccupationTeamId; - public HashSet Pirates; - - public Tile(TileType type, int coins = 0) - { - Type = type; - Coins = coins; - - OccupationTeamId = null; // free - Pirates = new HashSet(); - } - } -} \ No newline at end of file diff --git a/Jackal/TileType.cs b/Jackal/TileType.cs deleted file mode 100644 index 3a7a9c9b..00000000 --- a/Jackal/TileType.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Jackal -{ - public enum TileType - { - Unknown, - Stone, - Water, - Grass - } -} \ No newline at end of file diff --git a/JackalHost/Actions/Attack.cs b/JackalHost/Actions/Attack.cs deleted file mode 100644 index da718501..00000000 --- a/JackalHost/Actions/Attack.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Jackal; - -namespace JackalHost.Actions -{ - class Attack : IGameAction - { - private readonly Position _to; - - public Attack(Position to) - { - _to = to; - } - - public void Act(Game game) - { - Board board = game.Board; - Tile tile = board.Map[_to.X, _to.Y]; - - foreach (var enemyPirate in tile.Pirates) - { - Team enemyTeam = board.Teams[enemyPirate.TeamId]; - tile.Coins += enemyPirate.Coins; - enemyPirate.Coins = 0; - enemyPirate.Position = enemyTeam.Ship.Position; - enemyTeam.Ship.Crew.Add(enemyPirate); - } - tile.OccupationTeamId = null; - tile.Pirates.Clear(); - } - } -} \ No newline at end of file diff --git a/JackalHost/Actions/DropCoin.cs b/JackalHost/Actions/DropCoin.cs deleted file mode 100644 index d32a090d..00000000 --- a/JackalHost/Actions/DropCoin.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Jackal; - -namespace JackalHost.Actions -{ - class DropCoin : IGameAction - { - private readonly Pirate _pirate; - - public DropCoin(Pirate pirate) - { - _pirate = pirate; - } - - public void Act(Game game) - { - Board board = game.Board; - if (_pirate.Coins == 0) - { - return; - } - - Position position = _pirate.Position; - Tile tile = board.Map[position.X, position.Y]; - tile.Coins += _pirate.Coins; - _pirate.Coins = 0; - } - } -} \ No newline at end of file diff --git a/JackalHost/Actions/Explore.cs b/JackalHost/Actions/Explore.cs deleted file mode 100644 index 983bce6f..00000000 --- a/JackalHost/Actions/Explore.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Jackal; - -namespace JackalHost.Actions -{ - class Explore : IGameAction - { - private readonly Position _position; - - public Explore(Position position) - { - _position = position; - } - - public void Act(Game game) - { - Board board = game.Board; - board.Map[_position.X, _position.Y] = board.Generator.GetNext(); - } - } -} \ No newline at end of file diff --git a/JackalHost/Actions/GameActionList.cs b/JackalHost/Actions/GameActionList.cs deleted file mode 100644 index 499a32a3..00000000 --- a/JackalHost/Actions/GameActionList.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Collections.Generic; -using Jackal; - -namespace JackalHost.Actions -{ - class GameActionList : IGameAction - { - private readonly List _actions; - - public GameActionList(params IGameAction[] actions) - { - _actions = new List(actions); - } - - public void Act(Game game) - { - foreach (var action in _actions) - { - action.Act(game); - } - } - - public static GameActionList Create(params IGameAction[] actions) - { - return new GameActionList(actions); - } - } -} \ No newline at end of file diff --git a/JackalHost/Actions/IGameAction.cs b/JackalHost/Actions/IGameAction.cs deleted file mode 100644 index d224976e..00000000 --- a/JackalHost/Actions/IGameAction.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace JackalHost.Actions -{ - interface IGameAction - { - void Act(Game game); - } -} \ No newline at end of file diff --git a/JackalHost/Actions/Landing.cs b/JackalHost/Actions/Landing.cs deleted file mode 100644 index 9d63d4f0..00000000 --- a/JackalHost/Actions/Landing.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using Jackal; - -namespace JackalHost.Actions -{ - class Landing : IGameAction - { - private readonly Pirate _pirate; - private readonly Ship _ship; - - public Landing(Pirate pirate, Ship ship) - { - _pirate = pirate; - _ship = ship; - } - - public void Act(Game game) - { - Position shipPosition = _ship.Position; - Position landingPosition; - if (shipPosition.X == 0) - { - landingPosition = new Position(shipPosition.X + 1, shipPosition.Y); - } - else if(shipPosition.X == (Board.Size-1)) - { - landingPosition = new Position(shipPosition.X - 1, shipPosition.Y); - } - else if(shipPosition.Y == 0) - { - landingPosition = new Position(shipPosition.X, shipPosition.Y + 1); - } - else if (shipPosition.Y == (Board.Size - 1)) - { - landingPosition = new Position(shipPosition.X, shipPosition.Y - 1); - } - else - { - throw new NotSupportedException(); - } - - _ship.Crew.Remove(_pirate); - _pirate.Position = landingPosition; - - var tile = game.Board.Map[landingPosition.X, landingPosition.Y]; - tile.OccupationTeamId = _pirate.TeamId; - tile.Pirates.Add(_pirate); - } - } -} \ No newline at end of file diff --git a/JackalHost/Actions/Navigation.cs b/JackalHost/Actions/Navigation.cs deleted file mode 100644 index c37866e7..00000000 --- a/JackalHost/Actions/Navigation.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Jackal; - -namespace JackalHost.Actions -{ - class Navigation : IGameAction - { - private readonly Ship _ship; - private readonly Position _to; - - public Navigation(Ship ship, Position to) - { - _ship = ship; - _to = to; - } - - public void Act(Game game) - { - _ship.Position = _to; - foreach (var pirate in _ship.Crew) - { - pirate.Position = _to; - } - } - } -} \ No newline at end of file diff --git a/JackalHost/Actions/Shipping.cs b/JackalHost/Actions/Shipping.cs deleted file mode 100644 index 90a7ef85..00000000 --- a/JackalHost/Actions/Shipping.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Jackal; - -namespace JackalHost.Actions -{ - class Shipping : IGameAction - { - private readonly Pirate _pirate; - private readonly Ship _ship; - - public Shipping(Pirate pirate, Ship ship) - { - _pirate = pirate; - _ship = ship; - } - - public void Act(Game game) - { - Position position = _pirate.Position; - Board board = game.Board; - Tile tile = board.Map[position.X, position.Y]; - tile.Pirates.Remove(_pirate); - if (tile.Pirates.Count == 0) - { - tile.OccupationTeamId = null; - } - - _pirate.Position = _ship.Position; - _ship.Crew.Add(_pirate); - var coins = _pirate.Coins; - _ship.Coins += coins; - _pirate.Coins = 0; - - game.Scores[_pirate.TeamId] += coins; - game.CoinsLeft -= coins; - } - } -} \ No newline at end of file diff --git a/JackalHost/Actions/TakeCoin.cs b/JackalHost/Actions/TakeCoin.cs deleted file mode 100644 index 75c1edb7..00000000 --- a/JackalHost/Actions/TakeCoin.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Jackal; - -namespace JackalHost.Actions -{ - class TakeCoin : IGameAction - { - private readonly Pirate _pirate; - - public TakeCoin(Pirate pirate) - { - _pirate = pirate; - } - - public void Act(Game game) - { - Board board = game.Board; - Position position = _pirate.Position; - Tile tile = board.Map[position.X, position.Y]; - if (tile.Coins == 0 || _pirate.Coins > 0) - { - return; - } - - tile.Coins--; - _pirate.Coins++; - } - } -} \ No newline at end of file diff --git a/JackalHost/Actions/Walk.cs b/JackalHost/Actions/Walk.cs deleted file mode 100644 index 7c2951a1..00000000 --- a/JackalHost/Actions/Walk.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Jackal; - -namespace JackalHost.Actions -{ - class Walk : IGameAction - { - private readonly Pirate _pirate; - private readonly Position _to; - - public Walk(Pirate pirate, Position to) - { - _pirate = pirate; - _to = to; - } - - public void Act(Game game) - { - Tile[,] map = game.Board.Map; - - var from = _pirate.Position; - - var fromTile = map[@from.X, @from.Y]; - fromTile.Pirates.Remove(_pirate); - if (fromTile.Pirates.Count == 0) - { - fromTile.OccupationTeamId = null; - } - - _pirate.Position = _to; - var toTile = map[_to.X, _to.Y]; - toTile.OccupationTeamId = _pirate.TeamId; - toTile.Pirates.Add(_pirate); - } - } -} \ No newline at end of file diff --git a/JackalHost/App.config b/JackalHost/App.config deleted file mode 100644 index 8e156463..00000000 --- a/JackalHost/App.config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/JackalHost/Game.cs b/JackalHost/Game.cs deleted file mode 100644 index 0f53de14..00000000 --- a/JackalHost/Game.cs +++ /dev/null @@ -1,248 +0,0 @@ -using System.Collections.Generic; -using Jackal; -using JackalHost.Actions; - -namespace JackalHost -{ - public class Game - { - private readonly IPlayer[] _players; - - public Board Board; - - public Dictionary Scores; // TeamId->Total couns - public int CoinsLeft; - - private readonly List _availableMoves; - private readonly List _actions; - - public Game(IPlayer[] players, Board board) - { - _players = players; - - Board = board; - Scores = new Dictionary(); - foreach (var team in Board.Teams) - { - Scores[team.Id] = 0; - } - CoinsLeft = Board.Generator.TotalCoins; - - _availableMoves = new List(); - _actions = new List(); - } - - public bool Turn() - { - int teamId = CurrentTeamId; - IPlayer me = _players[teamId]; - - GetAvailableMoves(teamId); - - int moveNo = me.OnMove(Board, _availableMoves.ToArray()); - - IGameAction action = _actions[moveNo]; - action.Act(this); - TurnNo++; - return true; - } - - private void GetAvailableMoves(int teamId) - { - _availableMoves.Clear(); - _actions.Clear(); - - Team team = Board.Teams[teamId]; - Ship ship = team.Ship; - - foreach (var pirate in team.Pirates) - { - Position position = pirate.Position; - - if (position.Y > 0) // N - { - Step(position.X, position.Y - 1, pirate, ship, team); - } - if (position.X < (Board.Size-1) && position.Y > 0) // NE - { - Step(position.X + 1, position.Y - 1, pirate, ship, team); - } - if (position.X < (Board.Size - 1)) // E - { - Step(position.X + 1, position.Y, pirate, ship, team); - } - if (position.X < (Board.Size - 1) && position.Y < (Board.Size-1)) // SE - { - Step(position.X + 1, position.Y + 1, pirate, ship, team); - } - if (position.Y < (Board.Size - 1)) // S - { - Step(position.X, position.Y + 1, pirate, ship, team); - } - if (position.X > 0 && position.Y < (Board.Size - 1)) // SW - { - Step(position.X - 1, position.Y + 1, pirate, ship, team); - } - if (position.X > 0) // W - { - Step(position.X - 1, position.Y, pirate, ship, team); - } - if (position.X > 0 && position.Y > 0) // NW - { - Step(position.X - 1, position.Y - 1, pirate, ship, team); - } - } - } - - private void Step(int toX, int toY, Pirate pirate, Ship ship, Team team) - { - var moves = _availableMoves; - var actions = _actions; - - var to = new Position(toX, toY); - Tile tile = Board.Map[toX, toY]; - - bool onShip = (ship.Position == pirate.Position); - - switch (tile.Type) - { - case TileType.Unknown: - { - // exploration - - if (onShip) - { - if (CanLanding(pirate, to)) - { - // landing - moves.Add(new Move(pirate, to, false)); - actions.Add(GameActionList.Create( - new DropCoin(pirate), - new Explore(to), - new Landing(pirate, ship))); - } - } - else - { - moves.Add(new Move(pirate, to, false)); - actions.Add(GameActionList.Create( - new DropCoin(pirate), - new Explore(to), - new Walk(pirate, to))); - } - - break; - } - case TileType.Stone: - { - return; - } - case TileType.Water: - { - if (to == ship.Position) - { - // shipping with coins - moves.Add(new Move(pirate, to, true)); - actions.Add(GameActionList.Create( - new TakeCoin(pirate), - new Shipping(pirate, ship))); - - // shipping without coins - moves.Add(new Move(pirate, to, false)); - actions.Add(GameActionList.Create( - new DropCoin(pirate), - new Shipping(pirate, ship))); - } - else if (pirate.Position == ship.Position) - { - if (((ship.Position.X == 0 || ship.Position.X == Board.Size - 1) && - (to.Y == 0 || to.Y == Board.Size - 1)) || - ((ship.Position.Y == 0 || ship.Position.Y == Board.Size - 1) && - (to.X == 0 || to.X == Board.Size - 1))) - { - break; // enemy water territories - } - - // navigation - moves.Add(new Move(pirate, to, true)); - actions.Add(GameActionList.Create( - new Navigation(ship, to))); - } - break; - } - case TileType.Grass: - { - var attack = tile.OccupationTeamId.HasValue && tile.OccupationTeamId.Value != pirate.TeamId; - if (attack) - { - // attack - if (onShip) - { - if (CanLanding(pirate, to)) - { - moves.Add(new Move(pirate, to, false)); - actions.Add(GameActionList.Create( - new DropCoin(pirate), - new Attack(to), - new Landing(pirate, ship))); - } - } - else - { - moves.Add(new Move(pirate, to, false)); - actions.Add(GameActionList.Create( - new DropCoin(pirate), - new Attack(to), - new Walk(pirate, to))); - } - } - else - { - if (onShip) - { - if (CanLanding(pirate, to)) - { - moves.Add(new Move(pirate, to, false)); - actions.Add(GameActionList.Create( - new Landing(pirate, ship))); - } - } - else - { - moves.Add(new Move(pirate, to, false)); - actions.Add(GameActionList.Create( - new DropCoin(pirate), - new Walk(pirate, to))); - - moves.Add(new Move(pirate, to, true)); - actions.Add(GameActionList.Create( - new TakeCoin(pirate), - new Walk(pirate, to))); - } - } - break; - } - } - } - - private static bool CanLanding(Pirate pirate, Position to) - { - return ((pirate.Position.Y == 0 || pirate.Position.Y == Board.Size - 1) && - pirate.Position.X - to.X == 0) || - ((pirate.Position.X == 0 || pirate.Position.X == Board.Size - 1) && - pirate.Position.Y - to.Y == 0); - } - - public bool IsGameOver - { - get { return CoinsLeft == 0; } - } - - public int TurnNo { get; set; } - - public int CurrentTeamId - { - get { return TurnNo%4; } - } - } -} diff --git a/JackalHost/JackalHost.csproj b/JackalHost/JackalHost.csproj deleted file mode 100644 index 14d6075c..00000000 --- a/JackalHost/JackalHost.csproj +++ /dev/null @@ -1,78 +0,0 @@ - - - - - Debug - AnyCPU - {C5859774-9403-40CE-945F-8527398E7D3F} - Exe - Properties - JackalHost - JackalHost - v4.5 - 512 - - - AnyCPU - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - AnyCPU - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {f09a6694-a08d-4c94-a6a2-9af9faa6aa6e} - Jackal - - - - - \ No newline at end of file diff --git a/JackalHost/Monitor.cs b/JackalHost/Monitor.cs deleted file mode 100644 index ca5a8e39..00000000 --- a/JackalHost/Monitor.cs +++ /dev/null @@ -1,149 +0,0 @@ -using System; -using System.Linq; -using Jackal; - -namespace JackalHost -{ - public class Monitor - { - private readonly Game _game; - private readonly Board _board; - private readonly string _horizontalLine = new string('=', Console.WindowWidth); - - public Monitor(Game game) - { - _game = game; - _board = game.Board; - } - - public void Draw() - { - Console.ResetColor(); - Console.SetCursorPosition(0, 0); - DrawStatsTable(); - Console.WriteLine(_horizontalLine); - DrawMap(); - } - - private void DrawStatsTable() - { - Console.ResetColor(); - Console.Write("Score:"); - foreach (var score in _game.Scores) - { - Console.Write('\t'); - Console.BackgroundColor = GetTeamColor(score.Key); - Console.Write("{0}: {1}", score.Key, score.Value); - Console.ResetColor(); - } - Console.WriteLine(); - Console.BackgroundColor = GetTeamColor(_game.CurrentTeamId); - Console.Write("TurnNo: {0}", _game.TurnNo); - Console.ResetColor(); - - Console.WriteLine(); - } - - private void DrawMap() - { - for (int y = 0; y < Board.Size; y++) - { - for (int x = 0; x < Board.Size; x++) - { - string symbol = "."; - - Tile tile = _board.Map[x, y]; - - ConsoleColor background = ConsoleColor.Black; - ConsoleColor foreground = ConsoleColor.White; - - switch (tile.Type) - { - case TileType.Unknown: - { - background = ConsoleColor.Gray; - break; - } - case TileType.Stone: - { - symbol = "^"; - background = ConsoleColor.DarkGray; - break; - } - case TileType.Water: - { - background = ConsoleColor.Cyan; - break; - } - case TileType.Grass: - { - symbol = " "; - background = ConsoleColor.Green; - break; - } - } - - if (tile.Coins > 0) - { - background = ConsoleColor.DarkYellow; - foreground = ConsoleColor.Yellow; - symbol = tile.Coins.ToString(); - } - - foreach (var team in _board.Teams) - { - var position = new Position(x, y); - - var ship = team.Ship; - if (ship.Position == position) - { - foreground = ConsoleColor.White; - background = GetTeamColor(team.Id); - int crewCount = ship.Crew.Count; - symbol = crewCount == 0 ? "S" : crewCount.ToString(); - } - else - { - Pirate pirate = tile.Pirates.FirstOrDefault(); - if (pirate != null) - { - foreground = ConsoleColor.White; - background = GetTeamColor(team.Id); - int coins = pirate.Coins; - if (coins > 0) - { - ; - } - symbol = coins == 0 ? "p" : coins.ToString(); - } - } - } - Console.BackgroundColor = background; - Console.ForegroundColor = foreground; - Console.Write(symbol); - } - Console.WriteLine(); - } - } - - private static ConsoleColor GetTeamColor(int teamId) - { - switch (teamId) - { - case 0: return ConsoleColor.DarkRed; - case 1: return ConsoleColor.DarkBlue; - case 2: return ConsoleColor.DarkGreen; - case 3: return ConsoleColor.DarkYellow; - default: throw new NotSupportedException(); - } - } - - public void GameOver() - { - Console.ResetColor(); - Console.Clear(); - Console.WriteLine("Game over"); - DrawStatsTable(); - } - } -} \ No newline at end of file diff --git a/JackalHost/Program.cs b/JackalHost/Program.cs deleted file mode 100644 index 9bad9a34..00000000 --- a/JackalHost/Program.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using Jackal; - -namespace JackalHost -{ - internal class Program - { - private static void Main(string[] args) - { - IPlayer[] players = - { - new RandomPlayer(), - new RandomPlayer(), - new RandomPlayer(), - new RandomPlayer(), - }; - const int mapId = 987412; - - var board = new Board(mapId); - var game = new Game(players, board); - var monitor = new Monitor(game); - - while (!game.IsGameOver) - { - - if (game.TurnNo%100 == 0) - { - monitor.Draw(); - } - //Console.ReadKey(); - game.Turn(); - } - monitor.GameOver(); - Console.ReadKey(); - } - } -} \ No newline at end of file diff --git a/JackalHost/Properties/AssemblyInfo.cs b/JackalHost/Properties/AssemblyInfo.cs deleted file mode 100644 index f65f627d..00000000 --- a/JackalHost/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("JackalHost")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("JackalHost")] -[assembly: AssemblyCopyright("Copyright © 2014")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("001f1519-1ad0-4d27-809a-17dbaa62be53")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/JackalHost/RandomPlayer.cs b/JackalHost/RandomPlayer.cs deleted file mode 100644 index b95939ef..00000000 --- a/JackalHost/RandomPlayer.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using Jackal; - -namespace JackalHost -{ - class RandomPlayer : IPlayer - { - static readonly Random Rnd = new Random(42); - - public int OnMove(Board board, Move[] availableMoves) - { - return Rnd.Next(0, availableMoves.Length); - } - } -} \ No newline at end of file diff --git a/JackalWebHost2/Controllers/Hubs/GameHub.cs b/JackalWebHost2/Controllers/Hubs/GameHub.cs new file mode 100644 index 00000000..c8099e1c --- /dev/null +++ b/JackalWebHost2/Controllers/Hubs/GameHub.cs @@ -0,0 +1,309 @@ +using Jackal.Core; +using Jackal.Core.MapGenerator.TilesPack; +using JackalWebHost2.Controllers.Models; +using JackalWebHost2.Controllers.Models.Game; +using JackalWebHost2.Controllers.Models.Services; +using JackalWebHost2.Data.Entities; +using JackalWebHost2.Data.Interfaces; +using JackalWebHost2.Infrastructure.Auth; +using JackalWebHost2.Models; +using JackalWebHost2.Services; +using Microsoft.AspNetCore.SignalR; +using SignalRSwaggerGen.Attributes; + +namespace JackalWebHost2.Controllers.Hubs; + +[FastAuth] +[SignalRHub] +public class GameHub : Hub +{ + private const string CALLBACK_NOTIFY = "Notify"; + private const string CALLBACK_LOAD_GAME_DATA = "LoadGameData"; + private const string CALLBACK_GET_START_DATA = "GetStartData"; + private const string CALLBACK_GET_MOVE_CHANGES = "GetMoveChanges"; + private const string CALLBACK_GET_ACTIVE_GAMES = "GetActiveGames"; + + private const string CALLBACK_GET_NET_GAME_DATA = "GetNetGameData"; + private const string CALLBACK_GET_ACTIVE_NET_GAMES = "GetActiveNetGames"; + + private readonly IGameService _gameService; + private readonly IStateRepository _gameStateRepository; + private readonly IStateRepository _netgameStateRepository; + private readonly Random _random; + + + public GameHub( + IGameService gameService, + IStateRepository gameStateRepository, + IStateRepository netgameStateRepository) + { + _gameService = gameService; + _gameStateRepository = gameStateRepository; + _netgameStateRepository = netgameStateRepository; + _random = new Random(DateTime.Now.Millisecond); + } + + public override async Task OnConnectedAsync() + { + var user = FastAuthJwtBearerHelper.ExtractUser(Context.User); + await Clients.Caller.SendAsync(CALLBACK_NOTIFY, $"{user.Login} вошел в игру"); + await Clients.Caller.SendAsync(CALLBACK_GET_ACTIVE_GAMES, new AllActiveGamesResponse + { + GamesEntries = _gameStateRepository.GetEntries().Select(ToActiveGame).ToList() + }); + await Clients.Caller.SendAsync(CALLBACK_GET_ACTIVE_NET_GAMES, new AllActiveGamesResponse + { + GamesEntries = _netgameStateRepository.GetEntries().Select(ToActiveGame).ToList() + }); + await base.OnConnectedAsync(); + } + + public override async Task OnDisconnectedAsync(Exception? exception) + { + var user = FastAuthJwtBearerHelper.ExtractUser(Context.User); + await Clients.Others.SendAsync(CALLBACK_NOTIFY, $"{user.Login} покинул игру"); + await base.OnDisconnectedAsync(exception); + } + + /// + /// Загрузка существующей игры + /// + public async Task Load(LoadGameRequest request) + { + var user = FastAuthJwtBearerHelper.ExtractUser(Context.User); + var result = await _gameService.LoadGame(user.Id, request.GameId); + + await Groups.AddToGroupAsync(Context.ConnectionId, GetGroupName(result.GameId)); + await Clients.Caller.SendAsync(CALLBACK_LOAD_GAME_DATA, new LoadGameResponse + { + GameId = result.GameId, + GameMode = result.GameMode, + TilesPackName = result.TilesPackName, + Pirates = result.Pirates, + Map = result.Map, + MapId = result.MapId, + Stats = result.Statistics, + Teams = result.Teams, + TeamScores = result.TeamScores, + Moves = result.Moves + }); + + if (!result.Statistics.IsGameOver && result.Moves.Count == 0) + { + await Move(new TurnGameRequest + { + GameId = result.GameId + }); + } + } + + /// + /// Старт новой игры + /// + public async Task Start(StartGameRequest request) + { + var user = FastAuthJwtBearerHelper.ExtractUser(Context.User); + var startGameModel = new StartGameModel { Settings = request.Settings }; + var result = await _gameService.StartGame(user, startGameModel); + + await Groups.AddToGroupAsync(Context.ConnectionId, GetGroupName(result.GameId)); + await Clients.Group(GetGroupName(result.GameId)).SendAsync(CALLBACK_GET_START_DATA, new StartGameResponse + { + GameId = result.GameId, + GameMode = result.GameMode, + TilesPackName = TilesPackFactory.CheckName(request.Settings.TilesPackName), + Pirates = result.Pirates, + Map = result.Map, + MapId = result.MapId, + Stats = result.Statistics, + Teams = result.Teams, + Moves = result.Moves + }); + + await Clients.All.SendAsync(CALLBACK_GET_ACTIVE_GAMES, new AllActiveGamesResponse + { + GamesEntries = _gameStateRepository.GetEntries().Select(ToActiveGame).ToList() + }); + _gameStateRepository.ResetChanges(); + + if (!result.Statistics.IsGameOver && result.Moves.Count == 0) + { + await Move(new TurnGameRequest + { + GameId = result.GameId + }); + } + } + + /// + /// Старт новой игры + /// + public async Task StartPublic(NetGameRequest request) + { + var user = FastAuthJwtBearerHelper.ExtractUser(Context.User); + var netGame = _netgameStateRepository.GetObject(request.Id); + if (netGame?.CreatorId != user.Id) return; + + var startGameModel = new StartGameModel { Settings = request.Settings }; + var result = await _gameService.StartGame(user, startGameModel); + + netGame.GameId = result.GameId; + _netgameStateRepository.UpdateObject(netGame.Id, netGame); + + await Clients.Group(GetNetGroupName(netGame.Id)).SendAsync(CALLBACK_GET_NET_GAME_DATA, ToNetGameResponse(netGame, result.GameId)); + + // скрываем завершённую сетевую игру + await Clients.All.SendAsync(CALLBACK_GET_ACTIVE_NET_GAMES, new AllActiveGamesResponse + { + GamesEntries = _netgameStateRepository.GetEntries().Select(ToActiveGame).ToList() + }); + _netgameStateRepository.ResetChanges(); + + // показываем новую публичную игру + await Clients.All.SendAsync(CALLBACK_GET_ACTIVE_GAMES, new AllActiveGamesResponse + { + GamesEntries = _gameStateRepository.GetEntries().Select(ToActiveGame).ToList() + }); + _gameStateRepository.ResetChanges(); + } + + + /// + /// Ход игры + /// + public async Task Move(TurnGameRequest request) + { + var user = FastAuthJwtBearerHelper.ExtractUser(Context.User); + var turnGameModel = new TurnGameModel + { + GameId = request.GameId, + TurnNum = request.TurnNum, + PirateId = request.PirateId + }; + var result = await _gameService.MakeGameTurn(user.Id, turnGameModel); + + await Groups.AddToGroupAsync(Context.ConnectionId, GetGroupName(request.GameId)); + await Clients.Group(GetGroupName(request.GameId)).SendAsync(CALLBACK_GET_MOVE_CHANGES, new TurnGameResponse + { + PirateChanges = result.PirateChanges, + Changes = result.Changes, + Stats = result.Statistics, + TeamScores = result.TeamScores, + Moves = result.Moves + }); + + if (!result.Statistics.IsGameOver && result.Moves.Count == 0) + { + await Move(new TurnGameRequest + { + GameId = request.GameId + }); + } + } + + + /// + /// Старт новой сетевой игры + /// + public async Task NetStart(NetGameRequest request) + { + var user = FastAuthJwtBearerHelper.ExtractUser(Context.User); + var netGame = new NetGameSettings(user) + { + Id = _random.NextInt64(1, 100_000_000), + Settings = request.Settings + }; + _netgameStateRepository.CreateObject(user, netGame.Id, netGame, netGame.Users); + + await Groups.AddToGroupAsync(Context.ConnectionId, GetNetGroupName(netGame.Id)); + await Clients.Group(GetNetGroupName(netGame.Id)).SendAsync(CALLBACK_GET_NET_GAME_DATA, ToNetGameResponse(netGame)); + + await Clients.All.SendAsync(CALLBACK_GET_ACTIVE_NET_GAMES, new AllActiveGamesResponse + { + GamesEntries = _netgameStateRepository.GetEntries().Select(ToActiveGame).ToList() + }); + _netgameStateRepository.ResetChanges(); + } + + + /// + /// Изменение сетевой игры + /// + public async Task NetUpdate(NetGameRequest request) + { + var user = FastAuthJwtBearerHelper.ExtractUser(Context.User); + var netGame = _netgameStateRepository.GetObject(request.Id); + if (netGame?.CreatorId != user.Id) return; + + netGame.Settings = request.Settings; + _netgameStateRepository.UpdateObject(netGame.Id, netGame); + + await Clients.Group(GetNetGroupName(netGame.Id)).SendAsync(CALLBACK_GET_NET_GAME_DATA, ToNetGameResponse(netGame)); + } + + /// + /// Присоединиться к сетевой игре + /// + public async Task NetJoin(NetUserRequest request) + { + var user = FastAuthJwtBearerHelper.ExtractUser(Context.User); + var netGame = _netgameStateRepository.GetObject(request.Id); + if (netGame == null) return; + + netGame.Users.Add(user); + _netgameStateRepository.UpdateObject(netGame.Id, netGame, netGame.Users); + + await Groups.AddToGroupAsync(Context.ConnectionId, GetNetGroupName(netGame.Id)); + await Clients.Group(GetNetGroupName(netGame.Id)).SendAsync(CALLBACK_GET_NET_GAME_DATA, ToNetGameResponse(netGame)); + } + + /// + /// Покинуть сетевую игру + /// + public async Task NetLeave(NetUserRequest request) + { + var user = FastAuthJwtBearerHelper.ExtractUser(Context.User); + var netGame = _netgameStateRepository.GetObject(request.Id); + if (netGame == null) return; + + netGame.Users.Remove(user); + _netgameStateRepository.UpdateObject(netGame.Id, netGame, netGame.Users); + + await Groups.RemoveFromGroupAsync(Context.ConnectionId, GetNetGroupName(netGame.Id)); + await Clients.Group(GetNetGroupName(netGame.Id)).SendAsync(CALLBACK_GET_NET_GAME_DATA, ToNetGameResponse(netGame)); + } + + private string GetNetGroupName(long netGameId) + { + return $"netgrp{netGameId}"; + } + + private string GetGroupName(long gameId) + { + return $"grp{gameId}"; + } + + private ActiveGameInfo ToActiveGame(CacheEntry entry) + { + return new ActiveGameInfo + { + GameId = entry.ObjectId, + Creator = entry.Creator, + Players = entry.Players, + TimeStamp = entry.TimeStamp + }; + } + + private NetGameResponse ToNetGameResponse(NetGameSettings netGame, long? gameId = null) + { + return new NetGameResponse + { + Id = netGame.Id, + CreatorId = netGame.CreatorId, + GameId = gameId, + Settings = netGame.Settings, + Viewers = netGame.Users.Select(it => it.Id).ToArray(), + Users = netGame.Users + }; + } +} \ No newline at end of file diff --git a/JackalWebHost2/Controllers/Mappings/LobbyControllerMappings.cs b/JackalWebHost2/Controllers/Mappings/LobbyControllerMappings.cs new file mode 100644 index 00000000..1bab463b --- /dev/null +++ b/JackalWebHost2/Controllers/Mappings/LobbyControllerMappings.cs @@ -0,0 +1,23 @@ +using JackalWebHost2.Controllers.Models.Lobby; +using JackalWebHost2.Models.Lobby; + +namespace JackalWebHost2.Controllers.Mappings; + +public static class LobbyControllerMappings +{ + public static LobbyModel ToDto(this Lobby lobby) => + new() + { + Id = lobby.Id, + OwnerId = lobby.OwnerId, + LobbyMembers = lobby.LobbyMembers.Values.ToDictionary(x => x.UserId, x => new LobbyMemberModel + { + UserId = x.UserId, + UserName = x.UserName, + TeamId = x.TeamId + }), + GameSettings = lobby.GameSettings, + NumberOfPlayers = lobby.NumberOfPlayers, + GameId = lobby.GameId + }; +} \ No newline at end of file diff --git a/JackalWebHost2/Controllers/Models/CheckRequest.cs b/JackalWebHost2/Controllers/Models/CheckRequest.cs new file mode 100644 index 00000000..2757604b --- /dev/null +++ b/JackalWebHost2/Controllers/Models/CheckRequest.cs @@ -0,0 +1,6 @@ +namespace JackalWebHost2.Controllers.Models; + +public class CheckRequest +{ + +} \ No newline at end of file diff --git a/JackalWebHost2/Controllers/Models/CheckResponse.cs b/JackalWebHost2/Controllers/Models/CheckResponse.cs new file mode 100644 index 00000000..2fd98f53 --- /dev/null +++ b/JackalWebHost2/Controllers/Models/CheckResponse.cs @@ -0,0 +1,8 @@ +namespace JackalWebHost2.Controllers.Models; + +public class CheckResponse +{ + public UserModel? User { get; set; } + + public bool IsAuthorised => User != null; +} \ No newline at end of file diff --git a/JackalWebHost2/Controllers/Models/ErrorModel.cs b/JackalWebHost2/Controllers/Models/ErrorModel.cs new file mode 100644 index 00000000..ecf28b01 --- /dev/null +++ b/JackalWebHost2/Controllers/Models/ErrorModel.cs @@ -0,0 +1,25 @@ +using JackalWebHost2.Exceptions; + +namespace JackalWebHost2.Controllers.Models; + +public class ErrorModel +{ + public ErrorModel(string errorMessage, string errorCode) + { + ErrorMessage = errorMessage; + ErrorCode = errorCode; + } + + public ErrorModel(BusinessException exception) + { + ErrorMessage = exception.ErrorMessage; + ErrorCode = exception.ErrorCode; + } + + + public string ErrorMessage { get; } + + public string ErrorCode { get; } + + public bool Error => true; +} \ No newline at end of file diff --git a/JackalWebHost2/Controllers/Models/Game/NetGameRequest.cs b/JackalWebHost2/Controllers/Models/Game/NetGameRequest.cs new file mode 100644 index 00000000..4fa17d6c --- /dev/null +++ b/JackalWebHost2/Controllers/Models/Game/NetGameRequest.cs @@ -0,0 +1,11 @@ +using JackalWebHost2.Models; + +namespace JackalWebHost2.Controllers.Models.Game +{ + public class NetGameRequest + { + public long Id { get; set; } + + public GameSettings Settings { get; set; } + } +} diff --git a/JackalWebHost2/Controllers/Models/Game/NetGameResponse.cs b/JackalWebHost2/Controllers/Models/Game/NetGameResponse.cs new file mode 100644 index 00000000..b47ba888 --- /dev/null +++ b/JackalWebHost2/Controllers/Models/Game/NetGameResponse.cs @@ -0,0 +1,21 @@ +using System.Diagnostics.CodeAnalysis; +using JackalWebHost2.Models; + +namespace JackalWebHost2.Controllers.Models.Game +{ + public class NetGameResponse + { + public long Id { get; set; } + public long CreatorId { get; set; } + public long? GameId { get; set; } + + [NotNull] + public GameSettings? Settings { get; set; } + + [NotNull] + public long[]? Viewers { get; set; } + + [NotNull] + public HashSet? Users { get; set; } + } +} diff --git a/JackalWebHost2/Controllers/Models/Game/NetUserRequest.cs b/JackalWebHost2/Controllers/Models/Game/NetUserRequest.cs new file mode 100644 index 00000000..5a25fa69 --- /dev/null +++ b/JackalWebHost2/Controllers/Models/Game/NetUserRequest.cs @@ -0,0 +1,7 @@ +namespace JackalWebHost2.Controllers.Models.Game +{ + public class NetUserRequest + { + public long Id { get; set; } + } +} diff --git a/JackalWebHost2/Controllers/Models/Leaderboard/LeaderboardOrderByType.cs b/JackalWebHost2/Controllers/Models/Leaderboard/LeaderboardOrderByType.cs new file mode 100644 index 00000000..49f837f8 --- /dev/null +++ b/JackalWebHost2/Controllers/Models/Leaderboard/LeaderboardOrderByType.cs @@ -0,0 +1,22 @@ +namespace JackalWebHost2.Controllers.Models.Leaderboard; + +/// +/// Сортировка лидеров +/// +public enum LeaderboardOrderByType +{ + /// + /// По добытым монетам + /// + TotalCoins = 0, + + /// + /// По победам + /// + TotalWin = 1, + + /// + /// По играм + /// + GamesCount = 2 +} \ No newline at end of file diff --git a/JackalWebHost2/Controllers/Models/Leaderboard/LeaderboardResponse.cs b/JackalWebHost2/Controllers/Models/Leaderboard/LeaderboardResponse.cs new file mode 100644 index 00000000..686fecfd --- /dev/null +++ b/JackalWebHost2/Controllers/Models/Leaderboard/LeaderboardResponse.cs @@ -0,0 +1,8 @@ +using Jackal.Core.Players; + +namespace JackalWebHost2.Controllers.Models.Leaderboard; + +public class LeaderboardResponse +{ + public List Leaderboard { get; set; } +} \ No newline at end of file diff --git a/JackalWebHost2/Controllers/Models/LoadGameRequest.cs b/JackalWebHost2/Controllers/Models/LoadGameRequest.cs new file mode 100644 index 00000000..9b225d34 --- /dev/null +++ b/JackalWebHost2/Controllers/Models/LoadGameRequest.cs @@ -0,0 +1,6 @@ +namespace JackalWebHost2.Controllers.Models; + +public class LoadGameRequest +{ + public long GameId { get; set; } +} \ No newline at end of file diff --git a/JackalWebHost2/Controllers/Models/LoadGameResponse.cs b/JackalWebHost2/Controllers/Models/LoadGameResponse.cs new file mode 100644 index 00000000..bb93e90f --- /dev/null +++ b/JackalWebHost2/Controllers/Models/LoadGameResponse.cs @@ -0,0 +1,27 @@ +using Jackal.Core; +using JackalWebHost2.Models; + +namespace JackalWebHost2.Controllers.Models; + +public class LoadGameResponse +{ + public long GameId { get; set; } + + public GameModeType GameMode { get; set; } + + public string TilesPackName { get; set; } + + public List Pirates { get; set; } + + public DrawMap Map { get; set; } + + public int MapId { get; set; } + + public GameStatistics Stats { get; set; } + + public List Teams { get; set; } + + public List TeamScores { get; set; } + + public List Moves { get; set; } +} \ No newline at end of file diff --git a/JackalWebHost2/Controllers/Models/Lobby/AssignTeamRequest.cs b/JackalWebHost2/Controllers/Models/Lobby/AssignTeamRequest.cs new file mode 100644 index 00000000..9943b7d5 --- /dev/null +++ b/JackalWebHost2/Controllers/Models/Lobby/AssignTeamRequest.cs @@ -0,0 +1,10 @@ +namespace JackalWebHost2.Controllers.Models.Lobby; + +public class AssignTeamRequest +{ + public string LobbyId { get; set; } + + public long UserId { get; set; } + + public long? TeamId { get; set; } +} \ No newline at end of file diff --git a/JackalWebHost2/Controllers/Models/Lobby/AssignTeamResponse.cs b/JackalWebHost2/Controllers/Models/Lobby/AssignTeamResponse.cs new file mode 100644 index 00000000..fed7eb43 --- /dev/null +++ b/JackalWebHost2/Controllers/Models/Lobby/AssignTeamResponse.cs @@ -0,0 +1,5 @@ +namespace JackalWebHost2.Controllers.Models.Lobby; + +public class AssignTeamResponse +{ +} \ No newline at end of file diff --git a/JackalWebHost2/Controllers/Models/Lobby/CreateLobbyRequest.cs b/JackalWebHost2/Controllers/Models/Lobby/CreateLobbyRequest.cs new file mode 100644 index 00000000..46df4059 --- /dev/null +++ b/JackalWebHost2/Controllers/Models/Lobby/CreateLobbyRequest.cs @@ -0,0 +1,8 @@ +using JackalWebHost2.Models; + +namespace JackalWebHost2.Controllers.Models.Lobby; + +public class CreateLobbyRequest +{ + public GameSettings Settings { get; set; } +} \ No newline at end of file diff --git a/JackalWebHost2/Controllers/Models/Lobby/CreateLobbyResponse.cs b/JackalWebHost2/Controllers/Models/Lobby/CreateLobbyResponse.cs new file mode 100644 index 00000000..d6abffb7 --- /dev/null +++ b/JackalWebHost2/Controllers/Models/Lobby/CreateLobbyResponse.cs @@ -0,0 +1,6 @@ +namespace JackalWebHost2.Controllers.Models.Lobby; + +public class CreateLobbyResponse +{ + public LobbyModel Lobby { get; set; } +} \ No newline at end of file diff --git a/JackalWebHost2/Controllers/Models/Lobby/GetLobbyRequest.cs b/JackalWebHost2/Controllers/Models/Lobby/GetLobbyRequest.cs new file mode 100644 index 00000000..7ab976e3 --- /dev/null +++ b/JackalWebHost2/Controllers/Models/Lobby/GetLobbyRequest.cs @@ -0,0 +1,6 @@ +namespace JackalWebHost2.Controllers.Models.Lobby; + +public class GetLobbyRequest +{ + public string LobbyId { get; set; } +} \ No newline at end of file diff --git a/JackalWebHost2/Controllers/Models/Lobby/GetLobbyResponse.cs b/JackalWebHost2/Controllers/Models/Lobby/GetLobbyResponse.cs new file mode 100644 index 00000000..39672f5a --- /dev/null +++ b/JackalWebHost2/Controllers/Models/Lobby/GetLobbyResponse.cs @@ -0,0 +1,6 @@ +namespace JackalWebHost2.Controllers.Models.Lobby; + +public class GetLobbyResponse +{ + public LobbyModel Lobby { get; set; } +} \ No newline at end of file diff --git a/JackalWebHost2/Controllers/Models/Lobby/JoinLobbyRequest.cs b/JackalWebHost2/Controllers/Models/Lobby/JoinLobbyRequest.cs new file mode 100644 index 00000000..eeaf0dbf --- /dev/null +++ b/JackalWebHost2/Controllers/Models/Lobby/JoinLobbyRequest.cs @@ -0,0 +1,6 @@ +namespace JackalWebHost2.Controllers.Models.Lobby; + +public class JoinLobbyRequest +{ + public string LobbyId { get; set; } +} \ No newline at end of file diff --git a/JackalWebHost2/Controllers/Models/Lobby/JoinLobbyResponse.cs b/JackalWebHost2/Controllers/Models/Lobby/JoinLobbyResponse.cs new file mode 100644 index 00000000..cc713395 --- /dev/null +++ b/JackalWebHost2/Controllers/Models/Lobby/JoinLobbyResponse.cs @@ -0,0 +1,6 @@ +namespace JackalWebHost2.Controllers.Models.Lobby; + +public class JoinLobbyResponse +{ + public LobbyModel Lobby { get; set; } +} \ No newline at end of file diff --git a/JackalWebHost2/Controllers/Models/Lobby/KickFromLobbyRequest.cs b/JackalWebHost2/Controllers/Models/Lobby/KickFromLobbyRequest.cs new file mode 100644 index 00000000..aae187f1 --- /dev/null +++ b/JackalWebHost2/Controllers/Models/Lobby/KickFromLobbyRequest.cs @@ -0,0 +1,8 @@ +namespace JackalWebHost2.Controllers.Models.Lobby; + +public class KickFromLobbyRequest +{ + public string LobbyId { get; set; } + + public long TargetUserId { get; set; } +} \ No newline at end of file diff --git a/JackalWebHost2/Controllers/Models/Lobby/KickFromLobbyResponse.cs b/JackalWebHost2/Controllers/Models/Lobby/KickFromLobbyResponse.cs new file mode 100644 index 00000000..1e87d24e --- /dev/null +++ b/JackalWebHost2/Controllers/Models/Lobby/KickFromLobbyResponse.cs @@ -0,0 +1,5 @@ +namespace JackalWebHost2.Controllers.Models.Lobby; + +public class KickFromLobbyResponse +{ +} \ No newline at end of file diff --git a/JackalWebHost2/Controllers/Models/Lobby/LeaveLobbyRequest.cs b/JackalWebHost2/Controllers/Models/Lobby/LeaveLobbyRequest.cs new file mode 100644 index 00000000..f6205781 --- /dev/null +++ b/JackalWebHost2/Controllers/Models/Lobby/LeaveLobbyRequest.cs @@ -0,0 +1,5 @@ +namespace JackalWebHost2.Controllers.Models.Lobby; + +public class LeaveLobbyRequest +{ +} \ No newline at end of file diff --git a/JackalWebHost2/Controllers/Models/Lobby/LeaveLobbyResponse.cs b/JackalWebHost2/Controllers/Models/Lobby/LeaveLobbyResponse.cs new file mode 100644 index 00000000..addfac1e --- /dev/null +++ b/JackalWebHost2/Controllers/Models/Lobby/LeaveLobbyResponse.cs @@ -0,0 +1,5 @@ +namespace JackalWebHost2.Controllers.Models.Lobby; + +public class LeaveLobbyResponse +{ +} \ No newline at end of file diff --git a/JackalWebHost2/Controllers/Models/Lobby/LobbyMemberModel.cs b/JackalWebHost2/Controllers/Models/Lobby/LobbyMemberModel.cs new file mode 100644 index 00000000..9faea6ce --- /dev/null +++ b/JackalWebHost2/Controllers/Models/Lobby/LobbyMemberModel.cs @@ -0,0 +1,10 @@ +namespace JackalWebHost2.Controllers.Models.Lobby; + +public class LobbyMemberModel +{ + public long UserId { get; set; } + + public string UserName { get; set; } + + public long? TeamId { get; set; } +} \ No newline at end of file diff --git a/JackalWebHost2/Controllers/Models/Lobby/LobbyModel.cs b/JackalWebHost2/Controllers/Models/Lobby/LobbyModel.cs new file mode 100644 index 00000000..1d68c1cb --- /dev/null +++ b/JackalWebHost2/Controllers/Models/Lobby/LobbyModel.cs @@ -0,0 +1,18 @@ +using JackalWebHost2.Models; + +namespace JackalWebHost2.Controllers.Models.Lobby; + +public class LobbyModel +{ + public string Id { get; set; } = ""; + + public long OwnerId { get; set; } + + public Dictionary LobbyMembers { get; set; } = new(); + + public GameSettings GameSettings { get; set; } + + public int NumberOfPlayers { get; set; } + + public long? GameId { get; set; } +} \ No newline at end of file diff --git a/JackalWebHost2/Controllers/Models/Lobby/StartGameFromLobbyRequest.cs b/JackalWebHost2/Controllers/Models/Lobby/StartGameFromLobbyRequest.cs new file mode 100644 index 00000000..1ed3baba --- /dev/null +++ b/JackalWebHost2/Controllers/Models/Lobby/StartGameFromLobbyRequest.cs @@ -0,0 +1,6 @@ +namespace JackalWebHost2.Controllers.Models.Lobby; + +public class StartGameFromLobbyRequest +{ + public string LobbyId { get; set; } +} \ No newline at end of file diff --git a/JackalWebHost2/Controllers/Models/Lobby/StartGameFromLobbyResponse.cs b/JackalWebHost2/Controllers/Models/Lobby/StartGameFromLobbyResponse.cs new file mode 100644 index 00000000..d9e57247 --- /dev/null +++ b/JackalWebHost2/Controllers/Models/Lobby/StartGameFromLobbyResponse.cs @@ -0,0 +1,6 @@ +namespace JackalWebHost2.Controllers.Models.Lobby; + +public class StartGameFromLobbyResponse +{ + public LobbyModel Lobby { get; set; } +} \ No newline at end of file diff --git a/JackalWebHost2/Controllers/Models/LogoutRequest.cs b/JackalWebHost2/Controllers/Models/LogoutRequest.cs new file mode 100644 index 00000000..faf43853 --- /dev/null +++ b/JackalWebHost2/Controllers/Models/LogoutRequest.cs @@ -0,0 +1,6 @@ +namespace JackalWebHost2.Controllers.Models; + +public class LogoutRequest +{ + +} \ No newline at end of file diff --git a/JackalWebHost2/Controllers/Models/LogoutResponse.cs b/JackalWebHost2/Controllers/Models/LogoutResponse.cs new file mode 100644 index 00000000..12706ef1 --- /dev/null +++ b/JackalWebHost2/Controllers/Models/LogoutResponse.cs @@ -0,0 +1,6 @@ +namespace JackalWebHost2.Controllers.Models; + +public class LogoutResponse +{ + +} \ No newline at end of file diff --git a/JackalWebHost2/Controllers/Models/Map/CheckLandingRequest.cs b/JackalWebHost2/Controllers/Models/Map/CheckLandingRequest.cs new file mode 100644 index 00000000..3fd9e7b6 --- /dev/null +++ b/JackalWebHost2/Controllers/Models/Map/CheckLandingRequest.cs @@ -0,0 +1,22 @@ +namespace JackalWebHost2.Controllers.Models.Map; + +/// +/// Запрос для проверки места высадки +/// +public class CheckLandingRequest +{ + /// + /// ИД карты, по нему генерируется расположение клеток + /// + public int MapId { get; set; } + + /// + /// Размер стороны карты с учетом воды + /// + public int MapSize { get; set; } + + /// + /// Название игрового набора клеток + /// + public string? TilesPackName { get; set; } +} \ No newline at end of file diff --git a/JackalWebHost2/Controllers/Models/Map/CheckLandingResponse.cs b/JackalWebHost2/Controllers/Models/Map/CheckLandingResponse.cs new file mode 100644 index 00000000..e07adaea --- /dev/null +++ b/JackalWebHost2/Controllers/Models/Map/CheckLandingResponse.cs @@ -0,0 +1,19 @@ +using JackalWebHost2.Models.Map; + +namespace JackalWebHost2.Controllers.Models.Map; + +/// +/// Результат проверки места высадки +/// +public class CheckLandingResponse +{ + /// + /// Позиция + /// + public MapPositionId Position { get; set; } + + /// + /// Сложность + /// + public DifficultyLevel Difficulty { get; set; } +} \ No newline at end of file diff --git a/JackalWebHost2/Controllers/Models/Map/TilesTypeModel.cs b/JackalWebHost2/Controllers/Models/Map/TilesTypeModel.cs new file mode 100644 index 00000000..dbfd8e0c --- /dev/null +++ b/JackalWebHost2/Controllers/Models/Map/TilesTypeModel.cs @@ -0,0 +1,8 @@ +namespace JackalWebHost2.Controllers.Models.Map; + +public class TilesTypeModel +{ + public string Name { get; set; } + + public int Count { get; set; } +} \ No newline at end of file diff --git a/JackalWebHost2/Controllers/Models/RegisterRequest.cs b/JackalWebHost2/Controllers/Models/RegisterRequest.cs new file mode 100644 index 00000000..bb8d88a3 --- /dev/null +++ b/JackalWebHost2/Controllers/Models/RegisterRequest.cs @@ -0,0 +1,6 @@ +namespace JackalWebHost2.Controllers.Models; + +public class RegisterRequest +{ + public string Login { get; set; } +} \ No newline at end of file diff --git a/JackalWebHost2/Controllers/Models/RegisterResponse.cs b/JackalWebHost2/Controllers/Models/RegisterResponse.cs new file mode 100644 index 00000000..b7cc393e --- /dev/null +++ b/JackalWebHost2/Controllers/Models/RegisterResponse.cs @@ -0,0 +1,8 @@ +namespace JackalWebHost2.Controllers.Models; + +public class RegisterResponse +{ + public UserModel? User { get; set; } + + public string? Token { get; set; } +} \ No newline at end of file diff --git a/JackalWebHost2/Controllers/Models/Services/AllActiveGamesResponse.cs b/JackalWebHost2/Controllers/Models/Services/AllActiveGamesResponse.cs new file mode 100644 index 00000000..3a80ad1a --- /dev/null +++ b/JackalWebHost2/Controllers/Models/Services/AllActiveGamesResponse.cs @@ -0,0 +1,20 @@ +using JackalWebHost2.Data.Entities; + +namespace JackalWebHost2.Controllers.Models.Services +{ + public class AllActiveGamesResponse + { + public IList? GamesEntries { get; set; } + } + + public class ActiveGameInfo + { + public long GameId { get; set; } + + public CacheEntryUser? Creator { get; set; } + + public CacheEntryUser[]? Players { get; set; } + + public long TimeStamp { get; set; } + } +} diff --git a/JackalWebHost2/Controllers/Models/StartGameRequest.cs b/JackalWebHost2/Controllers/Models/StartGameRequest.cs new file mode 100644 index 00000000..2152a7ff --- /dev/null +++ b/JackalWebHost2/Controllers/Models/StartGameRequest.cs @@ -0,0 +1,8 @@ +using JackalWebHost2.Models; + +namespace JackalWebHost2.Controllers.Models; + +public class StartGameRequest +{ + public GameSettings Settings { get; set; } +} \ No newline at end of file diff --git a/JackalWebHost2/Controllers/Models/StartGameResponse.cs b/JackalWebHost2/Controllers/Models/StartGameResponse.cs new file mode 100644 index 00000000..9ffc623a --- /dev/null +++ b/JackalWebHost2/Controllers/Models/StartGameResponse.cs @@ -0,0 +1,25 @@ +using Jackal.Core; +using JackalWebHost2.Models; + +namespace JackalWebHost2.Controllers.Models; + +public class StartGameResponse +{ + public long GameId { get; set; } + + public GameModeType GameMode { get; set; } + + public string TilesPackName { get; set; } + + public List Pirates { get; set; } + + public DrawMap Map { get; set; } + + public int MapId { get; set; } + + public GameStatistics Stats { get; set; } + + public List Teams { get; set; } + + public List Moves { get; set; } +} \ No newline at end of file diff --git a/JackalWebHost2/Controllers/Models/TurnGameRequest.cs b/JackalWebHost2/Controllers/Models/TurnGameRequest.cs new file mode 100644 index 00000000..fcf37f7d --- /dev/null +++ b/JackalWebHost2/Controllers/Models/TurnGameRequest.cs @@ -0,0 +1,10 @@ +namespace JackalWebHost2.Controllers.Models; + +public class TurnGameRequest +{ + public long GameId { get; set; } + + public int? TurnNum { get; set; } + + public Guid? PirateId { get; set; } +} \ No newline at end of file diff --git a/JackalWebHost2/Controllers/Models/TurnGameResponse.cs b/JackalWebHost2/Controllers/Models/TurnGameResponse.cs new file mode 100644 index 00000000..7e8cfd92 --- /dev/null +++ b/JackalWebHost2/Controllers/Models/TurnGameResponse.cs @@ -0,0 +1,16 @@ +using JackalWebHost2.Models; + +namespace JackalWebHost2.Controllers.Models; + +public class TurnGameResponse +{ + public List PirateChanges { get; set; } + + public List Changes { get; set; } + + public GameStatistics Stats { get; set; } + + public List TeamScores { get; set; } + + public List Moves { get; set; } +} \ No newline at end of file diff --git a/JackalWebHost2/Controllers/Models/UserModel.cs b/JackalWebHost2/Controllers/Models/UserModel.cs new file mode 100644 index 00000000..803b511b --- /dev/null +++ b/JackalWebHost2/Controllers/Models/UserModel.cs @@ -0,0 +1,10 @@ +namespace JackalWebHost2.Controllers.Models; + +public class UserModel +{ + public long Id { get; set; } + + public required string Login { get; set; } + + public required string Rank { get; set; } +} \ No newline at end of file diff --git a/JackalWebHost2/Controllers/Models/ValidationEntryModel.cs b/JackalWebHost2/Controllers/Models/ValidationEntryModel.cs new file mode 100644 index 00000000..38ac7bed --- /dev/null +++ b/JackalWebHost2/Controllers/Models/ValidationEntryModel.cs @@ -0,0 +1,8 @@ +namespace JackalWebHost2.Controllers.Models; + +public class ValidationEntryModel +{ + public string? Property { get; set; } + + public string[] Errors { get; set; } = []; +} \ No newline at end of file diff --git a/JackalWebHost2/Controllers/Models/ValidationErrorModel.cs b/JackalWebHost2/Controllers/Models/ValidationErrorModel.cs new file mode 100644 index 00000000..5018fa5d --- /dev/null +++ b/JackalWebHost2/Controllers/Models/ValidationErrorModel.cs @@ -0,0 +1,13 @@ +using JackalWebHost2.Exceptions; + +namespace JackalWebHost2.Controllers.Models; + +public class ValidationErrorModel : ErrorModel +{ + public ValidationErrorModel(string errorMessage, ValidationEntryModel[] details) : base(errorMessage, ErrorCodes.ValidationError) + { + Details = details; + } + + public ValidationEntryModel[] Details { get; set; } +} \ No newline at end of file diff --git a/JackalWebHost2/Controllers/V1/AuthController.cs b/JackalWebHost2/Controllers/V1/AuthController.cs new file mode 100644 index 00000000..51edf921 --- /dev/null +++ b/JackalWebHost2/Controllers/V1/AuthController.cs @@ -0,0 +1,81 @@ +using JackalWebHost2.Controllers.Models; +using JackalWebHost2.Data.Interfaces; +using JackalWebHost2.Exceptions; +using JackalWebHost2.Infrastructure.Auth; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace JackalWebHost2.Controllers.V1; + +[FastAuth] +[Route("/api/v1/auth")] +public class AuthController(IUserRepository userRepository) : Controller +{ + /// + /// Зарегистрироваться (без авторизации) + /// + [AllowAnonymous] + [HttpPost("register")] + public async Task Register([FromBody] RegisterRequest request, CancellationToken token) + { + if (HttpContext.User.Identity?.IsAuthenticated == true) + { + throw new UserIsAlreadyLoggedInException(); + } + + var user = await userRepository.GetUser(request.Login, token) + ?? await userRepository.CreateUser(request.Login, token); + + return new RegisterResponse + { + User = new UserModel + { + Id = user.Id, + Login = user.Login, + Rank = user.Rank + }, + Token = await FastAuthJwtBearerHelper.SignInUser(HttpContext, user) + }; + } + + /// + /// Проверить состояние авторизации + /// + [AllowAnonymous] + [HttpPost("check")] + public async Task Check([FromBody] CheckRequest request, CancellationToken token) + { + if (HttpContext.User.Identity?.IsAuthenticated == false) + { + return new CheckResponse(); + } + + var tokenUser = FastAuthJwtBearerHelper.ExtractUser(HttpContext.User); + var user = await userRepository.GetUser(tokenUser.Id, token); + if(user == null) + { + return new CheckResponse(); + } + + return new CheckResponse + { + User = new UserModel + { + Id = user.Id, + Login = user.Login, + Rank = user.Rank + } + }; + } + + /// + /// Выйти + /// + [HttpPost("logout")] + public async Task Logout([FromBody] LogoutRequest request, CancellationToken token) + { + // TODO: уведомляем сервер о разлогине + var user = FastAuthJwtBearerHelper.ExtractUser(HttpContext.User); + return new LogoutResponse(); + } +} \ No newline at end of file diff --git a/JackalWebHost2/Controllers/V1/LeaderboardController.cs b/JackalWebHost2/Controllers/V1/LeaderboardController.cs new file mode 100644 index 00000000..100d0594 --- /dev/null +++ b/JackalWebHost2/Controllers/V1/LeaderboardController.cs @@ -0,0 +1,21 @@ +using JackalWebHost2.Controllers.Models.Leaderboard; +using JackalWebHost2.Data.Interfaces; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace JackalWebHost2.Controllers.V1; + +[AllowAnonymous] +[Route("/api/v1/leaderboard")] +public class LeaderboardController(IGamePlayerRepository gamePlayerRepository) : Controller +{ + /// + /// Таблица лидеров + /// + [HttpGet] + public async Task GetLeaderboard(LeaderboardOrderByType? orderBy) => + new() + { + Leaderboard = await gamePlayerRepository.GetLeaderboard(orderBy ?? LeaderboardOrderByType.TotalWin) + }; +} \ No newline at end of file diff --git a/JackalWebHost2/Controllers/V1/LobbyController.cs b/JackalWebHost2/Controllers/V1/LobbyController.cs new file mode 100644 index 00000000..ab318df5 --- /dev/null +++ b/JackalWebHost2/Controllers/V1/LobbyController.cs @@ -0,0 +1,107 @@ +using JackalWebHost2.Controllers.Mappings; +using JackalWebHost2.Controllers.Models.Lobby; +using JackalWebHost2.Infrastructure.Auth; +using JackalWebHost2.Services; +using Microsoft.AspNetCore.Mvc; + +namespace JackalWebHost2.Controllers.V1; + +[FastAuth] +[Route("/api/v1/lobby")] +public class LobbyController(ILobbyService lobbyService) : Controller +{ + /// + /// Создать лобби с заданными параметрами + /// + [HttpPost("create-lobby")] + public async Task CreateLobby([FromBody] CreateLobbyRequest request, CancellationToken token) + { + // todo Валидировать приходящий null json по всем контроллерам + + var user = FastAuthJwtBearerHelper.ExtractUser(HttpContext.User); + var result = await lobbyService.CreateLobby(user, request.Settings, token); + + return new CreateLobbyResponse + { + Lobby = result.ToDto() + }; + } + + /// + /// Присоединиться к лобби + /// + [HttpPost("join-lobby")] + public async Task JoinLobby([FromBody] JoinLobbyRequest request, CancellationToken token) + { + var user = FastAuthJwtBearerHelper.ExtractUser(HttpContext.User); + var result = await lobbyService.JoinLobby(request.LobbyId, user, token); + + return new JoinLobbyResponse + { + Lobby = result.ToDto() + }; + } + + /// + /// Покинуть лобби + /// + [HttpPost("leave-lobby")] + public async Task LeaveLobby([FromBody] LeaveLobbyRequest request, CancellationToken token) + { + var user = FastAuthJwtBearerHelper.ExtractUser(HttpContext.User); + await lobbyService.LeaveLobby(user, token); + return new LeaveLobbyResponse(); + } + + /// + /// Получить информацию о лобби + /// + [HttpPost("get-lobby")] + public async Task GetLobby([FromBody] GetLobbyRequest request, CancellationToken token) + { + var user = FastAuthJwtBearerHelper.ExtractUser(HttpContext.User); + var result = await lobbyService.GetLobbyInfo(request.LobbyId, user, token); + + return new GetLobbyResponse + { + Lobby = result.ToDto() + }; + } + + /// + /// Выгнать игрока из лобби + /// + [HttpPost("kick-from-lobby")] + public async Task KickPlayer([FromBody] KickFromLobbyRequest request, CancellationToken token) + { + var user = FastAuthJwtBearerHelper.ExtractUser(HttpContext.User); + await lobbyService.KickPlayer(request.LobbyId, user, request.TargetUserId, token); + return new KickFromLobbyResponse(); + } + + /// + /// Сменить команду для игрока + /// + [HttpPost("assign-team")] + public async Task AssignTeam([FromBody] AssignTeamRequest request, CancellationToken token) + { + var user = FastAuthJwtBearerHelper.ExtractUser(HttpContext.User); + await lobbyService.AssignTeam(request.LobbyId, user, request.UserId, request.TeamId, token); + return new AssignTeamResponse(); + } + + /// + /// Начать игру из лобби + /// + [HttpPost("start-game")] + public async Task StartGame([FromBody] StartGameFromLobbyRequest request, CancellationToken token) + { + var user = FastAuthJwtBearerHelper.ExtractUser(HttpContext.User); + var result = await lobbyService.StartGame(request.LobbyId, user, token); + + return new StartGameFromLobbyResponse + { + Lobby = result.ToDto() + }; + } +} \ No newline at end of file diff --git a/JackalWebHost2/Controllers/V1/MapController.cs b/JackalWebHost2/Controllers/V1/MapController.cs new file mode 100644 index 00000000..30a1784c --- /dev/null +++ b/JackalWebHost2/Controllers/V1/MapController.cs @@ -0,0 +1,57 @@ +using Jackal.Core.MapGenerator.TilesPack; +using JackalWebHost2.Controllers.Models.Map; +using JackalWebHost2.Models.Map; +using JackalWebHost2.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace JackalWebHost2.Controllers.V1; + +[AllowAnonymous] +[Route("/api/v1/map")] +public class MapController(IMapService mapService) : Controller +{ + /// + /// Список названий игровых наборов, + /// используется при создании игры + /// + [HttpGet("tiles-pack-names")] + public List TilesPackNames() + { + return TilesPackFactory.GetAll(); + } + + /// + /// Состав клеток игрового набора + /// + [HttpGet("tiles-types")] + public List GetTiles(string tilesPackName) + { + return TilesPackFactory.Create(tilesPackName).AllTiles + .GroupBy(p => p.Type) + .Select(g => new TilesTypeModel + { + Name = g.Key.ToString(), + Count = g.Count() + }) + .OrderByDescending(g => g.Count) + .ToList(); + } + + /// + /// Проверить высадку + /// + [HttpGet("check-landing")] + public List CheckLanding([FromQuery] CheckLandingRequest request) + { + var landingResults = mapService.CheckLanding(request); + return landingResults.Select(ToCheckLandingResponse).ToList(); + } + + private static CheckLandingResponse ToCheckLandingResponse(CheckLandingResult landing) => + new() + { + Position = landing.Position, + Difficulty = landing.Difficulty + }; +} \ No newline at end of file diff --git a/JackalWebHost2/Controllers/Validators/CheckLandingRequestValidator.cs b/JackalWebHost2/Controllers/Validators/CheckLandingRequestValidator.cs new file mode 100644 index 00000000..52872555 --- /dev/null +++ b/JackalWebHost2/Controllers/Validators/CheckLandingRequestValidator.cs @@ -0,0 +1,15 @@ +using FluentValidation; +using JackalWebHost2.Controllers.Models.Map; + +namespace JackalWebHost2.Controllers.Validators; + +public class CheckLandingRequestValidator : AbstractValidator +{ + public CheckLandingRequestValidator() + { + RuleFor(x => x.MapSize) + .NotEmpty() + .Must(x => x is 5 or 7 or 9 or 11 or 13) + .WithMessage("MapSize must be 5, 7, 9, 11 or 13"); + } +} \ No newline at end of file diff --git a/JackalWebHost2/Controllers/Validators/RegisterRequestValidator.cs b/JackalWebHost2/Controllers/Validators/RegisterRequestValidator.cs new file mode 100644 index 00000000..2df4d1dd --- /dev/null +++ b/JackalWebHost2/Controllers/Validators/RegisterRequestValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using JackalWebHost2.Controllers.Models; + +namespace JackalWebHost2.Controllers.Validators; + +public class RegisterRequestValidator : AbstractValidator +{ + public RegisterRequestValidator() + { + RuleFor(x => x.Login) + .NotEmpty() + .MaximumLength(30); + } +} \ No newline at end of file diff --git a/JackalWebHost2/Data/Entities/CacheEntry.cs b/JackalWebHost2/Data/Entities/CacheEntry.cs new file mode 100644 index 00000000..a1575340 --- /dev/null +++ b/JackalWebHost2/Data/Entities/CacheEntry.cs @@ -0,0 +1,21 @@ +namespace JackalWebHost2.Data.Entities +{ + public class CacheEntry + { + public long ObjectId { get; init; } + + public CacheEntryUser Creator { get; init; } = new CacheEntryUser { Name = "Неизвестно" }; + + public CacheEntryUser[]? Players { get; set; } + + public long TimeStamp { get; set; } + } + + + public class CacheEntryUser + { + public long Id { get; init; } + + public string? Name { get; init; } + } +} diff --git a/JackalWebHost2/Data/Entities/GameEntity.cs b/JackalWebHost2/Data/Entities/GameEntity.cs new file mode 100644 index 00000000..fb4bd089 --- /dev/null +++ b/JackalWebHost2/Data/Entities/GameEntity.cs @@ -0,0 +1,60 @@ +using Jackal.Core; + +namespace JackalWebHost2.Data.Entities; + +public class GameEntity +{ + /// + /// ИД игры + /// + public long Id { get; set; } + + /// + /// ИД карты, по нему генерируется расположение клеток + /// + public int MapId { get; set; } + + /// + /// Название игрового набора клеток + /// + public string TilesPackName { get; set; } + + /// + /// Размер стороны карты с учетом воды + /// + public int MapSize { get; set; } + + /// + /// Режим игры + /// + public GameModeType GameMode { get; set; } + + /// + /// ИД пользователя создателя игры + /// + public long CreatorUserId { get; set; } + + /// + /// Дата создания + /// + public DateTime Created { get; set; } + + /// + /// Дата обновления + /// + public DateTime Updated { get; set; } + + /// + /// Номер хода + /// + public int TurnNumber { get; set; } + + /// + /// Игра завершена + /// + public bool GameOver { get; set; } + + public virtual UserEntity CreatorUser { get; set; } + + public virtual List GamePlayers { get; set; } +} \ No newline at end of file diff --git a/JackalWebHost2/Data/Entities/GamePlayerEntity.cs b/JackalWebHost2/Data/Entities/GamePlayerEntity.cs new file mode 100644 index 00000000..684cab40 --- /dev/null +++ b/JackalWebHost2/Data/Entities/GamePlayerEntity.cs @@ -0,0 +1,48 @@ +namespace JackalWebHost2.Data.Entities; + +public class GamePlayerEntity +{ + /// + /// ИД сущности + /// + public long Id { get; set; } + + /// + /// ИД игры + /// + public long GameId { get; set; } + + /// + /// ИД команды, уникально в рамках игры + /// + public int TeamId { get; set; } + + /// + /// ИД пользователя, если бот то null + /// + public long? UserId { get; set; } + + /// + /// Имя пользователя или бота + /// + public string PlayerName { get; set; } + + /// + /// ИД позиции относительно игровой карты + /// + public byte MapPositionId { get; set; } + + /// + /// Монеты на корабле + /// + public int Coins { get; set; } + + /// + /// Победитель игры + /// + public bool Winner { get; set; } + + public virtual GameEntity Game { get; set; } + + public virtual UserEntity User { get; set; } +} \ No newline at end of file diff --git a/JackalWebHost2/Data/Entities/UserEntity.cs b/JackalWebHost2/Data/Entities/UserEntity.cs new file mode 100644 index 00000000..0bcdc887 --- /dev/null +++ b/JackalWebHost2/Data/Entities/UserEntity.cs @@ -0,0 +1,23 @@ +namespace JackalWebHost2.Data.Entities; + +public class UserEntity +{ + /// + /// ИД пользователя + /// + public long Id { get; set; } + + /// + /// Логин пользователя + /// + public string Login { get; set; } + + /// + /// Дата создания + /// + public DateTime Created { get; set; } + + public virtual List Games { get; set; } + + public virtual List GamePlayers { get; set; } +} \ No newline at end of file diff --git a/JackalWebHost2/Data/EntityConfigurations/GameEntityConfiguration.cs b/JackalWebHost2/Data/EntityConfigurations/GameEntityConfiguration.cs new file mode 100644 index 00000000..93eb3166 --- /dev/null +++ b/JackalWebHost2/Data/EntityConfigurations/GameEntityConfiguration.cs @@ -0,0 +1,24 @@ +using JackalWebHost2.Data.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace JackalWebHost2.Data.EntityConfigurations; + +public class GameEntityConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder + .Property(b => b.Id) + .ValueGeneratedOnAdd(); + + builder + .Property(b => b.TilesPackName) + .HasMaxLength(30); + + builder + .HasOne(b => b.CreatorUser) + .WithMany(b => b.Games) + .HasForeignKey(g => g.CreatorUserId); + } +} \ No newline at end of file diff --git a/JackalWebHost2/Data/EntityConfigurations/GamePlayerEntityConfiguration.cs b/JackalWebHost2/Data/EntityConfigurations/GamePlayerEntityConfiguration.cs new file mode 100644 index 00000000..9761c7e8 --- /dev/null +++ b/JackalWebHost2/Data/EntityConfigurations/GamePlayerEntityConfiguration.cs @@ -0,0 +1,26 @@ +using JackalWebHost2.Data.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace JackalWebHost2.Data.EntityConfigurations; + +public class GamePlayerEntityConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder + .Property(b => b.Id) + .ValueGeneratedOnAdd(); + + builder + .HasOne(b => b.Game) + .WithMany(b => b.GamePlayers) + .HasForeignKey(b => b.GameId); + + builder + .HasOne(b => b.User) + .WithMany(b => b.GamePlayers) + .HasForeignKey(g => g.UserId) + .IsRequired(false); + } +} \ No newline at end of file diff --git a/JackalWebHost2/Data/EntityConfigurations/UserEntityConfiguration.cs b/JackalWebHost2/Data/EntityConfigurations/UserEntityConfiguration.cs new file mode 100644 index 00000000..00afd037 --- /dev/null +++ b/JackalWebHost2/Data/EntityConfigurations/UserEntityConfiguration.cs @@ -0,0 +1,24 @@ +using JackalWebHost2.Data.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace JackalWebHost2.Data.EntityConfigurations; + +public class UserEntityConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder + .Property(b => b.Id) + .ValueGeneratedOnAdd(); + + builder + .Property(b => b.Login) + .IsRequired() + .HasMaxLength(30); + + builder + .HasIndex(b => b.Login) + .IsUnique(); + } +} \ No newline at end of file diff --git a/JackalWebHost2/Data/Interfaces/IGamePlayerRepository.cs b/JackalWebHost2/Data/Interfaces/IGamePlayerRepository.cs new file mode 100644 index 00000000..c2d26cad --- /dev/null +++ b/JackalWebHost2/Data/Interfaces/IGamePlayerRepository.cs @@ -0,0 +1,9 @@ +using Jackal.Core.Players; +using JackalWebHost2.Controllers.Models.Leaderboard; + +namespace JackalWebHost2.Data.Interfaces; + +public interface IGamePlayerRepository +{ + Task> GetLeaderboard(LeaderboardOrderByType orderBy); +} \ No newline at end of file diff --git a/JackalWebHost2/Data/Interfaces/IGameRepository.cs b/JackalWebHost2/Data/Interfaces/IGameRepository.cs new file mode 100644 index 00000000..692ac104 --- /dev/null +++ b/JackalWebHost2/Data/Interfaces/IGameRepository.cs @@ -0,0 +1,10 @@ +using Jackal.Core; + +namespace JackalWebHost2.Data.Interfaces; + +public interface IGameRepository +{ + Task CreateGame(long userId, Game game); + + Task UpdateGame(long gameId, Game game); +} \ No newline at end of file diff --git a/JackalWebHost2/Data/Interfaces/ILobbyRepository.cs b/JackalWebHost2/Data/Interfaces/ILobbyRepository.cs new file mode 100644 index 00000000..46e9e14b --- /dev/null +++ b/JackalWebHost2/Data/Interfaces/ILobbyRepository.cs @@ -0,0 +1,51 @@ +using JackalWebHost2.Models.Lobby; + +namespace JackalWebHost2.Data.Interfaces; + +public interface ILobbyRepository +{ + /// + /// Создать лобби + /// + Task CreateLobby(Lobby lobby, CancellationToken token); + + /// + /// Добавить пользователя в лобби + /// + Task AddUserToLobby(string lobbyId, LobbyMember lobbyMember, CancellationToken token); + + /// + /// Получить лобби по пользователю + /// + Task GetLobbyByUser(long userId, CancellationToken token); + + /// + /// Получить данные по лобби + /// + Task GetLobbyInfo(string lobbyId, bool includeDeleted, CancellationToken token); + + /// + /// Удалить пользователя из всех лобби + /// + Task RemoveUserFromLobbies(long userId, CancellationToken token); + + /// + /// Обновить метку времени lastSeen пользователя лобби + /// + Task UpdateUserKeepAlive(string lobbyId, long userId, DateTimeOffset time, CancellationToken token); + + /// + /// Закрыть лобби и проставить идентификатор игры (если есть) + /// + Task Close(string lobbyId, DateTimeOffset time, long? gameId, long[]? gameMembers, CancellationToken token); + + /// + /// Выгнать всех пользователей из лобби + /// + Task RemoveUsersFromLobby(string lobbyId, CancellationToken token); + + /// + /// Назначить пользователю команду в лобби + /// + Task AssignTeam(string lobbyId, long userId, long? teamId, CancellationToken token); +} \ No newline at end of file diff --git a/JackalWebHost2/Data/Interfaces/IStateRepository.cs b/JackalWebHost2/Data/Interfaces/IStateRepository.cs new file mode 100644 index 00000000..d5970be2 --- /dev/null +++ b/JackalWebHost2/Data/Interfaces/IStateRepository.cs @@ -0,0 +1,40 @@ +using Jackal.Core; +using JackalWebHost2.Data.Entities; +using JackalWebHost2.Models; + +namespace JackalWebHost2.Data.Interfaces; + +public interface IStateRepository where T : class, ICompletable +{ + /// + /// Флаг наличия изменений + /// + bool HasChanges(); + + /// + /// Сброс флага наличия изменений + /// + void ResetChanges(); + + /// + /// Получить описание всех активных сущностей + /// + IList GetEntries(); + + /// + /// Получить сущность + /// + T? GetObject(long objectId); + + /// + /// Создать новую сущность + /// + void CreateObject(User user, long objectId, T value); + void CreateObject(User user, long objectId, T value, HashSet players); + + /// + /// Обновить сущность + /// + void UpdateObject(long objectId, T value); + void UpdateObject(long objectId, T value, HashSet? players); +} \ No newline at end of file diff --git a/JackalWebHost2/Data/Interfaces/IUserRepository.cs b/JackalWebHost2/Data/Interfaces/IUserRepository.cs new file mode 100644 index 00000000..d91df6cb --- /dev/null +++ b/JackalWebHost2/Data/Interfaces/IUserRepository.cs @@ -0,0 +1,23 @@ +using JackalWebHost2.Models; + +namespace JackalWebHost2.Data.Interfaces; + +public interface IUserRepository +{ + /// + /// Получить пользователя по идентификатору + /// + Task GetUser(long id, CancellationToken token); + + Task> GetUsers(long[] ids, CancellationToken token); + + /// + /// Получить пользователя по логину + /// + Task GetUser(string login, CancellationToken token); + + /// + /// Создать нового пользователя + /// + Task CreateUser(string login, CancellationToken token); +} \ No newline at end of file diff --git a/JackalWebHost2/Data/JackalDbContext.cs b/JackalWebHost2/Data/JackalDbContext.cs new file mode 100644 index 00000000..d181499f --- /dev/null +++ b/JackalWebHost2/Data/JackalDbContext.cs @@ -0,0 +1,21 @@ +using JackalWebHost2.Data.Entities; +using JackalWebHost2.Data.EntityConfigurations; +using Microsoft.EntityFrameworkCore; + +namespace JackalWebHost2.Data; + +public class JackalDbContext(DbContextOptions options) : DbContext(options) +{ + public DbSet Users { get; set; } + + public DbSet Games { get; set; } + + public DbSet GamePlayers { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.ApplyConfiguration(new UserEntityConfiguration()); + modelBuilder.ApplyConfiguration(new GameEntityConfiguration()); + modelBuilder.ApplyConfiguration(new GamePlayerEntityConfiguration()); + } +} \ No newline at end of file diff --git a/JackalWebHost2/Data/JackalDbContextFactory.cs b/JackalWebHost2/Data/JackalDbContextFactory.cs new file mode 100644 index 00000000..8751e75e --- /dev/null +++ b/JackalWebHost2/Data/JackalDbContextFactory.cs @@ -0,0 +1,17 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; + +namespace JackalWebHost2.Data; + +public class JackalDbContextFactory : IDesignTimeDbContextFactory +{ + private const string ConnectionString = "Server=127.0.0.1;Port=5432;Database=jackal;User Id=postgres;Password=postgres;"; + + public JackalDbContext CreateDbContext(string[] args) + { + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseNpgsql(ConnectionString); + + return new JackalDbContext(optionsBuilder.Options); + } +} \ No newline at end of file diff --git a/JackalWebHost2/Data/Repositories/GamePlayerRepository.cs b/JackalWebHost2/Data/Repositories/GamePlayerRepository.cs new file mode 100644 index 00000000..8daed0f9 --- /dev/null +++ b/JackalWebHost2/Data/Repositories/GamePlayerRepository.cs @@ -0,0 +1,47 @@ +using Jackal.Core.Players; +using JackalWebHost2.Controllers.Models.Leaderboard; +using JackalWebHost2.Data.Interfaces; +using Microsoft.EntityFrameworkCore; + +namespace JackalWebHost2.Data.Repositories; + +public class GamePlayerRepository(JackalDbContext jackalDbContext) : IGamePlayerRepository +{ + public async Task> GetLeaderboard(LeaderboardOrderByType orderBy) + { + TimeZoneInfo mskTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Russian Standard Time"); + + DateTime nowUtc = DateTime.UtcNow; + DateTime todayStartMsk = TimeZoneInfo.ConvertTimeFromUtc(nowUtc, mskTimeZone).Date; + + DateTime todayStartUtc = TimeZoneInfo.ConvertTimeToUtc(todayStartMsk, mskTimeZone); + DateTime weekStartUtc = TimeZoneInfo.ConvertTimeToUtc(todayStartMsk.AddDays(-7), mskTimeZone); + DateTime monthStartUtc = TimeZoneInfo.ConvertTimeToUtc(todayStartMsk.AddDays(-30), mskTimeZone); + + var bestWinningPlayers = await jackalDbContext.GamePlayers + .Where(p => p.Game.GameOver) + .GroupBy(p => p.PlayerName) + .Select(g => new GamePlayerStat + { + PlayerName = g.Key, + WinCountToday = g.Count(x => x.Game.Created >= todayStartUtc && x.Winner), + WinCountThisWeek = g.Count(x => x.Game.Created >= weekStartUtc && x.Winner), + WinCountThisMonth = g.Count(x => x.Game.Created >= monthStartUtc && x.Winner), + TotalWin = g.Count(x => x.Winner), + GamesCountToday = g.Count(x => x.Game.Created >= todayStartUtc), + GamesCountThisWeek = g.Count(x => x.Game.Created >= weekStartUtc), + GamesCountThisMonth = g.Count(x => x.Game.Created >= monthStartUtc), + GamesCountTotal = g.Count(), + TotalCoins = g.Sum(x => x.Coins) + }) + .ToListAsync(); + + return orderBy switch + { + LeaderboardOrderByType.TotalCoins => bestWinningPlayers.OrderByDescending(g => g.TotalCoins).ToList(), + LeaderboardOrderByType.TotalWin => bestWinningPlayers.OrderByDescending(g => g.TotalWin).ToList(), + LeaderboardOrderByType.GamesCount => bestWinningPlayers.OrderByDescending(g => g.GamesCountTotal).ToList(), + _ => throw new ArgumentOutOfRangeException(nameof(orderBy), orderBy, null) + }; + } +} \ No newline at end of file diff --git a/JackalWebHost2/Data/Repositories/GamePlayerRepositoryStub.cs b/JackalWebHost2/Data/Repositories/GamePlayerRepositoryStub.cs new file mode 100644 index 00000000..62e129af --- /dev/null +++ b/JackalWebHost2/Data/Repositories/GamePlayerRepositoryStub.cs @@ -0,0 +1,13 @@ +using Jackal.Core.Players; +using JackalWebHost2.Controllers.Models.Leaderboard; +using JackalWebHost2.Data.Interfaces; + +namespace JackalWebHost2.Data.Repositories; + +public class GamePlayerRepositoryStub : IGamePlayerRepository +{ + public Task> GetLeaderboard(LeaderboardOrderByType orderBy) + { + return Task.FromResult(new List()); + } +} \ No newline at end of file diff --git a/JackalWebHost2/Data/Repositories/GameRepository.cs b/JackalWebHost2/Data/Repositories/GameRepository.cs new file mode 100644 index 00000000..08ca3da5 --- /dev/null +++ b/JackalWebHost2/Data/Repositories/GameRepository.cs @@ -0,0 +1,69 @@ +using Jackal.Core; +using JackalWebHost2.Data.Entities; +using JackalWebHost2.Data.Interfaces; +using JackalWebHost2.Exceptions; +using JackalWebHost2.Models.Map; +using Microsoft.EntityFrameworkCore; + +namespace JackalWebHost2.Data.Repositories; + +public class GameRepository(JackalDbContext jackalDbContext) : IGameRepository +{ + public async Task CreateGame(long userId, Game game) + { + var gameEntity = new GameEntity + { + MapId = game.Board.Generator.MapId, + TilesPackName = game.Board.Generator.TilesPackName, + MapSize = game.Board.MapSize, + GameMode = game.GameMode, + CreatorUserId = userId, + Created = DateTime.UtcNow + }; + await jackalDbContext.Games.AddAsync(gameEntity); + await jackalDbContext.SaveChangesAsync(); + + foreach (var team in game.Board.Teams) + { + var gamePlayerEntity = new GamePlayerEntity + { + GameId = gameEntity.Id, + TeamId = team.Id, + UserId = team.UserId != 0 ? team.UserId : null, + PlayerName = team.Name, + MapPositionId = (byte)MapUtils.ToMapPositionId(team.ShipPosition, game.Board.MapSize) + }; + await jackalDbContext.GamePlayers.AddAsync(gamePlayerEntity); + } + + await jackalDbContext.SaveChangesAsync(); + + return gameEntity.Id; + } + + public async Task UpdateGame(long gameId, Game game) + { + var gameEntity = await jackalDbContext.Games + .Include(g => g.GamePlayers) + .FirstOrDefaultAsync(g => g.Id == gameId); + + if (gameEntity == null) + throw new GameNotFoundException(); + + gameEntity.Updated = DateTime.UtcNow; + gameEntity.TurnNumber = game.TurnNumber; + gameEntity.GameOver = game.IsGameOver; + + var maxCoins = game.IsGameOver + ? game.Board.Teams.Max(x => x.Coins) + : 0; + + foreach (var playerEntity in gameEntity.GamePlayers) + { + playerEntity.Coins = game.Board.Teams[playerEntity.TeamId].Coins; + playerEntity.Winner = game.IsGameOver && playerEntity.Coins == maxCoins; + } + + await jackalDbContext.SaveChangesAsync(); + } +} \ No newline at end of file diff --git a/JackalWebHost2/Data/Repositories/GameRepositoryStub.cs b/JackalWebHost2/Data/Repositories/GameRepositoryStub.cs new file mode 100644 index 00000000..46da3316 --- /dev/null +++ b/JackalWebHost2/Data/Repositories/GameRepositoryStub.cs @@ -0,0 +1,19 @@ +using Jackal.Core; +using JackalWebHost2.Data.Interfaces; + +namespace JackalWebHost2.Data.Repositories; + +public class GameRepositoryStub : IGameRepository +{ + private static long _gameId; + + public Task CreateGame(long userId, Game game) + { + return Task.FromResult(_gameId++); + } + + public Task UpdateGame(long gameId, Game game) + { + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/JackalWebHost2/Data/Repositories/LobbyRepositoryInMemory.cs b/JackalWebHost2/Data/Repositories/LobbyRepositoryInMemory.cs new file mode 100644 index 00000000..ade30853 --- /dev/null +++ b/JackalWebHost2/Data/Repositories/LobbyRepositoryInMemory.cs @@ -0,0 +1,136 @@ +using JackalWebHost2.Data.Interfaces; +using JackalWebHost2.Models.Lobby; +using Microsoft.Extensions.Caching.Memory; + +namespace JackalWebHost2.Data.Repositories; + +public class LobbyRepositoryInMemory : ILobbyRepository +{ + private readonly IMemoryCache _memoryCache; + private readonly MemoryCacheEntryOptions _cacheEntryOptions; + + public LobbyRepositoryInMemory(IMemoryCache memoryCache) + { + _memoryCache = memoryCache; + _cacheEntryOptions = new MemoryCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromHours(1)); + } + + public Task CreateLobby(Lobby lobby, CancellationToken token) + { + _memoryCache.Set(GetLobbyKey(lobby.Id), lobby, _cacheEntryOptions); + _memoryCache.Set(GetLobbyOwnerKey(lobby.OwnerId), lobby.Id, _cacheEntryOptions); + _memoryCache.Set(GetLobbyMemberKey(lobby.OwnerId), lobby.Id, _cacheEntryOptions); + return Task.CompletedTask; + } + + public Task AddUserToLobby(string lobbyId, LobbyMember lobbyMember, CancellationToken token) + { + if (!_memoryCache.TryGetValue(GetLobbyKey(lobbyId), out var lobby) ) + { + throw new NotSupportedException(); + } + + lobby!.LobbyMembers[lobbyMember.UserId] = lobbyMember; + _memoryCache.Set(GetLobbyMemberKey(lobbyMember.UserId), lobby.Id, _cacheEntryOptions); + return Task.CompletedTask; + } + + public Task GetLobbyByUser(long userId, CancellationToken token) + { + if (!_memoryCache.TryGetValue(GetLobbyOwnerKey(userId), out var lobbyId)) + { + return Task.FromResult(null); + } + + return _memoryCache.TryGetValue(GetLobbyKey(lobbyId!), out var lobby) + ? Task.FromResult(lobby) + : Task.FromResult(null); + } + + public Task GetLobbyInfo(string lobbyId, bool includeDeleted, CancellationToken token) + { + if (!_memoryCache.TryGetValue(GetLobbyKey(lobbyId), out var lobby) ) + { + return Task.FromResult(null); + } + + // Обновляем TTL + _memoryCache.TryGetValue(GetLobbyOwnerKey(lobby!.OwnerId), out _); + return Task.FromResult(lobby); + } + + public Task RemoveUserFromLobbies(long userId, CancellationToken token) + { + if (!_memoryCache.TryGetValue(GetLobbyMemberKey(userId), out var lobbyId)) + { + return Task.CompletedTask; + } + + _memoryCache.Remove(GetLobbyMemberKey(userId)); + _memoryCache.Remove(GetLobbyOwnerKey(userId)); + if (_memoryCache.TryGetValue(GetLobbyKey(lobbyId!), out var lobby)) + { + lobby!.LobbyMembers.Remove(userId); + } + + return Task.CompletedTask; + } + + public Task UpdateUserKeepAlive(string lobbyId, long userId, DateTimeOffset time, CancellationToken token) + { + if (!_memoryCache.TryGetValue(GetLobbyKey(lobbyId), out var lobby) ) + { + throw new NotSupportedException(); + } + + // Обновляем TTL + _memoryCache.TryGetValue(GetLobbyMemberKey(userId), out _); + lobby!.LobbyMembers[userId].LastSeen = time; + return Task.CompletedTask; + } + + public Task Close(string lobbyId, DateTimeOffset time, long? gameId, long[]? gameMembers, CancellationToken token) + { + if (!_memoryCache.TryGetValue(GetLobbyKey(lobbyId), out var lobby) ) + { + throw new NotSupportedException(); + } + + lobby!.ClosedAt = time; + lobby.GameId = gameId; + lobby.GameMembers = gameMembers ?? []; + return Task.CompletedTask; + } + + public Task RemoveUsersFromLobby(string lobbyId, CancellationToken token) + { + if (!_memoryCache.TryGetValue(GetLobbyKey(lobbyId), out var lobby) ) + { + throw new NotSupportedException(); + } + + _memoryCache.Remove(GetLobbyOwnerKey(lobby!.OwnerId)); + foreach (var member in lobby.LobbyMembers.Keys) + { + _memoryCache.Remove(GetLobbyMemberKey(member)); + } + + lobby!.LobbyMembers.Clear(); + return Task.CompletedTask; + } + + public Task AssignTeam(string lobbyId, long userId, long? teamId, CancellationToken token) + { + if (!_memoryCache.TryGetValue(GetLobbyKey(lobbyId), out var lobby) ) + { + throw new NotSupportedException(); + } + + lobby!.LobbyMembers[userId].TeamId = teamId; + return Task.CompletedTask; + } + + private static string GetLobbyKey(string str) => "lobby:" + str; + private static string GetLobbyOwnerKey(long num) => "lobby-owner:" + num; + private static string GetLobbyMemberKey(long num) => "lobby-member:" + num; +} \ No newline at end of file diff --git a/JackalWebHost2/Data/Repositories/StateRepositoryInMemory.cs b/JackalWebHost2/Data/Repositories/StateRepositoryInMemory.cs new file mode 100644 index 00000000..41ba5919 --- /dev/null +++ b/JackalWebHost2/Data/Repositories/StateRepositoryInMemory.cs @@ -0,0 +1,132 @@ +using System.Collections.Concurrent; +using Jackal.Core; +using JackalWebHost2.Data.Entities; +using JackalWebHost2.Data.Interfaces; +using JackalWebHost2.Models; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; + +namespace JackalWebHost2.Data.Repositories; + +public class StateRepositoryInMemory : IStateRepository where T : class, ICompletable +{ + private readonly IMemoryCache _memoryCache; + private readonly MemoryCacheEntryOptions _cacheEntryOptions; + + private bool _hasChanges; + private readonly ConcurrentDictionary _entries; + + public StateRepositoryInMemory() + { + _entries = new ConcurrentDictionary(); + _memoryCache = new MemoryCache(Options.Create(new MemoryCacheOptions())); + _cacheEntryOptions = new MemoryCacheEntryOptions() + .SetSlidingExpiration(TimeSpan.FromHours(1)) + .RegisterPostEvictionCallback(callback: EvictionCallback); + } + + private void EvictionCallback(object? key, object? value, EvictionReason reason, object? state) + { + if (key is not long gameId) + { + return; + } + + if (reason is EvictionReason.None or EvictionReason.Replaced) + { + return; + } + + if (_entries.TryRemove(gameId, out _)) + { + _hasChanges = true; + } + } + + public bool HasChanges() + { + return _hasChanges; + } + + public void ResetChanges() + { + _hasChanges = false; + } + + public IList GetEntries() + { + return _entries.Values.ToList(); + } + + public T? GetObject(long objectId) + { + return _memoryCache.TryGetValue(objectId, out T? value) ? value : null; + } + + public void CreateObject(User user, long objectId, T value) + { + _memoryCache.Set(objectId, value, _cacheEntryOptions); + if (_entries.TryAdd(objectId, new CacheEntry + { + ObjectId = objectId, + Creator = new CacheEntryUser + { + Id = user.Id, + Name = user.Login + }, + TimeStamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + })) + { + _hasChanges = true; + } + } + + public void UpdateObject(long objectId, T value) + { + _memoryCache.Set(objectId, value, _cacheEntryOptions); + if (value.IsCompleted) + { + _entries.TryRemove(objectId, out _); + } + else if (_entries.TryGetValue(objectId, out CacheEntry? entry)) + { + entry.TimeStamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + } + _hasChanges = true; + } + + public void CreateObject(User user, long objectId, T value, HashSet players) + { + _memoryCache.Set(objectId, value, _cacheEntryOptions); + if (_entries.TryAdd(objectId, new CacheEntry + { + ObjectId = objectId, + Creator = new CacheEntryUser + { + Id = user.Id, + Name = user.Login + }, + Players = players.Select(it => new CacheEntryUser{ Id = it.Id, Name = it.Login }).ToArray(), + TimeStamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + })) + { + _hasChanges = true; + } + } + + public void UpdateObject(long objectId, T value, HashSet? players) + { + _memoryCache.Set(objectId, value, _cacheEntryOptions); + if (value.IsCompleted) + { + _entries.TryRemove(objectId, out _); + } + else if (_entries.TryGetValue(objectId, out CacheEntry? entry)) + { + if (players?.Count > 0) entry.Players = players.Select(it => new CacheEntryUser { Id = it.Id, Name = it.Login }).ToArray(); + entry.TimeStamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + } + _hasChanges = true; + } + +} \ No newline at end of file diff --git a/JackalWebHost2/Data/Repositories/UserRepository.cs b/JackalWebHost2/Data/Repositories/UserRepository.cs new file mode 100644 index 00000000..d8a258d9 --- /dev/null +++ b/JackalWebHost2/Data/Repositories/UserRepository.cs @@ -0,0 +1,62 @@ +using Jackal.Core.Players; +using JackalWebHost2.Data.Entities; +using JackalWebHost2.Data.Interfaces; +using JackalWebHost2.Models; +using Microsoft.EntityFrameworkCore; + +namespace JackalWebHost2.Data.Repositories; + +public class UserRepository(JackalDbContext jackalDbContext) : IUserRepository +{ + public async Task GetUser(long id, CancellationToken token) + { + var userEntity = await jackalDbContext.Users + .Include(u => u.GamePlayers) + .FirstOrDefaultAsync(u => u.Id == id, token); + + return userEntity != null ? ToUser(userEntity) : null; + } + + public async Task GetUser(string login, CancellationToken token) + { + var userEntity = await jackalDbContext.Users + .Include(u => u.GamePlayers) + .FirstOrDefaultAsync( + u => u.Login.ToLower() == login.ToLower(), + token + ); + + return userEntity != null ? ToUser(userEntity) : null; + } + + public async Task> GetUsers(long[] ids, CancellationToken token) + { + var users = await jackalDbContext.Users + .Include(u => u.GamePlayers) + .Where(u => ids.Contains(u.Id)).ToListAsync(token); + + return users.Select(ToUser).ToList(); + } + + public async Task CreateUser(string login, CancellationToken token) + { + var userEntity = new UserEntity + { + Login = login, + Created = DateTime.UtcNow + }; + + await jackalDbContext.Users.AddAsync(userEntity, token); + await jackalDbContext.SaveChangesAsync(token); + + return ToUser(userEntity); + } + + private static User ToUser(UserEntity entity) => + new() + { + Id = entity.Id, + Login = entity.Login, + Rank = entity.GamePlayers.Count(p => p.Winner).GetHeroes2Rank() + }; +} \ No newline at end of file diff --git a/JackalWebHost2/Data/Repositories/UserRepositoryInMemory.cs b/JackalWebHost2/Data/Repositories/UserRepositoryInMemory.cs new file mode 100644 index 00000000..81561f7c --- /dev/null +++ b/JackalWebHost2/Data/Repositories/UserRepositoryInMemory.cs @@ -0,0 +1,57 @@ +using JackalWebHost2.Data.Interfaces; +using JackalWebHost2.Models; +using Microsoft.Extensions.Caching.Memory; + +namespace JackalWebHost2.Data.Repositories; + +public class UserRepositoryInMemory : IUserRepository +{ + private static long Id; + + private readonly IMemoryCache _memoryCache; + private readonly MemoryCacheEntryOptions _cacheEntryOptions; + + public UserRepositoryInMemory(IMemoryCache memoryCache) + { + _memoryCache = memoryCache; + _cacheEntryOptions = new MemoryCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromHours(24)); + } + + public async Task GetUser(long id, CancellationToken token) + { + return _memoryCache.TryGetValue(GetKey(id), out var user) + ? user + : null; + } + + public async Task> GetUsers(long[] ids, CancellationToken token) + { + var list = new List(); + foreach (var id in ids) + { + var user = await GetUser(id, token); + if (user != null) list.Add(user); + } + return list; + } + + public async Task GetUser(string login, CancellationToken token) + { + return null; + } + + public Task CreateUser(string login, CancellationToken token) + { + var user = new User + { + Id = Interlocked.Increment(ref Id), + Login = login, + Rank = "Peasant" + }; + + _memoryCache.Set(GetKey(user.Id), user, _cacheEntryOptions); + return Task.FromResult(user); + } + + private static string GetKey(long num) => "user:" + num; +} \ No newline at end of file diff --git a/JackalWebHost2/Dockerfile b/JackalWebHost2/Dockerfile new file mode 100644 index 00000000..aa38a41f --- /dev/null +++ b/JackalWebHost2/Dockerfile @@ -0,0 +1,22 @@ +#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. + +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +WORKDIR /app +EXPOSE 80 +EXPOSE 443 + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /src +COPY ["JackalWebHost2/JackalWebHost2.csproj", "JackalWebHost2/"] +RUN dotnet restore "JackalWebHost2/JackalWebHost2.csproj" +COPY . . +WORKDIR "/src/JackalWebHost2" +RUN dotnet build "JackalWebHost2.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "JackalWebHost2.csproj" -c Release -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "JackalWebHost2.dll"] \ No newline at end of file diff --git a/JackalWebHost2/Exceptions/AllLobbyMembersMustHaveTeamException.cs b/JackalWebHost2/Exceptions/AllLobbyMembersMustHaveTeamException.cs new file mode 100644 index 00000000..db4a8623 --- /dev/null +++ b/JackalWebHost2/Exceptions/AllLobbyMembersMustHaveTeamException.cs @@ -0,0 +1,8 @@ +namespace JackalWebHost2.Exceptions; + +public class AllLobbyMembersMustHaveTeamException : BusinessException +{ + public override string ErrorMessage => "Все участники лобби должны выбрать команду"; + + public override string ErrorCode => ErrorCodes.AllLobbyMembersMustHaveTeam; +} \ No newline at end of file diff --git a/JackalWebHost2/Exceptions/BusinessException.cs b/JackalWebHost2/Exceptions/BusinessException.cs new file mode 100644 index 00000000..65539a14 --- /dev/null +++ b/JackalWebHost2/Exceptions/BusinessException.cs @@ -0,0 +1,8 @@ +namespace JackalWebHost2.Exceptions; + +public abstract class BusinessException : Exception +{ + public abstract string ErrorMessage { get; } + + public abstract string ErrorCode { get; } +} \ No newline at end of file diff --git a/JackalWebHost2/Exceptions/ErrorCodes.cs b/JackalWebHost2/Exceptions/ErrorCodes.cs new file mode 100644 index 00000000..a320c33a --- /dev/null +++ b/JackalWebHost2/Exceptions/ErrorCodes.cs @@ -0,0 +1,17 @@ +namespace JackalWebHost2.Exceptions; + +public static class ErrorCodes +{ + public const string ValidationError = "ValidationError"; + public const string GameNotFound = "GameNotFound"; + public const string LobbyNotFound = "LobbyNotFound"; + public const string LobbyIsFull = "LobbyIsFull"; + public const string UserIsNotLobbyMember = "UserIsNotLobbyMember"; + public const string LobbyIsClosed = "LobbyIsClosed"; + public const string UserIsNotLobbyOwner = "UserIsNotLobbyOwner"; + public const string AllLobbyMembersMustHaveTeam = "AllLobbyMembersMustHaveTeam"; + public const string UserIsAlreadyLoggedIn = "UserIsAlreadyLoggedIn"; + public const string UserIsNotFound = "UserIsNotFound"; + public const string TeamIsNotFound = "TeamIsNotFound"; + public const string PlayerNotFound = "PlayerNotFound"; +} \ No newline at end of file diff --git a/JackalWebHost2/Exceptions/GameNotFoundException.cs b/JackalWebHost2/Exceptions/GameNotFoundException.cs new file mode 100644 index 00000000..84e8e303 --- /dev/null +++ b/JackalWebHost2/Exceptions/GameNotFoundException.cs @@ -0,0 +1,8 @@ +namespace JackalWebHost2.Exceptions; + +public class GameNotFoundException : BusinessException +{ + public override string ErrorMessage => "Игра не найдена"; + + public override string ErrorCode => ErrorCodes.GameNotFound; +} \ No newline at end of file diff --git a/JackalWebHost2/Exceptions/LobbyIsClosedException.cs b/JackalWebHost2/Exceptions/LobbyIsClosedException.cs new file mode 100644 index 00000000..670d4097 --- /dev/null +++ b/JackalWebHost2/Exceptions/LobbyIsClosedException.cs @@ -0,0 +1,8 @@ +namespace JackalWebHost2.Exceptions; + +public class LobbyIsClosedException : BusinessException +{ + public override string ErrorMessage => "Лобби больше не существует"; + + public override string ErrorCode => ErrorCodes.LobbyIsClosed; +} \ No newline at end of file diff --git a/JackalWebHost2/Exceptions/LobbyIsFullException.cs b/JackalWebHost2/Exceptions/LobbyIsFullException.cs new file mode 100644 index 00000000..28df2225 --- /dev/null +++ b/JackalWebHost2/Exceptions/LobbyIsFullException.cs @@ -0,0 +1,8 @@ +namespace JackalWebHost2.Exceptions; + +public class LobbyIsFullException : BusinessException +{ + public override string ErrorMessage => "Лобби заполнено"; + + public override string ErrorCode => ErrorCodes.LobbyIsFull; +} \ No newline at end of file diff --git a/JackalWebHost2/Exceptions/LobbyNotFoundException.cs b/JackalWebHost2/Exceptions/LobbyNotFoundException.cs new file mode 100644 index 00000000..cafab406 --- /dev/null +++ b/JackalWebHost2/Exceptions/LobbyNotFoundException.cs @@ -0,0 +1,8 @@ +namespace JackalWebHost2.Exceptions; + +public class LobbyNotFoundException : BusinessException +{ + public override string ErrorMessage => "Лобби не найдено"; + + public override string ErrorCode => ErrorCodes.LobbyNotFound; +} \ No newline at end of file diff --git a/JackalWebHost2/Exceptions/PlayerNotFoundException.cs b/JackalWebHost2/Exceptions/PlayerNotFoundException.cs new file mode 100644 index 00000000..497e1a3a --- /dev/null +++ b/JackalWebHost2/Exceptions/PlayerNotFoundException.cs @@ -0,0 +1,8 @@ +namespace JackalWebHost2.Exceptions; + +public class PlayerNotFoundException : BusinessException +{ + public override string ErrorMessage => "Игрок не найден"; + + public override string ErrorCode => ErrorCodes.PlayerNotFound; +} \ No newline at end of file diff --git a/JackalWebHost2/Exceptions/TeamIsNotFoundException.cs b/JackalWebHost2/Exceptions/TeamIsNotFoundException.cs new file mode 100644 index 00000000..6cac9305 --- /dev/null +++ b/JackalWebHost2/Exceptions/TeamIsNotFoundException.cs @@ -0,0 +1,8 @@ +namespace JackalWebHost2.Exceptions; + +public class TeamIsNotFoundException : BusinessException +{ + public override string ErrorMessage => "Выбранная команда не найдена"; + + public override string ErrorCode => ErrorCodes.TeamIsNotFound; +} \ No newline at end of file diff --git a/JackalWebHost2/Exceptions/UserIsAlreadyLoggedInException.cs b/JackalWebHost2/Exceptions/UserIsAlreadyLoggedInException.cs new file mode 100644 index 00000000..bfc97b21 --- /dev/null +++ b/JackalWebHost2/Exceptions/UserIsAlreadyLoggedInException.cs @@ -0,0 +1,8 @@ +namespace JackalWebHost2.Exceptions; + +public class UserIsAlreadyLoggedInException : BusinessException +{ + public override string ErrorMessage => "Пользователь уже вошел"; + + public override string ErrorCode => ErrorCodes.UserIsAlreadyLoggedIn; +} \ No newline at end of file diff --git a/JackalWebHost2/Exceptions/UserIsNotFoundException.cs b/JackalWebHost2/Exceptions/UserIsNotFoundException.cs new file mode 100644 index 00000000..b80985b5 --- /dev/null +++ b/JackalWebHost2/Exceptions/UserIsNotFoundException.cs @@ -0,0 +1,8 @@ +namespace JackalWebHost2.Exceptions; + +public class UserIsNotFoundException : BusinessException +{ + public override string ErrorMessage => "Пользователь не найден"; + + public override string ErrorCode => ErrorCodes.UserIsNotFound; +} \ No newline at end of file diff --git a/JackalWebHost2/Exceptions/UserIsNotLobbyMemberException.cs b/JackalWebHost2/Exceptions/UserIsNotLobbyMemberException.cs new file mode 100644 index 00000000..05e071d1 --- /dev/null +++ b/JackalWebHost2/Exceptions/UserIsNotLobbyMemberException.cs @@ -0,0 +1,8 @@ +namespace JackalWebHost2.Exceptions; + +public class UserIsNotLobbyMemberException : BusinessException +{ + public override string ErrorMessage => "Пользователь не имеет доступа в лобби"; + + public override string ErrorCode => ErrorCodes.UserIsNotLobbyMember; +} \ No newline at end of file diff --git a/JackalWebHost2/Exceptions/UserIsNotLobbyOwnerException.cs b/JackalWebHost2/Exceptions/UserIsNotLobbyOwnerException.cs new file mode 100644 index 00000000..eda6a4e2 --- /dev/null +++ b/JackalWebHost2/Exceptions/UserIsNotLobbyOwnerException.cs @@ -0,0 +1,8 @@ +namespace JackalWebHost2.Exceptions; + +public class UserIsNotLobbyOwnerException : BusinessException +{ + public override string ErrorMessage => "Пользователь должен быть владельцем лобби"; + + public override string ErrorCode => ErrorCodes.UserIsNotLobbyOwner; +} \ No newline at end of file diff --git a/JackalWebHost2/Infrastructure/Auth/AuthDefaults.cs b/JackalWebHost2/Infrastructure/Auth/AuthDefaults.cs new file mode 100644 index 00000000..5ec9530f --- /dev/null +++ b/JackalWebHost2/Infrastructure/Auth/AuthDefaults.cs @@ -0,0 +1,19 @@ +using Microsoft.IdentityModel.Tokens; +using System.Text; + +namespace JackalWebHost2.Infrastructure.Auth; + +public static class AuthDefaults +{ + public const string FastAuthScheme = "FastAuthScheme"; + public const string FastAuthPolicy = "FastAuthPolicy"; + public const string FastAuthUserId = "UserId"; + public const string FastAuthLogin = "Login"; + + public const string Issuer = "jackal.team"; + public const string Audience = "jackal.team"; + public static SymmetricSecurityKey GetSymmetricSecurityKey() => + new SymmetricSecurityKey(Encoding.UTF8.GetBytes(SecKey)); + + const string SecKey = "otoqPRQij8WUxi0C7YDdMEiT6Xh9dWczyFShVmPYcLZvNewFY7n4Nh68A/X8MbCB"; +} \ No newline at end of file diff --git a/JackalWebHost2/Infrastructure/Auth/FastAuthAttribute.cs b/JackalWebHost2/Infrastructure/Auth/FastAuthAttribute.cs new file mode 100644 index 00000000..49843c05 --- /dev/null +++ b/JackalWebHost2/Infrastructure/Auth/FastAuthAttribute.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Authorization; + +namespace JackalWebHost2.Infrastructure.Auth; + +public class FastAuthAttribute : AuthorizeAttribute +{ + public FastAuthAttribute() + { + Policy = AuthDefaults.FastAuthPolicy; + } +} \ No newline at end of file diff --git a/JackalWebHost2/Infrastructure/Auth/FastAuthJwtBearerHelper.cs b/JackalWebHost2/Infrastructure/Auth/FastAuthJwtBearerHelper.cs new file mode 100644 index 00000000..23597e2e --- /dev/null +++ b/JackalWebHost2/Infrastructure/Auth/FastAuthJwtBearerHelper.cs @@ -0,0 +1,39 @@ +using System.IdentityModel.Tokens.Jwt; +using JackalWebHost2.Models; +using System.Security.Claims; +using Microsoft.IdentityModel.Tokens; + +namespace JackalWebHost2.Infrastructure.Auth +{ + public static class FastAuthJwtBearerHelper + { + public static User ExtractUser(ClaimsPrincipal? user) + { + var idClaim = user?.FindFirst(AuthDefaults.FastAuthUserId); + var loginClaim = user?.FindFirst(AuthDefaults.FastAuthLogin); + + return new User + { + Id = long.TryParse(idClaim?.Value, out var id) ? id : 0, + Login = loginClaim?.Value ?? "dark incognito" + }; + } + + public static async Task SignInUser(HttpContext httpContext, User user) + { + var jwt = new JwtSecurityToken( + issuer: AuthDefaults.Issuer, + audience: AuthDefaults.Audience, + claims: new List + { + new Claim(AuthDefaults.FastAuthUserId, user.Id.ToString()), + new Claim(AuthDefaults.FastAuthLogin, user.Login) + }, + expires: DateTime.UtcNow.Add(TimeSpan.FromDays(365)), + signingCredentials: new SigningCredentials(AuthDefaults.GetSymmetricSecurityKey(), SecurityAlgorithms.HmacSha256)); + + return new JwtSecurityTokenHandler().WriteToken(jwt); + } + + } +} diff --git a/JackalWebHost2/Infrastructure/BusinessExceptionFilter.cs b/JackalWebHost2/Infrastructure/BusinessExceptionFilter.cs new file mode 100644 index 00000000..e958d731 --- /dev/null +++ b/JackalWebHost2/Infrastructure/BusinessExceptionFilter.cs @@ -0,0 +1,21 @@ +using JackalWebHost2.Controllers.Models; +using JackalWebHost2.Exceptions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace JackalWebHost2.Infrastructure; + +public class BusinessExceptionFilter : IExceptionFilter +{ + public void OnException(ExceptionContext context) + { + if (context.ExceptionHandled || context.Result != null || context.Exception is not BusinessException businessException) + { + return; + } + + context.Result = new JsonResult(new ErrorModel(businessException)); + context.HttpContext.Response.StatusCode = 400; + context.ExceptionHandled = true; + } +} \ No newline at end of file diff --git a/JackalWebHost2/Infrastructure/CorsDefaults.cs b/JackalWebHost2/Infrastructure/CorsDefaults.cs new file mode 100644 index 00000000..1c0f196e --- /dev/null +++ b/JackalWebHost2/Infrastructure/CorsDefaults.cs @@ -0,0 +1,6 @@ +namespace JackalWebHost2.Infrastructure; + +public static class CorsDefaults +{ + public const string AllOrigins = "AllOrigins"; +} \ No newline at end of file diff --git a/JackalWebHost2/Infrastructure/Middleware/ApplicationBuilderExtensions.cs b/JackalWebHost2/Infrastructure/Middleware/ApplicationBuilderExtensions.cs new file mode 100644 index 00000000..93794e2b --- /dev/null +++ b/JackalWebHost2/Infrastructure/Middleware/ApplicationBuilderExtensions.cs @@ -0,0 +1,9 @@ +namespace JackalWebHost2.Infrastructure.Middleware; + +public static class ApplicationBuilderExtensions +{ + public static IApplicationBuilder UseCorsHeaders(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } +} \ No newline at end of file diff --git a/JackalWebHost2/Infrastructure/Middleware/CorsHeadersMiddleware.cs b/JackalWebHost2/Infrastructure/Middleware/CorsHeadersMiddleware.cs new file mode 100644 index 00000000..bd8c86b3 --- /dev/null +++ b/JackalWebHost2/Infrastructure/Middleware/CorsHeadersMiddleware.cs @@ -0,0 +1,26 @@ +namespace JackalWebHost2.Infrastructure.Middleware; + +public class CorsHeadersMiddleware +{ + private readonly RequestDelegate _next; + + public CorsHeadersMiddleware(RequestDelegate next) + { + _next = next; + } + + public Task Invoke(HttpContext context) + { + if (context.Request.Method == "OPTIONS") + { + context.Response.Headers.Add("Access-Control-Allow-Origin", new[] { (string)context.Request.Headers["Origin"] }); + context.Response.Headers.Add("Access-Control-Allow-Headers", new[] { "Origin, X-Requested-With, Content-Type, Accept" }); + context.Response.Headers.Add("Access-Control-Allow-Methods", new[] { "GET, POST, PUT, DELETE, OPTIONS" }); + context.Response.Headers.Add("Access-Control-Allow-Credentials", new[] { "true" }); + context.Response.StatusCode = 200; + return context.Response.WriteAsync("OK"); + } + + return _next.Invoke(context); + } +} \ No newline at end of file diff --git a/JackalWebHost2/Infrastructure/ValidationFilter.cs b/JackalWebHost2/Infrastructure/ValidationFilter.cs new file mode 100644 index 00000000..bea33f38 --- /dev/null +++ b/JackalWebHost2/Infrastructure/ValidationFilter.cs @@ -0,0 +1,33 @@ +using JackalWebHost2.Controllers.Models; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace JackalWebHost2.Infrastructure; + +public class ValidationFilter : IActionFilter +{ + public void OnActionExecuting(ActionExecutingContext context) + { + if (context.ModelState.IsValid) + { + return; + } + + var details = context.ModelState + .Where(x => x.Value?.ValidationState == ModelValidationState.Invalid) + .Select(x => new ValidationEntryModel + { + Property = x.Key, + Errors = x.Value?.Errors.Select(y => y.ErrorMessage).ToArray() ?? [] + }) + .ToArray(); + + context.Result = new JsonResult(new ValidationErrorModel("В запросе переданы некорректные данные", details)); + context.HttpContext.Response.StatusCode = 400; + } + + public void OnActionExecuted(ActionExecutedContext context) + { + } +} \ No newline at end of file diff --git a/JackalWebHost2/JackalWebHost2.csproj b/JackalWebHost2/JackalWebHost2.csproj new file mode 100644 index 00000000..637e9ba3 --- /dev/null +++ b/JackalWebHost2/JackalWebHost2.csproj @@ -0,0 +1,36 @@ + + + + net8.0 + latest + enable + enable + aspnet-JackalWebHost2-a136218e-936f-4c31-bf67-6a52e5070a60 + Linux + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/JackalWebHost2/Migrations/20250322075415_Initial.Designer.cs b/JackalWebHost2/Migrations/20250322075415_Initial.Designer.cs new file mode 100644 index 00000000..298c70e0 --- /dev/null +++ b/JackalWebHost2/Migrations/20250322075415_Initial.Designer.cs @@ -0,0 +1,55 @@ +// +using System; +using JackalWebHost2.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace JackalWebHost2.Migrations +{ + [DbContext(typeof(JackalDbContext))] + [Migration("20250322075415_Initial")] + partial class Initial + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("JackalWebHost2.Data.Entities.GameEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("TurnNumber") + .HasColumnType("integer"); + + b.Property("Updated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Games"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/JackalWebHost2/Migrations/20250322075415_Initial.cs b/JackalWebHost2/Migrations/20250322075415_Initial.cs new file mode 100644 index 00000000..50be8a4a --- /dev/null +++ b/JackalWebHost2/Migrations/20250322075415_Initial.cs @@ -0,0 +1,36 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace JackalWebHost2.Migrations +{ + public partial class Initial : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Games", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Code = table.Column(type: "text", nullable: false), + Created = table.Column(type: "timestamp with time zone", nullable: false), + Updated = table.Column(type: "timestamp with time zone", nullable: false), + TurnNumber = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Games", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Games"); + } + } +} diff --git a/JackalWebHost2/Migrations/20250322182153_UsersTable.Designer.cs b/JackalWebHost2/Migrations/20250322182153_UsersTable.Designer.cs new file mode 100644 index 00000000..6971b54e --- /dev/null +++ b/JackalWebHost2/Migrations/20250322182153_UsersTable.Designer.cs @@ -0,0 +1,80 @@ +// +using System; +using JackalWebHost2.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace JackalWebHost2.Migrations +{ + [DbContext(typeof(JackalDbContext))] + [Migration("20250322182153_UsersTable")] + partial class UsersTable + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("JackalWebHost2.Data.Entities.GameEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("character varying(36)"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("TurnNumber") + .HasColumnType("integer"); + + b.Property("Updated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Games"); + }); + + modelBuilder.Entity("JackalWebHost2.Data.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Login") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.HasKey("Id"); + + b.HasIndex("Login") + .IsUnique(); + + b.ToTable("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/JackalWebHost2/Migrations/20250322182153_UsersTable.cs b/JackalWebHost2/Migrations/20250322182153_UsersTable.cs new file mode 100644 index 00000000..26e77ec7 --- /dev/null +++ b/JackalWebHost2/Migrations/20250322182153_UsersTable.cs @@ -0,0 +1,58 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace JackalWebHost2.Migrations +{ + public partial class UsersTable : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Code", + table: "Games", + type: "character varying(36)", + maxLength: 36, + nullable: false, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Login = table.Column(type: "character varying(30)", maxLength: 30, nullable: false), + Created = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_Users_Login", + table: "Users", + column: "Login", + unique: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Users"); + + migrationBuilder.AlterColumn( + name: "Code", + table: "Games", + type: "text", + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(36)", + oldMaxLength: 36); + } + } +} diff --git a/JackalWebHost2/Migrations/20250323160747_GameUsersTable.Designer.cs b/JackalWebHost2/Migrations/20250323160747_GameUsersTable.Designer.cs new file mode 100644 index 00000000..68ed88ca --- /dev/null +++ b/JackalWebHost2/Migrations/20250323160747_GameUsersTable.Designer.cs @@ -0,0 +1,135 @@ +// +using System; +using JackalWebHost2.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace JackalWebHost2.Migrations +{ + [DbContext(typeof(JackalDbContext))] + [Migration("20250323160747_GameUsersTable")] + partial class GameUsersTable + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("JackalWebHost2.Data.Entities.GameEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("character varying(36)"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("TurnNumber") + .HasColumnType("integer"); + + b.Property("Updated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Games"); + }); + + modelBuilder.Entity("JackalWebHost2.Data.Entities.GameUserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("GameId") + .HasColumnType("bigint"); + + b.Property("MapPositionId") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("GameId"); + + b.HasIndex("UserId"); + + b.ToTable("GameUsers"); + }); + + modelBuilder.Entity("JackalWebHost2.Data.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Login") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.HasKey("Id"); + + b.HasIndex("Login") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("JackalWebHost2.Data.Entities.GameUserEntity", b => + { + b.HasOne("JackalWebHost2.Data.Entities.GameEntity", "Game") + .WithMany("GameUsers") + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("JackalWebHost2.Data.Entities.UserEntity", "User") + .WithMany("GameUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Game"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("JackalWebHost2.Data.Entities.GameEntity", b => + { + b.Navigation("GameUsers"); + }); + + modelBuilder.Entity("JackalWebHost2.Data.Entities.UserEntity", b => + { + b.Navigation("GameUsers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/JackalWebHost2/Migrations/20250323160747_GameUsersTable.cs b/JackalWebHost2/Migrations/20250323160747_GameUsersTable.cs new file mode 100644 index 00000000..46a32d7e --- /dev/null +++ b/JackalWebHost2/Migrations/20250323160747_GameUsersTable.cs @@ -0,0 +1,56 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace JackalWebHost2.Migrations +{ + public partial class GameUsersTable : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "GameUsers", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + GameId = table.Column(type: "bigint", nullable: false), + UserId = table.Column(type: "bigint", nullable: false), + MapPositionId = table.Column(type: "smallint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_GameUsers", x => x.Id); + table.ForeignKey( + name: "FK_GameUsers_Games_GameId", + column: x => x.GameId, + principalTable: "Games", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_GameUsers_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_GameUsers_GameId", + table: "GameUsers", + column: "GameId"); + + migrationBuilder.CreateIndex( + name: "IX_GameUsers_UserId", + table: "GameUsers", + column: "UserId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "GameUsers"); + } + } +} diff --git a/JackalWebHost2/Migrations/20250405082506_GamesTable_RemoveCode.Designer.cs b/JackalWebHost2/Migrations/20250405082506_GamesTable_RemoveCode.Designer.cs new file mode 100644 index 00000000..949c164d --- /dev/null +++ b/JackalWebHost2/Migrations/20250405082506_GamesTable_RemoveCode.Designer.cs @@ -0,0 +1,130 @@ +// +using System; +using JackalWebHost2.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace JackalWebHost2.Migrations +{ + [DbContext(typeof(JackalDbContext))] + [Migration("20250405082506_GamesTable_RemoveCode")] + partial class GamesTable_RemoveCode + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("JackalWebHost2.Data.Entities.GameEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("TurnNumber") + .HasColumnType("integer"); + + b.Property("Updated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Games"); + }); + + modelBuilder.Entity("JackalWebHost2.Data.Entities.GameUserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("GameId") + .HasColumnType("bigint"); + + b.Property("MapPositionId") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("GameId"); + + b.HasIndex("UserId"); + + b.ToTable("GameUsers"); + }); + + modelBuilder.Entity("JackalWebHost2.Data.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Login") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.HasKey("Id"); + + b.HasIndex("Login") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("JackalWebHost2.Data.Entities.GameUserEntity", b => + { + b.HasOne("JackalWebHost2.Data.Entities.GameEntity", "Game") + .WithMany("GameUsers") + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("JackalWebHost2.Data.Entities.UserEntity", "User") + .WithMany("GameUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Game"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("JackalWebHost2.Data.Entities.GameEntity", b => + { + b.Navigation("GameUsers"); + }); + + modelBuilder.Entity("JackalWebHost2.Data.Entities.UserEntity", b => + { + b.Navigation("GameUsers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/JackalWebHost2/Migrations/20250405082506_GamesTable_RemoveCode.cs b/JackalWebHost2/Migrations/20250405082506_GamesTable_RemoveCode.cs new file mode 100644 index 00000000..41e0aebb --- /dev/null +++ b/JackalWebHost2/Migrations/20250405082506_GamesTable_RemoveCode.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace JackalWebHost2.Migrations +{ + public partial class GamesTable_RemoveCode : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Code", + table: "Games"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Code", + table: "Games", + type: "character varying(36)", + maxLength: 36, + nullable: false, + defaultValue: ""); + } + } +} diff --git a/JackalWebHost2/Migrations/20250607191701_GamePlayersTable.Designer.cs b/JackalWebHost2/Migrations/20250607191701_GamePlayersTable.Designer.cs new file mode 100644 index 00000000..12456e71 --- /dev/null +++ b/JackalWebHost2/Migrations/20250607191701_GamePlayersTable.Designer.cs @@ -0,0 +1,150 @@ +// +using System; +using JackalWebHost2.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace JackalWebHost2.Migrations +{ + [DbContext(typeof(JackalDbContext))] + [Migration("20250607191701_GamePlayersTable")] + partial class GamePlayersTable + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("JackalWebHost2.Data.Entities.GameEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatorUserId") + .HasColumnType("bigint"); + + b.Property("TurnNumber") + .HasColumnType("integer"); + + b.Property("Updated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatorUserId"); + + b.ToTable("Games"); + }); + + modelBuilder.Entity("JackalWebHost2.Data.Entities.GamePlayerEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("GameId") + .HasColumnType("bigint"); + + b.Property("MapPositionId") + .HasColumnType("smallint"); + + b.Property("PlayerName") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("GameId"); + + b.HasIndex("UserId"); + + b.ToTable("GamePlayers"); + }); + + modelBuilder.Entity("JackalWebHost2.Data.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Login") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.HasKey("Id"); + + b.HasIndex("Login") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("JackalWebHost2.Data.Entities.GameEntity", b => + { + b.HasOne("JackalWebHost2.Data.Entities.UserEntity", "CreatorUser") + .WithMany("Games") + .HasForeignKey("CreatorUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CreatorUser"); + }); + + modelBuilder.Entity("JackalWebHost2.Data.Entities.GamePlayerEntity", b => + { + b.HasOne("JackalWebHost2.Data.Entities.GameEntity", "Game") + .WithMany("GamePlayers") + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("JackalWebHost2.Data.Entities.UserEntity", "User") + .WithMany("GamePlayers") + .HasForeignKey("UserId"); + + b.Navigation("Game"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("JackalWebHost2.Data.Entities.GameEntity", b => + { + b.Navigation("GamePlayers"); + }); + + modelBuilder.Entity("JackalWebHost2.Data.Entities.UserEntity", b => + { + b.Navigation("GamePlayers"); + + b.Navigation("Games"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/JackalWebHost2/Migrations/20250607191701_GamePlayersTable.cs b/JackalWebHost2/Migrations/20250607191701_GamePlayersTable.cs new file mode 100644 index 00000000..6a447302 --- /dev/null +++ b/JackalWebHost2/Migrations/20250607191701_GamePlayersTable.cs @@ -0,0 +1,130 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace JackalWebHost2.Migrations +{ + public partial class GamePlayersTable : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql("DELETE FROM public.\"Games\"", true); + + migrationBuilder.DropTable( + name: "GameUsers"); + + migrationBuilder.AddColumn( + name: "CreatorUserId", + table: "Games", + type: "bigint", + nullable: false, + defaultValue: 0L); + + migrationBuilder.CreateTable( + name: "GamePlayers", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + GameId = table.Column(type: "bigint", nullable: false), + UserId = table.Column(type: "bigint", nullable: true), + PlayerName = table.Column(type: "text", nullable: false), + MapPositionId = table.Column(type: "smallint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_GamePlayers", x => x.Id); + table.ForeignKey( + name: "FK_GamePlayers_Games_GameId", + column: x => x.GameId, + principalTable: "Games", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_GamePlayers_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id"); + }); + + migrationBuilder.CreateIndex( + name: "IX_Games_CreatorUserId", + table: "Games", + column: "CreatorUserId"); + + migrationBuilder.CreateIndex( + name: "IX_GamePlayers_GameId", + table: "GamePlayers", + column: "GameId"); + + migrationBuilder.CreateIndex( + name: "IX_GamePlayers_UserId", + table: "GamePlayers", + column: "UserId"); + + migrationBuilder.AddForeignKey( + name: "FK_Games_Users_CreatorUserId", + table: "Games", + column: "CreatorUserId", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Games_Users_CreatorUserId", + table: "Games"); + + migrationBuilder.DropTable( + name: "GamePlayers"); + + migrationBuilder.DropIndex( + name: "IX_Games_CreatorUserId", + table: "Games"); + + migrationBuilder.DropColumn( + name: "CreatorUserId", + table: "Games"); + + migrationBuilder.CreateTable( + name: "GameUsers", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + GameId = table.Column(type: "bigint", nullable: false), + UserId = table.Column(type: "bigint", nullable: false), + MapPositionId = table.Column(type: "smallint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_GameUsers", x => x.Id); + table.ForeignKey( + name: "FK_GameUsers_Games_GameId", + column: x => x.GameId, + principalTable: "Games", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_GameUsers_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_GameUsers_GameId", + table: "GameUsers", + column: "GameId"); + + migrationBuilder.CreateIndex( + name: "IX_GameUsers_UserId", + table: "GameUsers", + column: "UserId"); + } + } +} diff --git a/JackalWebHost2/Migrations/20250612172335_GamesTable_AddAdditionalData.Designer.cs b/JackalWebHost2/Migrations/20250612172335_GamesTable_AddAdditionalData.Designer.cs new file mode 100644 index 00000000..9b853015 --- /dev/null +++ b/JackalWebHost2/Migrations/20250612172335_GamesTable_AddAdditionalData.Designer.cs @@ -0,0 +1,173 @@ +// +using System; +using JackalWebHost2.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace JackalWebHost2.Migrations +{ + [DbContext(typeof(JackalDbContext))] + [Migration("20250612172335_GamesTable_AddAdditionalData")] + partial class GamesTable_AddAdditionalData + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("JackalWebHost2.Data.Entities.GameEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatorUserId") + .HasColumnType("bigint"); + + b.Property("GameMode") + .HasColumnType("integer"); + + b.Property("GameOver") + .HasColumnType("boolean"); + + b.Property("MapId") + .HasColumnType("integer"); + + b.Property("MapSize") + .HasColumnType("integer"); + + b.Property("TilesPackName") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("TurnNumber") + .HasColumnType("integer"); + + b.Property("Updated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatorUserId"); + + b.ToTable("Games"); + }); + + modelBuilder.Entity("JackalWebHost2.Data.Entities.GamePlayerEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Coins") + .HasColumnType("integer"); + + b.Property("GameId") + .HasColumnType("bigint"); + + b.Property("MapPositionId") + .HasColumnType("smallint"); + + b.Property("PlayerName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TeamId") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("GameId"); + + b.HasIndex("UserId"); + + b.ToTable("GamePlayers"); + }); + + modelBuilder.Entity("JackalWebHost2.Data.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Login") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.HasKey("Id"); + + b.HasIndex("Login") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("JackalWebHost2.Data.Entities.GameEntity", b => + { + b.HasOne("JackalWebHost2.Data.Entities.UserEntity", "CreatorUser") + .WithMany("Games") + .HasForeignKey("CreatorUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CreatorUser"); + }); + + modelBuilder.Entity("JackalWebHost2.Data.Entities.GamePlayerEntity", b => + { + b.HasOne("JackalWebHost2.Data.Entities.GameEntity", "Game") + .WithMany("GamePlayers") + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("JackalWebHost2.Data.Entities.UserEntity", "User") + .WithMany("GamePlayers") + .HasForeignKey("UserId"); + + b.Navigation("Game"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("JackalWebHost2.Data.Entities.GameEntity", b => + { + b.Navigation("GamePlayers"); + }); + + modelBuilder.Entity("JackalWebHost2.Data.Entities.UserEntity", b => + { + b.Navigation("GamePlayers"); + + b.Navigation("Games"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/JackalWebHost2/Migrations/20250612172335_GamesTable_AddAdditionalData.cs b/JackalWebHost2/Migrations/20250612172335_GamesTable_AddAdditionalData.cs new file mode 100644 index 00000000..97aa8ff3 --- /dev/null +++ b/JackalWebHost2/Migrations/20250612172335_GamesTable_AddAdditionalData.cs @@ -0,0 +1,93 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace JackalWebHost2.Migrations +{ + public partial class GamesTable_AddAdditionalData : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "GameMode", + table: "Games", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "GameOver", + table: "Games", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "MapId", + table: "Games", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "MapSize", + table: "Games", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "TilesPackName", + table: "Games", + type: "character varying(30)", + maxLength: 30, + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "Coins", + table: "GamePlayers", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "TeamId", + table: "GamePlayers", + type: "integer", + nullable: false, + defaultValue: 0); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "GameMode", + table: "Games"); + + migrationBuilder.DropColumn( + name: "GameOver", + table: "Games"); + + migrationBuilder.DropColumn( + name: "MapId", + table: "Games"); + + migrationBuilder.DropColumn( + name: "MapSize", + table: "Games"); + + migrationBuilder.DropColumn( + name: "TilesPackName", + table: "Games"); + + migrationBuilder.DropColumn( + name: "Coins", + table: "GamePlayers"); + + migrationBuilder.DropColumn( + name: "TeamId", + table: "GamePlayers"); + } + } +} diff --git a/JackalWebHost2/Migrations/20250613090611_GamePlayersTable_AddWinner.Designer.cs b/JackalWebHost2/Migrations/20250613090611_GamePlayersTable_AddWinner.Designer.cs new file mode 100644 index 00000000..cd2227e8 --- /dev/null +++ b/JackalWebHost2/Migrations/20250613090611_GamePlayersTable_AddWinner.Designer.cs @@ -0,0 +1,176 @@ +// +using System; +using JackalWebHost2.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace JackalWebHost2.Migrations +{ + [DbContext(typeof(JackalDbContext))] + [Migration("20250613090611_GamePlayersTable_AddWinner")] + partial class GamePlayersTable_AddWinner + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("JackalWebHost2.Data.Entities.GameEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatorUserId") + .HasColumnType("bigint"); + + b.Property("GameMode") + .HasColumnType("integer"); + + b.Property("GameOver") + .HasColumnType("boolean"); + + b.Property("MapId") + .HasColumnType("integer"); + + b.Property("MapSize") + .HasColumnType("integer"); + + b.Property("TilesPackName") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("TurnNumber") + .HasColumnType("integer"); + + b.Property("Updated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatorUserId"); + + b.ToTable("Games"); + }); + + modelBuilder.Entity("JackalWebHost2.Data.Entities.GamePlayerEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Coins") + .HasColumnType("integer"); + + b.Property("GameId") + .HasColumnType("bigint"); + + b.Property("MapPositionId") + .HasColumnType("smallint"); + + b.Property("PlayerName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TeamId") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("Winner") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("GameId"); + + b.HasIndex("UserId"); + + b.ToTable("GamePlayers"); + }); + + modelBuilder.Entity("JackalWebHost2.Data.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Login") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.HasKey("Id"); + + b.HasIndex("Login") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("JackalWebHost2.Data.Entities.GameEntity", b => + { + b.HasOne("JackalWebHost2.Data.Entities.UserEntity", "CreatorUser") + .WithMany("Games") + .HasForeignKey("CreatorUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CreatorUser"); + }); + + modelBuilder.Entity("JackalWebHost2.Data.Entities.GamePlayerEntity", b => + { + b.HasOne("JackalWebHost2.Data.Entities.GameEntity", "Game") + .WithMany("GamePlayers") + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("JackalWebHost2.Data.Entities.UserEntity", "User") + .WithMany("GamePlayers") + .HasForeignKey("UserId"); + + b.Navigation("Game"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("JackalWebHost2.Data.Entities.GameEntity", b => + { + b.Navigation("GamePlayers"); + }); + + modelBuilder.Entity("JackalWebHost2.Data.Entities.UserEntity", b => + { + b.Navigation("GamePlayers"); + + b.Navigation("Games"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/JackalWebHost2/Migrations/20250613090611_GamePlayersTable_AddWinner.cs b/JackalWebHost2/Migrations/20250613090611_GamePlayersTable_AddWinner.cs new file mode 100644 index 00000000..50bbb5a7 --- /dev/null +++ b/JackalWebHost2/Migrations/20250613090611_GamePlayersTable_AddWinner.cs @@ -0,0 +1,26 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace JackalWebHost2.Migrations +{ + public partial class GamePlayersTable_AddWinner : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Winner", + table: "GamePlayers", + type: "boolean", + nullable: false, + defaultValue: false); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Winner", + table: "GamePlayers"); + } + } +} diff --git a/JackalWebHost2/Migrations/ApplicationDbContextModelSnapshot.cs b/JackalWebHost2/Migrations/ApplicationDbContextModelSnapshot.cs new file mode 100644 index 00000000..8f46480c --- /dev/null +++ b/JackalWebHost2/Migrations/ApplicationDbContextModelSnapshot.cs @@ -0,0 +1,174 @@ +// +using System; +using JackalWebHost2.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace JackalWebHost2.Migrations +{ + [DbContext(typeof(JackalDbContext))] + partial class ApplicationDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("JackalWebHost2.Data.Entities.GameEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatorUserId") + .HasColumnType("bigint"); + + b.Property("GameMode") + .HasColumnType("integer"); + + b.Property("GameOver") + .HasColumnType("boolean"); + + b.Property("MapId") + .HasColumnType("integer"); + + b.Property("MapSize") + .HasColumnType("integer"); + + b.Property("TilesPackName") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("TurnNumber") + .HasColumnType("integer"); + + b.Property("Updated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatorUserId"); + + b.ToTable("Games"); + }); + + modelBuilder.Entity("JackalWebHost2.Data.Entities.GamePlayerEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Coins") + .HasColumnType("integer"); + + b.Property("GameId") + .HasColumnType("bigint"); + + b.Property("MapPositionId") + .HasColumnType("smallint"); + + b.Property("PlayerName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TeamId") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("Winner") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("GameId"); + + b.HasIndex("UserId"); + + b.ToTable("GamePlayers"); + }); + + modelBuilder.Entity("JackalWebHost2.Data.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Login") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.HasKey("Id"); + + b.HasIndex("Login") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("JackalWebHost2.Data.Entities.GameEntity", b => + { + b.HasOne("JackalWebHost2.Data.Entities.UserEntity", "CreatorUser") + .WithMany("Games") + .HasForeignKey("CreatorUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CreatorUser"); + }); + + modelBuilder.Entity("JackalWebHost2.Data.Entities.GamePlayerEntity", b => + { + b.HasOne("JackalWebHost2.Data.Entities.GameEntity", "Game") + .WithMany("GamePlayers") + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("JackalWebHost2.Data.Entities.UserEntity", "User") + .WithMany("GamePlayers") + .HasForeignKey("UserId"); + + b.Navigation("Game"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("JackalWebHost2.Data.Entities.GameEntity", b => + { + b.Navigation("GamePlayers"); + }); + + modelBuilder.Entity("JackalWebHost2.Data.Entities.UserEntity", b => + { + b.Navigation("GamePlayers"); + + b.Navigation("Games"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/JackalWebHost2/Models/Auth/UserComparer.cs b/JackalWebHost2/Models/Auth/UserComparer.cs new file mode 100644 index 00000000..02a727c8 --- /dev/null +++ b/JackalWebHost2/Models/Auth/UserComparer.cs @@ -0,0 +1,15 @@ +namespace JackalWebHost2.Models.Auth +{ + public class UserComparer : IEqualityComparer + { + public bool Equals(User? x, User? y) + { + return x?.Id == y?.Id; + } + + public int GetHashCode(User obj) + { + return obj.Login.GetHashCode(); + } + } +} diff --git a/JackalWebHost2/Models/DrawMap.cs b/JackalWebHost2/Models/DrawMap.cs new file mode 100644 index 00000000..24dc7ba0 --- /dev/null +++ b/JackalWebHost2/Models/DrawMap.cs @@ -0,0 +1,8 @@ +namespace JackalWebHost2.Models; + +public class DrawMap +{ + public int Width; + public int Height; + public List Changes; +} \ No newline at end of file diff --git a/JackalWebHost2/Models/DrawMove.cs b/JackalWebHost2/Models/DrawMove.cs new file mode 100644 index 00000000..da7f2cc3 --- /dev/null +++ b/JackalWebHost2/Models/DrawMove.cs @@ -0,0 +1,15 @@ +namespace JackalWebHost2.Models; + +public class DrawMove +{ + public int MoveNum; + public PiratePosition From; + public PiratePosition To; + public DrawPosition? Prev; + public bool WithRumBottle; + public bool WithCoin; + public bool WithBigCoin; + public bool WithRespawn; + public bool WithLighthouse; + public bool WithQuake; +} \ No newline at end of file diff --git a/JackalWebHost2/Models/DrawPosition.cs b/JackalWebHost2/Models/DrawPosition.cs new file mode 100644 index 00000000..5115dfc4 --- /dev/null +++ b/JackalWebHost2/Models/DrawPosition.cs @@ -0,0 +1,10 @@ +using Jackal.Core.Domain; + +namespace JackalWebHost2.Models; + +public class DrawPosition(Position position) +{ + public int X = position.X; + + public int Y = position.Y; +} \ No newline at end of file diff --git a/JackalWebHost2/Models/DrawTeam.cs b/JackalWebHost2/Models/DrawTeam.cs new file mode 100644 index 00000000..d95a0d44 --- /dev/null +++ b/JackalWebHost2/Models/DrawTeam.cs @@ -0,0 +1,31 @@ +using Jackal.Core.Domain; + +namespace JackalWebHost2.Models; + +public class DrawTeam(Team team) +{ + /// + /// ИД команды + /// + public readonly int Id = team.Id; + + /// + /// Имя команды + /// + public readonly string Name = team.Name; + + /// + /// ИД пользователя + /// + public long UserId => team.UserId; + + /// + /// Человек + /// + public bool IsHuman => team.UserId > 0; + + /// + /// Позиция корабля + /// + public DrawPosition Ship = new(team.ShipPosition); +} \ No newline at end of file diff --git a/JackalWebHost2/Models/GameSettings.cs b/JackalWebHost2/Models/GameSettings.cs new file mode 100644 index 00000000..908d48d9 --- /dev/null +++ b/JackalWebHost2/Models/GameSettings.cs @@ -0,0 +1,32 @@ +using Jackal.Core; +using JackalWebHost2.Models.Player; + +namespace JackalWebHost2.Models; + +public class GameSettings +{ + /// + /// Игроки robot/human + /// + public PlayerModel[] Players { get; set; } = null!; + + /// + /// ИД карты, по нему генерируется расположение клеток + /// + public int? MapId { get; set; } + + /// + /// Размер стороны карты с учетом воды + /// + public int? MapSize { get; set; } + + /// + /// Название игрового набора клеток + /// + public string? TilesPackName { get; set; } + + /// + /// Режим игры + /// + public GameModeType? GameMode { get; set; } +} \ No newline at end of file diff --git a/JackalWebHost2/Models/GameStatistics.cs b/JackalWebHost2/Models/GameStatistics.cs new file mode 100644 index 00000000..3dc52a72 --- /dev/null +++ b/JackalWebHost2/Models/GameStatistics.cs @@ -0,0 +1,32 @@ +namespace JackalWebHost2.Models; + +/// +/// Текущая статистика игры +/// +public class GameStatistics +{ + /// + /// Номер хода + /// + public int TurnNumber; + + /// + /// Конец игры + /// + public bool IsGameOver; + + /// + /// Игровое сообщение + /// + public string GameMessage; + + /// + /// ИД команды пиратов чей ход + /// + public int CurrentTeamId; + + /// + /// ИД пользователя чей ход + /// + public long CurrentUserId; +} \ No newline at end of file diff --git a/JackalWebHost2/Models/LevelChange.cs b/JackalWebHost2/Models/LevelChange.cs new file mode 100644 index 00000000..b82c91a5 --- /dev/null +++ b/JackalWebHost2/Models/LevelChange.cs @@ -0,0 +1,8 @@ +namespace JackalWebHost2.Models; + +public class LevelChange +{ + public int Level; + public int Coins; + public int BigCoins; +} \ No newline at end of file diff --git a/JackalWebHost2/Models/LevelPosition.cs b/JackalWebHost2/Models/LevelPosition.cs new file mode 100644 index 00000000..bc01b420 --- /dev/null +++ b/JackalWebHost2/Models/LevelPosition.cs @@ -0,0 +1,13 @@ +using Jackal.Core; +using Jackal.Core.Domain; + +namespace JackalWebHost2.Models; + +public class LevelPosition(TilePosition position) +{ + public int X = position.X; + + public int Y = position.Y; + + public int Level = position.Level; +} \ No newline at end of file diff --git a/JackalWebHost2/Models/LoadGameResult.cs b/JackalWebHost2/Models/LoadGameResult.cs new file mode 100644 index 00000000..faef2a85 --- /dev/null +++ b/JackalWebHost2/Models/LoadGameResult.cs @@ -0,0 +1,26 @@ +using Jackal.Core; + +namespace JackalWebHost2.Models; + +public class LoadGameResult +{ + public long GameId { get; init; } + + public GameModeType GameMode { get; init; } + + public string TilesPackName { get; init; } + + public List Pirates { get; init; } + + public DrawMap Map { get; init; } + + public int MapId { get; init; } + + public GameStatistics Statistics { get; init; } + + public List Teams { get; set; } + + public List TeamScores { get; set; } + + public List Moves { get; init; } +} \ No newline at end of file diff --git a/JackalWebHost2/Models/Lobby/Lobby.cs b/JackalWebHost2/Models/Lobby/Lobby.cs new file mode 100644 index 00000000..7ae2179f --- /dev/null +++ b/JackalWebHost2/Models/Lobby/Lobby.cs @@ -0,0 +1,22 @@ +namespace JackalWebHost2.Models.Lobby; + +public class Lobby +{ + public string Id { get; set; } = ""; + + public long OwnerId { get; set; } + + public Dictionary LobbyMembers { get; set; } = new(); + + public DateTimeOffset CreatedAt { get; set; } + + public DateTimeOffset? ClosedAt { get; set; } + + public GameSettings GameSettings { get; set; } + + public int NumberOfPlayers { get; set; } + + public long? GameId { get; set; } + + public long[] GameMembers { get; set; } = []; +} \ No newline at end of file diff --git a/JackalWebHost2/Models/Lobby/LobbyMember.cs b/JackalWebHost2/Models/Lobby/LobbyMember.cs new file mode 100644 index 00000000..7421366c --- /dev/null +++ b/JackalWebHost2/Models/Lobby/LobbyMember.cs @@ -0,0 +1,14 @@ +namespace JackalWebHost2.Models.Lobby; + +public class LobbyMember +{ + public long UserId { get; set; } + + public string UserName { get; set; } + + public long? TeamId { get; set; } + + public DateTimeOffset LastSeen { get; set; } + + public DateTimeOffset JoinedAt { get; set; } +} \ No newline at end of file diff --git a/JackalWebHost2/Models/Map/CheckLandingResult.cs b/JackalWebHost2/Models/Map/CheckLandingResult.cs new file mode 100644 index 00000000..459e7fc6 --- /dev/null +++ b/JackalWebHost2/Models/Map/CheckLandingResult.cs @@ -0,0 +1,27 @@ +namespace JackalWebHost2.Models.Map; + +/// +/// Результат проверки места высадки +/// +public class CheckLandingResult(MapPositionId position) +{ + /// + /// Позиция + /// + public MapPositionId Position { get; } = position; + + /// + /// Сложность + /// + public DifficultyLevel Difficulty { get; set; } + + /// + /// Золото + /// + public int Coins { get; set; } + + /// + /// Людоеды + /// + public int Cannibals { get; set; } +} \ No newline at end of file diff --git a/JackalWebHost2/Models/Map/DifficultyLevel.cs b/JackalWebHost2/Models/Map/DifficultyLevel.cs new file mode 100644 index 00000000..b39f90bb --- /dev/null +++ b/JackalWebHost2/Models/Map/DifficultyLevel.cs @@ -0,0 +1,8 @@ +namespace JackalWebHost2.Models.Map; + +public enum DifficultyLevel +{ + Easy = 0, + Medium = 1, + Hard = 2 +} \ No newline at end of file diff --git a/JackalWebHost2/Models/Map/MapPositionId.cs b/JackalWebHost2/Models/Map/MapPositionId.cs new file mode 100644 index 00000000..200cf770 --- /dev/null +++ b/JackalWebHost2/Models/Map/MapPositionId.cs @@ -0,0 +1,13 @@ +namespace JackalWebHost2.Models.Map; + +/// +/// Позиция относительно игровой карты, +/// на фронте определяется по индексу +/// +public enum MapPositionId : byte +{ + Down = 0, + Left = 1, + Up = 2, + Right = 3 +} \ No newline at end of file diff --git a/JackalWebHost2/Models/Map/MapUtils.cs b/JackalWebHost2/Models/Map/MapUtils.cs new file mode 100644 index 00000000..934a30cf --- /dev/null +++ b/JackalWebHost2/Models/Map/MapUtils.cs @@ -0,0 +1,23 @@ +using Jackal.Core.Domain; + +namespace JackalWebHost2.Models.Map; + +public static class MapUtils +{ + public static MapPositionId ToMapPositionId(Position shipPosition, int mapSize) + { + if (shipPosition.Y == 0) + return MapPositionId.Down; + + if (shipPosition.X == 0) + return MapPositionId.Left; + + if (shipPosition.Y == mapSize - 1) + return MapPositionId.Up; + + if (shipPosition.X == mapSize - 1) + return MapPositionId.Right; + + throw new ArgumentException("Wrong init ship position", nameof(shipPosition)); + } +} \ No newline at end of file diff --git a/JackalWebHost2/Models/NetGameSettings.cs b/JackalWebHost2/Models/NetGameSettings.cs new file mode 100644 index 00000000..ce57b1b8 --- /dev/null +++ b/JackalWebHost2/Models/NetGameSettings.cs @@ -0,0 +1,29 @@ +using Jackal.Core; +using JackalWebHost2.Models.Auth; + +namespace JackalWebHost2.Models +{ + public class NetGameSettings : ICompletable + { + public NetGameSettings() + { + } + + public NetGameSettings(User creator) + { + Users.Add(creator); + CreatorId = creator.Id; + } + + public long Id { get; set; } + + public long? GameId { get; set; } + public bool IsCompleted => GameId.HasValue; + + public long CreatorId { get; set; } + + public HashSet Users { get; } = new (new UserComparer()); + + public GameSettings Settings { get; set; } = new(); + } +} diff --git a/JackalWebHost2/Models/PirateChange.cs b/JackalWebHost2/Models/PirateChange.cs new file mode 100644 index 00000000..ce738141 --- /dev/null +++ b/JackalWebHost2/Models/PirateChange.cs @@ -0,0 +1,22 @@ +using Jackal.Core.Domain; + +namespace JackalWebHost2.Models; + +public class PirateChange(Pirate pirate) +{ + public Guid Id = pirate.Id; + + public PirateType Type = pirate.Type; + + public int TeamId = pirate.TeamId; + + public LevelPosition Position = new(pirate.Position); + + public bool? IsAlive = null; + + public bool? IsDrunk = pirate.IsDrunk; + + public bool? IsInTrap = pirate.IsInTrap; + + public bool? IsInHole = pirate.IsInHole; +} \ No newline at end of file diff --git a/JackalWebHost2/Models/PiratePosition.cs b/JackalWebHost2/Models/PiratePosition.cs new file mode 100644 index 00000000..3872a0bf --- /dev/null +++ b/JackalWebHost2/Models/PiratePosition.cs @@ -0,0 +1,9 @@ +namespace JackalWebHost2.Models; + +public class PiratePosition +{ + public List PirateIds; + public int Level; + public int X; + public int Y; +} \ No newline at end of file diff --git a/JackalWebHost2/Models/Player/PlayerModel.cs b/JackalWebHost2/Models/Player/PlayerModel.cs new file mode 100644 index 00000000..4c4bd3e1 --- /dev/null +++ b/JackalWebHost2/Models/Player/PlayerModel.cs @@ -0,0 +1,12 @@ +using JackalWebHost2.Models.Map; + +namespace JackalWebHost2.Models.Player; + +public class PlayerModel +{ + public long UserId { get; set; } + + public PlayerType Type { get; set; } + + public MapPositionId Position { get; set; } +} \ No newline at end of file diff --git a/JackalWebHost2/Models/Player/PlayerType.cs b/JackalWebHost2/Models/Player/PlayerType.cs new file mode 100644 index 00000000..d6bf753c --- /dev/null +++ b/JackalWebHost2/Models/Player/PlayerType.cs @@ -0,0 +1,9 @@ +namespace JackalWebHost2.Models.Player; + +public enum PlayerType +{ + Human = 0, + Robot = 1, + Robot2 = 2, + Robot3 = 3 +} \ No newline at end of file diff --git a/JackalWebHost2/Models/StartGameModel.cs b/JackalWebHost2/Models/StartGameModel.cs new file mode 100644 index 00000000..9454297f --- /dev/null +++ b/JackalWebHost2/Models/StartGameModel.cs @@ -0,0 +1,6 @@ +namespace JackalWebHost2.Models; + +public class StartGameModel +{ + public GameSettings Settings { get; set; } +} \ No newline at end of file diff --git a/JackalWebHost2/Models/StartGameResult.cs b/JackalWebHost2/Models/StartGameResult.cs new file mode 100644 index 00000000..c124992e --- /dev/null +++ b/JackalWebHost2/Models/StartGameResult.cs @@ -0,0 +1,22 @@ +using Jackal.Core; + +namespace JackalWebHost2.Models; + +public class StartGameResult +{ + public long GameId { get; init; } + + public GameModeType GameMode { get; init; } + + public List Pirates { get; init; } + + public DrawMap Map { get; init; } + + public int MapId { get; init; } + + public GameStatistics Statistics { get; init; } + + public List Teams { get; set; } + + public List Moves { get; init; } +} \ No newline at end of file diff --git a/JackalWebHost2/Models/TeamScore.cs b/JackalWebHost2/Models/TeamScore.cs new file mode 100644 index 00000000..bf5cde84 --- /dev/null +++ b/JackalWebHost2/Models/TeamScore.cs @@ -0,0 +1,12 @@ +using Jackal.Core.Domain; + +namespace JackalWebHost2.Models; + +public class TeamScore(Team team) +{ + public int TeamId = team.Id; + + public int Coins = team.Coins; + + public int RumBottles = team.RumBottles; +} \ No newline at end of file diff --git a/JackalWebHost2/Models/TileChange.cs b/JackalWebHost2/Models/TileChange.cs new file mode 100644 index 00000000..75c816a3 --- /dev/null +++ b/JackalWebHost2/Models/TileChange.cs @@ -0,0 +1,14 @@ +namespace JackalWebHost2.Models; + +public class TileChange +{ + public string BackgroundImageSrc; + public int Rotate; + + public bool IsUnknown; + + public LevelChange[] Levels; + + public int X; + public int Y; +} \ No newline at end of file diff --git a/JackalWebHost2/Models/TurnGameModel.cs b/JackalWebHost2/Models/TurnGameModel.cs new file mode 100644 index 00000000..7dcd3787 --- /dev/null +++ b/JackalWebHost2/Models/TurnGameModel.cs @@ -0,0 +1,10 @@ +namespace JackalWebHost2.Models; + +public class TurnGameModel +{ + public long GameId { get; set; } + + public int? TurnNum { get; set; } + + public Guid? PirateId { get; set; } +} \ No newline at end of file diff --git a/JackalWebHost2/Models/TurnGameResult.cs b/JackalWebHost2/Models/TurnGameResult.cs new file mode 100644 index 00000000..d6631bac --- /dev/null +++ b/JackalWebHost2/Models/TurnGameResult.cs @@ -0,0 +1,14 @@ +namespace JackalWebHost2.Models; + +public class TurnGameResult +{ + public List PirateChanges { get; init; } + + public List Changes { get; init; } + + public GameStatistics Statistics { get; init; } + + public List TeamScores { get; set; } + + public List Moves { get; init; } +} \ No newline at end of file diff --git a/JackalWebHost2/Models/User.cs b/JackalWebHost2/Models/User.cs new file mode 100644 index 00000000..e0f81e55 --- /dev/null +++ b/JackalWebHost2/Models/User.cs @@ -0,0 +1,10 @@ +namespace JackalWebHost2.Models; + +public class User +{ + public long Id { get; init; } + + public required string Login { get; init; } + + public string Rank { get; init; } +} \ No newline at end of file diff --git a/JackalWebHost2/Program.cs b/JackalWebHost2/Program.cs new file mode 100644 index 00000000..e94cfab3 --- /dev/null +++ b/JackalWebHost2/Program.cs @@ -0,0 +1,182 @@ +using FluentValidation; +using FluentValidation.AspNetCore; +using Jackal.Core; +using JackalWebHost2.Controllers.Hubs; +using JackalWebHost2.Data; +using JackalWebHost2.Data.Interfaces; +using JackalWebHost2.Data.Repositories; +using JackalWebHost2.Infrastructure; +using JackalWebHost2.Infrastructure.Auth; +using JackalWebHost2.Infrastructure.Middleware; +using JackalWebHost2.Models; +using JackalWebHost2.Services; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace JackalWebHost2; + +public class Program +{ + public static async Task Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + ConfigureServices(builder); + + var app = builder.Build(); + ConfigurePipeline(app); + await app.RunAsync(); + } + + private static void ConfigurePipeline(WebApplication app) + { + if (app.Environment.IsDevelopment() || + app.Environment.IsStaging()) + { + app.UseSwagger(); + app.UseSwaggerUI(); + } + + app.UseCors(CorsDefaults.AllOrigins); + app.UseStaticFiles(); + app.UseCorsHeaders(); + + app.UseRouting(); + + app.UseCookiePolicy(); + app.UseAuthentication(); + app.UseAuthorization(); + + app.MapHub("/gamehub"); + app.MapControllers(); + } + + private static void ConfigureServices(WebApplicationBuilder builder) + { + var services = builder.Services; + + services.AddSignalR().AddNewtonsoftJsonProtocol(jsonOpt => + { + var enumConverter = new StringEnumConverter(); + jsonOpt.PayloadSerializerSettings.Converters.Add(enumConverter); + jsonOpt.PayloadSerializerSettings.NullValueHandling = NullValueHandling.Ignore; + jsonOpt.PayloadSerializerSettings.DateFormatString = "dd.MM.yyyy"; + }); + + services + .AddControllers(opt => + { + opt.SuppressAsyncSuffixInActionNames = true; + opt.Filters.Add(); + opt.Filters.Add(); + }) + .AddNewtonsoftJson(jsonOpt => + { + var enumConverter = new StringEnumConverter(); + jsonOpt.SerializerSettings.Converters.Add(enumConverter); + jsonOpt.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; + jsonOpt.SerializerSettings.DateFormatString = "dd.MM.yyyy"; + }); + + services + .AddSwaggerGen(opt => + { + opt.AddSignalRSwaggerGen(); + }) + .AddMemoryCache() + .AddCors(options => + { + options.AddPolicy(name: CorsDefaults.AllOrigins, + act => + { + act.WithOrigins( + "http://localhost:5130", + "http://localhost:5173", + "http://116.203.101.2", + "http://jackal2.online", + "http://jackal.team", + "https://jackal.team" + ); + act.AllowAnyMethod(); + act.AllowAnyHeader(); + act.AllowCredentials(); + }); + }); + + services + .AddAuthentication(options => + { + options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(AuthDefaults.FastAuthScheme, options => + { + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidIssuer = AuthDefaults.Issuer, + ValidateAudience = true, + ValidAudience = AuthDefaults.Audience, + ValidateLifetime = true, + IssuerSigningKey = AuthDefaults.GetSymmetricSecurityKey(), + ValidateIssuerSigningKey = true, + }; + options.Events = new JwtBearerEvents + { + OnMessageReceived = context => + { + var accessToken = context.Request.Query["access_token"]; + + var path = context.HttpContext.Request.Path; + if (!string.IsNullOrEmpty(accessToken) && + (path.StartsWithSegments("/gamehub"))) + { + context.Token = accessToken; + } + return Task.CompletedTask; + } + }; + }); + + services + .AddAuthorization(options => + { + options.AddPolicy(AuthDefaults.FastAuthPolicy, policy => policy + .AddAuthenticationSchemes(AuthDefaults.FastAuthScheme) + .RequireAuthenticatedUser()); + }); + + services + .AddValidatorsFromAssemblyContaining() + .AddFluentValidationAutoValidation(); + + services.AddHostedService(); + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + var connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); + if (!string.IsNullOrEmpty(connectionString)) + { + services.AddDbContext(options => options.UseNpgsql(connectionString)); + services.AddDatabaseDeveloperPageExceptionFilter(); + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + } + else + { + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + } + + services.AddSingleton, StateRepositoryInMemory>(); + services.AddSingleton, StateRepositoryInMemory>(); + services.AddScoped(); + } +} \ No newline at end of file diff --git a/JackalWebHost2/Properties/launchSettings.json b/JackalWebHost2/Properties/launchSettings.json new file mode 100644 index 00000000..89e54b4e --- /dev/null +++ b/JackalWebHost2/Properties/launchSettings.json @@ -0,0 +1,39 @@ +{ + "profiles": { + "WebApiDev": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "http://localhost:5130/swagger/index.html", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5130;" + }, + "WebApiTest": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "http://localhost:5130/swagger/index.html", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Staging" + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5130;" + }, + "WebApiProd": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Production" + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5130;" + }, + "Docker": { + "commandName": "Docker", + "launchBrowser": true, + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", + "publishAllPorts": true, + "useSSL": true + } + } +} \ No newline at end of file diff --git a/JackalWebHost2/Properties/serviceDependencies.json b/JackalWebHost2/Properties/serviceDependencies.json new file mode 100644 index 00000000..33703d58 --- /dev/null +++ b/JackalWebHost2/Properties/serviceDependencies.json @@ -0,0 +1,3 @@ +{ + "dependencies": {} +} \ No newline at end of file diff --git a/JackalWebHost2/Properties/serviceDependencies.local.json b/JackalWebHost2/Properties/serviceDependencies.local.json new file mode 100644 index 00000000..33703d58 --- /dev/null +++ b/JackalWebHost2/Properties/serviceDependencies.local.json @@ -0,0 +1,3 @@ +{ + "dependencies": {} +} \ No newline at end of file diff --git a/JackalWebHost2/Services/ActiveGamesPollingService.cs b/JackalWebHost2/Services/ActiveGamesPollingService.cs new file mode 100644 index 00000000..2dd5303d --- /dev/null +++ b/JackalWebHost2/Services/ActiveGamesPollingService.cs @@ -0,0 +1,57 @@ +using Jackal.Core; +using JackalWebHost2.Controllers.Hubs; +using JackalWebHost2.Controllers.Models.Services; +using JackalWebHost2.Data.Entities; +using JackalWebHost2.Data.Interfaces; +using Microsoft.AspNetCore.SignalR; + +namespace JackalWebHost2.Services +{ + public class ActiveGamesPollingService : BackgroundService + { + private const string CALLBACK_GET_ACTIVE_GAMES = "GetActiveGames"; + + private readonly IServiceProvider _services; + private readonly IStateRepository _gameStateRepository; + + public ActiveGamesPollingService( + IServiceProvider services, + IStateRepository gameStateRepository) + { + _services = services; + _gameStateRepository = gameStateRepository; + } + + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + if (_gameStateRepository.HasChanges()) + { + using var scope = _services.CreateScope(); + var gameHubContext = scope.ServiceProvider.GetRequiredService>(); + var currentValue = new AllActiveGamesResponse + { + GamesEntries = _gameStateRepository.GetEntries().Select(ToActiveGame).ToList() + }; + await gameHubContext.Clients.All.SendAsync(CALLBACK_GET_ACTIVE_GAMES, currentValue, stoppingToken); + _gameStateRepository.ResetChanges(); + } + + await Task.Delay(15000, stoppingToken); + } + } + + private ActiveGameInfo ToActiveGame(CacheEntry entry) + { + return new ActiveGameInfo + { + GameId = entry.ObjectId, + Creator = entry.Creator, + Players = entry.Players, + TimeStamp = entry.TimeStamp + }; + } + } +} diff --git a/JackalWebHost2/Services/DrawService.cs b/JackalWebHost2/Services/DrawService.cs new file mode 100644 index 00000000..d44be327 --- /dev/null +++ b/JackalWebHost2/Services/DrawService.cs @@ -0,0 +1,291 @@ +using Jackal.Core; +using Jackal.Core.Domain; +using JackalWebHost2.Models; + +namespace JackalWebHost2.Services; + +public class DrawService : IDrawService +{ + public List GetPirateChanges(Board board, Board prevBoard) + { + var pirateChanges = new List(); + + var idList = board.AllPirates.Union(prevBoard.AllPirates).Select(x => x.Id).Distinct(); + foreach (var guid in idList) + { + var newPirate = board.AllPirates.FirstOrDefault(x => x.Id == guid); + var oldPirate = prevBoard.AllPirates.FirstOrDefault(x => x.Id == guid); + + PirateChange pirateChange; + if (newPirate == null) + { + var deadPirate = board.DeadPirates.First(x => x.Id == oldPirate.Id); + + pirateChange = new PirateChange(deadPirate) { IsAlive = false }; + pirateChanges.Add(pirateChange); + } + else if (oldPirate == null) + { + pirateChange = new PirateChange(newPirate) { IsAlive = true }; + pirateChanges.Add(pirateChange); + } + else if (oldPirate.Position != newPirate.Position + || oldPirate.IsDrunk != newPirate.IsDrunk + || oldPirate.IsInTrap != newPirate.IsInTrap + || oldPirate.IsInHole != newPirate.IsInHole) + { + pirateChange = new PirateChange(newPirate) + { + IsDrunk = oldPirate.IsDrunk != newPirate.IsDrunk ? newPirate.IsDrunk : null, + IsInTrap = newPirate.IsInTrap ? true : null, + IsInHole = oldPirate.IsInHole != newPirate.IsInHole ? newPirate.IsInHole : null + }; + pirateChanges.Add(pirateChange); + } + } + + return pirateChanges; + } + + public List GetTileChanges(Board board, Board prevBoard) + { + var tileChanges = new List(); + + for (int y = 0; y < board.MapSize; y++) + { + for (int x = 0; x < board.MapSize; x++) + { + var tile = board.Map[x, y]; + var prevTile = prevBoard.Map[x, y]; + if (tile == prevTile) + continue; + + var tileChange = Draw(tile, board.Teams); + tileChange.X = x; + tileChange.Y = y; + tileChanges.Add(tileChange); + } + } + + return tileChanges; + } + + public GameStatistics GetStatistics(Game game) => + new() + { + TurnNumber = game.TurnNumber, + IsGameOver = game.IsGameOver, + GameMessage = game.GameMessage, + CurrentTeamId = game.CurrentTeamId, + CurrentUserId = game.Board.Teams[game.CurrentPlayerIndex].UserId + }; + + public List GetAvailableMoves(Game game) + { + var result = new List(); + var pirates = new List(); + + int index = 0; + foreach (var move in game.GetAvailableMoves()) + { + var pirate = pirates.FirstOrDefault(p => + p.X == move.From.X && p.Y == move.From.Y && (move.WithRumBottle || p.Level == move.From.Level) + ); + + if (pirate == null) + { + var pirateIds = game.Board.AllPirates + .Where(x => !x.IsDrunk && !x.IsInHole && x.Position == move.From) + .Where(x => (!x.IsInTrap && !move.WithRumBottle) || move.WithRumBottle) + .Select(p => p.Id) + .ToList(); + + pirate = new PiratePosition + { + PirateIds = pirateIds, + X = move.From.X, + Y = move.From.Y, + Level = move.From.Level + }; + pirates.Add(pirate); + } + + result.Add(new DrawMove + { + MoveNum = index++, + WithRumBottle = move.WithRumBottle, + WithCoin = move.WithCoin, + WithBigCoin = move.WithBigCoin, + WithRespawn = move.WithRespawn, + WithLighthouse = move.WithLighthouse, + WithQuake = move.WithQuake, + From = pirate, + To = new PiratePosition + { + X = move.To.X, + Y = move.To.Y, + Level = move.To.Level + }, + Prev = move.Prev != null ? new DrawPosition(move.Prev) : null + }); + } + return result; + } + + public DrawMap Map(Board board) + { + var changes = new List(); + + for (int y = 0; y < board.MapSize; y++) + { + for (int x = 0; x < board.MapSize; x++) + { + var tile = board.Map[x, y]; + var tileChange = Draw(tile, board.Teams); + tileChange.X = x; + tileChange.Y = y; + changes.Add(tileChange); + } + } + + return new DrawMap{ + Width = board.MapSize, + Height = board.MapSize, + Changes = changes + }; + } + + private static TileChange Draw(Tile tile, Team[] teams) + { + var tileElement = new TileChange + { + Levels = new LevelChange[tile.Levels.Count] + }; + var teamShip = teams.FirstOrDefault(item => item.ShipPosition == tile.Position); + + for (int i = 0; i < tile.Levels.Count; i++) + { + var level = tile.Levels[i]; + tileElement.Levels[i] = DrawCoins(level, i, teamShip); + } + DrawTileBackground(tile, teamShip, ref tileElement); + + return tileElement; + } + + private static LevelChange DrawCoins(TileLevel level, int levelIndex, Team? teamShip) => + new() + { + Level = levelIndex, + Coins = teamShip?.Coins ?? level.Coins, + BigCoins = level.BigCoins + }; + + private static void DrawTileBackground(Tile tile, Team? teamShip, ref TileChange tileChange) + { + string filename; + switch (tile.Type) + { + case TileType.Unknown: + tileChange.IsUnknown = true; + filename = "back"; + break; + case TileType.Water: + filename = teamShip != null ? $"ship_{teamShip.Id + 1}" : "water"; + break; + case TileType.Empty: + filename = $"empty{tile.Code}"; + break; + case TileType.Coin: + case TileType.BigCoin: + filename = "chest"; + break; + case TileType.Fort: + filename = "fort"; + break; + case TileType.RespawnFort: + filename = "respawn"; + break; + case TileType.RumBarrel: + filename = "rumbar"; + break; + case TileType.RumBottle: + filename = tile.Used ? $"used_rum{tile.Code}" : $"rum{tile.Code}"; + break; + case TileType.Horse: + filename = "horse"; + break; + case TileType.Cannon: + filename = "cannon"; + break; + case TileType.Crocodile: + filename = "croc"; + break; + case TileType.Airplane: + filename = tile.Used ? "used_airplane" : "airplane"; + break; + case TileType.Balloon: + filename = "balloon"; + break; + case TileType.Ice: + filename = "ice"; + break; + case TileType.Trap: + filename = "trap"; + break; + case TileType.Cannibal: + filename = "canibal"; + break; + case TileType.Lighthouse: + filename = "lighthouse"; + break; + case TileType.BenGunn: + filename = tile.Used ? "used_bengunn" : "bengunn"; + break; + case TileType.Caramba: + filename = "caramba"; + break; + case TileType.Jungle: + filename = "jungle"; + break; + case TileType.Hole: + filename = "hole"; + break; + case TileType.Quake: + filename = "quake"; + break; + case TileType.Cannabis: + filename = "cannabis"; + break; + case TileType.Spinning: + switch (tile.SpinningCount) + { + case 2: + filename = "forest"; + break; + case 3: + filename = "desert"; + break; + case 4: + filename = "swamp"; + break; + case 5: + filename = "mount"; + break; + default: + throw new NotSupportedException(); + } + break; + case TileType.Arrow: + var search = ArrowsCodesHelper.Search(tile.Code); + filename = $"arrow{search.ArrowType + 1}"; + break; + + default: + throw new NotSupportedException(); + } + + tileChange.BackgroundImageSrc = $"/fields/{filename}.png"; + tileChange.Rotate = (int)tile.Direction; + } +} \ No newline at end of file diff --git a/JackalWebHost2/Services/GameService.cs b/JackalWebHost2/Services/GameService.cs new file mode 100644 index 00000000..d9e35300 --- /dev/null +++ b/JackalWebHost2/Services/GameService.cs @@ -0,0 +1,188 @@ +using Jackal.Core; +using Jackal.Core.MapGenerator; +using Jackal.Core.Players; +using JackalWebHost2.Data.Interfaces; +using JackalWebHost2.Exceptions; +using JackalWebHost2.Models; +using JackalWebHost2.Models.Player; + +namespace JackalWebHost2.Services; + +public class GameService : IGameService +{ + private readonly IStateRepository _gameStateRepository; + private readonly IGameRepository _gameRepository; + private readonly IUserRepository _userRepository; + private readonly IDrawService _drawService; + + public GameService( + IStateRepository gameStateRepository, + IGameRepository gameRepository, + IUserRepository userRepository, + IDrawService drawService) + { + _gameStateRepository = gameStateRepository; + _gameRepository = gameRepository; + _userRepository = userRepository; + _drawService = drawService; + } + + public async Task LoadGame(long userId, long gameId) + { + var game = _gameStateRepository.GetObject(gameId); + if (game == null) + { + throw new GameNotFoundException(); + } + + var map = _drawService.Map(game.Board); + + List pirateChanges = []; + foreach (var pirate in game.Board.AllPirates) + { + pirateChanges.Add(new PirateChange(pirate)); + } + + return new LoadGameResult + { + GameId = gameId, + GameMode = game.GameMode, + TilesPackName = game.Board.Generator.TilesPackName, + Pirates = pirateChanges, + Map = map, + MapId = game.Board.Generator.MapId, + Statistics = _drawService.GetStatistics(game), + Teams = game.Board.Teams.Select(team => new DrawTeam(team)).ToList(), + TeamScores = game.Board.Teams.Select(team => new TeamScore(team)).ToList(), + Moves = game.CurrentPlayer is HumanPlayer + ? _drawService.GetAvailableMoves(game) + : [] + }; + } + + public async Task StartGame(User user, StartGameModel request) + { + GameSettings gameSettings = request.Settings; + IPlayer[] gamePlayers = new IPlayer[gameSettings.Players.Length]; + int index = 0; + + foreach (var player in gameSettings.Players) + { + User? userPlayer = null; + if (player.Type == PlayerType.Human) + { + userPlayer = await _userRepository.GetUser(player.UserId, CancellationToken.None); + } + + gamePlayers[index++] = player.Type switch + { + PlayerType.Robot => new RandomPlayer(), + PlayerType.Robot2 => new EasyPlayer(), + PlayerType.Robot3 => new OakioPlayer(), + PlayerType.Human => userPlayer != null + ? new HumanPlayer(userPlayer.Id, userPlayer.Login) + : throw new PlayerNotFoundException(), + _ => throw new PlayerNotFoundException() + }; + } + + gameSettings.MapId ??= new Random().Next(); + + // для ручной отладки можно использовать закомментированные генераторы карт + int mapSize = gameSettings.MapSize ?? 5; + IMapGenerator mapGenerator = new RandomMapGenerator(gameSettings.MapId.Value, mapSize, gameSettings.TilesPackName); + // mapGenerator = new OneTileMapGenerator(TileParams.Airplane()); + // mapGenerator = new ThreeTileMapGenerator( + // TileParams.Airplane(), TileParams.Airplane(), TileParams.Quake() + // ); + + var gameMode = gameSettings.GameMode ?? GameModeType.FreeForAll; + var gameRequest = new GameRequest(mapSize, mapGenerator, gamePlayers, gameMode); + var game = new Game(gameRequest); + + var gameId = await _gameRepository.CreateGame(user.Id, game); + + var players = await _userRepository.GetUsers( + gameSettings.Players.Where(it => it.UserId > 0).Select(it => it.UserId).Distinct().ToArray(), CancellationToken.None); + _gameStateRepository.CreateObject(user, gameId, game, players.ToHashSet()); + + var map = _drawService.Map(game.Board); + + List pirateChanges = []; + foreach (var pirate in game.Board.AllPirates) + { + pirateChanges.Add(new PirateChange(pirate)); + } + + return new StartGameResult + { + GameId = gameId, + GameMode = gameMode, + Pirates = pirateChanges, + Map = map, + MapId = gameSettings.MapId.Value, + Statistics = _drawService.GetStatistics(game), + Teams = game.Board.Teams.Select(team => new DrawTeam(team)).ToList(), + Moves = game.CurrentPlayer is HumanPlayer + ? _drawService.GetAvailableMoves(game) + : [] + }; + } + + public async Task MakeGameTurn(long userId, TurnGameModel request) + { + var game = _gameStateRepository.GetObject(request.GameId); + if (game == null) + { + throw new GameNotFoundException(); + } + + if (game.IsGameOver) + { + return new TurnGameResult + { + PirateChanges = [], + Changes = [], + Statistics = _drawService.GetStatistics(game), + TeamScores = game.Board.Teams.Select(team => new TeamScore(team)).ToList(), + Moves = [] + }; + } + + var prevBoardStr = JsonHelper.SerializeWithType(game.Board); + + if (game.CurrentPlayer is IHumanPlayer humanPlayer) + { + if (humanPlayer.UserId != userId) + { + throw new PlayerNotFoundException(); + } + + if (request.TurnNum.HasValue) + { + humanPlayer.SetMove(request.TurnNum.Value, request.PirateId); + } + } + + game.Turn(); + if (game.IsGameOver) + { + game.Board.ShowUnknownTiles(); + } + + await _gameRepository.UpdateGame(request.GameId, game); + _gameStateRepository.UpdateObject(request.GameId, game); + var prevBoard = JsonHelper.DeserializeWithType(prevBoardStr); + + return new TurnGameResult + { + PirateChanges = _drawService.GetPirateChanges(game.Board, prevBoard), + Changes = _drawService.GetTileChanges(game.Board, prevBoard), + Statistics = _drawService.GetStatistics(game), + TeamScores = game.Board.Teams.Select(team => new TeamScore(team)).ToList(), + Moves = game.CurrentPlayer is HumanPlayer + ? _drawService.GetAvailableMoves(game) + : [] + }; + } +} \ No newline at end of file diff --git a/JackalWebHost2/Services/IDrawService.cs b/JackalWebHost2/Services/IDrawService.cs new file mode 100644 index 00000000..5e6b0f63 --- /dev/null +++ b/JackalWebHost2/Services/IDrawService.cs @@ -0,0 +1,32 @@ +using Jackal.Core; +using JackalWebHost2.Models; + +namespace JackalWebHost2.Services; + +public interface IDrawService +{ + /// + /// Получить изменения пиратов + /// + List GetPirateChanges(Board board, Board prevBoard); + + /// + /// Получить изменения клеток + /// + public List GetTileChanges(Board board, Board prevBoard); + + /// + /// Формирование статистики после хода + /// + GameStatistics GetStatistics(Game game); + + /// + /// Получить доступные ходы + /// + List GetAvailableMoves(Game game); + + /// + /// Отрисовать карту + /// + DrawMap Map(Board board); +} \ No newline at end of file diff --git a/JackalWebHost2/Services/IGameService.cs b/JackalWebHost2/Services/IGameService.cs new file mode 100644 index 00000000..8a7bbe9f --- /dev/null +++ b/JackalWebHost2/Services/IGameService.cs @@ -0,0 +1,21 @@ +using JackalWebHost2.Models; + +namespace JackalWebHost2.Services; + +public interface IGameService +{ + /// + /// Загрузка игры + /// + Task LoadGame(long userId, long gameId); + + /// + /// Запуск игры + /// + Task StartGame(User userId, StartGameModel request); + + /// + /// Ход игры + /// + Task MakeGameTurn(long userId, TurnGameModel request); +} \ No newline at end of file diff --git a/JackalWebHost2/Services/ILobbyService.cs b/JackalWebHost2/Services/ILobbyService.cs new file mode 100644 index 00000000..1b131c29 --- /dev/null +++ b/JackalWebHost2/Services/ILobbyService.cs @@ -0,0 +1,42 @@ +using JackalWebHost2.Models; +using JackalWebHost2.Models.Lobby; + +namespace JackalWebHost2.Services; + +public interface ILobbyService +{ + /// + /// Создать лобби с заданными параметрами + /// + Task CreateLobby(User user, GameSettings gameSettings, CancellationToken token); + + /// + /// Присоединиться к лобби + /// + Task JoinLobby(string lobbyId, User user, CancellationToken token); + + /// + /// Начать игру из лобби + /// + Task StartGame(string lobbyId, User user, CancellationToken token); + + /// + /// Выгнать игрока + /// + Task KickPlayer(string lobbyId, User kickInitiator, long kickTarget, CancellationToken token); + + /// + /// Задать команду игроку + /// + Task AssignTeam(string lobbyId, User user, long assignFor, long? teamId, CancellationToken token); + + /// + /// Покинуть лобби + /// + Task LeaveLobby(User user, CancellationToken token); + + /// + /// Получить информацию о лобби + /// + Task GetLobbyInfo(string lobbyId, User user, CancellationToken token); +} \ No newline at end of file diff --git a/JackalWebHost2/Services/IMapService.cs b/JackalWebHost2/Services/IMapService.cs new file mode 100644 index 00000000..eb8717e2 --- /dev/null +++ b/JackalWebHost2/Services/IMapService.cs @@ -0,0 +1,9 @@ +using JackalWebHost2.Controllers.Models.Map; +using JackalWebHost2.Models.Map; + +namespace JackalWebHost2.Services; + +public interface IMapService +{ + List CheckLanding(CheckLandingRequest request); +} \ No newline at end of file diff --git a/JackalWebHost2/Services/LobbyService.cs b/JackalWebHost2/Services/LobbyService.cs new file mode 100644 index 00000000..f7fb267b --- /dev/null +++ b/JackalWebHost2/Services/LobbyService.cs @@ -0,0 +1,267 @@ +using JackalWebHost2.Data.Interfaces; +using JackalWebHost2.Exceptions; +using JackalWebHost2.Models; +using JackalWebHost2.Models.Lobby; + +namespace JackalWebHost2.Services; + +public class LobbyService : ILobbyService +{ + private readonly ILogger _logger; + private readonly ILobbyRepository _lobbyRepository; + private readonly IGameService _gameService; + private readonly TimeProvider _timeProvider; + + public LobbyService( + ILogger logger, + ILobbyRepository lobbyRepository, + IGameService gameService, + TimeProvider timeProvider) + { + _logger = logger; + _lobbyRepository = lobbyRepository; + _gameService = gameService; + _timeProvider = timeProvider; + } + + public async Task CreateLobby(User user, GameSettings gameSettings, CancellationToken token) + { + var userId = user.Id; + await LeaveLobby(user, token); + var lobby = new Lobby + { + Id = Guid.NewGuid().ToString("D"), + OwnerId = userId, + LobbyMembers = new Dictionary + { + [userId] = new() + { + UserId = userId, + UserName = user.Login, + TeamId = null, + LastSeen = _timeProvider.GetUtcNow(), + JoinedAt = _timeProvider.GetUtcNow() + } + }, + CreatedAt = _timeProvider.GetUtcNow(), + GameSettings = gameSettings, + NumberOfPlayers = gameSettings.Players.Length, + GameId = null + }; + + await _lobbyRepository.CreateLobby(lobby, token); + _logger.LogInformation("Lobby {LobbyId} created by user {UserId}", lobby.Id, userId); + return lobby; + } + + public async Task JoinLobby(string lobbyId, User user, CancellationToken token) + { + var userId = user.Id; + var userLobby = await _lobbyRepository.GetLobbyByUser(userId, token); + if (userLobby != null) + { + if (userLobby.Id == lobbyId) + { + return userLobby; + } + + await LeaveLobby(userLobby, userId, token); + } + + var lobby = await _lobbyRepository.GetLobbyInfo(lobbyId, false, token); + if (lobby == null) + { + throw new LobbyNotFoundException(); + } + + if (lobby.LobbyMembers.Count >= lobby.NumberOfPlayers) + { + throw new LobbyIsFullException(); + } + + var lobbyMember = new LobbyMember + { + UserId = userId, + UserName = user.Login, + TeamId = null, + LastSeen = _timeProvider.GetUtcNow(), + JoinedAt = _timeProvider.GetUtcNow() + }; + + await _lobbyRepository.AddUserToLobby(lobbyId, lobbyMember, token); + _logger.LogInformation("User {UserId} joined to {LobbyId} ", userId, lobbyId); + return await _lobbyRepository.GetLobbyInfo(lobbyId, false, token) ?? throw new NotSupportedException("Unexpected NRE"); + } + + public async Task StartGame(string lobbyId, User user, CancellationToken token) + { + var lobby = await _lobbyRepository.GetLobbyInfo(lobbyId, false, token); + if (lobby == null || lobby.ClosedAt != null) + { + throw new LobbyNotFoundException(); + } + + if (lobby.OwnerId != user.Id) + { + throw new UserIsNotLobbyOwnerException(); + } + + if (lobby.LobbyMembers.Values.Any(x => x.TeamId == null)) + { + throw new AllLobbyMembersMustHaveTeamException(); + } + + // todo нужно доработать назначение игроков - расставить правильно игроков по позициям + var startGameModel = new StartGameModel { Settings = lobby.GameSettings }; + var game = await _gameService.StartGame(user, startGameModel); + + var gameMembers = lobby.LobbyMembers.Values + .Where(x => x.TeamId != null) + .Select(x => x.UserId) + .ToArray(); + + await _lobbyRepository.RemoveUsersFromLobby(lobbyId, token); + await _lobbyRepository.Close(lobbyId, _timeProvider.GetUtcNow(), game.GameId, gameMembers, token); + _logger.LogInformation("User {UserId} created game {GameId} from lobby {LobbyId} ", user.Id, game.GameId, lobbyId); + return await _lobbyRepository.GetLobbyInfo(lobbyId, true, token) ?? throw new NotSupportedException("Unexpected NRE"); + } + + public async Task KickPlayer(string lobbyId, User kickInitiator, long kickTarget, CancellationToken token) + { + var userId = kickInitiator.Id; + var lobby = await _lobbyRepository.GetLobbyInfo(lobbyId, false, token); + if (lobby == null) + { + throw new LobbyNotFoundException(); + } + + if (lobby.OwnerId != userId) + { + throw new UserIsNotLobbyOwnerException(); + } + + if (!lobby.LobbyMembers.ContainsKey(kickTarget)) + { + throw new UserIsNotFoundException(); + } + + await _lobbyRepository.RemoveUserFromLobbies(kickTarget, token); + _logger.LogInformation("User {UserId} was kicked from lobby {LobbyId} ", kickTarget, lobbyId); + } + + public async Task AssignTeam(string lobbyId, User user, long assignFor, long? teamId, CancellationToken token) + { + var userId = user.Id; + var lobby = await _lobbyRepository.GetLobbyInfo(lobbyId, false, token); + if (lobby == null) + { + throw new LobbyNotFoundException(); + } + + if (!lobby.LobbyMembers.ContainsKey(userId)) + { + throw new UserIsNotLobbyMemberException(); + } + + if (!lobby.LobbyMembers.TryGetValue(assignFor, out var lobbyMember)) + { + throw new UserIsNotFoundException(); + } + + if (lobbyMember.TeamId != null && lobby.OwnerId != userId) + { + throw new UserIsNotLobbyOwnerException(); + } + + if (teamId == null) + { + await _lobbyRepository.AssignTeam(lobbyId, assignFor, teamId, token); + return; + } + + if (teamId < 0 || teamId >= lobby.NumberOfPlayers) + { + // TODO Пока нет никакой привязки teamId, оставим просто массив игроков + // TODO Нужно привязывать игрока к конкретной команде + throw new TeamIsNotFoundException(); + } + + var memberWithTeam = lobby.LobbyMembers.Values.FirstOrDefault(x => x.TeamId == teamId); + if (memberWithTeam != null) + { + if (lobby.OwnerId != userId) + { + throw new UserIsNotLobbyOwnerException(); + } + + await _lobbyRepository.AssignTeam(lobbyId, memberWithTeam.UserId, lobbyMember.TeamId, token); + } + + await _lobbyRepository.AssignTeam(lobbyId, assignFor, teamId, token); + } + + public async Task LeaveLobby(User user, CancellationToken token) + { + var userId = user.Id; + var userLobby = await _lobbyRepository.GetLobbyByUser(userId, token); + if (userLobby == null) + { + return; + } + + await LeaveLobby(userLobby, userId, token); + } + + public async Task GetLobbyInfo(string lobbyId, User user, CancellationToken token) + { + var userId = user.Id; + var lobby = await _lobbyRepository.GetLobbyInfo(lobbyId, true, token); + if (lobby == null) + { + throw new LobbyNotFoundException(); + } + + if (!lobby.LobbyMembers.ContainsKey(userId)) + { + // Игра еще не началась, юзер не участник лобби + if (lobby.ClosedAt == null) + { + throw new UserIsNotLobbyMemberException(); + } + + // Игра началась, лобби уже пустое и юзер - не участник игры + if (!lobby.GameMembers.Contains(userId)) + { + throw new LobbyNotFoundException(); + } + } + + if (lobby.ClosedAt != null) + { + if (lobby.GameId == null) + { + throw new LobbyIsClosedException(); + } + + _logger.LogInformation("User {UserId} navigated to game {GameId} from {LobbyId} ", userId, lobby.GameId, lobbyId); + return lobby; + } + + await _lobbyRepository.UpdateUserKeepAlive(lobbyId, userId, _timeProvider.GetUtcNow(), token); + return lobby; + } + + private async Task LeaveLobby(Lobby userLobby, long userId, CancellationToken token) + { + if (userLobby.OwnerId == userId) + { + await _lobbyRepository.RemoveUsersFromLobby(userLobby.Id, token); + await _lobbyRepository.Close(userLobby.Id, _timeProvider.GetUtcNow(), userLobby.GameId, null, token); + _logger.LogInformation("User {UserId} closed his lobby {LobbyId} ", userId, userLobby.Id); + return; + } + + await _lobbyRepository.RemoveUserFromLobbies(userId, token); + _logger.LogInformation("User {UserId} left lobby {LobbyId} ", userId, userLobby.Id); + } +} \ No newline at end of file diff --git a/JackalWebHost2/Services/MapService.cs b/JackalWebHost2/Services/MapService.cs new file mode 100644 index 00000000..ff71e7d5 --- /dev/null +++ b/JackalWebHost2/Services/MapService.cs @@ -0,0 +1,94 @@ +using Jackal.Core; +using Jackal.Core.Domain; +using Jackal.Core.MapGenerator; +using JackalWebHost2.Controllers.Models.Map; +using JackalWebHost2.Models.Map; + +namespace JackalWebHost2.Services; + +public class MapService : IMapService +{ + public List CheckLanding(CheckLandingRequest request) + { + var mapGenerator = new RandomMapGenerator(request.MapId, request.MapSize, request.TilesPackName); + + var downLanding = new CheckLandingResult(MapPositionId.Down); + var leftLanding = new CheckLandingResult(MapPositionId.Left); + var upLanding = new CheckLandingResult(MapPositionId.Up); + var rightLanding = new CheckLandingResult(MapPositionId.Right); + + for (int i = 2; i <= request.MapSize - 3; i++) + { + var downTile = mapGenerator.GetNext(new Position(i, 1)); + SetLandingResult(downLanding, downTile); + + var leftTile = mapGenerator.GetNext(new Position(1, i)); + SetLandingResult(leftLanding, leftTile); + + var upTile = mapGenerator.GetNext(new Position(i, request.MapSize - 2)); + SetLandingResult(upLanding, upTile); + + var rightTile = mapGenerator.GetNext(new Position(request.MapSize - 2, i)); + SetLandingResult(rightLanding, rightTile); + } + + // порядок возврата на фронт: по возрастанию MapPositionId + var landingResults = new List + { + downLanding, leftLanding, upLanding, rightLanding + }; + + foreach (var landing in landingResults) + { + SetLandingDifficulty(landing, request.MapSize - 4); + } + + return landingResults; + } + + private static void SetLandingResult(CheckLandingResult landing, Tile tile) + { + landing.Coins += tile.CoinsCount(); + landing.Coins += tile.BigCoinsCount() * Constants.BigCoinValue; + landing.Cannibals += tile.Type == TileType.Cannibal ? 1 : 0; + } + + private static void SetLandingDifficulty(CheckLandingResult landing, int landSize) + { + switch (landSize) + { + case 1: + case 3: + if (landing.Cannibals > 0) + { + landing.Difficulty = DifficultyLevel.Hard; + } + else if (landing.Coins == 0) + { + landing.Difficulty = DifficultyLevel.Medium; + } + else + { + landing.Difficulty = DifficultyLevel.Easy; + } + + break; + default: + if (landing is { Cannibals: > 1, Coins: < 10 }) + { + landing.Difficulty = DifficultyLevel.Hard; + } + else if (landing.Cannibals > 1 || + landing is { Cannibals: > 0, Coins: < 10 }) + { + landing.Difficulty = DifficultyLevel.Medium; + } + else + { + landing.Difficulty = DifficultyLevel.Easy; + } + + break; + } + } +} \ No newline at end of file diff --git a/JackalWebHost2/appsettings.Development.json b/JackalWebHost2/appsettings.Development.json new file mode 100644 index 00000000..a7e3a2d4 --- /dev/null +++ b/JackalWebHost2/appsettings.Development.json @@ -0,0 +1,6 @@ +{ + "DetailedErrors": true, + "ConnectionStrings": { + "DefaultConnection": "" + } +} diff --git a/JackalWebHost2/appsettings.Staging.json b/JackalWebHost2/appsettings.Staging.json new file mode 100644 index 00000000..f8bc2f88 --- /dev/null +++ b/JackalWebHost2/appsettings.Staging.json @@ -0,0 +1,5 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Server=127.0.0.1;Port=5432;Database=jackal;User Id=postgres;Password=postgres;" + } +} diff --git a/JackalWebHost2/appsettings.json b/JackalWebHost2/appsettings.json new file mode 100644 index 00000000..6db8ab8f --- /dev/null +++ b/JackalWebHost2/appsettings.json @@ -0,0 +1,11 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Information", + "Microsoft.AspNetCore.Mvc": "Warning", + "Microsoft.AspNetCore.Routing": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..f288702d --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 00000000..2719d01b --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# Jackal game +

Web site https://jackal.team

+

Telegram web app https://t.me/jackal_game_bot/jackal

+

Rules https://jackal.su/rules

diff --git a/configs/nginx.conf b/configs/nginx.conf new file mode 100644 index 00000000..b3ba83b8 --- /dev/null +++ b/configs/nginx.conf @@ -0,0 +1,64 @@ +user root; +worker_processes auto; +pid /run/nginx.pid; +include /etc/nginx/modules-enabled/*.conf; + +events { + worker_connections 768; + # multi_accept on; +} + +http { + + ## + # Basic Settings + ## + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + # server_tokens off; + + # server_names_hash_bucket_size 64; + # server_name_in_redirect off; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + ## + # SSL Settings + ## + + ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE + ssl_prefer_server_ciphers on; + + ## + # Logging Settings + ## + + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + + ## + # Gzip Settings + ## + + gzip on; + + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_buffers 16 8k; + gzip_http_version 1.1; + gzip_min_length 256; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; + + ## + # Virtual Host Configs + ## + + include /etc/nginx/conf.d/*.conf; + include /etc/nginx/sites-enabled/*; +} \ No newline at end of file diff --git a/configs/nginx.site.conf b/configs/nginx.site.conf new file mode 100644 index 00000000..810c6fdb --- /dev/null +++ b/configs/nginx.site.conf @@ -0,0 +1,53 @@ +server { + listen 80 default_server; + listen [::]:80 default_server; + + server_name _; + + root /root/madddmax/jackal/Front/dist; + index index.html; + + location /api { + proxy_pass http://localhost:5130; + } + + location / { + add_header Cache-Control "max-age=2592000"; + try_files $uri $uri/ /index.html; + } +} + +server { + listen 443 ssl; + listen [::]:443 ipv6only=on ssl; + + server_name jackal.team; + + root /root/madddmax/jackal/Front/dist; + index index.html; + + ssl_certificate /etc/ssl/jackal_team_2025_05_04_bundle.crt; + ssl_certificate_key /etc/ssl/jackal_team_2025_05_04.key; + ssl_session_timeout 5m; + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + ssl_ciphers ALL:EECDH+aRSA+AESGCM:EDH+aRSA+AESGCM:EECDH+aRSA+AES:EDH+aRSA+AES; + ssl_prefer_server_ciphers on; + + location /api { + proxy_pass http://localhost:5130; + } + + location /gamehub { + proxy_pass http://localhost:5130; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $http_connection; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + } + + location / { + add_header Cache-Control "max-age=2592000"; + try_files $uri $uri/ /index.html; + } +} \ No newline at end of file diff --git a/global.json b/global.json new file mode 100644 index 00000000..dad2db5e --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "8.0.0", + "rollForward": "latestMajor", + "allowPrerelease": true + } +} \ No newline at end of file