diff --git a/packages/backend/src/core/MahjongService.ts b/packages/backend/src/core/MahjongService.ts index bd89ffcc11..8957edd4e8 100644 --- a/packages/backend/src/core/MahjongService.ts +++ b/packages/backend/src/core/MahjongService.ts @@ -26,7 +26,7 @@ import { ReversiGameEntityService } from './entities/ReversiGameEntityService.js import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common'; const INVITATION_TIMEOUT_MS = 1000 * 20; // 20sec -const CALL_AND_RON_ASKING_TIMEOUT_MS = 1000 * 7; // 7sec +const CALL_AND_RON_ASKING_TIMEOUT_MS = 1000 * 10; // 10sec const TURN_TIMEOUT_MS = 1000 * 30; // 30sec const NEXT_KYOKU_CONFIRMATION_TIMEOUT_MS = 1000 * 15; // 15sec @@ -58,9 +58,9 @@ type Room = { gameState?: Mahjong.MasterState; }; -type CallAndRonAnswers = { +type CallingAnswers = { pon: null | boolean; - cii: null | boolean; + cii: null | false | [Mahjong.Tile, Mahjong.Tile, Mahjong.Tile]; kan: null | boolean; ron: { e: null | boolean; @@ -305,8 +305,8 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit { } @bindThis - private async answer(room: Room, engine: Mahjong.MasterGameEngine, answers: CallAndRonAnswers) { - const res = engine.commit_resolveCallAndRonInterruption({ + private async answer(room: Room, engine: Mahjong.MasterGameEngine, answers: CallingAnswers) { + const res = engine.commit_resolveCallingInterruption({ pon: answers.pon ?? false, cii: answers.cii ?? false, kan: answers.kan ?? false, @@ -386,7 +386,7 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit { if (res.asking) { console.log('asking', res); - const answers: CallAndRonAnswers = { + const answers: CallingAnswers = { pon: null, cii: null, kan: null, @@ -428,12 +428,12 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit { } } - this.redisClient.set(`mahjong:gameCallAndRonAsking:${room.id}`, JSON.stringify(answers)); + this.redisClient.set(`mahjong:gameCallingAsking:${room.id}`, JSON.stringify(answers)); const waitingStartedAt = Date.now(); const interval = setInterval(async () => { - const current = await this.redisClient.get(`mahjong:gameCallAndRonAsking:${room.id}`); - if (current == null) throw new Error('arienai (gameCallAndRonAsking)'); - const currentAnswers = JSON.parse(current) as CallAndRonAnswers; + const current = await this.redisClient.get(`mahjong:gameCallingAsking:${room.id}`); + if (current == null) throw new Error('arienai (gameCallingAsking)'); + const currentAnswers = JSON.parse(current) as CallingAnswers; const allAnswered = !( (res.canPonHouse != null && currentAnswers.pon == null) || (res.canCiiHouse != null && currentAnswers.cii == null) || @@ -445,7 +445,7 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit { ); if (allAnswered || (Date.now() - waitingStartedAt > CALL_AND_RON_ASKING_TIMEOUT_MS)) { console.log(allAnswered ? 'ask all answerd' : 'ask timeout'); - await this.redisClient.del(`mahjong:gameCallAndRonAsking:${room.id}`); + await this.redisClient.del(`mahjong:gameCallingAsking:${room.id}`); clearInterval(interval); this.answer(room, engine, currentAnswers); return; @@ -511,7 +511,7 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit { } @bindThis - public async commit_kakan(roomId: MiMahjongGame['id'], user: MiUser) { + public async commit_kakan(roomId: MiMahjongGame['id'], user: MiUser, tile: string) { const room = await this.getRoom(roomId); if (room == null) return; if (room.gameState == null) return; @@ -521,7 +521,7 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit { await this.clearTurnWaitingTimer(room.id); - const res = engine.commit_kakan(myHouse); + const res = engine.commit_kakan(myHouse, tile); this.globalEventService.publishMahjongRoomStream(room.id, 'kakanned', { }); } @@ -551,14 +551,14 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit { const engine = new Mahjong.MasterGameEngine(room.gameState); const myHouse = getHouseOfUserId(room, engine, user.id); - // TODO: 自分にロン回答する権利がある状態かバリデーション + // TODO: 自分に回答する権利がある状態かバリデーション // TODO: この辺の処理はアトミックに行いたいけどJSONサポートはRedis Stackが必要 - const current = await this.redisClient.get(`mahjong:gameCallAndRonAsking:${room.id}`); + const current = await this.redisClient.get(`mahjong:gameCallingAsking:${room.id}`); if (current == null) throw new Error('no asking found'); - const currentAnswers = JSON.parse(current) as CallAndRonAnswers; + const currentAnswers = JSON.parse(current) as CallingAnswers; currentAnswers.ron[myHouse] = true; - await this.redisClient.set(`mahjong:gameCallAndRonAsking:${room.id}`, JSON.stringify(currentAnswers)); + await this.redisClient.set(`mahjong:gameCallingAsking:${room.id}`, JSON.stringify(currentAnswers)); } @bindThis @@ -567,14 +567,46 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit { if (room == null) return; if (room.gameState == null) return; - // TODO: 自分にポン回答する権利がある状態かバリデーション + // TODO: 自分に回答する権利がある状態かバリデーション // TODO: この辺の処理はアトミックに行いたいけどJSONサポートはRedis Stackが必要 - const current = await this.redisClient.get(`mahjong:gameCallAndRonAsking:${room.id}`); + const current = await this.redisClient.get(`mahjong:gameCallingAsking:${room.id}`); if (current == null) throw new Error('no asking found'); - const currentAnswers = JSON.parse(current) as CallAndRonAnswers; + const currentAnswers = JSON.parse(current) as CallingAnswers; currentAnswers.pon = true; - await this.redisClient.set(`mahjong:gameCallAndRonAsking:${room.id}`, JSON.stringify(currentAnswers)); + await this.redisClient.set(`mahjong:gameCallingAsking:${room.id}`, JSON.stringify(currentAnswers)); + } + + @bindThis + public async commit_kan(roomId: MiMahjongGame['id'], user: MiUser) { + const room = await this.getRoom(roomId); + if (room == null) return; + if (room.gameState == null) return; + + // TODO: 自分に回答する権利がある状態かバリデーション + + // TODO: この辺の処理はアトミックに行いたいけどJSONサポートはRedis Stackが必要 + const current = await this.redisClient.get(`mahjong:gameCallingAsking:${room.id}`); + if (current == null) throw new Error('no asking found'); + const currentAnswers = JSON.parse(current) as CallingAnswers; + currentAnswers.kan = true; + await this.redisClient.set(`mahjong:gameCallingAsking:${room.id}`, JSON.stringify(currentAnswers)); + } + + @bindThis + public async commit_cii(roomId: MiMahjongGame['id'], user: MiUser, tiles: [Mahjong.Tile, Mahjong.Tile, Mahjong.Tile]) { + const room = await this.getRoom(roomId); + if (room == null) return; + if (room.gameState == null) return; + + // TODO: 自分に回答する権利がある状態かバリデーション + + // TODO: この辺の処理はアトミックに行いたいけどJSONサポートはRedis Stackが必要 + const current = await this.redisClient.get(`mahjong:gameCallingAsking:${room.id}`); + if (current == null) throw new Error('no asking found'); + const currentAnswers = JSON.parse(current) as CallingAnswers; + currentAnswers.cii = tiles; + await this.redisClient.set(`mahjong:gameCallingAsking:${room.id}`, JSON.stringify(currentAnswers)); } @bindThis @@ -587,14 +619,14 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit { const myHouse = getHouseOfUserId(room, engine, user.id); // TODO: この辺の処理はアトミックに行いたいけどJSONサポートはRedis Stackが必要 - const current = await this.redisClient.get(`mahjong:gameCallAndRonAsking:${room.id}`); + const current = await this.redisClient.get(`mahjong:gameCallingAsking:${room.id}`); if (current == null) throw new Error('no asking found'); - const currentAnswers = JSON.parse(current) as CallAndRonAnswers; + const currentAnswers = JSON.parse(current) as CallingAnswers; if (engine.state.ponAsking?.caller === myHouse) currentAnswers.pon = false; if (engine.state.ciiAsking?.caller === myHouse) currentAnswers.cii = false; if (engine.state.kanAsking?.caller === myHouse) currentAnswers.kan = false; if (engine.state.ronAsking != null && engine.state.ronAsking.callers.includes(myHouse)) currentAnswers.ron[myHouse] = false; - await this.redisClient.set(`mahjong:gameCallAndRonAsking:${room.id}`, JSON.stringify(currentAnswers)); + await this.redisClient.set(`mahjong:gameCallingAsking:${room.id}`, JSON.stringify(currentAnswers)); } /** diff --git a/packages/backend/src/server/api/stream/channels/mahjong-room.ts b/packages/backend/src/server/api/stream/channels/mahjong-room.ts index 23e5b3a3d4..53363bd5f3 100644 --- a/packages/backend/src/server/api/stream/channels/mahjong-room.ts +++ b/packages/backend/src/server/api/stream/channels/mahjong-room.ts @@ -56,6 +56,10 @@ class MahjongRoomChannel extends Channel { case 'tsumoHora': this.tsumoHora(); break; case 'ronHora': this.ronHora(); break; case 'pon': this.pon(); break; + case 'cii': this.cii(body.tiles); break; + case 'kan': this.kan(); break; + case 'ankan': this.ankan(body.tile); break; + case 'kakan': this.kakan(body.tile); break; case 'nop': this.nop(); break; case 'claimTimeIsUp': this.claimTimeIsUp(); break; } @@ -117,6 +121,34 @@ class MahjongRoomChannel extends Channel { this.mahjongService.commit_pon(this.roomId!, this.user); } + @bindThis + private async cii(tiles: string[]) { + if (this.user == null) return; + + this.mahjongService.commit_cii(this.roomId!, this.user, tiles); + } + +@bindThis + private async kan() { + if (this.user == null) return; + + this.mahjongService.commit_kan(this.roomId!, this.user); + } + + @bindThis +private async ankan(tile: string) { + if (this.user == null) return; + + this.mahjongService.commit_ankan(this.roomId!, this.user, tile); +} + + @bindThis + private async kakan(tile: string) { + if (this.user == null) return; + + this.mahjongService.commit_kakan(this.roomId!, this.user, tile); + } + @bindThis private async nop() { if (this.user == null) return; diff --git a/packages/frontend/src/pages/mahjong/room.game.vue b/packages/frontend/src/pages/mahjong/room.game.vue index 75e8fd5bbc..dec7add7b2 100644 --- a/packages/frontend/src/pages/mahjong/room.game.vue +++ b/packages/frontend/src/pages/mahjong/room.game.vue @@ -187,9 +187,13 @@ SPDX-License-Identifier: AGPL-3.0-only
@@ -197,7 +201,15 @@ SPDX-License-Identifier: AGPL-3.0-only