This commit is contained in:
syuilo
2024-02-11 12:45:50 +09:00
parent c99d55e0cb
commit c47203b888
12 changed files with 1236 additions and 353 deletions

View File

@@ -0,0 +1,730 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* 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 } from './common.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',
'seat-wind',
'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;
export type YakuName = typeof NORMAL_YAKU_NAMES[number] | typeof YAKUMAN_NAMES[number];
export type EnvForCalcYaku = {
house: House;
/**
* 和了る人の手牌(副露牌は含まず、ツモ、ロン牌は含む)
*/
handTiles: TileType[];
tenpaiTiles: TileType[];
/**
* 河
*/
hoTiles: TileType[];
/**
* 副露
*/
huros: ({
type: 'pon';
tile: TileType;
} | {
type: 'cii';
tiles: [TileType, TileType, TileType];
} | {
type: 'ankan';
tile: TileType;
} | {
type: 'minkan';
tile: TileType;
})[];
tsumoTile: TileType;
ronTile: TileType;
/**
* 場風
*/
fieldWind: House;
/**
* 自風
*/
seatWind: House;
/**
* リーチしたかどうか
*/
riichi: boolean;
/**
* 一巡目以内かどうか
*/
ippatsu: boolean;
};
type YakuDefiniyion = {
name: YakuName;
fan: number;
isYakuman?: boolean;
kuisagari?: boolean;
calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => boolean;
};
function countTiles(tiles: TileType[], target: TileType): number {
return tiles.filter(t => t === target).length;
}
export const YAKU_DEFINITIONS: YakuDefiniyion[] = [{
name: 'tsumo',
fan: 1,
isYakuman: false,
calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => {
if (fourMentsuOneJyantou == null) return false;
// 面前じゃないとダメ
if (state.huros.some(huro => CALL_HURO_TYPES.includes(huro.type))) return false;
return state.isTsumo;
},
}, {
name: 'riichi',
fan: 1,
isYakuman: false,
calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => {
return state.riichi;
},
}, {
name: 'ippatsu',
fan: 1,
isYakuman: false,
calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => {
return state.ippatsu;
},
}, {
name: 'red',
fan: 1,
isYakuman: false,
calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => {
if (fourMentsuOneJyantou == null) return false;
return (
(countTiles(state.handTiles, 'chun') >= 3) ||
(state.huros.filter(huro =>
huro.type === 'pon' ? huro.tile === 'chun' :
huro.type === 'ankan' ? huro.tile === 'chun' :
huro.type === 'minkan' ? huro.tile === 'chun' :
false).length >= 3)
);
},
}, {
name: 'white',
fan: 1,
isYakuman: false,
calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => {
if (fourMentsuOneJyantou == null) return false;
return (
(countTiles(state.handTiles, 'haku') >= 3) ||
(state.huros.filter(huro =>
huro.type === 'pon' ? huro.tile === 'haku' :
huro.type === 'ankan' ? huro.tile === 'haku' :
huro.type === 'minkan' ? huro.tile === 'haku' :
false).length >= 3)
);
},
}, {
name: 'green',
fan: 1,
isYakuman: false,
calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => {
if (fourMentsuOneJyantou == null) return false;
return (
(countTiles(state.handTiles, 'hatsu') >= 3) ||
(state.huros.filter(huro =>
huro.type === 'pon' ? huro.tile === 'hatsu' :
huro.type === 'ankan' ? huro.tile === 'hatsu' :
huro.type === 'minkan' ? huro.tile === 'hatsu' :
false).length >= 3)
);
},
}, {
name: 'field-wind-e',
fan: 1,
isYakuman: false,
calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => {
if (fourMentsuOneJyantou == null) return false;
return state.fieldWind === 'e' && (
(countTiles(state.handTiles, 'e') >= 3) ||
(state.huros.filter(huro =>
huro.type === 'pon' ? huro.tile === 'e' :
huro.type === 'ankan' ? huro.tile === 'e' :
huro.type === 'minkan' ? huro.tile === 'e' :
false).length >= 3)
);
},
}, {
name: 'field-wind-s',
fan: 1,
isYakuman: false,
calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => {
if (fourMentsuOneJyantou == null) return false;
return state.fieldWind === 's' && (
(countTiles(state.handTiles, 's') >= 3) ||
(state.huros.filter(huro =>
huro.type === 'pon' ? huro.tile === 's' :
huro.type === 'ankan' ? huro.tile === 's' :
huro.type === 'minkan' ? huro.tile === 's' :
false).length >= 3)
);
},
}, {
name: 'seat-wind-e',
fan: 1,
isYakuman: false,
calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => {
if (fourMentsuOneJyantou == null) return false;
return state.house === 'e' && (
(countTiles(state.handTiles, 'e') >= 3) ||
(state.huros.filter(huro =>
huro.type === 'pon' ? huro.tile === 'e' :
huro.type === 'ankan' ? huro.tile === 'e' :
huro.type === 'minkan' ? huro.tile === 'e' :
false).length >= 3)
);
},
}, {
name: 'seat-wind-s',
fan: 1,
isYakuman: false,
calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => {
if (fourMentsuOneJyantou == null) return false;
return state.house === 's' && (
(countTiles(state.handTiles, 's') >= 3) ||
(state.huros.filter(huro =>
huro.type === 'pon' ? huro.tile === 's' :
huro.type === 'ankan' ? huro.tile === 's' :
huro.type === 'minkan' ? huro.tile === 's' :
false).length >= 3)
);
},
}, {
name: 'seat-wind-w',
fan: 1,
isYakuman: false,
calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => {
if (fourMentsuOneJyantou == null) return false;
return state.house === 'w' && (
(countTiles(state.handTiles, 'w') >= 3) ||
(state.huros.filter(huro =>
huro.type === 'pon' ? huro.tile === 'w' :
huro.type === 'ankan' ? huro.tile === 'w' :
huro.type === 'minkan' ? huro.tile === 'w' :
false).length >= 3)
);
},
}, {
name: 'seat-wind-n',
fan: 1,
isYakuman: false,
calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => {
if (fourMentsuOneJyantou == null) return false;
return state.house === 'n' && (
(countTiles(state.handTiles, 'n') >= 3) ||
(state.huros.filter(huro =>
huro.type === 'pon' ? huro.tile === 'n' :
huro.type === 'ankan' ? huro.tile === 'n' :
huro.type === 'minkan' ? huro.tile === 'n' :
false).length >= 3)
);
},
}, {
name: 'tanyao',
fan: 1,
isYakuman: false,
calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => {
if (fourMentsuOneJyantou == null) return false;
return (
(!state.handTiles.some(t => YAOCHU_TILES.includes(t))) &&
(state.huros.filter(huro =>
huro.type === 'pon' ? YAOCHU_TILES.includes(huro.tile) :
huro.type === 'ankan' ? YAOCHU_TILES.includes(huro.tile) :
huro.type === 'minkan' ? YAOCHU_TILES.includes(huro.tile) :
huro.type === 'cii' ? huro.tiles.some(t2 => YAOCHU_TILES.includes(t2)) :
false).length === 0)
);
},
}, {
name: 'pinfu',
fan: 1,
isYakuman: false,
calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => {
if (fourMentsuOneJyantou == null) return false;
// 面前じゃないとダメ
if (state.huros.some(huro => CALL_HURO_TYPES.includes(huro.type))) return false;
// 三元牌はダメ
if (state.handTiles.some(t => ['haku', 'hatsu', 'chun'].includes(t))) return false;
// TODO: 両面待ちかどうか
// 風牌判定(役牌でなければ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) => {
if (fourMentsuOneJyantou == null) return false;
const tiles = state.handTiles;
let manzuCount = tiles.filter(t => MANZU_TILES.includes(t)).length;
let pinzuCount = tiles.filter(t => PINZU_TILES.includes(t)).length;
let souzuCount = tiles.filter(t => SOUZU_TILES.includes(t)).length;
let charCount = tiles.filter(t => CHAR_TILES.includes(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 => MANZU_TILES.includes(t)).length;
pinzuCount += huroTiles.filter(t => PINZU_TILES.includes(t)).length;
souzuCount += huroTiles.filter(t => SOUZU_TILES.includes(t)).length;
charCount += huroTiles.filter(t => CHAR_TILES.includes(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) => {
if (fourMentsuOneJyantou == null) return false;
const tiles = state.handTiles;
let manzuCount = tiles.filter(t => MANZU_TILES.includes(t)).length;
let pinzuCount = tiles.filter(t => PINZU_TILES.includes(t)).length;
let souzuCount = tiles.filter(t => SOUZU_TILES.includes(t)).length;
let charCount = tiles.filter(t => CHAR_TILES.includes(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 => MANZU_TILES.includes(t)).length;
pinzuCount += huroTiles.filter(t => PINZU_TILES.includes(t)).length;
souzuCount += huroTiles.filter(t => SOUZU_TILES.includes(t)).length;
charCount += huroTiles.filter(t => CHAR_TILES.includes(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 => CALL_HURO_TYPES.includes(huro.type))) return false;
// 同じ順子が2つあるか
return fourMentsuOneJyantou.mentsus.some((mentsu) =>
fourMentsuOneJyantou.mentsus.filter((mentsu2) =>
mentsu2[0] === mentsu[0] && mentsu2[1] === mentsu[1] && mentsu2[2] === mentsu[2]).length >= 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: FourMentsuOneJyantou | null) => {
},
}, {
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));
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: 'chitoitsu',
fan: 2,
isYakuman: false,
calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => {
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;
},
}, {
name: 'daisangen',
fan: 13,
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',
fan: 13,
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',
fan: 13,
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',
fan: 13,
isYakuman: true,
calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => {
if (fourMentsuOneJyantou == null) return false;
const tiles = state.handTiles;
let manzuCount = tiles.filter(t => MANZU_TILES.includes(t)).length;
let pinzuCount = tiles.filter(t => PINZU_TILES.includes(t)).length;
let souzuCount = tiles.filter(t => SOUZU_TILES.includes(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 => MANZU_TILES.includes(t)).length;
pinzuCount += huroTiles.filter(t => PINZU_TILES.includes(t)).length;
souzuCount += huroTiles.filter(t => SOUZU_TILES.includes(t)).length;
}
if (manzuCount > 0 || pinzuCount > 0 || souzuCount > 0) return false;
return true;
},
}, {
name: 'ryuiso',
fan: 13,
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: 'kokushi',
fan: 13,
isYakuman: true,
calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => {
return KOKUSHI_TILES.every(t => state.handTiles.includes(t));
},
}];
export function calcYakus(state: EnvForCalcYaku): YakuName[] {
const oneHeadFourMentsuPatterns: (FourMentsuOneJyantou | null)[] = analyzeFourMentsuOneJyantou(state.handTiles);
if (oneHeadFourMentsuPatterns.length === 0) oneHeadFourMentsuPatterns.push(null);
const yakuPatterns = oneHeadFourMentsuPatterns.map(fourMentsuOneJyantou => {
return YAKU_DEFINITIONS.map(yakuDef => {
const result = yakuDef.calc(state, fourMentsuOneJyantou);
return result ? yakuDef : null;
}).filter(yaku => yaku != null) as YakuDefiniyion[];
}).filter(yakus => yakus.length > 0);
const isMenzen = state.huros.some(huro => CALL_HURO_TYPES.includes(huro.type));
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 maxYakus.map(yaku => yaku.name);
}