diff --git a/packages/backend/src/core/MahjongService.ts b/packages/backend/src/core/MahjongService.ts index fd7e5c0c48..0120b36e8a 100644 --- a/packages/backend/src/core/MahjongService.ts +++ b/packages/backend/src/core/MahjongService.ts @@ -428,8 +428,7 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit { if (aiHouses.includes(res.canPonHouse)) { // TODO: ちゃんと思考するようにする - //answers.pon = Math.random() < 0.25; - answers.pon = false; + answers.pon = Math.random() < 0.25; } if (aiHouses.includes(res.canCiiHouse)) { // TODO: ちゃんと思考するようにする @@ -438,8 +437,7 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit { } if (aiHouses.includes(res.canKanHouse)) { // TODO: ちゃんと思考するようにする - //answers.kan = Math.random() < 0.25; - answers.kan = false; + answers.kan = Math.random() < 0.25; } for (const h of res.canRonHouses) { if (aiHouses.includes(h)) { @@ -501,7 +499,7 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit { const room = await this.getRoom(roomId); if (room == null) return; if (room.gameState == null) return; - if (!Mahjong.Utils.isTile(tile)) return; + if (!Mahjong.Common.isTile(tile)) return; const engine = new Mahjong.MasterGameEngine(room.gameState); const myHouse = getHouseOfUserId(room, engine, user.id); @@ -629,7 +627,7 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit { if (engine.state.riichis[house]) { // リーチ時はアガリ牌でない限りツモ切り const handTiles = engine.state.handTiles[house]; - const horaSets = Mahjong.Utils.getHoraSets(handTiles); + const horaSets = Mahjong.Common.getHoraSets(handTiles); if (horaSets.length === 0) { setTimeout(() => { this.dahai(room, engine, house, handTiles.at(-1)); diff --git a/packages/frontend/assets/mahjong/bg.jpg b/packages/frontend/assets/mahjong/bg.jpg index 528c8b154e..a4b4d07a1e 100644 Binary files a/packages/frontend/assets/mahjong/bg.jpg and b/packages/frontend/assets/mahjong/bg.jpg differ diff --git a/packages/frontend/src/pages/mahjong/room.game.vue b/packages/frontend/src/pages/mahjong/room.game.vue index 6deea07465..ec9bc2a298 100644 --- a/packages/frontend/src/pages/mahjong/room.game.vue +++ b/packages/frontend/src/pages/mahjong/room.game.vue @@ -10,20 +10,20 @@ SPDX-License-Identifier: AGPL-3.0-only
- {{ Mahjong.Utils.prevHouse(Mahjong.Utils.prevHouse(engine.myHouse)) === 'e' ? i18n.ts._mahjong.east : Mahjong.Utils.prevHouse(Mahjong.Utils.prevHouse(engine.myHouse)) === 's' ? i18n.ts._mahjong.south : Mahjong.Utils.prevHouse(Mahjong.Utils.prevHouse(engine.myHouse)) === 'w' ? i18n.ts._mahjong.west : i18n.ts._mahjong.north }} - {{ engine.state.points[Mahjong.Utils.prevHouse(Mahjong.Utils.prevHouse(engine.myHouse))] }} + {{ Mahjong.Common.prevHouse(Mahjong.Common.prevHouse(engine.myHouse)) === 'e' ? i18n.ts._mahjong.east : Mahjong.Common.prevHouse(Mahjong.Common.prevHouse(engine.myHouse)) === 's' ? i18n.ts._mahjong.south : Mahjong.Common.prevHouse(Mahjong.Common.prevHouse(engine.myHouse)) === 'w' ? i18n.ts._mahjong.west : i18n.ts._mahjong.north }} + {{ engine.state.points[Mahjong.Common.prevHouse(Mahjong.Common.prevHouse(engine.myHouse))] }}
- {{ Mahjong.Utils.prevHouse(engine.myHouse) === 'e' ? i18n.ts._mahjong.east : Mahjong.Utils.prevHouse(engine.myHouse) === 's' ? i18n.ts._mahjong.south : Mahjong.Utils.prevHouse(engine.myHouse) === 'w' ? i18n.ts._mahjong.west : i18n.ts._mahjong.north }} - {{ engine.state.points[Mahjong.Utils.prevHouse(engine.myHouse)] }} + {{ Mahjong.Common.prevHouse(engine.myHouse) === 'e' ? i18n.ts._mahjong.east : Mahjong.Common.prevHouse(engine.myHouse) === 's' ? i18n.ts._mahjong.south : Mahjong.Common.prevHouse(engine.myHouse) === 'w' ? i18n.ts._mahjong.west : i18n.ts._mahjong.north }} + {{ engine.state.points[Mahjong.Common.prevHouse(engine.myHouse)] }}
- {{ Mahjong.Utils.nextHouse(engine.myHouse) === 'e' ? i18n.ts._mahjong.east : Mahjong.Utils.nextHouse(engine.myHouse) === 's' ? i18n.ts._mahjong.south : Mahjong.Utils.nextHouse(engine.myHouse) === 'w' ? i18n.ts._mahjong.west : i18n.ts._mahjong.north }} - {{ engine.state.points[Mahjong.Utils.nextHouse(engine.myHouse)] }} + {{ Mahjong.Common.nextHouse(engine.myHouse) === 'e' ? i18n.ts._mahjong.east : Mahjong.Common.nextHouse(engine.myHouse) === 's' ? i18n.ts._mahjong.south : Mahjong.Common.nextHouse(engine.myHouse) === 'w' ? i18n.ts._mahjong.west : i18n.ts._mahjong.north }} + {{ engine.state.points[Mahjong.Common.nextHouse(engine.myHouse)] }}
@@ -32,23 +32,26 @@ SPDX-License-Identifier: AGPL-3.0-only {{ engine.state.points[engine.myHouse] }}
+
+
{{ engine.state.tilesCount }}
+
-
+
-
+
-
+
@@ -56,21 +59,21 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
-
+
-
+
@@ -86,7 +89,7 @@ SPDX-License-Identifier: AGPL-3.0-only
@@ -116,25 +119,25 @@ SPDX-License-Identifier: AGPL-3.0-only
- - - - - + + + + +
- - - - - + + + + +
- - - - - + + + + +
@@ -190,7 +193,7 @@ const isMyTurn = computed(() => { }); const canHora = computed(() => { - return Mahjong.Utils.getHoraSets(engine.value.myHandTiles).length > 0; + return Mahjong.Common.getHoraSets(engine.value.myHandTiles).length > 0; }); const selectableTiles = ref(null); @@ -201,7 +204,7 @@ const kanSerifHouses = reactive>({ e: fals const tsumoSerifHouses = reactive>({ e: false, s: false, w: false, n: false }); /* -console.log(Mahjong.Utils.getTilesForRiichi([ +console.log(Mahjong.Common.getTilesForRiichi([ 'm1', 'm2', 'm2', @@ -219,7 +222,7 @@ console.log(Mahjong.Utils.getTilesForRiichi([ ])); */ /* -console.log(Mahjong.Utils.getHoraSets([ +console.log(Mahjong.Common.getHoraSets([ 'm3', 'm3', 'm4', @@ -298,8 +301,8 @@ function riichi() { if (!isMyTurn.value) return; riichiSelect = true; - selectableTiles.value = Mahjong.Utils.getTilesForRiichi(engine.value.myHandTiles); - console.log(Mahjong.Utils.getTilesForRiichi(engine.value.myHandTiles)); + selectableTiles.value = Mahjong.Common.getTilesForRiichi(engine.value.myHandTiles); + console.log(Mahjong.Common.getTilesForRiichi(engine.value.myHandTiles)); } function kakan() { @@ -403,10 +406,10 @@ function onStreamDahaiAndTsumo(log) { triggerRef(engine); window.setTimeout(() => { - engine.value.commit_tsumo(Mahjong.Utils.nextHouse(log.dahaiHouse), log.tsumoTile); + engine.value.commit_tsumo(Mahjong.Common.nextHouse(log.dahaiHouse), log.tsumoTile); triggerRef(engine); - if (Mahjong.Utils.nextHouse(log.dahaiHouse) === engine.value.myHouse) { + if (Mahjong.Common.nextHouse(log.dahaiHouse) === engine.value.myHouse) { iTsumoed.value = true; } }, 100); diff --git a/packages/misskey-mahjong/src/common.ts b/packages/misskey-mahjong/src/common.ts index 52de43f39b..27c2276ad4 100644 --- a/packages/misskey-mahjong/src/common.ts +++ b/packages/misskey-mahjong/src/common.ts @@ -360,3 +360,249 @@ export function calcTsumoHoraPointDeltas(house: House, fans: number): Record { + const aIndex = TILE_TYPES.indexOf(a); + const bIndex = TILE_TYPES.indexOf(b); + return aIndex - bIndex; + }); + return tiles; +} + +export function nextHouse(house: House): House { + switch (house) { + case 'e': return 's'; + case 's': return 'w'; + case 'w': return 'n'; + case 'n': return 'e'; + default: throw new Error(`unrecognized house: ${house}`); + } +} + +export function prevHouse(house: House): House { + switch (house) { + case 'e': return 'n'; + case 's': return 'e'; + case 'w': return 's'; + case 'n': return 'w'; + default: throw new Error(`unrecognized house: ${house}`); + } +} + +type HoraSet = { + head: Tile; + mentsus: [Tile, Tile, Tile][]; +}; + +export const SHUNTU_PATTERNS: [Tile, Tile, Tile][] = [ + ['m1', 'm2', 'm3'], + ['m2', 'm3', 'm4'], + ['m3', 'm4', 'm5'], + ['m4', 'm5', 'm6'], + ['m5', 'm6', 'm7'], + ['m6', 'm7', 'm8'], + ['m7', 'm8', 'm9'], + ['p1', 'p2', 'p3'], + ['p2', 'p3', 'p4'], + ['p3', 'p4', 'p5'], + ['p4', 'p5', 'p6'], + ['p5', 'p6', 'p7'], + ['p6', 'p7', 'p8'], + ['p7', 'p8', 'p9'], + ['s1', 's2', 's3'], + ['s2', 's3', 's4'], + ['s3', 's4', 's5'], + ['s4', 's5', 's6'], + ['s5', 's6', 's7'], + ['s6', 's7', 's8'], + ['s7', 's8', 's9'], +]; + +const SHUNTU_PATTERN_IDS = [ + 'm123', + 'm234', + 'm345', + 'm456', + 'm567', + 'm678', + 'm789', + 'p123', + 'p234', + 'p345', + 'p456', + 'p567', + 'p678', + 'p789', + 's123', + 's234', + 's345', + 's456', + 's567', + 's678', + 's789', +] as const; + +/** + * アガリ形パターン一覧を取得 + * @param handTiles ポン、チー、カンした牌を含まない手牌 + * @returns + */ +export function getHoraSets(handTiles: Tile[]): HoraSet[] { + const horaSets: HoraSet[] = []; + + const headSet: Tile[] = []; + const countMap = new Map(); + for (const tile of handTiles) { + const count = (countMap.get(tile) ?? 0) + 1; + countMap.set(tile, count); + if (count === 2) { + headSet.push(tile); + } + } + + for (const head of headSet) { + const tempHandTiles = [...handTiles]; + tempHandTiles.splice(tempHandTiles.indexOf(head), 1); + tempHandTiles.splice(tempHandTiles.indexOf(head), 1); + + const kotsuTileSet: Tile[] = []; // インデックスアクセスしたいため配列だが実態はSet + for (const [t, c] of countMap.entries()) { + if (t === head) continue; // 同じ牌種は4枚しかないので、頭と同じ牌種は刻子になりえない + if (c >= 3) { + kotsuTileSet.push(t); + } + } + + let kotsuPatterns: Tile[][]; + if (kotsuTileSet.length === 0) { + kotsuPatterns = [ + [], + ]; + } else if (kotsuTileSet.length === 1) { + kotsuPatterns = [ + [], + [kotsuTileSet[0]], + ]; + } else if (kotsuTileSet.length === 2) { + kotsuPatterns = [ + [], + [kotsuTileSet[0]], + [kotsuTileSet[1]], + [kotsuTileSet[0], kotsuTileSet[1]], + ]; + } else if (kotsuTileSet.length === 3) { + kotsuPatterns = [ + [], + [kotsuTileSet[0]], + [kotsuTileSet[1]], + [kotsuTileSet[2]], + [kotsuTileSet[0], kotsuTileSet[1]], + [kotsuTileSet[0], kotsuTileSet[2]], + [kotsuTileSet[1], kotsuTileSet[2]], + [kotsuTileSet[0], kotsuTileSet[1], kotsuTileSet[2]], + ]; + } else if (kotsuTileSet.length === 4) { + kotsuPatterns = [ + [], + [kotsuTileSet[0]], + [kotsuTileSet[1]], + [kotsuTileSet[2]], + [kotsuTileSet[3]], + [kotsuTileSet[0], kotsuTileSet[1]], + [kotsuTileSet[0], kotsuTileSet[2]], + [kotsuTileSet[0], kotsuTileSet[3]], + [kotsuTileSet[1], kotsuTileSet[2]], + [kotsuTileSet[1], kotsuTileSet[3]], + [kotsuTileSet[2], kotsuTileSet[3]], + [kotsuTileSet[0], kotsuTileSet[1], kotsuTileSet[2]], + [kotsuTileSet[0], kotsuTileSet[1], kotsuTileSet[3]], + [kotsuTileSet[0], kotsuTileSet[2], kotsuTileSet[3]], + [kotsuTileSet[1], kotsuTileSet[2], kotsuTileSet[3]], + [kotsuTileSet[0], kotsuTileSet[1], kotsuTileSet[2], kotsuTileSet[3]], + ]; + } else { + throw new Error('arienai'); + } + + for (const kotsuPattern of kotsuPatterns) { + const tempHandTilesWithoutKotsu = [...tempHandTiles]; + for (const kotsuTile of kotsuPattern) { + tempHandTilesWithoutKotsu.splice(tempHandTilesWithoutKotsu.indexOf(kotsuTile), 1); + tempHandTilesWithoutKotsu.splice(tempHandTilesWithoutKotsu.indexOf(kotsuTile), 1); + tempHandTilesWithoutKotsu.splice(tempHandTilesWithoutKotsu.indexOf(kotsuTile), 1); + } + + tempHandTilesWithoutKotsu.sort((a, b) => { + const aIndex = TILE_TYPES.indexOf(a); + const bIndex = TILE_TYPES.indexOf(b); + return aIndex - bIndex; + }); + + const tempHandTilesWithoutKotsuAndShuntsu: (Tile | null)[] = [...tempHandTilesWithoutKotsu]; + + const shuntsus: [Tile, Tile, Tile][] = []; + while (tempHandTilesWithoutKotsuAndShuntsu.length > 0) { + let isShuntu = false; + for (const shuntuPattern of SHUNTU_PATTERNS) { + if ( + tempHandTilesWithoutKotsuAndShuntsu[0] === shuntuPattern[0] && + tempHandTilesWithoutKotsuAndShuntsu.includes(shuntuPattern[1]) && + tempHandTilesWithoutKotsuAndShuntsu.includes(shuntuPattern[2]) + ) { + shuntsus.push(shuntuPattern); + tempHandTilesWithoutKotsuAndShuntsu.splice(0, 1); + tempHandTilesWithoutKotsuAndShuntsu.splice(tempHandTilesWithoutKotsuAndShuntsu.indexOf(shuntuPattern[1]), 1); + tempHandTilesWithoutKotsuAndShuntsu.splice(tempHandTilesWithoutKotsuAndShuntsu.indexOf(shuntuPattern[2]), 1); + isShuntu = true; + break; + } + } + + if (!isShuntu) tempHandTilesWithoutKotsuAndShuntsu.splice(0, 1); + } + + if (shuntsus.length * 3 === tempHandTilesWithoutKotsu.length) { // アガリ形 + horaSets.push({ + head, + mentsus: [...kotsuPattern.map(t => [t, t, t] as [Tile, Tile, Tile]), ...shuntsus], + }); + } + } + } + + return horaSets; +} + +/** + * アガリ牌リストを取得 + * @param handTiles ポン、チー、カンした牌を含まない手牌 + */ +export function getHoraTiles(handTiles: Tile[]): Tile[] { + return TILE_TYPES.filter(tile => { + const tempHandTiles = [...handTiles, tile]; + const horaSets = getHoraSets(tempHandTiles); + return horaSets.length > 0; + }); +} + +// TODO: 国士無双判定関数 + +// TODO: 七対子判定関数 + +export function getTilesForRiichi(handTiles: Tile[]): Tile[] { + return handTiles.filter(tile => { + const tempHandTiles = [...handTiles]; + tempHandTiles.splice(tempHandTiles.indexOf(tile), 1); + const horaTiles = getHoraTiles(tempHandTiles); + return horaTiles.length > 0; + }); +} + +export function nextTileForDora(tile: Tile): Tile { + return NEXT_TILE_FOR_DORA_MAP[tile]; +} diff --git a/packages/misskey-mahjong/src/engine.master.ts b/packages/misskey-mahjong/src/engine.master.ts index 8f331a1f3d..26707bfb65 100644 --- a/packages/misskey-mahjong/src/engine.master.ts +++ b/packages/misskey-mahjong/src/engine.master.ts @@ -6,7 +6,6 @@ import CRC32 from 'crc-32'; import { Tile, House, Huro, TILE_TYPES, YAKU_DEFINITIONS } from './common.js'; import * as Common from './common.js'; -import * as Utils from './utils.js'; import { PlayerState } from './engine.player.js'; export type MasterState = { @@ -116,7 +115,7 @@ export class MasterGameEngine { } public get doras(): Tile[] { - return this.state.kingTiles.slice(0, this.state.activatedDorasCount).map(t => Utils.nextTileForDora(t)); + return this.state.kingTiles.slice(0, this.state.activatedDorasCount).map(t => Common.nextTileForDora(t)); } public static createInitialState(): MasterState { @@ -199,7 +198,7 @@ export class MasterGameEngine { // TODO: ポンされるなどして自分の河にない場合の考慮 if (this.state.hoTiles[house].includes(tile)) return false; - const horaSets = Utils.getHoraSets(this.state.handTiles[house].concat(tile)); + const horaSets = Common.getHoraSets(this.state.handTiles[house].concat(tile)); if (horaSets.length === 0) return false; // 完成形じゃない // TODO @@ -213,8 +212,12 @@ export class MasterGameEngine { return this.state.handTiles[house].filter(t => t === tile).length === 2; } - private canCii(house: House, tile: Tile): boolean { - // TODO + private canCii(caller: House, callee: House, tile: Tile): boolean { + if (callee !== Common.prevHouse(caller)) return false; + const hand = this.state.handTiles[caller]; + return Common.SHUNTU_PATTERNS.some(pattern => + pattern.includes(tile) && + pattern.filter(t => hand.includes(t)).length >= 2); } public getHouse(index: 1 | 2 | 3 | 4): House { @@ -266,7 +269,7 @@ export class MasterGameEngine { if (riichi) { const tempHandTiles = [...this.state.handTiles[house]]; tempHandTiles.splice(tempHandTiles.indexOf(tile), 1); - if (Utils.getHoraTiles(tempHandTiles).length === 0) throw new Error('Not tenpai'); + if (Common.getHoraTiles(tempHandTiles).length === 0) throw new Error('Not tenpai'); if (this.state.points[house] < 1000) throw new Error('Not enough points'); } @@ -360,7 +363,7 @@ export class MasterGameEngine { }; } this.state.turn = null; - this.state.nextTurnAfterAsking = Utils.nextHouse(house); + this.state.nextTurnAfterAsking = Common.nextHouse(house); return { asking: true as const, canRonHouses: canRonHouses, @@ -370,7 +373,7 @@ export class MasterGameEngine { }; } - this.state.turn = Utils.nextHouse(house); + this.state.turn = Common.nextHouse(house); const tsumoTile = this.tsumo(); diff --git a/packages/misskey-mahjong/src/engine.player.ts b/packages/misskey-mahjong/src/engine.player.ts index 971fe384b2..0f6d86b874 100644 --- a/packages/misskey-mahjong/src/engine.player.ts +++ b/packages/misskey-mahjong/src/engine.player.ts @@ -6,7 +6,6 @@ import CRC32 from 'crc-32'; import { Tile, House, Huro, TILE_TYPES, YAKU_DEFINITIONS } from './common.js'; import * as Common from './common.js'; -import * as Utils from './utils.js'; export type PlayerState = { user1House: House; @@ -97,7 +96,7 @@ export class PlayerGameEngine { } public get doras(): Tile[] { - return this.state.doraIndicateTiles.map(t => Utils.nextTileForDora(t)); + return this.state.doraIndicateTiles.map(t => Common.nextTileForDora(t)); } public commit_tsumo(house: House, tile: Tile) { @@ -131,7 +130,7 @@ export class PlayerGameEngine { if (house === this.myHouse) { } else { - const canRon = Utils.getHoraSets(this.myHandTiles.concat(tile)).length > 0; + const canRon = Common.getHoraSets(this.myHandTiles.concat(tile)).length > 0; const canPon = this.myHandTiles.filter(t => t === tile).length === 2; // TODO: canCii @@ -243,7 +242,7 @@ export class PlayerGameEngine { if (this.state.riichis[this.myHouse]) return false; if (this.state.points[this.myHouse] < 1000) return false; if (!this.isMenzen) return false; - if (Utils.getTilesForRiichi(this.myHandTiles).length === 0) return false; + if (Common.getTilesForRiichi(this.myHandTiles).length === 0) return false; return true; } } diff --git a/packages/misskey-mahjong/src/index.ts b/packages/misskey-mahjong/src/index.ts index 305aa56750..2bf313eccf 100644 --- a/packages/misskey-mahjong/src/index.ts +++ b/packages/misskey-mahjong/src/index.ts @@ -5,7 +5,6 @@ export * as Serializer from './serializer.js'; export * as Common from './common.js'; -export * as Utils from './utils.js'; export { MasterGameEngine } from './engine.master.js'; export type { MasterState } from './engine.master.js'; diff --git a/packages/misskey-mahjong/src/serializer.ts b/packages/misskey-mahjong/src/serializer.ts index 6bf1417d28..12b249880d 100644 --- a/packages/misskey-mahjong/src/serializer.ts +++ b/packages/misskey-mahjong/src/serializer.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Tile } from './engine.player.js'; +import { Tile } from './common.js'; export type Log = { time: number; diff --git a/packages/misskey-mahjong/src/utils.ts b/packages/misskey-mahjong/src/utils.ts deleted file mode 100644 index 5e356b06fd..0000000000 --- a/packages/misskey-mahjong/src/utils.ts +++ /dev/null @@ -1,248 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and other misskey contributors - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { House, NEXT_TILE_FOR_DORA_MAP, TILE_TYPES, Tile } from './common.js'; - -export function isTile(tile: string): tile is Tile { - return TILE_TYPES.includes(tile as Tile); -} - -export function sortTiles(tiles: Tile[]): Tile[] { - tiles.sort((a, b) => { - const aIndex = TILE_TYPES.indexOf(a); - const bIndex = TILE_TYPES.indexOf(b); - return aIndex - bIndex; - }); - return tiles; -} - -export function nextHouse(house: House): House { - switch (house) { - case 'e': return 's'; - case 's': return 'w'; - case 'w': return 'n'; - case 'n': return 'e'; - default: throw new Error(`unrecognized house: ${house}`); - } -} - -export function prevHouse(house: House): House { - switch (house) { - case 'e': return 'n'; - case 's': return 'e'; - case 'w': return 's'; - case 'n': return 'w'; - default: throw new Error(`unrecognized house: ${house}`); - } -} - -type HoraSet = { - head: Tile; - mentsus: [Tile, Tile, Tile][]; -}; - -export const SHUNTU_PATTERNS: [Tile, Tile, Tile][] = [ - ['m1', 'm2', 'm3'], - ['m2', 'm3', 'm4'], - ['m3', 'm4', 'm5'], - ['m4', 'm5', 'm6'], - ['m5', 'm6', 'm7'], - ['m6', 'm7', 'm8'], - ['m7', 'm8', 'm9'], - ['p1', 'p2', 'p3'], - ['p2', 'p3', 'p4'], - ['p3', 'p4', 'p5'], - ['p4', 'p5', 'p6'], - ['p5', 'p6', 'p7'], - ['p6', 'p7', 'p8'], - ['p7', 'p8', 'p9'], - ['s1', 's2', 's3'], - ['s2', 's3', 's4'], - ['s3', 's4', 's5'], - ['s4', 's5', 's6'], - ['s5', 's6', 's7'], - ['s6', 's7', 's8'], - ['s7', 's8', 's9'], -]; - -const SHUNTU_PATTERN_IDS = [ - 'm123', - 'm234', - 'm345', - 'm456', - 'm567', - 'm678', - 'm789', - 'p123', - 'p234', - 'p345', - 'p456', - 'p567', - 'p678', - 'p789', - 's123', - 's234', - 's345', - 's456', - 's567', - 's678', - 's789', -] as const; - -/** - * アガリ形パターン一覧を取得 - * @param handTiles ポン、チー、カンした牌を含まない手牌 - * @returns - */ -export function getHoraSets(handTiles: Tile[]): HoraSet[] { - const horaSets: HoraSet[] = []; - - const headSet: Tile[] = []; - const countMap = new Map(); - for (const tile of handTiles) { - const count = (countMap.get(tile) ?? 0) + 1; - countMap.set(tile, count); - if (count === 2) { - headSet.push(tile); - } - } - - for (const head of headSet) { - const tempHandTiles = [...handTiles]; - tempHandTiles.splice(tempHandTiles.indexOf(head), 1); - tempHandTiles.splice(tempHandTiles.indexOf(head), 1); - - const kotsuTileSet: Tile[] = []; // インデックスアクセスしたいため配列だが実態はSet - for (const [t, c] of countMap.entries()) { - if (t === head) continue; // 同じ牌種は4枚しかないので、頭と同じ牌種は刻子になりえない - if (c >= 3) { - kotsuTileSet.push(t); - } - } - - let kotsuPatterns: Tile[][]; - if (kotsuTileSet.length === 0) { - kotsuPatterns = [ - [], - ]; - } else if (kotsuTileSet.length === 1) { - kotsuPatterns = [ - [], - [kotsuTileSet[0]], - ]; - } else if (kotsuTileSet.length === 2) { - kotsuPatterns = [ - [], - [kotsuTileSet[0]], - [kotsuTileSet[1]], - [kotsuTileSet[0], kotsuTileSet[1]], - ]; - } else if (kotsuTileSet.length === 3) { - kotsuPatterns = [ - [], - [kotsuTileSet[0]], - [kotsuTileSet[1]], - [kotsuTileSet[2]], - [kotsuTileSet[0], kotsuTileSet[1]], - [kotsuTileSet[0], kotsuTileSet[2]], - [kotsuTileSet[1], kotsuTileSet[2]], - [kotsuTileSet[0], kotsuTileSet[1], kotsuTileSet[2]], - ]; - } else if (kotsuTileSet.length === 4) { - kotsuPatterns = [ - [], - [kotsuTileSet[0]], - [kotsuTileSet[1]], - [kotsuTileSet[2]], - [kotsuTileSet[3]], - [kotsuTileSet[0], kotsuTileSet[1]], - [kotsuTileSet[0], kotsuTileSet[2]], - [kotsuTileSet[0], kotsuTileSet[3]], - [kotsuTileSet[1], kotsuTileSet[2]], - [kotsuTileSet[1], kotsuTileSet[3]], - [kotsuTileSet[2], kotsuTileSet[3]], - [kotsuTileSet[0], kotsuTileSet[1], kotsuTileSet[2]], - [kotsuTileSet[0], kotsuTileSet[1], kotsuTileSet[3]], - [kotsuTileSet[0], kotsuTileSet[2], kotsuTileSet[3]], - [kotsuTileSet[1], kotsuTileSet[2], kotsuTileSet[3]], - [kotsuTileSet[0], kotsuTileSet[1], kotsuTileSet[2], kotsuTileSet[3]], - ]; - } else { - throw new Error('arienai'); - } - - for (const kotsuPattern of kotsuPatterns) { - const tempHandTilesWithoutKotsu = [...tempHandTiles]; - for (const kotsuTile of kotsuPattern) { - tempHandTilesWithoutKotsu.splice(tempHandTilesWithoutKotsu.indexOf(kotsuTile), 1); - tempHandTilesWithoutKotsu.splice(tempHandTilesWithoutKotsu.indexOf(kotsuTile), 1); - tempHandTilesWithoutKotsu.splice(tempHandTilesWithoutKotsu.indexOf(kotsuTile), 1); - } - - tempHandTilesWithoutKotsu.sort((a, b) => { - const aIndex = TILE_TYPES.indexOf(a); - const bIndex = TILE_TYPES.indexOf(b); - return aIndex - bIndex; - }); - - const tempHandTilesWithoutKotsuAndShuntsu: (Tile | null)[] = [...tempHandTilesWithoutKotsu]; - - const shuntsus: [Tile, Tile, Tile][] = []; - while (tempHandTilesWithoutKotsuAndShuntsu.length > 0) { - let isShuntu = false; - for (const shuntuPattern of SHUNTU_PATTERNS) { - if ( - tempHandTilesWithoutKotsuAndShuntsu[0] === shuntuPattern[0] && - tempHandTilesWithoutKotsuAndShuntsu.includes(shuntuPattern[1]) && - tempHandTilesWithoutKotsuAndShuntsu.includes(shuntuPattern[2]) - ) { - shuntsus.push(shuntuPattern); - tempHandTilesWithoutKotsuAndShuntsu.splice(0, 1); - tempHandTilesWithoutKotsuAndShuntsu.splice(tempHandTilesWithoutKotsuAndShuntsu.indexOf(shuntuPattern[1]), 1); - tempHandTilesWithoutKotsuAndShuntsu.splice(tempHandTilesWithoutKotsuAndShuntsu.indexOf(shuntuPattern[2]), 1); - isShuntu = true; - break; - } - } - - if (!isShuntu) tempHandTilesWithoutKotsuAndShuntsu.splice(0, 1); - } - - if (shuntsus.length * 3 === tempHandTilesWithoutKotsu.length) { // アガリ形 - horaSets.push({ - head, - mentsus: [...kotsuPattern.map(t => [t, t, t] as [Tile, Tile, Tile]), ...shuntsus], - }); - } - } - } - - return horaSets; -} - -/** - * アガリ牌リストを取得 - * @param handTiles ポン、チー、カンした牌を含まない手牌 - */ -export function getHoraTiles(handTiles: Tile[]): Tile[] { - return TILE_TYPES.filter(tile => { - const tempHandTiles = [...handTiles, tile]; - const horaSets = getHoraSets(tempHandTiles); - return horaSets.length > 0; - }); -} - -export function getTilesForRiichi(handTiles: Tile[]): Tile[] { - return handTiles.filter(tile => { - const tempHandTiles = [...handTiles]; - tempHandTiles.splice(tempHandTiles.indexOf(tile), 1); - const horaTiles = getHoraTiles(tempHandTiles); - return horaTiles.length > 0; - }); -} - -export function nextTileForDora(tile: Tile): Tile { - return NEXT_TILE_FOR_DORA_MAP[tile]; -}