diff --git a/README.md b/README.md index 324680c..5cdb1d2 100644 --- a/README.md +++ b/README.md @@ -1,72 +1,227 @@ # Chess App -## Overview -This project is a web-based chess application with a JavaScript frontend and a Rust backend. The frontend displays the chessboard and handles the game logic, while the backend serves the JavaScript files via static HTML and stores all player games and moves in a PostgreSQL database. +This project is a web-based chess application with a **JavaScript frontend** and a **Rust backend**. The frontend displays the chessboard and handles game logic, while the backend serves JavaScript files via static HTML and stores all player games and moves in a **PostgreSQL** database. -## Frontend +--- +## 📌 Clone the Repository +```bash +git clone git@github.com:timwhite06/chess-rust-and-javascript.git +cd chess-rust-and-javascript +``` + +--- +## 🔧 Setup Instructions + +### ✅ Prerequisites + +#### 1️⃣ **Docker Desktop** +Install [Docker Desktop](https://www.docker.com/products/docker-desktop) and ensure it is running in **Linux container mode** (right-click the Docker icon in the system tray and select *"Switch to Linux containers..."* if needed). + +#### 2️⃣ **Node.js & npm** +Install [Node.js](https://nodejs.org/) (which includes npm) for managing frontend dependencies. + +#### 3️⃣ **Rust Toolchain** +Install Rust via [rustup](https://rustup.rs/). + +#### 4️⃣ **SQLx CLI** +To run database migrations using SQLx, install the CLI: +```bash +cargo install sqlx-cli --no-default-features --features postgres +``` + +--- + +## Setting Up DBeaver for PostgreSQL + +This guide will walk you through the process of setting up DBeaver to connect to your PostgreSQL database for the project. + +### 1. Download and Install DBeaver + +- **Download:** + Visit the [DBeaver Community Edition website](https://dbeaver.io/download/) and download the installer for your operating system. + +- **Install:** + Follow the installation instructions specific to your OS. + +### 2. Create a New PostgreSQL Connection + +1. **Open DBeaver.** + +2. **Start a New Connection:** + - Click on **Database** in the menu and select **New Database Connection**. + - Alternatively, click the **New Connection** button in the toolbar. + +3. **Select Database Type:** + - In the "Connect to a database" dialog, scroll through the list and select **PostgreSQL**. + - Click **Next**. + +### 3. Configure Connection Settings + +You need to provide the correct JDBC URL and credentials to connect to your database. + +#### Option A: Use the Connection Fields + +- **Host:** `localhost` +- **Port:** `5432` +- **Database:** `chess-rust-javascript` +- **Username:** `admin` +- **Password:** `admin` + +DBeaver will automatically construct the JDBC URL for you when these fields are filled out. + +#### Option B: Use a Custom JDBC URL + +If you prefer to use a JDBC URL, use the following format: +``` +jdbc:postgresql://localhost:5432/chess-rust-javascript?user=admin&password=admin +``` +- Enter this URL into the **URL** field. +- You can leave the username and password fields blank if they are specified in the URL, or fill them in as above. + +### 4. Test the Connection + +- Click the **Test Connection** button. +- If prompted for a driver download, allow DBeaver to download the PostgreSQL JDBC driver. +- You should see a message indicating the connection was successful. + +### 5. Finish and Save the Connection + +- Once the connection test is successful, click **Finish**. +- Your new connection will appear in the **Database Navigator** panel on the left. + +### 6. Refresh the Schema and Verify Tables + +- **Refresh:** + Right-click on your connection or the `public` schema and choose **Refresh** or **Reload**. + +- **Verify Tables:** + Open an SQL Editor in DBeaver and run: + ```sql + SELECT table_name + FROM information_schema.tables + WHERE table_schema = 'public'; + +--- +## 🚀 Running the Project + +### 1️⃣ Start the Database +Ensure Docker is running, then execute: + + +Verify database connection: + +```bash +docker exec -it $(docker ps -q -f name=postgres) psql -U admin -d chess-rust-javascript -c "\dt" +``` + + +**Expected Output:** + +List of relations +``` +Schema | Name | Type | Owner +-------+--------+-------+------- +public | games | table | admin +public | moves | table | admin +(2 rows) +``` +If tables are missing, proceed with migrations. + +--- +### 2️⃣ Run Database Migrations +Ensure `DATABASE_URL` is set: +```bash +export DATABASE_URL="postgres://admin:admin@localhost:5432/chess-rust-javascript" +``` +For Windows PowerShell: +```powershell +$env:DATABASE_URL="postgres://admin:admin@localhost:5432/chess-rust-javascript" +``` +Run migrations: +```bash +sqlx migrate run +``` +Verify migrations: +```bash +sqlx migrate info +``` + +--- +### 3️⃣ Build the Backend +```bash +npm run build:backend +``` + +### 4️⃣ Run the Project +```bash +npm run serve +``` + +--- +## 🏗️ Project Structure + +### 🎨 Frontend - **Language:** JavaScript -- **Description:** The frontend is responsible for rendering the chessboard, handling user interactions, and implementing the game logic. It follows an object-oriented design. -- **Files:** - - `index.html`: The main HTML file that includes the chessboard container. - - `css/styles.css`: The stylesheet for the application. - - `scripts/main.js`: The main JavaScript file that initializes the game. - - `scripts/board.js`: Handles the chessboard rendering. - - `scripts/game.js`: Manages the game state. - - `scripts/moveLogic.js`: Contains the logic for validating and executing moves. - - `scripts/piece.js`: Defines the `Piece` class. - -## Backend +- **Description:** Handles rendering the chessboard, user interactions, and game logic. +- **Key Files:** + - `index.html` – Main HTML file with the chessboard container. + - `css/styles.css` – Stylesheet for UI. + - `scripts/main.js` – Initializes the game. + - `scripts/board.js` – Manages chessboard rendering. + - `scripts/game.js` – Controls game state. + - `scripts/moveLogic.js` – Implements move validation and execution. + - `scripts/piece.js` – Defines chess pieces. + +### 🔧 Backend - **Language:** Rust -- **Description:** The backend is a web server that hosts the frontend files and interacts with a PostgreSQL database to store game data. - **Database:** PostgreSQL -- **Functionality:** - - Serves static HTML, CSS, and JavaScript files. - - Supports WebSocket connections for real-time communication. - - Stores player games and moves in the database. - -## Libraries Used -The backend uses the following Rust libraries to provide functionality: - -1. **Axum** - - **Purpose:** A modern, ergonomic web framework for building APIs and web servers. - - **Usage in Project:** Used to handle HTTP routes and serve static files. - - **Why Axum:** Its simplicity and compatibility with async programming make it ideal for building performant and scalable applications. - -2. **Tower HTTP** - - **Purpose:** Provides middleware and utilities for web servers. - - **Usage in Project:** Used to serve static files (HTML, CSS, JS) from a directory on the server. - - **Why Tower HTTP:** It integrates seamlessly with Axum for static file serving and other HTTP-specific tasks. - -3. **Tokio** - - **Purpose:** An asynchronous runtime for Rust. - - **Usage in Project:** Powers asynchronous operations, such as handling multiple HTTP requests and WebSocket connections concurrently. - - **Why Tokio:** It’s fast, reliable, and widely used in the Rust ecosystem for async programming. - -4. **SQLx** - - **Purpose:** A Rust library for interacting with databases. - - **Usage in Project:** Used to connect to the PostgreSQL database and execute SQL queries for storing and retrieving game data. - - **Why SQLx:** It supports async queries, has compile-time query validation, and integrates well with Rust. - -5. **Tracing** - - **Purpose:** A structured logging and diagnostics library for Rust. - - **Usage in Project:** Provides detailed logs for debugging and monitoring application behavior. - - **Why Tracing:** It enables rich, structured logs that help identify issues during development and production. - -6. **Tracing Subscriber** - - **Purpose:** A library that processes and outputs tracing data. - - **Usage in Project:** Configures how logs are displayed in the application. - - **Why Tracing Subscriber:** It works in tandem with tracing to provide a flexible logging setup. - -## Why Async is Important -Asynchronous programming is crucial for this project because it allows the backend to handle multiple tasks concurrently without blocking the execution of other tasks. This is particularly important for: -- Handling multiple HTTP requests simultaneously, ensuring that the server remains responsive even under heavy load. -- Managing WebSocket connections for real-time communication between the frontend and backend. -- Performing database operations without blocking the main thread, which improves the overall performance and scalability of the application. - -## Usage +- **Features:** + - Serves static files (HTML, CSS, JS). + - Supports WebSockets for real-time communication. + - Stores games and moves in the database. + +--- +## 📚 Rust Libraries Used + +### 🚀 Actix Web +- **Purpose:** Fast web framework for handling HTTP requests. +- **Usage:** Serves frontend files and API routes. + +### ⚡ Actix RT +- **Purpose:** Async runtime for handling multiple tasks. +- **Usage:** Runs non-blocking operations like WebSockets and database queries. + +### 🔄 Actix Web Actors +- **Purpose:** Enables WebSocket connections. +- **Usage:** Handles real-time chess game updates. + +### 📂 Actix Files +- **Purpose:** Middleware for serving static files. +- **Usage:** Serves frontend files efficiently. + +### 🗄️ SQLx +- **Purpose:** Async SQL library for Rust. +- **Usage:** Executes database queries and migrations. + +### 📊 Tracing & Tracing Subscriber +- **Purpose:** Structured logging and diagnostics. +- **Usage:** Logs application activity for debugging and monitoring. + +--- +## 🔄 Why Async is Important? +Asynchronous programming improves performance by allowing the backend to handle: +✅ Multiple HTTP requests concurrently. +✅ WebSocket connections for real-time communication. +✅ Database operations without blocking execution. + +--- +## 🎮 Usage - Open the application in a web browser. - Play chess by interacting with the chessboard. -- The backend will store all game data in the PostgreSQL database. +- The backend stores all game data in PostgreSQL. -## License -This project is licensed under the MIT License. \ No newline at end of file +--- +## 📜 License +This project is licensed under the **MIT License**. diff --git a/backend/.env b/backend/.env new file mode 100644 index 0000000..cb348f5 --- /dev/null +++ b/backend/.env @@ -0,0 +1 @@ +DATABASE_URL="postgres://admin:admin@localhost:5432/chess-rust-javascript" \ No newline at end of file diff --git a/backend/.sqlx/query-2b11e0412f71e5b56ef55d4aba30be4da33ca50b285e5582c3e420d862f6fec7.json b/backend/.sqlx/query-2b11e0412f71e5b56ef55d4aba30be4da33ca50b285e5582c3e420d862f6fec7.json new file mode 100644 index 0000000..2668919 --- /dev/null +++ b/backend/.sqlx/query-2b11e0412f71e5b56ef55d4aba30be4da33ca50b285e5582c3e420d862f6fec7.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id FROM games WHERE game_url_id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false + ] + }, + "hash": "2b11e0412f71e5b56ef55d4aba30be4da33ca50b285e5582c3e420d862f6fec7" +} diff --git a/backend/.sqlx/query-9de15277059ca12888b14414bfecdde272897892c893ad1de96a0411892e597c.json b/backend/.sqlx/query-9de15277059ca12888b14414bfecdde272897892c893ad1de96a0411892e597c.json new file mode 100644 index 0000000..dce6225 --- /dev/null +++ b/backend/.sqlx/query-9de15277059ca12888b14414bfecdde272897892c893ad1de96a0411892e597c.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO moves (game_id, move_from, move_to, piece_color, piece_id, piece_image_path, piece_type)\n VALUES ($1, $2, $3, $4, $5, $6, $7)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int4", + "Text", + "Text", + "Text", + "Text", + "Text", + "Text" + ] + }, + "nullable": [] + }, + "hash": "9de15277059ca12888b14414bfecdde272897892c893ad1de96a0411892e597c" +} diff --git a/backend/.sqlx/query-da8ba9b311782704162738df41c77a28986b53e2545cf3fa097d5359b918df69.json b/backend/.sqlx/query-da8ba9b311782704162738df41c77a28986b53e2545cf3fa097d5359b918df69.json new file mode 100644 index 0000000..490f4f4 --- /dev/null +++ b/backend/.sqlx/query-da8ba9b311782704162738df41c77a28986b53e2545cf3fa097d5359b918df69.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO games (game_url_id) VALUES ($1) ON CONFLICT (game_url_id) DO NOTHING", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + }, + "hash": "da8ba9b311782704162738df41c77a28986b53e2545cf3fa097d5359b918df69" +} diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 4288664..2995a02 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -8,7 +8,9 @@ version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de7fa236829ba0841304542f7614c42b80fca007455315c45c785ccfa873a85b" dependencies = [ + "actix-macros", "actix-rt", + "actix_derive", "bitflags", "bytes", "crossbeam-channel", @@ -250,6 +252,17 @@ dependencies = [ "syn", ] +[[package]] +name = "actix_derive" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6ac1e58cded18cb28ddc17143c4dea5345b3ad575e14f32f66e4054a56eb271" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "addr2line" version = "0.24.2" @@ -327,10 +340,12 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" name = "backend" version = "0.1.0" dependencies = [ + "actix", "actix-files", "actix-rt", "actix-web", "actix-web-actors", + "serde_json", "sqlx", "tracing", "tracing-subscriber", @@ -1708,9 +1723,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.137" +version = "1.0.138" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b" +checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" dependencies = [ "itoa", "memchr", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 11d170b..1003216 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -8,6 +8,8 @@ actix-web = "4.3" actix-rt = "2.8" actix-web-actors = "4" # for WebSocket actor support actix-files = "0.6" +actix="0.13" tracing = "0.1.41" tracing-subscriber = "0.3.19" sqlx = { version = "0.8.3", features = ["runtime-tokio-native-tls", "postgres"] } +serde_json = "1.0.138" diff --git a/backend/DockerFile b/backend/DockerFile new file mode 100644 index 0000000..0b139b5 --- /dev/null +++ b/backend/DockerFile @@ -0,0 +1,26 @@ +# Use the official Rust image as a base +FROM rust:1.72-slim AS builder + +# Set the working directory +WORKDIR /app + +# Copy the Rust project files +COPY . . + +# Build the application +RUN cargo build --release + +# Use a smaller base image for the runtime +FROM debian:bullseye-slim + +# Set the working directory +WORKDIR /src + +# Copy the binary from the builder stage +COPY --from=builder /app/target/release/backend /app/backend + +# Expose the port your backend runs on +EXPOSE 8080 + +# Set the default command to run your application +CMD ["/src/backend"] diff --git a/backend/migrations/20250204213601_init.sql b/backend/migrations/20250204213601_init.sql new file mode 100644 index 0000000..1aeb031 --- /dev/null +++ b/backend/migrations/20250204213601_init.sql @@ -0,0 +1,16 @@ +-- Add migration script here +CREATE TABLE games ( + id SERIAL PRIMARY KEY, + game_url_id TEXT UNIQUE NOT NULL +); + +CREATE TABLE moves ( + id SERIAL PRIMARY KEY, + game_id INT NOT NULL REFERENCES games(id) ON DELETE CASCADE, + move_from TEXT NOT NULL, + move_to TEXT NOT NULL, + piece_color TEXT NOT NULL, + piece_id TEXT NOT NULL, + piece_image_path TEXT NOT NULL, + piece_type TEXT NOT NULL +); diff --git a/backend/src/database_handler.rs b/backend/src/database_handler.rs new file mode 100644 index 0000000..77f0f93 --- /dev/null +++ b/backend/src/database_handler.rs @@ -0,0 +1,69 @@ +// IMPORTANT: Before building, either set DATABASE_URL or run `cargo sqlx prepare`. + +use sqlx::{PgPool, postgres::PgPoolOptions}; + +#[derive(Clone)] +pub struct DatabaseHandler { + pub pool: PgPool, +} + +impl DatabaseHandler { + /// Creates a new database handler with a connection pool. + pub async fn new(database_url: &str) -> Self { + let pool = PgPoolOptions::new() + .max_connections(5) + .connect(database_url) + .await + .expect("Failed to connect to Postgres"); + + DatabaseHandler { pool } + } + + /// Logs a move into the database. + /// + /// This method: + /// 1. Inserts a game record with the given game_url_id if it doesn't already exist. + /// 2. Retrieves the game's numeric id. + /// 3. Inserts the move into the moves table. + pub async fn log_move( + &self, + game_url_id: &str, + move_from: &str, + move_to: &str, + piece_color: &str, + piece_id: &str, + piece_image_path: &str, + piece_type: &str, + ) -> Result<(), sqlx::Error> { + // Insert the game if it doesn't exist. + sqlx::query!( + "INSERT INTO games (game_url_id) VALUES ($1) ON CONFLICT (game_url_id) DO NOTHING", + game_url_id + ) + .execute(&self.pool) + .await?; + + // Retrieve the game's id. + let game = sqlx::query!("SELECT id FROM games WHERE game_url_id = $1", game_url_id) + .fetch_one(&self.pool) + .await?; + let game_id = game.id; + + // Insert the move. + sqlx::query!( + "INSERT INTO moves (game_id, move_from, move_to, piece_color, piece_id, piece_image_path, piece_type) + VALUES ($1, $2, $3, $4, $5, $6, $7)", + game_id, + move_from, + move_to, + piece_color, + piece_id, + piece_image_path, + piece_type + ) + .execute(&self.pool) + .await?; + + Ok(()) + } +} diff --git a/backend/src/main.rs b/backend/src/main.rs index 4f4ba70..5ea4619 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -1,20 +1,33 @@ -use actix_files as fs; // For serving static files -use actix_web::{get, App, HttpResponse, HttpServer, Responder}; +mod websocket; +mod database_handler; + +use std::sync::Arc; // Import Arc for shared ownership. +use crate::database_handler::DatabaseHandler; // Import DatabaseHandler. +use actix_files as fs; +use actix_web::{get, web, App, HttpResponse, HttpServer}; -/// Simple HTTP endpoint at "/api" #[get("/api")] -async fn api_endpoint() -> impl Responder { +async fn api_endpoint() -> impl actix_web::Responder { HttpResponse::Ok().body("Hello from Actix API!") } #[actix_web::main] async fn main() -> std::io::Result<()> { - // Attempt to bind and run the server + // Read the database URL from an environment variable (or use a default) + let database_url = std::env::var("DATABASE_URL") + .unwrap_or_else(|_| "postgres://user:password@localhost/dbname".to_string()); + + // Initialize the database handler. + let db_handler = Arc::new(DatabaseHandler::new(&database_url).await); + let server_result = HttpServer::new(move || { App::new() + .app_data(web::Data::new(db_handler.clone())) + .service(api_endpoint) + .route("/ws", web::get().to(websocket::websocket_handler)) // WebSocket route .service( fs::Files::new("/", "../frontend") - .index_file("index.html") // Serve index.html by default + .index_file("index.html") ) }) .bind(("127.0.0.1", 3000)); @@ -25,7 +38,7 @@ async fn main() -> std::io::Result<()> { server.run().await } Err(e) => { - eprintln!("\x1b[31mFailed to start server: {}\x1b[0m", e); // Print error in red + eprintln!("\x1b[31mFailed to start server: {}\x1b[0m", e); Err(e) } } diff --git a/backend/src/websocket.rs b/backend/src/websocket.rs new file mode 100644 index 0000000..0c00629 --- /dev/null +++ b/backend/src/websocket.rs @@ -0,0 +1,125 @@ +use actix::{Actor, StreamHandler}; +use actix_web::{web, Error, HttpRequest, HttpResponse}; +use actix_web_actors::ws; +use std::sync::Arc; +use std::time::Instant; +use serde_json::Value; + +// Import the database handler. +use crate::database_handler::DatabaseHandler; + +/// WebSocket session struct with a database handler. +pub struct WebSocketSession { + pub hb: Instant, + pub db: Arc, +} + +impl Actor for WebSocketSession { + type Context = ws::WebsocketContext; + + fn started(&mut self, ctx: &mut Self::Context) { + self.hb = Instant::now(); + ctx.text("Connected to WebSocket server"); + } +} + +// Use the fully-qualified standard Result here to avoid conflicts. +impl StreamHandler> for WebSocketSession { + fn handle(&mut self, msg: std::result::Result, ctx: &mut Self::Context) { + match msg { + Ok(ws::Message::Text(text)) => { + // Parse the JSON text. + match serde_json::from_str::(&text) { + Ok(json_val) => { + println!("Received: {:?}", json_val); + // Check for the "logMove" message type. + if json_val["message"]["messageType"] == "logMove" { + println!("Received logMove message"); + + // Extract move details from the JSON and convert them to owned strings. + let game_url_id = json_val["gameUrlId"] + .as_str() + .unwrap_or("default_game") + .to_string(); + let move_from = json_val["message"]["from"] + .as_str() + .unwrap_or("") + .to_string(); + let move_to = json_val["message"]["to"] + .as_str() + .unwrap_or("") + .to_string(); + let piece_color = json_val["message"]["piece"]["color"] + .as_str() + .unwrap_or("") + .to_string(); + let piece_id = json_val["message"]["piece"]["id"] + .as_str() + .unwrap_or("") + .to_string(); + let piece_image_path = json_val["message"]["piece"]["imagePath"] + .as_str() + .unwrap_or("") + .to_string(); + let piece_type = json_val["message"]["piece"]["type"] + .as_str() + .unwrap_or("") + .to_string(); + + // Spawn an asynchronous task to log the move. + let db = Arc::clone(&self.db); + actix::spawn(async move { + if let Err(e) = db + .log_move( + &game_url_id, + &move_from, + &move_to, + &piece_color, + &piece_id, + &piece_image_path, + &piece_type, + ) + .await + { + println!("Error logging move: {:?}", e); + } else { + println!("Move logged successfully."); + } + }); + } + } + Err(e) => println!("Error parsing JSON: {:?}", e), + } + // Send an echo message back to the client. + ctx.text(format!("Echo: {}", text)); + } + Ok(ws::Message::Binary(bin)) => ctx.binary(bin), + Ok(ws::Message::Ping(msg)) => ctx.pong(&msg), + Ok(ws::Message::Close(_)) => println!("WebSocket closed"), + _ => (), + } + } +} + +/// WebSocket route handler function. +/// We assume that the DatabaseHandler is registered as shared data. +pub async fn websocket_handler( + req: HttpRequest, + stream: web::Payload, +) -> std::result::Result { + // Extract the database handler from the shared application state. + let db_data = req + .app_data::>>() + .expect("Database handler not configured") + .get_ref() + .clone(); + + ws::start( + WebSocketSession { + hb: Instant::now(), + db: db_data, + }, + &req, + stream, + ) +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..cf00a06 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,28 @@ +services: + postgres: + image: postgres:13 + restart: always + environment: + POSTGRES_USER: admin + POSTGRES_PASSWORD: admin + POSTGRES_DB: chess-rust-javascript + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + + # backend: + # build: + # context: ./backend + # environment: + # DATABASE_URL: ${DATABASE_URL} + # depends_on: + # - postgres + # command: ["cargo", "run"] + # volumes: + # - ./backend:/src + # ports: + # - "8080:8080" + +volumes: + postgres_data: diff --git a/frontend/index.html b/frontend/index.html index c00dab0..e2e0be9 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,25 +1,18 @@ - - - Chess App - + + + Chess App + + + + +
-
- -
Hello
-
-
-
-
- -
- - - - + + diff --git a/frontend/assets/images/black/bishop.png b/frontend/public/images/black/bishop.png similarity index 100% rename from frontend/assets/images/black/bishop.png rename to frontend/public/images/black/bishop.png diff --git a/frontend/assets/images/black/king.png b/frontend/public/images/black/king.png similarity index 100% rename from frontend/assets/images/black/king.png rename to frontend/public/images/black/king.png diff --git a/frontend/assets/images/black/knight.png b/frontend/public/images/black/knight.png similarity index 100% rename from frontend/assets/images/black/knight.png rename to frontend/public/images/black/knight.png diff --git a/frontend/assets/images/black/pawn.png b/frontend/public/images/black/pawn.png similarity index 100% rename from frontend/assets/images/black/pawn.png rename to frontend/public/images/black/pawn.png diff --git a/frontend/assets/images/black/queen.png b/frontend/public/images/black/queen.png similarity index 100% rename from frontend/assets/images/black/queen.png rename to frontend/public/images/black/queen.png diff --git a/frontend/assets/images/black/rook.png b/frontend/public/images/black/rook.png similarity index 100% rename from frontend/assets/images/black/rook.png rename to frontend/public/images/black/rook.png diff --git a/frontend/assets/images/white/bishop.png b/frontend/public/images/white/bishop.png similarity index 100% rename from frontend/assets/images/white/bishop.png rename to frontend/public/images/white/bishop.png diff --git a/frontend/assets/images/white/king.png b/frontend/public/images/white/king.png similarity index 100% rename from frontend/assets/images/white/king.png rename to frontend/public/images/white/king.png diff --git a/frontend/assets/images/white/knight.png b/frontend/public/images/white/knight.png similarity index 100% rename from frontend/assets/images/white/knight.png rename to frontend/public/images/white/knight.png diff --git a/frontend/assets/images/white/pawn.png b/frontend/public/images/white/pawn.png similarity index 100% rename from frontend/assets/images/white/pawn.png rename to frontend/public/images/white/pawn.png diff --git a/frontend/assets/images/white/queen.png b/frontend/public/images/white/queen.png similarity index 100% rename from frontend/assets/images/white/queen.png rename to frontend/public/images/white/queen.png diff --git a/frontend/assets/images/white/rook.png b/frontend/public/images/white/rook.png similarity index 100% rename from frontend/assets/images/white/rook.png rename to frontend/public/images/white/rook.png diff --git a/frontend/scripts/board.js b/frontend/scripts/board.js index 860c01b..5ebb01e 100644 --- a/frontend/scripts/board.js +++ b/frontend/scripts/board.js @@ -46,35 +46,35 @@ export default class Board { setupPieces() { // Place pieces on the board to start the game - const whitePiecesImageLocation = 'assets/images/white/'; - const blackPiecesImageLocation = 'assets/images/black/'; + const whitePiecesImageLocation = 'public/images/white/'; + const blackPiecesImageLocation = 'public/images/black/'; // White pieces setup this.board[7].forEach((cell, colIndex) => { switch (colIndex) { case 0: case 7: - cell.piece = { type: 'rook', color: 'white', imagePath: `${whitePiecesImageLocation}rook.png` }; + cell.piece = { type: 'rook', color: 'white', imagePath: `${whitePiecesImageLocation}rook.png`, id: `white-rook-${colIndex}` }; break; case 1: case 6: - cell.piece = { type: 'knight', color: 'white', imagePath: `${whitePiecesImageLocation}knight.png` }; + cell.piece = { type: 'knight', color: 'white', imagePath: `${whitePiecesImageLocation}knight.png`, id: `white-knight-${colIndex}` }; break; case 2: case 5: - cell.piece = { type: 'bishop', color: 'white', imagePath: `${whitePiecesImageLocation}bishop.png` }; + cell.piece = { type: 'bishop', color: 'white', imagePath: `${whitePiecesImageLocation}bishop.png`, id: `white-bishop-${colIndex}` }; break; case 3: - cell.piece = { type: 'queen', color: 'white', imagePath: `${whitePiecesImageLocation}queen.png` }; + cell.piece = { type: 'queen', color: 'white', imagePath: `${whitePiecesImageLocation}queen.png`, id: `white-queen-${colIndex}` }; break; case 4: - cell.piece = { type: 'king', color: 'white', imagePath: `${whitePiecesImageLocation}king.png` }; + cell.piece = { type: 'king', color: 'white', imagePath: `${whitePiecesImageLocation}king.png`, id: `white-king-${colIndex}` }; break; } }); - this.board[6].forEach((cell) => { - cell.piece = { type: 'pawn', color: 'white', imagePath: `${whitePiecesImageLocation}pawn.png` }; + this.board[6].forEach((cell, index) => { + cell.piece = { type: 'pawn', color: 'white', imagePath: `${whitePiecesImageLocation}pawn.png`, id: `white-pawn-${index}` }; }); // Black pieces setup @@ -82,27 +82,27 @@ export default class Board { switch (colIndex) { case 0: case 7: - cell.piece = { type: 'rook', color: 'black', imagePath: `${blackPiecesImageLocation}rook.png` }; + cell.piece = { type: 'rook', color: 'black', imagePath: `${blackPiecesImageLocation}rook.png`, id: `black-rook-${colIndex}` }; break; case 1: case 6: - cell.piece = { type: 'knight', color: 'black', imagePath: `${blackPiecesImageLocation}knight.png` }; + cell.piece = { type: 'knight', color: 'black', imagePath: `${blackPiecesImageLocation}knight.png`, id: `black-knight-${colIndex}` }; break; case 2: case 5: - cell.piece = { type: 'bishop', color: 'black', imagePath: `${blackPiecesImageLocation}bishop.png` }; + cell.piece = { type: 'bishop', color: 'black', imagePath: `${blackPiecesImageLocation}bishop.png`, id: `black-bishop-${colIndex}` }; break; case 3: - cell.piece = { type: 'queen', color: 'black', imagePath: `${blackPiecesImageLocation}queen.png` }; + cell.piece = { type: 'queen', color: 'black', imagePath: `${blackPiecesImageLocation}queen.png`, id: `black-queen-${colIndex}` }; break; case 4: - cell.piece = { type: 'king', color: 'black', imagePath: `${blackPiecesImageLocation}king.png` }; + cell.piece = { type: 'king', color: 'black', imagePath: `${blackPiecesImageLocation}king.png`, id: `black-king-${colIndex}` }; break; } }); - this.board[1].forEach((cell) => { - cell.piece = { type: 'pawn', color: 'black', imagePath: `${blackPiecesImageLocation}pawn.png` }; + this.board[1].forEach((cell, index) => { + cell.piece = { type: 'pawn', color: 'black', imagePath: `${blackPiecesImageLocation}pawn.png`, id: `black-pawn-${index}` }; }); } diff --git a/frontend/scripts/game.js b/frontend/scripts/game.js index 52b24da..6cee33c 100644 --- a/frontend/scripts/game.js +++ b/frontend/scripts/game.js @@ -1,22 +1,27 @@ -// game.js +/** + * Manages the overall game state, move history, turn switching, and communication with the backend. + * Responsible for logging moves as part of its state management. + */ import MoveLogic from './moveLogic.js'; export default class ChessGame { - constructor(board) { - this.board = board; - this.currentTurn = 'white'; // Tracks whose turn it is - this.moveLogic = new MoveLogic(this); // Pass ChessGame instance to MoveLogic - this.moveHistory = []; // To log moves + constructor(sendMessageToBackend) { + this.board = null; + this.currentTurn = 'white'; + this.moveHistory = []; + this.sendMessageToBackend = sendMessageToBackend; + + // Inject this game instance into MoveLogic + this.moveLogic = new MoveLogic(this); } setBoard(board) { this.board = board; - this.moveLogic = new MoveLogic(this); // Initialize MoveLogic with this game instance + this.moveLogic = new MoveLogic(this); } init() { this.board.render(); - console.log("Game initialized!"); } switchTurn() { @@ -25,13 +30,24 @@ export default class ChessGame { } logMove(move) { + // Store the move this.moveHistory.push(move); - const moveHistoryElement = document.getElementById('moveHistory'); - if (moveHistoryElement) { - const moveItem = document.createElement('div'); - moveItem.innerText = move; - moveHistoryElement.appendChild(moveItem); + + const query = new URLSearchParams(window.location.search); + + // Grab the game ID + let gameId; + if (query.has('game')) { + gameId = query.get('game'); + move.gameUrlId = gameId; } - console.log("Move logged:", move); + + // Set the type of message to be sent to the backend + move.messageType = "logMove" + + console.log(move); + // Send the move to the backend + + this.sendMessageToBackend(move); } } diff --git a/frontend/scripts/main.js b/frontend/scripts/main.js index 02dccaa..da6ff80 100644 --- a/frontend/scripts/main.js +++ b/frontend/scripts/main.js @@ -1,13 +1,127 @@ -// main.js import Board from './board.js'; import ChessGame from './game.js'; window.addEventListener('DOMContentLoaded', () => { - const boardElement = document.getElementById('chessboard'); - const game = new ChessGame(); // Temporarily pass null - const board = new Board(boardElement, game); // Now pass ChessGame instance - game.board = board; // Set the board reference in ChessGame - board.render(); - game.init(); - console.log('Chess game initialized!'); + // A simple router function that reads the URL and loads the corresponding view. + function router() { + const query = new URLSearchParams(window.location.search); + + // If a game id is present in the URL, load the game view. + if (query.has('game')) { + const gameId = query.get('game'); + loadGameView(gameId); + } + // If the view is set to "history", load the history view. + else if (query.has('view') && query.get('view') === 'history') { + loadHistoryView(); + } + // Otherwise, load the home view. + else { + renderHome(); + } + } + + // Render the home view with the two main buttons. + function renderHome() { + const mainContainer = document.getElementById('mainContainer'); + mainContainer.innerHTML = ` +

Chess App

+ + + `; + + document.getElementById('newGameBtn').addEventListener('click', () => { + // Generate a new game id. + const gameId = Date.now(); + // Push the new URL with the game query parameter. + history.pushState({ gameId }, '', '/frontend/?game=' + gameId); + // Load the game view. + loadGameView(gameId); + }); + + document.getElementById('viewGamesBtn').addEventListener('click', () => { + history.pushState({ view: 'history' }, '', '/?view=history'); + loadHistoryView(); + }); + } + + // Load the game view, reusing your chess logic. + function loadGameView(gameId) { + const mainContainer = document.getElementById('mainContainer'); + mainContainer.innerHTML = ` +
+

Chess Game: ${gameId}

+ +
+
+
Turn: ?
+
+
+ + +
+ `; + + // Initialize your WebSocket worker and chess game logic here. + const boardElement = document.getElementById('chessboard'); + const socketWorker = new Worker('./workers/websocketWorker.js', { type: 'module' }); + + // Listen for messages from the worker. + socketWorker.addEventListener('message', (event) => { + const { type, data } = event.data; + if (type === "status") { + console.log("Worker status:", data); + } else if (type === "message") { + console.log("Message from server via worker:", data); + } else if (type === "error") { + console.error("Worker error:", data); + } + }); + + // Function to send a message via the worker. + function sendMessageToBackend(payload) { + socketWorker.postMessage({ + action: "send", + payload: { message: payload } + }); + } + + function closeConnection() { + socketWorker.postMessage({ action: "close" }); + } + + const game = new ChessGame(sendMessageToBackend); + const board = new Board(boardElement, game); + game.board = board; + board.render(); + game.init(); + + // Bind to buttons. + document.getElementById("sendBtn")?.addEventListener("click", () => sendMessageToBackend("Hello from game view!")); + document.getElementById("disconnectBtn")?.addEventListener("click", closeConnection); + document.getElementById("backBtn")?.addEventListener("click", () => { + history.pushState({}, '', '/'); + renderHome(); + }); + } + + // Load a simple history view. + function loadHistoryView() { + const mainContainer = document.getElementById('mainContainer'); + mainContainer.innerHTML = ` +

Previous Games

+

List of previous games goes here...

+ + `; + document.getElementById('backBtn').addEventListener('click', () => { + history.pushState({}, '', '/'); + renderHome(); + }); + } + + // Listen for popstate events to handle browser navigation (back/forward buttons). + window.addEventListener('popstate', router); + + // Initialize the router. + router(); }); diff --git a/frontend/scripts/moveLogic.js b/frontend/scripts/moveLogic.js index 4617e74..5f3661c 100644 --- a/frontend/scripts/moveLogic.js +++ b/frontend/scripts/moveLogic.js @@ -1,10 +1,8 @@ -// moveLogic.js export default class MoveLogic { constructor(game) { this.game = game; // Reference to ChessGame instance this.selectedPiece = null; this.selectedTargetCell = null; - // Bind the method to ensure correct 'this' context this.handleCellClick = this.handleCellClick.bind(this); } @@ -26,21 +24,29 @@ export default class MoveLogic { if (!cell.piece || cell.piece.color !== this.selectedPiece.color) { this.selectedTargetCell = cell; - console.log('Selected target cell:', this.selectedTargetCell); - // Validate the move if (this.isValidMove(this.selectedSourceCell, this.selectedTargetCell)) { // Move the piece this.selectedTargetCell.piece = this.selectedPiece; this.selectedSourceCell.piece = { type: null, color: null, imagePath: null }; + // Create the move log data + const moveData = { + from: this.selectedSourceCell.location.notation, + to: cell.location.notation, + piece: this.selectedPiece + }; + + // Delegate logging to the ChessGame instance + // You can either pass the move object or a JSON string, as needed + this.game.logMove(moveData); + + // Clear the selection this.selectedPiece = null; this.selectedSourceCell = null; this.selectedTargetCell = null; - console.log('Moved piece to:', cell.location.notation); - // Re-render the board board.render(); } else { diff --git a/frontend/workers/websocketWorker.js b/frontend/workers/websocketWorker.js new file mode 100644 index 0000000..55c0dc7 --- /dev/null +++ b/frontend/workers/websocketWorker.js @@ -0,0 +1,43 @@ +// Create a WebSocket connection +const socket = new WebSocket("ws://127.0.0.1:3000/ws"); + +// Listen for the connection open event +socket.addEventListener("open", () => { + // Inform the main thread + postMessage({ type: "status", data: "Connected to WebSocket server!" }); + // Optionally send an initial message + socket.send(JSON.stringify({ message: "Hello, server!" })); +}); + +// Listen for messages from the WebSocket server +socket.addEventListener("message", (event) => { + postMessage({ type: "message", data: event.data }); +}); + +// Listen for the connection close event +socket.addEventListener("close", () => { + postMessage({ type: "status", data: "Disconnected from WebSocket server." }); +}); + +// Listen for WebSocket errors +socket.addEventListener("error", (error) => { + postMessage({ type: "error", data: error }); +}); + +// Listen for messages from the main thread +self.addEventListener("message", (event) => { + const { action, payload } = event.data; + + if (action === "send") { + // Send a message through the WebSocket if it's open + if (socket.readyState === WebSocket.OPEN) { + socket.send(JSON.stringify(payload)); + } else { + postMessage({ type: "error", data: "WebSocket is not open." }); + } + } else if (action === "close") { + // Optionally send a close notification then close the socket + socket.send(JSON.stringify({ action: "close" })); + socket.close(); + } +}); \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..11cca9c --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "chess-rust-and-javascript", + "version": "1.0.0", + "description": "Chess made with JavaScript and Rust", + "scripts": { + "serve": "cd ./backend && cargo run", + "build:backend": "cd ./backend && cargo build", + "docker:up": "cd ./backend && docker-compose up -d postgres", + "docker:build": "cd ./backend && docker-compose up --build", + "docker:kill": "cd ./backend && docker rm -f $(docker ps -aq)" + }, + "author": "Timothy White", + "license": "MIT" +}