feat/maze-representation #170

Merged
OragonEfreet merged 16 commits from feat/maze-representation into main 2026-07-30 21:21:55 +02:00
Owner
No description provided.
OragonEfreet added this to the 1.1 milestone 2026-07-30 21:05:32 +02:00
Introduce the core maze data structure, designed to encode any of the
topologies from Jamis Buck's "Mazes for Programmers" (orthogonal, polar,
hex, triangular, 3D, weave, wrapped surfaces) behind a single type.

The representation splits into three layers:

  - Topology: a fixed-stride adjacency array, `degree` slots per cell,
    holding neighbour indices or BJ_MAZE_NO_CELL. Built once, then
    immutable. Slot index carries direction semantics per family.
  - State: one byte per cell, bit s marking a passage through slot s.
    The only mutable part of a maze.
  - Attributes: none. Cells are dense uint32_t indices, so per-cell data
    (distances, weights, marks) is a caller-owned parallel array.

Fixed stride is preferred over CSR: degrees are small and bounded, fill
is high, and O(1) slot access without an offset indirection matters for
the random-access generation algorithms. Geometry lives in a private
per-family tag plus union, so coordinate helpers can validate the family
instead of trusting caller-supplied dimensions.

Passages are always bidirectional; the reciprocal slot is resolved
through a per-family callback table, since no arithmetic rule holds
across all topologies (polar inward/outward is the counter-example).
Opening a border slot is refused, keeping the link-implies-adjacency
invariant that dead-end detection relies on.

This commit covers the orthogonal 2D family only: constructor,
destructor, coordinate lookup, open/close/query, and the accessors a
caller needs to run its own traversals. Generation algorithms and
rendering come later.

Also drops the stray BANJO_EXPORT on bj_allocate_bitmap's definition,
which was the only one in src/ and inconsistent with the rest.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add the generation half of the maze API, plus the tests and the RNG
groundwork it needed.

An algorithm is a callback, so the built-in ones and a caller's own are
the same kind of thing. Generation is driven a step at a time rather than
run to completion behind the caller's back, which lets a program spread
it over frames and watch the maze appear; bj_run_maze_builder is there
for callers that only want the result.

A step reports whether there is more to do, rather than whether it is
finished. The two read alike but fail differently: a callback that
returns nothing in particular ends generation early instead of spinning
forever, and the same holds for the guards on a bad context.

Algorithm state lives in the context as a single block the callback
allocates and the builder frees, so an abandoned generation leaks
nothing and no callback releases its own state. Binary tree is the first
algorithm to use it, with a cursor and nothing else.

Wire the error system through maze creation: dimensions that cannot
work and allocation failures now say so. Build steps deliberately stay
out of it, since they run once per cell and the error system allocates;
a step that cannot continue logs and stops. Note that a failed call has
to be recognised by its return value rather than by the error being
set — when memory runs out, the error object cannot be allocated either.

Also add to the RNG:

  - bj_uniform_uint32_distribution, for drawing cell indices, which can
    exceed the range the signed variant covers.
  - bj_rand_generator, exposing the global engine to the distribution
    API. It combines three draws because bj_rand yields 15 bits, and a
    single cast leaves the high bits clear — which collapses every
    rejection-based distribution onto its lower bound.
  - A fix for signed overflow in bj_uniform_int32_distribution when the
    span exceeds INT32_MAX: the high half was narrowed before being
    added to the lower bound. Only observable under UBSan.

Tests cover the maze representation, the builder lifecycle, and the
properties a generated maze must hold — every cell reachable, exactly
one passage fewer than there are cells, and the same seed giving the
same maze.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the maze example stub with a program that watches a maze being
carved rather than showing the finished result. Each frame runs one build
step and redraws only when the step reports having done something, which
is the reason bj_step_maze_builder answers that question rather than
"are we there yet".

Cells still fully walled are drawn as solid blocks, so the algorithm
sweeping the grid is visible as it goes.

The algorithms live in a table of function pointers, so the further ones
each cost a line here once they exist, and the layout already arranges
several mazes side by side. The window follows resizes: registering the
resize callback also fires it once, so the layout is computed in exactly
one place for both the first frame and every later size.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Polling for events also blocked until the display's next refresh: on
macOS by waiting on a CADisplayLink semaphore, on Windows by calling
DwmFlush. Together with bj_set_frame_rate that left two clocks deciding
how long a frame lasts, and the two beat against each other.

On macOS the pacing did not even work. The semaphore starts at zero, the
display link signals it on every refresh, and poll consumes one per
frame. A loop slower than the display banks the difference — around 1500
unused ticks a minute at 30 Hz on a 60 Hz screen — so the wait returns
immediately and nothing is paced at all. It only ever blocks once that
credit runs dry, which is what happens when the display slows down: a
window that goes idle, is occluded, or sits on a variable-refresh panel.
The result was a loop that ran free most of the time and stalled for a
refresh interval, or the whole 100 ms timeout, at moments that had
nothing to do with the program.

Pacing belongs to bj_set_frame_rate, which already measures the
iteration and sleeps off the remainder. Poll now returns as soon as the
queue is empty, matching what X11 and Wayland already did, so the same
frame rate request behaves the same way on all four backends.

The display link and DwmFlush existed only to feed those waits, so both
go, along with the runtime dwmapi.dll dependency they needed.

Note that presentation is no longer aligned to the display refresh. It
was not reliably aligned before, since presenting is a framebuffer blit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every mouse event landing on a banjo window was marked handled and never
forwarded to AppKit. Dragging a border, dragging the title bar and the
three window buttons are AppKit's own doing and need those events, so
none of them worked: the resize cursor appeared over the border, and
nothing followed.

Only key events need swallowing, which is what the beep the code set out
to avoid actually comes from. Mouse events never beep, so they now go
through after banjo has pushed its own.

BanjoView implements none of the mouse methods, so forwarding cannot
dispatch anything twice.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The example already reserved a band under every maze for a caption but
drew nothing in it. Fill it with the algorithm's name, so a window
showing several of them side by side says which is which.

The caption rectangle is derived from the maze's own, so it follows the
window through a resize without repeating the layout arithmetic.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An algorithm is now three functions and its settings, handed over
together. Setup prepares whatever the algorithm remembers between steps,
teardown releases it, and the builder never looks inside either. What
used to be a documented convention — allocate one block, we will free it
— is now something the shape of the API enforces, and an algorithm may
keep its state in whatever form suits it.

Setup also runs during creation rather than on the first step, which is
where refusals belong: a family it cannot carve, or directions that
describe no sweep, now make bj_create_maze_builder fail and say why.
A step could only log and stop, since it runs too often to afford the
error system.

Carrying the settings in the description is what lets each algorithm
take its own settings type. Passing a binary tree's pair of directions
to sidewinder used to compile and quietly carve the wrong maze; the two
structs have the same shape, so nothing caught it. It is a compile error
now.

The description travels by value, so one assembled at the call site
needs no name and no lifetime:

    bj_create_maze_builder(maze, bj_maze_algorithm_sidewinder(&s), 0, 0, 0);

Add restarting a builder in place, which is the same teardown and setup
run again. It leaves the maze untouched: a builder does not own it, and
carving twice over the same grid is a legitimate thing to want. Starting
clean is bj_close_maze, added here along with bj_open_maze for the
wall-adding algorithms that begin from an open field. Note that the
outer boundary needs neither: edge cells have no neighbour to open
towards, so it survives on its own.

Add sidewinder, and with it the run-drawing that made the state fit in
three integers: a run member is drawn as the run grows, so the run never
has to be stored, however far apart its cells lie.

Split the sources by subject, with a private header for what they share,
and let the example show a maze being carved: each algorithm named under
its own grid, SPACE to generate again, the engine seeded per run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat(maze): add Wilson's algorithm and document the maze API
Some checks failed
C++ Compatibility / C++ Compatibility Check (pull_request) Failing after 7s
Documentation / build-docs (pull_request) Failing after 8s
QA / cert (pull_request) Successful in 29s
Build and Test / linux-linux-gcc / shared (pull_request) Successful in 17s
Build and Test / linux-linux-gcc / static (pull_request) Successful in 17s
Build and Test / linux-windows-mingw / shared (pull_request) Successful in 25s
Build and Test / linux-windows-mingw / static (pull_request) Successful in 27s
0bcd72a17c
Wilson walks from a cell outside the maze until it runs into it, drops
whatever loops it made on the way, and carves what is left. It draws
every maze a grid admits as evenly as Aldous-Broder does and usually
gets there sooner. The loops it erases give their marks back, so a
program watching a generation sees a discarded walk disappear rather
than linger over cells that were never carved.

Both headers now carry Doxygen throughout. maze.h explains what the
representation is and is not — dense cell indices, neighbours in
numbered slots, passages as bits — and documents the seven exports that
had nothing, among them the invariant the drawing and the dead-end count
rely on: a passage always leads somewhere, so a border slot cannot be
opened. maze_builder.h gains an overview of what a builder is for, why a
step reports having worked rather than having finished, and which of the
four algorithms holds for which family. Two of its samples called
bj_create_maze_builder with the old five-argument signature and would
not have compiled.

The example gains a tutorial header in the style of the others and a
layout meant to be read next to it.

Changelog entries carry their issue numbers, except the example, which
has no card yet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ci
All checks were successful
C++ Compatibility / C++ Compatibility Check (pull_request) Successful in 7s
Documentation / build-docs (pull_request) Successful in 8s
QA / cert (pull_request) Successful in 29s
Build and Test / linux-linux-gcc / shared (pull_request) Successful in 17s
Build and Test / linux-linux-gcc / static (pull_request) Successful in 17s
Build and Test / linux-windows-mingw / shared (pull_request) Successful in 27s
Build and Test / linux-windows-mingw / static (pull_request) Successful in 26s
dd4f3596cb
OragonEfreet deleted branch feat/maze-representation 2026-07-30 21:21:55 +02:00
OragonEfreet referenced this pull request from a commit 2026-07-30 21:21:56 +02:00
OragonEfreet referenced this pull request from a commit 2026-07-31 08:43:38 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
OragonEfreet/banjo!170
No description provided.