From eeafa92e9018bf328e29d48f650ea9026ec7d52a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E8=88=AA=E5=AE=87?= <3364451258@qq.com> Date: Sun, 31 May 2026 00:20:32 +0800 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20=E7=B1=BB=E5=9E=8B=E5=AE=9A?= =?UTF-8?q?=E4=B9=89=20+=20i18n=20=E4=B8=AD=E8=8B=B1=E5=8F=8C=E8=AF=AD=20+?= =?UTF-8?q?=20Zustand=20store?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 --- src/core/constants.ts | 9 ++++ src/core/types.ts | 34 +++++++++++++++ src/i18n/en.json | 40 +++++++++++++++++ src/i18n/index.ts | 18 ++++++++ src/i18n/zh-CN.json | 40 +++++++++++++++++ src/store/gameStore.ts | 97 ++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 238 insertions(+) create mode 100644 src/core/constants.ts create mode 100644 src/core/types.ts create mode 100644 src/i18n/en.json create mode 100644 src/i18n/index.ts create mode 100644 src/i18n/zh-CN.json create mode 100644 src/store/gameStore.ts diff --git a/src/core/constants.ts b/src/core/constants.ts new file mode 100644 index 0000000..491b6c7 --- /dev/null +++ b/src/core/constants.ts @@ -0,0 +1,9 @@ +export const DEFAULT_BOARD_SIZE = 15; +export const MIN_BOARD_SIZE = 9; +export const MAX_BOARD_SIZE = 19; + +export const CELL_COLORS: Record = { + 0: 'transparent', + 1: '#1a1a1a', + 2: '#f5f5f5', +}; diff --git a/src/core/types.ts b/src/core/types.ts new file mode 100644 index 0000000..58160db --- /dev/null +++ b/src/core/types.ts @@ -0,0 +1,34 @@ +export type Color = 'Black' | 'White'; + +export interface Position { + x: number; + y: number; +} + +export type CellState = 0 | 1 | 2; // 0=Empty, 1=Black, 2=White + +export type GameStatus = 'waiting' | 'playing' | 'ai_thinking' | 'game_over'; + +export type GameModeType = 'Local' | 'VsAi' | 'Online' | 'Replay'; + +export interface GameConfig { + boardSize: number; + useForbiddenRules: boolean; + useTimer: boolean; + timeLimitSecs: number; + aiDifficulty: number; + playerColor: Color; + isServer: boolean; +} + +export interface MoveResult { + position: Position; + is_win: boolean; + is_forbidden: boolean; +} + +export interface Move { + position: Position; + color: Color; + turn: number; +} diff --git a/src/i18n/en.json b/src/i18n/en.json new file mode 100644 index 0000000..820e4d9 --- /dev/null +++ b/src/i18n/en.json @@ -0,0 +1,40 @@ +{ + "app": { "title": "Gobang v2.0" }, + "menu": { + "local_game": "Local 2-Player", + "ai_game": "VS AI", + "online_game": "Online", + "load_replay": "Load Replay", + "settings": "Settings" + }, + "game": { + "black_turn": "Black's Turn", + "white_turn": "White's Turn", + "black_win": "Black Wins!", + "white_win": "White Wins!", + "draw": "Draw", + "ai_thinking": "AI Thinking...", + "undo": "Undo", + "resign": "Resign", + "save": "Save Record", + "new_game": "New Game", + "waiting_opponent": "Waiting for Opponent...", + "your_turn": "Your Turn", + "opponent_turn": "Opponent's Turn" + }, + "replay": { + "play": "Play", + "pause": "Pause", + "next": "Next", + "prev": "Prev", + "step": "Step {{current}}/{{total}}" + }, + "settings": { + "board_size": "Board Size", + "forbidden_rules": "Forbidden Rules", + "timer": "Timer", + "time_limit": "Time Limit (s)", + "difficulty": "AI Difficulty", + "language": "Language" + } +} diff --git a/src/i18n/index.ts b/src/i18n/index.ts new file mode 100644 index 0000000..63c11d8 --- /dev/null +++ b/src/i18n/index.ts @@ -0,0 +1,18 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import zhCN from './zh-CN.json'; +import en from './en.json'; + +i18n + .use(initReactI18next) + .init({ + resources: { + 'zh-CN': { translation: zhCN }, + en: { translation: en }, + }, + lng: 'zh-CN', + fallbackLng: 'zh-CN', + interpolation: { escapeValue: false }, + }); + +export default i18n; diff --git a/src/i18n/zh-CN.json b/src/i18n/zh-CN.json new file mode 100644 index 0000000..fc7ef80 --- /dev/null +++ b/src/i18n/zh-CN.json @@ -0,0 +1,40 @@ +{ + "app": { "title": "五子棋 v2.0" }, + "menu": { + "local_game": "本地双人", + "ai_game": "人机对战", + "online_game": "网络对战", + "load_replay": "加载棋谱", + "settings": "设置" + }, + "game": { + "black_turn": "黑棋回合", + "white_turn": "白棋回合", + "black_win": "黑棋获胜!", + "white_win": "白棋获胜!", + "draw": "平局", + "ai_thinking": "AI 思考中...", + "undo": "悔棋", + "resign": "认输", + "save": "保存棋谱", + "new_game": "新游戏", + "waiting_opponent": "等待对手加入...", + "your_turn": "你的回合", + "opponent_turn": "对手回合" + }, + "replay": { + "play": "播放", + "pause": "暂停", + "next": "下一步", + "prev": "上一步", + "step": "第 {{current}}/{{total}} 步" + }, + "settings": { + "board_size": "棋盘大小", + "forbidden_rules": "禁手规则", + "timer": "计时器", + "time_limit": "时间限制(秒)", + "difficulty": "AI 难度", + "language": "语言" + } +} diff --git a/src/store/gameStore.ts b/src/store/gameStore.ts new file mode 100644 index 0000000..12ddf04 --- /dev/null +++ b/src/store/gameStore.ts @@ -0,0 +1,97 @@ +import { create } from 'zustand'; +import { invoke } from '@tauri-apps/api/core'; +import type { CellState, Color, GameConfig, GameModeType, GameStatus, Move, MoveResult } from '../core/types'; + +interface GameState { + mode: GameModeType; + board: CellState[][]; + boardSize: number; + currentColor: Color; + status: GameStatus; + winner: Color | null; + moves: Move[]; + config: GameConfig; + isSaving: boolean; + + startGame: (mode: GameModeType, config: GameConfig) => Promise; + placePiece: (x: number, y: number) => Promise; + undo: (steps?: number) => Promise; + aiMove: () => Promise; + refreshBoard: () => Promise; + loadReplayBoard: (board: CellState[][], moves: Move[]) => void; +} + +export const useGameStore = create((set, get) => ({ + mode: 'Local', + board: [], + boardSize: 15, + currentColor: 'Black', + status: 'waiting', + winner: null, + moves: [], + config: { + boardSize: 15, + useForbiddenRules: true, + useTimer: false, + timeLimitSecs: 60, + aiDifficulty: 3, + playerColor: 'Black', + isServer: false, + }, + isSaving: false, + + startGame: async (mode, config) => { + await invoke('new_game', { mode, config }); + set({ + mode, + config, + boardSize: config.boardSize, + status: mode === 'VsAi' && config.playerColor === 'White' ? 'ai_thinking' : 'playing', + currentColor: 'Black', + winner: null, + moves: [], + }); + await get().refreshBoard(); + }, + + placePiece: async (x, y) => { + const result: MoveResult = await invoke('place_piece', { x, y }); + await get().refreshBoard(); + if (result.is_win) { + set({ status: 'game_over' }); + } + return result; + }, + + undo: async (steps = 1) => { + await invoke('undo', { steps }); + await get().refreshBoard(); + }, + + aiMove: async () => { + set({ status: 'ai_thinking' }); + const pos: [number, number] | null = await invoke('ai_move'); + if (pos) { + const result = await get().placePiece(pos[0], pos[1]); + if (!result.is_win) { + set({ status: 'playing' }); + } + } else { + set({ status: 'playing' }); + } + }, + + refreshBoard: async () => { + const state: { board: CellState[][]; current_color: string; game_over: boolean } = + await invoke('get_game_state'); + set({ + board: state.board, + currentColor: state.current_color as Color, + status: state.game_over ? 'game_over' : get().status === 'ai_thinking' ? 'ai_thinking' : 'playing', + }); + }, + + loadReplayBoard: (board, moves) => { + set({ board, moves, mode: 'Replay', status: 'playing' }); + }, +}));