A falling-block puzzle game where gravity is driven by system memory pressure. The fuller your machine's memory, the faster the pieces drop.
Single C source file. Builds on Linux and Windows with no external library on Windows and only ncurses on Linux.
memblocks - gravity from /proc/meminfo
| . . . . . . . . . .| SCORE 1400
| . . . . . . . . . .| LINES 6
| . . . .[] . . . . .|
| . . . .[] . . . . .| NEXT
| . . .[][] . . . . .| [][]
| . . . . . . . . . .| [][]
| . . . . . . . . . .|
| . . . . . . . . . .| SYSTEM MEMORY
| . . . . . . . . . .| 2300 / 3997 MB used (57.5%)
| . . . . . . . . . .| [########### ]
| . . . . . . . . . .| held 2048/2048 MB free 1441 MB
| . . . .:: . . . . .|
| . . . .:: . . . . .| GRAVITY
| . . .:::: . . . . .| [########## ]
----------------------- 386 ms x2.1 top x26.7
It started as a way to make operating system concepts visible. The gauge is a live readout of memory state, and the balloon allocator lets you push on that state and watch the game react. The interesting part is not the game loop, it is that reserving memory does nothing until pages are actually touched.
sudo apt install libncurses-dev
cc -Wall -Wextra -O2 -o memblocks memblocks.c $(pkg-config --libs ncurses)
Use pkg-config rather than a bare -lncurses. Some distributions split
terminfo into a separate library, and on those a plain -lncurses fails at
link time with undefined references to setupterm or tgetent.
gcc -Wall -Wextra -O2 -o memblocks.exe memblocks.c
No -l flag and no library to install. The Win32 console backend is compiled
into the source. Get a compiler from MSYS2 (pacman -S mingw-w64-x86_64-gcc),
from w64devkit, or from the Code::Blocks installer whose filename contains
mingw-setup. The plain Code::Blocks installer ships no compiler at all.
To use PDCurses instead of the built-in backend:
gcc -DUSE_PDCURSES -O2 -o memblocks.exe memblocks.c -lpdcurses
Often the easiest route if you already have a Linux box.
sudo apt install mingw-w64
x86_64-w64-mingw32-gcc -Wall -Wextra -O2 -o memblocks.exe memblocks.c
Copy the resulting .exe to the Windows machine. Verify it first:
file memblocks.exe # want: PE32+ executable (console) x86-64
The result depends only on KERNEL32.dll and msvcrt.dll, both of which ship
with Windows, so nothing needs to be distributed alongside it.
./memblocks # Linux
.\memblocks.exe # Windows, from PowerShell
Needs a terminal at least 59 columns by 24 rows.
Run the self-test first if anything looks wrong. It skips the display entirely, so error messages stay readable:
./memblocks --selftest
meminfo: 3997 MB total, 320 MB used (8.0%)
defaults: reserve 256 MB, high anchor 0.872, drop 800 -> 30 ms (top x26.7)
all checks passed
| Key | Action |
|---|---|
| Left, Right | Move |
Up, x |
Rotate clockwise |
c |
Rotate counter-clockwise |
| Down | Soft drop |
| Space | Hard drop |
a |
Allocate one chunk of memory |
A |
Allocate four chunks |
z |
Release one chunk |
Z |
Release everything |
p |
Pause |
r |
Restart |
q |
Quit |
Hold A for a few seconds and watch the gravity bar climb.
| Flag | Default | Meaning |
|---|---|---|
-l LOW |
usage at startup | Memory fraction mapped to the slowest drop |
-h HIGH |
highest reachable | Memory fraction mapped to the fastest drop |
-B MS |
800 | Slowest drop interval |
-F MS |
30 | Fastest drop interval |
-g CURVE |
1.5 | Response shape. 0 is linear, higher ramps up sooner |
-m MB |
256 | Size of one balloon allocation |
-r MB |
5% of RAM, min 256 | Memory to leave available, never allocated |
-M MB |
0 | Ceiling on total balloon size, 0 means no ceiling |
-p MS |
6 | Page-in budget per frame |
--selftest |
Run logic checks without opening a terminal |
Examples:
./memblocks -F 15 -g 3 # much more aggressive speed-up
./memblocks -g 0 # linear response
./memblocks -r 1024 # leave a gigabyte alone
./memblocks -M 2048 # never hold more than 2 GB
Memory usage is sampled twice a second and smoothed with an exponential moving
average, otherwise the interval jitters on every cache fluctuation. The usage
fraction is mapped onto the drop interval between -B and -F.
The low anchor defaults to whatever usage is at launch. Without that, a machine sitting at 60% used would start the game already sprinting. The high anchor is computed from the reserve, so pressure 1.0 lands exactly at the last allocation the reserve permits. A fixed anchor left the top of the speed range unreachable.
The response curve is p * (1 + k) / (1 + k * p), which maps 0 to 0 and 1 to 1
for any k greater than -1, so bending it cannot break the endpoints. This is
used in preference to pow() so the program does not need to link libm.
Measured on a 4 GB machine with default settings:
held used drop speed
0 MB 226 MB 5.7% 800 ms x1.0
523 MB 735 MB 18.4% 588 ms x1.4
1052 MB 1279 MB 32.0% 404 ms x2.0
1578 MB 1782 MB 44.6% 280 ms x2.9
2257 MB 2474 MB 61.9% 159 ms x5.0
3328 MB 3589 MB 89.8% 30 ms x26.7
The last row is where the reserve stops further allocation, which is also where the speed range tops out.
Available, not free. Free memory on Linux is nearly always small and says
nothing useful, because the kernel uses spare RAM for page cache. The program
reads MemAvailable from /proc/meminfo, which accounts for reclaimable cache
and slab. On Windows it uses GlobalMemoryStatusEx and reads ullAvailPhys,
which is what Task Manager calls Available and counts the standby list. The two
are close analogues, though Windows keeps a much larger standby list, so the
same physical machine reports different startup figures under each OS.
Reserving memory does not consume it. Both mmap and VirtualAlloc return
address space that no physical frame backs until something writes to it. The
balloon allocator therefore walks each allocation and stores one byte per page.
That single line is what actually moves the gauge:
base[b->faulted] = 1; /* one write per page faults it */Faulting is spread across frames. Doing it up front with MAP_POPULATE
measured about 700 ms for 256 MB, so pressing A would have frozen the game for
nearly three seconds. Pages are touched under a per-frame time budget instead,
which keeps input live and makes the memory bar fill visibly rather than jump.
The reserve accounts for pending pages. Memory that is mapped but not yet
faulted has not appeared in MemAvailable yet, so it is subtracted before
deciding whether another allocation is allowed. Without that, a burst of
keypresses commits far past the limit.
Pieces carry their own background colour. Drawing them as a foreground colour on the terminal default made the white piece invisible on light themes and the blue one unreadable on dark ones. Solid cells are now black on the piece colour, so contrast does not depend on the terminal theme.
Four operations differ between the two systems. Everything else is one copy of the source.
| Linux | Windows | |
|---|---|---|
| Memory | /proc/meminfo |
GlobalMemoryStatusEx |
| Reserve pages | mmap |
VirtualAlloc |
| Release pages | munmap |
VirtualFree |
| Page size | sysconf(_SC_PAGESIZE) |
GetSystemInfo |
| Clock | clock_gettime |
QueryPerformanceCounter |
| Display | ncurses | built-in Win32 console backend |
clock_gettime lives in libwinpthread on MinGW rather than the default
libraries, which is why Windows uses the performance counter. getopt is
replaced by a small hand-rolled parser because MSVC has none and MinGW's
differs in error reporting. %zu is avoided throughout because MinGW's C
runtime handles it inconsistently.
Builds clean under -Wall -Wextra -Wpedantic and against -std=c90, c99
and c11.
fatal error: ncurses.h: No such file or directory
The development headers are missing. The runtime library being present is not
enough. sudo apt install libncurses-dev, or dnf install ncurses-devel on
Fedora.
undefined reference to 'stdscr', 'wmove', 'waddch'
Compiling worked, linking did not. The ncurses library is not being linked. In
an IDE, put ncurses under Linker settings, not compiler options, and set it on
the top-level project rather than a single build target.
The exe will not run on Windows, or Windows asks which program to open it It is probably a Linux binary. Check the first four bytes:
$b = [IO.File]::ReadAllBytes(".\yourfile")[0..3]
(($b | % { '0x{0:X2}' -f $_ }) -join ' ')0x4D 0x5A is a Windows executable. 0x7F 0x45 0x4C 0x46 is Linux ELF, which
means it was compiled with a Linux toolchain and has to be rebuilt with a
Windows one. The source is portable; the binary is not.
terminal too small: need at least 59 cols x 24 rows
Resize the window. The classic 80x25 console clears it by a single row.
A console window flashes and vanishes
The program exited immediately and took its error message with it. Run it from
a terminal rather than double-clicking, and start with --selftest.
SmartScreen warns about the exe
Signing does not fix this. Microsoft removed the instant reputation that EV
certificates used to grant, so signed and unsigned files both have to build
reputation, and every rebuild resets it. SmartScreen fires on Mark of the Web,
which is attached to downloaded files. Copy the exe across the filesystem
instead of downloading it, or run Unblock-File .\memblocks.exe.
The memory reading looks too small
If you built and ran inside WSL, /proc/meminfo reports the VM's memory budget
rather than the host's, typically half of physical RAM. Raise it with
memory=12GB in .wslconfig, or build a Windows binary and run it from
PowerShell, where it reports actual Windows memory.
The balloon allocator will not drive available memory below the reserve, which
defaults to 5% of RAM with a floor of 256 MB. Hammering the allocate key stops
at the limit rather than triggering the OOM killer, and all memory is returned
on exit or on interrupt. Lower it with -r at your own risk.