wip
This commit is contained in:
parent
c99d55e0cb
commit
c47203b888
|
@ -2631,7 +2631,7 @@ _mahjong:
|
||||||
"chinroto": "清老頭"
|
"chinroto": "清老頭"
|
||||||
"sukantsu": "四槓子"
|
"sukantsu": "四槓子"
|
||||||
"churen": "九蓮宝燈"
|
"churen": "九蓮宝燈"
|
||||||
"pure-churen": "純正九連宝灯"
|
"churen-9": "九連宝灯九面待"
|
||||||
"tenho": "天和"
|
"tenho": "天和"
|
||||||
"chiho": "地和"
|
"chiho": "地和"
|
||||||
|
|
||||||
|
|
|
@ -657,7 +657,7 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit {
|
||||||
|
|
||||||
if (mj.riichis[house]) {
|
if (mj.riichis[house]) {
|
||||||
// リーチ時はアガリ牌でない限りツモ切り
|
// リーチ時はアガリ牌でない限りツモ切り
|
||||||
if (!Mmj.canHora(mj.handTileTypes[house])) {
|
if (!Mmj.isAgarikei(mj.handTileTypes[house])) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.dahai(room, mj, house, mj.handTiles[house].at(-1));
|
this.dahai(room, mj, house, mj.handTiles[house].at(-1));
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
|
@ -123,6 +123,33 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
</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"/>
|
<XHandTiles :class="$style.handTilesOfMe" :tiles="mj.myHandTiles" :doras="mj.doras" :selectableTiles="selectableTiles" :separateLast="isMyTurn && iTsumoed" @choose="chooseTile"/>
|
||||||
|
|
||||||
<div :class="$style.serifContainer">
|
<div :class="$style.serifContainer">
|
||||||
|
@ -292,9 +319,16 @@ const isMyTurn = computed(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const canHora = 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 selectableTiles = ref<Mmj.TileType[] | null>(null);
|
||||||
const ronSerifHouses = reactive<Record<Mmj.House, boolean>>({ e: false, s: false, w: false, n: false });
|
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 });
|
const ciiSerifHouses = reactive<Record<Mmj.House, boolean>>({ e: false, s: false, w: false, n: false });
|
||||||
|
@ -801,7 +835,7 @@ onUnmounted(() => {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
max-width: 800px;
|
max-width: 600px;
|
||||||
min-height: 600px;
|
min-height: 600px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -947,7 +981,7 @@ onUnmounted(() => {
|
||||||
.handTilesOfToimen {
|
.handTilesOfToimen {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 80px;
|
right: 40px;
|
||||||
}
|
}
|
||||||
.handTileImgOfToimen {
|
.handTileImgOfToimen {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
@ -963,14 +997,14 @@ onUnmounted(() => {
|
||||||
|
|
||||||
.handTilesOfSimotya {
|
.handTilesOfSimotya {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 80px;
|
bottom: 80px;
|
||||||
right: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.handTilesOfMe {
|
.handTilesOfMe {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 80px;
|
left: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.huroTilesOfMe {
|
.huroTilesOfMe {
|
||||||
|
@ -1066,6 +1100,43 @@ onUnmounted(() => {
|
||||||
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr;
|
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 {
|
.serifContainer {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
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\"",
|
"watch": "nodemon -w src -e ts,js,cjs,mjs,json --exec \"pnpm run build:tsc\"",
|
||||||
"eslint": "eslint . --ext .js,.jsx,.ts,.tsx",
|
"eslint": "eslint . --ext .js,.jsx,.ts,.tsx",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"lint": "pnpm typecheck && pnpm eslint"
|
"lint": "pnpm typecheck && pnpm eslint",
|
||||||
|
"jest": "jest --coverage --detectOpenHandles",
|
||||||
|
"test": "npm run jest"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@misskey-dev/eslint-plugin": "1.0.0",
|
"@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/eslint-plugin": "6.18.1",
|
||||||
"@typescript-eslint/parser": "6.18.1",
|
"@typescript-eslint/parser": "6.18.1",
|
||||||
"eslint": "8.56.0",
|
"eslint": "8.56.0",
|
||||||
"nodemon": "3.0.2",
|
"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": [
|
"files": [
|
||||||
"built"
|
"built"
|
||||||
|
|
|
@ -237,297 +237,68 @@ export const PREV_TILE_FOR_SHUNTSU: Record<TileType, TileType | null> = {
|
||||||
chun: null,
|
chun: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const KOKUSHI_TILES: TileType[] = ['m1', 'm9', 'p1', 'p9', 's1', 's9', 'e', 's', 'w', 'n', 'haku', 'hatsu', 'chun'];
|
export const TILE_NUMBER_MAP: Record<TileType, number | null> = {
|
||||||
|
m1: 1,
|
||||||
type EnvForCalcYaku = {
|
m2: 2,
|
||||||
house: House;
|
m3: 3,
|
||||||
|
m4: 4,
|
||||||
/**
|
m5: 5,
|
||||||
* 和了る人の手牌(副露牌および和了る際のツモ牌・ロン牌は含まない)
|
m6: 6,
|
||||||
*/
|
m7: 7,
|
||||||
handTiles: TileType[];
|
m8: 8,
|
||||||
|
m9: 9,
|
||||||
/**
|
p1: 1,
|
||||||
* 河
|
p2: 2,
|
||||||
*/
|
p3: 3,
|
||||||
hoTiles: TileType[];
|
p4: 4,
|
||||||
|
p5: 5,
|
||||||
/**
|
p6: 6,
|
||||||
* 副露
|
p7: 7,
|
||||||
*/
|
p8: 8,
|
||||||
huros: Huro[];
|
p9: 9,
|
||||||
|
s1: 1,
|
||||||
/**
|
s2: 2,
|
||||||
* ツモ牌
|
s3: 3,
|
||||||
*/
|
s4: 4,
|
||||||
tsumoTile: TileType | null;
|
s5: 5,
|
||||||
|
s6: 6,
|
||||||
/**
|
s7: 7,
|
||||||
* ロン牌
|
s8: 8,
|
||||||
*/
|
s9: 9,
|
||||||
ronTile: TileType | null;
|
e: null,
|
||||||
|
s: null,
|
||||||
/**
|
w: null,
|
||||||
* ドラ表示牌
|
n: null,
|
||||||
*/
|
haku: null,
|
||||||
doraTiles: TileType[];
|
hatsu: null,
|
||||||
|
chun: null,
|
||||||
/**
|
|
||||||
* 赤ドラ表示牌
|
|
||||||
*/
|
|
||||||
redDoraTiles: TileType[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 場風
|
|
||||||
*/
|
|
||||||
fieldWind: House;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 自風
|
|
||||||
*/
|
|
||||||
seatWind: House;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* リーチしたかどうか
|
|
||||||
*/
|
|
||||||
riichi: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 一巡目以内かどうか
|
|
||||||
*/
|
|
||||||
ippatsu: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const YAKU_DEFINITIONS = [{
|
export const MANZU_TILES = ['m1', 'm2', 'm3', 'm4', 'm5', 'm6', 'm7', 'm8', 'm9'] as const satisfies TileType[];
|
||||||
name: 'riichi',
|
export const PINZU_TILES = ['p1', 'p2', 'p3', 'p4', 'p5', 'p6', 'p7', 'p8', 'p9'] as const satisfies TileType[];
|
||||||
fan: 1,
|
export const SOUZU_TILES = ['s1', 's2', 's3', 's4', 's5', 's6', 's7', 's8', 's9'] as const satisfies TileType[];
|
||||||
isYakuman: false,
|
export const CHAR_TILES = ['e', 's', 'w', 'n', 'haku', 'hatsu', 'chun'] as const satisfies TileType[];
|
||||||
calc: (state: EnvForCalcYaku) => {
|
export const YAOCHU_TILES = ['m1', 'm9', 'p1', 'p9', 's1', 's9', 'e', 's', 'w', 'n', 'haku', 'hatsu', 'chun'] as const satisfies TileType[];
|
||||||
return state.riichi;
|
const KOKUSHI_TILES: TileType[] = ['m1', 'm9', 'p1', 'p9', 's1', 's9', 'e', 's', 'w', 'n', 'haku', 'hatsu', 'chun'];
|
||||||
},
|
|
||||||
}, {
|
|
||||||
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;
|
|
||||||
|
|
||||||
// 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)
|
export function isPinzu<T extends TileType>(tile: T): tile is typeof PINZU_TILES[number] {
|
||||||
if (horaSet.head === state.seatWind) return false;
|
return PINZU_TILES.includes(tile);
|
||||||
if (horaSet.head === state.fieldWind) return false;
|
}
|
||||||
|
|
||||||
// 全て順子か?
|
export function isSouzu<T extends TileType>(tile: T): tile is typeof SOUZU_TILES[number] {
|
||||||
if (horaSet.mentsus.some((mentsu) => mentsu[0] === mentsu[1])) return false;
|
return SOUZU_TILES.includes(tile);
|
||||||
});
|
}
|
||||||
},
|
|
||||||
}, {
|
export function isSameNumberTile(a: TileType, b: TileType): boolean {
|
||||||
name: 'iipeko',
|
const aNumber = TILE_NUMBER_MAP[a];
|
||||||
fan: 1,
|
const bNumber = TILE_NUMBER_MAP[b];
|
||||||
isYakuman: false,
|
if (aNumber == null || bNumber == null) return false;
|
||||||
calc: (state: EnvForCalcYaku) => {
|
return aNumber === bNumber;
|
||||||
// 面前じゃないとダメ
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
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 {
|
export function fanToPoint(fan: number, isParent: boolean): number {
|
||||||
let point;
|
let point;
|
||||||
|
@ -658,11 +429,19 @@ export function prevHouse(house: House): House {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type HoraSet = {
|
export type FourMentsuOneJyantou = {
|
||||||
head: TileType;
|
head: TileType;
|
||||||
mentsus: [TileType, TileType, 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][] = [
|
export const SHUNTU_PATTERNS: [TileType, TileType, TileType][] = [
|
||||||
['m1', 'm2', 'm3'],
|
['m1', 'm2', 'm3'],
|
||||||
['m2', 'm3', 'm4'],
|
['m2', 'm3', 'm4'],
|
||||||
|
@ -720,13 +499,8 @@ function extractShuntsus(tiles: TileType[]): [TileType, TileType, TileType][] {
|
||||||
return shuntsus;
|
return shuntsus;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export function analyzeFourMentsuOneJyantou(handTiles: TileType[], all = true): FourMentsuOneJyantou[] {
|
||||||
* アガリ形パターン一覧を取得
|
const horaSets: FourMentsuOneJyantou[] = [];
|
||||||
* @param handTiles ポン、チー、カンした牌を含まない手牌
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
function analyze1head3mentsuSets(handTiles: TileType[]): HoraSet[] {
|
|
||||||
const horaSets: HoraSet[] = [];
|
|
||||||
|
|
||||||
const headSet: TileType[] = [];
|
const headSet: TileType[] = [];
|
||||||
const countMap = new Map<TileType, number>();
|
const countMap = new Map<TileType, number>();
|
||||||
|
@ -817,6 +591,8 @@ function analyze1head3mentsuSets(handTiles: TileType[]): HoraSet[] {
|
||||||
head,
|
head,
|
||||||
mentsus: [...kotsuPattern.map(t => [t, t, t] as [TileType, TileType, TileType]), ...shuntsus],
|
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;
|
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 {
|
export function nextTileForDora(tile: TileType): TileType {
|
||||||
return NEXT_TILE_FOR_DORA_MAP[tile];
|
return NEXT_TILE_FOR_DORA_MAP[tile];
|
||||||
}
|
}
|
||||||
|
@ -893,3 +627,40 @@ export function getAvailableCiiPatterns(handTiles: TileType[], targetTile: TileT
|
||||||
}
|
}
|
||||||
return patterns;
|
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: ポンされるなどして自分の河にない場合の考慮
|
// TODO: ポンされるなどして自分の河にない場合の考慮
|
||||||
if (this.hoTileTypes[house].includes($type(tid))) return false;
|
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
|
// TODO
|
||||||
//const yakus = YAKU_DEFINITIONS.filter(yaku => yaku.calc(this.state, { tsumoTile: null, ronTile: tile }));
|
//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');
|
if (tx.$state.riichis[house]) throw new Error('Already riichi');
|
||||||
const tempHandTiles = [...tx.handTileTypes[house]];
|
const tempHandTiles = [...tx.handTileTypes[house]];
|
||||||
tempHandTiles.splice(tempHandTiles.indexOf($type(tid)), 1);
|
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');
|
if (tx.$state.points[house] < 1000) throw new Error('Not enough points');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -217,8 +217,12 @@ export class PlayerGameEngine {
|
||||||
this.state.turn = null;
|
this.state.turn = null;
|
||||||
|
|
||||||
if (house === this.myHouse) {
|
if (house === this.myHouse) {
|
||||||
|
this.state.canRon = null;
|
||||||
|
this.state.canPon = null;
|
||||||
|
this.state.canKan = null;
|
||||||
|
this.state.canCii = null;
|
||||||
} else {
|
} 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 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 canKan = !this.isMeRiichi && this.myHandTileTypes.filter(t => t === $type(tid)).length === 3;
|
||||||
const canCii = !this.isMeRiichi && house === Common.prevHouse(this.myHouse) &&
|
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/**/*"
|
"src/**/*"
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"node_modules",
|
"node_modules"
|
||||||
"test/**/*"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1190,9 +1190,12 @@ importers:
|
||||||
'@misskey-dev/eslint-plugin':
|
'@misskey-dev/eslint-plugin':
|
||||||
specifier: 1.0.0
|
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)
|
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':
|
'@types/node':
|
||||||
specifier: 20.11.5
|
specifier: 20.11.17
|
||||||
version: 20.11.5
|
version: 20.11.17
|
||||||
'@typescript-eslint/eslint-plugin':
|
'@typescript-eslint/eslint-plugin':
|
||||||
specifier: 6.18.1
|
specifier: 6.18.1
|
||||||
version: 6.18.1(@typescript-eslint/parser@6.18.1)(eslint@8.56.0)(typescript@5.3.3)
|
version: 6.18.1(@typescript-eslint/parser@6.18.1)(eslint@8.56.0)(typescript@5.3.3)
|
||||||
|
@ -1202,9 +1205,18 @@ importers:
|
||||||
eslint:
|
eslint:
|
||||||
specifier: 8.56.0
|
specifier: 8.56.0
|
||||||
version: 8.56.0
|
version: 8.56.0
|
||||||
|
jest:
|
||||||
|
specifier: 29.7.0
|
||||||
|
version: 29.7.0(@types/node@20.11.17)
|
||||||
nodemon:
|
nodemon:
|
||||||
specifier: 3.0.2
|
specifier: 3.0.2
|
||||||
version: 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:
|
typescript:
|
||||||
specifier: 5.3.3
|
specifier: 5.3.3
|
||||||
version: 5.3.3
|
version: 5.3.3
|
||||||
|
@ -8252,6 +8264,13 @@ packages:
|
||||||
pretty-format: 29.7.0
|
pretty-format: 29.7.0
|
||||||
dev: true
|
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:
|
/@types/js-yaml@4.0.9:
|
||||||
resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==}
|
resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -10024,6 +10043,13 @@ packages:
|
||||||
node-releases: 2.0.14
|
node-releases: 2.0.14
|
||||||
update-browserslist-db: 1.0.13(browserslist@4.22.2)
|
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:
|
/bser@2.1.1:
|
||||||
resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==}
|
resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -15048,7 +15074,6 @@ packages:
|
||||||
|
|
||||||
/lodash.memoize@4.1.2:
|
/lodash.memoize@4.1.2:
|
||||||
resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==}
|
resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==}
|
||||||
dev: false
|
|
||||||
|
|
||||||
/lodash.merge@4.6.2:
|
/lodash.merge@4.6.2:
|
||||||
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
||||||
|
@ -15193,6 +15218,10 @@ packages:
|
||||||
semver: 7.5.4
|
semver: 7.5.4
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/make-error@1.3.6:
|
||||||
|
resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/make-fetch-happen@13.0.0:
|
/make-fetch-happen@13.0.0:
|
||||||
resolution: {integrity: sha512-7ThobcL8brtGo9CavByQrQi+23aIfgYU++wg4B87AIS8Rb2ZBt/MEaDqzA00Xwv/jUjAjYkLHjVolYuTLKda2A==}
|
resolution: {integrity: sha512-7ThobcL8brtGo9CavByQrQi+23aIfgYU++wg4B87AIS8Rb2ZBt/MEaDqzA00Xwv/jUjAjYkLHjVolYuTLKda2A==}
|
||||||
engines: {node: ^16.14.0 || >=18.0.0}
|
engines: {node: ^16.14.0 || >=18.0.0}
|
||||||
|
@ -19399,6 +19428,47 @@ packages:
|
||||||
engines: {node: '>=6.10'}
|
engines: {node: '>=6.10'}
|
||||||
dev: true
|
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:
|
/ts-map@1.0.3:
|
||||||
resolution: {integrity: sha512-vDWbsl26LIcPGmDpoVzjEP6+hvHZkBkLW7JpvwbCv/5IYPJlsbzCVXY3wsCeAxAUeTclNOUZxnLdGh3VBD/J6w==}
|
resolution: {integrity: sha512-vDWbsl26LIcPGmDpoVzjEP6+hvHZkBkLW7JpvwbCv/5IYPJlsbzCVXY3wsCeAxAUeTclNOUZxnLdGh3VBD/J6w==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
Loading…
Reference in a new issue