mirror of
https://github.com/LHY0125/Gobang-Game.git
synced 2026-06-28 16:35:55 +08:00
fix: 代码审查修复 — serde camelCase/CSP/TS检查/replay/undo/AI禁手/星位/未使用依赖
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useRef, useCallback } from 'react';
|
||||
import { useGameStore } from '../../store/gameStore';
|
||||
import { useEffect, useRef, useCallback, useMemo } from 'react';
|
||||
import { useGameStore, buildReplayBoard } from '../../store/gameStore';
|
||||
import {
|
||||
computeBoardDimensions,
|
||||
canvasToBoard,
|
||||
@@ -15,8 +15,26 @@ export default function BoardCanvas() {
|
||||
const placePiece = useGameStore((s) => s.placePiece);
|
||||
const aiMove = useGameStore((s) => s.aiMove);
|
||||
const moves = useGameStore((s) => s.moves);
|
||||
const replayStep = useGameStore((s) => s.replayStep);
|
||||
|
||||
const lastMove = moves.length > 0 ? moves[moves.length - 1].position : null;
|
||||
// 复盘模式下根据 replayStep 重建棋盘
|
||||
const displayBoard = useMemo(() => {
|
||||
if (mode === 'Replay') {
|
||||
return buildReplayBoard(boardSize, moves, replayStep);
|
||||
}
|
||||
return board;
|
||||
}, [mode, board, boardSize, moves, replayStep]);
|
||||
|
||||
// 复盘模式下的最后一手
|
||||
const displayLastMove = useMemo(() => {
|
||||
if (mode !== 'Replay') {
|
||||
return moves.length > 0 ? moves[moves.length - 1].position : null;
|
||||
}
|
||||
if (replayStep > 0 && replayStep <= moves.length) {
|
||||
return moves[replayStep - 1].position;
|
||||
}
|
||||
return null;
|
||||
}, [mode, moves, replayStep]);
|
||||
|
||||
const render = useCallback(() => {
|
||||
const canvas = canvasRef.current;
|
||||
@@ -31,8 +49,8 @@ export default function BoardCanvas() {
|
||||
ctx.scale(dpr, dpr);
|
||||
|
||||
const cfg = computeBoardDimensions(boardSize, rect.width, rect.height);
|
||||
renderBoard(ctx, board, cfg, lastMove);
|
||||
}, [board, boardSize, lastMove]);
|
||||
renderBoard(ctx, displayBoard, cfg, displayLastMove);
|
||||
}, [displayBoard, boardSize, displayLastMove]);
|
||||
|
||||
useEffect(() => {
|
||||
render();
|
||||
|
||||
@@ -6,6 +6,26 @@ export interface RenderConfig {
|
||||
boardSize: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据棋盘大小动态生成星位坐标。
|
||||
* 标准五子棋/围棋星位: 四角和中心及边中点。
|
||||
* 9x9 起可用,更小的棋盘仅使用中心点。
|
||||
*/
|
||||
export function computeStarPoints(boardSize: number): [number, number][] {
|
||||
if (boardSize < 9) {
|
||||
const mid = Math.floor(boardSize / 2);
|
||||
return [[mid, mid]];
|
||||
}
|
||||
const a = Math.min(3, Math.floor(boardSize / 4));
|
||||
const b = Math.floor(boardSize / 2);
|
||||
const c = boardSize - 1 - a;
|
||||
return [
|
||||
[a, a], [a, b], [a, c],
|
||||
[b, a], [b, b], [b, c],
|
||||
[c, a], [c, b], [c, c],
|
||||
];
|
||||
}
|
||||
|
||||
export function computeBoardDimensions(boardSize: number, canvasWidth: number, canvasHeight: number): RenderConfig {
|
||||
const maxBoardPixelSize = Math.min(canvasWidth, canvasHeight) * 0.85;
|
||||
const cellSize = Math.floor(maxBoardPixelSize / (boardSize - 1));
|
||||
@@ -64,12 +84,8 @@ export function renderBoard(
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// 星位
|
||||
const starPoints = [
|
||||
[3, 3], [3, 7], [3, 11],
|
||||
[7, 3], [7, 7], [7, 11],
|
||||
[11, 3], [11, 7], [11, 11],
|
||||
];
|
||||
// 星位 — 根据棋盘大小动态计算
|
||||
const starPoints = computeStarPoints(boardSize);
|
||||
ctx.fillStyle = '#8B7355';
|
||||
for (const [r, c] of starPoints) {
|
||||
if (r < boardSize && c < boardSize) {
|
||||
|
||||
@@ -8,7 +8,6 @@ interface Props {
|
||||
export default function GameControls({ onBackToMenu }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const undo = useGameStore((s) => s.undo);
|
||||
const mode = useGameStore((s) => s.mode);
|
||||
const status = useGameStore((s) => s.status);
|
||||
|
||||
const handleUndo = () => {
|
||||
|
||||
@@ -24,6 +24,7 @@ export default function AiGameSetup({ onBack, onStart }: Props) {
|
||||
aiDifficulty: difficulty,
|
||||
playerColor,
|
||||
isServer: false,
|
||||
remoteAddress: '',
|
||||
};
|
||||
await startGame('VsAi', config);
|
||||
onStart();
|
||||
|
||||
@@ -20,6 +20,7 @@ export default function LocalGameSetup({ onBack, onStart }: Props) {
|
||||
aiDifficulty: 3,
|
||||
playerColor: 'Black',
|
||||
isServer: false,
|
||||
remoteAddress: '',
|
||||
};
|
||||
await startGame('Local', config);
|
||||
onStart();
|
||||
|
||||
@@ -13,6 +13,7 @@ export default function OnlineSetup({ onBack, onStart }: Props) {
|
||||
const baseConfig: GameConfig = {
|
||||
boardSize: 15, useForbiddenRules: true, useTimer: false,
|
||||
timeLimitSecs: 60, aiDifficulty: 3, playerColor: 'Black', isServer: false,
|
||||
remoteAddress: '',
|
||||
};
|
||||
|
||||
const handleHost = async () => {
|
||||
@@ -21,7 +22,7 @@ export default function OnlineSetup({ onBack, onStart }: Props) {
|
||||
};
|
||||
|
||||
const handleJoin = async () => {
|
||||
await startGame('Online', { ...baseConfig });
|
||||
await startGame('Online', { ...baseConfig, remoteAddress: ip });
|
||||
onStart();
|
||||
};
|
||||
|
||||
|
||||
@@ -12,31 +12,34 @@ interface Props {
|
||||
export default function ReplayView({ onBackToMenu }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const moves = useGameStore((s) => s.moves);
|
||||
const [step, setStep] = useState(moves.length);
|
||||
const replayStep = useGameStore((s) => s.replayStep);
|
||||
const setReplayStep = useGameStore((s) => s.setReplayStep);
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
|
||||
const step = replayStep;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPlaying) return;
|
||||
if (step >= moves.length) {
|
||||
setIsPlaying(false);
|
||||
return;
|
||||
}
|
||||
const timer = setInterval(() => setStep((s) => s + 1), 500);
|
||||
const timer = setInterval(() => setReplayStep(step + 1), 500);
|
||||
return () => clearInterval(timer);
|
||||
}, [isPlaying, step, moves.length]);
|
||||
}, [isPlaying, step, moves.length, setReplayStep]);
|
||||
|
||||
return (
|
||||
<div className="replay-view">
|
||||
<div className="board-container">
|
||||
<BoardCanvas />
|
||||
</div>
|
||||
<StepSlider current={step} total={moves.length} onChange={setStep} />
|
||||
<StepSlider current={step} total={moves.length} onChange={setReplayStep} />
|
||||
<div>{t('replay.step', { current: step, total: moves.length })}</div>
|
||||
<ReplayControls
|
||||
isPlaying={isPlaying}
|
||||
onTogglePlay={() => setIsPlaying(!isPlaying)}
|
||||
onPrev={() => setStep(Math.max(0, step - 1))}
|
||||
onNext={() => setStep(Math.min(moves.length, step + 1))}
|
||||
onPrev={() => setReplayStep(Math.max(0, step - 1))}
|
||||
onNext={() => setReplayStep(Math.min(moves.length, step + 1))}
|
||||
/>
|
||||
<button onClick={onBackToMenu}>返回菜单</button>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
export const DEFAULT_BOARD_SIZE = 15;
|
||||
export const MIN_BOARD_SIZE = 9;
|
||||
export const MAX_BOARD_SIZE = 19;
|
||||
|
||||
export const CELL_COLORS: Record<number, string> = {
|
||||
0: 'transparent',
|
||||
1: '#1a1a1a',
|
||||
2: '#f5f5f5',
|
||||
};
|
||||
|
||||
@@ -19,6 +19,7 @@ export interface GameConfig {
|
||||
aiDifficulty: number;
|
||||
playerColor: Color;
|
||||
isServer: boolean;
|
||||
remoteAddress: string;
|
||||
}
|
||||
|
||||
export interface MoveResult {
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useGameStore } from '../store/gameStore';
|
||||
import type { GameConfig, GameModeType } from '../core/types';
|
||||
|
||||
export function useGame() {
|
||||
const store = useGameStore();
|
||||
|
||||
const startGame = useCallback(async (mode: GameModeType, config: GameConfig) => {
|
||||
await store.startGame(mode, config);
|
||||
}, [store]);
|
||||
|
||||
return { ...store, startGame };
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
export function useTimer(seconds: number, active: boolean, onTimeout: () => void) {
|
||||
const [time, setTime] = useState(seconds);
|
||||
|
||||
useEffect(() => {
|
||||
if (!active) return;
|
||||
setTime(seconds);
|
||||
const timer = setInterval(() => {
|
||||
setTime((t) => {
|
||||
if (t <= 1) { clearInterval(timer); onTimeout(); return 0; }
|
||||
return t - 1;
|
||||
});
|
||||
}, 1000);
|
||||
return () => clearInterval(timer);
|
||||
}, [active, seconds, onTimeout]);
|
||||
|
||||
return time;
|
||||
}
|
||||
+21
-1
@@ -2,6 +2,17 @@ import { create } from 'zustand';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import type { CellState, Color, GameConfig, GameModeType, GameStatus, Move, MoveResult } from '../core/types';
|
||||
|
||||
/** 根据落子列表重建棋盘到指定步数 */
|
||||
export function buildReplayBoard(boardSize: number, moves: Move[], step: number): CellState[][] {
|
||||
const b: CellState[][] = Array.from({ length: boardSize }, () => Array(boardSize).fill(0) as CellState[]);
|
||||
const limit = Math.min(step, moves.length);
|
||||
for (let i = 0; i < limit; i++) {
|
||||
const m = moves[i];
|
||||
b[m.position.x][m.position.y] = (m.color === 'Black' ? 1 : 2) as CellState;
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
interface GameState {
|
||||
mode: GameModeType;
|
||||
board: CellState[][];
|
||||
@@ -12,6 +23,7 @@ interface GameState {
|
||||
moves: Move[];
|
||||
config: GameConfig;
|
||||
isSaving: boolean;
|
||||
replayStep: number;
|
||||
|
||||
startGame: (mode: GameModeType, config: GameConfig) => Promise<void>;
|
||||
placePiece: (x: number, y: number) => Promise<MoveResult>;
|
||||
@@ -19,6 +31,7 @@ interface GameState {
|
||||
aiMove: () => Promise<void>;
|
||||
refreshBoard: () => Promise<void>;
|
||||
loadReplayBoard: (board: CellState[][], moves: Move[]) => void;
|
||||
setReplayStep: (step: number) => void;
|
||||
}
|
||||
|
||||
export const useGameStore = create<GameState>((set, get) => ({
|
||||
@@ -37,8 +50,10 @@ export const useGameStore = create<GameState>((set, get) => ({
|
||||
aiDifficulty: 3,
|
||||
playerColor: 'Black',
|
||||
isServer: false,
|
||||
remoteAddress: '',
|
||||
},
|
||||
isSaving: false,
|
||||
replayStep: 0,
|
||||
|
||||
startGame: async (mode, config) => {
|
||||
await invoke('new_game', { mode, config });
|
||||
@@ -50,6 +65,7 @@ export const useGameStore = create<GameState>((set, get) => ({
|
||||
currentColor: 'Black',
|
||||
winner: null,
|
||||
moves: [],
|
||||
replayStep: 0,
|
||||
});
|
||||
await get().refreshBoard();
|
||||
},
|
||||
@@ -92,6 +108,10 @@ export const useGameStore = create<GameState>((set, get) => ({
|
||||
},
|
||||
|
||||
loadReplayBoard: (board, moves) => {
|
||||
set({ board, moves, mode: 'Replay', status: 'playing' });
|
||||
set({ board, moves, mode: 'Replay', status: 'playing', replayStep: moves.length });
|
||||
},
|
||||
|
||||
setReplayStep: (step) => {
|
||||
set({ replayStep: step });
|
||||
},
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user