wip
This commit is contained in:
parent
d57f20dc84
commit
be3b2558d1
4 changed files with 287 additions and 145 deletions
|
@ -43,28 +43,84 @@ export const TILE_TYPES = [
|
||||||
'chun',
|
'chun',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type Tile = typeof TILE_TYPES[number];
|
export type TileType = typeof TILE_TYPES[number];
|
||||||
|
|
||||||
|
export type TileInstance = {
|
||||||
|
t: TileType;
|
||||||
|
red?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TileId = number;
|
||||||
|
|
||||||
|
export const TILE_ID_MAP = new Map<TileId, TileInstance>([
|
||||||
|
/* eslint-disable no-multi-spaces */
|
||||||
|
[1, { t: 'm1' }], [2, { t: 'm1' }], [3, { t: 'm1' }], [4, { t: 'm1' }],
|
||||||
|
[5, { t: 'm2' }], [6, { t: 'm2' }], [7, { t: 'm2' }], [8, { t: 'm2' }],
|
||||||
|
[9, { t: 'm3' }], [10, { t: 'm3' }], [11, { t: 'm3' }], [12, { t: 'm3' }],
|
||||||
|
[13, { t: 'm4' }], [14, { t: 'm4' }], [15, { t: 'm4' }], [16, { t: 'm4' }],
|
||||||
|
[17, { t: 'm5' }], [18, { t: 'm5' }], [19, { t: 'm5' }], [20, { t: 'm5', red: true }],
|
||||||
|
[21, { t: 'm6' }], [22, { t: 'm6' }], [23, { t: 'm6' }], [24, { t: 'm6' }],
|
||||||
|
[25, { t: 'm7' }], [26, { t: 'm7' }], [27, { t: 'm7' }], [28, { t: 'm7' }],
|
||||||
|
[29, { t: 'm8' }], [30, { t: 'm8' }], [31, { t: 'm8' }], [32, { t: 'm8' }],
|
||||||
|
[33, { t: 'm9' }], [34, { t: 'm9' }], [35, { t: 'm9' }], [36, { t: 'm9' }],
|
||||||
|
[37, { t: 'p1' }], [38, { t: 'p1' }], [39, { t: 'p1' }], [40, { t: 'p1' }],
|
||||||
|
[41, { t: 'p2' }], [42, { t: 'p2' }], [43, { t: 'p2' }], [44, { t: 'p2' }],
|
||||||
|
[45, { t: 'p3' }], [46, { t: 'p3' }], [47, { t: 'p3' }], [48, { t: 'p3' }],
|
||||||
|
[49, { t: 'p4' }], [50, { t: 'p4' }], [51, { t: 'p4' }], [52, { t: 'p4' }],
|
||||||
|
[53, { t: 'p5' }], [54, { t: 'p5' }], [55, { t: 'p5' }], [56, { t: 'p5', red: true }],
|
||||||
|
[57, { t: 'p6' }], [58, { t: 'p6' }], [59, { t: 'p6' }], [60, { t: 'p6' }],
|
||||||
|
[61, { t: 'p7' }], [62, { t: 'p7' }], [63, { t: 'p7' }], [64, { t: 'p7' }],
|
||||||
|
[65, { t: 'p8' }], [66, { t: 'p8' }], [67, { t: 'p8' }], [68, { t: 'p8' }],
|
||||||
|
[69, { t: 'p9' }], [70, { t: 'p9' }], [71, { t: 'p9' }], [72, { t: 'p9' }],
|
||||||
|
[73, { t: 's1' }], [74, { t: 's1' }], [75, { t: 's1' }], [76, { t: 's1' }],
|
||||||
|
[77, { t: 's2' }], [78, { t: 's2' }], [79, { t: 's2' }], [80, { t: 's2' }],
|
||||||
|
[81, { t: 's3' }], [82, { t: 's3' }], [83, { t: 's3' }], [84, { t: 's3' }],
|
||||||
|
[85, { t: 's4' }], [86, { t: 's4' }], [87, { t: 's4' }], [88, { t: 's4' }],
|
||||||
|
[89, { t: 's5' }], [90, { t: 's5' }], [91, { t: 's5' }], [92, { t: 's5', red: true }],
|
||||||
|
[93, { t: 's6' }], [94, { t: 's6' }], [95, { t: 's6' }], [96, { t: 's6' }],
|
||||||
|
[97, { t: 's7' }], [98, { t: 's7' }], [99, { t: 's7' }], [100, { t: 's7' }],
|
||||||
|
[101, { t: 's8' }], [102, { t: 's8' }], [103, { t: 's8' }], [104, { t: 's8' }],
|
||||||
|
[105, { t: 's9' }], [106, { t: 's9' }], [107, { t: 's9' }], [108, { t: 's9' }],
|
||||||
|
[109, { t: 'e' }], [110, { t: 'e' }], [111, { t: 'e' }], [112, { t: 'e' }],
|
||||||
|
[113, { t: 's' }], [114, { t: 's' }], [115, { t: 's' }], [116, { t: 's' }],
|
||||||
|
[117, { t: 'w' }], [118, { t: 'w' }], [119, { t: 'w' }], [120, { t: 'w' }],
|
||||||
|
[121, { t: 'n' }], [122, { t: 'n' }], [123, { t: 'n' }], [124, { t: 'n' }],
|
||||||
|
[125, { t: 'haku' }], [126, { t: 'haku' }], [127, { t: 'haku' }], [128, { t: 'haku' }],
|
||||||
|
[129, { t: 'hatsu' }], [130, { t: 'hatsu' }], [131, { t: 'hatsu' }], [132, { t: 'hatsu' }],
|
||||||
|
[133, { t: 'chun' }], [134, { t: 'chun' }], [135, { t: 'chun' }], [136, { t: 'chun' }],
|
||||||
|
/* eslint-enable no-multi-spaces */
|
||||||
|
]);
|
||||||
|
|
||||||
|
export function findTileByIdOrFail(tileId: TileId): TileInstance {
|
||||||
|
const tile = TILE_ID_MAP.get(tileId);
|
||||||
|
if (tile == null) throw new Error(`tile not found: ${tileId}`);
|
||||||
|
return tile;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findTileById(tileId: TileId): TileInstance | null {
|
||||||
|
return TILE_ID_MAP.get(tileId) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
export type House = 'e' | 's' | 'w' | 'n';
|
export type House = 'e' | 's' | 'w' | 'n';
|
||||||
|
|
||||||
export type Huro = {
|
export type Huro = {
|
||||||
type: 'pon';
|
type: 'pon';
|
||||||
tile: Tile;
|
tiles: [TileId, TileId, TileId];
|
||||||
from: House;
|
from: House;
|
||||||
} | {
|
} | {
|
||||||
type: 'cii';
|
type: 'cii';
|
||||||
tiles: [Tile, Tile, Tile];
|
tiles: [TileId, TileId, TileId];
|
||||||
from: House;
|
from: House;
|
||||||
} | {
|
} | {
|
||||||
type: 'ankan';
|
type: 'ankan';
|
||||||
tile: Tile;
|
tiles: [TileId, TileId, TileId, TileId];
|
||||||
} | {
|
} | {
|
||||||
type: 'minkan';
|
type: 'minkan';
|
||||||
tile: Tile;
|
tiles: [TileId, TileId, TileId, TileId];
|
||||||
from: House | null; // null で加槓
|
from: House | null; // null で加槓
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NEXT_TILE_FOR_DORA_MAP: Record<Tile, Tile> = {
|
export const NEXT_TILE_FOR_DORA_MAP: Record<TileType, TileType> = {
|
||||||
m1: 'm2',
|
m1: 'm2',
|
||||||
m2: 'm3',
|
m2: 'm3',
|
||||||
m3: 'm4',
|
m3: 'm4',
|
||||||
|
@ -101,7 +157,7 @@ export const NEXT_TILE_FOR_DORA_MAP: Record<Tile, Tile> = {
|
||||||
chun: 'haku',
|
chun: 'haku',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NEXT_TILE_FOR_SHUNTSU: Record<Tile, Tile | null> = {
|
export const NEXT_TILE_FOR_SHUNTSU: Record<TileType, TileType | null> = {
|
||||||
m1: 'm2',
|
m1: 'm2',
|
||||||
m2: 'm3',
|
m2: 'm3',
|
||||||
m3: 'm4',
|
m3: 'm4',
|
||||||
|
@ -138,7 +194,7 @@ export const NEXT_TILE_FOR_SHUNTSU: Record<Tile, Tile | null> = {
|
||||||
chun: null,
|
chun: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PREV_TILE_FOR_SHUNTSU: Record<Tile, Tile | null> = {
|
export const PREV_TILE_FOR_SHUNTSU: Record<TileType, TileType | null> = {
|
||||||
m1: null,
|
m1: null,
|
||||||
m2: 'm1',
|
m2: 'm1',
|
||||||
m3: 'm2',
|
m3: 'm2',
|
||||||
|
@ -181,12 +237,12 @@ type EnvForCalcYaku = {
|
||||||
/**
|
/**
|
||||||
* 和了る人の手牌(副露牌および和了る際のツモ牌・ロン牌は含まない)
|
* 和了る人の手牌(副露牌および和了る際のツモ牌・ロン牌は含まない)
|
||||||
*/
|
*/
|
||||||
handTiles: Tile[];
|
handTiles: TileType[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 河
|
* 河
|
||||||
*/
|
*/
|
||||||
hoTiles: Tile[];
|
hoTiles: TileType[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 副露
|
* 副露
|
||||||
|
@ -196,22 +252,22 @@ type EnvForCalcYaku = {
|
||||||
/**
|
/**
|
||||||
* ツモ牌
|
* ツモ牌
|
||||||
*/
|
*/
|
||||||
tsumoTile: Tile | null;
|
tsumoTile: TileType | null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ロン牌
|
* ロン牌
|
||||||
*/
|
*/
|
||||||
ronTile: Tile | null;
|
ronTile: TileType | null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ドラ表示牌
|
* ドラ表示牌
|
||||||
*/
|
*/
|
||||||
doraTiles: Tile[];
|
doraTiles: TileType[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 赤ドラ表示牌
|
* 赤ドラ表示牌
|
||||||
*/
|
*/
|
||||||
redDoraTiles: Tile[];
|
redDoraTiles: TileType[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 場風
|
* 場風
|
||||||
|
@ -362,7 +418,7 @@ export const YAKU_DEFINITIONS = [{
|
||||||
name: 'tanyao',
|
name: 'tanyao',
|
||||||
fan: 1,
|
fan: 1,
|
||||||
calc: (state: EnvForCalcYaku) => {
|
calc: (state: EnvForCalcYaku) => {
|
||||||
const yaochuTiles: Tile[] = ['m1', 'm9', 'p1', 'p9', 's1', 's9', 'e', 's', 'w', 'n', 'haku', 'hatsu', 'chun'];
|
const yaochuTiles: TileType[] = ['m1', 'm9', 'p1', 'p9', 's1', 's9', 'e', 's', 'w', 'n', 'haku', 'hatsu', 'chun'];
|
||||||
return (
|
return (
|
||||||
(!state.handTiles.some(t => yaochuTiles.includes(t))) &&
|
(!state.handTiles.some(t => yaochuTiles.includes(t))) &&
|
||||||
(state.huros.filter(huro =>
|
(state.huros.filter(huro =>
|
||||||
|
@ -424,7 +480,7 @@ export function fanToPoint(fan: number, isParent: boolean): number {
|
||||||
return point;
|
return point;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function calcOwnedDoraCount(handTiles: Tile[], huros: Huro[], doras: Tile[]): number {
|
export function calcOwnedDoraCount(handTiles: TileType[], huros: Huro[], doras: TileType[]): number {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
for (const t of handTiles) {
|
for (const t of handTiles) {
|
||||||
if (doras.includes(t)) count++;
|
if (doras.includes(t)) count++;
|
||||||
|
@ -474,11 +530,11 @@ export function calcTsumoHoraPointDeltas(house: House, fans: number): Record<Hou
|
||||||
return deltas;
|
return deltas;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isTile(tile: string): tile is Tile {
|
export function isTile(tile: string): tile is TileType {
|
||||||
return TILE_TYPES.includes(tile as Tile);
|
return TILE_TYPES.includes(tile as TileType);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sortTiles(tiles: Tile[]): Tile[] {
|
export function sortTiles(tiles: TileType[]): TileType[] {
|
||||||
tiles.sort((a, b) => {
|
tiles.sort((a, b) => {
|
||||||
const aIndex = TILE_TYPES.indexOf(a);
|
const aIndex = TILE_TYPES.indexOf(a);
|
||||||
const bIndex = TILE_TYPES.indexOf(b);
|
const bIndex = TILE_TYPES.indexOf(b);
|
||||||
|
@ -508,11 +564,11 @@ export function prevHouse(house: House): House {
|
||||||
}
|
}
|
||||||
|
|
||||||
type HoraSet = {
|
type HoraSet = {
|
||||||
head: Tile;
|
head: TileType;
|
||||||
mentsus: [Tile, Tile, Tile][];
|
mentsus: [TileType, TileType, TileType][];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SHUNTU_PATTERNS: [Tile, Tile, Tile][] = [
|
export const SHUNTU_PATTERNS: [TileType, TileType, TileType][] = [
|
||||||
['m1', 'm2', 'm3'],
|
['m1', 'm2', 'm3'],
|
||||||
['m2', 'm3', 'm4'],
|
['m2', 'm3', 'm4'],
|
||||||
['m3', 'm4', 'm5'],
|
['m3', 'm4', 'm5'],
|
||||||
|
@ -536,7 +592,7 @@ export const SHUNTU_PATTERNS: [Tile, Tile, Tile][] = [
|
||||||
['s7', 's8', 's9'],
|
['s7', 's8', 's9'],
|
||||||
];
|
];
|
||||||
|
|
||||||
function extractShuntsus(tiles: Tile[]): [Tile, Tile, Tile][] {
|
function extractShuntsus(tiles: TileType[]): [TileType, TileType, TileType][] {
|
||||||
const tempTiles = [...tiles];
|
const tempTiles = [...tiles];
|
||||||
|
|
||||||
tempTiles.sort((a, b) => {
|
tempTiles.sort((a, b) => {
|
||||||
|
@ -545,7 +601,7 @@ function extractShuntsus(tiles: Tile[]): [Tile, Tile, Tile][] {
|
||||||
return aIndex - bIndex;
|
return aIndex - bIndex;
|
||||||
});
|
});
|
||||||
|
|
||||||
const shuntsus: [Tile, Tile, Tile][] = [];
|
const shuntsus: [TileType, TileType, TileType][] = [];
|
||||||
while (tempTiles.length > 0) {
|
while (tempTiles.length > 0) {
|
||||||
let isShuntu = false;
|
let isShuntu = false;
|
||||||
for (const shuntuPattern of SHUNTU_PATTERNS) {
|
for (const shuntuPattern of SHUNTU_PATTERNS) {
|
||||||
|
@ -574,11 +630,11 @@ function extractShuntsus(tiles: Tile[]): [Tile, Tile, Tile][] {
|
||||||
* @param handTiles ポン、チー、カンした牌を含まない手牌
|
* @param handTiles ポン、チー、カンした牌を含まない手牌
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function getHoraSets(handTiles: Tile[]): HoraSet[] {
|
export function getHoraSets(handTiles: TileType[]): HoraSet[] {
|
||||||
const horaSets: HoraSet[] = [];
|
const horaSets: HoraSet[] = [];
|
||||||
|
|
||||||
const headSet: Tile[] = [];
|
const headSet: TileType[] = [];
|
||||||
const countMap = new Map<Tile, number>();
|
const countMap = new Map<TileType, number>();
|
||||||
for (const tile of handTiles) {
|
for (const tile of handTiles) {
|
||||||
const count = (countMap.get(tile) ?? 0) + 1;
|
const count = (countMap.get(tile) ?? 0) + 1;
|
||||||
countMap.set(tile, count);
|
countMap.set(tile, count);
|
||||||
|
@ -592,7 +648,7 @@ export function getHoraSets(handTiles: Tile[]): HoraSet[] {
|
||||||
tempHandTiles.splice(tempHandTiles.indexOf(head), 1);
|
tempHandTiles.splice(tempHandTiles.indexOf(head), 1);
|
||||||
tempHandTiles.splice(tempHandTiles.indexOf(head), 1);
|
tempHandTiles.splice(tempHandTiles.indexOf(head), 1);
|
||||||
|
|
||||||
const kotsuTileSet: Tile[] = []; // インデックスアクセスしたいため配列だが実態はSet
|
const kotsuTileSet: TileType[] = []; // インデックスアクセスしたいため配列だが実態はSet
|
||||||
for (const [t, c] of countMap.entries()) {
|
for (const [t, c] of countMap.entries()) {
|
||||||
if (t === head) continue; // 同じ牌種は4枚しかないので、頭と同じ牌種は刻子になりえない
|
if (t === head) continue; // 同じ牌種は4枚しかないので、頭と同じ牌種は刻子になりえない
|
||||||
if (c >= 3) {
|
if (c >= 3) {
|
||||||
|
@ -600,7 +656,7 @@ export function getHoraSets(handTiles: Tile[]): HoraSet[] {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let kotsuPatterns: Tile[][];
|
let kotsuPatterns: TileType[][];
|
||||||
if (kotsuTileSet.length === 0) {
|
if (kotsuTileSet.length === 0) {
|
||||||
kotsuPatterns = [
|
kotsuPatterns = [
|
||||||
[],
|
[],
|
||||||
|
@ -664,7 +720,7 @@ export function getHoraSets(handTiles: Tile[]): HoraSet[] {
|
||||||
if (shuntsus.length * 3 === tempHandTilesWithoutKotsu.length) { // アガリ形
|
if (shuntsus.length * 3 === tempHandTilesWithoutKotsu.length) { // アガリ形
|
||||||
horaSets.push({
|
horaSets.push({
|
||||||
head,
|
head,
|
||||||
mentsus: [...kotsuPattern.map(t => [t, t, t] as [Tile, Tile, Tile]), ...shuntsus],
|
mentsus: [...kotsuPattern.map(t => [t, t, t] as [TileType, TileType, TileType]), ...shuntsus],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -677,7 +733,7 @@ export function getHoraSets(handTiles: Tile[]): HoraSet[] {
|
||||||
* アガリ牌リストを取得
|
* アガリ牌リストを取得
|
||||||
* @param handTiles ポン、チー、カンした牌を含まない手牌
|
* @param handTiles ポン、チー、カンした牌を含まない手牌
|
||||||
*/
|
*/
|
||||||
export function getHoraTiles(handTiles: Tile[]): Tile[] {
|
export function getHoraTiles(handTiles: TileType[]): TileType[] {
|
||||||
return TILE_TYPES.filter(tile => {
|
return TILE_TYPES.filter(tile => {
|
||||||
const tempHandTiles = [...handTiles, tile];
|
const tempHandTiles = [...handTiles, tile];
|
||||||
const horaSets = getHoraSets(tempHandTiles);
|
const horaSets = getHoraSets(tempHandTiles);
|
||||||
|
@ -689,7 +745,7 @@ export function getHoraTiles(handTiles: Tile[]): Tile[] {
|
||||||
|
|
||||||
// TODO: 七対子判定関数
|
// TODO: 七対子判定関数
|
||||||
|
|
||||||
export function getTilesForRiichi(handTiles: Tile[]): Tile[] {
|
export function getTilesForRiichi(handTiles: TileType[]): TileType[] {
|
||||||
return handTiles.filter(tile => {
|
return handTiles.filter(tile => {
|
||||||
const tempHandTiles = [...handTiles];
|
const tempHandTiles = [...handTiles];
|
||||||
tempHandTiles.splice(tempHandTiles.indexOf(tile), 1);
|
tempHandTiles.splice(tempHandTiles.indexOf(tile), 1);
|
||||||
|
@ -698,12 +754,12 @@ export function getTilesForRiichi(handTiles: Tile[]): Tile[] {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function nextTileForDora(tile: Tile): Tile {
|
export function nextTileForDora(tile: TileType): TileType {
|
||||||
return NEXT_TILE_FOR_DORA_MAP[tile];
|
return NEXT_TILE_FOR_DORA_MAP[tile];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAvailableCiiPatterns(handTiles: Tile[], targetTile: Tile): [Tile, Tile, Tile][] {
|
export function getAvailableCiiPatterns(handTiles: TileType[], targetTile: TileType): [TileType, TileType, TileType][] {
|
||||||
const patterns: [Tile, Tile, Tile][] = [];
|
const patterns: [TileType, TileType, TileType][] = [];
|
||||||
const prev1 = PREV_TILE_FOR_SHUNTSU[targetTile];
|
const prev1 = PREV_TILE_FOR_SHUNTSU[targetTile];
|
||||||
const prev2 = prev1 != null ? PREV_TILE_FOR_SHUNTSU[prev1] : null;
|
const prev2 = prev1 != null ? PREV_TILE_FOR_SHUNTSU[prev1] : null;
|
||||||
const next1 = NEXT_TILE_FOR_SHUNTSU[targetTile];
|
const next1 = NEXT_TILE_FOR_SHUNTSU[targetTile];
|
||||||
|
|
|
@ -4,10 +4,20 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import CRC32 from 'crc-32';
|
import CRC32 from 'crc-32';
|
||||||
import { Tile, House, Huro, TILE_TYPES, YAKU_DEFINITIONS } from './common.js';
|
import { TileType, House, Huro, TILE_TYPES, YAKU_DEFINITIONS, TileId } from './common.js';
|
||||||
import * as Common from './common.js';
|
import * as Common from './common.js';
|
||||||
import { PlayerState } from './engine.player.js';
|
import { PlayerState } from './engine.player.js';
|
||||||
|
|
||||||
|
//#region syntax suger
|
||||||
|
function $(tileId: TileId): Common.TileInstance {
|
||||||
|
return Common.findTileByIdOrFail(tileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function $type(tileId: TileId): TileType {
|
||||||
|
return Common.findTileByIdOrFail(tileId).t;
|
||||||
|
}
|
||||||
|
//#endregion
|
||||||
|
|
||||||
export type MasterState = {
|
export type MasterState = {
|
||||||
user1House: House;
|
user1House: House;
|
||||||
user2House: House;
|
user2House: House;
|
||||||
|
@ -17,25 +27,25 @@ export type MasterState = {
|
||||||
round: 'e' | 's' | 'w' | 'n';
|
round: 'e' | 's' | 'w' | 'n';
|
||||||
kyoku: number;
|
kyoku: number;
|
||||||
|
|
||||||
tiles: Tile[];
|
tiles: TileId[];
|
||||||
kingTiles: Tile[];
|
kingTiles: TileId[];
|
||||||
activatedDorasCount: number;
|
activatedDorasCount: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 副露した牌を含まない手牌
|
* 副露した牌を含まない手牌
|
||||||
*/
|
*/
|
||||||
handTiles: {
|
handTiles: {
|
||||||
e: Tile[];
|
e: TileId[];
|
||||||
s: Tile[];
|
s: TileId[];
|
||||||
w: Tile[];
|
w: TileId[];
|
||||||
n: Tile[];
|
n: TileId[];
|
||||||
};
|
};
|
||||||
|
|
||||||
hoTiles: {
|
hoTiles: {
|
||||||
e: Tile[];
|
e: TileId[];
|
||||||
s: Tile[];
|
s: TileId[];
|
||||||
w: Tile[];
|
w: TileId[];
|
||||||
n: Tile[];
|
n: TileId[];
|
||||||
};
|
};
|
||||||
huros: {
|
huros: {
|
||||||
e: Huro[];
|
e: Huro[];
|
||||||
|
@ -114,14 +124,33 @@ export class MasterGameEngine {
|
||||||
this.state = state;
|
this.state = state;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get doras(): Tile[] {
|
public get doras(): TileType[] {
|
||||||
return this.state.kingTiles.slice(0, this.state.activatedDorasCount).map(t => Common.nextTileForDora(t));
|
return this.state.kingTiles.slice(0, this.state.activatedDorasCount)
|
||||||
|
.map(id => Common.nextTileForDora($type(id)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public get handTileTypes(): Record<House, TileType[]> {
|
||||||
|
return {
|
||||||
|
e: this.state.handTiles.e.map(id => $type(id)),
|
||||||
|
s: this.state.handTiles.s.map(id => $type(id)),
|
||||||
|
w: this.state.handTiles.w.map(id => $type(id)),
|
||||||
|
n: this.state.handTiles.n.map(id => $type(id)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public get hoTileTypes(): Record<House, TileType[]> {
|
||||||
|
return {
|
||||||
|
e: this.state.hoTiles.e.map(id => $type(id)),
|
||||||
|
s: this.state.hoTiles.s.map(id => $type(id)),
|
||||||
|
w: this.state.hoTiles.w.map(id => $type(id)),
|
||||||
|
n: this.state.hoTiles.n.map(id => $type(id)),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static createInitialState(): MasterState {
|
public static createInitialState(): MasterState {
|
||||||
const ikasama: Tile[] = ['haku', 'hatsu', 'm3', 'p5', 'p6', 'p7', 's2', 's3', 's4', 'chun', 'chun', 'chun', 'n', 'n'];
|
const ikasama: TileId[] = [125, 129, 9, 54, 57, 61, 77, 81, 85, 133, 134, 135, 121, 122];
|
||||||
|
|
||||||
const tiles = [...TILE_TYPES.slice(), ...TILE_TYPES.slice(), ...TILE_TYPES.slice(), ...TILE_TYPES.slice()];
|
const tiles = [...Common.TILE_ID_MAP.keys()];
|
||||||
tiles.sort(() => Math.random() - 0.5);
|
tiles.sort(() => Math.random() - 0.5);
|
||||||
|
|
||||||
for (const tile of ikasama) {
|
for (const tile of ikasama) {
|
||||||
|
@ -185,7 +214,7 @@ export class MasterGameEngine {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private tsumo(): Tile {
|
private tsumo(): TileId {
|
||||||
const tile = this.state.tiles.pop();
|
const tile = this.state.tiles.pop();
|
||||||
if (tile == null) throw new Error('No tiles left');
|
if (tile == null) throw new Error('No tiles left');
|
||||||
if (this.state.turn == null) throw new Error('Not your turn');
|
if (this.state.turn == null) throw new Error('Not your turn');
|
||||||
|
@ -193,12 +222,12 @@ export class MasterGameEngine {
|
||||||
return tile;
|
return tile;
|
||||||
}
|
}
|
||||||
|
|
||||||
private canRon(house: House, tile: Tile): boolean {
|
private canRon(house: House, tile: TileId): boolean {
|
||||||
// フリテン
|
// フリテン
|
||||||
// TODO: ポンされるなどして自分の河にない場合の考慮
|
// TODO: ポンされるなどして自分の河にない場合の考慮
|
||||||
if (this.state.hoTiles[house].includes(tile)) return false;
|
if (this.hoTileTypes[house].includes($type(tile))) return false;
|
||||||
|
|
||||||
const horaSets = Common.getHoraSets(this.state.handTiles[house].concat(tile));
|
const horaSets = Common.getHoraSets(this.handTileTypes[house].concat($type(tile)));
|
||||||
if (horaSets.length === 0) return false; // 完成形じゃない
|
if (horaSets.length === 0) return false; // 完成形じゃない
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
|
@ -208,15 +237,15 @@ export class MasterGameEngine {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private canPon(house: House, tile: Tile): boolean {
|
private canPon(house: House, tile: TileId): boolean {
|
||||||
return this.state.handTiles[house].filter(t => t === tile).length === 2;
|
return this.handTileTypes[house].filter(t => t === $type(tile)).length === 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
private canCii(caller: House, callee: House, tile: Tile): boolean {
|
private canCii(caller: House, callee: House, tile: TileId): boolean {
|
||||||
if (callee !== Common.prevHouse(caller)) return false;
|
if (callee !== Common.prevHouse(caller)) return false;
|
||||||
const hand = this.state.handTiles[caller];
|
const hand = this.handTileTypes[caller];
|
||||||
return Common.SHUNTU_PATTERNS.some(pattern =>
|
return Common.SHUNTU_PATTERNS.some(pattern =>
|
||||||
pattern.includes(tile) &&
|
pattern.includes($type(tile)) &&
|
||||||
pattern.filter(t => hand.includes(t)).length >= 2);
|
pattern.filter(t => hand.includes(t)).length >= 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -245,13 +274,14 @@ export class MasterGameEngine {
|
||||||
for (const house of callers) {
|
for (const house of callers) {
|
||||||
const yakus = YAKU_DEFINITIONS.filter(yaku => yaku.calc({
|
const yakus = YAKU_DEFINITIONS.filter(yaku => yaku.calc({
|
||||||
house: house,
|
house: house,
|
||||||
handTiles: this.state.handTiles[house],
|
handTiles: this.handTileTypes[house],
|
||||||
huros: this.state.huros[house],
|
huros: this.state.huros[house],
|
||||||
tsumoTile: null,
|
tsumoTile: null,
|
||||||
ronTile: this.state.hoTiles[callee].at(-1)!,
|
ronTile: this.hoTileTypes[callee].at(-1)!,
|
||||||
riichi: this.state.riichis[house],
|
riichi: this.state.riichis[house],
|
||||||
}));
|
}));
|
||||||
const doraCount = Common.calcOwnedDoraCount(this.state.handTiles[house], this.state.huros[house], this.doras);
|
const doraCount = Common.calcOwnedDoraCount(this.handTileTypes[house], this.state.huros[house], this.doras);
|
||||||
|
// TODO: 赤ドラ
|
||||||
const fans = yakus.map(yaku => yaku.fan).reduce((a, b) => a + b, 0) + doraCount;
|
const fans = yakus.map(yaku => yaku.fan).reduce((a, b) => a + b, 0) + doraCount;
|
||||||
const point = Common.fanToPoint(fans, house === 'e');
|
const point = Common.fanToPoint(fans, house === 'e');
|
||||||
this.state.points[callee] -= point;
|
this.state.points[callee] -= point;
|
||||||
|
@ -263,12 +293,12 @@ export class MasterGameEngine {
|
||||||
this.endKyoku();
|
this.endKyoku();
|
||||||
}
|
}
|
||||||
|
|
||||||
public commit_dahai(house: House, tile: Tile, riichi = false) {
|
public commit_dahai(house: House, tile: TileId, riichi = false) {
|
||||||
if (this.state.turn !== house) throw new Error('Not your turn');
|
if (this.state.turn !== house) throw new Error('Not your turn');
|
||||||
|
|
||||||
if (riichi) {
|
if (riichi) {
|
||||||
const tempHandTiles = [...this.state.handTiles[house]];
|
const tempHandTiles = [...this.handTileTypes[house]];
|
||||||
tempHandTiles.splice(tempHandTiles.indexOf(tile), 1);
|
tempHandTiles.splice(tempHandTiles.indexOf($type(tile)), 1);
|
||||||
if (Common.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');
|
if (this.state.points[house] < 1000) throw new Error('Not enough points');
|
||||||
}
|
}
|
||||||
|
@ -371,11 +401,11 @@ export class MasterGameEngine {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public commit_kakan(house: House, tile: Tile) {
|
public commit_kakan(house: House, tile: TileId) {
|
||||||
const pon = this.state.huros[house].find(h => h.type === 'pon' && h.tile === tile);
|
const pon = this.state.huros[house].find(h => h.type === 'pon' && $type(h.tiles[0]) === $type(tile));
|
||||||
if (pon == null) throw new Error('No such pon');
|
if (pon == null) throw new Error('No such pon');
|
||||||
this.state.handTiles[house].splice(this.state.handTiles[house].indexOf(tile), 1);
|
this.state.handTiles[house].splice(this.state.handTiles[house].indexOf(tile), 1);
|
||||||
this.state.huros[house].push({ type: 'minkan', tile, from: pon.from });
|
this.state.huros[house].push({ type: 'minkan', tiles: [...pon.tiles, tile], from: pon.from });
|
||||||
|
|
||||||
this.state.activatedDorasCount++;
|
this.state.activatedDorasCount++;
|
||||||
|
|
||||||
|
@ -386,12 +416,20 @@ export class MasterGameEngine {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public commit_ankan(house: House, tile: Tile) {
|
public commit_ankan(house: House, tile: TileId) {
|
||||||
this.state.handTiles[house].splice(this.state.handTiles[house].indexOf(tile), 1);
|
const t1 = this.state.handTiles[house].filter(t => $type(t) === $type(tile)).at(0);
|
||||||
this.state.handTiles[house].splice(this.state.handTiles[house].indexOf(tile), 1);
|
if (t1 == null) throw new Error('No such tile');
|
||||||
this.state.handTiles[house].splice(this.state.handTiles[house].indexOf(tile), 1);
|
const t2 = this.state.handTiles[house].filter(t => $type(t) === $type(tile)).at(1);
|
||||||
this.state.handTiles[house].splice(this.state.handTiles[house].indexOf(tile), 1);
|
if (t2 == null) throw new Error('No such tile');
|
||||||
this.state.huros[house].push({ type: 'ankan', tile });
|
const t3 = this.state.handTiles[house].filter(t => $type(t) === $type(tile)).at(2);
|
||||||
|
if (t3 == null) throw new Error('No such tile');
|
||||||
|
const t4 = this.state.handTiles[house].filter(t => $type(t) === $type(tile)).at(3);
|
||||||
|
if (t4 == null) throw new Error('No such tile');
|
||||||
|
this.state.handTiles[house].splice(this.state.handTiles[house].indexOf(t1), 1);
|
||||||
|
this.state.handTiles[house].splice(this.state.handTiles[house].indexOf(t2), 1);
|
||||||
|
this.state.handTiles[house].splice(this.state.handTiles[house].indexOf(t3), 1);
|
||||||
|
this.state.handTiles[house].splice(this.state.handTiles[house].indexOf(t4), 1);
|
||||||
|
this.state.huros[house].push({ type: 'ankan', tiles: [t1, t2, t3, t4] });
|
||||||
|
|
||||||
this.state.activatedDorasCount++;
|
this.state.activatedDorasCount++;
|
||||||
|
|
||||||
|
@ -411,13 +449,14 @@ export class MasterGameEngine {
|
||||||
|
|
||||||
const yakus = YAKU_DEFINITIONS.filter(yaku => yaku.calc({
|
const yakus = YAKU_DEFINITIONS.filter(yaku => yaku.calc({
|
||||||
house: house,
|
house: house,
|
||||||
handTiles: this.state.handTiles[house],
|
handTiles: this.handTileTypes[house],
|
||||||
huros: this.state.huros[house],
|
huros: this.state.huros[house],
|
||||||
tsumoTile: this.state.handTiles[house].at(-1)!,
|
tsumoTile: this.handTileTypes[house].at(-1)!,
|
||||||
ronTile: null,
|
ronTile: null,
|
||||||
riichi: this.state.riichis[house],
|
riichi: this.state.riichis[house],
|
||||||
}));
|
}));
|
||||||
const doraCount = Common.calcOwnedDoraCount(this.state.handTiles[house], this.state.huros[house], this.doras);
|
const doraCount = Common.calcOwnedDoraCount(this.handTileTypes[house], this.state.huros[house], this.doras);
|
||||||
|
// TODO: 赤ドラ
|
||||||
const fans = yakus.map(yaku => yaku.fan).reduce((a, b) => a + b, 0) + doraCount;
|
const fans = yakus.map(yaku => yaku.fan).reduce((a, b) => a + b, 0) + doraCount;
|
||||||
const pointDeltas = Common.calcTsumoHoraPointDeltas(house, fans);
|
const pointDeltas = Common.calcTsumoHoraPointDeltas(house, fans);
|
||||||
this.state.points.e += pointDeltas.e;
|
this.state.points.e += pointDeltas.e;
|
||||||
|
@ -463,8 +502,19 @@ export class MasterGameEngine {
|
||||||
} else if (kan != null && answers.kan) {
|
} else if (kan != null && answers.kan) {
|
||||||
// 大明槓
|
// 大明槓
|
||||||
|
|
||||||
|
const t1 = this.state.handTiles[kan.caller].filter(t => $type(t) === $type(tile)).at(0);
|
||||||
|
if (t1 == null) throw new Error('No such tile');
|
||||||
|
const t2 = this.state.handTiles[kan.caller].filter(t => $type(t) === $type(tile)).at(1);
|
||||||
|
if (t2 == null) throw new Error('No such tile');
|
||||||
|
const t3 = this.state.handTiles[kan.caller].filter(t => $type(t) === $type(tile)).at(2);
|
||||||
|
if (t3 == null) throw new Error('No such tile');
|
||||||
const tile = this.state.hoTiles[kan.callee].pop()!;
|
const tile = this.state.hoTiles[kan.callee].pop()!;
|
||||||
this.state.huros[kan.caller].push({ type: 'minkan', tile, from: kan.callee });
|
|
||||||
|
this.state.handTiles[kan.caller].splice(this.state.handTiles[kan.caller].indexOf(t1), 1);
|
||||||
|
this.state.handTiles[kan.caller].splice(this.state.handTiles[kan.caller].indexOf(t2), 1);
|
||||||
|
this.state.handTiles[kan.caller].splice(this.state.handTiles[kan.caller].indexOf(t3), 1);
|
||||||
|
|
||||||
|
this.state.huros[kan.caller].push({ type: 'minkan', tiles: [tile, t1, t2, t3], from: kan.callee });
|
||||||
|
|
||||||
this.state.activatedDorasCount++;
|
this.state.activatedDorasCount++;
|
||||||
|
|
||||||
|
@ -481,10 +531,16 @@ export class MasterGameEngine {
|
||||||
turn: this.state.turn,
|
turn: this.state.turn,
|
||||||
};
|
};
|
||||||
} else if (pon != null && answers.pon) {
|
} else if (pon != null && answers.pon) {
|
||||||
|
const t1 = this.state.handTiles[pon.caller].filter(t => $type(t) === $type(tile)).at(0);
|
||||||
|
if (t1 == null) throw new Error('No such tile');
|
||||||
|
const t2 = this.state.handTiles[pon.caller].filter(t => $type(t) === $type(tile)).at(1);
|
||||||
|
if (t2 == null) throw new Error('No such tile');
|
||||||
const tile = this.state.hoTiles[pon.callee].pop()!;
|
const tile = this.state.hoTiles[pon.callee].pop()!;
|
||||||
this.state.handTiles[pon.caller].splice(this.state.handTiles[pon.caller].indexOf(tile), 1);
|
|
||||||
this.state.handTiles[pon.caller].splice(this.state.handTiles[pon.caller].indexOf(tile), 1);
|
this.state.handTiles[pon.caller].splice(this.state.handTiles[pon.caller].indexOf(t1), 1);
|
||||||
this.state.huros[pon.caller].push({ type: 'pon', tile, from: pon.callee });
|
this.state.handTiles[pon.caller].splice(this.state.handTiles[pon.caller].indexOf(t2), 1);
|
||||||
|
|
||||||
|
this.state.huros[pon.caller].push({ type: 'pon', tiles: [tile, t1, t2], from: pon.callee });
|
||||||
|
|
||||||
this.state.turn = pon.caller;
|
this.state.turn = pon.caller;
|
||||||
|
|
||||||
|
@ -497,37 +553,49 @@ export class MasterGameEngine {
|
||||||
};
|
};
|
||||||
} else if (cii != null && answers.cii) {
|
} else if (cii != null && answers.cii) {
|
||||||
const tile = this.state.hoTiles[cii.callee].pop()!;
|
const tile = this.state.hoTiles[cii.callee].pop()!;
|
||||||
let tiles: [Tile, Tile, Tile];
|
let tiles: [TileId, TileId, TileId];
|
||||||
|
|
||||||
switch (answers.cii) {
|
switch (answers.cii) {
|
||||||
case 'x__': {
|
case 'x__': {
|
||||||
const a = Common.NEXT_TILE_FOR_SHUNTSU[tile];
|
const a = Common.NEXT_TILE_FOR_SHUNTSU[$type(tile)];
|
||||||
if (a == null) throw new Error();
|
if (a == null) throw new Error();
|
||||||
const b = Common.NEXT_TILE_FOR_SHUNTSU[a];
|
const b = Common.NEXT_TILE_FOR_SHUNTSU[a];
|
||||||
if (b == null) throw new Error();
|
if (b == null) throw new Error();
|
||||||
this.state.handTiles[cii.caller].splice(this.state.handTiles[cii.caller].indexOf(a), 1);
|
const aTile = this.state.handTiles[cii.caller].find(t => $type(t) === a);
|
||||||
this.state.handTiles[cii.caller].splice(this.state.handTiles[cii.caller].indexOf(b), 1);
|
if (aTile == null) throw new Error();
|
||||||
tiles = [tile, a, b];
|
const bTile = this.state.handTiles[cii.caller].find(t => $type(t) === b);
|
||||||
|
if (bTile == null) throw new Error();
|
||||||
|
this.state.handTiles[cii.caller].splice(this.state.handTiles[cii.caller].indexOf(aTile), 1);
|
||||||
|
this.state.handTiles[cii.caller].splice(this.state.handTiles[cii.caller].indexOf(bTile), 1);
|
||||||
|
tiles = [tile, aTile, bTile];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case '_x_': {
|
case '_x_': {
|
||||||
const a = Common.PREV_TILE_FOR_SHUNTSU[tile];
|
const a = Common.PREV_TILE_FOR_SHUNTSU[$type(tile)];
|
||||||
if (a == null) throw new Error();
|
if (a == null) throw new Error();
|
||||||
const b = Common.NEXT_TILE_FOR_SHUNTSU[tile];
|
const b = Common.NEXT_TILE_FOR_SHUNTSU[$type(tile)];
|
||||||
if (b == null) throw new Error();
|
if (b == null) throw new Error();
|
||||||
this.state.handTiles[cii.caller].splice(this.state.handTiles[cii.caller].indexOf(a), 1);
|
const aTile = this.state.handTiles[cii.caller].find(t => $type(t) === a);
|
||||||
this.state.handTiles[cii.caller].splice(this.state.handTiles[cii.caller].indexOf(b), 1);
|
if (aTile == null) throw new Error();
|
||||||
tiles = [a, tile, b];
|
const bTile = this.state.handTiles[cii.caller].find(t => $type(t) === b);
|
||||||
|
if (bTile == null) throw new Error();
|
||||||
|
this.state.handTiles[cii.caller].splice(this.state.handTiles[cii.caller].indexOf(aTile), 1);
|
||||||
|
this.state.handTiles[cii.caller].splice(this.state.handTiles[cii.caller].indexOf(bTile), 1);
|
||||||
|
tiles = [aTile, tile, bTile];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case '__x': {
|
case '__x': {
|
||||||
const a = Common.PREV_TILE_FOR_SHUNTSU[tile];
|
const a = Common.PREV_TILE_FOR_SHUNTSU[$type(tile)];
|
||||||
if (a == null) throw new Error();
|
if (a == null) throw new Error();
|
||||||
const b = Common.PREV_TILE_FOR_SHUNTSU[a];
|
const b = Common.PREV_TILE_FOR_SHUNTSU[a];
|
||||||
if (b == null) throw new Error();
|
if (b == null) throw new Error();
|
||||||
this.state.handTiles[cii.caller].splice(this.state.handTiles[cii.caller].indexOf(a), 1);
|
const aTile = this.state.handTiles[cii.caller].find(t => $type(t) === a);
|
||||||
this.state.handTiles[cii.caller].splice(this.state.handTiles[cii.caller].indexOf(b), 1);
|
if (aTile == null) throw new Error();
|
||||||
tiles = [b, a, tile];
|
const bTile = this.state.handTiles[cii.caller].find(t => $type(t) === b);
|
||||||
|
if (bTile == null) throw new Error();
|
||||||
|
this.state.handTiles[cii.caller].splice(this.state.handTiles[cii.caller].indexOf(aTile), 1);
|
||||||
|
this.state.handTiles[cii.caller].splice(this.state.handTiles[cii.caller].indexOf(bTile), 1);
|
||||||
|
tiles = [bTile, aTile, tile];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,9 +4,19 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import CRC32 from 'crc-32';
|
import CRC32 from 'crc-32';
|
||||||
import { Tile, House, Huro, TILE_TYPES, YAKU_DEFINITIONS } from './common.js';
|
import { TileType, House, Huro, TileId, YAKU_DEFINITIONS } from './common.js';
|
||||||
import * as Common from './common.js';
|
import * as Common from './common.js';
|
||||||
|
|
||||||
|
//#region syntax suger
|
||||||
|
function $(tileId: TileId): Common.TileInstance {
|
||||||
|
return Common.findTileByIdOrFail(tileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function $type(tileId: TileId): TileType {
|
||||||
|
return Common.findTileByIdOrFail(tileId).t;
|
||||||
|
}
|
||||||
|
//#endregion
|
||||||
|
|
||||||
export type PlayerState = {
|
export type PlayerState = {
|
||||||
user1House: House;
|
user1House: House;
|
||||||
user2House: House;
|
user2House: House;
|
||||||
|
@ -17,23 +27,23 @@ export type PlayerState = {
|
||||||
kyoku: number;
|
kyoku: number;
|
||||||
|
|
||||||
tilesCount: number;
|
tilesCount: number;
|
||||||
doraIndicateTiles: Tile[];
|
doraIndicateTiles: TileId[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 副露した牌を含まない手牌
|
* 副露した牌を含まない手牌
|
||||||
*/
|
*/
|
||||||
handTiles: {
|
handTiles: {
|
||||||
e: Tile[] | null[];
|
e: TileId[] | null[];
|
||||||
s: Tile[] | null[];
|
s: TileId[] | null[];
|
||||||
w: Tile[] | null[];
|
w: TileId[] | null[];
|
||||||
n: Tile[] | null[];
|
n: TileId[] | null[];
|
||||||
};
|
};
|
||||||
|
|
||||||
hoTiles: {
|
hoTiles: {
|
||||||
e: Tile[];
|
e: TileId[];
|
||||||
s: Tile[];
|
s: TileId[];
|
||||||
w: Tile[];
|
w: TileId[];
|
||||||
n: Tile[];
|
n: TileId[];
|
||||||
};
|
};
|
||||||
huros: {
|
huros: {
|
||||||
e: Huro[];
|
e: Huro[];
|
||||||
|
@ -53,7 +63,7 @@ export type PlayerState = {
|
||||||
w: number;
|
w: number;
|
||||||
n: number;
|
n: number;
|
||||||
};
|
};
|
||||||
latestDahaiedTile: Tile | null;
|
latestDahaiedTile: TileId | null;
|
||||||
turn: House | null;
|
turn: House | null;
|
||||||
canPon: { callee: House } | null;
|
canPon: { callee: House } | null;
|
||||||
canCii: { callee: House } | null;
|
canCii: { callee: House } | null;
|
||||||
|
@ -90,19 +100,23 @@ export class PlayerGameEngine {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public get myHandTiles(): Tile[] {
|
public get myHandTiles(): TileId[] {
|
||||||
return this.state.handTiles[this.myHouse] as Tile[];
|
return this.state.handTiles[this.myHouse] as TileId[];
|
||||||
|
}
|
||||||
|
|
||||||
|
public get myHandTileTypes(): TileType[] {
|
||||||
|
return this.myHandTiles.map(t => $type(t));
|
||||||
}
|
}
|
||||||
|
|
||||||
public get isMeRiichi(): boolean {
|
public get isMeRiichi(): boolean {
|
||||||
return this.state.riichis[this.myHouse];
|
return this.state.riichis[this.myHouse];
|
||||||
}
|
}
|
||||||
|
|
||||||
public get doras(): Tile[] {
|
public get doras(): TileType[] {
|
||||||
return this.state.doraIndicateTiles.map(t => Common.nextTileForDora(t));
|
return this.state.doraIndicateTiles.map(t => Common.nextTileForDora($type(t)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public commit_tsumo(house: House, tile: Tile) {
|
public commit_tsumo(house: House, tile: TileId) {
|
||||||
console.log('commit_tsumo', this.state.turn, house, tile);
|
console.log('commit_tsumo', this.state.turn, house, tile);
|
||||||
this.state.tilesCount--;
|
this.state.tilesCount--;
|
||||||
this.state.turn = house;
|
this.state.turn = house;
|
||||||
|
@ -113,7 +127,7 @@ export class PlayerGameEngine {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public commit_dahai(house: House, tile: Tile, riichi = false) {
|
public commit_dahai(house: House, tile: TileId, riichi = false) {
|
||||||
console.log('commit_dahai', this.state.turn, house, tile, riichi);
|
console.log('commit_dahai', this.state.turn, house, tile, riichi);
|
||||||
if (this.state.turn !== house) throw new PlayerGameEngine.InvalidOperationError();
|
if (this.state.turn !== house) throw new PlayerGameEngine.InvalidOperationError();
|
||||||
|
|
||||||
|
@ -133,13 +147,13 @@ export class PlayerGameEngine {
|
||||||
|
|
||||||
if (house === this.myHouse) {
|
if (house === this.myHouse) {
|
||||||
} else {
|
} else {
|
||||||
const canRon = Common.getHoraSets(this.myHandTiles.concat(tile)).length > 0;
|
const canRon = Common.getHoraSets(this.myHandTiles.concat(tile).map(id => $type(id))).length > 0;
|
||||||
const canPon = this.myHandTiles.filter(t => t === tile).length === 2;
|
const canPon = this.myHandTiles.filter(t => t === tile).length === 2;
|
||||||
const canKan = this.myHandTiles.filter(t => t === tile).length === 3;
|
const canKan = this.myHandTiles.filter(t => t === tile).length === 3;
|
||||||
const canCii = house === Common.prevHouse(this.myHouse) &&
|
const canCii = house === Common.prevHouse(this.myHouse) &&
|
||||||
Common.SHUNTU_PATTERNS.some(pattern =>
|
Common.SHUNTU_PATTERNS.some(pattern =>
|
||||||
pattern.includes(tile) &&
|
pattern.includes($type(tile)) &&
|
||||||
pattern.filter(t => this.myHandTiles.includes(t)).length >= 2);
|
pattern.filter(t => this.myHandTileTypes.includes(t)).length >= 2);
|
||||||
|
|
||||||
if (canRon) this.state.canRon = { callee: house };
|
if (canRon) this.state.canRon = { callee: house };
|
||||||
if (canPon) this.state.canPon = { callee: house };
|
if (canPon) this.state.canPon = { callee: house };
|
||||||
|
@ -148,23 +162,24 @@ export class PlayerGameEngine {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public commit_kakan(house: House, tile: Tile) {
|
public commit_kakan(house: House, tile: TileId) {
|
||||||
console.log('commit_kakan', this.state.turn, house, tile);
|
console.log('commit_kakan', this.state.turn, house, tile);
|
||||||
if (this.state.turn !== house) throw new PlayerGameEngine.InvalidOperationError();
|
if (this.state.turn !== house) throw new PlayerGameEngine.InvalidOperationError();
|
||||||
}
|
}
|
||||||
|
|
||||||
public commit_tsumoHora(house: House, handTiles: Tile[], tsumoTile: Tile): KyokuResult {
|
public commit_tsumoHora(house: House, handTiles: TileId[], tsumoTile: TileId): KyokuResult {
|
||||||
console.log('commit_tsumoHora', this.state.turn, house);
|
console.log('commit_tsumoHora', this.state.turn, house);
|
||||||
|
|
||||||
const yakus = YAKU_DEFINITIONS.filter(yaku => yaku.calc({
|
const yakus = YAKU_DEFINITIONS.filter(yaku => yaku.calc({
|
||||||
house: house,
|
house: house,
|
||||||
handTiles: handTiles,
|
handTiles: handTiles.map(id => $type(id)),
|
||||||
huros: this.state.huros[house],
|
huros: this.state.huros[house],
|
||||||
tsumoTile: tsumoTile,
|
tsumoTile: $type(tsumoTile),
|
||||||
ronTile: null,
|
ronTile: null,
|
||||||
riichi: this.state.riichis[house],
|
riichi: this.state.riichis[house],
|
||||||
}));
|
}));
|
||||||
const doraCount = Common.calcOwnedDoraCount(handTiles, this.state.huros[house], this.doras);
|
const doraCount = Common.calcOwnedDoraCount(handTiles.map(id => $type(id)), this.state.huros[house], this.doras);
|
||||||
|
// TODO: 赤ドラ
|
||||||
const fans = yakus.map(yaku => yaku.fan).reduce((a, b) => a + b, 0) + doraCount;
|
const fans = yakus.map(yaku => yaku.fan).reduce((a, b) => a + b, 0) + doraCount;
|
||||||
const pointDeltas = Common.calcTsumoHoraPointDeltas(house, fans);
|
const pointDeltas = Common.calcTsumoHoraPointDeltas(house, fans);
|
||||||
this.state.points.e += pointDeltas.e;
|
this.state.points.e += pointDeltas.e;
|
||||||
|
@ -188,10 +203,10 @@ export class PlayerGameEngine {
|
||||||
* @param callee 牌を捨てた人
|
* @param callee 牌を捨てた人
|
||||||
*/
|
*/
|
||||||
public commit_ronHora(callers: House[], callee: House, handTiles: {
|
public commit_ronHora(callers: House[], callee: House, handTiles: {
|
||||||
e: Tile[];
|
e: TileId[];
|
||||||
s: Tile[];
|
s: TileId[];
|
||||||
w: Tile[];
|
w: TileId[];
|
||||||
n: Tile[];
|
n: TileId[];
|
||||||
}): Record<House, KyokuResult | null> {
|
}): Record<House, KyokuResult | null> {
|
||||||
console.log('commit_ronHora', this.state.turn, callers, callee);
|
console.log('commit_ronHora', this.state.turn, callers, callee);
|
||||||
|
|
||||||
|
@ -207,13 +222,14 @@ export class PlayerGameEngine {
|
||||||
for (const house of callers) {
|
for (const house of callers) {
|
||||||
const yakus = YAKU_DEFINITIONS.filter(yaku => yaku.calc({
|
const yakus = YAKU_DEFINITIONS.filter(yaku => yaku.calc({
|
||||||
house: house,
|
house: house,
|
||||||
handTiles: handTiles[house],
|
handTiles: handTiles[house].map(id => $type(id)),
|
||||||
huros: this.state.huros[house],
|
huros: this.state.huros[house],
|
||||||
tsumoTile: null,
|
tsumoTile: null,
|
||||||
ronTile: this.state.hoTiles[callee].at(-1)!,
|
ronTile: $type(this.state.hoTiles[callee].at(-1)!),
|
||||||
riichi: this.state.riichis[house],
|
riichi: this.state.riichis[house],
|
||||||
}));
|
}));
|
||||||
const doraCount = Common.calcOwnedDoraCount(handTiles[house], this.state.huros[house], this.doras);
|
const doraCount = Common.calcOwnedDoraCount(handTiles[house].map(id => $type(id)), this.state.huros[house], this.doras);
|
||||||
|
// TODO: 赤ドラ
|
||||||
const fans = yakus.map(yaku => yaku.fan).reduce((a, b) => a + b, 0) + doraCount;
|
const fans = yakus.map(yaku => yaku.fan).reduce((a, b) => a + b, 0) + doraCount;
|
||||||
const point = Common.fanToPoint(fans, house === 'e');
|
const point = Common.fanToPoint(fans, house === 'e');
|
||||||
this.state.points[callee] -= point;
|
this.state.points[callee] -= point;
|
||||||
|
@ -237,19 +253,19 @@ export class PlayerGameEngine {
|
||||||
* @param caller ポンした人
|
* @param caller ポンした人
|
||||||
* @param callee 牌を捨てた人
|
* @param callee 牌を捨てた人
|
||||||
*/
|
*/
|
||||||
public commit_pon(caller: House, callee: House) {
|
public commit_pon(caller: House, callee: House, tiles: TileId[]) {
|
||||||
this.state.canPon = null;
|
this.state.canPon = null;
|
||||||
|
|
||||||
const lastTile = this.state.hoTiles[callee].pop();
|
this.state.hoTiles[callee].pop();
|
||||||
if (lastTile == null) throw new PlayerGameEngine.InvalidOperationError();
|
|
||||||
if (caller === this.myHouse) {
|
if (caller === this.myHouse) {
|
||||||
this.myHandTiles.splice(this.myHandTiles.indexOf(lastTile), 1);
|
if (this.myHandTiles.includes(tiles[0])) this.myHandTiles.splice(this.myHandTiles.indexOf(tiles[0]), 1);
|
||||||
this.myHandTiles.splice(this.myHandTiles.indexOf(lastTile), 1);
|
if (this.myHandTiles.includes(tiles[1])) this.myHandTiles.splice(this.myHandTiles.indexOf(tiles[1]), 1);
|
||||||
|
if (this.myHandTiles.includes(tiles[2])) this.myHandTiles.splice(this.myHandTiles.indexOf(tiles[2]), 1);
|
||||||
} else {
|
} else {
|
||||||
this.state.handTiles[caller].unshift();
|
this.state.handTiles[caller].unshift();
|
||||||
this.state.handTiles[caller].unshift();
|
this.state.handTiles[caller].unshift();
|
||||||
}
|
}
|
||||||
this.state.huros[caller].push({ type: 'pon', tile: lastTile, from: callee });
|
this.state.huros[caller].push({ type: 'pon', tiles: tiles, from: callee });
|
||||||
|
|
||||||
this.state.turn = caller;
|
this.state.turn = caller;
|
||||||
}
|
}
|
||||||
|
@ -271,25 +287,27 @@ export class PlayerGameEngine {
|
||||||
if (this.state.riichis[this.myHouse]) return false;
|
if (this.state.riichis[this.myHouse]) return false;
|
||||||
if (this.state.points[this.myHouse] < 1000) return false;
|
if (this.state.points[this.myHouse] < 1000) return false;
|
||||||
if (!this.isMenzen) return false;
|
if (!this.isMenzen) return false;
|
||||||
if (Common.getTilesForRiichi(this.myHandTiles).length === 0) return false;
|
if (Common.getTilesForRiichi(this.myHandTileTypes).length === 0) return false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public canAnkan(): boolean {
|
public canAnkan(): boolean {
|
||||||
if (this.state.turn !== this.myHouse) return false;
|
if (this.state.turn !== this.myHouse) return false;
|
||||||
return this.myHandTiles.filter(t => this.myHandTiles.filter(tt => tt === t).length >= 4).length > 0;
|
return this.myHandTiles
|
||||||
|
.filter(t => this.myHandTiles
|
||||||
|
.filter(tt => $type(tt) === $type(t)).length >= 4).length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public canKakan(): boolean {
|
public canKakan(): boolean {
|
||||||
if (this.state.turn !== this.myHouse) return false;
|
if (this.state.turn !== this.myHouse) return false;
|
||||||
return this.state.huros[this.myHouse].filter(h => h.type === 'pon' && this.myHandTiles.includes(h.tile)).length > 0;
|
return this.state.huros[this.myHouse].filter(h => h.type === 'pon' && this.myHandTileTypes.includes($type(h.tiles[0]))).length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getAnkanableTiles(): Tile[] {
|
public getAnkanableTiles(): TileId[] {
|
||||||
return this.myHandTiles.filter(t => this.myHandTiles.filter(tt => tt === t).length >= 4);
|
return this.myHandTiles.filter(t => this.myHandTiles.filter(tt => $type(tt) === $type(t)).length >= 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getKakanableTiles(): Tile[] {
|
public getKakanableTiles(): TileId[] {
|
||||||
return this.state.huros[this.myHouse].filter(h => h.type === 'pon' && this.myHandTiles.includes(h.tile)).map(h => h.tile);
|
return this.myHandTiles.filter(t => this.state.huros[this.myHouse].some(h => h.type === 'pon' && $type(t) === $type(h.tiles[0])));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Tile } from './common.js';
|
import { TileType } from './common.js';
|
||||||
|
|
||||||
export type Log = {
|
export type Log = {
|
||||||
time: number;
|
time: number;
|
||||||
|
@ -14,7 +14,7 @@ export type Log = {
|
||||||
|
|
||||||
export type SerializedLog = number[];
|
export type SerializedLog = number[];
|
||||||
|
|
||||||
export const TILE_MAP: Record<Tile, number> = {
|
export const TILE_MAP: Record<TileType, number> = {
|
||||||
'm1': 1,
|
'm1': 1,
|
||||||
'm2': 2,
|
'm2': 2,
|
||||||
'm3': 3,
|
'm3': 3,
|
||||||
|
@ -51,12 +51,12 @@ export const TILE_MAP: Record<Tile, number> = {
|
||||||
'chun': 34,
|
'chun': 34,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function serializeTile(tile: Tile): number {
|
export function serializeTile(tile: TileType): number {
|
||||||
return TILE_MAP[tile];
|
return TILE_MAP[tile];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deserializeTile(tile: number): Tile {
|
export function deserializeTile(tile: number): TileType {
|
||||||
return Object.keys(TILE_MAP).find(key => TILE_MAP[key as Tile] === tile) as Tile;
|
return Object.keys(TILE_MAP).find(key => TILE_MAP[key as TileType] === tile) as TileType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function serializeLogs(logs: Log[]) {
|
export function serializeLogs(logs: Log[]) {
|
||||||
|
|
Loading…
Reference in a new issue