mirror of
https://github.com/LHY0125/Gobang-Game.git
synced 2026-06-28 16:35:55 +08:00
feat(frontend): 对局视图 + 回放视图 + 计时器 hook
This commit is contained in:
@@ -0,0 +1,26 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useGameStore } from '../../store/gameStore';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onBackToMenu: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = () => {
|
||||||
|
undo(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="game-controls">
|
||||||
|
<button onClick={handleUndo} disabled={status === 'game_over'}>
|
||||||
|
{t('game.undo')}
|
||||||
|
</button>
|
||||||
|
<button onClick={onBackToMenu}>{t('game.new_game')}</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useGameStore } from '../../store/gameStore';
|
||||||
|
|
||||||
|
export default function GameInfo() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const currentColor = useGameStore((s) => s.currentColor);
|
||||||
|
const status = useGameStore((s) => s.status);
|
||||||
|
const winner = useGameStore((s) => s.winner);
|
||||||
|
|
||||||
|
let text = '';
|
||||||
|
if (status === 'game_over' && winner) {
|
||||||
|
text = winner === 'Black' ? t('game.black_win') : t('game.white_win');
|
||||||
|
} else if (status === 'ai_thinking') {
|
||||||
|
text = t('game.ai_thinking');
|
||||||
|
} else if (status === 'playing') {
|
||||||
|
text = currentColor === 'Black' ? t('game.black_turn') : t('game.white_turn');
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="game-info">{text}</div>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import BoardCanvas from '../board/BoardCanvas';
|
||||||
|
import GameInfo from './GameInfo';
|
||||||
|
import GameControls from './GameControls';
|
||||||
|
import TimerDisplay from './TimerDisplay';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onBackToMenu: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function GameView({ onBackToMenu }: Props) {
|
||||||
|
return (
|
||||||
|
<div className="game-view">
|
||||||
|
<GameInfo />
|
||||||
|
<div className="board-container">
|
||||||
|
<BoardCanvas />
|
||||||
|
</div>
|
||||||
|
<TimerDisplay />
|
||||||
|
<GameControls onBackToMenu={onBackToMenu} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useGameStore } from '../../store/gameStore';
|
||||||
|
|
||||||
|
export default function TimerDisplay() {
|
||||||
|
const config = useGameStore((s) => s.config);
|
||||||
|
const currentColor = useGameStore((s) => s.currentColor);
|
||||||
|
const status = useGameStore((s) => s.status);
|
||||||
|
const [time, setTime] = useState(config.timeLimitSecs);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!config.useTimer || status !== 'playing') return;
|
||||||
|
setTime(config.timeLimitSecs);
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
setTime((t) => {
|
||||||
|
if (t <= 1) { clearInterval(timer); return 0; }
|
||||||
|
return t - 1;
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
return () => clearInterval(timer);
|
||||||
|
}, [currentColor, config.useTimer, config.timeLimitSecs, status]);
|
||||||
|
|
||||||
|
if (!config.useTimer) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`timer-display ${time <= 10 ? 'timer-warning' : ''}`}>
|
||||||
|
{Math.floor(time / 60)}:{(time % 60).toString().padStart(2, '0')}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
isPlaying: boolean;
|
||||||
|
onTogglePlay: () => void;
|
||||||
|
onPrev: () => void;
|
||||||
|
onNext: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ReplayControls({ isPlaying, onTogglePlay, onPrev, onNext }: Props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<div className="replay-controls">
|
||||||
|
<button onClick={onPrev}>{t('replay.prev')}</button>
|
||||||
|
<button onClick={onTogglePlay}>{isPlaying ? t('replay.pause') : t('replay.play')}</button>
|
||||||
|
<button onClick={onNext}>{t('replay.next')}</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useGameStore } from '../../store/gameStore';
|
||||||
|
import BoardCanvas from '../board/BoardCanvas';
|
||||||
|
import StepSlider from './StepSlider';
|
||||||
|
import ReplayControls from './ReplayControls';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onBackToMenu: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ReplayView({ onBackToMenu }: Props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const moves = useGameStore((s) => s.moves);
|
||||||
|
const [step, setStep] = useState(moves.length);
|
||||||
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isPlaying) return;
|
||||||
|
if (step >= moves.length) {
|
||||||
|
setIsPlaying(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const timer = setInterval(() => setStep((s) => s + 1), 500);
|
||||||
|
return () => clearInterval(timer);
|
||||||
|
}, [isPlaying, step, moves.length]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="replay-view">
|
||||||
|
<div className="board-container">
|
||||||
|
<BoardCanvas />
|
||||||
|
</div>
|
||||||
|
<StepSlider current={step} total={moves.length} onChange={setStep} />
|
||||||
|
<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))}
|
||||||
|
/>
|
||||||
|
<button onClick={onBackToMenu}>返回菜单</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
interface Props {
|
||||||
|
current: number;
|
||||||
|
total: number;
|
||||||
|
onChange: (step: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function StepSlider({ current, total, onChange }: Props) {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min={0}
|
||||||
|
max={total}
|
||||||
|
value={current}
|
||||||
|
onChange={(e) => onChange(Number(e.target.value))}
|
||||||
|
className="step-slider"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
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 };
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user