misskey/packages/misskey-mahjong/test/engine.ts

236 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as assert from 'node:assert';
import * as Common from '../src/common.js';
import { TileType, TileId } from '../src/common.js';
import { MasterGameEngine, MasterState, INITIAL_POINT } from '../src/engine.master.js';
const TILES = [71, 132, 108, 51, 39, 19, 3, 86, 104, 18, 50, 7, 45, 82, 43, 34, 111, 78, 53, 105, 126, 91, 112, 75, 119, 55, 95, 93, 65, 9, 66, 52, 79, 32, 99, 109, 56, 5, 101, 92, 1, 37, 62, 23, 27, 117, 77, 14, 31, 96, 120, 130, 29, 135, 100, 17, 102, 124, 59, 89, 49, 115, 107, 97, 90, 48, 25, 110, 68, 15, 74, 129, 69, 61, 73, 81, 11, 41, 44, 84, 13, 40, 33, 58, 30, 8, 38, 10, 87, 125, 57, 121, 21, 2, 54, 46, 22, 4, 133, 16, 76, 70, 60, 103, 114, 122, 24, 88, 36, 123, 47, 12, 128, 118, 116, 63, 26, 94, 67, 131, 64, 35, 113, 134, 6, 127, 80, 72, 42, 98, 85, 20, 106, 136, 83, 28];
const INITIAL_TILES_LENGTH = 69;
class TileSetBuilder {
private restTiles = [...TILES];
private handTiles: {
e: TileId[] | null,
s: TileId[] | null,
w: TileId[] | null,
n: TileId[] | null,
} = {
e: null,
s: null,
w: null,
n: null,
};
private tiles = new Map<number, TileId>;
public setHandTiles(house: Common.House, tileTypes: TileType[]): this {
if (this.handTiles[house] != null) {
throw new TypeError(`Hand tiles of house '${house}' is already set`);
}
const tiles = tileTypes.map(tile => {
const index = this.restTiles.findIndex(tileId => Common.TILE_ID_MAP.get(tileId)!.t == tile);
if (index == -1) {
throw new TypeError(`Tile '${tile}' is not left`);
}
return this.restTiles.splice(index, 1)[0];
});
this.handTiles[house] = tiles;
return this;
}
/**
* 山のn番目0始まりの牌を指定する。nが負の場合、海底を-1として海底側から数える
*/
public setTile(n: number, tileType: TileType): this {
if (n < 0) {
n += INITIAL_TILES_LENGTH;
}
if (n < 0 || n >= INITIAL_TILES_LENGTH) {
throw new RangeError(`Cannot set ${n}th tile`);
}
const indexInTiles = INITIAL_TILES_LENGTH - n - 1;
if (this.tiles.has(indexInTiles)) {
throw new TypeError(`${n}th tile is already set`);
}
const indexInRestTiles = this.restTiles.findIndex(tileId => Common.TILE_ID_MAP.get(tileId)!.t == tileType);
if (indexInRestTiles == -1) {
throw new TypeError(`Tile '${tileType}' is not left`);
}
this.tiles.set(indexInTiles, this.restTiles.splice(indexInRestTiles, 1)[0]);
return this;
}
public build(): Pick<MasterState, 'tiles' | 'kingTiles' | 'handTiles'> {
const handTiles: MasterState['handTiles'] = {
e: this.handTiles.e ?? this.restTiles.splice(0, 14),
s: this.handTiles.s ?? this.restTiles.splice(0, 13),
w: this.handTiles.w ?? this.restTiles.splice(0, 13),
n: this.handTiles.n ?? this.restTiles.splice(0, 13),
};
const kingTiles: MasterState['kingTiles'] = this.restTiles.splice(0, 14);
const tiles = [...this.restTiles];
for (const [index, tile] of [...this.tiles.entries()].sort(([index1], [index2]) => index1 - index2)) {
tiles.splice(index, 0, tile);
}
return {
tiles,
kingTiles,
handTiles,
};
}
}
function tsumogiri(engine: MasterGameEngine, riichi = false): void {
const house = engine.turn;
if (house == null) {
throw new Error('No one\'s turn');
}
engine.commit_dahai(house, engine.handTiles[house].at(-1)!, riichi);
}
function tsumogiriAndIgnore(engine: MasterGameEngine, riichi = false): void {
tsumogiri(engine, riichi);
if (engine.askings.pon != null || engine.askings.cii != null || engine.askings.kan != null || engine.askings.ron != null) {
engine.commit_resolveCallingInterruption({
pon: false,
cii: false,
kan: false,
ron: [],
});
}
}
describe('Master game engine', () => {
it('tenho', () => {
const engine = new MasterGameEngine(MasterGameEngine.createInitialState(
new TileSetBuilder().setHandTiles('e', ['m1', 'm2', 'm3', 'p6', 'p6', 'p6', 's6', 's7', 's8', 'n', 'n', 'n', 'm3', 'm3']).build(),
));
expect(engine.commit_tsumoHora('e', false).yakus.yakuNames).toEqual(['tenho']);
expect(engine.$state.points).toEqual({
e: INITIAL_POINT + 48000,
s: INITIAL_POINT - 16000,
w: INITIAL_POINT - 16000,
n: INITIAL_POINT - 16000,
});
});
it('chiho', () => {
const engine = new MasterGameEngine(MasterGameEngine.createInitialState(
new TileSetBuilder()
.setHandTiles('s', ['m1', 'm2', 'm3', 'p6', 'p6', 'p6', 's6', 's7', 's8', 'n', 'n', 'n', 'm3'])
.setTile(0, 'm3')
.build(),
));
tsumogiriAndIgnore(engine);
expect(engine.commit_tsumoHora('s', false).yakus.yakuNames).toEqual(['chiho']);
expect(engine.$state.points).toEqual({
e: INITIAL_POINT - 16000,
s: INITIAL_POINT + 32000,
w: INITIAL_POINT - 8000,
n: INITIAL_POINT - 8000,
});
});
it('rinshan', () => {
const engine = new MasterGameEngine(MasterGameEngine.createInitialState(
new TileSetBuilder()
.setHandTiles('e', ['m1', 'm2', 'm3', 'p6', 'p6', 'p6', 's6', 's7', 's8', 'n', 'n', 'n', 'm3', 'n'])
.setTile(-1, 'm3')
.build(),
));
engine.commit_ankan('e', engine.$state.handTiles.e.at(-1)!);
expect(engine.commit_tsumoHora('e', false).yakus.yakuNames).toEqual(['tsumo', 'rinshan']);
expect(engine.$state.points).toEqual({
e: INITIAL_POINT + 3000,
s: INITIAL_POINT - 1000,
w: INITIAL_POINT - 1000,
n: INITIAL_POINT - 1000,
});
});
it('double-riichi ippatsu tsumo', () => {
const engine = new MasterGameEngine(MasterGameEngine.createInitialState(
new TileSetBuilder()
.setHandTiles('e', ['m1', 'm2', 'm3', 'p6', 'p6', 'p6', 's6', 's7', 's8', 'n', 'n', 'n', 'm3', 's'])
.setTile(3, 'm3')
.build(),
));
tsumogiriAndIgnore(engine, true);
tsumogiriAndIgnore(engine);
tsumogiriAndIgnore(engine);
tsumogiriAndIgnore(engine);
expect(engine.commit_tsumoHora('e', false).yakus.yakuNames).toEqual(['tsumo', 'double-riichi', 'ippatsu']);
expect(engine.$state.points).toEqual({
e: INITIAL_POINT + 12000,
s: INITIAL_POINT - 4000,
w: INITIAL_POINT - 4000,
n: INITIAL_POINT - 4000,
});
});
it('double-riichi haitei tsumo', () => {
const engine = new MasterGameEngine(MasterGameEngine.createInitialState(
new TileSetBuilder()
.setHandTiles('s', ['m1', 'm2', 'm3', 'p6', 'p6', 'p6', 's6', 's7', 's8', 'n', 'n', 'n', 'm3'])
.setTile(-1, 'm3')
.build(),
));
tsumogiriAndIgnore(engine);
tsumogiriAndIgnore(engine, true);
while (engine.$state.tiles.length > 0) {
tsumogiriAndIgnore(engine);
}
expect(engine.commit_tsumoHora('s', false).yakus.yakuNames).toEqual(['tsumo', 'double-riichi', 'haitei']);
expect(engine.$state.points).toEqual({
e: INITIAL_POINT - 4000,
s: INITIAL_POINT + 8000,
w: INITIAL_POINT - 2000,
n: INITIAL_POINT - 2000,
});
});
it('double-riichi hotei', () => {
const engine = new MasterGameEngine(MasterGameEngine.createInitialState(
new TileSetBuilder()
.setHandTiles('e', ['m1', 'm2', 'm3', 'p6', 'p6', 'p6', 's6', 's7', 's8', 'n', 'n', 'n', 'm3', 's'])
.setHandTiles('s', ['m3', 'm6', 'p2', 'p5', 'p8', 's4', 'e', 's', 'w', 'haku', 'hatsu', 'chun', 'chun'])
.setTile(-1, 'm3')
.build(),
));
tsumogiriAndIgnore(engine, true);
while (engine.$state.tiles.length > 0) {
tsumogiriAndIgnore(engine);
}
tsumogiri(engine);
expect(engine.commit_resolveCallingInterruption({
pon: false,
cii: false,
kan: false,
ron: ['e'],
}, false).yakus?.e?.yakuNames).toEqual(['double-riichi', 'hotei']);
expect(engine.$state.points).toEqual({
e: INITIAL_POINT + 6000,
s: INITIAL_POINT - 6000,
w: INITIAL_POINT,
n: INITIAL_POINT,
});
});
});