mirror of
https://github.com/misskey-dev/misskey
synced 2025-06-30 08:42:50 +02:00
1113 lines
34 KiB
TypeScript
1113 lines
34 KiB
TypeScript
/*
|
||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||
* SPDX-License-Identifier: AGPL-3.0-only
|
||
*/
|
||
|
||
import { CALL_HURO_TYPES, CHAR_TILES, FourMentsuOneJyantou, House, MANZU_TILES, PINZU_TILES, SOUZU_TILES, TileType, YAOCHU_TILES, TILE_TYPES, analyzeFourMentsuOneJyantou, isShuntu, isManzu, isPinzu, isSameNumberTile, isSouzu, isKotsu, includes, TERMINAL_TILES, mentsuEquals, Huro, TILE_ID_MAP } from './common.js';
|
||
import { calcWaitPatterns, isRyanmen, isToitsu, FourMentsuOneJyantouWithWait } from './common.fu.js';
|
||
|
||
const RYUISO_TILES: TileType[] = ['s2', 's3', 's4', 's6', 's8', 'hatsu'];
|
||
const KOKUSHI_TILES: TileType[] = ['m1', 'm9', 'p1', 'p9', 's1', 's9', 'e', 's', 'w', 'n', 'haku', 'hatsu', 'chun'];
|
||
|
||
export const NORMAL_YAKU_NAMES = [
|
||
'riichi',
|
||
'ippatsu',
|
||
'tsumo',
|
||
'tanyao',
|
||
'pinfu',
|
||
'iipeko',
|
||
'field-wind-e',
|
||
'field-wind-s',
|
||
'field-wind-w',
|
||
'field-wind-n',
|
||
'seat-wind-e',
|
||
'seat-wind-s',
|
||
'seat-wind-w',
|
||
'seat-wind-n',
|
||
'white',
|
||
'green',
|
||
'red',
|
||
'rinshan',
|
||
'chankan',
|
||
'haitei',
|
||
'hotei',
|
||
'sanshoku-dojun',
|
||
'sanshoku-doko',
|
||
'ittsu',
|
||
'chanta',
|
||
'chitoitsu',
|
||
'toitoi',
|
||
'sananko',
|
||
'honroto',
|
||
'sankantsu',
|
||
'shosangen',
|
||
'double-riichi',
|
||
'honitsu',
|
||
'junchan',
|
||
'ryampeko',
|
||
'chinitsu',
|
||
'dora',
|
||
'red-dora',
|
||
] as const;
|
||
|
||
export const YAKUMAN_NAMES = [
|
||
'kokushi',
|
||
'kokushi-13',
|
||
'suanko',
|
||
'suanko-tanki',
|
||
'daisangen',
|
||
'tsuiso',
|
||
'shosushi',
|
||
'daisushi',
|
||
'ryuiso',
|
||
'chinroto',
|
||
'sukantsu',
|
||
'churen',
|
||
'churen-9',
|
||
'tenho',
|
||
'chiho',
|
||
] as const;
|
||
|
||
type NormalYakuName = typeof NORMAL_YAKU_NAMES[number];
|
||
|
||
type YakumanName = typeof YAKUMAN_NAMES[number];
|
||
|
||
export type YakuName = NormalYakuName | YakumanName;
|
||
|
||
type Pon = {
|
||
type: 'pon';
|
||
tile: TileType;
|
||
};
|
||
|
||
type Cii = {
|
||
type: 'cii';
|
||
tiles: [TileType, TileType, TileType];
|
||
};
|
||
|
||
type Ankan = {
|
||
type: 'ankan';
|
||
tile: TileType;
|
||
};
|
||
|
||
type Minkan = {
|
||
type: 'minkan';
|
||
tile: TileType;
|
||
};
|
||
|
||
export type HuroForCalcYaku = Pon | Cii | Ankan | Minkan;
|
||
|
||
export type EnvForCalcYaku = {
|
||
/**
|
||
* 和了る人の手牌(副露牌は含まず、ツモ、ロン牌は含む)
|
||
*/
|
||
handTiles: TileType[];
|
||
|
||
/**
|
||
* 河
|
||
*/
|
||
hoTiles?: TileType[];
|
||
|
||
/**
|
||
* 副露
|
||
*/
|
||
huros: HuroForCalcYaku[];
|
||
|
||
/**
|
||
* 場風
|
||
*/
|
||
fieldWind?: House;
|
||
|
||
/**
|
||
* 自風
|
||
*/
|
||
seatWind?: House;
|
||
|
||
/**
|
||
* 局が始まってから誰の副露もない一巡目かどうか
|
||
*/
|
||
firstTurn?: boolean;
|
||
|
||
/**
|
||
* リーチしたかどうか
|
||
*/
|
||
riichi?: boolean;
|
||
|
||
/**
|
||
* 誰の副露もない一巡目でリーチしたかどうか
|
||
*/
|
||
doubleRiichi?: boolean;
|
||
|
||
/**
|
||
* リーチしてから誰の副露もない一巡目以内かどうか
|
||
*/
|
||
ippatsu?: boolean;
|
||
} & ({
|
||
tsumoTile: TileType;
|
||
ronTile?: null;
|
||
|
||
/**
|
||
* 嶺上牌のツモか
|
||
*/
|
||
rinshan?: boolean;
|
||
|
||
/**
|
||
* 海底牌か
|
||
*/
|
||
haitei?: boolean;
|
||
} | {
|
||
tsumoTile?: null;
|
||
ronTile: TileType;
|
||
|
||
/**
|
||
* 河底牌か
|
||
*/
|
||
hotei?: boolean;
|
||
});
|
||
|
||
interface YakuDataBase {
|
||
name: YakuName;
|
||
upper?: YakuName | null;
|
||
fan?: number | null;
|
||
isYakuman?: boolean;
|
||
}
|
||
|
||
interface NormalYakuData extends YakuDataBase {
|
||
name: NormalYakuName;
|
||
fan: number;
|
||
isYakuman?: false;
|
||
kuisagari?: boolean;
|
||
}
|
||
|
||
interface YakumanData extends YakuDataBase {
|
||
name: YakumanName;
|
||
isYakuman: true;
|
||
isDoubleYakuman?: boolean;
|
||
}
|
||
|
||
export type YakuData = Required<NormalYakuData> | Required<YakumanData>;
|
||
|
||
abstract class YakuSetBase<IsYakuman extends boolean> {
|
||
public readonly isYakuman: IsYakuman;
|
||
|
||
public readonly yakus: YakuData[];
|
||
|
||
public get yakuNames(): YakuName[] {
|
||
return this.yakus.map(yaku => yaku.name);
|
||
}
|
||
|
||
constructor(isYakuman: IsYakuman, yakus: YakuData[]) {
|
||
this.isYakuman = isYakuman;
|
||
this.yakus = yakus;
|
||
}
|
||
}
|
||
|
||
class NormalYakuSet extends YakuSetBase<false> {
|
||
public readonly isMenzen: boolean;
|
||
|
||
public readonly fan: number;
|
||
|
||
constructor(isMenzen: boolean, yakus: Required<NormalYakuData>[]) {
|
||
super(false, yakus);
|
||
this.isMenzen = isMenzen;
|
||
this.fan = yakus.reduce((fan, yaku) => fan + (!isMenzen && yaku.kuisagari ? yaku.fan - 1 : yaku.fan), 0);
|
||
}
|
||
}
|
||
|
||
class YakumanSet extends YakuSetBase<true> {
|
||
/**
|
||
* 何倍役満か
|
||
*/
|
||
public readonly value: number;
|
||
|
||
constructor(yakus: Required<YakumanData>[]) {
|
||
super(true, yakus);
|
||
this.value = yakus.reduce((value, yaku) => value + (yaku.isDoubleYakuman ? 2 : 1), 0);
|
||
}
|
||
}
|
||
|
||
export type YakuSet = NormalYakuSet | YakumanSet;
|
||
|
||
type YakuDefinitionBase = {
|
||
calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantouWithWait | null) => boolean;
|
||
};
|
||
|
||
type NormalYakuDefinition = YakuDefinitionBase & NormalYakuData;
|
||
|
||
type YakumanDefinition = YakuDefinitionBase & YakumanData;
|
||
|
||
function countTiles(tiles: TileType[], target: TileType): number {
|
||
return tiles.filter(t => t === target).length;
|
||
}
|
||
|
||
class Yakuhai implements NormalYakuDefinition {
|
||
readonly name: NormalYakuName;
|
||
|
||
readonly fan = 1;
|
||
|
||
readonly isYakuman = false;
|
||
|
||
readonly tile: typeof CHAR_TILES[number];
|
||
|
||
constructor(name: NormalYakuName, house: typeof CHAR_TILES[number]) {
|
||
this.name = name;
|
||
this.tile = house;
|
||
}
|
||
|
||
calc(state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null): boolean {
|
||
if (fourMentsuOneJyantou == null) return false;
|
||
|
||
return (
|
||
(countTiles(state.handTiles, this.tile) >= 3) ||
|
||
(state.huros.some(huro =>
|
||
huro.type === 'pon' ? huro.tile === this.tile :
|
||
huro.type === 'ankan' ? huro.tile === this.tile :
|
||
huro.type === 'minkan' ? huro.tile === this.tile :
|
||
false))
|
||
);
|
||
}
|
||
}
|
||
|
||
class FieldWind extends Yakuhai {
|
||
calc(state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null): boolean {
|
||
return super.calc(state, fourMentsuOneJyantou) && state.fieldWind === this.tile;
|
||
}
|
||
}
|
||
|
||
class SeatWind extends Yakuhai {
|
||
calc(state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null): boolean {
|
||
return super.calc(state, fourMentsuOneJyantou) && state.seatWind === this.tile;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 2つの同じ面子の組を数える (一盃口なら1、二盃口なら2)
|
||
*/
|
||
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++) {
|
||
if (mentsuEquals(mentsu, singleMentsus[i])) {
|
||
result++;
|
||
singleMentsus.splice(i, 1);
|
||
continue loop;
|
||
}
|
||
}
|
||
singleMentsus.push(mentsu);
|
||
}
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* 暗刻の数を数える (三暗刻なら3、四暗刻なら4)
|
||
*/
|
||
function countAnkos(state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantouWithWait) {
|
||
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)) {
|
||
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) => {
|
||
// 門前じゃないとダメ
|
||
if (state.huros.some(huro => includes(CALL_HURO_TYPES, huro.type))) return false;
|
||
|
||
return state.tsumoTile != null;
|
||
},
|
||
},
|
||
{
|
||
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: '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;
|
||
|
||
// 面前じゃないとダメ
|
||
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;
|
||
|
||
// 風牌判定(役牌でなければ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;
|
||
|
||
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;
|
||
|
||
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;
|
||
|
||
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;
|
||
|
||
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;
|
||
|
||
return true;
|
||
},
|
||
},
|
||
{
|
||
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;
|
||
|
||
// 同じ順子が2つあるか?
|
||
return countIndenticalMentsuPairs(fourMentsuOneJyantou.mentsus) === 1;
|
||
},
|
||
},
|
||
{
|
||
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;
|
||
|
||
// 2つの同じ順子が2組あるか?
|
||
return countIndenticalMentsuPairs(fourMentsuOneJyantou.mentsus) === 2;
|
||
},
|
||
},
|
||
{
|
||
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 (!fourMentsuOneJyantou.mentsus.every((mentsu) => mentsu[0] === mentsu[1])) return false;
|
||
|
||
return true;
|
||
},
|
||
},
|
||
{
|
||
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)) &&
|
||
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;
|
||
|
||
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]))) {
|
||
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;
|
||
},
|
||
},
|
||
{
|
||
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));
|
||
|
||
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;
|
||
},
|
||
},
|
||
{
|
||
name: 'chanta',
|
||
fan: 2,
|
||
isYakuman: false,
|
||
kuisagari: true,
|
||
calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => {
|
||
if (fourMentsuOneJyantou == null) 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) ||
|
||
mentsus.some(mentsu => includes(CHAR_TILES, mentsu[0])) ||
|
||
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' ?
|
||
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;
|
||
|
||
const { head, mentsus } = fourMentsuOneJyantou;
|
||
const { huros } = state;
|
||
|
||
// 雀頭は老頭牌じゃないとダメ
|
||
if (!includes(TERMINAL_TILES, head)) 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' ?
|
||
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<TileType, number>();
|
||
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;
|
||
|
||
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 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);
|
||
}
|
||
}
|
||
|
||
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) {
|
||
case 'pon':
|
||
case 'ankan':
|
||
case 'minkan':
|
||
return {
|
||
type: huro.type,
|
||
tile: TILE_ID_MAP.get(huro.tiles[0])!.t,
|
||
};
|
||
case 'cii':
|
||
return {
|
||
type: 'cii',
|
||
tiles: huro.tiles.map(tile => TILE_ID_MAP.get(tile)!.t) as [TileType, TileType, TileType],
|
||
};
|
||
}
|
||
}
|
||
|
||
const NORMAL_YAKU_DATA_MAP = new Map<NormalYakuName, Required<NormalYakuData>>(
|
||
NORMAL_YAKU_DEFINITIONS.map(yaku => [yaku.name, {
|
||
name: yaku.name,
|
||
upper: yaku.upper ?? null,
|
||
fan: yaku.fan,
|
||
isYakuman: false,
|
||
kuisagari: yaku.kuisagari ?? false,
|
||
}] as const),
|
||
);
|
||
|
||
const YAKUMAN_DATA_MAP = new Map<YakuName, Required<YakumanData>>(
|
||
YAKUMAN_DEFINITIONS.map(yaku => [yaku.name, {
|
||
name: yaku.name,
|
||
upper: yaku.upper ?? null,
|
||
fan: null,
|
||
isYakuman: true,
|
||
isDoubleYakuman: yaku.isDoubleYakuman ?? false,
|
||
}]),
|
||
);
|
||
|
||
export function calcYakusWithDetail(state: EnvForCalcYaku): YakuSet {
|
||
if (state.riichi && state.huros.some(huro => includes(CALL_HURO_TYPES, huro.type)) ) {
|
||
throw new TypeError('Invalid riichi state with call huros');
|
||
}
|
||
|
||
const agariTile = state.tsumoTile ?? state.ronTile;
|
||
if (!state.handTiles.includes(agariTile)) {
|
||
throw new TypeError('Agari tile not included in hand tiles');
|
||
}
|
||
|
||
if (state.handTiles.length + state.huros.length * 3 !== 14) {
|
||
throw new TypeError('Invalid tile count');
|
||
}
|
||
|
||
const oneHeadFourMentsuPatterns: (FourMentsuOneJyantou | null)[] = analyzeFourMentsuOneJyantou(state.handTiles);
|
||
if (oneHeadFourMentsuPatterns.length === 0) oneHeadFourMentsuPatterns.push(null);
|
||
|
||
const waitPatterns = oneHeadFourMentsuPatterns.map(
|
||
fourMentsuOneJyantou => calcWaitPatterns(fourMentsuOneJyantou, agariTile),
|
||
).flat();
|
||
|
||
const yakumanPatterns = waitPatterns.map(fourMentsuOneJyantouWithWait => {
|
||
const matchedYakus: Required<YakumanData>[] = [];
|
||
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);
|
||
|
||
if (yakumanPatterns.length > 0) {
|
||
return new YakumanSet(yakumanPatterns[0]);
|
||
}
|
||
|
||
const yakuPatterns = waitPatterns.map(
|
||
fourMentsuOneJyantouWithWait => NORMAL_YAKU_DEFINITIONS.filter(
|
||
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) {
|
||
return new NormalYakuSet(isMenzen, []);
|
||
}
|
||
|
||
let maxYakus = yakuPatterns[0];
|
||
let maxFan = 0;
|
||
for (const yakus of yakuPatterns) {
|
||
let fan = 0;
|
||
for (const yaku of yakus) {
|
||
if (yaku.kuisagari && !isMenzen) {
|
||
fan += yaku.fan! - 1;
|
||
} else {
|
||
fan += yaku.fan!;
|
||
}
|
||
}
|
||
if (fan > maxFan) {
|
||
maxFan = fan;
|
||
maxYakus = yakus;
|
||
}
|
||
}
|
||
|
||
return new NormalYakuSet(isMenzen, maxYakus);
|
||
}
|
||
|
||
export function calcYakus(state: EnvForCalcYaku): YakuName[] {
|
||
return calcYakusWithDetail(state).yakuNames;
|
||
}
|