Snakes & Ladders - Backend Review Blog
A technical deep-dive into the backend architecture, API endpoints, database design, deployment pipeline, and College Board alignment for our Snakes & Ladders educational game.
- Backend Team
- System Architecture Overview
- App Routes & Blueprint Registration
- API Endpoints
- How the Backend Works
- Authentication Flow
- Game Progression Pipeline
- Real-Time Multiplayer (Socket.IO)
- Boss Battle (Co-op, up to 10 players)
- PvP Arena (1v1 Competitive)
- SlitherRush (32-player Snake Arena)
- Database Design
- User Management
- Registration & Login
- Role-Based Access
- Profile Management
- Big Idea 3: Input
- Big Idea 3: Persistent Data Storage
- Big Idea 3: Sequencing
- Big Idea 3: Selection
- Big Idea 3: Iteration
- Big Idea 3: Lists / Collections
- Big Idea 3: Procedures
- Big Idea 3: Algorithms
- Big Idea 4: The Internet
- Big Idea 5: Impact of Computing
Backend Team
Moiz led deployment, Docker/Nginx configuration, JWT authentication, CORS, and production environment setup. Arnav assisted with backend API development, database models for boss battles, and server-side game logic.
Deployed at: snakes.opencodingsociety.com
System Architecture Overview
GitHub Pages
Port 8306
WebSockets (eventlet)
SQLite / MySQL
Gunicorn (eventlet worker)
Jekyll Frontend — This is what the user sees. Jekyll is a static site generator that turns our HTML/CSS/JS files into a website hosted for free on GitHub Pages. It has zero backend logic — it just sends requests to our server.
Flask REST API — Flask is a Python web framework. "REST API" means our server exposes URLs (like
/api/snakes/leaderboard) that the frontend can call to read or write data. Think of it like a waiter: the frontend places an order (request), Flask processes it, and sends back the result (response) as JSON.Socket.IO (WebSockets) — Normal HTTP is one-way: the frontend asks, the server answers. WebSockets open a two-way connection that stays open, so the server can push updates to players instantly. This is how multiplayer works — when Player A moves, the server immediately tells Player B without Player B having to ask. Eventlet is the async engine that lets one server handle many simultaneous socket connections.
SQLAlchemy ORM — ORM stands for Object-Relational Mapping. Instead of writing raw SQL queries like
SELECT * FROM users WHERE id=5, we write Python: User.query.filter_by(id=5).first(). SQLAlchemy translates our Python classes into database tables automatically.Docker + Nginx — Docker packages our entire app (code + Python + all dependencies) into a container, so it runs identically on any machine. Nginx is a web server that sits in front of our app — when someone visits
snakes.opencodingsociety.com, Nginx receives the request and forwards it to our Flask container on port 8306. Gunicorn is the production-grade server that actually runs our Flask code inside the container.
App Routes & Blueprint Registration
/api/snakes/leaderboard, Flask looks up which function handles that URL and runs it. Blueprints are Flask's way of organizing routes into groups — instead of putting all 20+ endpoints in one file, we split them into logical modules (game routes, admin routes, user routes, etc.) and register each blueprint with the main app.
# main.py — Blueprint registration
# Each line connects a group of URL routes to the main app
app.register_blueprint(snakes_game_api) # /api/snakes — core game CRUD
app.register_blueprint(snakes_bp) # /api/snakes — extended endpoints
app.register_blueprint(admin_api) # /api/admin — admin dashboard
app.register_blueprint(boss_api) # /api/boss — boss battle rooms
app.register_blueprint(game_api) # /api/game — game progress
app.register_blueprint(user_api) # /api/user — user management
register_blueprint() call takes all the routes defined in a separate file and attaches them to the main Flask app. For example, snakes_game_api is defined in api/snakes_game.py — it contains all the game endpoints like leaderboard, champions, etc. When the app starts, Flask knows: "if a request comes in for /api/snakes/leaderboard, run the function in the snakes_game_api blueprint." This keeps our code modular — each file handles one responsibility.
| Route Pattern | Purpose |
|---|---|
/api/snakes/* |
All game state, leaderboard, progress, completion |
/api/admin/* |
Admin dashboard, player management |
/api/boss/* |
Boss room creation and joining |
/api/user/* |
Registration, login, profile CRUD |
/api/authenticate |
JWT token generation |
/api/health |
Health check for monitoring |
API Endpoints
fetch() in JavaScript. The four main HTTP methods are:- GET = Read data (like loading your game progress)
- POST = Create or submit data (like answering a question)
- PUT = Update existing data (like autosaving your position)
- DELETE = Remove data (like deleting a game record)
Game State Endpoints (/api/snakes/)
| Endpoint | Method | Purpose | Payload / Params |
|---|---|---|---|
/api/snakes/ | GET | Load current user's game progress | — |
/api/snakes/ | POST | Create new game record | {username, selected_character} |
/api/snakes/ | PUT | Autosave full game state (every 10s) | {current_square, lives, total_bullets, time_played, ...} |
/api/snakes/ | DELETE | Delete game record | — |
/api/snakes/progress | GET | Get progress (auto-creates if missing) | — |
/api/snakes/complete-lesson | POST | Mark lesson complete, award bullets | {lesson_number, bullets_earned} |
/api/snakes/answer-question | POST | Record answer, update position | {square, bullets_earned, correct} |
/api/snakes/update-square | POST | Update board position | {square} |
/api/snakes/add-bullets | POST | Award bullets to player | {bullets} |
/api/snakes/complete | POST | Mark game as completed | — |
/api/snakes/reset | POST | Full reset (preserves champion status) | — |
/api/snakes/leaderboard | GET | Top 10 players by bullets | ?limit=10 |
/api/snakes/champions | GET | All game completers (Hall of Champions) | — |
/api/snakes/active-players | GET | Players updated in last 10s | — |
/api/snakes/unvisited-squares | GET | List of unvisited question squares | — |
fetch('https://snakes.opencodingsociety.com/api/snakes/complete-lesson', { method: 'POST', body: JSON.stringify({lesson_number: 3, bullets_earned: 10}) }). The Flask server receives this, finds the player's database record, adds lesson 3 to their completed_lessons list, adds 10 to their bullet count, checks if all 5 lessons are done (and if so, unlocks the next board section), saves to the database, and sends back the updated state as JSON.
Admin Endpoints (/api/admin/)
| Endpoint | Method | Purpose |
|---|---|---|
/api/admin/dashboard | GET | Overview stats — total users, players, bullets, time, boss stats |
/api/admin/players | GET | List all active players with progress details |
/api/admin/users | GET POST | User management — list all, create new |
/api/admin/user/<uid> | PUT DELETE | Edit or delete individual user |
User & Auth Endpoints
| Endpoint | Method | Purpose |
|---|---|---|
/api/authenticate | POST | Login — returns JWT token in HttpOnly cookie |
/api/user/ | POST | Register new user |
/api/user/ | GET | Get current user profile |
/api/user/ | PUT | Update user profile |
How the Backend Works
Authentication Flow
# api/jwt_authorize.py — Token decorator
def token_required():
def decorator(f):
@wraps(f)
def decorated(*args, **kwargs):
token = request.cookies.get('jwt')
data = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=['HS256'])
g.current_user = User.query.filter_by(_uid=data['_uid']).first()
return f(*args, **kwargs)
return decorated
return decorator
token = request.cookies.get('jwt') — Grabs the JWT token from the browser's cookies that were sent with the request.
jwt.decode(token, SECRET_KEY, algorithms=['HS256']) — Decodes (decrypts) the token using our secret key. HS256 is the encryption algorithm. If someone tampered with the token, this line would fail and reject the request.
g.current_user = User.query.filter_by(_uid=data['_uid']).first() — Uses the user ID from inside the token to look up the full user record from the database, then stores it in Flask's g object so any endpoint function can access g.current_user.
The decorator pattern: @token_required() is placed above any endpoint function that needs authentication. It runs this validation code before the endpoint's actual logic — like a security guard checking your ID before letting you into a building.
Game Progression Pipeline
POST /complete-lesson
POST /answer-question
WebSocket events
unlocked_sections list starts as ['half1'] — they can only access the first part of the board (lessons). After completing all 5 lessons, the server appends 'half2' to the list, unlocking the question section. After reaching square 56, 'boss' gets appended, unlocking battle modes. The frontend checks this list to show/hide sections — but critically, the backend also validates every request, so even if someone tried to hack the frontend to skip ahead, the server would reject it.
Real-Time Multiplayer (Socket.IO)
Three multiplayer modes run over WebSocket connections:
Boss Battle (Co-op, up to 10 players)
boss_join_lobby→ Pre-battle chat roomboss_join_room→ Enter arena (server allocates spawn)boss_player_move→ Position broadcast (50ms interval)boss_shoot→ Fire bullet, server validates collisionboss_defeated→ Aggregate all player stats- Powerups spawn every 5s (server-controlled rate limiting)
PvP Arena (1v1 Competitive)
pvp_join→ Auto-matchmaking viaget_or_create_open_room()pvp_ready→ Dual-ready confirmation systempvp_battle_start→ Server starts the match- Server-authoritative collision:
resolve_player_collision() - Tab-away detection + position authority corrections
SlitherRush (32-player Snake Arena)
slitherrush_join→ Arena auto-assignment (max 32)slitherrush_input→ Direction + shoot- 30Hz server tick loop — full simulation server-side
- State snapshots emitted at 15fps
- Leaderboard broadcasts every 450ms
- Party system groups friends into same arena
WebSocket JWT auth — Socket connections authenticate via cookies using _resolve_socket_user():
# socketio_handlers/slitherrush_events.py
def _resolve_socket_user():
token = request.cookies.get(current_app.config.get('JWT_TOKEN_NAME', 'jwt'))
if not token: return None
decoded = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=['HS256'])
return User.query.filter_by(_uid=decoded.get('_uid')).first()
None (the player connects as a guest). This is how the server knows who is sending each boss_shoot or slitherrush_input event.
Database Design
SnakesGameData Model
# model/snakes_game.py — Core game state per user
class SnakesGameData(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), unique=True)
username = db.Column(db.String(255))
total_bullets = db.Column(db.Integer, default=0)
current_square = db.Column(db.Integer, default=1)
lives = db.Column(db.Integer, default=5)
time_played = db.Column(db.Float, default=0.0)
selected_character = db.Column(db.String(50))
visited_squares = db.Column(MutableList)
completed_lessons = db.Column(MutableList)
unlocked_sections = db.Column(MutableList)
boss_battle_attempts = db.Column(db.Integer, default=0)
game_status = db.Column(db.String(20), default='active')
completed_at = db.Column(db.DateTime, nullable=True)
last_updated = db.Column(db.DateTime)
id / user_id — Unique identifiers. user_id links to the Users table (ForeignKey) and is unique=True so each user gets exactly one game record.
total_bullets — Currency earned from lessons and correct answers. Carries over to boss battle as ammo.
current_square — Player's position on the board (1–56). Defaults to square 1.
visited_squares — A JSON list like [7, 12, 23, 34] tracking which question squares the player has landed on. MutableList means SQLAlchemy detects when we .append() to it and auto-saves the change.
completed_lessons — A JSON list like [1, 2, 3, 4, 5]. When all 5 are present, the server unlocks the next section.
unlocked_sections — Controls what parts of the game the player can access: ['half1'] → ['half1','half2'] → ['half1','half2','boss'].
game_status / completed_at — Flips to 'completed' with a timestamp when the player finishes. Used by the Hall of Champions to list winners in order.
Entity Relationships
- 1:1 (one-to-one): Each User has exactly one SnakesGameData record. One player, one save file.
- 1:N (one-to-many): One SnakesGameData record can have many SquareCompletion records — because one player visits many squares. Similarly, one BossRoom can have many BossPlayers (up to 10 in co-op).
User Management
Registration & Login
POST /api/user/ creates a user with hashed password (bcrypt). POST /api/authenticate validates credentials and returns a JWT token in a secure HttpOnly cookie with SameSite flags.
Role-Based Access
Users have a role field — 'User' or 'Admin'. The @admin_required() decorator chains with @token_required() to verify admin privileges before allowing access to /api/admin/* endpoints.
Profile Management
GET/PUT /api/user/ allows users to view and update their profile. Admins can manage any user via /api/admin/user/<uid> — including password reset and account deletion.
$2b$12$LJ3m4.... Even if someone stole the database, they couldn't reverse the hashes back into passwords. When a user logs in, we hash what they typed and compare it to the stored hash.
Admin Panel
@admin_api.route('/dashboard', methods=['GET'])
@admin_required()
def admin_dashboard():
total_users = User.query.count()
total_players = GameProgress.query.count()
total_bullets = db.session.query(db.func.sum(GameProgress.bullets)).scalar() or 0
total_time = db.session.query(db.func.sum(GameProgress.time_played_minutes)).scalar() or 0
# + boss battle stats, squares completed, etc.
@admin_api.route('/dashboard', methods=['GET']) — This function runs when someone visits /api/admin/dashboard with a GET request.
@admin_required() — Before running, it checks: (1) is the user logged in? (2) is their role 'Admin'? If not, it returns a 403 Forbidden error. Regular players can never access this.
User.query.count() — Counts every row in the Users table (total registered users).
db.func.sum(GameProgress.bullets).scalar() — SQL SUM function — adds up the bullets column across all players. .scalar() returns a single number. The or 0 handles the case where no records exist (returns 0 instead of None).
Admin capabilities:
- View total users, active players, total bullets earned, total time played
- List all player progress records with game state details
- Create, edit, and delete user accounts
- View boss battle statistics (rooms, players, completion rates)
Docker & Deployment
Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt && \
pip install --no-cache-dir gunicorn eventlet
COPY . /app
ENV GUNICORN_CMD_ARGS="--worker-class eventlet --workers=1 \
--bind=0.0.0.0:8306 --timeout=120"
EXPOSE 8306
CMD ["gunicorn", "--worker-class", "eventlet", "-w", "1", \
"--bind", "0.0.0.0:8306", "main:app"]
FROM python:3.11-slim — Start with a minimal Linux image that has Python 3.11 pre-installed. "slim" means no extra tools we don't need, keeping the image small.
WORKDIR /app — Set the working directory inside the container to /app. All following commands run from here.
COPY requirements.txt . then RUN pip install — Copy the dependency list first and install packages. Docker caches this layer — so if we change our code but not our dependencies, it skips reinstalling (much faster rebuilds).
COPY . /app — Copy our actual source code into the container.
--worker-class eventlet --workers=1 — Use eventlet (async) instead of default sync workers. We need exactly 1 worker because Socket.IO keeps state in memory — multiple workers would create separate copies and players couldn't see each other.
--bind=0.0.0.0:8306 — Listen on all network interfaces on port 8306. 0.0.0.0 means "accept connections from anywhere," not just localhost.
CMD ["gunicorn", ...] — The command that runs when the container starts. Gunicorn is a production WSGI server — unlike Flask's built-in dev server, it can handle many concurrent requests reliably.
docker-compose.yml
version: '3'
services:
web:
image: flask_open
build: .
env_file: .env
ports:
- "8306:8306"
volumes:
- ./instance:/app/instance # Persist SQLite DB
restart: unless-stopped
build: . means "build the Dockerfile in the current directory." ports: "8306:8306" maps the container's internal port to the host machine's port so outside traffic can reach it. volumes mounts a folder from the host into the container — this is critical because it keeps the SQLite database file on the host machine, so the data survives even if we rebuild the container. restart: unless-stopped means if the container crashes, Docker automatically restarts it.
Common Docker Commands
# Build the image and start the container in detached (background) mode
docker-compose up --build -d
# Stream live server logs (Ctrl+C to stop watching)
docker-compose logs -f web
# Stop the container, rebuild with new code, and restart
docker-compose down && docker-compose up --build -d
# Open a terminal inside the running container (for debugging)
docker exec -it <container_id> /bin/bash
# List all running containers (find container IDs here)
docker ps
# Rebuild from scratch, ignoring cached layers
docker-compose build --no-cache
# Monitor CPU/memory usage of running containers
docker stats
Nginx Reverse Proxy
snakes.opencodingsociety.com (port 80/443). Nginx sits between the internet and our app — it receives requests on the standard web port and forwards them to Flask on 8306. It also handles SSL (HTTPS), load balancing, and serving static files faster than Python can.
server {
listen 80;
server_name snakes.opencodingsociety.com;
location / {
proxy_pass http://localhost:8306;
proxy_http_version 1.1;
# WebSocket support — these headers tell Nginx to upgrade
# the HTTP connection to a persistent WebSocket connection
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
listen 80 — Nginx listens on port 80 (standard HTTP port).
server_name snakes.opencodingsociety.com — Only handle requests for this domain.
proxy_pass http://localhost:8306 — Forward all requests to Flask running on port 8306.
Upgrade / Connection "upgrade" — These headers are essential for WebSocket. Without them, Socket.IO connections would fail and fall back to slow HTTP polling. These headers tell Nginx: "this isn't a normal request — upgrade it to a persistent two-way connection."
X-Real-IP — Passes the user's real IP address to Flask (otherwise Flask would only see Nginx's local IP).
How Everything Flows (Visual Summary)
Request Lifecycle
snakes.opencodingsociety.com
:8306
Blueprint routing
JWT validation
DB read/write
1. Browser sends
POST /api/snakes/answer-question with {square: 23, correct: true, bullets_earned: 5}2. Nginx receives the HTTPS request at
snakes.opencodingsociety.com and forwards it to localhost:83063. Gunicorn hands the request to the Flask app
4. Flask looks at the URL and matches it to the
answer_question() function in the snakes_bp blueprint5. @token_required runs first — extracts the JWT from the cookie, decodes it, loads the user from the DB
6. SQLAlchemy finds the player's
SnakesGameData record, updates current_square=23, appends 23 to visited_squares, adds 5 to total_bullets, and commits to the database7. JSON Response is sent back:
{"current_square": 23, "total_bullets": 45, "visited_squares": [7,12,18,23], ...}
Multiplayer Data Flow
WASD / Mouse
Collision + bounds
boss_player_move and boss_shoot event via Socket.IO. The server receives these, checks if the new position is within arena bounds, runs collision detection against the boss and other players using resolve_player_collision(), then broadcasts the validated positions to every player in the room. All clients receive this and update their canvas to show Player A's new position and bullet. This happens 20 times per second (50ms intervals).
Section Unlocking Logic
unlocked: ['half1']
+= 'half2'
+= 'boss'
game_status = 'completed'
unlocked_sections list in the database acts as a key ring. You start with one key (half1). Completing all lessons gives you the second key (half2). Reaching square 56 gives you the boss key. The frontend hides locked sections, and the backend rejects any API calls that try to access locked content.
College Board AP CSP Requirements
Big Idea 3: Input
HTML forms, keyboard events (WASD/arrows), mouse aiming, click/spacebar shooting — all captured via event listeners and sent to backend via REST or WebSocket.
Big Idea 3: Persistent Data Storage
SQLAlchemy ORM persists all game state to SQLite/MySQL. Player progress (current_square, total_bullets, completed_lessons, visited_squares) survives across sessions via authenticated API calls.
Big Idea 3: Sequencing
Server enforces ordered progression: lessons must complete before questions unlock, questions before boss. API calls execute sequentially: fetch → validate → update → respond.
Big Idea 3: Selection
if correct: award bullets, if lives <= 0: game over, if square >= 56: unlock boss, if bossHealth <= 0: victory. Backend validates all conditions before state changes.
Big Idea 3: Iteration
SlitherRush 30Hz tick loop iterates all arenas/players/bullets per frame. requestAnimationFrame for rendering. setInterval for autosave (10s), position broadcast (50ms), powerup spawning (5s).
Big Idea 3: Lists / Collections
visited_squares[], completed_lessons[], unlocked_sections[], arena['bullets'], slither_segments[] — all managed as MutableList/JSON columns or in-memory arrays with server-side mutation.
Big Idea 3: Procedures
resolve_player_collision(x, y, ox, oy, min_dist) — 5 parameters, returns adjusted coordinates. allocate_boss_spawn(), spawn_powerup_for_room(), @token_required() decorator with nested functions.
Big Idea 3: Algorithms
Boss AI with pattern switching (dash, zigzag, chase, circle). Collision detection via distance formula (math.sqrt). SlitherRush bullet-hit uses squared-distance optimization. Safe spawn uses random sampling + grid fallback.
Big Idea 4: The Internet
HTTP/HTTPS REST APIs (GET, POST, PUT, DELETE). WebSocket via Socket.IO with eventlet async. JWT tokens authenticate both HTTP and socket connections. CORS controls cross-origin access. Nginx reverse proxies all traffic.
Big Idea 5: Impact of Computing
Game teaches CS ethics and data privacy through lesson content. Guest mode demonstrates data minimization — zero personal data collected without authentication.
CPT (Create Performance Task) Mapping
| CPT Requirement | Our Implementation |
|---|---|
| Program Purpose | Teach AP CSP through gamified learning |
| Input → Output | Answers → bullet rewards; keyboard/mouse → character movement |
| List Usage | visited_squares[] tracks answered questions; completed_lessons[] gates progression |
| Procedure with Parameters | resolve_player_collision(desired_x, desired_y, other_x, other_y, min_dist) — 5 params, returns adjusted coords |
| Algorithm (Seq + Sel + Iter) | _step_bullets() — sequences bullet movement, iterates all bullets vs. all players, selects action via distance check |
Key Backend Code Snippets
1. Server-Authoritative Collision Resolution
# socketio_handlers/boss_battle.py
def resolve_player_collision(desired_x, desired_y, other_x, other_y, min_dist):
dx = desired_x - other_x
dy = desired_y - other_y
dist = math.sqrt(dx * dx + dy * dy)
if dist < 0.001:
return other_x + min_dist, desired_y, True
if dist >= min_dist:
return desired_x, desired_y, False
overlap = min_dist - dist
nx, ny = dx / dist, dy / dist
return desired_x + nx * overlap, desired_y + ny * overlap, True
desired_x/y), the other player's position (other_x/y), and the minimum allowed distance between them (min_dist).
dx / dy — Calculate the horizontal and vertical distance between the two players.
dist = math.sqrt(dx*dx + dy*dy) — Use the Pythagorean theorem (distance formula) to get the actual straight-line distance between them.
if dist < 0.001 — Edge case: if both players are in the exact same spot (distance ~0), push one to the right by min_dist to avoid division by zero.
if dist >= min_dist — If they're far enough apart, no collision. Return the desired position unchanged, with False meaning "no collision happened."
overlap = min_dist - dist — How much they're overlapping (e.g., if they need 56px apart but are only 40px apart, overlap = 16px).
nx, ny = dx/dist, dy/dist — Normalize the direction vector (make it length 1) so we can push in the right direction.
Return: Push the player's position outward along that direction by the overlap amount. Return True meaning "collision was resolved."
2. Section Unlock on Lesson Completion
# api/snakes_extended.py
if lesson_number not in record.completed_lessons:
record.completed_lessons.append(lesson_number)
record.total_bullets += bullets_earned
if len(set(record.completed_lessons)) >= 5 and 'half2' not in record.unlocked_sections:
record.unlocked_sections.append('half2')
if lesson_number not in record.completed_lessons — Only process if this lesson hasn't been completed before (prevents exploiting the same lesson for infinite bullets).
record.completed_lessons.append(lesson_number) — Add this lesson number (e.g., 3) to the player's completed list. Since this is a MutableList column, SQLAlchemy detects the change and will save it to the database.
record.total_bullets += bullets_earned — Add the bullet reward to the player's total.
len(set(record.completed_lessons)) >= 5 — set() removes duplicates, then check if they've completed at least 5 unique lessons. If yes, and 'half2' isn't already unlocked, unlock it. This is the gating logic — you can't skip to the questions without finishing all lessons first.
3. Admin Dashboard Aggregation
# api/admin.py
@admin_api.route('/dashboard', methods=['GET'])
@admin_required()
def admin_dashboard():
total_users = User.query.count()
total_players = GameProgress.query.count()
total_bullets = db.session.query(db.func.sum(GameProgress.bullets)).scalar() or 0
@admin_required() — Two checks happen before this function runs: (1) is the user logged in? (2) is their role 'Admin'? If either fails, the request is rejected with 403 Forbidden.
User.query.count() — SQL SELECT COUNT(*) FROM users — counts every registered user.
db.func.sum(GameProgress.bullets).scalar() — SQL SELECT SUM(bullets) FROM game_progress — adds up all bullets across all players. .scalar() extracts a single number from the result. or 0 returns 0 if there are no records (instead of None).
Summary
| Layer | Technology | What It Does |
|---|---|---|
| Frontend | Jekyll + vanilla JS | Static site on GitHub Pages — what users see and interact with |
| Backend | Flask + Flask-RESTful | Python web server with 15+ Blueprints and 20+ REST endpoints |
| Real-time | Socket.IO + eventlet | Persistent two-way connections for 3 multiplayer game modes |
| Database | SQLAlchemy (SQLite/MySQL) | Stores all player progress, game state, and user accounts permanently |
| Auth | JWT (PyJWT) + bcrypt | Secure login via encrypted tokens; passwords hashed before storage |
| Admin | @admin_required() |
Role-gated dashboard with site-wide stats and user management |
| Deployment | Docker + Gunicorn + Nginx | Containerized app with production server and reverse proxy |
| Domain | snakes.opencodingsociety.com | Nginx routes traffic to Docker container with WebSocket support |