diff --git a/packages/backend/src/core/MahjongService.ts b/packages/backend/src/core/MahjongService.ts index 498563ac6b..a10720bf79 100644 --- a/packages/backend/src/core/MahjongService.ts +++ b/packages/backend/src/core/MahjongService.ts @@ -345,8 +345,8 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit { }); this.endKyoku(room, mj); break; - case 'ryukyoku': - this.globalEventService.publishMahjongRoomStream(room.id, 'ryukyoku', { + case 'ryuukyoku': + this.globalEventService.publishMahjongRoomStream(room.id, 'ryuukyoku', { }); this.endKyoku(room, mj); break; @@ -387,7 +387,11 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit { const aiHouses = [[1, room.user1Ai], [2, room.user2Ai], [3, room.user3Ai], [4, room.user4Ai]].filter(([id, ai]) => ai).map(([id, ai]) => mj.getHouse(id)); - if (res.asking) { + if (res.ryuukyoku) { + this.endKyoku(room, mj); + this.globalEventService.publishMahjongRoomStream(room.id, 'ryuukyoku', { + }); + } else if (res.asking) { const answers: CallingAnswers = { pon: null, cii: null, diff --git a/packages/frontend/assets/mahjong/99.png b/packages/frontend/assets/mahjong/99.png new file mode 100644 index 0000000000..2e72339a65 Binary files /dev/null and b/packages/frontend/assets/mahjong/99.png differ diff --git a/packages/frontend/assets/mahjong/ryuukyoku.png b/packages/frontend/assets/mahjong/ryuukyoku.png new file mode 100644 index 0000000000..1cdc4cce67 Binary files /dev/null and b/packages/frontend/assets/mahjong/ryuukyoku.png differ diff --git a/packages/frontend/src/pages/mahjong/room.game.vue b/packages/frontend/src/pages/mahjong/room.game.vue index 8b8883756d..2f8b7af8cb 100644 --- a/packages/frontend/src/pages/mahjong/room.game.vue +++ b/packages/frontend/src/pages/mahjong/room.game.vue @@ -168,6 +168,17 @@ SPDX-License-Identifier: AGPL-3.0-only +
+ + + +
+
Ron Pon @@ -259,6 +270,22 @@ const ponSerifHouses = reactive>({ e: false, s: false const kanSerifHouses = reactive>({ e: false, s: false, w: false, n: false }); const tsumoSerifHouses = reactive>({ e: false, s: false, w: false, n: false }); const riichiSerifHouses = reactive>({ e: false, s: false, w: false, n: false }); +const iTsumoed = ref(false); +const showKyokuResults = ref(false); +const kyokuResults = ref; +} | null>>({ + e: null, + s: null, + w: null, + n: null, +}); +const ryuukyokued = ref(false); /* if (room.value.isStarted && !room.value.isEnded) { @@ -387,7 +414,7 @@ function cii(ev: MouseEvent) { os.popupMenu(patterns.map(pattern => ({ text: pattern.join(' '), action: () => { - const index = Mmj.sortTileTypes(pattern).indexOf(targetTile); + const index = Mmj.sortTileTypes(pattern).indexOf(mj$type(targetTile)); props.connection!.send('cii', { pattern: index === 0 ? 'x__' : index === 1 ? '_x_' : '__x', }); @@ -407,26 +434,6 @@ function skip() { props.connection!.send('nop', {}); } -const iTsumoed = ref(false); -const showKyokuResults = ref(false); -const kyokuResults = ref; -} | null>>({ - e: null, - s: null, - w: null, - n: null, -}); - -function kyokuEnd() { - showKyokuResults.value = true; -} - function onStreamDahai(log) { console.log('onStreamDahai', log); @@ -617,6 +624,16 @@ function onStreamTsumoHora(log) { console.log('tsumohora', res); } +function onStreamRyuukyoku(log) { + console.log('onStreamRyuukyoku', log); + + ryuukyokued.value = true; + + window.setTimeout(() => { + showKyokuResults.value = true; + }, 1500); +} + function restoreRoom(_room) { room.value = deepClone(_room); @@ -635,6 +652,7 @@ onMounted(() => { props.connection.on('ciied', onStreamCiied); props.connection.on('ronned', onStreamRonned); props.connection.on('tsumoHora', onStreamTsumoHora); + props.connection.on('ryuukyoku', onStreamRyuukyoku); } }); @@ -650,6 +668,7 @@ onActivated(() => { props.connection.on('ciied', onStreamCiied); props.connection.on('ronned', onStreamRonned); props.connection.on('tsumoHora', onStreamTsumoHora); + props.connection.on('ryuukyoku', onStreamRyuukyoku); } }); @@ -665,6 +684,7 @@ onDeactivated(() => { props.connection.off('ciied', onStreamCiied); props.connection.off('ronned', onStreamRonned); props.connection.off('tsumoHora', onStreamTsumoHora); + props.connection.off('ryuukyoku', onStreamRyuukyoku); } }); @@ -680,6 +700,7 @@ onUnmounted(() => { props.connection.off('ciied', onStreamCiied); props.connection.off('ronned', onStreamRonned); props.connection.off('tsumoHora', onStreamTsumoHora); + props.connection.off('ryuukyoku', onStreamRyuukyoku); } }); @@ -969,6 +990,19 @@ onUnmounted(() => { height: min-content; } +.ryuukyokuContainer { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + width: 200px; + height: min-content; + margin: auto; + z-index: 100; + pointer-events: none; +} + .sideTile { margin-bottom: -26px; } diff --git a/packages/misskey-mahjong/src/engine.master.ts b/packages/misskey-mahjong/src/engine.master.ts index 64a87d6cc0..8361aaf34f 100644 --- a/packages/misskey-mahjong/src/engine.master.ts +++ b/packages/misskey-mahjong/src/engine.master.ts @@ -253,12 +253,12 @@ export class MasterGameEngine { return tile; } - private canRon(house: House, tile: TileId): boolean { + private canRon(house: House, tileId: TileId): boolean { // フリテン // TODO: ポンされるなどして自分の河にない場合の考慮 - if (this.hoTileTypes[house].includes($type(tile))) return false; + if (this.hoTileTypes[house].includes($type(tileId))) return false; - const horaSets = Common.getHoraSets(this.handTileTypes[house].concat($type(tile))); + const horaSets = Common.getHoraSets(this.handTileTypes[house].concat($type(tileId))); if (horaSets.length === 0) return false; // 完成形じゃない // TODO @@ -268,15 +268,15 @@ export class MasterGameEngine { return true; } - private canPon(house: House, tile: TileId): boolean { - return this.handTileTypes[house].filter(t => t === $type(tile)).length === 2; + private canPon(house: House, tileId: TileId): boolean { + return this.handTileTypes[house].filter(t => t === $type(tileId)).length === 2; } - private canCii(caller: House, callee: House, tile: TileId): boolean { + private canCii(caller: House, callee: House, tileId: TileId): boolean { if (callee !== Common.prevHouse(caller)) return false; const hand = this.handTileTypes[caller]; return Common.SHUNTU_PATTERNS.some(pattern => - pattern.includes($type(tile)) && + pattern.includes($type(tileId)) && pattern.filter(t => hand.includes(t)).length >= 2); } @@ -325,20 +325,20 @@ export class MasterGameEngine { this.endKyoku(); } - public commit_dahai(house: House, tile: TileId, riichi = false) { + public commit_dahai(house: House, tileId: TileId, riichi = false) { if (this.state.turn !== house) throw new Error('Not your turn'); if (riichi) { const tempHandTiles = [...this.handTileTypes[house]]; - tempHandTiles.splice(tempHandTiles.indexOf($type(tile)), 1); + tempHandTiles.splice(tempHandTiles.indexOf($type(tileId)), 1); if (Common.getHoraTiles(tempHandTiles).length === 0) throw new Error('Not tenpai'); if (this.state.points[house] < 1000) throw new Error('Not enough points'); } const handTiles = this.state.handTiles[house]; - if (!handTiles.includes(tile)) throw new Error('No such tile in your hand'); - handTiles.splice(handTiles.indexOf(tile), 1); - this.state.hoTiles[house].push(tile); + if (!handTiles.includes(tileId)) throw new Error('No such tile in your hand'); + handTiles.splice(handTiles.indexOf(tileId), 1); + this.state.hoTiles[house].push(tileId); if (riichi) { this.state.riichis[house] = true; @@ -347,24 +347,24 @@ export class MasterGameEngine { const canRonHouses: House[] = []; switch (house) { case 'e': - if (this.canRon('s', tile)) canRonHouses.push('s'); - if (this.canRon('w', tile)) canRonHouses.push('w'); - if (this.canRon('n', tile)) canRonHouses.push('n'); + if (this.canRon('s', tileId)) canRonHouses.push('s'); + if (this.canRon('w', tileId)) canRonHouses.push('w'); + if (this.canRon('n', tileId)) canRonHouses.push('n'); break; case 's': - if (this.canRon('e', tile)) canRonHouses.push('e'); - if (this.canRon('w', tile)) canRonHouses.push('w'); - if (this.canRon('n', tile)) canRonHouses.push('n'); + if (this.canRon('e', tileId)) canRonHouses.push('e'); + if (this.canRon('w', tileId)) canRonHouses.push('w'); + if (this.canRon('n', tileId)) canRonHouses.push('n'); break; case 'w': - if (this.canRon('e', tile)) canRonHouses.push('e'); - if (this.canRon('s', tile)) canRonHouses.push('s'); - if (this.canRon('n', tile)) canRonHouses.push('n'); + if (this.canRon('e', tileId)) canRonHouses.push('e'); + if (this.canRon('s', tileId)) canRonHouses.push('s'); + if (this.canRon('n', tileId)) canRonHouses.push('n'); break; case 'n': - if (this.canRon('e', tile)) canRonHouses.push('e'); - if (this.canRon('s', tile)) canRonHouses.push('s'); - if (this.canRon('w', tile)) canRonHouses.push('w'); + if (this.canRon('e', tileId)) canRonHouses.push('e'); + if (this.canRon('s', tileId)) canRonHouses.push('s'); + if (this.canRon('w', tileId)) canRonHouses.push('w'); break; } @@ -372,18 +372,18 @@ export class MasterGameEngine { let canPonHouse: House | null = null; switch (house) { - case 'e': canPonHouse = this.canPon('s', tile) ? 's' : this.canPon('w', tile) ? 'w' : this.canPon('n', tile) ? 'n' : null; break; - case 's': canPonHouse = this.canPon('e', tile) ? 'e' : this.canPon('w', tile) ? 'w' : this.canPon('n', tile) ? 'n' : null; break; - case 'w': canPonHouse = this.canPon('e', tile) ? 'e' : this.canPon('s', tile) ? 's' : this.canPon('n', tile) ? 'n' : null; break; - case 'n': canPonHouse = this.canPon('e', tile) ? 'e' : this.canPon('s', tile) ? 's' : this.canPon('w', tile) ? 'w' : null; break; + case 'e': canPonHouse = this.canPon('s', tileId) ? 's' : this.canPon('w', tileId) ? 'w' : this.canPon('n', tileId) ? 'n' : null; break; + case 's': canPonHouse = this.canPon('e', tileId) ? 'e' : this.canPon('w', tileId) ? 'w' : this.canPon('n', tileId) ? 'n' : null; break; + case 'w': canPonHouse = this.canPon('e', tileId) ? 'e' : this.canPon('s', tileId) ? 's' : this.canPon('n', tileId) ? 'n' : null; break; + case 'n': canPonHouse = this.canPon('e', tileId) ? 'e' : this.canPon('s', tileId) ? 's' : this.canPon('w', tileId) ? 'w' : null; break; } let canCiiHouse: House | null = null; switch (house) { - case 'e': canCiiHouse = this.canCii('s', house, tile) ? 's' : this.canCii('w', house, tile) ? 'w' : this.canCii('n', house, tile) ? 'n' : null; break; - case 's': canCiiHouse = this.canCii('e', house, tile) ? 'e' : this.canCii('w', house, tile) ? 'w' : this.canCii('n', house, tile) ? 'n' : null; break; - case 'w': canCiiHouse = this.canCii('e', house, tile) ? 'e' : this.canCii('s', house, tile) ? 's' : this.canCii('n', house, tile) ? 'n' : null; break; - case 'n': canCiiHouse = this.canCii('e', house, tile) ? 'e' : this.canCii('s', house, tile) ? 's' : this.canCii('w', house, tile) ? 'w' : null; break; + case 'e': canCiiHouse = this.canCii('s', house, tileId) ? 's' : this.canCii('w', house, tileId) ? 'w' : this.canCii('n', house, tileId) ? 'n' : null; break; + case 's': canCiiHouse = this.canCii('e', house, tileId) ? 'e' : this.canCii('w', house, tileId) ? 'w' : this.canCii('n', house, tileId) ? 'n' : null; break; + case 'w': canCiiHouse = this.canCii('e', house, tileId) ? 'e' : this.canCii('s', house, tileId) ? 's' : this.canCii('n', house, tileId) ? 'n' : null; break; + case 'n': canCiiHouse = this.canCii('e', house, tileId) ? 'e' : this.canCii('s', house, tileId) ? 's' : this.canCii('w', house, tileId) ? 'w' : null; break; } if (canRonHouses.length > 0 || canPonHouse != null || canCiiHouse != null) { @@ -422,6 +422,18 @@ export class MasterGameEngine { }; } + // 流局 + if (this.state.tiles.length === 0) { + this.state.turn = null; + + this.endKyoku(); + + return { + asking: false as const, + ryuukyoku: true as const, + }; + } + this.state.turn = Common.nextHouse(house); const tsumoTile = this.tsumo(); @@ -433,11 +445,11 @@ export class MasterGameEngine { }; } - public commit_kakan(house: House, tile: TileId) { - const pon = this.state.huros[house].find(h => h.type === 'pon' && $type(h.tiles[0]) === $type(tile)); + public commit_kakan(house: House, tileId: TileId) { + const pon = this.state.huros[house].find(h => h.type === 'pon' && $type(h.tiles[0]) === $type(tileId)); if (pon == null) throw new Error('No such pon'); - this.state.handTiles[house].splice(this.state.handTiles[house].indexOf(tile), 1); - const tiles = [tile, ...pon.tiles]; + this.state.handTiles[house].splice(this.state.handTiles[house].indexOf(tileId), 1); + const tiles = [tileId, ...pon.tiles]; this.state.huros[house].push({ type: 'minkan', tiles: tiles, from: pon.from }); this.state.activatedDorasCount++; @@ -451,14 +463,14 @@ export class MasterGameEngine { }; } - public commit_ankan(house: House, tile: TileId) { - const t1 = this.state.handTiles[house].filter(t => $type(t) === $type(tile)).at(0); + public commit_ankan(house: House, tileId: TileId) { + const t1 = this.state.handTiles[house].filter(t => $type(t) === $type(tileId)).at(0); if (t1 == null) throw new Error('No such tile'); - const t2 = this.state.handTiles[house].filter(t => $type(t) === $type(tile)).at(1); + const t2 = this.state.handTiles[house].filter(t => $type(t) === $type(tileId)).at(1); if (t2 == null) throw new Error('No such tile'); - const t3 = this.state.handTiles[house].filter(t => $type(t) === $type(tile)).at(2); + const t3 = this.state.handTiles[house].filter(t => $type(t) === $type(tileId)).at(2); if (t3 == null) throw new Error('No such tile'); - const t4 = this.state.handTiles[house].filter(t => $type(t) === $type(tile)).at(3); + const t4 = this.state.handTiles[house].filter(t => $type(t) === $type(tileId)).at(3); if (t4 == null) throw new Error('No such tile'); this.state.handTiles[house].splice(this.state.handTiles[house].indexOf(t1), 1); this.state.handTiles[house].splice(this.state.handTiles[house].indexOf(t2), 1); @@ -660,7 +672,7 @@ export class MasterGameEngine { this.endKyoku(); return { - type: 'ryukyoku' as const, + type: 'ryuukyoku' as const, }; } else { this.state.turn = this.state.nextTurnAfterAsking!; diff --git a/packages/misskey-mahjong/src/engine.player.ts b/packages/misskey-mahjong/src/engine.player.ts index 4d571be782..f2ba137eff 100644 --- a/packages/misskey-mahjong/src/engine.player.ts +++ b/packages/misskey-mahjong/src/engine.player.ts @@ -152,19 +152,19 @@ export class PlayerGameEngine { return this.state.riichis[this.myHouse]; } - public commit_tsumo(house: House, tile: TileId) { - console.log('commit_tsumo', this.state.turn, house, tile); + public commit_tsumo(house: House, tileId: TileId) { + console.log('commit_tsumo', this.state.turn, house, tileId); this.state.tilesCount--; this.state.turn = house; if (house === this.myHouse) { - this.myHandTiles.push(tile); + this.myHandTiles.push(tileId); } else { this.state.handTiles[house].push(0); } } - public commit_dahai(house: House, tile: TileId, riichi = false) { - console.log('commit_dahai', this.state.turn, house, tile, riichi); + public commit_dahai(house: House, tileId: TileId, riichi = false) { + console.log('commit_dahai', this.state.turn, house, tileId, riichi); if (this.state.turn !== house) throw new PlayerGameEngine.InvalidOperationError(); if (riichi) { @@ -172,29 +172,29 @@ export class PlayerGameEngine { } if (house === this.myHouse) { - this.myHandTiles.splice(this.myHandTiles.indexOf(tile), 1); - this.state.hoTiles[this.myHouse].push(tile); + this.myHandTiles.splice(this.myHandTiles.indexOf(tileId), 1); + this.state.hoTiles[this.myHouse].push(tileId); } else { this.state.handTiles[house].pop(); - this.state.hoTiles[house].push(tile); + this.state.hoTiles[house].push(tileId); } this.state.turn = null; if (house === this.myHouse) { } else { - const canRon = Common.getHoraSets(this.myHandTiles.concat(tile).map(id => $type(id))).length > 0; - const canPon = !this.isMeRiichi && this.myHandTiles.filter(t => t === tile).length === 2; - const canKan = !this.isMeRiichi && this.myHandTiles.filter(t => t === tile).length === 3; + const canRon = Common.getHoraSets(this.myHandTiles.concat(tileId).map(id => $type(id))).length > 0; + const canPon = !this.isMeRiichi && this.myHandTileTypes.filter(t => t === $type(tileId)).length === 2; + const canKan = !this.isMeRiichi && this.myHandTileTypes.filter(t => t === $type(tileId)).length === 3; const canCii = !this.isMeRiichi && house === Common.prevHouse(this.myHouse) && Common.SHUNTU_PATTERNS.some(pattern => - pattern.includes($type(tile)) && + pattern.includes($type(tileId)) && pattern.filter(t => this.myHandTileTypes.includes(t)).length >= 2); - if (canRon) this.state.canRon = { callee: house }; - if (canPon) this.state.canPon = { callee: house }; - if (canKan) this.state.canKan = { callee: house }; - if (canCii) this.state.canCii = { callee: house }; + this.state.canRon = canRon ? { callee: house } : null; + this.state.canPon = canPon ? { callee: house } : null; + this.state.canKan = canKan ? { callee: house } : null; + this.state.canCii = canCii ? { callee: house } : null; } }