diff --git a/src/components/menu/AiGameSetup.tsx b/src/components/menu/AiGameSetup.tsx new file mode 100644 index 0000000..e10f727 --- /dev/null +++ b/src/components/menu/AiGameSetup.tsx @@ -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('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 ( +
+

{t('menu.ai_game')}

+ + + +
+ + +
+
+ ); +} diff --git a/src/components/menu/LoadReplay.tsx b/src/components/menu/LoadReplay.tsx new file mode 100644 index 0000000..951235e --- /dev/null +++ b/src/components/menu/LoadReplay.tsx @@ -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(null); + + const handleFile = (e: React.ChangeEvent) => { + 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 ( +
+

{t('menu.load_replay')}

+ + +
+ ); +} diff --git a/src/components/menu/LocalGameSetup.tsx b/src/components/menu/LocalGameSetup.tsx new file mode 100644 index 0000000..0af544b --- /dev/null +++ b/src/components/menu/LocalGameSetup.tsx @@ -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 ( +
+

{t('menu.local_game')}

+
+ + +
+
+ ); +} diff --git a/src/components/menu/MainMenu.tsx b/src/components/menu/MainMenu.tsx new file mode 100644 index 0000000..7e2a86c --- /dev/null +++ b/src/components/menu/MainMenu.tsx @@ -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('main'); + + if (view === 'local') return setView('main')} onStart={onGameStart} />; + if (view === 'ai') return setView('main')} onStart={onGameStart} />; + if (view === 'online') return setView('main')} onStart={onGameStart} />; + if (view === 'replay') return setView('main')} onStart={onGameStart} />; + + return ( +
+

{t('app.title')}

+
+ + + + +
+
+ ); +} diff --git a/src/components/menu/OnlineSetup.tsx b/src/components/menu/OnlineSetup.tsx new file mode 100644 index 0000000..eb44e37 --- /dev/null +++ b/src/components/menu/OnlineSetup.tsx @@ -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 ( +
+

{t('menu.online_game')}

+ +
+ setIp(e.target.value)} placeholder="IP:端口" /> + +
+ +
+ ); +}