forked from mirrors/misskey
wip
This commit is contained in:
@@ -237,297 +237,68 @@ export const PREV_TILE_FOR_SHUNTSU: Record<TileType, TileType | null> = {
|
||||
chun: null,
|
||||
};
|
||||
|
||||
const KOKUSHI_TILES: TileType[] = ['m1', 'm9', 'p1', 'p9', 's1', 's9', 'e', 's', 'w', 'n', 'haku', 'hatsu', 'chun'];
|
||||
|
||||
type EnvForCalcYaku = {
|
||||
house: House;
|
||||
|
||||
/**
|
||||
* 和了る人の手牌(副露牌および和了る際のツモ牌・ロン牌は含まない)
|
||||
*/
|
||||
handTiles: TileType[];
|
||||
|
||||
/**
|
||||
* 河
|
||||
*/
|
||||
hoTiles: TileType[];
|
||||
|
||||
/**
|
||||
* 副露
|
||||
*/
|
||||
huros: Huro[];
|
||||
|
||||
/**
|
||||
* ツモ牌
|
||||
*/
|
||||
tsumoTile: TileType | null;
|
||||
|
||||
/**
|
||||
* ロン牌
|
||||
*/
|
||||
ronTile: TileType | null;
|
||||
|
||||
/**
|
||||
* ドラ表示牌
|
||||
*/
|
||||
doraTiles: TileType[];
|
||||
|
||||
/**
|
||||
* 赤ドラ表示牌
|
||||
*/
|
||||
redDoraTiles: TileType[];
|
||||
|
||||
/**
|
||||
* 場風
|
||||
*/
|
||||
fieldWind: House;
|
||||
|
||||
/**
|
||||
* 自風
|
||||
*/
|
||||
seatWind: House;
|
||||
|
||||
/**
|
||||
* リーチしたかどうか
|
||||
*/
|
||||
riichi: boolean;
|
||||
|
||||
/**
|
||||
* 一巡目以内かどうか
|
||||
*/
|
||||
ippatsu: boolean;
|
||||
export const TILE_NUMBER_MAP: Record<TileType, number | null> = {
|
||||
m1: 1,
|
||||
m2: 2,
|
||||
m3: 3,
|
||||
m4: 4,
|
||||
m5: 5,
|
||||
m6: 6,
|
||||
m7: 7,
|
||||
m8: 8,
|
||||
m9: 9,
|
||||
p1: 1,
|
||||
p2: 2,
|
||||
p3: 3,
|
||||
p4: 4,
|
||||
p5: 5,
|
||||
p6: 6,
|
||||
p7: 7,
|
||||
p8: 8,
|
||||
p9: 9,
|
||||
s1: 1,
|
||||
s2: 2,
|
||||
s3: 3,
|
||||
s4: 4,
|
||||
s5: 5,
|
||||
s6: 6,
|
||||
s7: 7,
|
||||
s8: 8,
|
||||
s9: 9,
|
||||
e: null,
|
||||
s: null,
|
||||
w: null,
|
||||
n: null,
|
||||
haku: null,
|
||||
hatsu: null,
|
||||
chun: null,
|
||||
};
|
||||
|
||||
export const YAKU_DEFINITIONS = [{
|
||||
name: 'riichi',
|
||||
fan: 1,
|
||||
isYakuman: false,
|
||||
calc: (state: EnvForCalcYaku) => {
|
||||
return state.riichi;
|
||||
},
|
||||
}, {
|
||||
name: 'tsumo',
|
||||
fan: 1,
|
||||
isYakuman: false,
|
||||
calc: (state: EnvForCalcYaku) => {
|
||||
return state.tsumoTile != null;
|
||||
},
|
||||
}, {
|
||||
name: 'ippatsu',
|
||||
fan: 1,
|
||||
isYakuman: false,
|
||||
calc: (state: EnvForCalcYaku) => {
|
||||
return state.ippatsu;
|
||||
},
|
||||
}, {
|
||||
name: 'red',
|
||||
fan: 1,
|
||||
isYakuman: false,
|
||||
calc: (state: EnvForCalcYaku) => {
|
||||
return (
|
||||
(state.handTiles.filter(t => t === 'chun').length >= 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) => {
|
||||
return (
|
||||
(state.handTiles.filter(t => t === 'haku').length >= 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) => {
|
||||
return (
|
||||
(state.handTiles.filter(t => t === 'hatsu').length >= 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) => {
|
||||
return state.fieldWind === 'e' && (
|
||||
(state.handTiles.filter(t => t === 'e').length >= 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) => {
|
||||
return state.fieldWind === 's' && (
|
||||
(state.handTiles.filter(t => t === 's').length >= 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) => {
|
||||
return state.house === 'e' && (
|
||||
(state.handTiles.filter(t => t === 'e').length >= 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) => {
|
||||
return state.house === 's' && (
|
||||
(state.handTiles.filter(t => t === 's').length >= 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) => {
|
||||
return state.house === 'w' && (
|
||||
(state.handTiles.filter(t => t === 'w').length >= 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) => {
|
||||
return state.house === 'n' && (
|
||||
(state.handTiles.filter(t => t === 'n').length >= 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) => {
|
||||
const yaochuTiles: TileType[] = ['m1', 'm9', 'p1', 'p9', 's1', 's9', 'e', 's', 'w', 'n', 'haku', 'hatsu', 'chun'];
|
||||
return (
|
||||
(!state.handTiles.some(t => yaochuTiles.includes(t))) &&
|
||||
(state.huros.filter(huro =>
|
||||
huro.type === 'pon' ? yaochuTiles.includes(huro.tile) :
|
||||
huro.type === 'ankan' ? yaochuTiles.includes(huro.tile) :
|
||||
huro.type === 'minkan' ? yaochuTiles.includes(huro.tile) :
|
||||
huro.type === 'cii' ? huro.tiles.some(t2 => yaochuTiles.includes(t2)) :
|
||||
false).length === 0)
|
||||
);
|
||||
},
|
||||
}, {
|
||||
name: 'pinfu',
|
||||
fan: 1,
|
||||
isYakuman: false,
|
||||
calc: (state: EnvForCalcYaku) => {
|
||||
// 面前じゃないとダメ
|
||||
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;
|
||||
export const MANZU_TILES = ['m1', 'm2', 'm3', 'm4', 'm5', 'm6', 'm7', 'm8', 'm9'] as const satisfies TileType[];
|
||||
export const PINZU_TILES = ['p1', 'p2', 'p3', 'p4', 'p5', 'p6', 'p7', 'p8', 'p9'] as const satisfies TileType[];
|
||||
export const SOUZU_TILES = ['s1', 's2', 's3', 's4', 's5', 's6', 's7', 's8', 's9'] as const satisfies TileType[];
|
||||
export const CHAR_TILES = ['e', 's', 'w', 'n', 'haku', 'hatsu', 'chun'] as const satisfies TileType[];
|
||||
export const YAOCHU_TILES = ['m1', 'm9', 'p1', 'p9', 's1', 's9', 'e', 's', 'w', 'n', 'haku', 'hatsu', 'chun'] as const satisfies TileType[];
|
||||
const KOKUSHI_TILES: TileType[] = ['m1', 'm9', 'p1', 'p9', 's1', 's9', 'e', 's', 'w', 'n', 'haku', 'hatsu', 'chun'];
|
||||
|
||||
// TODO: 両面待ちかどうか
|
||||
export function isManzu<T extends TileType>(tile: T): tile is typeof MANZU_TILES[number] {
|
||||
return MANZU_TILES.includes(tile);
|
||||
}
|
||||
|
||||
const horaSets = analyze1head3mentsuSets(state.handTiles.concat(state.tsumoTile ?? state.ronTile));
|
||||
return horaSets.some(horaSet => {
|
||||
// 風牌判定(役牌でなければOK)
|
||||
if (horaSet.head === state.seatWind) return false;
|
||||
if (horaSet.head === state.fieldWind) return false;
|
||||
export function isPinzu<T extends TileType>(tile: T): tile is typeof PINZU_TILES[number] {
|
||||
return PINZU_TILES.includes(tile);
|
||||
}
|
||||
|
||||
// 全て順子か?
|
||||
if (horaSet.mentsus.some((mentsu) => mentsu[0] === mentsu[1])) return false;
|
||||
});
|
||||
},
|
||||
}, {
|
||||
name: 'iipeko',
|
||||
fan: 1,
|
||||
isYakuman: false,
|
||||
calc: (state: EnvForCalcYaku) => {
|
||||
// 面前じゃないとダメ
|
||||
if (state.huros.some(huro => CALL_HURO_TYPES.includes(huro.type))) return false;
|
||||
export function isSouzu<T extends TileType>(tile: T): tile is typeof SOUZU_TILES[number] {
|
||||
return SOUZU_TILES.includes(tile);
|
||||
}
|
||||
|
||||
const horaSets = analyze1head3mentsuSets(state.handTiles.concat(state.tsumoTile ?? state.ronTile));
|
||||
return horaSets.some(horaSet => {
|
||||
// 同じ順子が2つあるか?
|
||||
return horaSet.mentsus.some((mentsu) =>
|
||||
horaSet.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) => {
|
||||
if (state.huros.length > 0) {
|
||||
if (state.huros.some(huro => huro.type === 'cii')) return false;
|
||||
}
|
||||
const horaSets = analyze1head3mentsuSets(state.handTiles.concat(state.tsumoTile ?? state.ronTile));
|
||||
return horaSets.some(horaSet => {
|
||||
// 全て刻子か?
|
||||
if (!horaSet.mentsus.every((mentsu) => mentsu[0] === mentsu[1])) return false;
|
||||
});
|
||||
},
|
||||
}, {
|
||||
name: 'chitoitsu',
|
||||
fan: 2,
|
||||
isYakuman: false,
|
||||
calc: (state: EnvForCalcYaku) => {
|
||||
return isChitoitsu(state.handTiles.concat(state.tsumoTile ?? state.ronTile));
|
||||
},
|
||||
}, {
|
||||
name: 'kokushi',
|
||||
fan: 13,
|
||||
isYakuman: true,
|
||||
calc: (state: EnvForCalcYaku) => {
|
||||
return isKokushi(state.handTiles.concat(state.tsumoTile ?? state.ronTile));
|
||||
},
|
||||
}];
|
||||
export function isSameNumberTile(a: TileType, b: TileType): boolean {
|
||||
const aNumber = TILE_NUMBER_MAP[a];
|
||||
const bNumber = TILE_NUMBER_MAP[b];
|
||||
if (aNumber == null || bNumber == null) return false;
|
||||
return aNumber === bNumber;
|
||||
}
|
||||
|
||||
export function fanToPoint(fan: number, isParent: boolean): number {
|
||||
let point;
|
||||
@@ -658,11 +429,19 @@ export function prevHouse(house: House): House {
|
||||
}
|
||||
}
|
||||
|
||||
type HoraSet = {
|
||||
export type FourMentsuOneJyantou = {
|
||||
head: TileType;
|
||||
mentsus: [TileType, TileType, TileType][];
|
||||
};
|
||||
|
||||
export function isShuntu(tiles: [TileType, TileType, TileType]): boolean {
|
||||
return tiles[0] !== tiles[1];
|
||||
}
|
||||
|
||||
export function isKotsu(tiles: [TileType, TileType, TileType]): boolean {
|
||||
return tiles[0] === tiles[1];
|
||||
}
|
||||
|
||||
export const SHUNTU_PATTERNS: [TileType, TileType, TileType][] = [
|
||||
['m1', 'm2', 'm3'],
|
||||
['m2', 'm3', 'm4'],
|
||||
@@ -720,13 +499,8 @@ function extractShuntsus(tiles: TileType[]): [TileType, TileType, TileType][] {
|
||||
return shuntsus;
|
||||
}
|
||||
|
||||
/**
|
||||
* アガリ形パターン一覧を取得
|
||||
* @param handTiles ポン、チー、カンした牌を含まない手牌
|
||||
* @returns
|
||||
*/
|
||||
function analyze1head3mentsuSets(handTiles: TileType[]): HoraSet[] {
|
||||
const horaSets: HoraSet[] = [];
|
||||
export function analyzeFourMentsuOneJyantou(handTiles: TileType[], all = true): FourMentsuOneJyantou[] {
|
||||
const horaSets: FourMentsuOneJyantou[] = [];
|
||||
|
||||
const headSet: TileType[] = [];
|
||||
const countMap = new Map<TileType, number>();
|
||||
@@ -817,6 +591,8 @@ function analyze1head3mentsuSets(handTiles: TileType[]): HoraSet[] {
|
||||
head,
|
||||
mentsus: [...kotsuPattern.map(t => [t, t, t] as [TileType, TileType, TileType]), ...shuntsus],
|
||||
});
|
||||
|
||||
if (!all) return horaSets;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -824,48 +600,6 @@ function analyze1head3mentsuSets(handTiles: TileType[]): HoraSet[] {
|
||||
return horaSets;
|
||||
}
|
||||
|
||||
export function canHora(handTiles: TileType[]): boolean {
|
||||
if (isKokushi(handTiles)) return true;
|
||||
if (isChitoitsu(handTiles)) return true;
|
||||
|
||||
const horaSets = analyze1head3mentsuSets(handTiles);
|
||||
return horaSets.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* アガリ牌リストを取得
|
||||
* @param handTiles ポン、チー、カンした牌を含まない手牌
|
||||
*/
|
||||
export function getHoraTiles(handTiles: TileType[]): TileType[] {
|
||||
return TILE_TYPES.filter(tile => {
|
||||
const tempHandTiles = [...handTiles, tile];
|
||||
const horaSets = analyze1head3mentsuSets(tempHandTiles);
|
||||
return horaSets.length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
function isKokushi(handTiles: TileType[]): boolean {
|
||||
return KOKUSHI_TILES.every(t => handTiles.includes(t));
|
||||
}
|
||||
|
||||
function isChitoitsu(handTiles: TileType[]): boolean {
|
||||
const countMap = new Map<TileType, number>();
|
||||
for (const tile of handTiles) {
|
||||
const count = (countMap.get(tile) ?? 0) + 1;
|
||||
countMap.set(tile, count);
|
||||
}
|
||||
return Array.from(countMap.values()).every(c => c === 2);
|
||||
}
|
||||
|
||||
export function getTilesForRiichi(handTiles: TileType[]): TileType[] {
|
||||
return handTiles.filter(tile => {
|
||||
const tempHandTiles = [...handTiles];
|
||||
tempHandTiles.splice(tempHandTiles.indexOf(tile), 1);
|
||||
const horaTiles = getHoraTiles(tempHandTiles);
|
||||
return horaTiles.length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
export function nextTileForDora(tile: TileType): TileType {
|
||||
return NEXT_TILE_FOR_DORA_MAP[tile];
|
||||
}
|
||||
@@ -893,3 +627,40 @@ export function getAvailableCiiPatterns(handTiles: TileType[], targetTile: TileT
|
||||
}
|
||||
return patterns;
|
||||
}
|
||||
|
||||
function isKokushiPattern(handTiles: TileType[]): boolean {
|
||||
return KOKUSHI_TILES.every(t => handTiles.includes(t));
|
||||
}
|
||||
|
||||
function isChitoitsuPattern(handTiles: TileType[]): boolean {
|
||||
if (handTiles.length !== 14) return false;
|
||||
const countMap = new Map<TileType, number>();
|
||||
for (const tile of handTiles) {
|
||||
const count = (countMap.get(tile) ?? 0) + 1;
|
||||
countMap.set(tile, count);
|
||||
}
|
||||
return Array.from(countMap.values()).every(c => c === 2);
|
||||
}
|
||||
|
||||
export function isAgarikei(handTiles: TileType[]): boolean {
|
||||
if (isKokushiPattern(handTiles)) return true;
|
||||
if (isChitoitsuPattern(handTiles)) return true;
|
||||
|
||||
const agarikeis = analyzeFourMentsuOneJyantou(handTiles, false);
|
||||
return agarikeis.length > 0;
|
||||
}
|
||||
|
||||
export function isTenpai(handTiles: TileType[]): boolean {
|
||||
return TILE_TYPES.some(tile => {
|
||||
const tempHandTiles = [...handTiles, tile];
|
||||
return isAgarikei(tempHandTiles);
|
||||
});
|
||||
}
|
||||
|
||||
export function getTilesForRiichi(handTiles: TileType[]): TileType[] {
|
||||
return handTiles.filter(tile => {
|
||||
const tempHandTiles = [...handTiles];
|
||||
tempHandTiles.splice(tempHandTiles.indexOf(tile), 1);
|
||||
return isTenpai(tempHandTiles);
|
||||
});
|
||||
}
|
||||
|
||||
730
packages/misskey-mahjong/src/common.yaku.ts
Normal file
730
packages/misskey-mahjong/src/common.yaku.ts
Normal 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);
|
||||
}
|
||||
@@ -108,7 +108,7 @@ class StateManager {
|
||||
// TODO: ポンされるなどして自分の河にない場合の考慮
|
||||
if (this.hoTileTypes[house].includes($type(tid))) return false;
|
||||
|
||||
if (!Common.canHora(this.handTileTypes[house].concat($type(tid)))) return false; // 完成形じゃない
|
||||
if (!Common.isAgarikei(this.handTileTypes[house].concat($type(tid)))) return false; // 完成形じゃない
|
||||
|
||||
// TODO
|
||||
//const yakus = YAKU_DEFINITIONS.filter(yaku => yaku.calc(this.state, { tsumoTile: null, ronTile: tile }));
|
||||
@@ -416,7 +416,7 @@ export class MasterGameEngine {
|
||||
if (tx.$state.riichis[house]) throw new Error('Already riichi');
|
||||
const tempHandTiles = [...tx.handTileTypes[house]];
|
||||
tempHandTiles.splice(tempHandTiles.indexOf($type(tid)), 1);
|
||||
if (Common.getHoraTiles(tempHandTiles).length === 0) throw new Error('Not tenpai');
|
||||
if (!Common.isTenpai(tempHandTiles)) throw new Error('Not tenpai');
|
||||
if (tx.$state.points[house] < 1000) throw new Error('Not enough points');
|
||||
}
|
||||
|
||||
|
||||
@@ -217,8 +217,12 @@ export class PlayerGameEngine {
|
||||
this.state.turn = null;
|
||||
|
||||
if (house === this.myHouse) {
|
||||
this.state.canRon = null;
|
||||
this.state.canPon = null;
|
||||
this.state.canKan = null;
|
||||
this.state.canCii = null;
|
||||
} else {
|
||||
const canRon = Common.canHora(this.myHandTiles.concat(tid).map(id => $type(id)));
|
||||
const canRon = Common.isAgarikei(this.myHandTiles.concat(tid).map(id => $type(id)));
|
||||
const canPon = !this.isMeRiichi && this.myHandTileTypes.filter(t => t === $type(tid)).length === 2;
|
||||
const canKan = !this.isMeRiichi && this.myHandTileTypes.filter(t => t === $type(tid)).length === 3;
|
||||
const canCii = !this.isMeRiichi && house === Common.prevHouse(this.myHouse) &&
|
||||
|
||||
Reference in New Issue
Block a user