From 34737280e2ca5c907345e988e338839f0969d1ac Mon Sep 17 00:00:00 2001 From: Shigeru Hagiwara Date: Wed, 22 Mar 2023 08:11:46 +0900 Subject: [PATCH] first step (#1) * first step --- build.gradle | 2 + docs/name.md | 14 + docs/spec.md | 21 + front/index.html | 4 +- front/package-lock.json | 142 ++++++- front/package.json | 10 +- front/public/1F3B4.svg | 12 + front/src/App.vue | 27 +- front/src/axios.ts | 13 + front/src/components/BetButton.vue | 42 ++ front/src/components/BetDialog.vue | 159 ++++++++ front/src/components/CardComponent.vue | 78 ++++ front/src/components/HelloWorld.vue | 38 -- front/src/components/PieComponent.vue | 66 ++++ front/src/computedStyles/namedColorSet.ts | 18 + front/src/main.ts | 5 +- front/src/router.ts | 27 ++ front/src/services/RoomService.ts | 59 +++ front/src/style.css | 109 +++--- front/src/types/Bet.ts | 7 + front/src/types/Card.ts | 54 +++ front/src/types/Room.ts | 21 + front/src/types/Status.ts | 12 + front/src/views/HomeView.vue | 78 ++++ front/src/views/RoomView.vue | 370 ++++++++++++++++++ front/tsconfig.node.json | 1 + .../moreslowly/oi/common/RoomLimitation.java | 10 + .../jp/moreslowly/oi/common/SessionKey.java | 10 + .../jp/moreslowly/oi/config/RedisConfig.java | 28 ++ .../RestResponseEntityExceptionHandler.java | 14 + .../oi/config/SchedulingConfig.java | 23 ++ .../oi/config/ServletCustomizer.java | 17 + .../jp/moreslowly/oi/config/TomcatConfig.java | 43 ++ .../jp/moreslowly/oi/config/WebMvcConfig.java | 15 + .../oi/controllers/RoomController.java | 102 +++++ .../oi/controllers/UsersController.java | 14 + src/main/java/jp/moreslowly/oi/dao/Bet.java | 35 ++ src/main/java/jp/moreslowly/oi/dao/Room.java | 156 ++++++++ .../java/jp/moreslowly/oi/dto/BetDto.java | 39 ++ src/main/java/jp/moreslowly/oi/dto/IdDto.java | 14 + .../jp/moreslowly/oi/dto/RequestCardDto.java | 16 + .../java/jp/moreslowly/oi/dto/RoomDto.java | 179 +++++++++ .../oi/exception/FullMemberException.java | 11 + .../oi/exception/InternalErrorException.java | 11 + .../oi/exception/NoRoomException.java | 11 + .../UnprocessableContentException.java | 14 + .../java/jp/moreslowly/oi/models/Card.java | 87 ++++ .../java/jp/moreslowly/oi/models/Member.java | 16 + .../jp/moreslowly/oi/models/Nickname.java | 25 ++ .../oi/repository/RoomRepository.java | 13 + .../jp/moreslowly/oi/service/CardService.java | 9 + .../oi/service/CardServiceImpl.java | 22 ++ .../jp/moreslowly/oi/service/RoomService.java | 25 ++ .../oi/service/RoomServiceImpl.java | 251 ++++++++++++ .../jp/moreslowly/oi/tasks/DealerManager.java | 75 ++++ .../jp/moreslowly/oi/tasks/DealerTask.java | 244 ++++++++++++ .../jp/moreslowly/oi/tasks/SweeperTask.java | 28 ++ src/main/resources/application.properties | 4 + src/main/resources/conf/logback-access.xml | 12 + 59 files changed, 2831 insertions(+), 131 deletions(-) create mode 100644 docs/name.md create mode 100644 docs/spec.md create mode 100644 front/public/1F3B4.svg create mode 100644 front/src/axios.ts create mode 100644 front/src/components/BetButton.vue create mode 100644 front/src/components/BetDialog.vue create mode 100644 front/src/components/CardComponent.vue delete mode 100644 front/src/components/HelloWorld.vue create mode 100644 front/src/components/PieComponent.vue create mode 100644 front/src/computedStyles/namedColorSet.ts create mode 100644 front/src/router.ts create mode 100644 front/src/services/RoomService.ts create mode 100644 front/src/types/Bet.ts create mode 100644 front/src/types/Card.ts create mode 100644 front/src/types/Room.ts create mode 100644 front/src/types/Status.ts create mode 100644 front/src/views/HomeView.vue create mode 100644 front/src/views/RoomView.vue create mode 100644 src/main/java/jp/moreslowly/oi/common/RoomLimitation.java create mode 100644 src/main/java/jp/moreslowly/oi/common/SessionKey.java create mode 100644 src/main/java/jp/moreslowly/oi/config/RedisConfig.java create mode 100644 src/main/java/jp/moreslowly/oi/config/RestResponseEntityExceptionHandler.java create mode 100644 src/main/java/jp/moreslowly/oi/config/SchedulingConfig.java create mode 100644 src/main/java/jp/moreslowly/oi/config/ServletCustomizer.java create mode 100644 src/main/java/jp/moreslowly/oi/config/TomcatConfig.java create mode 100644 src/main/java/jp/moreslowly/oi/config/WebMvcConfig.java create mode 100644 src/main/java/jp/moreslowly/oi/controllers/RoomController.java create mode 100644 src/main/java/jp/moreslowly/oi/controllers/UsersController.java create mode 100644 src/main/java/jp/moreslowly/oi/dao/Bet.java create mode 100644 src/main/java/jp/moreslowly/oi/dao/Room.java create mode 100644 src/main/java/jp/moreslowly/oi/dto/BetDto.java create mode 100644 src/main/java/jp/moreslowly/oi/dto/IdDto.java create mode 100644 src/main/java/jp/moreslowly/oi/dto/RequestCardDto.java create mode 100644 src/main/java/jp/moreslowly/oi/dto/RoomDto.java create mode 100644 src/main/java/jp/moreslowly/oi/exception/FullMemberException.java create mode 100644 src/main/java/jp/moreslowly/oi/exception/InternalErrorException.java create mode 100644 src/main/java/jp/moreslowly/oi/exception/NoRoomException.java create mode 100644 src/main/java/jp/moreslowly/oi/exception/UnprocessableContentException.java create mode 100644 src/main/java/jp/moreslowly/oi/models/Card.java create mode 100644 src/main/java/jp/moreslowly/oi/models/Member.java create mode 100644 src/main/java/jp/moreslowly/oi/models/Nickname.java create mode 100644 src/main/java/jp/moreslowly/oi/repository/RoomRepository.java create mode 100644 src/main/java/jp/moreslowly/oi/service/CardService.java create mode 100644 src/main/java/jp/moreslowly/oi/service/CardServiceImpl.java create mode 100644 src/main/java/jp/moreslowly/oi/service/RoomService.java create mode 100644 src/main/java/jp/moreslowly/oi/service/RoomServiceImpl.java create mode 100644 src/main/java/jp/moreslowly/oi/tasks/DealerManager.java create mode 100644 src/main/java/jp/moreslowly/oi/tasks/DealerTask.java create mode 100644 src/main/java/jp/moreslowly/oi/tasks/SweeperTask.java create mode 100644 src/main/resources/conf/logback-access.xml diff --git a/build.gradle b/build.gradle index 6faeba1..2b0f2c5 100644 --- a/build.gradle +++ b/build.gradle @@ -28,6 +28,8 @@ dependencies { annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' + + implementation group: 'ch.qos.logback', name: 'logback-access', version: '1.4.6' } tasks.named('test') { diff --git a/docs/name.md b/docs/name.md new file mode 100644 index 0000000..21a93ef --- /dev/null +++ b/docs/name.md @@ -0,0 +1,14 @@ +# names + +サクラ +ウメ +カエデ +シラカバ +アジサイ +ツツジ +モミジ +ツバキ +マツ +モクレン +タケ +ヒイラギ diff --git a/docs/spec.md b/docs/spec.md new file mode 100644 index 0000000..eec8356 --- /dev/null +++ b/docs/spec.md @@ -0,0 +1,21 @@ +# spec + +入口 `https://oi.moreslowly.jp/` + +名前を入力 +セッションに保存 +名前の表示 +名前の変更 + +`/?room=UUID` + +- 開始 start +- シャッフル shuffle +- カードを配る hand out cards +- 掛け金設定 wait to bet +- 全員OK +- 3枚目のカードの要否 wait to request additional card +- 全員OK +- 親が自分のカードをめくる parent turns over cards +- 親が三枚目のカードを取るか決める parent hand is determined +- 清算 liquidation diff --git a/front/index.html b/front/index.html index 143557b..ce407e7 100644 --- a/front/index.html +++ b/front/index.html @@ -2,9 +2,9 @@ - + - Vite + Vue + TS + Oi
diff --git a/front/package-lock.json b/front/package-lock.json index c67c8e6..9a7f78d 100644 --- a/front/package-lock.json +++ b/front/package-lock.json @@ -163,6 +163,18 @@ "dev": true, "optional": true }, + "@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", + "dev": true + }, + "@types/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==", + "dev": true + }, "@vitejs/plugin-vue": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.1.0.tgz", @@ -269,6 +281,11 @@ "@vue/shared": "3.2.47" } }, + "@vue/devtools-api": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.5.0.tgz", + "integrity": "sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q==" + }, "@vue/reactivity": { "version": "3.2.47", "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.2.47.tgz", @@ -322,6 +339,21 @@ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.47.tgz", "integrity": "sha512-BHGyyGN3Q97EZx0taMQ+OLNuZcW3d37ZEVmEAyeoA9ERdGvm9Irc/0Fua8SNyOtV1w6BS4q25wbMzJujO9HIfQ==" }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "axios": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.3.4.tgz", + "integrity": "sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==", + "requires": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -337,6 +369,23 @@ "balanced-match": "^1.0.0" } }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, "csstype": { "version": "2.6.21", "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz", @@ -348,6 +397,11 @@ "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", "dev": true }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, "esbuild": { "version": "0.17.12", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.12.tgz", @@ -383,6 +437,21 @@ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" }, + "follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" + }, + "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==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, "fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -393,18 +462,31 @@ "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "get-intrinsic": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", + "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + } }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, "requires": { "function-bind": "^1.1.1" } }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + }, "he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -428,6 +510,19 @@ "sourcemap-codec": "^1.4.8" } }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "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==", + "requires": { + "mime-db": "1.52.0" + } + }, "minimatch": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-6.2.0.tgz", @@ -448,6 +543,11 @@ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==" }, + "object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==" + }, "path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -469,6 +569,19 @@ "source-map-js": "^1.0.2" } }, + "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==" + }, + "qs": { + "version": "6.11.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.1.tgz", + "integrity": "sha512-0wsrzgTz/kAVIeuxSjnpGC56rzYtr6JT/2BwEvMaPhFIoYa1aGO8LbzuU1R0uUYQkLpWBTOj0l/CLAJB64J6nQ==", + "requires": { + "side-channel": "^1.0.4" + } + }, "resolve": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", @@ -489,6 +602,16 @@ "fsevents": "~2.3.2" } }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -516,6 +639,11 @@ "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "dev": true }, + "uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" + }, "vite": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/vite/-/vite-4.2.0.tgz", @@ -541,6 +669,14 @@ "@vue/shared": "3.2.47" } }, + "vue-router": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.1.6.tgz", + "integrity": "sha512-DYWYwsG6xNPmLq/FmZn8Ip+qrhFEzA14EI12MsMgVxvHFDYvlr4NXpVF5hrRH1wVcDP8fGi5F4rxuJSl8/r+EQ==", + "requires": { + "@vue/devtools-api": "^6.4.5" + } + }, "vue-template-compiler": { "version": "2.7.14", "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.14.tgz", diff --git a/front/package.json b/front/package.json index a04df5a..2248f0a 100644 --- a/front/package.json +++ b/front/package.json @@ -4,14 +4,20 @@ "version": "0.0.0", "type": "module", "scripts": { - "dev": "vite", + "dev": "vite --debug --host", "build": "vue-tsc && vite build", "preview": "vite preview" }, "dependencies": { - "vue": "^3.2.47" + "axios": "^1.3.4", + "qs": "^6.11.1", + "uuid": "^9.0.0", + "vue": "^3.2.47", + "vue-router": "^4.1.6" }, "devDependencies": { + "@types/qs": "^6.9.7", + "@types/uuid": "^9.0.1", "@vitejs/plugin-vue": "^4.1.0", "typescript": "^4.9.3", "vite": "^4.2.0", diff --git a/front/public/1F3B4.svg b/front/public/1F3B4.svg new file mode 100644 index 0000000..35d60eb --- /dev/null +++ b/front/public/1F3B4.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/front/src/App.vue b/front/src/App.vue index bb666a8..6e994e4 100644 --- a/front/src/App.vue +++ b/front/src/App.vue @@ -1,30 +1,7 @@ - - diff --git a/front/src/axios.ts b/front/src/axios.ts new file mode 100644 index 0000000..3f0003d --- /dev/null +++ b/front/src/axios.ts @@ -0,0 +1,13 @@ +import axios from 'axios' +import qs from 'qs' + +const instance = axios.create({ + baseURL: '/api', + paramsSerializer: { + serialize: (params) => { + return qs.stringify(params, { arrayFormat: 'repeat' }) + }, + }, +}) + +export default instance diff --git a/front/src/components/BetButton.vue b/front/src/components/BetButton.vue new file mode 100644 index 0000000..1b78b5b --- /dev/null +++ b/front/src/components/BetButton.vue @@ -0,0 +1,42 @@ + + + + + \ No newline at end of file diff --git a/front/src/components/BetDialog.vue b/front/src/components/BetDialog.vue new file mode 100644 index 0000000..87cbdaa --- /dev/null +++ b/front/src/components/BetDialog.vue @@ -0,0 +1,159 @@ + + + + + diff --git a/front/src/components/CardComponent.vue b/front/src/components/CardComponent.vue new file mode 100644 index 0000000..5c6180f --- /dev/null +++ b/front/src/components/CardComponent.vue @@ -0,0 +1,78 @@ + + + + + diff --git a/front/src/components/HelloWorld.vue b/front/src/components/HelloWorld.vue deleted file mode 100644 index 7b25f3f..0000000 --- a/front/src/components/HelloWorld.vue +++ /dev/null @@ -1,38 +0,0 @@ - - - - - diff --git a/front/src/components/PieComponent.vue b/front/src/components/PieComponent.vue new file mode 100644 index 0000000..51a8f47 --- /dev/null +++ b/front/src/components/PieComponent.vue @@ -0,0 +1,66 @@ + + + + + diff --git a/front/src/computedStyles/namedColorSet.ts b/front/src/computedStyles/namedColorSet.ts new file mode 100644 index 0000000..c2277f6 --- /dev/null +++ b/front/src/computedStyles/namedColorSet.ts @@ -0,0 +1,18 @@ +const nameToColorMap = new Map([ + ["サクラ", 'material-red'], + ["ウメ", 'material-pink'], + ["カエデ", 'material-purple'], + ["シラカバ", 'material-deep-purple'], + ["アジサイ", 'material-indigo'], + ["ツツジ", 'material-blue'], + ["モミジ", 'material-light-blue'], + ["ツバキ", 'material-cyan'], + ["マツ", 'material-teal'], + ["モクレン", 'material-green'], + ["タケ", 'material-light-green'], + ["ヒイラギ", 'material-lime'], +]) as ReadonlyMap + +export const namedColorSet = (name: string): string => { + return nameToColorMap.get(name) || 'material-grey' +} diff --git a/front/src/main.ts b/front/src/main.ts index 2425c0f..3273150 100644 --- a/front/src/main.ts +++ b/front/src/main.ts @@ -1,5 +1,8 @@ import { createApp } from 'vue' import './style.css' +import router from './router' import App from './App.vue' -createApp(App).mount('#app') +const app = createApp(App) +app.use(router) +app.mount('#app') diff --git a/front/src/router.ts b/front/src/router.ts new file mode 100644 index 0000000..edc5341 --- /dev/null +++ b/front/src/router.ts @@ -0,0 +1,27 @@ +import qs from 'qs' +import { createRouter, createWebHistory } from 'vue-router' +import HomeView from './views/HomeView.vue' +import RoomView from './views/RoomView.vue' + +const router = createRouter({ + history: createWebHistory(import.meta.env.BASE_URL), + routes: [ + { + path: '/', + name: 'home', + component: HomeView + }, + { + path: '/room', + name: 'room', + component: RoomView + }, + { + path: '/*', + redirect: '/' + } + ], + stringifyQuery: (query) => { return qs.stringify(query, { arrayFormat: 'repeat' }) } +}) + +export default router diff --git a/front/src/services/RoomService.ts b/front/src/services/RoomService.ts new file mode 100644 index 0000000..3901dd0 --- /dev/null +++ b/front/src/services/RoomService.ts @@ -0,0 +1,59 @@ +import { AxiosError } from 'axios' +import axios from '../axios' + +import { Room } from '../types/Room' + +export default class RoomService { + static async enterRoom(roomId: string): Promise { + const { data } = await axios.post(`/room/`, { id: roomId }) + if (data.status == 503) { + console.log('Room is full') + } + return data + } + + static isSubscribed = false + + static stopSubscribe() { + this.isSubscribed = false + } + + static async subscribeToRoom(roomId: string, callback: (room: Room) => void): Promise { + if (this.isSubscribed) { + return Promise.reject('Already subscribed') + } + this.isSubscribed = true + while (this.isSubscribed) { + try { + const response = await axios.post(`/room/subscribe`, { id: roomId }) + if (!this.isSubscribed) { + return Promise.reject('Unsubscribed') + } + if (response.status == 200) { + let room = response.data + callback(room) + } else { + console.log("not 200: ", response.status, response.data.message) + await new Promise((resolve) => setTimeout(resolve, 1000)) + } + } catch (error) { + if (error instanceof AxiosError) { + console.log("Error: ", error.response?.data.message) + } + await new Promise((resolve) => setTimeout(resolve, 1000)) + } + await new Promise((resolve) => setTimeout(resolve, 500)) + } + return Promise.reject('Unsubscribed') + } + + static async bet(roomId: string, userName: string, handIndex: number, betAmount: number): Promise { + const { data } = await axios.post(`/room/bet`, { roomId, userName, handIndex, betAmount }) + return data + } + + static async requestCard(roomId: string, userName: string, handIndex: number): Promise { + const { data } = await axios.post(`/room/requestCard`, { roomId, userName, handIndex }) + return data + } +} diff --git a/front/src/style.css b/front/src/style.css index 7294765..fa09fc5 100644 --- a/front/src/style.css +++ b/front/src/style.css @@ -1,80 +1,69 @@ -:root { - font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; +body { + font-family: Arial, Helvetica, sans-serif; +} - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; +/* Material Colors with Contrasting Text Colors */ +.material-grey { + color: #ffffff; + background-color: #9e9e9e; +} - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - -webkit-text-size-adjust: 100%; +.material-red { + color: #ffffff; + background-color: #f44336; } -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; +.material-pink { + color: #ffffff; + background-color: #e91e63; } -a:hover { - color: #535bf2; + +.material-purple { + color: #ffffff; + background-color: #9c27b0; } -body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; +.material-deep-purple { + color: #ffffff; + background-color: #673ab7; } -h1 { - font-size: 3.2em; - line-height: 1.1; +.material-indigo { + color: #ffffff; + background-color: #3f51b5; } -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; +.material-blue { + color: #ffffff; + background-color: #2196f3; } -button:hover { - border-color: #646cff; + +.material-light-blue { + color: #000000; + background-color: #03a9f4; +} + +.material-cyan { + color: #000000; + background-color: #00bcd4; } -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; + +.material-teal { + color: #ffffff; + background-color: #009688; } -.card { - padding: 2em; +.material-green { + color: #ffffff; + background-color: #4caf50; } -#app { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; +.material-light-green { + color: #000000; + background-color: #8bc34a; } -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } +.material-lime { + color: #000000; + background-color: #cddc39; } diff --git a/front/src/types/Bet.ts b/front/src/types/Bet.ts new file mode 100644 index 0000000..7e4a9c9 --- /dev/null +++ b/front/src/types/Bet.ts @@ -0,0 +1,7 @@ +export interface Bet { + roomId: string + userName: string + handIndex: number + betAmount: number + result: string +} diff --git a/front/src/types/Card.ts b/front/src/types/Card.ts new file mode 100644 index 0000000..dc5e83a --- /dev/null +++ b/front/src/types/Card.ts @@ -0,0 +1,54 @@ +export const Suit = { + SPADE: 'SPADE', + HEART: 'HEART', + DIAMOND: 'DIAMOND', + CLUB: 'CLUB', +} as const + +export type SuitType = typeof Suit[keyof typeof Suit]; + +export const PrintableSuit: Map = new Map([ + [Suit.SPADE, '♠'], + [Suit.HEART, '♥'], + [Suit.DIAMOND, '♦'], + [Suit.CLUB, '♣'], +]) + +export const Rank = { + ACE: 'ACE', + TWO: 'TWO', + THREE: 'THREE', + FOUR: 'FOUR', + FIVE: 'FIVE', + SIX: 'SIX', + SEVEN: 'SEVEN', + EIGHT: 'EIGHT', + NINE: 'NINE', + TEN: 'TEN', + JACK: 'JACK', + QUEEN: 'QUEEN', + KING: 'KING', +} as const + +export type RankType = typeof Rank[keyof typeof Rank]; + +export const PrintableRank: Map = new Map([ + [Rank.ACE, 'A'], + [Rank.TWO, '2'], + [Rank.THREE, '3'], + [Rank.FOUR, '4'], + [Rank.FIVE, '5'], + [Rank.SIX, '6'], + [Rank.SEVEN, '7'], + [Rank.EIGHT, '8'], + [Rank.NINE, '9'], + [Rank.TEN, '10'], + [Rank.JACK, 'J'], + [Rank.QUEEN, 'Q'], + [Rank.KING, 'K'], +]) + +export interface Card { + suit: SuitType + rank: RankType +} diff --git a/front/src/types/Room.ts b/front/src/types/Room.ts new file mode 100644 index 0000000..bdffafa --- /dev/null +++ b/front/src/types/Room.ts @@ -0,0 +1,21 @@ +import { Bet } from './Bet' +import { Card } from './Card' +import { StatusType } from './Status' + +export interface Room { + id: string + yourName: string + members: string[] + wallets: any + hands1: Card[] + hands2: Card[] + hands3: Card[] + hands4: Card[] + hands5: Card[] + hands6: Card[] + hands7: Card[] + bets: Bet[] + status: StatusType + timeLeft: number + timeLeftDenominator: number +} diff --git a/front/src/types/Status.ts b/front/src/types/Status.ts new file mode 100644 index 0000000..cbcf8ec --- /dev/null +++ b/front/src/types/Status.ts @@ -0,0 +1,12 @@ +export const Status = { + START: 'START', + SHUFFLE: 'SHUFFLE', + HAND_OUT_CARDS: 'HAND_OUT_CARDS', + WAIT_TO_BET: 'WAIT_TO_BET', + WAIT_TO_REQUEST: 'WAIT_TO_REQUEST', + DEALER_TURN: 'DEALER_TURN', + LIQUIDATION: 'LIQUIDATION', + END: 'END', +} as const + +export type StatusType = typeof Status[keyof typeof Status]; diff --git a/front/src/views/HomeView.vue b/front/src/views/HomeView.vue new file mode 100644 index 0000000..358f08c --- /dev/null +++ b/front/src/views/HomeView.vue @@ -0,0 +1,78 @@ + + + + + diff --git a/front/src/views/RoomView.vue b/front/src/views/RoomView.vue new file mode 100644 index 0000000..6e36793 --- /dev/null +++ b/front/src/views/RoomView.vue @@ -0,0 +1,370 @@ + + + + + diff --git a/front/tsconfig.node.json b/front/tsconfig.node.json index 9d31e2a..a6dfb11 100644 --- a/front/tsconfig.node.json +++ b/front/tsconfig.node.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "strict": true, "composite": true, "module": "ESNext", "moduleResolution": "Node", diff --git a/src/main/java/jp/moreslowly/oi/common/RoomLimitation.java b/src/main/java/jp/moreslowly/oi/common/RoomLimitation.java new file mode 100644 index 0000000..827b8ba --- /dev/null +++ b/src/main/java/jp/moreslowly/oi/common/RoomLimitation.java @@ -0,0 +1,10 @@ +package jp.moreslowly.oi.common; + +public final class RoomLimitation { + public static final int MAX_ROOM_SIZE = 10; + public static final int MAX_MEMBER_SIZE = 6; + public static final int MAX_HAND_OUT_SIZE = 7; + + private RoomLimitation() { + } +} diff --git a/src/main/java/jp/moreslowly/oi/common/SessionKey.java b/src/main/java/jp/moreslowly/oi/common/SessionKey.java new file mode 100644 index 0000000..3cb3dd9 --- /dev/null +++ b/src/main/java/jp/moreslowly/oi/common/SessionKey.java @@ -0,0 +1,10 @@ +package jp.moreslowly.oi.common; + +public final class SessionKey { + public static final String USER_ID = "userId"; + public static final String NICKNAME = "nickname"; + public static final String ROOM_ID = "roomId"; + + private SessionKey() { + } +} diff --git a/src/main/java/jp/moreslowly/oi/config/RedisConfig.java b/src/main/java/jp/moreslowly/oi/config/RedisConfig.java new file mode 100644 index 0000000..74e4550 --- /dev/null +++ b/src/main/java/jp/moreslowly/oi/config/RedisConfig.java @@ -0,0 +1,28 @@ +package jp.moreslowly.oi.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; +import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession; + +@Configuration +@EnableRedisRepositories +@EnableRedisHttpSession +public class RedisConfig { + @Bean + RedisConnectionFactory connectionFactory() { + return new LettuceConnectionFactory(); + } + + @Bean + RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { + + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(connectionFactory); + + return template; + } +} diff --git a/src/main/java/jp/moreslowly/oi/config/RestResponseEntityExceptionHandler.java b/src/main/java/jp/moreslowly/oi/config/RestResponseEntityExceptionHandler.java new file mode 100644 index 0000000..f2f459d --- /dev/null +++ b/src/main/java/jp/moreslowly/oi/config/RestResponseEntityExceptionHandler.java @@ -0,0 +1,14 @@ +package jp.moreslowly.oi.config; + +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +@ControllerAdvice +public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler { + @ExceptionHandler(value = { IllegalArgumentException.class }) + protected org.springframework.http.ResponseEntity handleIllegalArgumentException( + RuntimeException ex) { + return org.springframework.http.ResponseEntity.badRequest().body(ex.getMessage()); + } +} diff --git a/src/main/java/jp/moreslowly/oi/config/SchedulingConfig.java b/src/main/java/jp/moreslowly/oi/config/SchedulingConfig.java new file mode 100644 index 0000000..c660aba --- /dev/null +++ b/src/main/java/jp/moreslowly/oi/config/SchedulingConfig.java @@ -0,0 +1,23 @@ +package jp.moreslowly.oi.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; + +import jp.moreslowly.oi.tasks.DealerManager; +import jp.moreslowly.oi.tasks.SweeperTask; + +@Configuration +@EnableScheduling +public class SchedulingConfig { + + @Bean + public DealerManager dealerManager() { + return new DealerManager(); + } + + @Bean + public SweeperTask sweeperTask() { + return new SweeperTask(); + } +} diff --git a/src/main/java/jp/moreslowly/oi/config/ServletCustomizer.java b/src/main/java/jp/moreslowly/oi/config/ServletCustomizer.java new file mode 100644 index 0000000..357c2b4 --- /dev/null +++ b/src/main/java/jp/moreslowly/oi/config/ServletCustomizer.java @@ -0,0 +1,17 @@ +package jp.moreslowly.oi.config; + +import org.springframework.boot.web.server.ErrorPage; +import org.springframework.boot.web.server.WebServerFactoryCustomizer; +import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; + +@Component +public class ServletCustomizer implements WebServerFactoryCustomizer { + @Override + public void customize(ConfigurableServletWebServerFactory factory) { + // vue-routerをHistoryモードで動かすと、/以外が404となってしまうため、index.htmlに飛ばすように修正。 + ErrorPage error404Page = new ErrorPage(HttpStatus.NOT_FOUND, "/index.html"); + factory.addErrorPages(error404Page); + } +} diff --git a/src/main/java/jp/moreslowly/oi/config/TomcatConfig.java b/src/main/java/jp/moreslowly/oi/config/TomcatConfig.java new file mode 100644 index 0000000..ff9fb3c --- /dev/null +++ b/src/main/java/jp/moreslowly/oi/config/TomcatConfig.java @@ -0,0 +1,43 @@ +package jp.moreslowly.oi.config; + +import java.util.Optional; + +import org.apache.catalina.connector.Connector; +import org.apache.coyote.ajp.AbstractAjpProtocol; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; +import org.springframework.boot.web.server.WebServerFactoryCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import ch.qos.logback.access.tomcat.LogbackValve; + +@Configuration +public class TomcatConfig { + @Value("${ajp.secret}") + private String ajpSecret; + + @Bean + public WebServerFactoryCustomizer servletContainer() { + return server -> { + LogbackValve logbackValve = new LogbackValve(); + logbackValve.setAsyncSupported(true); + Optional.ofNullable(server) + .ifPresent(s -> s.addContextValves(logbackValve)); + Optional.ofNullable(server) + .ifPresent(s -> s.addAdditionalTomcatConnectors(redirectConnector())); + }; + } + + private Connector redirectConnector() { + Connector connector = new Connector("AJP/1.3"); + connector.setScheme("http"); + connector.setPort(8009); + connector.setAllowTrace(false); + + final AbstractAjpProtocol protocol = (AbstractAjpProtocol) connector.getProtocolHandler(); + connector.setSecure(true); + protocol.setSecret(ajpSecret); + return connector; + } +} diff --git a/src/main/java/jp/moreslowly/oi/config/WebMvcConfig.java b/src/main/java/jp/moreslowly/oi/config/WebMvcConfig.java new file mode 100644 index 0000000..88615c9 --- /dev/null +++ b/src/main/java/jp/moreslowly/oi/config/WebMvcConfig.java @@ -0,0 +1,15 @@ +package jp.moreslowly.oi.config; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + @Override + public void addCorsMappings(CorsRegistry registry) { + // viteとの開発用に、3000からのアクセスを許可する。 + registry.addMapping("/api/**") + .allowedMethods("*") + .allowedOrigins("http://localhost:3000", "http://127.0.0.1:5173", "http://192.168.1.175:5173"); + } +} diff --git a/src/main/java/jp/moreslowly/oi/controllers/RoomController.java b/src/main/java/jp/moreslowly/oi/controllers/RoomController.java new file mode 100644 index 0000000..c681789 --- /dev/null +++ b/src/main/java/jp/moreslowly/oi/controllers/RoomController.java @@ -0,0 +1,102 @@ +package jp.moreslowly.oi.controllers; + +import java.util.List; +import java.util.UUID; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.request.async.DeferredResult; + +import jakarta.servlet.http.HttpSession; +import jp.moreslowly.oi.common.SessionKey; +import jp.moreslowly.oi.dto.BetDto; +import jp.moreslowly.oi.dto.IdDto; +import jp.moreslowly.oi.dto.RequestCardDto; +import jp.moreslowly.oi.dto.RoomDto; +import jp.moreslowly.oi.exception.UnprocessableContentException; +import jp.moreslowly.oi.service.RoomService; + +@RestController +@RequestMapping("/api/room") +public class RoomController { + + @Autowired + private RoomService roomService; + + @Autowired + private HttpSession session; + + @GetMapping("/") + public List getRoomIdList() { + return roomService.getRoomIdList(); + } + + @PostMapping("/") + public RoomDto enterRoom(@RequestBody IdDto dto) { + // UUID validation + UUID.fromString(dto.getId()); + + return roomService.enterRoom(session, dto.getId()); + } + + @PostMapping("/subscribe") + public DeferredResult subscribe(@RequestBody IdDto dto) { + // UUID validation + UUID.fromString(dto.getId()); + + DeferredResult deferredResult = new DeferredResult<>(); + roomService.subscribe(dto.getId(), session, deferredResult); + + return deferredResult; + } + + @PostMapping("/reset") + public void reset(@RequestBody IdDto dto) { + // UUID validation + UUID.fromString(dto.getId()); + + roomService.reset(dto.getId()); + } + + @PostMapping("/bet") + public void bet(@RequestBody BetDto dto) { + // UUID validation + UUID roomIdUUID = UUID.fromString(dto.getRoomId()); + + String sessionRoomId = (String) session.getAttribute(SessionKey.ROOM_ID); + UUID sessionRoomIdUUID = UUID.fromString(sessionRoomId); + if (!roomIdUUID.equals(sessionRoomIdUUID)) { + throw new UnprocessableContentException("Invalid room id"); + } + + String sessionNickname = (String) session.getAttribute(SessionKey.NICKNAME); + if (!dto.getUserName().equals(sessionNickname)) { + throw new UnprocessableContentException("Invalid nickname on bet validation"); + } + + roomService.bet(session, dto); + } + + @PostMapping("/requestCard") + public void requestOneMore(@RequestBody RequestCardDto dto) { + // UUID validation + UUID roomIdUUID = UUID.fromString(dto.getRoomId()); + + String sessionRoomId = (String) session.getAttribute(SessionKey.ROOM_ID); + UUID sessionRoomIdUUID = UUID.fromString(sessionRoomId); + if (!roomIdUUID.equals(sessionRoomIdUUID)) { + throw new UnprocessableContentException("Invalid room id"); + } + + String sessionNickname = (String) session.getAttribute(SessionKey.NICKNAME); + if (!dto.getUserName().equals(sessionNickname)) { + throw new UnprocessableContentException("Invalid nickname on request card validation"); + } + + roomService.requestCard(session, dto); + } +} diff --git a/src/main/java/jp/moreslowly/oi/controllers/UsersController.java b/src/main/java/jp/moreslowly/oi/controllers/UsersController.java new file mode 100644 index 0000000..02b7f42 --- /dev/null +++ b/src/main/java/jp/moreslowly/oi/controllers/UsersController.java @@ -0,0 +1,14 @@ +package jp.moreslowly.oi.controllers; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/users") +public class UsersController { + @GetMapping("/count") + public String count() { + return "たくさん"; + } +} diff --git a/src/main/java/jp/moreslowly/oi/dao/Bet.java b/src/main/java/jp/moreslowly/oi/dao/Bet.java new file mode 100644 index 0000000..4258f87 --- /dev/null +++ b/src/main/java/jp/moreslowly/oi/dao/Bet.java @@ -0,0 +1,35 @@ +package jp.moreslowly.oi.dao; + +import org.springframework.data.annotation.Id; +import org.springframework.data.redis.core.RedisHash; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@RedisHash("bet") +@Data +@SuperBuilder +@AllArgsConstructor +@NoArgsConstructor +public class Bet { + public enum Result { + WIN("WIN"), + LOSE("LOSE"), + DRAW("DRAW"); + private String value; + private Result(String value) { + this.value = value; + } + public String getValue() { + return value; + } + } + @Id private String id; + private String roomId; + private String userName; + private Integer handIndex; + private Integer betAmount; + private Result result; +} diff --git a/src/main/java/jp/moreslowly/oi/dao/Room.java b/src/main/java/jp/moreslowly/oi/dao/Room.java new file mode 100644 index 0000000..84f48df --- /dev/null +++ b/src/main/java/jp/moreslowly/oi/dao/Room.java @@ -0,0 +1,156 @@ +package jp.moreslowly.oi.dao; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import org.springframework.data.annotation.Id; +import org.springframework.data.redis.core.RedisHash; + +import jp.moreslowly.oi.models.Card; +import jp.moreslowly.oi.models.Member; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.experimental.SuperBuilder; + +@RedisHash("room") +@Data +@SuperBuilder +@AllArgsConstructor +public class Room { + public enum Status { + START, + SHUFFLE, + HAND_OUT_CARDS, + WAIT_TO_BET, + WAIT_TO_REQUEST, + DEALER_TURN, + LIQUIDATION, + END; + private static final Status[] values = values(); + public Status prev() { + return values[(this.ordinal() - 1 + values.length) % values.length]; + } + public Status next() { + return values[(this.ordinal() + 1) % values.length]; + } + } + + @Id private String id; + @Builder.Default private List members = new ArrayList<>(); + @Builder.Default private Map wallets = new HashMap<>(); + @Builder.Default private List deck = new ArrayList<>(); + @Builder.Default private List hands1 = new ArrayList<>(); + @Builder.Default private List hands2 = new ArrayList<>(); + @Builder.Default private List hands3 = new ArrayList<>(); + @Builder.Default private List hands4 = new ArrayList<>(); + @Builder.Default private List hands5 = new ArrayList<>(); + @Builder.Default private List hands6 = new ArrayList<>(); + @Builder.Default private List hands7 = new ArrayList<>(); + @Builder.Default private List bets = new ArrayList<>(); + @Builder.Default private Status status = Status.START; + @Builder.Default private Long timeLeft = 0L; + @Builder.Default private Long timeLeftDenominator = 1L; + private LocalDateTime lastAccessedAt; + private LocalDateTime updatedAt; + + public Room() { + this.status = Status.START; + } + + public void reset() { + this.deck = new ArrayList<>(); + if (Objects.isNull(this.members)) { + this.members = new ArrayList<>(); + } + this.hands1 = new ArrayList<>(); + this.hands2 = new ArrayList<>(); + this.hands3 = new ArrayList<>(); + this.hands4 = new ArrayList<>(); + this.hands5 = new ArrayList<>(); + this.hands6 = new ArrayList<>(); + this.hands7 = new ArrayList<>(); + this.bets = new ArrayList<>(); + this.status = Status.START; + this.timeLeft = 0L; + this.timeLeftDenominator = 1L; + this.updatedAt = LocalDateTime.now(); + } + + public List getHandsAt(int index) { + switch (index) { + case 1: + if (Objects.isNull(this.hands1)) { + this.hands1 = new ArrayList<>(); + } + return getHands1(); + case 2: + if (Objects.isNull(this.hands2)) { + this.hands2 = new ArrayList<>(); + } + return getHands2(); + case 3: + if (Objects.isNull(this.hands3)) { + this.hands3 = new ArrayList<>(); + } + return getHands3(); + case 4: + if (Objects.isNull(this.hands4)) { + this.hands4 = new ArrayList<>(); + } + return getHands4(); + case 5: + if (Objects.isNull(this.hands5)) { + this.hands5 = new ArrayList<>(); + } + return getHands5(); + case 6: + if (Objects.isNull(this.hands6)) { + this.hands6 = new ArrayList<>(); + } + return getHands6(); + case 7: + if (Objects.isNull(this.hands7)) { + this.hands7 = new ArrayList<>(); + } + return getHands7(); + default: + throw new IllegalArgumentException("index must be 1 to 7"); + } + } + + public void setHandsAt(int index, List hands) { + if (Objects.isNull(hands)) { + throw new IllegalArgumentException("hands must not be null"); + } + switch (index) { + case 1: + setHands1(hands); + break; + case 2: + setHands2(hands); + break; + case 3: + setHands3(hands); + break; + case 4: + setHands4(hands); + break; + case 5: + setHands5(hands); + break; + case 6: + setHands6(hands); + break; + case 7: + setHands7(hands); + break; + default: + throw new IllegalArgumentException("index must be 1 to 7"); + } + } +} diff --git a/src/main/java/jp/moreslowly/oi/dto/BetDto.java b/src/main/java/jp/moreslowly/oi/dto/BetDto.java new file mode 100644 index 0000000..47c4cf0 --- /dev/null +++ b/src/main/java/jp/moreslowly/oi/dto/BetDto.java @@ -0,0 +1,39 @@ +package jp.moreslowly.oi.dto; + +import jp.moreslowly.oi.dao.Bet; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@AllArgsConstructor +@NoArgsConstructor +public class BetDto { + private String roomId; + private String userName; + private Integer handIndex; + private Integer betAmount; + private Bet.Result result; + + public static BetDto fromEntity(Bet bet) { + return BetDto.builder() + .roomId(bet.getRoomId()) + .userName(bet.getUserName()) + .handIndex(bet.getHandIndex()) + .betAmount(bet.getBetAmount()) + .result(bet.getResult()) + .build(); + } + + public Bet toEntity() { + return Bet.builder() + .roomId(this.roomId) + .userName(this.userName) + .handIndex(this.handIndex) + .betAmount(this.betAmount) + .result(this.result) + .build(); + } +} diff --git a/src/main/java/jp/moreslowly/oi/dto/IdDto.java b/src/main/java/jp/moreslowly/oi/dto/IdDto.java new file mode 100644 index 0000000..026d61e --- /dev/null +++ b/src/main/java/jp/moreslowly/oi/dto/IdDto.java @@ -0,0 +1,14 @@ +package jp.moreslowly.oi.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@AllArgsConstructor +@NoArgsConstructor +public class IdDto { + private String id; +} diff --git a/src/main/java/jp/moreslowly/oi/dto/RequestCardDto.java b/src/main/java/jp/moreslowly/oi/dto/RequestCardDto.java new file mode 100644 index 0000000..8e62376 --- /dev/null +++ b/src/main/java/jp/moreslowly/oi/dto/RequestCardDto.java @@ -0,0 +1,16 @@ +package jp.moreslowly.oi.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@AllArgsConstructor +@NoArgsConstructor +public class RequestCardDto { + private String roomId; + private String userName; + private Integer handIndex; +} diff --git a/src/main/java/jp/moreslowly/oi/dto/RoomDto.java b/src/main/java/jp/moreslowly/oi/dto/RoomDto.java new file mode 100644 index 0000000..e2e5a95 --- /dev/null +++ b/src/main/java/jp/moreslowly/oi/dto/RoomDto.java @@ -0,0 +1,179 @@ +package jp.moreslowly.oi.dto; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +import jp.moreslowly.oi.dao.Bet; +import jp.moreslowly.oi.dao.Room; +import jp.moreslowly.oi.dao.Room.Status; +import jp.moreslowly.oi.models.Card; +import jp.moreslowly.oi.models.Member; +import lombok.Data; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +public class RoomDto { + private String id; + private String yourName; + private List members; + private Map wallets; + private List hands1; + private List hands2; + private List hands3; + private List hands4; + private List hands5; + private List hands6; + private List hands7; + private List bets; + private Long timeLeft; + private Long timeLeftDenominator; + private Status status; + + public static RoomDto fromEntity(Room room, String yourName) { + if (Objects.isNull(room.getMembers())) { + room.setMembers(new ArrayList<>()); + } + if (room.getStatus() == Status.START) { + return maskedFromEntity(room, yourName); + } else if (room.getStatus() == Status.SHUFFLE) { + return maskedFromEntity(room, yourName); + } else if (room.getStatus() == Status.HAND_OUT_CARDS) { + return maskedFromEntity(room, yourName); + } else if (room.getStatus() == Status.WAIT_TO_BET) { + return maskedWithoutMeFromEntity(room, yourName); + } else if (room.getStatus() == Status.WAIT_TO_REQUEST) { + return maskedWithoutMeFromEntity(room, yourName); + } else if (room.getStatus() == Status.DEALER_TURN) { + return maskedWithoutMeFromEntity(room, yourName); + } else if (room.getStatus() == Status.LIQUIDATION) { + return fullOpenFromEntity(room, yourName); + } else if (room.getStatus() == Status.END) { + return fullOpenFromEntity(room, yourName); + } else { + return fullOpenFromEntity(room, yourName); + } + } + + private static RoomDto fullOpenFromEntity(Room room, String yourName) { + return RoomDto.builder() + .id(room.getId()) + .yourName(yourName) + .members(room.getMembers().stream().map(Member::getNickname).collect(Collectors.toList())) + .wallets(room.getWallets()) + .hands1(room.getHands1()) + .hands2(room.getHands2()) + .hands3(room.getHands3()) + .hands4(room.getHands4()) + .hands5(room.getHands5()) + .hands6(room.getHands6()) + .hands7(room.getHands7()) + .bets(Objects.isNull(room.getBets()) + ? new ArrayList<>() + : room.getBets().stream().map(BetDto::fromEntity).collect(Collectors.toList())) + .status(room.getStatus()) + .timeLeft(room.getTimeLeft()) + .timeLeftDenominator(room.getTimeLeftDenominator()) + .build(); + } + + private static RoomDto maskedWithoutMeFromEntity(Room room, String yourName) { + List bets = room.getBets(); + if (Objects.nonNull(bets)) { + bets = bets.stream().filter(bet -> bet.getUserName().equals(yourName)).collect(Collectors.toList()); + } else { + bets = new ArrayList<>(); + } + List betIndexes = bets.stream().map(Bet::getHandIndex).collect(Collectors.toList()); + + RoomDto dto = RoomDto.builder() + .id(room.getId()) + .yourName(yourName) + .members(room.getMembers().stream().map(Member::getNickname).collect(Collectors.toList())) + .wallets(room.getWallets()) + .bets(Objects.isNull(room.getBets()) + ? new ArrayList<>() + : room.getBets().stream().map(BetDto::fromEntity).collect(Collectors.toList())) + .status(room.getStatus()) + .timeLeft(room.getTimeLeft()) + .timeLeftDenominator(room.getTimeLeftDenominator()) + .build(); + + for (int handIndex : Arrays.asList(1, 2, 3, 4, 5, 6, 7)) { + if (betIndexes.contains(handIndex)) { + dto.setHandsAt(handIndex, room.getHandsAt(handIndex)); + } else { + List cards = new ArrayList<>(); + if (room.getHandsAt(handIndex).size() == 3) { + cards.add(room.getHandsAt(handIndex).get(0)); + cards.add(room.getHandsAt(handIndex).get(1)); + } else { + cards.add(room.getHandsAt(handIndex).get(0)); + } + dto.setHandsAt(handIndex, cards); + } + } + + return dto; + } + + private static RoomDto maskedFromEntity(Room room, String yourName) { + RoomDto dto = RoomDto.builder() + .id(room.getId()) + .yourName(yourName) + .members(room.getMembers().stream().map(Member::getNickname).collect(Collectors.toList())) + .wallets(room.getWallets()) + .bets(Objects.isNull(room.getBets()) + ? new ArrayList<>() + : room.getBets().stream().map(BetDto::fromEntity).collect(Collectors.toList())) + .status(room.getStatus()) + .timeLeft(room.getTimeLeft()) + .timeLeftDenominator(room.getTimeLeftDenominator()) + .build(); + + for (int handIndex : Arrays.asList(1, 2, 3, 4, 5, 6, 7)) { + List cards = new ArrayList<>(); + if (room.getHandsAt(handIndex).size() == 3) { + cards.add(room.getHandsAt(handIndex).get(0)); + cards.add(room.getHandsAt(handIndex).get(1)); + } else if (room.getHandsAt(handIndex).size() == 2) { + cards.add(room.getHandsAt(handIndex).get(0)); + } + dto.setHandsAt(handIndex, cards); + } + + return dto; + } + + public void setHandsAt(int handIndex, List cards) { + switch (handIndex) { + case 1: + this.hands1 = cards; + break; + case 2: + this.hands2 = cards; + break; + case 3: + this.hands3 = cards; + break; + case 4: + this.hands4 = cards; + break; + case 5: + this.hands5 = cards; + break; + case 6: + this.hands6 = cards; + break; + case 7: + this.hands7 = cards; + break; + default: + throw new IllegalArgumentException("handIndex must be 1 to 7"); + } + } +} diff --git a/src/main/java/jp/moreslowly/oi/exception/FullMemberException.java b/src/main/java/jp/moreslowly/oi/exception/FullMemberException.java new file mode 100644 index 0000000..b52a3bd --- /dev/null +++ b/src/main/java/jp/moreslowly/oi/exception/FullMemberException.java @@ -0,0 +1,11 @@ +package jp.moreslowly.oi.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.SERVICE_UNAVAILABLE) +public class FullMemberException extends RuntimeException { + public FullMemberException(String message) { + super(message); + } +} diff --git a/src/main/java/jp/moreslowly/oi/exception/InternalErrorException.java b/src/main/java/jp/moreslowly/oi/exception/InternalErrorException.java new file mode 100644 index 0000000..a1f2be2 --- /dev/null +++ b/src/main/java/jp/moreslowly/oi/exception/InternalErrorException.java @@ -0,0 +1,11 @@ +package jp.moreslowly.oi.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) +public class InternalErrorException extends RuntimeException { + public InternalErrorException(String message) { + super(message); + } +} diff --git a/src/main/java/jp/moreslowly/oi/exception/NoRoomException.java b/src/main/java/jp/moreslowly/oi/exception/NoRoomException.java new file mode 100644 index 0000000..fc3c6cd --- /dev/null +++ b/src/main/java/jp/moreslowly/oi/exception/NoRoomException.java @@ -0,0 +1,11 @@ +package jp.moreslowly.oi.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.SERVICE_UNAVAILABLE) +public class NoRoomException extends RuntimeException { + public NoRoomException(String message) { + super(message); + } +} diff --git a/src/main/java/jp/moreslowly/oi/exception/UnprocessableContentException.java b/src/main/java/jp/moreslowly/oi/exception/UnprocessableContentException.java new file mode 100644 index 0000000..f68a0c9 --- /dev/null +++ b/src/main/java/jp/moreslowly/oi/exception/UnprocessableContentException.java @@ -0,0 +1,14 @@ +package jp.moreslowly.oi.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY) +public class UnprocessableContentException extends RuntimeException { + + public static final String ROOM_IS_NOT_FOUND = "Room is not found."; + + public UnprocessableContentException(String message) { + super(message); + } +} diff --git a/src/main/java/jp/moreslowly/oi/models/Card.java b/src/main/java/jp/moreslowly/oi/models/Card.java new file mode 100644 index 0000000..5416515 --- /dev/null +++ b/src/main/java/jp/moreslowly/oi/models/Card.java @@ -0,0 +1,87 @@ +package jp.moreslowly.oi.models; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@AllArgsConstructor +@NoArgsConstructor +public final class Card implements Serializable { + public enum Suit { + SPADE("♠️"), + HEART("♥️"), + DIAMOND("♦️"), + CLUB("♣️"); + private String symbol; + private Suit(String symbol) { + this.symbol = symbol; + } + @Override + public String toString() { + return symbol; + } + } + + public enum Rank { + ACE("A"), + TWO("2"), + THREE("3"), + FOUR("4"), + FIVE("5"), + SIX("6"), + SEVEN("7"), + EIGHT("8"), + NINE("9"), + TEN("10"), + JACK("J"), + QUEEN("Q"), + KING("K"); + private String rankValue; + private Rank(String rank) { + this.rankValue = rank; + } + @Override + public String toString() { + return rankValue; + } + public int getPoint() { + if (this == ACE) { + return 1; + } else if (this == JACK) { + return 11; + } else if (this == QUEEN) { + return 12; + } else if (this == KING) { + return 13; + } else { + return Integer.parseInt(rankValue); + } + } + } + + private Suit suit; + private Rank rank; + + public String toString() { + return suit.toString() + rank.toString(); + } + + public static List generateCardDeck() { + List cardDeck = new ArrayList<>(); + for (Suit suit : Suit.values()) { + for (Rank rank : Rank.values()) { + cardDeck.add(new Card(suit, rank)); + } + } + Collections.shuffle(cardDeck); + return cardDeck; + } +} diff --git a/src/main/java/jp/moreslowly/oi/models/Member.java b/src/main/java/jp/moreslowly/oi/models/Member.java new file mode 100644 index 0000000..212d856 --- /dev/null +++ b/src/main/java/jp/moreslowly/oi/models/Member.java @@ -0,0 +1,16 @@ +package jp.moreslowly.oi.models; +import java.util.UUID; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@AllArgsConstructor +@NoArgsConstructor +public class Member { + private String nickname; + private UUID id; +} diff --git a/src/main/java/jp/moreslowly/oi/models/Nickname.java b/src/main/java/jp/moreslowly/oi/models/Nickname.java new file mode 100644 index 0000000..37ceb33 --- /dev/null +++ b/src/main/java/jp/moreslowly/oi/models/Nickname.java @@ -0,0 +1,25 @@ +package jp.moreslowly.oi.models; + +import java.util.Arrays; +import java.util.List; + +public final class Nickname { + public static final List NICKNAME_LIST = Arrays.asList( + "サクラ", + "ウメ", + "カエデ", + "シラカバ", + "アジサイ", + "ツツジ", + "モミジ", + "ツバキ", + "マツ", + "モクレン", + "タケ", + "ヒイラギ" + ); + public static final int NICKNAME_LIST_SIZE = NICKNAME_LIST.size(); + + private Nickname() { + } +} diff --git a/src/main/java/jp/moreslowly/oi/repository/RoomRepository.java b/src/main/java/jp/moreslowly/oi/repository/RoomRepository.java new file mode 100644 index 0000000..ad4d174 --- /dev/null +++ b/src/main/java/jp/moreslowly/oi/repository/RoomRepository.java @@ -0,0 +1,13 @@ +package jp.moreslowly.oi.repository; + +import java.util.Optional; + +import org.springframework.data.repository.CrudRepository; + +import jp.moreslowly.oi.dao.Room; + +public interface RoomRepository extends CrudRepository { + boolean existsById(String id); + Optional findById(String id); + long count(); +} diff --git a/src/main/java/jp/moreslowly/oi/service/CardService.java b/src/main/java/jp/moreslowly/oi/service/CardService.java new file mode 100644 index 0000000..d66b815 --- /dev/null +++ b/src/main/java/jp/moreslowly/oi/service/CardService.java @@ -0,0 +1,9 @@ +package jp.moreslowly.oi.service; + +import java.util.List; + +import jp.moreslowly.oi.models.Card; + +public interface CardService { + int evaluate(List cards); +} diff --git a/src/main/java/jp/moreslowly/oi/service/CardServiceImpl.java b/src/main/java/jp/moreslowly/oi/service/CardServiceImpl.java new file mode 100644 index 0000000..b18040a --- /dev/null +++ b/src/main/java/jp/moreslowly/oi/service/CardServiceImpl.java @@ -0,0 +1,22 @@ +package jp.moreslowly.oi.service; + +import java.util.List; +import java.util.Optional; + +import org.springframework.stereotype.Service; + +import jp.moreslowly.oi.exception.InternalErrorException; +import jp.moreslowly.oi.models.Card; + +@Service +public class CardServiceImpl implements CardService { + + @Override + public int evaluate(List cards) { + Optional maybeSum = cards.stream().map(card -> card.getRank().getPoint()).reduce((a, b) -> a + b); + if (!maybeSum.isPresent()) { + throw new InternalErrorException("some thing wrong in point calculation"); + } + return maybeSum.get() % 10; + } +} diff --git a/src/main/java/jp/moreslowly/oi/service/RoomService.java b/src/main/java/jp/moreslowly/oi/service/RoomService.java new file mode 100644 index 0000000..d395fd2 --- /dev/null +++ b/src/main/java/jp/moreslowly/oi/service/RoomService.java @@ -0,0 +1,25 @@ +package jp.moreslowly.oi.service; + +import java.util.List; + +import org.springframework.web.context.request.async.DeferredResult; + +import jakarta.servlet.http.HttpSession; +import jp.moreslowly.oi.dto.BetDto; +import jp.moreslowly.oi.dto.RequestCardDto; +import jp.moreslowly.oi.dto.RoomDto; + +public interface RoomService { + + List getRoomIdList(); + + RoomDto enterRoom(HttpSession session, String id); + + void subscribe(String id, HttpSession session, DeferredResult deferredResult); + + void reset(String id); + + void bet(HttpSession session, BetDto betDto); + + void requestCard(HttpSession session, RequestCardDto requestOneMoreDto); +} diff --git a/src/main/java/jp/moreslowly/oi/service/RoomServiceImpl.java b/src/main/java/jp/moreslowly/oi/service/RoomServiceImpl.java new file mode 100644 index 0000000..9885149 --- /dev/null +++ b/src/main/java/jp/moreslowly/oi/service/RoomServiceImpl.java @@ -0,0 +1,251 @@ +package jp.moreslowly.oi.service; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.stream.Collectors; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; +import org.springframework.web.context.request.async.DeferredResult; + +import jakarta.servlet.http.HttpSession; +import jp.moreslowly.oi.common.RoomLimitation; +import jp.moreslowly.oi.common.SessionKey; +import jp.moreslowly.oi.dao.Bet; +import jp.moreslowly.oi.dao.Room; +import jp.moreslowly.oi.dao.Room.Status; +import jp.moreslowly.oi.dto.BetDto; +import jp.moreslowly.oi.dto.RequestCardDto; +import jp.moreslowly.oi.dto.RoomDto; +import jp.moreslowly.oi.exception.FullMemberException; +import jp.moreslowly.oi.exception.NoRoomException; +import jp.moreslowly.oi.exception.UnprocessableContentException; +import jp.moreslowly.oi.models.Card; +import jp.moreslowly.oi.models.Member; +import jp.moreslowly.oi.models.Nickname; +import jp.moreslowly.oi.repository.RoomRepository; +import jp.moreslowly.oi.tasks.DealerManager; +import jp.moreslowly.oi.tasks.DealerManager.UpdateStatus; +import lombok.extern.log4j.Log4j2; + +@Service +@Log4j2 +public class RoomServiceImpl implements RoomService { + + @Autowired + private RoomRepository roomRepository; + + @Autowired + private DealerManager dealerManager; + + @Override + public List getRoomIdList() { + List roomIdList = new ArrayList<>(); + roomRepository.findAll().forEach(room -> roomIdList.add(room.getId())); + return roomIdList; + } + + private Room findOrCreateRoom(String id) { + dealerManager.updateAndNotify(id, () -> { + Optional maybeRoom = roomRepository.findById(id); + if (maybeRoom.isPresent()) { + return UpdateStatus.NOT_UPDATED; + } + if (roomRepository.count() >= RoomLimitation.MAX_ROOM_SIZE) { + throw new NoRoomException("No Room"); + } + Room room = Room.builder() + .id(id) + .status(Status.START) + .members(new ArrayList<>()) + .wallets(new HashMap<>()) + .lastAccessedAt(LocalDateTime.now()) + .updatedAt(LocalDateTime.now()) + .build(); + room.getWallets().put("dummy", 0); + log.info("#### create room {}", id); + roomRepository.save(room); + + return UpdateStatus.NOT_UPDATED; + }); + + Room room = roomRepository.findById(id) + .orElseThrow(() -> new UnprocessableContentException(UnprocessableContentException.ROOM_IS_NOT_FOUND)); + room.setLastAccessedAt(LocalDateTime.now()); + dealerManager.updateAndNotify(id, () -> { + roomRepository.save(room); + return UpdateStatus.NOT_UPDATED; + }); + return room; + } + + private UUID getUserId(HttpSession session) { + String userId = (String) session.getAttribute(SessionKey.USER_ID); + if (Objects.isNull(userId)) { + userId = UUID.randomUUID().toString(); + session.setAttribute(SessionKey.USER_ID, userId); + } + return UUID.fromString(userId); + } + + private String getNickname(HttpSession session, UUID userId, Room room) { + dealerManager.updateAndNotify(room.getId(), () -> { + if (Objects.isNull(room.getMembers())) { + room.setMembers(new ArrayList<>()); + } + + Optional maybeMember = room.getMembers().stream().filter(m -> m.getId().equals(userId)).findFirst(); + if (maybeMember.isPresent()) { + session.setAttribute(SessionKey.NICKNAME, maybeMember.get().getNickname()); + return UpdateStatus.NOT_UPDATED; + } + + List unusedNames = Nickname.NICKNAME_LIST.stream().filter(name -> { + return room.getMembers().stream().noneMatch(m -> m.getNickname().equals(name)); + }).collect(Collectors.toList()); + if (unusedNames.isEmpty()) { + throw new FullMemberException("Nickname is full"); + } + String nickname = unusedNames.get((int) (Math.random() * unusedNames.size())); + session.setAttribute(SessionKey.NICKNAME, nickname); + if (room.getMembers().size() >= RoomLimitation.MAX_MEMBER_SIZE) { + return UpdateStatus.NOT_UPDATED; + } + room.getMembers().add(Member.builder().id(userId).nickname(nickname).build()); + + Integer amount = room.getWallets().get(nickname); + if (Objects.nonNull(amount)) { + return UpdateStatus.NOT_UPDATED; + } + room.getWallets().putIfAbsent(nickname, 10000); + + roomRepository.save(room); + return UpdateStatus.UPDATED; + }); + return (String) session.getAttribute(SessionKey.NICKNAME); + } + + @Override + public RoomDto enterRoom(HttpSession session, String id) { + String enteredRoomId = (String) session.getAttribute(SessionKey.ROOM_ID); + if (Objects.isNull(enteredRoomId)) { + session.setAttribute(SessionKey.ROOM_ID, id); + enteredRoomId = id; + } + if (!enteredRoomId.equals(id)) { + session.removeAttribute(SessionKey.NICKNAME); + session.setAttribute(SessionKey.ROOM_ID, id); + } + + Room room = findOrCreateRoom(id); + UUID userId = getUserId(session); + String nickname = getNickname(session, userId, room); + + return RoomDto.fromEntity(room, nickname); + } + + private ExecutorService runners = Executors + .newFixedThreadPool(RoomLimitation.MAX_ROOM_SIZE * RoomLimitation.MAX_MEMBER_SIZE); + + @Override + public void subscribe(String id, HttpSession session, DeferredResult deferredResult) { + Room room = findOrCreateRoom(id); + UUID userId = getUserId(session); + String yourName = getNickname(session, userId, room); + + runners.execute(() -> { + dealerManager.waitForUpdating(id, () -> { + Room newRoom = roomRepository.findById(id) + .orElseThrow(() -> new UnprocessableContentException(UnprocessableContentException.ROOM_IS_NOT_FOUND)); + RoomDto dto = RoomDto.fromEntity(newRoom, yourName); + deferredResult.setResult(dto); + }); + }); + } + + @Override + public void reset(String id) { + dealerManager.updateAndNotify(id, () -> { + Room room = roomRepository.findById(id) + .orElseThrow(() -> new UnprocessableContentException(UnprocessableContentException.ROOM_IS_NOT_FOUND)); + room.reset(); + room.setMembers(new ArrayList<>()); + room.setUpdatedAt(LocalDateTime.now()); + roomRepository.save(room); + return UpdateStatus.UPDATED; + }); + } + + @Override + public void bet(HttpSession session, BetDto betDto) { + UUID userId = getUserId(session); + dealerManager.updateAndNotify(betDto.getRoomId(), () -> { + Optional maybeRoom = roomRepository.findById(betDto.getRoomId()); + if (!maybeRoom.isPresent()) { + return UpdateStatus.NOT_UPDATED; + } + Room room = maybeRoom.get(); + Member you = Member.builder().id(userId).nickname(betDto.getUserName()).build(); + if (!room.getMembers().contains(you)) { + throw new UnprocessableContentException("Invalid nickname"); + } + + if (CollectionUtils.isEmpty(room.getBets())) { + room.setBets(new ArrayList<>()); + } + Integer wallet = room.getWallets().get(betDto.getUserName()); + wallet -= betDto.getBetAmount(); + room.getWallets().put(betDto.getUserName(), wallet); + room.getBets().add(betDto.toEntity()); + roomRepository.save(room); + + return UpdateStatus.UPDATED; + }); + } + + @Override + public void requestCard(HttpSession session, RequestCardDto requestOneMoreDto) { + UUID userId = getUserId(session); + dealerManager.updateAndNotify(requestOneMoreDto.getRoomId(), () -> { + Optional maybeRoom = roomRepository.findById(requestOneMoreDto.getRoomId()); + if (!maybeRoom.isPresent()) { + return UpdateStatus.NOT_UPDATED; + } + Room room = maybeRoom.get(); + Member you = Member.builder().id(userId).nickname(requestOneMoreDto.getUserName()).build(); + if (!room.getMembers().contains(you)) { + throw new UnprocessableContentException("You are not member of this room"); + } + + Optional maybeBet = room.getBets().stream().filter( + bet -> Objects.equals(bet.getHandIndex(), requestOneMoreDto.getHandIndex())).findFirst(); + if (!maybeBet.isPresent()) { + throw new UnprocessableContentException("You are not owner of this hand"); + } + Bet bet = maybeBet.get(); + if (!bet.getUserName().equals(requestOneMoreDto.getUserName())) { + throw new UnprocessableContentException("You are not owner of this hand: " + bet.getUserName() + ", " + + requestOneMoreDto.getUserName()); + } + + List hands = room.getHandsAt(requestOneMoreDto.getHandIndex()); + if (hands.size() == 3) { + throw new UnprocessableContentException("You can't request more card"); + } + Card aCard = room.getDeck().remove(0); + hands.add(aCard); + + roomRepository.save(room); + + return UpdateStatus.UPDATED; + }); + } +} diff --git a/src/main/java/jp/moreslowly/oi/tasks/DealerManager.java b/src/main/java/jp/moreslowly/oi/tasks/DealerManager.java new file mode 100644 index 0000000..a4b422f --- /dev/null +++ b/src/main/java/jp/moreslowly/oi/tasks/DealerManager.java @@ -0,0 +1,75 @@ +package jp.moreslowly.oi.tasks; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; + +import jp.moreslowly.oi.common.RoomLimitation; +import jp.moreslowly.oi.repository.RoomRepository; +import jp.moreslowly.oi.service.CardService; + +public class DealerManager { + + public enum UpdateStatus { + UPDATED, + NOT_UPDATED + } + + public interface BeNotified { + void afterUpdate(); + } + + public interface Updatable { + UpdateStatus update(); + } + + @Autowired + private RoomRepository roomRepository; + + @Autowired + private CardService cardService; + + private ConcurrentHashMap lockMap = new ConcurrentHashMap<>(); + + private ExecutorService runners = Executors + .newFixedThreadPool(RoomLimitation.MAX_ROOM_SIZE * 2); + + private Object getLock(String roomId) { + return lockMap.computeIfAbsent(roomId, k -> new Object()); + } + + public CardService getCardService() { + return cardService; + } + + public void waitForUpdating(String roomId, BeNotified proc) { + Object lock = getLock(roomId); + synchronized (lock) { + try { + lock.wait(); + } catch (InterruptedException e) { + // do nothing + } + proc.afterUpdate(); + } + } + + public void updateAndNotify(String roomId, Updatable proc) { + Object lock = getLock(roomId); + synchronized (lock) { + if (proc.update() == UpdateStatus.UPDATED) { + lock.notifyAll(); + } + } + } + + @Scheduled(fixedRate = 1000) + public void startDealer() { + roomRepository.findAll().forEach(room -> { + runners.submit(new DealerTask(this, roomRepository, room.getId())); + }); + } +} diff --git a/src/main/java/jp/moreslowly/oi/tasks/DealerTask.java b/src/main/java/jp/moreslowly/oi/tasks/DealerTask.java new file mode 100644 index 0000000..994f48e --- /dev/null +++ b/src/main/java/jp/moreslowly/oi/tasks/DealerTask.java @@ -0,0 +1,244 @@ +package jp.moreslowly.oi.tasks; + +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import org.springframework.util.CollectionUtils; + +import jp.moreslowly.oi.common.RoomLimitation; +import jp.moreslowly.oi.dao.Bet; +import jp.moreslowly.oi.dao.Room; +import jp.moreslowly.oi.models.Card; +import jp.moreslowly.oi.repository.RoomRepository; +import jp.moreslowly.oi.tasks.DealerManager.UpdateStatus; +import lombok.extern.log4j.Log4j2; + +@Log4j2 +public class DealerTask implements Runnable { + + private DealerManager manager; + private RoomRepository roomRepository; + private String roomId; + + public DealerTask(DealerManager manager, RoomRepository roomRepository, String roomId) { + this.manager = manager; + this.roomRepository = roomRepository; + this.roomId = roomId; + } + + @Override + public void run() { + manager.updateAndNotify(roomId, () -> { + Optional maybeRoom = roomRepository.findById(roomId); + if (!maybeRoom.isPresent()) { + return UpdateStatus.NOT_UPDATED; + } + Room room = maybeRoom.get(); + + switch (room.getStatus()) { + case START: + return processStart(room); + case SHUFFLE: + return processShuffle(room); + case HAND_OUT_CARDS: + return processHandOutCards(room); + case WAIT_TO_BET: + return processWaitToBet(room); + case WAIT_TO_REQUEST: + return processWaitToRequest(room); + case DEALER_TURN: + return processDealerTurn(room); + case LIQUIDATION: + return processLiquidation(room); + case END: + return processEnd(room); + default: + return UpdateStatus.NOT_UPDATED; + } + }); + } + + private static final int SHORT_TIMEOUT_SEC = 5; + private static final int GENERAL_TIMEOUT_SEC = 30; + + private UpdateStatus processStart(Room room) { + log.info("processStart: room id {}", room.getId()); + room.reset(); + room.setStatus(Room.Status.START.next()); + room.setUpdatedAt(LocalDateTime.now()); + room.setTimeLeft((long) SHORT_TIMEOUT_SEC); + room.setTimeLeftDenominator((long) SHORT_TIMEOUT_SEC); + roomRepository.save(room); + return UpdateStatus.UPDATED; + } + + private UpdateStatus processShuffle(Room room) { + LocalDateTime now = LocalDateTime.now(); + LocalDateTime timeLimit = room.getUpdatedAt().plusSeconds(SHORT_TIMEOUT_SEC); + if (Objects.nonNull(room.getUpdatedAt()) && now.isBefore(timeLimit)) { + room.setTimeLeft(ChronoUnit.SECONDS.between(now, timeLimit)); + roomRepository.save(room); + return UpdateStatus.NOT_UPDATED; + } + + List deck = Card.generateCardDeck(); + room.setDeck(deck); + room.setStatus(Room.Status.SHUFFLE.next()); + room.setUpdatedAt(LocalDateTime.now()); + roomRepository.save(room); + return UpdateStatus.UPDATED; + } + + private UpdateStatus processHandOutCards(Room room) { + List> hands = new ArrayList<>(); + for (int i = 0; i < RoomLimitation.MAX_HAND_OUT_SIZE; i++) { + List hand = new ArrayList<>(); + for (int j = 0; j < 2; j++) { + hand.add(room.getDeck().remove(0)); + } + hands.add(hand); + } + + room.setHands1(hands.get(0)); + room.setHands2(hands.get(1)); + room.setHands3(hands.get(2)); + room.setHands4(hands.get(3)); + room.setHands5(hands.get(4)); + room.setHands6(hands.get(5)); + room.setHands7(hands.get(6)); + room.setStatus(Room.Status.HAND_OUT_CARDS.next()); + room.setUpdatedAt(LocalDateTime.now()); + room.setTimeLeft((long) GENERAL_TIMEOUT_SEC); + room.setTimeLeftDenominator((long) GENERAL_TIMEOUT_SEC); + roomRepository.save(room); + return UpdateStatus.UPDATED; + } + + private UpdateStatus processWaitToBet(Room room) { + LocalDateTime now = LocalDateTime.now(); + LocalDateTime timeLimit = room.getUpdatedAt().plusSeconds(GENERAL_TIMEOUT_SEC); + if (Objects.nonNull(room.getUpdatedAt()) && now.isBefore(timeLimit)) { + room.setTimeLeft(ChronoUnit.SECONDS.between(now, timeLimit)); + roomRepository.save(room); + return UpdateStatus.NOT_UPDATED; + } + + room.setStatus(Room.Status.WAIT_TO_BET.next()); + room.setUpdatedAt(LocalDateTime.now()); + room.setTimeLeft((long) GENERAL_TIMEOUT_SEC); + room.setTimeLeftDenominator((long) GENERAL_TIMEOUT_SEC); + roomRepository.save(room); + return UpdateStatus.UPDATED; + } + + private UpdateStatus processWaitToRequest(Room room) { + if (CollectionUtils.isEmpty(room.getBets())) { + room.setStatus(Room.Status.WAIT_TO_REQUEST.next()); + room.setUpdatedAt(LocalDateTime.now()); + room.setTimeLeft((long) SHORT_TIMEOUT_SEC); + room.setTimeLeftDenominator((long) SHORT_TIMEOUT_SEC); + roomRepository.save(room); + return UpdateStatus.UPDATED; + } + + LocalDateTime now = LocalDateTime.now(); + LocalDateTime timeLimit = room.getUpdatedAt().plusSeconds(GENERAL_TIMEOUT_SEC); + if (Objects.nonNull(room.getUpdatedAt()) && now.isBefore(timeLimit)) { + room.setTimeLeft(ChronoUnit.SECONDS.between(now, timeLimit)); + roomRepository.save(room); + return UpdateStatus.NOT_UPDATED; + } + + room.setStatus(Room.Status.WAIT_TO_REQUEST.next()); + room.setUpdatedAt(LocalDateTime.now()); + room.setTimeLeft((long) SHORT_TIMEOUT_SEC); + room.setTimeLeftDenominator((long) SHORT_TIMEOUT_SEC); + roomRepository.save(room); + return UpdateStatus.UPDATED; + } + + private UpdateStatus processDealerTurn(Room room) { + LocalDateTime now = LocalDateTime.now(); + LocalDateTime timeLimit = room.getUpdatedAt().plusSeconds(SHORT_TIMEOUT_SEC); + if (Objects.nonNull(room.getUpdatedAt()) && now.isBefore(timeLimit)) { + room.setTimeLeft(ChronoUnit.SECONDS.between(now, timeLimit)); + roomRepository.save(room); + return UpdateStatus.NOT_UPDATED; + } + + List parentCards = room.getHands7(); + int point = manager.getCardService().evaluate(parentCards); + int threshold = Math.random() < 0.5 ? 5 : 6; + if (point <= threshold) { + parentCards.add(room.getDeck().remove(0)); + room.setHands7(parentCards); + } + + room.setStatus(Room.Status.DEALER_TURN.next()); + room.setUpdatedAt(LocalDateTime.now()); + roomRepository.save(room); + return UpdateStatus.UPDATED; + } + + private UpdateStatus processLiquidation(Room room) { + List parentCards = room.getHands7(); + int parentPoint = manager.getCardService().evaluate(parentCards); + List bets = room.getBets(); + if (!CollectionUtils.isEmpty(bets)) { + room.getBets().stream().forEach(bet -> { + List hands = room.getHandsAt(bet.getHandIndex()); + int point = manager.getCardService().evaluate(hands); + if (point > parentPoint) { + bet.setResult(Bet.Result.WIN); + int betAmount = bet.getBetAmount(); + int wallet = room.getWallets().get(bet.getUserName()); + room.getWallets().put(bet.getUserName(), wallet + betAmount * 2); + } else if (point == parentPoint) { + bet.setResult(Bet.Result.DRAW); + int betAmount = bet.getBetAmount(); + int wallet = room.getWallets().get(bet.getUserName()); + room.getWallets().put(bet.getUserName(), wallet + betAmount); + } else { + bet.setResult(Bet.Result.LOSE); + } + }); + } + + room.setStatus(Room.Status.LIQUIDATION.next()); + room.setUpdatedAt(LocalDateTime.now()); + int timeout = GENERAL_TIMEOUT_SEC; + if (CollectionUtils.isEmpty(room.getBets())) { + timeout = SHORT_TIMEOUT_SEC; + } + room.setTimeLeft((long) timeout); + room.setTimeLeftDenominator((long) timeout); + roomRepository.save(room); + return UpdateStatus.UPDATED; + } + + private UpdateStatus processEnd(Room room) { + int timeout = GENERAL_TIMEOUT_SEC; + if (CollectionUtils.isEmpty(room.getBets())) { + timeout = SHORT_TIMEOUT_SEC; + } + LocalDateTime now = LocalDateTime.now(); + LocalDateTime timeLimit = room.getUpdatedAt().plusSeconds(timeout); + if (Objects.nonNull(room.getUpdatedAt()) && now.isBefore(timeLimit)) { + room.setTimeLeft(ChronoUnit.SECONDS.between(now, timeLimit)); + roomRepository.save(room); + return UpdateStatus.NOT_UPDATED; + } + + room.reset(); + room.setStatus(Room.Status.END.next()); + room.setUpdatedAt(LocalDateTime.now()); + room.setTimeLeft(0L); + room.setTimeLeftDenominator(1L); + roomRepository.save(room); + return UpdateStatus.UPDATED; + } +} diff --git a/src/main/java/jp/moreslowly/oi/tasks/SweeperTask.java b/src/main/java/jp/moreslowly/oi/tasks/SweeperTask.java new file mode 100644 index 0000000..efe3b10 --- /dev/null +++ b/src/main/java/jp/moreslowly/oi/tasks/SweeperTask.java @@ -0,0 +1,28 @@ +package jp.moreslowly.oi.tasks; + +import java.time.LocalDateTime; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; + +import jp.moreslowly.oi.repository.RoomRepository; +import lombok.extern.log4j.Log4j2; + +@Log4j2 +public class SweeperTask { + + private static final int EXPIRED_MINUTES = 15; + private static final int SWEEP_FIXED_DELAY = 10 * 1000; + + @Autowired private RoomRepository roomRepository; + + @Scheduled(fixedDelay = SWEEP_FIXED_DELAY) + public void sweep() { + roomRepository.findAll().forEach(room -> { + if (room.getLastAccessedAt().plusMinutes(EXPIRED_MINUTES).isBefore(LocalDateTime.now())) { + log.info("### Sweeping room: {}", room.getId()); + roomRepository.delete(room); + } + }); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8b13789..0342fb2 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,5 @@ +ajp.secret=${AJP_SECRET:password} +spring.session.store-type=redis +server.servlet.session.timeout=5184000 +spring.mvc.async.request-timeout=2m diff --git a/src/main/resources/conf/logback-access.xml b/src/main/resources/conf/logback-access.xml new file mode 100644 index 0000000..8bc5dbb --- /dev/null +++ b/src/main/resources/conf/logback-access.xml @@ -0,0 +1,12 @@ + + + + + + combined + + + + + +