diff --git a/packages/misskey-mahjong/build.js b/packages/misskey-mahjong/build.js index 4744dfaf7b..293557ed83 100644 --- a/packages/misskey-mahjong/build.js +++ b/packages/misskey-mahjong/build.js @@ -1,31 +1,31 @@ -import { build } from "esbuild"; -import { globSync } from "glob"; +import { build } from 'esbuild'; +import { globSync } from 'glob'; -const entryPoints = globSync("./src/**/**.{ts,tsx}"); +const entryPoints = globSync('./src/**/**.{ts,tsx}'); /** @type {import('esbuild').BuildOptions} */ const options = { - entryPoints, - minify: true, - outdir: "./built/esm", - target: "es2022", - platform: "browser", - format: "esm", + entryPoints, + minify: true, + outdir: './built/esm', + target: 'es2022', + platform: 'browser', + format: 'esm', }; -if (process.env.WATCH === "true") { - options.watch = { - onRebuild(error, result) { - if (error) { - console.error("watch build failed:", error); - } else { - console.log("watch build succeeded:", result); - } - }, - }; +if (process.env.WATCH === 'true') { + options.watch = { + onRebuild(error, result) { + if (error) { + console.error('watch build failed:', error); + } else { + console.log('watch build succeeded:', result); + } + }, + }; } build(options).catch((err) => { - process.stderr.write(err.stderr); - process.exit(1); + process.stderr.write(err.stderr); + process.exit(1); }); diff --git a/packages/misskey-mahjong/src/common.fu.ts b/packages/misskey-mahjong/src/common.fu.ts index 820529dce1..56e5dd61e6 100644 --- a/packages/misskey-mahjong/src/common.fu.ts +++ b/packages/misskey-mahjong/src/common.fu.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { FourMentsuOneJyantou, mentsuEquals, TILE_NUMBER_MAP, TileType } from "./common.js"; +import { FourMentsuOneJyantou, mentsuEquals, TILE_NUMBER_MAP, TileType } from './common.js'; export type Shape = 'fourMentsuOneJyantou' | 'chitoitsu' | 'kokushi'; @@ -24,7 +24,7 @@ export function calcWaitPatterns(fourMentsuOneJyantou: FourMentsuOneJyantou | nu const result: FourMentsuOneJyantouWithWait[] = []; - if (fourMentsuOneJyantou.head == agariTile) { + if (fourMentsuOneJyantou.head === agariTile) { result.push({ head: fourMentsuOneJyantou.head, mentsus: fourMentsuOneJyantou.mentsus, @@ -44,7 +44,7 @@ export function calcWaitPatterns(fourMentsuOneJyantou: FourMentsuOneJyantou | nu waitedFor: 'mentsu', agariTile, waitedTaatsu: mentsu.toSpliced(agariTileIndex, 1) as [TileType, TileType], - }) + }); checkedMentsus.push(mentsu); } @@ -55,9 +55,9 @@ export function isRyanmen(taatsu: [TileType, TileType]): boolean { const number1 = TILE_NUMBER_MAP[taatsu[0]]; const number2 = TILE_NUMBER_MAP[taatsu[1]]; if (number1 == null || number2 == null) return false; - return number1 != 1 && number2 != 9 && number1 + 1 == number2; + return number1 !== 1 && number2 !== 9 && number1 + 1 === number2; } export function isToitsu(taatsu: [TileType, TileType]): boolean { - return taatsu[0] == taatsu[1]; + return taatsu[0] === taatsu[1]; } diff --git a/packages/misskey-mahjong/src/common.ts b/packages/misskey-mahjong/src/common.ts index 265d6b513f..3146c26f70 100644 --- a/packages/misskey-mahjong/src/common.ts +++ b/packages/misskey-mahjong/src/common.ts @@ -54,7 +54,7 @@ export type TileId = number; // NOTE: 0 は"不明"(他プレイヤーの手牌など)を表すものとして予約されている export const TILE_ID_MAP = new Map([ - /* eslint-disable no-multi-spaces */ + /* eslint-disable @stylistic/no-multi-spaces */ [1, { t: 'm1' }], [2, { t: 'm1' }], [3, { t: 'm1' }], [4, { t: 'm1' }], [5, { t: 'm2' }], [6, { t: 'm2' }], [7, { t: 'm2' }], [8, { t: 'm2' }], [9, { t: 'm3' }], [10, { t: 'm3' }], [11, { t: 'm3' }], [12, { t: 'm3' }], @@ -89,7 +89,7 @@ export const TILE_ID_MAP = new Map([ [125, { t: 'haku' }], [126, { t: 'haku' }], [127, { t: 'haku' }], [128, { t: 'haku' }], [129, { t: 'hatsu' }], [130, { t: 'hatsu' }], [131, { t: 'hatsu' }], [132, { t: 'hatsu' }], [133, { t: 'chun' }], [134, { t: 'chun' }], [135, { t: 'chun' }], [136, { t: 'chun' }], - /* eslint-enable no-multi-spaces */ + /* eslint-enable @stylistic/no-multi-spaces */ ]); export function findTileByIdOrFail(tid: TileId): TileInstance { @@ -130,7 +130,7 @@ export type PointFactor = { } | { isYakuman: true; value: number; -} +}; export const CALL_HURO_TYPES = ['pon', 'cii', 'minkan'] as const; @@ -386,7 +386,7 @@ export function calcTsumoHoraPointDeltas(house: House, fansOrFactor: number | Po n: 0, }; - const point = typeof fansOrFactor == 'number' ? fanToPoint(fansOrFactor, isParent) : calcPoint(fansOrFactor, isParent); + const point = typeof fansOrFactor === 'number' ? fanToPoint(fansOrFactor, isParent) : calcPoint(fansOrFactor, isParent); deltas[house] = point; if (isParent) { const childPoint = Math.ceil(point / 3); @@ -464,7 +464,7 @@ export function isKotsu(tiles: [TileType, TileType, TileType]): boolean { } export function mentsuEquals(tiles1: [TileType, TileType, TileType], tiles2: [TileType, TileType, TileType]): boolean { - return tiles1[0] == tiles2[0] && tiles1[1] == tiles2[1] && tiles1[2] == tiles2[2]; + return tiles1[0] === tiles2[0] && tiles1[1] === tiles2[1] && tiles1[2] === tiles2[2]; } export const SHUNTU_PATTERNS: [TileType, TileType, TileType][] = [ diff --git a/packages/misskey-mahjong/src/common.yaku.ts b/packages/misskey-mahjong/src/common.yaku.ts index 675650a58c..dd551b0bea 100644 --- a/packages/misskey-mahjong/src/common.yaku.ts +++ b/packages/misskey-mahjong/src/common.yaku.ts @@ -68,7 +68,7 @@ export const YAKUMAN_NAMES = [ 'chiho', ] as const; -type NormalYakuName = typeof NORMAL_YAKU_NAMES[number] +type NormalYakuName = typeof NORMAL_YAKU_NAMES[number]; type YakumanName = typeof YAKUMAN_NAMES[number]; @@ -286,7 +286,7 @@ function countIndenticalMentsuPairs(mentsus: [TileType, TileType, TileType][]) { let result = 0; const singleMentsus: [TileType, TileType, TileType][] = []; loop: for (const mentsu of mentsus) { - for (let i = 0 ; i < singleMentsus.length ; i++) { + for (let i = 0; i < singleMentsus.length; i++) { if (mentsuEquals(mentsu, singleMentsus[i])) { result++; singleMentsus.splice(i, 1); @@ -302,665 +302,705 @@ function countIndenticalMentsuPairs(mentsus: [TileType, TileType, TileType][]) { * 暗刻の数を数える (三暗刻なら3、四暗刻なら4) */ function countAnkos(state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantouWithWait) { - let ankans = state.huros.filter(huro => huro.type == 'ankan').length; + const ankans = state.huros.filter(huro => huro.type === 'ankan').length; const handKotsus = fourMentsuOneJyantou.mentsus.filter(mentsu => isKotsu(mentsu)).length; // ロンによりできた刻子は暗刻ではない - if (state.ronTile != null && fourMentsuOneJyantou.waitedFor == 'mentsu' && isToitsu(fourMentsuOneJyantou.waitedTaatsu)) { + if (state.ronTile != null && fourMentsuOneJyantou.waitedFor === 'mentsu' && isToitsu(fourMentsuOneJyantou.waitedTaatsu)) { return ankans + handKotsus - 1; } return ankans + handKotsus; } -export const NORMAL_YAKU_DEFINITIONS: NormalYakuDefinition[] = [{ - name: 'tsumo', - fan: 1, - isYakuman: false, - calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { +export const NORMAL_YAKU_DEFINITIONS: NormalYakuDefinition[] = [ + { + name: 'tsumo', + fan: 1, + isYakuman: false, + calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { // 門前じゃないとダメ - if (state.huros.some(huro => includes(CALL_HURO_TYPES, huro.type))) return false; + if (state.huros.some(huro => includes(CALL_HURO_TYPES, huro.type))) return false; - return state.tsumoTile != null; + return state.tsumoTile != null; + }, }, -}, { - name: 'riichi', - fan: 1, - isYakuman: false, - calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { - return !state.doubleRiichi && (state.riichi ?? false); + { + name: 'riichi', + fan: 1, + isYakuman: false, + calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { + return !state.doubleRiichi && (state.riichi ?? false); + }, }, -}, { - name: 'double-riichi', - fan: 2, - isYakuman: false, - calc: (state: EnvForCalcYaku) => { - return state.doubleRiichi ?? false; - } -}, { - name: 'ippatsu', - fan: 1, - isYakuman: false, - calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { - return state.ippatsu ?? false; + { + name: 'double-riichi', + fan: 2, + isYakuman: false, + calc: (state: EnvForCalcYaku) => { + return state.doubleRiichi ?? false; + }, }, -}, { - name: 'rinshan', - fan: 1, - isYakuman: false, - calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { - return (state.tsumoTile != null && state.rinshan) ?? false; - } -}, { - name: 'haitei', - fan: 1, - isYakuman: false, - calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { - return (state.tsumoTile != null && state.haitei) ?? false; - } -}, { - name: 'hotei', - fan: 1, - isYakuman: false, - calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { - return (state.ronTile != null && state.hotei) ?? false; - } -}, -new Yakuhai('red', 'chun'), -new Yakuhai('white', 'haku'), -new Yakuhai('green', 'hatsu'), -new FieldWind('field-wind-e', 'e'), -new FieldWind('field-wind-s', 's'), -new FieldWind('field-wind-w', 'w'), -new FieldWind('field-wind-n', 'n'), -new SeatWind('seat-wind-e', 'e'), -new SeatWind('seat-wind-s', 's'), -new SeatWind('seat-wind-w', 'w'), -new SeatWind('seat-wind-n', 'n'), -{ - name: 'tanyao', - fan: 1, - isYakuman: false, - calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { - return ( - (!state.handTiles.some(t => includes(YAOCHU_TILES, t))) && + { + name: 'ippatsu', + fan: 1, + isYakuman: false, + calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { + return state.ippatsu ?? false; + }, + }, + { + name: 'rinshan', + fan: 1, + isYakuman: false, + calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { + return (state.tsumoTile != null && state.rinshan) ?? false; + }, + }, + { + name: 'haitei', + fan: 1, + isYakuman: false, + calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { + return (state.tsumoTile != null && state.haitei) ?? false; + }, + }, + { + name: 'hotei', + fan: 1, + isYakuman: false, + calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { + return (state.ronTile != null && state.hotei) ?? false; + }, + }, + new Yakuhai('red', 'chun'), + new Yakuhai('white', 'haku'), + new Yakuhai('green', 'hatsu'), + new FieldWind('field-wind-e', 'e'), + new FieldWind('field-wind-s', 's'), + new FieldWind('field-wind-w', 'w'), + new FieldWind('field-wind-n', 'n'), + new SeatWind('seat-wind-e', 'e'), + new SeatWind('seat-wind-s', 's'), + new SeatWind('seat-wind-w', 'w'), + new SeatWind('seat-wind-n', 'n'), + { + name: 'tanyao', + fan: 1, + isYakuman: false, + calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { + return ( + (!state.handTiles.some(t => includes(YAOCHU_TILES, t))) && (state.huros.filter(huro => huro.type === 'pon' ? includes(YAOCHU_TILES, huro.tile) : huro.type === 'ankan' ? includes(YAOCHU_TILES, huro.tile) : huro.type === 'minkan' ? includes(YAOCHU_TILES, huro.tile) : huro.type === 'cii' ? huro.tiles.some(t2 => includes(YAOCHU_TILES, t2)) : false).length === 0) - ); + ); + }, }, -}, { - name: 'pinfu', - fan: 1, - isYakuman: false, - calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantouWithWait | null) => { - if (fourMentsuOneJyantou == null) return false; + { + name: 'pinfu', + fan: 1, + isYakuman: false, + calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantouWithWait | null) => { + if (fourMentsuOneJyantou == null) return false; - // 面前じゃないとダメ - if (state.huros.some(huro => includes(CALL_HURO_TYPES, huro.type))) return false; - // 三元牌はダメ - if (state.handTiles.some(t => ['haku', 'hatsu', 'chun'].includes(t))) return false; + // 面前じゃないとダメ + if (state.huros.some(huro => includes(CALL_HURO_TYPES, huro.type))) return false; + // 三元牌はダメ + if (state.handTiles.some(t => ['haku', 'hatsu', 'chun'].includes(t))) return false; - // 両面待ちかどうか - if (!(fourMentsuOneJyantou != null && fourMentsuOneJyantou.waitedFor == 'mentsu' && isRyanmen(fourMentsuOneJyantou.waitedTaatsu))) return false; + // 両面待ちかどうか + if (!(fourMentsuOneJyantou != null && fourMentsuOneJyantou.waitedFor === 'mentsu' && isRyanmen(fourMentsuOneJyantou.waitedTaatsu))) return false; - // 風牌判定(役牌でなければOK) - if (fourMentsuOneJyantou.head === state.seatWind) return false; - if (fourMentsuOneJyantou.head === state.fieldWind) return false; + // 風牌判定(役牌でなければOK) + if (fourMentsuOneJyantou.head === state.seatWind) return false; + if (fourMentsuOneJyantou.head === state.fieldWind) return false; - // 全て順子か? - if (fourMentsuOneJyantou.mentsus.some((mentsu) => mentsu[0] === mentsu[1])) return false; + // 全て順子か? + if (fourMentsuOneJyantou.mentsus.some((mentsu) => mentsu[0] === mentsu[1])) return false; - return true; + return true; + }, }, -}, { - name: 'honitsu', - fan: 3, - isYakuman: false, - kuisagari: true, - calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { - const tiles = state.handTiles; - let manzuCount = tiles.filter(t => includes(MANZU_TILES, t)).length; - let pinzuCount = tiles.filter(t => includes(PINZU_TILES, t)).length; - let souzuCount = tiles.filter(t => includes(SOUZU_TILES, t)).length; - let charCount = tiles.filter(t => includes(CHAR_TILES, t)).length; + { + name: 'honitsu', + fan: 3, + isYakuman: false, + kuisagari: true, + calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { + const tiles = state.handTiles; + let manzuCount = tiles.filter(t => includes(MANZU_TILES, t)).length; + let pinzuCount = tiles.filter(t => includes(PINZU_TILES, t)).length; + let souzuCount = tiles.filter(t => includes(SOUZU_TILES, t)).length; + let charCount = tiles.filter(t => includes(CHAR_TILES, t)).length; - for (const huro of state.huros) { - const huroTiles = huro.type === 'cii' ? huro.tiles : huro.type === 'pon' ? [huro.tile, huro.tile, huro.tile] : [huro.tile, huro.tile, huro.tile, huro.tile]; - manzuCount += huroTiles.filter(t => includes(MANZU_TILES, t)).length; - pinzuCount += huroTiles.filter(t => includes(PINZU_TILES, t)).length; - souzuCount += huroTiles.filter(t => includes(SOUZU_TILES, t)).length; - charCount += huroTiles.filter(t => includes(CHAR_TILES, t)).length; - } + for (const huro of state.huros) { + const huroTiles = huro.type === 'cii' ? huro.tiles : huro.type === 'pon' ? [huro.tile, huro.tile, huro.tile] : [huro.tile, huro.tile, huro.tile, huro.tile]; + manzuCount += huroTiles.filter(t => includes(MANZU_TILES, t)).length; + pinzuCount += huroTiles.filter(t => includes(PINZU_TILES, t)).length; + souzuCount += huroTiles.filter(t => includes(SOUZU_TILES, t)).length; + charCount += huroTiles.filter(t => includes(CHAR_TILES, t)).length; + } - if (manzuCount > 0 && pinzuCount > 0) return false; - if (manzuCount > 0 && souzuCount > 0) return false; - if (pinzuCount > 0 && souzuCount > 0) return false; - if (charCount === 0) return false; + if (manzuCount > 0 && pinzuCount > 0) return false; + if (manzuCount > 0 && souzuCount > 0) return false; + if (pinzuCount > 0 && souzuCount > 0) return false; + if (charCount === 0) return false; - return true; + return true; + }, }, -}, { - name: 'chinitsu', - fan: 6, - isYakuman: false, - kuisagari: true, - calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { - const tiles = state.handTiles; - let manzuCount = tiles.filter(t => includes(MANZU_TILES, t)).length; - let pinzuCount = tiles.filter(t => includes(PINZU_TILES, t)).length; - let souzuCount = tiles.filter(t => includes(SOUZU_TILES, t)).length; - let charCount = tiles.filter(t => includes(CHAR_TILES, t)).length; + { + name: 'chinitsu', + fan: 6, + isYakuman: false, + kuisagari: true, + calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { + const tiles = state.handTiles; + let manzuCount = tiles.filter(t => includes(MANZU_TILES, t)).length; + let pinzuCount = tiles.filter(t => includes(PINZU_TILES, t)).length; + let souzuCount = tiles.filter(t => includes(SOUZU_TILES, t)).length; + let charCount = tiles.filter(t => includes(CHAR_TILES, t)).length; - for (const huro of state.huros) { - const huroTiles = huro.type === 'cii' ? huro.tiles : huro.type === 'pon' ? [huro.tile, huro.tile, huro.tile] : [huro.tile, huro.tile, huro.tile, huro.tile]; - manzuCount += huroTiles.filter(t => includes(MANZU_TILES, t)).length; - pinzuCount += huroTiles.filter(t => includes(PINZU_TILES, t)).length; - souzuCount += huroTiles.filter(t => includes(SOUZU_TILES, t)).length; - charCount += huroTiles.filter(t => includes(CHAR_TILES, t)).length; - } + for (const huro of state.huros) { + const huroTiles = huro.type === 'cii' ? huro.tiles : huro.type === 'pon' ? [huro.tile, huro.tile, huro.tile] : [huro.tile, huro.tile, huro.tile, huro.tile]; + manzuCount += huroTiles.filter(t => includes(MANZU_TILES, t)).length; + pinzuCount += huroTiles.filter(t => includes(PINZU_TILES, t)).length; + souzuCount += huroTiles.filter(t => includes(SOUZU_TILES, t)).length; + charCount += huroTiles.filter(t => includes(CHAR_TILES, t)).length; + } - if (charCount > 0) return false; - if (manzuCount > 0 && pinzuCount > 0) return false; - if (manzuCount > 0 && souzuCount > 0) return false; - if (pinzuCount > 0 && souzuCount > 0) return false; + if (charCount > 0) return false; + if (manzuCount > 0 && pinzuCount > 0) return false; + if (manzuCount > 0 && souzuCount > 0) return false; + if (pinzuCount > 0 && souzuCount > 0) return false; - return true; + return true; + }, }, -}, { - name: 'iipeko', - fan: 1, - isYakuman: false, - calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { - if (fourMentsuOneJyantou == null) return false; + { + name: 'iipeko', + fan: 1, + isYakuman: false, + calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { + if (fourMentsuOneJyantou == null) return false; - // 面前じゃないとダメ - if (state.huros.some(huro => includes(CALL_HURO_TYPES, huro.type))) return false; + // 面前じゃないとダメ + if (state.huros.some(huro => includes(CALL_HURO_TYPES, huro.type))) return false; - // 同じ順子が2つあるか? - return countIndenticalMentsuPairs(fourMentsuOneJyantou.mentsus) == 1; + // 同じ順子が2つあるか? + return countIndenticalMentsuPairs(fourMentsuOneJyantou.mentsus) === 1; + }, }, -}, { - name: 'ryampeko', - fan: 3, - isYakuman: false, - calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { - if (fourMentsuOneJyantou == null) return false; + { + name: 'ryampeko', + fan: 3, + isYakuman: false, + calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { + if (fourMentsuOneJyantou == null) return false; - // 面前じゃないとダメ - if (state.huros.some(huro => includes(CALL_HURO_TYPES, huro.type))) return false; + // 面前じゃないとダメ + if (state.huros.some(huro => includes(CALL_HURO_TYPES, huro.type))) return false; - // 2つの同じ順子が2組あるか? - return countIndenticalMentsuPairs(fourMentsuOneJyantou.mentsus) == 2; + // 2つの同じ順子が2組あるか? + return countIndenticalMentsuPairs(fourMentsuOneJyantou.mentsus) === 2; + }, }, -}, { - name: 'toitoi', - fan: 2, - isYakuman: false, - calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { - if (fourMentsuOneJyantou == null) return false; + { + name: 'toitoi', + fan: 2, + isYakuman: false, + calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { + if (fourMentsuOneJyantou == null) return false; - if (state.huros.length > 0) { - if (state.huros.some(huro => huro.type === 'cii')) return false; - } + if (state.huros.length > 0) { + if (state.huros.some(huro => huro.type === 'cii')) return false; + } - // 全て刻子か? - if (!fourMentsuOneJyantou.mentsus.every((mentsu) => mentsu[0] === mentsu[1])) return false; + // 全て刻子か? + if (!fourMentsuOneJyantou.mentsus.every((mentsu) => mentsu[0] === mentsu[1])) return false; - return true; + return true; + }, }, -}, { - name: 'sananko', - fan: 2, - isYakuman: false, - calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantouWithWait | null) => { - return fourMentsuOneJyantou != null && countAnkos(state, fourMentsuOneJyantou) == 3; + { + name: 'sananko', + fan: 2, + isYakuman: false, + calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantouWithWait | null) => { + return fourMentsuOneJyantou != null && countAnkos(state, fourMentsuOneJyantou) === 3; + }, }, -}, { - name: 'honroto', - fan: 2, - isYakuman: false, - calc: (state: EnvForCalcYaku) => { - return state.huros.every(huro => huro.type != 'cii' && includes(YAOCHU_TILES, huro.tile)) && + { + name: 'honroto', + fan: 2, + isYakuman: false, + calc: (state: EnvForCalcYaku) => { + return state.huros.every(huro => huro.type !== 'cii' && includes(YAOCHU_TILES, huro.tile)) && state.handTiles.every(tile => includes(YAOCHU_TILES, tile)); - } -}, { - name: 'sankantsu', - fan: 2, - isYakuman: false, - calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { - return fourMentsuOneJyantou != null && - state.huros.filter(huro => huro.type == 'ankan' || huro.type == 'minkan').length == 3; - } -}, { - name: 'sanshoku-dojun', - fan: 2, - isYakuman: false, - kuisagari: true, - calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { - if (fourMentsuOneJyantou == null) return false; + }, + }, + { + name: 'sankantsu', + fan: 2, + isYakuman: false, + calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { + return fourMentsuOneJyantou != null && + state.huros.filter(huro => huro.type === 'ankan' || huro.type === 'minkan').length === 3; + }, + }, + { + name: 'sanshoku-dojun', + fan: 2, + isYakuman: false, + kuisagari: true, + calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { + if (fourMentsuOneJyantou == null) return false; - const shuntsus = fourMentsuOneJyantou.mentsus.filter(tiles => isShuntu(tiles)); + const shuntsus = fourMentsuOneJyantou.mentsus.filter(tiles => isShuntu(tiles)); - for (const shuntsu of shuntsus) { - if (isManzu(shuntsu[0])) { - if (shuntsus.some(tiles => isPinzu(tiles[0]) && isSameNumberTile(tiles[0], shuntsu[0]) && isSameNumberTile(tiles[1], shuntsu[1]) && isSameNumberTile(tiles[2], shuntsu[2]))) { - if (shuntsus.some(tiles => isSouzu(tiles[0]) && isSameNumberTile(tiles[0], shuntsu[0]) && isSameNumberTile(tiles[1], shuntsu[1]) && isSameNumberTile(tiles[2], shuntsu[2]))) { - return true; - } - } - } else if (isPinzu(shuntsu[0])) { - if (shuntsus.some(tiles => isManzu(tiles[0]) && isSameNumberTile(tiles[0], shuntsu[0]) && isSameNumberTile(tiles[1], shuntsu[1]) && isSameNumberTile(tiles[2], shuntsu[2]))) { - if (shuntsus.some(tiles => isSouzu(tiles[0]) && isSameNumberTile(tiles[0], shuntsu[0]) && isSameNumberTile(tiles[1], shuntsu[1]) && isSameNumberTile(tiles[2], shuntsu[2]))) { - return true; - } - } - } else if (isSouzu(shuntsu[0])) { - if (shuntsus.some(tiles => isManzu(tiles[0]) && isSameNumberTile(tiles[0], shuntsu[0]) && isSameNumberTile(tiles[1], shuntsu[1]) && isSameNumberTile(tiles[2], shuntsu[2]))) { + for (const shuntsu of shuntsus) { + if (isManzu(shuntsu[0])) { if (shuntsus.some(tiles => isPinzu(tiles[0]) && isSameNumberTile(tiles[0], shuntsu[0]) && isSameNumberTile(tiles[1], shuntsu[1]) && isSameNumberTile(tiles[2], shuntsu[2]))) { - return true; + if (shuntsus.some(tiles => isSouzu(tiles[0]) && isSameNumberTile(tiles[0], shuntsu[0]) && isSameNumberTile(tiles[1], shuntsu[1]) && isSameNumberTile(tiles[2], shuntsu[2]))) { + return true; + } + } + } else if (isPinzu(shuntsu[0])) { + if (shuntsus.some(tiles => isManzu(tiles[0]) && isSameNumberTile(tiles[0], shuntsu[0]) && isSameNumberTile(tiles[1], shuntsu[1]) && isSameNumberTile(tiles[2], shuntsu[2]))) { + if (shuntsus.some(tiles => isSouzu(tiles[0]) && isSameNumberTile(tiles[0], shuntsu[0]) && isSameNumberTile(tiles[1], shuntsu[1]) && isSameNumberTile(tiles[2], shuntsu[2]))) { + return true; + } + } + } else if (isSouzu(shuntsu[0])) { + if (shuntsus.some(tiles => isManzu(tiles[0]) && isSameNumberTile(tiles[0], shuntsu[0]) && isSameNumberTile(tiles[1], shuntsu[1]) && isSameNumberTile(tiles[2], shuntsu[2]))) { + if (shuntsus.some(tiles => isPinzu(tiles[0]) && isSameNumberTile(tiles[0], shuntsu[0]) && isSameNumberTile(tiles[1], shuntsu[1]) && isSameNumberTile(tiles[2], shuntsu[2]))) { + return true; + } } } } - } - return false; + return false; + }, }, -}, { - name: 'sanshoku-doko', - fan: 2, - isYakuman: false, - kuisagari: false, - calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { - if (fourMentsuOneJyantou == null) return false; + { + name: 'sanshoku-doko', + fan: 2, + isYakuman: false, + kuisagari: false, + calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { + if (fourMentsuOneJyantou == null) return false; - const kotsus = fourMentsuOneJyantou.mentsus.filter(tiles => isKotsu(tiles)); + const kotsus = fourMentsuOneJyantou.mentsus.filter(tiles => isKotsu(tiles)); - for (const kotsu of kotsus) { - if (isManzu(kotsu[0])) { - if (kotsus.some(tiles => isPinzu(tiles[0]) && isSameNumberTile(tiles[0], kotsu[0]))) { - if (kotsus.some(tiles => isSouzu(tiles[0]) && isSameNumberTile(tiles[0], kotsu[0]))) { - return true; - } - } - } else if (isPinzu(kotsu[0])) { - if (kotsus.some(tiles => isManzu(tiles[0]) && isSameNumberTile(tiles[0], kotsu[0]))) { - if (kotsus.some(tiles => isSouzu(tiles[0]) && isSameNumberTile(tiles[0], kotsu[0]))) { - return true; - } - } - } else if (isSouzu(kotsu[0])) { - if (kotsus.some(tiles => isManzu(tiles[0]) && isSameNumberTile(tiles[0], kotsu[0]))) { + for (const kotsu of kotsus) { + if (isManzu(kotsu[0])) { if (kotsus.some(tiles => isPinzu(tiles[0]) && isSameNumberTile(tiles[0], kotsu[0]))) { + if (kotsus.some(tiles => isSouzu(tiles[0]) && isSameNumberTile(tiles[0], kotsu[0]))) { + return true; + } + } + } else if (isPinzu(kotsu[0])) { + if (kotsus.some(tiles => isManzu(tiles[0]) && isSameNumberTile(tiles[0], kotsu[0]))) { + if (kotsus.some(tiles => isSouzu(tiles[0]) && isSameNumberTile(tiles[0], kotsu[0]))) { + return true; + } + } + } else if (isSouzu(kotsu[0])) { + if (kotsus.some(tiles => isManzu(tiles[0]) && isSameNumberTile(tiles[0], kotsu[0]))) { + if (kotsus.some(tiles => isPinzu(tiles[0]) && isSameNumberTile(tiles[0], kotsu[0]))) { + return true; + } + } + } + } + + return false; + }, + }, + { + name: 'ittsu', + fan: 2, + isYakuman: false, + kuisagari: true, + calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { + if (fourMentsuOneJyantou == null) return false; + + const shuntsus = fourMentsuOneJyantou.mentsus.filter(tiles => isShuntu(tiles)); + shuntsus.push(...state.huros.filter((huro): huro is Cii => huro.type === 'cii').map(huro => huro.tiles)); + + if (shuntsus.some(tiles => tiles[0] === 'm1' && tiles[1] === 'm2' && tiles[2] === 'm3')) { + if (shuntsus.some(tiles => tiles[0] === 'm4' && tiles[1] === 'm5' && tiles[2] === 'm6')) { + if (shuntsus.some(tiles => tiles[0] === 'm7' && tiles[1] === 'm8' && tiles[2] === 'm9')) { + return true; + } + } + } + if (shuntsus.some(tiles => tiles[0] === 'p1' && tiles[1] === 'p2' && tiles[2] === 'p3')) { + if (shuntsus.some(tiles => tiles[0] === 'p4' && tiles[1] === 'p5' && tiles[2] === 'p6')) { + if (shuntsus.some(tiles => tiles[0] === 'p7' && tiles[1] === 'p8' && tiles[2] === 'p9')) { + return true; + } + } + } + if (shuntsus.some(tiles => tiles[0] === 's1' && tiles[1] === 's2' && tiles[2] === 's3')) { + if (shuntsus.some(tiles => tiles[0] === 's4' && tiles[1] === 's5' && tiles[2] === 's6')) { + if (shuntsus.some(tiles => tiles[0] === 's7' && tiles[1] === 's8' && tiles[2] === 's9')) { return true; } } } - } - return false; + return false; + }, }, -}, { - name: 'ittsu', - fan: 2, - isYakuman: false, - kuisagari: true, - calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { - if (fourMentsuOneJyantou == null) return false; + { + name: 'chanta', + fan: 2, + isYakuman: false, + kuisagari: true, + calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { + if (fourMentsuOneJyantou == null) return false; - const shuntsus = fourMentsuOneJyantou.mentsus.filter(tiles => isShuntu(tiles)); - shuntsus.push(...state.huros.filter((huro): huro is Cii => huro.type == 'cii').map(huro => huro.tiles)); + const { head, mentsus } = fourMentsuOneJyantou; + const { huros } = state; - if (shuntsus.some(tiles => tiles[0] === 'm1' && tiles[1] === 'm2' && tiles[2] === 'm3')) { - if (shuntsus.some(tiles => tiles[0] === 'm4' && tiles[1] === 'm5' && tiles[2] === 'm6')) { - if (shuntsus.some(tiles => tiles[0] === 'm7' && tiles[1] === 'm8' && tiles[2] === 'm9')) { - return true; - } - } - } - if (shuntsus.some(tiles => tiles[0] === 'p1' && tiles[1] === 'p2' && tiles[2] === 'p3')) { - if (shuntsus.some(tiles => tiles[0] === 'p4' && tiles[1] === 'p5' && tiles[2] === 'p6')) { - if (shuntsus.some(tiles => tiles[0] === 'p7' && tiles[1] === 'p8' && tiles[2] === 'p9')) { - return true; - } - } - } - if (shuntsus.some(tiles => tiles[0] === 's1' && tiles[1] === 's2' && tiles[2] === 's3')) { - if (shuntsus.some(tiles => tiles[0] === 's4' && tiles[1] === 's5' && tiles[2] === 's6')) { - if (shuntsus.some(tiles => tiles[0] === 's7' && tiles[1] === 's8' && tiles[2] === 's9')) { - return true; - } - } - } + // 雀頭は幺九牌じゃないとダメ + if (!includes(YAOCHU_TILES, head)) return false; - return false; - }, -}, { - name: 'chanta', - fan: 2, - isYakuman: false, - kuisagari: true, - calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { - if (fourMentsuOneJyantou == null) return false; + // 順子は1つ以上じゃないとダメ + if (!mentsus.some(mentsu => isShuntu(mentsu))) return false; - const { head, mentsus } = fourMentsuOneJyantou; - const { huros } = state; - - // 雀頭は幺九牌じゃないとダメ - if (!includes(YAOCHU_TILES, head)) return false; - - // 順子は1つ以上じゃないとダメ - if (!mentsus.some(mentsu => isShuntu(mentsu))) return false; - - // いずれかの雀頭か面子に字牌を含まないとダメ - if (!(includes(CHAR_TILES, head) || + // いずれかの雀頭か面子に字牌を含まないとダメ + if (!(includes(CHAR_TILES, head) || mentsus.some(mentsu => includes(CHAR_TILES, mentsu[0])) || - huros.some(huro => huro.type != 'cii' && includes(CHAR_TILES, huro.tile)))) return false; + huros.some(huro => huro.type !== 'cii' && includes(CHAR_TILES, huro.tile)))) return false; - // 全ての面子に幺九牌が含まれる - return (mentsus.every(mentsu => mentsu.some(tile => includes(YAOCHU_TILES, tile))) && - huros.every(huro => huro.type == 'cii' ? + // 全ての面子に幺九牌が含まれる + return (mentsus.every(mentsu => mentsu.some(tile => includes(YAOCHU_TILES, tile))) && + huros.every(huro => huro.type === 'cii' ? huro.tiles.some(tile => includes(YAOCHU_TILES, tile)) : includes(YAOCHU_TILES, huro.tile))); + }, }, -}, { - name: 'junchan', - fan: 3, - isYakuman: false, - kuisagari: true, - calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { - if (fourMentsuOneJyantou == null) return false; + { + name: 'junchan', + fan: 3, + isYakuman: false, + kuisagari: true, + calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { + if (fourMentsuOneJyantou == null) return false; - const { head, mentsus } = fourMentsuOneJyantou; - const { huros } = state; + const { head, mentsus } = fourMentsuOneJyantou; + const { huros } = state; - // 雀頭は老頭牌じゃないとダメ - if (!includes(TERMINAL_TILES, head)) return false; + // 雀頭は老頭牌じゃないとダメ + if (!includes(TERMINAL_TILES, head)) return false; - // 順子は1つ以上じゃないとダメ - if (!mentsus.some(mentsu => isShuntu(mentsu))) return false; + // 順子は1つ以上じゃないとダメ + if (!mentsus.some(mentsu => isShuntu(mentsu))) return false; - // 全ての面子に老頭牌が含まれる - return (mentsus.every(mentsu => mentsu.some(tile => includes(TERMINAL_TILES, tile))) && - huros.every(huro => huro.type == 'cii' ? + // 全ての面子に老頭牌が含まれる + return (mentsus.every(mentsu => mentsu.some(tile => includes(TERMINAL_TILES, tile))) && + huros.every(huro => huro.type === 'cii' ? huro.tiles.some(tile => includes(TERMINAL_TILES, tile)) : includes(TERMINAL_TILES, huro.tile))); + }, }, -}, { - name: 'chitoitsu', - fan: 2, - isYakuman: false, - calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { - if (fourMentsuOneJyantou != null) return false; - if (state.huros.length > 0) return false; - const countMap = new Map(); - for (const tile of state.handTiles) { - const count = (countMap.get(tile) ?? 0) + 1; - countMap.set(tile, count); - } - return Array.from(countMap.values()).every(c => c === 2); + { + name: 'chitoitsu', + fan: 2, + isYakuman: false, + calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { + if (fourMentsuOneJyantou != null) return false; + if (state.huros.length > 0) return false; + const countMap = new Map(); + for (const tile of state.handTiles) { + const count = (countMap.get(tile) ?? 0) + 1; + countMap.set(tile, count); + } + return Array.from(countMap.values()).every(c => c === 2); + }, }, -}, { - name: 'shosangen', - fan: 2, - isYakuman: false, - calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { - if (fourMentsuOneJyantou == null) return false; + { + name: 'shosangen', + fan: 2, + isYakuman: false, + calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { + if (fourMentsuOneJyantou == null) return false; - const kotsuTiles = fourMentsuOneJyantou.mentsus.filter(tiles => isKotsu(tiles)).map(tiles => tiles[0]); + const kotsuTiles = fourMentsuOneJyantou.mentsus.filter(tiles => isKotsu(tiles)).map(tiles => tiles[0]); - for (const huro of state.huros) { - if (huro.type === 'cii') { + for (const huro of state.huros) { + if (huro.type === 'cii') { // nop - } else if (huro.type === 'pon') { - kotsuTiles.push(huro.tile); - } else { - kotsuTiles.push(huro.tile); + } else if (huro.type === 'pon') { + kotsuTiles.push(huro.tile); + } else { + kotsuTiles.push(huro.tile); + } } - } - switch (fourMentsuOneJyantou.head) { - case 'haku': return kotsuTiles.includes('hatsu') && kotsuTiles.includes('chun'); - case 'hatsu': return kotsuTiles.includes('haku') && kotsuTiles.includes('chun'); - case 'chun': return kotsuTiles.includes('haku') && kotsuTiles.includes('hatsu'); - } - - return false; - }, -}]; - -export const YAKUMAN_DEFINITIONS: YakumanDefinition[] = [{ - name: 'suanko-tanki', - isYakuman: true, - isDoubleYakuman: true, - calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantouWithWait | null) => { - return fourMentsuOneJyantou != null && fourMentsuOneJyantou.waitedFor == 'head' && countAnkos(state, fourMentsuOneJyantou) == 4; - } -}, { - name: 'suanko', - isYakuman: true, - upper: 'suanko-tanki', - calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantouWithWait | null) => { - return fourMentsuOneJyantou != null && countAnkos(state, fourMentsuOneJyantou) == 4; - } -}, { - name: 'daisangen', - isYakuman: true, - calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { - if (fourMentsuOneJyantou == null) return false; - - const kotsuTiles = fourMentsuOneJyantou.mentsus.filter(tiles => isKotsu(tiles)).map(tiles => tiles[0]); - - for (const huro of state.huros) { - if (huro.type === 'cii') { - // nop - } else if (huro.type === 'pon') { - kotsuTiles.push(huro.tile); - } else { - kotsuTiles.push(huro.tile); + switch (fourMentsuOneJyantou.head) { + case 'haku': return kotsuTiles.includes('hatsu') && kotsuTiles.includes('chun'); + case 'hatsu': return kotsuTiles.includes('haku') && kotsuTiles.includes('chun'); + case 'chun': return kotsuTiles.includes('haku') && kotsuTiles.includes('hatsu'); } - } - return kotsuTiles.includes('haku') && kotsuTiles.includes('hatsu') && kotsuTiles.includes('chun'); - }, -}, { - name: 'shosushi', - isYakuman: true, - calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { - if (fourMentsuOneJyantou == null) return false; - - let all = [...state.handTiles]; - for (const huro of state.huros) { - if (huro.type === 'cii') { - all = [...all, ...huro.tiles]; - } else if (huro.type === 'pon') { - all = [...all, huro.tile, huro.tile, huro.tile]; - } else { - all = [...all, huro.tile, huro.tile, huro.tile, huro.tile]; - } - } - - switch (fourMentsuOneJyantou.head) { - case 'e': return (countTiles(all, 's') === 3) && (countTiles(all, 'w') === 3) && (countTiles(all, 'n') === 3); - case 's': return (countTiles(all, 'e') === 3) && (countTiles(all, 'w') === 3) && (countTiles(all, 'n') === 3); - case 'w': return (countTiles(all, 'e') === 3) && (countTiles(all, 's') === 3) && (countTiles(all, 'n') === 3); - case 'n': return (countTiles(all, 'e') === 3) && (countTiles(all, 's') === 3) && (countTiles(all, 'w') === 3); - } - - return false; - }, -}, { - name: 'daisushi', - isYakuman: true, - calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { - if (fourMentsuOneJyantou == null) return false; - - const kotsuTiles = fourMentsuOneJyantou.mentsus.filter(tiles => isKotsu(tiles)).map(tiles => tiles[0]); - - for (const huro of state.huros) { - if (huro.type === 'cii') { - // nop - } else if (huro.type === 'pon') { - kotsuTiles.push(huro.tile); - } else { - kotsuTiles.push(huro.tile); - } - } - - return kotsuTiles.includes('e') && kotsuTiles.includes('s') && kotsuTiles.includes('w') && kotsuTiles.includes('n'); - }, -}, { - name: 'tsuiso', - isYakuman: true, - calc: (state: EnvForCalcYaku) => { - const tiles = state.handTiles; - let manzuCount = tiles.filter(t => includes(MANZU_TILES, t)).length; - let pinzuCount = tiles.filter(t => includes(PINZU_TILES, t)).length; - let souzuCount = tiles.filter(t => includes(SOUZU_TILES, t)).length; - - for (const huro of state.huros) { - const huroTiles = huro.type === 'cii' ? huro.tiles : huro.type === 'pon' ? [huro.tile, huro.tile, huro.tile] : [huro.tile, huro.tile, huro.tile, huro.tile]; - manzuCount += huroTiles.filter(t => includes(MANZU_TILES, t)).length; - pinzuCount += huroTiles.filter(t => includes(PINZU_TILES, t)).length; - souzuCount += huroTiles.filter(t => includes(SOUZU_TILES, t)).length; - } - - if (manzuCount > 0 || pinzuCount > 0 || souzuCount > 0) return false; - - return true; - }, -}, { - name: 'ryuiso', - isYakuman: true, - calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { - if (fourMentsuOneJyantou == null) return false; - - if (state.handTiles.some(t => !RYUISO_TILES.includes(t))) return false; - - for (const huro of state.huros) { - const huroTiles = huro.type === 'cii' ? huro.tiles : huro.type === 'pon' ? [huro.tile, huro.tile, huro.tile] : [huro.tile, huro.tile, huro.tile, huro.tile]; - if (huroTiles.some(t => !RYUISO_TILES.includes(t))) return false; - } - - return true; - }, -}, { - name: 'chinroto', - isYakuman: true, - calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { - return fourMentsuOneJyantou != null && - state.huros.every(huro => huro.type != 'cii' && includes(TERMINAL_TILES, huro.tile)) && - state.handTiles.every(tile => includes(TERMINAL_TILES, tile)); - } -}, { - name: 'sukantsu', - isYakuman: true, - calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { - return fourMentsuOneJyantou != null && - state.huros.filter(huro => huro.type == 'ankan' || huro.type == 'minkan').length == 4; - } -}, { - name: 'churen-9', - isYakuman: true, - isDoubleYakuman: true, - calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { - if (fourMentsuOneJyantou == null) return false; - - // 面前じゃないとダメ - if (state.huros.some(huro => includes(CALL_HURO_TYPES, huro.type))) return false; - - const agariTile = state.tsumoTile ?? state.ronTile; - if (agariTile == null) { return false; - } - const tempaiTiles = [...state.handTiles]; - tempaiTiles.splice(state.handTiles.indexOf(agariTile), 1); - - if (isManzu(agariTile)) { - if ((countTiles(tempaiTiles, 'm1') === 3) && (countTiles(tempaiTiles, 'm9') === 3)) { - if (tempaiTiles.includes('m2') && tempaiTiles.includes('m3') && tempaiTiles.includes('m4') && tempaiTiles.includes('m5') && tempaiTiles.includes('m6') && tempaiTiles.includes('m7') && tempaiTiles.includes('m8')) { - return true; - } - } - } else if (isPinzu(agariTile)) { - if ((countTiles(tempaiTiles, 'p1') === 3) && (countTiles(tempaiTiles, 'p9') === 3)) { - if (tempaiTiles.includes('p2') && tempaiTiles.includes('p3') && tempaiTiles.includes('p4') && tempaiTiles.includes('p5') && tempaiTiles.includes('p6') && tempaiTiles.includes('p7') && tempaiTiles.includes('p8')) { - return true; - } - } - } else if (isSouzu(agariTile)) { - if ((countTiles(tempaiTiles, 's1') === 3) && (countTiles(tempaiTiles, 's9') === 3)) { - if (tempaiTiles.includes('s2') && tempaiTiles.includes('s3') && tempaiTiles.includes('s4') && tempaiTiles.includes('s5') && tempaiTiles.includes('s6') && tempaiTiles.includes('s7') && tempaiTiles.includes('s8')) { - return true; - } - } - } - - return false; + }, }, -}, { - name: 'churen', - upper: 'churen-9', - isYakuman: true, - calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { - if (fourMentsuOneJyantou == null) return false; +]; - // 面前じゃないとダメ - if (state.huros.some(huro => includes(CALL_HURO_TYPES, huro.type))) return false; - - if (isManzu(state.handTiles[0])) { - if ((countTiles(state.handTiles, 'm1') === 3) && (countTiles(state.handTiles, 'm9') === 3)) { - if (state.handTiles.includes('m2') && state.handTiles.includes('m3') && state.handTiles.includes('m4') && state.handTiles.includes('m5') && state.handTiles.includes('m6') && state.handTiles.includes('m7') && state.handTiles.includes('m8')) { - return true; - } - } - } else if (isPinzu(state.handTiles[0])) { - if ((countTiles(state.handTiles, 'p1') === 3) && (countTiles(state.handTiles, 'p9') === 3)) { - if (state.handTiles.includes('p2') && state.handTiles.includes('p3') && state.handTiles.includes('p4') && state.handTiles.includes('p5') && state.handTiles.includes('p6') && state.handTiles.includes('p7') && state.handTiles.includes('p8')) { - return true; - } - } - } else if (isSouzu(state.handTiles[0])) { - if ((countTiles(state.handTiles, 's1') === 3) && (countTiles(state.handTiles, 's9') === 3)) { - if (state.handTiles.includes('s2') && state.handTiles.includes('s3') && state.handTiles.includes('s4') && state.handTiles.includes('s5') && state.handTiles.includes('s6') && state.handTiles.includes('s7') && state.handTiles.includes('s8')) { - return true; - } - } - } - - return false; +export const YAKUMAN_DEFINITIONS: YakumanDefinition[] = [ + { + name: 'suanko-tanki', + isYakuman: true, + isDoubleYakuman: true, + calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantouWithWait | null) => { + return fourMentsuOneJyantou != null && fourMentsuOneJyantou.waitedFor === 'head' && countAnkos(state, fourMentsuOneJyantou) === 4; + }, }, -}, { - name: 'kokushi-13', - isYakuman: true, - isDoubleYakuman: true, - calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { - const agariTile = state.tsumoTile ?? state.ronTile; - return KOKUSHI_TILES.every(t => state.handTiles.includes(t)) && countTiles(state.handTiles, agariTile) == 2; - } -}, { - name: 'kokushi', - isYakuman: true, - upper: 'kokushi-13', - calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { - return KOKUSHI_TILES.every(t => state.handTiles.includes(t)) && KOKUSHI_TILES.some(t => countTiles(state.handTiles, t) == 2); + { + name: 'suanko', + isYakuman: true, + upper: 'suanko-tanki', + calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantouWithWait | null) => { + return fourMentsuOneJyantou != null && countAnkos(state, fourMentsuOneJyantou) === 4; + }, }, -}, { - name: 'tenho', - isYakuman: true, - calc: (state: EnvForCalcYaku) => { - return (state.firstTurn ?? false) && state.tsumoTile != null && state.seatWind == 'e'; - } -}, { - name: 'chiho', - isYakuman: true, - calc: (state: EnvForCalcYaku) => { - return (state.firstTurn ?? false) && state.tsumoTile != null && state.seatWind != 'e'; - } -}]; + { + name: 'daisangen', + isYakuman: true, + calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { + if (fourMentsuOneJyantou == null) return false; + + const kotsuTiles = fourMentsuOneJyantou.mentsus.filter(tiles => isKotsu(tiles)).map(tiles => tiles[0]); + + for (const huro of state.huros) { + if (huro.type === 'cii') { + // nop + } else if (huro.type === 'pon') { + kotsuTiles.push(huro.tile); + } else { + kotsuTiles.push(huro.tile); + } + } + + return kotsuTiles.includes('haku') && kotsuTiles.includes('hatsu') && kotsuTiles.includes('chun'); + }, + }, + { + name: 'shosushi', + isYakuman: true, + calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { + if (fourMentsuOneJyantou == null) return false; + + let all = [...state.handTiles]; + for (const huro of state.huros) { + if (huro.type === 'cii') { + all = [...all, ...huro.tiles]; + } else if (huro.type === 'pon') { + all = [...all, huro.tile, huro.tile, huro.tile]; + } else { + all = [...all, huro.tile, huro.tile, huro.tile, huro.tile]; + } + } + + switch (fourMentsuOneJyantou.head) { + case 'e': return (countTiles(all, 's') === 3) && (countTiles(all, 'w') === 3) && (countTiles(all, 'n') === 3); + case 's': return (countTiles(all, 'e') === 3) && (countTiles(all, 'w') === 3) && (countTiles(all, 'n') === 3); + case 'w': return (countTiles(all, 'e') === 3) && (countTiles(all, 's') === 3) && (countTiles(all, 'n') === 3); + case 'n': return (countTiles(all, 'e') === 3) && (countTiles(all, 's') === 3) && (countTiles(all, 'w') === 3); + } + + return false; + }, + }, + { + name: 'daisushi', + isYakuman: true, + calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { + if (fourMentsuOneJyantou == null) return false; + + const kotsuTiles = fourMentsuOneJyantou.mentsus.filter(tiles => isKotsu(tiles)).map(tiles => tiles[0]); + + for (const huro of state.huros) { + if (huro.type === 'cii') { + // nop + } else if (huro.type === 'pon') { + kotsuTiles.push(huro.tile); + } else { + kotsuTiles.push(huro.tile); + } + } + + return kotsuTiles.includes('e') && kotsuTiles.includes('s') && kotsuTiles.includes('w') && kotsuTiles.includes('n'); + }, + }, + { + name: 'tsuiso', + isYakuman: true, + calc: (state: EnvForCalcYaku) => { + const tiles = state.handTiles; + let manzuCount = tiles.filter(t => includes(MANZU_TILES, t)).length; + let pinzuCount = tiles.filter(t => includes(PINZU_TILES, t)).length; + let souzuCount = tiles.filter(t => includes(SOUZU_TILES, t)).length; + + for (const huro of state.huros) { + const huroTiles = huro.type === 'cii' ? huro.tiles : huro.type === 'pon' ? [huro.tile, huro.tile, huro.tile] : [huro.tile, huro.tile, huro.tile, huro.tile]; + manzuCount += huroTiles.filter(t => includes(MANZU_TILES, t)).length; + pinzuCount += huroTiles.filter(t => includes(PINZU_TILES, t)).length; + souzuCount += huroTiles.filter(t => includes(SOUZU_TILES, t)).length; + } + + if (manzuCount > 0 || pinzuCount > 0 || souzuCount > 0) return false; + + return true; + }, + }, + { + name: 'ryuiso', + isYakuman: true, + calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { + if (fourMentsuOneJyantou == null) return false; + + if (state.handTiles.some(t => !RYUISO_TILES.includes(t))) return false; + + for (const huro of state.huros) { + const huroTiles = huro.type === 'cii' ? huro.tiles : huro.type === 'pon' ? [huro.tile, huro.tile, huro.tile] : [huro.tile, huro.tile, huro.tile, huro.tile]; + if (huroTiles.some(t => !RYUISO_TILES.includes(t))) return false; + } + + return true; + }, + }, + { + name: 'chinroto', + isYakuman: true, + calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { + return fourMentsuOneJyantou != null && + state.huros.every(huro => huro.type !== 'cii' && includes(TERMINAL_TILES, huro.tile)) && + state.handTiles.every(tile => includes(TERMINAL_TILES, tile)); + }, + }, + { + name: 'sukantsu', + isYakuman: true, + calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { + return fourMentsuOneJyantou != null && + state.huros.filter(huro => huro.type === 'ankan' || huro.type === 'minkan').length === 4; + }, + }, + { + name: 'churen-9', + isYakuman: true, + isDoubleYakuman: true, + calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { + if (fourMentsuOneJyantou == null) return false; + + // 面前じゃないとダメ + if (state.huros.some(huro => includes(CALL_HURO_TYPES, huro.type))) return false; + + const agariTile = state.tsumoTile ?? state.ronTile; + if (agariTile == null) { + return false; + } + const tempaiTiles = [...state.handTiles]; + tempaiTiles.splice(state.handTiles.indexOf(agariTile), 1); + + if (isManzu(agariTile)) { + if ((countTiles(tempaiTiles, 'm1') === 3) && (countTiles(tempaiTiles, 'm9') === 3)) { + if (tempaiTiles.includes('m2') && tempaiTiles.includes('m3') && tempaiTiles.includes('m4') && tempaiTiles.includes('m5') && tempaiTiles.includes('m6') && tempaiTiles.includes('m7') && tempaiTiles.includes('m8')) { + return true; + } + } + } else if (isPinzu(agariTile)) { + if ((countTiles(tempaiTiles, 'p1') === 3) && (countTiles(tempaiTiles, 'p9') === 3)) { + if (tempaiTiles.includes('p2') && tempaiTiles.includes('p3') && tempaiTiles.includes('p4') && tempaiTiles.includes('p5') && tempaiTiles.includes('p6') && tempaiTiles.includes('p7') && tempaiTiles.includes('p8')) { + return true; + } + } + } else if (isSouzu(agariTile)) { + if ((countTiles(tempaiTiles, 's1') === 3) && (countTiles(tempaiTiles, 's9') === 3)) { + if (tempaiTiles.includes('s2') && tempaiTiles.includes('s3') && tempaiTiles.includes('s4') && tempaiTiles.includes('s5') && tempaiTiles.includes('s6') && tempaiTiles.includes('s7') && tempaiTiles.includes('s8')) { + return true; + } + } + } + + return false; + }, + }, + { + name: 'churen', + upper: 'churen-9', + isYakuman: true, + calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { + if (fourMentsuOneJyantou == null) return false; + + // 面前じゃないとダメ + if (state.huros.some(huro => includes(CALL_HURO_TYPES, huro.type))) return false; + + if (isManzu(state.handTiles[0])) { + if ((countTiles(state.handTiles, 'm1') === 3) && (countTiles(state.handTiles, 'm9') === 3)) { + if (state.handTiles.includes('m2') && state.handTiles.includes('m3') && state.handTiles.includes('m4') && state.handTiles.includes('m5') && state.handTiles.includes('m6') && state.handTiles.includes('m7') && state.handTiles.includes('m8')) { + return true; + } + } + } else if (isPinzu(state.handTiles[0])) { + if ((countTiles(state.handTiles, 'p1') === 3) && (countTiles(state.handTiles, 'p9') === 3)) { + if (state.handTiles.includes('p2') && state.handTiles.includes('p3') && state.handTiles.includes('p4') && state.handTiles.includes('p5') && state.handTiles.includes('p6') && state.handTiles.includes('p7') && state.handTiles.includes('p8')) { + return true; + } + } + } else if (isSouzu(state.handTiles[0])) { + if ((countTiles(state.handTiles, 's1') === 3) && (countTiles(state.handTiles, 's9') === 3)) { + if (state.handTiles.includes('s2') && state.handTiles.includes('s3') && state.handTiles.includes('s4') && state.handTiles.includes('s5') && state.handTiles.includes('s6') && state.handTiles.includes('s7') && state.handTiles.includes('s8')) { + return true; + } + } + } + + return false; + }, + }, + { + name: 'kokushi-13', + isYakuman: true, + isDoubleYakuman: true, + calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { + const agariTile = state.tsumoTile ?? state.ronTile; + return KOKUSHI_TILES.every(t => state.handTiles.includes(t)) && countTiles(state.handTiles, agariTile) === 2; + }, + }, + { + name: 'kokushi', + isYakuman: true, + upper: 'kokushi-13', + calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { + return KOKUSHI_TILES.every(t => state.handTiles.includes(t)) && KOKUSHI_TILES.some(t => countTiles(state.handTiles, t) === 2); + }, + }, + { + name: 'tenho', + isYakuman: true, + calc: (state: EnvForCalcYaku) => { + return (state.firstTurn ?? false) && state.tsumoTile != null && state.seatWind === 'e'; + }, + }, + { + name: 'chiho', + isYakuman: true, + calc: (state: EnvForCalcYaku) => { + return (state.firstTurn ?? false) && state.tsumoTile != null && state.seatWind !== 'e'; + }, + }, +]; export function convertHuroForCalcYaku(huro: Huro): HuroForCalcYaku { switch (huro.type) { @@ -970,7 +1010,7 @@ export function convertHuroForCalcYaku(huro: Huro): HuroForCalcYaku { return { type: huro.type, tile: TILE_ID_MAP.get(huro.tiles[0])!.t, - } + }; case 'cii': return { type: 'cii', @@ -986,7 +1026,7 @@ const NORMAL_YAKU_DATA_MAP = new Map>( fan: yaku.fan, isYakuman: false, kuisagari: yaku.kuisagari ?? false, - }] as const) + }] as const), ); const YAKUMAN_DATA_MAP = new Map>( @@ -996,7 +1036,7 @@ const YAKUMAN_DATA_MAP = new Map>( fan: null, isYakuman: true, isDoubleYakuman: yaku.isDoubleYakuman ?? false, - }]) + }]), ); export function calcYakusWithDetail(state: EnvForCalcYaku): YakuSet { @@ -1009,7 +1049,7 @@ export function calcYakusWithDetail(state: EnvForCalcYaku): YakuSet { throw new TypeError('Agari tile not included in hand tiles'); } - if (state.handTiles.length + state.huros.length * 3 != 14) { + if (state.handTiles.length + state.huros.length * 3 !== 14) { throw new TypeError('Invalid tile count'); } @@ -1017,20 +1057,20 @@ export function calcYakusWithDetail(state: EnvForCalcYaku): YakuSet { if (oneHeadFourMentsuPatterns.length === 0) oneHeadFourMentsuPatterns.push(null); const waitPatterns = oneHeadFourMentsuPatterns.map( - fourMentsuOneJyantou => calcWaitPatterns(fourMentsuOneJyantou, agariTile) + fourMentsuOneJyantou => calcWaitPatterns(fourMentsuOneJyantou, agariTile), ).flat(); const yakumanPatterns = waitPatterns.map(fourMentsuOneJyantouWithWait => { - const matchedYakus: Required[] = []; - for (const yakuDef of YAKUMAN_DEFINITIONS) { - if (yakuDef.upper && matchedYakus.some(yaku => yaku.name === yakuDef.upper)) continue; - const matched = yakuDef.calc(state, fourMentsuOneJyantouWithWait); - if (matched) { - matchedYakus.push(YAKUMAN_DATA_MAP.get(yakuDef.name)!); - } + const matchedYakus: Required[] = []; + for (const yakuDef of YAKUMAN_DEFINITIONS) { + if (yakuDef.upper && matchedYakus.some(yaku => yaku.name === yakuDef.upper)) continue; + const matched = yakuDef.calc(state, fourMentsuOneJyantouWithWait); + if (matched) { + matchedYakus.push(YAKUMAN_DATA_MAP.get(yakuDef.name)!); } - return matchedYakus; - }).filter(yakus => yakus.length > 0); + } + return matchedYakus; + }).filter(yakus => yakus.length > 0); if (yakumanPatterns.length > 0) { return new YakumanSet(yakumanPatterns[0]); @@ -1038,13 +1078,13 @@ export function calcYakusWithDetail(state: EnvForCalcYaku): YakuSet { const yakuPatterns = waitPatterns.map( fourMentsuOneJyantouWithWait => NORMAL_YAKU_DEFINITIONS.filter( - yakuDef => yakuDef.calc(state, fourMentsuOneJyantouWithWait) - ).map(yakuDef => NORMAL_YAKU_DATA_MAP.get(yakuDef.name)!) + yakuDef => yakuDef.calc(state, fourMentsuOneJyantouWithWait), + ).map(yakuDef => NORMAL_YAKU_DATA_MAP.get(yakuDef.name)!), ).filter(yakus => yakus.length > 0); const isMenzen = state.huros.some(huro => includes(CALL_HURO_TYPES, huro.type)); - if (yakuPatterns.length == 0) { + if (yakuPatterns.length === 0) { return new NormalYakuSet(isMenzen, []); } diff --git a/packages/misskey-mahjong/src/engine.master.ts b/packages/misskey-mahjong/src/engine.master.ts index 10fcde24f4..e480b9ea50 100644 --- a/packages/misskey-mahjong/src/engine.master.ts +++ b/packages/misskey-mahjong/src/engine.master.ts @@ -613,7 +613,7 @@ export class MasterGameEngine { public commit_kakan(house: House, tid: TileId) { const tx = this.startTransaction(); - const pon = tx.$state.huros[house].find(h => h.type === 'pon' && $type(h.tiles[0]) === $type(tid)) as Huro & {type: 'pon'}; + const pon = tx.$state.huros[house].find(h => h.type === 'pon' && $type(h.tiles[0]) === $type(tid)) as Huro & { type: 'pon' }; if (pon == null) throw new Error('No such pon'); tx.$state.handTiles[house].splice(tx.$state.handTiles[house].indexOf(tid), 1); const tiles = [tid, ...pon.tiles] as const; @@ -686,7 +686,7 @@ export class MasterGameEngine { doubleRiichi: tx.$state.doubleRiichis[house], ippatsu: tx.$state.ippatsus[house], rinshan: tx.$state.rinshanFlags[house], - haitei: tx.$state.tiles.length == 0, + haitei: tx.$state.tiles.length === 0, }); const doraCount = Common.calcOwnedDoraCount(tx.handTileTypes[house], tx.$state.huros[house], tx.doras) + @@ -743,7 +743,7 @@ export class MasterGameEngine { riichi: tx.$state.riichis[house], doubleRiichi: tx.$state.doubleRiichis[house], ippatsu: tx.$state.ippatsus[house], - hotei: tx.$state.tiles.length == 0, + hotei: tx.$state.tiles.length === 0, }); const doraCount = Common.calcOwnedDoraCount(tx.handTileTypes[house], tx.$state.huros[house], tx.doras) + diff --git a/packages/misskey-mahjong/src/engine.player.ts b/packages/misskey-mahjong/src/engine.player.ts index 092b10ef24..7b13337acf 100644 --- a/packages/misskey-mahjong/src/engine.player.ts +++ b/packages/misskey-mahjong/src/engine.player.ts @@ -270,7 +270,7 @@ export class PlayerGameEngine { doubleRiichi: this.state.doubleRiichis[house], ippatsu: this.state.ippatsus[house], rinshan: this.state.rinshanFlags[house], - haitei: this.state.tilesCount == 0, + haitei: this.state.tilesCount === 0, }); const doraCount = Common.calcOwnedDoraCount(handTiles.map(id => $type(id)), this.state.huros[house], this.doras) + @@ -322,7 +322,7 @@ export class PlayerGameEngine { riichi: this.state.riichis[house], doubleRiichi: this.state.doubleRiichis[house], ippatsu: this.state.ippatsus[house], - hotei: this.state.tilesCount == 0, + hotei: this.state.tilesCount === 0, }); const doraCount = Common.calcOwnedDoraCount(handTiles[house].map(id => $type(id)), this.state.huros[house], this.doras) +