1
0
mirror of https://github.com/misskey-dev/misskey.git synced 2026-05-25 15:34:03 +02:00
This commit is contained in:
syuilo
2024-02-02 20:56:39 +09:00
parent 9ea29fe84c
commit 7cdaa10d46
9 changed files with 303 additions and 303 deletions

View File

@@ -360,3 +360,249 @@ export function calcTsumoHoraPointDeltas(house: House, fans: number): Record<Hou
return deltas;
}
export function isTile(tile: string): tile is Tile {
return TILE_TYPES.includes(tile as Tile);
}
export function sortTiles(tiles: Tile[]): Tile[] {
tiles.sort((a, b) => {
const aIndex = TILE_TYPES.indexOf(a);
const bIndex = TILE_TYPES.indexOf(b);
return aIndex - bIndex;
});
return tiles;
}
export function nextHouse(house: House): House {
switch (house) {
case 'e': return 's';
case 's': return 'w';
case 'w': return 'n';
case 'n': return 'e';
default: throw new Error(`unrecognized house: ${house}`);
}
}
export function prevHouse(house: House): House {
switch (house) {
case 'e': return 'n';
case 's': return 'e';
case 'w': return 's';
case 'n': return 'w';
default: throw new Error(`unrecognized house: ${house}`);
}
}
type HoraSet = {
head: Tile;
mentsus: [Tile, Tile, Tile][];
};
export const SHUNTU_PATTERNS: [Tile, Tile, Tile][] = [
['m1', 'm2', 'm3'],
['m2', 'm3', 'm4'],
['m3', 'm4', 'm5'],
['m4', 'm5', 'm6'],
['m5', 'm6', 'm7'],
['m6', 'm7', 'm8'],
['m7', 'm8', 'm9'],
['p1', 'p2', 'p3'],
['p2', 'p3', 'p4'],
['p3', 'p4', 'p5'],
['p4', 'p5', 'p6'],
['p5', 'p6', 'p7'],
['p6', 'p7', 'p8'],
['p7', 'p8', 'p9'],
['s1', 's2', 's3'],
['s2', 's3', 's4'],
['s3', 's4', 's5'],
['s4', 's5', 's6'],
['s5', 's6', 's7'],
['s6', 's7', 's8'],
['s7', 's8', 's9'],
];
const SHUNTU_PATTERN_IDS = [
'm123',
'm234',
'm345',
'm456',
'm567',
'm678',
'm789',
'p123',
'p234',
'p345',
'p456',
'p567',
'p678',
'p789',
's123',
's234',
's345',
's456',
's567',
's678',
's789',
] as const;
/**
* アガリ形パターン一覧を取得
* @param handTiles ポン、チー、カンした牌を含まない手牌
* @returns
*/
export function getHoraSets(handTiles: Tile[]): HoraSet[] {
const horaSets: HoraSet[] = [];
const headSet: Tile[] = [];
const countMap = new Map<Tile, number>();
for (const tile of handTiles) {
const count = (countMap.get(tile) ?? 0) + 1;
countMap.set(tile, count);
if (count === 2) {
headSet.push(tile);
}
}
for (const head of headSet) {
const tempHandTiles = [...handTiles];
tempHandTiles.splice(tempHandTiles.indexOf(head), 1);
tempHandTiles.splice(tempHandTiles.indexOf(head), 1);
const kotsuTileSet: Tile[] = []; // インデックスアクセスしたいため配列だが実態はSet
for (const [t, c] of countMap.entries()) {
if (t === head) continue; // 同じ牌種は4枚しかないので、頭と同じ牌種は刻子になりえない
if (c >= 3) {
kotsuTileSet.push(t);
}
}
let kotsuPatterns: Tile[][];
if (kotsuTileSet.length === 0) {
kotsuPatterns = [
[],
];
} else if (kotsuTileSet.length === 1) {
kotsuPatterns = [
[],
[kotsuTileSet[0]],
];
} else if (kotsuTileSet.length === 2) {
kotsuPatterns = [
[],
[kotsuTileSet[0]],
[kotsuTileSet[1]],
[kotsuTileSet[0], kotsuTileSet[1]],
];
} else if (kotsuTileSet.length === 3) {
kotsuPatterns = [
[],
[kotsuTileSet[0]],
[kotsuTileSet[1]],
[kotsuTileSet[2]],
[kotsuTileSet[0], kotsuTileSet[1]],
[kotsuTileSet[0], kotsuTileSet[2]],
[kotsuTileSet[1], kotsuTileSet[2]],
[kotsuTileSet[0], kotsuTileSet[1], kotsuTileSet[2]],
];
} else if (kotsuTileSet.length === 4) {
kotsuPatterns = [
[],
[kotsuTileSet[0]],
[kotsuTileSet[1]],
[kotsuTileSet[2]],
[kotsuTileSet[3]],
[kotsuTileSet[0], kotsuTileSet[1]],
[kotsuTileSet[0], kotsuTileSet[2]],
[kotsuTileSet[0], kotsuTileSet[3]],
[kotsuTileSet[1], kotsuTileSet[2]],
[kotsuTileSet[1], kotsuTileSet[3]],
[kotsuTileSet[2], kotsuTileSet[3]],
[kotsuTileSet[0], kotsuTileSet[1], kotsuTileSet[2]],
[kotsuTileSet[0], kotsuTileSet[1], kotsuTileSet[3]],
[kotsuTileSet[0], kotsuTileSet[2], kotsuTileSet[3]],
[kotsuTileSet[1], kotsuTileSet[2], kotsuTileSet[3]],
[kotsuTileSet[0], kotsuTileSet[1], kotsuTileSet[2], kotsuTileSet[3]],
];
} else {
throw new Error('arienai');
}
for (const kotsuPattern of kotsuPatterns) {
const tempHandTilesWithoutKotsu = [...tempHandTiles];
for (const kotsuTile of kotsuPattern) {
tempHandTilesWithoutKotsu.splice(tempHandTilesWithoutKotsu.indexOf(kotsuTile), 1);
tempHandTilesWithoutKotsu.splice(tempHandTilesWithoutKotsu.indexOf(kotsuTile), 1);
tempHandTilesWithoutKotsu.splice(tempHandTilesWithoutKotsu.indexOf(kotsuTile), 1);
}
tempHandTilesWithoutKotsu.sort((a, b) => {
const aIndex = TILE_TYPES.indexOf(a);
const bIndex = TILE_TYPES.indexOf(b);
return aIndex - bIndex;
});
const tempHandTilesWithoutKotsuAndShuntsu: (Tile | null)[] = [...tempHandTilesWithoutKotsu];
const shuntsus: [Tile, Tile, Tile][] = [];
while (tempHandTilesWithoutKotsuAndShuntsu.length > 0) {
let isShuntu = false;
for (const shuntuPattern of SHUNTU_PATTERNS) {
if (
tempHandTilesWithoutKotsuAndShuntsu[0] === shuntuPattern[0] &&
tempHandTilesWithoutKotsuAndShuntsu.includes(shuntuPattern[1]) &&
tempHandTilesWithoutKotsuAndShuntsu.includes(shuntuPattern[2])
) {
shuntsus.push(shuntuPattern);
tempHandTilesWithoutKotsuAndShuntsu.splice(0, 1);
tempHandTilesWithoutKotsuAndShuntsu.splice(tempHandTilesWithoutKotsuAndShuntsu.indexOf(shuntuPattern[1]), 1);
tempHandTilesWithoutKotsuAndShuntsu.splice(tempHandTilesWithoutKotsuAndShuntsu.indexOf(shuntuPattern[2]), 1);
isShuntu = true;
break;
}
}
if (!isShuntu) tempHandTilesWithoutKotsuAndShuntsu.splice(0, 1);
}
if (shuntsus.length * 3 === tempHandTilesWithoutKotsu.length) { // アガリ形
horaSets.push({
head,
mentsus: [...kotsuPattern.map(t => [t, t, t] as [Tile, Tile, Tile]), ...shuntsus],
});
}
}
}
return horaSets;
}
/**
* アガリ牌リストを取得
* @param handTiles ポン、チー、カンした牌を含まない手牌
*/
export function getHoraTiles(handTiles: Tile[]): Tile[] {
return TILE_TYPES.filter(tile => {
const tempHandTiles = [...handTiles, tile];
const horaSets = getHoraSets(tempHandTiles);
return horaSets.length > 0;
});
}
// TODO: 国士無双判定関数
// TODO: 七対子判定関数
export function getTilesForRiichi(handTiles: Tile[]): Tile[] {
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: Tile): Tile {
return NEXT_TILE_FOR_DORA_MAP[tile];
}

View File

@@ -6,7 +6,6 @@
import CRC32 from 'crc-32';
import { Tile, House, Huro, TILE_TYPES, YAKU_DEFINITIONS } from './common.js';
import * as Common from './common.js';
import * as Utils from './utils.js';
import { PlayerState } from './engine.player.js';
export type MasterState = {
@@ -116,7 +115,7 @@ export class MasterGameEngine {
}
public get doras(): Tile[] {
return this.state.kingTiles.slice(0, this.state.activatedDorasCount).map(t => Utils.nextTileForDora(t));
return this.state.kingTiles.slice(0, this.state.activatedDorasCount).map(t => Common.nextTileForDora(t));
}
public static createInitialState(): MasterState {
@@ -199,7 +198,7 @@ export class MasterGameEngine {
// TODO: ポンされるなどして自分の河にない場合の考慮
if (this.state.hoTiles[house].includes(tile)) return false;
const horaSets = Utils.getHoraSets(this.state.handTiles[house].concat(tile));
const horaSets = Common.getHoraSets(this.state.handTiles[house].concat(tile));
if (horaSets.length === 0) return false; // 完成形じゃない
// TODO
@@ -213,8 +212,12 @@ export class MasterGameEngine {
return this.state.handTiles[house].filter(t => t === tile).length === 2;
}
private canCii(house: House, tile: Tile): boolean {
// TODO
private canCii(caller: House, callee: House, tile: Tile): boolean {
if (callee !== Common.prevHouse(caller)) return false;
const hand = this.state.handTiles[caller];
return Common.SHUNTU_PATTERNS.some(pattern =>
pattern.includes(tile) &&
pattern.filter(t => hand.includes(t)).length >= 2);
}
public getHouse(index: 1 | 2 | 3 | 4): House {
@@ -266,7 +269,7 @@ export class MasterGameEngine {
if (riichi) {
const tempHandTiles = [...this.state.handTiles[house]];
tempHandTiles.splice(tempHandTiles.indexOf(tile), 1);
if (Utils.getHoraTiles(tempHandTiles).length === 0) throw new Error('Not tenpai');
if (Common.getHoraTiles(tempHandTiles).length === 0) throw new Error('Not tenpai');
if (this.state.points[house] < 1000) throw new Error('Not enough points');
}
@@ -360,7 +363,7 @@ export class MasterGameEngine {
};
}
this.state.turn = null;
this.state.nextTurnAfterAsking = Utils.nextHouse(house);
this.state.nextTurnAfterAsking = Common.nextHouse(house);
return {
asking: true as const,
canRonHouses: canRonHouses,
@@ -370,7 +373,7 @@ export class MasterGameEngine {
};
}
this.state.turn = Utils.nextHouse(house);
this.state.turn = Common.nextHouse(house);
const tsumoTile = this.tsumo();

View File

@@ -6,7 +6,6 @@
import CRC32 from 'crc-32';
import { Tile, House, Huro, TILE_TYPES, YAKU_DEFINITIONS } from './common.js';
import * as Common from './common.js';
import * as Utils from './utils.js';
export type PlayerState = {
user1House: House;
@@ -97,7 +96,7 @@ export class PlayerGameEngine {
}
public get doras(): Tile[] {
return this.state.doraIndicateTiles.map(t => Utils.nextTileForDora(t));
return this.state.doraIndicateTiles.map(t => Common.nextTileForDora(t));
}
public commit_tsumo(house: House, tile: Tile) {
@@ -131,7 +130,7 @@ export class PlayerGameEngine {
if (house === this.myHouse) {
} else {
const canRon = Utils.getHoraSets(this.myHandTiles.concat(tile)).length > 0;
const canRon = Common.getHoraSets(this.myHandTiles.concat(tile)).length > 0;
const canPon = this.myHandTiles.filter(t => t === tile).length === 2;
// TODO: canCii
@@ -243,7 +242,7 @@ export class PlayerGameEngine {
if (this.state.riichis[this.myHouse]) return false;
if (this.state.points[this.myHouse] < 1000) return false;
if (!this.isMenzen) return false;
if (Utils.getTilesForRiichi(this.myHandTiles).length === 0) return false;
if (Common.getTilesForRiichi(this.myHandTiles).length === 0) return false;
return true;
}
}

View File

@@ -5,7 +5,6 @@
export * as Serializer from './serializer.js';
export * as Common from './common.js';
export * as Utils from './utils.js';
export { MasterGameEngine } from './engine.master.js';
export type { MasterState } from './engine.master.js';

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Tile } from './engine.player.js';
import { Tile } from './common.js';
export type Log = {
time: number;

View File

@@ -1,248 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { House, NEXT_TILE_FOR_DORA_MAP, TILE_TYPES, Tile } from './common.js';
export function isTile(tile: string): tile is Tile {
return TILE_TYPES.includes(tile as Tile);
}
export function sortTiles(tiles: Tile[]): Tile[] {
tiles.sort((a, b) => {
const aIndex = TILE_TYPES.indexOf(a);
const bIndex = TILE_TYPES.indexOf(b);
return aIndex - bIndex;
});
return tiles;
}
export function nextHouse(house: House): House {
switch (house) {
case 'e': return 's';
case 's': return 'w';
case 'w': return 'n';
case 'n': return 'e';
default: throw new Error(`unrecognized house: ${house}`);
}
}
export function prevHouse(house: House): House {
switch (house) {
case 'e': return 'n';
case 's': return 'e';
case 'w': return 's';
case 'n': return 'w';
default: throw new Error(`unrecognized house: ${house}`);
}
}
type HoraSet = {
head: Tile;
mentsus: [Tile, Tile, Tile][];
};
export const SHUNTU_PATTERNS: [Tile, Tile, Tile][] = [
['m1', 'm2', 'm3'],
['m2', 'm3', 'm4'],
['m3', 'm4', 'm5'],
['m4', 'm5', 'm6'],
['m5', 'm6', 'm7'],
['m6', 'm7', 'm8'],
['m7', 'm8', 'm9'],
['p1', 'p2', 'p3'],
['p2', 'p3', 'p4'],
['p3', 'p4', 'p5'],
['p4', 'p5', 'p6'],
['p5', 'p6', 'p7'],
['p6', 'p7', 'p8'],
['p7', 'p8', 'p9'],
['s1', 's2', 's3'],
['s2', 's3', 's4'],
['s3', 's4', 's5'],
['s4', 's5', 's6'],
['s5', 's6', 's7'],
['s6', 's7', 's8'],
['s7', 's8', 's9'],
];
const SHUNTU_PATTERN_IDS = [
'm123',
'm234',
'm345',
'm456',
'm567',
'm678',
'm789',
'p123',
'p234',
'p345',
'p456',
'p567',
'p678',
'p789',
's123',
's234',
's345',
's456',
's567',
's678',
's789',
] as const;
/**
* アガリ形パターン一覧を取得
* @param handTiles ポン、チー、カンした牌を含まない手牌
* @returns
*/
export function getHoraSets(handTiles: Tile[]): HoraSet[] {
const horaSets: HoraSet[] = [];
const headSet: Tile[] = [];
const countMap = new Map<Tile, number>();
for (const tile of handTiles) {
const count = (countMap.get(tile) ?? 0) + 1;
countMap.set(tile, count);
if (count === 2) {
headSet.push(tile);
}
}
for (const head of headSet) {
const tempHandTiles = [...handTiles];
tempHandTiles.splice(tempHandTiles.indexOf(head), 1);
tempHandTiles.splice(tempHandTiles.indexOf(head), 1);
const kotsuTileSet: Tile[] = []; // インデックスアクセスしたいため配列だが実態はSet
for (const [t, c] of countMap.entries()) {
if (t === head) continue; // 同じ牌種は4枚しかないので、頭と同じ牌種は刻子になりえない
if (c >= 3) {
kotsuTileSet.push(t);
}
}
let kotsuPatterns: Tile[][];
if (kotsuTileSet.length === 0) {
kotsuPatterns = [
[],
];
} else if (kotsuTileSet.length === 1) {
kotsuPatterns = [
[],
[kotsuTileSet[0]],
];
} else if (kotsuTileSet.length === 2) {
kotsuPatterns = [
[],
[kotsuTileSet[0]],
[kotsuTileSet[1]],
[kotsuTileSet[0], kotsuTileSet[1]],
];
} else if (kotsuTileSet.length === 3) {
kotsuPatterns = [
[],
[kotsuTileSet[0]],
[kotsuTileSet[1]],
[kotsuTileSet[2]],
[kotsuTileSet[0], kotsuTileSet[1]],
[kotsuTileSet[0], kotsuTileSet[2]],
[kotsuTileSet[1], kotsuTileSet[2]],
[kotsuTileSet[0], kotsuTileSet[1], kotsuTileSet[2]],
];
} else if (kotsuTileSet.length === 4) {
kotsuPatterns = [
[],
[kotsuTileSet[0]],
[kotsuTileSet[1]],
[kotsuTileSet[2]],
[kotsuTileSet[3]],
[kotsuTileSet[0], kotsuTileSet[1]],
[kotsuTileSet[0], kotsuTileSet[2]],
[kotsuTileSet[0], kotsuTileSet[3]],
[kotsuTileSet[1], kotsuTileSet[2]],
[kotsuTileSet[1], kotsuTileSet[3]],
[kotsuTileSet[2], kotsuTileSet[3]],
[kotsuTileSet[0], kotsuTileSet[1], kotsuTileSet[2]],
[kotsuTileSet[0], kotsuTileSet[1], kotsuTileSet[3]],
[kotsuTileSet[0], kotsuTileSet[2], kotsuTileSet[3]],
[kotsuTileSet[1], kotsuTileSet[2], kotsuTileSet[3]],
[kotsuTileSet[0], kotsuTileSet[1], kotsuTileSet[2], kotsuTileSet[3]],
];
} else {
throw new Error('arienai');
}
for (const kotsuPattern of kotsuPatterns) {
const tempHandTilesWithoutKotsu = [...tempHandTiles];
for (const kotsuTile of kotsuPattern) {
tempHandTilesWithoutKotsu.splice(tempHandTilesWithoutKotsu.indexOf(kotsuTile), 1);
tempHandTilesWithoutKotsu.splice(tempHandTilesWithoutKotsu.indexOf(kotsuTile), 1);
tempHandTilesWithoutKotsu.splice(tempHandTilesWithoutKotsu.indexOf(kotsuTile), 1);
}
tempHandTilesWithoutKotsu.sort((a, b) => {
const aIndex = TILE_TYPES.indexOf(a);
const bIndex = TILE_TYPES.indexOf(b);
return aIndex - bIndex;
});
const tempHandTilesWithoutKotsuAndShuntsu: (Tile | null)[] = [...tempHandTilesWithoutKotsu];
const shuntsus: [Tile, Tile, Tile][] = [];
while (tempHandTilesWithoutKotsuAndShuntsu.length > 0) {
let isShuntu = false;
for (const shuntuPattern of SHUNTU_PATTERNS) {
if (
tempHandTilesWithoutKotsuAndShuntsu[0] === shuntuPattern[0] &&
tempHandTilesWithoutKotsuAndShuntsu.includes(shuntuPattern[1]) &&
tempHandTilesWithoutKotsuAndShuntsu.includes(shuntuPattern[2])
) {
shuntsus.push(shuntuPattern);
tempHandTilesWithoutKotsuAndShuntsu.splice(0, 1);
tempHandTilesWithoutKotsuAndShuntsu.splice(tempHandTilesWithoutKotsuAndShuntsu.indexOf(shuntuPattern[1]), 1);
tempHandTilesWithoutKotsuAndShuntsu.splice(tempHandTilesWithoutKotsuAndShuntsu.indexOf(shuntuPattern[2]), 1);
isShuntu = true;
break;
}
}
if (!isShuntu) tempHandTilesWithoutKotsuAndShuntsu.splice(0, 1);
}
if (shuntsus.length * 3 === tempHandTilesWithoutKotsu.length) { // アガリ形
horaSets.push({
head,
mentsus: [...kotsuPattern.map(t => [t, t, t] as [Tile, Tile, Tile]), ...shuntsus],
});
}
}
}
return horaSets;
}
/**
* アガリ牌リストを取得
* @param handTiles ポン、チー、カンした牌を含まない手牌
*/
export function getHoraTiles(handTiles: Tile[]): Tile[] {
return TILE_TYPES.filter(tile => {
const tempHandTiles = [...handTiles, tile];
const horaSets = getHoraSets(tempHandTiles);
return horaSets.length > 0;
});
}
export function getTilesForRiichi(handTiles: Tile[]): Tile[] {
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: Tile): Tile {
return NEXT_TILE_FOR_DORA_MAP[tile];
}