forked from mirror/misskey
enhance(reversi): tweak reversi
This commit is contained in:
parent
6039f27bd5
commit
b3cc17ea0d
BIN
packages/frontend/assets/reversi/lose.mp3
Normal file
BIN
packages/frontend/assets/reversi/lose.mp3
Normal file
Binary file not shown.
BIN
packages/frontend/assets/reversi/win.mp3
Normal file
BIN
packages/frontend/assets/reversi/win.mp3
Normal file
Binary file not shown.
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MkSpacer :contentMax="600">
|
<MkSpacer :contentMax="500">
|
||||||
<div :class="$style.root" class="_gaps">
|
<div :class="$style.root" class="_gaps">
|
||||||
<div style="display: flex; align-items: center; justify-content: center; gap: 10px;">
|
<div style="display: flex; align-items: center; justify-content: center; gap: 10px;">
|
||||||
<span>({{ i18n.ts._reversi.black }})</span>
|
<span>({{ i18n.ts._reversi.black }})</span>
|
||||||
@ -35,53 +35,55 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div :class="$style.board">
|
<div :class="$style.board">
|
||||||
<div v-if="showBoardLabels" :class="$style.labelsX">
|
<div :class="$style.boardInner">
|
||||||
<span v-for="i in game.map[0].length" :class="$style.labelsXLabel">{{ String.fromCharCode(64 + i) }}</span>
|
<div v-if="showBoardLabels" :class="$style.labelsX">
|
||||||
</div>
|
<span v-for="i in game.map[0].length" :class="$style.labelsXLabel">{{ String.fromCharCode(64 + i) }}</span>
|
||||||
<div style="display: flex;">
|
|
||||||
<div v-if="showBoardLabels" :class="$style.labelsY">
|
|
||||||
<div v-for="i in game.map.length" :class="$style.labelsYLabel">{{ i }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.boardCells" :style="cellsStyle">
|
<div style="display: flex;">
|
||||||
<div
|
<div v-if="showBoardLabels" :class="$style.labelsY">
|
||||||
v-for="(stone, i) in engine.board"
|
<div v-for="i in game.map.length" :class="$style.labelsYLabel">{{ i }}</div>
|
||||||
:key="i"
|
</div>
|
||||||
v-tooltip="`${String.fromCharCode(65 + engine.posToXy(i)[0])}${engine.posToXy(i)[1] + 1}`"
|
<div :class="$style.boardCells" :style="cellsStyle">
|
||||||
:class="[$style.boardCell, {
|
<div
|
||||||
[$style.boardCell_empty]: stone == null,
|
v-for="(stone, i) in engine.board"
|
||||||
[$style.boardCell_none]: engine.map[i] === 'null',
|
:key="i"
|
||||||
[$style.boardCell_isEnded]: game.isEnded,
|
v-tooltip="`${String.fromCharCode(65 + engine.posToXy(i)[0])}${engine.posToXy(i)[1] + 1}`"
|
||||||
[$style.boardCell_myTurn]: !game.isEnded && isMyTurn,
|
:class="[$style.boardCell, {
|
||||||
[$style.boardCell_can]: turnUser ? engine.canPut(turnUser.id === blackUser.id, i) : null,
|
[$style.boardCell_empty]: stone == null,
|
||||||
[$style.boardCell_prev]: engine.prevPos === i
|
[$style.boardCell_none]: engine.map[i] === 'null',
|
||||||
}]"
|
[$style.boardCell_isEnded]: game.isEnded,
|
||||||
@click="putStone(i)"
|
[$style.boardCell_myTurn]: !game.isEnded && isMyTurn,
|
||||||
>
|
[$style.boardCell_can]: turnUser ? engine.canPut(turnUser.id === blackUser.id, i) : null,
|
||||||
<Transition
|
[$style.boardCell_prev]: engine.prevPos === i
|
||||||
:enterActiveClass="$style.transition_flip_enterActive"
|
}]"
|
||||||
:leaveActiveClass="$style.transition_flip_leaveActive"
|
@click="putStone(i)"
|
||||||
:enterFromClass="$style.transition_flip_enterFrom"
|
|
||||||
:leaveToClass="$style.transition_flip_leaveTo"
|
|
||||||
mode="default"
|
|
||||||
>
|
>
|
||||||
<template v-if="useAvatarAsStone">
|
<Transition
|
||||||
<img v-if="stone === true" :class="$style.boardCellStone" :src="blackUser.avatarUrl"/>
|
:enterActiveClass="$style.transition_flip_enterActive"
|
||||||
<img v-else-if="stone === false" :class="$style.boardCellStone" :src="whiteUser.avatarUrl"/>
|
:leaveActiveClass="$style.transition_flip_leaveActive"
|
||||||
</template>
|
:enterFromClass="$style.transition_flip_enterFrom"
|
||||||
<template v-else>
|
:leaveToClass="$style.transition_flip_leaveTo"
|
||||||
<img v-if="stone === true" :class="$style.boardCellStone" src="/client-assets/reversi/stone_b.png"/>
|
mode="default"
|
||||||
<img v-else-if="stone === false" :class="$style.boardCellStone" src="/client-assets/reversi/stone_w.png"/>
|
>
|
||||||
</template>
|
<template v-if="useAvatarAsStone">
|
||||||
</Transition>
|
<img v-if="stone === true" :class="$style.boardCellStone" :src="blackUser.avatarUrl"/>
|
||||||
|
<img v-else-if="stone === false" :class="$style.boardCellStone" :src="whiteUser.avatarUrl"/>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<img v-if="stone === true" :class="$style.boardCellStone" src="/client-assets/reversi/stone_b.png"/>
|
||||||
|
<img v-else-if="stone === false" :class="$style.boardCellStone" src="/client-assets/reversi/stone_w.png"/>
|
||||||
|
</template>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="showBoardLabels" :class="$style.labelsY">
|
||||||
|
<div v-for="i in game.map.length" :class="$style.labelsYLabel">{{ i }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="showBoardLabels" :class="$style.labelsY">
|
<div v-if="showBoardLabels" :class="$style.labelsX">
|
||||||
<div v-for="i in game.map.length" :class="$style.labelsYLabel">{{ i }}</div>
|
<span v-for="i in game.map[0].length" :class="$style.labelsXLabel">{{ String.fromCharCode(64 + i) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="showBoardLabels" :class="$style.labelsX">
|
|
||||||
<span v-for="i in game.map[0].length" :class="$style.labelsXLabel">{{ String.fromCharCode(64 + i) }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="game.isEnded" class="_panel _gaps_s" style="padding: 16px;">
|
<div v-if="game.isEnded" class="_panel _gaps_s" style="padding: 16px;">
|
||||||
@ -155,6 +157,7 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
|
|||||||
import { userPage } from '@/filters/user.js';
|
import { userPage } from '@/filters/user.js';
|
||||||
import * as sound from '@/scripts/sound.js';
|
import * as sound from '@/scripts/sound.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
|
import { confetti } from '@/scripts/confetti.js';
|
||||||
|
|
||||||
const $i = signinRequired();
|
const $i = signinRequired();
|
||||||
|
|
||||||
@ -329,6 +332,22 @@ function onStreamLog(log: Reversi.Serializer.Log & { id: string | null }) {
|
|||||||
|
|
||||||
function onStreamEnded(x) {
|
function onStreamEnded(x) {
|
||||||
game.value = deepClone(x.game);
|
game.value = deepClone(x.game);
|
||||||
|
|
||||||
|
if (game.value.winnerId === $i.id) {
|
||||||
|
confetti({
|
||||||
|
duration: 1000 * 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
sound.playUrl('/client-assets/reversi/win.mp3', {
|
||||||
|
volume: 1,
|
||||||
|
playbackRate: 1,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
sound.playUrl('/client-assets/reversi/lose.mp3', {
|
||||||
|
volume: 1,
|
||||||
|
playbackRate: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkEnd() {
|
function checkEnd() {
|
||||||
@ -465,8 +484,27 @@ $gap: 4px;
|
|||||||
|
|
||||||
.board {
|
.board {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 500px;
|
box-sizing: border-box;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|
||||||
|
padding: 7px;
|
||||||
|
background: #8C4F26;
|
||||||
|
box-shadow: 0 6px 16px #0007, 0 0 1px 1px #693410, inset 0 0 2px 1px #ce8a5c;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boardInner {
|
||||||
|
padding: 32px;
|
||||||
|
|
||||||
|
background: var(--panel);
|
||||||
|
box-shadow: 0 0 2px 1px #ce8a5c, inset 0 0 1px 1px #693410;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@container (max-width: 400px) {
|
||||||
|
.boardInner {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.labelsX {
|
.labelsX {
|
||||||
|
@ -8,72 +8,74 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<MkSpacer :contentMax="600">
|
<MkSpacer :contentMax="600">
|
||||||
<div style="text-align: center;"><b><MkUserName :user="game.user1"/></b> vs <b><MkUserName :user="game.user2"/></b></div>
|
<div style="text-align: center;"><b><MkUserName :user="game.user1"/></b> vs <b><MkUserName :user="game.user2"/></b></div>
|
||||||
|
|
||||||
<div class="_gaps">
|
<div :class="{ [$style.disallow]: isReady }">
|
||||||
<div style="font-size: 1.5em; text-align: center;">{{ i18n.ts._reversi.gameSettings }}</div>
|
<div class="_gaps" :class="{ [$style.disallowInner]: isReady }">
|
||||||
|
<div style="font-size: 1.5em; text-align: center;">{{ i18n.ts._reversi.gameSettings }}</div>
|
||||||
|
|
||||||
<div class="_panel">
|
<div class="_panel">
|
||||||
<div style="display: flex; align-items: center; padding: 16px; border-bottom: solid 1px var(--divider);">
|
<div style="display: flex; align-items: center; padding: 16px; border-bottom: solid 1px var(--divider);">
|
||||||
<div>{{ mapName }}</div>
|
<div>{{ mapName }}</div>
|
||||||
<MkButton style="margin-left: auto;" @click="chooseMap">{{ i18n.ts._reversi.chooseBoard }}</MkButton>
|
<MkButton style="margin-left: auto;" @click="chooseMap">{{ i18n.ts._reversi.chooseBoard }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="padding: 16px;">
|
<div style="padding: 16px;">
|
||||||
<div v-if="game.map == null"><i class="ti ti-dice"></i></div>
|
<div v-if="game.map == null"><i class="ti ti-dice"></i></div>
|
||||||
<div v-else :class="$style.board" :style="{ 'grid-template-rows': `repeat(${ game.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.map[0].length }, 1fr)` }">
|
<div v-else :class="$style.board" :style="{ 'grid-template-rows': `repeat(${ game.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.map[0].length }, 1fr)` }">
|
||||||
<div v-for="(x, i) in game.map.join('')" :class="[$style.boardCell, { [$style.boardCellNone]: x == ' ' }]" @click="onMapCellClick(i, x)">
|
<div v-for="(x, i) in game.map.join('')" :class="[$style.boardCell, { [$style.boardCellNone]: x == ' ' }]" @click="onMapCellClick(i, x)">
|
||||||
<i v-if="x === 'b' || x === 'w'" style="pointer-events: none; user-select: none;" :class="x === 'b' ? 'ti ti-circle-filled' : 'ti ti-circle'"></i>
|
<i v-if="x === 'b' || x === 'w'" style="pointer-events: none; user-select: none;" :class="x === 'b' ? 'ti ti-circle-filled' : 'ti ti-circle'"></i>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<MkFolder :defaultOpen="true">
|
||||||
|
<template #label>{{ i18n.ts._reversi.blackOrWhite }}</template>
|
||||||
|
|
||||||
|
<MkRadios v-model="game.bw">
|
||||||
|
<option value="random">{{ i18n.ts.random }}</option>
|
||||||
|
<option :value="'1'">
|
||||||
|
<I18n :src="i18n.ts._reversi.blackIs" tag="span">
|
||||||
|
<template #name>
|
||||||
|
<b><MkUserName :user="game.user1"/></b>
|
||||||
|
</template>
|
||||||
|
</I18n>
|
||||||
|
</option>
|
||||||
|
<option :value="'2'">
|
||||||
|
<I18n :src="i18n.ts._reversi.blackIs" tag="span">
|
||||||
|
<template #name>
|
||||||
|
<b><MkUserName :user="game.user2"/></b>
|
||||||
|
</template>
|
||||||
|
</I18n>
|
||||||
|
</option>
|
||||||
|
</MkRadios>
|
||||||
|
</MkFolder>
|
||||||
|
|
||||||
|
<MkFolder :defaultOpen="true">
|
||||||
|
<template #label>{{ i18n.ts._reversi.timeLimitForEachTurn }}</template>
|
||||||
|
<template #suffix>{{ game.timeLimitForEachTurn }}{{ i18n.ts._time.second }}</template>
|
||||||
|
|
||||||
|
<MkRadios v-model="game.timeLimitForEachTurn">
|
||||||
|
<option :value="5">5{{ i18n.ts._time.second }}</option>
|
||||||
|
<option :value="10">10{{ i18n.ts._time.second }}</option>
|
||||||
|
<option :value="30">30{{ i18n.ts._time.second }}</option>
|
||||||
|
<option :value="60">60{{ i18n.ts._time.second }}</option>
|
||||||
|
<option :value="90">90{{ i18n.ts._time.second }}</option>
|
||||||
|
<option :value="120">120{{ i18n.ts._time.second }}</option>
|
||||||
|
<option :value="180">180{{ i18n.ts._time.second }}</option>
|
||||||
|
<option :value="3600">3600{{ i18n.ts._time.second }}</option>
|
||||||
|
</MkRadios>
|
||||||
|
</MkFolder>
|
||||||
|
|
||||||
|
<MkFolder :defaultOpen="true">
|
||||||
|
<template #label>{{ i18n.ts._reversi.rules }}</template>
|
||||||
|
|
||||||
|
<div class="_gaps_s">
|
||||||
|
<MkSwitch v-model="game.isLlotheo" @update:modelValue="updateSettings('isLlotheo')">{{ i18n.ts._reversi.isLlotheo }}</MkSwitch>
|
||||||
|
<MkSwitch v-model="game.loopedBoard" @update:modelValue="updateSettings('loopedBoard')">{{ i18n.ts._reversi.loopedMap }}</MkSwitch>
|
||||||
|
<MkSwitch v-model="game.canPutEverywhere" @update:modelValue="updateSettings('canPutEverywhere')">{{ i18n.ts._reversi.canPutEverywhere }}</MkSwitch>
|
||||||
|
</div>
|
||||||
|
</MkFolder>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MkFolder :defaultOpen="true">
|
|
||||||
<template #label>{{ i18n.ts._reversi.blackOrWhite }}</template>
|
|
||||||
|
|
||||||
<MkRadios v-model="game.bw">
|
|
||||||
<option value="random">{{ i18n.ts.random }}</option>
|
|
||||||
<option :value="'1'">
|
|
||||||
<I18n :src="i18n.ts._reversi.blackIs" tag="span">
|
|
||||||
<template #name>
|
|
||||||
<b><MkUserName :user="game.user1"/></b>
|
|
||||||
</template>
|
|
||||||
</I18n>
|
|
||||||
</option>
|
|
||||||
<option :value="'2'">
|
|
||||||
<I18n :src="i18n.ts._reversi.blackIs" tag="span">
|
|
||||||
<template #name>
|
|
||||||
<b><MkUserName :user="game.user2"/></b>
|
|
||||||
</template>
|
|
||||||
</I18n>
|
|
||||||
</option>
|
|
||||||
</MkRadios>
|
|
||||||
</MkFolder>
|
|
||||||
|
|
||||||
<MkFolder :defaultOpen="true">
|
|
||||||
<template #label>{{ i18n.ts._reversi.timeLimitForEachTurn }}</template>
|
|
||||||
<template #suffix>{{ game.timeLimitForEachTurn }}{{ i18n.ts._time.second }}</template>
|
|
||||||
|
|
||||||
<MkRadios v-model="game.timeLimitForEachTurn">
|
|
||||||
<option :value="5">5{{ i18n.ts._time.second }}</option>
|
|
||||||
<option :value="10">10{{ i18n.ts._time.second }}</option>
|
|
||||||
<option :value="30">30{{ i18n.ts._time.second }}</option>
|
|
||||||
<option :value="60">60{{ i18n.ts._time.second }}</option>
|
|
||||||
<option :value="90">90{{ i18n.ts._time.second }}</option>
|
|
||||||
<option :value="120">120{{ i18n.ts._time.second }}</option>
|
|
||||||
<option :value="180">180{{ i18n.ts._time.second }}</option>
|
|
||||||
<option :value="3600">3600{{ i18n.ts._time.second }}</option>
|
|
||||||
</MkRadios>
|
|
||||||
</MkFolder>
|
|
||||||
|
|
||||||
<MkFolder :defaultOpen="true">
|
|
||||||
<template #label>{{ i18n.ts._reversi.rules }}</template>
|
|
||||||
|
|
||||||
<div class="_gaps_s">
|
|
||||||
<MkSwitch v-model="game.isLlotheo" @update:modelValue="updateSettings('isLlotheo')">{{ i18n.ts._reversi.isLlotheo }}</MkSwitch>
|
|
||||||
<MkSwitch v-model="game.loopedBoard" @update:modelValue="updateSettings('loopedBoard')">{{ i18n.ts._reversi.loopedMap }}</MkSwitch>
|
|
||||||
<MkSwitch v-model="game.canPutEverywhere" @update:modelValue="updateSettings('canPutEverywhere')">{{ i18n.ts._reversi.canPutEverywhere }}</MkSwitch>
|
|
||||||
</div>
|
|
||||||
</MkFolder>
|
|
||||||
</div>
|
</div>
|
||||||
</MkSpacer>
|
</MkSpacer>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@ -123,7 +125,7 @@ const props = defineProps<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const game = ref<Misskey.entities.ReversiGameDetailed>(deepClone(props.game));
|
const game = ref<Misskey.entities.ReversiGameDetailed>(deepClone(props.game));
|
||||||
const isLlotheo = ref<boolean>(false);
|
|
||||||
const mapName = computed(() => {
|
const mapName = computed(() => {
|
||||||
if (game.value.map == null) return 'Random';
|
if (game.value.map == null) return 'Random';
|
||||||
const found = Object.values(Reversi.maps).find(x => x.data.join('') === game.value.map.join(''));
|
const found = Object.values(Reversi.maps).find(x => x.data.join('') === game.value.map.join(''));
|
||||||
@ -236,6 +238,15 @@ onUnmounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
.disallow {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.disallowInner {
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
.board {
|
.board {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-gap: 4px;
|
grid-gap: 4px;
|
||||||
|
Loading…
Reference in New Issue
Block a user