testbed (raylib)

This commit is contained in:
2026-06-25 16:50:29 +02:00
parent 95c6c3b746
commit 33ce1c27d1
3 changed files with 1470 additions and 1 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
heap :: import "@std/mem/heap" heap :: import "@std/mem/heap"
printf c_func(fmt *c_char, ...) c_int #printf c_func(fmt *c_char, ...) c_int
main func() i32 { main func() i32 {
memory ?*mut u8 = heap.alloc(4) memory ?*mut u8 = heap.alloc(4)
+248
View File
@@ -0,0 +1,248 @@
# 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.
# --- 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 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) Color {
col :: match k {
.circle: Color{ r = 235, g = 90, b = 90, a = 255 }
.square: Color{ r = 90, g = 205, b = 130, a = 255 }
.triangle: 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 IsMouseButtonPressed(MOUSE_BUTTON_LEFT) return .spawn{ GetMousePosition() }
if IsKeyPressed(KEY_SPACE) return .clear
fx f32 = 0.0
fy f32 = 0.0
if IsKeyDown(KEY_A) fx -= FORCE
if IsKeyDown(KEY_D) fx += FORCE
if IsKeyDown(KEY_W) fy -= FORCE
if IsKeyDown(KEY_S) fy += FORCE
if IsKeyDown(KEY_LEFT) fx -= FORCE
if IsKeyDown(KEY_RIGHT) fx += FORCE
if IsKeyDown(KEY_UP) fy -= FORCE
if IsKeyDown(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 Vector2 = Vector2{ x = b.x, y = b.y }
match b.kind {
.circle: DrawCircleV(center, b.radius, col)
.square: DrawPoly(center, 4, b.radius, 45.0, col)
.triangle: DrawPoly(center, 3, b.radius, 0.0, col)
}
if highlight {
ring Color = Color{ r = 250, g = 245, b = 200, a = 255 }
DrawPoly(center, 24, b.radius + RING_PAD, 0.0, ring)
}
}
main func() i32 {
SetConfigFlags(FLAG_MSAA_4X_HINT)
InitWindow(W, H, "brolang — bouncing shapes")
defer CloseWindow() # runs on every exit path out of main
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 :: Color{ r = 24, g = 26, b = 34, a = 255 }
text :: Color{ r = 225, g = 225, b = 230, a = 255 }
warn :: Color{ r = 245, g = 180, b = 90, a = 255 }
while !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 :: GetMousePosition()
sel :: for 0..(count) |i| blk: {
c Vector2 = Vector2{ x = balls[i].x, y = balls[i].y }
if CheckCollisionPointCircle(mouse, c, balls[i].radius) yield :blk i
yield none
}
# --- draw -----------------------------------------------------------
BeginDrawing()
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)
}
DrawText(help, 16, 16, 20, text)
if sel |s| {
label :: match balls[s].kind {
.circle: "circle"
.square: "square"
.triangle: "triangle"
}
DrawText(label, 16, H - 36, 20, text)
}
if (at_cap) DrawText("at capacity", W - 170, 16, 20, warn)
DrawFPS(W - 90, H - 28)
EndDrawing()
}
return 0
}
File diff suppressed because it is too large Load Diff