A terminal-based voxel sandbox game inspired by Minecraft, rendered entirely using ncurses with colored ANSI characters, as a bitmap on a framebuffer device, or in a native SDL window.
Description
Cursedcraft is a 3D voxel sandbox that runs in your terminal or in a native window. It features software-rendered 3D graphics using 32-bit fixed-point math, procedurally generated terrain, and block-based building mechanics. The entire world is rendered using either 16 ANSI colors and ASCII characters for shading, as a bitmap on the framebuffer, or in an SDL window with configurable resolution (160x100 to 640x480).
Screenshots
You can find more screenshots here.
Features
- 3D Software Renderer: Real-time voxel rendering using optimized DDA raycasting algorithm
- Trigonometric lookup tables (2048 entries) eliminate floating-point trig
- Ray-world bounding box intersection for 3-5x sky performance improvement
- Fully fixed-point arithmetic pipeline for consistent performance
- Triple Rendering Modes:
- ncurses mode: 16 ANSI colors with ASCII character shading
- Framebuffer mode: Direct framebuffer rendering with configurable resolution (160x100 to 640x480) and integer scaling (build with FBDEV=1)
- SDL window mode: Native windowed rendering with integer scaling and fullscreen borderless display (build with SDL_WINDOW=1)
- Persistent Settings System: Configure and save preferences
- Field of View (4 presets)
- Render distance (4 presets: 40-100 blocks)
- Framebuffer resolution (8 presets)
- Sound effects and music toggles
- Mouse sensitivity (6 levels: 0.4x-2.0x)
- Custom input bindings for keyboard, mouse, and joystick
- All settings saved to options.txt
- Minecraft-like Menu System: Animated ASCII art logo with scrolling marquee
- Diagonal rainbow color animation on title text
- Scrolling splash text from splash_scroller.txt
- Arrow keys or IJKL navigation with visual highlighting
- Boxed menu options with consistent spacing
- Sound effects for menu navigation and selection
- Back buttons on all submenus
- Input Remapping System: Full rebinding support for all actions (build with KEYEVENT=1)
- Dedicated Input settings submenu
- Mouse sensitivity adjustment (6 presets)
- Per-action custom bindings for keyboard keys, mouse buttons, joystick buttons, and axes
- SET/RESET interface with 1-second capture timeout
- Bindings work as additional mappings on top of defaults
- Real-time preview in settings menu
- Joystick Support: Native gamepad/joystick input (build with KEYEVENT=1)
- Automatic detection of /dev/input/js* devices (user-accessible)
- Fallback to /dev/input/event* devices (requires permissions)
- Button and axis input with configurable dead zones
- Works seamlessly with custom input bindings
- No root access required for /dev/input/js* devices
- Perlin Noise Terrain: Realistic procedural generation with configurable seed and steepness
- Biomes: Sand beaches with underwater layers at lower elevations, grass and dirt at higher elevations
- Cave Systems: 3D noise-based cave carving with surface entry points
- Building: Place and remove blocks freely (16 different block types)
- Physics Engine: Optional gravity, collision, and jumping mechanics with V key toggle (optional, build with PHYSICS=1)
- Sound Effects: PC speaker beeps for block placement and breaking in a separate thread (optional, build with SOUND=1)
- Music Playback: Background music from .pcsp files with smart priority system (optional, build with SOUND=1 MUSIC=1) or MIDI files via SDL2_mixer (build with SOUND=1 SDL=1 MIDI=1)
- World Management: Scrolling world selection screen with intuitive navigation
- World Persistence: Saves world and player position automatically on exit
- Custom World Creation: Name your worlds and set custom dimensions, seed, and terrain steepness
- Loading Screen: Visual progress bar during world generation
- Smart Spawning: Automatically spawns player on surface
- Block Highlighting: Currently targeted block blinks for easy identification
Requirements
- C99-compatible compiler (gcc, tcc, clang)
- ncurses library
- Standard math library
- POSIX-compatible system (Linux, macOS, BSD, Haiku)
- Optional: GPM library for mouse support (build with MOUSE=1)
- Optional: SDL2 headers for SDL sound, SDL MIDI support and SDL video
Installation
Building from Source
Without sound (default):
make
With PC speaker sound effects:
make SOUND=1
This enables block placement and breaking sound effects via the PC speaker on Linux/BSD. Requires access to /dev/input/eventX devices. Sound effects are played in a separate thread to avoid blocking gameplay.
Notes on Haiku support: When building with SOUND=1 on a haiku system, due to the lack of proper PC speaker access, a C++ wrapper will be enabled instead which uses Haikus C++ exclusive BSoundPlayer system. This will slightly increase overhead compared to Linux/BSD systems and will result in a larger binary. It does however fully implement the note events and thus enables both sound effect and music playback.
With music playback:
make SOUND=1 MUSIC=1
Enables background music playback from .pcsp files stored in the music/ directory. Music plays in random order with 30-120 second breaks between tracks. Sound effects take priority over music - when a block is placed or broken, music pauses automatically to play the sound effect without interference.
With SDL2 audio (alternative to PC speaker):
make SOUND=1 SDL=1
Uses SDL2 for audio output instead of the PC speaker (Linux) or BSoundPlayer (Haiku). This provides a cross-platform audio backend with better compatibility and no special device access required. SDL2 generates square waves through the standard audio system. Requires SDL2 development libraries (libsdl2-dev on Debian/Ubuntu). When SDL is enabled, it takes priority over platform-specific audio backends.
With MIDI music playback:
make SOUND=1 SDL=1 MIDI=1
Enables MIDI music playback using SDL2_mixer instead of the PC speaker music system. MIDI files are loaded from the music/midis/ directory and played in random order with 30-120 second breaks between tracks. This provides higher quality music playback with standard MIDI files. Requires SDL2 and SDL2_mixer development libraries (libsdl2-dev libsdl2-mixer-dev on Debian/Ubuntu). When MIDI is enabled, the PC speaker music system (.pcsp files) is automatically disabled, but sound effects continue to work through SDL2 audio.
With debug info (music playback):
make SOUND=1 MUSIC=1 DEBUG=1
# or with MIDI:
make SOUND=1 SDL=1 MIDI=1 DEBUG=1
Adds a debug status line below the FPS display showing current music track and countdown timer until next song. Useful for troubleshooting music playback issues.
With framebuffer device rendering:
make FBDEV=1
This enables direct rendering to the Linux framebuffer device (/dev/fb0) instead of using ncurses for graphics. The game renders at configurable resolution (default 320x240) and automatically scales to fit your framebuffer using integer scaling. The HUD text remains rendered with ncurses at the bottom of the screen. This mode provides much better visual quality with smooth gradients and true RGB colors.
With SDL window rendering:
make SDL_WINDOW=1
This enables rendering in a native SDL window instead of ncurses. When you start a game, a fullscreen borderless window opens with the game rendered at configurable resolution (default 320x240) and automatically scaled to fit your display using integer scaling. The menu system continues to use ncurses in the terminal. When you return to the menu (ESC), the window closes automatically. This mode provides the best visual quality with hardware-accelerated rendering and clean HUD text via SDL_ttf. Requires SDL2 and SDL2_ttf development libraries (libsdl2-dev libsdl2-ttf-dev on Debian/Ubuntu).
With physics engine:
make PHYSICS=1
Enables player physics with gravity, collision detection, and jumping. Player is 2 blocks tall and must stand on solid ground. Press V to toggle between physics mode and noclip mode (free-flight). In physics mode: Space jumps, WASD moves with collision, auto-climbs 1-block ledges. In noclip mode: Space/Y/Z fly freely (default behavior).
With mouse support:
make MOUSE=1
Enables mouse controls via GPM (General Purpose Mouse). Move the mouse to adjust camera view, left-click to remove blocks, right-click to place blocks, and use scroll wheel to change selected block type. Requires the GPM daemon to be running (gpm) and the libgpm-dev package installed. Mouse events are processed non-blocking to maintain smooth framerate.
With keyboard event and joystick support:
make KEYEVENT=1
Enables direct keyboard input via Linux evdev (event device interface) or raw mode. Reads raw KEY_UP and KEY_DOWN events from /dev/input/eventX for more responsive controls. Also enables joystick/gamepad support through /dev/input/js* and /dev/input/event* devices. Automatically detects input devices and falls back to ncurses if unavailable. Includes input remapping UI accessible through Settings -> Input. Linux-only feature - other platforms automatically use ncurses. No additional libraries required.
Joystick features:
- Automatic detection of joystick devices
- /dev/input/js* support (accessible to regular users)
- /dev/input/event* fallback (requires input group membership or root)
- Button and axis input with dead zone handling
- Fully configurable through Input settings menu
- Works alongside keyboard and mouse
Combine options:
# minimal dependency mode
make SOUND=1 MUSIC=1 PHYSICS=1 KEYEVENT=1 FBDEV=1
# with mouse support
make SOUND=1 MUSIC=1 PHYSICS=1 MOUSE=1 KEYEVENT=1 FBDEV=1
# with SDL audio backend
make SOUND=1 SDL=1 MUSIC=1 PHYSICS=1 MOUSE=1 KEYEVENT=1 FBDEV=1
# with MIDI music
make SOUND=1 SDL=1 MUSIC=1 MIDI=1 PHYSICS=1 MOUSE=1 KEYEVENT=1 FBDEV=1
# or with SDL windowed mode instead of framebuffer:
make SDL_WINDOW=1 PHYSICS=1 SOUND=1 MUSIC=1 MIDI=1 SDL=1
Note: Some options like FBDEV and SDL_WINDOW are mutually exclusive. Running make with exclusive options will result in an error being thrown instead of the project building.
Errors may also be encountered when rebuilding with altered flags without a make clean beforehand.
Strip debug symbols (smaller binary):
make strip
This reduces the binary size from ~40KB to ~35KB by removing debugging symbols. This can increase performance on systems with low memory bandwidth such as i486.
Running
./cursedcraft
Or use:
make run
System-wide Installation (Optional)
sudo make install
Controls
Keyboard
| Key | Action |
|---|---|
| W | Move forward |
| S | Move backward |
| A | Move left |
| D | Move right |
| Space | Move up |
| Y / Z / Shift | Move down |
| ↑ / I | Look up (also menu navigation) |
| ↓ / K | Look down (also menu navigation) |
| ← / J | Look left (also menu navigation) |
| → / L | Look right (also menu navigation) |
| E | Place block |
| Q | Remove block |
| F | Select next block type |
| G | Select previous block type |
| V | Toggle physics/noclip mode |
| Enter | Confirm selection (menus) |
| ESC | Return to menu / Quit (from menu) |
Mouse
When built with MOUSE=1 (GPM support for terminal) or SDL_WINDOW=1 (SDL window mode):
| Input | Action |
|---|---|
| Mouse movement | Adjust camera view |
| Left click | Remove block (same as Q) |
| Right click | Place block (same as E) |
| Scroll up | Next block type (same as F) |
| Scroll down | Previous block (same as G) |
Note: In SDL window mode, the mouse is automatically captured with relative mouse mode when the game starts, and released when you return to the menu.
Joystick/Gamepad
When built with KEYEVENT=1:
Joystick support is enabled automatically when KEYEVENT=1 is set. The game detects connected joysticks/gamepads from /dev/input/js* (user-accessible) or /dev/input/event* (requires permissions).
Default behavior:
- Joystick has no default mappings
- All controls must be configured through Settings -> Input
Configuring controls:
- Start the game and go to Settings -> Input
- Navigate to any action (e.g., "Move Forward")
- Press Right arrow to select [SET]
- Press Enter
- Press the desired joystick button or move an axis
- The binding is saved immediately
Features:
- Button support (all buttons on your controller)
- Axis support (analog sticks, triggers)
- Configurable per-action with positive/negative axis direction
- Works alongside keyboard and mouse (bindings are additive)
- [RESET] button clears custom bindings
Note on button behavior:
- Movement/look actions trigger continuously while held
- Place/remove blocks auto-repeat every 200ms when held
- Next/previous block, toggle physics, and quit trigger once per press
Menu Navigation
The game features a modern, intuitive menu system with arrow key or IJKL navigation.
Main Menu
Use Arrow Keys or I/J/K/L to navigate between options:
- New World: Create a new world with custom settings
- Load World: Browse and load existing worlds
- Settings: Configure game options
- Quit Game: Exit the application
Press Enter or Space to select, ESC to quit.
Creating Worlds
New World Creation
- Navigate to and select "[ New world ]" from the main menu
- Enter a name for your world (without .dat extension)
- Set the X dimension (width) - default: 64, range: 1-256
- Set the Y dimension (height) - default: 64, range: 1-256
- Set the Z dimension (depth) - default: 64, range: 1-256
- Optionally set a seed (leave blank for random)
- Set terrain steepness - default: 1.0, range: 0.1-3.0
- The world will be generated and saved to
worlds/[name].dat
Tips:
- Larger worlds take more memory and may render slower
- Press ESC at any prompt to cancel and return to the main menu
- If you don't enter dimensions, the default 64x64x64 will be used
- The [ Back ] button is shown at the top of each prompt
- Input is responsive with backspace support
Loading Worlds
Select "[ Load world ]" to see a scrolling list of all saved worlds in the worlds/ directory.
- Use Arrow Keys or I/K to scroll through worlds
- Press Enter or Space to load the selected world
- Select [ Back ] or press ESC to return to the main menu
- If the world list is longer than the screen, scroll indicators ("^ More ^" / "v More v") appear automatically
Settings
Select "[ Settings ]" from the main menu to configure game options.
Navigation:
- Use Arrow Keys or I/K to move between options
- Press Enter or Space to cycle through values
- Select [ Back ] or press ESC to save and return
Available Options:
Field of View (FOV):
- Narrow (0.50): More zoomed in view, good for precise building
- Normal (0.66): Default balanced view (recommended)
- Wide (0.85): Wider peripheral vision
- Ultra (1.00): Maximum wide-angle view
Higher FOV values give you more peripheral vision but can cause slight distortion at the edges. Lower values are more focused but show less of your surroundings.
Render Distance:
- Tiny (40 blocks): Fastest performance
- Short (60 blocks): Better view, still fast
- Normal (80 blocks): Default balanced option
- Far (100 blocks): Maximum visibility, may reduce FPS
Sound Effects (when built with SOUND=1):
- Toggle sound effects on/off
Music (when built with SOUND=1 MUSIC=1):
- Toggle background music on/off
Show Sun (when built with FBDEV=1 or SDL_WINDOW=1):
- Toggle sun rendering in framebuffer and SDL window modes
FB Resolution (when built with FBDEV=1 or SDL_WINDOW=1):
- 160x100: Retro low-res aesthetic, very fast
- 160x120: Low-res 4:3 aspect ratio
- 320x200: Classic DOS-era resolution
- 320x240: Default 4:3 ratio (recommended)
- 480x300: Medium resolution
- 480x360: Medium 4:3 ratio
- 640x400: High resolution
- 640x480: VGA resolution, sharpest but slowest
All settings are automatically saved to options.txt and loaded on startup.
World Format
Worlds are stored in the worlds/ directory as binary files with the .dat extension.
File Structure
[Header: 6 bytes]
- Width: 2 bytes (big-endian uint16)
- Height: 2 bytes (big-endian uint16)
- Depth: 2 bytes (big-endian uint16)
[Player Position: 12 bytes]
- X: 4 bytes (big-endian int32 fixed-point)
- Y: 4 bytes (big-endian int32 fixed-point)
- Z: 4 bytes (big-endian int32 fixed-point)
[Block Data: width * height * depth bytes]
- Each byte represents a block ID (0-15)
- Stored in Z-Y-X order
Note: The game automatically saves your position when you quit (ESC). When you reload the world, you'll spawn exactly where you left off.
Block IDs
0: Air (transparent)1-15: Colored blocks corresponding to ANSI colors:- 1: Red
- 2: Green
- 3: Yellow
- 4: Blue
- 5: Magenta
- 6: Cyan
- 7: White
- 8: Bright Black (Gray)
- 9: Bright Red
- 10: Bright Green
- 11: Bright Yellow
- 12: Bright Blue
- 13: Bright Magenta
- 14: Bright Cyan
- 15: Bright White
Architecture
The codebase is organized into modular components:
- types.h: Core data structures and fixed-point math macros
- world_file.c/h: World loading, saving, and block manipulation
- world_gen.c/h: Procedural terrain generation
- display.c/h: ncurses display abstraction and framebuffer initialization
- input.c/h: Keyboard and mouse input handling
- voxel_renderer.c/h: 3D rendering engine with raycasting and lookup tables
- trig_tables.h: Pre-computed sin/cos lookup tables (2048 entries, generated)
- tables.py: Python script to generate trigonometric lookup tables
- physics.c/h: Physics engine with gravity, collision, and jumping (optional)
- sounds.c/h: PC speaker sound effects and music playback system (optional)
- sdl_beep.c/h: SDL2 audio backend for cross-platform sound (optional)
- midi_player.c/h: MIDI music playback using SDL2_mixer (optional)
- fbdev.c/h: Direct framebuffer rendering with configurable resolution (optional)
- sdl_window.c/h: SDL window rendering with texture upload and HUD (optional)
- options.c/h: Persistent settings system with load/save functionality
- main.c: Game loop, menu system, world creation, and main logic
Rendering Pipeline
- Framebuffer Creation: Allocates color, character, and depth buffers (ncurses) or RGB framebuffer (SDL/FBDEV)
- Ray Generation: Creates rays for each screen pixel based on camera orientation using lookup tables
- Raycasting: Optimized DDA algorithm with ray-world bounding box intersection
- Shading: Calculates brightness based on face orientation and distance
- Display: Outputs colored ASCII characters (ncurses), RGB pixels (FBDEV), or texture-rendered window (SDL)
Technical Details
Fixed-Point Math
The renderer uses 32-bit fixed-point arithmetic (16.16 format) for performance:
- Integer part: 16 bits
- Fractional part: 16 bits
- Allows decimal precision without floating-point overhead
Rendering Performance
- ncurses mode: 18-23 FPS, adaptive to terminal size
- FBDEV mode: 10-17 FPS at 320x240, highly resolution-dependent
- SDL window mode: 25+ FPS at 320x240, 15+ FPS at 640x480
- Trigonometric lookup tables eliminate floating-point trig overhead
- Fully fixed-point arithmetic pipeline for consistent performance
- Optimized raycasting with early termination
Tips
- Terminal Size: Larger terminals provide better visuals but may reduce FPS
- Color Support: Ensure your terminal supports 256 colors for best results
- Building: Use the highlighted block indicator to see where you're placing blocks
- Navigation: The position display in the bottom-right shows your coordinates
Known Limitations
- Terminal must support color
- Performance depends on terminal size and emulator efficiency
- No multiplayer support
- Limited to 16 block types in this version
Troubleshooting
SDL window doesn't open or shows black screen
If you compiled with SDL_WINDOW=1 but the window doesn't appear or shows only black:
-
Check SDL libraries are installed:
sudo apt-get install libsdl2-dev libsdl2-ttf-dev -
Verify font availability: SDL window mode requires DejaVu Sans Mono font. On Debian/Ubuntu:
sudo apt-get install fonts-dejavu-core -
Try different resolution: Lower resolutions (160x100, 320x200) perform better on older hardware. Adjust in settings menu before starting game.
Framebuffer mode crashes before rendering anything
Framebuffer mode can only be used in a real TTY and with a framebuffer kernel driver loaded. You can verify that the framebuffer is supported and enabled with the fbset command. It should give an output similar to the following:
mode "1920x1200"
geometry 1920 1200 1920 1200 32
timings 0 0 0 0 0 0 0
rgba 8/16,8/8,8/0,0/0
endmode
If you do not get a comparable output, use sudo modprobe <driver> to load your framebuffer driver like cirrusfb, udlfb or svgafb.
Ensure your framebuffer device is writeable for your user. If you are unsure, try the following:
sudo chmod a+rw /dev/fb0
If parts of your screen are cut off in a multi-monitor setup, you can use fbset to adjust your main screen:
fbset -xres 1920 -yres 1200 # Replace with your main screens physical resolution
On a single monitor setup, the above command will change your screens resolution. If you have a high resolution display, setting it to 800x600 and letting the monitor handle upscaling can drastically increase performance, especially with rendering devices like displaylink which have a very limited bandwidth.
Sound playback does not work
If you compiled with sound and/or music but are not getting any output, verify that the pcspkr kernel module is loaded with lsmod | grep pcspkr. If it isn't loaded, sudo modprobe pcspkr should resolve the issue. Should there still not be any output (especially on older thinkpads), check the beep volume in alsamixer.
Game doesn't display colors
Ensure your terminal supports colors. Try:
echo $TERM
Should output something like xterm-256color or linux. If not, set it:
export TERM=xterm-256color
Compilation errors
Make sure you have ncurses development headers:
Ubuntu/Debian:
sudo apt-get install libncurses5-dev
Void Linux:
sudo xbps-install ncurses-devel
macOS (assuming tigerbrew is installed):
brew install ncurses
Controls not responding
Some terminals may have issues with special key detection. The arrow keys should work in most terminal emulators. If Shift detection doesn't work for moving down, use 'Y/Z' instead.
License
This project is provided as-is with absolutely no warranty. It may be modified and/or redistributed as desired.

