diff --git a/.env.development b/.env.development index 562a716..4e162d6 100644 --- a/.env.development +++ b/.env.development @@ -1,4 +1,4 @@ VITE_API_BASE_URL=/api/v1 VITE_GAME_WS_URL=/ws -VITE_API_PROXY_TARGET=http://127.0.0.1:19000 -VITE_WS_PROXY_TARGET=http://127.0.0.1:19000 +VITE_API_PROXY_TARGET=http://192.168.2.16:19000 +VITE_WS_PROXY_TARGET=http://192.168.2.16:19000 diff --git a/pictures/微信图片_20260318170012_3_20.png b/pictures/微信图片_20260318170012_3_20.png deleted file mode 100644 index b146b26..0000000 Binary files a/pictures/微信图片_20260318170012_3_20.png and /dev/null differ diff --git a/pictures/微信图片_20260318170013_4_20.png b/pictures/微信图片_20260318170013_4_20.png deleted file mode 100644 index 522f5ea..0000000 Binary files a/pictures/微信图片_20260318170013_4_20.png and /dev/null differ diff --git a/pictures/微信图片_20260318170016_5_20.png b/pictures/微信图片_20260318170016_5_20.png deleted file mode 100644 index 1708ca3..0000000 Binary files a/pictures/微信图片_20260318170016_5_20.png and /dev/null differ diff --git a/pictures/微信图片_20260318170019_6_20.png b/pictures/微信图片_20260318170019_6_20.png deleted file mode 100644 index 509a740..0000000 Binary files a/pictures/微信图片_20260318170019_6_20.png and /dev/null differ diff --git a/pictures/微信图片_20260318170025_7_20.png b/pictures/微信图片_20260318170025_7_20.png deleted file mode 100644 index e47d5a9..0000000 Binary files a/pictures/微信图片_20260318170025_7_20.png and /dev/null differ diff --git a/scripts/open-four-players.mjs b/scripts/open-four-players.mjs deleted file mode 100644 index 838f094..0000000 --- a/scripts/open-four-players.mjs +++ /dev/null @@ -1,168 +0,0 @@ -import { chromium } from 'playwright' -import fs from 'node:fs/promises' -import path from 'node:path' -import process from 'node:process' - -const baseUrl = 'http://127.0.0.1:5173' -const chromePath = 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe' -const rootDir = process.cwd() -const runtimeDir = path.join(rootDir, '.tmp', 'manual-four-players', String(Date.now())) - -const players = [ - { username: 'pwtest04', loginId: '13812345681', password: 'play123', owner: true }, - { username: 'pwtest01', loginId: '13812345678', password: 'play123', owner: false }, - { username: 'pwtest02', loginId: '13812345679', password: 'play123', owner: false }, - { username: 'pwtest03', loginId: '13812345680', password: 'play123', owner: false }, -] - -function log(message) { - console.log(`[setup] ${message}`) -} - -async function launchPlayer(player) { - const profileDir = path.join(runtimeDir, player.username) - await fs.mkdir(profileDir, { recursive: true }) - log(`launch browser for ${player.username}`) - - const context = await chromium.launchPersistentContext(profileDir, { - headless: false, - executablePath: chromePath, - args: ['--no-first-run', '--no-default-browser-check'], - viewport: { width: 1400, height: 960 }, - }) - - let page = context.pages()[0] - if (!page) { - page = await context.newPage() - } - await page.goto(`${baseUrl}/login`, { waitUntil: 'domcontentloaded' }) - return { ...player, context, page, profileDir } -} - -async function login(page, player) { - log(`login ${player.username}`) - await page.goto(`${baseUrl}/login`, { waitUntil: 'domcontentloaded' }) - await page.getByRole('textbox', { name: '登录ID' }).fill(player.loginId) - await page.getByRole('textbox', { name: '密码' }).fill(player.password) - const submitButton = page.locator('form').getByRole('button', { name: '登录' }) - await Promise.all([ - page.waitForURL('**/hall', { timeout: 15000 }), - submitButton.click(), - ]) - await page.getByRole('heading', { name: '麻将游戏大厅' }).waitFor({ timeout: 15000 }) - log(`logged in ${player.username}`) -} - -async function createRoom(page) { - const roomName = `manual-${Date.now()}` - log(`create room ${roomName}`) - await page.getByRole('button', { name: '创建房间' }).click() - await page.getByRole('textbox', { name: '房间名' }).fill(roomName) - await page.getByRole('button', { name: '创建', exact: true }).click() - const roomText = await page.getByText(/房间ID:/).textContent() - const roomId = roomText?.replace('房间ID:', '').trim() - if (!roomId) { - throw new Error('Failed to read room id') - } - await Promise.all([ - page.waitForURL(`**/game/chengdu/${roomId}*`, { timeout: 15000 }), - page.getByRole('button', { name: '进入房间' }).click(), - ]) - log(`owner entered room ${roomId}`) - return { roomId, roomName } -} - -async function joinRoom(page, roomId, username) { - log(`join room ${roomId} as ${username}`) - await page.goto(`${baseUrl}/hall`, { waitUntil: 'domcontentloaded' }) - await page.getByRole('textbox', { name: '输入 room_id' }).fill(roomId) - await Promise.all([ - page.waitForURL(`**/game/chengdu/${roomId}*`, { timeout: 15000 }), - page.getByRole('button', { name: '加入' }).click(), - ]) - log(`joined room ${roomId} as ${username}`) -} - -async function snapshotPage(session) { - const bodyText = ((await session.page.locator('body').textContent()) ?? '').replace(/\s+/g, ' ').trim() - return { - username: session.username, - loginId: session.loginId, - url: session.page.url(), - started: bodyText.includes('对局中') || bodyText.includes('牌局进行中'), - bodyPreview: bodyText.slice(0, 240), - } -} - -async function main() { - await fs.mkdir(runtimeDir, { recursive: true }) - log(`runtimeDir ${runtimeDir}`) - - const sessions = [] - for (const player of players) { - sessions.push(await launchPlayer(player)) - } - - try { - for (const session of sessions) { - await login(session.page, session) - } - - const owner = sessions.find((session) => session.owner) - if (!owner) { - throw new Error('Owner session missing') - } - - const { roomId } = await createRoom(owner.page) - log(`room ready ${roomId}`) - - for (const session of sessions) { - if (!session.owner) { - await joinRoom(session.page, roomId, session.username) - } - } - - await new Promise((resolve) => setTimeout(resolve, 8000)) - - const playersSnapshot = [] - for (const session of sessions) { - await session.page.bringToFront() - await new Promise((resolve) => setTimeout(resolve, 2000)) - let snapshot = await snapshotPage(session) - if (!snapshot.started) { - log(`focus retry for ${session.username}`) - await session.page.bringToFront() - await new Promise((resolve) => setTimeout(resolve, 3000)) - snapshot = await snapshotPage(session) - } - playersSnapshot.push(snapshot) - } - - console.log(JSON.stringify({ - roomId, - roomUrl: `${baseUrl}/game/chengdu/${roomId}`, - players: playersSnapshot, - }, null, 2)) - - if (process.env.KEEP_ALIVE === '1') { - log('setup finished; keeping browsers alive for manual testing') - await new Promise(() => {}) - } - - for (const session of sessions) { - await session.context.close() - } - } catch (error) { - console.error(error) - if (process.env.KEEP_ALIVE !== '1') { - for (const session of sessions) { - try { - await session.context.close() - } catch {} - } - } - throw error - } -} - -await main() diff --git a/src/common/id.ts b/src/common/id.ts new file mode 100644 index 0000000..0fa2dc7 --- /dev/null +++ b/src/common/id.ts @@ -0,0 +1,4 @@ +// 生成 requestId / traceId +export function createRequestId(prefix: string): string { + return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}` +} \ No newline at end of file diff --git a/src/constants/index.ts b/src/constants/index.ts deleted file mode 100644 index bd75c8f..0000000 --- a/src/constants/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './suits' -export * from './ws-events' diff --git a/src/constants/suits.ts b/src/constants/suits.ts deleted file mode 100644 index 754f2a1..0000000 --- a/src/constants/suits.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const MAHJONG_SUITS = ['wan', 'tong', 'tiao'] as const - -export type MahjongSuit = (typeof MAHJONG_SUITS)[number] diff --git a/src/constants/ws-events.ts b/src/constants/ws-events.ts deleted file mode 100644 index 820d4a5..0000000 --- a/src/constants/ws-events.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const WS_EVENT = { - roomSnapshot: 'room_snapshot', - roomState: 'room_state', - gameState: 'game_state', - myHand: 'my_hand', -} as const diff --git a/src/models/tile.ts b/src/domain/tile/tile.ts similarity index 89% rename from src/models/tile.ts rename to src/domain/tile/tile.ts index 5b16263..6b205a4 100644 --- a/src/models/tile.ts +++ b/src/domain/tile/tile.ts @@ -1,11 +1,4 @@ -export type Suit = 'W' | 'T' | 'B' - -export interface Tile { - id: number - suit: Suit - value: number -} - +import type {Suit, Tile} from "../../types/tile.ts"; export class TileModel { id: number diff --git a/src/game/actions.ts b/src/game/actions.ts new file mode 100644 index 0000000..3d3cb64 --- /dev/null +++ b/src/game/actions.ts @@ -0,0 +1,37 @@ +import type { ActionButtonState } from "./types.ts" +import { wsClient } from "@/ws/client" // 新增 + +export function sendGameAction(type: ActionButtonState['type']): void { + // 原来是判断 ws,这里改成用 wsClient 的状态(简单处理) + if (!currentUserId.value) { + return + } + + const requestId = createRequestId(type) + const payload: Record = {} + + if (type === 'discard' && selectedTile.value) { + payload.tile = selectedTile.value + payload.discard_tile = selectedTile.value + payload.code = selectedTile.value + } + + actionPending.value = true + + const message = { + type, + sender: currentUserId.value, + target: 'room', + roomId: roomState.value.id || roomId.value, + seq: Date.now(), + requestId, + trace_id: createRequestId('trace'), + payload, + } + + logWsSend(message) + + wsClient.send(message) // ✅ 改这里 + + pushWsMessage(`[client] 请求${type} requestId=${requestId}`) +} \ No newline at end of file diff --git a/src/game/chengdu/index.ts b/src/game/chengdu/index.ts deleted file mode 100644 index 1080bd6..0000000 --- a/src/game/chengdu/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { useChengduGameRoom } from './useChengduGameRoom' -export type { ActionButtonState, ChengduGameRoomModel, SeatKey, SeatView } from './types' diff --git a/src/game/chengdu/parser-utils.ts b/src/game/chengdu/parser-utils.ts deleted file mode 100644 index e8d86a6..0000000 --- a/src/game/chengdu/parser-utils.ts +++ /dev/null @@ -1,71 +0,0 @@ -export function humanizeSuit(value: string): string { - const suitMap: Record = { - W: '万', - B: '筒', - T: '条', - } - - return suitMap[value] ?? value -} - -export function toRecord(value: unknown): Record | null { - return typeof value === 'object' && value !== null ? (value as Record) : null -} - -export function toStringOrEmpty(value: unknown): string { - if (typeof value === 'string') { - return value - } - if (typeof value === 'number' && Number.isFinite(value)) { - return String(value) - } - return '' -} - -export function toFiniteNumber(value: unknown): number | null { - if (typeof value === 'number' && Number.isFinite(value)) { - return value - } - if (typeof value === 'string' && value.trim()) { - const parsed = Number(value) - return Number.isFinite(parsed) ? parsed : null - } - return null -} - -export function toBoolean(value: unknown): boolean { - if (typeof value === 'boolean') { - return value - } - if (typeof value === 'number') { - return value !== 0 - } - if (typeof value === 'string') { - const normalized = value.trim().toLowerCase() - return normalized === '1' || normalized === 'true' || normalized === 'yes' - } - return false -} - -export function normalizeActionName(value: unknown): string { - const raw = toStringOrEmpty(value).trim().toLowerCase() - if (!raw) { - return '' - } - const actionMap: Record = { - candraw: 'draw', - draw: 'draw', - candiscard: 'discard', - discard: 'discard', - canpeng: 'peng', - peng: 'peng', - cangang: 'gang', - gang: 'gang', - canhu: 'hu', - hu: 'hu', - canpass: 'pass', - pass: 'pass', - } - - return actionMap[raw.replace(/[_\-\s]/g, '')] ?? raw -} diff --git a/src/game/chengdu/room-normalizers.ts b/src/game/chengdu/room-normalizers.ts deleted file mode 100644 index 9b9bf20..0000000 --- a/src/game/chengdu/room-normalizers.ts +++ /dev/null @@ -1,421 +0,0 @@ -import { - DEFAULT_MAX_PLAYERS, - type GameState, - type RoomPlayerState, - type RoomState, -} from '../../store/active-room-store' -import { type PendingClaimOption } from './types' -import { - humanizeSuit, - normalizeActionName, - toBoolean, - toFiniteNumber, - toRecord, - toStringOrEmpty, -} from './parser-utils' - -function normalizeScores(value: unknown): Record { - const record = toRecord(value) - if (!record) { - return {} - } - - const scores: Record = {} - for (const [key, score] of Object.entries(record)) { - const parsed = toFiniteNumber(score) - if (parsed !== null) { - scores[key] = parsed - } - } - return scores -} - -export function normalizeTileList(value: unknown): string[] { - if (!Array.isArray(value)) { - return [] - } - - return value - .flatMap((item) => { - if (Array.isArray(item)) { - return item.map((nested) => toStringOrEmpty(nested)).filter(Boolean) - } - const record = toRecord(item) - if (record) { - const explicit = - toStringOrEmpty( - record.tile ?? record.Tile ?? record.code ?? record.Code ?? record.name ?? record.Name, - ) || '' - if (explicit) { - return [explicit] - } - - const suit = toStringOrEmpty(record.suit ?? record.Suit) - const tileValue = toStringOrEmpty(record.value ?? record.Value) - if (suit && tileValue) { - return [`${humanizeSuit(suit)}${tileValue}`] - } - - return [] - } - return [toStringOrEmpty(item)].filter(Boolean) - }) - .filter(Boolean) -} - -function normalizeMeldGroups(value: unknown): string[][] { - if (!Array.isArray(value)) { - return [] - } - - return value - .map((item) => { - if (Array.isArray(item)) { - return normalizeTileList(item) - } - const record = toRecord(item) - if (record) { - const nested = record.tiles ?? record.Tiles ?? record.meld ?? record.Meld - if (nested) { - return normalizeTileList(nested) - } - } - return normalizeTileList([item]) - }) - .filter((group) => group.length > 0) -} - -function normalizePlayer(input: unknown, fallbackIndex: number): RoomPlayerState | null { - const player = toRecord(input) - if (!player) { - return null - } - - const playerId = toStringOrEmpty( - player.playerId ?? - player.player_id ?? - player.PlayerID ?? - player.UserID ?? - player.user_id ?? - player.id, - ) - if (!playerId) { - return null - } - - const seatIndex = toFiniteNumber( - player.index ?? - player.Index ?? - player.seat ?? - player.Seat ?? - player.position ?? - player.Position ?? - player.player_index, - ) - const hand = normalizeTileList(player.hand ?? player.Hand) - - return { - index: seatIndex ?? fallbackIndex, - playerId, - displayName: - toStringOrEmpty( - player.playerName ?? - player.player_name ?? - player.PlayerName ?? - player.username ?? - player.nickname, - ) || undefined, - ready: Boolean(player.ready), - handCount: - toFiniteNumber(player.handCount ?? player.hand_count ?? player.HandCount) ?? - (hand.length > 0 ? hand.length : undefined), - hand, - melds: normalizeMeldGroups(player.melds ?? player.Melds), - outTiles: normalizeTileList(player.outTiles ?? player.out_tiles ?? player.OutTiles), - hasHu: toBoolean(player.hasHu ?? player.has_hu ?? player.HasHu), - missingSuit: - toStringOrEmpty(player.missingSuit ?? player.missing_suit ?? player.MissingSuit) || null, - } -} - -function normalizePublicGameState(source: Record): GameState | null { - const publicPlayers = (Array.isArray(source.players) ? source.players : []) - .map((item, index) => normalizePlayer(item, index)) - .filter((item): item is RoomPlayerState => Boolean(item)) - .sort((a, b) => a.index - b.index) - - const currentTurnPlayerId = toStringOrEmpty( - source.current_turn_player ?? source.currentTurnPlayer ?? source.current_turn_player_id, - ) - const currentTurnIndex = - publicPlayers.find((player) => player.playerId === currentTurnPlayerId)?.index ?? null - - return { - rule: null, - state: { - phase: toStringOrEmpty(source.phase), - dealerIndex: 0, - currentTurn: currentTurnIndex ?? 0, - needDraw: toBoolean(source.need_draw ?? source.needDraw), - players: publicPlayers.map((player) => ({ - playerId: player.playerId, - index: player.index, - ready: player.ready, - })), - wall: Array.from({ - length: toFiniteNumber(source.wall_count ?? source.wallCount) ?? 0, - }).map((_, index) => `wall-${index}`), - lastDiscardTile: - toStringOrEmpty(source.last_discard_tile ?? source.lastDiscardTile) || null, - lastDiscardBy: toStringOrEmpty(source.last_discard_by ?? source.lastDiscardBy), - pendingClaim: toRecord(source.pending_claim ?? source.pendingClaim), - winners: Array.isArray(source.winners) - ? source.winners.map((item) => toStringOrEmpty(item)).filter(Boolean) - : [], - scores: normalizeScores(source.scores), - lastDrawPlayerId: '', - lastDrawFromGang: false, - lastDrawIsLastTile: false, - huWay: '', - }, - } -} - -export function normalizePendingClaimOptions(value: unknown): PendingClaimOption[] { - const pendingClaim = toRecord(value) - if (!pendingClaim) { - return [] - } - - const rawOptions = - (Array.isArray(pendingClaim.options) ? pendingClaim.options : null) ?? - (Array.isArray(pendingClaim.Options) ? pendingClaim.Options : null) ?? - [] - - const optionsFromArray = rawOptions - .map((option) => { - const record = toRecord(option) - if (!record) { - return null - } - const playerId = toStringOrEmpty( - record.playerId ?? record.player_id ?? record.PlayerID ?? record.user_id ?? record.UserID, - ) - if (!playerId) { - return null - } - const actions = new Set() - for (const [key, enabled] of Object.entries(record)) { - if (typeof enabled === 'boolean' && enabled) { - const normalized = normalizeActionName(key) - if (normalized && normalized !== 'playerid' && normalized !== 'userid') { - actions.add(normalized) - } - } - } - if (Array.isArray(record.actions)) { - for (const action of record.actions) { - const normalized = normalizeActionName(action) - if (normalized) { - actions.add(normalized) - } - } - } - - return { playerId, actions: [...actions] } - }) - .filter((item): item is PendingClaimOption => Boolean(item)) - - if (optionsFromArray.length > 0) { - return optionsFromArray - } - - const claimPlayerId = toStringOrEmpty( - pendingClaim.playerId ?? - pendingClaim.player_id ?? - pendingClaim.PlayerID ?? - pendingClaim.user_id ?? - pendingClaim.UserID, - ) - if (!claimPlayerId) { - return [] - } - - const actions = Object.entries(pendingClaim) - .filter(([, enabled]) => typeof enabled === 'boolean' && enabled) - .map(([key]) => normalizeActionName(key)) - .filter(Boolean) - - return actions.length > 0 ? [{ playerId: claimPlayerId, actions }] : [] -} - -function extractCurrentTurnIndex(value: Record): number | null { - const game = toRecord(value.game) - const gameState = toRecord(game?.state) - const keys = [ - gameState?.currentTurn, - gameState?.current_turn, - gameState?.currentTurnIndex, - gameState?.current_turn_index, - value.currentTurnIndex, - value.current_turn_index, - value.currentPlayerIndex, - value.current_player_index, - value.turnIndex, - value.turn_index, - value.activePlayerIndex, - value.active_player_index, - ] - for (const key of keys) { - const parsed = toFiniteNumber(key) - if (parsed !== null) { - return parsed - } - } - return null -} - -function normalizeGame(input: unknown): GameState | null { - const game = toRecord(input) - if (!game) { - return null - } - - const rule = toRecord(game.rule) - const rawState = toRecord(game.state) - const playersRaw = - (Array.isArray(rawState?.players) ? rawState?.players : null) ?? - (Array.isArray(rawState?.playerStates) ? rawState?.playerStates : null) ?? - [] - - const normalizedPlayers = playersRaw - .map((item, index) => normalizePlayer(item, index)) - .filter((item): item is RoomPlayerState => Boolean(item)) - - return { - rule: rule - ? { - name: toStringOrEmpty(rule.name), - isBloodFlow: toBoolean(rule.isBloodFlow ?? rule.is_blood_flow), - hasHongZhong: toBoolean(rule.hasHongZhong ?? rule.has_hong_zhong), - } - : null, - state: rawState - ? { - phase: toStringOrEmpty(rawState.phase), - dealerIndex: toFiniteNumber(rawState.dealerIndex ?? rawState.dealer_index) ?? 0, - currentTurn: toFiniteNumber(rawState.currentTurn ?? rawState.current_turn) ?? 0, - needDraw: toBoolean(rawState.needDraw ?? rawState.need_draw), - players: normalizedPlayers, - wall: Array.isArray(rawState.wall) - ? rawState.wall.map((item) => toStringOrEmpty(item)).filter(Boolean) - : [], - lastDiscardTile: toStringOrEmpty(rawState.lastDiscardTile ?? rawState.last_discard_tile) || null, - lastDiscardBy: toStringOrEmpty(rawState.lastDiscardBy ?? rawState.last_discard_by), - pendingClaim: toRecord(rawState.pendingClaim ?? rawState.pending_claim), - winners: Array.isArray(rawState.winners) - ? rawState.winners.map((item) => toStringOrEmpty(item)).filter(Boolean) - : [], - scores: normalizeScores(rawState.scores), - lastDrawPlayerId: toStringOrEmpty(rawState.lastDrawPlayerID ?? rawState.last_draw_player_id), - lastDrawFromGang: toBoolean(rawState.lastDrawFromGang ?? rawState.last_draw_from_gang), - lastDrawIsLastTile: toBoolean(rawState.lastDrawIsLastTile ?? rawState.last_draw_is_last_tile), - huWay: toStringOrEmpty(rawState.huWay ?? rawState.hu_way), - } - : null, - } -} - -export function normalizeRoom(input: unknown, currentRoomState: RoomState): RoomState | null { - const source = toRecord(input) - if (!source) { - return null - } - - let room = source - let id = toStringOrEmpty(room.roomId ?? room.room_id ?? room.id) - if (!id) { - const nestedRoom = toRecord(room.data) - if (nestedRoom) { - room = nestedRoom - id = toStringOrEmpty(room.roomId ?? room.room_id ?? room.id) - } - } - if (!id) { - return null - } - - const maxPlayers = - toFiniteNumber(room.maxPlayers ?? room.max_players) ?? currentRoomState.maxPlayers ?? DEFAULT_MAX_PLAYERS - const playersRaw = - (Array.isArray(room.players) ? room.players : null) ?? - (Array.isArray(room.playerList) ? room.playerList : null) ?? - (Array.isArray(room.player_list) ? room.player_list : null) ?? - [] - const playerIdsRaw = - (Array.isArray(room.player_ids) ? room.player_ids : null) ?? - (Array.isArray(room.playerIds) ? room.playerIds : null) ?? - [] - - const players = playersRaw - .map((item, index) => normalizePlayer(item, index)) - .filter((item): item is RoomPlayerState => Boolean(item)) - .sort((a, b) => a.index - b.index) - const playersFromIds = playerIdsRaw - .map((item, index) => ({ - index, - playerId: toStringOrEmpty(item), - ready: false, - hand: [], - melds: [], - outTiles: [], - hasHu: false, - })) - .filter((item) => Boolean(item.playerId)) - const resolvedPlayers = players.length > 0 ? players : playersFromIds - const parsedPlayerCount = toFiniteNumber(room.player_count ?? room.playerCount) - const game = normalizeGame(room.game) ?? normalizePublicGameState(room) - const playersFromGame = game?.state?.players - .map((player, index) => - normalizePlayer( - { - player_id: player.playerId, - index: player.index ?? index, - }, - index, - ), - ) - .filter((item): item is RoomPlayerState => Boolean(item)) - const finalPlayers = - resolvedPlayers.length > 0 - ? resolvedPlayers - : playersFromGame && playersFromGame.length > 0 - ? playersFromGame - : [] - const derivedTurnIndex = - extractCurrentTurnIndex(room) ?? - (game?.state - ? finalPlayers.find( - (player) => player.playerId === toStringOrEmpty(room.current_turn_player ?? room.currentTurnPlayer), - )?.index ?? null - : null) - - return { - id, - name: toStringOrEmpty(room.name) || currentRoomState.name, - gameType: toStringOrEmpty(room.gameType ?? room.game_type) || currentRoomState.gameType || 'chengdu', - ownerId: toStringOrEmpty(room.ownerId ?? room.owner_id ?? room.OwnerID ?? room.ownerID), - maxPlayers, - playerCount: - parsedPlayerCount ?? - toFiniteNumber(room.player_count ?? room.playerCount ?? room.playerCount) ?? - finalPlayers.length, - status: toStringOrEmpty(room.status) || currentRoomState.status || 'waiting', - createdAt: toStringOrEmpty(room.createdAt ?? room.created_at) || currentRoomState.createdAt, - updatedAt: toStringOrEmpty(room.updatedAt ?? room.updated_at) || currentRoomState.updatedAt, - game: game ?? currentRoomState.game, - players: finalPlayers, - currentTurnIndex: derivedTurnIndex, - myHand: [], - } -} diff --git a/src/game/chengdu/types.ts b/src/game/chengdu/types.ts deleted file mode 100644 index a4e9acb..0000000 --- a/src/game/chengdu/types.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { ComputedRef, Ref } from 'vue' -import type { StoredAuth } from '../../types/session' -import type { RoomPlayerState } from '../../store/active-room-store' -import { activeRoomState } from '../../store/active-room-store' - -export type SeatKey = 'top' | 'right' | 'bottom' | 'left' - -export interface ActionEventLike { - type?: unknown - status?: unknown - requestId?: unknown - request_id?: unknown - roomId?: unknown - room_id?: unknown - payload?: unknown - data?: unknown -} - -export interface PendingClaimOption { - playerId: string - actions: string[] -} - -export interface ActionButtonState { - type: 'draw' | 'discard' | 'peng' | 'gang' | 'hu' | 'pass' - label: string - disabled: boolean -} - -export interface SeatView { - key: SeatKey - player: RoomPlayerState | null - isSelf: boolean - isTurn: boolean - label: string - subLabel: string -} - -export interface ChengduGameRoomModel { - auth: Ref - roomState: typeof activeRoomState - roomId: ComputedRef - roomName: ComputedRef - currentUserId: ComputedRef - loggedInUserName: ComputedRef - wsStatus: Ref<'disconnected' | 'connecting' | 'connected'> - wsError: Ref - wsMessages: Ref - startGamePending: Ref - leaveRoomPending: Ref - canStartGame: ComputedRef - seatViews: ComputedRef - selectedTile: Ref - actionButtons: ComputedRef - connectWs: () => Promise - sendStartGame: () => void - selectTile: (tile: string) => void - sendGameAction: (type: ActionButtonState['type']) => void - backHall: () => void -} diff --git a/src/game/index.ts b/src/game/index.ts deleted file mode 100644 index 532305b..0000000 --- a/src/game/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './chengdu' diff --git a/src/game/types.ts b/src/game/types.ts new file mode 100644 index 0000000..68303de --- /dev/null +++ b/src/game/types.ts @@ -0,0 +1,5 @@ +export type ActionButtonState = { + type: 'discard' | 'peng' | 'gang' | 'hu' | 'pass' + label: string + disabled: boolean +} \ No newline at end of file diff --git a/src/game/chengdu/useChengduGameRoom.ts b/src/game/useChengduGameRoom.ts similarity index 98% rename from src/game/chengdu/useChengduGameRoom.ts rename to src/game/useChengduGameRoom.ts index e5d90e3..5d7093c 100644 --- a/src/game/chengdu/useChengduGameRoom.ts +++ b/src/game/useChengduGameRoom.ts @@ -1,8 +1,8 @@ import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue' import type { RouteLocationNormalizedLoaded, Router } from 'vue-router' -import type { AuthSession } from '../../api/authed-request' -import { refreshAccessToken } from '../../api/auth' -import { getUserInfo } from '../../api/user' +import type { AuthSession } from '../api/authed-request.ts' +import { refreshAccessToken } from '../api/auth.ts' +import { getUserInfo } from '../api/user.ts' import { activeRoomState, destroyActiveRoomState, @@ -11,7 +11,7 @@ import { type RoomPlayerState, type RoomState, } from '../../store/active-room-store' -import { readStoredAuth, writeStoredAuth } from '../../utils/auth-storage' +import { readStoredAuth, writeStoredAuth } from '../utils/auth-storage.ts' import type { ActionButtonState, ActionEventLike, diff --git a/src/models/index.ts b/src/models/index.ts deleted file mode 100644 index ebd9765..0000000 --- a/src/models/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './tile' -export * from './room-state' diff --git a/src/models/room-state/constants.ts b/src/models/room-state/constants.ts deleted file mode 100644 index a6abd72..0000000 --- a/src/models/room-state/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const DEFAULT_MAX_PLAYERS = 4 diff --git a/src/models/room-state/engine-state.ts b/src/models/room-state/engine-state.ts deleted file mode 100644 index 783a955..0000000 --- a/src/models/room-state/engine-state.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { GamePlayerState } from './game-player-state' - -export interface EngineState { - phase: string - dealerIndex: number - currentTurn: number - needDraw: boolean - players: GamePlayerState[] - wall: string[] - lastDiscardTile: string | null - lastDiscardBy: string - pendingClaim: Record | null - winners: string[] - scores: Record - lastDrawPlayerId: string - lastDrawFromGang: boolean - lastDrawIsLastTile: boolean - huWay: string -} diff --git a/src/models/room-state/game-player-state.ts b/src/models/room-state/game-player-state.ts deleted file mode 100644 index 35f4ada..0000000 --- a/src/models/room-state/game-player-state.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface GamePlayerState { - playerId: string - index: number - ready: boolean -} diff --git a/src/models/room-state/game-state.ts b/src/models/room-state/game-state.ts deleted file mode 100644 index c25c764..0000000 --- a/src/models/room-state/game-state.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { EngineState } from './engine-state' -import type { RuleState } from './rule-state' - -export interface GameState { - rule: RuleState | null - state: EngineState | null -} diff --git a/src/models/room-state/index.ts b/src/models/room-state/index.ts deleted file mode 100644 index 20475c5..0000000 --- a/src/models/room-state/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export { DEFAULT_MAX_PLAYERS } from './constants' -export type { RoomStatus } from './room-status' -export type { PlayerState } from './player-state' -export type { RoomPlayerState } from './room-player-state' -export type { RuleState } from './rule-state' -export type { GamePlayerState } from './game-player-state' -export type { EngineState } from './engine-state' -export type { GameState } from './game-state' -export type { RoomState } from './room-state' diff --git a/src/models/room-state/player-state.ts b/src/models/room-state/player-state.ts deleted file mode 100644 index e1fc6ca..0000000 --- a/src/models/room-state/player-state.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface PlayerState { - playerId: string - hand: string[] - melds: string[][] - outTiles: string[] - hasHu: boolean -} diff --git a/src/models/room-state/room-player-state.ts b/src/models/room-state/room-player-state.ts deleted file mode 100644 index 6b6e544..0000000 --- a/src/models/room-state/room-player-state.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { PlayerState } from './player-state' - -export interface RoomPlayerState extends PlayerState { - index: number - displayName?: string - ready: boolean - handCount?: number - missingSuit?: string | null -} diff --git a/src/models/room-state/room-state.ts b/src/models/room-state/room-state.ts deleted file mode 100644 index ded2b57..0000000 --- a/src/models/room-state/room-state.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { GameState } from './game-state' -import type { RoomPlayerState } from './room-player-state' -import type { RoomStatus } from './room-status' - -export interface RoomState { - id: string - name: string - gameType: string - ownerId: string - maxPlayers: number - playerCount: number - status: RoomStatus | string - createdAt: string - updatedAt: string - game: GameState | null - players: RoomPlayerState[] - currentTurnIndex: number | null - myHand: string[] -} diff --git a/src/models/room-state/room-status.ts b/src/models/room-state/room-status.ts deleted file mode 100644 index bec836d..0000000 --- a/src/models/room-state/room-status.ts +++ /dev/null @@ -1 +0,0 @@ -export type RoomStatus = 'waiting' | 'playing' | 'finished' diff --git a/src/models/room-state/rule-state.ts b/src/models/room-state/rule-state.ts deleted file mode 100644 index 615df9c..0000000 --- a/src/models/room-state/rule-state.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface RuleState { - name: string - isBloodFlow: boolean - hasHongZhong: boolean -} diff --git a/src/modules/game/engine.ts b/src/modules/game/engine.ts new file mode 100644 index 0000000..41a3c80 --- /dev/null +++ b/src/modules/game/engine.ts @@ -0,0 +1,15 @@ +// 游戏对象 + +import type {GameState} from "../../types/state"; + +export interface Game { + rule: Rule + state: GameState +} + +// 规则配置(仅描述,不包含判定逻辑) +export interface Rule { + name: string // 玩法名称 + isBloodFlow: boolean // 是否血流玩法 + hasHongZhong: boolean // 是否包含红中 +} \ No newline at end of file diff --git a/src/store/active-room-store.ts b/src/store/active-room-store.ts deleted file mode 100644 index be9c7a6..0000000 --- a/src/store/active-room-store.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { ref } from 'vue' -import { - DEFAULT_MAX_PLAYERS, - type RoomPlayerState, - type RoomState, -} from '../models/room-state' - -export { - DEFAULT_MAX_PLAYERS, - type EngineState, - type GamePlayerState, - type GameState, - type PlayerState, - type RoomPlayerState, - type RoomState, - type RoomStatus, - type RuleState, -} from '../models/room-state' - -function createInitialRoomState(): RoomState { - return { - id: '', - name: '', - gameType: 'chengdu', - ownerId: '', - maxPlayers: DEFAULT_MAX_PLAYERS, - playerCount: 0, - status: 'waiting', - createdAt: '', - updatedAt: '', - game: null, - players: [], - currentTurnIndex: null, - myHand: [], - } -} - -export const activeRoomState = ref(createInitialRoomState()) - -export function destroyActiveRoomState(): void { - activeRoomState.value = createInitialRoomState() -} - -export function resetActiveRoomState(seed?: Partial): void { - destroyActiveRoomState() - if (!seed) { - return - } - - activeRoomState.value = { - ...activeRoomState.value, - ...seed, - players: seed.players ?? [], - myHand: seed.myHand ?? [], - } -} - -export function mergeActiveRoomState(next: RoomState): void { - if (activeRoomState.value.id && next.id && next.id !== activeRoomState.value.id) { - return - } - - activeRoomState.value = { - ...activeRoomState.value, - ...next, - name: next.name || activeRoomState.value.name, - gameType: next.gameType || activeRoomState.value.gameType, - ownerId: next.ownerId || activeRoomState.value.ownerId, - status: next.status || activeRoomState.value.status, - createdAt: next.createdAt || activeRoomState.value.createdAt, - updatedAt: next.updatedAt || activeRoomState.value.updatedAt, - game: next.game ?? activeRoomState.value.game, - players: next.players.length > 0 ? next.players : activeRoomState.value.players, - currentTurnIndex: - next.currentTurnIndex !== null ? next.currentTurnIndex : activeRoomState.value.currentTurnIndex, - myHand: next.myHand.length > 0 ? next.myHand : activeRoomState.value.myHand, - } -} - -export function hydrateActiveRoomFromSelection(input: { - roomId: string - roomName?: string - gameType?: string - ownerId?: string - maxPlayers?: number - playerCount?: number - status?: string - createdAt?: string - updatedAt?: string - players?: RoomPlayerState[] - currentTurnIndex?: number | null - myHand?: string[] -}): void { - resetActiveRoomState({ - id: input.roomId, - name: input.roomName ?? '', - gameType: input.gameType ?? 'chengdu', - ownerId: input.ownerId ?? '', - maxPlayers: input.maxPlayers ?? DEFAULT_MAX_PLAYERS, - playerCount: input.playerCount ?? 0, - status: input.status ?? 'waiting', - createdAt: input.createdAt ?? '', - updatedAt: input.updatedAt ?? '', - players: input.players ?? [], - currentTurnIndex: input.currentTurnIndex ?? null, - myHand: input.myHand ?? [], - }) -} diff --git a/src/types/state/claimOption.ts b/src/types/state/claimOption.ts new file mode 100644 index 0000000..871f4de --- /dev/null +++ b/src/types/state/claimOption.ts @@ -0,0 +1,9 @@ +export const CLAIM_OPTIONS = { + PENG: 'peng', + GANG: 'gang', + HU: 'hu', + PASS: 'pass', +} as const + +export type ClaimOption = + typeof CLAIM_OPTIONS[keyof typeof CLAIM_OPTIONS] \ No newline at end of file diff --git a/src/types/state/gamePhase.ts b/src/types/state/gamePhase.ts new file mode 100644 index 0000000..d9aa646 --- /dev/null +++ b/src/types/state/gamePhase.ts @@ -0,0 +1,9 @@ +export const GAME_PHASE = { + WAITING: 'waiting', + DEALING: 'dealing', + PLAYING: 'playing', + SETTLEMENT: 'settlement', +} as const + +export type GamePhase = + typeof GAME_PHASE[keyof typeof GAME_PHASE] \ No newline at end of file diff --git a/src/types/state/gamestate.ts b/src/types/state/gamestate.ts new file mode 100644 index 0000000..2d72b9b --- /dev/null +++ b/src/types/state/gamestate.ts @@ -0,0 +1,52 @@ +import type {Player} from "./player.ts"; +import type {Tile} from "../../models"; +import type {PendingClaim} from "./pendingClaim.ts"; +import type {HuWay} from "./huWay.ts"; +import type {GamePhase} from "./gamePhase.ts"; + +export interface GameState { + // 当前阶段 + phase: GamePhase + + // 庄家位置 + dealerIndex: number + + // 当前操作玩家(座位) + currentTurn: number + + // 是否必须先摸牌 + needDraw: boolean + + // 玩家列表 + players: Player[] + + // ⚠️ 建议:只保留剩余数量(不要完整牌墙) + remainingTiles: number + + // 最近弃牌 + lastDiscardTile?: Tile + + // 最近弃牌玩家 + lastDiscardBy?: string + + // 操作响应窗口(碰/杠/胡) + pendingClaim?: PendingClaim + + // 胡牌玩家 + winners: string[] + + // 分数(playerId -> score) + scores: Record + + // 最近摸牌玩家 + lastDrawPlayerId: string + + // 是否杠后补牌 + lastDrawFromGang: boolean + + // 是否最后一张牌 + lastDrawIsLastTile: boolean + + // 胡牌方式 + huWay: HuWay +} \ No newline at end of file diff --git a/src/types/state/huWay.ts b/src/types/state/huWay.ts new file mode 100644 index 0000000..e9a720d --- /dev/null +++ b/src/types/state/huWay.ts @@ -0,0 +1,12 @@ +// 胡牌类型常量(用于避免直接写字符串) +export const HU_WAY = { + PING_HU: 'pinghu', // 平胡 + QI_DUI: 'qidui', // 七对 + PENG_PENG_HU: 'pengpenghu', // 碰碰胡 + QING_YI_SE: 'qingyise', // 清一色 + UNKNOWN: 'unknown', // 未知 / 未判定 +} as const + +// 胡牌类型(从常量中推导) +export type HuWay = + typeof HU_WAY[keyof typeof HU_WAY] \ No newline at end of file diff --git a/src/types/state/index.ts b/src/types/state/index.ts new file mode 100644 index 0000000..096ee3c --- /dev/null +++ b/src/types/state/index.ts @@ -0,0 +1,7 @@ +export * from './gamestate' +export * from './player' +export * from './meld' +export * from './pendingClaim' +export * from './claimOption' +export * from './gamePhase' +export * from './huWay' \ No newline at end of file diff --git a/src/types/state/meld.ts b/src/types/state/meld.ts new file mode 100644 index 0000000..930f382 --- /dev/null +++ b/src/types/state/meld.ts @@ -0,0 +1,17 @@ +import type {Tile} from "../../models"; + +export type Meld = + | { + type: 'peng' + tiles: Tile[] + fromPlayerId: string +} + | { + type: 'ming_gang' + tiles: Tile[] + fromPlayerId: string +} + | { + type: 'an_gang' + tiles: Tile[] +} \ No newline at end of file diff --git a/src/types/state/pendingClaim.ts b/src/types/state/pendingClaim.ts new file mode 100644 index 0000000..217a7e1 --- /dev/null +++ b/src/types/state/pendingClaim.ts @@ -0,0 +1,13 @@ +import type {Tile} from "../../models"; +import type {ClaimOption} from "./claimOption.ts"; + +export interface PendingClaim { + // 当前被响应的牌 + tile: Tile + + // 出牌人 + fromPlayerId: string + + // 当前玩家可执行操作 + options: ClaimOption[] +} \ No newline at end of file diff --git a/src/types/state/player.ts b/src/types/state/player.ts new file mode 100644 index 0000000..6362f83 --- /dev/null +++ b/src/types/state/player.ts @@ -0,0 +1,22 @@ +import type {Tile} from "../../models"; +import type {Meld} from "./meld.ts"; + +export interface Player{ + playerId: string + seatIndex: number + + // 手牌(只有自己有完整数据,后端可控制) + handTiles: Tile[] + + // 副露(碰/杠) + melds: Meld[] + + // 出牌区 + discardTiles: Tile[] + + // 分数 + score: number + + // 是否准备 + isReady: boolean +} diff --git a/src/types/tile.ts b/src/types/tile.ts new file mode 100644 index 0000000..6a89bb3 --- /dev/null +++ b/src/types/tile.ts @@ -0,0 +1,9 @@ +export type Suit = 'W' | 'T' | 'B' + +export interface Tile { + id: number + suit: Suit + value: number +} + + diff --git a/src/views/ChengduGamePage.vue b/src/views/ChengduGamePage.vue index 6b10914..b767e20 100644 --- a/src/views/ChengduGamePage.vue +++ b/src/views/ChengduGamePage.vue @@ -17,7 +17,6 @@ import RightPlayerCard from '../components/game/RightPlayerCard.vue' import BottomPlayerCard from '../components/game/BottomPlayerCard.vue' import LeftPlayerCard from '../components/game/LeftPlayerCard.vue' import type {SeatPlayerCardModel} from '../components/game/seat-player-card' -import {type SeatKey, useChengduGameRoom} from '../game/chengdu' const route = useRoute() const router = useRouter() diff --git a/src/ws/client.ts b/src/ws/client.ts new file mode 100644 index 0000000..c88ab34 --- /dev/null +++ b/src/ws/client.ts @@ -0,0 +1,136 @@ +type WsStatus = 'idle' | 'connecting' | 'connected' | 'closed' | 'error' + +type MessageHandler = (data: any) => void +type StatusHandler = (status: WsStatus) => void +type ErrorHandler = (err: string) => void + +class WsClient { + private ws: WebSocket | null = null + private url: string = '' + private token: string = '' // 保存token + + private status: WsStatus = 'idle' + + private messageHandlers: MessageHandler[] = [] + private statusHandlers: StatusHandler[] = [] + private errorHandlers: ErrorHandler[] = [] + + private reconnectTimer: number | null = null + private reconnectDelay = 2000 + + + // 构造带token的url + private buildUrl(): string { + if (!this.token) return this.url + + const hasQuery = this.url.includes('?') + const connector = hasQuery ? '&' : '?' + + return `${this.url}${connector}token=${encodeURIComponent(this.token)}` + } + + + // 连接 + connect(url: string, token?: string) { + if (this.ws && this.status === 'connected') { + return + } + + this.url = url + if (token !== undefined) { + this.token = token // 保存token(用于重连) + } + + this.setStatus('connecting') + + const finalUrl = this.buildUrl() // 使用带token的url + this.ws = new WebSocket(finalUrl) + + this.ws.onopen = () => { + this.setStatus('connected') + } + + this.ws.onmessage = (event) => { + try { + const data = JSON.parse(event.data) + this.messageHandlers.forEach(fn => fn(data)) + } catch (e) { + this.messageHandlers.forEach(fn => fn(event.data)) + } + } + + this.ws.onerror = () => { + this.setStatus('error') + this.emitError('WebSocket error') + } + + this.ws.onclose = () => { + this.setStatus('closed') + this.tryReconnect() + } + } + + + // 发送 + send(data: any) { + if (this.ws && this.status === 'connected') { + this.ws.send(JSON.stringify(data)) + } + } + + + // 关闭 + close() { + if (this.ws) { + this.ws.close() + this.ws = null + } + this.clearReconnect() + } + + + // 订阅 + onMessage(handler: MessageHandler) { + this.messageHandlers.push(handler) + } + + onStatusChange(handler: StatusHandler) { + this.statusHandlers.push(handler) + } + + onError(handler: ErrorHandler) { + this.errorHandlers.push(handler) + } + + + // 内部方法 + private setStatus(status: WsStatus) { + this.status = status + this.statusHandlers.forEach(fn => fn(status)) + } + + private emitError(msg: string) { + this.errorHandlers.forEach(fn => fn(msg)) + } + + private tryReconnect() { + this.clearReconnect() + + this.reconnectTimer = window.setTimeout(() => { + if (this.url) { + this.connect(this.url) // 重连自动带token + } + }, this.reconnectDelay) + } + + private clearReconnect() { + if (this.reconnectTimer !== null) { + window.clearTimeout(this.reconnectTimer) + this.reconnectTimer = null + } + } +} + + +// 单例导出 +export const wsClient = new WsClient() \ No newline at end of file diff --git a/src/ws/handler.ts b/src/ws/handler.ts new file mode 100644 index 0000000..91979ca --- /dev/null +++ b/src/ws/handler.ts @@ -0,0 +1,25 @@ +import {wsClient} from './client' + +type Handler = (msg: any) => void + +const handlerMap: Record = {} + + +// 注册 handler +export function registerHandler(type: string, handler: Handler) { + handlerMap[type] = handler +} + + +// 初始化监听 +export function initWsHandler() { + wsClient.onMessage((msg) => { + const handler = handlerMap[msg.type] + + if (handler) { + handler(msg) + } else { + console.warn('[WS] 未处理消息:', msg.type, msg) + } + }) +} \ No newline at end of file diff --git a/src/ws/message.ts b/src/ws/message.ts new file mode 100644 index 0000000..3b19bf2 --- /dev/null +++ b/src/ws/message.ts @@ -0,0 +1,31 @@ +// 通用消息结构 +/** + * 客户端 → 服务端(请求 / 操作) + */ +export interface ActionMessage { + type: string + sender?: string + target?: string + roomId?: string + seq?: number + status?: string + requestId?: string + trace_id?: string + + payload?: T +} + +/** + * 服务端 → 客户端(事件 / 推送) + */ +export interface ActionEvent { + type: string + target?: string + roomId?: string + seq?: number + status?: string + requestId?: string + trace_id?: string + + payload?: T +} \ No newline at end of file diff --git a/src/ws/url.ts b/src/ws/url.ts new file mode 100644 index 0000000..a8054ed --- /dev/null +++ b/src/ws/url.ts @@ -0,0 +1,13 @@ +const WS_BASE_URL = import.meta.env.VITE_GAME_WS_URL ?? '/api/v1/ws' + +export function buildWsUrl(token: string): string { + const baseUrl = /^wss?:\/\//.test(WS_BASE_URL) + ? new URL(WS_BASE_URL) + : new URL( + WS_BASE_URL.startsWith('/') ? WS_BASE_URL : `/${WS_BASE_URL}`, + `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}`, + ) + + baseUrl.searchParams.set('token', token) + return baseUrl.toString() +} \ No newline at end of file diff --git a/tests/e2e/chengdu-flow.spec.ts b/tests/e2e/chengdu-flow.spec.ts deleted file mode 100644 index 990facc..0000000 --- a/tests/e2e/chengdu-flow.spec.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { expect, test } from '@playwright/test' - -function uniqueUser() { - const stamp = Date.now().toString() - return { - username: `pw${stamp.slice(-8)}`, - phone: `13${stamp.slice(-9)}`, - email: `pw${stamp}@example.com`, - password: 'playwright123', - roomName: `pw-room-${stamp.slice(-6)}`, - } -} - -test('register, login, create room, enter game, and back to hall', async ({ page }) => { - const user = uniqueUser() - - await page.goto('/register') - - const registerInputs = page.locator('.auth-card .form input') - await registerInputs.nth(0).fill(user.username) - await registerInputs.nth(1).fill(user.phone) - await registerInputs.nth(2).fill(user.email) - await registerInputs.nth(3).fill(user.password) - await registerInputs.nth(4).fill(user.password) - await page.locator('.auth-card .primary-btn[type="submit"]').click() - - await page.waitForURL(/\/login/) - - const loginInputs = page.locator('.auth-card .form input') - await expect(loginInputs.nth(0)).toHaveValue(user.phone) - await loginInputs.nth(1).fill(user.password) - await page.locator('.auth-card .primary-btn[type="submit"]').click() - - await page.waitForURL(/\/hall/) - await expect(page.locator('.hall-page')).toBeVisible() - - await page.locator('.room-actions-footer .primary-btn').click() - await expect(page.locator('.modal-card')).toBeVisible() - - await page.locator('.modal-card .field input').first().fill(user.roomName) - await page.locator('.modal-card .modal-actions .primary-btn').click() - - await expect(page.locator('.copy-line')).toHaveCount(2) - await page.locator('.modal-card .modal-actions .primary-btn').click() - - await page.waitForURL(/\/game\/chengdu\//) - await expect(page.locator('.game-page')).toBeVisible() - await expect(page.locator('.table-felt')).toBeVisible() - - await page.locator('.topbar-back-btn').click() - await page.waitForURL(/\/hall/) - await expect(page.locator('.hall-page')).toBeVisible() -})