Nine agent skills for building Makepad/Rust games, ported from majidmanzarpour/threejs-game-skills. Same director-routed workflow and premium bar; runtime rewritten for this fork's game crates. - makepad-game-director entrypoint, routing, continuity, asset probe - makepad-gameplay-systems loop, movers vs rigid bodies, input, camera, netplay - makepad-aaa-graphics-builder lighting, shaders, budget, visual scorecard - makepad-game-ui-designer HUD, menus, touch and XR UI - makepad-debug-profiler defect bisection and profiling - makepad-qa-release verification ladder, evidence, packaging - makepad-3d-generator CC0 model search, casts, procedural geometry - makepad-image-generator texgen textures, palettes, sky, icons - makepad-audio-generator sample bank, mixer, material impacts, 3D audio Written against the real APIs in libs/game/*, libs/sim and apps/arcade: the game.* verb table, GameRenderer adaptive quality, the packed 6-float GameMeshVertex layout, script_mod! splash styling, makepad-test driving, and the BUDGETS.md numbers. No paid generation API is required - the CC0 library plus seeded makepad-game-gen replaces them. Includes install.sh (Codex/Claude), validate-skills.sh and a repo-aware probe_assets.sh; both scripts verified against this checkout.
4.5 KiB
Test harness
Which kind of test
| Claim | Test |
|---|---|
| A rule holds (physics, scoring, generation, netcode) | unit test in the Cx-free crate |
| Two systems compose correctly | integration test with both crates as dev-deps |
| The app responds to input | makepad-test driven test |
| It looks right | driven test + screenshot() + your inspection |
| It is fast enough | probe example in --release + frame_p90_ms() |
Prefer the lowest rung that can express the claim. Driven tests are slow and flaky relative to unit tests; use them for what only they can prove.
Unit tests
#[test]
fn checkpoint_counts_once_per_crossing() {
let mut kit = RaceKit::new(/* .. */);
cross(&mut kit, 0);
cross(&mut kit, 0); // same checkpoint again
assert_eq!(kit.standings()[0].lap, 0, "re-crossing must not advance the lap");
}
#[test]
fn generator_is_deterministic() {
let a = generate_level(1234);
let b = generate_level(1234);
assert_eq!(a, b, "same seed must give byte-identical output");
}
Determinism tests matter more here than in most codebases: netplay and (preset, seed, position) replication both depend on them, and a non-deterministic generator fails silently until a second player joins.
Integration tests
When the claim spans crates, add the other crate as a dev-dependency and test the composition. The house precedent: makepad-game-blocks dev-depends on gen and render because "a walker can enter a REAL generated room" and "the car's body does not float above the road" are claims about real geometry meeting real physics. Split across two crates they become two half-tests that both pass while the car hovers — which is exactly what happened.
#[test]
fn car_body_rests_on_the_road() {
let road = makepad_game_gen::/* generate real road */;
let car = Car::new(CarConfig::default());
let settled = settle(car, &road, 120);
let lowest = lowest_vertex_y(&real_glb_body(), settled.transform());
assert!((lowest - road_surface_y).abs() < 0.05, "car floats or sinks: {lowest}");
}
Driven tests
use makepad_test::{makepad_test, TestApp, KeyCode, Selector};
#[makepad_test]
fn pause_and_resume(app: TestApp) {
app.wait_for_log_contains("game: ready");
app.press_key(KeyCode::Escape);
app.wait_for_log_contains("state: paused");
let paused = app.screenshot();
app.press_key(KeyCode::Escape);
app.wait_for_log_contains("state: playing");
println!("paused shot: {}", paused.display());
}
Available on TestApp: type_text, press_return, press_key, press_key_with_modifiers, touch_down, screenshot, widget_dump, widget_snapshot, wait_for_log_contains, locator(Selector), forward(Vec<StudioToApp>). Each has a try_ variant returning TestResult for when a failure is expected.
widget_dump() prints the tree — use it to discover ids rather than guessing selectors.
Synchronisation
Never sleep. Emit one log line per state transition and wait on it:
log!("game: ready");
log!("state: paused");
log!("score: {}", score);
log!("race: won");
This costs one line per transition and converts the entire game flow into something machine-checkable. It is the highest-value habit in this document.
Asset-dependent tests
They skip without the library, which is correct for CI and dangerous for reporting.
fn require_assets() -> Option<AssetIndex> {
let idx = load_index().ok()?;
if idx.is_empty() {
eprintln!("SKIP: run ./apps/arcade/download_assets.sh for real-asset coverage");
return None;
}
Some(idx)
}
Check test output for skips before reporting a pass. And assert loading, since a bad id degrades silently:
assert!(renderer.model_is_loaded("kenney/racing/vehicle-truck-yellow"));
Bot playtest
#[makepad_test]
fn five_minute_soak(app: TestApp) {
app.wait_for_log_contains("game: ready");
for _ in 0..300 {
drive_one_second(&app);
assert!(!app.widget_dump().contains("panic"));
}
app.wait_for_log_contains("soak: complete");
}
Catches what unit tests structurally cannot: softlocks, memory growth, frame-time drift, and incomplete reset() (win twice in a row).
Flakiness
A flaky test is worse than no test — it trains everyone to ignore failures. Causes here: sleeping instead of waiting on a log; asserting exact floats (use an epsilon); depending on iteration order of an unordered container; assuming assets exist; and racing the first frame. Fix the cause; never add a retry.