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:
parent
c6b21b9a6b
commit
859730f02d
10 changed files with 1039 additions and 0 deletions
85
tests/benchmarks/gauntlet/trials/__init__.py
Normal file
85
tests/benchmarks/gauntlet/trials/__init__.py
Normal 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]
|
||||
88
tests/benchmarks/gauntlet/trials/animation_storm.py
Normal file
88
tests/benchmarks/gauntlet/trials/animation_storm.py
Normal 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()
|
||||
92
tests/benchmarks/gauntlet/trials/entity_swarm.py
Normal file
92
tests/benchmarks/gauntlet/trials/entity_swarm.py
Normal 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()
|
||||
87
tests/benchmarks/gauntlet/trials/grid_titan.py
Normal file
87
tests/benchmarks/gauntlet/trials/grid_titan.py
Normal 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()
|
||||
74
tests/benchmarks/gauntlet/trials/pathfinder_rush.py
Normal file
74
tests/benchmarks/gauntlet/trials/pathfinder_rush.py
Normal 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()
|
||||
100
tests/benchmarks/gauntlet/trials/sightline_siege.py
Normal file
100
tests/benchmarks/gauntlet/trials/sightline_siege.py
Normal 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()
|
||||
95
tests/benchmarks/gauntlet/trials/ui_avalanche.py
Normal file
95
tests/benchmarks/gauntlet/trials/ui_avalanche.py
Normal 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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue