mirror of
https://github.com/misskey-dev/misskey.git
synced 2025-01-15 01:31:14 +09:00
wip
This commit is contained in:
parent
0111b8736a
commit
dad8430040
@ -195,6 +195,10 @@ export interface ReversiGameEventTypes {
|
||||
}
|
||||
|
||||
export interface MahjongRoomEventTypes {
|
||||
joined: {
|
||||
index: number;
|
||||
user: Packed<'UserLite'>;
|
||||
};
|
||||
changeReadyStates: {
|
||||
user1: boolean;
|
||||
user2: boolean;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
BIN
packages/frontend/assets/mahjong/ron.png
Normal file
BIN
packages/frontend/assets/mahjong/ron.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
@ -7,19 +7,19 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div :class="$style.root">
|
||||
<div :class="$style.taku">
|
||||
<div :class="$style.handTilesOfToimen">
|
||||
<div v-for="tile in engine.getHandTilesOf(Mahjong.Utils.prevHouse(Mahjong.Utils.prevHouse(engine.myHouse)))" style="display: inline-block;">
|
||||
<div v-for="tile in engine.state.handTiles[Mahjong.Utils.prevHouse(Mahjong.Utils.prevHouse(engine.myHouse))]" style="display: inline-block;">
|
||||
<img :src="`/client-assets/mahjong/tile-back.png`" style="display: inline-block; width: 32px;"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="$style.handTilesOfKamitya">
|
||||
<div v-for="tile in engine.getHandTilesOf(Mahjong.Utils.prevHouse(engine.myHouse))" :class="$style.sideTile">
|
||||
<div v-for="tile in engine.state.handTiles[Mahjong.Utils.prevHouse(engine.myHouse)]" :class="$style.sideTile">
|
||||
<img :src="`/client-assets/mahjong/tile-side.png`" style="display: inline-block; width: 32px;"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="$style.handTilesOfSimotya">
|
||||
<div v-for="tile in engine.getHandTilesOf(Mahjong.Utils.nextHouse(engine.myHouse))" :class="$style.sideTile">
|
||||
<div v-for="tile in engine.state.handTiles[Mahjong.Utils.nextHouse(engine.myHouse)]" :class="$style.sideTile">
|
||||
<img :src="`/client-assets/mahjong/tile-side.png`" style="display: inline-block; width: 32px; scale: -1 1;"/>
|
||||
</div>
|
||||
</div>
|
||||
@ -27,28 +27,28 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div :class="$style.hoTilesContainer">
|
||||
<div :class="$style.hoTilesContainerOfToimen">
|
||||
<div :class="$style.hoTilesOfToimen">
|
||||
<div v-for="tile in engine.getHoTilesOf(Mahjong.Utils.prevHouse(Mahjong.Utils.prevHouse(engine.myHouse)))" :class="$style.hoTile">
|
||||
<div v-for="tile in engine.state.hoTiles[Mahjong.Utils.prevHouse(Mahjong.Utils.prevHouse(engine.myHouse))]" :class="$style.hoTile">
|
||||
<XTile :tile="tile" direction="v"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.hoTilesContainerOfKamitya">
|
||||
<div :class="$style.hoTilesOfKamitya">
|
||||
<div v-for="tile in engine.getHoTilesOf(Mahjong.Utils.prevHouse(engine.myHouse))" :class="$style.hoTile">
|
||||
<div v-for="tile in engine.state.hoTiles[Mahjong.Utils.prevHouse(engine.myHouse)]" :class="$style.hoTile">
|
||||
<XTile :tile="tile" direction="v"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.hoTilesContainerOfSimotya">
|
||||
<div :class="$style.hoTilesOfSimotya">
|
||||
<div v-for="tile in engine.getHoTilesOf(Mahjong.Utils.nextHouse(engine.myHouse))" :class="$style.hoTile">
|
||||
<div v-for="tile in engine.state.hoTiles[Mahjong.Utils.nextHouse(engine.myHouse)]" :class="$style.hoTile">
|
||||
<XTile :tile="tile" direction="v"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.hoTilesContainerOfMe">
|
||||
<div :class="$style.hoTilesOfMe">
|
||||
<div v-for="tile in engine.myHoTiles" :class="$style.hoTile">
|
||||
<div v-for="tile in engine.state.hoTiles[engine.myHouse]" :class="$style.hoTile">
|
||||
<XTile :tile="tile" direction="v"/>
|
||||
</div>
|
||||
</div>
|
||||
@ -67,7 +67,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
|
||||
<div :class="$style.huroTilesOfMe">
|
||||
<div v-for="huro in engine.getHurosOf(engine.myHouse)" style="display: inline-block;">
|
||||
<div v-for="huro in engine.state.huros[engine.myHouse]" style="display: inline-block;">
|
||||
<div v-if="huro.type === 'pon'">
|
||||
<XTile :tile="huro.tile" direction="v"/>
|
||||
<XTile :tile="huro.tile" direction="v"/>
|
||||
@ -111,7 +111,7 @@ const props = defineProps<{
|
||||
|
||||
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 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(() => {
|
||||
|
@ -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)
|
||||
);
|
||||
},
|
||||
}];
|
||||
|
455
packages/misskey-mahjong/src/engine.master.ts
Normal file
455
packages/misskey-mahjong/src/engine.master.ts
Normal file
@ -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
|
||||
}
|
||||
}
|
177
packages/misskey-mahjong/src/engine.player.ts
Normal file
177
packages/misskey-mahjong/src/engine.player.ts
Normal file
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
},
|
||||
}];
|
@ -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';
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user