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];
-}