Skip to content

muhammad-fiaz/SerpentASM

Repository files navigation

SerpentASM

A GUI Snake Game Written in x86-64 Assembly

License Platform Architecture Language Compiler Version Docker

SerpentASM is a GUI Snake game written in x86-64 assembly (NASM syntax) for Windows. It uses native Win32 APIs and GDI double-buffered graphics, showcasing direct hardware interaction, a custom PRNG, and precise 16-byte stack alignment throughout.

Screenshots

Main Menu Gameplay
Main Menu Gameplay
Game Over Settings
Settings Game Over

Overview

SerpentASM creates a native Win32 window and renders graphics directly using the Windows GDI (Graphics Device Interface). It is assembled with NASM and linked via MinGW-w64 GCC — zero external runtime dependencies.

Important

SerpentASM is a Windows-native application. It is built entirely on the Win32 API (kernel32.dll, user32.dll, gdi32.dll, winmm.dll, advapi32.dll, dwmapi.dll). Windows 10 / 11 (x86-64) is the primary and officially supported platform.

Tip

Linux & macOS users: You can run SerpentASM using Wine, which translates Win32 API calls to native POSIX equivalents. See the Linux & macOS (Optional) section for setup instructions, or use the included Docker image for a one-command experience.


Features

  • Win32 Windowing: Registers window classes and creates windows directly via the Win32 API.
  • Double-Buffered GDI Rendering: All drawing goes to a memory DC backbuffer; a single BitBlt transfer presents the frame — zero flicker.
  • Fixed Timestep Loop: Uses QueryPerformanceCounter for physics-decoupled, frame-rate-independent gameplay.
  • Persistent High Scores: Scores are saved to and loaded from config.toml via native Win32 File APIs.
  • Debounced Input: Keyboard state is double-buffered across ticks to eliminate direction-reversal self-collision bugs.
  • Seamless Background Music: MCI-based BGM plays continuously across all screens (menu, gameplay, settings, about) — no restart on screen change. Pausing suspends the track position; resuming picks up exactly where it left off.
  • Sound Effects: Food pickup, move tick, and game-over sounds via winmm.dll MCI.
  • Dynamic Themes: Dark, light, and system-adaptive colour schemes — toggled live from the Settings screen with immediate GDI brush rebuild and title-bar update.
  • In-Game Settings Screen: Toggle Sound, Wrap Screen, and Theme at runtime. All changes persist to config.toml immediately.
  • Config-Driven: Game name, version, difficulty, high score, sound, wrap, and theme are all loaded from config.toml at startup.
  • Mouse + Keyboard Navigation: All menus support hover highlighting and click activation alongside full keyboard (W/S/Enter/ESC) navigation.
  • Diagnostic Logging: Startup checkpoints and key events are written to logs/debug_log.txt (guarded by enable_logging in config.toml).

Platform Support

Platform Status Method
Windows 10 / 11 (x86-64) Native — Primary Target Runs directly out of the box
Windows ARM64 ✅ Supported Via built-in WoW64 x86-64 emulation
Linux x86-64 🟡 Optional Via Wine or Docker
macOS x86-64 / Apple Silicon 🟡 Optional Via Wine + Rosetta 2 or Docker
Any OS with Docker 🟡 Optional Via Docker + Wine container

Note

Linux and macOS are not primary targets. They are supported on a best-effort basis through Wine (directly or via Docker). If you encounter Wine-specific issues, check the WineHQ AppDB or open a GitHub issue with your platform details.

Warning

SerpentASM uses Windows GDI, WinMM, DWM, and the Win32 registry — none of which have Linux/macOS native equivalents. Wine emulates these, but audio and theme registry queries may behave differently. The game falls back to dark theme if AppsUseLightTheme is absent from the Wine registry.


Features In Detail

State Machine

The game runs a 6-state machine:

State Description
STATE_MENU Main menu — music plays, hover/click navigation
STATE_PLAYING Active gameplay — snake moves on fixed timestep
STATE_PAUSED Paused overlay — music suspended at current position
STATE_GAME_OVER Game over screen — game-over SFX plays
STATE_ABOUT About screen — music continues uninterrupted
STATE_SETTINGS Settings screen — live toggle of sound/wrap/theme

Audio Architecture

Note

All MCI commands are asynchronous (mciSendStringA returns immediately). The notify flag on BGM play commands sends MM_MCINOTIFY to the window when the track ends — the WndProc handler loops it automatically, regardless of which screen is currently active.

BGM behaviour across transitions:

Transition BGM Action
Startup play bgm from 0 notify — fresh start
Menu → Playing None — music continues seamlessly
Menu → Settings / About None — music continues seamlessly
Playing → Paused pause bgm — position is preserved
Paused → Playing play bgm notify — resumes from pause point
Paused → Menu play bgm notify — resumes from pause point
Playing → Game Over stop bgm, then play sfx_gameover
Game Over → New Game play bgm from 0 notify — fresh restart
Game Over → Menu play bgm from 0 notify — fresh restart

Diagnostic Logging

Note

When enable_logging = true in config.toml, File_Log appends timestamped checkpoint strings to logs/debug_log.txt. The function opens, seeks to EOF, writes, and closes the file on every call — no file handle is held open between calls, making it safe and corruption-free. Logging is completely bypassed (early-return guard) when enable_logging = false, adding zero overhead.


Architecture

  • Contiguous Layouts: Snake coordinate arrays are contiguous in memory for cache-friendly iteration.
  • Calling Conventions: Strictly adheres to the Microsoft x64 Calling Convention at every call site.
  • Static Memory: All state is allocated statically in .bss / .data — no heap, no fragmentation.

1. Register Classification & Preservations

Register Classification Role in SerpentASM
RAX Volatile Return value / arithmetic scratch
RCX Volatile 1st parameter
RDX Volatile 2nd parameter / division remainder
R8 Volatile 3rd parameter
R9 Volatile 4th parameter
R10, R11 Volatile Scratch registers
RBX Non-Volatile Window handles, loop counters
RDI, RSI Non-Volatile Used in MemSet / MemCopy / string loops
RSP Non-Volatile Stack pointer — must stay 16-byte aligned
RBP Non-Volatile Stack frame anchor
R12 – R15 Non-Volatile Must be preserved if modified

Important

Every procedure that modifies a non-volatile register (RBX, RDI, RSI, RBP, R12–R15) MUST push it in the prologue and pop it in the epilogue, or the calling function's state will be silently corrupted.

2. Stack Alignment & Shadow Space

Caution

Misaligned stacks (RSP not divisible by 16 before a call) cause silent crashes or corrupted XMM registers on MOVAPS instructions inside Win32 API internals. Every procedure in SerpentASM is audited for correct alignment.

Rules followed at every call site:

  1. Shadow Space: At least 32 bytes (sub rsp, 32) allocated below the return address before any Win32 call.
  2. 16-Byte Alignment: Since call pushes an 8-byte return address, RSP enters each function with RSP % 16 == 8. The prologue must allocate S bytes where S ≡ 8 (mod 16) — e.g. sub rsp, 40 (one push) or sub rsp, 104 (four pushes).

Argument passing:

  • Args 1–4: RCX, RDX, R8, R9
  • Args 5+: [rsp+32], [rsp+40], [rsp+48], …

3. Data Structures

[GameState]   —  20 Bytes  (state enum, difficulty, current score, high score, running flag)
[SnakeState]  — 8208 Bytes (length, direction, pending direction, grow flag, 1024×Point body)
[FoodState]   —  12 Bytes  (position Point, active flag)
[InputState]  — 512 Bytes  (256-byte current key map + 256-byte previous key map for debounce)
[TimerState]  —  32 Bytes  (QPC frequency, last sample, accumulator, step_ticks)

Project Structure

SerpentASM/
│
├── src/
│   ├── main.asm                 # Master entry point — single compilation unit
│   │
│   ├── game/
│   │   ├── game.asm             # 6-state machine: update + render dispatch
│   │   ├── snake.asm            # Motion, direction, segment array management
│   │   ├── food.asm             # Randomised spawning with overlap prevention
│   │   ├── collision.asm        # Wall, self-collision, and food pickup checks
│   │   ├── score.asm            # Score arithmetic and high score persistence
│   │   └── difficulty.asm       # Tick-rate mapping (easy / medium / hard)
│   │
│   ├── engine/
│   │   ├── renderer.asm         # GDI double-buffering, brushes, grid, logo
│   │   ├── input.asm            # Double-buffered keyboard state + debounce
│   │   ├── timer.asm            # QPC-based fixed timestep accumulator
│   │   ├── random.asm           # XorShift64 PRNG for food placement
│   │   ├── window.asm           # WndProc, mouse input, MCI notify loop
│   │   ├── ui.asm               # Menu, Settings, About, HUD, Game Over overlays
│   │   └── config.asm           # TOML parser/serialiser, Theme_Init, Config_Save
│   │
│   ├── platform/
│   │   ├── win32.asm            # File_Log, Play_MCI_Command, Sound_Init/Shutdown
│   │   └── memory.asm           # MemCopy and MemSet (REP MOVSB / STOSB)
│   │
│   └── include/
│       ├── constants.inc        # State IDs, VK codes, menu Y positions, SETT_Y positions
│       ├── macros.inc           # DEBUG assertion macros
│       ├── structures.inc       # Struct layouts (GameState, SnakeState, TimerState, …)
│       └── globals.inc          # Statically allocated globals, strings, BSS variables
│
├── assets/                      # Application icon (.ico, .icns, .png)
├── sound/                       # BGM and SFX audio files (.mp3)
├── build/                       # Object files output directory
├── scripts/
│   ├── build.bat                # Windows CMD build script ← Primary
│   ├── clean.bat                # Windows CMD clean script
│   ├── build.sh                 # Linux/macOS cross-compile script (optional)
│   └── clean.sh                 # Linux/macOS clean script
├── config.toml                  # Runtime configuration (all settings)
├── Dockerfile                   # Multi-stage build: compile + Wine runtime
├── docker-compose.yml           # One-command Docker run
├── .dockerignore                # Excludes logs, .git, build/ from Docker context
├── LICENSE                      # MIT License
└── README.md                    # This document

Installation & Build

Prerequisites — Windows (Required)

Important

Windows is the primary build and execution environment. Install these tools on Windows before building.

  • NASM Assembler: Download from nasm.us and add the install folder to your system PATH.
  • MinGW-w64 GCC (Linker): Download from mingw-w64.org and add its bin/ folder to your PATH.

Tip

Verify both tools are on your PATH by running nasm --version and gcc --version in a Command Prompt. Both must respond with a version string before the build will succeed.

Clone

git clone https://github.com/muhammad-fiaz/SerpentASM.git
cd SerpentASM

Compiling & Running

Windows — Native (Primary)

Open Command Prompt or PowerShell in the project root:

.\scripts\build.bat
SerpentASM.exe

To clean build artefacts:

.\scripts\clean.bat

Windows — Git Bash / MSYS2 / WSL

chmod +x scripts/build.sh scripts/clean.sh
./scripts/build.sh
./SerpentASM.exe

Debug Build

To enable diagnostic checkpoint logging and assembly-level assertions:

.\scripts\build.bat debug

Note

In debug mode, all File_Log calls write to logs/debug_log.txt. Each entry is a checkpoint string such as "Checkpoint 2: Renderer Init Done". This file is appended on each run; delete it manually to start fresh. Logging can also be disabled at runtime by setting enable_logging = false in config.toml — the guard is a single cmp/je at the top of File_Log, adding negligible overhead.


Linux & macOS (Optional, via Wine)

Note

SerpentASM is not natively compiled for Linux or macOS. It must be cross-compiled on Unix hosts with mingw-w64 and run via the Wine Windows compatibility layer.

Warning

Wine audio support requires a working ALSA / PulseAudio / PipeWire stack on Linux, or CoreAudio on macOS. If enable_sound = true but no audio device is available, the game still runs — MCI commands fail silently. Set enable_sound = false in config.toml to suppress all audio calls.

Tip

Linux (Debian / Ubuntu): Install the required tools with:

sudo apt update
sudo apt install nasm mingw-w64 wine -y

Then build and run:

chmod +x scripts/build.sh
./scripts/build.sh
wine ./SerpentASM.exe

Tip

macOS: Install via Homebrew:

brew install nasm mingw-w64 wine-stable

Then build and run:

chmod +x scripts/build.sh
./scripts/build.sh
wine ./SerpentASM.exe

How Wine Makes It Work

SerpentASM API Wine Translation
Win32 GDI (drawing) X11/Wayland on Linux; Quartz/Cocoa on macOS
WinMM / MCI (audio) ALSA / PulseAudio / PipeWire (Linux); CoreAudio (macOS)
Registry (theme) Wine stub registry — falls back to dark theme if absent
DWM (title bar) No-op on Wine — title bar theme ignored gracefully

Docker Support (Optional)

Note

Docker is an optional convenience for Linux and macOS users who want a fully self-contained Wine environment without manual tool installation. Windows users should run SerpentASM.exe natively — Docker adds no benefit on Windows.

File Purpose
Dockerfile Multi-stage: compile .exe in builder, run via Wine in runtime
docker-compose.yml One-command wrapper (docker compose up)
.dockerignore Excludes logs/, .git/, build/ from Docker context

Prerequisites

Install Docker Desktop (Linux, macOS, or Windows with WSL2).

Quick Start — Docker Compose (Recommended)

Tip

Linux users: Allow Docker containers to connect to your X11 display server first:

xhost +local:docker
# Build the image
docker compose build

# Run the game
docker compose up

Stop and clean up:

docker compose down

Manual docker run — Linux

# 1. Allow the container to access your X11 display
xhost +local:docker

# 2. Build the image
docker build -t serpentasm .

# 3. Run with X11 display forwarding
docker run -it --rm \
  -e DISPLAY=$DISPLAY \
  -v /tmp/.X11-unix:/tmp/.X11-unix \
  --network host \
  serpentasm

Manual docker run — macOS (XQuartz required)

Tip

Install XQuartz first. Then open XQuartz → Preferences → Security and enable "Allow connections from network clients".

# Allow local connections
xhost + localhost

# Build and run
docker build -t serpentasm .
docker run -it --rm \
  -e DISPLAY=host.docker.internal:0 \
  serpentasm

Manual docker run — Windows (WSL2 + WSLg)

Tip

Windows 11 with WSL2 includes WSLg, which provides an X11-compatible display automatically — no extra X server needed.

# Run from inside a WSL2 terminal
docker build -t serpentasm .
docker run -it --rm \
  -e DISPLAY=$DISPLAY \
  -v /tmp/.X11-unix:/tmp/.X11-unix \
  serpentasm

If you are on Windows 10 without WSLg, install VcXsrv or Xming, then:

docker run -it --rm \
  -e DISPLAY=host.docker.internal:0 \
  serpentasm

Build Only (No Display / CI)

# Extract just the compiled .exe without running it
docker build --target builder -t serpentasm-builder .
docker create --name tmp serpentasm-builder
docker cp tmp:/build/SerpentASM.exe ./SerpentASM.exe
docker rm tmp

Controls

In-Game

Key Action
W / Move up
S / Move down
A / Move left
D / Move right
SPACE or ESC Pause
SPACE (paused) Resume
ESC (paused) Return to Main Menu
ENTER (Game Over) Start new game
ESC (Game Over) Return to Main Menu

Main Menu

Key / Mouse Effect
W / — or hover cursor Move selection up
S / — or hover cursor Move selection down
ENTER / SPACE / Left Click Activate selected item
ESC Exit the application

Items:

  • Start Game — Begin a new game. Music continues without interruption.
  • Difficulty — Cycle Easy → Medium → Hard. Saved to config.toml immediately.
  • Reset High Score — Set high score to 0 and save to config.toml.
  • Settings — Open the Settings screen.
  • About — Show project information.
  • Exit — Close the application.

Settings Screen

Key / Mouse Effect
W / — or hover cursor Move selection up
S / — or hover cursor Move selection down
ENTER / SPACE / Left Click Toggle / activate selected row
ESC Return to Main Menu

Rows:

  • Sound — Toggle audio ON / OFF. Persists to config.toml.
  • Wrap Screen — Toggle wall wrap ON / OFF. Persists to config.toml.
  • Theme — Cycle Dark → Light → System → Dark. GDI brushes and title bar update instantly. Persists to config.toml.
  • Back to Menu — Return to the Main Menu.

Note

All settings changes on the Settings screen are written to config.toml immediately and synchronously via Config_Save, which uses CreateFileA(CREATE_ALWAYS) to atomically overwrite the file with the full serialised state. No separate "Apply" or "Save" step is needed.


Configuration (config.toml)

All game settings live in config.toml at the project root.

Tip

Most settings can be changed at runtime from the in-game Settings screen — no manual file editing needed. Changes are persisted instantly.

Warning

Do not edit config.toml while the game is running. Config_Save writes the full file on every setting change and will overwrite any concurrent manual edits.

# =============================================================================
# SerpentASM — Configuration File
# =============================================================================
# Read at startup. Written back at runtime on every setting change.
# Change most settings live from the in-game Settings screen.
# =============================================================================

# The name shown in the window title and on the Main Menu header.
name = "Serpent Rebooted"

# The version string shown under the game name on the Main Menu.
version = "1.0.0"

# ---------------------------------------------------------------------------
# [game] — Gameplay Settings
# ---------------------------------------------------------------------------
[game]

# Wrap Screen — what happens when the snake hits a wall.
#   false = wall kills the snake (classic mode)
#   true  = snake wraps to the opposite side (infinite borders)
# ← Toggle live from Settings screen.
wrap_screen = false

# Sound — enables or disables all audio (BGM + SFX).
#   true  = audio active
#   false = completely silent (MCI play commands are skipped)
# ← Toggle live from Settings screen.
enable_sound = true

# Difficulty — snake movement speed.
#   "easy"   = slow  (~8 ticks/s)
#   "medium" = normal (~12 ticks/s)
#   "hard"   = fast  (~18 ticks/s)
# ← Cycle live from the Difficulty item on the Main Menu.
difficulty = "medium"

# High Score — persisted across sessions.
# Updated automatically on a new record. Reset via Main Menu.
high_score = 0

# ---------------------------------------------------------------------------
# [logging] — Diagnostic Logging
# ---------------------------------------------------------------------------
[logging]

# Enable file logging of startup checkpoints and key events.
#   true  = writes to log_folder/debug_log.txt (appended each run)
#   false = no file I/O; zero overhead (early-return guard in File_Log)
enable_logging = true

# Folder for log files (relative to project root). Created if absent.
log_folder = "logs"

# ---------------------------------------------------------------------------
# [theme] — Visual Theme
# ---------------------------------------------------------------------------
[theme]

# Colour scheme for background, grid, text, and accent.
#   "dark"   — #121212 bg, cyan accent
#   "light"  — #F5F5F5 bg, orange accent
#   "system" — mirrors Windows AppsUseLightTheme registry value
#   "auto"   — alias for "system"
# ← Cycle live from Settings screen (Dark → Light → System → Dark).
# On Wine, falls back to "dark" if the registry value is absent.
theme = "system"

Design Goals

  1. Zero External Dependencies — built solely on native Windows DLLs (no SDL, no OpenGL, no CRT).
  2. No Heap Allocation — all state is statically allocated in .bss and .data; no malloc, no fragmentation.
  3. Correct Stack Alignment Everywhere — every procedure is audited; misalignment is a silent crash vector on Win32.
  4. Seamless Audio — BGM uses MCI pause/resume semantics so the track position is never lost across screen transitions.
  5. Config-Driven — changing game name, version, theme, difficulty, or high score requires only a config.toml edit (or Settings screen action).

Rendering Pipeline

[GDI Commands]  ──►  [hdc_back Memory DC]  ──►  [BitBlt to Screen DC]
  (Clear, Grid,         (in-memory canvas)        (WM_PAINT / direct call)
   Snake, Food,
   HUD, Overlays)

All drawing targets hdc_back (a CreateCompatibleDC device context). A single BitBlt call transfers the complete frame to the visible window DC, producing a flicker-free experience at up to 60 FPS.


Timing System

  1. QPC SamplingQueryPerformanceCounter is called each frame to compute delta ticks.
  2. Accumulator — delta ticks are added to a running accumulator.
  3. Simulation Step — when the accumulator exceeds step_ticks (set by difficulty), one physics tick fires and step_ticks is subtracted.
  4. Decoupled Rendering — physics runs at 8–18 steps/second; the render loop redraws as fast as the message pump allows.

Contributing

Contributions are welcome! Please see CONTRIBUTING.md for guidelines on coding standards, branch naming, commit messages, and the Pull Request workflow.

Tip

Before submitting a PR, run a debug build (.\scripts\build.bat debug) and check logs/debug_log.txt to confirm all checkpoints pass. Ensure nasm --version reports ≥ 2.15 and gcc --version reports a MinGW-w64 build.


License

This project is licensed under the MIT License. See LICENSE for full details.

Releases

Sponsor this project

Packages

Used by

Contributors

Languages