@@ -111,7 +111,7 @@ const props = defineProps<{
const room = ref(deepClone(props.room));
const myUserNumber = computed(() => room.value.user1Id === $i.id ? 1 : room.value.user2Id === $i.id ? 2 : room.value.user3Id === $i.id ? 3 : 4);
-const engine = shallowRef(new Mahjong.Engine.PlayerGameEngine(myUserNumber.value, room.value.gameState));
+const engine = shallowRef(new Mahjong.PlayerGameEngine(myUserNumber.value, room.value.gameState));
const isMyTurn = computed(() => {
return engine.value.state.turn === engine.value.myHouse;
@@ -205,7 +205,7 @@ if (!props.room.isEnded) {
function dahai(tile: Mahjong.Common.Tile, ev: MouseEvent) {
if (!isMyTurn.value) return;
- engine.value.op_dahai(engine.value.myHouse, tile);
+ engine.value.commit_dahai(engine.value.myHouse, tile);
iTsumoed.value = false;
triggerRef(engine);
@@ -217,7 +217,7 @@ function dahai(tile: Mahjong.Common.Tile, ev: MouseEvent) {
function riichi() {
if (!isMyTurn.value) return;
- engine.value.op_dahai(engine.value.myHouse, tile, true);
+ engine.value.commit_dahai(engine.value.myHouse, tile, true);
iTsumoed.value = false;
triggerRef(engine);
@@ -228,7 +228,7 @@ function riichi() {
}
function ron() {
- engine.value.op_ron(engine.value.state.canRonSource, engine.value.myHouse);
+ engine.value.commit_ron(engine.value.state.canRonSource, engine.value.myHouse);
triggerRef(engine);
props.connection!.send('ron', {
@@ -236,7 +236,7 @@ function ron() {
}
function pon() {
- engine.value.op_pon(engine.value.state.canPonSource, engine.value.myHouse);
+ engine.value.commit_pon(engine.value.state.canPonSource, engine.value.myHouse);
triggerRef(engine);
props.connection!.send('pon', {
@@ -244,7 +244,7 @@ function pon() {
}
function skip() {
- engine.value.op_nop(engine.value.myHouse);
+ engine.value.commit_nop(engine.value.myHouse);
triggerRef(engine);
props.connection!.send('nop', {});
@@ -270,7 +270,7 @@ function onStreamDahai(log) {
// return;
//}
- engine.value.op_dahai(log.house, log.tile);
+ engine.value.commit_dahai(log.house, log.tile);
triggerRef(engine);
myTurnTimerRmain.value = room.value.timeLimitForEachTurn;
@@ -287,7 +287,7 @@ function onStreamTsumo(log) {
// return;
//}
- engine.value.op_tsumo(log.house, log.tile);
+ engine.value.commit_tsumo(log.house, log.tile);
triggerRef(engine);
if (log.house === engine.value.myHouse) {
@@ -309,12 +309,12 @@ function onStreamDahaiAndTsumo(log) {
//}
if (log.dahaiHouse !== engine.value.myHouse) {
- engine.value.op_dahai(log.dahaiHouse, log.dahaiTile);
+ engine.value.commit_dahai(log.dahaiHouse, log.dahaiTile);
triggerRef(engine);
}
window.setTimeout(() => {
- engine.value.op_tsumo(Mahjong.Utils.nextHouse(log.dahaiHouse), log.tsumoTile);
+ engine.value.commit_tsumo(Mahjong.Utils.nextHouse(log.dahaiHouse), log.tsumoTile);
triggerRef(engine);
if (Mahjong.Utils.nextHouse(log.dahaiHouse) === engine.value.myHouse) {
@@ -338,7 +338,7 @@ function onStreamPonned(log) {
if (log.target === engine.value.myHouse) return;
- engine.value.op_pon(log.source, log.target);
+ engine.value.commit_pon(log.source, log.target);
triggerRef(engine);
myTurnTimerRmain.value = room.value.timeLimitForEachTurn;
@@ -347,7 +347,7 @@ function onStreamPonned(log) {
function restoreRoom(_room) {
room.value = deepClone(_room);
- engine.value = new Mahjong.Engine.PlayerGameEngine(myUserNumber, room.value.gameState);
+ engine.value = new Mahjong.PlayerGameEngine(myUserNumber, room.value.gameState);
}
onMounted(() => {
diff --git a/packages/misskey-mahjong/src/common.ts b/packages/misskey-mahjong/src/common.ts
index bf7d15491e..0e3e0243bc 100644
--- a/packages/misskey-mahjong/src/common.ts
+++ b/packages/misskey-mahjong/src/common.ts
@@ -46,3 +46,190 @@ export const TILE_TYPES = [
export type Tile = typeof TILE_TYPES[number];
export type House = 'e' | 's' | 'w' | 'n';
+
+export type Huro = {
+ type: 'pon';
+ tile: Tile;
+ from: House;
+} | {
+ type: 'cii';
+ tiles: [Tile, Tile, Tile];
+ from: House;
+} | {
+ type: 'minkan';
+ tile: Tile;
+ from: House;
+} | {
+ type: 'ankan';
+ tile: Tile;
+ from: House;
+};
+
+export const yakuNames = [
+ 'riichi',
+ 'ippatsu',
+ 'tsumo',
+ 'tanyao',
+ 'pinfu',
+ 'iipeko',
+ 'field-wind',
+ 'seat-wind',
+ 'white',
+ 'green',
+ 'red',
+ 'rinshan',
+ 'chankan',
+ 'haitei',
+ 'hotei',
+ 'sanshoku-dojun',
+ 'sanshoku-doko',
+ 'ittsu',
+ 'chanta',
+ 'chitoitsu',
+ 'toitoi',
+ 'sananko',
+ 'honroto',
+ 'sankantsu',
+ 'shosangen',
+ 'double-riichi',
+ 'honitsu',
+ 'junchan',
+ 'ryampeko',
+ 'chinitsu',
+ 'dora',
+ 'red-dora',
+] as const;
+
+export const yakumanNames = [
+ 'kokushi',
+ 'kokushi-13',
+ 'suanko',
+ 'suanko-tanki',
+ 'daisangen',
+ 'tsuiso',
+ 'shosushi',
+ 'daisushi',
+ 'ryuiso',
+ 'chinroto',
+ 'sukantsu',
+ 'churen',
+ 'pure-churen',
+ 'tenho',
+ 'chiho',
+] as const;
+
+type EnvForCalcYaku = {
+ house: House;
+
+ /**
+ * 和了る人の手牌(副露した牌は含まない)
+ */
+ handTiles: Tile[];
+
+ /**
+ * 河
+ */
+ hoTiles: Tile[];
+
+ /**
+ * 副露
+ */
+ huros: Huro[];
+
+ /**
+ * ツモ牌
+ */
+ tsumoTile: Tile | null;
+
+ /**
+ * ロン牌
+ */
+ ronTile: Tile | null;
+
+ /**
+ * ドラ表示牌
+ */
+ doraTiles: Tile[];
+
+ /**
+ * 赤ドラ表示牌
+ */
+ redDoraTiles: Tile[];
+
+ /**
+ * 場風
+ */
+ fieldWind: House;
+
+ /**
+ * 自風
+ */
+ seatWind: House;
+
+ /**
+ * リーチしたかどうか
+ */
+ riichi: boolean;
+};
+
+const YAKU_DEFINITIONS = [{
+ name: 'riichi',
+ fan: 1,
+ calc: (state: EnvForCalcYaku) => {
+ return state.riichi;
+ },
+}, {
+ name: 'red',
+ fan: 1,
+ calc: (state: EnvForCalcYaku) => {
+ return (
+ (state.handTiles.filter(t => t === 'chun').length >= 3) ||
+ (state.huros.filter(huro =>
+ huro.type === 'pon' ? huro.tile === 'chun' :
+ huro.type === 'ankan' ? huro.tile === 'chun' :
+ huro.type === 'minkan' ? huro.tile === 'chun' :
+ false).length >= 3)
+ );
+ },
+}, {
+ name: 'white',
+ fan: 1,
+ calc: (state: EnvForCalcYaku) => {
+ return (
+ (state.handTiles.filter(t => t === 'haku').length >= 3) ||
+ (state.huros.filter(huro =>
+ huro.type === 'pon' ? huro.tile === 'haku' :
+ huro.type === 'ankan' ? huro.tile === 'haku' :
+ huro.type === 'minkan' ? huro.tile === 'haku' :
+ false).length >= 3)
+ );
+ },
+}, {
+ name: 'green',
+ fan: 1,
+ calc: (state: EnvForCalcYaku) => {
+ return (
+ (state.handTiles.filter(t => t === 'hatsu').length >= 3) ||
+ (state.huros.filter(huro =>
+ huro.type === 'pon' ? huro.tile === 'hatsu' :
+ huro.type === 'ankan' ? huro.tile === 'hatsu' :
+ huro.type === 'minkan' ? huro.tile === 'hatsu' :
+ false).length >= 3)
+ );
+ },
+}, {
+ name: 'tanyao',
+ fan: 1,
+ calc: (state: EnvForCalcYaku) => {
+ const yaochuTiles: Tile[] = ['m1', 'm9', 'p1', 'p9', 's1', 's9', 'e', 's', 'w', 'n', 'haku', 'hatsu', 'chun'];
+ return (
+ (state.handTiles.filter(t => yaochuTiles.includes(t)).length === 0) &&
+ (state.huros.filter(huro =>
+ huro.type === 'pon' ? yaochuTiles.includes(huro.tile) :
+ huro.type === 'ankan' ? yaochuTiles.includes(huro.tile) :
+ huro.type === 'minkan' ? yaochuTiles.includes(huro.tile) :
+ huro.type === 'cii' ? huro.tiles.some(t2 => yaochuTiles.includes(t2)) :
+ false).length === 0)
+ );
+ },
+}];
diff --git a/packages/misskey-mahjong/src/engine.master.ts b/packages/misskey-mahjong/src/engine.master.ts
new file mode 100644
index 0000000000..b581ba9191
--- /dev/null
+++ b/packages/misskey-mahjong/src/engine.master.ts
@@ -0,0 +1,455 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import CRC32 from 'crc-32';
+import { Tile, House, Huro, TILE_TYPES } from './common.js';
+import * as Utils from './utils.js';
+import { PlayerState } from './engine.player.js';
+
+export type MasterState = {
+ user1House: House;
+ user2House: House;
+ user3House: House;
+ user4House: House;
+ tiles: Tile[];
+
+ /**
+ * 副露した牌を含まない手牌
+ */
+ 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: {
+ /**
+ * 牌を捨てた人
+ */
+ 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 MasterGameEngine {
+ public state: MasterState;
+
+ constructor(state: MasterState) {
+ this.state = state;
+ }
+
+ public static createInitialState(): MasterState {
+ 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);
+ const sHandTiles = tiles.splice(0, 13);
+ const wHandTiles = tiles.splice(0, 13);
+ const nHandTiles = tiles.splice(0, 13);
+
+ return {
+ user1House: 'e',
+ user2House: 's',
+ user3House: 'w',
+ user4House: 'n',
+ tiles,
+ 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;
+ }
+
+ 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 commit_dahai(house: House, tile: Tile, riichi = false) {
+ if (this.state.turn !== house) throw new Error('Not your turn');
+
+ 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 = {
+ 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,
+ };
+ }
+ this.state.turn = null;
+ this.state.nextTurnAfterAsking = Utils.nextHouse(house);
+ return {
+ asking: true,
+ canRonHouses: canRonHouses,
+ canKanHouse: canKanHouse,
+ canPonHouse: canPonHouse,
+ canCiiHouse: canCiiHouse,
+ };
+ }
+
+ this.state.turn = Utils.nextHouse(house);
+
+ const tsumoTile = this.tsumo();
+
+ return {
+ asking: false,
+ tsumoTile: tsumoTile,
+ };
+ }
+
+ public commit_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 clearAsking = () => {
+ this.state.ponAsking = null;
+ this.state.ciiAsking = null;
+ this.state.kanAsking = null;
+ this.state.ronAsking = null;
+ };
+
+ if (this.state.ronAsking != null && answers.ron.length > 0) {
+ // TODO
+ return;
+ }
+
+ if (this.state.kanAsking != null && answers.kan) {
+ const source = this.state.kanAsking.source;
+ const target = this.state.kanAsking.target;
+
+ const tile = this.state.hoTiles[source].pop()!;
+ this.state.huros[target].push({ type: 'minkan', tile, from: source });
+
+ clearAsking();
+ this.state.turn = target;
+ // TODO
+ return;
+ }
+
+ if (this.state.ponAsking != null && answers.pon) {
+ const source = this.state.ponAsking.source;
+ const target = this.state.ponAsking.target;
+
+ const tile = this.state.hoTiles[source].pop()!;
+ this.state.handTiles[target].splice(this.state.handTiles[target].indexOf(tile), 1);
+ this.state.handTiles[target].splice(this.state.handTiles[target].indexOf(tile), 1);
+ this.state.huros[target].push({ type: 'pon', tile, from: source });
+
+ clearAsking();
+ this.state.turn = target;
+ return {
+ type: 'ponned',
+ source,
+ target,
+ tile,
+ };
+ }
+
+ if (this.state.ciiAsking != null && answers.cii) {
+ const source = this.state.ciiAsking.source;
+ const target = this.state.ciiAsking.target;
+
+ const tile = this.state.hoTiles[source].pop()!;
+ this.state.huros[target].push({ type: 'cii', tile, from: source });
+
+ clearAsking();
+ this.state.turn = target;
+ return {
+ type: 'ciied',
+ source,
+ target,
+ tile,
+ };
+ }
+
+ clearAsking();
+ this.state.turn = this.state.nextTurnAfterAsking;
+ this.state.nextTurnAfterAsking = null;
+
+ const tile = this.tsumo();
+
+ return {
+ type: 'tsumo',
+ house: this.state.turn,
+ tile,
+ };
+ }
+
+ 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,
+ 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
+ }
+}
diff --git a/packages/misskey-mahjong/src/engine.player.ts b/packages/misskey-mahjong/src/engine.player.ts
new file mode 100644
index 0000000000..f1c9a6cb3a
--- /dev/null
+++ b/packages/misskey-mahjong/src/engine.player.ts
@@ -0,0 +1,177 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import CRC32 from 'crc-32';
+import { Tile, House, Huro, TILE_TYPES } from './common.js';
+import * as Utils from './utils.js';
+
+export type PlayerState = {
+ user1House: House;
+ user2House: House;
+ user3House: House;
+ user4House: House;
+ tilesCount: number;
+
+ /**
+ * 副露した牌を含まない手牌
+ */
+ handTiles: {
+ e: Tile[] | null[];
+ s: Tile[] | null[];
+ w: Tile[] | null[];
+ n: Tile[] | null[];
+ };
+
+ 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;
+ };
+ latestDahaiedTile: Tile | null;
+ turn: House | null;
+ canPonSource: House | null;
+ canCiiSource: House | null;
+ canKanSource: House | null;
+ canRonSource: House | null;
+ canCiiTo: House | null;
+ canKanTo: House | null;
+ canRonTo: House | null;
+};
+
+export class PlayerGameEngine {
+ /**
+ * このエラーが発生したときはdesyncが疑われる
+ */
+ public static InvalidOperationError = class extends Error {};
+
+ private myUserNumber: 1 | 2 | 3 | 4;
+ public state: PlayerState;
+
+ constructor(myUserNumber: PlayerGameEngine['myUserNumber'], state: PlayerState) {
+ this.myUserNumber = myUserNumber;
+ this.state = state;
+ }
+
+ public get myHouse(): House {
+ switch (this.myUserNumber) {
+ 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 get myHandTiles(): Tile[] {
+ return this.state.handTiles[this.myHouse] as Tile[];
+ }
+
+ public get isMeRiichi(): boolean {
+ return this.state.riichis[this.myHouse];
+ }
+
+ public commit_tsumo(house: House, tile: Tile) {
+ console.log('commit_tsumo', this.state.turn, house, tile);
+ this.state.turn = house;
+ if (house === this.myHouse) {
+ this.myHandTiles.push(tile);
+ } else {
+ this.state.handTiles[house].push(null);
+ }
+ }
+
+ public commit_dahai(house: House, tile: Tile, riichi = false) {
+ console.log('commit_dahai', this.state.turn, house, tile, riichi);
+ if (this.state.turn !== house) throw new PlayerGameEngine.InvalidOperationError();
+
+ if (riichi) {
+ this.state.riichis[house] = true;
+ }
+
+ if (house === this.myHouse) {
+ this.myHandTiles.splice(this.myHandTiles.indexOf(tile), 1);
+ this.state.hoTiles[this.myHouse].push(tile);
+ } else {
+ this.state.handTiles[house].pop();
+ this.state.hoTiles[house].push(tile);
+ }
+
+ this.state.turn = null;
+
+ if (house === this.myHouse) {
+ } else {
+ const canRon = Utils.getHoraSets(this.myHandTiles.concat(tile)).length > 0;
+ const canPon = this.myHandTiles.filter(t => t === tile).length === 2;
+
+ // TODO: canCii
+
+ if (canRon) this.state.canRonSource = house;
+ if (canPon) this.state.canPonSource = house;
+ }
+ }
+
+ /**
+ * ロンします
+ * @param source 牌を捨てた人
+ * @param target ロンした人
+ */
+ public commit_ron(source: House, target: House) {
+ this.state.canRonSource = null;
+
+ const lastTile = this.state.hoTiles[source].pop();
+ if (lastTile == null) throw new PlayerGameEngine.InvalidOperationError();
+ if (target === this.myHouse) {
+ this.myHandTiles.push(lastTile);
+ } else {
+ this.state.handTiles[target].push(null);
+ }
+ this.state.turn = null;
+ }
+
+ /**
+ * ポンします
+ * @param source 牌を捨てた人
+ * @param target ポンした人
+ */
+ public commit_pon(source: House, target: House) {
+ this.state.canPonSource = null;
+
+ const lastTile = this.state.hoTiles[source].pop();
+ if (lastTile == null) throw new PlayerGameEngine.InvalidOperationError();
+ if (target === this.myHouse) {
+ this.myHandTiles.splice(this.myHandTiles.indexOf(lastTile), 1);
+ this.myHandTiles.splice(this.myHandTiles.indexOf(lastTile), 1);
+ } else {
+ this.state.handTiles[target].unshift();
+ this.state.handTiles[target].unshift();
+ }
+ this.state.huros[target].push({ type: 'pon', tile: lastTile, from: source });
+
+ this.state.turn = target;
+ }
+
+ public commit_nop() {
+ this.state.canRonSource = null;
+ this.state.canPonSource = null;
+ }
+}
diff --git a/packages/misskey-mahjong/src/engine.ts b/packages/misskey-mahjong/src/engine.ts
deleted file mode 100644
index 27371b86d3..0000000000
--- a/packages/misskey-mahjong/src/engine.ts
+++ /dev/null
@@ -1,723 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import CRC32 from 'crc-32';
-import { Tile, House, TILE_TYPES } from './common.js';
-import * as Utils from './utils.js';
-
-type Huro = {
- type: 'pon';
- tile: Tile;
- from: House;
-} | {
- type: 'cii';
- tiles: [Tile, Tile, Tile];
- from: House;
-} | {
- type: 'kan';
- tile: Tile;
- from: House;
-} | {
- type: 'kakan';
- tile: Tile;
- from: House;
-} | {
- type: 'ankan';
- tile: Tile;
- from: House;
-};
-
-export type MasterState = {
- user1House: House;
- user2House: House;
- user3House: House;
- user4House: House;
- tiles: Tile[];
- eHandTiles: Tile[];
- sHandTiles: Tile[];
- wHandTiles: Tile[];
- nHandTiles: Tile[];
- eHoTiles: Tile[];
- sHoTiles: Tile[];
- wHoTiles: Tile[];
- nHoTiles: Tile[];
- eHuros: Huro[];
- sHuros: Huro[];
- wHuros: Huro[];
- nHuros: Huro[];
- eRiichi: boolean;
- sRiichi: boolean;
- wRiichi: boolean;
- nRiichi: boolean;
- ePoints: number;
- sPoints: number;
- wPoints: number;
- nPoints: number;
- turn: House | null;
- nextTurnAfterAsking: House | null;
-
- 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 MasterGameEngine {
- public state: MasterState;
-
- constructor(state: MasterState) {
- this.state = state;
- }
-
- public static createInitialState(): MasterState {
- 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);
- const sHandTiles = tiles.splice(0, 13);
- const wHandTiles = tiles.splice(0, 13);
- const nHandTiles = tiles.splice(0, 13);
-
- return {
- user1House: 'e',
- user2House: 's',
- user3House: 'w',
- user4House: 'n',
- tiles,
- eHandTiles,
- sHandTiles,
- wHandTiles,
- nHandTiles,
- eHoTiles: [],
- sHoTiles: [],
- wHoTiles: [],
- nHoTiles: [],
- eHuros: [],
- sHuros: [],
- wHuros: [],
- nHuros: [],
- eRiichi: false,
- sRiichi: false,
- wRiichi: false,
- nRiichi: false,
- ePoints: 25000,
- sPoints: 25000,
- wPoints: 25000,
- nPoints: 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.getHandTilesOf(this.state.turn).push(tile);
- return 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; // 完成形じゃない
-
- // 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.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;
- default: throw new Error(`unrecognized house: ${house}`);
- }
- }
-
- 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;
- default: throw new Error(`unrecognized house: ${house}`);
- }
- }
-
- public getHurosOf(house: House): Huro[] {
- switch (house) {
- case 'e': return this.state.eHuros;
- case 's': return this.state.sHuros;
- case 'w': return this.state.wHuros;
- case 'n': return this.state.nHuros;
- default: throw new Error(`unrecognized house: ${house}`);
- }
- }
-
- public getPointsOf(house: House): number {
- switch (house) {
- case 'e': return this.state.ePoints;
- case 's': return this.state.sPoints;
- case 'w': return this.state.wPoints;
- case 'n': return this.state.nPoints;
- default: throw new Error(`unrecognized house: ${house}`);
- }
- }
-
- public setPointsOf(house: House, points: number) {
- switch (house) {
- case 'e': this.state.ePoints = points; break;
- case 's': this.state.sPoints = points; break;
- case 'w': this.state.wPoints = points; break;
- case 'n': this.state.nPoints = points; break;
- default: throw new Error(`unrecognized house: ${house}`);
- }
- }
-
- public isRiichiHouse(house: House): boolean {
- switch (house) {
- case 'e': return this.state.eRiichi;
- case 's': return this.state.sRiichi;
- case 'w': return this.state.wRiichi;
- case 'n': return this.state.nRiichi;
- default: throw new Error(`unrecognized house: ${house}`);
- }
- }
-
- public op_dahai(house: House, tile: Tile, riichi = false) {
- 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);
-
- if (riichi) {
- switch (house) {
- case 'e': this.state.eRiichi = true; break;
- case 's': this.state.sRiichi = true; break;
- case 'w': this.state.wRiichi = true; break;
- case 'n': this.state.nRiichi = true; break;
- }
- }
-
- 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 = {
- 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,
- };
- }
- this.state.turn = null;
- this.state.nextTurnAfterAsking = Utils.nextHouse(house);
- return {
- asking: true,
- canRonHouses: canRonHouses,
- canKanHouse: canKanHouse,
- canPonHouse: canPonHouse,
- canCiiHouse: canCiiHouse,
- };
- }
-
- this.state.turn = Utils.nextHouse(house);
-
- const tsumoTile = this.tsumo();
-
- return {
- asking: false,
- tsumoTile: tsumoTile,
- };
- }
-
- 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 clearAsking = () => {
- this.state.ponAsking = null;
- this.state.ciiAsking = null;
- this.state.kanAsking = null;
- this.state.ronAsking = null;
- };
-
- if (this.state.ronAsking != null && answers.ron.length > 0) {
- // TODO
- return;
- }
-
- 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.getHurosOf(target).push({ type: 'kan', tile, from: source });
-
- clearAsking();
- this.state.turn = target;
- // TODO
- return;
- }
-
- if (this.state.ponAsking != null && answers.pon) {
- const source = this.state.ponAsking.source;
- const target = this.state.ponAsking.target;
-
- const tile = this.getHoTilesOf(source).pop()!;
- this.getHandTilesOf(target).splice(this.getHandTilesOf(target).indexOf(tile), 1);
- this.getHandTilesOf(target).splice(this.getHandTilesOf(target).indexOf(tile), 1);
- this.getHurosOf(target).push({ type: 'pon', tile, from: source });
-
- clearAsking();
- this.state.turn = target;
- return {
- type: 'ponned',
- source,
- target,
- tile,
- };
- }
-
- 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.getHurosOf(target).push({ type: 'cii', tile, from: source });
-
- clearAsking();
- this.state.turn = target;
- return {
- type: 'ciied',
- source,
- target,
- tile,
- };
- }
-
- clearAsking();
- this.state.turn = this.state.nextTurnAfterAsking;
- this.state.nextTurnAfterAsking = null;
-
- const tile = this.tsumo();
-
- return {
- type: 'tsumo',
- house: this.state.turn,
- tile,
- };
- }
-
- 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,
- eHuros: this.state.eHuros,
- sHuros: this.state.sHuros,
- wHuros: this.state.wHuros,
- nHuros: this.state.nHuros,
- 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
- }
-
- public calcCrc32ForUser2(): number {
- // TODO
- }
-
- public calcCrc32ForUser3(): number {
- // TODO
- }
-
- public calcCrc32ForUser4(): number {
- // TODO
- }
-}
-
-export type PlayerState = {
- user1House: House;
- user2House: House;
- user3House: House;
- user4House: House;
- tilesCount: number;
- eHandTiles: Tile[] | null[];
- sHandTiles: Tile[] | null[];
- wHandTiles: Tile[] | null[];
- nHandTiles: Tile[] | null[];
- eHoTiles: Tile[];
- sHoTiles: Tile[];
- wHoTiles: Tile[];
- nHoTiles: Tile[];
- eHuros: Huro[];
- sHuros: Huro[];
- wHuros: Huro[];
- nHuros: Huro[];
- eRiichi: boolean;
- sRiichi: boolean;
- wRiichi: boolean;
- nRiichi: boolean;
- ePoints: number;
- sPoints: number;
- wPoints: number;
- nPoints: number;
- latestDahaiedTile: Tile | null;
- turn: House | null;
- canPonSource: House | null;
- canCiiSource: House | null;
- canKanSource: House | null;
- canRonSource: House | null;
- canCiiTo: House | null;
- canKanTo: House | null;
- canRonTo: House | null;
-};
-
-export class PlayerGameEngine {
- /**
- * このエラーが発生したときはdesyncが疑われる
- */
- public static InvalidOperationError = class extends Error {};
-
- private myUserNumber: 1 | 2 | 3 | 4;
- public state: PlayerState;
-
- constructor(myUserNumber: PlayerGameEngine['myUserNumber'], state: PlayerState) {
- this.myUserNumber = myUserNumber;
- this.state = state;
- }
-
- public get myHouse(): House {
- switch (this.myUserNumber) {
- 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 get myHandTiles(): Tile[] {
- switch (this.myHouse) {
- case 'e': return this.state.eHandTiles as Tile[];
- case 's': return this.state.sHandTiles as Tile[];
- case 'w': return this.state.wHandTiles as Tile[];
- case 'n': return this.state.nHandTiles as Tile[];
- }
- }
-
- public get myHoTiles(): Tile[] {
- switch (this.myHouse) {
- 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 get isMeRiichi(): boolean {
- switch (this.myHouse) {
- case 'e': return this.state.eRiichi;
- case 's': return this.state.sRiichi;
- case 'w': return this.state.wRiichi;
- case 'n': return this.state.nRiichi;
- }
- }
-
- 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;
- default: throw new Error(`unrecognized house: ${house}`);
- }
- }
-
- 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;
- default: throw new Error(`unrecognized house: ${house}`);
- }
- }
-
- public getHurosOf(house: House): Huro[] {
- switch (house) {
- case 'e': return this.state.eHuros;
- case 's': return this.state.sHuros;
- case 'w': return this.state.wHuros;
- case 'n': return this.state.nHuros;
- default: throw new Error(`unrecognized house: ${house}`);
- }
- }
-
- public op_tsumo(house: House, tile: Tile) {
- console.log('op_tsumo', this.state.turn, house, tile);
- this.state.turn = house;
- if (house === this.myHouse) {
- this.myHandTiles.push(tile);
- } else {
- this.getHandTilesOf(house).push(null);
- }
- }
-
- public op_dahai(house: House, tile: Tile, riichi = false) {
- console.log('op_dahai', this.state.turn, house, tile, riichi);
- if (this.state.turn !== house) throw new PlayerGameEngine.InvalidOperationError();
-
- if (riichi) {
- switch (house) {
- case 'e': this.state.eRiichi = true; break;
- case 's': this.state.sRiichi = true; break;
- case 'w': this.state.wRiichi = true; break;
- case 'n': this.state.nRiichi = true; break;
- }
- }
-
- if (house === this.myHouse) {
- this.myHandTiles.splice(this.myHandTiles.indexOf(tile), 1);
- this.myHoTiles.push(tile);
- } else {
- this.getHandTilesOf(house).pop();
- this.getHoTilesOf(house).push(tile);
- }
-
- this.state.turn = null;
-
- if (house === this.myHouse) {
- } else {
- const canRon = Utils.getHoraSets(this.myHandTiles.concat(tile)).length > 0;
- const canPon = this.myHandTiles.filter(t => t === tile).length === 2;
-
- // TODO: canCii
-
- if (canRon) this.state.canRonSource = house;
- if (canPon) this.state.canPonSource = house;
- }
- }
-
- /**
- * ロンします
- * @param source 牌を捨てた人
- * @param target ロンした人
- */
- public op_ron(source: House, target: House) {
- this.state.canRonSource = null;
-
- const lastTile = this.getHoTilesOf(source).pop();
- if (lastTile == null) throw new PlayerGameEngine.InvalidOperationError();
- if (target === this.myHouse) {
- this.myHandTiles.push(lastTile);
- } else {
- this.getHandTilesOf(target).push(null);
- }
- this.state.turn = null;
- }
-
- /**
- * ポンします
- * @param source 牌を捨てた人
- * @param target ポンした人
- */
- public op_pon(source: House, target: House) {
- this.state.canPonSource = null;
-
- const lastTile = this.getHoTilesOf(source).pop();
- if (lastTile == null) throw new PlayerGameEngine.InvalidOperationError();
- if (target === this.myHouse) {
- this.myHandTiles.splice(this.myHandTiles.indexOf(lastTile), 1);
- this.myHandTiles.splice(this.myHandTiles.indexOf(lastTile), 1);
- } else {
- this.getHandTilesOf(target).unshift();
- this.getHandTilesOf(target).unshift();
- }
- this.getHurosOf(target).push({ type: 'pon', tile: lastTile, from: source });
-
- this.state.turn = target;
- }
-
- public op_nop() {
- this.state.canRonSource = null;
- this.state.canPonSource = 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;
- },
-}];
diff --git a/packages/misskey-mahjong/src/index.ts b/packages/misskey-mahjong/src/index.ts
index c2cc36da3f..05b836f87a 100644
--- a/packages/misskey-mahjong/src/index.ts
+++ b/packages/misskey-mahjong/src/index.ts
@@ -3,7 +3,9 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-export * as Engine from './engine.js';
export * as Serializer from './serializer.js';
export * as Common from './common.js';
export * as Utils from './utils.js';
+
+export { MasterGameEngine, MasterState } from './engine.master.js';
+export { PlayerGameEngine, PlayerState } from './engine.player.js';
diff --git a/packages/misskey-mahjong/src/serializer.ts b/packages/misskey-mahjong/src/serializer.ts
index 94be1c6947..6bf1417d28 100644
--- a/packages/misskey-mahjong/src/serializer.ts
+++ b/packages/misskey-mahjong/src/serializer.ts
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { Tile } from './engine.js';
+import { Tile } from './engine.player.js';
export type Log = {
time: number;