misskey/packages/misskey-mahjong/src/engine.master.ts

603 lines
15 KiB
TypeScript
Raw Normal View History

2024-01-29 10:46:23 +09:00
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import CRC32 from 'crc-32';
2024-01-29 17:15:09 +09:00
import { Tile, House, Huro, TILE_TYPES, YAKU_DEFINITIONS } from './common.js';
2024-01-31 18:31:02 +09:00
import * as Common from './common.js';
2024-01-29 10:46:23 +09:00
import * as Utils from './utils.js';
import { PlayerState } from './engine.player.js';
export type MasterState = {
user1House: House;
user2House: House;
user3House: House;
user4House: House;
2024-01-29 14:14:00 +09:00
round: 'e' | 's' | 'w' | 'n';
kyoku: number;
2024-01-29 10:46:23 +09:00
tiles: Tile[];
2024-01-31 18:31:02 +09:00
kingTiles: Tile[];
activatedDorasCount: number;
2024-01-29 10:46:23 +09:00
/**
*
*/
handTiles: {
e: Tile[];
s: Tile[];
w: Tile[];
n: Tile[];
};
hoTiles: {
e: Tile[];
s: Tile[];
w: Tile[];
n: Tile[];
};
huros: {
e: Huro[];
s: Huro[];
w: Huro[];
n: Huro[];
};
riichis: {
e: boolean;
s: boolean;
w: boolean;
n: boolean;
};
points: {
e: number;
s: number;
w: number;
n: number;
};
turn: House | null;
nextTurnAfterAsking: House | null;
ronAsking: {
/**
*
*/
2024-01-29 17:15:09 +09:00
callee: House;
2024-01-29 10:46:23 +09:00
/**
*
*/
2024-01-29 17:15:09 +09:00
callers: House[];
2024-01-29 10:46:23 +09:00
} | null;
ponAsking: {
/**
*
*/
2024-01-29 17:15:09 +09:00
callee: House;
2024-01-29 10:46:23 +09:00
/**
*
*/
2024-01-29 17:15:09 +09:00
caller: House;
2024-01-29 10:46:23 +09:00
} | null;
ciiAsking: {
/**
*
*/
2024-01-29 17:15:09 +09:00
callee: House;
2024-01-29 10:46:23 +09:00
/**
2024-01-29 17:15:09 +09:00
* (calleeの下家なのは自明だがプログラム簡略化のため)
2024-01-29 10:46:23 +09:00
*/
2024-01-29 17:15:09 +09:00
caller: House;
2024-01-29 10:46:23 +09:00
} | null;
kanAsking: {
/**
*
*/
2024-01-29 17:15:09 +09:00
callee: House;
2024-01-29 10:46:23 +09:00
/**
*
*/
2024-01-29 17:15:09 +09:00
caller: House;
2024-01-29 10:46:23 +09:00
} | null;
};
export class MasterGameEngine {
public state: MasterState;
constructor(state: MasterState) {
this.state = state;
}
2024-01-31 18:31:02 +09:00
public get doras(): Tile[] {
return this.state.kingTiles.slice(0, this.state.activatedDorasCount).map(t => Utils.nextTileForDora(t));
}
2024-01-29 10:46:23 +09:00
public static createInitialState(): MasterState {
2024-01-31 18:31:02 +09:00
const ikasama: Tile[] = ['haku', 'hatsu', 'm3', 'p5', 'p6', 'p7', 's2', 's3', 's4', 'chun', 'chun', 'chun', 'n', 'n'];
2024-01-29 17:15:09 +09:00
2024-01-29 10:46:23 +09:00
const tiles = [...TILE_TYPES.slice(), ...TILE_TYPES.slice(), ...TILE_TYPES.slice(), ...TILE_TYPES.slice()];
tiles.sort(() => Math.random() - 0.5);
2024-01-29 17:15:09 +09:00
for (const tile of ikasama) {
const index = tiles.indexOf(tile);
tiles.splice(index, 1);
}
//const eHandTiles = tiles.splice(0, 14);
const eHandTiles = ikasama;
2024-01-29 10:46:23 +09:00
const sHandTiles = tiles.splice(0, 13);
const wHandTiles = tiles.splice(0, 13);
const nHandTiles = tiles.splice(0, 13);
2024-01-31 18:31:02 +09:00
const kingTiles = tiles.splice(0, 14);
2024-01-29 10:46:23 +09:00
return {
user1House: 'e',
user2House: 's',
user3House: 'w',
user4House: 'n',
2024-01-29 14:14:00 +09:00
round: 'e',
kyoku: 1,
2024-01-29 10:46:23 +09:00
tiles,
2024-01-31 18:31:02 +09:00
kingTiles,
activatedDorasCount: 1,
2024-01-29 10:46:23 +09:00
handTiles: {
e: eHandTiles,
s: sHandTiles,
w: wHandTiles,
n: nHandTiles,
},
hoTiles: {
e: [],
s: [],
w: [],
n: [],
},
huros: {
e: [],
s: [],
w: [],
n: [],
},
riichis: {
e: false,
s: false,
w: false,
n: false,
},
points: {
e: 25000,
s: 25000,
w: 25000,
n: 25000,
},
turn: 'e',
nextTurnAfterAsking: null,
ponAsking: null,
ciiAsking: null,
kanAsking: null,
ronAsking: null,
};
}
private tsumo(): Tile {
const tile = this.state.tiles.pop();
if (tile == null) throw new Error('No tiles left');
if (this.state.turn == null) throw new Error('Not your turn');
this.state.handTiles[this.state.turn].push(tile);
return tile;
}
private canRon(house: House, tile: Tile): boolean {
// フリテン
// TODO: ポンされるなどして自分の河にない場合の考慮
if (this.state.hoTiles[house].includes(tile)) return false;
const horaSets = Utils.getHoraSets(this.state.handTiles[house].concat(tile));
if (horaSets.length === 0) return false; // 完成形じゃない
// TODO
//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 {
return this.state.handTiles[house].filter(t => t === tile).length === 2;
}
2024-02-01 20:34:05 +09:00
private canCii(house: House, tile: Tile): boolean {
// TODO
}
2024-01-29 10:46:23 +09:00
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;
}
}
2024-01-29 14:14:00 +09:00
private endKyoku() {
2024-01-29 17:15:09 +09:00
console.log('endKyoku');
2024-01-29 14:14:00 +09:00
const newState = MasterGameEngine.createInitialState();
newState.kyoku = this.state.kyoku + 1;
newState.points = this.state.points;
}
2024-01-31 18:31:02 +09:00
/**
2024-01-31 18:49:42 +09:00
*
2024-01-31 18:31:02 +09:00
* @param callers
* @param callee
*/
2024-01-31 18:49:42 +09:00
private ronHora(callers: House[], callee: House) {
2024-01-29 17:15:09 +09:00
for (const house of callers) {
const yakus = YAKU_DEFINITIONS.filter(yaku => yaku.calc({
house: house,
handTiles: this.state.handTiles[house],
huros: this.state.huros[house],
tsumoTile: null,
ronTile: this.state.hoTiles[callee].at(-1)!,
riichi: this.state.riichis[house],
}));
2024-01-31 18:31:02 +09:00
const doraCount = Common.calcOwnedDoraCount(this.state.handTiles[house], this.state.huros[house], this.doras);
const fans = yakus.map(yaku => yaku.fan).reduce((a, b) => a + b, 0) + doraCount;
const point = Common.fanToPoint(fans, house === 'e');
this.state.points[callee] -= point;
this.state.points[house] += point;
console.log('fans point', fans, point);
2024-01-30 11:27:08 +09:00
console.log('yakus', house, yakus);
2024-01-29 17:15:09 +09:00
}
this.endKyoku();
}
2024-01-29 10:46:23 +09:00
public commit_dahai(house: House, tile: Tile, riichi = false) {
if (this.state.turn !== house) throw new Error('Not your turn');
2024-01-30 11:34:57 +09:00
if (riichi) {
2024-01-30 17:11:16 +09:00
const tempHandTiles = [...this.state.handTiles[house]];
tempHandTiles.splice(tempHandTiles.indexOf(tile), 1);
if (Utils.getHoraTiles(tempHandTiles).length === 0) throw new Error('Not tenpai');
2024-01-30 11:34:57 +09:00
if (this.state.points[house] < 1000) throw new Error('Not enough points');
}
2024-01-29 10:46:23 +09:00
const handTiles = this.state.handTiles[house];
if (!handTiles.includes(tile)) throw new Error('No such tile in your hand');
handTiles.splice(handTiles.indexOf(tile), 1);
this.state.hoTiles[house].push(tile);
if (riichi) {
this.state.riichis[house] = true;
}
const canRonHouses: House[] = [];
switch (house) {
case 'e':
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.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.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.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;
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') {
// canCii = this.state.sHandTiles...
//} else if (house === 's') {
// canCii = this.state.wHandTiles...
//} else if (house === 'w') {
// canCii = this.state.nHandTiles...
//} else if (house === 'n') {
// canCii = this.state.eHandTiles...
//}
if (canRonHouses.length > 0 || canPonHouse != null) {
if (canRonHouses.length > 0) {
this.state.ronAsking = {
2024-01-29 17:15:09 +09:00
callee: house,
callers: canRonHouses,
2024-01-29 10:46:23 +09:00
};
}
if (canKanHouse != null) {
this.state.kanAsking = {
2024-01-29 17:15:09 +09:00
callee: house,
caller: canKanHouse,
2024-01-29 10:46:23 +09:00
};
}
if (canPonHouse != null) {
this.state.ponAsking = {
2024-01-29 17:15:09 +09:00
callee: house,
caller: canPonHouse,
2024-01-29 10:46:23 +09:00
};
}
if (canCiiHouse != null) {
this.state.ciiAsking = {
2024-01-29 17:15:09 +09:00
callee: house,
caller: canCiiHouse,
2024-01-29 10:46:23 +09:00
};
}
this.state.turn = null;
this.state.nextTurnAfterAsking = Utils.nextHouse(house);
return {
2024-01-30 11:27:08 +09:00
asking: true as const,
2024-01-29 10:46:23 +09:00
canRonHouses: canRonHouses,
canKanHouse: canKanHouse,
canPonHouse: canPonHouse,
canCiiHouse: canCiiHouse,
};
}
this.state.turn = Utils.nextHouse(house);
const tsumoTile = this.tsumo();
return {
2024-01-30 11:27:08 +09:00
asking: false as const,
2024-01-29 10:46:23 +09:00
tsumoTile: tsumoTile,
};
}
2024-02-01 20:34:05 +09:00
public commit_kakan(house: House, tile: Tile) {
const pon = this.state.huros[house].find(h => h.type === 'pon' && h.tile === tile);
if (pon == null) throw new Error('No such pon');
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.activatedDorasCount++;
const rinsyan = this.tsumo();
return {
rinsyan,
};
}
public commit_ankan(house: House, tile: Tile) {
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.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: 'ankan', tile });
this.state.activatedDorasCount++;
const rinsyan = this.tsumo();
return {
rinsyan,
};
2024-01-29 14:14:00 +09:00
}
/**
*
* @param house
*/
2024-01-31 18:49:42 +09:00
public commit_tsumoHora(house: House) {
2024-01-29 14:14:00 +09:00
if (this.state.turn !== house) throw new Error('Not your turn');
2024-01-31 18:31:02 +09:00
const yakus = YAKU_DEFINITIONS.filter(yaku => yaku.calc({
house: house,
handTiles: this.state.handTiles[house],
huros: this.state.huros[house],
tsumoTile: this.state.handTiles[house].at(-1)!,
ronTile: null,
riichi: this.state.riichis[house],
}));
const doraCount = Common.calcOwnedDoraCount(this.state.handTiles[house], this.state.huros[house], this.doras);
const fans = yakus.map(yaku => yaku.fan).reduce((a, b) => a + b, 0) + doraCount;
2024-01-31 18:49:42 +09:00
const pointDeltas = Common.calcTsumoHoraPointDeltas(house, fans);
this.state.points.e += pointDeltas.e;
this.state.points.s += pointDeltas.s;
this.state.points.w += pointDeltas.w;
this.state.points.n += pointDeltas.n;
2024-01-31 18:31:02 +09:00
console.log('yakus', house, yakus);
2024-01-29 14:14:00 +09:00
this.endKyoku();
}
2024-01-29 10:46:23 +09:00
public commit_resolveCallAndRonInterruption(answers: {
pon: boolean;
2024-02-01 20:34:05 +09:00
cii: false | [Tile, Tile];
2024-01-29 10:46:23 +09:00
kan: boolean;
ron: House[];
}) {
if (this.state.ponAsking == null && this.state.ciiAsking == null && this.state.kanAsking == null && this.state.ronAsking == null) throw new Error();
2024-01-30 11:27:08 +09:00
const pon = this.state.ponAsking;
const cii = this.state.ciiAsking;
const kan = this.state.kanAsking;
const ron = this.state.ronAsking;
2024-01-29 10:46:23 +09:00
2024-01-30 11:27:08 +09:00
this.state.ponAsking = null;
this.state.ciiAsking = null;
this.state.kanAsking = null;
this.state.ronAsking = null;
2024-01-29 20:35:25 +09:00
2024-01-30 11:27:08 +09:00
if (ron != null && answers.ron.length > 0) {
2024-01-31 18:49:42 +09:00
this.ronHora(answers.ron, ron.callee);
2024-01-29 14:14:00 +09:00
return {
2024-01-30 11:27:08 +09:00
type: 'ronned' as const,
callers: ron.callers,
callee: ron.callee,
turn: null,
2024-01-29 14:14:00 +09:00
};
2024-01-30 11:27:08 +09:00
} else if (kan != null && answers.kan) {
// 大明槓
2024-01-29 10:46:23 +09:00
2024-01-30 11:27:08 +09:00
const tile = this.state.hoTiles[kan.callee].pop()!;
this.state.huros[kan.caller].push({ type: 'minkan', tile, from: kan.callee });
2024-01-29 10:46:23 +09:00
2024-01-31 18:31:02 +09:00
this.state.activatedDorasCount++;
2024-01-29 20:35:25 +09:00
const rinsyan = this.tsumo();
2024-01-30 11:27:08 +09:00
this.state.turn = kan.caller;
2024-01-29 20:35:25 +09:00
return {
2024-01-30 11:27:08 +09:00
type: 'kanned' as const,
caller: kan.caller,
callee: kan.callee,
2024-01-29 20:35:25 +09:00
tile,
rinsyan,
2024-01-30 11:27:08 +09:00
turn: this.state.turn,
2024-01-29 20:35:25 +09:00
};
2024-01-30 11:27:08 +09:00
} else if (pon != null && answers.pon) {
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.huros[pon.caller].push({ type: 'pon', tile, from: pon.callee });
2024-01-29 10:46:23 +09:00
2024-01-30 11:27:08 +09:00
this.state.turn = pon.caller;
return {
type: 'ponned' as const,
caller: pon.caller,
callee: pon.callee,
tile,
turn: this.state.turn,
};
} else if (cii != null && answers.cii) {
const tile = this.state.hoTiles[cii.callee].pop()!;
2024-02-01 20:34:05 +09:00
this.state.handTiles[cii.caller].splice(this.state.handTiles[cii.caller].indexOf(answers.cii[0]), 1);
this.state.handTiles[cii.caller].splice(this.state.handTiles[cii.caller].indexOf(answers.cii[1]), 1);
this.state.huros[cii.caller].push({ type: 'cii', tiles: [tile, answers.cii[0], answers.cii[1]], from: cii.callee });
2024-01-29 10:46:23 +09:00
2024-01-30 11:27:08 +09:00
this.state.turn = cii.caller;
2024-01-29 10:46:23 +09:00
return {
2024-01-30 11:27:08 +09:00
type: 'ciied' as const,
caller: cii.caller,
callee: cii.callee,
2024-02-01 20:34:05 +09:00
tiles: [tile, answers.cii[0], answers.cii[1]],
2024-01-30 11:27:08 +09:00
turn: this.state.turn,
2024-01-29 10:46:23 +09:00
};
2024-01-30 11:27:08 +09:00
} else if (this.state.tiles.length === 0) {
// 流局
2024-01-29 10:46:23 +09:00
2024-01-30 11:27:08 +09:00
this.state.turn = null;
this.state.nextTurnAfterAsking = null;
2024-01-29 10:46:23 +09:00
2024-01-30 11:27:08 +09:00
this.endKyoku();
2024-01-29 10:46:23 +09:00
return {
2024-01-30 11:27:08 +09:00
type: 'ryukyoku' as const,
2024-01-29 10:46:23 +09:00
};
2024-01-30 11:27:08 +09:00
} else {
this.state.turn = this.state.nextTurnAfterAsking!;
this.state.nextTurnAfterAsking = null;
2024-01-29 10:46:23 +09:00
2024-01-30 11:27:08 +09:00
const tile = this.tsumo();
2024-01-29 10:46:23 +09:00
2024-01-30 11:27:08 +09:00
return {
type: 'tsumo' as const,
house: this.state.turn,
tile,
turn: this.state.turn,
};
}
2024-01-29 10:46:23 +09:00
}
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,
2024-01-29 14:14:00 +09:00
round: this.state.round,
kyoku: this.state.kyoku,
2024-01-29 10:46:23 +09:00
tilesCount: this.state.tiles.length,
2024-01-31 18:31:02 +09:00
doraIndicateTiles: this.state.kingTiles.slice(0, this.state.activatedDorasCount),
2024-01-29 10:46:23 +09:00
handTiles: {
e: house === 'e' ? this.state.handTiles.e : this.state.handTiles.e.map(() => null),
s: house === 's' ? this.state.handTiles.s : this.state.handTiles.s.map(() => null),
w: house === 'w' ? this.state.handTiles.w : this.state.handTiles.w.map(() => null),
n: house === 'n' ? this.state.handTiles.n : this.state.handTiles.n.map(() => null),
},
hoTiles: {
e: this.state.hoTiles.e,
s: this.state.hoTiles.s,
w: this.state.hoTiles.w,
n: this.state.hoTiles.n,
},
huros: {
e: this.state.huros.e,
s: this.state.huros.s,
w: this.state.huros.w,
n: this.state.huros.n,
},
riichis: {
e: this.state.riichis.e,
s: this.state.riichis.s,
w: this.state.riichis.w,
n: this.state.riichis.n,
},
points: {
e: this.state.points.e,
s: this.state.points.s,
w: this.state.points.w,
n: this.state.points.n,
},
latestDahaiedTile: null,
turn: this.state.turn,
};
}
public calcCrc32ForUser1(): number {
// TODO
}
public calcCrc32ForUser2(): number {
// TODO
}
public calcCrc32ForUser3(): number {
// TODO
}
public calcCrc32ForUser4(): number {
// TODO
}
}