mirror of
https://github.com/LHY0125/Gobang-Game.git
synced 2026-06-28 16:35:55 +08:00
feat(frontend): 菜单组件 — 主菜单/本地双人/AI设置/网络/加载棋谱
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,60 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useGameStore } from '../../store/gameStore';
|
||||||
|
import type { Color, GameConfig } from '../../core/types';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onBack: () => void;
|
||||||
|
onStart: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AiGameSetup({ onBack, onStart }: Props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const startGame = useGameStore((s) => s.startGame);
|
||||||
|
const [difficulty, setDifficulty] = useState(3);
|
||||||
|
const [playerColor, setPlayerColor] = useState<Color>('Black');
|
||||||
|
const [useForbidden, setUseForbidden] = useState(true);
|
||||||
|
|
||||||
|
const handleStart = async () => {
|
||||||
|
const config: GameConfig = {
|
||||||
|
boardSize: 15,
|
||||||
|
useForbiddenRules: useForbidden,
|
||||||
|
useTimer: false,
|
||||||
|
timeLimitSecs: 60,
|
||||||
|
aiDifficulty: difficulty,
|
||||||
|
playerColor,
|
||||||
|
isServer: false,
|
||||||
|
};
|
||||||
|
await startGame('VsAi', config);
|
||||||
|
onStart();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="setup-panel">
|
||||||
|
<h2>{t('menu.ai_game')}</h2>
|
||||||
|
<label>
|
||||||
|
{t('settings.difficulty')}:
|
||||||
|
<select value={difficulty} onChange={(e) => setDifficulty(Number(e.target.value))}>
|
||||||
|
{[1, 2, 3, 4, 5].map((d) => (
|
||||||
|
<option key={d} value={d}>{d}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
先手:
|
||||||
|
<select value={playerColor} onChange={(e) => setPlayerColor(e.target.value as Color)}>
|
||||||
|
<option value="Black">黑棋 (先手)</option>
|
||||||
|
<option value="White">白棋 (后手)</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" checked={useForbidden} onChange={(e) => setUseForbidden(e.target.checked)} />
|
||||||
|
{t('settings.forbidden_rules')}
|
||||||
|
</label>
|
||||||
|
<div className="setup-actions">
|
||||||
|
<button onClick={handleStart}>{t('game.new_game')}</button>
|
||||||
|
<button onClick={onBack}>返回</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useGameStore } from '../../store/gameStore';
|
||||||
|
import { useRef } from 'react';
|
||||||
|
import type { CellState, Move } from '../../core/types';
|
||||||
|
|
||||||
|
interface Props { onBack: () => void; onStart: () => void; }
|
||||||
|
|
||||||
|
export default function LoadReplay({ onBack, onStart }: Props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const loadReplayBoard = useGameStore((s) => s.loadReplayBoard);
|
||||||
|
const fileRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const handleFile = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(reader.result as string);
|
||||||
|
const board: CellState[][] = Array.from({ length: json.board_size }, () =>
|
||||||
|
Array(json.board_size).fill(0)
|
||||||
|
);
|
||||||
|
const moves: Move[] = [];
|
||||||
|
for (const m of json.moves) {
|
||||||
|
board[m.x][m.y] = (m.color === 'Black' ? 1 : 2) as CellState;
|
||||||
|
moves.push({ position: { x: m.x, y: m.y }, color: m.color, turn: m.turn });
|
||||||
|
}
|
||||||
|
loadReplayBoard(board, moves);
|
||||||
|
onStart();
|
||||||
|
} catch {
|
||||||
|
alert('无效的棋谱文件');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="setup-panel">
|
||||||
|
<h2>{t('menu.load_replay')}</h2>
|
||||||
|
<input ref={fileRef} type="file" accept=".json" onChange={handleFile} />
|
||||||
|
<button onClick={onBack} style={{ marginTop: 12 }}>返回</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useGameStore } from '../../store/gameStore';
|
||||||
|
import type { GameConfig } from '../../core/types';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onBack: () => void;
|
||||||
|
onStart: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LocalGameSetup({ onBack, onStart }: Props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const startGame = useGameStore((s) => s.startGame);
|
||||||
|
|
||||||
|
const handleStart = async () => {
|
||||||
|
const config: GameConfig = {
|
||||||
|
boardSize: 15,
|
||||||
|
useForbiddenRules: true,
|
||||||
|
useTimer: false,
|
||||||
|
timeLimitSecs: 60,
|
||||||
|
aiDifficulty: 3,
|
||||||
|
playerColor: 'Black',
|
||||||
|
isServer: false,
|
||||||
|
};
|
||||||
|
await startGame('Local', config);
|
||||||
|
onStart();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="setup-panel">
|
||||||
|
<h2>{t('menu.local_game')}</h2>
|
||||||
|
<div className="setup-actions">
|
||||||
|
<button onClick={handleStart}>{t('game.new_game')}</button>
|
||||||
|
<button onClick={onBack}>返回</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import LocalGameSetup from './LocalGameSetup';
|
||||||
|
import AiGameSetup from './AiGameSetup';
|
||||||
|
import OnlineSetup from './OnlineSetup';
|
||||||
|
import LoadReplay from './LoadReplay';
|
||||||
|
|
||||||
|
type View = 'main' | 'local' | 'ai' | 'online' | 'replay';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onGameStart: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MainMenu({ onGameStart }: Props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [view, setView] = useState<View>('main');
|
||||||
|
|
||||||
|
if (view === 'local') return <LocalGameSetup onBack={() => setView('main')} onStart={onGameStart} />;
|
||||||
|
if (view === 'ai') return <AiGameSetup onBack={() => setView('main')} onStart={onGameStart} />;
|
||||||
|
if (view === 'online') return <OnlineSetup onBack={() => setView('main')} onStart={onGameStart} />;
|
||||||
|
if (view === 'replay') return <LoadReplay onBack={() => setView('main')} onStart={onGameStart} />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="main-menu">
|
||||||
|
<h1 className="menu-title">{t('app.title')}</h1>
|
||||||
|
<div className="menu-buttons">
|
||||||
|
<button onClick={() => setView('local')}>{t('menu.local_game')}</button>
|
||||||
|
<button onClick={() => setView('ai')}>{t('menu.ai_game')}</button>
|
||||||
|
<button onClick={() => setView('online')}>{t('menu.online_game')}</button>
|
||||||
|
<button onClick={() => setView('replay')}>{t('menu.load_replay')}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useGameStore } from '../../store/gameStore';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import type { GameConfig } from '../../core/types';
|
||||||
|
|
||||||
|
interface Props { onBack: () => void; onStart: () => void; }
|
||||||
|
|
||||||
|
export default function OnlineSetup({ onBack, onStart }: Props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const startGame = useGameStore((s) => s.startGame);
|
||||||
|
const [ip, setIp] = useState('');
|
||||||
|
|
||||||
|
const baseConfig: GameConfig = {
|
||||||
|
boardSize: 15, useForbiddenRules: true, useTimer: false,
|
||||||
|
timeLimitSecs: 60, aiDifficulty: 3, playerColor: 'Black', isServer: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleHost = async () => {
|
||||||
|
await startGame('Online', { ...baseConfig, isServer: true });
|
||||||
|
onStart();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleJoin = async () => {
|
||||||
|
await startGame('Online', { ...baseConfig });
|
||||||
|
onStart();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="setup-panel">
|
||||||
|
<h2>{t('menu.online_game')}</h2>
|
||||||
|
<button onClick={handleHost}>创建房间</button>
|
||||||
|
<div style={{ display: 'flex', gap: 8, marginTop: 12 }}>
|
||||||
|
<input value={ip} onChange={(e) => setIp(e.target.value)} placeholder="IP:端口" />
|
||||||
|
<button onClick={handleJoin} disabled={!ip}>加入房间</button>
|
||||||
|
</div>
|
||||||
|
<button onClick={onBack} style={{ marginTop: 12 }}>返回</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user