Introduction

Jugar (Spanish: "to play") is a WASM-native universal game engine built on the Batuta Sovereign AI Stack. It compiles to a single .wasm binary with ABSOLUTE ZERO JavaScript dependencies, making it ideal for secure, deterministic game development.

Why Jugar?

  • Pure WASM: Single .wasm binary with zero JavaScript
  • Mobile-First to Ultrawide: Scales from phones to 49" 32:9 monitors
  • Batuta Ecosystem: Built on trueno (SIMD/GPU compute) and aprender (ML/AI)
  • Extreme TDD: 1500+ tests, 95%+ coverage, mutation testing
  • Toyota Way: Quality-first design principles baked in

Key Features

FeatureDescription
ECS ArchitectureHigh-performance Entity-Component-System
PhysicsWebGPU → WASM-SIMD → Scalar tiered backends
AI SystemsGOAP, Behavior Trees, Steering Behaviors
Responsive UIAnchor-based layouts for any screen
Spatial Audio2D positional audio with channel mixing
ProcgenNoise, dungeons, Wave Function Collapse
TestingProbar: Rust-native WASM game testing

Quick Example

use jugar::prelude::*;

fn main() {
    let mut engine = JugarEngine::new(JugarConfig::default());

    engine.run(|ctx| {
        if ctx.input().key_pressed(KeyCode::Escape) {
            return LoopControl::Exit;
        }
        LoopControl::Continue
    });
}

Design Philosophy: The Toyota Way

"The right process will produce the right results." — Toyota Way

PrincipleApplication in Jugar
Mieruka (Visual Control)Telemetry overlays in dev builds
Poka-Yoke (Error Proofing)Type-safe APIs prevent bugs at compile time
Jidoka (Autonomation)Fail-fast on invalid state
Heijunka (Leveling)Fixed timestep for deterministic physics
Genchi Genbutsu (Go & See)Examples as source of truth

Next Steps

Quick Start

Get up and running with Jugar in 5 minutes.

Prerequisites

  • Rust 1.70+ with wasm32-unknown-unknown target
  • (Optional) wasm-pack for web builds
# Add WASM target
rustup target add wasm32-unknown-unknown

# Install wasm-pack (optional, for web builds)
cargo install wasm-pack

Create a New Project

cargo new my-game
cd my-game

Add Dependencies

Add Jugar to your Cargo.toml:

[dependencies]
jugar = "0.1"

Write Your First Game

Replace src/main.rs:

use jugar::prelude::*;

fn main() {
    // Create engine with default 1920x1080 configuration
    let mut engine = JugarEngine::new(JugarConfig::default());

    // Spawn a player entity
    let player = engine.world_mut().spawn();
    engine.world_mut().add_component(player, Position::new(100.0, 100.0));
    engine.world_mut().add_component(player, Velocity::new(0.0, 0.0));

    // Run the game loop
    engine.run(|ctx| {
        // Handle input
        let input = ctx.input();
        let mut vel = Vec2::ZERO;

        if input.key_held(KeyCode::W) { vel.y -= 1.0; }
        if input.key_held(KeyCode::S) { vel.y += 1.0; }
        if input.key_held(KeyCode::A) { vel.x -= 1.0; }
        if input.key_held(KeyCode::D) { vel.x += 1.0; }

        // Update velocity component
        // ... game logic

        LoopControl::Continue
    });
}

Build and Run

Native Build

cargo run

WASM Build

# Build for WASM target
cargo build --target wasm32-unknown-unknown --release

# Verify no JavaScript in output
ls target/wasm32-unknown-unknown/release/*.wasm

Next Steps

Installation

Requirements

  • Rust: 1.70 or later
  • Target: wasm32-unknown-unknown

Setup

1. Install Rust

If you don't have Rust installed:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

2. Add WASM Target

rustup target add wasm32-unknown-unknown

3. Install Development Tools (Optional)

# For web builds
cargo install wasm-pack

# For quality tooling
cargo install cargo-llvm-cov cargo-mutants cargo-nextest

# Install all tools at once
make install-tools

Adding Jugar to Your Project

From crates.io

[dependencies]
jugar = "0.1"

From Git (Latest)

[dependencies]
jugar = { git = "https://github.com/paiml/jugar" }

Local Development

If you're contributing or developing locally:

[dependencies]
jugar = { path = "../jugar/crates/jugar" }

Feature Flags

Jugar supports optional features:

[dependencies]
jugar = { version = "0.1", features = ["full"] }
FeatureDescription
defaultCore engine functionality
aiGOAP and Behavior Trees
audioSpatial 2D audio
procgenProcedural generation
fullAll features

Verify Installation

Create a test project:

cargo new jugar-test
cd jugar-test
echo 'jugar = "0.1"' >> Cargo.toml
cargo build --target wasm32-unknown-unknown

If the build succeeds with no JavaScript output, you're ready to go!

Your First Game

Let's build a simple game step by step to understand Jugar's core concepts.

Game Concept

We'll create a simple "Collect the Coins" game:

  • Player moves with WASD or arrow keys
  • Coins spawn randomly
  • Collect coins to increase score

Step 1: Setup

use jugar::prelude::*;

// Define game components
#[derive(Clone, Copy)]
struct Player;

#[derive(Clone, Copy)]
struct Coin;

#[derive(Clone, Copy)]
struct Score(u32);

fn main() {
    let config = JugarConfig {
        width: 800,
        height: 600,
        title: "Coin Collector".to_string(),
        ..Default::default()
    };

    let mut engine = JugarEngine::new(config);

    // Initialize game state
    setup(&mut engine);

    engine.run(game_loop);
}

Step 2: Setup Function

#![allow(unused)]
fn main() {
fn setup(engine: &mut JugarEngine) {
    let world = engine.world_mut();

    // Spawn player at center
    let player = world.spawn();
    world.add_component(player, Player);
    world.add_component(player, Position::new(400.0, 300.0));
    world.add_component(player, Velocity::new(0.0, 0.0));

    // Spawn initial coins
    for _ in 0..5 {
        spawn_coin(world);
    }

    // Add score resource
    world.add_resource(Score(0));
}

fn spawn_coin(world: &mut World) {
    let x = fastrand::f32() * 800.0;
    let y = fastrand::f32() * 600.0;

    let coin = world.spawn();
    world.add_component(coin, Coin);
    world.add_component(coin, Position::new(x, y));
}
}

Step 3: Game Loop

#![allow(unused)]
fn main() {
fn game_loop(ctx: &mut GameContext) -> LoopControl {
    // Handle input
    handle_input(ctx);

    // Update physics
    update_movement(ctx);

    // Check collisions
    check_coin_collection(ctx);

    LoopControl::Continue
}

fn handle_input(ctx: &mut GameContext) {
    let input = ctx.input();
    let world = ctx.world_mut();

    let speed = 200.0; // pixels per second
    let mut vel = Vec2::ZERO;

    if input.key_held(KeyCode::W) || input.key_held(KeyCode::ArrowUp) {
        vel.y -= speed;
    }
    if input.key_held(KeyCode::S) || input.key_held(KeyCode::ArrowDown) {
        vel.y += speed;
    }
    if input.key_held(KeyCode::A) || input.key_held(KeyCode::ArrowLeft) {
        vel.x -= speed;
    }
    if input.key_held(KeyCode::D) || input.key_held(KeyCode::ArrowRight) {
        vel.x += speed;
    }

    // Update player velocity
    for (_, (_, velocity)) in world.query::<(&Player, &mut Velocity)>() {
        *velocity = Velocity(vel);
    }
}

fn update_movement(ctx: &mut GameContext) {
    let dt = ctx.delta_time();
    let world = ctx.world_mut();

    for (_, (pos, vel)) in world.query::<(&mut Position, &Velocity)>() {
        pos.x += vel.x * dt;
        pos.y += vel.y * dt;
    }
}

fn check_coin_collection(ctx: &mut GameContext) {
    let world = ctx.world_mut();
    let collect_radius = 20.0;

    // Get player position
    let player_pos = world.query::<(&Player, &Position)>()
        .iter()
        .next()
        .map(|(_, (_, pos))| *pos);

    let Some(player_pos) = player_pos else { return };

    // Find coins to collect
    let mut collected = Vec::new();
    for (entity, (_, coin_pos)) in world.query::<(&Coin, &Position)>() {
        let dx = player_pos.x - coin_pos.x;
        let dy = player_pos.y - coin_pos.y;
        let dist = (dx * dx + dy * dy).sqrt();

        if dist < collect_radius {
            collected.push(entity);
        }
    }

    // Remove collected coins and update score
    for entity in collected {
        world.despawn(entity);
        if let Some(score) = world.get_resource_mut::<Score>() {
            score.0 += 1;
        }
        // Spawn a new coin
        spawn_coin(world);
    }
}
}

Step 4: Build and Run

# Native
cargo run

# WASM
cargo build --target wasm32-unknown-unknown --release

Testing Your Game

Add tests using Probar:

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use super::*;
    use jugar_probar::Assertion;

    #[test]
    fn test_coin_collection_increases_score() {
        let mut engine = JugarEngine::new(JugarConfig::default());
        setup(&mut engine);

        let initial_score = engine.world().get_resource::<Score>().unwrap().0;

        // Simulate collecting a coin
        // ... test logic

        let assertion = Assertion::equals(&initial_score, &0);
        assert!(assertion.passed);
    }
}
}

Next Steps

WASM Build

Jugar compiles to pure WASM with ABSOLUTE ZERO JavaScript. This guide covers building and deploying to the web.

Build Commands

Basic WASM Build

cargo build --target wasm32-unknown-unknown --release
wasm-pack build --target web --out-dir pkg

Using Make

# Build WASM
make build-wasm

# Build for web with wasm-pack
make build-web

Output Verification

Verify your build has zero JavaScript:

# Should only show .wasm files
ls target/wasm32-unknown-unknown/release/*.wasm

# Verify with make target
make verify-no-js

Web Deployment

Minimal HTML Loader

Jugar games require a minimal HTML loader that:

  • Fetches and instantiates the WASM module
  • Forwards browser events to WASM
  • Renders output from WASM

Important: The HTML loader contains ZERO game logic. All computation happens in WASM.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>My Jugar Game</title>
    <style>
        body { margin: 0; overflow: hidden; }
        canvas { display: block; }
    </style>
</head>
<body>
    <canvas id="canvas"></canvas>
    <script type="module">
        import init, { WebPlatform } from './pkg/my_game.js';

        async function main() {
            await init();
            const platform = new WebPlatform();

            // Event forwarding only - no game logic
            document.addEventListener('keydown', e => platform.key_down(e.code));
            document.addEventListener('keyup', e => platform.key_up(e.code));

            function frame(timestamp) {
                const commands = platform.frame(timestamp, '[]');
                // Render commands from WASM
                requestAnimationFrame(frame);
            }
            requestAnimationFrame(frame);
        }
        main();
    </script>
</body>
</html>

Local Testing

# Build and serve
make build-web
make serve-web

# Open http://localhost:8080

Performance Targets

MetricTarget
WASM Binary Size< 2 MiB
Gzipped Size< 500 KB
Cold Start< 100ms
Frame Rate60 FPS minimum

Optimization

Size Optimization

In Cargo.toml:

[profile.release]
opt-level = 'z'  # Optimize for size
lto = true       # Link-time optimization
codegen-units = 1
panic = 'abort'

Use wasm-opt

wasm-opt -Oz -o optimized.wasm input.wasm

Tiered Backend Selection

Jugar automatically selects the best compute backend at runtime:

TierBackendCapability
1WebGPU compute shaders10,000+ rigid bodies
2WASM SIMD 128-bit1,000+ rigid bodies
3Scalar fallbackBasic physics

Detection happens automatically via trueno capability probing.

Architecture

Jugar follows a layered architecture built on the Batuta Sovereign AI Stack.

Layer Stack

┌─────────────────────────────────────────────────────────────────────────┐
│                    JUGAR WASM BUNDLE (Single .wasm file)                │
│                         NO JAVASCRIPT WHATSOEVER                        │
├─────────────────────────────────────────────────────────────────────────┤
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐     │
│  │  Game Loop  │  │  AI Agents  │  │   Render    │  │ Responsive  │     │
│  │  (ECS)      │  │  (GOAP/BT)  │  │  (Viewport) │  │  UI Layout  │     │
│  └──────┬──────┘  └──────┬──────┘  └──────┬──────┘  └──────┬──────┘     │
│         │                │                │                │            │
│  ┌──────┴────────────────┴────────────────┴────────────────┴──────┐     │
│  │                         jugar (entry)                           │     │
│  ├─────────────────────────────────────────────────────────────────┤     │
│  │  jugar-core   │  jugar-physics  │  jugar-ai    │  jugar-render  │     │
│  │  jugar-input  │  jugar-audio    │  jugar-ui    │  jugar-procgen │     │
│  └─────────────────────────────────────────────────────────────────┘     │
│                                  │                                       │
│                          BATUTA ECOSYSTEM                                │
│                    (trueno v0.7+ / aprender v0.14+)                     │
└─────────────────────────────────────────────────────────────────────────┘

Crate Structure

CrateDescriptionDependencies
jugarMain entry point, JugarEngineAll crates
jugar-coreECS, Game Loop, Statehecs, glam
jugar-physicsRigid body simulationtrueno
jugar-aiGOAP, Behavior Treesaprender
jugar-renderViewport, Anchorstrueno-viz
jugar-uiWidget systemjugar-render
jugar-inputTouch/Mouse/KB/Gamepad-
jugar-audioSpatial 2D audio-
jugar-procgenNoise, Dungeons, WFC-
jugar-yamlDeclarative game definitions-
jugar-probarWASM-native testingwasmtime
jugar-webWeb platform bindingswasm-bindgen

Core Components

JugarEngine

The main engine struct that orchestrates all systems:

#![allow(unused)]
fn main() {
pub struct JugarEngine {
    world: World,           // ECS world
    config: JugarConfig,    // Engine configuration
    time: TimeState,        // Fixed timestep tracking
    input: InputState,      // Input handling
}
}

Game Loop

Jugar uses a fixed timestep loop (Heijunka principle):

#![allow(unused)]
fn main() {
// Internal loop structure
loop {
    // Accumulate time
    accumulator += delta_time;

    // Fixed timestep updates
    while accumulator >= FIXED_DT {
        physics_update(FIXED_DT);
        ai_update(FIXED_DT);
        accumulator -= FIXED_DT;
    }

    // Variable timestep render
    render(accumulator / FIXED_DT);
}
}

ECS (Entity-Component-System)

Built on hecs for high-performance entity management:

#![allow(unused)]
fn main() {
// Spawn entity with components
let entity = world.spawn();
world.add_component(entity, Position::new(0.0, 0.0));
world.add_component(entity, Velocity::new(1.0, 0.0));

// Query entities
for (entity, (pos, vel)) in world.query::<(&mut Position, &Velocity)>() {
    pos.x += vel.x * dt;
    pos.y += vel.y * dt;
}
}

Physics Backend Selection

Runtime capability detection selects the optimal backend:

┌─────────────────────────────────────────────────────────┐
│  Backend Selection (Automatic)                          │
├─────────────────────────────────────────────────────────┤
│  1. Check WebGPU support                                │
│     ├─► Available: Use compute shaders (10K+ bodies)    │
│     └─► Not available: Continue                         │
│  2. Check WASM SIMD support                             │
│     ├─► Available: Use SIMD 128-bit (1K+ bodies)        │
│     └─► Not available: Continue                         │
│  3. Use scalar fallback (basic physics)                 │
└─────────────────────────────────────────────────────────┘

Responsive Design

Jugar supports mobile-first to 32:9 ultrawide:

Safe Area Calculation

#![allow(unused)]
fn main() {
// 16:9 gameplay area with peripheral extension
let safe_area = viewport.calculate_safe_area(AspectRatio::HD_16_9);
let extended = viewport.calculate_extended_area();  // For ultrawide

// UI anchors adapt to screen dimensions
let anchor = Anchor::BottomCenter { margin: 20.0 };
let pos = anchor.calculate(viewport.dimensions());
}

Universal Scaling Model

  • Gameplay Layer: 16:9 safe area with peripheral extension for ultrawide
  • UI Layer: Anchor-based responsive layout scaling on shortest dimension
  • Input: Touch/Click abstraction with virtual joysticks on touch devices only

Zero JavaScript Policy

Jugar enforces ABSOLUTE ZERO JavaScript in all game computation. This is a critical architectural constraint.

Why Zero JavaScript?

ProblemJavaScriptPure WASM
DeterminismNon-deterministicFully deterministic
GC PausesUnpredictable pausesNo garbage collector
SecurityLarge attack surfaceSandboxed execution
PerformanceJIT variabilityPredictable performance
ReplayDifficultFrame-perfect replay

What's Forbidden

❌ FORBIDDEN                         ✅ REQUIRED
─────────────────────────────────────────────────────
• JavaScript files (.js/.ts)         • Pure Rust only
• npm/node_modules/package.json      • wasm32-unknown-unknown
• Any JS bundler                     • web-sys (Rust bindings)
• JS interop beyond web-sys          • Single .wasm binary output
• wasm-bindgen-futures               • Pure async
• gloo, bevy, macroquad, ggez        • Batuta stack components

What's Allowed

The only JavaScript permitted is a minimal HTML loader that:

  1. Fetches and instantiates the WASM module
  2. Forwards browser events to WASM (keydown, mouse, touch)
  3. Renders output commands from WASM

Zero game logic in JavaScript. All computation happens in WASM.

Verification

Automated Check

make verify-no-js

This checks for:

  • Standalone .js files (excluding wasm-pack output)
  • .ts files (excluding .d.ts type definitions)
  • package.json in project root
  • node_modules directory
  • Forbidden crates in Cargo.toml

Manual Verification

# Check for JavaScript files
find . -name "*.js" -not -path "./target/*" -not -path "*/pkg/*"

# Check for TypeScript files
find . -name "*.ts" -not -name "*.d.ts" -not -path "./target/*"

# Check for npm artifacts
ls package.json node_modules 2>/dev/null

Forbidden Crates

These crates violate the zero-JS policy:

CrateReason
wasm-bindgen-futuresRelies on JS promises
glooJavaScript wrapper library
bevyLarge with JS dependencies
macroquadJavaScript glue required
ggezNot pure WASM

Use Batuta Stack Instead

All functionality comes from the Batuta ecosystem:

NeedUse
SIMD/GPU computetrueno
ML/AI algorithmsaprender
Renderingtrueno-viz
Platform abstractionpresentar-core
Data loadingalimentar
Asset registrypacha

Benefits

  1. Deterministic Replay: Record inputs, replay exactly
  2. Testing: Probar can test without a browser
  3. Security: No JavaScript attack surface
  4. Performance: Predictable, no GC pauses
  5. Size: Single .wasm file < 2MB

Toyota Way Principles

Jugar embodies the Toyota Production System (TPS) principles to ensure quality and reliability.

"The right process will produce the right results." — Toyota Way

Core Principles

Mieruka (Visual Control)

"Make problems visible"

In Jugar:

  • Telemetry overlays enabled by default in dev builds
  • Visual debugging shows physics bodies, AI paths, collision shapes
  • No hidden state - everything is inspectable
#![allow(unused)]
fn main() {
// Enable debug visualization
let config = JugarConfig {
    debug_overlay: true,
    show_physics_bodies: true,
    show_ai_paths: true,
    ..Default::default()
};
}

Poka-Yoke (Error Proofing)

"Mistake-proof the process"

In Jugar:

  • Rust's type system prevents null pointers and unhandled errors
  • Option<T> and Result<T, E> enforce explicit error handling
  • Type-safe entity selectors in Probar
#![allow(unused)]
fn main() {
// Compile-time error prevention
let entity: Entity = world.spawn();  // Cannot be null
let pos: Option<&Position> = world.get_component(entity);

// Must handle the Option
match pos {
    Some(p) => println!("Position: {:?}", p),
    None => println!("Entity has no position"),
}
}

Jidoka (Autonomation)

"Stop and fix problems immediately"

In Jugar:

  • Fail-fast on invalid state with console_error_panic_hook
  • Never continue with corrupted state
  • Tests fail immediately on assertion failure
#![allow(unused)]
fn main() {
// Fail-fast on invalid state
impl World {
    pub fn add_component<T>(&mut self, entity: Entity, component: T) {
        if !self.is_alive(entity) {
            panic!("Cannot add component to dead entity");  // Jidoka
        }
        // ...
    }
}
}

Heijunka (Leveling)

"Level the workload"

In Jugar:

  • Fixed timestep logic (fixed_dt) ensures consistent physics
  • Works identically on 30fps mobile and 144Hz monitors
  • No frame-rate dependent behavior
#![allow(unused)]
fn main() {
const FIXED_DT: f32 = 1.0 / 60.0;  // 60 updates per second

// Physics runs at fixed rate regardless of frame rate
while accumulator >= FIXED_DT {
    physics_update(FIXED_DT);
    accumulator -= FIXED_DT;
}
}

Genchi Genbutsu (Go & See)

"Go to the source to understand"

In Jugar:

  • examples/ directory is the source of truth
  • Every feature has a working example
  • Documentation follows code, not the other way around
# See how physics works
cargo run --example physics_demo

# See how AI works
cargo run --example ai_behavior_tree

# See Probar in action
cargo run --example pong_simulation -p jugar-probar

Just-in-Time (JIT)

"Produce only what is needed, when needed"

In Jugar:

  • Assets are streamed on-demand
  • Render pipelines are compiled lazily
  • Minimal startup time on mobile
#![allow(unused)]
fn main() {
// Lazy asset loading
let texture = assets.load_texture("player.png");  // Deferred
// Texture only loads when first used
}

Kaizen (Continuous Improvement)

"Always be improving"

In Jugar:

  • Hot-reloadable assets for rapid iteration
  • State serialization for checkpoint/restore
  • Makefile kaizen target for improvement analysis
# Run kaizen analysis
make kaizen

# Outputs:
# - Code metrics
# - Coverage analysis
# - Complexity analysis
# - Technical debt grading
# - Improvement recommendations

Quality Metrics

MetricTargetPrinciple
Test Coverage≥95%Poka-Yoke
Mutation Score≥80%Jidoka
SATD Comments0Kaizen
Unsafe Code0Poka-Yoke
TDG GradeA+Kaizen

Tiered Workflow

Inspired by Toyota's quality gates:

# Tier 1: ON-SAVE (sub-second)
make tier1  # Type check + fast tests

# Tier 2: ON-COMMIT (1-5 minutes)
make tier2  # Full validation

# Tier 3: ON-MERGE (hours)
make tier3  # Mutation testing + benchmarks

Batuta Stack Integration

Jugar is built on the Batuta Sovereign AI Stack - a collection of pure Rust crates for AI and compute.

Overview

┌─────────────────────────────────────────────────────────────────┐
│                    BATUTA SOVEREIGN AI STACK                     │
│                   (USE THESE COMPONENTS FIRST)                   │
├─────────────────────────────────────────────────────────────────┤
│  trueno v0.7+     │  SIMD/GPU compute primitives (MANDATORY)    │
│  aprender v0.14+  │  ML algorithms, behavior trees (MANDATORY)  │
│  trueno-viz       │  WebGPU/WebGL2 rendering                    │
│  presentar-core   │  Platform abstraction, event loop           │
│  alimentar        │  Data loading with encryption               │
│  pacha            │  Asset registry with signatures             │
└─────────────────────────────────────────────────────────────────┘

Mandatory Components

trueno

SIMD and GPU compute primitives. Required for physics acceleration.

# Cargo.toml
[dependencies]
trueno = "0.7"

# Or for local development
trueno = { path = "../batuta/crates/trueno" }

Usage:

#![allow(unused)]
fn main() {
use trueno::prelude::*;

// SIMD-accelerated vector operations
let a = Vec4::new(1.0, 2.0, 3.0, 4.0);
let b = Vec4::new(5.0, 6.0, 7.0, 8.0);
let c = a + b;  // Uses SIMD when available
}

aprender

ML algorithms and behavior trees. Required for AI systems.

[dependencies]
aprender = "0.14"

Usage:

#![allow(unused)]
fn main() {
use aprender::prelude::*;

// Behavior tree
let tree = BehaviorTree::new()
    .selector()
        .sequence()
            .condition(|ctx| ctx.health < 20)
            .action(|ctx| ctx.flee())
        .end()
        .action(|ctx| ctx.patrol())
    .end()
    .build();
}

Dependency Decision Tree

Need a capability?
    │
    ├─► Does batuta stack have it? ──► YES ──► USE IT (mandatory)
    │                                    │
    │                                    └─► Extend it if needed
    │
    └─► NO ──► Can we build it in pure Rust? ──► YES ──► Build it
                                                  │
                                                  └─► NO ──► REJECT
                                                        (find another way)

Forbidden Crates

Never import these - they violate zero-JS or use non-Batuta components:

CrateReason
bevyHeavy, JS dependencies
macroquadJavaScript glue required
ggezNot pure WASM
wasm-bindgen-futuresJS promise dependency
glooJavaScript wrapper

Version Synchronization

Keep Batuta dependencies in sync:

# Check current versions
cargo tree | grep trueno
cargo tree | grep aprender

# Check latest versions
cargo search trueno
cargo search aprender

# Update
cargo update trueno aprender

Local Development

For contributing to both Jugar and Batuta:

# Cargo.toml - Use local paths
[dependencies]
trueno = { path = "../batuta/crates/trueno" }
aprender = { path = "../batuta/crates/aprender" }

For releases, switch to crates.io:

# Cargo.toml - Use crates.io
[dependencies]
trueno = "0.7"
aprender = "0.14"

Verification

# Verify batuta dependencies are used
make verify-batuta-deps

# Outputs:
# ✅ trueno dependency found
# ✅ aprender dependency found
# ✅ Using local batuta components (recommended for development)

Crates Overview

Jugar is organized as a workspace with multiple specialized crates.

Crate Structure

crates/
├── jugar/               # Main entry point
├── jugar-core/          # ECS, Game Loop, Components
├── jugar-physics/       # Rigid body simulation
├── jugar-ai/            # GOAP, Behavior Trees
├── jugar-render/        # Viewport, Anchors
├── jugar-ui/            # Widget system
├── jugar-input/         # Touch/Mouse/KB/Gamepad
├── jugar-audio/         # Spatial 2D audio
├── jugar-procgen/       # Noise, Dungeons, WFC
├── jugar-yaml/          # Declarative game definitions
├── jugar-probar/        # WASM-native testing
├── jugar-probar-derive/ # Proc-macro for type-safe selectors
├── jugar-web/           # Web platform bindings
└── physics-toy-sandbox/ # Physics demo crate

Summary Table

CrateDescriptionTests
jugarMain entry point, JugarEngine17
jugar-coreECS, Game Loop, Components52
jugar-physicsRigid body simulation7
jugar-aiGOAP, Behavior Trees17
jugar-renderViewport, Anchors10
jugar-uiWidget system10
jugar-inputTouch/Mouse/KB/Gamepad10
jugar-audioSpatial 2D audio21
jugar-procgenNoise, Dungeons, WFC18
jugar-yamlELI5 YAML-First declarative games334
jugar-probarRust-native WASM game testing128
jugar-webWASM Web platform95

Total: 1500+ tests

Dependency Graph

jugar (entry)
├── jugar-core
│   └── hecs, glam
├── jugar-physics
│   └── trueno (SIMD/GPU)
├── jugar-ai
│   └── aprender (ML)
├── jugar-render
│   └── trueno-viz
├── jugar-ui
│   └── jugar-render
├── jugar-input
├── jugar-audio
└── jugar-procgen

Feature Flags

Each crate supports optional features:

[dependencies]
jugar = { version = "0.1", features = ["full"] }
FeatureIncludes
defaultCore engine
aijugar-ai
audiojugar-audio
procgenjugar-procgen
yamljugar-yaml
fullAll features

jugar

The main entry point crate that provides JugarEngine and the prelude.

Usage

[dependencies]
jugar = "0.1"

JugarEngine

The central engine struct:

use jugar::prelude::*;

fn main() {
    let config = JugarConfig {
        width: 1920,
        height: 1080,
        title: "My Game".to_string(),
        fixed_dt: 1.0 / 60.0,
        ..Default::default()
    };

    let mut engine = JugarEngine::new(config);

    engine.run(|ctx| {
        // Game logic here
        if ctx.input().key_pressed(KeyCode::Escape) {
            return LoopControl::Exit;
        }
        LoopControl::Continue
    });
}

Configuration

#![allow(unused)]
fn main() {
pub struct JugarConfig {
    pub width: u32,
    pub height: u32,
    pub title: String,
    pub fixed_dt: f32,          // Physics timestep
    pub debug_overlay: bool,     // Show debug info
    pub vsync: bool,
}
}

Prelude

The prelude re-exports common types:

#![allow(unused)]
fn main() {
use jugar::prelude::*;

// Includes:
// - JugarEngine, JugarConfig
// - Position, Velocity, Entity
// - KeyCode, MouseButton
// - Vec2, Vec3, Mat4
// - LoopControl
// - GameContext
}

Game Context

Available during the game loop:

#![allow(unused)]
fn main() {
engine.run(|ctx: &mut GameContext| {
    // Time
    let dt = ctx.delta_time();
    let total = ctx.total_time();

    // Input
    let input = ctx.input();

    // World (ECS)
    let world = ctx.world();
    let world_mut = ctx.world_mut();

    LoopControl::Continue
});
}

jugar-core

Core engine functionality: ECS, game loop, components, and state management.

ECS (Entity-Component-System)

Built on hecs for high-performance entity management.

Entities

#![allow(unused)]
fn main() {
use jugar_core::prelude::*;

let mut world = World::new();

// Spawn entity
let entity = world.spawn();

// Check if alive
assert!(world.is_alive(entity));

// Despawn
world.despawn(entity);
}

Components

#![allow(unused)]
fn main() {
// Add components
world.add_component(entity, Position::new(100.0, 200.0));
world.add_component(entity, Velocity::new(1.0, 0.0));
world.add_component(entity, Health(100));

// Get component
if let Some(pos) = world.get_component::<Position>(entity) {
    println!("Position: {:?}", pos);
}

// Remove component
world.remove_component::<Health>(entity);
}

Queries

#![allow(unused)]
fn main() {
// Query all entities with Position and Velocity
for (entity, (pos, vel)) in world.query::<(&mut Position, &Velocity)>() {
    pos.x += vel.x * dt;
    pos.y += vel.y * dt;
}

// Query with filter
for (entity, pos) in world.query::<&Position>()
    .filter::<&Player>()
{
    // Only entities with both Position and Player
}
}

Built-in Components

ComponentDescription
Position2D position (x, y)
Velocity2D velocity (x, y)
RotationAngle in radians
ScaleUniform or non-uniform scale
TransformCombined position/rotation/scale
SpriteSprite rendering info
ColliderCollision shape
RigidBodyPhysics body

Resources

Global state accessible from the world:

#![allow(unused)]
fn main() {
// Add resource
world.add_resource(Score(0));

// Get resource
if let Some(score) = world.get_resource::<Score>() {
    println!("Score: {}", score.0);
}

// Mutate resource
if let Some(score) = world.get_resource_mut::<Score>() {
    score.0 += 10;
}
}

Game Loop

Fixed timestep with variable rendering:

#![allow(unused)]
fn main() {
pub struct GameLoop {
    fixed_dt: f32,
    accumulator: f32,
}

impl GameLoop {
    pub fn update(&mut self, dt: f32, mut update_fn: impl FnMut(f32)) {
        self.accumulator += dt;

        while self.accumulator >= self.fixed_dt {
            update_fn(self.fixed_dt);
            self.accumulator -= self.fixed_dt;
        }
    }

    pub fn interpolation_alpha(&self) -> f32 {
        self.accumulator / self.fixed_dt
    }
}
}

jugar-physics

Physics simulation with tiered backend selection.

Backend Tiers

TierBackendCapability
1WebGPU compute shaders10,000+ rigid bodies
2WASM SIMD 128-bit1,000+ rigid bodies
3Scalar fallbackBasic physics

Backend is selected automatically at runtime.

Rigid Bodies

#![allow(unused)]
fn main() {
use jugar_physics::prelude::*;

let mut physics = PhysicsWorld::new();

// Create static body
let ground = physics.create_static_body(
    Position::new(400.0, 550.0),
    Collider::box_shape(800.0, 100.0),
);

// Create dynamic body
let ball = physics.create_dynamic_body(
    Position::new(400.0, 100.0),
    Collider::circle(20.0),
    RigidBodyConfig {
        mass: 1.0,
        restitution: 0.8,
        friction: 0.3,
    },
);
}

Collision Detection

Spatial hashing for broad-phase:

#![allow(unused)]
fn main() {
// Check collisions
for contact in physics.get_contacts() {
    match (contact.body_a, contact.body_b) {
        (a, b) if a == player && is_coin(b) => {
            collect_coin(b);
        }
        _ => {}
    }
}
}

Forces and Impulses

#![allow(unused)]
fn main() {
// Apply force (continuous)
physics.apply_force(body, Vec2::new(0.0, -100.0));

// Apply impulse (instant)
physics.apply_impulse(body, Vec2::new(500.0, 0.0));

// Set velocity directly
physics.set_velocity(body, Vec2::new(10.0, 0.0));
}

Raycasting

#![allow(unused)]
fn main() {
let ray = Ray::new(
    Vec2::new(100.0, 100.0),  // origin
    Vec2::new(1.0, 0.0),       // direction
);

if let Some(hit) = physics.raycast(ray, 500.0) {
    println!("Hit body {:?} at {:?}", hit.body, hit.point);
}
}

Configuration

#![allow(unused)]
fn main() {
let config = PhysicsConfig {
    gravity: Vec2::new(0.0, 980.0),  // pixels/s²
    iterations: 8,                     // solver iterations
    sleep_threshold: 0.1,              // velocity threshold
};

let mut physics = PhysicsWorld::with_config(config);
}

jugar-ai

AI systems: GOAP, Behavior Trees, and Steering Behaviors.

GOAP (Goal-Oriented Action Planning)

#![allow(unused)]
fn main() {
use jugar_ai::goap::*;

// Define world state
let mut state = WorldState::new();
state.set("has_weapon", false);
state.set("enemy_dead", false);
state.set("at_armory", false);

// Define actions
let actions = vec![
    Action::new("go_to_armory")
        .precondition("at_armory", false)
        .effect("at_armory", true)
        .cost(5),

    Action::new("get_weapon")
        .precondition("at_armory", true)
        .precondition("has_weapon", false)
        .effect("has_weapon", true)
        .cost(2),

    Action::new("attack")
        .precondition("has_weapon", true)
        .effect("enemy_dead", true)
        .cost(1),
];

// Define goal
let goal = Goal::new()
    .require("enemy_dead", true);

// Plan
let planner = GoapPlanner::new();
let plan = planner.plan(&state, &goal, &actions);
// Returns: ["go_to_armory", "get_weapon", "attack"]
}

Behavior Trees

#![allow(unused)]
fn main() {
use jugar_ai::behavior_tree::*;

let tree = BehaviorTree::new()
    .selector()                          // Try children until one succeeds
        .sequence()                       // All children must succeed
            .condition(|ctx| ctx.health < 20)
            .action(|ctx| ctx.flee())
        .end()
        .sequence()
            .condition(|ctx| ctx.sees_enemy())
            .action(|ctx| ctx.attack())
        .end()
        .action(|ctx| ctx.patrol())
    .end()
    .build();

// Tick the tree each frame
let status = tree.tick(&mut context);
match status {
    Status::Success => { /* completed */ }
    Status::Running => { /* still executing */ }
    Status::Failure => { /* failed */ }
}
}

Node Types

NodeDescription
SelectorTry children until one succeeds
SequenceRun children until one fails
ParallelRun all children simultaneously
ConditionCheck a predicate
ActionExecute behavior
DecoratorModify child behavior

Steering Behaviors

#![allow(unused)]
fn main() {
use jugar_ai::steering::*;

let mut agent = SteeringAgent::new(position, max_speed);

// Individual behaviors
let seek = agent.seek(target);
let flee = agent.flee(danger);
let arrive = agent.arrive(destination, slow_radius);
let wander = agent.wander(circle_distance, circle_radius);

// Combine behaviors
let steering = SteeringCombiner::new()
    .add(seek, 1.0)
    .add(agent.separation(&neighbors), 2.0)
    .add(agent.cohesion(&neighbors), 0.5)
    .add(agent.alignment(&neighbors), 0.5)
    .calculate();

agent.apply_steering(steering, dt);
}
#![allow(unused)]
fn main() {
use jugar_ai::navmesh::*;

// Create navmesh from polygons
let navmesh = NavMesh::from_polygons(&walkable_areas);

// Find path
let path = navmesh.find_path(start, end);

// Smooth path
let smooth_path = navmesh.smooth_path(&path);
}

jugar-render

Viewport management and resolution-independent rendering.

Viewport

#![allow(unused)]
fn main() {
use jugar_render::prelude::*;

let viewport = Viewport::new(1920, 1080);

// Safe area for 16:9 gameplay
let safe_area = viewport.safe_area();

// Extended area for ultrawide
let extended = viewport.extended_area();

// Current aspect ratio
let ratio = viewport.aspect_ratio();
}

Aspect Ratio Handling

#![allow(unused)]
fn main() {
// Define safe gameplay area
let gameplay_area = viewport.calculate_safe_area(AspectRatio::HD_16_9);

// For 32:9 ultrawide, extends horizontally
// For 9:16 portrait, extends vertically
}

Anchor System

Position UI elements relative to screen edges:

#![allow(unused)]
fn main() {
use jugar_render::anchor::*;

// Corner anchors
let top_left = Anchor::TopLeft { margin: 10.0 };
let bottom_right = Anchor::BottomRight { margin: 10.0 };

// Edge anchors
let top_center = Anchor::TopCenter { margin: 20.0 };
let left_center = Anchor::LeftCenter { margin: 20.0 };

// Center
let center = Anchor::Center;

// Calculate screen position
let screen_pos = anchor.calculate(viewport.dimensions());
}

Camera

#![allow(unused)]
fn main() {
use jugar_render::camera::*;

let mut camera = Camera2D::new(viewport.dimensions());

// Follow target
camera.follow(player_position, dt);

// Smooth follow
camera.follow_smooth(player_position, 0.1, dt);

// Zoom
camera.set_zoom(2.0);

// Convert coordinates
let world_pos = camera.screen_to_world(mouse_pos);
let screen_pos = camera.world_to_screen(entity_pos);
}

Responsive Scaling

#![allow(unused)]
fn main() {
// Scale UI based on shortest dimension
let ui_scale = viewport.ui_scale_factor();

// Scale gameplay based on reference resolution
let game_scale = viewport.game_scale_factor(1920, 1080);
}

Render Commands

Jugar uses a command-based rendering system:

#![allow(unused)]
fn main() {
pub enum RenderCommand {
    DrawSprite {
        texture: TextureId,
        position: Vec2,
        rotation: f32,
        scale: Vec2,
        color: Color,
    },
    DrawRect {
        position: Vec2,
        size: Vec2,
        color: Color,
    },
    DrawText {
        text: String,
        position: Vec2,
        font_size: f32,
        color: Color,
    },
    // ...
}
}

Commands are serialized to JSON and sent to the HTML renderer.

jugar-ui

Widget system for responsive game UI.

Widgets

#![allow(unused)]
fn main() {
use jugar_ui::prelude::*;

// Text label
let label = Label::new("Score: 100")
    .font_size(24.0)
    .color(Color::WHITE);

// Button
let button = Button::new("Start Game")
    .size(200.0, 50.0)
    .on_click(|| {
        println!("Button clicked!");
    });

// Progress bar
let health_bar = ProgressBar::new(0.75)
    .size(200.0, 20.0)
    .foreground(Color::RED)
    .background(Color::DARK_GRAY);
}

Layout

#![allow(unused)]
fn main() {
// Horizontal layout
let h_layout = HBox::new()
    .spacing(10.0)
    .add(button1)
    .add(button2)
    .add(button3);

// Vertical layout
let v_layout = VBox::new()
    .spacing(5.0)
    .add(title)
    .add(health_bar)
    .add(score_label);

// Grid layout
let grid = Grid::new(3, 3)
    .cell_size(64.0, 64.0)
    .spacing(4.0);
}

Panels

#![allow(unused)]
fn main() {
// Container panel
let panel = Panel::new()
    .size(300.0, 200.0)
    .background(Color::rgba(0, 0, 0, 180))
    .padding(10.0)
    .add(v_layout);

// Anchored panel
let hud = Panel::new()
    .anchor(Anchor::TopLeft { margin: 10.0 })
    .add(health_bar)
    .add(score_label);
}

Responsive Scaling

UI automatically scales based on screen size:

#![allow(unused)]
fn main() {
let ui = UiContext::new(viewport);

// Scale factor based on shortest dimension
let scale = ui.scale_factor();

// Reference resolution scaling
let scaled_size = ui.scale_from_reference(100.0, 1920);
}

Input Handling

#![allow(unused)]
fn main() {
impl Widget for Button {
    fn handle_input(&mut self, input: &InputState) -> bool {
        let mouse_pos = input.mouse_position();

        if self.bounds.contains(mouse_pos) {
            if input.mouse_just_pressed(MouseButton::Left) {
                self.on_click.call();
                return true;
            }
        }
        false
    }
}
}

Custom Widgets

#![allow(unused)]
fn main() {
struct MiniMap {
    position: Vec2,
    size: Vec2,
    entities: Vec<MinimapEntity>,
}

impl Widget for MiniMap {
    fn update(&mut self, dt: f32) {
        // Update minimap entities
    }

    fn render(&self) -> Vec<RenderCommand> {
        let mut commands = vec![];
        commands.push(RenderCommand::DrawRect {
            position: self.position,
            size: self.size,
            color: Color::rgba(0, 0, 0, 150),
        });
        // Draw entities as dots
        for entity in &self.entities {
            commands.push(RenderCommand::DrawCircle {
                position: entity.minimap_pos,
                radius: 3.0,
                color: entity.color,
            });
        }
        commands
    }
}
}

jugar-input

Unified input handling for touch, mouse, keyboard, and gamepad.

Keyboard

#![allow(unused)]
fn main() {
use jugar_input::prelude::*;

let input = InputState::new();

// Key states
if input.key_pressed(KeyCode::Space) {
    // Just pressed this frame
}

if input.key_held(KeyCode::W) {
    // Currently held down
}

if input.key_released(KeyCode::Escape) {
    // Just released this frame
}
}

Mouse

#![allow(unused)]
fn main() {
// Position
let pos = input.mouse_position();

// Buttons
if input.mouse_pressed(MouseButton::Left) {
    spawn_projectile(pos);
}

if input.mouse_held(MouseButton::Right) {
    aim_at(pos);
}

// Scroll wheel
let scroll = input.scroll_delta();
camera.zoom += scroll.y * 0.1;
}

Touch

#![allow(unused)]
fn main() {
// All active touches
for touch in input.touches() {
    match touch.phase {
        TouchPhase::Started => {
            // New touch
        }
        TouchPhase::Moved => {
            // Touch moved
            let delta = touch.position - touch.previous_position;
        }
        TouchPhase::Ended | TouchPhase::Cancelled => {
            // Touch ended
        }
    }
}

// Gestures
if let Some(pinch) = input.pinch_gesture() {
    camera.zoom *= pinch.scale;
}

if let Some(pan) = input.pan_gesture() {
    camera.position -= pan.delta;
}
}

Gamepad

#![allow(unused)]
fn main() {
// Check if gamepad connected
if let Some(gamepad) = input.gamepad(0) {
    // Buttons
    if gamepad.button_pressed(GamepadButton::South) {
        player.jump();
    }

    // Analog sticks
    let left_stick = gamepad.left_stick();
    player.velocity.x = left_stick.x * max_speed;

    let right_stick = gamepad.right_stick();
    player.aim_direction = right_stick.normalize();

    // Triggers
    let fire_intensity = gamepad.right_trigger();
    if fire_intensity > 0.5 {
        player.fire();
    }
}
}

Virtual Joystick

For touch devices:

#![allow(unused)]
fn main() {
let mut joystick = VirtualJoystick::new()
    .position(100.0, 400.0)
    .radius(60.0)
    .dead_zone(0.1);

// Update with touch input
joystick.update(&input);

// Get direction
let direction = joystick.direction();
player.velocity = direction * max_speed;
}

Input Abstraction

Unify different input methods:

#![allow(unused)]
fn main() {
// Abstract action
let move_input = input.get_axis_2d(
    // Keyboard
    KeyCode::W, KeyCode::S, KeyCode::A, KeyCode::D,
    // Gamepad
    GamepadAxis::LeftStickX, GamepadAxis::LeftStickY,
);

// Returns normalized Vec2 from any input source
player.velocity = move_input * max_speed;
}

Key Codes

Common key codes:

CategoryKeys
LettersA - Z
NumbersKey0 - Key9
ArrowsArrowUp, ArrowDown, ArrowLeft, ArrowRight
SpecialSpace, Enter, Escape, Tab, Backspace
ModifiersShiftLeft, ControlLeft, AltLeft
FunctionF1 - F12

jugar-audio

Spatial 2D audio with channel mixing.

Basic Playback

#![allow(unused)]
fn main() {
use jugar_audio::prelude::*;

let mut audio = AudioEngine::new();

// Load sound
let sound = audio.load_sound("explosion.wav");

// Play sound
audio.play(sound);

// Play with volume
audio.play_with_volume(sound, 0.5);
}

Channels

#![allow(unused)]
fn main() {
// Audio channels
pub enum Channel {
    Master,   // Overall volume
    Music,    // Background music
    Effects,  // Sound effects
    Voice,    // Dialogue
    Ambient,  // Environmental sounds
}

// Set channel volume
audio.set_channel_volume(Channel::Music, 0.7);
audio.set_channel_volume(Channel::Effects, 1.0);

// Play on specific channel
audio.play_on_channel(sound, Channel::Effects);
}

Spatial Audio

#![allow(unused)]
fn main() {
// Create audio source at position
let source = audio.create_source(Position::new(500.0, 300.0));
audio.play_at_source(sound, source);

// Set listener position (usually player)
audio.set_listener_position(player_position);

// Update source position
audio.set_source_position(source, enemy_position);
}

Attenuation

#![allow(unused)]
fn main() {
// Configure distance attenuation
let source = audio.create_source_with_config(
    position,
    SpatialConfig {
        min_distance: 50.0,   // Full volume within this range
        max_distance: 500.0,  // Inaudible beyond this range
        rolloff: Rolloff::Linear,
    },
);
}

Rolloff Models

ModelDescription
LinearLinear falloff between min and max
Inverse1/distance falloff
InverseSquare1/distance² (realistic)
NoneNo distance attenuation

Stereo Panning

#![allow(unused)]
fn main() {
// Automatic panning based on position
// Sounds to the left of listener pan left
// Sounds to the right pan right

// Manual pan (-1.0 = left, 0.0 = center, 1.0 = right)
audio.set_pan(source, -0.5);
}

Music

#![allow(unused)]
fn main() {
// Background music (loops by default)
let music = audio.load_music("background.ogg");
audio.play_music(music);

// Crossfade to new track
audio.crossfade_music(new_music, 2.0);  // 2 second fade

// Pause/resume
audio.pause_music();
audio.resume_music();
}

Sound Groups

#![allow(unused)]
fn main() {
// Create a group for related sounds
let footsteps = SoundGroup::new()
    .add(audio.load_sound("step1.wav"))
    .add(audio.load_sound("step2.wav"))
    .add(audio.load_sound("step3.wav"));

// Play random sound from group
footsteps.play_random(&mut audio);

// Play sequential
footsteps.play_next(&mut audio);
}

Example: Complete Setup

#![allow(unused)]
fn main() {
fn setup_audio(audio: &mut AudioEngine) {
    // Set channel volumes
    audio.set_channel_volume(Channel::Master, 0.8);
    audio.set_channel_volume(Channel::Music, 0.5);
    audio.set_channel_volume(Channel::Effects, 1.0);

    // Start background music
    let music = audio.load_music("theme.ogg");
    audio.play_music(music);
}

fn update_audio(audio: &mut AudioEngine, player_pos: Position) {
    // Update listener to follow player
    audio.set_listener_position(player_pos);
}

fn play_explosion(audio: &mut AudioEngine, position: Position) {
    let sound = audio.load_sound("explosion.wav");
    let source = audio.create_source(position);
    audio.play_at_source_on_channel(sound, source, Channel::Effects);
}
}

jugar-procgen

Procedural generation: noise, dungeons, and Wave Function Collapse.

Value Noise

#![allow(unused)]
fn main() {
use jugar_procgen::noise::*;

let noise = ValueNoise::new(seed);

// Single value at point
let value = noise.sample(x, y);  // Returns 0.0 to 1.0

// With octaves (fractal noise)
let config = NoiseConfig {
    octaves: 4,
    persistence: 0.5,  // Amplitude decrease per octave
    lacunarity: 2.0,   // Frequency increase per octave
    scale: 0.01,
};
let value = noise.sample_octaves(x, y, &config);
}

Noise Types

#![allow(unused)]
fn main() {
// Value noise (smooth)
let value = ValueNoise::new(seed);

// Perlin noise (gradient-based)
let perlin = PerlinNoise::new(seed);

// Simplex noise (faster, less artifacts)
let simplex = SimplexNoise::new(seed);

// Worley noise (cellular)
let worley = WorleyNoise::new(seed);
}

Generating Terrain

#![allow(unused)]
fn main() {
fn generate_heightmap(width: usize, height: usize, seed: u64) -> Vec<f32> {
    let noise = SimplexNoise::new(seed);
    let config = NoiseConfig::default();

    let mut heightmap = vec![0.0; width * height];

    for y in 0..height {
        for x in 0..width {
            let value = noise.sample_octaves(
                x as f32,
                y as f32,
                &config,
            );
            heightmap[y * width + x] = value;
        }
    }

    heightmap
}
}

Dungeon Generation

#![allow(unused)]
fn main() {
use jugar_procgen::dungeon::*;

// BSP-based dungeon
let config = DungeonConfig {
    width: 80,
    height: 60,
    min_room_size: 5,
    max_room_size: 15,
    corridor_width: 2,
};

let dungeon = BspDungeon::generate(config, seed);

// Access tiles
for y in 0..dungeon.height {
    for x in 0..dungeon.width {
        match dungeon.get_tile(x, y) {
            Tile::Floor => { /* walkable */ }
            Tile::Wall => { /* blocked */ }
            Tile::Door => { /* door */ }
        }
    }
}

// Get rooms
for room in dungeon.rooms() {
    spawn_enemies_in_room(room);
}
}

Wave Function Collapse

#![allow(unused)]
fn main() {
use jugar_procgen::wfc::*;

// Define tiles and rules
let tiles = vec![
    Tile::new("grass").edges(["g", "g", "g", "g"]),
    Tile::new("water").edges(["w", "w", "w", "w"]),
    Tile::new("shore_n").edges(["g", "g", "w", "g"]),
    // ... more tiles with edge constraints
];

let wfc = WaveFunctionCollapse::new(tiles);

// Generate map
let result = wfc.generate(width, height, seed);

for y in 0..height {
    for x in 0..width {
        let tile = result.get(x, y);
        render_tile(tile, x, y);
    }
}
}

WFC Constraints

#![allow(unused)]
fn main() {
// Socket-based constraints
let tiles = vec![
    Tile::new("road_horizontal")
        .sockets(Socket {
            north: "grass",
            south: "grass",
            east: "road",
            west: "road",
        }),
    Tile::new("road_vertical")
        .sockets(Socket {
            north: "road",
            south: "road",
            east: "grass",
            west: "grass",
        }),
    // ...
];
}

Room Placement

#![allow(unused)]
fn main() {
use jugar_procgen::rooms::*;

let mut placer = RoomPlacer::new(100, 100);

// Add rooms with constraints
placer.add_room(Room::new(10, 10).tag("spawn"));
placer.add_room(Room::new(15, 15).tag("boss"));
placer.add_rooms(5, Room::random(5..10, 5..10).tag("normal"));

// Connect rooms
placer.connect_all_rooms();

// Generate
let layout = placer.generate(seed);
}

Combining Techniques

#![allow(unused)]
fn main() {
fn generate_world(seed: u64) -> World {
    // 1. Generate heightmap with noise
    let heightmap = generate_heightmap(256, 256, seed);

    // 2. Generate dungeon layout
    let dungeon = BspDungeon::generate(config, seed);

    // 3. Use WFC to fill in details
    let details = wfc.generate(dungeon.width, dungeon.height, seed);

    // 4. Combine everything
    // ...
}
}

jugar-yaml

ELI5 YAML-First declarative game definitions. Define entire games in YAML.

Overview

jugar-yaml provides a declarative way to define games using YAML configuration files. This follows the "ELI5" (Explain Like I'm 5) philosophy - making game development accessible.

Basic Structure

game:
  title: "My First Game"
  width: 800
  height: 600
  background: "#1a1a2e"

entities:
  - name: player
    position: [400, 300]
    sprite: "player.png"
    components:
      - type: velocity
        value: [0, 0]
      - type: collider
        shape: circle
        radius: 20

  - name: enemy
    position: [100, 100]
    sprite: "enemy.png"
    ai:
      behavior: patrol
      waypoints:
        - [100, 100]
        - [700, 100]
        - [700, 500]
        - [100, 500]

Loading Games

#![allow(unused)]
fn main() {
use jugar_yaml::prelude::*;

// Load from file
let game = GameDefinition::load("game.yaml")?;

// Create engine from definition
let engine = JugarEngine::from_definition(game);
engine.run(game_loop);
}

Components

Define components declaratively:

entities:
  - name: ball
    position: [400, 300]
    components:
      # Physics
      - type: rigid_body
        mass: 1.0
        restitution: 0.9

      # Rendering
      - type: sprite
        texture: "ball.png"
        scale: [1.0, 1.0]

      # Custom data
      - type: custom
        data:
          health: 100
          damage: 10

Behaviors

AI behaviors in YAML:

entities:
  - name: guard
    ai:
      behavior: behavior_tree
      tree:
        type: selector
        children:
          - type: sequence
            children:
              - type: condition
                check: "health < 20"
              - type: action
                do: flee

          - type: sequence
            children:
              - type: condition
                check: "sees_player"
              - type: action
                do: attack

          - type: action
            do: patrol

Input Mapping

input:
  actions:
    move_up:
      - key: W
      - key: ArrowUp
      - gamepad: LeftStickUp

    move_down:
      - key: S
      - key: ArrowDown
      - gamepad: LeftStickDown

    fire:
      - key: Space
      - mouse: Left
      - gamepad: South

Scenes

Define multiple scenes:

scenes:
  main_menu:
    entities:
      - name: title
        position: [400, 200]
        text: "My Game"
        font_size: 48

      - name: start_button
        position: [400, 350]
        button:
          text: "Start"
          action: load_scene:gameplay

  gameplay:
    entities:
      - name: player
        # ...

Templates

Reusable entity templates:

templates:
  coin:
    sprite: "coin.png"
    components:
      - type: collider
        shape: circle
        radius: 15
      - type: custom
        data:
          value: 10

entities:
  - template: coin
    position: [100, 200]

  - template: coin
    position: [200, 200]

  - template: coin
    position: [300, 200]
    components:
      - type: custom
        data:
          value: 50  # Override

Physics World

physics:
  gravity: [0, 980]
  iterations: 8

  static_bodies:
    - name: ground
      position: [400, 580]
      shape:
        type: box
        size: [800, 40]

    - name: left_wall
      position: [0, 300]
      shape:
        type: box
        size: [20, 600]

Validation

The YAML is validated at load time:

#![allow(unused)]
fn main() {
let result = GameDefinition::load("game.yaml");
match result {
    Ok(game) => { /* valid */ }
    Err(YamlError::InvalidPosition(msg)) => {
        println!("Position error: {}", msg);
    }
    Err(YamlError::MissingField(field)) => {
        println!("Missing required field: {}", field);
    }
    // ...
}
}

Example: Complete Pong

game:
  title: "YAML Pong"
  width: 800
  height: 600

physics:
  gravity: [0, 0]

entities:
  - name: ball
    position: [400, 300]
    components:
      - type: velocity
        value: [300, 200]
      - type: collider
        shape: circle
        radius: 10

  - name: paddle_left
    position: [50, 300]
    components:
      - type: collider
        shape: box
        size: [10, 80]
    input:
      up: W
      down: S

  - name: paddle_right
    position: [750, 300]
    components:
      - type: collider
        shape: box
        size: [10, 80]
    ai:
      behavior: follow_ball
      speed: 200

ui:
  - name: score
    position: [400, 50]
    text: "0 - 0"
    font_size: 32

jugar-web

Web platform bindings for running Jugar games in the browser.

Overview

jugar-web provides the WASM bindings for running Jugar games in web browsers. It handles:

  • WASM module initialization
  • Event forwarding from browser to game
  • Render command output to JavaScript
  • WebAudio integration
  • Touch/mouse/keyboard input

WebPlatform

The main interface exposed to JavaScript:

#![allow(unused)]
fn main() {
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub struct WebPlatform {
    game: GameState,
    config: WebConfig,
}

#[wasm_bindgen]
impl WebPlatform {
    #[wasm_bindgen(constructor)]
    pub fn new(width: u32, height: u32) -> Self {
        // Initialize platform
    }

    pub fn frame(&mut self, timestamp: f64, input_json: &str) -> String {
        // Process frame, return render commands as JSON
    }

    pub fn key_down(&mut self, code: &str) {
        // Handle key press
    }

    pub fn key_up(&mut self, code: &str) {
        // Handle key release
    }

    pub fn mouse_move(&mut self, x: f32, y: f32) {
        // Handle mouse movement
    }

    pub fn touch_start(&mut self, id: u32, x: f32, y: f32) {
        // Handle touch start
    }
}
}

Building

# Build with wasm-pack
wasm-pack build crates/jugar-web --target web --out-dir pkg

# Or use make
make build-web

HTML Integration

Minimal loader (no game logic in JS):

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Jugar Game</title>
</head>
<body>
    <canvas id="canvas"></canvas>
    <script type="module">
        import init, { WebPlatform } from './pkg/jugar_web.js';

        async function main() {
            await init();

            const canvas = document.getElementById('canvas');
            const ctx = canvas.getContext('2d');
            const platform = new WebPlatform(800, 600);

            // Event forwarding only
            document.addEventListener('keydown', e => {
                platform.key_down(e.code);
            });

            document.addEventListener('keyup', e => {
                platform.key_up(e.code);
            });

            canvas.addEventListener('mousemove', e => {
                platform.mouse_move(e.offsetX, e.offsetY);
            });

            function frame(timestamp) {
                const commands = platform.frame(timestamp, '[]');
                render(ctx, JSON.parse(commands));
                requestAnimationFrame(frame);
            }

            requestAnimationFrame(frame);
        }

        function render(ctx, commands) {
            for (const cmd of commands) {
                switch (cmd.type) {
                    case 'clear':
                        ctx.fillStyle = cmd.color;
                        ctx.fillRect(0, 0, 800, 600);
                        break;
                    case 'rect':
                        ctx.fillStyle = cmd.color;
                        ctx.fillRect(cmd.x, cmd.y, cmd.w, cmd.h);
                        break;
                    case 'text':
                        ctx.fillStyle = cmd.color;
                        ctx.font = `${cmd.size}px sans-serif`;
                        ctx.fillText(cmd.text, cmd.x, cmd.y);
                        break;
                }
            }
        }

        main();
    </script>
</body>
</html>

Render Commands

JSON format for render commands:

[
    {"type": "clear", "color": "#1a1a2e"},
    {"type": "rect", "x": 100, "y": 200, "w": 50, "h": 50, "color": "#ffffff"},
    {"type": "text", "x": 400, "y": 50, "text": "Score: 100", "size": 24, "color": "#ffffff"},
    {"type": "circle", "x": 300, "y": 300, "r": 10, "color": "#ff0000"}
]

Audio Commands

[
    {"type": "playTone", "frequency": 440, "duration": 0.1},
    {"type": "playSound", "id": "explosion", "volume": 0.8}
]

Testing

E2E tests using Probar:

# Run Probar tests (replaces Playwright)
make test-e2e

# Or directly
cargo test -p jugar-web --test probar_pong

Configuration

#![allow(unused)]
fn main() {
pub struct WebConfig {
    pub width: u32,
    pub height: u32,
    pub canvas_id: String,
    pub audio_enabled: bool,
    pub touch_enabled: bool,
}
}

Feature Flags

[features]
default = ["audio"]
audio = []  # Enable WebAudio support
debug = []  # Enable debug overlays

Probar: WASM-Native Game Testing

Probar (Spanish: "to test/prove") is a pure Rust testing framework for WASM games that provides full Playwright feature parity while adding WASM-native capabilities.

Why Probar?

AspectPlaywrightProbar
LanguageTypeScriptPure Rust
BrowserRequired (Chromium)Not needed
Game StateBlack box (DOM only)Direct API access
CI SetupNode.js + browserJust cargo test
Zero JS❌ Violates constraint✅ Pure Rust

Key Features

Playwright Parity

  • CSS, text, testid, XPath, role-based locators
  • All standard assertions (visibility, text, count)
  • All actions (click, fill, type, hover, drag)
  • Auto-waiting with configurable timeouts
  • Network interception and mobile emulation

WASM-Native Extensions

  • Zero-copy memory views - Direct WASM memory inspection
  • Type-safe entity selectors - Compile-time verified game object access
  • Deterministic replay - Record inputs with seed, replay identically
  • Invariant fuzzing - Concolic testing for game invariants
  • Frame-perfect timing - Fixed timestep control
  • WCAG accessibility - Color contrast and photosensitivity checking

Quick Example

#![allow(unused)]
fn main() {
use jugar_probar::Assertion;
use jugar_web::{WebConfig, WebPlatform};

#[test]
fn test_game_starts() {
    let config = WebConfig::new(800, 600);
    let mut platform = WebPlatform::new_for_test(config);

    // Run a frame
    let output = platform.frame(0.0, "[]");

    // Verify output
    assert!(output.contains("commands"));

    // Use Probar assertions
    let assertion = Assertion::in_range(60.0, 0.0, 100.0);
    assert!(assertion.passed);
}
}

Running Tests

# All Probar E2E tests
cargo test -p jugar-web --test probar_pong

# Verbose output
cargo test -p jugar-web --test probar_pong -- --nocapture

# Via Makefile
make test-e2e
make test-e2e-verbose

Test Suites

SuiteTestsCoverage
Pong WASM Game (Core)6WASM loading, rendering, input
Pong Demo Features22Game modes, HUD, AI widgets
Release Readiness11Stress tests, performance, edge cases

Total: 39 tests

Architecture

Dual-Runtime Strategy

┌─────────────────────────────────┐     ┌─────────────────────────────────┐
│  WasmRuntime (wasmtime)         │     │  BrowserController (Chrome)     │
│  ─────────────────────────      │     │  ─────────────────────────      │
│  Purpose: LOGIC-ONLY testing    │     │  Purpose: GOLDEN MASTER         │
│                                 │     │                                 │
│  ✓ Unit tests                   │     │  ✓ E2E tests                    │
│  ✓ Deterministic replay         │     │  ✓ Visual regression            │
│  ✓ Invariant fuzzing            │     │  ✓ Browser compatibility        │
│  ✓ Performance benchmarks       │     │  ✓ Production parity            │
│                                 │     │                                 │
│  ✗ NOT for rendering            │     │  This is the SOURCE OF TRUTH    │
│  ✗ NOT for browser APIs         │     │  for "does it work?"            │
└─────────────────────────────────┘     └─────────────────────────────────┘

Toyota Way Principles

PrincipleApplication
Poka-YokeType-safe selectors prevent typos at compile time
MudaZero-copy memory views eliminate serialization
Genchi GenbutsuProbarDriver abstraction for swappable backends
Andon CordFail-fast mode stops on first critical failure
JidokaQuality built into the type system

Next Steps

Why Probar?

Probar was created as a complete replacement for Playwright in the Jugar ecosystem.

The Problem with Playwright

  1. JavaScript Dependency: Playwright requires Node.js and npm
  2. Browser Overhead: Must download and run Chromium
  3. Black Box Testing: Can only inspect DOM, not game state
  4. CI Complexity: Requires browser installation in CI
  5. Violates Zero-JS: Contradicts Jugar's core constraint

What Probar Solves

Zero JavaScript

Before (Playwright):
├── package.json
├── node_modules/
├── tests/
│   └── pong.spec.ts  ← TypeScript!
└── playwright.config.ts

After (Probar):
└── tests/
    └── probar_pong.rs  ← Pure Rust!

Direct State Access

Playwright treats the game as a black box:

// Can only check DOM
await expect(page.locator('#score')).toHaveText('10');

Probar can inspect game state directly:

#![allow(unused)]
fn main() {
// Direct access to game internals
let score = platform.get_game_state().score;
assert_eq!(score, 10);

// Check entity positions
for entity in platform.query_entities::<Ball>() {
    assert!(entity.position.y < 600.0);
}
}

Deterministic Testing

Playwright: Non-deterministic due to browser timing

// Flaky! Timing varies between runs
await page.waitForTimeout(100);
await expect(ball).toBeVisible();

Probar: Fully deterministic

#![allow(unused)]
fn main() {
// Exact frame control
for _ in 0..100 {
    platform.advance_frame(1.0 / 60.0);
}
let ball_pos = platform.get_ball_position();
assert_eq!(ball_pos, expected_pos);  // Always passes
}

Simpler CI

Playwright CI:

- name: Install Node.js
  uses: actions/setup-node@v3
- name: Install dependencies
  run: npm ci
- name: Install Playwright
  run: npx playwright install chromium
- name: Run tests
  run: npm test

Probar CI:

- name: Run tests
  run: cargo test

Feature Comparison

FeaturePlaywrightProbar
LanguageTypeScriptPure Rust
Browser requiredYesNo
Game state accessDOM onlyDirect
DeterministicNoYes
CI setupComplexSimple
Frame controlApproximateExact
Memory inspectionNoYes
Replay supportNoYes
FuzzingNoYes

Migration Example

Before (Playwright)

import { test, expect } from '@playwright/test';

test('ball bounces off walls', async ({ page }) => {
  await page.goto('http://localhost:8080');

  // Wait for game to load
  await page.waitForSelector('#game-canvas');

  // Simulate gameplay
  await page.waitForTimeout(2000);

  // Check score changed (indirect verification)
  const score = await page.locator('#score').textContent();
  expect(parseInt(score)).toBeGreaterThan(0);
});

After (Probar)

#![allow(unused)]
fn main() {
#[test]
fn ball_bounces_off_walls() {
    let mut platform = WebPlatform::new_for_test(WebConfig::default());

    // Advance exactly 120 frames (2 seconds at 60fps)
    for _ in 0..120 {
        platform.advance_frame(1.0 / 60.0);
    }

    // Direct state verification
    let state = platform.get_game_state();
    assert!(state.ball_bounces > 0, "Ball should have bounced");
    assert!(state.score > 0, "Score should have increased");
}
}

Performance Comparison

MetricPlaywrightProbar
Test startup~3s~0.1s
Per-test overhead~500ms~10ms
39 tests total~45s~3s
CI setup time~2min0
Memory usage~500MB~50MB

When to Use Each

Use Probar for:

  • Unit tests
  • Integration tests
  • Deterministic replay
  • Fuzzing
  • Performance benchmarks
  • CI/CD pipelines

Use Browser Testing for:

  • Visual regression (golden master)
  • Cross-browser compatibility
  • Real user interaction testing
  • Production smoke tests

Probar Quick Start

Get started with Probar testing in 5 minutes.

Add Dependency

[dev-dependencies]
jugar-probar = { path = "../jugar-probar" }

Write Your First Test

#![allow(unused)]
fn main() {
use jugar_probar::Assertion;
use jugar_web::{WebConfig, WebPlatform};

#[test]
fn test_game_initializes() {
    // Create test platform
    let config = WebConfig::new(800, 600);
    let mut platform = WebPlatform::new_for_test(config);

    // Run initial frame
    let output = platform.frame(0.0, "[]");

    // Verify game started
    assert!(output.contains("commands"));
}
}

Run Tests

# Run all Probar tests
cargo test -p jugar-web --test probar_pong

# With verbose output
cargo test -p jugar-web --test probar_pong -- --nocapture

# Via Makefile
make test-e2e

Test Structure

Basic Assertions

#![allow(unused)]
fn main() {
use jugar_probar::Assertion;

#[test]
fn test_assertions() {
    // Equality
    let eq = Assertion::equals(&42, &42);
    assert!(eq.passed);

    // Range
    let range = Assertion::in_range(50.0, 0.0, 100.0);
    assert!(range.passed);

    // Boolean
    let truthy = Assertion::is_true(true);
    assert!(truthy.passed);

    // Approximate equality (for floats)
    let approx = Assertion::approx_eq(3.14, 3.14159, 0.01);
    assert!(approx.passed);
}
}

Testing Game Logic

#![allow(unused)]
fn main() {
#[test]
fn test_ball_movement() {
    let mut platform = WebPlatform::new_for_test(WebConfig::default());

    // Get initial position
    let initial_pos = platform.get_ball_position();

    // Advance 60 frames (1 second)
    for _ in 0..60 {
        platform.advance_frame(1.0 / 60.0);
    }

    // Ball should have moved
    let new_pos = platform.get_ball_position();
    assert_ne!(initial_pos, new_pos);
}
}

Testing Input

#![allow(unused)]
fn main() {
#[test]
fn test_paddle_responds_to_input() {
    let mut platform = WebPlatform::new_for_test(WebConfig::default());

    let initial_y = platform.get_paddle_y(Player::Left);

    // Simulate pressing W key
    platform.key_down("KeyW");
    for _ in 0..30 {
        platform.advance_frame(1.0 / 60.0);
    }
    platform.key_up("KeyW");

    // Paddle should have moved up
    let new_y = platform.get_paddle_y(Player::Left);
    assert!(new_y < initial_y);
}
}

Examples

Run the included examples:

# Deterministic simulation with replay
cargo run --example pong_simulation -p jugar-probar

# Locator API demo
cargo run --example locator_demo -p jugar-probar

# Accessibility checking
cargo run --example accessibility_demo -p jugar-probar

# Coverage demo
cargo run --example coverage_demo -p jugar-probar

Example Output

=== Probar Pong Simulation Demo ===

--- Demo 1: Pong Simulation ---
Initial state:
  Ball: (400.0, 300.0)
  Paddles: P1=300.0, P2=300.0
  Score: 0 - 0

Simulating 300 frames...

Final state after 300 frames:
  Ball: (234.5, 412.3)
  Paddles: P1=180.0, P2=398.2
  Score: 2 - 1
  State valid: true

--- Demo 2: Deterministic Replay ---
Recording simulation (seed=42, frames=500)...
  Completed: true
  Final hash: 6233835744931225727

Replaying simulation...
  Determinism verified: true
  Hashes match: true

Next Steps

Assertions

Probar provides a rich set of assertions for testing game state.

Basic Assertions

#![allow(unused)]
fn main() {
use jugar_probar::Assertion;

// Equality
let eq = Assertion::equals(&actual, &expected);
assert!(eq.passed);
assert_eq!(eq.message, "Values are equal");

// Inequality
let ne = Assertion::not_equals(&a, &b);

// Boolean
let truthy = Assertion::is_true(condition);
let falsy = Assertion::is_false(condition);
}

Numeric Assertions

#![allow(unused)]
fn main() {
// Range check
let range = Assertion::in_range(value, min, max);

// Approximate equality (for floats)
let approx = Assertion::approx_eq(3.14159, std::f64::consts::PI, 0.001);

// Greater/Less than
let gt = Assertion::greater_than(value, threshold);
let lt = Assertion::less_than(value, threshold);
let gte = Assertion::greater_than_or_equal(value, threshold);
let lte = Assertion::less_than_or_equal(value, threshold);
}

Collection Assertions

#![allow(unused)]
fn main() {
// Contains
let contains = Assertion::contains(&collection, &item);

// Length
let len = Assertion::has_length(&vec, expected_len);

// Empty
let empty = Assertion::is_empty(&vec);
let not_empty = Assertion::is_not_empty(&vec);

// All match predicate
let all = Assertion::all_match(&vec, |x| x > 0);

// Any match predicate
let any = Assertion::any_match(&vec, |x| x == 42);
}

String Assertions

#![allow(unused)]
fn main() {
// Contains substring
let contains = Assertion::string_contains(&text, "expected");

// Starts/ends with
let starts = Assertion::starts_with(&text, "prefix");
let ends = Assertion::ends_with(&text, "suffix");

// Regex match
let matches = Assertion::matches_regex(&text, r"\d{3}-\d{4}");

// Length
let len = Assertion::string_length(&text, expected_len);
}

Option/Result Assertions

#![allow(unused)]
fn main() {
// Option
let some = Assertion::is_some(&option_value);
let none = Assertion::is_none(&option_value);

// Result
let ok = Assertion::is_ok(&result);
let err = Assertion::is_err(&result);
}

Custom Assertions

#![allow(unused)]
fn main() {
// Create custom assertion
fn assert_valid_score(score: u32) -> Assertion {
    Assertion::custom(
        score <= 10,
        format!("Score {} should be <= 10", score),
    )
}

// Use it
let assertion = assert_valid_score(game.score);
assert!(assertion.passed);
}

Assertion Result

All assertions return an Assertion struct:

#![allow(unused)]
fn main() {
pub struct Assertion {
    pub passed: bool,
    pub message: String,
    pub expected: Option<String>,
    pub actual: Option<String>,
}
}

Combining Assertions

#![allow(unused)]
fn main() {
// All must pass
let all_pass = Assertion::all(&[
    Assertion::in_range(x, 0.0, 800.0),
    Assertion::in_range(y, 0.0, 600.0),
    Assertion::greater_than(health, 0),
]);

// Any must pass
let any_pass = Assertion::any(&[
    Assertion::equals(&state, &State::Running),
    Assertion::equals(&state, &State::Paused),
]);
}

Game-Specific Assertions

#![allow(unused)]
fn main() {
// Entity exists
let exists = Assertion::entity_exists(&world, entity_id);

// Component value
let has_component = Assertion::has_component::<Position>(&world, entity);

// Position bounds
let in_bounds = Assertion::position_in_bounds(
    position,
    Bounds::new(0.0, 0.0, 800.0, 600.0),
);

// Collision occurred
let collided = Assertion::entities_colliding(&world, entity_a, entity_b);
}

Example Test

#![allow(unused)]
fn main() {
#[test]
fn test_game_state_validity() {
    let mut platform = WebPlatform::new_for_test(WebConfig::default());

    // Advance game
    for _ in 0..100 {
        platform.advance_frame(1.0 / 60.0);
    }

    let state = platform.get_game_state();

    // Multiple assertions
    assert!(Assertion::in_range(state.ball.x, 0.0, 800.0).passed);
    assert!(Assertion::in_range(state.ball.y, 0.0, 600.0).passed);
    assert!(Assertion::in_range(state.paddle_left.y, 0.0, 600.0).passed);
    assert!(Assertion::in_range(state.paddle_right.y, 0.0, 600.0).passed);
    assert!(Assertion::lte(state.score_left, 10).passed);
    assert!(Assertion::lte(state.score_right, 10).passed);
}
}

Simulation

Probar provides deterministic game simulation for testing.

Basic Simulation

#![allow(unused)]
fn main() {
use jugar_probar::{run_simulation, SimulationConfig};

let config = SimulationConfig::new(seed, num_frames);
let result = run_simulation(config, |frame| {
    // Return inputs for this frame
    vec![]  // No inputs
});

assert!(result.completed);
println!("Final state hash: {}", result.state_hash);
}

Simulation with Inputs

#![allow(unused)]
fn main() {
use jugar_probar::{run_simulation, SimulationConfig, InputEvent};

let config = SimulationConfig::new(42, 300);
let result = run_simulation(config, |frame| {
    // Move paddle up for first 100 frames
    if frame < 100 {
        vec![InputEvent::key_held("KeyW")]
    } else {
        vec![]
    }
});
}

Input Events

#![allow(unused)]
fn main() {
// Keyboard
InputEvent::key_press("Space")      // Just pressed
InputEvent::key_held("KeyW")        // Held down
InputEvent::key_release("Escape")   // Just released

// Mouse
InputEvent::mouse_move(400.0, 300.0)
InputEvent::mouse_press(MouseButton::Left)
InputEvent::mouse_release(MouseButton::Left)

// Touch
InputEvent::touch_start(0, 100.0, 200.0)  // id, x, y
InputEvent::touch_move(0, 150.0, 250.0)
InputEvent::touch_end(0)
}

Deterministic Replay

#![allow(unused)]
fn main() {
use jugar_probar::{run_simulation, run_replay, SimulationConfig};

// Record a simulation
let config = SimulationConfig::new(42, 500);
let recording = run_simulation(config, |frame| {
    vec![InputEvent::key_press("ArrowUp")]
});

// Replay it
let replay = run_replay(&recording);

// Verify determinism
assert!(replay.determinism_verified);
assert_eq!(recording.state_hash, replay.state_hash);
}

Simulation Config

#![allow(unused)]
fn main() {
pub struct SimulationConfig {
    pub seed: u64,           // Random seed for reproducibility
    pub frames: u32,         // Number of frames to simulate
    pub fixed_dt: f32,       // Timestep (default: 1/60)
    pub max_time: f32,       // Max real time (for timeout)
}

let config = SimulationConfig {
    seed: 12345,
    frames: 1000,
    fixed_dt: 1.0 / 60.0,
    max_time: 60.0,
};
}

Simulation Result

#![allow(unused)]
fn main() {
pub struct SimulationResult {
    pub completed: bool,
    pub frames_run: u32,
    pub state_hash: u64,
    pub final_state: GameState,
    pub recording: Recording,
    pub events: Vec<GameEvent>,
}
}

Recording Format

#![allow(unused)]
fn main() {
pub struct Recording {
    pub seed: u64,
    pub frames: Vec<FrameInputs>,
    pub state_snapshots: Vec<StateSnapshot>,
}

pub struct FrameInputs {
    pub frame: u32,
    pub inputs: Vec<InputEvent>,
}
}

Invariant Checking

#![allow(unused)]
fn main() {
use jugar_probar::{run_simulation_with_invariants, Invariant};

let invariants = vec![
    Invariant::new("ball_in_bounds", |state| {
        state.ball.x >= 0.0 && state.ball.x <= 800.0 &&
        state.ball.y >= 0.0 && state.ball.y <= 600.0
    }),
    Invariant::new("score_valid", |state| {
        state.score_left <= 10 && state.score_right <= 10
    }),
];

let result = run_simulation_with_invariants(config, invariants, |_| vec![]);

assert!(result.all_invariants_held);
for violation in &result.violations {
    println!("Violation at frame {}: {}", violation.frame, violation.invariant);
}
}

Scenario Testing

#![allow(unused)]
fn main() {
#[test]
fn test_game_scenarios() {
    let scenarios = vec![
        ("player_wins", |f| if f < 500 { vec![key("KeyW")] } else { vec![] }),
        ("ai_wins", |_| vec![]),  // No player input
        ("timeout", |_| vec![key("KeyP")]),  // Pause
    ];

    for (name, input_fn) in scenarios {
        let config = SimulationConfig::new(42, 1000);
        let result = run_simulation(config, input_fn);

        println!("Scenario '{}': score = {} - {}",
            name, result.final_state.score_left, result.final_state.score_right);
    }
}
}

Performance Benchmarking

#![allow(unused)]
fn main() {
use std::time::Instant;

#[test]
fn benchmark_simulation() {
    let config = SimulationConfig::new(42, 10000);

    let start = Instant::now();
    let result = run_simulation(config, |_| vec![]);
    let elapsed = start.elapsed();

    println!("10000 frames in {:?}", elapsed);
    println!("FPS: {}", 10000.0 / elapsed.as_secs_f64());

    // Should run faster than real-time
    assert!(elapsed.as_secs_f64() < 10000.0 / 60.0);
}
}

Fuzzing

Probar includes fuzzing support for finding edge cases in game logic.

Random Walk Agent

#![allow(unused)]
fn main() {
use jugar_probar::{RandomWalkAgent, Seed};

let seed = Seed::from_u64(12345);
let mut agent = RandomWalkAgent::new(seed);

// Generate random inputs for each frame
for frame in 0..1000 {
    let inputs = agent.next_inputs();
    platform.process_inputs(&inputs);
    platform.advance_frame(1.0 / 60.0);
}
}

Fuzzing with Invariants

#![allow(unused)]
fn main() {
use jugar_probar::{fuzz_with_invariants, FuzzConfig, Invariant};

let invariants = vec![
    Invariant::new("no_crashes", |state| state.is_valid()),
    Invariant::new("ball_visible", |state| {
        state.ball.x.is_finite() && state.ball.y.is_finite()
    }),
    Invariant::new("score_bounded", |state| {
        state.score_left <= 100 && state.score_right <= 100
    }),
];

let config = FuzzConfig {
    iterations: 1000,
    frames_per_iteration: 500,
    seed: 42,
};

let result = fuzz_with_invariants(config, invariants);

if !result.all_passed {
    for failure in &result.failures {
        println!("Invariant '{}' failed at iteration {} frame {}",
            failure.invariant_name,
            failure.iteration,
            failure.frame);
        println!("Reproducing seed: {}", failure.seed);
    }
}
}

Input Generation Strategies

#![allow(unused)]
fn main() {
// Random inputs
let mut agent = RandomWalkAgent::new(seed);

// Biased toward movement
let mut agent = RandomWalkAgent::new(seed)
    .with_key_probability("KeyW", 0.3)
    .with_key_probability("KeyS", 0.3)
    .with_key_probability("Space", 0.1);

// Chaos monkey (random everything)
let mut agent = ChaosAgent::new(seed);

// Adversarial (try to break the game)
let mut agent = AdversarialAgent::new(seed)
    .target_invariant(|state| state.ball.y >= 0.0);
}

Property-Based Testing

#![allow(unused)]
fn main() {
use proptest::prelude::*;

proptest! {
    #[test]
    fn ball_stays_in_bounds(seed in 0u64..10000) {
        let config = SimulationConfig::new(seed, 1000);
        let result = run_simulation(config, |_| vec![]);

        prop_assert!(result.final_state.ball.x >= 0.0);
        prop_assert!(result.final_state.ball.x <= 800.0);
        prop_assert!(result.final_state.ball.y >= 0.0);
        prop_assert!(result.final_state.ball.y <= 600.0);
    }

    #[test]
    fn score_is_valid(
        seed in 0u64..10000,
        frames in 100u32..5000
    ) {
        let config = SimulationConfig::new(seed, frames);
        let result = run_simulation(config, |_| vec![]);

        prop_assert!(result.final_state.score_left <= 10);
        prop_assert!(result.final_state.score_right <= 10);
    }
}
}

Seed Management

#![allow(unused)]
fn main() {
use jugar_probar::Seed;

// From u64
let seed = Seed::from_u64(42);

// From bytes
let seed = Seed::from_bytes(&[1, 2, 3, 4, 5, 6, 7, 8]);

// Random
let seed = Seed::random();

// Get value for reproduction
println!("Failing seed: {}", seed.as_u64());
}

Reproducing Failures

When fuzzing finds a failure, reproduce it:

#![allow(unused)]
fn main() {
#[test]
fn reproduce_bug_12345() {
    // Seed from fuzzing failure
    let seed = Seed::from_u64(12345);

    let config = SimulationConfig::new(seed.as_u64(), 500);
    let result = run_simulation(config, |_| vec![]);

    // This should fail with the original bug
    assert!(result.final_state.ball.y >= 0.0);
}
}

Fuzzing Configuration

#![allow(unused)]
fn main() {
pub struct FuzzConfig {
    pub iterations: u32,           // Number of random runs
    pub frames_per_iteration: u32, // Frames per run
    pub seed: u64,                 // Base seed
    pub timeout_seconds: u32,      // Max time per iteration
    pub parallel: bool,            // Run in parallel
    pub save_failures: bool,       // Save failing cases
}

let config = FuzzConfig {
    iterations: 10000,
    frames_per_iteration: 1000,
    seed: 42,
    timeout_seconds: 10,
    parallel: true,
    save_failures: true,
};
}

Shrinking

When a failure is found, Probar automatically shrinks the input:

#![allow(unused)]
fn main() {
let result = fuzz_with_shrinking(config, invariants);

if let Some(failure) = result.first_failure {
    println!("Original failure at frame {}", failure.original_frame);
    println!("Shrunk to frame {}", failure.shrunk_frame);
    println!("Minimal inputs: {:?}", failure.minimal_inputs);
}
}

Continuous Fuzzing

Run fuzzing in CI:

# Run fuzzing for 10 minutes
FUZZ_DURATION=600 cargo test fuzz_ -- --ignored

# Or via make
make fuzz-ci

Locators

Probar provides Playwright-style locators for finding game elements.

Basic Locators

#![allow(unused)]
fn main() {
use jugar_probar::locator::*;

// By ID
let player = Locator::id("player");

// By name
let score = Locator::name("score_display");

// By component type
let balls = Locator::component::<Ball>();

// By tag
let enemies = Locator::tag("enemy");
}

Entity Queries

#![allow(unused)]
fn main() {
let platform = WebPlatform::new_for_test(config);

// Find single entity
let player = platform.locate(Locator::id("player"))?;
let pos = platform.get_position(player);

// Find all matching
let coins: Vec<Entity> = platform.locate_all(Locator::tag("coin"));
assert_eq!(coins.len(), 5);

// First matching
let first_enemy = platform.locate_first(Locator::tag("enemy"));
}

Compound Locators

#![allow(unused)]
fn main() {
// AND - must match all
let armed_enemy = Locator::tag("enemy")
    .and(Locator::has_component::<Weapon>());

// OR - match any
let interactable = Locator::tag("door")
    .or(Locator::tag("chest"));

// NOT - exclude
let non_player = Locator::component::<Character>()
    .not(Locator::id("player"));
}

Spatial Locators

#![allow(unused)]
fn main() {
// Within radius
let nearby = Locator::within_radius(player_pos, 100.0);

// In bounds
let visible = Locator::in_bounds(screen_bounds);

// Nearest to point
let closest_enemy = Locator::nearest_to(player_pos)
    .with_filter(Locator::tag("enemy"));
}

Component-Based Locators

#![allow(unused)]
fn main() {
// Has specific component
let physics_entities = Locator::has_component::<RigidBody>();

// Component matches predicate
let low_health = Locator::component_matches::<Health>(|h| h.value < 20);

// Has all components
let complete_entities = Locator::has_all_components::<(
    Position,
    Velocity,
    Sprite,
)>();
}

Type-Safe Locators (with derive)

Using jugar-probar-derive for compile-time checked selectors:

#![allow(unused)]
fn main() {
use jugar_probar_derive::Entity;

#[derive(Entity)]
#[entity(id = "player")]
struct Player;

#[derive(Entity)]
#[entity(tag = "enemy")]
struct Enemy;

// Compile-time verified
let player = platform.locate::<Player>()?;
let enemies = platform.locate_all::<Enemy>();
}

Waiting for Elements

#![allow(unused)]
fn main() {
// Wait for entity to exist
let boss = platform.wait_for(
    Locator::id("boss"),
    Duration::from_secs(5),
)?;

// Wait for condition
platform.wait_until(
    || platform.locate(Locator::id("door")).is_some(),
    Duration::from_secs(2),
)?;
}

Locator Chains

#![allow(unused)]
fn main() {
// Find children
let player_weapon = Locator::id("player")
    .child(Locator::tag("weapon"));

// Find parent
let weapon_owner = Locator::id("sword")
    .parent();

// Find siblings
let adjacent_tiles = Locator::id("current_tile")
    .siblings();
}

Actions on Located Elements

#![allow(unused)]
fn main() {
let button = platform.locate(Locator::id("start_button"))?;

// Get info
let pos = platform.get_position(button);
let bounds = platform.get_bounds(button);
let visible = platform.is_visible(button);

// Interact
platform.click(button);
platform.hover(button);

// Check state
let enabled = platform.is_enabled(button);
let focused = platform.is_focused(button);
}

Example Test

#![allow(unused)]
fn main() {
#[test]
fn test_coin_collection() {
    let mut platform = WebPlatform::new_for_test(config);

    // Count initial coins
    let initial_coins = platform.locate_all(Locator::tag("coin")).len();
    assert_eq!(initial_coins, 5);

    // Move player to first coin
    let first_coin = platform.locate_first(Locator::tag("coin")).unwrap();
    let coin_pos = platform.get_position(first_coin);

    // Simulate movement
    move_player_to(&mut platform, coin_pos);

    // Coin should be collected
    let remaining_coins = platform.locate_all(Locator::tag("coin")).len();
    assert_eq!(remaining_coins, 4);

    // Score should increase
    let score_display = platform.locate(Locator::id("score")).unwrap();
    let score_text = platform.get_text(score_display);
    assert!(score_text.contains("10"));
}
}

Coverage Tooling

Probar includes advanced coverage instrumentation for WASM games.

Overview

Traditional coverage tools (LLVM, gcov) don't work well with WASM. Probar implements a renderfarm-inspired block coverage model where:

  • WASM code is decomposed into coverage blocks (like render buckets)
  • Blocks are independently testable and falsifiable
  • Coverage aggregation uses SIMD-accelerated operations via Trueno

Basic Coverage

#![allow(unused)]
fn main() {
use jugar_probar::coverage::*;

// Enable coverage collection
let mut coverage = CoverageCollector::new();

// Run tests with coverage
coverage.start();
run_tests();
let report = coverage.finish();

// Print summary
println!("Line coverage: {:.1}%", report.line_coverage * 100.0);
println!("Branch coverage: {:.1}%", report.branch_coverage * 100.0);
println!("Function coverage: {:.1}%", report.function_coverage * 100.0);
}

Block-Based Coverage

#![allow(unused)]
fn main() {
use jugar_probar::coverage::{BlockId, FunctionId, EdgeId};

// Type-safe identifiers (Poka-Yoke)
let block = BlockId::new(42);
let function = FunctionId::new(1);

// EdgeId encodes source and target
let edge = EdgeId::new(BlockId::new(10), BlockId::new(20));
assert_eq!(edge.source().as_u32(), 10);
assert_eq!(edge.target().as_u32(), 20);
}

Thread-Local Buffering (Muda Elimination)

#![allow(unused)]
fn main() {
use jugar_probar::coverage::ThreadLocalCounters;

// Traditional: Atomic increment on every hit (contention)
// Probar: Thread-local buffering, batch flush

let counters = ThreadLocalCounters::new(1000);  // 1000 blocks

// Fast local increment
counters.hit(BlockId::new(42));

// Periodic flush to global
counters.flush();
}

Running Coverage

Via Makefile

# Full coverage report
make coverage

# E2E coverage
make test-e2e-coverage

# Quick summary
make coverage-summary

# Open HTML report
make coverage-open

Via Cargo

# Generate report
cargo llvm-cov --html --output-dir target/coverage

# Summary only
cargo llvm-cov report --summary-only

# With nextest
cargo llvm-cov nextest --workspace

Coverage Targets

MetricMinimumTarget
Line Coverage85%95%
Branch Coverage75%90%
Function Coverage90%100%
Mutation Score80%90%

Coverage Report

#![allow(unused)]
fn main() {
pub struct CoverageReport {
    pub line_coverage: f64,
    pub branch_coverage: f64,
    pub function_coverage: f64,
    pub covered_lines: u32,
    pub total_lines: u32,
    pub covered_branches: u32,
    pub total_branches: u32,
    pub covered_functions: u32,
    pub total_functions: u32,
    pub uncovered_lines: Vec<LineInfo>,
}
}

Superblock Scheduling

For parallel coverage analysis:

#![allow(unused)]
fn main() {
use jugar_probar::coverage::{Superblock, Scheduler};

// Group blocks into superblocks
let superblocks = Scheduler::create_superblocks(&blocks, num_workers);

// Execute in parallel
let results = superblocks
    .par_iter()
    .map(|sb| execute_superblock(sb))
    .collect();

// Merge results
let final_coverage = CoverageMerger::merge(&results);
}

Soft Jidoka (Error Classification)

#![allow(unused)]
fn main() {
use jugar_probar::coverage::{Error, Severity};

// Distinguish fatal vs recoverable errors
match error.severity {
    Severity::Fatal => {
        // Stop immediately (Andon cord)
        panic!("Fatal error in coverage: {}", error);
    }
    Severity::Recoverable => {
        // Log and continue
        log::warn!("Recoverable error: {}", error);
        continue;
    }
    Severity::Ignorable => {
        // Skip silently
    }
}
}

Coverage Example

cargo run --example coverage_demo -p jugar-probar

Output:

=== Probar Coverage Demo ===

--- Type-Safe Identifiers (Poka-Yoke) ---
BlockId(42) - Type-safe block identifier
FunctionId(1) - Type-safe function identifier
EdgeId(10 -> 20) - Encodes source and target

--- Thread-Local Counters (Muda Elimination) ---
Created counters for 1000 blocks
Hit block 42: 1000 times
Hit block 99: 500 times
After flush: block 42 = 1000, block 99 = 500

--- Superblock Scheduling ---
4 workers, 16 superblocks
Superblock 0: blocks [0..62]
Superblock 1: blocks [63..125]
...

✅ Coverage demo complete!

Integration with CI

- name: Generate coverage
  run: |
    cargo llvm-cov --lcov --output-path lcov.info
    cargo llvm-cov report --summary-only

- name: Upload coverage
  uses: codecov/codecov-action@v3
  with:
    files: lcov.info

Deterministic Replay

Probar enables frame-perfect replay of game sessions.

Why Deterministic Replay?

  • Bug Reproduction: Replay exact sequence that caused a bug
  • Regression Testing: Verify behavior matches after changes
  • Test Generation: Record gameplay, convert to tests
  • Demo Playback: Record and replay gameplay sequences

Recording a Session

#![allow(unused)]
fn main() {
use jugar_probar::{Recorder, Recording};

let mut recorder = Recorder::new(seed);
let mut platform = WebPlatform::new_for_test(config);

// Play game and record
for frame in 0..1000 {
    let inputs = get_user_inputs();
    recorder.record_frame(frame, &inputs);

    platform.process_inputs(&inputs);
    platform.advance_frame(1.0 / 60.0);
}

// Get recording
let recording = recorder.finish();

// Save to file
recording.save("gameplay.replay")?;
}

Replaying a Session

#![allow(unused)]
fn main() {
use jugar_probar::{Recording, Replayer};

// Load recording
let recording = Recording::load("gameplay.replay")?;

let mut replayer = Replayer::new(&recording);
let mut platform = WebPlatform::new_for_test(config);

// Replay exactly
while let Some(inputs) = replayer.next_frame() {
    platform.process_inputs(&inputs);
    platform.advance_frame(1.0 / 60.0);
}

// Verify final state matches
assert_eq!(
    replayer.expected_final_hash(),
    platform.state_hash()
);
}

Recording Format

#![allow(unused)]
fn main() {
pub struct Recording {
    pub version: u32,
    pub seed: u64,
    pub config: GameConfig,
    pub frames: Vec<FrameData>,
    pub final_state_hash: u64,
}

pub struct FrameData {
    pub frame_number: u32,
    pub inputs: Vec<InputEvent>,
    pub state_hash: Option<u64>,  // Optional checkpoints
}
}

State Hashing

#![allow(unused)]
fn main() {
// Hash game state for comparison
let hash = platform.state_hash();

// Or hash specific components
let ball_hash = hash_state(&platform.get_ball_state());
let score_hash = hash_state(&platform.get_score());
}

Verification

#![allow(unused)]
fn main() {
use jugar_probar::{verify_replay, ReplayVerification};

let result = verify_replay(&recording);

match result {
    ReplayVerification::Perfect => {
        println!("Replay is deterministic!");
    }
    ReplayVerification::Diverged { frame, expected, actual } => {
        println!("Diverged at frame {}", frame);
        println!("Expected hash: {}", expected);
        println!("Actual hash: {}", actual);
    }
    ReplayVerification::Failed(error) => {
        println!("Replay failed: {}", error);
    }
}
}

Checkpoints

Add checkpoints for faster debugging:

#![allow(unused)]
fn main() {
let mut recorder = Recorder::new(seed)
    .with_checkpoint_interval(60);  // Every 60 frames

// Or manual checkpoints
recorder.add_checkpoint(platform.snapshot());
}

Binary Replay Format

#![allow(unused)]
fn main() {
// Compact binary format for storage
let bytes = recording.to_bytes();
let recording = Recording::from_bytes(&bytes)?;

// Compressed
let compressed = recording.to_compressed_bytes();
let recording = Recording::from_compressed_bytes(&compressed)?;
}

Replay Speed Control

#![allow(unused)]
fn main() {
let mut replayer = Replayer::new(&recording);

// Normal speed
replayer.set_speed(1.0);

// Fast forward
replayer.set_speed(4.0);

// Slow motion
replayer.set_speed(0.25);

// Step by step
replayer.step();  // Advance one frame
}

Example: Test from Replay

#![allow(unused)]
fn main() {
#[test]
fn test_from_recorded_gameplay() {
    let recording = Recording::load("tests/fixtures/win_game.replay").unwrap();

    let mut replayer = Replayer::new(&recording);
    let mut platform = WebPlatform::new_for_test(recording.config.clone());

    // Replay all frames
    while let Some(inputs) = replayer.next_frame() {
        platform.process_inputs(&inputs);
        platform.advance_frame(1.0 / 60.0);
    }

    // Verify end state
    let state = platform.get_game_state();
    assert_eq!(state.winner, Some(Player::Left));
    assert_eq!(state.score_left, 10);
}
}

CI Integration

# Verify all replay files are still deterministic
cargo test replay_verification -- --include-ignored

# Or via make
make verify-replays

Debugging with Replays

#![allow(unused)]
fn main() {
// Find frame where bug occurs
let bug_frame = binary_search_replay(&recording, |state| {
    state.ball.y < 0.0  // Bug condition
});

println!("Bug first occurs at frame {}", bug_frame);

// Get inputs leading up to bug
let inputs = recording.frames[..bug_frame].to_vec();
println!("Inputs: {:?}", inputs);
}

Accessibility Testing

Probar includes WCAG accessibility checking for games.

Overview

Probar validates accessibility requirements:

  • Color contrast ratios (WCAG 2.1)
  • Photosensitivity (flashing content)
  • Text readability
  • Input alternatives

Color Contrast

#![allow(unused)]
fn main() {
use jugar_probar::accessibility::*;

// Check contrast ratio
let ratio = contrast_ratio(foreground_color, background_color);
assert!(ratio >= 4.5);  // WCAG AA for normal text
assert!(ratio >= 7.0);  // WCAG AAA for normal text
assert!(ratio >= 3.0);  // WCAG AA for large text

// Automatic checking
let result = check_text_contrast(&platform);
assert!(result.passes_aa);
}

Contrast Levels

LevelNormal TextLarge Text
AA4.5:13:1
AAA7:14.5:1

Photosensitivity

#![allow(unused)]
fn main() {
use jugar_probar::accessibility::*;

// Check for problematic flashing
let mut checker = FlashChecker::new();

for frame in 0..180 {  // 3 seconds at 60fps
    let screenshot = platform.capture_frame();
    checker.add_frame(&screenshot);
}

let result = checker.analyze();
assert!(result.safe_for_photosensitive);

// WCAG 2.3.1: No more than 3 flashes per second
assert!(result.max_flashes_per_second <= 3.0);
}

Color Blindness Simulation

#![allow(unused)]
fn main() {
use jugar_probar::accessibility::*;

// Simulate different types
let normal = platform.capture_frame();
let protanopia = simulate_color_blindness(&normal, ColorBlindType::Protanopia);
let deuteranopia = simulate_color_blindness(&normal, ColorBlindType::Deuteranopia);
let tritanopia = simulate_color_blindness(&normal, ColorBlindType::Tritanopia);

// Check important elements are still distinguishable
assert!(elements_distinguishable(&protanopia, "player", "enemy"));
}

Text Accessibility

#![allow(unused)]
fn main() {
use jugar_probar::accessibility::*;

// Check text size
let text_elements = platform.locate_all(Locator::component::<Text>());

for text in text_elements {
    let size = platform.get_font_size(text);
    assert!(size >= 12.0, "Text too small: {}", size);

    // Check contrast
    let fg = platform.get_text_color(text);
    let bg = platform.get_background_color(text);
    let ratio = contrast_ratio(fg, bg);
    assert!(ratio >= 4.5, "Insufficient contrast: {}", ratio);
}
}

Input Alternatives

#![allow(unused)]
fn main() {
use jugar_probar::accessibility::*;

// Verify all actions have keyboard alternatives
let result = check_keyboard_accessibility(&platform);
assert!(result.all_actions_keyboard_accessible);

// List any mouse-only actions
for action in &result.mouse_only_actions {
    println!("Missing keyboard alternative: {}", action);
}
}

Running Accessibility Tests

# Run accessibility demo
cargo run --example accessibility_demo -p jugar-probar

# Run accessibility tests
cargo test -p jugar-web accessibility_

Accessibility Report

#![allow(unused)]
fn main() {
pub struct AccessibilityReport {
    pub passes_wcag_aa: bool,
    pub passes_wcag_aaa: bool,
    pub contrast_issues: Vec<ContrastIssue>,
    pub flash_warnings: Vec<FlashWarning>,
    pub keyboard_issues: Vec<KeyboardIssue>,
    pub overall_score: f32,  // 0.0 - 100.0
}
}

Example Test

#![allow(unused)]
fn main() {
#[test]
fn test_game_accessibility() {
    let mut platform = WebPlatform::new_for_test(config);

    // Run a few frames
    for _ in 0..60 {
        platform.advance_frame(1.0 / 60.0);
    }

    // Check accessibility
    let report = check_accessibility(&platform);

    // Must pass WCAG AA
    assert!(report.passes_wcag_aa, "WCAG AA failures: {:?}", report.contrast_issues);

    // No flash warnings
    assert!(report.flash_warnings.is_empty(), "Flash warnings: {:?}", report.flash_warnings);

    // Score should be high
    assert!(report.overall_score >= 80.0, "Accessibility score too low: {}", report.overall_score);
}
}

Continuous Monitoring

#![allow(unused)]
fn main() {
use jugar_probar::accessibility::*;

// Monitor throughout gameplay
let mut monitor = AccessibilityMonitor::new();

for frame in 0..6000 {  // 100 seconds
    platform.advance_frame(1.0 / 60.0);

    // Check each frame
    monitor.check_frame(&platform);
}

let report = monitor.finish();
println!("Accessibility issues found: {}", report.issues.len());
}

Configuration

#![allow(unused)]
fn main() {
pub struct AccessibilityConfig {
    pub min_contrast_ratio: f32,     // Default: 4.5 (AA)
    pub min_text_size: f32,          // Default: 12.0
    pub max_flashes_per_second: f32, // Default: 3.0
    pub require_keyboard_nav: bool,  // Default: true
}

let config = AccessibilityConfig {
    min_contrast_ratio: 7.0,  // AAA level
    ..Default::default()
};

let report = check_accessibility_with_config(&platform, &config);
}

Universal Pong

A responsive Pong implementation that works from mobile to 32:9 ultrawide.

Features

  • Touch controls (mobile)
  • Keyboard (W/S, Up/Down)
  • Gamepad support
  • Responsive paddle positioning
  • AI opponent with Dynamic Difficulty Adjustment
  • SHAP-like explainability widgets

Running

# Build WASM
make build-web

# Serve locally
make serve-web

# Open http://localhost:8080

Controls

InputPlayer 1Player 2
KeyboardW/SUp/Down
TouchLeft sideRight side
GamepadLeft stickRight stick

Architecture

Game State

#![allow(unused)]
fn main() {
pub struct PongState {
    pub ball: Ball,
    pub paddle_left: Paddle,
    pub paddle_right: Paddle,
    pub score_left: u32,
    pub score_right: u32,
    pub game_mode: GameMode,
}
}

Ball Physics

#![allow(unused)]
fn main() {
impl Ball {
    pub fn update(&mut self, dt: f32) {
        self.position += self.velocity * dt;

        // Wall bounce
        if self.position.y <= 0.0 || self.position.y >= 600.0 {
            self.velocity.y = -self.velocity.y;
        }
    }

    pub fn check_paddle_collision(&mut self, paddle: &Paddle) {
        if self.bounds().intersects(paddle.bounds()) {
            self.velocity.x = -self.velocity.x;
            // Add spin based on hit position
            let offset = (self.position.y - paddle.position.y) / paddle.height;
            self.velocity.y += offset * 200.0;
        }
    }
}
}

AI Opponent

The AI uses a trained model (.apr format) with Dynamic Difficulty Adjustment:

#![allow(unused)]
fn main() {
pub struct PongAI {
    model: AprModel,
    difficulty: f32,  // 0.0 - 1.0
}

impl PongAI {
    pub fn update(&mut self, state: &PongState) -> f32 {
        // Predict optimal position
        let optimal = self.model.predict(state);

        // Add error based on difficulty
        let error = (1.0 - self.difficulty) * random_offset();

        optimal + error
    }
}
}

Responsive Design

Viewport Scaling

#![allow(unused)]
fn main() {
// Safe area for 16:9 gameplay
let safe = viewport.safe_area();

// Extended for ultrawide
let extended = viewport.extended_area();

// Position paddles at edges
let left_paddle_x = safe.left + PADDLE_MARGIN;
let right_paddle_x = safe.right - PADDLE_MARGIN;
}

Touch Zones

#![allow(unused)]
fn main() {
// Left half: Player 1
// Right half: Player 2
fn get_touch_player(touch_x: f32, screen_width: f32) -> Player {
    if touch_x < screen_width / 2.0 {
        Player::Left
    } else {
        Player::Right
    }
}
}

Testing

# Run Probar E2E tests
make test-e2e

# Run with verbose output
make test-e2e-verbose

Test Suites

SuiteTestsCoverage
Core Functionality6WASM loading, rendering
Demo Features22Game modes, HUD, AI
Release Readiness11Stress tests, edge cases

Explainability Widgets

The AI's decision-making is visualized:

#![allow(unused)]
fn main() {
pub struct ShapWidget {
    features: Vec<FeatureContribution>,
}

impl ShapWidget {
    pub fn render(&self) -> Vec<RenderCommand> {
        // Show feature contributions as bars
        self.features.iter().map(|f| {
            RenderCommand::DrawRect {
                width: f.contribution.abs() * 100.0,
                color: if f.contribution > 0.0 { GREEN } else { RED },
                // ...
            }
        }).collect()
    }
}
}

Source Code

  • crates/jugar-web/src/demo.rs - Pong demo implementation
  • crates/jugar-web/src/ai.rs - AI opponent
  • examples/pong-web/index.html - HTML loader

Physics Toy Sandbox

A remixable Rube Goldberg physics builder.

Overview

The Physics Toy Sandbox is a creative physics playground where users can:

  • Build contraptions from various materials
  • Watch physics simulations run
  • Remix and share designs
  • Learn physics concepts through play

Running

# Build
make sandbox

# Test
make test-sandbox

# Build WASM
make build-sandbox-wasm

Concepts

Materials

#![allow(unused)]
fn main() {
pub enum Material {
    Wood,
    Metal,
    Rubber,
    Glass,
    Stone,
}

impl Material {
    pub fn density(&self) -> f32 {
        match self {
            Material::Wood => 0.6,
            Material::Metal => 7.8,
            Material::Rubber => 1.1,
            Material::Glass => 2.5,
            Material::Stone => 2.4,
        }
    }

    pub fn restitution(&self) -> f32 {
        match self {
            Material::Wood => 0.3,
            Material::Metal => 0.2,
            Material::Rubber => 0.9,
            Material::Glass => 0.1,
            Material::Stone => 0.1,
        }
    }
}
}

Contraptions

#![allow(unused)]
fn main() {
pub struct Contraption {
    pub parts: Vec<Part>,
    pub joints: Vec<Joint>,
    pub version: EngineVersion,
}

pub struct Part {
    pub shape: Shape,
    pub material: Material,
    pub position: Vec2,
    pub rotation: f32,
}

pub enum Joint {
    Hinge { a: PartId, b: PartId, anchor: Vec2 },
    Spring { a: PartId, b: PartId, stiffness: f32 },
    Rope { a: PartId, b: PartId, length: f32 },
}
}

Complexity Thermometer

Visual control for contraption complexity (Mieruka principle):

#![allow(unused)]
fn main() {
pub struct ComplexityThermometer {
    part_count: u32,
    joint_count: u32,
    physics_budget: f32,
}

impl ComplexityThermometer {
    pub fn score(&self) -> f32 {
        let parts = self.part_count as f32 * 1.0;
        let joints = self.joint_count as f32 * 2.0;
        (parts + joints) / self.physics_budget
    }

    pub fn color(&self) -> Color {
        let score = self.score();
        if score < 0.5 {
            Color::GREEN
        } else if score < 0.8 {
            Color::YELLOW
        } else {
            Color::RED
        }
    }
}
}

Remix System

Contraptions can be remixed and shared:

#![allow(unused)]
fn main() {
pub struct RemixMetadata {
    pub original_author: String,
    pub original_version: String,
    pub remix_chain: Vec<RemixInfo>,
}

impl Contraption {
    pub fn remix(&self, author: &str) -> Contraption {
        Contraption {
            parts: self.parts.clone(),
            joints: self.joints.clone(),
            version: EngineVersion::current(),
            metadata: RemixMetadata {
                remix_chain: {
                    let mut chain = self.metadata.remix_chain.clone();
                    chain.push(RemixInfo {
                        author: author.to_string(),
                        timestamp: now(),
                    });
                    chain
                },
                ..self.metadata.clone()
            },
        }
    }
}
}

Engine Versioning

Replay compatibility across versions (Jidoka principle):

#![allow(unused)]
fn main() {
pub struct EngineVersion {
    pub major: u32,
    pub minor: u32,
    pub patch: u32,
}

impl EngineVersion {
    pub fn is_compatible(&self, other: &EngineVersion) -> bool {
        self.major == other.major
    }

    pub fn current() -> Self {
        EngineVersion { major: 1, minor: 0, patch: 0 }
    }
}
}

Toyota Way Principles

PrincipleApplication
Poka-YokeNonZeroU32 for density (no division by zero)
JidokaEngine versioning for replay compatibility
MierukaComplexityThermometer for visual control
MudaNo scalar fallback (WebGPU/WASM SIMD only)

Testing

# All tests
make test-sandbox

# With coverage
make test-sandbox-coverage

# Mutation testing
make sandbox-mutate

Source Code

  • crates/physics-toy-sandbox/src/lib.rs - Main library
  • crates/physics-toy-sandbox/src/material.rs - Material system
  • crates/physics-toy-sandbox/src/contraption.rs - Contraption building
  • crates/physics-toy-sandbox/src/remix.rs - Remix system

Running Examples

Available Examples

Probar Testing Examples

# Deterministic simulation with replay
cargo run --example pong_simulation -p jugar-probar

# Playwright-style locator API demo
cargo run --example locator_demo -p jugar-probar

# WCAG accessibility checking
cargo run --example accessibility_demo -p jugar-probar

# Coverage instrumentation demo
cargo run --example coverage_demo -p jugar-probar

Web Examples

# Build and serve Pong
make build-web
make serve-web
# Open http://localhost:8080

Physics Sandbox

# Build and test
make sandbox

# Build for WASM
make build-sandbox-wasm

Example Output

Pong Simulation

=== Probar Pong Simulation Demo ===

--- Demo 1: Pong Simulation ---
Initial state:
  Ball: (400.0, 300.0)
  Paddles: P1=300.0, P2=300.0
  Score: 0 - 0

Simulating 300 frames...

Final state after 300 frames:
  Ball: (234.5, 412.3)
  Paddles: P1=180.0, P2=398.2
  Score: 2 - 1
  State valid: true

--- Demo 2: Deterministic Replay ---
Recording simulation (seed=42, frames=500)...
  Completed: true
  Final hash: 6233835744931225727

Replaying simulation...
  Determinism verified: true
  Hashes match: true

--- Demo 3: Invariant Fuzzing ---
Running 100 fuzz iterations...
  All invariants held across all iterations
  Invariants checked:
    - ball_in_bounds: 100/100 passed
    - score_valid: 100/100 passed
    - paddle_in_bounds: 100/100 passed

Locator Demo

=== Probar Locator Demo ===

--- Basic Locators ---
Locator::id("player") -> Found entity #42
Locator::tag("enemy") -> Found 5 entities
Locator::component::<Ball>() -> Found 1 entity

--- Compound Locators ---
Locator::tag("enemy").and(has_component::<Weapon>()) -> Found 2 entities
Locator::within_radius(player_pos, 100.0) -> Found 3 entities

--- Type-Safe Locators ---
@[derive(Entity)] Player -> Found at (400, 300)
@[derive(Entity)] Enemy -> Found 5 instances

Accessibility Demo

=== Probar Accessibility Demo ===

--- Color Contrast ---
Text "Score: 0" - Foreground: #FFFFFF, Background: #1A1A2E
  Contrast ratio: 12.5:1 (WCAG AAA: PASS)

Button "Start" - Foreground: #000000, Background: #4CAF50
  Contrast ratio: 6.2:1 (WCAG AA: PASS)

--- Photosensitivity ---
Analyzed 180 frames (3 seconds at 60fps)
  Max flashes per second: 0.3
  Safe for photosensitive users: YES

--- Color Blindness ---
Protanopia simulation: All important elements distinguishable
Deuteranopia simulation: All important elements distinguishable

✅ Accessibility check passed!

Creating Your Own Examples

Project Structure

my-jugar-game/
├── Cargo.toml
├── src/
│   └── main.rs
└── examples/
    └── demo.rs

Cargo.toml

[package]
name = "my-jugar-game"
version = "0.1.0"
edition = "2021"

[dependencies]
jugar = "0.1"

[dev-dependencies]
jugar-probar = "0.1"

[[example]]
name = "demo"
path = "examples/demo.rs"

Example Code

// examples/demo.rs
use jugar::prelude::*;

fn main() {
    let config = JugarConfig::default();
    let mut engine = JugarEngine::new(config);

    println!("=== My Jugar Demo ===");

    // Demo your game features
    let player = engine.world_mut().spawn();
    engine.world_mut().add_component(player, Position::new(100.0, 100.0));

    println!("Spawned player at (100, 100)");

    // Run a few frames
    for i in 0..10 {
        engine.update(1.0 / 60.0);
        println!("Frame {}", i);
    }

    println!("Demo complete!");
}

Run Your Example

cargo run --example demo

API Documentation

Online Documentation

The complete API documentation is available at:

https://docs.rs/jugar

Generating Locally

cargo doc --open --no-deps

Main Types

JugarEngine

#![allow(unused)]
fn main() {
pub struct JugarEngine {
    // Fields are private
}

impl JugarEngine {
    pub fn new(config: JugarConfig) -> Self;
    pub fn from_definition(def: GameDefinition) -> Self;

    pub fn world(&self) -> &World;
    pub fn world_mut(&mut self) -> &mut World;

    pub fn run<F>(&mut self, game_loop: F)
    where
        F: FnMut(&mut GameContext) -> LoopControl;

    pub fn update(&mut self, dt: f32);
}
}

JugarConfig

#![allow(unused)]
fn main() {
pub struct JugarConfig {
    pub width: u32,
    pub height: u32,
    pub title: String,
    pub fixed_dt: f32,
    pub debug_overlay: bool,
    pub vsync: bool,
}

impl Default for JugarConfig {
    fn default() -> Self {
        JugarConfig {
            width: 1920,
            height: 1080,
            title: "Jugar Game".to_string(),
            fixed_dt: 1.0 / 60.0,
            debug_overlay: cfg!(debug_assertions),
            vsync: true,
        }
    }
}
}

World (ECS)

#![allow(unused)]
fn main() {
pub struct World { /* ... */ }

impl World {
    pub fn new() -> Self;

    // Entities
    pub fn spawn(&mut self) -> Entity;
    pub fn despawn(&mut self, entity: Entity);
    pub fn is_alive(&self, entity: Entity) -> bool;

    // Components
    pub fn add_component<T: Component>(&mut self, entity: Entity, component: T);
    pub fn remove_component<T: Component>(&mut self, entity: Entity);
    pub fn get_component<T: Component>(&self, entity: Entity) -> Option<&T>;
    pub fn get_component_mut<T: Component>(&mut self, entity: Entity) -> Option<&mut T>;

    // Queries
    pub fn query<Q: Query>(&self) -> QueryIter<Q>;

    // Resources
    pub fn add_resource<T: Resource>(&mut self, resource: T);
    pub fn get_resource<T: Resource>(&self) -> Option<&T>;
    pub fn get_resource_mut<T: Resource>(&mut self) -> Option<&mut T>;
}
}

Components

#![allow(unused)]
fn main() {
// Position
pub struct Position {
    pub x: f32,
    pub y: f32,
}

// Velocity
pub struct Velocity {
    pub x: f32,
    pub y: f32,
}

// Transform
pub struct Transform {
    pub position: Vec2,
    pub rotation: f32,
    pub scale: Vec2,
}
}

Input

#![allow(unused)]
fn main() {
pub struct InputState { /* ... */ }

impl InputState {
    // Keyboard
    pub fn key_pressed(&self, key: KeyCode) -> bool;
    pub fn key_held(&self, key: KeyCode) -> bool;
    pub fn key_released(&self, key: KeyCode) -> bool;

    // Mouse
    pub fn mouse_position(&self) -> Vec2;
    pub fn mouse_pressed(&self, button: MouseButton) -> bool;
    pub fn mouse_held(&self, button: MouseButton) -> bool;
    pub fn scroll_delta(&self) -> Vec2;

    // Touch
    pub fn touches(&self) -> &[Touch];

    // Gamepad
    pub fn gamepad(&self, id: usize) -> Option<&Gamepad>;
}
}

Crate-Specific APIs

jugar-physics

#![allow(unused)]
fn main() {
// PhysicsWorld
pub struct PhysicsWorld { /* ... */ }

impl PhysicsWorld {
    pub fn create_static_body(&mut self, pos: Position, collider: Collider) -> Body;
    pub fn create_dynamic_body(&mut self, pos: Position, collider: Collider, config: RigidBodyConfig) -> Body;
    pub fn apply_force(&mut self, body: Body, force: Vec2);
    pub fn apply_impulse(&mut self, body: Body, impulse: Vec2);
    pub fn raycast(&self, ray: Ray, max_distance: f32) -> Option<RayHit>;
}
}

jugar-ai

#![allow(unused)]
fn main() {
// Behavior Trees
pub struct BehaviorTree { /* ... */ }

impl BehaviorTree {
    pub fn new() -> BehaviorTreeBuilder;
    pub fn tick(&self, context: &mut Context) -> Status;
}

// GOAP
pub struct GoapPlanner { /* ... */ }

impl GoapPlanner {
    pub fn plan(&self, state: &WorldState, goal: &Goal, actions: &[Action]) -> Option<Vec<Action>>;
}
}

jugar-probar

#![allow(unused)]
fn main() {
// Assertions
pub struct Assertion {
    pub passed: bool,
    pub message: String,
}

impl Assertion {
    pub fn equals<T: PartialEq>(a: &T, b: &T) -> Assertion;
    pub fn in_range(value: f32, min: f32, max: f32) -> Assertion;
    pub fn is_true(condition: bool) -> Assertion;
    // ... more
}

// Simulation
pub fn run_simulation(config: SimulationConfig, input_fn: impl Fn(u32) -> Vec<InputEvent>) -> SimulationResult;
pub fn run_replay(recording: &Recording) -> ReplayResult;
}

Prelude

The prelude re-exports common types:

#![allow(unused)]
fn main() {
pub use crate::{
    JugarEngine, JugarConfig, GameContext, LoopControl,
    World, Entity, Component,
    Position, Velocity, Transform,
    KeyCode, MouseButton, InputState,
    Vec2, Vec3, Mat4, Color,
};
}

Import with:

#![allow(unused)]
fn main() {
use jugar::prelude::*;
}

Quality Standards

Jugar follows strict quality standards based on PMAT methodology and Toyota Production System principles.

Quality Metrics

MetricMinimumTargetStatus
Test Coverage85%95%
Mutation Score80%90%
TDG GradeB+A+
SATD Comments50
Unsafe Code00
JavaScript0 bytes0 bytes

Running Quality Checks

Tiered Workflow

# Tier 1: ON-SAVE (sub-second)
make tier1

# Tier 2: ON-COMMIT (1-5 minutes)
make tier2

# Tier 3: ON-MERGE (hours)
make tier3

Individual Checks

# Formatting
cargo fmt -- --check

# Linting
cargo clippy --all-targets --all-features -- -D warnings

# Tests
cargo test --all-features

# Coverage
make coverage

# Mutation testing
make mutate

PMAT Tools

# Technical Debt Grading
make pmat-tdg

# Repository score
make pmat-score

# Rust project score
make pmat-rust-score

# All PMAT checks
make pmat-all

Test Coverage

Target: 95%

# Generate coverage report
make coverage

# Open HTML report
make coverage-open

# CI mode (LCOV)
make coverage-ci

Excluded from Coverage

  • Binary files (bin/*.rs)
  • External crate internals (wasmtime.*)
  • Proc-macro crates (probar-derive)
  • Browser-only code (browser.rs)

Mutation Testing

Target: 80% Kill Rate

# Full mutation testing
make mutate

# Quick (single module)
make mutate-quick

# Specific file
make mutate-file FILE=crates/jugar-web/src/juice.rs

# View report
make mutate-report

Technical Debt Grading

Target: B+ Minimum

The TDG system grades code quality:

GradeDescription
A+Excellent - minimal debt
AVery good
A-Good
B+Acceptable
BNeeds improvement
CSignificant debt
DCritical debt
FFailing

Continuous Improvement (Kaizen)

# Run kaizen analysis
make kaizen

# Outputs:
# - Code metrics (LOC, complexity)
# - Coverage analysis
# - Technical debt grade
# - Improvement recommendations

Pre-Commit Checks

Before committing, always run:

make tier2

This verifies:

  • Zero JavaScript (policy)
  • Batuta dependencies (policy)
  • Formatting
  • Linting
  • All tests
  • Property tests
  • Coverage
  • TDG grade
  • SATD comments

CI/CD Pipeline

All PRs must pass:

- cargo fmt -- --check
- cargo clippy --all-targets -- -D warnings
- cargo test --all-features
- make verify-no-js
- make pmat-tdg

Quality Gate Failures

If quality gates fail:

  1. Coverage below 95%: Add missing tests
  2. Mutation score below 80%: Strengthen assertions
  3. TDG below B+: Address technical debt
  4. JavaScript detected: Remove and use Rust/WASM
  5. Clippy warnings: Fix all warnings

Tools Required

# Install all quality tools
make install-tools

# Includes:
# - cargo-llvm-cov (coverage)
# - cargo-mutants (mutation testing)
# - cargo-nextest (fast test runner)
# - cargo-audit (security)
# - pmat (quality metrics)

Makefile Targets

Complete reference for all Makefile targets.

Quick Reference

TargetDescription
make tier1Sub-second feedback (ON-SAVE)
make tier2Full validation (ON-COMMIT)
make tier3Mutation testing (ON-MERGE)
make build-webBuild WASM for web
make test-e2eRun Probar E2E tests
make coverageGenerate coverage report

Tiered Workflow

Tier 1: ON-SAVE (Sub-second)

make tier1

Runs:

  1. Type checking (cargo check)
  2. Fast clippy (library only)
  3. Unit tests
  4. Property tests (10 cases)

Tier 2: ON-COMMIT (1-5 minutes)

make tier2

Runs:

  1. Zero JavaScript verification
  2. Batuta dependency check
  3. Format check
  4. Full clippy
  5. All tests
  6. Property tests (256 cases)
  7. Coverage analysis
  8. TDG grade check

Tier 3: ON-MERGE (Hours)

make tier3

Runs:

  1. All Tier 2 checks
  2. Mutation testing (80% target)
  3. Security audit
  4. Benchmarks
  5. PMAT repo score

Build Targets

make build           # Host target (dev)
make build-release   # Host target (optimized)
make build-wasm      # WASM target (release)
make build-wasm-dev  # WASM target (debug)
make build-web       # Build with wasm-pack
make serve-web       # Serve locally (port 8080)

Test Targets

make test            # All tests
make test-fast       # Library tests only (<2 min)
make test-e2e        # Probar E2E tests
make test-e2e-verbose # E2E with output
make test-property   # Property tests (50 cases)
make test-property-full # Property tests (500 cases)

Quality Targets

make lint            # Full clippy
make fmt             # Format code
make fmt-check       # Check formatting

Coverage Targets

make coverage        # Generate HTML report
make coverage-summary # Show summary
make coverage-open   # Open in browser
make coverage-check  # Enforce 95% threshold
make coverage-ci     # Generate LCOV for CI
make coverage-clean  # Clean artifacts

PMAT Targets

make pmat-tdg          # Technical Debt Grading
make pmat-analyze      # Complexity, SATD, dead code
make pmat-score        # Repository health score
make pmat-rust-score   # Rust project score
make pmat-validate-docs # Documentation validation
make pmat-all          # All PMAT checks

Mutation Testing

make mutate          # jugar-web crate (<5 min)
make mutate-quick    # Single module (<2 min)
make mutate-file FILE=path/to/file.rs
make mutate-report   # View results

Load Testing

make load-test       # All load tests
make load-test-quick # Quick chaos tests
make load-test-full  # Full detailed tests
make ai-test         # AI CLI tests
make ai-simulate     # Run AI simulation

Verification

make verify-no-js    # Zero JavaScript check
make verify-batuta-deps # Batuta dependencies
make verify-wasm-output # Pure WASM output

Kaizen

make kaizen          # Continuous improvement analysis

Physics Sandbox

make sandbox         # Build and test
make test-sandbox    # Run tests
make test-sandbox-coverage # With coverage
make sandbox-lint    # Pedantic clippy
make sandbox-mutate  # Mutation testing
make build-sandbox-wasm # WASM build

Development

make dev             # Watch mode
make install-tools   # Install required tools
make clean           # Clean build artifacts
make clean-pmat      # Clean PMAT artifacts

Book

make book            # Build mdbook
make book-serve      # Serve locally
make book-open       # Open in browser

Help

make help            # Show all targets with descriptions

Contributing

We welcome contributions to Jugar!

Getting Started

Clone the Repository

git clone https://github.com/paiml/jugar.git
cd jugar

Install Tools

make install-tools

Run Tests

make tier2

Development Workflow

1. Create a Branch

git checkout -b feature/my-feature

2. Make Changes

Follow the coding standards below.

3. Run Checks

# Quick check during development
make tier1

# Full check before committing
make tier2

4. Commit

git add .
git commit -m "feat: add my feature"

5. Push and Create PR

git push origin feature/my-feature
# Create PR on GitHub

Coding Standards

Zero JavaScript

CRITICAL: No JavaScript in game logic.

  • ❌ No .js or .ts files
  • ❌ No npm or package.json
  • ❌ No JavaScript bundlers
  • ✅ Pure Rust only
  • ✅ WASM output only

Batuta-First

Use Batuta stack components before external crates:

  • trueno for SIMD/GPU compute
  • aprender for ML/AI
  • trueno-viz for rendering

Formatting

cargo fmt

Linting

cargo clippy --all-targets -- -D warnings

No warnings allowed.

Tests

  • Write tests for all new code
  • Maintain 95%+ coverage
  • Include property tests for complex logic
make coverage

Documentation

  • Add doc comments to public APIs
  • Update book if adding features
  • Include examples for new functionality

Commit Messages

Follow conventional commits:

type(scope): description

[optional body]

[optional footer]

Types:

  • feat: New feature
  • fix: Bug fix
  • docs: Documentation
  • refactor: Code refactoring
  • test: Adding tests
  • chore: Maintenance

Examples:

feat(physics): add continuous collision detection
fix(ai): correct pathfinding edge case
docs(book): add probar chapter

Pull Request Guidelines

Requirements

All PRs must:

  • Pass CI (tier2 equivalent)
  • Have tests for new code
  • Update documentation if needed
  • Have descriptive commit messages

Review Process

  1. Automated checks run
  2. Maintainer reviews code
  3. Feedback addressed
  4. Approval and merge

PR Template

## Summary

Brief description of changes.

## Changes

- Change 1
- Change 2

## Testing

How was this tested?

## Checklist

- [ ] Tests added
- [ ] Documentation updated
- [ ] `make tier2` passes

Architecture Decisions

For significant changes:

  1. Open an issue first
  2. Discuss the approach
  3. Get approval before implementing

Code of Conduct

Be respectful and constructive. We're all here to build something great together.

Getting Help

  • Open an issue for questions
  • Check existing issues first
  • Join discussions on GitHub

License

By contributing, you agree that your contributions will be licensed under the MIT License.