From cfe95ab3d7b9a75e3cf8617cd3f9fad27f3058c0 Mon Sep 17 00:00:00 2001 From: oleh <opoiasnik@gmail.com> Date: Thu, 3 Apr 2025 11:57:59 +0200 Subject: [PATCH] 2 zadanie --- z2/.gitignore | 47 ++++ z2/README.md | 131 ++++++++++++ z2/backend/Dockerfile | 20 ++ z2/backend/index.js | 200 ++++++++++++++++++ z2/backend/package.json | 18 ++ z2/deployment.yaml | 59 ++++++ z2/docker-compose.yml | 51 +++++ z2/frontend/.gitignore | 24 +++ z2/frontend/Dockerfile | 28 +++ z2/frontend/README.md | 110 ++++++++++ z2/frontend/eslint.config.js | 33 +++ z2/frontend/index.html | 13 ++ z2/frontend/package.json | 27 +++ z2/frontend/public/vite.svg | 1 + z2/frontend/src/App.css | 42 ++++ z2/frontend/src/App.jsx | 107 ++++++++++ z2/frontend/src/assets/react.svg | 1 + z2/frontend/src/components/Board.css | 9 + z2/frontend/src/components/Board.jsx | 46 ++++ z2/frontend/src/components/Cell.css | 24 +++ z2/frontend/src/components/Cell.jsx | 24 +++ z2/frontend/src/components/GameBoard.css | 66 ++++++ z2/frontend/src/components/GameBoard.jsx | 174 +++++++++++++++ z2/frontend/src/components/Header.css | 12 ++ z2/frontend/src/components/Header.jsx | 12 ++ z2/frontend/src/components/PlacementBoard.css | 73 +++++++ z2/frontend/src/components/PlacementBoard.jsx | 135 ++++++++++++ z2/frontend/src/components/PlayerForm.css | 123 +++++++++++ z2/frontend/src/components/PlayerForm.jsx | 104 +++++++++ z2/frontend/src/index.css | 0 z2/frontend/src/main.jsx | 10 + z2/frontend/vite.config.js | 7 + z2/namespace.yaml | 4 + z2/prepare-app.sh | 8 + z2/remove-app.sh | 14 ++ z2/service.yaml | 40 ++++ z2/start-app.sh | 14 ++ z2/statefulset.yaml | 58 +++++ z2/stop-app.sh | 13 ++ 39 files changed, 1882 insertions(+) create mode 100644 z2/.gitignore create mode 100644 z2/README.md create mode 100644 z2/backend/Dockerfile create mode 100644 z2/backend/index.js create mode 100644 z2/backend/package.json create mode 100644 z2/deployment.yaml create mode 100644 z2/docker-compose.yml create mode 100644 z2/frontend/.gitignore create mode 100644 z2/frontend/Dockerfile create mode 100644 z2/frontend/README.md create mode 100644 z2/frontend/eslint.config.js create mode 100644 z2/frontend/index.html create mode 100644 z2/frontend/package.json create mode 100644 z2/frontend/public/vite.svg create mode 100644 z2/frontend/src/App.css create mode 100644 z2/frontend/src/App.jsx create mode 100644 z2/frontend/src/assets/react.svg create mode 100644 z2/frontend/src/components/Board.css create mode 100644 z2/frontend/src/components/Board.jsx create mode 100644 z2/frontend/src/components/Cell.css create mode 100644 z2/frontend/src/components/Cell.jsx create mode 100644 z2/frontend/src/components/GameBoard.css create mode 100644 z2/frontend/src/components/GameBoard.jsx create mode 100644 z2/frontend/src/components/Header.css create mode 100644 z2/frontend/src/components/Header.jsx create mode 100644 z2/frontend/src/components/PlacementBoard.css create mode 100644 z2/frontend/src/components/PlacementBoard.jsx create mode 100644 z2/frontend/src/components/PlayerForm.css create mode 100644 z2/frontend/src/components/PlayerForm.jsx create mode 100644 z2/frontend/src/index.css create mode 100644 z2/frontend/src/main.jsx create mode 100644 z2/frontend/vite.config.js create mode 100644 z2/namespace.yaml create mode 100644 z2/prepare-app.sh create mode 100644 z2/remove-app.sh create mode 100644 z2/service.yaml create mode 100644 z2/start-app.sh create mode 100644 z2/statefulset.yaml create mode 100644 z2/stop-app.sh diff --git a/z2/.gitignore b/z2/.gitignore new file mode 100644 index 0000000..6c96855 --- /dev/null +++ b/z2/.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/z2/README.md b/z2/README.md new file mode 100644 index 0000000..0e19ecf --- /dev/null +++ b/z2/README.md @@ -0,0 +1,131 @@ +# Battleship Web Application + +## 1. Podmienky na nasadenie a spustenie + +Na spustenie tejto aplikácie je potrebný nasledujúci softvér: +- **Docker** (nainštalovaný a správne nakonfigurovaný) +- **Docker Compose** (verzia 3.8 a vyššia) +- **Bash** (pre spúšťanie skriptov) + +Odporúčané prostredie: Linux alebo Windows s nainštalovaným WSL2. + +## 2. Opis aplikácie + +Táto webová aplikácia predstavuje jednoduchú hru Battleship. Aplikácia je rozdelená na tri hlavné služby: +- **Frontend:** Webové rozhranie, kde hráči zadávajú svoje mená, umiestňujú svoje lode a hrajú hru. +- **Backend:** Serverová logika, ktorá spracováva požiadavky od frontendu, komunikuje s databázou a ukladá výsledky hier. +- **Databáza (MySQL):** Ukladá trvalé dáta – informácie o hrách, výsledkoch a nastaveniach. Dáta sú uchovávané v persistent volume, aby prežili reštarty kontajnerov. + +## 3. Opis virtuálnych sietí a pomenovaných zväzkov + +### Virtuálna sieť +- **app-net:** + Všetky kontajnery (frontend, backend, MySQL) bežia v rámci jednej Docker siete s názvom `app-net`. + - Ak sieť nie je vytvorená, skripty ju vytvoria. + - Sieť umožňuje vzájomnú komunikáciu kontajnerov pomocou mien služieb (napr. backend pristupuje k databáze pomocou mena `mysql`). + +### Pomenované zväzky +- **db_data:** + Tento persistent volume je pripojený k službe MySQL a ukladá dáta (obsah databázy) na ceste `/var/lib/mysql`. + - Zabezpečuje, že aj pri reštarte alebo odstránení kontajnera sa dáta nevymažú, pokiaľ ich zámselne neodstránite. + +## 4. Opis konfigurácie kontajnerov + +### MySQL +- **Image:** `mysql:8.0.30` +- **Premenné prostredia:** + - `MYSQL_ROOT_PASSWORD=somepassword` + - `MYSQL_DATABASE=battleship` +- **Port:** 3306 (mapovaný na localhost:3306) +- **Persistent Volume:** `db_data` pripojený na `/var/lib/mysql` +- **Sieť:** `app-net` +- **Reštart:** `unless-stopped` + +### Backend +- **Build:** Dockerfile v priečinku `./backend` +- **Port:** 4000 (mapovaný na localhost:4000) +- **Environment Variables:** + - `DB_HOST=mysql` (meno služby MySQL v rámci docker-compose) + - `DB_PORT=3306` + - `DB_USER=root` + - `DB_PASSWORD=somepassword` + - `DB_NAME=battleship` +- **Závislosť:** Závisí od služby MySQL (nastavené v `depends_on`) +- **Sieť:** `app-net` +- **Reštart:** `unless-stopped` + +### Frontend +- **Build:** Dockerfile v priečinku `./frontend` +- **Port:** 80 (mapovaný na localhost:3001) +- **Závislosť:** Závisí od backendu (nastavené v `depends_on`) +- **Sieť:** `app-net` +- **Reštart:** `unless-stopped` + +## 5. Zoznam použitých kontajnerov a ich stručný opis + +- **battleship_mysql:** + Beží MySQL databáza, ktorá ukladá trvalé dáta aplikácie. Používa persistent volume `db_data`. + +- **battleship_backend:** + Node.js aplikácia, ktorá poskytuje API pre hru, komunikuje s databázou a spracováva hernú logiku. + +- **battleship_frontend:** + Webové rozhranie aplikácie (React), prostredníctvom ktorého hráči interagujú s hrou. + +## 6. Návod ako pripraviť, spustiť, pozastaviť a vymazať aplikáciu + +Súčasťou projektu sú štyri skripty: + +### Príprava aplikácie +**prepare-app.sh** +Tento skript: +- Vytvorí externú sieť `app-net` (ak ešte neexistuje). +- Zostaví Docker obrazy pre MySQL, backend a frontend prostredníctvom docker-compose. + +Spustenie: +```bash +./prepare-app.sh +``` + +### Spustenie aplikácie +**start-app.sh** +Tento skript: +- Spustí všetky kontajnery (MySQL, backend, frontend) pomocou `docker-compose up -d`. +- Vypíše informácie o dostupnosti aplikácie. + +Spustenie: +```bash +./start-app.sh +``` + +### Pozastavenie aplikácie +**stop-app.sh** +Tento skript: +- Pozastaví všetky služby, pričom dáta v databáze ostanú zachované (persistent volume). + +Spustenie: +```bash +./stop-app.sh +``` + +### Odstránenie aplikácie +**remove-app.sh** +Tento skript: +- Odstráni všetky vytvorené kontajnery, obrazy a persistent volume (ak sa použije parameter `-v`). + +Spustenie: +```bash +./remove-app.sh +``` + +## 7. Návod ako si pozrieť aplikáciu na webovom prehliadači + +### Frontend: +Otvorte webový prehliadač a zadajte: [http://localhost:3001](http://localhost:3001) + +### Backend API: +API je dostupné na: [http://localhost:4000](http://localhost:4000) + +### MySQL: +Pripojte sa k databáze na `localhost:3306` pomocou MySQL klienta. + diff --git a/z2/backend/Dockerfile b/z2/backend/Dockerfile new file mode 100644 index 0000000..7d22d72 --- /dev/null +++ b/z2/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/z2/backend/index.js b/z2/backend/index.js new file mode 100644 index 0000000..6874362 --- /dev/null +++ b/z2/backend/index.js @@ -0,0 +1,200 @@ +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: process.env.DB_HOST || 'localhost', + user: process.env.DB_USER || 'root', + password: process.env.DB_PASSWORD || 'somepassword', + database: process.env.DB_NAME || '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/z2/backend/package.json b/z2/backend/package.json new file mode 100644 index 0000000..ff56f5c --- /dev/null +++ b/z2/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/z2/deployment.yaml b/z2/deployment.yaml new file mode 100644 index 0000000..3e591cb --- /dev/null +++ b/z2/deployment.yaml @@ -0,0 +1,59 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: battleship-backend + namespace: battleship-app + labels: + app: battleship-backend +spec: + replicas: 1 + selector: + matchLabels: + app: battleship-backend + template: + metadata: + labels: + app: battleship-backend + spec: + containers: + - name: battleship-backend + image: battleship-backend:latest + imagePullPolicy: Never + ports: + - containerPort: 4000 + env: + - name: DB_HOST + value: mysql + - name: DB_PORT + value: "3306" + - name: DB_USER + value: root + - name: DB_PASSWORD + value: somepassword + - name: DB_NAME + value: battleship +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: battleship-frontend + namespace: battleship-app + labels: + app: battleship-frontend +spec: + replicas: 1 + selector: + matchLabels: + app: battleship-frontend + template: + metadata: + labels: + app: battleship-frontend + spec: + containers: + - name: battleship-frontend + image: battleship-frontend:latest + imagePullPolicy: Never + ports: + - containerPort: 80 diff --git a/z2/docker-compose.yml b/z2/docker-compose.yml new file mode 100644 index 0000000..a3756c0 --- /dev/null +++ b/z2/docker-compose.yml @@ -0,0 +1,51 @@ +version: '3.8' + +services: + mysql: + image: mysql:8.0.30 + container_name: battleship_mysql + restart: unless-stopped + environment: + - MYSQL_ROOT_PASSWORD=somepassword + - MYSQL_DATABASE=battleship + ports: + - "3306:3306" + networks: + - app-net + volumes: + - db_data:/var/lib/mysql + + backend: + build: ./backend + container_name: battleship_backend + restart: unless-stopped + environment: + - DB_HOST=mysql + - DB_PORT=3306 + - DB_USER=root + - DB_PASSWORD=somepassword + - DB_NAME=battleship + ports: + - "4000:4000" + networks: + - app-net + depends_on: + - mysql + + frontend: + build: ./frontend + container_name: battleship_frontend + restart: unless-stopped + depends_on: + - backend + ports: + - "3001:80" + networks: + - app-net + +networks: + app-net: + driver: bridge + +volumes: + db_data: diff --git a/z2/frontend/.gitignore b/z2/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/z2/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/z2/frontend/Dockerfile b/z2/frontend/Dockerfile new file mode 100644 index 0000000..5336955 --- /dev/null +++ b/z2/frontend/Dockerfile @@ -0,0 +1,28 @@ +# --- Build stage --- + FROM node:18-alpine AS build + + WORKDIR /app + + + COPY package*.json ./ + + + RUN npm install + + + COPY . . + + + RUN npm run build + + + FROM nginx:alpine + + + COPY --from=build /app/dist /usr/share/nginx/html + + + EXPOSE 80 + + CMD ["nginx", "-g", "daemon off;"] + \ No newline at end of file diff --git a/z2/frontend/README.md b/z2/frontend/README.md new file mode 100644 index 0000000..aa575c9 --- /dev/null +++ b/z2/frontend/README.md @@ -0,0 +1,110 @@ +# Battleship Application on Kubernetes + +## Overview +This application implements a Battleship game system composed of three main components: +- **Backend:** Handles game logic and communicates with the database. +- **Frontend:** Provides the web user interface for playing the game. +- **MySQL Database:** Stores game data persistently. + +The application is deployed in a Kubernetes cluster using Deployments, a StatefulSet with persistent storage, and Services for network exposure. + +## Containers Used +- **battleship-backend:** + - **Description:** Contains the backend logic for the game. + - **Port:** Listens on port 4000. + - **Configuration:** Uses environment variables to connect to the MySQL database. + +- **battleship-frontend:** + - **Description:** Hosts the web user interface for the game. + - **Port:** Listens on port 80. + - **Exposure:** Exposed via a NodePort (30001). + +- **MySQL (battleship-mysql):** + - **Description:** Runs a MySQL database (version 8.0.30) in a StatefulSet. + - **Port:** Uses port 3306. + - **Persistence:** Uses a PersistentVolume (PV) and PersistentVolumeClaim (PVC) for data storage. + +## Kubernetes Objects +- **Namespace:** + - **battleship-app** + All objects (Deployments, StatefulSet, Services, PV, PVC) are created within this namespace. + +- **Deployments:** + - **battleship-backend:** Manages the backend pods. + - **battleship-frontend:** Manages the frontend pods. + +- **StatefulSet:** + - **battleship-mysql:** Manages the MySQL pods with stable identities and persistent storage. It includes: + - **PersistentVolume (mysql-pv):** Provides 1Gi storage (using a hostPath at `/mnt/data/mysql`). + - **PersistentVolumeClaim (mysql-pvc):** Claims the persistent storage for MySQL. + +- **Services:** + - **MySQL Service:** Exposes the MySQL StatefulSet on port 3306. + - **Backend Service:** Exposes the backend on port 4000 (via NodePort 30000). + - **Frontend Service:** Exposes the frontend on port 80 (via NodePort 30001). + +## Virtual Networks and Named Volumes +- **Virtual Networks:** + Kubernetes provides internal networking for the cluster, and Services offer stable endpoints for communication between pods. + *Note:* In the original Docker Compose configuration, a network named `app-net` was defined; in Kubernetes, the cluster’s internal network is used. + +- **Named Volumes:** + The MySQL StatefulSet uses a PersistentVolume and a PersistentVolumeClaim to ensure data persistence. This setup guarantees that data (e.g., the game database) survives pod restarts or rescheduling. + +## Container Configuration +- **Environment Variables:** + - **Backend Container:** + Configured with variables such as `DB_HOST`, `DB_PORT`, `DB_USER`, `DB_PASSWORD`, and `DB_NAME` to connect to the MySQL database. + - **MySQL Container:** + Configured with variables `MYSQL_ROOT_PASSWORD` and `MYSQL_DATABASE` to initialize the database. + +- **Ports:** + - **Backend:** Container port 4000, exposed on NodePort 30000. + - **Frontend:** Container port 80, exposed on NodePort 30001. + - **MySQL:** Container port 3306, exposed internally on port 3306. + +- **Image Pull Policy:** + The backend and frontend Deployments use `imagePullPolicy: Never` assuming images are built locally. + +## How to Prepare, Run, Pause, and Remove the Application + +### Preparation +1. **Build Docker Images:** + Execute the `prepare-app.sh` script to build the Docker images for the backend and frontend: + ```bash + ./prepare-app.sh + ``` + +### Running the Application +1. **Deploy the Application:** + Use the `start-app.sh` script to deploy the namespace, Deployments, StatefulSet, and Services: + ```bash + ./start-app.sh + ``` + +### Pausing the Application +1. **Scale Down the Application:** + If you need to temporarily pause the application without deleting objects (thus preserving the data), use: + ```bash + ./stop-app.sh + ``` + *Note:* This script scales the Deployments and StatefulSet to 0 replicas. + +### Removing the Application +1. **Delete All Kubernetes Objects:** + To completely remove the application—including Deployments, StatefulSet, Services, and the Namespace—execute: + ```bash + ./remove-app.sh + ``` + *Important:* Deleting the objects may also remove the associated PersistentVolume depending on its reclaim policy. To preserve data, consider setting the PersistentVolume's reclaim policy to `Retain`. + +## How to Access the Application in a Web Browser +- **Frontend Access:** + The frontend is exposed via a NodePort on port **30001**. + Open a web browser and navigate to: + ``` + http://<Node-IP>:30001 + ``` + Replace `<Node-IP>` with the IP address of one of your Kubernetes nodes. + + diff --git a/z2/frontend/eslint.config.js b/z2/frontend/eslint.config.js new file mode 100644 index 0000000..ec2b712 --- /dev/null +++ b/z2/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/z2/frontend/index.html b/z2/frontend/index.html new file mode 100644 index 0000000..0c589ec --- /dev/null +++ b/z2/frontend/index.html @@ -0,0 +1,13 @@ +<!doctype html> +<html lang="en"> + <head> + <meta charset="UTF-8" /> + <link rel="icon" type="image/svg+xml" href="/vite.svg" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <title>Vite + React</title> + </head> + <body> + <div id="root"></div> + <script type="module" src="/src/main.jsx"></script> + </body> +</html> diff --git a/z2/frontend/package.json b/z2/frontend/package.json new file mode 100644 index 0000000..902fd98 --- /dev/null +++ b/z2/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/z2/frontend/public/vite.svg b/z2/frontend/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/z2/frontend/public/vite.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg> \ No newline at end of file diff --git a/z2/frontend/src/App.css b/z2/frontend/src/App.css new file mode 100644 index 0000000..2e22a36 --- /dev/null +++ b/z2/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/z2/frontend/src/App.jsx b/z2/frontend/src/App.jsx new file mode 100644 index 0000000..b97fc75 --- /dev/null +++ b/z2/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:30000/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:30000/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 ( + <div className="app-container"> + {step === 0 && <PlayerForm onSubmit={handlePlayersSubmit} />} + {step === 1 && ( + <PlacementBoard + playerName={player1Name} + onPlacementDone={handlePlacementDone} + /> + )} + {step === 2 && ( + <PlacementBoard + playerName={player2Name} + onPlacementDone={handlePlacementDone} + /> + )} + {step === 3 && ( + <GameBoard + gameId={gameId} + player1Name={player1Name} + player2Name={player2Name} + player1Ships={player1Ships} + player2Ships={player2Ships} + onNewGame={resetGame} + /> + )} + </div> + ); +} + +export default App; diff --git a/z2/frontend/src/assets/react.svg b/z2/frontend/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/z2/frontend/src/assets/react.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg> \ No newline at end of file diff --git a/z2/frontend/src/components/Board.css b/z2/frontend/src/components/Board.css new file mode 100644 index 0000000..3deddb7 --- /dev/null +++ b/z2/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/z2/frontend/src/components/Board.jsx b/z2/frontend/src/components/Board.jsx new file mode 100644 index 0000000..9111972 --- /dev/null +++ b/z2/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 ( + <div className="board"> + {rows.map((r) => ( + <div key={r} className="board-row"> + {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 ( + <Cell + key={`${r}-${c}`} + row={r} + col={c} + hasShip={hasShip && !hideShips} + isHit={isHit} + isMiss={isMiss} + isSunk={isSunk} + onClick={onCellClick} + /> + ); + })} + </div> + ))} + </div> + ); +} + +export default Board; diff --git a/z2/frontend/src/components/Cell.css b/z2/frontend/src/components/Cell.css new file mode 100644 index 0000000..c5c5ee2 --- /dev/null +++ b/z2/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/z2/frontend/src/components/Cell.jsx b/z2/frontend/src/components/Cell.jsx new file mode 100644 index 0000000..b7fdffb --- /dev/null +++ b/z2/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 <div className={className} onClick={handleClick} />; +} + +export default Cell; diff --git a/z2/frontend/src/components/GameBoard.css b/z2/frontend/src/components/GameBoard.css new file mode 100644 index 0000000..f1d4ecf --- /dev/null +++ b/z2/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/z2/frontend/src/components/GameBoard.jsx b/z2/frontend/src/components/GameBoard.jsx new file mode 100644 index 0000000..ba23cea --- /dev/null +++ b/z2/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:30000/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 ( + <div className="game-board"> + {winner ? ( + <div className="winner-section"> + <h2>Winner: {winner}!</h2> + <button className="new-game-button" onClick={onNewGame}>Start New Game</button> + </div> + ) : ( + <h2 className="turn-title">Current turn: {currentPlayer === 1 ? player1Name : player2Name}</h2> + )} + + <div className="boards-container"> + <div className="board-wrapper"> + <h3>{player1Name}'s Board</h3> + <Board + boardSize={10} + ships={player1Ships.flat()} + hits={hitsP2} + misses={missesP2} + sunkShips={sunkShipsP1} + onCellClick={ + currentPlayer === 2 ? (r, c) => handleAttack(r, c, 1) : null + } + hideShips={true} + /> + </div> + + <div className="board-wrapper"> + <h3>{player2Name}'s Board</h3> + <Board + boardSize={10} + ships={player2Ships.flat()} + hits={hitsP1} + misses={missesP1} + sunkShips={sunkShipsP2} + onCellClick={ + currentPlayer === 1 ? (r, c) => handleAttack(r, c, 2) : null + } + hideShips={true} + /> + </div> + </div> + </div> + ); +} + +export default GameBoard; diff --git a/z2/frontend/src/components/Header.css b/z2/frontend/src/components/Header.css new file mode 100644 index 0000000..37e0c60 --- /dev/null +++ b/z2/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/z2/frontend/src/components/Header.jsx b/z2/frontend/src/components/Header.jsx new file mode 100644 index 0000000..bf0127c --- /dev/null +++ b/z2/frontend/src/components/Header.jsx @@ -0,0 +1,12 @@ +import React from 'react'; +import './Header.css'; + +const Header = ({ title }) => { + return ( + <header className="header"> + <h1>{title}</h1> + </header> + ); +}; + +export default Header; diff --git a/z2/frontend/src/components/PlacementBoard.css b/z2/frontend/src/components/PlacementBoard.css new file mode 100644 index 0000000..fba4856 --- /dev/null +++ b/z2/frontend/src/components/PlacementBoard.css @@ -0,0 +1,73 @@ +.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; +} + +.controls { + display: flex; + flex-wrap: wrap; + gap: 1rem; + justify-content: center; + margin-bottom: 1.5rem; +} + +.control-group { + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.ship-label, +.orientation-label { + font-size: 1.1rem; + margin-bottom: 0.5rem; + color: #555; + font-weight: 600; +} + +.ship-select, +.orientation-select { + font-size: 1.1rem; + padding: 0.6rem; + border: 1px solid #ccc; + border-radius: 6px; + background-color: #f9f9f9; +} + +.ship-select { + width: 220px; +} + +.orientation-select { + width: 150px; +} + +.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/z2/frontend/src/components/PlacementBoard.jsx b/z2/frontend/src/components/PlacementBoard.jsx new file mode 100644 index 0000000..3541053 --- /dev/null +++ b/z2/frontend/src/components/PlacementBoard.jsx @@ -0,0 +1,135 @@ +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 [orientation, setOrientation] = useState('horizontal'); + + const isOverlap = (newShip) => { + for (const ship of ships) { + for (const cell of ship) { + for (const newCell of newShip) { + if (cell.row === newCell.row && cell.col === newCell.col) { + return true; + } + } + } + } + return false; + }; + + 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; + } + + let newShip = []; + if (orientation === 'horizontal') { + if (col + shipData.size > 10) { + alert('Ship is out of bounds horizontally!'); + return; + } + for (let i = 0; i < shipData.size; i++) { + newShip.push({ row, col: col + i }); + } + } else { + if (row + shipData.size > 10) { + alert('Ship is out of bounds vertically!'); + return; + } + for (let i = 0; i < shipData.size; i++) { + newShip.push({ row: row + i, col }); + } + } + + if (isOverlap(newShip)) { + alert('Ships cannot overlap!'); + return; + } + + 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 ( + <div className="placement-board"> + <h2>Ship Placement: {playerName}</h2> + + <div className="controls"> + <div className="control-group"> + <label className="ship-label">Ship type:</label> + <select + className="ship-select" + onChange={(e) => setSelectedShipType(e.target.value)} + value={selectedShipType} + > + {Object.keys(SHIPS_LIMIT).map((ship, index) => ( + <option key={index} value={ship}> + {ship} ({shipCounts[ship] || 0}/{SHIPS_LIMIT[ship].max}) + </option> + ))} + </select> + </div> + + <div className="control-group"> + <label className="orientation-label">Orientation:</label> + <select + className="orientation-select" + onChange={(e) => setOrientation(e.target.value)} + value={orientation} + > + <option value="horizontal">Horizontal</option> + <option value="vertical">Vertical</option> + </select> + </div> + </div> + + <Board + boardSize={10} + ships={ships.flat()} + onCellClick={handleCellClick} + hideShips={false} + /> + + <button className="finish-button" onClick={handleDone}> + Finish Placement + </button> + </div> + ); +} + +export default PlacementBoard; diff --git a/z2/frontend/src/components/PlayerForm.css b/z2/frontend/src/components/PlayerForm.css new file mode 100644 index 0000000..2a1a51a --- /dev/null +++ b/z2/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/z2/frontend/src/components/PlayerForm.jsx b/z2/frontend/src/components/PlayerForm.jsx new file mode 100644 index 0000000..93cade1 --- /dev/null +++ b/z2/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:30000/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 ( + <div className="player-form-container"> + <div className="player-form-card"> + <h2 className="form-title">Enter Player Names</h2> + <form onSubmit={handleSubmit} className="player-form"> + <div className="form-group"> + <label>Player 1:</label> + <input + type="text" + value={p1} + onChange={(e) => setP1(e.target.value)} + placeholder="Enter Player 1 name" + required + /> + </div> + <div className="form-group"> + <label>Player 2:</label> + <input + type="text" + value={p2} + onChange={(e) => setP2(e.target.value)} + placeholder="Enter Player 2 name" + required + /> + </div> + <button type="submit" className="submit-button">Start Game</button> + </form> + </div> + + <div className="results-section"> + <button className="toggle-results-button" onClick={handleToggleResults}> + {showResults ? 'Hide Results' : 'Show Results'} + </button> + {showResults && ( + <div className="results-card"> + <h3 className="results-title">Previous Game Results</h3> + {results.length > 0 ? ( + <table className="results-table"> + <thead> + <tr> + <th>Winner</th> + <th>Loser</th> + <th>Shots</th> + </tr> + </thead> + <tbody> + {results.map((result, index) => ( + <tr key={index}> + <td>{result.winner}</td> + <td>{result.loser}</td> + <td>{result.moves}</td> + </tr> + ))} + </tbody> + </table> + ) : ( + <p className="no-results">No previous results available.</p> + )} + </div> + )} + </div> + </div> + ); +} + +export default PlayerForm; diff --git a/z2/frontend/src/index.css b/z2/frontend/src/index.css new file mode 100644 index 0000000..e69de29 diff --git a/z2/frontend/src/main.jsx b/z2/frontend/src/main.jsx new file mode 100644 index 0000000..b9a1a6d --- /dev/null +++ b/z2/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( + <StrictMode> + <App /> + </StrictMode>, +) diff --git a/z2/frontend/vite.config.js b/z2/frontend/vite.config.js new file mode 100644 index 0000000..8b0f57b --- /dev/null +++ b/z2/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/z2/namespace.yaml b/z2/namespace.yaml new file mode 100644 index 0000000..6a1748b --- /dev/null +++ b/z2/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: battleship-app diff --git a/z2/prepare-app.sh b/z2/prepare-app.sh new file mode 100644 index 0000000..92c2617 --- /dev/null +++ b/z2/prepare-app.sh @@ -0,0 +1,8 @@ +#!/bin/bash +echo "Building Docker image for backend..." +docker build -t battleship-backend ./backend + +echo "Building Docker image for frontend..." +docker build -t battleship-frontend ./frontend + +echo "Docker images built." diff --git a/z2/remove-app.sh b/z2/remove-app.sh new file mode 100644 index 0000000..26739de --- /dev/null +++ b/z2/remove-app.sh @@ -0,0 +1,14 @@ +#!/bin/bash +echo "Deleting Services for MySQL, backend, and frontend..." +kubectl delete -f service.yaml -n battleship-app + +echo "Deleting StatefulSet for MySQL..." +kubectl delete -f statefulset.yaml -n battleship-app + +echo "Deleting Deployments for backend and frontend..." +kubectl delete -f deployment.yaml -n battleship-app + +echo "Deleting namespace battleship-app (and all objects within it)..." +kubectl delete -f namespace.yaml + +echo "Application completely removed from Kubernetes." diff --git a/z2/service.yaml b/z2/service.yaml new file mode 100644 index 0000000..22022c2 --- /dev/null +++ b/z2/service.yaml @@ -0,0 +1,40 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: mysql + namespace: battleship-app +spec: + ports: + - port: 3306 + targetPort: 3306 + selector: + app: battleship-mysql +--- +apiVersion: v1 +kind: Service +metadata: + name: backend + namespace: battleship-app +spec: + type: NodePort + ports: + - port: 4000 + targetPort: 4000 + nodePort: 30000 + selector: + app: battleship-backend +--- +apiVersion: v1 +kind: Service +metadata: + name: frontend + namespace: battleship-app +spec: + type: NodePort + ports: + - port: 80 + targetPort: 80 + nodePort: 30001 + selector: + app: battleship-frontend diff --git a/z2/start-app.sh b/z2/start-app.sh new file mode 100644 index 0000000..c57fd99 --- /dev/null +++ b/z2/start-app.sh @@ -0,0 +1,14 @@ +#!/bin/bash +echo "Creating namespace battleship-app..." +kubectl apply -f namespace.yaml + +echo "Creating Deployment for backend and frontend..." +kubectl apply -f deployment.yaml -n battleship-app + +echo "Creating StatefulSet for MySQL..." +kubectl apply -f statefulset.yaml -n battleship-app + +echo "Creating Service for MySQL, backend, and frontend..." +kubectl apply -f service.yaml -n battleship-app + +echo "Application deployed in Kubernetes." diff --git a/z2/statefulset.yaml b/z2/statefulset.yaml new file mode 100644 index 0000000..ea29ea3 --- /dev/null +++ b/z2/statefulset.yaml @@ -0,0 +1,58 @@ +apiVersion: v1 +kind: PersistentVolume +metadata: + name: mysql-pv + namespace: battleship-app +spec: + capacity: + storage: 1Gi + accessModes: + - ReadWriteOnce + hostPath: + path: /mnt/data/mysql +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: mysql-pvc + namespace: battleship-app +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: battleship-mysql + namespace: battleship-app +spec: + serviceName: "mysql" + replicas: 1 + selector: + matchLabels: + app: battleship-mysql + template: + metadata: + labels: + app: battleship-mysql + spec: + containers: + - name: battleship-mysql + image: mysql:8.0.30 + ports: + - containerPort: 3306 + env: + - name: MYSQL_ROOT_PASSWORD + value: somepassword + - name: MYSQL_DATABASE + value: battleship + volumeMounts: + - name: mysql-data + mountPath: /var/lib/mysql + volumes: + - name: mysql-data + persistentVolumeClaim: + claimName: mysql-pvc diff --git a/z2/stop-app.sh b/z2/stop-app.sh new file mode 100644 index 0000000..7d4d729 --- /dev/null +++ b/z2/stop-app.sh @@ -0,0 +1,13 @@ +#!/bin/bash +echo "Stopping the application by scaling down to 0 replicas..." + +echo "Scaling down the Deployment for backend to 0..." +kubectl scale deployment battleship-backend -n battleship-app --replicas=0 + +echo "Scaling down the Deployment for frontend to 0..." +kubectl scale deployment battleship-frontend -n battleship-app --replicas=0 + +echo "Scaling down the StatefulSet for MySQL to 0..." +kubectl scale statefulset battleship-mysql -n battleship-app --replicas=0 + +echo "Application stopped, data (e.g., database) is preserved."