From 11b062b959caf4b364014151efd1d6f2d0b05607 Mon Sep 17 00:00:00 2001 From: oleh Date: Fri, 14 Mar 2025 20:11:39 +0100 Subject: [PATCH] first --- .gitignore | 47 +++++ backend/Dockerfile | 20 +++ backend/index.js | 199 +++++++++++++++++++++ backend/package.json | 18 ++ docker-compose.yml | 32 ++++ frontend/.gitignore | 24 +++ frontend/Dockerfile | 28 +++ frontend/README.md | 12 ++ frontend/eslint.config.js | 33 ++++ frontend/index.html | 13 ++ frontend/package.json | 27 +++ frontend/public/vite.svg | 1 + frontend/src/App.css | 42 +++++ frontend/src/App.jsx | 107 +++++++++++ frontend/src/assets/react.svg | 1 + frontend/src/components/Board.css | 9 + frontend/src/components/Board.jsx | 46 +++++ frontend/src/components/Cell.css | 24 +++ frontend/src/components/Cell.jsx | 24 +++ frontend/src/components/GameBoard.css | 66 +++++++ frontend/src/components/GameBoard.jsx | 174 ++++++++++++++++++ frontend/src/components/Header.css | 12 ++ frontend/src/components/Header.jsx | 12 ++ frontend/src/components/PlacementBoard.css | 52 ++++++ frontend/src/components/PlacementBoard.jsx | 90 ++++++++++ frontend/src/components/PlayerForm.css | 123 +++++++++++++ frontend/src/components/PlayerForm.jsx | 104 +++++++++++ frontend/src/index.css | 0 frontend/src/main.jsx | 10 ++ frontend/vite.config.js | 7 + prepare-app.sh | 34 ++++ remove-app.sh | 10 ++ start-app.sh | 9 + stop-app.sh | 6 + 34 files changed, 1416 insertions(+) create mode 100644 .gitignore create mode 100644 backend/Dockerfile create mode 100644 backend/index.js create mode 100644 backend/package.json create mode 100644 docker-compose.yml create mode 100644 frontend/.gitignore create mode 100644 frontend/Dockerfile create mode 100644 frontend/README.md create mode 100644 frontend/eslint.config.js create mode 100644 frontend/index.html create mode 100644 frontend/package.json create mode 100644 frontend/public/vite.svg create mode 100644 frontend/src/App.css create mode 100644 frontend/src/App.jsx create mode 100644 frontend/src/assets/react.svg create mode 100644 frontend/src/components/Board.css create mode 100644 frontend/src/components/Board.jsx create mode 100644 frontend/src/components/Cell.css create mode 100644 frontend/src/components/Cell.jsx create mode 100644 frontend/src/components/GameBoard.css create mode 100644 frontend/src/components/GameBoard.jsx create mode 100644 frontend/src/components/Header.css create mode 100644 frontend/src/components/Header.jsx create mode 100644 frontend/src/components/PlacementBoard.css create mode 100644 frontend/src/components/PlacementBoard.jsx create mode 100644 frontend/src/components/PlayerForm.css create mode 100644 frontend/src/components/PlayerForm.jsx create mode 100644 frontend/src/index.css create mode 100644 frontend/src/main.jsx create mode 100644 frontend/vite.config.js create mode 100644 prepare-app.sh create mode 100644 remove-app.sh create mode 100644 start-app.sh create mode 100644 stop-app.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..146054c --- /dev/null +++ b/.gitignore @@ -0,0 +1,47 @@ + +.DS_Store +Thumbs.db + + +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + + +node_modules/ +package-lock.json +.pnpm-debug.log +.npmrc + + +build/ +dist/ + + +*.env +docker-compose.override.yml +docker-compose*.log +.dockerignore + + +.vscode/ +.idea/ +*.sw? + + +.DS_Store + + +*~ + + +*.lnk + + +tmp/ +temp/ + + +.cache/ +.history/ diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..11ff20c --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,20 @@ + +FROM node:14-alpine + + +WORKDIR /app + + +COPY package*.json ./ + + +RUN npm install + + +COPY . . + + +EXPOSE 4000 + + +CMD ["npm", "start"] diff --git a/backend/index.js b/backend/index.js new file mode 100644 index 0000000..6f02de8 --- /dev/null +++ b/backend/index.js @@ -0,0 +1,199 @@ +const express = require('express'); +const cors = require('cors'); +const mysql = require('mysql2/promise'); + +const app = express(); +app.use(cors()); +app.use(express.json()); + +(async () => { + console.log("Initializing database connection..."); + + try { + const pool = await mysql.createPool({ + host: 'host.docker.internal', + user: 'root', + password: 'somepassword', + database: 'battleship', + waitForConnections: true, + connectionLimit: 10, + queueLimit: 0 + }); + + console.log("Successfully connected to MySQL!"); + + + const conn = await pool.getConnection(); + const [rows] = await conn.query("SELECT VERSION() AS mysql_version"); + console.log(`MySQL Version: ${rows[0].mysql_version}`); + conn.release(); + + + console.log("Checking or creating tables..."); + await pool.execute(` + CREATE TABLE IF NOT EXISTS games ( + id INT AUTO_INCREMENT PRIMARY KEY, + player1 VARCHAR(255), + player2 VARCHAR(255), + player1_ships JSON, + player2_ships JSON, + current_turn INT DEFAULT 1, + game_state JSON NOT NULL + ) + `); + console.log("Table 'games' is ready!"); + + await pool.execute(` + CREATE TABLE IF NOT EXISTS game_results ( + id INT AUTO_INCREMENT PRIMARY KEY, + game_id INT, + winner VARCHAR(255), + loser VARCHAR(255), + moves INT + ) + `); + console.log("Table 'game_results' is ready!"); + +app.get('/api/results', async (req, res) => { + try { + const [results] = await pool.execute('SELECT * FROM game_results ORDER BY id DESC'); + res.json({ results }); + } catch (err) { + console.error("Error fetching results:", err); + res.status(500).json({ error: "Database error" }); + } +}); + + + app.post('/api/new-game', async (req, res) => { + const { player1, player2 } = req.body; + if (!player1 || !player2) { + console.warn("Missing player names in request."); + return res.status(400).json({ error: 'Player names required' }); + } + + const gameState = { + boardSize: 5, + hits: [], + misses: [], + gameOver: false + }; + + try { + console.log(`Creating new game for ${player1} vs ${player2}...`); + const [result] = await pool.execute( + 'INSERT INTO games (player1, player2, game_state) VALUES (?, ?, ?)', + [player1, player2, JSON.stringify(gameState)] + ); + console.log(`Game created with ID: ${result.insertId}`); + res.json({ gameId: result.insertId, message: 'Game created' }); + } catch (err) { + console.error('Error creating game:', err); + res.status(500).json({ error: 'Database error' }); + } + }); + + + app.post('/api/place-ships', async (req, res) => { + const { gameId, player, ships } = req.body; + if (!gameId || !player || !ships) { + console.warn("Missing parameters for placing ships."); + return res.status(400).json({ error: 'Missing parameters' }); + } + + try { + console.log(`Placing ships for player ${player} in game ID: ${gameId}...`); + const [rows] = await pool.execute('SELECT * FROM games WHERE id = ?', [gameId]); + if (rows.length === 0) { + console.warn(`Game with ID ${gameId} not found.`); + return res.status(404).json({ error: 'Game not found' }); + } + + let updateField = player === 1 ? 'player1_ships' : 'player2_ships'; + await pool.execute(`UPDATE games SET ${updateField} = ? WHERE id = ?`, [JSON.stringify(ships), gameId]); + console.log(`Ships placed for player ${player} in game ID: ${gameId}`); + + res.json({ message: `Ships placed for player ${player}` }); + } catch (err) { + console.error('Error placing ships:', err); + res.status(500).json({ error: 'Database error' }); + } + }); + + // 🎯 Атака + app.post('/api/attack', async (req, res) => { + const { gameId, row, col, player } = req.body; + if (!gameId || typeof row === 'undefined' || typeof col === 'undefined' || !player) { + console.warn("Missing attack parameters in request."); + return res.status(400).json({ error: 'Missing parameters' }); + } + + try { + console.log(`Player ${player} attacks (${row}, ${col}) in game ID: ${gameId}`); + const [rows] = await pool.execute('SELECT * FROM games WHERE id = ?', [gameId]); + if (rows.length === 0) { + console.warn(`Game with ID ${gameId} not found.`); + return res.status(404).json({ error: 'Game not found' }); + } + + let game = rows[0]; + let state = JSON.parse(game.game_state); + let opponentShips = player === 1 ? JSON.parse(game.player2_ships || "[]") : JSON.parse(game.player1_ships || "[]"); + + const hit = opponentShips.some(ship => ship.row === row && ship.col === col); + + if (hit) { + console.log(`HIT! Player ${player} hit an opponent's ship at (${row}, ${col})`); + state.hits.push({ row, col }); + if (state.hits.length === opponentShips.length) { + state.gameOver = true; + console.log(`Player ${player} has won the game!`); + } + } else { + console.log(`MISS! Player ${player} attacked (${row}, ${col}) but missed.`); + state.misses.push({ row, col }); + } + + state.current_turn = player === 1 ? 2 : 1; + + await pool.execute('UPDATE games SET game_state = ? WHERE id = ?', [JSON.stringify(state), gameId]); + + res.json({ hit, state }); + } catch (err) { + console.error("Attack error:", err); + res.status(500).json({ error: 'Database error' }); + } + }); + + app.post('/api/save-result', async (req, res) => { + const { gameId, winner, loser, moves } = req.body; + + if (!gameId || !winner || !loser || !moves) { + console.warn("Missing game result parameters."); + return res.status(400).json({ error: 'Missing parameters' }); + } + + try { + console.log(`Saving result: Winner - ${winner}, Loser - ${loser}, Moves - ${moves}`); + await pool.execute( + `INSERT INTO game_results (game_id, winner, loser, moves) VALUES (?, ?, ?, ?)`, + [gameId, winner, loser, moves] + ); + + console.log(`Game result saved for game ID ${gameId}`); + res.json({ message: "Game result saved" }); + } catch (err) { + console.error("Error saving game result:", err); + res.status(500).json({ error: "Database error" }); + } + }); + + const port = 4000; + app.listen(port, () => { + console.log(`Backend started on port ${port}`); + }); + + } catch (err) { + console.error("Database connection failed!", err); + } +})(); diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..fbfbbc5 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,18 @@ +{ + "name": "battleship-backend", + "version": "1.0.0", + "description": "Backend for Battleship game", + "main": "index.js", + "scripts": { + "start": "node index.js" + }, + "keywords": [], + "author": "", + "license": "MIT", + "dependencies": { + "cors": "^2.8.5", + "express": "^4.18.2", + "mysql2": "^3.12.0", + "pg": "^8.9.0" + } +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..4d226dc --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,32 @@ +version: '3.8' + +services: + backend: + build: ./backend + container_name: battleship_backend + restart: unless-stopped + environment: + - DB_HOST=battleship-mysql + - DB_PORT=3306 + - DB_USER=root + - DB_PASSWORD=somepassword + - DB_NAME=battleship + ports: + - "4000:4000" + networks: + - app-net + + frontend: + build: ./frontend + container_name: battleship_frontend + restart: unless-stopped + depends_on: + - backend + ports: + - "3001:80" + networks: + - app-net + +networks: + app-net: + external: true diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..6f0ccdb --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,28 @@ +# --- Build stage --- + FROM node:18-alpine AS build + + WORKDIR /app + + # Копируем package.json и package-lock.json (если есть) + COPY package*.json ./ + + # Устанавливаем зависимости + RUN npm install + + # Копируем исходный код + COPY . . + + # Собираем проект (Vite по умолчанию генерирует папку dist) + RUN npm run build + + # --- Production stage --- + FROM nginx:alpine + + # Копируем собранный проект в nginx + COPY --from=build /app/dist /usr/share/nginx/html + + # Открываем порт 80 для nginx + EXPOSE 80 + + CMD ["nginx", "-g", "daemon off;"] + \ No newline at end of file diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..fd3b758 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,12 @@ +# React + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend using TypeScript and enable type-aware lint rules. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..ec2b712 --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,33 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' + +export default [ + { ignores: ['dist'] }, + { + files: ['**/*.{js,jsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + parserOptions: { + ecmaVersion: 'latest', + ecmaFeatures: { jsx: true }, + sourceType: 'module', + }, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...js.configs.recommended.rules, + ...reactHooks.configs.recommended.rules, + 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, + }, +] diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..0c589ec --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + + +
+ + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..902fd98 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,27 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@eslint/js": "^9.21.0", + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", + "@vitejs/plugin-react": "^4.3.4", + "eslint": "^9.21.0", + "eslint-plugin-react-hooks": "^5.1.0", + "eslint-plugin-react-refresh": "^0.4.19", + "globals": "^15.15.0", + "vite": "^6.2.0" + } +} diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/App.css b/frontend/src/App.css new file mode 100644 index 0000000..2e22a36 --- /dev/null +++ b/frontend/src/App.css @@ -0,0 +1,42 @@ +.app-container { + font-family: 'Arial', sans-serif; + text-align: center; + padding: 20px; + background-color: #f0f8ff; + min-height: 100vh; +} + +.game-controls { + margin: 20px 0; +} + +.btn { + padding: 10px 20px; + font-size: 16px; + background-color: #0077cc; + color: white; + border: none; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.3s; +} + +.btn:hover { + background-color: #005fa3; +} + +.game-board { + margin-top: 20px; +} + +.game-message { + margin-top: 20px; + font-size: 18px; + color: #333; +} + +.game-over { + font-size: 24px; + color: red; + margin-top: 10px; +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx new file mode 100644 index 0000000..713b3d5 --- /dev/null +++ b/frontend/src/App.jsx @@ -0,0 +1,107 @@ +import React, { useState } from 'react'; +import PlayerForm from './components/PlayerForm'; +import PlacementBoard from './components/PlacementBoard'; +import GameBoard from './components/GameBoard'; + +function App() { + const [step, setStep] = useState(0); + const [gameId, setGameId] = useState(null); + const [player1Name, setPlayer1Name] = useState(''); + const [player2Name, setPlayer2Name] = useState(''); + const [player1Ships, setPlayer1Ships] = useState([]); + const [player2Ships, setPlayer2Ships] = useState([]); + + const handlePlayersSubmit = async (p1, p2) => { + setPlayer1Name(p1); + setPlayer2Name(p2); + + try { + const response = await fetch('http://localhost:4000/api/new-game', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ player1: p1, player2: p2 }) + }); + + const data = await response.json(); + if (data.gameId) { + setGameId(data.gameId); + setStep(1); + } else { + console.error('Game creation error:', data); + } + } catch (error) { + console.error('Network error:', error); + } + }; + + const handlePlacementDone = async (ships) => { + if (!gameId) { + console.error('Error: gameId is missing!'); + return; + } + + try { + const response = await fetch('http://localhost:4000/api/place-ships', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ gameId, player: step, ships }) + }); + + const data = await response.json(); + if (data.message) { + console.log(`Ships of player ${step} saved on server.`); + if (step === 1) { + setPlayer1Ships(ships); + setStep(2); + } else if (step === 2) { + setPlayer2Ships(ships); + setStep(3); + } + } else { + console.error('Error saving ships:', data); + } + } catch (error) { + console.error('Network error when saving ships:', error); + } + }; + + // Функция для сброса состояния и начала новой игры + const resetGame = () => { + setStep(0); + setGameId(null); + setPlayer1Name(''); + setPlayer2Name(''); + setPlayer1Ships([]); + setPlayer2Ships([]); + }; + + return ( +
+ {step === 0 && } + {step === 1 && ( + + )} + {step === 2 && ( + + )} + {step === 3 && ( + + )} +
+ ); +} + +export default App; diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/frontend/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/components/Board.css b/frontend/src/components/Board.css new file mode 100644 index 0000000..b39caca --- /dev/null +++ b/frontend/src/components/Board.css @@ -0,0 +1,9 @@ +.board { + display: inline-block; + border: 2px solid #333; + } + + .board-row { + display: flex; + } + \ No newline at end of file diff --git a/frontend/src/components/Board.jsx b/frontend/src/components/Board.jsx new file mode 100644 index 0000000..a646558 --- /dev/null +++ b/frontend/src/components/Board.jsx @@ -0,0 +1,46 @@ +import React from 'react'; +import Cell from './Cell'; +import './Board.css'; + +function Board({ + boardSize, + ships = [], + hits = [], + misses = [], + sunkShips = [], + onCellClick, + hideShips = false +}) { + const rows = Array.from({ length: boardSize }, (_, i) => i); + const cols = Array.from({ length: boardSize }, (_, i) => i); + + return ( +
+ {rows.map((r) => ( +
+ {cols.map((c) => { + const hasShip = ships.some((s) => s.row === r && s.col === c); + const isHit = hits.some((h) => h.row === r && h.col === c); + const isMiss = misses.some((m) => m.row === r && m.col === c); + const isSunk = sunkShips.some((s) => s.row === r && s.col === c); + + return ( + + ); + })} +
+ ))} +
+ ); +} + +export default Board; diff --git a/frontend/src/components/Cell.css b/frontend/src/components/Cell.css new file mode 100644 index 0000000..0764c4e --- /dev/null +++ b/frontend/src/components/Cell.css @@ -0,0 +1,24 @@ +.cell { + width: 30px; + height: 30px; + border: 1px solid black; + display: inline-block; + background-color: white; + } + + .cell.hit { + background-color: red !important; + } + + .cell.miss { + background-color: green !important; + } + + .cell.sunk { + background-color: black !important; + } + + .cell.has-ship { + background-color: blue; + } + \ No newline at end of file diff --git a/frontend/src/components/Cell.jsx b/frontend/src/components/Cell.jsx new file mode 100644 index 0000000..fb9739d --- /dev/null +++ b/frontend/src/components/Cell.jsx @@ -0,0 +1,24 @@ +import React from 'react'; +import './Cell.css'; + +function Cell({ row, col, hasShip, isHit, isMiss, isSunk, onClick }) { + let className = 'cell'; + + if (isSunk) { + className += ' sunk'; + } else if (isHit) { + className += ' hit'; + } else if (isMiss) { + className += ' miss'; + } else if (hasShip) { + className += ' has-ship'; + } + + const handleClick = () => { + if (onClick) onClick(row, col); + }; + + return
; +} + +export default Cell; diff --git a/frontend/src/components/GameBoard.css b/frontend/src/components/GameBoard.css new file mode 100644 index 0000000..253d0cc --- /dev/null +++ b/frontend/src/components/GameBoard.css @@ -0,0 +1,66 @@ +.game-board { + display: flex; + flex-direction: column; + align-items: center; + margin: 2rem auto; + max-width: 1000px; + padding: 2rem; + background-color: #ffffff; + border-radius: 12px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + font-family: 'Arial', sans-serif; +} + +.turn-title { + margin-bottom: 1.5rem; + font-size: 2rem; + color: #333; +} + +.winner-section { + text-align: center; + margin-bottom: 1.5rem; +} + +.winner-section h2 { + font-size: 2rem; + color: #28a745; + margin-bottom: 1rem; +} + +.new-game-button { + padding: 1rem 2rem; + font-size: 1.3rem; + border: none; + border-radius: 8px; + background-color: #007bff; + color: #fff; + cursor: pointer; + transition: background-color 0.3s; +} + +.new-game-button:hover { + background-color: #0056b3; +} + +.boards-container { + display: flex; + flex-wrap: wrap; + gap: 2rem; + justify-content: center; + width: 100%; +} + +.board-wrapper { + flex: 1; + min-width: 300px; + display: flex; + flex-direction: column; + align-items: center; +} + +.board-wrapper h3 { + margin-bottom: 1rem; + font-size: 1.5rem; + color: #007bff; +} diff --git a/frontend/src/components/GameBoard.jsx b/frontend/src/components/GameBoard.jsx new file mode 100644 index 0000000..6108b56 --- /dev/null +++ b/frontend/src/components/GameBoard.jsx @@ -0,0 +1,174 @@ +import React, { useState, useEffect } from 'react'; +import Board from './Board'; +import './GameBoard.css'; + +function checkSunkShip(ships, hits) { + const sunkShipsArrays = ships.filter((ship) => + ship.every((cell) => + hits.some((hit) => hit.row === cell.row && hit.col === cell.col) + ) + ); + + sunkShipsArrays.forEach((ship) => { + const size = ship.length; + if (size === 4) { + console.log('Battleship (4 cells) is sunk!'); + } else if (size === 3) { + console.log('Cruiser (3 cells) is sunk!'); + } else if (size === 2) { + console.log('Destroyer (2 cells) is sunk!'); + } else if (size === 1) { + console.log('Submarine (1 cell) is sunk!'); + } + }); + + return sunkShipsArrays.flat(); +} + +function GameBoard({ gameId, player1Name, player2Name, player1Ships, player2Ships, onNewGame }) { + const [currentPlayer, setCurrentPlayer] = useState(1); + const [hitsP1, setHitsP1] = useState([]); + const [missesP1, setMissesP1] = useState([]); + const [hitsP2, setHitsP2] = useState([]); + const [missesP2, setMissesP2] = useState([]); + const [sunkShipsP1, setSunkShipsP1] = useState([]); + const [sunkShipsP2, setSunkShipsP2] = useState([]); + const [winner, setWinner] = useState(null); + + useEffect(() => { + console.log('player1Ships:', JSON.stringify(player1Ships, null, 2)); + console.log('player2Ships:', JSON.stringify(player2Ships, null, 2)); + }, []); + + const saveGameResult = async (winnerName, loserName) => { + try { + const resp = await fetch('http://localhost:4000/api/save-result', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + gameId, + winner: winnerName, + loser: loserName, + moves: hitsP1.length + hitsP2.length + }) + }); + const data = await resp.json(); + console.log('Game result saved:', data); + } catch (err) { + console.error('Error saving game result:', err); + } + }; + + useEffect(() => { + const totalCellsP1 = player1Ships.flat().length; + const totalCellsP2 = player2Ships.flat().length; + + if (hitsP1.length === totalCellsP2) { + setWinner(player1Name); + saveGameResult(player1Name, player2Name); + } else if (hitsP2.length === totalCellsP1) { + setWinner(player2Name); + saveGameResult(player2Name, player1Name); + } + }, [hitsP1, hitsP2, player1Ships, player2Ships, player1Name, player2Name]); + + useEffect(() => { + const sunkForP1 = checkSunkShip(player1Ships, hitsP2); + const sunkForP2 = checkSunkShip(player2Ships, hitsP1); + + setSunkShipsP1(sunkForP1); + setSunkShipsP2(sunkForP2); + }, [hitsP1, hitsP2, player1Ships, player2Ships]); + + const handleAttack = (row, col, opponent) => { + if (winner) return; + + let alreadyAttacked; + if (opponent === 2) { + alreadyAttacked = [...hitsP1, ...missesP1].some( + (cell) => cell.row === row && cell.col === col + ); + } else { + alreadyAttacked = [...hitsP2, ...missesP2].some( + (cell) => cell.row === row && cell.col === col + ); + } + + if (alreadyAttacked) { + alert('You have already attacked this cell!'); + return; + } + + let isHit = false; + if (opponent === 2) { + isHit = player2Ships.flat().some((cell) => cell.row === row && cell.col === col); + if (isHit) { + setHitsP1([...hitsP1, { row, col }]); + console.log(`Player1 hit a ship at [${row},${col}] on Player2's board`); + } else { + setMissesP1([...missesP1, { row, col }]); + console.log(`Player1 missed at [${row},${col}] on Player2's board`); + } + } else { + isHit = player1Ships.flat().some((cell) => cell.row === row && cell.col === col); + if (isHit) { + setHitsP2([...hitsP2, { row, col }]); + console.log(`Player2 hit a ship at [${row},${col}] on Player1's board`); + } else { + setMissesP2([...missesP2, { row, col }]); + console.log(`Player2 missed at [${row},${col}] on Player1's board`); + } + } + + if (!isHit) { + setCurrentPlayer(currentPlayer === 1 ? 2 : 1); + } + }; + + return ( +
+ {winner ? ( +
+

Winner: {winner}!

+ +
+ ) : ( +

Current turn: {currentPlayer === 1 ? player1Name : player2Name}

+ )} + +
+
+

{player1Name}'s Board

+ handleAttack(r, c, 1) : null + } + hideShips={true} + /> +
+ +
+

{player2Name}'s Board

+ handleAttack(r, c, 2) : null + } + hideShips={true} + /> +
+
+
+ ); +} + +export default GameBoard; diff --git a/frontend/src/components/Header.css b/frontend/src/components/Header.css new file mode 100644 index 0000000..158ad91 --- /dev/null +++ b/frontend/src/components/Header.css @@ -0,0 +1,12 @@ +.header { + background-color: #0077cc; + color: white; + padding: 20px 0; + margin-bottom: 20px; + } + + .header h1 { + margin: 0; + font-size: 2.5em; + } + \ No newline at end of file diff --git a/frontend/src/components/Header.jsx b/frontend/src/components/Header.jsx new file mode 100644 index 0000000..56fc20b --- /dev/null +++ b/frontend/src/components/Header.jsx @@ -0,0 +1,12 @@ +import React from 'react'; +import './Header.css'; + +const Header = ({ title }) => { + return ( +
+

{title}

+
+ ); +}; + +export default Header; diff --git a/frontend/src/components/PlacementBoard.css b/frontend/src/components/PlacementBoard.css new file mode 100644 index 0000000..44d2c01 --- /dev/null +++ b/frontend/src/components/PlacementBoard.css @@ -0,0 +1,52 @@ +.placement-board { + display: flex; + flex-direction: column; + align-items: center; + margin: 2rem auto; + max-width: 800px; + text-align: center; + background: #ffffff; + padding: 2rem; + border-radius: 12px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +} + +.placement-board h2 { + margin-bottom: 1.5rem; + font-size: 2rem; + color: #333; +} + +.ship-label { + font-size: 1.2rem; + margin-bottom: 0.5rem; + color: #555; + display: block; +} + +.ship-select { + font-size: 1.2rem; + padding: 0.6rem; + margin-bottom: 1.5rem; + border: 1px solid #ccc; + border-radius: 6px; + width: 100%; + max-width: 300px; + background-color: #f9f9f9; +} + +.finish-button { + margin-top: 1.5rem; + padding: 0.8rem 1.5rem; + font-size: 1.2rem; + cursor: pointer; + background-color: #28a745; + color: #fff; + border: none; + border-radius: 6px; + transition: background-color 0.3s; +} + +.finish-button:hover { + background-color: #218838; +} diff --git a/frontend/src/components/PlacementBoard.jsx b/frontend/src/components/PlacementBoard.jsx new file mode 100644 index 0000000..e1161f4 --- /dev/null +++ b/frontend/src/components/PlacementBoard.jsx @@ -0,0 +1,90 @@ +import React, { useState } from 'react'; +import Board from './Board'; +import './PlacementBoard.css'; + +const SHIPS_LIMIT = { + 'Battleship (4 cells)': { size: 4, max: 1 }, + 'Cruiser (3 cells)': { size: 3, max: 2 }, + 'Destroyer (2 cells)': { size: 2, max: 3 }, + 'Submarine (1 cell)': { size: 1, max: 4 } +}; + +function PlacementBoard({ playerName, onPlacementDone }) { + const [ships, setShips] = useState([]); + const [selectedShipType, setSelectedShipType] = useState('Battleship (4 cells)'); + const [shipCounts, setShipCounts] = useState({}); + + const handleCellClick = (row, col) => { + const shipData = SHIPS_LIMIT[selectedShipType]; + if (!shipData) { + alert('Invalid ship type!'); + return; + } + + const currentCount = shipCounts[selectedShipType] || 0; + if (currentCount >= shipData.max) { + alert(`You can only place ${shipData.max} ${selectedShipType}!`); + return; + } + + if (col + shipData.size > 10) { + alert('Ship is out of bounds!'); + return; + } + + const newShip = []; + for (let i = 0; i < shipData.size; i++) { + newShip.push({ row, col: col + i }); + } + + setShips([...ships, newShip]); + setShipCounts({ + ...shipCounts, + [selectedShipType]: currentCount + 1 + }); + }; + + const handleDone = () => { + if ( + Object.keys(SHIPS_LIMIT).some( + (type) => (shipCounts[type] || 0) < SHIPS_LIMIT[type].max + ) + ) { + alert('You must place all required ships!'); + return; + } + onPlacementDone(ships); + }; + + return ( +
+

Ship Placement: {playerName}

+ + + + + + + +
+ ); +} + +export default PlacementBoard; diff --git a/frontend/src/components/PlayerForm.css b/frontend/src/components/PlayerForm.css new file mode 100644 index 0000000..3dd5687 --- /dev/null +++ b/frontend/src/components/PlayerForm.css @@ -0,0 +1,123 @@ +.player-form-container { + display: flex; + flex-direction: column; + align-items: center; + padding: 2rem; + background: #f7f9fc; + min-height: 100vh; + font-family: 'Arial', sans-serif; +} + +.player-form-card, +.results-card { + background: #ffffff; + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + width: 400px; + margin-bottom: 2rem; + padding: 2rem; +} + +.form-title { + text-align: center; + margin-bottom: 1.5rem; + font-size: 1.8rem; + color: #333; +} + +.player-form { + display: flex; + flex-direction: column; +} + +.form-group { + display: flex; + flex-direction: column; + margin-bottom: 1.2rem; +} + +.form-group label { + margin-bottom: 0.5rem; + font-size: 1.1rem; + color: #555; +} + +.form-group input { + padding: 0.8rem; + font-size: 1.1rem; + border: 1px solid #ccc; + border-radius: 4px; + transition: border-color 0.3s; +} + +.form-group input:focus { + border-color: #007bff; + outline: none; +} + +.submit-button { + padding: 0.8rem; + font-size: 1.2rem; + background-color: #007bff; + color: #fff; + border: none; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.3s; +} + +.submit-button:hover { + background-color: #0056b3; +} + +.results-section { + width: 400px; + text-align: center; +} + +.toggle-results-button { + padding: 0.8rem; + font-size: 1.2rem; + background-color: #28a745; + color: #fff; + border: none; + border-radius: 4px; + cursor: pointer; + margin-bottom: 1rem; + transition: background-color 0.3s; +} + +.toggle-results-button:hover { + background-color: #218838; +} + +.results-title { + font-size: 1.6rem; + text-align: center; + margin-bottom: 1rem; + color: #333; +} + +.results-table { + width: 100%; + border-collapse: collapse; +} + +.results-table th, +.results-table td { + border: 1px solid #ddd; + padding: 0.8rem; + font-size: 1.1rem; + text-align: center; +} + +.results-table th { + background-color: #007bff; + color: #fff; +} + +.no-results { + font-size: 1.1rem; + text-align: center; + color: #777; +} diff --git a/frontend/src/components/PlayerForm.jsx b/frontend/src/components/PlayerForm.jsx new file mode 100644 index 0000000..b332c95 --- /dev/null +++ b/frontend/src/components/PlayerForm.jsx @@ -0,0 +1,104 @@ +import React, { useState } from 'react'; +import './PlayerForm.css'; + +function PlayerForm({ onSubmit }) { + const [p1, setP1] = useState(''); + const [p2, setP2] = useState(''); + const [results, setResults] = useState([]); + const [showResults, setShowResults] = useState(false); + + const handleSubmit = (e) => { + e.preventDefault(); + if (p1.trim() === '' || p2.trim() === '') { + alert('Please enter both player names!'); + console.log('Error: Missing player names'); + return; + } + console.log('Starting new game with players:', p1, p2); + onSubmit(p1, p2); + }; + + const fetchResults = () => { + console.log('Fetching previous game results...'); + fetch('http://localhost:4000/api/results') + .then((resp) => resp.json()) + .then((data) => { + console.log('Fetched results:', data.results); + setResults(data.results); + }) + .catch((err) => console.error('Error fetching results:', err)); + }; + + const handleToggleResults = () => { + if (!showResults) { + fetchResults(); + } + setShowResults(!showResults); + }; + + return ( +
+
+

Enter Player Names

+
+
+ + setP1(e.target.value)} + placeholder="Enter Player 1 name" + required + /> +
+
+ + setP2(e.target.value)} + placeholder="Enter Player 2 name" + required + /> +
+ +
+
+ +
+ + {showResults && ( +
+

Previous Game Results

+ {results.length > 0 ? ( + + + + + + + + + + {results.map((result, index) => ( + + + + + + ))} + +
WinnerLoserShots
{result.winner}{result.loser}{result.moves}
+ ) : ( +

No previous results available.

+ )} +
+ )} +
+
+ ); +} + +export default PlayerForm; diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx new file mode 100644 index 0000000..b9a1a6d --- /dev/null +++ b/frontend/src/main.jsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.jsx' + +createRoot(document.getElementById('root')).render( + + + , +) diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..8b0f57b --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +}) diff --git a/prepare-app.sh b/prepare-app.sh new file mode 100644 index 0000000..df88ff3 --- /dev/null +++ b/prepare-app.sh @@ -0,0 +1,34 @@ + +echo "Preparing app..." + +docker network inspect app-net >/dev/null 2>&1 || docker network create app-net + +if ! docker image inspect mysql:8.0.30 >/dev/null 2>&1; then + echo "MySQL image not found locally. Pulling image..." + docker pull mysql:8.0.30 +fi + +MYSQL_CONTAINER=$(docker ps -aq -f name=battleship-mysql) + +if [ -n "$MYSQL_CONTAINER" ]; then + + if [ "$(docker ps -q -f name=battleship-mysql)" ]; then + echo "MySQL container is already running." + else + echo "Starting existing MySQL container..." + docker start battleship-mysql + docker network connect app-net battleship-mysql || true + fi +else + echo "Creating and starting MySQL container..." + docker run --network app-net --name battleship-mysql \ + -e MYSQL_ROOT_PASSWORD=somepassword \ + -e MYSQL_DATABASE=battleship \ + -p 3306:3306 \ + -d mysql:8.0.30 +fi + + +docker-compose build + +echo "Preparation done." diff --git a/remove-app.sh b/remove-app.sh new file mode 100644 index 0000000..94a9301 --- /dev/null +++ b/remove-app.sh @@ -0,0 +1,10 @@ + +echo "Removing app..." + +docker-compose down -v --rmi local + +# Если нужно удалить базу данных, можно удалить контейнер MySQL отдельно: +docker rm -f battleship-mysql +docker volume rm db_data + +echo "App removed completely." diff --git a/start-app.sh b/start-app.sh new file mode 100644 index 0000000..d7db20b --- /dev/null +++ b/start-app.sh @@ -0,0 +1,9 @@ + +echo "Starting app..." + +docker-compose up -d + +echo "App is running." +echo "Frontend is available at: http://localhost:3001" +echo "Backend is available at: http://localhost:4000" +docker-compose ps diff --git a/stop-app.sh b/stop-app.sh new file mode 100644 index 0000000..ec1f076 --- /dev/null +++ b/stop-app.sh @@ -0,0 +1,6 @@ + +echo "Stopping app..." + +docker-compose stop + +echo "App stopped. You can run ./start-app.sh to resume."