feat: 初步完成五子棋功能

This commit is contained in:
Litrix 2024-12-19 20:14:35 +08:00
parent b68d20d5ea
commit d52aa2af21
3 changed files with 140 additions and 43 deletions

View File

@ -187,7 +187,7 @@ const loginFormRules = reactive<FormRules<typeof loginFormData>>({
],
verifyCode: [
{ required: true, message: '请输入验证码' },
{ pattern: /[0-9A-Za-z]{4}/, message: '验证码不符合格式' },
{ pattern: /^[0-9A-Za-z]{4}$/, message: '验证码不符合格式' },
],
});
const logining = ref(false);

View File

@ -34,7 +34,16 @@
:disabled="(row as RoomRender).state === RoomState.GAMING"
@click="onJoinButtonClick(row)"
>
<template v-if="(row as RoomRender).state === RoomState.GAMING">已满</template>
<template
v-if="
arrayIncludes(
[RoomState.GAMING, RoomState.FINISHED],
(row as RoomRender).state,
)
"
>
已满
</template>
<template v-else>加入</template>
</el-button>
</template>
@ -70,6 +79,8 @@ export enum RoomState {
WAITING = 'WAITING',
/** 正在游戏 */
GAMING = 'GAMING',
/** 正在游戏,但对局已经结束 */
FINISHED = 'FINISHED',
}
/** 房间UUID */
export type RoomId = string;
@ -87,6 +98,9 @@ export type RoomDetail = {
pieces: (-1 | 0 | 1)[][];
whiteUser?: UserInfo;
blackUser?: UserInfo;
blackRequestRestart?: boolean;
whiteRequestRestart?: boolean;
canWhiteDown: boolean;
};
export enum WinFace {
UP = 'UP',
@ -115,10 +129,12 @@ export type Resp =
| PayloadPart<
'HasPlayerWin',
{
face: WinFace;
isWhite: boolean;
originalX: number;
originalY: number;
winInfo: {
face: WinFace;
isWhite: boolean;
originalX: number;
originalY: number;
};
}
>
| PayloadPart<'PlayerWin'>
@ -128,17 +144,27 @@ const relations = {
CreateRoom: 'RoomCreated',
PlayerJoin: ['PlayerSideAllocation', 'RoomInfo'],
PlaceChessPiece: ['RoomInfo'],
ResetRoom: ['RoomInfo'],
} as const satisfies Relations<Request['name'], Resp['name']>;
const DEBUG = true;
export function useGobangSocket(
options: Omit<UseGameSocketOptions<Request, Resp, typeof relations>, 'url' | 'relations'>,
) {
return useGameSocket<Request, Resp>()(
Object.assign(options, {
url: `wss://wzpmc.cn:18080/chess/${useUserStore().token}`,
url: DEBUG
? `ws://172.16.114.84:58080/chess/${useUserStore().token}`
: `wss://wzpmc.cn:18080/chess/${useUserStore().token}`,
relations,
}),
);
}
const playerCountMap: Record<RoomState, string> = {
[RoomState.CREATED]: '0/2',
[RoomState.WAITING]: '1/2',
[RoomState.GAMING]: '2/2',
[RoomState.FINISHED]: '2/2',
};
</script>
<script lang="ts" setup>
import CreateGobangRoomDialog from '@/components/gobang/CreateGobangRoomDialog.vue';
@ -146,6 +172,7 @@ import GobangHeader from '@/components/gobang/GobangHeader.vue';
import router from '@/router';
import type { UserInfo } from '@/schemas';
import { useUserStore } from '@/stores/user';
import { arrayIncludes } from '@/utils';
import {
useGameSocket,
type PayloadPart,
@ -190,14 +217,7 @@ onMounted(() => {
refresh();
});
function getPlayerCount(row: RoomRender) {
switch (row.state) {
case RoomState.CREATED:
return '0/2';
case RoomState.WAITING:
return '1/2';
case RoomState.GAMING:
return '2/2';
}
return playerCountMap[row.state];
}
function onJoinButtonClick(room: RoomRender) {
play(room.id);

View File

@ -16,11 +16,10 @@
</gobang-header>
<el-container direction="horizontal">
<el-main class="gobang-play-page-main !flex flex-col justify-center items-center">
<div class="gobang__state">
<!-- {{ stateDisplayMap[state] }} -->
</div>
<div
v-loading="matchState === MatchState.FINISHED"
class="gobang-chessboard"
element-loading-svg-view-box="0 0 0 0"
:class="{
'gobang-chessboard--enabled': enabled,
}"
@ -70,10 +69,24 @@
</div>
<div class="flex flex-col items-center">
<gobang-user v-if="userStore.userInfo" :user="userStore.userInfo" />
<div v-if="otherUser" class="font-bold text-5">
<div v-if="matchState === MatchState.GAMING" class="font-bold text-5">
<template v-if="selfRound">请落子.</template>
<template v-else>等待对方落子.</template>
</div>
<template v-else-if="matchState === MatchState.FINISHED">
<div class="font-bold text-5">
<template v-if="restartState === RestartState.WAITING">等待对方重新开始.</template>
<template v-else>等待重新开始.</template>
</div>
<el-button
type="primary"
:loading="restartState === RestartState.RESTARTING"
:disabled="restartState === RestartState.WAITING"
@click="restart"
>
重新开始
</el-button>
</template>
</div>
</el-aside>
</el-container>
@ -119,7 +132,9 @@
@apply bg-black border-solid;
}
&--win-blink {
box-shadow: 5px 5px 5px black;
// box-shadow: 0 0 10px red;
//
z-index: 2001;
}
}
&--enabled {
@ -159,9 +174,9 @@ import GobangUser from '@/components/gobang/GobangUser.vue';
import router from '@/router';
import { useMediaStore } from '@/stores/media';
import { useUserStore } from '@/stores/user';
import { create2DArray } from '@/utils';
import { arrayIncludes, create2DArray } from '@/utils';
import { useGobangSocket, WinFace, type RoomDetail, type RoomId } from '@/views/GobangListPage.vue';
import { ElMessage } from 'element-plus';
import { ElMessage, ElNotification } from 'element-plus';
import { storeToRefs } from 'pinia';
import { computed, onMounted, ref, watch, watchEffect, type CSSProperties } from 'vue';
import { useRoute } from 'vue-router';
@ -232,9 +247,9 @@ function getCellStyle(chess: Chess | undefined, x: number, y: number) {
}
const firstLoading = ref(true);
enum MatchState {
WAITING,
GAMING,
FINISHED,
WAITING = 'WAITING',
GAMING = 'GAMING',
FINISHED = 'FINISHED',
}
const matchState = ref<MatchState>(MatchState.WAITING);
function updateMatchState(finished?: boolean) {
@ -272,9 +287,13 @@ const otherUser = computed(() => {
const { whiteUser, blackUser } = room.value;
return [whiteUser, blackUser].find((v) => v && v.id !== userStore.userInfo!.id);
});
watch(otherUser, (v, old) => {
updateMatchState();
});
watch(
() => otherUser.value?.id,
(v, old) => {
updateMatchState();
},
);
const isWhite = ref(false);
const grid = computed<Grid | undefined>(() =>
room.value?.pieces.map((row) => row.map((v) => (v !== -1 ? { isWhite: !!v } : undefined))),
);
@ -282,7 +301,26 @@ const blink = ref<boolean[][]>();
function resetBlink() {
blink.value = undefined;
}
const isWhite = ref(false);
const win = ref(false);
enum RestartState {
NO = 'NO',
RESTARTING = 'RESTARTING',
WAITING = 'WAITING',
}
const restartState = ref<RestartState>(RestartState.NO);
function resetRestartState() {
restartState.value = RestartState.NO;
}
const otherRequiresRestart = ref(false);
function resetOtherRequiresRestart() {
otherRequiresRestart.value = false;
}
function resetNecessaryStates() {
resetSelfRound();
resetBlink();
resetRestartState();
}
function onCellClick(cell: Chess | undefined, x: number, y: number) {
if (cell || !enabled.value) return;
placing.value = true;
@ -308,25 +346,57 @@ const { send } = useGobangSocket({
},
RoomInfo(p) {
room.value = p;
const { whiteRequestRestart, blackRequestRestart } = room.value;
const selfRequiresRestart = isWhite.value ? whiteRequestRestart : blackRequestRestart;
otherRequiresRestart.value =
(isWhite.value ? blackRequestRestart : whiteRequestRestart) ?? false;
if (
arrayIncludes([RestartState.RESTARTING, RestartState.WAITING], restartState.value) &&
!selfRequiresRestart &&
!otherRequiresRestart.value
) {
resetRestartState();
resetOtherRequiresRestart();
resetNecessaryStates();
updateMatchState();
}
if (matchState.value === MatchState.GAMING) {
selfRound.value = !selfRound.value;
selfRound.value = isWhite.value === p.canWhiteDown;
}
},
PlayerLeave() {
ElMessage.warning('对方已离开房间,对局自动结束');
ElMessage.warning('对方已离开房间, 对局自动结束.');
room.value = undefined;
resetSelfRound();
resetRoom();
resetBlink();
sendResetRoom();
resetNecessaryStates();
},
HasPlayerWin(p) {
const [deltaX, deltaY] = deltaMap[p.face];
const { originalX, originalY } = p;
blink.value = create2DArray(
boardLength,
boardLength,
(x, y) => x === originalX + deltaX && y === originalY + deltaY,
);
HasPlayerWin({ winInfo }) {
const [deltaX, deltaY] = deltaMap[winInfo.face];
const { originalX, originalY } = winInfo;
blink.value = create2DArray(boardLength, boardLength, false);
for (let i = 0; i < 5; i++) {
blink.value[originalY + deltaY * i][originalX + deltaX * i] = true;
}
resetRestartState();
updateMatchState(true);
},
PlayerWin() {
win.value = true;
ElNotification({
title: '你赢了!',
message: '🎉🥳🎊',
type: 'success',
position: mdLess.value ? 'bottom-right' : 'top-right',
});
},
PlayerLose() {
win.value = false;
ElNotification({
title: '你输了!',
message: '🤣👉🤡',
type: 'error',
position: mdLess.value ? 'bottom-right' : 'top-right',
});
},
},
error: {
@ -345,13 +415,20 @@ const { send } = useGobangSocket({
PlaceChessPiece() {
placing.value = false;
},
ResetRoom() {
restartState.value = RestartState.WAITING;
},
},
});
function resetRoom() {
function sendResetRoom() {
send({
name: 'ResetRoom',
});
}
function restart() {
sendResetRoom();
restartState.value = RestartState.RESTARTING;
}
onMounted(() => {
if (!roomId) {
matchState.value = MatchState.GAMING;