• Admin Panel
  • Docker & Deployment
  • How Everything Flows (Visual Summary)
  • College Board AP CSP Requirements
    • 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
  • CPT (Create Performance Task) Mapping
  • Key Backend Code Snippets
  • Summary
  • Backend Team

    Moiz — Lead Deployer & DevOps
    Arnav — Assistant Backend Developer

    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

    Jekyll Frontend
    GitHub Pages
    ↓ ↓ ↓
    Flask REST API
    Port 8306
    Socket.IO
    WebSockets (eventlet)
    ↓ ↓ ↓
    SQLAlchemy ORM
    SQLite / MySQL
    ↓ ↓ ↓
    Docker + Nginx
    Gunicorn (eventlet worker)
    What each layer does in plain English:

    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

    What are app routes? A route is a URL pattern that maps to a Python function. When someone visits /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
    
    Line-by-line: Each 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

    What is an API endpoint? An endpoint is a specific URL + HTTP method combination that does one thing. The frontend calls these endpoints using 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/)

    EndpointMethodPurposePayload / Params
    /api/snakes/GETLoad current user's game progress
    /api/snakes/POSTCreate new game record{username, selected_character}
    /api/snakes/PUTAutosave full game state (every 10s){current_square, lives, total_bullets, time_played, ...}
    /api/snakes/DELETEDelete game record
    /api/snakes/progressGETGet progress (auto-creates if missing)
    /api/snakes/complete-lessonPOSTMark lesson complete, award bullets{lesson_number, bullets_earned}
    /api/snakes/answer-questionPOSTRecord answer, update position{square, bullets_earned, correct}
    /api/snakes/update-squarePOSTUpdate board position{square}
    /api/snakes/add-bulletsPOSTAward bullets to player{bullets}
    /api/snakes/completePOSTMark game as completed
    /api/snakes/resetPOSTFull reset (preserves champion status)
    /api/snakes/leaderboardGETTop 10 players by bullets?limit=10
    /api/snakes/championsGETAll game completers (Hall of Champions)
    /api/snakes/active-playersGETPlayers updated in last 10s
    /api/snakes/unvisited-squaresGETList of unvisited question squares
    How the frontend uses these: When a player finishes a lesson, the JavaScript on the page runs 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/)

    EndpointMethodPurpose
    /api/admin/dashboardGETOverview stats — total users, players, bullets, time, boss stats
    /api/admin/playersGETList all active players with progress details
    /api/admin/usersGET POSTUser management — list all, create new
    /api/admin/user/<uid>PUT DELETEEdit or delete individual user

    User & Auth Endpoints

    EndpointMethodPurpose
    /api/authenticatePOSTLogin — returns JWT token in HttpOnly cookie
    /api/user/POSTRegister new user
    /api/user/GETGet current user profile
    /api/user/PUTUpdate user profile

    How the Backend Works

    Authentication Flow

    User enters credentials
    POST /api/authenticate
    Server validates + creates JWT
    JWT stored in HttpOnly cookie
    @token_required() on every API call
    What is this flow showing? When a user logs in, their username and password are sent to the server. The server checks if they match a record in the database. If yes, it creates a JWT (JSON Web Token) — a small encrypted string that contains the user's ID. This token gets stored in an HttpOnly cookie (a cookie that JavaScript cannot read, making it safe from XSS attacks). From now on, every time the frontend makes an API call, the browser automatically sends this cookie along. The server reads the token, decodes who the user is, and knows which game data to load or update.
    # 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
    
    What this code does step by step: 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

    Login/Guest
    Lessons 1-5
    POST /complete-lesson
    Unlock half2
    Questions (sq 7-56)
    POST /answer-question
    Unlock boss
    Battle Mode
    WebSocket events
    POST /complete
    What is section gating? The backend enforces a strict order of progression. The player's 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)

    Why WebSockets instead of regular HTTP? Regular HTTP works like texting — the frontend sends a message, waits for a reply, conversation over. For multiplayer gaming, we need a phone call — both sides can talk at any time. Socket.IO keeps a persistent connection open between each player and the server. When Player A moves their character, the server instantly pushes that position to all other players in the same room, without them having to ask "did anyone move?" every few milliseconds.

    Three multiplayer modes run over WebSocket connections:

    Boss Battle (Co-op, up to 10 players)

    • boss_join_lobby → Pre-battle chat room
    • boss_join_room → Enter arena (server allocates spawn)
    • boss_player_move → Position broadcast (50ms interval)
    • boss_shoot → Fire bullet, server validates collision
    • boss_defeated → Aggregate all player stats
    • Powerups spawn every 5s (server-controlled rate limiting)

    PvP Arena (1v1 Competitive)

    • pvp_join → Auto-matchmaking via get_or_create_open_room()
    • pvp_ready → Dual-ready confirmation system
    • pvp_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
    "Server-authoritative" — what does that mean? The server is the single source of truth. Players send their inputs (key presses, mouse clicks) to the server, and the server calculates all positions, collisions, and damage. The server then tells every client what happened. This prevents cheating — a player can't modify their local code to say "I have 999 health" because the server tracks health, not the client. The SlitherRush mode takes this furthest: the entire game simulation (30 frames per second) runs on the server. Clients just send steering input and receive the game state to render.

    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()
    
    Why is this needed? WebSocket connections don't automatically come with authentication like HTTP requests. This function manually checks: does this socket connection have a valid JWT cookie attached? If yes, decode it and look up the user in the database. If no token exists, return 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

    Why do we need a database? Without a database, all game progress would vanish when the server restarts. The database is a permanent storage file on the server. Every time a player completes a lesson, answers a question, or earns bullets, we write that change to the database. When they log back in tomorrow, we read it back. SQLAlchemy lets us define database tables as Python classes — each class becomes a table, each attribute becomes a column.

    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)
    
    Reading this model like a spreadsheet: Each row in this table is one player's game save. The columns are: 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

    User
    1:1
    SnakesGameData
    1:N
    SquareCompletion
    BossRoom
    1:N
    BossPlayer
    1:1
    BossBattleStats
    What do 1:1 and 1:N mean?
    - 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.

    Why bcrypt? We never store passwords as plain text. Bcrypt is a hashing algorithm that turns "mypassword123" into an unreadable string like $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.
    
    Line-by-line: @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

    Why Docker? "It works on my machine" is a classic developer problem. Docker solves this by packaging the app, Python 3.11, and every library into a container — a lightweight, isolated environment. The container runs the same way on a developer's laptop, on a teammate's laptop, and on the production server. Think of it like shipping a product in a sealed box instead of loose parts.

    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"]
    
    Line-by-line: 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
    
    What this does: Docker Compose defines how to run our container. 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

    What is a reverse proxy? Our Flask app runs on port 8306, but users visit 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;
        }
    }
    
    Key lines: 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

    Browser
    Nginx
    snakes.opencodingsociety.com
    Gunicorn
    :8306
    Flask App
    Blueprint routing
    @token_required
    JWT validation
    SQLAlchemy
    DB read/write
    JSON Response
    Walking through a real request: A player clicks "Roll Dice" and lands on square 23. Here's what happens:
    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:8306
    3. 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 blueprint
    5. @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 database
    7. JSON Response is sent back: {"current_square": 23, "total_bullets": 45, "visited_squares": [7,12,18,23], ...}

    Multiplayer Data Flow

    Player Input
    WASD / Mouse
    Socket.IO emit
    Server validates
    Collision + bounds
    Broadcast to room
    All clients render
    Example — Boss Battle: Player A presses W to move up and clicks to shoot. Their browser emits a 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

    Start
    unlocked: ['half1']
    Complete 5 lessons
    += 'half2'
    Reach square 56
    += 'boss'
    Defeat boss
    game_status = 'completed'
    Why this matters: This is the sequencing that College Board requires. The game isn't just "click any page" — there's a forced order enforced by the backend. The 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
    
    What this does in plain English: This function checks if two players are overlapping and pushes them apart if so. It takes 5 parameters — the position a player wants to move to (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')
    
    What this does: 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)) >= 5set() 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
    
    What this does: This powers the admin dashboard with site-wide statistics. @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