Gauntlet #340: spec, package skeleton, scoring, six trials

Add the DESIGN.md (spec copy + implementation deviations), the trials package
(Trial base + ordered registry + six stress trials: entity swarm, animation
storm, grid titan, pathfinder rush, ui avalanche, sightline siege), and the
RampController/scoring logic. All pure Python under tests/benchmarks/gauntlet/.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
John McCardle 2026-07-02 20:19:30 -04:00
commit 859730f02d
10 changed files with 1039 additions and 0 deletions

View file

@ -0,0 +1,85 @@
"""Trial base class and ordered TRIALS registry for The Gauntlet.
Each trial stress-tests one engine subsystem. The harness owns all Timers; a
trial only implements setup / set_load / tick / teardown and holds no timers of
its own, so teardown fully disposes its scene contents and trials never
contaminate each other.
"""
class Trial:
# -- identity (override in subclasses) --------------------------------
key = "trial"
name = "TRIAL"
unit = "units"
accent = (200, 200, 200)
description = "one line"
# -- ramp parameters --------------------------------------------------
base_load = 50
growth = 1.6
# -- lifecycle --------------------------------------------------------
def __init__(self):
self.scene = None
self.ui = None
self.load = 0
def setup(self, scene, ui):
"""Build the arena under `scene`. `ui` is scene.children."""
self.scene = scene
self.ui = ui
def set_load(self, level_value):
"""Create/destroy stress objects so the live load matches level_value."""
raise NotImplementedError
def tick(self, dt_ms):
"""Periodic simulation work (100 ms sim cadence). Default: no-op."""
pass
def teardown(self):
"""Remove everything this trial created."""
if self.scene is not None:
children = self.scene.children
# Clear any grids' entity collections first.
for child in list(children):
ents = getattr(child, "entities", None)
if ents is not None:
try:
while len(ents):
ents.remove(ents[len(ents) - 1])
except Exception:
pass
while len(children):
children.remove(children[len(children) - 1])
self.scene = None
self.ui = None
self.load = 0
# -- helpers ----------------------------------------------------------
def _count_children(self):
return len(self.ui) if self.ui is not None else 0
# Populated at import time from the individual trial modules.
from .entity_swarm import EntitySwarm
from .animation_storm import AnimationStorm
from .grid_titan import GridTitan
from .pathfinder_rush import PathfinderRush
from .ui_avalanche import UIAvalanche
from .sightline_siege import SightlineSiege
TRIALS = [
EntitySwarm,
AnimationStorm,
GridTitan,
PathfinderRush,
UIAvalanche,
SightlineSiege,
]
def make_trials():
"""Return a fresh instance of every trial, in order."""
return [cls() for cls in TRIALS]

View file

@ -0,0 +1,88 @@
"""Trial 2: ANIMATION STORM -- animation manager stress.
N small Frames, each running two concurrent, self-relaunching animations
(position with random easing + a fill_color pulse). Load = live animations = 2N.
"""
import random
import mcrfpy
from . import Trial
ARENA_X, ARENA_Y = 20, 110
ARENA_W, ARENA_H = 984, 620
BOX = 16
class AnimationStorm(Trial):
key = "animation_storm"
name = "ANIMATION STORM"
unit = "animations"
accent = (229, 85, 157)
description = "N frames x 2 self-sustaining animations"
base_load = 60
growth = 1.6
def setup(self, scene, ui):
super().setup(scene, ui)
self.rng = random.Random(0xA817)
self.frames = []
self.live = set()
self.easings = [
mcrfpy.Easing.LINEAR, mcrfpy.Easing.EASE_IN_OUT_SINE,
mcrfpy.Easing.EASE_OUT_CUBIC, mcrfpy.Easing.EASE_IN_OUT_BACK,
mcrfpy.Easing.EASE_OUT_BOUNCE,
]
def _new_frame(self):
x = self.rng.uniform(ARENA_X, ARENA_X + ARENA_W - BOX)
y = self.rng.uniform(ARENA_Y, ARENA_Y + ARENA_H - BOX)
fr = mcrfpy.Frame(pos=(x, y), size=(BOX, BOX))
fr.fill_color = mcrfpy.Color(self.rng.randint(80, 255),
self.rng.randint(40, 200),
self.rng.randint(120, 255))
self.ui.append(fr)
self.frames.append(fr)
self.live.add(fr)
self._launch_pos(fr)
self._launch_color(fr)
def _launch_pos(self, fr):
if fr not in self.live:
return
tx = self.rng.uniform(ARENA_X, ARENA_X + ARENA_W - BOX)
dur = self.rng.uniform(0.4, 1.2)
fr.animate("x", tx, dur, self.rng.choice(self.easings),
callback=self._on_pos_done)
def _launch_color(self, fr):
if fr not in self.live:
return
tgt = float(self.rng.choice((40, 255)))
dur = self.rng.uniform(0.3, 0.9)
fr.animate("fill_color.r", tgt, dur, mcrfpy.Easing.EASE_IN_OUT_SINE,
callback=self._on_color_done)
def _on_pos_done(self, target, prop, val):
self._launch_pos(target)
def _on_color_done(self, target, prop, val):
self._launch_color(target)
def set_load(self, level_value):
target_frames = max(1, int(level_value) // 2)
while len(self.frames) < target_frames:
self._new_frame()
while len(self.frames) > target_frames:
fr = self.frames.pop()
self.live.discard(fr)
try:
self.ui.remove(fr)
except Exception:
pass
self.load = len(self.frames) * 2
def teardown(self):
self.live = set()
self.frames = []
super().teardown()

View file

@ -0,0 +1,92 @@
"""Trial 1: ENTITY SWARM -- entity step + render stress.
One 40x25 grid, N entities with SEEK behavior chasing a wandering target via a
Dijkstra map, advanced with grid.step() on the sim tick. Load = entity count.
"""
import random
import mcrfpy
from . import Trial
GRID_W, GRID_H = 40, 25
CELL = 24 # display px per cell (zoom applied)
ARENA_X, ARENA_Y = 40, 110
SEEK = int(mcrfpy.Behavior.SEEK)
class EntitySwarm(Trial):
key = "entity_swarm"
name = "ENTITY SWARM"
unit = "entities"
accent = (245, 165, 36)
description = "N seeking entities, grid.step() every tick"
base_load = 60
growth = 1.6
def setup(self, scene, ui):
super().setup(scene, ui)
self.rng = random.Random(0xE117)
self.ticks = 0
self.target = (GRID_W // 2, GRID_H // 2)
grid = mcrfpy.Grid(grid_size=(GRID_W, GRID_H),
pos=(ARENA_X, ARENA_Y),
size=(GRID_W * CELL, GRID_H * CELL))
grid.zoom = CELL / 16.0
grid.fill_color = mcrfpy.Color(18, 22, 30)
for y in range(GRID_H):
for x in range(GRID_W):
c = grid.at(x, y)
edge = (x == 0 or y == 0 or x == GRID_W - 1 or y == GRID_H - 1)
c.walkable = not edge
c.transparent = not edge
ui.append(grid)
self.grid = grid
self.entities = []
self.dmap = grid.get_dijkstra_map(self.target)
grid.center_camera((GRID_W / 2.0, GRID_H / 2.0))
def _spawn(self):
while True:
x = self.rng.randint(1, GRID_W - 2)
y = self.rng.randint(1, GRID_H - 2)
if (x, y) != self.target:
break
e = mcrfpy.Entity((x, y), grid=self.grid)
e.sprite_index = self.rng.choice((84, 85, 86, 100, 101))
e.move_speed = 0.0
e.set_behavior(SEEK, pathfinder=self.dmap)
self.entities.append(e)
def set_load(self, level_value):
target_n = max(1, int(level_value))
while len(self.entities) < target_n:
self._spawn()
while len(self.entities) > target_n:
e = self.entities.pop()
try:
e.die()
except Exception:
pass
self.load = len(self.entities)
def _move_target(self):
self.target = (self.rng.randint(1, GRID_W - 2),
self.rng.randint(1, GRID_H - 2))
self.grid.clear_dijkstra_maps()
self.dmap = self.grid.get_dijkstra_map(self.target)
for e in self.entities:
e.set_behavior(SEEK, pathfinder=self.dmap)
def tick(self, dt_ms):
self.ticks += 1
if self.ticks % 6 == 0:
self._move_target()
self.grid.step()
def teardown(self):
self.entities = []
self.grid = None
self.dmap = None
super().teardown()

View file

@ -0,0 +1,87 @@
"""Trial 3: GRID TITAN -- grid render + layer-write stress.
Square SxS grid with a TileLayer base and a ColorLayer overlay. Every tick a
32x32 ColorLayer region is rewritten (rolling window) and the camera orbits so
render chunks keep invalidating. Load = S (grid side); cells = S*S.
"""
import math
import mcrfpy
from . import Trial
VIEW_PX = 620
ARENA_X, ARENA_Y = 200, 115
REGION = 32
class GridTitan(Trial):
key = "grid_titan"
name = "GRID TITAN"
unit = "grid side"
accent = (53, 193, 214)
description = "SxS grid, rolling color region + orbiting camera"
base_load = 20
growth = 1.4
def setup(self, scene, ui):
super().setup(scene, ui)
self.grid = None
self.color = None
self.angle = 0.0
self.roll = 0
self.side = 0
def _build(self, side):
if self.grid is not None:
try:
self.ui.remove(self.grid)
except Exception:
pass
self.side = side
grid = mcrfpy.Grid(grid_size=(side, side),
pos=(ARENA_X, ARENA_Y),
size=(VIEW_PX, VIEW_PX),
texture=mcrfpy.default_texture)
grid.zoom = VIEW_PX / float(side * 16)
grid.fill_color = mcrfpy.Color(10, 12, 18)
base = mcrfpy.TileLayer(z_index=-2, name="base", texture=mcrfpy.default_texture)
grid.add_layer(base)
base.fill(0)
color = mcrfpy.ColorLayer(z_index=-1, name="overlay")
grid.add_layer(color)
color.fill(mcrfpy.Color(20, 26, 38, 120))
self.ui.append(grid)
self.grid = grid
self.color = color
grid.center_camera((side / 2.0, side / 2.0))
def set_load(self, level_value):
side = max(4, int(level_value))
self._build(side)
self.load = side
def tick(self, dt_ms):
if self.grid is None:
return
s = self.side
# Orbit the camera around the grid centre.
self.angle += 0.18
r = s * 0.15
cx, cy = s / 2.0, s / 2.0
self.grid.center_camera((cx + r * math.cos(self.angle),
cy + r * math.sin(self.angle)))
# Rewrite a rolling REGIONxREGION color window.
w = min(REGION, s)
span = max(1, s - w)
self.roll = (self.roll + 3) % span
ox = self.roll
oy = (self.roll * 2) % span
phase = (self.angle * 40) % 255
col = mcrfpy.Color(int(phase), int(255 - phase), 160, 180)
self.color.fill_rect((ox, oy), (w, w), col)
def teardown(self):
self.grid = None
self.color = None
super().teardown()

View file

@ -0,0 +1,74 @@
"""Trial 4: PATHFINDER RUSH -- A* query stress.
A static maze grid; every tick issues Q grid.find_path queries between random
walkable pairs. Load = queries per tick.
"""
import random
import mcrfpy
from . import Trial
GRID_W, GRID_H = 60, 40
CELL = 15
ARENA_X, ARENA_Y = 60, 120
class PathfinderRush(Trial):
key = "pathfinder_rush"
name = "PATHFINDER RUSH"
unit = "queries/tick"
accent = (76, 194, 110)
description = "A* find_path queries across a static maze"
base_load = 5
growth = 1.6
def setup(self, scene, ui):
super().setup(scene, ui)
self.rng = random.Random(0x9A2E)
self.queries = 0
grid = mcrfpy.Grid(grid_size=(GRID_W, GRID_H),
pos=(ARENA_X, ARENA_Y),
size=(GRID_W * CELL, GRID_H * CELL))
grid.zoom = CELL / 16.0
grid.fill_color = mcrfpy.Color(10, 14, 20)
floor = mcrfpy.ColorLayer(z_index=-1, name="floor")
grid.add_layer(floor)
floor.fill(mcrfpy.Color(40, 54, 44))
wall_c = mcrfpy.Color(14, 20, 16)
self.walkables = []
for y in range(GRID_H):
for x in range(GRID_W):
c = grid.at(x, y)
wall = (x == 0 or y == 0 or x == GRID_W - 1 or y == GRID_H - 1)
# Pillar/room maze: isolated blockers keep the map connected but windy.
if not wall and x % 4 == 2 and y % 3 == 1:
wall = True
c.walkable = not wall
c.transparent = not wall
if wall:
floor.set((x, y), wall_c)
else:
self.walkables.append((x, y))
ui.append(grid)
self.grid = grid
def set_load(self, level_value):
self.queries = max(1, int(level_value))
self.load = self.queries
def tick(self, dt_ms):
if self.grid is None:
return
w = self.walkables
for _ in range(self.queries):
a = w[self.rng.randrange(len(w))]
b = w[self.rng.randrange(len(w))]
self.grid.find_path(a, b)
def teardown(self):
self.grid = None
self.walkables = []
super().teardown()

View file

@ -0,0 +1,100 @@
"""Trial 6: SIGHTLINE SIEGE -- FOV + perspective writeback stress.
A grid with scattered walls; N entities each recompute an active FOV (radius 10)
via update_visibility() on the sim tick as they random-walk. Load = FOV entities.
"""
import random
import mcrfpy
from . import Trial
GRID_W, GRID_H = 50, 32
CELL = 18
ARENA_X, ARENA_Y = 60, 118
SIGHT = 10
STEPS = ((1, 0), (-1, 0), (0, 1), (0, -1))
class SightlineSiege(Trial):
key = "sightline_siege"
name = "SIGHTLINE SIEGE"
unit = "FOV entities"
accent = (229, 72, 77)
description = "N random-walking entities, per-entity FOV recompute"
base_load = 20
growth = 1.6
def setup(self, scene, ui):
super().setup(scene, ui)
self.rng = random.Random(0xF0F0)
grid = mcrfpy.Grid(grid_size=(GRID_W, GRID_H),
pos=(ARENA_X, ARENA_Y),
size=(GRID_W * CELL, GRID_H * CELL),
texture=mcrfpy.default_texture)
grid.zoom = CELL / 16.0
grid.fill_color = mcrfpy.Color(8, 10, 16)
grid.fov_radius = SIGHT
floor = mcrfpy.ColorLayer(z_index=-1, name="floor")
grid.add_layer(floor)
floor.fill(mcrfpy.Color(38, 30, 34))
wall_c = mcrfpy.Color(16, 12, 14)
self.walkables = []
for y in range(GRID_H):
for x in range(GRID_W):
c = grid.at(x, y)
wall = (x == 0 or y == 0 or x == GRID_W - 1 or y == GRID_H - 1)
if not wall and self.rng.random() < 0.16:
wall = True
c.walkable = not wall
c.transparent = not wall
if wall:
floor.set((x, y), wall_c)
else:
self.walkables.append((x, y))
ui.append(grid)
self.grid = grid
self.entities = []
def _spawn(self):
x, y = self.walkables[self.rng.randrange(len(self.walkables))]
e = mcrfpy.Entity((x, y), grid=self.grid)
e.sprite_index = 90
e.sight_radius = SIGHT
e.move_speed = 0.0
e.update_visibility()
self.entities.append(e)
def set_load(self, level_value):
target_n = max(1, int(level_value))
while len(self.entities) < target_n:
self._spawn()
while len(self.entities) > target_n:
e = self.entities.pop()
try:
e.die()
except Exception:
pass
self.load = len(self.entities)
def tick(self, dt_ms):
g = self.grid
for e in self.entities:
x, y = e.grid_x, e.grid_y
dirs = list(STEPS)
self.rng.shuffle(dirs)
for dx, dy in dirs:
nx, ny = x + dx, y + dy
if 0 <= nx < GRID_W and 0 <= ny < GRID_H and g.at(nx, ny).walkable:
e.grid_x = nx
e.grid_y = ny
break
e.update_visibility()
def teardown(self):
self.entities = []
self.grid = None
self.walkables = []
super().teardown()

View file

@ -0,0 +1,95 @@
"""Trial 5: UI AVALANCHE -- UI hierarchy + draw-call stress.
Nested Frame trees (depth 5), each level carrying a Caption and a Sprite, plus a
z-order shuffle of the top-level frames every tick to defeat render caching.
Load = total UI elements.
"""
import random
import mcrfpy
from . import Trial
ARENA_X, ARENA_Y = 20, 108
COL_W, ROW_H = 132, 132
COLS = 7
DEPTH = 5
PER_TREE = 15 # 1 root frame + 5*(caption+sprite) + 4 nested frames
class UIAvalanche(Trial):
key = "ui_avalanche"
name = "UI AVALANCHE"
unit = "elements"
accent = (154, 110, 245)
description = "Depth-5 frame trees, z-shuffled every tick"
base_load = 60
growth = 1.6
def setup(self, scene, ui):
super().setup(scene, ui)
self.rng = random.Random(0x5A1A)
self.trees = [] # list of root Frames
self.total = 0
def _build_tree(self, index):
col = index % COLS
row = index // COLS
x = ARENA_X + col * COL_W
y = ARENA_Y + row * ROW_H
root = mcrfpy.Frame(pos=(x, y), size=(120, 120))
root.fill_color = mcrfpy.Color(self.rng.randint(30, 90),
self.rng.randint(20, 70),
self.rng.randint(60, 120))
root.outline = 1
root.outline_color = mcrfpy.Color(154, 110, 245)
self.ui.append(root)
count = 1
parent = root
size = 120
for d in range(DEPTH):
cap = mcrfpy.Caption(text="L%d" % d, pos=(4, 2))
cap.fill_color = mcrfpy.Color(220, 220, 240)
parent.children.append(cap)
count += 1
spr = mcrfpy.Sprite(pos=(4, 18), texture=mcrfpy.default_texture,
sprite_index=self.rng.randint(0, 120))
parent.children.append(spr)
count += 1
if d < DEPTH - 1:
size -= 18
child = mcrfpy.Frame(pos=(10, 30), size=(size, size))
child.fill_color = mcrfpy.Color(self.rng.randint(30, 90),
self.rng.randint(20, 70),
self.rng.randint(60, 120))
parent.children.append(child)
count += 1
parent = child
self.trees.append(root)
self.total += count
def set_load(self, level_value):
target = max(PER_TREE, int(level_value))
while self.total < target:
self._build_tree(len(self.trees))
# Shed whole trees while we can stay at or above target.
while self.trees and (self.total - PER_TREE) >= target:
root = self.trees.pop()
try:
self.ui.remove(root)
except Exception:
pass
self.total -= PER_TREE
self.load = self.total
def tick(self, dt_ms):
# Shuffle z-order of top-level frames to invalidate caches.
order = list(range(len(self.trees)))
self.rng.shuffle(order)
for z, root in zip(order, self.trees):
root.z_index = z
def teardown(self):
self.trees = []
self.total = 0
super().teardown()