diff --git a/packages/backend/src/core/MahjongService.ts b/packages/backend/src/core/MahjongService.ts index ca8c2f685e..98803590e6 100644 --- a/packages/backend/src/core/MahjongService.ts +++ b/packages/backend/src/core/MahjongService.ts @@ -657,8 +657,7 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit { if (mj.riichis[house]) { // リーチ時はアガリ牌でない限りツモ切り - const horaSets = Mmj.getHoraSets(mj.handTileTypes[house]); - if (horaSets.length === 0) { + if (!Mmj.canHora(mj.handTileTypes[house])) { setTimeout(() => { this.dahai(room, mj, house, mj.handTiles[house].at(-1)); }, 500); diff --git a/packages/frontend/src/pages/mahjong/room.game.vue b/packages/frontend/src/pages/mahjong/room.game.vue index a0d50c3330..7de89858ad 100644 --- a/packages/frontend/src/pages/mahjong/room.game.vue +++ b/packages/frontend/src/pages/mahjong/room.game.vue @@ -292,7 +292,7 @@ const isMyTurn = computed(() => { }); const canHora = computed(() => { - return Mmj.getHoraSets(mj.value.myHandTileTypes).length > 0; + return Mmj.canHora(mj.value.myHandTileTypes).length; }); const selectableTiles = ref(null); diff --git a/packages/misskey-mahjong/src/common.ts b/packages/misskey-mahjong/src/common.ts index 63314e522d..2278a2e81f 100644 --- a/packages/misskey-mahjong/src/common.ts +++ b/packages/misskey-mahjong/src/common.ts @@ -237,6 +237,8 @@ export const PREV_TILE_FOR_SHUNTSU: Record = { chun: null, }; +const KOKUSHI_TILES: TileType[] = ['m1', 'm9', 'p1', 'p9', 's1', 's9', 'e', 's', 'w', 'n', 'haku', 'hatsu', 'chun']; + type EnvForCalcYaku = { house: House; @@ -471,7 +473,7 @@ export const YAKU_DEFINITIONS = [{ // TODO: 両面待ちかどうか - const horaSets = getHoraSets(state.handTiles.concat(state.tsumoTile ?? state.ronTile)); + const horaSets = analyze1head3mentsuSets(state.handTiles.concat(state.tsumoTile ?? state.ronTile)); return horaSets.some(horaSet => { // 風牌判定(役牌でなければOK) if (horaSet.head === state.seatWind) return false; @@ -489,7 +491,7 @@ export const YAKU_DEFINITIONS = [{ // 面前じゃないとダメ if (state.huros.some(huro => CALL_HURO_TYPES.includes(huro.type))) return false; - const horaSets = getHoraSets(state.handTiles.concat(state.tsumoTile ?? state.ronTile)); + const horaSets = analyze1head3mentsuSets(state.handTiles.concat(state.tsumoTile ?? state.ronTile)); return horaSets.some(horaSet => { // 同じ順子が2つあるか? return horaSet.mentsus.some((mentsu) => @@ -505,12 +507,26 @@ export const YAKU_DEFINITIONS = [{ if (state.huros.length > 0) { if (state.huros.some(huro => huro.type === 'cii')) return false; } - const horaSets = getHoraSets(state.handTiles.concat(state.tsumoTile ?? state.ronTile)); + const horaSets = analyze1head3mentsuSets(state.handTiles.concat(state.tsumoTile ?? state.ronTile)); return horaSets.some(horaSet => { // 全て刻子か? if (!horaSet.mentsus.every((mentsu) => mentsu[0] === mentsu[1])) return false; }); }, +}, { + name: 'chitoitsu', + fan: 2, + isYakuman: false, + calc: (state: EnvForCalcYaku) => { + return isChitoitsu(state.handTiles.concat(state.tsumoTile ?? state.ronTile)); + }, +}, { + name: 'kokushi', + fan: 13, + isYakuman: true, + calc: (state: EnvForCalcYaku) => { + return isKokushi(state.handTiles.concat(state.tsumoTile ?? state.ronTile)); + }, }]; export function fanToPoint(fan: number, isParent: boolean): number { @@ -709,7 +725,7 @@ function extractShuntsus(tiles: TileType[]): [TileType, TileType, TileType][] { * @param handTiles ポン、チー、カンした牌を含まない手牌 * @returns */ -export function getHoraSets(handTiles: TileType[]): HoraSet[] { +function analyze1head3mentsuSets(handTiles: TileType[]): HoraSet[] { const horaSets: HoraSet[] = []; const headSet: TileType[] = []; @@ -808,6 +824,14 @@ export function getHoraSets(handTiles: TileType[]): HoraSet[] { return horaSets; } +export function canHora(handTiles: TileType[]): boolean { + if (isKokushi(handTiles)) return true; + if (isChitoitsu(handTiles)) return true; + + const horaSets = analyze1head3mentsuSets(handTiles); + return horaSets.length > 0; +} + /** * アガリ牌リストを取得 * @param handTiles ポン、チー、カンした牌を含まない手牌 @@ -815,14 +839,23 @@ export function getHoraSets(handTiles: TileType[]): HoraSet[] { export function getHoraTiles(handTiles: TileType[]): TileType[] { return TILE_TYPES.filter(tile => { const tempHandTiles = [...handTiles, tile]; - const horaSets = getHoraSets(tempHandTiles); + const horaSets = analyze1head3mentsuSets(tempHandTiles); return horaSets.length > 0; }); } -// TODO: 国士無双判定関数 +function isKokushi(handTiles: TileType[]): boolean { + return KOKUSHI_TILES.every(t => handTiles.includes(t)); +} -// TODO: 七対子判定関数 +function isChitoitsu(handTiles: TileType[]): boolean { + const countMap = new Map(); + for (const tile of handTiles) { + const count = (countMap.get(tile) ?? 0) + 1; + countMap.set(tile, count); + } + return Array.from(countMap.values()).every(c => c === 2); +} export function getTilesForRiichi(handTiles: TileType[]): TileType[] { return handTiles.filter(tile => { diff --git a/packages/misskey-mahjong/src/engine.master.ts b/packages/misskey-mahjong/src/engine.master.ts index e611e3bc00..8821192867 100644 --- a/packages/misskey-mahjong/src/engine.master.ts +++ b/packages/misskey-mahjong/src/engine.master.ts @@ -108,8 +108,7 @@ class StateManager { // TODO: ポンされるなどして自分の河にない場合の考慮 if (this.hoTileTypes[house].includes($type(tid))) return false; - const horaSets = Common.getHoraSets(this.handTileTypes[house].concat($type(tid))); - if (horaSets.length === 0) return false; // 完成形じゃない + if (!Common.canHora(this.handTileTypes[house].concat($type(tid)))) return false; // 完成形じゃない // TODO //const yakus = YAKU_DEFINITIONS.filter(yaku => yaku.calc(this.state, { tsumoTile: null, ronTile: tile })); diff --git a/packages/misskey-mahjong/src/engine.player.ts b/packages/misskey-mahjong/src/engine.player.ts index 3e4fbdb894..d1a9094133 100644 --- a/packages/misskey-mahjong/src/engine.player.ts +++ b/packages/misskey-mahjong/src/engine.player.ts @@ -218,7 +218,7 @@ export class PlayerGameEngine { if (house === this.myHouse) { } else { - const canRon = Common.getHoraSets(this.myHandTiles.concat(tid).map(id => $type(id))).length > 0; + const canRon = Common.canHora(this.myHandTiles.concat(tid).map(id => $type(id))); const canPon = !this.isMeRiichi && this.myHandTileTypes.filter(t => t === $type(tid)).length === 2; const canKan = !this.isMeRiichi && this.myHandTileTypes.filter(t => t === $type(tid)).length === 3; const canCii = !this.isMeRiichi && house === Common.prevHouse(this.myHouse) &&