From 802968df3dea486a2aaf8b0cbe439634c27a3a64 Mon Sep 17 00:00:00 2001 From: Slavasil Date: Mon, 2 Nov 2026 14:29:23 +0300 Subject: [PATCH] implement ship placing UI --- frontend/src/components/GameScreen.jsx | 157 +++++- frontend/src/components/SeaBoard.jsx | 109 +++++ .../src/components/ShipPlacementScreen.jsx | 455 ++++++++++++++++++ frontend/src/game.js | 68 ++- frontend/src/shipLogic.js | 226 +++++++++ frontend/src/styles.css | 405 ++++++++++++++++ 6 files changed, 1379 insertions(+), 41 deletions(-) create mode 100644 frontend/src/components/SeaBoard.jsx create mode 100644 frontend/src/components/ShipPlacementScreen.jsx create mode 100644 frontend/src/shipLogic.js diff --git a/frontend/src/components/GameScreen.jsx b/frontend/src/components/GameScreen.jsx index 94bfcf7..c953966 100644 --- a/frontend/src/components/GameScreen.jsx +++ b/frontend/src/components/GameScreen.jsx @@ -1,19 +1,70 @@ -import { useEffect, useRef, useState } from 'react' -import { createGameConnection } from '../game' +import { useEffect, useRef, useState, useCallback } from 'react' +import { + createGameConnection, + STATE_PLACING_SHIPS, + STATE_OPPONENT_READY, + STATE_WAIT_OPPONENT_READY, + STATE_YOUR_TURN, + STATE_WAIT_OPPONENT_TURN, +} from '../game' +import ShipPlacementScreen from './ShipPlacementScreen' export default function GameScreen({ session, onLeave }) { - const [status, setStatus] = useState('connecting') // 'connecting' | 'ready' | 'error' + const [status, setStatus] = useState('connecting') // 'connecting' | 'placing' | 'game' | 'error' + const [gameState, setGameState] = useState(STATE_PLACING_SHIPS) + const [isSubmitting, setIsSubmitting] = useState(false) + const [opponentReady, setOpponentReady] = useState(false) const [error, setError] = useState(null) + + const connectionRef = useRef(null) const attemptRef = useRef(0) useEffect(() => { const attempt = ++attemptRef.current let cancelled = false - const connection = createGameConnection(session) + const connInstance = createGameConnection(session) - connection.promise - .then(() => { - if (!cancelled && attempt === attemptRef.current) setStatus('ready') + connInstance.promise + .then((conn) => { + if (cancelled || attempt !== attemptRef.current) return + connectionRef.current = conn + + conn.onState = (newState) => { + if (cancelled || attempt !== attemptRef.current) return + + if (newState === STATE_OPPONENT_READY) { + setOpponentReady(true) + } else if ( + newState === STATE_WAIT_OPPONENT_READY || + newState === STATE_YOUR_TURN || + newState === STATE_WAIT_OPPONENT_TURN + ) { + setIsSubmitting(false) + setGameState(newState) + setStatus('game') + } + } + + conn.onEnd = (_code, reason) => { + if (cancelled || attempt !== attemptRef.current) return + setIsSubmitting(false) + setError(reason || 'Партия завершена') + setStatus('error') + } + + conn.onClose = () => { + if (cancelled || attempt !== attemptRef.current) return + setIsSubmitting(false) + setStatus((prev) => { + if (prev !== 'error') { + setError('Соединение с сервером прервано') + return 'error' + } + return prev + }) + } + + setStatus('placing') }) .catch((err) => { if (cancelled || attempt !== attemptRef.current) return @@ -23,33 +74,89 @@ export default function GameScreen({ session, onLeave }) { return () => { cancelled = true - connection.cancel() + connInstance.cancel() } }, [session]) + const handleReady = useCallback((ships) => { + if (!connectionRef.current) return + setIsSubmitting(true) + connectionRef.current.placeShips(ships) + }, []) + + // Текст заглушки в зависимости от состояния игры + const getGameStubText = () => { + switch (gameState) { + case STATE_WAIT_OPPONENT_READY: + return 'Соперник расставляет корабли...' + case STATE_YOUR_TURN: + return 'Ваш ход' + case STATE_WAIT_OPPONENT_TURN: + return 'Соперник ходит' + default: + return '' + } + } + return ( -
-
- {status === 'connecting' && ( - <> -
Подключение к игре...
+
+ {status === 'connecting' && ( +
+
Подключение к игре...
+ +
+ )} + + {status === 'placing' && ( +
+ +
+ )} + + {status === 'game' && ( +
+
{getGameStubText()}
+ {(gameState === STATE_WAIT_OPPONENT_READY || + gameState === STATE_WAIT_OPPONENT_TURN) && ( - - )} - {status === 'ready' &&
Расстановка кораблей
} - {status === 'error' && ( - <> -
{error}
- - - )} -
+ )} +
+ )} + + {status === 'error' && ( +
+
{error}
+ +
+ )} + + {/* Модальный pop-up с надписью "Загрузка...", пока сервер не ответит сменой состояния */} + {isSubmitting && ( +
+
+

Загрузка...

+
+ + + +
+
+
+ )}
) } diff --git a/frontend/src/components/SeaBoard.jsx b/frontend/src/components/SeaBoard.jsx new file mode 100644 index 0000000..aa92d44 --- /dev/null +++ b/frontend/src/components/SeaBoard.jsx @@ -0,0 +1,109 @@ +import { ROW_LETTERS, COL_NUMBERS } from '../shipLogic' + +/** + * Компонент игрового поля 10x10 с вертикальной (А-К) и горизонтальной (1-10) линейками. + * Линейки идут слева и сверху параллельно полю и отделены небольшим отступом. + */ +export default function SeaBoard({ + ships = [], + preview = null, + onShipPointerDown, + gridRef, + className = '', +}) { + return ( +
+ {/* Пустой угол (пересечение линеек) */} +