feat: 完成五子棋WS事件分配器

This commit is contained in:
Litrix 2024-12-16 20:19:24 +08:00
parent 4ae374a8b6
commit bb77439200
5 changed files with 118 additions and 80 deletions

View File

@ -1,10 +0,0 @@
.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);
width: 100%;
z-index: 100;
user-select: none;
}

View File

@ -20,6 +20,7 @@
z-index: 100;
user-select: none;
&__left {
z-index: 1;
gap: 10px;
}
}

View File

@ -3,9 +3,8 @@ import { reactive, readonly, type Ref, ref, watch } from 'vue';
export * from './2d-array';
export * from './api';
export function timeout(delay: number = 0) {
return new Promise<void>((resolve) => setTimeout(resolve, delay));
}
export const timeout = (delay: number = 0) =>
new Promise<void>((resolve) => setTimeout(resolve, delay));
export function useRefresh() {
const key = ref(0);
@ -121,9 +120,8 @@ export class AsyncQueue<T> {
}
}
export function arrayIncludes<T, R>(array: T[], value: R): value is T & R {
return array.includes(value as any);
}
export const arrayIncludes = <T, R>(array: T[], value: R): value is T & R =>
array.includes(value as any);
export function waitRef<T>(ref: Ref<T>): Promise<void>;
export function waitRef<T, const R extends T>(ref: Ref<T>, ...expect: R[]): Promise<R>;
@ -141,3 +139,9 @@ export function waitRef<T, const R extends T>(ref: Ref<T>, ...expect: R[]) {
});
}
export type MaybePromise<T> = T | Promise<T>;
export type ValueOf<T extends object> = T[keyof T];
/**
* key是否为obj的键key的类型.
*/
export const isKeyOf = <T extends object>(obj: T, key: keyof any): key is keyof typeof obj =>
key in obj;

View File

@ -4,7 +4,9 @@
<gobang-header class="justify-between">
<div class="flex align-center">
<el-button type="primary" :loading="loading" @click="refresh">刷新</el-button>
<el-button @click="showDialog = true" type="success">创建房间</el-button>
<el-button type="success" :disabled="loading" @click="showDialog = true">
创建房间
</el-button>
<el-button type="primary" disabled>单人游戏</el-button>
</div>
</gobang-header>
@ -94,81 +96,109 @@ export type Request =
| SimplePart<'CreateRoom'>
| PayloadPart<'PlayerJoin', { roomId: RoomId }>
| SimplePart<'ResetRoom'>;
export type RequestOf<T extends Request['name']> = Extract<Request, { name: T }>;
export const relations = {
export type Resp =
| PayloadPart<'UserInfo', SucceedUserInfoResponse>
| PayloadPart<'RoomList', { rooms: Room[] }>
| PayloadPart<'RoomCreated', { roomId: RoomId }>
| PayloadPart<'PlayerSideAllocation', { isWhite: boolean }>;
const relations = {
RoomList: 'RoomList',
RoomCreated: 'CreateRoom',
PlayerSideAllocation: 'PlayerJoin',
} as const;
const reversedRelations: Record<string, string[]> = {};
} as const satisfies Partial<Record<Resp['name'], Request['name']>>;
type RelatedRespNames = keyof typeof relations;
type RelatedRequestNames = ValueOf<typeof relations>;
const reversedRelations = {} as Record<RelatedRequestNames, string[]>;
for (const [k, v] of Object.entries(relations)) {
(reversedRelations[v] ??= []).push(k);
}
export interface RelatedPart<N extends keyof typeof relations, P extends Payload>
extends PayloadPart<N, P> {
originalPacketName?: (typeof relations)[N];
}
export interface ErrorResp extends PayloadPart<'Error', { reason: string }> {
originalPacketName: Request['name'];
}
export type Resp =
| PayloadPart<'UserInfo', SucceedUserInfoResponse>
| RelatedPart<'RoomList', { rooms: Room[] }>
| RelatedPart<'RoomCreated', { roomId: RoomId }>
| RelatedPart<'PlayerSideAllocation', { isWhite: boolean }>;
export type RequestOf<T extends Request['name']> = Extract<Request, { name: T }>;
export type RespOf<T extends Resp['name']> = Extract<Resp, { name: T }>;
interface UseGobangSocketOptions {
export interface UseGobangSocketOptions {
succeed: {
[P in Resp['name']]?: (payload: RespOf<P>['payload']) => void;
};
error?: {
[P in Request['name']]?: (reason: string) => boolean | void;
};
finally?: {
[P in Request['name']]?: () => void;
// finally
[P in RelatedRequestNames]?: (error: boolean) => void;
};
}
export function useGobangSocket(options: UseGobangSocketOptions) {
const userStore = useUserStore();
const map = new Map<string, Set<string>>();
const { send: _send } = useWebSocket<string>(
`ws://172.16.114.84:58080/chess/${userStore.token}`,
{
onMessage(_, { data }) {
const resp: Resp | ErrorResp = JSON.parse(data);
const { name } = resp;
let reqName: Request['name'] | undefined;
try {
if (name === 'Error') {
const { originalPacketName } = resp;
ElMessage.error(resp.payload.reason);
reqName = originalPacketName;
map.delete(originalPacketName);
return;
const relationMap = new Map<RelatedRequestNames, [Request, Set<string>]>();
let firstConnected = true;
const ws = useWebSocket<string>(`wss://wzpmc.cn:18080/chess/${userStore.token}`, {
onConnected() {
//
if (firstConnected) {
firstConnected = false;
return;
}
for (const [req] of relationMap.values()) {
send(req);
}
},
onMessage(_, { data }) {
const resp: Resp | ErrorResp = JSON.parse(data);
console.log(resp);
const { name } = resp;
let reqName: Request['name'] | undefined;
let error = false;
try {
if (name === 'Error') {
const { originalPacketName } = resp;
reqName = originalPacketName;
error = true;
if (isKeyOf(relations, originalPacketName)) {
relationMap.delete(originalPacketName);
}
if ('originalPacketName' in resp && resp.originalPacketName) {
const { originalPacketName } = resp;
const set = map.get(originalPacketName);
if (set) {
set.delete(name);
if (!set.size) {
map.delete(name);
reqName = originalPacketName;
}
const errorHandler = options.error?.[originalPacketName];
if (errorHandler) {
const res = errorHandler(resp.payload.reason);
if (res) return;
}
ElMessage.error(resp.payload.reason);
return;
}
if (isKeyOf(relations, name)) {
const v = relationMap.get(relations[name]);
if (v) {
const [, set] = v;
set.delete(name);
if (!set.size) {
relationMap.delete(relations[name]);
reqName = relations[name];
}
}
options.succeed[name]?.(resp.payload as any);
} finally {
if (reqName) {
options.finally?.[reqName]?.();
}
}
},
options.succeed[name]?.(resp.payload as any);
} finally {
if (reqName && isKeyOf(reversedRelations, reqName)) {
options.finally?.[reqName]?.(error);
}
}
},
);
autoReconnect: true,
onDisconnected() {
console.log('disconnected');
},
});
const send = (data: Request) => {
const { name } = data;
if (isKeyOf(reversedRelations, name)) {
relationMap.set(name, [data, new Set(reversedRelations[name])]);
}
console.log('send', data);
ws.send(JSON.stringify(data));
};
return {
send(data: Request) {
const respNames = reversedRelations[data.name];
map.set(data.name, respNames && new Set(respNames));
_send(JSON.stringify(data));
},
send,
};
}
</script>
@ -178,6 +208,7 @@ import GobangHeader from '@/components/gobang/GobangHeader.vue';
import router from '@/router';
import type { SucceedUserInfoResponse } from '@/schemas';
import { useUserStore } from '@/stores/user';
import { isKeyOf, type ValueOf } from '@/utils';
import { useWebSocket } from '@vueuse/core';
import { ElMessage } from 'element-plus';
import { onMounted, ref, toRaw } from 'vue';

View File

@ -89,9 +89,10 @@
</style>
<script setup lang="ts">
import GobangHeader from '@/components/gobang/GobangHeader.vue';
import router from '@/router';
import { useMediaStore } from '@/stores/media';
import { create2DArray } from '@/utils';
import { type RequestOf, type RoomId, useGobangSocket } from '@/views/GobangListPage.vue';
import { useGobangSocket, type RoomId } from '@/views/GobangListPage.vue';
import { storeToRefs } from 'pinia';
import { computed, onMounted, ref, watchEffect, type CSSProperties } from 'vue';
import { useRoute } from 'vue-router';
@ -179,20 +180,31 @@ function getCellStyle(chess: Chess | undefined, x: number, y: number) {
const state = ref<'firstLoading' | 'loading' | 'idle' | 'waiting'>('firstLoading');
const isWhite = ref(false);
const { send } = useGobangSocket({
PlayerSideAllocation(p) {
isWhite.value = p.isWhite;
state.value = 'idle';
succeed: {
PlayerSideAllocation(p) {
isWhite.value = p.isWhite;
},
},
error: {
PlayerJoin() {
// router.back();
},
},
finally: {
PlayerJoin(error) {
if (!error) {
state.value = 'idle';
}
},
},
});
function playerJoin(roomId: RoomId) {
send(
JSON.stringify({
name: 'PlayerJoin',
payload: {
roomId,
},
} satisfies RequestOf<'PlayerJoin'>),
);
send({
name: 'PlayerJoin',
payload: {
roomId,
},
});
}
onMounted(() => {
if (roomId) {