This commit is contained in:
syuilo 2024-01-28 13:49:56 +09:00
parent ab404d491d
commit 55629f2b39
43 changed files with 475 additions and 226 deletions

View file

@ -292,10 +292,10 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit {
await this.saveRoom(room); await this.saveRoom(room);
if (res.type === 'tsumo') { if (res.type === 'tsumo') {
this.globalEventService.publishMahjongRoomStream(room.id, 'log', { operation: 'tsumo', house: res.house, tile: res.tile }); this.globalEventService.publishMahjongRoomStream(room.id, 'tsumo', { house: res.house, tile: res.tile });
this.next(room, engine); this.next(room, engine);
} else if (res.type === 'ponned') { } else if (res.type === 'ponned') {
this.globalEventService.publishMahjongRoomStream(room.id, 'log', { operation: 'ponned', house: res.house, tile: res.tile }); this.globalEventService.publishMahjongRoomStream(room.id, 'ponned', { source: res.source, target: res.target, tile: res.tile });
const userId = engine.state.user1House === engine.state.turn ? room.user1Id : engine.state.user2House === engine.state.turn ? room.user2Id : engine.state.user3House === engine.state.turn ? room.user3Id : room.user4Id; 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); this.waitForDahai(room, userId, engine);
} else if (res.type === 'kanned') { } else if (res.type === 'kanned') {
@ -314,7 +314,7 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit {
const house = engine.state.turn; 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; 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, handTiles.at(-1));
}, 1000); }, 500);
return; return;
} else { } 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; 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;
@ -323,7 +323,7 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit {
} }
@bindThis @bindThis
private async dahai(room: Room, engine: Mahjong.Engine.MasterGameEngine, house: Mahjong.Common.House, tile: Mahjong.Common.Tile, operationId?: string) { private async dahai(room: Room, engine: Mahjong.Engine.MasterGameEngine, house: Mahjong.Common.House, tile: Mahjong.Common.Tile) {
const res = engine.op_dahai(house, tile); const res = engine.op_dahai(house, tile);
room.gameState = engine.state; room.gameState = engine.state;
await this.saveRoom(room); await this.saveRoom(room);
@ -346,17 +346,23 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit {
}; };
if (aiHouses.includes(res.canPonHouse)) { if (aiHouses.includes(res.canPonHouse)) {
// TODO // TODO: ちゃんと思考するようにする
//answers.pon = Math.random() < 0.25;
answers.pon = false;
} }
if (aiHouses.includes(res.canChiHouse)) { if (aiHouses.includes(res.canCiiHouse)) {
// TODO // TODO: ちゃんと思考するようにする
//answers.cii = Math.random() < 0.25;
answers.cii = false;
} }
if (aiHouses.includes(res.canKanHouse)) { if (aiHouses.includes(res.canKanHouse)) {
// TODO // TODO: ちゃんと思考するようにする
//answers.kan = Math.random() < 0.25;
answers.kan = false;
} }
for (const h of res.canRonHouses) { for (const h of res.canRonHouses) {
if (aiHouses.includes(h)) { if (aiHouses.includes(h)) {
// TODO // TODO: ちゃんと思考するようにする
} }
} }
@ -376,24 +382,24 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit {
(res.canRonHouses.includes('n') && currentAnswers.ron.n == null) (res.canRonHouses.includes('n') && currentAnswers.ron.n == null)
); );
if (allAnswered || (Date.now() - waitingStartedAt > CALL_AND_RON_ASKING_TIMEOUT_MS)) { if (allAnswered || (Date.now() - waitingStartedAt > CALL_AND_RON_ASKING_TIMEOUT_MS)) {
console.log('answerd'); console.log(allAnswered ? 'ask all answerd' : 'ask timeout');
await this.redisClient.del(`mahjong:gamePonAsking:${room.id}`); await this.redisClient.del(`mahjong:gameCallAndRonAsking:${room.id}`);
clearInterval(interval); clearInterval(interval);
this.answer(room, engine, currentAnswers); this.answer(room, engine, currentAnswers);
return; return;
} }
}, 2000); }, 2000);
this.globalEventService.publishMahjongRoomStream(room.id, 'log', { operation: 'dahai', house: house, tile, id: operationId }); this.globalEventService.publishMahjongRoomStream(room.id, 'dahai', { house: house, tile });
} else { } else {
this.globalEventService.publishMahjongRoomStream(room.id, 'log', { operation: 'dahaiAndTsumo', house: house, dahaiTile: tile, tsumoTile: res.tsumoTile, id: operationId }); this.globalEventService.publishMahjongRoomStream(room.id, 'dahaiAndTsumo', { dahaiHouse: house, dahaiTile: tile, tsumoTile: res.tsumoTile });
this.next(room, engine); this.next(room, engine);
} }
} }
@bindThis @bindThis
public async op_dahai(roomId: MiMahjongGame['id'], user: MiUser, tile: string, operationId: string) { public async op_dahai(roomId: MiMahjongGame['id'], user: MiUser, tile: 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;
@ -403,7 +409,7 @@ 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;
await this.dahai(room, engine, myHouse, tile, operationId); await this.dahai(room, engine, myHouse, tile);
} }
@bindThis @bindThis
@ -462,7 +468,7 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit {
console.log('dahai timeout', userId, id); console.log('dahai timeout', userId, id);
clearInterval(interval); 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 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 = engine.getHandTilesOf(house);
await this.dahai(room, engine, house, handTiles.at(-1)); await this.dahai(room, engine, house, handTiles.at(-1));
return; return;
} }

View file

@ -38,7 +38,7 @@ 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 'dahai': this.dahai(body.tile, body.id); break; case 'dahai': this.dahai(body.tile); break;
case 'pon': this.pon(); break; case 'pon': this.pon(); break;
case 'nop': this.nop(); break; case 'nop': this.nop(); break;
case 'claimTimeIsUp': this.claimTimeIsUp(); break; case 'claimTimeIsUp': this.claimTimeIsUp(); break;
@ -67,10 +67,10 @@ class MahjongRoomChannel extends Channel {
} }
@bindThis @bindThis
private async dahai(tile: string, id: string) { private async dahai(tile: string) {
if (this.user == null) return; if (this.user == null) return;
this.mahjongService.op_dahai(this.roomId!, this.user, tile, id); this.mahjongService.op_dahai(this.roomId!, this.user, tile);
} }
@bindThis @bindThis

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View file

@ -4,47 +4,82 @@ SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <template>
<MkSpacer :contentMax="500"> <div :class="$style.root">
<div class="_gaps"> <div :class="$style.taku">
<div> <div :class="$style.handTilesOfToimen">
{{ engine.myHouse }} {{ engine.state.turn }} <div v-for="tile in engine.getHandTilesOf(Mahjong.Utils.prevHouse(Mahjong.Utils.prevHouse(engine.myHouse)))" style="display: inline-block;">
</div> <img :src="`/client-assets/mahjong/tile-back.png`" style="display: inline-block; width: 32px;"/>
<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> </div>
<div class="_panel"> <div :class="$style.handTilesOfKamitya">
<div>My hand</div> <div v-for="tile in engine.getHandTilesOf(Mahjong.Utils.prevHouse(engine.myHouse))" :class="$style.sideTile">
<div v-for="tile in Mahjong.Utils.sortTiles(engine.myHandTiles)" style="display: inline-block;" @click="dahai(tile, $event)"> <img :src="`/client-assets/mahjong/tile-side.png`" style="display: inline-block; width: 32px;"/>
<img :src="`/client-assets/mahjong/tiles/${tile}.gif`"/>
</div> </div>
<MkButton v-if="engine.state.canPon" @click="pon">Pon</MkButton> </div>
<MkButton v-if="engine.state.canPon" @click="skip">Skip pon</MkButton>
<div :class="$style.handTilesOfSimotya">
<div v-for="tile in engine.getHandTilesOf(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>
<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">
<img :src="`/client-assets/mahjong/tiles/${tile}.png`" style="position: absolute; width: 100%;"/>
</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">
<img :src="`/client-assets/mahjong/tiles/${tile}.png`" style="position: absolute; width: 100%;"/>
</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">
<img :src="`/client-assets/mahjong/tiles/${tile}.png`" style="position: absolute; width: 100%;"/>
</div>
</div>
</div>
<div :class="$style.hoTilesContainerOfMe">
<div :class="$style.hoTilesOfMe">
<div v-for="tile in engine.myHoTiles" :class="$style.hoTile">
<img :src="`/client-assets/mahjong/tiles/${tile}.png`" style="position: absolute; width: 100%;"/>
</div>
</div>
</div>
</div>
<div :class="$style.handTilesOfMe">
<div v-for="tile in Mahjong.Utils.sortTiles((isMyTurn && iTsumoed) ? engine.myHandTiles.slice(0, engine.myHandTiles.length - 1) : engine.myHandTiles)" :class="$style.myTile" @click="dahai(tile, $event)">
<img :src="`/client-assets/mahjong/tile-front.png`" :class="$style.myTileBg"/>
<img :src="`/client-assets/mahjong/tiles/${tile}.png`" :class="$style.myTileFg"/>
</div>
<div v-if="isMyTurn && iTsumoed" style="display: inline-block; margin-left: 5px;" :class="$style.myTile" @click="dahai(engine.myHandTiles.at(-1), $event)">
<img :src="`/client-assets/mahjong/tile-front.png`" :class="$style.myTileBg"/>
<img :src="`/client-assets/mahjong/tiles/${engine.myHandTiles.at(-1)}.png`" :class="$style.myTileFg"/>
</div>
</div>
<div :class="$style.huroTilesOfMe">
<div v-for="huro in engine.getHurosOf(engine.myHouse)" style="display: inline-block;">
<div v-if="huro.type === 'pon'">
<img :src="`/client-assets/mahjong/tiles/${huro.tile}.png`"/>
<img :src="`/client-assets/mahjong/tiles/${huro.tile}.png`"/>
<img :src="`/client-assets/mahjong/tiles/${huro.tile}.png`"/>
</div>
</div>
</div>
</div>
<MkButton v-if="engine.state.canPonSource != null" @click="pon">Pon</MkButton>
<MkButton v-if="engine.state.canPonSource != null" @click="skip">Skip pon</MkButton>
<MkButton v-if="isMyTurn && canHora">Hora</MkButton> <MkButton v-if="isMyTurn && canHora">Hora</MkButton>
</div> </div>
</div>
</MkSpacer>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -83,6 +118,25 @@ const canHora = computed(() => {
return Mahjong.Utils.getHoraSets(engine.value.myHandTiles).length > 0; return Mahjong.Utils.getHoraSets(engine.value.myHandTiles).length > 0;
}); });
/*
console.log(Mahjong.Utils.getHoraSets([
'm3',
'm3',
'm4',
'm4',
'm5',
'm5',
'p4',
'p4',
'p7',
'p8',
'p9',
's4',
's5',
's6',
]));
*/
/* /*
if (room.value.isStarted && !room.value.isEnded) { if (room.value.isStarted && !room.value.isEnded) {
useInterval(() => { useInterval(() => {
@ -102,10 +156,7 @@ if (room.value.isStarted && !room.value.isEnded) {
} }
*/ */
const appliedOps: string[] = [];
const myTurnTimerRmain = ref<number>(room.value.timeLimitForEachTurn); const myTurnTimerRmain = ref<number>(room.value.timeLimitForEachTurn);
const opTurnTimerRmain = ref<number>(room.value.timeLimitForEachTurn);
/* /*
const TIMER_INTERVAL_SEC = 3; const TIMER_INTERVAL_SEC = 3;
@ -131,37 +182,36 @@ function dahai(tile: Mahjong.Common.Tile, ev: MouseEvent) {
if (!isMyTurn.value) return; if (!isMyTurn.value) return;
engine.value.op_dahai(engine.value.myHouse, tile); engine.value.op_dahai(engine.value.myHouse, tile);
iTsumoed.value = false;
triggerRef(engine);
const id = Math.random().toString(36).slice(2);
appliedOps.push(id);
props.connection!.send('dahai', { props.connection!.send('dahai', {
tile: tile, tile: tile,
id,
}); });
} }
function pon() { function pon() {
engine.value.op_pon(engine.value.canPonTo, engine.value.myHouse); engine.value.op_pon(engine.value.state.canPonSource, engine.value.myHouse);
triggerRef(engine);
const id = Math.random().toString(36).slice(2);
appliedOps.push(id);
props.connection!.send('pon', { props.connection!.send('pon', {
id,
}); });
} }
function skip() { function skip() {
engine.value.op_nop(engine.value.myHouse); engine.value.op_nop(engine.value.myHouse);
triggerRef(engine);
const id = Math.random().toString(36).slice(2);
appliedOps.push(id);
props.connection!.send('nop', {}); props.connection!.send('nop', {});
} }
async function onStreamLog(log) { const iTsumoed = ref(false);
if (log.id == null || !appliedOps.includes(log.id)) {
switch (log.operation) { function onStreamDahai(log) {
case 'dahai': { console.log('onStreamDahai', log);
if (log.house === engine.value.myHouse) return;
sound.playUrl('/client-assets/mahjong/dahai.mp3', { sound.playUrl('/client-assets/mahjong/dahai.mp3', {
volume: 1, volume: 1,
playbackRate: 1, playbackRate: 1,
@ -179,15 +229,10 @@ async function onStreamLog(log) {
triggerRef(engine); triggerRef(engine);
myTurnTimerRmain.value = room.value.timeLimitForEachTurn; myTurnTimerRmain.value = room.value.timeLimitForEachTurn;
opTurnTimerRmain.value = room.value.timeLimitForEachTurn;
break;
} }
case 'dahaiAndTsumo': { function onStreamTsumo(log) {
sound.playUrl('/client-assets/mahjong/dahai.mp3', { console.log('onStreamTsumo', log);
volume: 1,
playbackRate: 1,
});
//if (log.house !== engine.value.state.turn) { // = desync //if (log.house !== engine.value.state.turn) { // = desync
// const _room = await misskeyApi('mahjong/show-room', { // const _room = await misskeyApi('mahjong/show-room', {
@ -197,23 +242,61 @@ async function onStreamLog(log) {
// return; // return;
//} //}
engine.value.op_dahai(log.house, log.dahaiTile); engine.value.op_tsumo(log.house, log.tile);
triggerRef(engine); triggerRef(engine);
window.setTimeout(() => { if (log.house === engine.value.myHouse) {
engine.value.op_tsumo(Mahjong.Utils.nextHouse(log.house), log.tsumoTile); iTsumoed.value = true;
triggerRef(engine); }
}, 1000);
myTurnTimerRmain.value = room.value.timeLimitForEachTurn; myTurnTimerRmain.value = room.value.timeLimitForEachTurn;
opTurnTimerRmain.value = room.value.timeLimitForEachTurn;
break;
} }
default: function onStreamDahaiAndTsumo(log) {
break; console.log('onStreamDahaiAndTsumo', log);
//if (log.house !== engine.value.state.turn) { // = desync
// const _room = await misskeyApi('mahjong/show-room', {
// roomId: props.room.id,
// });
// restoreRoom(_room);
// return;
//}
if (log.dahaiHouse !== engine.value.myHouse) {
engine.value.op_dahai(log.dahaiHouse, log.dahaiTile);
triggerRef(engine);
} }
window.setTimeout(() => {
engine.value.op_tsumo(Mahjong.Utils.nextHouse(log.dahaiHouse), log.tsumoTile);
triggerRef(engine);
if (Mahjong.Utils.nextHouse(log.dahaiHouse) === engine.value.myHouse) {
iTsumoed.value = true;
} }
}, 100);
myTurnTimerRmain.value = room.value.timeLimitForEachTurn;
}
function onStreamPonned(log) {
console.log('onStreamPonned', log);
//if (log.house !== engine.value.state.turn) { // = desync
// const _room = await misskeyApi('mahjong/show-room', {
// roomId: props.room.id,
// });
// restoreRoom(_room);
// return;
//}
if (log.target === engine.value.myHouse) return;
engine.value.op_pon(log.source, log.target);
triggerRef(engine);
myTurnTimerRmain.value = room.value.timeLimitForEachTurn;
} }
function restoreRoom(_room) { function restoreRoom(_room) {
@ -224,28 +307,187 @@ function restoreRoom(_room) {
onMounted(() => { onMounted(() => {
if (props.connection != null) { if (props.connection != null) {
props.connection.on('log', onStreamLog); props.connection.on('dahai', onStreamDahai);
props.connection.on('tsumo', onStreamTsumo);
props.connection.on('dahaiAndTsumo', onStreamDahaiAndTsumo);
props.connection.on('ponned', onStreamPonned);
} }
}); });
onActivated(() => { onActivated(() => {
if (props.connection != null) { if (props.connection != null) {
props.connection.on('log', onStreamLog); props.connection.on('dahai', onStreamDahai);
props.connection.on('tsumo', onStreamTsumo);
props.connection.on('dahaiAndTsumo', onStreamDahaiAndTsumo);
props.connection.on('ponned', onStreamPonned);
} }
}); });
onDeactivated(() => { onDeactivated(() => {
if (props.connection != null) { if (props.connection != null) {
props.connection.off('log', onStreamLog); props.connection.off('dahai', onStreamDahai);
props.connection.off('tsumo', onStreamTsumo);
props.connection.off('dahaiAndTsumo', onStreamDahaiAndTsumo);
props.connection.off('ponned', onStreamPonned);
} }
}); });
onUnmounted(() => { onUnmounted(() => {
if (props.connection != null) { if (props.connection != null) {
props.connection.off('log', onStreamLog); props.connection.off('dahai', onStreamDahai);
props.connection.off('tsumo', onStreamTsumo);
props.connection.off('dahaiAndTsumo', onStreamDahaiAndTsumo);
props.connection.off('ponned', onStreamPonned);
} }
}); });
</script> </script>
<style lang="scss" module> <style lang="scss" module>
.root {
background: #3C7A43;
padding: 30px;
}
.taku {
position: relative;
width: 100%;
height: 100%;
max-width: 800px;
min-height: 600px;
margin: auto;
box-sizing: border-box;
}
.handTilesOfToimen {
position: absolute;
top: 0;
left: 80px;
}
.handTilesOfKamitya {
position: absolute;
top: 80px;
left: 0;
}
.handTilesOfSimotya {
position: absolute;
top: 80px;
right: 0;
}
.handTilesOfMe {
position: absolute;
bottom: 0;
left: 80px;
}
.huroTilesOfMe {
position: absolute;
bottom: 0;
right: 80px;
}
.hoTilesContainer {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
transform-origin: center;
scale: 0.8;
}
.hoTilesContainerOfToimen {
position: absolute;
bottom: calc(50% + 100px);
left: 0;
right: 0;
margin: auto;
width: min-content;
}
.hoTilesOfToimen {
rotate: 180deg;
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr;
}
.hoTilesContainerOfKamitya {
position: absolute;
right: calc(50% + 100px);
top: 0;
bottom: 0;
margin: auto;
height: min-content;
}
.hoTilesOfKamitya {
rotate: 90deg;
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr;
grid-template-rows: 1fr 1fr 1fr 1fr 1fr 1fr;
aspect-ratio: 1;
}
.hoTilesContainerOfSimotya {
position: absolute;
left: calc(50% + 100px);
top: 0;
bottom: 0;
margin: auto;
height: min-content;
}
.hoTilesOfSimotya {
rotate: -90deg;
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr;
grid-template-rows: 1fr 1fr 1fr 1fr 1fr 1fr;
aspect-ratio: 1;
}
.hoTilesContainerOfMe {
position: absolute;
top: calc(50% + 100px);
left: 0;
right: 0;
margin: auto;
width: min-content;
}
.hoTilesOfMe {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr;
}
.sideTile {
margin-bottom: -26px;
}
.hoTile {
position: relative;
display: inline-block;
width: 32px;
aspect-ratio: 0.7;
background: #fff;
margin-bottom: -8px;
}
.myTile {
display: inline-block;
position: relative;
width: 35px;
aspect-ratio: 0.7;
}
.myTileBg {
position: absolute;
top: 0;
left: 0;
width: 100%;
}
.myTileFg {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 70%;
object-fit: contain;
}
</style> </style>

View file

@ -7,6 +7,28 @@ import CRC32 from 'crc-32';
import { Tile, House, TILE_TYPES } from './common.js'; import { Tile, House, TILE_TYPES } from './common.js';
import * as Utils from './utils.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 = { export type MasterState = {
user1House: House; user1House: House;
user2House: House; user2House: House;
@ -21,14 +43,10 @@ export type MasterState = {
sHoTiles: Tile[]; sHoTiles: Tile[];
wHoTiles: Tile[]; wHoTiles: Tile[];
nHoTiles: Tile[]; nHoTiles: Tile[];
ePonnedTiles: { tile: Tile; from: House; }[]; eHuros: Huro[];
sPonnedTiles: { tile: Tile; from: House; }[]; sHuros: Huro[];
wPonnedTiles: { tile: Tile; from: House; }[]; wHuros: Huro[];
nPonnedTiles: { tile: Tile; from: House; }[]; nHuros: Huro[];
eCiiedTiles: { tiles: [Tile, Tile, Tile]; from: House; }[];
sCiiedTiles: { tiles: [Tile, Tile, Tile]; from: House; }[];
wCiiedTiles: { tiles: [Tile, Tile, Tile]; from: House; }[];
nCiiedTiles: { tiles: [Tile, Tile, Tile]; from: House; }[];
eRiichi: boolean; eRiichi: boolean;
sRiichi: boolean; sRiichi: boolean;
wRiichi: boolean; wRiichi: boolean;
@ -38,6 +56,7 @@ export type MasterState = {
wPoints: number; wPoints: number;
nPoints: number; nPoints: number;
turn: House | null; turn: House | null;
nextTurnAfterAsking: House | null;
ronAsking: { ronAsking: {
/** /**
@ -118,14 +137,10 @@ export class MasterGameEngine {
sHoTiles: [], sHoTiles: [],
wHoTiles: [], wHoTiles: [],
nHoTiles: [], nHoTiles: [],
ePonnedTiles: [], eHuros: [],
sPonnedTiles: [], sHuros: [],
wPonnedTiles: [], wHuros: [],
nPonnedTiles: [], nHuros: [],
eCiiedTiles: [],
sCiiedTiles: [],
wCiiedTiles: [],
nCiiedTiles: [],
eRiichi: false, eRiichi: false,
sRiichi: false, sRiichi: false,
wRiichi: false, wRiichi: false,
@ -135,14 +150,18 @@ export class MasterGameEngine {
wPoints: 25000, wPoints: 25000,
nPoints: 25000, nPoints: 25000,
turn: 'e', turn: 'e',
nextTurnAfterAsking: null,
ponAsking: null, ponAsking: null,
ciiAsking: null, ciiAsking: null,
kanAsking: null,
ronAsking: null,
}; };
} }
private tsumo(): Tile { private tsumo(): Tile {
const tile = this.state.tiles.pop(); const tile = this.state.tiles.pop();
if (tile == null) throw new Error('No tiles left'); 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); this.getHandTilesOf(this.state.turn).push(tile);
return tile; return tile;
} }
@ -235,6 +254,8 @@ export class MasterGameEngine {
target: canCiiHouse, target: canCiiHouse,
}; };
} }
this.state.turn = null;
this.state.nextTurnAfterAsking = Utils.nextHouse(house);
return { return {
asking: true, asking: true,
canRonHouses: canRonHouses, canRonHouses: canRonHouses,
@ -278,8 +299,8 @@ export class MasterGameEngine {
const source = this.state.kanAsking.source; const source = this.state.kanAsking.source;
const target = this.state.kanAsking.target; const target = this.state.kanAsking.target;
const tile = this.getHoTilesOf(source).pop(); const tile = this.getHoTilesOf(source).pop()!;
this.getKannedTilesOf(target).push({ tile, from: source }); this.getHurosOf(target).push({ type: 'kan', tile, from: source });
clearAsking(); clearAsking();
this.state.turn = target; this.state.turn = target;
@ -291,14 +312,17 @@ export class MasterGameEngine {
const source = this.state.ponAsking.source; const source = this.state.ponAsking.source;
const target = this.state.ponAsking.target; const target = this.state.ponAsking.target;
const tile = this.getHoTilesOf(source).pop(); const tile = this.getHoTilesOf(source).pop()!;
this.getPonnedTilesOf(target).push({ tile, from: source }); 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(); clearAsking();
this.state.turn = target; this.state.turn = target;
return { return {
type: 'ponned', type: 'ponned',
house: this.state.turn, source,
target,
tile, tile,
}; };
} }
@ -307,20 +331,22 @@ export class MasterGameEngine {
const source = this.state.ciiAsking.source; const source = this.state.ciiAsking.source;
const target = this.state.ciiAsking.target; const target = this.state.ciiAsking.target;
const tile = this.getHoTilesOf(source).pop(); const tile = this.getHoTilesOf(source).pop()!;
this.getCiiedTilesOf(target).push({ tile, from: source }); this.getCiiedTilesOf(target).push({ tile, from: source });
clearAsking(); clearAsking();
this.state.turn = target; this.state.turn = target;
return { return {
type: 'ciied', type: 'ciied',
house: this.state.turn, source,
target,
tile, tile,
}; };
} }
clearAsking(); clearAsking();
this.state.turn = Utils.nextHouse(this.state.turn); this.state.turn = this.state.nextTurnAfterAsking;
this.state.nextTurnAfterAsking = null;
const tile = this.tsumo(); const tile = this.tsumo();
@ -364,6 +390,7 @@ export class MasterGameEngine {
case 's': return this.state.sHandTiles; case 's': return this.state.sHandTiles;
case 'w': return this.state.wHandTiles; case 'w': return this.state.wHandTiles;
case 'n': return this.state.nHandTiles; case 'n': return this.state.nHandTiles;
default: throw new Error(`unrecognized house: ${house}`);
} }
} }
@ -373,33 +400,17 @@ export class MasterGameEngine {
case 's': return this.state.sHoTiles; case 's': return this.state.sHoTiles;
case 'w': return this.state.wHoTiles; case 'w': return this.state.wHoTiles;
case 'n': return this.state.nHoTiles; case 'n': return this.state.nHoTiles;
default: throw new Error(`unrecognized house: ${house}`);
} }
} }
public getPonnedTilesOf(house: House): { tile: Tile; from: House; }[] { public getHurosOf(house: House): Huro[] {
switch (house) { switch (house) {
case 'e': return this.state.ePonnedTiles; case 'e': return this.state.eHuros;
case 's': return this.state.sPonnedTiles; case 's': return this.state.sHuros;
case 'w': return this.state.wPonnedTiles; case 'w': return this.state.wHuros;
case 'n': return this.state.nPonnedTiles; case 'n': return this.state.nHuros;
} default: throw new Error(`unrecognized house: ${house}`);
}
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;
} }
} }
@ -420,14 +431,10 @@ export class MasterGameEngine {
sHoTiles: this.state.sHoTiles, sHoTiles: this.state.sHoTiles,
wHoTiles: this.state.wHoTiles, wHoTiles: this.state.wHoTiles,
nHoTiles: this.state.nHoTiles, nHoTiles: this.state.nHoTiles,
ePonnedTiles: this.state.ePonnedTiles, eHuros: this.state.eHuros,
sPonnedTiles: this.state.sPonnedTiles, sHuros: this.state.sHuros,
wPonnedTiles: this.state.wPonnedTiles, wHuros: this.state.wHuros,
nPonnedTiles: this.state.nPonnedTiles, nHuros: this.state.nHuros,
eCiiedTiles: this.state.eCiiedTiles,
sCiiedTiles: this.state.sCiiedTiles,
wCiiedTiles: this.state.wCiiedTiles,
nCiiedTiles: this.state.nCiiedTiles,
eRiichi: this.state.eRiichi, eRiichi: this.state.eRiichi,
sRiichi: this.state.sRiichi, sRiichi: this.state.sRiichi,
wRiichi: this.state.wRiichi, wRiichi: this.state.wRiichi,
@ -472,14 +479,10 @@ export type PlayerState = {
sHoTiles: Tile[]; sHoTiles: Tile[];
wHoTiles: Tile[]; wHoTiles: Tile[];
nHoTiles: Tile[]; nHoTiles: Tile[];
ePonnedTiles: { tile: Tile; from: House; }[]; eHuros: Huro[];
sPonnedTiles: { tile: Tile; from: House; }[]; sHuros: Huro[];
wPonnedTiles: { tile: Tile; from: House; }[]; wHuros: Huro[];
nPonnedTiles: { tile: Tile; from: House; }[]; nHuros: Huro[];
eCiiedTiles: { tiles: [Tile, Tile, Tile]; from: House; }[];
sCiiedTiles: { tiles: [Tile, Tile, Tile]; from: House; }[];
wCiiedTiles: { tiles: [Tile, Tile, Tile]; from: House; }[];
nCiiedTiles: { tiles: [Tile, Tile, Tile]; from: House; }[];
eRiichi: boolean; eRiichi: boolean;
sRiichi: boolean; sRiichi: boolean;
wRiichi: boolean; wRiichi: boolean;
@ -490,7 +493,7 @@ export type PlayerState = {
nPoints: number; nPoints: number;
latestDahaiedTile: Tile | null; latestDahaiedTile: Tile | null;
turn: House | null; turn: House | null;
canPonTo: House | null; canPonSource: House | null;
canCiiTo: House | null; canCiiTo: House | null;
canKanTo: House | null; canKanTo: House | null;
canRonTo: House | null; canRonTo: House | null;
@ -543,6 +546,7 @@ export class PlayerGameEngine {
case 's': return this.state.sHandTiles; case 's': return this.state.sHandTiles;
case 'w': return this.state.wHandTiles; case 'w': return this.state.wHandTiles;
case 'n': return this.state.nHandTiles; case 'n': return this.state.nHandTiles;
default: throw new Error(`unrecognized house: ${house}`);
} }
} }
@ -552,37 +556,23 @@ export class PlayerGameEngine {
case 's': return this.state.sHoTiles; case 's': return this.state.sHoTiles;
case 'w': return this.state.wHoTiles; case 'w': return this.state.wHoTiles;
case 'n': return this.state.nHoTiles; case 'n': return this.state.nHoTiles;
default: throw new Error(`unrecognized house: ${house}`);
} }
} }
public getPonnedTilesOf(house: House): { tile: Tile; from: House; }[] { public getHurosOf(house: House): Huro[] {
switch (house) { switch (house) {
case 'e': return this.state.ePonnedTiles; case 'e': return this.state.eHuros;
case 's': return this.state.sPonnedTiles; case 's': return this.state.sHuros;
case 'w': return this.state.wPonnedTiles; case 'w': return this.state.wHuros;
case 'n': return this.state.nPonnedTiles; case 'n': return this.state.nHuros;
} default: throw new Error(`unrecognized house: ${house}`);
}
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) {
console.log('op_tsumo', this.state.turn, house, tile);
this.state.turn = house;
if (house === this.myHouse) { if (house === this.myHouse) {
this.myHandTiles.push(tile); this.myHandTiles.push(tile);
} else { } else {
@ -591,8 +581,7 @@ export class PlayerGameEngine {
} }
public op_dahai(house: House, tile: Tile) { public op_dahai(house: House, tile: Tile) {
console.log(this.state.turn, house, tile); console.log('op_dahai', 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) {
@ -603,7 +592,7 @@ export class PlayerGameEngine {
this.getHoTilesOf(house).push(tile); this.getHoTilesOf(house).push(tile);
} }
this.state.turn = Utils.nextHouse(this.state.turn); this.state.turn = null;
if (house === this.myHouse) { if (house === this.myHouse) {
} else { } else {
@ -611,22 +600,34 @@ export class PlayerGameEngine {
// TODO: canCii // TODO: canCii
if (canPon) this.state.canPonTo = house; if (canPon) this.state.canPonSource = house;
} }
} }
/**
*
* @param source
* @param target
*/
public op_pon(source: House, target: House) { public op_pon(source: House, target: House) {
this.state.canPonTo = null; this.state.canPonSource = null;
const lastTile = this.getHoTilesOf(source).pop(); const lastTile = this.getHoTilesOf(source).pop();
if (lastTile == null) throw new PlayerGameEngine.InvalidOperationError(); if (lastTile == null) throw new PlayerGameEngine.InvalidOperationError();
this.getPonnedTilesOf(target).push({ tile: lastTile, from: source }); 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; this.state.turn = target;
} }
public op_nop() { public op_nop() {
this.state.canPonTo = null; this.state.canPonSource = null;
} }
} }

View file

@ -24,6 +24,7 @@ export function nextHouse(house: House): House {
case 's': return 'w'; case 's': return 'w';
case 'w': return 'n'; case 'w': return 'n';
case 'n': return 'e'; case 'n': return 'e';
default: throw new Error(`unrecognized house: ${house}`);
} }
} }
@ -33,6 +34,7 @@ export function prevHouse(house: House): House {
case 's': return 'e'; case 's': return 'e';
case 'w': return 's'; case 'w': return 's';
case 'n': return 'w'; case 'n': return 'w';
default: throw new Error(`unrecognized house: ${house}`);
} }
} }
@ -179,38 +181,36 @@ export function getHoraSets(handTiles: Tile[]): HoraSet[] {
tempHandTilesWithoutKotsu.splice(tempHandTilesWithoutKotsu.indexOf(kotsuTile), 1); tempHandTilesWithoutKotsu.splice(tempHandTilesWithoutKotsu.indexOf(kotsuTile), 1);
} }
// 連番に並ぶようにソート
tempHandTilesWithoutKotsu.sort((a, b) => { tempHandTilesWithoutKotsu.sort((a, b) => {
const aIndex = TILE_TYPES.indexOf(a); const aIndex = TILE_TYPES.indexOf(a);
const bIndex = TILE_TYPES.indexOf(b); const bIndex = TILE_TYPES.indexOf(b);
return aIndex - bIndex; return aIndex - bIndex;
}); });
const tempTempHandTilesWithoutKotsuAndShuntsu: (Tile | null)[] = [...tempHandTilesWithoutKotsu]; const tempHandTilesWithoutKotsuAndShuntsu: (Tile | null)[] = [...tempHandTilesWithoutKotsu];
const shuntsus: [Tile, Tile, Tile][] = []; const shuntsus: [Tile, Tile, Tile][] = [];
let i = 0; while (tempHandTilesWithoutKotsuAndShuntsu.length > 0) {
while (i < tempHandTilesWithoutKotsu.length) { let isShuntu = false;
const headThree = tempHandTilesWithoutKotsu.slice(i, i + 3);
if (headThree.length !== 3) break;
for (const shuntuPattern of SHUNTU_PATTERNS) { for (const shuntuPattern of SHUNTU_PATTERNS) {
if (headThree[0] === shuntuPattern[0] && headThree[1] === shuntuPattern[1] && headThree[2] === shuntuPattern[2]) { if (
tempHandTilesWithoutKotsuAndShuntsu[0] === shuntuPattern[0] &&
tempHandTilesWithoutKotsuAndShuntsu.includes(shuntuPattern[1]) &&
tempHandTilesWithoutKotsuAndShuntsu.includes(shuntuPattern[2])
) {
shuntsus.push(shuntuPattern); shuntsus.push(shuntuPattern);
tempTempHandTilesWithoutKotsuAndShuntsu[i] = null; tempHandTilesWithoutKotsuAndShuntsu.splice(0, 1);
tempTempHandTilesWithoutKotsuAndShuntsu[i + 1] = null; tempHandTilesWithoutKotsuAndShuntsu.splice(tempHandTilesWithoutKotsuAndShuntsu.indexOf(shuntuPattern[1]), 1);
tempTempHandTilesWithoutKotsuAndShuntsu[i + 2] = null; tempHandTilesWithoutKotsuAndShuntsu.splice(tempHandTilesWithoutKotsuAndShuntsu.indexOf(shuntuPattern[2]), 1);
i += 3; isShuntu = true;
break; break;
} }
} }
i++; if (!isShuntu) tempHandTilesWithoutKotsuAndShuntsu.splice(0, 1);
} }
const tempHandTilesWithoutKotsuAndShuntsu = tempTempHandTilesWithoutKotsuAndShuntsu.filter(t => t != null) as Tile[]; if (shuntsus.length * 3 === tempHandTilesWithoutKotsu.length) { // アガリ形
if (tempHandTilesWithoutKotsuAndShuntsu.length === 0) { // アガリ形
horaSets.push({ horaSets.push({
head, head,
mentsus: [...kotsuPattern.map(t => [t, t, t] as [Tile, Tile, Tile]), ...shuntsus], mentsus: [...kotsuPattern.map(t => [t, t, t] as [Tile, Tile, Tile]), ...shuntsus],