feat: 添加落子功能

This commit is contained in:
Litrix2 2024-12-18 22:47:58 +08:00
parent e3440c72e2
commit 8d5a19248c
9 changed files with 125 additions and 55 deletions

View File

@ -37,6 +37,7 @@
"@types/crypto-js": "^4.2.2",
"@types/lodash-es": "^4.17.12",
"@types/node": "^20.17.9",
"@unocss/transformer-directives": "^0.65.1",
"@vitejs/plugin-legacy": "^6.0.0",
"@vitejs/plugin-vue": "^5.2.1",
"@vitejs/plugin-vue-jsx": "^4.1.1",

View File

@ -1,6 +1,6 @@
<template>
<el-container class="app__container">
<el-header class="app-header flex items-center">
<el-header class="app-header flex items-center bg-white">
<el-icon v-show="mdLess" size="30" @click="showVerticalHeaderMenu = true">
<icon-cs-menu />
</el-icon>
@ -85,7 +85,6 @@
z-index: 100;
user-select: none;
top: 0;
background-color: white;
border-bottom: 1px solid var(--el-border-color);
//
.app-header-user {

View File

@ -2,7 +2,11 @@
<transition appear name="game-container" @after-enter="onContainerEnterComplete">
<div :style="containerSizeStyle" class="game-container">
<transition name="mask">
<div v-show="gameStatus !== 'playing' && !hideMask" class="mask" @click="onMaskClick">
<div
v-show="gameStatus !== 'playing' && !hideMask"
class="mask bg-white bg-op-50"
@click="onMaskClick"
>
<div v-if="gameStatus === 'succeed'">
<b>你赢了!</b>
<div>🎉🥳🎊</div>
@ -120,7 +124,6 @@
align-items: center;
font-size: 50px;
text-align: center;
background-color: rgb(white, 0.5);
color: rgb(119, 110, 101);
z-index: 1;
user-select: none;

View File

@ -1,5 +1,5 @@
<template>
<el-header class="gobang-header flex items-center">
<el-header class="gobang-header flex items-center bg-white">
<router-link custom :to="{ name: 'GobangList' }" v-slot="{ navigate }">
<div class="gobang-header__left flex items-center" @click="navigate">
<el-icon :size="30"><icon-cs-gobang /></el-icon>
@ -12,7 +12,6 @@
<style lang="scss" scoped>
.gobang-header {
--el-header-height: var(--header-height);
background-color: white;
border-bottom: 1px solid var(--el-border-color);
position: fixed;
top: var(--header-height);

View File

@ -1,5 +1,8 @@
<template>
<div class="gobang-play-page-aside-user flex justify-center items-center" :class="{ 'flex-col': !footer }">
<div
class="gobang-play-page-aside-user flex justify-center items-center"
:class="{ 'flex-col': !footer }"
>
<el-avatar :size="40" :icon="user.avatar ?? undefined" />
<div class="gobang-play-page-aside-user__name">{{ user.name }}</div>
</div>
@ -11,5 +14,7 @@
</style>
<script setup lang="ts">
import type { UserInfo } from '@/schemas';
const props = defineProps<{ user: UserInfo; footer: boolean }>();
withDefaults(defineProps<{ user: UserInfo; footer?: boolean }>(), {
footer: false,
});
</script>

View File

@ -1,5 +1,4 @@
import router from '@/router';
import { useUserStore } from '@/stores/user';
import { useWebSocket } from '@vueuse/core';
import { ElMessage } from 'element-plus';
import type { ValueOf } from 'element-plus/es/components/table/src/table-column/defaults.mjs';
@ -51,7 +50,6 @@ export function useGameSocket<TRequest extends SimplePart<string>, TResp extends
(reversedRelations[v] ??= new Set()).add(k);
}
}
const userStore = useUserStore();
const relationMap = new Map<keyof TRelations, [TRequest, Set<string>]>();
const ws = useWebSocket<string>(options.url, {
// autoReconnect: {

View File

@ -1,5 +1,5 @@
<template>
<el-main class="gobang-list-page__wrapper !flex">
<el-main class="gobang-list-page__wrapper !flex bg-white">
<el-container>
<gobang-header class="justify-between">
<div class="flex items-center">
@ -44,9 +44,6 @@
</el-main>
</template>
<style lang="scss" scoped>
.gobang-list-page__wrapper {
background-color: white;
}
.gobang-list-page-header__title {
font-size: 25px;
font-weight: bold;
@ -92,7 +89,8 @@ export type Request =
| SimplePart<'RoomList'>
| SimplePart<'CreateRoom'>
| PayloadPart<'PlayerJoin', { roomId: RoomId }>
| SimplePart<'ResetRoom'>;
| SimplePart<'ResetRoom'>
| PayloadPart<'PlaceChessPiece', { x: number; y: number }>;
export type Resp =
| PayloadPart<'UserInfo', UserInfo>
| PayloadPart<'RoomList', { rooms: Room[] }>
@ -105,6 +103,7 @@ export const relations = {
RoomList: 'RoomList',
CreateRoom: 'RoomCreated',
PlayerJoin: ['PlayerSideAllocation', 'RoomInfo'],
PlaceChessPiece: ['RoomInfo'],
} as const;
export function useGobangSocket(
options: Omit<UseGameSocketOptions<Request, Resp, typeof relations>, 'url' | 'relations'>,

View File

@ -1,9 +1,6 @@
<template>
<el-main
v-loading="state !== 'GAMING' && state !== 'WAITING'"
class="gobang-play-page__wrapper !flex"
>
<el-container direction="vertical" v-if="state !== 'FIRST_LOADING'">
<el-main v-loading="firstLoading || placing" class="gobang-play-page__wrapper !flex bg-white">
<el-container direction="vertical">
<gobang-header class="justify-between">
<div class="flex items-center">
<template v-if="roomId">
@ -22,17 +19,18 @@
<div class="gobang__state">
<!-- {{ stateDisplayMap[state] }} -->
</div>
<div class="gobang-user">
<div class="gobang-user__name">{{}}</div>
</div>
<div class="gobang-chessboard">
<div
class="gobang-chessboard"
:class="{
'gobang-chessboard--enabled': enabled,
}"
>
<canvas class="gobang-chessboard__background" ref="canvas"></canvas>
<template v-for="(row, y) of grid">
<template v-for="(cell, x) of row">
<!-- eslint-disable-next-line vue/require-v-for-key -->
<div
v-if="state !== 'WAITING'"
class="gobang-chessboard__cell"
class="gobang-chessboard__cell absolute border border-black rounded-full"
:class="[
cell
? cell.isWhite
@ -41,6 +39,7 @@
: undefined,
]"
:style="getCellStyle(cell, x, y)"
@click="onCellClick(cell, x, y)"
></div>
</template>
</template>
@ -50,8 +49,28 @@
v-if="!mdLess"
class="gobang-play-page-aside !flex flex-col items-center justify-between"
>
<gobang-user v-if="otherUser" :footer="false" :user="otherUser" />
<div class="gobang-play-page-aside__vs absolute-self-center">VS</div>
<div><gobang-user v-if="otherUser" :footer="false" :user="otherUser" /></div>
<div>
<div
v-if="otherUser"
class="absolute-self-center font-bold text-12.5 flex flex-col items-center select-none"
>
VS
</div>
<div
v-else
class="absolute-self-center text-center text-5 font-bold w-100% select-none"
>
等待玩家加入.
</div>
</div>
<div class="flex flex-col items-center">
<gobang-user v-if="userStore.userInfo" :user="userStore.userInfo" />
<div class="font-bold text-5">
<template v-if="selfRound">请落子.</template>
<template v-else>等待对方落子.</template>
</div>
</div>
</el-aside>
</el-container>
<el-footer v-if="mdLess" class="gobang-play-page-footer flex items-center">
@ -62,9 +81,6 @@
</template>
<style lang="scss" scoped>
.gobang-play-page {
&__wrapper {
background-color: white;
}
&-main {
--el-main-padding: 0;
margin-top: var(--header-height);
@ -76,10 +92,6 @@
border-left: 1px solid var(--el-border-color);
width: 200px;
padding: 80px 0;
&__vs {
font-size: 50px;
font-weight: bold;
}
}
&-footer {
border-top: 1px solid var(--el-border-color);
@ -92,39 +104,37 @@
zoom: v-bind(boardZoom);
&__cell {
--size: 24px;
position: absolute;
transform: translate(-50%, -50%);
width: var(--size);
height: var(--size);
border-radius: 50%;
user-select: none;
@mixin border {
border: 1px solid black;
}
&--white {
@include border;
background-color: white;
@apply bg-white;
}
&--black {
@include border;
background-color: black;
@apply bg-black;
}
&:not(&--white, &--black):hover {
background-color: red;
opacity: 0.5;
}
&--enabled {
.gobang-chessboard__cell {
&:not(&--white, &--black):hover {
background-color: rgb(red, 0.5);
cursor: pointer;
}
}
}
}
</style>
<script setup lang="ts">
import GobangUser from '@/components/gobang/GobangUser.vue';
import GobangHeader from '@/components/gobang/GobangHeader.vue';
import GobangUser from '@/components/gobang/GobangUser.vue';
import router from '@/router';
import { useMediaStore } from '@/stores/media';
import { useUserStore } from '@/stores/user';
import { useGobangSocket, type RoomDetail, type RoomId } from '@/views/GobangListPage.vue';
import { ElMessage } from 'element-plus';
import { storeToRefs } from 'pinia';
import { computed, onMounted, ref, watchEffect, type CSSProperties } from 'vue';
import { computed, onMounted, ref, watch, watchEffect, type CSSProperties } from 'vue';
import { useRoute } from 'vue-router';
const userStore = useUserStore();
const { smLess, mdLess } = storeToRefs(useMediaStore());
@ -200,20 +210,62 @@ function getCellStyle(chess: Chess | undefined, x: number, y: number) {
};
return res;
}
type State = 'FIRST_LOADING' | 'LOADING' | 'GAMING' | 'WAITING';
const firstLoading = ref(true);
enum MatchState {
WAITING,
GAMING,
FINISHED,
}
const matchState = ref<MatchState>(MatchState.WAITING);
function updateMatchState(finished?: boolean) {
if (finished) {
matchState.value = MatchState.FINISHED;
return;
}
matchState.value = otherUser.value ? MatchState.GAMING : MatchState.WAITING;
}
watch(matchState, (v) => {
console.log(v);
switch (v) {
case MatchState.GAMING:
ElMessage('对局开始');
break;
case MatchState.FINISHED:
resetLoadingStates();
break;
}
});
const placing = ref(false);
const selfRound = ref(false);
const enabled = computed(
() => matchState.value === MatchState.GAMING && selfRound.value && !placing.value,
);
function resetLoadingStates() {
placing.value = false;
}
const room = ref<RoomDetail>();
const otherUser = computed(() => {
if (!room.value) return;
const { whiteUser, blackUser } = room.value;
return [whiteUser, blackUser].find((v) => v && v.id !== userStore.userInfo!.id);
});
watch(otherUser, (v, old) => {
updateMatchState();
});
const grid = computed<Grid | undefined>(() =>
room.value?.pieces.map((row) =>
row.map((v) => (v === 0 ? { isWhite: false } : v === 1 ? { isWhite: true } : undefined)),
),
);
const state = ref<State>('FIRST_LOADING');
const isWhite = ref(false);
function onCellClick(cell: Chess | undefined, x: number, y: number) {
if (cell || !enabled.value) return;
placing.value = true;
send({
name: 'PlaceChessPiece',
payload: { x, y },
});
}
const { send } = useGobangSocket({
succeed: {
UserInfo() {
@ -226,29 +278,41 @@ const { send } = useGobangSocket({
});
},
PlayerSideAllocation(p) {
isWhite.value = p.isWhite;
console.log(p.isWhite);
selfRound.value = !(isWhite.value = p.isWhite);
},
RoomInfo(p) {
room.value = p;
if (matchState.value === MatchState.GAMING) {
selfRound.value = !selfRound.value;
}
},
PlayerLeave() {
ElMessage.warning('对方已离开房间,对局自动结束');
room.value = undefined;
},
},
error: {
PlayerJoin() {
router.back();
queueMicrotask(() => router.back());
return false;
},
},
finally: {
PlayerJoin(error) {
if (!error) {
state.value = 'GAMING';
firstLoading.value = false;
updateMatchState();
}
},
PlaceChessPiece() {
placing.value = false;
},
},
});
onMounted(() => {
if (!roomId) {
state.value = 'GAMING';
matchState.value = MatchState.GAMING;
}
});
</script>

View File

@ -1,4 +1,5 @@
import { defineConfig } from 'unocss';
import { transformerDirectives } from 'unocss';
export default defineConfig({
rules: [
[
@ -11,4 +12,5 @@ export default defineConfig({
},
],
],
transformers: [transformerDirectives()],
});