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.
127 lines
4.5 KiB
Markdown
127 lines
4.5 KiB
Markdown
# 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
|
|
|
|
```rust
|
|
#[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.
|
|
|
|
```rust
|
|
#[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
|
|
|
|
```rust
|
|
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:
|
|
|
|
```rust
|
|
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.
|
|
|
|
```rust
|
|
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:
|
|
|
|
```rust
|
|
assert!(renderer.model_is_loaded("kenney/racing/vehicle-truck-yellow"));
|
|
```
|
|
|
|
## Bot playtest
|
|
|
|
```rust
|
|
#[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.
|