diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts index 57c1b3da40..d7318a43ad 100644 --- a/packages/backend/src/core/GlobalEventService.ts +++ b/packages/backend/src/core/GlobalEventService.ts @@ -205,18 +205,23 @@ export interface MahjongRoomEventTypes { room: Packed<'MahjongRoomDetailed'>; }; tsumo: { - house: Mahjong.Engine.House; + house: Mahjong.Common.House; tile: Mahjong.Common.Tile; }; dahai: { - house: Mahjong.Engine.House; + house: Mahjong.Common.House; tile: Mahjong.Common.Tile; }; dahaiAndTsumo: { - house: Mahjong.Engine.House; + dahaiHouse: Mahjong.Common.House; dahaiTile: Mahjong.Common.Tile; tsumoTile: Mahjong.Common.Tile; }; + ponned: { + source: Mahjong.Common.House; + target: Mahjong.Common.House; + tile: Mahjong.Common.Tile; + }; } //#endregion diff --git a/packages/backend/src/core/MahjongService.ts b/packages/backend/src/core/MahjongService.ts index 82bd7e8818..3e8b20a5a1 100644 --- a/packages/backend/src/core/MahjongService.ts +++ b/packages/backend/src/core/MahjongService.ts @@ -412,6 +412,25 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit { await this.dahai(room, engine, myHouse, tile); } + @bindThis + public async op_ron(roomId: MiMahjongGame['id'], user: MiUser) { + const room = await this.getRoom(roomId); + if (room == null) return; + if (room.gameState == null) return; + + const engine = new Mahjong.Engine.MasterGameEngine(room.gameState); + const myHouse = user.id === room.user1Id ? engine.state.user1House : user.id === room.user2Id ? engine.state.user2House : user.id === room.user3Id ? engine.state.user3House : engine.state.user4House; + + // TODO: 自分にロン回答する権利がある状態かバリデーション + + // TODO: この辺の処理はアトミックに行いたいけどJSONサポートはRedis Stackが必要 + const current = await this.redisClient.get(`mahjong:gameCallAndRonAsking:${room.id}`); + if (current == null) throw new Error('no asking found'); + const currentAnswers = JSON.parse(current) as CallAndRonAnswers; + currentAnswers.ron[myHouse] = true; + await this.redisClient.set(`mahjong:gameCallAndRonAsking:${room.id}`, JSON.stringify(currentAnswers)); + } + @bindThis public async op_pon(roomId: MiMahjongGame['id'], user: MiUser) { const room = await this.getRoom(roomId); 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 1472a7013a..e3556fbc8b 100644 --- a/packages/backend/src/server/api/stream/channels/mahjong-room.ts +++ b/packages/backend/src/server/api/stream/channels/mahjong-room.ts @@ -39,6 +39,7 @@ class MahjongRoomChannel extends Channel { case 'updateSettings': this.updateSettings(body.key, body.value); break; case 'addAi': this.addAi(); break; case 'dahai': this.dahai(body.tile); break; + case 'ron': this.ron(); break; case 'pon': this.pon(); break; case 'nop': this.nop(); break; case 'claimTimeIsUp': this.claimTimeIsUp(); break; @@ -73,6 +74,13 @@ class MahjongRoomChannel extends Channel { this.mahjongService.op_dahai(this.roomId!, this.user, tile); } + @bindThis + private async ron() { + if (this.user == null) return; + + this.mahjongService.op_ron(this.roomId!, this.user); + } + @bindThis private async pon() { if (this.user == null) return; diff --git a/packages/frontend/assets/mahjong/tile-top-h.png b/packages/frontend/assets/mahjong/tile-top-h.png new file mode 100644 index 0000000000..a575deca27 Binary files /dev/null and b/packages/frontend/assets/mahjong/tile-top-h.png differ diff --git a/packages/frontend/assets/mahjong/tile-top-v.png b/packages/frontend/assets/mahjong/tile-top-v.png new file mode 100644 index 0000000000..d087d195b4 Binary files /dev/null and b/packages/frontend/assets/mahjong/tile-top-v.png differ diff --git a/packages/frontend/assets/mahjong/tile-top.png b/packages/frontend/assets/mahjong/tile-top.png deleted file mode 100644 index 861608a43d..0000000000 Binary files a/packages/frontend/assets/mahjong/tile-top.png and /dev/null differ diff --git a/packages/frontend/src/pages/mahjong/room.game.vue b/packages/frontend/src/pages/mahjong/room.game.vue index ab4572e508..a38df8025e 100644 --- a/packages/frontend/src/pages/mahjong/room.game.vue +++ b/packages/frontend/src/pages/mahjong/room.game.vue @@ -28,28 +28,28 @@ SPDX-License-Identifier: AGPL-3.0-only
- +
- +
- +
- +
@@ -69,16 +69,17 @@ SPDX-License-Identifier: AGPL-3.0-only
- - - + + +
- Pon - Skip pon - Hora + Ron + Pon + Skip + Tsumo @@ -86,6 +87,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, onActivated, onDeactivated, onMounted, onUnmounted, ref, shallowRef, triggerRef, watch } from 'vue'; import * as Misskey from 'misskey-js'; import * as Mahjong from 'misskey-mahjong'; +import XTile from './tile.vue'; import MkButton from '@/components/MkButton.vue'; import MkFolder from '@/components/MkFolder.vue'; import MkSwitch from '@/components/MkSwitch.vue'; @@ -190,6 +192,14 @@ function dahai(tile: Mahjong.Common.Tile, ev: MouseEvent) { }); } +function ron() { + engine.value.op_ron(engine.value.state.canRonSource, engine.value.myHouse); + triggerRef(engine); + + props.connection!.send('ron', { + }); +} + function pon() { engine.value.op_pon(engine.value.state.canPonSource, engine.value.myHouse); triggerRef(engine); @@ -464,9 +474,6 @@ onUnmounted(() => { .hoTile { position: relative; display: inline-block; - width: 32px; - aspect-ratio: 0.7; - background: #fff; margin-bottom: -8px; } diff --git a/packages/frontend/src/pages/mahjong/tile.vue b/packages/frontend/src/pages/mahjong/tile.vue new file mode 100644 index 0000000000..a86c5f8463 --- /dev/null +++ b/packages/frontend/src/pages/mahjong/tile.vue @@ -0,0 +1,45 @@ + + + + + + + diff --git a/packages/misskey-mahjong/src/engine.ts b/packages/misskey-mahjong/src/engine.ts index 01e37b1a64..a71dca8e14 100644 --- a/packages/misskey-mahjong/src/engine.ts +++ b/packages/misskey-mahjong/src/engine.ts @@ -365,8 +365,9 @@ export class MasterGameEngine { const horaSets = Utils.getHoraSets(this.getHandTilesOf(house).concat(tile)); if (horaSets.length === 0) return false; // 完成形じゃない - const yakus = YAKU_DEFINITIONS.filter(yaku => yaku.calc(this.state, { tsumoTile: null, ronTile: tile })); - if (yakus.length === 0) return false; // 役がない + // TODO + //const yakus = YAKU_DEFINITIONS.filter(yaku => yaku.calc(this.state, { tsumoTile: null, ronTile: tile })); + //if (yakus.length === 0) return false; // 役がない return true; } @@ -494,6 +495,9 @@ export type PlayerState = { latestDahaiedTile: Tile | null; turn: House | null; canPonSource: House | null; + canCiiSource: House | null; + canKanSource: House | null; + canRonSource: House | null; canCiiTo: House | null; canKanTo: House | null; canRonTo: House | null; @@ -596,14 +600,34 @@ export class PlayerGameEngine { if (house === this.myHouse) { } else { + const canRon = Utils.getHoraSets(this.myHandTiles.concat(tile)).length > 0; const canPon = this.myHandTiles.filter(t => t === tile).length === 2; // TODO: canCii + if (canRon) this.state.canRonSource = house; if (canPon) this.state.canPonSource = house; } } + /** + * ロンします + * @param source 牌を捨てた人 + * @param target ロンした人 + */ + public op_ron(source: House, target: House) { + this.state.canRonSource = null; + + const lastTile = this.getHoTilesOf(source).pop(); + if (lastTile == null) throw new PlayerGameEngine.InvalidOperationError(); + if (target === this.myHouse) { + this.myHandTiles.push(lastTile); + } else { + this.getHandTilesOf(target).push(null); + } + this.state.turn = null; + } + /** * ポンします * @param source 牌を捨てた人 @@ -627,6 +651,7 @@ export class PlayerGameEngine { } public op_nop() { + this.state.canRonSource = null; this.state.canPonSource = null; } }