forked from mirrors/misskey
wip
This commit is contained in:
48
packages/misskey-mahjong/src/common.ts
Normal file
48
packages/misskey-mahjong/src/common.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
// NOTE: アガリ形の判定に使われるため並び順が重要
|
||||
// 具体的には、文字列としてソートした際に同じ牌種の1~9が順に並んでいる必要がある
|
||||
// また、字牌は最後にある必要がある
|
||||
export const TILE_TYPES = [
|
||||
'm1',
|
||||
'm2',
|
||||
'm3',
|
||||
'm4',
|
||||
'm5',
|
||||
'm6',
|
||||
'm7',
|
||||
'm8',
|
||||
'm9',
|
||||
'p1',
|
||||
'p2',
|
||||
'p3',
|
||||
'p4',
|
||||
'p5',
|
||||
'p6',
|
||||
'p7',
|
||||
'p8',
|
||||
'p9',
|
||||
's1',
|
||||
's2',
|
||||
's3',
|
||||
's4',
|
||||
's5',
|
||||
's6',
|
||||
's7',
|
||||
's8',
|
||||
's9',
|
||||
'e',
|
||||
's',
|
||||
'w',
|
||||
'n',
|
||||
'haku',
|
||||
'hatsu',
|
||||
'chun',
|
||||
] as const;
|
||||
|
||||
export type Tile = typeof TILE_TYPES[number];
|
||||
|
||||
export type House = 'e' | 's' | 'w' | 'n';
|
||||
@@ -4,51 +4,8 @@
|
||||
*/
|
||||
|
||||
import CRC32 from 'crc-32';
|
||||
|
||||
export const TILE_TYPES = [
|
||||
'bamboo1',
|
||||
'bamboo2',
|
||||
'bamboo3',
|
||||
'bamboo4',
|
||||
'bamboo5',
|
||||
'bamboo6',
|
||||
'bamboo7',
|
||||
'bamboo8',
|
||||
'bamboo9',
|
||||
'character1',
|
||||
'character2',
|
||||
'character3',
|
||||
'character4',
|
||||
'character5',
|
||||
'character6',
|
||||
'character7',
|
||||
'character8',
|
||||
'character9',
|
||||
'circle1',
|
||||
'circle2',
|
||||
'circle3',
|
||||
'circle4',
|
||||
'circle5',
|
||||
'circle6',
|
||||
'circle7',
|
||||
'circle8',
|
||||
'circle9',
|
||||
'wind-east',
|
||||
'wind-south',
|
||||
'wind-west',
|
||||
'wind-north',
|
||||
'dragon-red',
|
||||
'dragon-green',
|
||||
'dragon-white',
|
||||
] as const;
|
||||
|
||||
export type Tile = typeof TILE_TYPES[number];
|
||||
|
||||
export function isTile(tile: string): tile is Tile {
|
||||
return TILE_TYPES.includes(tile as Tile);
|
||||
}
|
||||
|
||||
export type House = 'e' | 's' | 'w' | 'n';
|
||||
import { Tile, House, TILE_TYPES } from './common.js';
|
||||
import * as Utils from './utils.js';
|
||||
|
||||
export type MasterState = {
|
||||
user1House: House;
|
||||
@@ -81,34 +38,56 @@ export type MasterState = {
|
||||
wPoints: number;
|
||||
nPoints: number;
|
||||
turn: House | null;
|
||||
ponAsking: {
|
||||
|
||||
ronAsking: {
|
||||
/**
|
||||
* 牌を捨てた人
|
||||
*/
|
||||
source: House;
|
||||
|
||||
/**
|
||||
* ロンする権利がある人
|
||||
*/
|
||||
targets: House[];
|
||||
} | null;
|
||||
|
||||
ponAsking: {
|
||||
/**
|
||||
* 牌を捨てた人
|
||||
*/
|
||||
source: House;
|
||||
|
||||
/**
|
||||
* ポンする権利がある人
|
||||
*/
|
||||
target: House;
|
||||
} | null;
|
||||
|
||||
ciiAsking: {
|
||||
/**
|
||||
* 牌を捨てた人
|
||||
*/
|
||||
source: House;
|
||||
|
||||
/**
|
||||
* チーする権利がある人(sourceの下家なのは自明だがプログラム簡略化のため)
|
||||
*/
|
||||
target: House;
|
||||
} | null;
|
||||
|
||||
kanAsking: {
|
||||
/**
|
||||
* 牌を捨てた人
|
||||
*/
|
||||
source: House;
|
||||
|
||||
/**
|
||||
* カンする権利がある人
|
||||
*/
|
||||
target: House;
|
||||
} | null;
|
||||
};
|
||||
|
||||
export class Common {
|
||||
public static nextHouse(house: House): House {
|
||||
switch (house) {
|
||||
case 'e':
|
||||
return 's';
|
||||
case 's':
|
||||
return 'w';
|
||||
case 'w':
|
||||
return 'n';
|
||||
case 'n':
|
||||
return 'e';
|
||||
}
|
||||
}
|
||||
|
||||
public static checkYaku(tiles: Tile[]) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export class MasterGameEngine {
|
||||
public state: MasterState;
|
||||
|
||||
@@ -117,7 +96,7 @@ export class MasterGameEngine {
|
||||
}
|
||||
|
||||
public static createInitialState(): MasterState {
|
||||
const tiles = TILE_TYPES.slice();
|
||||
const tiles = [...TILE_TYPES.slice(), ...TILE_TYPES.slice(), ...TILE_TYPES.slice(), ...TILE_TYPES.slice()];
|
||||
tiles.sort(() => Math.random() - 0.5);
|
||||
|
||||
const eHandTiles = tiles.splice(0, 14);
|
||||
@@ -161,62 +140,64 @@ export class MasterGameEngine {
|
||||
};
|
||||
}
|
||||
|
||||
private ツモ(): Tile {
|
||||
private tsumo(): Tile {
|
||||
const tile = this.state.tiles.pop();
|
||||
switch (this.state.turn) {
|
||||
case 'e':
|
||||
this.state.eHandTiles.push(tile);
|
||||
break;
|
||||
case 's':
|
||||
this.state.sHandTiles.push(tile);
|
||||
break;
|
||||
case 'w':
|
||||
this.state.wHandTiles.push(tile);
|
||||
break;
|
||||
case 'n':
|
||||
this.state.nHandTiles.push(tile);
|
||||
break;
|
||||
}
|
||||
if (tile == null) throw new Error('No tiles left');
|
||||
this.getHandTilesOf(this.state.turn).push(tile);
|
||||
return tile;
|
||||
}
|
||||
|
||||
public op_dahai(house: House, tile: Tile) {
|
||||
if (this.state.turn !== house) throw new Error('Not your turn');
|
||||
|
||||
const handTiles = this.getHandTilesOf(house);
|
||||
if (!handTiles.includes(tile)) throw new Error('No such tile in your hand');
|
||||
handTiles.splice(handTiles.indexOf(tile), 1);
|
||||
this.getHoTilesOf(house).push(tile);
|
||||
|
||||
const canRonHouses: House[] = [];
|
||||
switch (house) {
|
||||
case 'e':
|
||||
if (!this.state.eHandTiles.includes(tile)) throw new Error('Invalid tile');
|
||||
this.state.eHandTiles.splice(this.state.eHandTiles.indexOf(tile), 1);
|
||||
this.state.eHoTiles.push(tile);
|
||||
if (this.canRon('s', tile)) canRonHouses.push('s');
|
||||
if (this.canRon('w', tile)) canRonHouses.push('w');
|
||||
if (this.canRon('n', tile)) canRonHouses.push('n');
|
||||
break;
|
||||
case 's':
|
||||
if (!this.state.sHandTiles.includes(tile)) throw new Error('Invalid tile');
|
||||
this.state.sHandTiles.splice(this.state.sHandTiles.indexOf(tile), 1);
|
||||
this.state.sHoTiles.push(tile);
|
||||
if (this.canRon('e', tile)) canRonHouses.push('e');
|
||||
if (this.canRon('w', tile)) canRonHouses.push('w');
|
||||
if (this.canRon('n', tile)) canRonHouses.push('n');
|
||||
break;
|
||||
case 'w':
|
||||
if (!this.state.wHandTiles.includes(tile)) throw new Error('Invalid tile');
|
||||
this.state.wHandTiles.splice(this.state.wHandTiles.indexOf(tile), 1);
|
||||
this.state.wHoTiles.push(tile);
|
||||
if (this.canRon('e', tile)) canRonHouses.push('e');
|
||||
if (this.canRon('s', tile)) canRonHouses.push('s');
|
||||
if (this.canRon('n', tile)) canRonHouses.push('n');
|
||||
break;
|
||||
case 'n':
|
||||
if (!this.state.nHandTiles.includes(tile)) throw new Error('Invalid tile');
|
||||
this.state.nHandTiles.splice(this.state.nHandTiles.indexOf(tile), 1);
|
||||
this.state.nHoTiles.push(tile);
|
||||
if (this.canRon('e', tile)) canRonHouses.push('e');
|
||||
if (this.canRon('s', tile)) canRonHouses.push('s');
|
||||
if (this.canRon('w', tile)) canRonHouses.push('w');
|
||||
break;
|
||||
}
|
||||
|
||||
const canKanHouse: House | null = null;
|
||||
|
||||
let canPonHouse: House | null = null;
|
||||
if (house === 'e') {
|
||||
canPonHouse = this.canPon('s', tile) ? 's' : this.canPon('w', tile) ? 'w' : this.canPon('n', tile) ? 'n' : null;
|
||||
} else if (house === 's') {
|
||||
canPonHouse = this.canPon('e', tile) ? 'e' : this.canPon('w', tile) ? 'w' : this.canPon('n', tile) ? 'n' : null;
|
||||
} else if (house === 'w') {
|
||||
canPonHouse = this.canPon('e', tile) ? 'e' : this.canPon('s', tile) ? 's' : this.canPon('n', tile) ? 'n' : null;
|
||||
} else if (house === 'n') {
|
||||
canPonHouse = this.canPon('e', tile) ? 'e' : this.canPon('s', tile) ? 's' : this.canPon('w', tile) ? 'w' : null;
|
||||
switch (house) {
|
||||
case 'e':
|
||||
canPonHouse = this.canPon('s', tile) ? 's' : this.canPon('w', tile) ? 'w' : this.canPon('n', tile) ? 'n' : null;
|
||||
break;
|
||||
case 's':
|
||||
canPonHouse = this.canPon('e', tile) ? 'e' : this.canPon('w', tile) ? 'w' : this.canPon('n', tile) ? 'n' : null;
|
||||
break;
|
||||
case 'w':
|
||||
canPonHouse = this.canPon('e', tile) ? 'e' : this.canPon('s', tile) ? 's' : this.canPon('n', tile) ? 'n' : null;
|
||||
break;
|
||||
case 'n':
|
||||
canPonHouse = this.canPon('e', tile) ? 'e' : this.canPon('s', tile) ? 's' : this.canPon('w', tile) ? 'w' : null;
|
||||
break;
|
||||
}
|
||||
|
||||
const canCiiHouse: House | null = null;
|
||||
// TODO
|
||||
//let canCii: boolean = false;
|
||||
//if (house === 'e') {
|
||||
@@ -229,96 +210,237 @@ export class MasterGameEngine {
|
||||
// canCii = this.state.eHandTiles...
|
||||
//}
|
||||
|
||||
if (canPonHouse) {
|
||||
this.state.ponAsking = {
|
||||
source: house,
|
||||
target: canPonHouse,
|
||||
};
|
||||
if (canRonHouses.length > 0 || canPonHouse != null) {
|
||||
if (canRonHouses.length > 0) {
|
||||
this.state.ronAsking = {
|
||||
source: house,
|
||||
targets: canRonHouses,
|
||||
};
|
||||
}
|
||||
if (canKanHouse != null) {
|
||||
this.state.kanAsking = {
|
||||
source: house,
|
||||
target: canKanHouse,
|
||||
};
|
||||
}
|
||||
if (canPonHouse != null) {
|
||||
this.state.ponAsking = {
|
||||
source: house,
|
||||
target: canPonHouse,
|
||||
};
|
||||
}
|
||||
if (canCiiHouse != null) {
|
||||
this.state.ciiAsking = {
|
||||
source: house,
|
||||
target: canCiiHouse,
|
||||
};
|
||||
}
|
||||
return {
|
||||
asking: true,
|
||||
canRonHouses: canRonHouses,
|
||||
canKanHouse: canKanHouse,
|
||||
canPonHouse: canPonHouse,
|
||||
canCiiHouse: canCiiHouse,
|
||||
};
|
||||
}
|
||||
|
||||
this.state.turn = Common.nextHouse(house);
|
||||
this.state.turn = Utils.nextHouse(house);
|
||||
|
||||
const tsumoTile = this.ツモ();
|
||||
const tsumoTile = this.tsumo();
|
||||
|
||||
return {
|
||||
tsumo: tsumoTile,
|
||||
asking: false,
|
||||
tsumoTile: tsumoTile,
|
||||
};
|
||||
}
|
||||
|
||||
public op_pon(house: House) {
|
||||
if (this.state.ponAsking == null) throw new Error('No one is asking for pon');
|
||||
if (this.state.ponAsking.target !== house) throw new Error('Not you');
|
||||
public op_resolveCallAndRonInterruption(answers: {
|
||||
pon: boolean;
|
||||
cii: boolean;
|
||||
kan: boolean;
|
||||
ron: House[];
|
||||
}) {
|
||||
if (this.state.ponAsking == null && this.state.ciiAsking == null && this.state.kanAsking == null && this.state.ronAsking == null) throw new Error();
|
||||
|
||||
const source = this.state.ponAsking.source;
|
||||
const target = this.state.ponAsking.target;
|
||||
this.state.ponAsking = null;
|
||||
const clearAsking = () => {
|
||||
this.state.ponAsking = null;
|
||||
this.state.ciiAsking = null;
|
||||
this.state.kanAsking = null;
|
||||
this.state.ronAsking = null;
|
||||
};
|
||||
|
||||
let tile: Tile;
|
||||
|
||||
switch (source) {
|
||||
case 'e':
|
||||
tile = this.state.eHoTiles.pop();
|
||||
break;
|
||||
case 's':
|
||||
tile = this.state.sHoTiles.pop();
|
||||
break;
|
||||
case 'w':
|
||||
tile = this.state.wHoTiles.pop();
|
||||
break;
|
||||
case 'n':
|
||||
tile = this.state.nHoTiles.pop();
|
||||
break;
|
||||
default: throw new Error('Invalid source');
|
||||
if (this.state.ronAsking != null && answers.ron.length > 0) {
|
||||
// TODO
|
||||
return;
|
||||
}
|
||||
|
||||
switch (target) {
|
||||
case 'e':
|
||||
this.state.ePonnedTiles.push({ tile, from: source });
|
||||
break;
|
||||
case 's':
|
||||
this.state.sPonnedTiles.push({ tile, from: source });
|
||||
break;
|
||||
case 'w':
|
||||
this.state.wPonnedTiles.push({ tile, from: source });
|
||||
break;
|
||||
case 'n':
|
||||
this.state.nPonnedTiles.push({ tile, from: source });
|
||||
break;
|
||||
if (this.state.kanAsking != null && answers.kan) {
|
||||
const source = this.state.kanAsking.source;
|
||||
const target = this.state.kanAsking.target;
|
||||
|
||||
const tile = this.getHoTilesOf(source).pop();
|
||||
this.getKannedTilesOf(target).push({ tile, from: source });
|
||||
|
||||
clearAsking();
|
||||
this.state.turn = target;
|
||||
// TODO
|
||||
return;
|
||||
}
|
||||
|
||||
this.state.turn = target;
|
||||
}
|
||||
if (this.state.ponAsking != null && answers.pon) {
|
||||
const source = this.state.ponAsking.source;
|
||||
const target = this.state.ponAsking.target;
|
||||
|
||||
public op_noOnePon() {
|
||||
if (this.state.ponAsking == null) throw new Error('No one is asking for pon');
|
||||
const tile = this.getHoTilesOf(source).pop();
|
||||
this.getPonnedTilesOf(target).push({ tile, from: source });
|
||||
|
||||
this.state.ponAsking = null;
|
||||
this.state.turn = Common.nextHouse(this.state.turn);
|
||||
clearAsking();
|
||||
this.state.turn = target;
|
||||
return {
|
||||
type: 'ponned',
|
||||
house: this.state.turn,
|
||||
tile,
|
||||
};
|
||||
}
|
||||
|
||||
const tile = this.ツモ();
|
||||
if (this.state.ciiAsking != null && answers.cii) {
|
||||
const source = this.state.ciiAsking.source;
|
||||
const target = this.state.ciiAsking.target;
|
||||
|
||||
const tile = this.getHoTilesOf(source).pop();
|
||||
this.getCiiedTilesOf(target).push({ tile, from: source });
|
||||
|
||||
clearAsking();
|
||||
this.state.turn = target;
|
||||
return {
|
||||
type: 'ciied',
|
||||
house: this.state.turn,
|
||||
tile,
|
||||
};
|
||||
}
|
||||
|
||||
clearAsking();
|
||||
this.state.turn = Utils.nextHouse(this.state.turn);
|
||||
|
||||
const tile = this.tsumo();
|
||||
|
||||
return {
|
||||
type: 'tsumo',
|
||||
house: this.state.turn,
|
||||
tile,
|
||||
};
|
||||
}
|
||||
|
||||
private canRon(house: House, tile: Tile): boolean {
|
||||
// フリテン
|
||||
// TODO: ポンされるなどして自分の河にない場合の考慮
|
||||
if (this.getHoTilesOf(house).includes(tile)) return false;
|
||||
|
||||
const horaSets = Utils.getHoraSets(this.getHandTilesOf(house).concat(tile));
|
||||
if (horaSets.length === 0) return false; // 完成形じゃない
|
||||
|
||||
const yakus = YAKU_DEFINITIONS.filter(yaku => yaku.calc(this.state, { tsumoTile: null, ronTile: tile }));
|
||||
if (yakus.length === 0) return false; // 役がない
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private canPon(house: House, tile: Tile): boolean {
|
||||
switch (house) {
|
||||
case 'e':
|
||||
return this.state.eHandTiles.filter(t => t === tile).length === 2;
|
||||
case 's':
|
||||
return this.state.sHandTiles.filter(t => t === tile).length === 2;
|
||||
case 'w':
|
||||
return this.state.wHandTiles.filter(t => t === tile).length === 2;
|
||||
case 'n':
|
||||
return this.state.nHandTiles.filter(t => t === tile).length === 2;
|
||||
return this.getHandTilesOf(house).filter(t => t === tile).length === 2;
|
||||
}
|
||||
|
||||
public getHouse(index: 1 | 2 | 3 | 4): House {
|
||||
switch (index) {
|
||||
case 1: return this.state.user1House;
|
||||
case 2: return this.state.user2House;
|
||||
case 3: return this.state.user3House;
|
||||
case 4: return this.state.user4House;
|
||||
}
|
||||
}
|
||||
|
||||
public getHandTilesOf(house: House): Tile[] {
|
||||
switch (house) {
|
||||
case 'e': return this.state.eHandTiles;
|
||||
case 's': return this.state.sHandTiles;
|
||||
case 'w': return this.state.wHandTiles;
|
||||
case 'n': return this.state.nHandTiles;
|
||||
}
|
||||
}
|
||||
|
||||
public getHoTilesOf(house: House): Tile[] {
|
||||
switch (house) {
|
||||
case 'e': return this.state.eHoTiles;
|
||||
case 's': return this.state.sHoTiles;
|
||||
case 'w': return this.state.wHoTiles;
|
||||
case 'n': return this.state.nHoTiles;
|
||||
}
|
||||
}
|
||||
|
||||
public getPonnedTilesOf(house: House): { tile: Tile; from: House; }[] {
|
||||
switch (house) {
|
||||
case 'e': return this.state.ePonnedTiles;
|
||||
case 's': return this.state.sPonnedTiles;
|
||||
case 'w': return this.state.wPonnedTiles;
|
||||
case 'n': return this.state.nPonnedTiles;
|
||||
}
|
||||
}
|
||||
|
||||
public getCiiedTilesOf(house: House): { tiles: [Tile, Tile, Tile]; from: House; }[] {
|
||||
switch (house) {
|
||||
case 'e': return this.state.eCiiedTiles;
|
||||
case 's': return this.state.sCiiedTiles;
|
||||
case 'w': return this.state.wCiiedTiles;
|
||||
case 'n': return this.state.nCiiedTiles;
|
||||
}
|
||||
}
|
||||
|
||||
public getKannedTilesOf(house: House): { tile: Tile; from: House; }[] {
|
||||
switch (house) {
|
||||
case 'e': return this.state.ePonnedTiles;
|
||||
case 's': return this.state.sPonnedTiles;
|
||||
case 'w': return this.state.wPonnedTiles;
|
||||
case 'n': return this.state.nPonnedTiles;
|
||||
}
|
||||
}
|
||||
|
||||
public createPlayerState(index: 1 | 2 | 3 | 4): PlayerState {
|
||||
const house = this.getHouse(index);
|
||||
|
||||
return {
|
||||
user1House: this.state.user1House,
|
||||
user2House: this.state.user2House,
|
||||
user3House: this.state.user3House,
|
||||
user4House: this.state.user4House,
|
||||
tilesCount: this.state.tiles.length,
|
||||
eHandTiles: house === 'e' ? this.state.eHandTiles : this.state.eHandTiles.map(() => null),
|
||||
sHandTiles: house === 's' ? this.state.sHandTiles : this.state.sHandTiles.map(() => null),
|
||||
wHandTiles: house === 'w' ? this.state.wHandTiles : this.state.wHandTiles.map(() => null),
|
||||
nHandTiles: house === 'n' ? this.state.nHandTiles : this.state.nHandTiles.map(() => null),
|
||||
eHoTiles: this.state.eHoTiles,
|
||||
sHoTiles: this.state.sHoTiles,
|
||||
wHoTiles: this.state.wHoTiles,
|
||||
nHoTiles: this.state.nHoTiles,
|
||||
ePonnedTiles: this.state.ePonnedTiles,
|
||||
sPonnedTiles: this.state.sPonnedTiles,
|
||||
wPonnedTiles: this.state.wPonnedTiles,
|
||||
nPonnedTiles: this.state.nPonnedTiles,
|
||||
eCiiedTiles: this.state.eCiiedTiles,
|
||||
sCiiedTiles: this.state.sCiiedTiles,
|
||||
wCiiedTiles: this.state.wCiiedTiles,
|
||||
nCiiedTiles: this.state.nCiiedTiles,
|
||||
eRiichi: this.state.eRiichi,
|
||||
sRiichi: this.state.sRiichi,
|
||||
wRiichi: this.state.wRiichi,
|
||||
nRiichi: this.state.nRiichi,
|
||||
ePoints: this.state.ePoints,
|
||||
sPoints: this.state.sPoints,
|
||||
wPoints: this.state.wPoints,
|
||||
nPoints: this.state.nPoints,
|
||||
latestDahaiedTile: null,
|
||||
turn: this.state.turn,
|
||||
};
|
||||
}
|
||||
|
||||
public calcCrc32ForUser1(): number {
|
||||
// TODO
|
||||
}
|
||||
@@ -368,6 +490,10 @@ export type PlayerState = {
|
||||
nPoints: number;
|
||||
latestDahaiedTile: Tile | null;
|
||||
turn: House | null;
|
||||
canPonTo: House | null;
|
||||
canCiiTo: House | null;
|
||||
canKanTo: House | null;
|
||||
canRonTo: House | null;
|
||||
};
|
||||
|
||||
export class PlayerGameEngine {
|
||||
@@ -411,113 +537,104 @@ export class PlayerGameEngine {
|
||||
}
|
||||
}
|
||||
|
||||
public getHandTilesOf(house: House) {
|
||||
switch (house) {
|
||||
case 'e': return this.state.eHandTiles;
|
||||
case 's': return this.state.sHandTiles;
|
||||
case 'w': return this.state.wHandTiles;
|
||||
case 'n': return this.state.nHandTiles;
|
||||
}
|
||||
}
|
||||
|
||||
public getHoTilesOf(house: House): Tile[] {
|
||||
switch (house) {
|
||||
case 'e': return this.state.eHoTiles;
|
||||
case 's': return this.state.sHoTiles;
|
||||
case 'w': return this.state.wHoTiles;
|
||||
case 'n': return this.state.nHoTiles;
|
||||
}
|
||||
}
|
||||
|
||||
public getPonnedTilesOf(house: House): { tile: Tile; from: House; }[] {
|
||||
switch (house) {
|
||||
case 'e': return this.state.ePonnedTiles;
|
||||
case 's': return this.state.sPonnedTiles;
|
||||
case 'w': return this.state.wPonnedTiles;
|
||||
case 'n': return this.state.nPonnedTiles;
|
||||
}
|
||||
}
|
||||
|
||||
public getCiiedTilesOf(house: House): { tiles: [Tile, Tile, Tile]; from: House; }[] {
|
||||
switch (house) {
|
||||
case 'e': return this.state.eCiiedTiles;
|
||||
case 's': return this.state.sCiiedTiles;
|
||||
case 'w': return this.state.wCiiedTiles;
|
||||
case 'n': return this.state.nCiiedTiles;
|
||||
}
|
||||
}
|
||||
|
||||
public getKannedTilesOf(house: House): { tile: Tile; from: House; }[] {
|
||||
switch (house) {
|
||||
case 'e': return this.state.ePonnedTiles;
|
||||
case 's': return this.state.sPonnedTiles;
|
||||
case 'w': return this.state.wPonnedTiles;
|
||||
case 'n': return this.state.nPonnedTiles;
|
||||
}
|
||||
}
|
||||
|
||||
public op_tsumo(house: House, tile: Tile) {
|
||||
if (house === this.myHouse) {
|
||||
this.myHandTiles.push(tile);
|
||||
} else {
|
||||
switch (house) {
|
||||
case 'e':
|
||||
this.state.eHandTiles.push(null);
|
||||
break;
|
||||
case 's':
|
||||
this.state.sHandTiles.push(null);
|
||||
break;
|
||||
case 'w':
|
||||
this.state.wHandTiles.push(null);
|
||||
break;
|
||||
case 'n':
|
||||
this.state.nHandTiles.push(null);
|
||||
break;
|
||||
}
|
||||
this.getHandTilesOf(house).push(null);
|
||||
}
|
||||
}
|
||||
|
||||
public op_dahai(house: House, tile: Tile) {
|
||||
console.log(this.state.turn, house, tile);
|
||||
|
||||
if (this.state.turn !== house) throw new PlayerGameEngine.InvalidOperationError();
|
||||
|
||||
if (house === this.myHouse) {
|
||||
this.myHandTiles.splice(this.myHandTiles.indexOf(tile), 1);
|
||||
this.myHoTiles.push(tile);
|
||||
} else {
|
||||
switch (house) {
|
||||
case 'e':
|
||||
this.state.eHandTiles.pop();
|
||||
this.state.eHoTiles.push(tile);
|
||||
break;
|
||||
case 's':
|
||||
this.state.sHandTiles.pop();
|
||||
this.state.sHoTiles.push(tile);
|
||||
break;
|
||||
case 'w':
|
||||
this.state.wHandTiles.pop();
|
||||
this.state.wHoTiles.push(tile);
|
||||
break;
|
||||
case 'n':
|
||||
this.state.nHandTiles.pop();
|
||||
this.state.nHoTiles.push(tile);
|
||||
break;
|
||||
}
|
||||
this.getHandTilesOf(house).pop();
|
||||
this.getHoTilesOf(house).push(tile);
|
||||
}
|
||||
|
||||
this.state.turn = Utils.nextHouse(this.state.turn);
|
||||
|
||||
if (house === this.myHouse) {
|
||||
this.state.turn = null;
|
||||
} else {
|
||||
const canPon = this.myHandTiles.filter(t => t === tile).length === 2;
|
||||
|
||||
// TODO: canCii
|
||||
|
||||
return {
|
||||
canPon,
|
||||
};
|
||||
if (canPon) this.state.canPonTo = house;
|
||||
}
|
||||
}
|
||||
|
||||
public op_pon(source: House, target: House) {
|
||||
let tile: Tile;
|
||||
this.state.canPonTo = null;
|
||||
|
||||
switch (source) {
|
||||
case 'e': {
|
||||
const lastTile = this.state.eHoTiles.pop();
|
||||
if (lastTile == null) throw new PlayerGameEngine.InvalidOperationError();
|
||||
tile = lastTile;
|
||||
break;
|
||||
}
|
||||
case 's': {
|
||||
const lastTile = this.state.sHoTiles.pop();
|
||||
if (lastTile == null) throw new PlayerGameEngine.InvalidOperationError();
|
||||
tile = lastTile;
|
||||
break;
|
||||
}
|
||||
case 'w': {
|
||||
const lastTile = this.state.wHoTiles.pop();
|
||||
if (lastTile == null) throw new PlayerGameEngine.InvalidOperationError();
|
||||
tile = lastTile;
|
||||
break;
|
||||
}
|
||||
case 'n': {
|
||||
const lastTile = this.state.nHoTiles.pop();
|
||||
if (lastTile == null) throw new PlayerGameEngine.InvalidOperationError();
|
||||
tile = lastTile;
|
||||
break;
|
||||
}
|
||||
default: throw new Error('Invalid source');
|
||||
}
|
||||
|
||||
switch (target) {
|
||||
case 'e':
|
||||
this.state.ePonnedTiles.push({ tile, from: source });
|
||||
break;
|
||||
case 's':
|
||||
this.state.sPonnedTiles.push({ tile, from: source });
|
||||
break;
|
||||
case 'w':
|
||||
this.state.wPonnedTiles.push({ tile, from: source });
|
||||
break;
|
||||
case 'n':
|
||||
this.state.nPonnedTiles.push({ tile, from: source });
|
||||
break;
|
||||
}
|
||||
const lastTile = this.getHoTilesOf(source).pop();
|
||||
if (lastTile == null) throw new PlayerGameEngine.InvalidOperationError();
|
||||
this.getPonnedTilesOf(target).push({ tile: lastTile, from: source });
|
||||
|
||||
this.state.turn = target;
|
||||
}
|
||||
|
||||
public op_nop() {
|
||||
this.state.canPonTo = null;
|
||||
}
|
||||
}
|
||||
|
||||
const YAKU_DEFINITIONS = [{
|
||||
name: 'riichi',
|
||||
fan: 1,
|
||||
calc: (state: PlayerState, ctx: { tsumoTile: Tile; ronTile: Tile; }) => {
|
||||
const house = state.turn;
|
||||
return house === 'e' ? state.eRiichi : house === 's' ? state.sRiichi : house === 'w' ? state.wRiichi : state.nRiichi;
|
||||
},
|
||||
}];
|
||||
|
||||
@@ -5,3 +5,5 @@
|
||||
|
||||
export * as Engine from './engine.js';
|
||||
export * as Serializer from './serializer.js';
|
||||
export * as Common from './common.js';
|
||||
export * as Utils from './utils.js';
|
||||
|
||||
@@ -15,40 +15,40 @@ export type Log = {
|
||||
export type SerializedLog = number[];
|
||||
|
||||
export const TILE_MAP: Record<Tile, number> = {
|
||||
'bamboo1': 1,
|
||||
'bamboo2': 2,
|
||||
'bamboo3': 3,
|
||||
'bamboo4': 4,
|
||||
'bamboo5': 5,
|
||||
'bamboo6': 6,
|
||||
'bamboo7': 7,
|
||||
'bamboo8': 8,
|
||||
'bamboo9': 9,
|
||||
'character1': 10,
|
||||
'character2': 11,
|
||||
'character3': 12,
|
||||
'character4': 13,
|
||||
'character5': 14,
|
||||
'character6': 15,
|
||||
'character7': 16,
|
||||
'character8': 17,
|
||||
'character9': 18,
|
||||
'circle1': 19,
|
||||
'circle2': 20,
|
||||
'circle3': 21,
|
||||
'circle4': 22,
|
||||
'circle5': 23,
|
||||
'circle6': 24,
|
||||
'circle7': 25,
|
||||
'circle8': 26,
|
||||
'circle9': 27,
|
||||
'wind-east': 28,
|
||||
'wind-south': 29,
|
||||
'wind-west': 30,
|
||||
'wind-north': 31,
|
||||
'dragon-red': 32,
|
||||
'dragon-green': 33,
|
||||
'dragon-white': 34,
|
||||
'm1': 1,
|
||||
'm2': 2,
|
||||
'm3': 3,
|
||||
'm4': 4,
|
||||
'm5': 5,
|
||||
'm6': 6,
|
||||
'm7': 7,
|
||||
'm8': 8,
|
||||
'm9': 9,
|
||||
'p1': 10,
|
||||
'p2': 11,
|
||||
'p3': 12,
|
||||
'p4': 13,
|
||||
'p5': 14,
|
||||
'p6': 15,
|
||||
'p7': 16,
|
||||
'p8': 17,
|
||||
'p9': 18,
|
||||
's1': 19,
|
||||
's2': 20,
|
||||
's3': 21,
|
||||
's4': 22,
|
||||
's5': 23,
|
||||
's6': 24,
|
||||
's7': 25,
|
||||
's8': 26,
|
||||
's9': 27,
|
||||
'e': 28,
|
||||
's': 29,
|
||||
'w': 30,
|
||||
'n': 31,
|
||||
'haku': 32,
|
||||
'hatsu': 33,
|
||||
'chun': 34,
|
||||
};
|
||||
|
||||
export function serializeTile(tile: Tile): number {
|
||||
|
||||
235
packages/misskey-mahjong/src/utils.ts
Normal file
235
packages/misskey-mahjong/src/utils.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { House, 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';
|
||||
}
|
||||
}
|
||||
|
||||
export function prevHouse(house: House): House {
|
||||
switch (house) {
|
||||
case 'e': return 'n';
|
||||
case 's': return 'e';
|
||||
case 'w': return 's';
|
||||
case 'n': return 'w';
|
||||
}
|
||||
}
|
||||
|
||||
type HoraSet = {
|
||||
head: Tile;
|
||||
mentsus: [Tile, Tile, Tile][];
|
||||
};
|
||||
|
||||
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 tempTempHandTilesWithoutKotsuAndShuntsu: (Tile | null)[] = [...tempHandTilesWithoutKotsu];
|
||||
|
||||
const shuntsus: [Tile, Tile, Tile][] = [];
|
||||
let i = 0;
|
||||
while (i < tempHandTilesWithoutKotsu.length) {
|
||||
const headThree = tempHandTilesWithoutKotsu.slice(i, i + 3);
|
||||
if (headThree.length !== 3) break;
|
||||
|
||||
for (const shuntuPattern of SHUNTU_PATTERNS) {
|
||||
if (headThree[0] === shuntuPattern[0] && headThree[1] === shuntuPattern[1] && headThree[2] === shuntuPattern[2]) {
|
||||
shuntsus.push(shuntuPattern);
|
||||
tempTempHandTilesWithoutKotsuAndShuntsu[i] = null;
|
||||
tempTempHandTilesWithoutKotsuAndShuntsu[i + 1] = null;
|
||||
tempTempHandTilesWithoutKotsuAndShuntsu[i + 2] = null;
|
||||
i += 3;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
const tempHandTilesWithoutKotsuAndShuntsu = tempTempHandTilesWithoutKotsuAndShuntsu.filter(t => t != null) as Tile[];
|
||||
|
||||
if (tempHandTilesWithoutKotsuAndShuntsu.length === 0) { // アガリ形
|
||||
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;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user