From 3f810a856c5c3024145782dada80929b098e86b7 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 11 Feb 2024 14:23:37 +0900 Subject: [PATCH] wip --- packages/misskey-mahjong/src/common.yaku.ts | 120 +++++++++++++++++--- packages/misskey-mahjong/test/yaku.ts | 100 +++++++++++++++- 2 files changed, 201 insertions(+), 19 deletions(-) diff --git a/packages/misskey-mahjong/src/common.yaku.ts b/packages/misskey-mahjong/src/common.yaku.ts index c993bd5ae8..2ca3475083 100644 --- a/packages/misskey-mahjong/src/common.yaku.ts +++ b/packages/misskey-mahjong/src/common.yaku.ts @@ -15,8 +15,14 @@ export const NORMAL_YAKU_NAMES = [ 'tanyao', 'pinfu', 'iipeko', - 'field-wind', - 'seat-wind', + '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', @@ -71,8 +77,6 @@ export type EnvForCalcYaku = { */ handTiles: TileType[]; - tenpaiTiles: TileType[]; - /** * 河 */ @@ -121,8 +125,10 @@ export type EnvForCalcYaku = { type YakuDefiniyion = { name: YakuName; - fan: number; + upper?: YakuName; + fan?: number; isYakuman?: boolean; + isDoubleYakuman?: boolean; kuisagari?: boolean; calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => boolean; }; @@ -131,7 +137,7 @@ function countTiles(tiles: TileType[], target: TileType): number { return tiles.filter(t => t === target).length; } -export const YAKU_DEFINITIONS: YakuDefiniyion[] = [{ +export const NORAML_YAKU_DEFINITIONS: YakuDefiniyion[] = [{ name: 'tsumo', fan: 1, isYakuman: false, @@ -141,7 +147,7 @@ export const YAKU_DEFINITIONS: YakuDefiniyion[] = [{ // 面前じゃないとダメ if (state.huros.some(huro => CALL_HURO_TYPES.includes(huro.type))) return false; - return state.isTsumo; + return state.tsumoTile != null; }, }, { name: 'riichi', @@ -579,9 +585,10 @@ export const YAKU_DEFINITIONS: YakuDefiniyion[] = [{ return false; }, -}, { +}]; + +export const YAKUMAN_DEFINITIONS: YakuDefiniyion[] = [{ name: 'daisangen', - fan: 13, isYakuman: true, calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { if (fourMentsuOneJyantou == null) return false; @@ -602,7 +609,6 @@ export const YAKU_DEFINITIONS: YakuDefiniyion[] = [{ }, }, { name: 'shosushi', - fan: 13, isYakuman: true, calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { if (fourMentsuOneJyantou == null) return false; @@ -629,7 +635,6 @@ export const YAKU_DEFINITIONS: YakuDefiniyion[] = [{ }, }, { name: 'daisushi', - fan: 13, isYakuman: true, calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { if (fourMentsuOneJyantou == null) return false; @@ -650,7 +655,6 @@ export const YAKU_DEFINITIONS: YakuDefiniyion[] = [{ }, }, { name: 'tsuiso', - fan: 13, isYakuman: true, calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { if (fourMentsuOneJyantou == null) return false; @@ -673,7 +677,6 @@ export const YAKU_DEFINITIONS: YakuDefiniyion[] = [{ }, }, { name: 'ryuiso', - fan: 13, isYakuman: true, calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { if (fourMentsuOneJyantou == null) return false; @@ -687,9 +690,76 @@ export const YAKU_DEFINITIONS: YakuDefiniyion[] = [{ return true; }, +}, { + name: 'churen-9', + isYakuman: true, + isDoubleYakuman: true, + calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { + if (fourMentsuOneJyantou == null) return false; + + // 面前じゃないとダメ + if (state.huros.some(huro => CALL_HURO_TYPES.includes(huro.type))) return false; + + const agariTile = state.tsumoTile ?? state.ronTile; + 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 => CALL_HURO_TYPES.includes(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', - fan: 13, isYakuman: true, calc: (state: EnvForCalcYaku, fourMentsuOneJyantou: FourMentsuOneJyantou | null) => { return KOKUSHI_TILES.every(t => state.handTiles.includes(t)); @@ -700,8 +770,24 @@ export function calcYakus(state: EnvForCalcYaku): YakuName[] { const oneHeadFourMentsuPatterns: (FourMentsuOneJyantou | null)[] = analyzeFourMentsuOneJyantou(state.handTiles); if (oneHeadFourMentsuPatterns.length === 0) oneHeadFourMentsuPatterns.push(null); + const yakumanPatterns = oneHeadFourMentsuPatterns.map(fourMentsuOneJyantou => { + const matchedYakus: YakuDefiniyion[] = []; + for (const yakuDef of YAKUMAN_DEFINITIONS) { + if (yakuDef.upper && matchedYakus.some(yaku => yaku.name === yakuDef.upper)) continue; + const matched = yakuDef.calc(state, fourMentsuOneJyantou); + if (matched) { + matchedYakus.push(yakuDef); + } + } + return matchedYakus; + }).filter(yakus => yakus.length > 0); + + if (yakumanPatterns.length > 0) { + return yakumanPatterns[0].map(yaku => yaku.name); + } + const yakuPatterns = oneHeadFourMentsuPatterns.map(fourMentsuOneJyantou => { - return YAKU_DEFINITIONS.map(yakuDef => { + return NORAML_YAKU_DEFINITIONS.map(yakuDef => { const result = yakuDef.calc(state, fourMentsuOneJyantou); return result ? yakuDef : null; }).filter(yaku => yaku != null) as YakuDefiniyion[]; @@ -715,9 +801,9 @@ export function calcYakus(state: EnvForCalcYaku): YakuName[] { let fan = 0; for (const yaku of yakus) { if (yaku.kuisagari && !isMenzen) { - fan += yaku.fan - 1; + fan += yaku.fan! - 1; } else { - fan += yaku.fan; + fan += yaku.fan!; } } if (fan > maxFan) { diff --git a/packages/misskey-mahjong/test/yaku.ts b/packages/misskey-mahjong/test/yaku.ts index ef67a6d80a..835ac1b5b8 100644 --- a/packages/misskey-mahjong/test/yaku.ts +++ b/packages/misskey-mahjong/test/yaku.ts @@ -16,5 +16,101 @@ describe('Yaku', () => { riichi: true, }), ['riichi']); }); - } -} + }); + + describe('churen', () => { + it('valid', () => { + assert.deepStrictEqual(calcYakus({ + house: 'e', + handTiles: ['m1', 'm1', 'm1', 'm2', 'm3', 'm3', 'm4', 'm6', 'm7', 'm8', 'm9', 'm9', 'm9', 'm5'], + huros: [], + tsumoTile: 'm5', + }), ['churen']); + }); + + it('invalid', () => { + assert.deepStrictEqual(calcYakus({ + house: 'e', + handTiles: ['m1', 'm1', 'm1', 'm2', 'm3', 'm3', 'm4', 'm6', 'm7', 'm8', 'm9', 'm9', 'm9', 'm2'], + huros: [], + tsumoTile: 'm2', + }).includes('churen'), false); + }); + }); + + describe('churen-9', () => { + it('valid', () => { + assert.deepStrictEqual(calcYakus({ + house: 'e', + handTiles: ['m1', 'm1', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'm7', 'm8', 'm9', 'm9', 'm9', 'm1'], + huros: [], + tsumoTile: 'm1', + }), ['churen-9']); + + assert.deepStrictEqual(calcYakus({ + house: 'e', + handTiles: ['m1', 'm1', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'm7', 'm8', 'm9', 'm9', 'm9', 'm2'], + huros: [], + tsumoTile: 'm2', + }), ['churen-9']); + + assert.deepStrictEqual(calcYakus({ + house: 'e', + handTiles: ['m1', 'm1', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'm7', 'm8', 'm9', 'm9', 'm9', 'm3'], + huros: [], + tsumoTile: 'm3', + }), ['churen-9']); + + assert.deepStrictEqual(calcYakus({ + house: 'e', + handTiles: ['m1', 'm1', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'm7', 'm8', 'm9', 'm9', 'm9', 'm4'], + huros: [], + tsumoTile: 'm4', + }), ['churen-9']); + + assert.deepStrictEqual(calcYakus({ + house: 'e', + handTiles: ['m1', 'm1', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'm7', 'm8', 'm9', 'm9', 'm9', 'm5'], + huros: [], + tsumoTile: 'm5', + }), ['churen-9']); + + assert.deepStrictEqual(calcYakus({ + house: 'e', + handTiles: ['m1', 'm1', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'm7', 'm8', 'm9', 'm9', 'm9', 'm6'], + huros: [], + tsumoTile: 'm6', + }), ['churen-9']); + + assert.deepStrictEqual(calcYakus({ + house: 'e', + handTiles: ['m1', 'm1', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'm7', 'm8', 'm9', 'm9', 'm9', 'm7'], + huros: [], + tsumoTile: 'm7', + }), ['churen-9']); + + assert.deepStrictEqual(calcYakus({ + house: 'e', + handTiles: ['m1', 'm1', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'm7', 'm8', 'm9', 'm9', 'm9', 'm8'], + huros: [], + tsumoTile: 'm8', + }), ['churen-9']); + + assert.deepStrictEqual(calcYakus({ + house: 'e', + handTiles: ['m1', 'm1', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'm7', 'm8', 'm9', 'm9', 'm9', 'm9'], + huros: [], + tsumoTile: 'm9', + }), ['churen-9']); + }); + + it('invalid', () => { + assert.deepStrictEqual(calcYakus({ + house: 'e', + handTiles: ['m1', 'm1', 'm1', 'm2', 'm3', 'm3', 'm4', 'm6', 'm7', 'm8', 'm9', 'm9', 'm9', 'm5'], + huros: [], + tsumoTile: 'm5', + }).includes('churen-9'), false); + }); + }); +});