Build your first scene
Build a working east-wall scene end-to-end. You'll seed two arguments, set a scene intensity, run MultiBeat, and print the resulting beats.
Learning objective: build a working two-actor argumentation scene with weighted attacks and a tunable β, run it end-to-end with cargo run, and read the resulting beat-by-beat acceptance trace — in under 10 minutes, with no prior argumentation-theory knowledge.
What you'll build
A small Rust program that runs a 4-beat scene between Alice and Bob over whether to fortify the east wall. Alice asserts an argument from expert opinion. Bob asserts a counter. At β=0.5, the scorer boosts Alice's claim, Bob rejects with his credulous counter, and the beats print deterministically.
Time: ~10 minutes.
Difficulty: Beginner.
You'll leave with: a running cargo run example and a mental model for "how to get arguments into a scene."
Prerequisites
- Rust 1.85+ (
rustc --version) - A terminal
- No prior argumentation-theory knowledge — we'll explain as we go
Step 1: Create the project
cargo new --bin my-first-scene
cd my-first-scene
Add to Cargo.toml:
[dependencies]
argumentation-schemes = "0.2"
argumentation-weighted = "0.2"
encounter-argumentation = "0.5"
encounter = { package = "encounter-rs", version = "0.1" }
Step 2: Get the registry and pick the scheme
Replace src/main.rs with:
use argumentation_schemes::catalog::default_catalog;
use argumentation_weighted::types::Budget;
use encounter_argumentation::EncounterArgumentationState;
use std::collections::HashMap;
fn main() {
let registry = default_catalog();
let scheme = registry.by_key("argument_from_expert_opinion").unwrap();
println!("Registry loaded; scheme picked.");
}
Run cargo run to confirm everything compiles.
Why we instantiate before creating the state:
schemeis a reference intoregistry. The state takes ownership ofregistry, which would invalidate any outstanding scheme reference. So we instantiate Alice's and Bob's arguments first (steps 3–4), then hand the registry over to the state (step 5).
Step 3: Build Alice's argument
Alice is a military expert arguing to fortify the east wall. Inside main(), after the scheme line:
let mut alice_bindings = HashMap::new();
alice_bindings.insert("expert".into(), "alice".into());
alice_bindings.insert("domain".into(), "military".into());
alice_bindings.insert("claim".into(), "fortify_east".into());
alice_bindings.insert("self".into(), "alice".into());
let alice_instance = scheme.instantiate(&alice_bindings).unwrap();
Step 4: Build Bob's counter-argument
Bob is a logistics expert:
let mut bob_bindings = HashMap::new();
bob_bindings.insert("expert".into(), "bob".into());
bob_bindings.insert("domain".into(), "logistics".into());
bob_bindings.insert("claim".into(), "abandon_east".into());
bob_bindings.insert("self".into(), "bob".into());
let bob_instance = scheme.instantiate(&bob_bindings).unwrap();
Step 5: Create the state and seed both arguments
Now hand the registry over to the state, register both instances, and wire Bob's attack on Alice:
let state = EncounterArgumentationState::new(registry);
state.set_intensity(Budget::new(0.5).unwrap()); // scene tension
let alice_id = state.add_scheme_instance_for_affordance(
"alice",
"argue_fortify_east",
&alice_bindings,
alice_instance,
);
let bob_id = state.add_scheme_instance_for_affordance(
"bob",
"argue_abandon_east",
&bob_bindings,
bob_instance,
);
// Bob's counter attacks Alice with weight 0.4
state.add_weighted_attack(&bob_id, &alice_id, 0.4).unwrap();
Step 6: Build the encounter catalog and practice
use encounter::affordance::{AffordanceSpec, CatalogEntry};
use encounter::practice::{DurationPolicy, PracticeSpec, TurnPolicy};
let make_spec = |name: &str| AffordanceSpec {
name: name.into(),
domain: "persuasion".into(),
bindings: vec!["self".into(), "expert".into(), "domain".into(), "claim".into()],
considerations: Vec::new(),
effects_on_accept: Vec::new(),
effects_on_reject: Vec::new(),
drive_alignment: Vec::new(),
};
let catalog = vec![
CatalogEntry { spec: make_spec("argue_fortify_east"), precondition: String::new() },
CatalogEntry { spec: make_spec("argue_abandon_east"), precondition: String::new() },
];
let practice = PracticeSpec {
name: "east-wall-debate".into(),
affordances: vec!["argue_fortify_east".into(), "argue_abandon_east".into()],
turn_policy: TurnPolicy::RoundRobin,
duration_policy: DurationPolicy::MultiBeat { max_beats: 4 },
entry_condition_source: String::new(),
};
Step 7: Write an inner scorer
The bridge boosts arguments, but you still need a base scorer. A uniform 1.0 scorer will do:
use encounter::scoring::{ActionScorer, ScoredAffordance};
struct UniformScorer;
impl<P: Clone> ActionScorer<P> for UniformScorer {
fn score_actions(
&self,
actor: &str,
available: &[CatalogEntry<P>],
_participants: &[String],
) -> Vec<ScoredAffordance<P>> {
available.iter().map(|e| {
let mut b = HashMap::new();
b.insert("self".into(), actor.to_string());
let claim = if e.spec.name == "argue_fortify_east" { "fortify_east" } else { "abandon_east" };
b.insert("claim".into(), claim.into());
b.insert("expert".into(), actor.to_string());
b.insert("domain".into(), "military".into());
ScoredAffordance { entry: e.clone(), score: 1.0, bindings: b }
}).collect()
}
}
Step 8: Resolve the scene
use encounter::resolution::MultiBeat;
use encounter_argumentation::{StateAcceptanceEval, StateActionScorer};
let scorer = StateActionScorer::new(&state, UniformScorer, 0.5);
let acceptance = StateAcceptanceEval::new(&state);
let participants = vec!["alice".into(), "bob".into()];
let result = MultiBeat.resolve(&participants, &practice, &catalog, &scorer, &acceptance);
for beat in &result.beats {
println!(
"{} proposed {} — {}",
beat.actor,
beat.action,
if beat.accepted { "accepted" } else { "rejected" },
);
}
for err in state.drain_errors() {
eprintln!("latched: {}", err);
}
Expected output:
alice proposed argue_fortify_east — rejected
bob proposed argue_abandon_east — accepted
alice proposed argue_fortify_east — rejected
bob proposed argue_abandon_east — accepted
Alice picks her boosted argument every beat, but Bob's credulous counter rejects her. Bob's unattacked argument is accepted.
Complete example
Your full src/main.rs should look like this working source — the integration test that ships with the library exercises the same scene.
What you learned
You can now:
- Seed argument instances per actor via
add_scheme_instance_for_affordance. - Add weighted attacks between argument ids.
- Set a scene intensity via
set_intensity. - Run
MultiBeatwith a bridge-backed scorer and acceptance eval. - Drain errors latched by bridge impls that can't propagate
Result.
Next steps
- Tune β — pick the right intensity per scene register.
- Author catalogs declaratively — move affordance definitions to TOML.
- Understand the encounter bridge — what those two trait impls actually do.