A full-stack crossword game application featuring a C backend for dynamic puzzle generation, a Node.js/Express.js server, and a vanilla HTML/CSS/JS frontend for gameplay.
- Dynamic Crossword Generation: The C backend algorithmically generates a new, random crossword puzzle for each game.
- User Authentication: Players can sign up and log in to track their scores and progress.
- Interactive Grid: Play the game in your browser with an interactive grid and clue lists.
- 1v1 Multiplayer Mode: Compete against another player in a real-time match.
- Scoring & Answer Checking: Your answers are checked in real-time, and a score is maintained.
- High Score Leaderboard: The game saves high scores to a PostgreSQL database.
This project is a full-stack web application that allows users to play dynamically generated crossword puzzles, either solo or against another player in a real-time 1v1 match.
The architecture consists of three main parts:
- Frontend: A responsive, browser-based client built with vanilla HTML, CSS, and JavaScript that handles user interaction, renders the crossword grid, and communicates with the backend via WebSockets.
- Backend: A Node.js server using the Express.js framework. It manages user authentication, game state, scoring, and real-time multiplayer functionality.
- Puzzle Generator: A high-performance C program that is executed by the Node.js server to generate unique crossword puzzles on demand from a given word list.
The server-side logic is centered around three key areas:
To ensure fast and unique puzzle creation, the core generation logic is written in C and executed as a child process by the Node.js server. The algorithm used is a form of recursive backtracking:
- Initialization: A grid of a specified size is created, and a word list is loaded from
data/words.txt. - First Word Placement: A random word is selected and placed horizontally or vertically near the center of the grid.
- Iterative Intersection: The algorithm then iterates through the letters of the words already on the grid, treating them as potential intersection points.
- Word Fitting: For each intersection point, it searches for a new, unused random word that contains the intersecting letter. It then attempts to place this new word on the grid, crossing the existing word.
- Validation & Backtracking: If a new word can be placed without overlapping incorrectly with other words or going out of bounds, it is added to the grid. If it cannot be placed, the algorithm "backtracks" by trying a different random word or moving to a different intersection point.
- Completion: This process repeats until the grid is reasonably full or a target number of words has been placed. The final grid and clues are then serialized to a JSON string and returned to the Node.js server.
The real-time matchmaking system uses a combination of a PostgreSQL database and Socket.IO to manage player state:
- Player Queue: When a player requests a 1v1 match, a record is created for them in the
playerstable in the database, and their status is marked asis_waiting = true. - Opponent Search: The server queries the database for any other player who is also in a waiting state.
- Match Creation: If a waiting opponent is found, a new game record is created in the
active_1v1table, linking the two players. Both players' statuses are updated tois_waiting = false. - Game Start: The server then generates a new puzzle and emits a
match-foundevent via Socket.IO to the two matched players, starting the game.
- Socket.IO Rooms: When a match is created, both players are joined to a private Socket.IO room specific to their game. This ensures that events like
player-finishedandgame-overare only broadcast to the relevant participants. - Database as Single Source of Truth: The database maintains the definitive state of all active and completed games. This allows the system to be resilient to temporary disconnects, as a player can reconnect, rejoin their game, and have their state restored based on the data stored in the database.
This section provides a more detailed breakdown of each component of the application.
The frontend is a client-side application built with vanilla HTML, CSS, and JavaScript. It is responsible for all user-facing presentation and interaction.
- Responsibilities:
- UI Rendering: Dynamically renders the crossword grid, clue lists, forms, and modals based on data received from the server.
- User Input: Captures all user input, including filling out the crossword, clicking buttons (Login, Forfeit, Finish Game), and submitting forms.
- State Management: Manages client-side state, such as the game timer and the content of input fields.
- Server Communication: Interacts with the backend through two methods:
- REST API Calls: Uses
fetchfor stateless actions like user signup, login, and fetching leaderboard data. - WebSockets (
Socket.IO): Establishes a persistent, real-time connection for multiplayer gameplay, including finding a match, receiving game data, and getting live updates about the opponent.
- REST API Calls: Uses
The Node.js backend, built with the Express.js framework, acts as the central nervous system of the application, connecting all other parts.
-
Key Responsibilities:
- Orchestration: Manages the overall flow of the application, from user authentication to initiating the C-based puzzle generator.
- Business Logic: Handles all game logic, such as checking answers, calculating scores, and determining the winner of a 1v1 match.
- Real-time Communication: Manages the WebSocket server, handling all multiplayer events and broadcasting state changes to the appropriate clients.
-
API Endpoints & Connections:
POST /api/signup: Creates a new user account.POST /api/login: Authenticates a user and creates a session.GET /api/generateCrossword: Executes the C backend to generate a new puzzle for single-player mode.POST /api/endGame: Saves a player's score and stats after a single-player game.GET /api/leaderboard: Retrieves the top player scores.- WebSocket Events:
find-1v1-match: A client signals it is ready to play.match-found: The server notifies two clients that they have been matched and provides the game data.player-finished: A client informs the server of their final score and time.game-over: The server announces the final results of the match to both players.player-forfeit: A client voluntarily forfeits the match.
-
Key Node.js Dependencies: | Dependency | Purpose | | :--- | :--- | |
express| Core web server framework for handling routes and middleware. | |pg| PostgreSQL client for connecting to and querying the database. | |socket.io| Enables real-time, bidirectional communication for multiplayer features. | |bcryptjs| Hashes and verifies user passwords securely. | |express-session| Manages user sessions and login state. | |connect-pg-simple| Stores session data directly in the PostgreSQL database. |
The database is the application's persistent storage layer and acts as the single source of truth for all user and game data.
- Key Tables & Views:
users: Stores user account information, including username, hashed password, and aggregate stats (total score, highscore, wins, losses, games played).player_games: Records the results of every completed single-player game, linked to a user viauser_id.players: A temporary table to manage the state of users who are currently searching for or playing a 1v1 match.active_1v1: Stores the state of all ongoing 1v1 matches, including the players involved and their real-time scores.leaderboard(View): A pre-sorted view of theuserstable, providing a ranked list of players by theirhighscore.
This is a standalone C program responsible for the CPU-intensive task of generating crossword puzzles. It is designed for performance and is called by the Node.js server.
- Data Structures:
- Grid Representation: A 2D character array (
char grid[HEIGHT][WIDTH]) is used to represent the crossword grid.- Why: This provides O(1) (constant time) access to any cell on the grid, which is essential for the algorithm's core logic of checking for valid placement and finding intersections. It is simple, memory-efficient, and fast.
- Word Storage: A dynamic array of strings (
char* words[]) holds the word list loaded from thewords.txtfile.- Why: A dynamic array allows the program to handle a word list of any size without a hardcoded limit.
- Clue Management: A struct is used to model each word placed on the grid. This struct likely contains the word string, its starting (row, col), its direction (Across/Down), and its clue number. A dynamic array of these structs is used to build the list of clues that gets returned to the Node.js server.
- Why: Using a struct logically groups all the related data for a single clue, making the code cleaner and easier to manage. An array of these structs is a straightforward way to manage the collection of all placed words.
- Grid Representation: A 2D character array (
- Backend: C (for puzzle generation), Node.js, Express.js, Socket.IO,
bcryptjsfor password hashing. - Frontend: HTML, CSS, Vanilla JavaScript
- Database: PostgreSQL
Follow these steps to get the application running on your local machine.
- A C compiler (like GCC or an equivalent toolchain).
- Node.js and npm.
- A running PostgreSQL server.
- Connect to your PostgreSQL server and create a database. For example:
CREATE DATABASE crossword_db;
- Connect to the new database (e.g., using
psql crossword_dbor a GUI tool). - Create the tables by running the SQL commands found in
backend/database.sql.
- In the root of the project, create a new file named
.env. - Add the following lines to the
.envfile, replacing the placeholder values with your local database connection string and a random secret.Note: For local development, you also need to installDATABASE_URL="postgresql://YOUR_USER:YOUR_PASSWORD@localhost:5432/crossword_db" SESSION_SECRET="a_long_random_string_for_sessions" ADMIN_PASSWORD_HASH="$2a$10$e/O/iTDO9RIhtvFJh5vLHe827cgfZJD/rT7K1blhPHv6zP55HRuAe"dotenv. Runnpm install dotenv.
Navigate to the project root directory and run:
# Install Node.js packages
npm install
# Compile the C backend
gcc backend/main.c -o backend/mainFrom the project root, start the server:
node server.jsThen, open your web browser and go to: http://localhost:3000
This application is configured for deployment on Render using its Git-based workflow. When you push changes to your connected GitHub repository, Render will automatically build and deploy the new version of your application.
Before creating the application server, set up the database first.
- From your Render dashboard, click New > PostgreSQL.
- Give it a unique name (e.g.,
crossword-db) and choose a region. - Click Create Database.
- Once it's created, go to the database's page and look for the "Internal Connection String". Copy this URL; you will need it for the
DATABASE_URLenvironment variable later.
This is the Node.js application server.
- From your Render dashboard, click New > Web Service.
- Connect the GitHub repository you forked for this project.
- In the settings form, enter the following:
- Name: Give your app a name (e.g.,
crossword-app). - Build Command:
./render-build.sh- Why? This script is crucial because it installs the C compiler (
build-essential) needed to compile yourbackend/main.cfile before starting the server.
- Why? This script is crucial because it installs the C compiler (
- Start Command:
node server.js- Why? This is the command that runs your main Node.js application after the build is complete.
- Name: Give your app a name (e.g.,
Before the first build, you must add your project's secrets. Go to the Environment tab for your newly created Web Service and add the following variables:
DATABASE_URL: Paste the "Internal Connection String" you copied from your Render PostgreSQL database in Step 1.SESSION_SECRET: A long, random string for session security. You can create one locally usingopenssl rand -hex 32or use an online generator.ADMIN_PASSWORD_HASH: The bcrypt hash for the admin login. You can use the default one from the.envexample or generate a new one.
Click the Create Web Service button. The initial build will take a few minutes. Once deployed, your application will be live at the URL provided by Render. You can monitor the build and server logs from the Render dashboard to ensure everything starts correctly.
bcryptvsbcryptjs: This project usesbcryptjs(the pure JavaScript version) instead ofbcrypt(the native C++ version). Whilebcryptis faster, it requires a native compiler toolchain in the deployment environment. Early deployment attempts failed because of issues with installing this native dependency on Render. Switching tobcryptjsremoves this build complexity, leading to a much more reliable deployment process at a negligible performance cost for this application's scale.
crossword_project/
├── backend/
│ ├── main.c # C code for crossword generation
│ ├── database.sql # Database schema
│ └── debug.bat # Windows script to compile and run
├── data/
│ └── words.txt # Word list for the generator
├── frontend/
│ ├── css/
│ │ └── style.css # All styles for the application
│ ├── js/
│ │ ├── *.js # Various frontend JavaScript files
│ ├── admin/
│ │ └── *.html # Admin panel pages
│ └── *.html # Main frontend pages
├── node_modules/ # Project dependencies
├── .env.example # Example environment file
├── package.json # Project metadata and dependencies
├── render-build.sh # Build script for Render
└── server.js # Node.js server