wip
This commit is contained in:
parent
0f2991cbaf
commit
ab404d491d
4
locales/index.d.ts
vendored
4
locales/index.d.ts
vendored
|
@ -9629,6 +9629,10 @@ export interface Locale extends ILocale {
|
||||||
* 退室
|
* 退室
|
||||||
*/
|
*/
|
||||||
"leave": string;
|
"leave": string;
|
||||||
|
/**
|
||||||
|
* CPUを追加
|
||||||
|
*/
|
||||||
|
"addCpu": string;
|
||||||
};
|
};
|
||||||
"_offlineScreen": {
|
"_offlineScreen": {
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -2566,6 +2566,7 @@ _mahjong:
|
||||||
ready: "準備完了"
|
ready: "準備完了"
|
||||||
cancelReady: "準備を再開"
|
cancelReady: "準備を再開"
|
||||||
leave: "退室"
|
leave: "退室"
|
||||||
|
addCpu: "CPUを追加"
|
||||||
|
|
||||||
_offlineScreen:
|
_offlineScreen:
|
||||||
title: "オフライン - サーバーに接続できません"
|
title: "オフライン - サーバーに接続できません"
|
||||||
|
|
|
@ -201,18 +201,21 @@ export interface MahjongRoomEventTypes {
|
||||||
user3: boolean;
|
user3: boolean;
|
||||||
user4: boolean;
|
user4: boolean;
|
||||||
};
|
};
|
||||||
|
started: {
|
||||||
|
room: Packed<'MahjongRoomDetailed'>;
|
||||||
|
};
|
||||||
tsumo: {
|
tsumo: {
|
||||||
house: Mahjong.Engine.House;
|
house: Mahjong.Engine.House;
|
||||||
tile: Mahjong.Engine.Tile;
|
tile: Mahjong.Common.Tile;
|
||||||
};
|
};
|
||||||
dahai: {
|
dahai: {
|
||||||
house: Mahjong.Engine.House;
|
house: Mahjong.Engine.House;
|
||||||
tile: Mahjong.Engine.Tile;
|
tile: Mahjong.Common.Tile;
|
||||||
};
|
};
|
||||||
dahaiAndTsumo: {
|
dahaiAndTsumo: {
|
||||||
house: Mahjong.Engine.House;
|
house: Mahjong.Engine.House;
|
||||||
dahaiTile: Mahjong.Engine.Tile;
|
dahaiTile: Mahjong.Common.Tile;
|
||||||
tsumoTile: Mahjong.Engine.Tile;
|
tsumoTile: Mahjong.Common.Tile;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
|
@ -26,7 +26,7 @@ import { ReversiGameEntityService } from './entities/ReversiGameEntityService.js
|
||||||
import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common';
|
import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common';
|
||||||
|
|
||||||
const INVITATION_TIMEOUT_MS = 1000 * 20; // 20sec
|
const INVITATION_TIMEOUT_MS = 1000 * 20; // 20sec
|
||||||
const PON_TIMEOUT_MS = 1000 * 10; // 10sec
|
const CALL_AND_RON_ASKING_TIMEOUT_MS = 1000 * 5; // 5sec
|
||||||
const DAHAI_TIMEOUT_MS = 1000 * 30; // 30sec
|
const DAHAI_TIMEOUT_MS = 1000 * 30; // 30sec
|
||||||
|
|
||||||
type Room = {
|
type Room = {
|
||||||
|
@ -52,10 +52,23 @@ type Room = {
|
||||||
user3Offline?: boolean;
|
user3Offline?: boolean;
|
||||||
user4Offline?: boolean;
|
user4Offline?: boolean;
|
||||||
isStarted?: boolean;
|
isStarted?: boolean;
|
||||||
|
timeLimitForEachTurn: number;
|
||||||
|
|
||||||
gameState?: Mahjong.Engine.MasterState;
|
gameState?: Mahjong.Engine.MasterState;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type CallAndRonAnswers = {
|
||||||
|
pon: null | boolean;
|
||||||
|
cii: null | boolean;
|
||||||
|
kan: null | boolean;
|
||||||
|
ron: {
|
||||||
|
e: null | boolean;
|
||||||
|
s: null | boolean;
|
||||||
|
w: null | boolean;
|
||||||
|
n: null | boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MahjongService implements OnApplicationShutdown, OnModuleInit {
|
export class MahjongService implements OnApplicationShutdown, OnModuleInit {
|
||||||
private notificationService: NotificationService;
|
private notificationService: NotificationService;
|
||||||
|
@ -98,6 +111,7 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit {
|
||||||
user2Ready: false,
|
user2Ready: false,
|
||||||
user3Ready: false,
|
user3Ready: false,
|
||||||
user4Ready: false,
|
user4Ready: false,
|
||||||
|
timeLimitForEachTurn: 30,
|
||||||
};
|
};
|
||||||
await this.saveRoom(room);
|
await this.saveRoom(room);
|
||||||
return room;
|
return room;
|
||||||
|
@ -152,21 +166,21 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit {
|
||||||
if (!room) return null;
|
if (!room) return null;
|
||||||
if (room.user1Id !== user.id) throw new Error('access denied');
|
if (room.user1Id !== user.id) throw new Error('access denied');
|
||||||
|
|
||||||
if (room.user2Id == null) {
|
if (room.user2Id == null && !room.user2Ai) {
|
||||||
room.user2Ai = true;
|
room.user2Ai = true;
|
||||||
room.user2Ready = true;
|
room.user2Ready = true;
|
||||||
await this.saveRoom(room);
|
await this.saveRoom(room);
|
||||||
this.globalEventService.publishMahjongRoomStream(room.id, 'joined', { index: 2, user: null });
|
this.globalEventService.publishMahjongRoomStream(room.id, 'joined', { index: 2, user: null });
|
||||||
return room;
|
return room;
|
||||||
}
|
}
|
||||||
if (room.user3Id == null) {
|
if (room.user3Id == null && !room.user3Ai) {
|
||||||
room.user3Ai = true;
|
room.user3Ai = true;
|
||||||
room.user3Ready = true;
|
room.user3Ready = true;
|
||||||
await this.saveRoom(room);
|
await this.saveRoom(room);
|
||||||
this.globalEventService.publishMahjongRoomStream(room.id, 'joined', { index: 3, user: null });
|
this.globalEventService.publishMahjongRoomStream(room.id, 'joined', { index: 3, user: null });
|
||||||
return room;
|
return room;
|
||||||
}
|
}
|
||||||
if (room.user4Id == null) {
|
if (room.user4Id == null && !room.user4Ai) {
|
||||||
room.user4Ai = true;
|
room.user4Ai = true;
|
||||||
room.user4Ready = true;
|
room.user4Ready = true;
|
||||||
await this.saveRoom(room);
|
await this.saveRoom(room);
|
||||||
|
@ -267,43 +281,129 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async dahai(room: Room, engine: Mahjong.Engine.MasterGameEngine, user: MiUser, tile: Mahjong.Engine.Tile) {
|
private async answer(room: Room, engine: Mahjong.Engine.MasterGameEngine, answers: CallAndRonAnswers) {
|
||||||
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;
|
const res = engine.op_resolveCallAndRonInterruption({
|
||||||
const res = engine.op_dahai(myHouse, tile);
|
pon: answers.pon ?? false,
|
||||||
if (res.canPonHouse) {
|
cii: answers.cii ?? false,
|
||||||
// TODO: 家がCPUだった場合の処理
|
kan: answers.kan ?? false,
|
||||||
this.redisClient.set(`mahjong:gamePonAsking:${room.id}`, '');
|
ron: [...(answers.ron.e ? ['e'] : []), ...(answers.ron.s ? ['s'] : []), ...(answers.ron.w ? ['w'] : []), ...(answers.ron.n ? ['n'] : [])],
|
||||||
const waitingStartedAt = Date.now();
|
});
|
||||||
const interval = setInterval(async () => {
|
room.gameState = engine.state;
|
||||||
const waiting = await this.redisClient.get(`mahjong:gamePonAsking:${room.id}`);
|
await this.saveRoom(room);
|
||||||
if (waiting == null) {
|
|
||||||
clearInterval(interval);
|
if (res.type === 'tsumo') {
|
||||||
return;
|
this.globalEventService.publishMahjongRoomStream(room.id, 'log', { operation: 'tsumo', house: res.house, tile: res.tile });
|
||||||
}
|
this.next(room, engine);
|
||||||
if (Date.now() - waitingStartedAt > PON_TIMEOUT_MS) {
|
} else if (res.type === 'ponned') {
|
||||||
await this.redisClient.del(`mahjong:gamePonAsking:${room.id}`);
|
this.globalEventService.publishMahjongRoomStream(room.id, 'log', { operation: 'ponned', house: res.house, tile: res.tile });
|
||||||
clearInterval(interval);
|
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;
|
||||||
const res = engine.op_noOnePon();
|
this.waitForDahai(room, userId, engine);
|
||||||
this.globalEventService.publishMahjongRoomStream(room.id, 'tsumo', { house: res.house, tile: res.tile });
|
} else if (res.type === 'kanned') {
|
||||||
return;
|
// TODO
|
||||||
}
|
} else if (res.type === 'ronned') {
|
||||||
}, 2000);
|
// TODO
|
||||||
this.globalEventService.publishMahjongRoomStream(room.id, 'dahai', { house: myHouse, tile });
|
|
||||||
} else {
|
|
||||||
this.globalEventService.publishMahjongRoomStream(room.id, 'dahaiAndTsumo', { house: myHouse, dahaiTile: tile, tsumoTile: res.tsumoTile });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async op_dahai(roomId: MiMahjongGame['id'], user: MiUser, tile: string) {
|
private async next(room: Room, engine: Mahjong.Engine.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));
|
||||||
|
|
||||||
|
if (aiHouses.includes(engine.state.turn)) {
|
||||||
|
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));
|
||||||
|
}, 1000);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
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.waitForDahai(room, userId, engine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private async dahai(room: Room, engine: Mahjong.Engine.MasterGameEngine, house: Mahjong.Common.House, tile: Mahjong.Common.Tile, operationId?: string) {
|
||||||
|
const res = engine.op_dahai(house, tile);
|
||||||
|
room.gameState = engine.state;
|
||||||
|
await this.saveRoom(room);
|
||||||
|
|
||||||
|
const aiHouses = [[1, room.user1Ai], [2, room.user2Ai], [3, room.user3Ai], [4, room.user4Ai]].filter(([id, ai]) => ai).map(([id, ai]) => engine.getHouse(id));
|
||||||
|
|
||||||
|
if (res.asking) {
|
||||||
|
console.log('asking', res);
|
||||||
|
|
||||||
|
const answers: CallAndRonAnswers = {
|
||||||
|
pon: null,
|
||||||
|
cii: null,
|
||||||
|
kan: null,
|
||||||
|
ron: {
|
||||||
|
e: null,
|
||||||
|
s: null,
|
||||||
|
w: null,
|
||||||
|
n: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (aiHouses.includes(res.canPonHouse)) {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
if (aiHouses.includes(res.canChiHouse)) {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
if (aiHouses.includes(res.canKanHouse)) {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
for (const h of res.canRonHouses) {
|
||||||
|
if (aiHouses.includes(h)) {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.redisClient.set(`mahjong:gameCallAndRonAsking:${room.id}`, JSON.stringify(answers));
|
||||||
|
const waitingStartedAt = Date.now();
|
||||||
|
const interval = setInterval(async () => {
|
||||||
|
const current = await this.redisClient.get(`mahjong:gameCallAndRonAsking:${room.id}`);
|
||||||
|
if (current == null) throw new Error('arienai (gameCallAndRonAsking)');
|
||||||
|
const currentAnswers = JSON.parse(current) as CallAndRonAnswers;
|
||||||
|
const allAnswered = !(
|
||||||
|
(res.canPonHouse != null && currentAnswers.pon == null) ||
|
||||||
|
(res.canCiiHouse != null && currentAnswers.cii == null) ||
|
||||||
|
(res.canKanHouse != null && currentAnswers.kan == null) ||
|
||||||
|
(res.canRonHouses.includes('e') && currentAnswers.ron.e == null) ||
|
||||||
|
(res.canRonHouses.includes('s') && currentAnswers.ron.s == null) ||
|
||||||
|
(res.canRonHouses.includes('w') && currentAnswers.ron.w == null) ||
|
||||||
|
(res.canRonHouses.includes('n') && currentAnswers.ron.n == null)
|
||||||
|
);
|
||||||
|
if (allAnswered || (Date.now() - waitingStartedAt > CALL_AND_RON_ASKING_TIMEOUT_MS)) {
|
||||||
|
console.log('answerd');
|
||||||
|
await this.redisClient.del(`mahjong:gamePonAsking:${room.id}`);
|
||||||
|
clearInterval(interval);
|
||||||
|
this.answer(room, engine, currentAnswers);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
this.globalEventService.publishMahjongRoomStream(room.id, 'log', { operation: 'dahai', house: house, tile, id: operationId });
|
||||||
|
} else {
|
||||||
|
this.globalEventService.publishMahjongRoomStream(room.id, 'log', { operation: 'dahaiAndTsumo', house: house, dahaiTile: tile, tsumoTile: res.tsumoTile, id: operationId });
|
||||||
|
|
||||||
|
this.next(room, engine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async op_dahai(roomId: MiMahjongGame['id'], user: MiUser, tile: string, operationId: string) {
|
||||||
const room = await this.getRoom(roomId);
|
const room = await this.getRoom(roomId);
|
||||||
if (room == null) return;
|
if (room == null) return;
|
||||||
if (room.gameState == null) return;
|
if (room.gameState == null) return;
|
||||||
|
if (!Mahjong.Utils.isTile(tile)) return;
|
||||||
|
|
||||||
await this.redisClient.del(`mahjong:gameDahaiWaiting:${room.id}`);
|
await this.redisClient.del(`mahjong:gameDahaiWaiting:${room.id}`);
|
||||||
|
|
||||||
const engine = new Mahjong.Engine.MasterGameEngine(room.gameState);
|
const engine = new Mahjong.Engine.MasterGameEngine(room.gameState);
|
||||||
await this.dahai(room, engine, user, tile);
|
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;
|
||||||
|
await this.dahai(room, engine, myHouse, tile, operationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -314,26 +414,56 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit {
|
||||||
|
|
||||||
const engine = new Mahjong.Engine.MasterGameEngine(room.gameState);
|
const engine = new Mahjong.Engine.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;
|
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;
|
||||||
const res = engine.op_pon(myHouse);
|
|
||||||
this.waitForDahai(room, user, engine);
|
// TODO: 自分にポン回答する権利がある状態かバリデーション
|
||||||
|
|
||||||
|
// TODO: この辺の処理はアトミックに行いたいけどJSONサポートはRedis Stackが必要
|
||||||
|
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;
|
||||||
|
currentAnswers.pon = true;
|
||||||
|
await this.redisClient.set(`mahjong:gameCallAndRonAsking:${room.id}`, JSON.stringify(currentAnswers));
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async waitForDahai(game: Room, user: MiUser, engine: Mahjong.Engine.MasterGameEngine) {
|
public async op_nop(roomId: MiMahjongGame['id'], user: MiUser) {
|
||||||
this.redisClient.set(`mahjong:gameDahaiWaiting:${game.id}`, '');
|
const room = await this.getRoom(roomId);
|
||||||
|
if (room == null) return;
|
||||||
|
if (room.gameState == null) return;
|
||||||
|
|
||||||
|
const engine = new Mahjong.Engine.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が必要
|
||||||
|
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;
|
||||||
|
await this.redisClient.set(`mahjong:gameCallAndRonAsking:${room.id}`, JSON.stringify(currentAnswers));
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private async waitForDahai(room: Room, userId: MiUser['id'], engine: Mahjong.Engine.MasterGameEngine) {
|
||||||
|
const id = Math.random().toString(36).slice(2);
|
||||||
|
console.log('waitForDahai', userId, id);
|
||||||
|
this.redisClient.sadd(`mahjong:gameDahaiWaiting:${room.id}`, id);
|
||||||
const waitingStartedAt = Date.now();
|
const waitingStartedAt = Date.now();
|
||||||
const interval = setInterval(async () => {
|
const interval = setInterval(async () => {
|
||||||
const waiting = await this.redisClient.get(`mahjong:gameDahaiWaiting:${game.id}`);
|
const waiting = await this.redisClient.sismember(`mahjong:gameDahaiWaiting:${room.id}`, id);
|
||||||
if (waiting == null) {
|
if (waiting === 0) {
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (Date.now() - waitingStartedAt > DAHAI_TIMEOUT_MS) {
|
if (Date.now() - waitingStartedAt > DAHAI_TIMEOUT_MS) {
|
||||||
await this.redisClient.del(`mahjong:gameDahaiWaiting:${game.id}`);
|
await this.redisClient.srem(`mahjong:gameDahaiWaiting:${room.id}`, id);
|
||||||
|
console.log('dahai timeout', userId, id);
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
const house = game.user1Id === user.id ? engine.state.user1House : game.user2Id === user.id ? engine.state.user2House : game.user3Id === user.id ? engine.state.user3House : engine.state.user4House;
|
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 = house === 'e' ? engine.state.eHandTiles : house === 's' ? engine.state.sHandTiles : house === 'w' ? engine.state.wHandTiles : engine.state.nHandTiles;
|
const handTiles = house === 'e' ? engine.state.eHandTiles : house === 's' ? engine.state.sHandTiles : house === 'w' ? engine.state.wHandTiles : engine.state.nHandTiles;
|
||||||
await this.dahai(game, engine, user, handTiles.at(-1));
|
await this.dahai(room, engine, house, handTiles.at(-1));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}, 2000);
|
}, 2000);
|
||||||
|
|
|
@ -106,5 +106,9 @@ export const packedMahjongRoomDetailedSchema = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
timeLimitForEachTurn: {
|
||||||
|
type: 'number',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
|
@ -38,7 +38,9 @@ class MahjongRoomChannel extends Channel {
|
||||||
case 'ready': this.ready(body); break;
|
case 'ready': this.ready(body); break;
|
||||||
case 'updateSettings': this.updateSettings(body.key, body.value); break;
|
case 'updateSettings': this.updateSettings(body.key, body.value); break;
|
||||||
case 'addAi': this.addAi(); break;
|
case 'addAi': this.addAi(); break;
|
||||||
case 'putStone': this.putStone(body.pos, body.id); break;
|
case 'dahai': this.dahai(body.tile, body.id); break;
|
||||||
|
case 'pon': this.pon(); break;
|
||||||
|
case 'nop': this.nop(); break;
|
||||||
case 'claimTimeIsUp': this.claimTimeIsUp(); break;
|
case 'claimTimeIsUp': this.claimTimeIsUp(); break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -65,10 +67,24 @@ class MahjongRoomChannel extends Channel {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async putStone(pos: number, id: string) {
|
private async dahai(tile: string, id: string) {
|
||||||
if (this.user == null) return;
|
if (this.user == null) return;
|
||||||
|
|
||||||
this.mahjongService.putStoneToRoom(this.roomId!, this.user, pos, id);
|
this.mahjongService.op_dahai(this.roomId!, this.user, tile, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private async pon() {
|
||||||
|
if (this.user == null) return;
|
||||||
|
|
||||||
|
this.mahjongService.op_pon(this.roomId!, this.user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private async nop() {
|
||||||
|
if (this.user == null) return;
|
||||||
|
|
||||||
|
this.mahjongService.op_nop(this.roomId!, this.user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
|
|
@ -5,6 +5,45 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MkSpacer :contentMax="500">
|
<MkSpacer :contentMax="500">
|
||||||
|
<div class="_gaps">
|
||||||
|
<div>
|
||||||
|
{{ engine.myHouse }} {{ engine.state.turn }}
|
||||||
|
</div>
|
||||||
|
<div class="_panel">
|
||||||
|
<div>{{ Mahjong.Utils.prevHouse(Mahjong.Utils.prevHouse(Mahjong.Utils.prevHouse(engine.myHouse))) }} ho</div>
|
||||||
|
<div v-for="tile in engine.getHoTilesOf(Mahjong.Utils.prevHouse(Mahjong.Utils.prevHouse(Mahjong.Utils.prevHouse(engine.myHouse))))" style="display: inline-block;">
|
||||||
|
<img :src="`/client-assets/mahjong/tiles/${tile}.gif`"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="_panel">
|
||||||
|
<div>{{ Mahjong.Utils.prevHouse(Mahjong.Utils.prevHouse(engine.myHouse)) }} ho</div>
|
||||||
|
<div v-for="tile in engine.getHoTilesOf(Mahjong.Utils.prevHouse(Mahjong.Utils.prevHouse(engine.myHouse)))" style="display: inline-block;">
|
||||||
|
<img :src="`/client-assets/mahjong/tiles/${tile}.gif`"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="_panel">
|
||||||
|
<div>{{ Mahjong.Utils.prevHouse(engine.myHouse) }} ho</div>
|
||||||
|
<div v-for="tile in engine.getHoTilesOf(Mahjong.Utils.prevHouse(engine.myHouse))" style="display: inline-block;">
|
||||||
|
<img :src="`/client-assets/mahjong/tiles/${tile}.gif`"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="_panel">
|
||||||
|
<div>{{ engine.myHouse }} ho</div>
|
||||||
|
<div v-for="tile in engine.myHoTiles" style="display: inline-block;">
|
||||||
|
<img :src="`/client-assets/mahjong/tiles/${tile}.gif`"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="_panel">
|
||||||
|
<div>My hand</div>
|
||||||
|
<div v-for="tile in Mahjong.Utils.sortTiles(engine.myHandTiles)" style="display: inline-block;" @click="dahai(tile, $event)">
|
||||||
|
<img :src="`/client-assets/mahjong/tiles/${tile}.gif`"/>
|
||||||
|
</div>
|
||||||
|
<MkButton v-if="engine.state.canPon" @click="pon">Pon</MkButton>
|
||||||
|
<MkButton v-if="engine.state.canPon" @click="skip">Skip pon</MkButton>
|
||||||
|
<MkButton v-if="isMyTurn && canHora">Hora</MkButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</MkSpacer>
|
</MkSpacer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -28,18 +67,22 @@ import { confetti } from '@/scripts/confetti.js';
|
||||||
const $i = signinRequired();
|
const $i = signinRequired();
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
room: Misskey.entities.ReversiRoomDetailed;
|
room: Misskey.entities.MahjongRoomDetailed;
|
||||||
connection?: Misskey.ChannelConnection | null;
|
connection?: Misskey.ChannelConnection | null;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const room = ref<Misskey.entities.ReversiRoomDetailed>(deepClone(props.room));
|
const room = ref<Misskey.entities.MahjongRoomDetailed>(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 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, room.value.gameState));
|
const engine = shallowRef(new Mahjong.Engine.PlayerGameEngine(myUserNumber.value, room.value.gameState));
|
||||||
|
|
||||||
const isMyTurn = computed(() => {
|
const isMyTurn = computed(() => {
|
||||||
return engine.value.state.turn === engine.value.myHouse;
|
return engine.value.state.turn === engine.value.myHouse;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const canHora = computed(() => {
|
||||||
|
return Mahjong.Utils.getHoraSets(engine.value.myHandTiles).length > 0;
|
||||||
|
});
|
||||||
|
|
||||||
/*
|
/*
|
||||||
if (room.value.isStarted && !room.value.isEnded) {
|
if (room.value.isStarted && !room.value.isEnded) {
|
||||||
useInterval(() => {
|
useInterval(() => {
|
||||||
|
@ -84,22 +127,53 @@ if (!props.room.isEnded) {
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
function dahai(tile: Mahjong.Common.Tile, ev: MouseEvent) {
|
||||||
|
if (!isMyTurn.value) return;
|
||||||
|
|
||||||
|
engine.value.op_dahai(engine.value.myHouse, tile);
|
||||||
|
|
||||||
|
const id = Math.random().toString(36).slice(2);
|
||||||
|
appliedOps.push(id);
|
||||||
|
props.connection!.send('dahai', {
|
||||||
|
tile: tile,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function pon() {
|
||||||
|
engine.value.op_pon(engine.value.canPonTo, engine.value.myHouse);
|
||||||
|
|
||||||
|
const id = Math.random().toString(36).slice(2);
|
||||||
|
appliedOps.push(id);
|
||||||
|
props.connection!.send('pon', {
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function skip() {
|
||||||
|
engine.value.op_nop(engine.value.myHouse);
|
||||||
|
|
||||||
|
const id = Math.random().toString(36).slice(2);
|
||||||
|
appliedOps.push(id);
|
||||||
|
props.connection!.send('nop', {});
|
||||||
|
}
|
||||||
|
|
||||||
async function onStreamLog(log) {
|
async function onStreamLog(log) {
|
||||||
if (log.id == null || !appliedOps.includes(log.id)) {
|
if (log.id == null || !appliedOps.includes(log.id)) {
|
||||||
switch (log.operation) {
|
switch (log.operation) {
|
||||||
case 'put': {
|
case 'dahai': {
|
||||||
sound.playUrl('/client-assets/mahjong/dahai.mp3', {
|
sound.playUrl('/client-assets/mahjong/dahai.mp3', {
|
||||||
volume: 1,
|
volume: 1,
|
||||||
playbackRate: 1,
|
playbackRate: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (log.house !== engine.value.turn) { // = desyncが発生している
|
//if (log.house !== engine.value.state.turn) { // = desyncが発生している
|
||||||
const _room = await misskeyApi('reversi/show-room', {
|
// const _room = await misskeyApi('mahjong/show-room', {
|
||||||
roomId: props.room.id,
|
// roomId: props.room.id,
|
||||||
});
|
// });
|
||||||
restoreRoom(_room);
|
// restoreRoom(_room);
|
||||||
return;
|
// return;
|
||||||
}
|
//}
|
||||||
|
|
||||||
engine.value.op_dahai(log.house, log.tile);
|
engine.value.op_dahai(log.house, log.tile);
|
||||||
triggerRef(engine);
|
triggerRef(engine);
|
||||||
|
@ -109,6 +183,33 @@ async function onStreamLog(log) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'dahaiAndTsumo': {
|
||||||
|
sound.playUrl('/client-assets/mahjong/dahai.mp3', {
|
||||||
|
volume: 1,
|
||||||
|
playbackRate: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
//if (log.house !== engine.value.state.turn) { // = desyncが発生している
|
||||||
|
// const _room = await misskeyApi('mahjong/show-room', {
|
||||||
|
// roomId: props.room.id,
|
||||||
|
// });
|
||||||
|
// restoreRoom(_room);
|
||||||
|
// return;
|
||||||
|
//}
|
||||||
|
|
||||||
|
engine.value.op_dahai(log.house, log.dahaiTile);
|
||||||
|
triggerRef(engine);
|
||||||
|
|
||||||
|
window.setTimeout(() => {
|
||||||
|
engine.value.op_tsumo(Mahjong.Utils.nextHouse(log.house), log.tsumoTile);
|
||||||
|
triggerRef(engine);
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
myTurnTimerRmain.value = room.value.timeLimitForEachTurn;
|
||||||
|
opTurnTimerRmain.value = room.value.timeLimitForEachTurn;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -124,184 +225,27 @@ function restoreRoom(_room) {
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (props.connection != null) {
|
if (props.connection != null) {
|
||||||
props.connection.on('log', onStreamLog);
|
props.connection.on('log', onStreamLog);
|
||||||
props.connection.on('ended', onStreamEnded);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onActivated(() => {
|
onActivated(() => {
|
||||||
if (props.connection != null) {
|
if (props.connection != null) {
|
||||||
props.connection.on('log', onStreamLog);
|
props.connection.on('log', onStreamLog);
|
||||||
props.connection.on('ended', onStreamEnded);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onDeactivated(() => {
|
onDeactivated(() => {
|
||||||
if (props.connection != null) {
|
if (props.connection != null) {
|
||||||
props.connection.off('log', onStreamLog);
|
props.connection.off('log', onStreamLog);
|
||||||
props.connection.off('ended', onStreamEnded);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (props.connection != null) {
|
if (props.connection != null) {
|
||||||
props.connection.off('log', onStreamLog);
|
props.connection.off('log', onStreamLog);
|
||||||
props.connection.off('ended', onStreamEnded);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
@use "sass:math";
|
|
||||||
|
|
||||||
.transition_flip_enterActive,
|
|
||||||
.transition_flip_leaveActive {
|
|
||||||
backface-visibility: hidden;
|
|
||||||
transition: opacity 0.5s ease, transform 0.5s ease;
|
|
||||||
}
|
|
||||||
.transition_flip_enterFrom {
|
|
||||||
transform: rotateY(-180deg);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
.transition_flip_leaveTo {
|
|
||||||
transform: rotateY(180deg);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
$label-size: 16px;
|
|
||||||
$gap: 4px;
|
|
||||||
|
|
||||||
.root {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.board {
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin: 0 auto;
|
|
||||||
|
|
||||||
padding: 7px;
|
|
||||||
background: #8C4F26;
|
|
||||||
box-shadow: 0 6px 16px #0007, 0 0 1px 1px #693410, inset 0 0 2px 1px #ce8a5c;
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.boardInner {
|
|
||||||
padding: 32px;
|
|
||||||
|
|
||||||
background: var(--panel);
|
|
||||||
box-shadow: 0 0 2px 1px #ce8a5c, inset 0 0 1px 1px #693410;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@container (max-width: 400px) {
|
|
||||||
.boardInner {
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.labelsX {
|
|
||||||
height: $label-size;
|
|
||||||
padding: 0 $label-size;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.labelsXLabel {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 0.8em;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
margin-left: -(math.div($gap, 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-right: -(math.div($gap, 2));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.labelsY {
|
|
||||||
width: $label-size;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.labelsYLabel {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 12px;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
margin-top: -(math.div($gap, 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: -(math.div($gap, 2));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.boardCells {
|
|
||||||
flex: 1;
|
|
||||||
display: grid;
|
|
||||||
grid-gap: $gap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.boardCell {
|
|
||||||
background: transparent;
|
|
||||||
border-radius: 100%;
|
|
||||||
aspect-ratio: 1;
|
|
||||||
transform-style: preserve-3d;
|
|
||||||
perspective: 150px;
|
|
||||||
transition: border 0.25s ease, opacity 0.25s ease;
|
|
||||||
|
|
||||||
&.boardCell_empty {
|
|
||||||
border: solid 2px var(--divider);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.boardCell_empty.boardCell_can {
|
|
||||||
border-color: var(--accent);
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.boardCell_empty.boardCell_myTurn {
|
|
||||||
border-color: var(--divider);
|
|
||||||
opacity: 1;
|
|
||||||
|
|
||||||
&.boardCell_can {
|
|
||||||
border-color: var(--accent);
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--accent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.boardCell_prev {
|
|
||||||
box-shadow: 0 0 0 4px var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.boardCell_isEnded {
|
|
||||||
border-color: var(--divider);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.boardCell_none {
|
|
||||||
border-color: transparent !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.boardCellStone {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
user-select: none;
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
border-radius: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -28,6 +28,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div v-if="room.user4Ready">OK</div>
|
<div v-if="room.user4Ready">OK</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<MkButton rounded primary @click="addCpu">{{ i18n.ts._mahjong.addCpu }}</MkButton>
|
||||||
|
</div>
|
||||||
</MkSpacer>
|
</MkSpacer>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div :class="$style.footer">
|
<div :class="$style.footer">
|
||||||
|
@ -99,6 +102,10 @@ function unready() {
|
||||||
props.connection.send('ready', false);
|
props.connection.send('ready', false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addCpu() {
|
||||||
|
props.connection.send('addAi', {});
|
||||||
|
}
|
||||||
|
|
||||||
function onChangeReadyStates(states) {
|
function onChangeReadyStates(states) {
|
||||||
room.value.user1Ready = states.user1;
|
room.value.user1Ready = states.user1;
|
||||||
room.value.user2Ready = states.user2;
|
room.value.user2Ready = states.user2;
|
||||||
|
@ -106,10 +113,40 @@ function onChangeReadyStates(states) {
|
||||||
room.value.user4Ready = states.user4;
|
room.value.user4Ready = states.user4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onJoined(x) {
|
||||||
|
switch (x.index) {
|
||||||
|
case 1:
|
||||||
|
room.value.user1 = x.user;
|
||||||
|
room.value.user1Ai = x.user == null;
|
||||||
|
room.value.user1Ready = room.value.user1Ai;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
room.value.user2 = x.user;
|
||||||
|
room.value.user2Ai = x.user == null;
|
||||||
|
room.value.user2Ready = room.value.user2Ai;
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
room.value.user3 = x.user;
|
||||||
|
room.value.user3Ai = x.user == null;
|
||||||
|
room.value.user3Ready = room.value.user3Ai;
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
room.value.user4 = x.user;
|
||||||
|
room.value.user4Ai = x.user == null;
|
||||||
|
room.value.user4Ready = room.value.user4Ai;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
props.connection.on('changeReadyStates', onChangeReadyStates);
|
props.connection.on('changeReadyStates', onChangeReadyStates);
|
||||||
|
props.connection.on('joined', onJoined);
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
props.connection.off('changeReadyStates', onChangeReadyStates);
|
props.connection.off('changeReadyStates', onChangeReadyStates);
|
||||||
|
props.connection.off('joined', onJoined);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* version: 2024.2.0-beta.7
|
* version: 2024.2.0-beta.7
|
||||||
* generatedAt: 2024-01-26T05:23:04.911Z
|
* generatedAt: 2024-01-26T07:53:15.923Z
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { SwitchCaseResponseType } from '../api.js';
|
import type { SwitchCaseResponseType } from '../api.js';
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* version: 2024.2.0-beta.7
|
* version: 2024.2.0-beta.7
|
||||||
* generatedAt: 2024-01-26T05:23:04.909Z
|
* generatedAt: 2024-01-26T07:53:15.921Z
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* version: 2024.2.0-beta.7
|
* version: 2024.2.0-beta.7
|
||||||
* generatedAt: 2024-01-26T05:23:04.908Z
|
* generatedAt: 2024-01-26T07:53:15.919Z
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { operations } from './types.js';
|
import { operations } from './types.js';
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* version: 2024.2.0-beta.7
|
* version: 2024.2.0-beta.7
|
||||||
* generatedAt: 2024-01-26T05:23:04.907Z
|
* generatedAt: 2024-01-26T07:53:15.918Z
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { components } from './types.js';
|
import { components } from './types.js';
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* version: 2024.2.0-beta.7
|
* version: 2024.2.0-beta.7
|
||||||
* generatedAt: 2024-01-26T05:23:04.825Z
|
* generatedAt: 2024-01-26T07:53:15.837Z
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -4595,6 +4595,7 @@ export type components = {
|
||||||
user2Ready: boolean;
|
user2Ready: boolean;
|
||||||
user3Ready: boolean;
|
user3Ready: boolean;
|
||||||
user4Ready: boolean;
|
user4Ready: boolean;
|
||||||
|
timeLimitForEachTurn: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
responses: never;
|
responses: never;
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"name": "misskey-mahjong",
|
"name": "misskey-mahjong",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
|
"types": "./built/dts/index.d.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"import": "./built/esm/index.js",
|
"import": "./built/esm/index.js",
|
||||||
|
|
48
packages/misskey-mahjong/src/common.ts
Normal file
48
packages/misskey-mahjong/src/common.ts
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
// NOTE: アガリ形の判定に使われるため並び順が重要
|
||||||
|
// 具体的には、文字列としてソートした際に同じ牌種の1~9が順に並んでいる必要がある
|
||||||
|
// また、字牌は最後にある必要がある
|
||||||
|
export const TILE_TYPES = [
|
||||||
|
'm1',
|
||||||
|
'm2',
|
||||||
|
'm3',
|
||||||
|
'm4',
|
||||||
|
'm5',
|
||||||
|
'm6',
|
||||||
|
'm7',
|
||||||
|
'm8',
|
||||||
|
'm9',
|
||||||
|
'p1',
|
||||||
|
'p2',
|
||||||
|
'p3',
|
||||||
|
'p4',
|
||||||
|
'p5',
|
||||||
|
'p6',
|
||||||
|
'p7',
|
||||||
|
'p8',
|
||||||
|
'p9',
|
||||||
|
's1',
|
||||||
|
's2',
|
||||||
|
's3',
|
||||||
|
's4',
|
||||||
|
's5',
|
||||||
|
's6',
|
||||||
|
's7',
|
||||||
|
's8',
|
||||||
|
's9',
|
||||||
|
'e',
|
||||||
|
's',
|
||||||
|
'w',
|
||||||
|
'n',
|
||||||
|
'haku',
|
||||||
|
'hatsu',
|
||||||
|
'chun',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type Tile = typeof TILE_TYPES[number];
|
||||||
|
|
||||||
|
export type House = 'e' | 's' | 'w' | 'n';
|
|
@ -4,51 +4,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import CRC32 from 'crc-32';
|
import CRC32 from 'crc-32';
|
||||||
|
import { Tile, House, TILE_TYPES } from './common.js';
|
||||||
export const TILE_TYPES = [
|
import * as Utils from './utils.js';
|
||||||
'bamboo1',
|
|
||||||
'bamboo2',
|
|
||||||
'bamboo3',
|
|
||||||
'bamboo4',
|
|
||||||
'bamboo5',
|
|
||||||
'bamboo6',
|
|
||||||
'bamboo7',
|
|
||||||
'bamboo8',
|
|
||||||
'bamboo9',
|
|
||||||
'character1',
|
|
||||||
'character2',
|
|
||||||
'character3',
|
|
||||||
'character4',
|
|
||||||
'character5',
|
|
||||||
'character6',
|
|
||||||
'character7',
|
|
||||||
'character8',
|
|
||||||
'character9',
|
|
||||||
'circle1',
|
|
||||||
'circle2',
|
|
||||||
'circle3',
|
|
||||||
'circle4',
|
|
||||||
'circle5',
|
|
||||||
'circle6',
|
|
||||||
'circle7',
|
|
||||||
'circle8',
|
|
||||||
'circle9',
|
|
||||||
'wind-east',
|
|
||||||
'wind-south',
|
|
||||||
'wind-west',
|
|
||||||
'wind-north',
|
|
||||||
'dragon-red',
|
|
||||||
'dragon-green',
|
|
||||||
'dragon-white',
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export type Tile = typeof TILE_TYPES[number];
|
|
||||||
|
|
||||||
export function isTile(tile: string): tile is Tile {
|
|
||||||
return TILE_TYPES.includes(tile as Tile);
|
|
||||||
}
|
|
||||||
|
|
||||||
export type House = 'e' | 's' | 'w' | 'n';
|
|
||||||
|
|
||||||
export type MasterState = {
|
export type MasterState = {
|
||||||
user1House: House;
|
user1House: House;
|
||||||
|
@ -81,34 +38,56 @@ export type MasterState = {
|
||||||
wPoints: number;
|
wPoints: number;
|
||||||
nPoints: number;
|
nPoints: number;
|
||||||
turn: House | null;
|
turn: House | null;
|
||||||
ponAsking: {
|
|
||||||
|
ronAsking: {
|
||||||
|
/**
|
||||||
|
* 牌を捨てた人
|
||||||
|
*/
|
||||||
source: House;
|
source: House;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ロンする権利がある人
|
||||||
|
*/
|
||||||
|
targets: House[];
|
||||||
|
} | null;
|
||||||
|
|
||||||
|
ponAsking: {
|
||||||
|
/**
|
||||||
|
* 牌を捨てた人
|
||||||
|
*/
|
||||||
|
source: House;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ポンする権利がある人
|
||||||
|
*/
|
||||||
target: House;
|
target: House;
|
||||||
} | null;
|
} | null;
|
||||||
|
|
||||||
ciiAsking: {
|
ciiAsking: {
|
||||||
|
/**
|
||||||
|
* 牌を捨てた人
|
||||||
|
*/
|
||||||
source: House;
|
source: House;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* チーする権利がある人(sourceの下家なのは自明だがプログラム簡略化のため)
|
||||||
|
*/
|
||||||
|
target: House;
|
||||||
|
} | null;
|
||||||
|
|
||||||
|
kanAsking: {
|
||||||
|
/**
|
||||||
|
* 牌を捨てた人
|
||||||
|
*/
|
||||||
|
source: House;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* カンする権利がある人
|
||||||
|
*/
|
||||||
|
target: House;
|
||||||
} | null;
|
} | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class Common {
|
|
||||||
public static nextHouse(house: House): House {
|
|
||||||
switch (house) {
|
|
||||||
case 'e':
|
|
||||||
return 's';
|
|
||||||
case 's':
|
|
||||||
return 'w';
|
|
||||||
case 'w':
|
|
||||||
return 'n';
|
|
||||||
case 'n':
|
|
||||||
return 'e';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static checkYaku(tiles: Tile[]) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class MasterGameEngine {
|
export class MasterGameEngine {
|
||||||
public state: MasterState;
|
public state: MasterState;
|
||||||
|
|
||||||
|
@ -117,7 +96,7 @@ export class MasterGameEngine {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static createInitialState(): MasterState {
|
public static createInitialState(): MasterState {
|
||||||
const tiles = TILE_TYPES.slice();
|
const tiles = [...TILE_TYPES.slice(), ...TILE_TYPES.slice(), ...TILE_TYPES.slice(), ...TILE_TYPES.slice()];
|
||||||
tiles.sort(() => Math.random() - 0.5);
|
tiles.sort(() => Math.random() - 0.5);
|
||||||
|
|
||||||
const eHandTiles = tiles.splice(0, 14);
|
const eHandTiles = tiles.splice(0, 14);
|
||||||
|
@ -161,62 +140,64 @@ export class MasterGameEngine {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private ツモ(): Tile {
|
private tsumo(): Tile {
|
||||||
const tile = this.state.tiles.pop();
|
const tile = this.state.tiles.pop();
|
||||||
switch (this.state.turn) {
|
if (tile == null) throw new Error('No tiles left');
|
||||||
case 'e':
|
this.getHandTilesOf(this.state.turn).push(tile);
|
||||||
this.state.eHandTiles.push(tile);
|
|
||||||
break;
|
|
||||||
case 's':
|
|
||||||
this.state.sHandTiles.push(tile);
|
|
||||||
break;
|
|
||||||
case 'w':
|
|
||||||
this.state.wHandTiles.push(tile);
|
|
||||||
break;
|
|
||||||
case 'n':
|
|
||||||
this.state.nHandTiles.push(tile);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return tile;
|
return tile;
|
||||||
}
|
}
|
||||||
|
|
||||||
public op_dahai(house: House, tile: Tile) {
|
public op_dahai(house: House, tile: Tile) {
|
||||||
if (this.state.turn !== house) throw new Error('Not your turn');
|
if (this.state.turn !== house) throw new Error('Not your turn');
|
||||||
|
|
||||||
|
const handTiles = this.getHandTilesOf(house);
|
||||||
|
if (!handTiles.includes(tile)) throw new Error('No such tile in your hand');
|
||||||
|
handTiles.splice(handTiles.indexOf(tile), 1);
|
||||||
|
this.getHoTilesOf(house).push(tile);
|
||||||
|
|
||||||
|
const canRonHouses: House[] = [];
|
||||||
switch (house) {
|
switch (house) {
|
||||||
case 'e':
|
case 'e':
|
||||||
if (!this.state.eHandTiles.includes(tile)) throw new Error('Invalid tile');
|
if (this.canRon('s', tile)) canRonHouses.push('s');
|
||||||
this.state.eHandTiles.splice(this.state.eHandTiles.indexOf(tile), 1);
|
if (this.canRon('w', tile)) canRonHouses.push('w');
|
||||||
this.state.eHoTiles.push(tile);
|
if (this.canRon('n', tile)) canRonHouses.push('n');
|
||||||
break;
|
break;
|
||||||
case 's':
|
case 's':
|
||||||
if (!this.state.sHandTiles.includes(tile)) throw new Error('Invalid tile');
|
if (this.canRon('e', tile)) canRonHouses.push('e');
|
||||||
this.state.sHandTiles.splice(this.state.sHandTiles.indexOf(tile), 1);
|
if (this.canRon('w', tile)) canRonHouses.push('w');
|
||||||
this.state.sHoTiles.push(tile);
|
if (this.canRon('n', tile)) canRonHouses.push('n');
|
||||||
break;
|
break;
|
||||||
case 'w':
|
case 'w':
|
||||||
if (!this.state.wHandTiles.includes(tile)) throw new Error('Invalid tile');
|
if (this.canRon('e', tile)) canRonHouses.push('e');
|
||||||
this.state.wHandTiles.splice(this.state.wHandTiles.indexOf(tile), 1);
|
if (this.canRon('s', tile)) canRonHouses.push('s');
|
||||||
this.state.wHoTiles.push(tile);
|
if (this.canRon('n', tile)) canRonHouses.push('n');
|
||||||
break;
|
break;
|
||||||
case 'n':
|
case 'n':
|
||||||
if (!this.state.nHandTiles.includes(tile)) throw new Error('Invalid tile');
|
if (this.canRon('e', tile)) canRonHouses.push('e');
|
||||||
this.state.nHandTiles.splice(this.state.nHandTiles.indexOf(tile), 1);
|
if (this.canRon('s', tile)) canRonHouses.push('s');
|
||||||
this.state.nHoTiles.push(tile);
|
if (this.canRon('w', tile)) canRonHouses.push('w');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const canKanHouse: House | null = null;
|
||||||
|
|
||||||
let canPonHouse: House | null = null;
|
let canPonHouse: House | null = null;
|
||||||
if (house === 'e') {
|
switch (house) {
|
||||||
|
case 'e':
|
||||||
canPonHouse = this.canPon('s', tile) ? 's' : this.canPon('w', tile) ? 'w' : this.canPon('n', tile) ? 'n' : null;
|
canPonHouse = this.canPon('s', tile) ? 's' : this.canPon('w', tile) ? 'w' : this.canPon('n', tile) ? 'n' : null;
|
||||||
} else if (house === 's') {
|
break;
|
||||||
|
case 's':
|
||||||
canPonHouse = this.canPon('e', tile) ? 'e' : this.canPon('w', tile) ? 'w' : this.canPon('n', tile) ? 'n' : null;
|
canPonHouse = this.canPon('e', tile) ? 'e' : this.canPon('w', tile) ? 'w' : this.canPon('n', tile) ? 'n' : null;
|
||||||
} else if (house === 'w') {
|
break;
|
||||||
|
case 'w':
|
||||||
canPonHouse = this.canPon('e', tile) ? 'e' : this.canPon('s', tile) ? 's' : this.canPon('n', tile) ? 'n' : null;
|
canPonHouse = this.canPon('e', tile) ? 'e' : this.canPon('s', tile) ? 's' : this.canPon('n', tile) ? 'n' : null;
|
||||||
} else if (house === 'n') {
|
break;
|
||||||
|
case 'n':
|
||||||
canPonHouse = this.canPon('e', tile) ? 'e' : this.canPon('s', tile) ? 's' : this.canPon('w', tile) ? 'w' : null;
|
canPonHouse = this.canPon('e', tile) ? 'e' : this.canPon('s', tile) ? 's' : this.canPon('w', tile) ? 'w' : null;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const canCiiHouse: House | null = null;
|
||||||
// TODO
|
// TODO
|
||||||
//let canCii: boolean = false;
|
//let canCii: boolean = false;
|
||||||
//if (house === 'e') {
|
//if (house === 'e') {
|
||||||
|
@ -229,94 +210,235 @@ export class MasterGameEngine {
|
||||||
// canCii = this.state.eHandTiles...
|
// canCii = this.state.eHandTiles...
|
||||||
//}
|
//}
|
||||||
|
|
||||||
if (canPonHouse) {
|
if (canRonHouses.length > 0 || canPonHouse != null) {
|
||||||
|
if (canRonHouses.length > 0) {
|
||||||
|
this.state.ronAsking = {
|
||||||
|
source: house,
|
||||||
|
targets: canRonHouses,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (canKanHouse != null) {
|
||||||
|
this.state.kanAsking = {
|
||||||
|
source: house,
|
||||||
|
target: canKanHouse,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (canPonHouse != null) {
|
||||||
this.state.ponAsking = {
|
this.state.ponAsking = {
|
||||||
source: house,
|
source: house,
|
||||||
target: canPonHouse,
|
target: canPonHouse,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
if (canCiiHouse != null) {
|
||||||
|
this.state.ciiAsking = {
|
||||||
|
source: house,
|
||||||
|
target: canCiiHouse,
|
||||||
|
};
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
|
asking: true,
|
||||||
|
canRonHouses: canRonHouses,
|
||||||
|
canKanHouse: canKanHouse,
|
||||||
canPonHouse: canPonHouse,
|
canPonHouse: canPonHouse,
|
||||||
|
canCiiHouse: canCiiHouse,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
this.state.turn = Common.nextHouse(house);
|
this.state.turn = Utils.nextHouse(house);
|
||||||
|
|
||||||
const tsumoTile = this.ツモ();
|
const tsumoTile = this.tsumo();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tsumo: tsumoTile,
|
asking: false,
|
||||||
|
tsumoTile: tsumoTile,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public op_pon(house: House) {
|
public op_resolveCallAndRonInterruption(answers: {
|
||||||
if (this.state.ponAsking == null) throw new Error('No one is asking for pon');
|
pon: boolean;
|
||||||
if (this.state.ponAsking.target !== house) throw new Error('Not you');
|
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.getKannedTilesOf(target).push({ tile, from: source });
|
||||||
|
|
||||||
|
clearAsking();
|
||||||
|
this.state.turn = target;
|
||||||
|
// TODO
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.ponAsking != null && answers.pon) {
|
||||||
const source = this.state.ponAsking.source;
|
const source = this.state.ponAsking.source;
|
||||||
const target = this.state.ponAsking.target;
|
const target = this.state.ponAsking.target;
|
||||||
this.state.ponAsking = null;
|
|
||||||
|
|
||||||
let tile: Tile;
|
const tile = this.getHoTilesOf(source).pop();
|
||||||
|
this.getPonnedTilesOf(target).push({ tile, from: source });
|
||||||
switch (source) {
|
|
||||||
case 'e':
|
|
||||||
tile = this.state.eHoTiles.pop();
|
|
||||||
break;
|
|
||||||
case 's':
|
|
||||||
tile = this.state.sHoTiles.pop();
|
|
||||||
break;
|
|
||||||
case 'w':
|
|
||||||
tile = this.state.wHoTiles.pop();
|
|
||||||
break;
|
|
||||||
case 'n':
|
|
||||||
tile = this.state.nHoTiles.pop();
|
|
||||||
break;
|
|
||||||
default: throw new Error('Invalid source');
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (target) {
|
|
||||||
case 'e':
|
|
||||||
this.state.ePonnedTiles.push({ tile, from: source });
|
|
||||||
break;
|
|
||||||
case 's':
|
|
||||||
this.state.sPonnedTiles.push({ tile, from: source });
|
|
||||||
break;
|
|
||||||
case 'w':
|
|
||||||
this.state.wPonnedTiles.push({ tile, from: source });
|
|
||||||
break;
|
|
||||||
case 'n':
|
|
||||||
this.state.nPonnedTiles.push({ tile, from: source });
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
clearAsking();
|
||||||
this.state.turn = target;
|
this.state.turn = target;
|
||||||
}
|
|
||||||
|
|
||||||
public op_noOnePon() {
|
|
||||||
if (this.state.ponAsking == null) throw new Error('No one is asking for pon');
|
|
||||||
|
|
||||||
this.state.ponAsking = null;
|
|
||||||
this.state.turn = Common.nextHouse(this.state.turn);
|
|
||||||
|
|
||||||
const tile = this.ツモ();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
type: 'ponned',
|
||||||
house: this.state.turn,
|
house: this.state.turn,
|
||||||
tile,
|
tile,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private canPon(house: House, tile: Tile): boolean {
|
if (this.state.ciiAsking != null && answers.cii) {
|
||||||
switch (house) {
|
const source = this.state.ciiAsking.source;
|
||||||
case 'e':
|
const target = this.state.ciiAsking.target;
|
||||||
return this.state.eHandTiles.filter(t => t === tile).length === 2;
|
|
||||||
case 's':
|
const tile = this.getHoTilesOf(source).pop();
|
||||||
return this.state.sHandTiles.filter(t => t === tile).length === 2;
|
this.getCiiedTilesOf(target).push({ tile, from: source });
|
||||||
case 'w':
|
|
||||||
return this.state.wHandTiles.filter(t => t === tile).length === 2;
|
clearAsking();
|
||||||
case 'n':
|
this.state.turn = target;
|
||||||
return this.state.nHandTiles.filter(t => t === tile).length === 2;
|
return {
|
||||||
|
type: 'ciied',
|
||||||
|
house: this.state.turn,
|
||||||
|
tile,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearAsking();
|
||||||
|
this.state.turn = Utils.nextHouse(this.state.turn);
|
||||||
|
|
||||||
|
const tile = this.tsumo();
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'tsumo',
|
||||||
|
house: this.state.turn,
|
||||||
|
tile,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private canRon(house: House, tile: Tile): boolean {
|
||||||
|
// フリテン
|
||||||
|
// TODO: ポンされるなどして自分の河にない場合の考慮
|
||||||
|
if (this.getHoTilesOf(house).includes(tile)) return false;
|
||||||
|
|
||||||
|
const horaSets = Utils.getHoraSets(this.getHandTilesOf(house).concat(tile));
|
||||||
|
if (horaSets.length === 0) return false; // 完成形じゃない
|
||||||
|
|
||||||
|
const yakus = YAKU_DEFINITIONS.filter(yaku => yaku.calc(this.state, { tsumoTile: null, ronTile: tile }));
|
||||||
|
if (yakus.length === 0) return false; // 役がない
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private canPon(house: House, tile: Tile): boolean {
|
||||||
|
return this.getHandTilesOf(house).filter(t => t === tile).length === 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getHouse(index: 1 | 2 | 3 | 4): House {
|
||||||
|
switch (index) {
|
||||||
|
case 1: return this.state.user1House;
|
||||||
|
case 2: return this.state.user2House;
|
||||||
|
case 3: return this.state.user3House;
|
||||||
|
case 4: return this.state.user4House;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getHandTilesOf(house: House): Tile[] {
|
||||||
|
switch (house) {
|
||||||
|
case 'e': return this.state.eHandTiles;
|
||||||
|
case 's': return this.state.sHandTiles;
|
||||||
|
case 'w': return this.state.wHandTiles;
|
||||||
|
case 'n': return this.state.nHandTiles;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getHoTilesOf(house: House): Tile[] {
|
||||||
|
switch (house) {
|
||||||
|
case 'e': return this.state.eHoTiles;
|
||||||
|
case 's': return this.state.sHoTiles;
|
||||||
|
case 'w': return this.state.wHoTiles;
|
||||||
|
case 'n': return this.state.nHoTiles;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getPonnedTilesOf(house: House): { tile: Tile; from: House; }[] {
|
||||||
|
switch (house) {
|
||||||
|
case 'e': return this.state.ePonnedTiles;
|
||||||
|
case 's': return this.state.sPonnedTiles;
|
||||||
|
case 'w': return this.state.wPonnedTiles;
|
||||||
|
case 'n': return this.state.nPonnedTiles;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCiiedTilesOf(house: House): { tiles: [Tile, Tile, Tile]; from: House; }[] {
|
||||||
|
switch (house) {
|
||||||
|
case 'e': return this.state.eCiiedTiles;
|
||||||
|
case 's': return this.state.sCiiedTiles;
|
||||||
|
case 'w': return this.state.wCiiedTiles;
|
||||||
|
case 'n': return this.state.nCiiedTiles;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getKannedTilesOf(house: House): { tile: Tile; from: House; }[] {
|
||||||
|
switch (house) {
|
||||||
|
case 'e': return this.state.ePonnedTiles;
|
||||||
|
case 's': return this.state.sPonnedTiles;
|
||||||
|
case 'w': return this.state.wPonnedTiles;
|
||||||
|
case 'n': return this.state.nPonnedTiles;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public createPlayerState(index: 1 | 2 | 3 | 4): PlayerState {
|
||||||
|
const house = this.getHouse(index);
|
||||||
|
|
||||||
|
return {
|
||||||
|
user1House: this.state.user1House,
|
||||||
|
user2House: this.state.user2House,
|
||||||
|
user3House: this.state.user3House,
|
||||||
|
user4House: this.state.user4House,
|
||||||
|
tilesCount: this.state.tiles.length,
|
||||||
|
eHandTiles: house === 'e' ? this.state.eHandTiles : this.state.eHandTiles.map(() => null),
|
||||||
|
sHandTiles: house === 's' ? this.state.sHandTiles : this.state.sHandTiles.map(() => null),
|
||||||
|
wHandTiles: house === 'w' ? this.state.wHandTiles : this.state.wHandTiles.map(() => null),
|
||||||
|
nHandTiles: house === 'n' ? this.state.nHandTiles : this.state.nHandTiles.map(() => null),
|
||||||
|
eHoTiles: this.state.eHoTiles,
|
||||||
|
sHoTiles: this.state.sHoTiles,
|
||||||
|
wHoTiles: this.state.wHoTiles,
|
||||||
|
nHoTiles: this.state.nHoTiles,
|
||||||
|
ePonnedTiles: this.state.ePonnedTiles,
|
||||||
|
sPonnedTiles: this.state.sPonnedTiles,
|
||||||
|
wPonnedTiles: this.state.wPonnedTiles,
|
||||||
|
nPonnedTiles: this.state.nPonnedTiles,
|
||||||
|
eCiiedTiles: this.state.eCiiedTiles,
|
||||||
|
sCiiedTiles: this.state.sCiiedTiles,
|
||||||
|
wCiiedTiles: this.state.wCiiedTiles,
|
||||||
|
nCiiedTiles: this.state.nCiiedTiles,
|
||||||
|
eRiichi: this.state.eRiichi,
|
||||||
|
sRiichi: this.state.sRiichi,
|
||||||
|
wRiichi: this.state.wRiichi,
|
||||||
|
nRiichi: this.state.nRiichi,
|
||||||
|
ePoints: this.state.ePoints,
|
||||||
|
sPoints: this.state.sPoints,
|
||||||
|
wPoints: this.state.wPoints,
|
||||||
|
nPoints: this.state.nPoints,
|
||||||
|
latestDahaiedTile: null,
|
||||||
|
turn: this.state.turn,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public calcCrc32ForUser1(): number {
|
public calcCrc32ForUser1(): number {
|
||||||
|
@ -368,6 +490,10 @@ export type PlayerState = {
|
||||||
nPoints: number;
|
nPoints: number;
|
||||||
latestDahaiedTile: Tile | null;
|
latestDahaiedTile: Tile | null;
|
||||||
turn: House | null;
|
turn: House | null;
|
||||||
|
canPonTo: House | null;
|
||||||
|
canCiiTo: House | null;
|
||||||
|
canKanTo: House | null;
|
||||||
|
canRonTo: House | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class PlayerGameEngine {
|
export class PlayerGameEngine {
|
||||||
|
@ -411,113 +537,104 @@ export class PlayerGameEngine {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getHandTilesOf(house: House) {
|
||||||
|
switch (house) {
|
||||||
|
case 'e': return this.state.eHandTiles;
|
||||||
|
case 's': return this.state.sHandTiles;
|
||||||
|
case 'w': return this.state.wHandTiles;
|
||||||
|
case 'n': return this.state.nHandTiles;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getHoTilesOf(house: House): Tile[] {
|
||||||
|
switch (house) {
|
||||||
|
case 'e': return this.state.eHoTiles;
|
||||||
|
case 's': return this.state.sHoTiles;
|
||||||
|
case 'w': return this.state.wHoTiles;
|
||||||
|
case 'n': return this.state.nHoTiles;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getPonnedTilesOf(house: House): { tile: Tile; from: House; }[] {
|
||||||
|
switch (house) {
|
||||||
|
case 'e': return this.state.ePonnedTiles;
|
||||||
|
case 's': return this.state.sPonnedTiles;
|
||||||
|
case 'w': return this.state.wPonnedTiles;
|
||||||
|
case 'n': return this.state.nPonnedTiles;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCiiedTilesOf(house: House): { tiles: [Tile, Tile, Tile]; from: House; }[] {
|
||||||
|
switch (house) {
|
||||||
|
case 'e': return this.state.eCiiedTiles;
|
||||||
|
case 's': return this.state.sCiiedTiles;
|
||||||
|
case 'w': return this.state.wCiiedTiles;
|
||||||
|
case 'n': return this.state.nCiiedTiles;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getKannedTilesOf(house: House): { tile: Tile; from: House; }[] {
|
||||||
|
switch (house) {
|
||||||
|
case 'e': return this.state.ePonnedTiles;
|
||||||
|
case 's': return this.state.sPonnedTiles;
|
||||||
|
case 'w': return this.state.wPonnedTiles;
|
||||||
|
case 'n': return this.state.nPonnedTiles;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public op_tsumo(house: House, tile: Tile) {
|
public op_tsumo(house: House, tile: Tile) {
|
||||||
if (house === this.myHouse) {
|
if (house === this.myHouse) {
|
||||||
this.myHandTiles.push(tile);
|
this.myHandTiles.push(tile);
|
||||||
} else {
|
} else {
|
||||||
switch (house) {
|
this.getHandTilesOf(house).push(null);
|
||||||
case 'e':
|
|
||||||
this.state.eHandTiles.push(null);
|
|
||||||
break;
|
|
||||||
case 's':
|
|
||||||
this.state.sHandTiles.push(null);
|
|
||||||
break;
|
|
||||||
case 'w':
|
|
||||||
this.state.wHandTiles.push(null);
|
|
||||||
break;
|
|
||||||
case 'n':
|
|
||||||
this.state.nHandTiles.push(null);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public op_dahai(house: House, tile: Tile) {
|
public op_dahai(house: House, tile: Tile) {
|
||||||
|
console.log(this.state.turn, house, tile);
|
||||||
|
|
||||||
if (this.state.turn !== house) throw new PlayerGameEngine.InvalidOperationError();
|
if (this.state.turn !== house) throw new PlayerGameEngine.InvalidOperationError();
|
||||||
|
|
||||||
if (house === this.myHouse) {
|
if (house === this.myHouse) {
|
||||||
this.myHandTiles.splice(this.myHandTiles.indexOf(tile), 1);
|
this.myHandTiles.splice(this.myHandTiles.indexOf(tile), 1);
|
||||||
this.myHoTiles.push(tile);
|
this.myHoTiles.push(tile);
|
||||||
} else {
|
} else {
|
||||||
switch (house) {
|
this.getHandTilesOf(house).pop();
|
||||||
case 'e':
|
this.getHoTilesOf(house).push(tile);
|
||||||
this.state.eHandTiles.pop();
|
|
||||||
this.state.eHoTiles.push(tile);
|
|
||||||
break;
|
|
||||||
case 's':
|
|
||||||
this.state.sHandTiles.pop();
|
|
||||||
this.state.sHoTiles.push(tile);
|
|
||||||
break;
|
|
||||||
case 'w':
|
|
||||||
this.state.wHandTiles.pop();
|
|
||||||
this.state.wHoTiles.push(tile);
|
|
||||||
break;
|
|
||||||
case 'n':
|
|
||||||
this.state.nHandTiles.pop();
|
|
||||||
this.state.nHoTiles.push(tile);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.state.turn = Utils.nextHouse(this.state.turn);
|
||||||
|
|
||||||
if (house === this.myHouse) {
|
if (house === this.myHouse) {
|
||||||
this.state.turn = null;
|
|
||||||
} else {
|
} else {
|
||||||
const canPon = this.myHandTiles.filter(t => t === tile).length === 2;
|
const canPon = this.myHandTiles.filter(t => t === tile).length === 2;
|
||||||
|
|
||||||
// TODO: canCii
|
// TODO: canCii
|
||||||
|
|
||||||
return {
|
if (canPon) this.state.canPonTo = house;
|
||||||
canPon,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public op_pon(source: House, target: House) {
|
public op_pon(source: House, target: House) {
|
||||||
let tile: Tile;
|
this.state.canPonTo = null;
|
||||||
|
|
||||||
switch (source) {
|
const lastTile = this.getHoTilesOf(source).pop();
|
||||||
case 'e': {
|
|
||||||
const lastTile = this.state.eHoTiles.pop();
|
|
||||||
if (lastTile == null) throw new PlayerGameEngine.InvalidOperationError();
|
if (lastTile == null) throw new PlayerGameEngine.InvalidOperationError();
|
||||||
tile = lastTile;
|
this.getPonnedTilesOf(target).push({ tile: lastTile, from: source });
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 's': {
|
|
||||||
const lastTile = this.state.sHoTiles.pop();
|
|
||||||
if (lastTile == null) throw new PlayerGameEngine.InvalidOperationError();
|
|
||||||
tile = lastTile;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'w': {
|
|
||||||
const lastTile = this.state.wHoTiles.pop();
|
|
||||||
if (lastTile == null) throw new PlayerGameEngine.InvalidOperationError();
|
|
||||||
tile = lastTile;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'n': {
|
|
||||||
const lastTile = this.state.nHoTiles.pop();
|
|
||||||
if (lastTile == null) throw new PlayerGameEngine.InvalidOperationError();
|
|
||||||
tile = lastTile;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: throw new Error('Invalid source');
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (target) {
|
|
||||||
case 'e':
|
|
||||||
this.state.ePonnedTiles.push({ tile, from: source });
|
|
||||||
break;
|
|
||||||
case 's':
|
|
||||||
this.state.sPonnedTiles.push({ tile, from: source });
|
|
||||||
break;
|
|
||||||
case 'w':
|
|
||||||
this.state.wPonnedTiles.push({ tile, from: source });
|
|
||||||
break;
|
|
||||||
case 'n':
|
|
||||||
this.state.nPonnedTiles.push({ tile, from: source });
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.state.turn = target;
|
this.state.turn = target;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public op_nop() {
|
||||||
|
this.state.canPonTo = null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const YAKU_DEFINITIONS = [{
|
||||||
|
name: 'riichi',
|
||||||
|
fan: 1,
|
||||||
|
calc: (state: PlayerState, ctx: { tsumoTile: Tile; ronTile: Tile; }) => {
|
||||||
|
const house = state.turn;
|
||||||
|
return house === 'e' ? state.eRiichi : house === 's' ? state.sRiichi : house === 'w' ? state.wRiichi : state.nRiichi;
|
||||||
|
},
|
||||||
|
}];
|
||||||
|
|
|
@ -5,3 +5,5 @@
|
||||||
|
|
||||||
export * as Engine from './engine.js';
|
export * as Engine from './engine.js';
|
||||||
export * as Serializer from './serializer.js';
|
export * as Serializer from './serializer.js';
|
||||||
|
export * as Common from './common.js';
|
||||||
|
export * as Utils from './utils.js';
|
||||||
|
|
|
@ -15,40 +15,40 @@ export type Log = {
|
||||||
export type SerializedLog = number[];
|
export type SerializedLog = number[];
|
||||||
|
|
||||||
export const TILE_MAP: Record<Tile, number> = {
|
export const TILE_MAP: Record<Tile, number> = {
|
||||||
'bamboo1': 1,
|
'm1': 1,
|
||||||
'bamboo2': 2,
|
'm2': 2,
|
||||||
'bamboo3': 3,
|
'm3': 3,
|
||||||
'bamboo4': 4,
|
'm4': 4,
|
||||||
'bamboo5': 5,
|
'm5': 5,
|
||||||
'bamboo6': 6,
|
'm6': 6,
|
||||||
'bamboo7': 7,
|
'm7': 7,
|
||||||
'bamboo8': 8,
|
'm8': 8,
|
||||||
'bamboo9': 9,
|
'm9': 9,
|
||||||
'character1': 10,
|
'p1': 10,
|
||||||
'character2': 11,
|
'p2': 11,
|
||||||
'character3': 12,
|
'p3': 12,
|
||||||
'character4': 13,
|
'p4': 13,
|
||||||
'character5': 14,
|
'p5': 14,
|
||||||
'character6': 15,
|
'p6': 15,
|
||||||
'character7': 16,
|
'p7': 16,
|
||||||
'character8': 17,
|
'p8': 17,
|
||||||
'character9': 18,
|
'p9': 18,
|
||||||
'circle1': 19,
|
's1': 19,
|
||||||
'circle2': 20,
|
's2': 20,
|
||||||
'circle3': 21,
|
's3': 21,
|
||||||
'circle4': 22,
|
's4': 22,
|
||||||
'circle5': 23,
|
's5': 23,
|
||||||
'circle6': 24,
|
's6': 24,
|
||||||
'circle7': 25,
|
's7': 25,
|
||||||
'circle8': 26,
|
's8': 26,
|
||||||
'circle9': 27,
|
's9': 27,
|
||||||
'wind-east': 28,
|
'e': 28,
|
||||||
'wind-south': 29,
|
's': 29,
|
||||||
'wind-west': 30,
|
'w': 30,
|
||||||
'wind-north': 31,
|
'n': 31,
|
||||||
'dragon-red': 32,
|
'haku': 32,
|
||||||
'dragon-green': 33,
|
'hatsu': 33,
|
||||||
'dragon-white': 34,
|
'chun': 34,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function serializeTile(tile: Tile): number {
|
export function serializeTile(tile: Tile): number {
|
||||||
|
|
235
packages/misskey-mahjong/src/utils.ts
Normal file
235
packages/misskey-mahjong/src/utils.ts
Normal file
|
@ -0,0 +1,235 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { House, TILE_TYPES, Tile } from './common.js';
|
||||||
|
|
||||||
|
export function isTile(tile: string): tile is Tile {
|
||||||
|
return TILE_TYPES.includes(tile as Tile);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sortTiles(tiles: Tile[]): Tile[] {
|
||||||
|
tiles.sort((a, b) => {
|
||||||
|
const aIndex = TILE_TYPES.indexOf(a);
|
||||||
|
const bIndex = TILE_TYPES.indexOf(b);
|
||||||
|
return aIndex - bIndex;
|
||||||
|
});
|
||||||
|
return tiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function nextHouse(house: House): House {
|
||||||
|
switch (house) {
|
||||||
|
case 'e': return 's';
|
||||||
|
case 's': return 'w';
|
||||||
|
case 'w': return 'n';
|
||||||
|
case 'n': return 'e';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function prevHouse(house: House): House {
|
||||||
|
switch (house) {
|
||||||
|
case 'e': return 'n';
|
||||||
|
case 's': return 'e';
|
||||||
|
case 'w': return 's';
|
||||||
|
case 'n': return 'w';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type HoraSet = {
|
||||||
|
head: Tile;
|
||||||
|
mentsus: [Tile, Tile, Tile][];
|
||||||
|
};
|
||||||
|
|
||||||
|
const SHUNTU_PATTERNS: [Tile, Tile, Tile][] = [
|
||||||
|
['m1', 'm2', 'm3'],
|
||||||
|
['m2', 'm3', 'm4'],
|
||||||
|
['m3', 'm4', 'm5'],
|
||||||
|
['m4', 'm5', 'm6'],
|
||||||
|
['m5', 'm6', 'm7'],
|
||||||
|
['m6', 'm7', 'm8'],
|
||||||
|
['m7', 'm8', 'm9'],
|
||||||
|
['p1', 'p2', 'p3'],
|
||||||
|
['p2', 'p3', 'p4'],
|
||||||
|
['p3', 'p4', 'p5'],
|
||||||
|
['p4', 'p5', 'p6'],
|
||||||
|
['p5', 'p6', 'p7'],
|
||||||
|
['p6', 'p7', 'p8'],
|
||||||
|
['p7', 'p8', 'p9'],
|
||||||
|
['s1', 's2', 's3'],
|
||||||
|
['s2', 's3', 's4'],
|
||||||
|
['s3', 's4', 's5'],
|
||||||
|
['s4', 's5', 's6'],
|
||||||
|
['s5', 's6', 's7'],
|
||||||
|
['s6', 's7', 's8'],
|
||||||
|
['s7', 's8', 's9'],
|
||||||
|
];
|
||||||
|
|
||||||
|
const SHUNTU_PATTERN_IDS = [
|
||||||
|
'm123',
|
||||||
|
'm234',
|
||||||
|
'm345',
|
||||||
|
'm456',
|
||||||
|
'm567',
|
||||||
|
'm678',
|
||||||
|
'm789',
|
||||||
|
'p123',
|
||||||
|
'p234',
|
||||||
|
'p345',
|
||||||
|
'p456',
|
||||||
|
'p567',
|
||||||
|
'p678',
|
||||||
|
'p789',
|
||||||
|
's123',
|
||||||
|
's234',
|
||||||
|
's345',
|
||||||
|
's456',
|
||||||
|
's567',
|
||||||
|
's678',
|
||||||
|
's789',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* アガリ形パターン一覧を取得
|
||||||
|
* @param handTiles ポン、チー、カンした牌を含まない手牌
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function getHoraSets(handTiles: Tile[]): HoraSet[] {
|
||||||
|
const horaSets: HoraSet[] = [];
|
||||||
|
|
||||||
|
const headSet: Tile[] = [];
|
||||||
|
const countMap = new Map<Tile, number>();
|
||||||
|
for (const tile of handTiles) {
|
||||||
|
const count = (countMap.get(tile) ?? 0) + 1;
|
||||||
|
countMap.set(tile, count);
|
||||||
|
if (count === 2) {
|
||||||
|
headSet.push(tile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const head of headSet) {
|
||||||
|
const tempHandTiles = [...handTiles];
|
||||||
|
tempHandTiles.splice(tempHandTiles.indexOf(head), 1);
|
||||||
|
tempHandTiles.splice(tempHandTiles.indexOf(head), 1);
|
||||||
|
|
||||||
|
const kotsuTileSet: Tile[] = []; // インデックスアクセスしたいため配列だが実態はSet
|
||||||
|
for (const [t, c] of countMap.entries()) {
|
||||||
|
if (t === head) continue; // 同じ牌種は4枚しかないので、頭と同じ牌種は刻子になりえない
|
||||||
|
if (c >= 3) {
|
||||||
|
kotsuTileSet.push(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let kotsuPatterns: Tile[][];
|
||||||
|
if (kotsuTileSet.length === 0) {
|
||||||
|
kotsuPatterns = [
|
||||||
|
[],
|
||||||
|
];
|
||||||
|
} else if (kotsuTileSet.length === 1) {
|
||||||
|
kotsuPatterns = [
|
||||||
|
[],
|
||||||
|
[kotsuTileSet[0]],
|
||||||
|
];
|
||||||
|
} else if (kotsuTileSet.length === 2) {
|
||||||
|
kotsuPatterns = [
|
||||||
|
[],
|
||||||
|
[kotsuTileSet[0]],
|
||||||
|
[kotsuTileSet[1]],
|
||||||
|
[kotsuTileSet[0], kotsuTileSet[1]],
|
||||||
|
];
|
||||||
|
} else if (kotsuTileSet.length === 3) {
|
||||||
|
kotsuPatterns = [
|
||||||
|
[],
|
||||||
|
[kotsuTileSet[0]],
|
||||||
|
[kotsuTileSet[1]],
|
||||||
|
[kotsuTileSet[2]],
|
||||||
|
[kotsuTileSet[0], kotsuTileSet[1]],
|
||||||
|
[kotsuTileSet[0], kotsuTileSet[2]],
|
||||||
|
[kotsuTileSet[1], kotsuTileSet[2]],
|
||||||
|
[kotsuTileSet[0], kotsuTileSet[1], kotsuTileSet[2]],
|
||||||
|
];
|
||||||
|
} else if (kotsuTileSet.length === 4) {
|
||||||
|
kotsuPatterns = [
|
||||||
|
[],
|
||||||
|
[kotsuTileSet[0]],
|
||||||
|
[kotsuTileSet[1]],
|
||||||
|
[kotsuTileSet[2]],
|
||||||
|
[kotsuTileSet[3]],
|
||||||
|
[kotsuTileSet[0], kotsuTileSet[1]],
|
||||||
|
[kotsuTileSet[0], kotsuTileSet[2]],
|
||||||
|
[kotsuTileSet[0], kotsuTileSet[3]],
|
||||||
|
[kotsuTileSet[1], kotsuTileSet[2]],
|
||||||
|
[kotsuTileSet[1], kotsuTileSet[3]],
|
||||||
|
[kotsuTileSet[2], kotsuTileSet[3]],
|
||||||
|
[kotsuTileSet[0], kotsuTileSet[1], kotsuTileSet[2]],
|
||||||
|
[kotsuTileSet[0], kotsuTileSet[1], kotsuTileSet[3]],
|
||||||
|
[kotsuTileSet[0], kotsuTileSet[2], kotsuTileSet[3]],
|
||||||
|
[kotsuTileSet[1], kotsuTileSet[2], kotsuTileSet[3]],
|
||||||
|
[kotsuTileSet[0], kotsuTileSet[1], kotsuTileSet[2], kotsuTileSet[3]],
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
throw new Error('arienai');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const kotsuPattern of kotsuPatterns) {
|
||||||
|
const tempHandTilesWithoutKotsu = [...tempHandTiles];
|
||||||
|
for (const kotsuTile of kotsuPattern) {
|
||||||
|
tempHandTilesWithoutKotsu.splice(tempHandTilesWithoutKotsu.indexOf(kotsuTile), 1);
|
||||||
|
tempHandTilesWithoutKotsu.splice(tempHandTilesWithoutKotsu.indexOf(kotsuTile), 1);
|
||||||
|
tempHandTilesWithoutKotsu.splice(tempHandTilesWithoutKotsu.indexOf(kotsuTile), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 連番に並ぶようにソート
|
||||||
|
tempHandTilesWithoutKotsu.sort((a, b) => {
|
||||||
|
const aIndex = TILE_TYPES.indexOf(a);
|
||||||
|
const bIndex = TILE_TYPES.indexOf(b);
|
||||||
|
return aIndex - bIndex;
|
||||||
|
});
|
||||||
|
|
||||||
|
const tempTempHandTilesWithoutKotsuAndShuntsu: (Tile | null)[] = [...tempHandTilesWithoutKotsu];
|
||||||
|
|
||||||
|
const shuntsus: [Tile, Tile, Tile][] = [];
|
||||||
|
let i = 0;
|
||||||
|
while (i < tempHandTilesWithoutKotsu.length) {
|
||||||
|
const headThree = tempHandTilesWithoutKotsu.slice(i, i + 3);
|
||||||
|
if (headThree.length !== 3) break;
|
||||||
|
|
||||||
|
for (const shuntuPattern of SHUNTU_PATTERNS) {
|
||||||
|
if (headThree[0] === shuntuPattern[0] && headThree[1] === shuntuPattern[1] && headThree[2] === shuntuPattern[2]) {
|
||||||
|
shuntsus.push(shuntuPattern);
|
||||||
|
tempTempHandTilesWithoutKotsuAndShuntsu[i] = null;
|
||||||
|
tempTempHandTilesWithoutKotsuAndShuntsu[i + 1] = null;
|
||||||
|
tempTempHandTilesWithoutKotsuAndShuntsu[i + 2] = null;
|
||||||
|
i += 3;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tempHandTilesWithoutKotsuAndShuntsu = tempTempHandTilesWithoutKotsuAndShuntsu.filter(t => t != null) as Tile[];
|
||||||
|
|
||||||
|
if (tempHandTilesWithoutKotsuAndShuntsu.length === 0) { // アガリ形
|
||||||
|
horaSets.push({
|
||||||
|
head,
|
||||||
|
mentsus: [...kotsuPattern.map(t => [t, t, t] as [Tile, Tile, Tile]), ...shuntsus],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return horaSets;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* アガリ牌リストを取得
|
||||||
|
* @param handTiles ポン、チー、カンした牌を含まない手牌
|
||||||
|
*/
|
||||||
|
export function getHoraTiles(handTiles: Tile[]): Tile[] {
|
||||||
|
return TILE_TYPES.filter(tile => {
|
||||||
|
const tempHandTiles = [...handTiles, tile];
|
||||||
|
const horaSets = getHoraSets(tempHandTiles);
|
||||||
|
return horaSets.length > 0;
|
||||||
|
});
|
||||||
|
}
|
|
@ -46,7 +46,7 @@ await execa('pnpm', ['--filter', 'misskey-bubble-game', 'build:tsc'], {
|
||||||
stderr: process.stderr,
|
stderr: process.stderr,
|
||||||
});
|
});
|
||||||
|
|
||||||
await execa('pnpm', ['--filter', 'misskey-mahjong', 'build'], {
|
await execa('pnpm', ['--filter', 'misskey-mahjong', 'build:tsc'], {
|
||||||
cwd: _dirname + '/../',
|
cwd: _dirname + '/../',
|
||||||
stdout: process.stdout,
|
stdout: process.stdout,
|
||||||
stderr: process.stderr,
|
stderr: process.stderr,
|
||||||
|
|
Loading…
Reference in a new issue