wip
This commit is contained in:
parent
c99d55e0cb
commit
c47203b888
|
@ -2631,7 +2631,7 @@ _mahjong:
|
|||
"chinroto": "清老頭"
|
||||
"sukantsu": "四槓子"
|
||||
"churen": "九蓮宝燈"
|
||||
"pure-churen": "純正九連宝灯"
|
||||
"churen-9": "九連宝灯九面待"
|
||||
"tenho": "天和"
|
||||
"chiho": "地和"
|
||||
|
||||
|
|
|
@ -657,7 +657,7 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit {
|
|||
|
||||
if (mj.riichis[house]) {
|
||||
// リーチ時はアガリ牌でない限りツモ切り
|
||||
if (!Mmj.canHora(mj.handTileTypes[house])) {
|
||||
if (!Mmj.isAgarikei(mj.handTileTypes[house])) {
|
||||
setTimeout(() => {
|
||||
this.dahai(room, mj, house, mj.handTiles[house].at(-1));
|
||||
}, 500);
|
||||
|
|
|
@ -123,6 +123,33 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="$style.playersContainer">
|
||||
<div :class="[$style.playerOfToimen, $style.player]">
|
||||
<template v-if="users[Mmj.prevHouse(Mmj.prevHouse(mj.myHouse))] != null">
|
||||
<MkAvatar :user="users[Mmj.prevHouse(Mmj.prevHouse(mj.myHouse))]" style="width: 30px; height: 30px;"/>
|
||||
</template>
|
||||
<template v-else>
|
||||
CPU
|
||||
</template>
|
||||
</div>
|
||||
<div :class="[$style.playerOfKamitya, $style.player]">
|
||||
<template v-if="users[Mmj.prevHouse(mj.myHouse)] != null">
|
||||
<MkAvatar :user="users[Mmj.prevHouse(mj.myHouse)]" style="width: 30px; height: 30px;"/>
|
||||
</template>
|
||||
<template v-else>
|
||||
CPU
|
||||
</template>
|
||||
</div>
|
||||
<div :class="[$style.playerOfSimotya, $style.player]">
|
||||
<template v-if="users[Mmj.nextHouse(mj.myHouse)] != null">
|
||||
<MkAvatar :user="users[Mmj.nextHouse(mj.myHouse)]" style="width: 30px; height: 30px;"/>
|
||||
</template>
|
||||
<template v-else>
|
||||
CPU
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<XHandTiles :class="$style.handTilesOfMe" :tiles="mj.myHandTiles" :doras="mj.doras" :selectableTiles="selectableTiles" :separateLast="isMyTurn && iTsumoed" @choose="chooseTile"/>
|
||||
|
||||
<div :class="$style.serifContainer">
|
||||
|
@ -292,9 +319,16 @@ const isMyTurn = computed(() => {
|
|||
});
|
||||
|
||||
const canHora = computed(() => {
|
||||
return Mmj.canHora(mj.value.myHandTileTypes).length;
|
||||
return Mmj.isAgarikei(mj.value.myHandTileTypes);
|
||||
});
|
||||
|
||||
const users = computed(() => ({
|
||||
e: houseToUser('e'),
|
||||
s: houseToUser('s'),
|
||||
w: houseToUser('w'),
|
||||
n: houseToUser('n'),
|
||||
}));
|
||||
|
||||
const selectableTiles = ref<Mmj.TileType[] | null>(null);
|
||||
const ronSerifHouses = reactive<Record<Mmj.House, boolean>>({ e: false, s: false, w: false, n: false });
|
||||
const ciiSerifHouses = reactive<Record<Mmj.House, boolean>>({ e: false, s: false, w: false, n: false });
|
||||
|
@ -801,7 +835,7 @@ onUnmounted(() => {
|
|||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 800px;
|
||||
max-width: 600px;
|
||||
min-height: 600px;
|
||||
margin: auto;
|
||||
box-sizing: border-box;
|
||||
|
@ -947,7 +981,7 @@ onUnmounted(() => {
|
|||
.handTilesOfToimen {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 80px;
|
||||
right: 40px;
|
||||
}
|
||||
.handTileImgOfToimen {
|
||||
display: inline-block;
|
||||
|
@ -963,14 +997,14 @@ onUnmounted(() => {
|
|||
|
||||
.handTilesOfSimotya {
|
||||
position: absolute;
|
||||
top: 80px;
|
||||
bottom: 80px;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.handTilesOfMe {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 80px;
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
.huroTilesOfMe {
|
||||
|
@ -1066,6 +1100,43 @@ onUnmounted(() => {
|
|||
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr;
|
||||
}
|
||||
|
||||
.playersContainer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
pointer-events: none;
|
||||
}
|
||||
.player {
|
||||
position: absolute;
|
||||
margin: auto;
|
||||
width: 180px;
|
||||
height: min-content;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
background: #0009;
|
||||
color: #fff;
|
||||
border-radius: 5px;
|
||||
font-size: 90%;
|
||||
}
|
||||
.playerOfToimen {
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
.playerOfKamitya {
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
.playerOfSimotya {
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.serifContainer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
|
212
packages/misskey-mahjong/jest.config.cjs
Normal file
212
packages/misskey-mahjong/jest.config.cjs
Normal file
|
@ -0,0 +1,212 @@
|
|||
/*
|
||||
* For a detailed explanation regarding each configuration property and type check, visit:
|
||||
* https://jestjs.io/docs/en/configuration.html
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
// All imported modules in your tests should be mocked automatically
|
||||
// automock: false,
|
||||
|
||||
// Stop running tests after `n` failures
|
||||
// bail: 0,
|
||||
|
||||
// The directory where Jest should store its cached dependency information
|
||||
// cacheDirectory: "C:\\Users\\ai\\AppData\\Local\\Temp\\jest",
|
||||
|
||||
// Automatically clear mock calls and instances between every test
|
||||
// clearMocks: false,
|
||||
|
||||
// Indicates whether the coverage information should be collected while executing the test
|
||||
// collectCoverage: false,
|
||||
|
||||
// An array of glob patterns indicating a set of files for which coverage information should be collected
|
||||
collectCoverageFrom: ['src/**/*.ts'],
|
||||
|
||||
// The directory where Jest should output its coverage files
|
||||
coverageDirectory: "coverage",
|
||||
|
||||
// An array of regexp pattern strings used to skip coverage collection
|
||||
// coveragePathIgnorePatterns: [
|
||||
// "\\\\node_modules\\\\"
|
||||
// ],
|
||||
|
||||
// Indicates which provider should be used to instrument code for coverage
|
||||
coverageProvider: "v8",
|
||||
|
||||
// A list of reporter names that Jest uses when writing coverage reports
|
||||
// coverageReporters: [
|
||||
// "json",
|
||||
// "text",
|
||||
// "lcov",
|
||||
// "clover"
|
||||
// ],
|
||||
|
||||
// An object that configures minimum threshold enforcement for coverage results
|
||||
// coverageThreshold: undefined,
|
||||
|
||||
// A path to a custom dependency extractor
|
||||
// dependencyExtractor: undefined,
|
||||
|
||||
// Make calling deprecated APIs throw helpful error messages
|
||||
// errorOnDeprecated: false,
|
||||
|
||||
// Force coverage collection from ignored files using an array of glob patterns
|
||||
// forceCoverageMatch: [],
|
||||
|
||||
// A path to a module which exports an async function that is triggered once before all test suites
|
||||
// globalSetup: undefined,
|
||||
|
||||
// A path to a module which exports an async function that is triggered once after all test suites
|
||||
// globalTeardown: undefined,
|
||||
|
||||
// A set of global variables that need to be available in all test environments
|
||||
//globals: {
|
||||
//"ts-jest": {
|
||||
//"useESM": true,
|
||||
//diagnostics: {
|
||||
//exclude: ['!test/**/*.ts'],
|
||||
//},
|
||||
//}
|
||||
//},
|
||||
|
||||
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
|
||||
// maxWorkers: "50%",
|
||||
|
||||
// An array of directory names to be searched recursively up from the requiring module's location
|
||||
// moduleDirectories: [
|
||||
// "node_modules"
|
||||
// ],
|
||||
|
||||
// An array of file extensions your modules use
|
||||
// moduleFileExtensions: [
|
||||
// "js",
|
||||
// "json",
|
||||
// "jsx",
|
||||
// "ts",
|
||||
// "tsx",
|
||||
// "node"
|
||||
// ],
|
||||
|
||||
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
|
||||
// moduleNameMapper: {},
|
||||
|
||||
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
|
||||
// modulePathIgnorePatterns: [],
|
||||
|
||||
// Activates notifications for test results
|
||||
// notify: false,
|
||||
|
||||
// An enum that specifies notification mode. Requires { notify: true }
|
||||
// notifyMode: "failure-change",
|
||||
|
||||
// A preset that is used as a base for Jest's configuration
|
||||
preset: "ts-jest/presets/js-with-ts-esm",
|
||||
|
||||
// Run tests from one or more projects
|
||||
// projects: undefined,
|
||||
|
||||
// Use this configuration option to add custom reporters to Jest
|
||||
// reporters: undefined,
|
||||
|
||||
// Automatically reset mock state between every test
|
||||
// resetMocks: false,
|
||||
|
||||
// Reset the module registry before running each individual test
|
||||
// resetModules: false,
|
||||
|
||||
// A path to a custom resolver
|
||||
resolver: "ts-jest-resolver",
|
||||
|
||||
// Automatically restore mock state between every test
|
||||
// restoreMocks: false,
|
||||
|
||||
// The root directory that Jest should scan for tests and modules within
|
||||
// rootDir: undefined,
|
||||
|
||||
// A list of paths to directories that Jest should use to search for files in
|
||||
roots: [
|
||||
"<rootDir>"
|
||||
],
|
||||
|
||||
// Allows you to use a custom runner instead of Jest's default test runner
|
||||
// runner: "jest-runner",
|
||||
|
||||
// The paths to modules that run some code to configure or set up the testing environment before each test
|
||||
// setupFiles: [],
|
||||
|
||||
// A list of paths to modules that run some code to configure or set up the testing framework before each test
|
||||
// setupFilesAfterEnv: [],
|
||||
|
||||
// The number of seconds after which a test is considered as slow and reported as such in the results.
|
||||
// slowTestThreshold: 5,
|
||||
|
||||
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
|
||||
// snapshotSerializers: [],
|
||||
|
||||
// The test environment that will be used for testing
|
||||
testEnvironment: "node",
|
||||
|
||||
// Options that will be passed to the testEnvironment
|
||||
// testEnvironmentOptions: {},
|
||||
|
||||
// Adds a location field to test results
|
||||
// testLocationInResults: false,
|
||||
|
||||
// The glob patterns Jest uses to detect test files
|
||||
testMatch: [
|
||||
"**/__tests__/**/*.[jt]s?(x)",
|
||||
"**/?(*.)+(spec|test).[tj]s?(x)",
|
||||
"<rootDir>/test/**/*"
|
||||
],
|
||||
|
||||
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
|
||||
// testPathIgnorePatterns: [
|
||||
// "\\\\node_modules\\\\"
|
||||
// ],
|
||||
|
||||
// The regexp pattern or array of patterns that Jest uses to detect test files
|
||||
// testRegex: [],
|
||||
|
||||
// This option allows the use of a custom results processor
|
||||
// testResultsProcessor: undefined,
|
||||
|
||||
// This option allows use of a custom test runner
|
||||
// testRunner: "jasmine2",
|
||||
|
||||
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
|
||||
// testURL: "http://localhost",
|
||||
|
||||
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
|
||||
// timers: "real",
|
||||
|
||||
// A map from regular expressions to paths to transformers
|
||||
transform: {
|
||||
"^.+\\.(ts|tsx)$": [
|
||||
"ts-jest",
|
||||
{
|
||||
"useESM": true,
|
||||
diagnostics: {
|
||||
exclude: ['!test/**/*.ts'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
|
||||
// transformIgnorePatterns: [
|
||||
// "\\\\node_modules\\\\",
|
||||
// "\\.pnp\\.[^\\\\]+$"
|
||||
// ],
|
||||
|
||||
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
|
||||
// unmockedModulePathPatterns: undefined,
|
||||
|
||||
// Indicates whether each individual test should be reported during the run
|
||||
// verbose: undefined,
|
||||
|
||||
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
|
||||
// watchPathIgnorePatterns: [],
|
||||
|
||||
// Whether to use watchman for file crawling
|
||||
// watchman: true
|
||||
};
|
|
@ -22,16 +22,22 @@
|
|||
"watch": "nodemon -w src -e ts,js,cjs,mjs,json --exec \"pnpm run build:tsc\"",
|
||||
"eslint": "eslint . --ext .js,.jsx,.ts,.tsx",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "pnpm typecheck && pnpm eslint"
|
||||
"lint": "pnpm typecheck && pnpm eslint",
|
||||
"jest": "jest --coverage --detectOpenHandles",
|
||||
"test": "npm run jest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@misskey-dev/eslint-plugin": "1.0.0",
|
||||
"@types/node": "20.11.5",
|
||||
"@types/node": "20.11.17",
|
||||
"@types/jest": "29.5.12",
|
||||
"@typescript-eslint/eslint-plugin": "6.18.1",
|
||||
"@typescript-eslint/parser": "6.18.1",
|
||||
"eslint": "8.56.0",
|
||||
"nodemon": "3.0.2",
|
||||
"typescript": "5.3.3"
|
||||
"typescript": "5.3.3",
|
||||
"jest": "29.7.0",
|
||||
"ts-jest": "29.1.2",
|
||||
"ts-jest-resolver": "2.0.1"
|
||||
},
|
||||
"files": [
|
||||
"built"
|
||||
|
|
|
@ -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: 両面待ちかどうか
|
||||
|
||||
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;
|
||||
|
||||
// 全て順子か?
|
||||
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;
|
||||
|
||||
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;
|
||||
export function isManzu<T extends TileType>(tile: T): tile is typeof MANZU_TILES[number] {
|
||||
return MANZU_TILES.includes(tile);
|
||||
}
|
||||
|
||||
export function isPinzu<T extends TileType>(tile: T): tile is typeof PINZU_TILES[number] {
|
||||
return PINZU_TILES.includes(tile);
|
||||
}
|
||||
|
||||
export function isSouzu<T extends TileType>(tile: T): tile is typeof SOUZU_TILES[number] {
|
||||
return SOUZU_TILES.includes(tile);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
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 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) &&
|
||||
|
|
20
packages/misskey-mahjong/test/yaku.ts
Normal file
20
packages/misskey-mahjong/test/yaku.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as assert from 'node:assert';
|
||||
import { calcYakus } from '../src/common.yaku.js';
|
||||
|
||||
describe('Yaku', () => {
|
||||
describe('Riichi', () => {
|
||||
it('valid', () => {
|
||||
assert.deepStrictEqual(calcYakus({
|
||||
house: 'e',
|
||||
handTiles: ['m1', 'm2', 'm3', 'p6', 'p6', 'p6', 's6', 's7', 's8', 'n', 'n', 'n', 'm3', 'm3'],
|
||||
huros: [],
|
||||
riichi: true,
|
||||
}), ['riichi']);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -27,7 +27,6 @@
|
|||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"test/**/*"
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1190,9 +1190,12 @@ importers:
|
|||
'@misskey-dev/eslint-plugin':
|
||||
specifier: 1.0.0
|
||||
version: 1.0.0(@typescript-eslint/eslint-plugin@6.18.1)(@typescript-eslint/parser@6.18.1)(eslint-plugin-import@2.29.1)(eslint@8.56.0)
|
||||
'@types/jest':
|
||||
specifier: 29.5.12
|
||||
version: 29.5.12
|
||||
'@types/node':
|
||||
specifier: 20.11.5
|
||||
version: 20.11.5
|
||||
specifier: 20.11.17
|
||||
version: 20.11.17
|
||||
'@typescript-eslint/eslint-plugin':
|
||||
specifier: 6.18.1
|
||||
version: 6.18.1(@typescript-eslint/parser@6.18.1)(eslint@8.56.0)(typescript@5.3.3)
|
||||
|
@ -1202,9 +1205,18 @@ importers:
|
|||
eslint:
|
||||
specifier: 8.56.0
|
||||
version: 8.56.0
|
||||
jest:
|
||||
specifier: 29.7.0
|
||||
version: 29.7.0(@types/node@20.11.17)
|
||||
nodemon:
|
||||
specifier: 3.0.2
|
||||
version: 3.0.2
|
||||
ts-jest:
|
||||
specifier: 29.1.2
|
||||
version: 29.1.2(@babel/core@7.23.5)(esbuild@0.19.11)(jest@29.7.0)(typescript@5.3.3)
|
||||
ts-jest-resolver:
|
||||
specifier: 2.0.1
|
||||
version: 2.0.1
|
||||
typescript:
|
||||
specifier: 5.3.3
|
||||
version: 5.3.3
|
||||
|
@ -8252,6 +8264,13 @@ packages:
|
|||
pretty-format: 29.7.0
|
||||
dev: true
|
||||
|
||||
/@types/jest@29.5.12:
|
||||
resolution: {integrity: sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==}
|
||||
dependencies:
|
||||
expect: 29.7.0
|
||||
pretty-format: 29.7.0
|
||||
dev: true
|
||||
|
||||
/@types/js-yaml@4.0.9:
|
||||
resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==}
|
||||
dev: true
|
||||
|
@ -10024,6 +10043,13 @@ packages:
|
|||
node-releases: 2.0.14
|
||||
update-browserslist-db: 1.0.13(browserslist@4.22.2)
|
||||
|
||||
/bs-logger@0.2.6:
|
||||
resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==}
|
||||
engines: {node: '>= 6'}
|
||||
dependencies:
|
||||
fast-json-stable-stringify: 2.1.0
|
||||
dev: true
|
||||
|
||||
/bser@2.1.1:
|
||||
resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==}
|
||||
dependencies:
|
||||
|
@ -15048,7 +15074,6 @@ packages:
|
|||
|
||||
/lodash.memoize@4.1.2:
|
||||
resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==}
|
||||
dev: false
|
||||
|
||||
/lodash.merge@4.6.2:
|
||||
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
||||
|
@ -15193,6 +15218,10 @@ packages:
|
|||
semver: 7.5.4
|
||||
dev: true
|
||||
|
||||
/make-error@1.3.6:
|
||||
resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
|
||||
dev: true
|
||||
|
||||
/make-fetch-happen@13.0.0:
|
||||
resolution: {integrity: sha512-7ThobcL8brtGo9CavByQrQi+23aIfgYU++wg4B87AIS8Rb2ZBt/MEaDqzA00Xwv/jUjAjYkLHjVolYuTLKda2A==}
|
||||
engines: {node: ^16.14.0 || >=18.0.0}
|
||||
|
@ -19399,6 +19428,47 @@ packages:
|
|||
engines: {node: '>=6.10'}
|
||||
dev: true
|
||||
|
||||
/ts-jest-resolver@2.0.1:
|
||||
resolution: {integrity: sha512-FolE73BqVZCs8/RbLKxC67iaAtKpBWx7PeLKFW2zJQlOf9j851I7JRxSDenri2NFvVH3QP7v3S8q1AmL24Zb9Q==}
|
||||
dependencies:
|
||||
jest-resolve: 29.7.0
|
||||
dev: true
|
||||
|
||||
/ts-jest@29.1.2(@babel/core@7.23.5)(esbuild@0.19.11)(jest@29.7.0)(typescript@5.3.3):
|
||||
resolution: {integrity: sha512-br6GJoH/WUX4pu7FbZXuWGKGNDuU7b8Uj77g/Sp7puZV6EXzuByl6JrECvm0MzVzSTkSHWTihsXt+5XYER5b+g==}
|
||||
engines: {node: ^16.10.0 || ^18.0.0 || >=20.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@babel/core': '>=7.0.0-beta.0 <8'
|
||||
'@jest/types': ^29.0.0
|
||||
babel-jest: ^29.0.0
|
||||
esbuild: '*'
|
||||
jest: ^29.0.0
|
||||
typescript: '>=4.3 <6'
|
||||
peerDependenciesMeta:
|
||||
'@babel/core':
|
||||
optional: true
|
||||
'@jest/types':
|
||||
optional: true
|
||||
babel-jest:
|
||||
optional: true
|
||||
esbuild:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/core': 7.23.5
|
||||
bs-logger: 0.2.6
|
||||
esbuild: 0.19.11
|
||||
fast-json-stable-stringify: 2.1.0
|
||||
jest: 29.7.0(@types/node@20.11.17)
|
||||
jest-util: 29.7.0
|
||||
json5: 2.2.3
|
||||
lodash.memoize: 4.1.2
|
||||
make-error: 1.3.6
|
||||
semver: 7.5.4
|
||||
typescript: 5.3.3
|
||||
yargs-parser: 21.1.1
|
||||
dev: true
|
||||
|
||||
/ts-map@1.0.3:
|
||||
resolution: {integrity: sha512-vDWbsl26LIcPGmDpoVzjEP6+hvHZkBkLW7JpvwbCv/5IYPJlsbzCVXY3wsCeAxAUeTclNOUZxnLdGh3VBD/J6w==}
|
||||
dev: true
|
||||
|
|
Loading…
Reference in a new issue