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.
| Main Menu | Gameplay |
|---|---|
![]() |
![]() |
| Game Over | Settings |
|---|---|
![]() |
![]() |
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.
- 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
BitBlttransfer presents the frame — zero flicker. - Fixed Timestep Loop: Uses
QueryPerformanceCounterfor physics-decoupled, frame-rate-independent gameplay. - Persistent High Scores: Scores are saved to and loaded from
config.tomlvia 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.dllMCI. - 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.tomlimmediately. - Config-Driven: Game name, version, difficulty, high score, sound, wrap, and theme are all loaded from
config.tomlat 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 byenable_logginginconfig.toml).
| 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.
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 |
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 |
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.
- 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.
| 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.
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:
- Shadow Space: At least 32 bytes (
sub rsp, 32) allocated below the return address before any Win32 call. - 16-Byte Alignment: Since
callpushes an 8-byte return address, RSP enters each function withRSP % 16 == 8. The prologue must allocateSbytes whereS ≡ 8 (mod 16)— e.g.sub rsp, 40(one push) orsub rsp, 104(four pushes).
Argument passing:
- Args 1–4:
RCX,RDX,R8,R9 - Args 5+:
[rsp+32],[rsp+40],[rsp+48], …
[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)
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
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 yourPATH.- Alternatively, install via the Qt Maintenance Tool which bundles MinGW.
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.
git clone https://github.com/muhammad-fiaz/SerpentASM.git
cd SerpentASMOpen Command Prompt or PowerShell in the project root:
.\scripts\build.bat
SerpentASM.exeTo clean build artefacts:
.\scripts\clean.batchmod +x scripts/build.sh scripts/clean.sh
./scripts/build.sh
./SerpentASM.exeTo enable diagnostic checkpoint logging and assembly-level assertions:
.\scripts\build.bat debugNote
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.
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 -yThen build and run:
chmod +x scripts/build.sh
./scripts/build.sh
wine ./SerpentASM.exeTip
macOS: Install via Homebrew:
brew install nasm mingw-w64 wine-stableThen build and run:
chmod +x scripts/build.sh
./scripts/build.sh
wine ./SerpentASM.exe| 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 |
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 |
Install Docker Desktop (Linux, macOS, or Windows with WSL2).
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 upStop and clean up:
docker compose down# 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 \
serpentasmTip
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 \
serpentasmTip
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 \
serpentasmIf you are on Windows 10 without WSLg, install VcXsrv or Xming, then:
docker run -it --rm \
-e DISPLAY=host.docker.internal:0 \
serpentasm# 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| 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 |
| 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.tomlimmediately. - 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.
| 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.
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"- Zero External Dependencies — built solely on native Windows DLLs (no SDL, no OpenGL, no CRT).
- No Heap Allocation — all state is statically allocated in
.bssand.data; nomalloc, no fragmentation. - Correct Stack Alignment Everywhere — every procedure is audited; misalignment is a silent crash vector on Win32.
- Seamless Audio — BGM uses MCI
pause/resumesemantics so the track position is never lost across screen transitions. - Config-Driven — changing game name, version, theme, difficulty, or high score requires only a
config.tomledit (or Settings screen action).
[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.
- QPC Sampling —
QueryPerformanceCounteris called each frame to compute delta ticks. - Accumulator — delta ticks are added to a running accumulator.
- Simulation Step — when the accumulator exceeds
step_ticks(set by difficulty), one physics tick fires andstep_ticksis subtracted. - Decoupled Rendering — physics runs at 8–18 steps/second; the render loop redraws as fast as the message pump allows.
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.
This project is licensed under the MIT License. See LICENSE for full details.



