From dad843004043f00ab3223742c277792e75614576 Mon Sep 17 00:00:00 2001 From: syuilo Date: Mon, 29 Jan 2024 10:46:23 +0900 Subject: [PATCH] wip --- .../backend/src/core/GlobalEventService.ts | 4 + packages/backend/src/core/MahjongService.ts | 55 +- .../api/stream/channels/mahjong-room.ts | 8 +- packages/frontend/assets/mahjong/ron.png | Bin 0 -> 17136 bytes .../frontend/src/pages/mahjong/room.game.vue | 40 +- packages/misskey-mahjong/src/common.ts | 187 +++++ packages/misskey-mahjong/src/engine.master.ts | 455 +++++++++++ packages/misskey-mahjong/src/engine.player.ts | 177 +++++ packages/misskey-mahjong/src/engine.ts | 723 ------------------ packages/misskey-mahjong/src/index.ts | 4 +- packages/misskey-mahjong/src/serializer.ts | 2 +- 11 files changed, 880 insertions(+), 775 deletions(-) create mode 100644 packages/frontend/assets/mahjong/ron.png create mode 100644 packages/misskey-mahjong/src/engine.master.ts create mode 100644 packages/misskey-mahjong/src/engine.player.ts delete mode 100644 packages/misskey-mahjong/src/engine.ts diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts index d7318a43ad..629ef1b01d 100644 --- a/packages/backend/src/core/GlobalEventService.ts +++ b/packages/backend/src/core/GlobalEventService.ts @@ -195,6 +195,10 @@ export interface ReversiGameEventTypes { } export interface MahjongRoomEventTypes { + joined: { + index: number; + user: Packed<'UserLite'>; + }; changeReadyStates: { user1: boolean; user2: boolean; diff --git a/packages/backend/src/core/MahjongService.ts b/packages/backend/src/core/MahjongService.ts index 692be75ab5..9e962dc22b 100644 --- a/packages/backend/src/core/MahjongService.ts +++ b/packages/backend/src/core/MahjongService.ts @@ -54,7 +54,7 @@ type Room = { isStarted?: boolean; timeLimitForEachTurn: number; - gameState?: Mahjong.Engine.MasterState; + gameState?: Mahjong.MasterState; }; type CallAndRonAnswers = { @@ -262,7 +262,7 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit { throw new Error('Not ready'); } - room.gameState = Mahjong.Engine.MasterGameEngine.createInitialState(); + room.gameState = Mahjong.MasterGameEngine.createInitialState(); room.isStarted = true; await this.saveRoom(room); @@ -281,8 +281,8 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit { } @bindThis - private async answer(room: Room, engine: Mahjong.Engine.MasterGameEngine, answers: CallAndRonAnswers) { - const res = engine.op_resolveCallAndRonInterruption({ + private async answer(room: Room, engine: Mahjong.MasterGameEngine, answers: CallAndRonAnswers) { + const res = engine.commit_resolveCallAndRonInterruption({ pon: answers.pon ?? false, cii: answers.cii ?? false, kan: answers.kan ?? false, @@ -306,7 +306,7 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit { } @bindThis - private async next(room: Room, engine: Mahjong.Engine.MasterGameEngine) { + private async next(room: Room, engine: Mahjong.MasterGameEngine) { const aiHouses = [[1, room.user1Ai], [2, room.user2Ai], [3, room.user3Ai], [4, room.user4Ai]].filter(([id, ai]) => ai).map(([id, ai]) => engine.getHouse(id)); const userId = engine.state.user1House === engine.state.turn ? room.user1Id : engine.state.user2House === engine.state.turn ? room.user2Id : engine.state.user3House === engine.state.turn ? room.user3Id : room.user4Id; @@ -314,13 +314,12 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit { // TODO: ちゃんと思考するようにする setTimeout(() => { const house = engine.state.turn; - const handTiles = house === 'e' ? engine.state.eHandTiles : house === 's' ? engine.state.sHandTiles : house === 'w' ? engine.state.wHandTiles : engine.state.nHandTiles; - this.dahai(room, engine, engine.state.turn, handTiles.at(-1)); + this.dahai(room, engine, engine.state.turn, engine.state.handTiles[house].at(-1)); }, 500); } else { - if (engine.isRiichiHouse(engine.state.turn)) { + if (engine.state.riichis[engine.state.turn]) { // リーチ時はアガリ牌でない限りツモ切り - const handTiles = engine.getHandTilesOf(engine.state.turn); + const handTiles = engine.state.handTiles[engine.state.turn]; const horaSets = Mahjong.Utils.getHoraSets(handTiles); if (horaSets.length === 0) { setTimeout(() => { @@ -336,8 +335,12 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit { } @bindThis - private async dahai(room: Room, engine: Mahjong.Engine.MasterGameEngine, house: Mahjong.Common.House, tile: Mahjong.Common.Tile, riichi = false) { - const res = engine.op_dahai(house, tile, riichi); + private async endKyoku(room: Room, engine: Mahjong.MasterGameEngine) { + } + + @bindThis + private async dahai(room: Room, engine: Mahjong.MasterGameEngine, house: Mahjong.Common.House, tile: Mahjong.Common.Tile, riichi = false) { + const res = engine.commit_dahai(house, tile, riichi); room.gameState = engine.state; await this.saveRoom(room); @@ -359,13 +362,13 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit { }; // リーチ中はポン、チー、カンできない - if (res.canPonHouse != null && engine.isRiichiHouse(res.canPonHouse)) { + if (res.canPonHouse != null && engine.state.riichis[res.canPonHouse]) { answers.pon = false; } - if (res.canCiiHouse != null && engine.isRiichiHouse(res.canCiiHouse)) { + if (res.canCiiHouse != null && engine.state.riichis[res.canCiiHouse]) { answers.cii = false; } - if (res.canKanHouse != null && engine.isRiichiHouse(res.canKanHouse)) { + if (res.canKanHouse != null && engine.state.riichis[res.canKanHouse]) { answers.kan = false; } @@ -423,18 +426,18 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit { } @bindThis - public async op_dahai(roomId: MiMahjongGame['id'], user: MiUser, tile: string, riichi = false) { + public async commit_dahai(roomId: MiMahjongGame['id'], user: MiUser, tile: string, riichi = false) { const room = await this.getRoom(roomId); if (room == null) return; if (room.gameState == null) return; if (!Mahjong.Utils.isTile(tile)) return; - const engine = new Mahjong.Engine.MasterGameEngine(room.gameState); + const engine = new Mahjong.MasterGameEngine(room.gameState); const myHouse = user.id === room.user1Id ? engine.state.user1House : user.id === room.user2Id ? engine.state.user2House : user.id === room.user3Id ? engine.state.user3House : engine.state.user4House; if (riichi) { - if (Mahjong.Utils.getHoraTiles(engine.getHandTilesOf(myHouse)).length === 0) return; - if (engine.getPointsOf(myHouse) < 1000) return; + if (Mahjong.Utils.getHoraTiles(engine.state.handTiles[myHouse]).length === 0) return; + if (engine.state.points[myHouse] < 1000) return; } await this.clearTurnWaitingTimer(room.id); @@ -443,12 +446,12 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit { } @bindThis - public async op_ron(roomId: MiMahjongGame['id'], user: MiUser) { + public async commit_ron(roomId: MiMahjongGame['id'], user: MiUser) { const room = await this.getRoom(roomId); if (room == null) return; if (room.gameState == null) return; - const engine = new Mahjong.Engine.MasterGameEngine(room.gameState); + const engine = new Mahjong.MasterGameEngine(room.gameState); const myHouse = user.id === room.user1Id ? engine.state.user1House : user.id === room.user2Id ? engine.state.user2House : user.id === room.user3Id ? engine.state.user3House : engine.state.user4House; // TODO: 自分にロン回答する権利がある状態かバリデーション @@ -462,12 +465,12 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit { } @bindThis - public async op_pon(roomId: MiMahjongGame['id'], user: MiUser) { + public async commit_pon(roomId: MiMahjongGame['id'], user: MiUser) { const room = await this.getRoom(roomId); if (room == null) return; if (room.gameState == null) return; - const engine = new Mahjong.Engine.MasterGameEngine(room.gameState); + const engine = new Mahjong.MasterGameEngine(room.gameState); const myHouse = user.id === room.user1Id ? engine.state.user1House : user.id === room.user2Id ? engine.state.user2House : user.id === room.user3Id ? engine.state.user3House : engine.state.user4House; // TODO: 自分にポン回答する権利がある状態かバリデーション @@ -481,12 +484,12 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit { } @bindThis - public async op_nop(roomId: MiMahjongGame['id'], user: MiUser) { + public async commit_nop(roomId: MiMahjongGame['id'], user: MiUser) { const room = await this.getRoom(roomId); if (room == null) return; if (room.gameState == null) return; - const engine = new Mahjong.Engine.MasterGameEngine(room.gameState); + const engine = new Mahjong.MasterGameEngine(room.gameState); const myHouse = user.id === room.user1Id ? engine.state.user1House : user.id === room.user2Id ? engine.state.user2House : user.id === room.user3Id ? engine.state.user3House : engine.state.user4House; // TODO: この辺の処理はアトミックに行いたいけどJSONサポートはRedis Stackが必要 @@ -509,7 +512,7 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit { * @param engine */ @bindThis - private async waitForTurn(room: Room, userId: MiUser['id'], engine: Mahjong.Engine.MasterGameEngine) { + private async waitForTurn(room: Room, userId: MiUser['id'], engine: Mahjong.MasterGameEngine) { const id = Math.random().toString(36).slice(2); console.log('waitForTurn', userId, id); this.redisClient.sadd(`mahjong:gameTurnWaiting:${room.id}`, id); @@ -525,7 +528,7 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit { console.log('turn timeout', userId, id); clearInterval(interval); const house = room.user1Id === userId ? engine.state.user1House : room.user2Id === userId ? engine.state.user2House : room.user3Id === userId ? engine.state.user3House : engine.state.user4House; - const handTiles = engine.getHandTilesOf(house); + const handTiles = engine.state.handTiles[house]; await this.dahai(room, engine, house, handTiles.at(-1)); return; } diff --git a/packages/backend/src/server/api/stream/channels/mahjong-room.ts b/packages/backend/src/server/api/stream/channels/mahjong-room.ts index 53d3b940c6..e7a0c81abd 100644 --- a/packages/backend/src/server/api/stream/channels/mahjong-room.ts +++ b/packages/backend/src/server/api/stream/channels/mahjong-room.ts @@ -71,28 +71,28 @@ class MahjongRoomChannel extends Channel { private async dahai(tile: string, riichi = false) { if (this.user == null) return; - this.mahjongService.op_dahai(this.roomId!, this.user, tile, riichi); + this.mahjongService.commit_dahai(this.roomId!, this.user, tile, riichi); } @bindThis private async ron() { if (this.user == null) return; - this.mahjongService.op_ron(this.roomId!, this.user); + this.mahjongService.commit_ron(this.roomId!, this.user); } @bindThis private async pon() { if (this.user == null) return; - this.mahjongService.op_pon(this.roomId!, this.user); + this.mahjongService.commit_pon(this.roomId!, this.user); } @bindThis private async nop() { if (this.user == null) return; - this.mahjongService.op_nop(this.roomId!, this.user); + this.mahjongService.commit_nop(this.roomId!, this.user); } @bindThis diff --git a/packages/frontend/assets/mahjong/ron.png b/packages/frontend/assets/mahjong/ron.png new file mode 100644 index 0000000000000000000000000000000000000000..8bd40d6dcaa2af91923bbe1b9a845b5939faa482 GIT binary patch literal 17136 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 z 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 literal 0 HcmV?d00001 diff --git a/packages/frontend/src/pages/mahjong/room.game.vue b/packages/frontend/src/pages/mahjong/room.game.vue index fcccb8b319..9105992e2d 100644 --- a/packages/frontend/src/pages/mahjong/room.game.vue +++ b/packages/frontend/src/pages/mahjong/room.game.vue @@ -7,19 +7,19 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
-
+
-
+
@@ -27,28 +27,28 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
-
+
-
+
-
+
@@ -67,7 +67,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
@@ -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;