This project has been created as part of the 42 curriculum by mteichma.
A small top-down 2D game in C with MiniLibX: load a tile map from a .ber file, pick up every collectible, then reach the exit without touching a patrolling enemy.
-
Map format. A
.berfile is a rectangle of characters (heremaps/maps_ok/map1.ber, the map in the GIF):1wall,0floor,Pplayer start,Ccollectible,Eexit,Xenemy. The name must end in exactly.ber.11111111111 1P000000001 10111111101 10100000101 101011C0101 10101110101 1010C000101 10101110101 1010000X101 1011111C101 10000000E01 11111111111 -
Validation. Before any window opens, the map is rejected with
Errorand a reason if it is not rectangular, contains another character, does not have exactly oneP, exactly oneEand at least oneC, is not closed by walls on every border cell, or if someCor theEcannot be reached fromP. The program then exits with status 1.maps/holds one file per error case (test2_no_exit.bertotest9_no_path.ber,not_ber.txt) and a validtest1.ber;maps/maps_ok/holds five more playable maps. -
Rules.
W,A,S,Dmove one tile per key press; walls block. Walking onto aCcollects it. The exit stays closed, and blocks movement, until everyCis collected; stepping on the open exit printsYou Win!. Touching an enemy printsGame Over. In both cases the player sprite changes, input stops, and the window closes by itself a moment later.Escor the window's close button quits. -
Move counter. Every move prints
Mouvements : Non stdout and updates aMoves: Ncounter in the top-left corner of the window. -
Enemies and animations. Each
Xwalks left and right along its row and turns around at walls. The player (idle), collectibles and enemies alternate between two sprites, and the exit has closed and open sprites. -
Window size. Tiles are 64 px; the window is capped at 1280x720 and the tile size shrinks to fit larger maps.
map1.ber, 19 moves, two collectibles left |
map2.ber, two enemies |
|---|---|
![]() |
![]() |
Captured on Debian under Xvfb from the current code.
main
├─ check_args argument count, ".ber" extension, open()
│ └─ read_and_parse_map
│ ├─ store_lines get_next_line, blank lines skipped
│ ├─ check_rectangular
│ ├─ parse_lines characters, P/E/C counts, enemies
│ └─ validate_map_content counts, border walls, is_path_valid
└─ init_window window, off-screen buffer, 14 XPM sprites, hooks, mlx_loop
Parsing (map_reader.c, map_parser.c): lines are read with get_next_line into a growing array, the width comes from the first line, and every line must match it. parse_lines copies each character into the grid, counts P, E and C, and moves every X into a separate t_enemy array, leaving floor in its cell.
Path check (path_check.c): the grid is copied and a recursive four-way flood fill starts at P, marking every non-wall cell it reaches. The map is valid only if every C and the E of the original grid are marked in the copy.
Rendering loop (enemy.c, render_map.c, render_entities.c): mlx_loop_hook calls update_game on every loop iteration. It advances a frame counter, moves the enemies every 30 iterations, checks whether an enemy shares the player's tile, then redraws: every tile, then the enemies, then the player are copied into one off-screen MLX image, which is pushed to the window with a single mlx_put_image_to_window. The move counter is drawn on top with mlx_string_put.
Event hooks (window.c, events.c, player_movement.c): KeyPress calls handle_keypress, which maps the X11 keysyms of Esc and W/A/S/D to close_game or move_player; DestroyNotify (the close button) calls close_game. move_player checks can_move_to, updates the position and the counter, collects an item, and checks for victory.
Linux with an X11 session. MiniLibX is vendored in mlx_linux/ and built by the Makefile.
sudo apt install gcc make libx11-dev libxext-dev zlib1g-dev # Debian/Ubuntu
make # builds Libft, MiniLibX, then ./so_long
./so_long maps/maps_ok/map1.ber
./so_long maps/test9_no_path.ber # Error / No valid path to all collectibles or exitTargets: all (default), bonus (same as all: enemies, animations and the on-screen counter are always built), clean, fclean, re. Flags: gcc -Wall -Wextra -Werror.
- Validate everything before opening a window (
file_checks.c,map_reader.c,map_validator.c): all map errors are found beforemlx_init, so a bad map never opens a window, and the whole file is checked before any MiniLibX resource exists. - One off-screen buffer per frame (
render_map.c,render_entities.c):blit_imgcopies sprite pixels straight into a single MLX image in layer order (tiles, enemies, player), then the frame is displayed once. This avoids flicker and onemlx_put_image_to_windowcall per tile. - Enemies kept out of the grid (
map_parser.c,enemy.c): anXbecomes floor in the map and at_enemyentry with a direction, so enemies can cross collectibles and the exit without erasing them, and the path check does not treat them as walls. - Flood fill on a copy (
path_check.c): the fill writesFinto a duplicate grid, so the playable map is never modified by validation. - Frame counter as the only clock (
enemy.c,render_entities.c): enemy steps, sprite animation and the delay before closing after a win or a loss are all derived from oneglobal_framecounter.
- Speed depends on the machine. Enemy moves, animations and the end-of-game delay count loop iterations (every 30, and 240 before closing), not time.
- Sprites are cropped, not scaled.
blit_imgalways copies atile_sizesquare, but the sprites are 64x60 and the wall 64x55: the missing rows come out black, which gives the dark bands under the tiles in the screenshots. When a large map shrinks the tiles, sprites are cut instead of resized. - The exit is walkable for the path check only. The flood fill passes through
E, butcan_move_toblocks the closed exit, so a map whose only route to aCcrosses the exit is accepted and cannot be finished. - Blank lines are ignored anywhere in the file, so a map split by an empty line is accepted as if the rows were joined, and an empty file exits with status 1 without printing
Error. - Linux and WASD only. Keysyms are hard-coded in
events.c(no arrow keys, no macOS keycodes). Theplayer_move_*sprites are loaded but never drawn. The flood fill is recursive and map size is not capped, so a very large open map could overflow the stack.
- MiniLibX for Linux, vendored here in
mlx_linux/, and the community MiniLibX documentation. - Flood fill: the four-way recursive variant used for the path check.


