True simultaneous multiplayer for the generative world: the transition consumes BOTH agents' actions per step (A=4 planes: dxA,dyA,dxB,dyB), so two participants drive two distinct entities in ONE world and co-render the same frame — not serialized. Paired with wai.world.model's signed action trace (each actor → a canonical slot), the merged log renders the IDENTICAL world on every peer (convergence), with both agents moving at once. - int_world.rs: generalized to step_planes() / decode_planes() — the transition takes an explicit set of action planes (A = plane count), so A=3 (one agent) and A=4 (two) share one path; step_delta/decode (A=3) now wrap them. Single-agent decode unchanged. - tools/wai_int_world2_export.py: trains a two-agent world (two markers, each agent independently controlled), PTQs T+g_s to WHM1, integer reference decode → hashes (float 27.0 / integer 21.4 dB). Reuses the 1-agent tool's WHM1/PTQ/decode helpers. - tests/int_world2.rs (never-skip) + committed vector tests/vectors/int_world2/: decode_planes regenerates the committed co-control clip byte-for-byte (clip 66b5821899323ff5). Rust == Python. - wai-web: WorldModel::stepPlanes (advance by explicit planes) + per-op slot (actor→canonical lane) in replayWorldOps, so each tab drives its own agent. Live (/world-shared, two BroadcastChannel tabs): each tab a distinct actor → a distinct agent; both replay the merged log and converge on the same session hash + byte-exact two-agent world. Playwright-verified: A drove 3 + B drove 2 → identical session 6748220a…, frame 4751a9d4…, 0 errors. Co-Authored-By: Claude Opus 4.8 <[email protected]>
54 lines
2.6 KiB
Rust
54 lines
2.6 KiB
Rust
//! `wai.world.model` TWO-AGENT co-control — both agents' actions drive one frame.
|
|
//! The committed transition + synthesis (A=4 input planes: dxA,dyA,dxB,dyB), an
|
|
//! initial latent, and a sequence of combined (agent-A, agent-B) actions regenerate a
|
|
//! frame sequence whose per-frame + clip hashes reproduce on every machine. Two
|
|
//! participants co-render one byte-exact world — true simultaneous multiplayer, not
|
|
//! serialized. Committed tiny vector under `tests/vectors/int_world2/`; never skips.
|
|
#![cfg(feature = "world_int")]
|
|
|
|
use std::path::PathBuf;
|
|
|
|
use wai::int_hyper_synth::IntModel;
|
|
use wai::int_world::{decode_planes, ACTIONS};
|
|
|
|
fn dir() -> PathBuf {
|
|
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/vectors/int_world2")
|
|
}
|
|
|
|
#[test]
|
|
fn two_agent_world_regenerates_byte_for_byte() {
|
|
let t = IntModel::from_bytes(&std::fs::read(dir().join("world2_T.bin")).unwrap()).expect("transition WHM1");
|
|
let gs = IntModel::from_bytes(&std::fs::read(dir().join("world2_gs.bin")).unwrap()).expect("synthesis WHM1");
|
|
let j: serde_json::Value = serde_json::from_slice(&std::fs::read(dir().join("world2.json")).unwrap()).unwrap();
|
|
|
|
let c = j["c"].as_u64().unwrap() as usize;
|
|
let lat = j["lat"].as_u64().unwrap() as usize;
|
|
let one = 1i64 << j["afrac"].as_u64().unwrap();
|
|
let z0: Vec<i64> = j["z0"].as_array().unwrap().iter().map(|v| v.as_i64().unwrap()).collect();
|
|
|
|
// Each step's action PLANE row combines both agents: [dxA, dyA, dxB, dyB] at
|
|
// 2^afrac — the A=4 input the two-agent transition consumes.
|
|
let rows: Vec<Vec<i64>> = j["pairs"]
|
|
.as_array()
|
|
.unwrap()
|
|
.iter()
|
|
.map(|p| {
|
|
let aa = p[0].as_u64().unwrap() as usize;
|
|
let ab = p[1].as_u64().unwrap() as usize;
|
|
let (axa, aya) = ACTIONS[aa];
|
|
let (axb, ayb) = ACTIONS[ab];
|
|
vec![axa * one, aya * one, axb * one, ayb * one]
|
|
})
|
|
.collect();
|
|
|
|
let clip = decode_planes(&t, &gs, &z0, &rows, c, lat);
|
|
let want = j["frame_hashes"].as_array().unwrap();
|
|
assert_eq!(clip.frames.len(), rows.len() + 1, "initial + one frame per combined action");
|
|
for i in 0..clip.frames.len() {
|
|
let got = format!("{:016x}", clip.frame_hash(i));
|
|
assert_eq!(got, want[i].as_str().unwrap(), "two-agent frame {i} must reproduce its committed hash");
|
|
}
|
|
let clip_hash = format!("{:016x}", clip.clip_hash());
|
|
assert_eq!(clip_hash, j["clip_hash"].as_str().unwrap(), "clip hash");
|
|
println!("int_world2: {} frames {}px, two agents co-rendered byte-exact, clip {clip_hash}", clip.frames.len(), clip.frame_px);
|
|
}
|