wip
This commit is contained in:
parent
7cdaa10d46
commit
2dd886e285
10 changed files with 351 additions and 133 deletions
|
@ -215,11 +215,13 @@ export interface MahjongRoomEventTypes {
|
|||
dahai: {
|
||||
house: Mahjong.Common.House;
|
||||
tile: Mahjong.Common.Tile;
|
||||
riichi: boolean;
|
||||
};
|
||||
dahaiAndTsumo: {
|
||||
dahaiHouse: Mahjong.Common.House;
|
||||
dahaiTile: Mahjong.Common.Tile;
|
||||
tsumoTile: Mahjong.Common.Tile;
|
||||
riichi: boolean;
|
||||
};
|
||||
ponned: {
|
||||
caller: Mahjong.Common.House;
|
||||
|
|
|
@ -26,7 +26,7 @@ import { ReversiGameEntityService } from './entities/ReversiGameEntityService.js
|
|||
import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common';
|
||||
|
||||
const INVITATION_TIMEOUT_MS = 1000 * 20; // 20sec
|
||||
const CALL_AND_RON_ASKING_TIMEOUT_MS = 1000 * 5; // 5sec
|
||||
const CALL_AND_RON_ASKING_TIMEOUT_MS = 1000 * 7; // 7sec
|
||||
const TURN_TIMEOUT_MS = 1000 * 30; // 30sec
|
||||
const NEXT_KYOKU_CONFIRMATION_TIMEOUT_MS = 1000 * 15; // 15sec
|
||||
|
||||
|
@ -318,7 +318,7 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit {
|
|||
switch (res.type) {
|
||||
case 'tsumo':
|
||||
this.globalEventService.publishMahjongRoomStream(room.id, 'tsumo', { house: res.house, tile: res.tile });
|
||||
this.next(room, engine);
|
||||
this.waitForTurn(room, res.turn, engine);
|
||||
break;
|
||||
case 'ponned':
|
||||
this.globalEventService.publishMahjongRoomStream(room.id, 'ponned', { caller: res.caller, callee: res.callee, tile: res.tile });
|
||||
|
@ -349,23 +349,6 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit {
|
|||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async next(room: Room, engine: Mahjong.MasterGameEngine) {
|
||||
const turn = engine.state.turn;
|
||||
if (turn == null) throw new Error('turn is null');
|
||||
|
||||
const aiHouses = [[1, room.user1Ai], [2, room.user2Ai], [3, room.user3Ai], [4, room.user4Ai]].filter(([id, ai]) => ai).map(([id, ai]) => engine.getHouse(id));
|
||||
|
||||
if (aiHouses.includes(turn)) {
|
||||
// TODO: ちゃんと思考するようにする
|
||||
setTimeout(() => {
|
||||
this.dahai(room, engine, turn, engine.state.handTiles[turn].at(-1));
|
||||
}, 500);
|
||||
} else {
|
||||
this.waitForTurn(room, turn, engine);
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async endKyoku(room: Room, engine: Mahjong.MasterGameEngine) {
|
||||
const confirmation: NextKyokuConfirmation = {
|
||||
|
@ -469,11 +452,11 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit {
|
|||
}
|
||||
}, 1000);
|
||||
|
||||
this.globalEventService.publishMahjongRoomStream(room.id, 'dahai', { house: house, tile });
|
||||
this.globalEventService.publishMahjongRoomStream(room.id, 'dahai', { house: house, tile, riichi });
|
||||
} else {
|
||||
this.globalEventService.publishMahjongRoomStream(room.id, 'dahaiAndTsumo', { dahaiHouse: house, dahaiTile: tile, tsumoTile: res.tsumoTile });
|
||||
this.globalEventService.publishMahjongRoomStream(room.id, 'dahaiAndTsumo', { dahaiHouse: house, dahaiTile: tile, tsumoTile: res.tsumoTile, riichi });
|
||||
|
||||
this.next(room, engine);
|
||||
this.waitForTurn(room, res.next, engine);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -556,7 +539,7 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit {
|
|||
|
||||
const res = engine.commit_tsumoHora(myHouse);
|
||||
|
||||
this.globalEventService.publishMahjongRoomStream(room.id, 'tsumoHora', { });
|
||||
this.globalEventService.publishMahjongRoomStream(room.id, 'tsumoHora', { house: myHouse, handTiles: res.handTiles, tsumoTile: res.tsumoTile });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
@ -624,6 +607,8 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit {
|
|||
*/
|
||||
@bindThis
|
||||
private async waitForTurn(room: Room, house: Mahjong.Common.House, 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));
|
||||
|
||||
if (engine.state.riichis[house]) {
|
||||
// リーチ時はアガリ牌でない限りツモ切り
|
||||
const handTiles = engine.state.handTiles[house];
|
||||
|
@ -636,6 +621,13 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit {
|
|||
}
|
||||
}
|
||||
|
||||
if (aiHouses.includes(house)) {
|
||||
setTimeout(() => {
|
||||
this.dahai(room, engine, house, engine.state.handTiles[house].at(-1));
|
||||
}, 500);
|
||||
return;
|
||||
}
|
||||
|
||||
const id = Math.random().toString(36).slice(2);
|
||||
console.log('waitForTurn', house, id);
|
||||
this.redisClient.sadd(`mahjong:gameTurnWaiting:${room.id}`, id);
|
||||
|
|
BIN
packages/frontend/assets/mahjong/riichi.png
Normal file
BIN
packages/frontend/assets/mahjong/riichi.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 64 KiB |
Binary file not shown.
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
Binary file not shown.
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
46
packages/frontend/src/pages/mahjong/huro.vue
Normal file
46
packages/frontend/src/pages/mahjong/huro.vue
Normal file
|
@ -0,0 +1,46 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div v-if="huro.type === 'ankan'" :class="[$style.root]">
|
||||
<XTile :tile="huro.tile" :variation="variation" :doras="doras"/>
|
||||
<XTile :tile="huro.tile" :variation="variation" :doras="doras"/>
|
||||
<XTile :tile="huro.tile" :variation="variation" :doras="doras"/>
|
||||
<XTile :tile="huro.tile" :variation="variation" :doras="doras"/>
|
||||
</div>
|
||||
<div v-else-if="huro.type === 'minkan'" :class="[$style.root]">
|
||||
<XTile :tile="huro.tile" :variation="variation" :doras="doras"/>
|
||||
<XTile :tile="huro.tile" :variation="variation" :doras="doras"/>
|
||||
<XTile :tile="huro.tile" :variation="variation" :doras="doras"/>
|
||||
<XTile :tile="huro.tile" :variation="variation" :doras="doras"/>
|
||||
</div>
|
||||
<div v-else-if="huro.type === 'cii'" :class="[$style.root]">
|
||||
<XTile :tile="huro.tiles[0]" :variation="variation" :doras="doras"/>
|
||||
<XTile :tile="huro.tiles[1]" :variation="variation" :doras="doras"/>
|
||||
<XTile :tile="huro.tiles[2]" :variation="variation" :doras="doras"/>
|
||||
</div>
|
||||
<div v-else :class="[$style.root]">
|
||||
<XTile :tile="huro.tile" :variation="variation" :doras="doras"/>
|
||||
<XTile :tile="huro.tile" :variation="variation" :doras="doras"/>
|
||||
<XTile :tile="huro.tile" :variation="variation" :doras="doras"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import * as Mahjong from 'misskey-mahjong';
|
||||
import XTile from './tile.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
huro: Mahjong.Common.Huro;
|
||||
variation: string;
|
||||
doras: Mahjong.Common.Tile[];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
}
|
||||
</style>
|
|
@ -56,6 +56,22 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="$style.huroTilesOfToimen">
|
||||
<XHuro v-for="huro in engine.state.huros[Mahjong.Common.prevHouse(Mahjong.Common.prevHouse(engine.myHouse))]" :huro="huro" :variation="1" :doras="engine.doras"/>
|
||||
</div>
|
||||
|
||||
<div :class="$style.huroTilesOfKamitya">
|
||||
<XHuro v-for="huro in engine.state.huros[Mahjong.Common.prevHouse(engine.myHouse)]" :huro="huro" :variation="1" :doras="engine.doras"/>
|
||||
</div>
|
||||
|
||||
<div :class="$style.huroTilesOfSimotya">
|
||||
<XHuro v-for="huro in engine.state.huros[Mahjong.Common.nextHouse(engine.myHouse)]" :huro="huro" :variation="1" :doras="engine.doras"/>
|
||||
</div>
|
||||
|
||||
<div :class="$style.huroTilesOfMe">
|
||||
<XHuro v-for="huro in engine.state.huros[engine.myHouse]" :huro="huro" :variation="1" :doras="engine.doras"/>
|
||||
</div>
|
||||
|
||||
<div :class="$style.hoTilesContainer">
|
||||
<div :class="$style.hoTilesContainerOfToimen">
|
||||
<div :class="$style.hoTilesOfToimen">
|
||||
|
@ -107,44 +123,66 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="$style.huroTilesOfMe">
|
||||
<div v-for="huro in engine.state.huros[engine.myHouse]" style="display: inline-block;">
|
||||
<div v-if="huro.type === 'pon'">
|
||||
<XTile :tile="huro.tile" variation="1" :doras="engine.doras"/>
|
||||
<XTile :tile="huro.tile" variation="1" :doras="engine.doras"/>
|
||||
<XTile :tile="huro.tile" variation="1" :doras="engine.doras"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="$style.serifContainer">
|
||||
<div :class="$style.serifContainerOfToimen">
|
||||
<img v-if="ronSerifHouses[Mahjong.Common.prevHouse(Mahjong.Common.prevHouse(engine.myHouse))]" :src="`/client-assets/mahjong/ron.png`" style="display: block; width: 100%;"/>
|
||||
<img v-else-if="ciiSerifHouses[Mahjong.Common.prevHouse(Mahjong.Common.prevHouse(engine.myHouse))]" :src="`/client-assets/mahjong/cii.png`" style="display: block; width: 100%;"/>
|
||||
<img v-else-if="ponSerifHouses[Mahjong.Common.prevHouse(Mahjong.Common.prevHouse(engine.myHouse))]" :src="`/client-assets/mahjong/pon.png`" style="display: block; width: 100%;"/>
|
||||
<img v-else-if="kanSerifHouses[Mahjong.Common.prevHouse(Mahjong.Common.prevHouse(engine.myHouse))]" :src="`/client-assets/mahjong/kan.png`" style="display: block; width: 100%;"/>
|
||||
<img v-else-if="tsumoSerifHouses[Mahjong.Common.prevHouse(Mahjong.Common.prevHouse(engine.myHouse))]" :src="`/client-assets/mahjong/tsumo.png`" style="display: block; width: 100%;"/>
|
||||
<Transition
|
||||
:enterActiveClass="$style.transition_serif_enterActive"
|
||||
:leaveActiveClass="$style.transition_serif_leaveActive"
|
||||
:enterFromClass="$style.transition_serif_enterFrom"
|
||||
:leaveToClass="$style.transition_serif_leaveTo"
|
||||
>
|
||||
<img v-if="ronSerifHouses[Mahjong.Common.prevHouse(Mahjong.Common.prevHouse(engine.myHouse))]" :src="`/client-assets/mahjong/ron.png`" style="display: block; width: 100%;"/>
|
||||
<img v-else-if="ciiSerifHouses[Mahjong.Common.prevHouse(Mahjong.Common.prevHouse(engine.myHouse))]" :src="`/client-assets/mahjong/cii.png`" style="display: block; width: 100%;"/>
|
||||
<img v-else-if="ponSerifHouses[Mahjong.Common.prevHouse(Mahjong.Common.prevHouse(engine.myHouse))]" :src="`/client-assets/mahjong/pon.png`" style="display: block; width: 100%;"/>
|
||||
<img v-else-if="kanSerifHouses[Mahjong.Common.prevHouse(Mahjong.Common.prevHouse(engine.myHouse))]" :src="`/client-assets/mahjong/kan.png`" style="display: block; width: 100%;"/>
|
||||
<img v-else-if="tsumoSerifHouses[Mahjong.Common.prevHouse(Mahjong.Common.prevHouse(engine.myHouse))]" :src="`/client-assets/mahjong/tsumo.png`" style="display: block; width: 100%;"/>
|
||||
<img v-else-if="riichiSerifHouses[Mahjong.Common.prevHouse(Mahjong.Common.prevHouse(engine.myHouse))]" :src="`/client-assets/mahjong/riichi.png`" style="display: block; width: 100%;"/>
|
||||
</Transition>
|
||||
</div>
|
||||
<div :class="$style.serifContainerOfKamitya">
|
||||
<img v-if="ronSerifHouses[Mahjong.Common.prevHouse(engine.myHouse)]" :src="`/client-assets/mahjong/ron.png`" style="display: block; width: 100%;"/>
|
||||
<img v-else-if="ciiSerifHouses[Mahjong.Common.prevHouse(engine.myHouse)]" :src="`/client-assets/mahjong/cii.png`" style="display: block; width: 100%;"/>
|
||||
<img v-else-if="ponSerifHouses[Mahjong.Common.prevHouse(engine.myHouse)]" :src="`/client-assets/mahjong/pon.png`" style="display: block; width: 100%;"/>
|
||||
<img v-else-if="kanSerifHouses[Mahjong.Common.prevHouse(engine.myHouse)]" :src="`/client-assets/mahjong/kan.png`" style="display: block; width: 100%;"/>
|
||||
<img v-else-if="tsumoSerifHouses[Mahjong.Common.prevHouse(engine.myHouse)]" :src="`/client-assets/mahjong/tsumo.png`" style="display: block; width: 100%;"/>
|
||||
<Transition
|
||||
:enterActiveClass="$style.transition_serif_enterActive"
|
||||
:leaveActiveClass="$style.transition_serif_leaveActive"
|
||||
:enterFromClass="$style.transition_serif_enterFrom"
|
||||
:leaveToClass="$style.transition_serif_leaveTo"
|
||||
>
|
||||
<img v-if="ronSerifHouses[Mahjong.Common.prevHouse(engine.myHouse)]" :src="`/client-assets/mahjong/ron.png`" style="display: block; width: 100%;"/>
|
||||
<img v-else-if="ciiSerifHouses[Mahjong.Common.prevHouse(engine.myHouse)]" :src="`/client-assets/mahjong/cii.png`" style="display: block; width: 100%;"/>
|
||||
<img v-else-if="ponSerifHouses[Mahjong.Common.prevHouse(engine.myHouse)]" :src="`/client-assets/mahjong/pon.png`" style="display: block; width: 100%;"/>
|
||||
<img v-else-if="kanSerifHouses[Mahjong.Common.prevHouse(engine.myHouse)]" :src="`/client-assets/mahjong/kan.png`" style="display: block; width: 100%;"/>
|
||||
<img v-else-if="tsumoSerifHouses[Mahjong.Common.prevHouse(engine.myHouse)]" :src="`/client-assets/mahjong/tsumo.png`" style="display: block; width: 100%;"/>
|
||||
<img v-else-if="riichiSerifHouses[Mahjong.Common.prevHouse(engine.myHouse)]" :src="`/client-assets/mahjong/riichi.png`" style="display: block; width: 100%;"/>
|
||||
</Transition>
|
||||
</div>
|
||||
<div :class="$style.serifContainerOfSimotya">
|
||||
<img v-if="ronSerifHouses[Mahjong.Common.nextHouse(engine.myHouse)]" :src="`/client-assets/mahjong/ron.png`" style="display: block; width: 100%;"/>
|
||||
<img v-else-if="ciiSerifHouses[Mahjong.Common.nextHouse(engine.myHouse)]" :src="`/client-assets/mahjong/cii.png`" style="display: block; width: 100%;"/>
|
||||
<img v-else-if="ponSerifHouses[Mahjong.Common.nextHouse(engine.myHouse)]" :src="`/client-assets/mahjong/pon.png`" style="display: block; width: 100%;"/>
|
||||
<img v-else-if="kanSerifHouses[Mahjong.Common.nextHouse(engine.myHouse)]" :src="`/client-assets/mahjong/kan.png`" style="display: block; width: 100%;"/>
|
||||
<img v-else-if="tsumoSerifHouses[Mahjong.Common.nextHouse(engine.myHouse)]" :src="`/client-assets/mahjong/tsumo.png`" style="display: block; width: 100%;"/>
|
||||
<Transition
|
||||
:enterActiveClass="$style.transition_serif_enterActive"
|
||||
:leaveActiveClass="$style.transition_serif_leaveActive"
|
||||
:enterFromClass="$style.transition_serif_enterFrom"
|
||||
:leaveToClass="$style.transition_serif_leaveTo"
|
||||
>
|
||||
<img v-if="ronSerifHouses[Mahjong.Common.nextHouse(engine.myHouse)]" :src="`/client-assets/mahjong/ron.png`" style="display: block; width: 100%;"/>
|
||||
<img v-else-if="ciiSerifHouses[Mahjong.Common.nextHouse(engine.myHouse)]" :src="`/client-assets/mahjong/cii.png`" style="display: block; width: 100%;"/>
|
||||
<img v-else-if="ponSerifHouses[Mahjong.Common.nextHouse(engine.myHouse)]" :src="`/client-assets/mahjong/pon.png`" style="display: block; width: 100%;"/>
|
||||
<img v-else-if="kanSerifHouses[Mahjong.Common.nextHouse(engine.myHouse)]" :src="`/client-assets/mahjong/kan.png`" style="display: block; width: 100%;"/>
|
||||
<img v-else-if="tsumoSerifHouses[Mahjong.Common.nextHouse(engine.myHouse)]" :src="`/client-assets/mahjong/tsumo.png`" style="display: block; width: 100%;"/>
|
||||
<img v-else-if="riichiSerifHouses[Mahjong.Common.nextHouse(engine.myHouse)]" :src="`/client-assets/mahjong/riichi.png`" style="display: block; width: 100%;"/>
|
||||
</Transition>
|
||||
</div>
|
||||
<div :class="$style.serifContainerOfMe">
|
||||
<img v-if="ronSerifHouses[engine.myHouse]" :src="`/client-assets/mahjong/ron.png`" style="display: block; width: 100%;"/>
|
||||
<img v-else-if="ciiSerifHouses[engine.myHouse]" :src="`/client-assets/mahjong/cii.png`" style="display: block; width: 100%;"/>
|
||||
<img v-else-if="ponSerifHouses[engine.myHouse]" :src="`/client-assets/mahjong/pon.png`" style="display: block; width: 100%;"/>
|
||||
<img v-else-if="kanSerifHouses[engine.myHouse]" :src="`/client-assets/mahjong/kan.png`" style="display: block; width: 100%;"/>
|
||||
<img v-else-if="tsumoSerifHouses[engine.myHouse]" :src="`/client-assets/mahjong/tsumo.png`" style="display: block; width: 100%;"/>
|
||||
<Transition
|
||||
:enterActiveClass="$style.transition_serif_enterActive"
|
||||
:leaveActiveClass="$style.transition_serif_leaveActive"
|
||||
:enterFromClass="$style.transition_serif_enterFrom"
|
||||
:leaveToClass="$style.transition_serif_leaveTo"
|
||||
>
|
||||
<img v-if="ronSerifHouses[engine.myHouse]" :src="`/client-assets/mahjong/ron.png`" style="display: block; width: 100%;"/>
|
||||
<img v-else-if="ciiSerifHouses[engine.myHouse]" :src="`/client-assets/mahjong/cii.png`" style="display: block; width: 100%;"/>
|
||||
<img v-else-if="ponSerifHouses[engine.myHouse]" :src="`/client-assets/mahjong/pon.png`" style="display: block; width: 100%;"/>
|
||||
<img v-else-if="kanSerifHouses[engine.myHouse]" :src="`/client-assets/mahjong/kan.png`" style="display: block; width: 100%;"/>
|
||||
<img v-else-if="tsumoSerifHouses[engine.myHouse]" :src="`/client-assets/mahjong/tsumo.png`" style="display: block; width: 100%;"/>
|
||||
<img v-else-if="riichiSerifHouses[engine.myHouse]" :src="`/client-assets/mahjong/riichi.png`" style="display: block; width: 100%;"/>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -156,6 +194,18 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkButton v-if="isMyTurn && engine.canRiichi()" primary @click="riichi">Riichi</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="kyokuEnded" :class="$style.kyokuResult">
|
||||
<div v-for="(res, house) in kyokuResults" :key="house">
|
||||
<div v-if="res != null">
|
||||
<div>{{ house === 'e' ? i18n.ts._mahjong.east : house === 's' ? i18n.ts._mahjong.south : house === 'w' ? i18n.ts._mahjong.west : i18n.ts._mahjong.north }}</div>
|
||||
<div v-for="yaku in res.yakus">
|
||||
<div>{{ yaku.name }} ({{ yaku.fan }})</div>
|
||||
</div>
|
||||
<div>{{ res.doraCount }} dora</div>
|
||||
<div>{{ res.pointDeltas.e }} / {{ res.pointDeltas.s }} / {{ res.pointDeltas.w }} / {{ res.pointDeltas.n }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -164,6 +214,7 @@ import { computed, onActivated, onDeactivated, onMounted, onUnmounted, reactive,
|
|||
import * as Misskey from 'misskey-js';
|
||||
import * as Mahjong from 'misskey-mahjong';
|
||||
import XTile from './tile.vue';
|
||||
import XHuro from './huro.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
|
@ -202,6 +253,7 @@ const ciiSerifHouses = reactive<Record<Mahjong.Common.House, boolean>>({ e: fals
|
|||
const ponSerifHouses = reactive<Record<Mahjong.Common.House, boolean>>({ e: false, s: false, w: false, n: false });
|
||||
const kanSerifHouses = reactive<Record<Mahjong.Common.House, boolean>>({ e: false, s: false, w: false, n: false });
|
||||
const tsumoSerifHouses = reactive<Record<Mahjong.Common.House, boolean>>({ e: false, s: false, w: false, n: false });
|
||||
const riichiSerifHouses = reactive<Record<Mahjong.Common.House, boolean>>({ e: false, s: false, w: false, n: false });
|
||||
|
||||
/*
|
||||
console.log(Mahjong.Common.getTilesForRiichi([
|
||||
|
@ -338,6 +390,19 @@ function skip() {
|
|||
|
||||
const iTsumoed = ref(false);
|
||||
const kyokuEnded = ref(false);
|
||||
const kyokuResults = ref<Record<Mahjong.Common.House, {
|
||||
yakus: {
|
||||
name: string;
|
||||
fan: number;
|
||||
}[];
|
||||
doraCount: number;
|
||||
pointDeltas: Record<Mahjong.Common.House, number>;
|
||||
} | null>>({
|
||||
e: null,
|
||||
s: null,
|
||||
w: null,
|
||||
n: null,
|
||||
});
|
||||
|
||||
function kyokuEnd() {
|
||||
kyokuEnded.value = true;
|
||||
|
@ -359,9 +424,14 @@ function onStreamDahai(log) {
|
|||
// return;
|
||||
//}
|
||||
|
||||
engine.value.commit_dahai(log.house, log.tile);
|
||||
engine.value.commit_dahai(log.house, log.tile, log.riichi);
|
||||
triggerRef(engine);
|
||||
|
||||
riichiSerifHouses[log.house] = log.riichi;
|
||||
window.setTimeout(() => {
|
||||
riichiSerifHouses[log.house] = false;
|
||||
}, 2000);
|
||||
|
||||
myTurnTimerRmain.value = room.value.timeLimitForEachTurn;
|
||||
}
|
||||
|
||||
|
@ -402,9 +472,14 @@ function onStreamDahaiAndTsumo(log) {
|
|||
playbackRate: 1,
|
||||
});
|
||||
|
||||
engine.value.commit_dahai(log.dahaiHouse, log.dahaiTile);
|
||||
engine.value.commit_dahai(log.dahaiHouse, log.dahaiTile, log.riichi);
|
||||
triggerRef(engine);
|
||||
|
||||
riichiSerifHouses[log.dahaiHouse] = log.riichi;
|
||||
window.setTimeout(() => {
|
||||
riichiSerifHouses[log.dahaiHouse] = false;
|
||||
}, 2000);
|
||||
|
||||
window.setTimeout(() => {
|
||||
engine.value.commit_tsumo(Mahjong.Common.nextHouse(log.dahaiHouse), log.tsumoTile);
|
||||
triggerRef(engine);
|
||||
|
@ -431,9 +506,9 @@ function onStreamPonned(log) {
|
|||
engine.value.commit_pon(log.caller, log.callee);
|
||||
triggerRef(engine);
|
||||
|
||||
ponSerifHouses[log.house] = true;
|
||||
ponSerifHouses[log.caller] = true;
|
||||
window.setTimeout(() => {
|
||||
ponSerifHouses[log.house] = false;
|
||||
ponSerifHouses[log.caller] = false;
|
||||
}, 2000);
|
||||
|
||||
myTurnTimerRmain.value = room.value.timeLimitForEachTurn;
|
||||
|
@ -442,21 +517,31 @@ function onStreamPonned(log) {
|
|||
function onStreamRonned(log) {
|
||||
console.log('onStreamRonned', log);
|
||||
|
||||
engine.value.commit_ronHora(log.callers, log.callee, log.handTiles);
|
||||
const res = engine.value.commit_ronHora(log.callers, log.callee, log.handTiles);
|
||||
triggerRef(engine);
|
||||
|
||||
kyokuResults.value = res;
|
||||
kyokuEnded.value = true;
|
||||
|
||||
for (const caller of log.callers) {
|
||||
ronSerifHouses[caller] = true;
|
||||
}
|
||||
|
||||
console.log('ronned', res);
|
||||
}
|
||||
|
||||
function onStreamTsumoHora(log) {
|
||||
console.log('onStreamTsumoHora', log);
|
||||
|
||||
const res = engine.value.commit_tsumoHora(log.house, log.handTiles, log.tsumoTile);
|
||||
triggerRef(engine);
|
||||
|
||||
kyokuResults.value[log.house] = res;
|
||||
kyokuEnded.value = true;
|
||||
|
||||
tsumoSerifHouses[log.house] = true;
|
||||
|
||||
engine.value.commit_tsumoHora();
|
||||
triggerRef(engine);
|
||||
console.log('tsumohora', res);
|
||||
}
|
||||
|
||||
function restoreRoom(_room) {
|
||||
|
@ -516,6 +601,18 @@ onUnmounted(() => {
|
|||
100% { translate: -130px; }
|
||||
}
|
||||
|
||||
.transition_serif_enterActive,
|
||||
.transition_serif_leaveActive {
|
||||
transition: opacity 0.1s linear, scale 0.1s linear;
|
||||
}
|
||||
.transition_serif_enterFrom {
|
||||
scale: 1.25;
|
||||
opacity: 0;
|
||||
}
|
||||
.transition_serif_leaveTo {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.root {
|
||||
background: #3C7A43;
|
||||
background-image: url('/client-assets/mahjong/bg.jpg');
|
||||
|
@ -535,6 +632,22 @@ onUnmounted(() => {
|
|||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.kyokuResult {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1000;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 800px;
|
||||
margin: auto;
|
||||
box-sizing: border-box;
|
||||
background: #0009;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.centerPanel {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
@ -634,6 +747,24 @@ onUnmounted(() => {
|
|||
right: 80px;
|
||||
}
|
||||
|
||||
.huroTilesOfToimen {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 80px;
|
||||
}
|
||||
|
||||
.huroTilesOfKamitya {
|
||||
position: absolute;
|
||||
top: 80px;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.huroTilesOfSimotya {
|
||||
position: absolute;
|
||||
top: 80px;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.hoTilesContainer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
@ -766,6 +897,7 @@ onUnmounted(() => {
|
|||
aspect-ratio: 0.7;
|
||||
transition: translate 0.1s ease;
|
||||
cursor: pointer;
|
||||
overflow: clip;
|
||||
}
|
||||
.myTile:hover {
|
||||
translate: 0 -10px;
|
||||
|
|
|
@ -158,7 +158,7 @@ type EnvForCalcYaku = {
|
|||
house: House;
|
||||
|
||||
/**
|
||||
* 和了る人の手牌(副露した牌は含まない)
|
||||
* 和了る人の手牌(副露牌および和了る際のツモ牌・ロン牌は含まない)
|
||||
*/
|
||||
handTiles: Tile[];
|
||||
|
||||
|
@ -214,6 +214,12 @@ export const YAKU_DEFINITIONS = [{
|
|||
calc: (state: EnvForCalcYaku) => {
|
||||
return state.riichi;
|
||||
},
|
||||
}, {
|
||||
name: 'tsumo',
|
||||
fan: 1,
|
||||
calc: (state: EnvForCalcYaku) => {
|
||||
return state.tsumoTile != null;
|
||||
},
|
||||
}, {
|
||||
name: 'red',
|
||||
fan: 1,
|
||||
|
@ -272,7 +278,7 @@ export const YAKU_DEFINITIONS = [{
|
|||
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.handTiles.some(t => yaochuTiles.includes(t))) &&
|
||||
(state.huros.filter(huro =>
|
||||
huro.type === 'pon' ? yaochuTiles.includes(huro.tile) :
|
||||
huro.type === 'ankan' ? yaochuTiles.includes(huro.tile) :
|
||||
|
@ -281,6 +287,27 @@ export const YAKU_DEFINITIONS = [{
|
|||
false).length === 0)
|
||||
);
|
||||
},
|
||||
}, {
|
||||
name: 'pinfu',
|
||||
fan: 1,
|
||||
calc: (state: EnvForCalcYaku) => {
|
||||
// 面前じゃないとダメ
|
||||
if (state.huros.length !== 0) return false;
|
||||
// 三元牌はダメ
|
||||
if (state.handTiles.some(t => ['haku', 'hatsu', 'chun'].includes(t))) return false;
|
||||
|
||||
// TODO: 両面待ちかどうか
|
||||
|
||||
const horaSets = getHoraSets(state.handTiles.concat(state.tsumoTile ?? state.ronTile));
|
||||
return horaSets.some(horaSet => {
|
||||
// 風牌判定(役牌でなければOK)
|
||||
if (horaSet.head === state.seatWind) return false;
|
||||
if (horaSet.head === state.fieldWind) return false;
|
||||
|
||||
// 全て順子か?
|
||||
if (horaSet.mentsus.some((mentsu) => mentsu[0] === mentsu[1])) return false;
|
||||
});
|
||||
},
|
||||
}];
|
||||
|
||||
export function fanToPoint(fan: number, isParent: boolean): number {
|
||||
|
@ -423,29 +450,44 @@ export const SHUNTU_PATTERNS: [Tile, Tile, Tile][] = [
|
|||
['s7', 's8', 's9'],
|
||||
];
|
||||
|
||||
const SHUNTU_PATTERN_IDS = [
|
||||
'm123',
|
||||
'm234',
|
||||
'm345',
|
||||
'm456',
|
||||
'm567',
|
||||
'm678',
|
||||
'm789',
|
||||
'p123',
|
||||
'p234',
|
||||
'p345',
|
||||
'p456',
|
||||
'p567',
|
||||
'p678',
|
||||
'p789',
|
||||
's123',
|
||||
's234',
|
||||
's345',
|
||||
's456',
|
||||
's567',
|
||||
's678',
|
||||
's789',
|
||||
] as const;
|
||||
export type KyokuResult = {
|
||||
yakus: { name: string; fan: number; }[];
|
||||
doraCount: number;
|
||||
pointDeltas: { e: number; s: number; w: number; n: number; };
|
||||
};
|
||||
|
||||
function extractShuntsus(tiles: Tile[]): [Tile, Tile, Tile][] {
|
||||
const tempTiles = [...tiles];
|
||||
|
||||
tempTiles.sort((a, b) => {
|
||||
const aIndex = TILE_TYPES.indexOf(a);
|
||||
const bIndex = TILE_TYPES.indexOf(b);
|
||||
return aIndex - bIndex;
|
||||
});
|
||||
|
||||
const shuntsus: [Tile, Tile, Tile][] = [];
|
||||
while (tempTiles.length > 0) {
|
||||
let isShuntu = false;
|
||||
for (const shuntuPattern of SHUNTU_PATTERNS) {
|
||||
if (
|
||||
tempTiles[0] === shuntuPattern[0] &&
|
||||
tempTiles.includes(shuntuPattern[1]) &&
|
||||
tempTiles.includes(shuntuPattern[2])
|
||||
) {
|
||||
shuntsus.push(shuntuPattern);
|
||||
tempTiles.splice(0, 1);
|
||||
tempTiles.splice(tempTiles.indexOf(shuntuPattern[1]), 1);
|
||||
tempTiles.splice(tempTiles.indexOf(shuntuPattern[2]), 1);
|
||||
isShuntu = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isShuntu) tempTiles.splice(0, 1);
|
||||
}
|
||||
|
||||
return shuntsus;
|
||||
}
|
||||
|
||||
/**
|
||||
* アガリ形パターン一覧を取得
|
||||
|
@ -537,34 +579,7 @@ export function getHoraSets(handTiles: Tile[]): HoraSet[] {
|
|||
tempHandTilesWithoutKotsu.splice(tempHandTilesWithoutKotsu.indexOf(kotsuTile), 1);
|
||||
}
|
||||
|
||||
tempHandTilesWithoutKotsu.sort((a, b) => {
|
||||
const aIndex = TILE_TYPES.indexOf(a);
|
||||
const bIndex = TILE_TYPES.indexOf(b);
|
||||
return aIndex - bIndex;
|
||||
});
|
||||
|
||||
const tempHandTilesWithoutKotsuAndShuntsu: (Tile | null)[] = [...tempHandTilesWithoutKotsu];
|
||||
|
||||
const shuntsus: [Tile, Tile, Tile][] = [];
|
||||
while (tempHandTilesWithoutKotsuAndShuntsu.length > 0) {
|
||||
let isShuntu = false;
|
||||
for (const shuntuPattern of SHUNTU_PATTERNS) {
|
||||
if (
|
||||
tempHandTilesWithoutKotsuAndShuntsu[0] === shuntuPattern[0] &&
|
||||
tempHandTilesWithoutKotsuAndShuntsu.includes(shuntuPattern[1]) &&
|
||||
tempHandTilesWithoutKotsuAndShuntsu.includes(shuntuPattern[2])
|
||||
) {
|
||||
shuntsus.push(shuntuPattern);
|
||||
tempHandTilesWithoutKotsuAndShuntsu.splice(0, 1);
|
||||
tempHandTilesWithoutKotsuAndShuntsu.splice(tempHandTilesWithoutKotsuAndShuntsu.indexOf(shuntuPattern[1]), 1);
|
||||
tempHandTilesWithoutKotsuAndShuntsu.splice(tempHandTilesWithoutKotsuAndShuntsu.indexOf(shuntuPattern[2]), 1);
|
||||
isShuntu = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isShuntu) tempHandTilesWithoutKotsuAndShuntsu.splice(0, 1);
|
||||
}
|
||||
const shuntsus = extractShuntsus(tempHandTilesWithoutKotsu);
|
||||
|
||||
if (shuntsus.length * 3 === tempHandTilesWithoutKotsu.length) { // アガリ形
|
||||
horaSets.push({
|
||||
|
|
|
@ -380,6 +380,7 @@ export class MasterGameEngine {
|
|||
return {
|
||||
asking: false as const,
|
||||
tsumoTile: tsumoTile,
|
||||
next: this.state.turn,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -439,6 +440,11 @@ export class MasterGameEngine {
|
|||
console.log('yakus', house, yakus);
|
||||
|
||||
this.endKyoku();
|
||||
|
||||
return {
|
||||
handTiles: this.state.handTiles[house],
|
||||
tsumoTile: this.state.handTiles[house].at(-1)!,
|
||||
};
|
||||
}
|
||||
|
||||
public commit_resolveCallAndRonInterruption(answers: {
|
||||
|
|
|
@ -64,6 +64,12 @@ export type PlayerState = {
|
|||
canRonTo: House | null;
|
||||
};
|
||||
|
||||
export type KyokuResult = {
|
||||
yakus: { name: string; fan: number; }[];
|
||||
doraCount: number;
|
||||
pointDeltas: { e: number; s: number; w: number; n: number; };
|
||||
};
|
||||
|
||||
export class PlayerGameEngine {
|
||||
/**
|
||||
* このエラーが発生したときはdesyncが疑われる
|
||||
|
@ -145,10 +151,33 @@ export class PlayerGameEngine {
|
|||
if (this.state.turn !== house) throw new PlayerGameEngine.InvalidOperationError();
|
||||
}
|
||||
|
||||
public commit_tsumoHora(house: House) {
|
||||
public commit_tsumoHora(house: House, handTiles: Tile[], tsumoTile: Tile): KyokuResult {
|
||||
console.log('commit_tsumoHora', this.state.turn, house);
|
||||
|
||||
// TODO: ツモした人の手牌情報を貰う必要がある
|
||||
const yakus = YAKU_DEFINITIONS.filter(yaku => yaku.calc({
|
||||
house: house,
|
||||
handTiles: handTiles,
|
||||
huros: this.state.huros[house],
|
||||
tsumoTile: tsumoTile,
|
||||
ronTile: null,
|
||||
riichi: this.state.riichis[house],
|
||||
}));
|
||||
const doraCount = Common.calcOwnedDoraCount(handTiles, this.state.huros[house], this.doras);
|
||||
const fans = yakus.map(yaku => yaku.fan).reduce((a, b) => a + b, 0) + doraCount;
|
||||
const pointDeltas = Common.calcTsumoHoraPointDeltas(house, fans);
|
||||
this.state.points.e += pointDeltas.e;
|
||||
this.state.points.s += pointDeltas.s;
|
||||
this.state.points.w += pointDeltas.w;
|
||||
this.state.points.n += pointDeltas.n;
|
||||
|
||||
return {
|
||||
yakus: yakus.map(yaku => ({
|
||||
name: yaku.name,
|
||||
fan: yaku.fan,
|
||||
})),
|
||||
doraCount,
|
||||
pointDeltas,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -161,23 +190,16 @@ export class PlayerGameEngine {
|
|||
s: Tile[];
|
||||
w: Tile[];
|
||||
n: Tile[];
|
||||
}) {
|
||||
}): Record<House, KyokuResult | null> {
|
||||
console.log('commit_ronHora', this.state.turn, callers, callee);
|
||||
|
||||
this.state.canRonSource = null;
|
||||
|
||||
const yakusMap: Record<House, { name: string; fan: number; }[]> = {
|
||||
e: [] as { name: string; fan: number; }[],
|
||||
s: [] as { name: string; fan: number; }[],
|
||||
w: [] as { name: string; fan: number; }[],
|
||||
n: [] as { name: string; fan: number; }[],
|
||||
};
|
||||
|
||||
const doraCountsMap: Record<House, number> = {
|
||||
e: 0,
|
||||
s: 0,
|
||||
w: 0,
|
||||
n: 0,
|
||||
const resultMap: Record<House, KyokuResult> = {
|
||||
e: { yakus: [], doraCount: 0, pointDeltas: { e: 0, s: 0, w: 0, n: 0 } },
|
||||
s: { yakus: [], doraCount: 0, pointDeltas: { e: 0, s: 0, w: 0, n: 0 } },
|
||||
w: { yakus: [], doraCount: 0, pointDeltas: { e: 0, s: 0, w: 0, n: 0 } },
|
||||
n: { yakus: [], doraCount: 0, pointDeltas: { e: 0, s: 0, w: 0, n: 0 } },
|
||||
};
|
||||
|
||||
for (const house of callers) {
|
||||
|
@ -194,14 +216,17 @@ export class PlayerGameEngine {
|
|||
const point = Common.fanToPoint(fans, house === 'e');
|
||||
this.state.points[callee] -= point;
|
||||
this.state.points[house] += point;
|
||||
yakusMap[house] = yakus.map(yaku => ({ name: yaku.name, fan: yaku.fan }));
|
||||
doraCountsMap[house] = doraCount;
|
||||
console.log('yakus', house, yakus);
|
||||
resultMap[house].yakus = yakus.map(yaku => ({ name: yaku.name, fan: yaku.fan }));
|
||||
resultMap[house].doraCount = doraCount;
|
||||
resultMap[house].pointDeltas[callee] = -point;
|
||||
resultMap[house].pointDeltas[house] = point;
|
||||
}
|
||||
|
||||
return {
|
||||
yakusMap,
|
||||
doraCountsMap,
|
||||
e: callers.includes('e') ? resultMap.e : null,
|
||||
s: callers.includes('s') ? resultMap.s : null,
|
||||
w: callers.includes('w') ? resultMap.w : null,
|
||||
n: callers.includes('n') ? resultMap.n : null,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue