Skip to content

Navigation Menu

Sign in
Sign up

Latest commit

History

21 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Ironclad Command

Tactical 3v3 Mech Squad Warfare

A real-time, turn-based multiplayer tactical combat game built from scratch — featuring live matchmaking, server-authoritative combat resolution, a competitive ELO ranking system, and a full player progression system.

Year 2104. In the fallout of the Mechanical Schism, rogue commanders duel in high-stakes tactical combat arenas. Command an elite 3-mech squad across a 5x5 tactical grid and prove your dominance.


Table of Contents

  1. Overview
  2. Features
  3. Tech Stack
  4. Game Rules & Gameplay
  5. How to Play
  6. Folder Structure
  7. Local Setup
  8. Environment Variables
  9. Deployment
  10. Development Journey
  11. Bugs Faced & How They Were Fixed
  12. Ideation & Design Decisions
  13. Future Scope

Overview

Ironclad Command is a full-stack, real-time multiplayer web game. Two players are matched automatically via a live queue, determine turn order through a transparent number-guessing toss, then battle head-to-head on a 5x5 tactical grid using three mech classes with distinct roles. All game logic — movement validation, combat resolution, turn management — is enforced entirely on the server to prevent cheating. Match results feed into a persistent ELO rating system, a global leaderboard, and detailed player profiles.

This project was built end-to-end: database design, backend API and WebSocket architecture, and a complete React frontend with a custom cyberpunk visual theme.


Features

  • Account system — signup/login with hashed passwords (bcrypt) and persistent sessions (refreshing the page keeps you logged in)
  • Real-time matchmaking — Socket.io-based queue that pairs players live, with stale-connection cleanup so cancelled or disconnected searches never leave "ghost" opponents
  • Pre-match toss — both players guess a number 1-100; a randomly generated target number determines who wins the guess and goes first; both guesses are revealed to both players for full transparency
  • Turn-based tactical combat — 5x5 grid, 3 Action Points per turn, movement (1 AP) and attacks (2 AP), auto-end-turn when AP is exhausted
  • Three mech classes:
    • Assault — high HP, melee/short-range
    • Sniper — low HP, long-range, high damage
    • Support — balanced stats
  • Server-authoritative state — every move and attack is validated server-side (turn ownership, range, adjacency, occupied cells) so the client cannot cheat
  • ELO rating system — standard chess-style ELO (K=32) recalculated after every match and persisted to the database
  • Global leaderboard — ranked by ELO, showing win rate and matches played, with the current player's row highlighted
  • Detailed player profiles — ELO, global rank, win rate, loss rate, matches played, and current win streak (calculated from match history)
  • Victory/Defeat screen — shows the winner, ELO change (+/-), and a "Play Again" flow that returns safely to the home dashboard
  • Fully responsive dark cyberpunk UI — custom color palette, inline SVG unit icons, live health bars, turn indicators, and move/attack range highlighting

Tech Stack

Frontend

  • React (Vite)
  • Tailwind CSS
  • Socket.io-client

Backend

  • Node.js + Express
  • Socket.io (WebSockets)
  • PostgreSQL (hosted on Neon.tech)
  • bcryptjs (password hashing)
  • express-session (session management)

Infrastructure

  • Database: Neon.tech (serverless PostgreSQL)
  • Backend hosting: Render
  • Frontend hosting: Vercel

Game Rules & Gameplay

Squad Composition

Each player commands 3 mechs, one of each class:

Class HP Attack Range Role
Assault 100 35 1 (melee) Tank / front line
Sniper 60 45 3 (long-range) High damage, fragile
Support 80 20 2 (mid-range) Balanced

Turn Structure

  • Each turn grants 3 Action Points (AP)
  • Move: 1 AP per tile (adjacent, unoccupied cells only)
  • Attack: 2 AP (target must be an enemy unit within the attacker's range, calculated via Manhattan distance)
  • Turn automatically ends when AP reaches 0, or a player can manually end their turn early
  • Victory is achieved when all 3 of the opponent's units reach 0 HP

The Toss

Before battle begins, both players privately guess a number between 1 and 100. The server generates a random target number. Whoever's guess is closest to the target goes first. In the event of a tie, the first turn is assigned randomly. Both players' guesses and the target number are revealed to both sides after the toss, ensuring the process is transparent and verifiable.

ELO System

Ratings start at 1200. After each match, both players' ratings are recalculated using the standard ELO formula with K=32:

Expected(A) = 1 / (1 + 10^((RatingB - RatingA) / 400)) NewRating(A) = RatingA + K * (ActualScore(A) - Expected(A))


How to Play

  1. Sign up or log in with a username and password
  2. From the Home dashboard, view your stats or click Find Match
  3. Wait in the matchmaking queue — you'll be paired with the next available opponent
  4. Guess a number (1-100) during the toss phase to determine who moves first
  5. On your turn: click one of your mechs to select it
    • Green-ringed cells show valid moves (click to move, costs 1 AP)
    • Amber-ringed cells show enemies in attack range (click to attack, costs 2 AP)
  6. Turn passes automatically when your AP hits 0, or click End Turn manually
  7. Destroy all 3 enemy mechs to win
  8. View your ELO change on the results screen, then Play Again or check the Leaderboard / My Profile

Folder Structure

ironclad-command/
├── backend/
│ ├── config/
│ │ └── db.js # PostgreSQL connection pool
│ ├── routes/
│ │ └── auth.js # Signup, login, session, leaderboard, profile endpoints
│ ├── sockets/
│ │ └── gameSocket.js # Matchmaking, toss, combat, turn logic
│ ├── utils/
│ │ └── elo.js # ELO rating calculation
│ ├── .env # Environment variables (not committed)
│ ├── schema.sql # Database schema
│ └── server.js # Express + Socket.io entry point
├── client/
│ ├── src/
│ │ ├── api/
│ │ │ └── auth.js # Frontend API helper functions
│ │ ├── components/
│ │ │ ├── AuthScreen.jsx
│ │ │ ├── HomeScreen.jsx
│ │ │ ├── MatchmakingScreen.jsx
│ │ │ ├── TossScreen.jsx
│ │ │ ├── BattleGrid.jsx
│ │ │ ├── LeaderboardScreen.jsx
│ │ │ └── ProfileScreen.jsx
│ │ ├── App.jsx # Screen routing / state machine
│ │ ├── index.css # Tailwind + global styles
│ │ └── socket.js # Socket.io client singleton
│ └── .env # Environment variables (not committed)
└── README.md

Local Setup

Prerequisites

  • Node.js (v18+ recommended)
  • A PostgreSQL database (e.g., a free Neon.tech project)

1. Clone the repository

git clone <your-repo-url>
cd ironclad-command

2. Backend setup

cd backend
npm install

Create a .env file in backend/ (see Environment Variables below).

Run the schema against your PostgreSQL database (paste schema.sql into your database's SQL editor, or use psql).

Start the backend:

node server.js

The server runs on http://localhost:5000 by default.

3. Frontend setup

Open a new terminal:

cd client
npm install

Create a .env file in client/ (see below).

Start the frontend:

npm run dev

The app runs on http://localhost:5173 by default.

4. Play

Open two browser windows (one normal, one incognito) at http://localhost:5173, sign up two accounts, and find a match against yourself to test.


Environment Variables

backend/.env

PORT=5000 NODE_ENV=development DATABASE_URL=postgresql://:@/?sslmode=require JWT_SECRET=your_secret_key_here SESSION_SECRET=your_session_secret_here

client/.env

VITE_SOCKET_URL=http://localhost:5000 VITE_API_URL=http://localhost:5000/api


Deployment

Layer Platform
Database Neon.tech (serverless PostgreSQL)
Backend (Express + Socket.io) Render
Frontend (React/Vite) Vercel

In production, client/.env points VITE_SOCKET_URL and VITE_API_URL at the deployed Render backend URL, and the backend's CORS configuration is updated to allow the deployed Vercel frontend origin.


Development Journey

This project was built incrementally, commit by commit, following a deliberately modular approach to catch bugs early rather than debugging a large tangle of code at the end:

  1. Project scaffolding and Git setup
  2. Backend server foundation (Express + Socket.io skeleton)
  3. Database schema design and ELO calculator (unit tested in isolation)
  4. PostgreSQL connection to Neon, verified with a dedicated test route before building on top of it
  5. Authentication routes (signup/login) with bcrypt password hashing, tested via curl before frontend integration
  6. Socket.io game logic (matchmaking, movement, combat, ELO resolution), tested with a standalone Socket.io client script before touching the UI
  7. React + Vite + Tailwind frontend scaffolding, with a color-palette sanity check before building real screens
  8. Login/Signup screen wired to the backend
  9. Real-time matchmaking screen
  10. The full tactical battle grid — movement, attacks, health bars, turn HUD
  11. Session persistence (so refreshing doesn't log you out) and a proper Home dashboard
  12. Global leaderboard and detailed player profile screens
  13. UI/UX bug fixes (see below)
  14. Pre-match toss feature for determining first turn

At each stage, changes were verified with real output (server logs, curl responses, or live two-browser-window tests) before moving forward, rather than trusting that generated code worked without proof.


Bugs Faced & How They Were Fixed

Building this project surfaced a number of real-world engineering problems:

Windows file encoding corruption Early file-creation commands (echo. > file.js in PowerShell) silently wrote UTF-16 BOM-prefixed files, causing SyntaxError: Invalid or unexpected token on the very first line. Fixed by explicitly deleting and recreating files with clean UTF-8 encoding rather than patching around the corruption.

Accidentally committed node_modules An early commit tracked the entire node_modules directory before .gitignore was correctly in place. Fixed with git rm -r --cached to untrack the folder without deleting it locally, followed by a clean .gitignore.

CORS blocking all API requests The frontend (localhost:5173, Vite's default port) was rejected by a backend CORS policy hardcoded to localhost:3000. Fixed by aligning the CORS origin. A second, subtler issue followed: requests using credentials: 'include' (required for session cookies) were still blocked because the server's CORS config didn't set credentials: true — browsers require both sides to explicitly agree to credentialed cross-origin requests.

Neon/PostgreSQL SSL handshake failure pg repeatedly threw "The server does not support SSL connections" despite a correctly formatted connection string. Root-caused through a bare-bones isolated connection script (bypassing Express entirely) to rule out application-level bugs. The fix required switching from Neon's pooled connection string to its direct connection string, simplifying the ssl option to 'require', and removing the channel_binding=require parameter, which an older pg version didn't handle correctly.

A poisoned terminal environment variable A one-off set DATABASE_URL=... command in a Windows terminal silently overrode the .env file's value for the rest of that terminal session (environment variables take priority over .env by default), causing every subsequent fix to appear not to work. Diagnosed by testing in a completely fresh terminal window.

Data shape mismatches between backend and frontend The initial battle grid component assumed a nested unit.position.x/y and unit.ownerUserId shape, and expected STATE_UPDATED events to arrive wrapped in a .gameState property. The actual backend sent flat unit.x/y, unit.owner, and an unwrapped game object. Fixed by cross-referencing the actual Socket.io emit calls in the backend against the frontend's assumptions.

"Play Again" pairing players with a ghost opponent After finishing a match and clicking Play Again, players were sometimes matched into a broken game with no real opponent. Root cause: React's StrictMode (enabled by default in Vite's React template) intentionally double-invokes effects in development, causing FIND_MATCH/CANCEL_MATCH Socket.io events to fire twice in rapid succession and corrupt the shared in-memory matchmaking queue. Fixed by removing StrictMode and hardening the backend queue-cleanup logic to filter out stale or duplicate entries before matching.

White page background bleeding through on scroll Pages taller than the viewport (e.g., the leaderboard) revealed the browser's default white background below the themed content, because only individual components set a dark background rather than the root HTML/body. Fixed with global html, body, and #root background rules in the global stylesheet.

Incomplete toss feature implementation An early attempt to add the pre-match toss feature resulted in mismatched event names between frontend and backend (SUBMIT_GUESS vs SUBMIT_TOSS), a completely different Rock-Paper-Scissors-style mechanic being built instead of the intended number-guessing toss, and a JSX syntax error from a stray character. Fixed by discarding the incorrect implementation and rebuilding all four affected files from an exact, verified specification, confirming each file's contents individually before integration testing.


Ideation & Design Decisions

  • Server-authoritative architecture: all combat and movement rules are validated on the server, not the client, so a modified client cannot cheat — a deliberate choice mirroring how real multiplayer games are built.
  • In-memory game state: active matches are held in server memory rather than the database for performance (a database write on every single move/attack would add unnecessary latency); only final match results are persisted.
  • ELO over a simpler win/loss counter: chosen to give matches long-term competitive stakes and make the leaderboard meaningful, rather than just counting wins.
  • The toss mechanic: added specifically to remove any perceived unfairness in who gets the first-move advantage, while keeping the mechanism transparent (both guesses are always revealed) rather than a hidden coin-flip.
  • Screen-based state machine in the frontend (home / matchmaking / toss / battle / gameover / leaderboard / profile) rather than a routing library, since the entire app is a single continuous session flow with no need for shareable URLs per screen.

Future Scope

Deck-Building System

Players select a custom 3-unit deck from a pool of 6 unit types before matchmaking, adding a strategic layer before combat even begins.

Board expansion: grid size increases from 5x5 to 11x11 to accommodate the larger unit roster and more tactical positioning. Fixed spawn columns per squad (e.g., columns 1, 6, and 11).

New unit types (alongside the existing Assault, Sniper, Support):

  • Doctor — no combat power; can move and heal. Each heal restores 50% of a target's max HP, limited to 3 heals total before becoming useless. If an ally is fully dead, reviving it requires 2 consecutive heals in a row and brings it back at full health only (cannot revive to half health).
  • Protector — absorbs the first 3 attacks directed at the team and dies after; while alive, the Protector's ability determines whether other units can be targeted at all (exact targeting rules to be finalized).
  • Sniper (revised) — retains long range but is limited to 6 total shots; once out of ammo, can only make short-range attacks at reduced damage.
  • Suicide Bomber — detonates on command, dealing damage to any unit (ally or enemy) directly adjacent (up/down/left/right), then is destroyed itself.

Tie/draw resolution: a formal draw condition for cases with no clear winner (e.g., mutual destruction from a bomber's blast, or a stalemate between units with no offensive capability), including an ELO adjustment formula for drawn matches.

Disconnect Handling Currently, if a player disconnects or closes their tab mid-match, their opponent is left stuck in an unresponsive game with no way to proceed. Planned fix: detect mid-game disconnects via Socket.io's disconnect event, start a short grace-period timer (e.g., 30-60 seconds) to allow for reconnection, and if the player doesn't return in time, automatically award the win to the remaining player, update their ELO/stats accordingly, and clean up the abandoned match from server memory.

Other Future Ideas

  • Spectator mode for watching ongoing matches
  • Match history log with replay of past games
  • Matchmaking by ELO range (currently first-come-first-served)
  • Mobile-responsive layout improvements
  • In-game chat between matched players
  • Rate limiting and additional server-side abuse protection ahead of public deployment

Author

Built by Mridul Jha(GitHub: Loki-Snape)

About

A real-time, turn-based multiplayer tactical combat game built from scratch featuring live matchmaking, server-authoritative combat resolution, a competitive ELO ranking system, and a full player progression system.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

AltStyle によって変換されたページ (->オリジナル) /