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