This commit is contained in:
syuilo
2024-01-29 17:15:09 +09:00
parent 668bf9a226
commit d427d24ca4
7 changed files with 252 additions and 91 deletions

View File

@@ -222,11 +222,13 @@ export interface MahjongRoomEventTypes {
tsumoTile: Mahjong.Common.Tile;
};
ponned: {
source: Mahjong.Common.House;
target: Mahjong.Common.House;
caller: Mahjong.Common.House;
callee: Mahjong.Common.House;
tile: Mahjong.Common.Tile;
};
endKyoku: {
ronned: {
};
hora: {
};
}
//#endregion

View File

@@ -28,6 +28,7 @@ import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common';
const INVITATION_TIMEOUT_MS = 1000 * 20; // 20sec
const CALL_AND_RON_ASKING_TIMEOUT_MS = 1000 * 5; // 5sec
const TURN_TIMEOUT_MS = 1000 * 30; // 30sec
const NEXT_KYOKU_CONFIRMATION_TIMEOUT_MS = 1000 * 15; // 15sec
type Room = {
id: string;
@@ -69,6 +70,13 @@ type CallAndRonAnswers = {
};
};
type NextKyokuConfirmation = {
user1: boolean;
user2: boolean;
user3: boolean;
user4: boolean;
};
@Injectable()
export class MahjongService implements OnApplicationShutdown, OnModuleInit {
private notificationService: NotificationService;
@@ -267,17 +275,25 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit {
await this.saveRoom(room);
const packed = await this.packRoom(room);
this.globalEventService.publishMahjongRoomStream(room.id, 'started', { room: packed });
this.globalEventService.publishMahjongRoomStream(room.id, 'started', { room: room });
return room;
}
@bindThis
public async packRoom(room: Room, me: MiUser) {
return {
...room,
};
if (room.gameState) {
const engine = new Mahjong.MasterGameEngine(room.gameState);
const myIndex = room.user1Id === me.id ? 1 : room.user2Id === me.id ? 2 : room.user3Id === me.id ? 3 : 4;
return {
...room,
gameState: engine.createPlayerState(myIndex),
};
} else {
return {
...room,
};
}
}
@bindThis
@@ -295,13 +311,14 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit {
this.globalEventService.publishMahjongRoomStream(room.id, 'tsumo', { house: res.house, tile: res.tile });
this.next(room, engine);
} else if (res.type === 'ponned') {
this.globalEventService.publishMahjongRoomStream(room.id, 'ponned', { source: res.source, target: res.target, tile: res.tile });
this.globalEventService.publishMahjongRoomStream(room.id, 'ponned', { caller: res.caller, callee: res.callee, tile: res.tile });
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;
this.waitForTurn(room, userId, engine);
} else if (res.type === 'kanned') {
// TODO
} else if (res.type === 'endKyoku') {
// TODO
} else if (res.type === 'ronned') {
this.globalEventService.publishMahjongRoomStream(room.id, 'ronned', { });
this.endKyoku(room, engine);
}
}
@@ -336,6 +353,28 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit {
@bindThis
private async endKyoku(room: Room, engine: Mahjong.MasterGameEngine) {
const confirmation: NextKyokuConfirmation = {
user1: false,
user2: false,
user3: false,
user4: false,
};
this.redisClient.set(`mahjong:gameNextKyokuConfirmation:${room.id}`, JSON.stringify(confirmation));
const waitingStartedAt = Date.now();
const interval = setInterval(async () => {
const confirmationRaw = await this.redisClient.get(`mahjong:gameNextKyokuConfirmation:${room.id}`);
if (confirmationRaw == null) {
clearInterval(interval);
return;
}
const confirmation = JSON.parse(confirmationRaw) as NextKyokuConfirmation;
const allConfirmed = confirmation.user1 && confirmation.user2 && confirmation.user3 && confirmation.user4;
if (allConfirmed || (Date.now() - waitingStartedAt > NEXT_KYOKU_CONFIRMATION_TIMEOUT_MS)) {
await this.redisClient.del(`mahjong:gameNextKyokuConfirmation:${room.id}`);
clearInterval(interval);
this.nextKyoku(room, engine);
}
}, 2000);
}
@bindThis
@@ -425,6 +464,23 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit {
}
}
@bindThis
public async confirmNextKyoku(roomId: Room['id'], user: MiUser) {
const room = await this.getRoom(roomId);
if (room == null) return;
if (room.gameState == null) return;
// TODO: この辺の処理はアトミックに行いたいけどJSONサポートはRedis Stackが必要
const confirmationRaw = await this.redisClient.get(`mahjong:gameNextKyokuConfirmation:${room.id}`);
if (confirmationRaw == null) return;
const confirmation = JSON.parse(confirmationRaw) as NextKyokuConfirmation;
if (user.id === room.user1Id) confirmation.user1 = true;
if (user.id === room.user2Id) confirmation.user2 = true;
if (user.id === room.user3Id) confirmation.user3 = true;
if (user.id === room.user4Id) confirmation.user4 = true;
await this.redisClient.set(`mahjong:gameNextKyokuConfirmation:${room.id}`, JSON.stringify(confirmation));
}
@bindThis
public async commit_dahai(roomId: MiMahjongGame['id'], user: MiUser, tile: string, riichi = false) {
const room = await this.getRoom(roomId);
@@ -528,10 +584,10 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit {
const current = await this.redisClient.get(`mahjong:gameCallAndRonAsking:${room.id}`);
if (current == null) throw new Error('no asking found');
const currentAnswers = JSON.parse(current) as CallAndRonAnswers;
if (engine.state.ponAsking?.target === myHouse) currentAnswers.pon = false;
if (engine.state.ciiAsking?.target === myHouse) currentAnswers.cii = false;
if (engine.state.kanAsking?.target === myHouse) currentAnswers.kan = false;
if (engine.state.ronAsking != null && engine.state.ronAsking.targets.includes(myHouse)) currentAnswers.ron[myHouse] = false;
if (engine.state.ponAsking?.caller === myHouse) currentAnswers.pon = false;
if (engine.state.ciiAsking?.caller === myHouse) currentAnswers.cii = false;
if (engine.state.kanAsking?.caller === myHouse) currentAnswers.kan = false;
if (engine.state.ronAsking != null && engine.state.ronAsking.callers.includes(myHouse)) currentAnswers.ron[myHouse] = false;
await this.redisClient.set(`mahjong:gameCallAndRonAsking:${room.id}`, JSON.stringify(currentAnswers));
}

View File

@@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { MahjongService } from '@/core/MahjongService.js';
import { GlobalEvents } from '@/core/GlobalEventService.js';
import Channel, { type MiChannelService } from '../channel.js';
class MahjongRoomChannel extends Channel {
@@ -29,7 +30,19 @@ class MahjongRoomChannel extends Channel {
public async init(params: any) {
this.roomId = params.roomId as string;
this.subscriber.on(`mahjongRoomStream:${this.roomId}`, this.send);
this.subscriber.on(`mahjongRoomStream:${this.roomId}`, this.onMahjongRoomStreamMessage);
}
@bindThis
private async onMahjongRoomStreamMessage(message: GlobalEvents['mahjongRoom']['payload']) {
if (message.type === 'started') {
const packed = await this.mahjongService.packRoom(message.body.room, this.user!);
this.send('started', {
room: packed,
});
} else {
this.send(message.type, message.body);
}
}
@bindThis
@@ -38,6 +51,7 @@ class MahjongRoomChannel extends Channel {
case 'ready': this.ready(body); break;
case 'updateSettings': this.updateSettings(body.key, body.value); break;
case 'addAi': this.addAi(); break;
case 'confirmNextKyoku': this.confirmNextKyoku(); break;
case 'dahai': this.dahai(body.tile, body.riichi); break;
case 'hora': this.hora(); break;
case 'ron': this.ron(); break;
@@ -61,6 +75,13 @@ class MahjongRoomChannel extends Channel {
this.mahjongService.changeReadyState(this.roomId!, this.user, ready);
}
@bindThis
private async confirmNextKyoku() {
if (this.user == null) return;
this.mahjongService.confirmNextKyoku(this.roomId!, this.user);
}
@bindThis
private async addAi() {
if (this.user == null) return;
@@ -113,7 +134,7 @@ class MahjongRoomChannel extends Channel {
@bindThis
public dispose() {
// Unsubscribe events
this.subscriber.off(`mahjongRoomStream:${this.roomId}`, this.send);
this.subscriber.off(`mahjongRoomStream:${this.roomId}`, this.onMahjongRoomStreamMessage);
}
}