# Bouncing-shapes sandbox — a tour of brolang on top of raylib. # # Click to spawn a shape under the cursor, WASD/arrows to blow them around, # SPACE to clear. The shape nearest the cursor is highlighted with its stats. # # Feature tour: native structs, enums, tagged unions + match (value, statement, # payload capture, void variants, contextual construction), optionals + unwrap, # value-loops (`yield :blk`), fallible functions with try/catch, defer, ranged # and pointer-capturing for-loops, while, break/continue, compound assignment, # scalar C interop, and multi-line strings. rl :: import "@vendor/raylib" # --- screen and physics constants ------------------------------------------- W :: 900 H :: 540 CAP usize :: 64 GRAV :: 0.18 # downward pull per frame DAMP :: 0.82 # energy kept on a wall bounce FORCE :: 0.9 # wind impulse from a key press SPINMAX :: 3.0 # spin magnitude cap RING_PAD :: 6.0 # highlight ring spacing # --- shapes ----------------------------------------------------------------- Kind :: enum { circle square triangle } Ball :: struct { x f32 y f32 dx f32 dy f32 radius f32 kind Kind } # A frame's worth of player intent, as a tagged union. Each arm carries exactly # the data that action needs (or `void` when it needs none). Command :: union(enum) { spawn rl.Vector2 # spawn a shape at this point push struct { dx f32, dy f32 } # blow every shape this way clear void idle void } # Spawning can fail when the backing array is full; the error carries the cap so # the caller can report it. SpawnError :: union(enum) { full struct { cap usize } } # value-match used as an expression source: each arm yields a Color. color_for func(k Kind) rl.Color { col :: match k { .circle: rl.Color{ r = 235, g = 90, b = 90, a = 255 } .square: rl.Color{ r = 90, g = 205, b = 130, a = 255 } .triangle: rl.Color{ r = 105, g = 160, b = 245, a = 255 } } return col } # value-match dispatching to a contextual return, used to cycle spawn kind. next_kind func(k Kind) Kind { return match k { .circle: .square .square: .triangle .triangle: .circle } } # Read this frame's input into a single Command (contextual union construction: # `.clear`, `.spawn{...}`, `.push{...}` are built against the return type). read_command func() Command { if rl.IsMouseButtonPressed(rl.MOUSE_BUTTON_LEFT) return .spawn{ rl.GetMousePosition() } if rl.IsKeyPressed(rl.KEY_SPACE) return .clear fx f32 = 0.0 fy f32 = 0.0 if rl.IsKeyDown(rl.KEY_A) fx -= FORCE if rl.IsKeyDown(rl.KEY_D) fx += FORCE if rl.IsKeyDown(rl.KEY_W) fy -= FORCE if rl.IsKeyDown(rl.KEY_S) fy += FORCE if rl.IsKeyDown(rl.KEY_LEFT) fx -= FORCE if rl.IsKeyDown(rl.KEY_RIGHT) fx += FORCE if rl.IsKeyDown(rl.KEY_UP) fy -= FORCE if rl.IsKeyDown(rl.KEY_DOWN) fy += FORCE moved :: fx != 0.0 or fy != 0.0 if (moved) return .push{ dx = fx, dy = fy } return .idle } # Fallible capacity check: returns the slot index to fill, or fails `.full`. reserve func(used usize) usize ! SpawnError { if (used >= CAP) return .full{ cap = CAP } return used } # Advance one ball: gravity, integrate, bounce off the four walls with damping. # `b` is a pointer into the array, so the writes land in place. step func(b @mut Ball) void { b.dy += GRAV b.x += b.dx b.y += b.dy if (b.x < b.radius) { b.x = b.radius b.dx = -b.dx * DAMP } right :: f32(W) - b.radius if (b.x > right) { b.x = right b.dx = -b.dx * DAMP } if (b.y < b.radius) { b.y = b.radius b.dy = -b.dy * DAMP } floor :: f32(H) - b.radius if (b.y > floor) { b.y = floor b.dy = -b.dy * DAMP } } draw_ball func(b @Ball, highlight bool) void { col :: color_for(b.kind) center rl.Vector2 = rl.Vector2{ x = b.x, y = b.y } match b.kind { .circle: rl.DrawCircleV(center, b.radius, col) .square: rl.DrawPoly(center, 4, b.radius, 45.0, col) .triangle: rl.DrawPoly(center, 3, b.radius, 0.0, col) } if highlight { ring rl.Color = rl.Color{ r = 250, g = 245, b = 200, a = 255 } rl.DrawPoly(center, 24, b.radius + RING_PAD, 0.0, ring) } } main func() i32 { rl.SetConfigFlags(rl.FLAG_MSAA_4X_HINT) rl.InitWindow(W, H, "brolang — bouncing shapes") defer rl.CloseWindow() # runs on every exit path out of main rl.SetTargetFPS(60) help :: `[click] spawn a shape [WASD/arrows] blow wind `[space] clear balls [CAP]mut Ball = undefined count usize = 0 # number of live balls, in slots 0..count kc Kind = .circle # next kind to spawn spin f32 = 1.0 # rotates spawn velocity for variety at_cap bool = false # show the "at capacity" banner bg :: rl.Color{ r = 24, g = 26, b = 34, a = 255 } text :: rl.Color{ r = 225, g = 225, b = 230, a = 255 } warn :: rl.Color{ r = 245, g = 180, b = 90, a = 255 } while !rl.WindowShouldClose() { # --- input -> command ----------------------------------------------- cmd :: read_command() match cmd { .spawn |at|: { slot :: reserve(count) catch |e| { match e { .full |info|: at_cap = true } yield CAP # sentinel: >= CAP means "didn't fit" } if (slot < CAP) { balls[slot] = Ball{ x = f32(at.x), y = f32(at.y), dx = FORCE * 6.0 * spin, dy = -FORCE * 5.0, radius = 18.0, kind = kc, } count += 1 kc = next_kind(kc) spin = -spin * 1.2 if (spin > SPINMAX or spin < -SPINMAX) spin = 1.0 at_cap = false } } .push |f|: { for (&balls) |@b, i| { if (i >= count) break b.dx += f.dx b.dy += f.dy } } .clear: { count = 0 at_cap = false } .idle: {} } # --- physics -------------------------------------------------------- for (&balls) |@b, i| { if (i >= count) break step(b) } # --- which shape is under the cursor? (optional via a value-loop) --- mouse :: rl.GetMousePosition() sel :: for 0..(count) |i| hover: { c rl.Vector2 = rl.Vector2{ x = balls[i].x, y = balls[i].y } if rl.CheckCollisionPointCircle(mouse, c, balls[i].radius) yield :hover i yield none } # --- draw ----------------------------------------------------------- rl.BeginDrawing() rl.ClearBackground(bg) for (&balls) |@b, i| { if (i >= count) break hot bool = false if sel |s| { if (s == i) hot = true # true only for the hovered ball } draw_ball(b, hot) } rl.DrawText(help, 16, 16, 20, text) if sel |s| { label :: match balls[s].kind { .circle: "circle" .square: "square" .triangle: "triangle" } rl.DrawText(label, 16, H - 36, 20, text) } if (at_cap) rl.DrawText("at capacity", W - 170, 16, 20, warn) rl.DrawFPS(W - 90, H - 28) rl.EndDrawing() } return 0 }