diff --git a/src/components/board/BoardCanvas.tsx b/src/components/board/BoardCanvas.tsx index 9e08d46..5fcfd4e 100644 --- a/src/components/board/BoardCanvas.tsx +++ b/src/components/board/BoardCanvas.tsx @@ -1,4 +1,5 @@ import { useEffect, useRef, useCallback, useMemo } from 'react'; +import { listen } from '@tauri-apps/api/event'; import { useGameStore, buildReplayBoard } from '../../store/gameStore'; import { computeBoardDimensions, @@ -59,6 +60,20 @@ export default function BoardCanvas() { return () => window.removeEventListener('resize', handleResize); }, [render]); + useEffect(() => { + if (mode !== 'Online') return; + let unlisten: (() => void) | undefined; + + const setup = async () => { + unlisten = await listen<{ x: number; y: number }>('remote-move', (event) => { + placePiece(event.payload.x, event.payload.y); + }); + }; + setup(); + + return () => { unlisten?.(); }; + }, [mode, placePiece]); + const handleClick = useCallback( (e: React.MouseEvent) => { if (status !== 'playing') return; diff --git a/src/components/game/GameControls.tsx b/src/components/game/GameControls.tsx index d427c14..1c7d253 100644 --- a/src/components/game/GameControls.tsx +++ b/src/components/game/GameControls.tsx @@ -10,6 +10,7 @@ export default function GameControls({ onBackToMenu }: Props) { const { t } = useTranslation(); const undo = useGameStore((s) => s.undo); const status = useGameStore((s) => s.status); + const mode = useGameStore((s) => s.mode); const refreshBoard = useGameStore((s) => s.refreshBoard); const handleUndo = () => { @@ -38,7 +39,7 @@ export default function GameControls({ onBackToMenu }: Props) { return (
- - +
diff --git a/src/components/menu/OnlineSetup.tsx b/src/components/menu/OnlineSetup.tsx index ad318e5..d5bc625 100644 --- a/src/components/menu/OnlineSetup.tsx +++ b/src/components/menu/OnlineSetup.tsx @@ -1,6 +1,8 @@ +import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useGameStore } from '../../store/gameStore'; -import { useState } from 'react'; +import { invoke } from '@tauri-apps/api/core'; +import { listen } from '@tauri-apps/api/event'; import { MIN_BOARD_SIZE, MAX_BOARD_SIZE } from '../../core/constants'; import type { GameConfig } from '../../core/types'; @@ -9,25 +11,67 @@ 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 [boardSize, setBoardSize] = useState(15); - - const baseConfig: GameConfig = { - boardSize, useForbiddenRules: true, useTimer: false, - timeLimitSecs: 60, aiDifficulty: 3, playerColor: 'Black', isServer: false, - remoteAddress: '', - }; + const [ip, setIp] = useState(''); + const [myAddress, setMyAddress] = useState(''); + const [isHosting, setIsHosting] = useState(false); const handleHost = async () => { - await startGame('Online', { ...baseConfig, isServer: true }); - onStart(); + try { + const port: number = await invoke('host_game', { port: 0 }); + setMyAddress(`127.0.0.1:${port}`); + setIsHosting(true); + + const unlisten = await listen('connection-status', async (event) => { + if (event.payload === 'connected') { + unlisten(); + const config: GameConfig = { + boardSize, useForbiddenRules: true, useTimer: false, + timeLimitSecs: 60, aiDifficulty: 3, playerColor: 'Black', + isServer: true, remoteAddress: '', hostPort: port, + useLlm: false, llmEndpoint: '', llmApiKey: '', llmModel: '', + }; + await startGame('Online', config); + onStart(); + } + }); + } catch (e) { + alert('创建房间失败: ' + e); + } }; const handleJoin = async () => { - await startGame('Online', { ...baseConfig, remoteAddress: ip }); - onStart(); + try { + const [_, portStr] = ip.split(':'); + const port = parseInt(portStr) || 0; + const config: GameConfig = { + boardSize, useForbiddenRules: true, useTimer: false, + timeLimitSecs: 60, aiDifficulty: 3, playerColor: 'White', + isServer: false, remoteAddress: ip, hostPort: port, + useLlm: false, llmEndpoint: '', llmApiKey: '', llmModel: '', + }; + await startGame('Online', config); + await invoke('join_game', { address: ip }); + onStart(); + } catch (e) { + alert('加入房间失败: ' + e); + } }; + if (isHosting) { + return ( +
+

{t('menu.online_game')}

+

等待对手加入...

+

+ {myAddress} +

+

将此地址发给对手

+ +
+ ); + } + return (

{t('menu.online_game')}

@@ -35,13 +79,13 @@ export default function OnlineSetup({ onBack, onStart }: Props) { {t('settings.board_size')}:
- setIp(e.target.value)} placeholder={t('menu.ip_placeholder') as string} /> + setIp(e.target.value)} placeholder={t('menu.ip_placeholder')} />
diff --git a/src/i18n/en.json b/src/i18n/en.json index fafe6ce..06d1ba3 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -33,7 +33,9 @@ "new_game": "New Game", "waiting_opponent": "Waiting for Opponent...", "your_turn": "Your Turn", - "opponent_turn": "Opponent's Turn" + "opponent_turn": "Opponent's Turn", + "opponent_connected": "Opponent Connected", + "opponent_disconnected": "Opponent Disconnected" }, "replay": { "play": "Play", diff --git a/src/i18n/zh-CN.json b/src/i18n/zh-CN.json index 8824310..fcb587f 100644 --- a/src/i18n/zh-CN.json +++ b/src/i18n/zh-CN.json @@ -33,7 +33,9 @@ "new_game": "新游戏", "waiting_opponent": "等待对手加入...", "your_turn": "你的回合", - "opponent_turn": "对手回合" + "opponent_turn": "对手回合", + "opponent_connected": "对手已连接", + "opponent_disconnected": "对手已断开" }, "replay": { "play": "播放",