feat(parser): add action parameters, repeater decorator, and named participants
Add syntax enhancements for more expressive behavior trees and relationships.
Action parameters:
- Support typed/positional parameters: WaitDuration(2.0)
- Support named parameters: SetValue(field: value)
- Enable inline values without field names
Repeater decorator:
- Add * { node } syntax for repeating behavior nodes
- Maps to Decorator("repeat", node)
Named participant blocks:
- Replace self/other blocks with named participant syntax
- Support multi-party relationships
- Example: Alice { role: seeker } instead of self { role: seeker }
Schedule block syntax:
- Require braces for schedule blocks to support narrative fields
- Update tests to use new syntax: 04:00 -> 06:00: Activity { }
This commit is contained in:
@@ -544,3 +544,485 @@ character Martha {
|
||||
// Parser catches this as UnrecognizedToken before validation
|
||||
assert!(stderr.contains("Parse error") || stderr.contains("UnrecognizedToken"));
|
||||
}
|
||||
|
||||
// ===== Prose Block Tests =====
|
||||
|
||||
#[test]
|
||||
fn test_prose_block_in_institution() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
|
||||
fs::write(
|
||||
dir.path().join("institution.sb"),
|
||||
r#"
|
||||
institution Bakery {
|
||||
type: "business"
|
||||
|
||||
---description
|
||||
A cozy neighborhood bakery that has been serving
|
||||
the community for over 30 years. Known for its
|
||||
fresh bread and friendly atmosphere.
|
||||
---
|
||||
|
||||
established: 1990
|
||||
}
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let output = Command::new(sb_binary())
|
||||
.arg("validate")
|
||||
.arg(dir.path())
|
||||
.output()
|
||||
.expect("Failed to execute sb validate");
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"Institution with prose block should validate successfully. Stderr: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(stdout.contains("✓ Validation successful"));
|
||||
assert!(stdout.contains("Institutions: 1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_prose_blocks_in_same_declaration() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
|
||||
fs::write(
|
||||
dir.path().join("multi_prose.sb"),
|
||||
r#"
|
||||
character Martha {
|
||||
age: 34
|
||||
|
||||
---backstory
|
||||
Martha grew up in a small town where she learned
|
||||
the art of baking from her grandmother. Her passion
|
||||
for creating delicious pastries has been lifelong.
|
||||
---
|
||||
|
||||
trust: 0.8
|
||||
|
||||
---personality
|
||||
Martha is kind-hearted and patient, always willing
|
||||
to help her neighbors. She has a warm smile that
|
||||
makes everyone feel welcome in her bakery.
|
||||
---
|
||||
|
||||
---goals
|
||||
She dreams of expanding her bakery and teaching
|
||||
baking classes to the next generation of bakers
|
||||
in the community.
|
||||
---
|
||||
}
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let output = Command::new(sb_binary())
|
||||
.arg("validate")
|
||||
.arg(dir.path())
|
||||
.output()
|
||||
.expect("Failed to execute sb validate");
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"Character with multiple prose blocks should validate successfully. Stderr: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(stdout.contains("✓ Validation successful"));
|
||||
assert!(stdout.contains("Characters: 1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prose_blocks_in_behavior_tree() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
|
||||
fs::write(
|
||||
dir.path().join("behavior.sb"),
|
||||
r#"
|
||||
behavior WakeUpRoutine {
|
||||
---description
|
||||
A simple morning routine that characters follow
|
||||
when they wake up each day.
|
||||
---
|
||||
|
||||
> {
|
||||
WakeUp
|
||||
GetDressed
|
||||
EatBreakfast
|
||||
}
|
||||
}
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let output = Command::new(sb_binary())
|
||||
.arg("validate")
|
||||
.arg(dir.path())
|
||||
.output()
|
||||
.expect("Failed to execute sb validate");
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"Behavior tree with prose block should validate successfully. Stderr: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(stdout.contains("✓ Validation successful"));
|
||||
assert!(stdout.contains("Behaviors: 1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prose_blocks_in_life_arc() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
|
||||
fs::write(
|
||||
dir.path().join("life_arc.sb"),
|
||||
r#"
|
||||
life_arc BakerCareer {
|
||||
---description
|
||||
The journey of becoming a master baker, from
|
||||
apprentice to owning one's own bakery.
|
||||
---
|
||||
|
||||
state Apprentice {
|
||||
---details
|
||||
Learning the basics of bread-making and pastry
|
||||
under the guidance of an experienced baker.
|
||||
---
|
||||
|
||||
on experience > 5 -> Journeyman
|
||||
}
|
||||
|
||||
state Journeyman {
|
||||
---details
|
||||
Working independently and developing signature
|
||||
recipes while saving to open a bakery.
|
||||
---
|
||||
|
||||
on savings > 50000 -> MasterBaker
|
||||
}
|
||||
|
||||
state MasterBaker {
|
||||
---details
|
||||
Running a successful bakery and mentoring the
|
||||
next generation of bakers.
|
||||
---
|
||||
}
|
||||
}
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let output = Command::new(sb_binary())
|
||||
.arg("validate")
|
||||
.arg(dir.path())
|
||||
.output()
|
||||
.expect("Failed to execute sb validate");
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"Life arc with prose blocks should validate successfully. Stderr: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(stdout.contains("✓ Validation successful"));
|
||||
assert!(stdout.contains("Life Arcs: 1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prose_blocks_in_schedule() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
|
||||
fs::write(
|
||||
dir.path().join("schedule.sb"),
|
||||
r#"
|
||||
schedule DailyRoutine {
|
||||
---description
|
||||
A typical weekday schedule for bakery workers
|
||||
who start their day before dawn.
|
||||
---
|
||||
|
||||
04:00 -> 06:00: PrepareDough { }
|
||||
06:00 -> 08:00: BakeBread { }
|
||||
08:00 -> 12:00: ServCustomers { }
|
||||
12:00 -> 13:00: LunchBreak { }
|
||||
13:00 -> 17:00: ServCustomers { }
|
||||
17:00 -> 18:00: Cleanup { }
|
||||
}
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let output = Command::new(sb_binary())
|
||||
.arg("validate")
|
||||
.arg(dir.path())
|
||||
.output()
|
||||
.expect("Failed to execute sb validate");
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"Schedule with prose block should validate successfully. Stderr: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(stdout.contains("✓ Validation successful"));
|
||||
assert!(stdout.contains("Schedules: 1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prose_blocks_with_special_characters() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
|
||||
fs::write(
|
||||
dir.path().join("special.sb"),
|
||||
r#"
|
||||
character Alice {
|
||||
age: 7
|
||||
|
||||
---backstory
|
||||
Alice was a curious young girl who often wondered about
|
||||
the world around her. One day, she saw a White Rabbit
|
||||
muttering "Oh dear! Oh dear! I shall be too late!" and
|
||||
followed it down a rabbit-hole.
|
||||
|
||||
She fell down, down, down--would the fall never come to
|
||||
an end? "I wonder how many miles I've fallen by this time?"
|
||||
she said aloud.
|
||||
---
|
||||
|
||||
curiosity: 0.95
|
||||
}
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let output = Command::new(sb_binary())
|
||||
.arg("validate")
|
||||
.arg(dir.path())
|
||||
.output()
|
||||
.expect("Failed to execute sb validate");
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"Prose block with quotes, dashes, and punctuation should validate. Stderr: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(stdout.contains("✓ Validation successful"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prose_blocks_in_relationship() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
|
||||
fs::write(
|
||||
dir.path().join("relationship.sb"),
|
||||
r#"
|
||||
character Martha {
|
||||
age: 34
|
||||
}
|
||||
|
||||
character David {
|
||||
age: 42
|
||||
}
|
||||
|
||||
relationship Spousal {
|
||||
Martha
|
||||
David
|
||||
|
||||
---bond_description
|
||||
Martha and David have been married for 8 years.
|
||||
They share a deep connection built on mutual
|
||||
respect, trust, and shared dreams for the future.
|
||||
---
|
||||
|
||||
bond: 0.9
|
||||
coordination: cohabiting
|
||||
}
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let output = Command::new(sb_binary())
|
||||
.arg("validate")
|
||||
.arg(dir.path())
|
||||
.output()
|
||||
.expect("Failed to execute sb validate");
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"Relationship with prose block should validate successfully. Stderr: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(stdout.contains("✓ Validation successful"));
|
||||
assert!(stdout.contains("Characters: 2"));
|
||||
assert!(stdout.contains("Relationships: 1"));
|
||||
}
|
||||
|
||||
// ===== Species Tests =====
|
||||
|
||||
#[test]
|
||||
fn test_species_with_identity_operator() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
|
||||
fs::write(
|
||||
dir.path().join("species.sb"),
|
||||
r#"
|
||||
species Human {
|
||||
lifespan: 70..90
|
||||
intelligence: "high"
|
||||
}
|
||||
|
||||
character Alice: Human {
|
||||
age: 7
|
||||
}
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let output = Command::new(sb_binary())
|
||||
.arg("validate")
|
||||
.arg(dir.path())
|
||||
.output()
|
||||
.expect("Failed to execute sb validate");
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"Character with species should validate successfully. Stderr: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(stdout.contains("✓ Validation successful"));
|
||||
assert!(stdout.contains("Characters: 1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_species_composition_with_includes() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
|
||||
fs::write(
|
||||
dir.path().join("hybrid_species.sb"),
|
||||
r#"
|
||||
species Horse {
|
||||
hoofed: true
|
||||
mane_style: "long"
|
||||
}
|
||||
|
||||
species Donkey {
|
||||
hoofed: true
|
||||
ear_size: "large"
|
||||
}
|
||||
|
||||
species Mule {
|
||||
include Horse
|
||||
include Donkey
|
||||
sterile: true
|
||||
}
|
||||
|
||||
character MyMule: Mule {
|
||||
age: 3
|
||||
name: "Betsy"
|
||||
}
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let output = Command::new(sb_binary())
|
||||
.arg("validate")
|
||||
.arg(dir.path())
|
||||
.output()
|
||||
.expect("Failed to execute sb validate");
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"Hybrid species with composition should validate successfully. Stderr: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(stdout.contains("✓ Validation successful"));
|
||||
assert!(stdout.contains("Characters: 1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_character_without_species() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
|
||||
fs::write(
|
||||
dir.path().join("no_species.sb"),
|
||||
r#"
|
||||
character TheNarrator {
|
||||
abstract_entity: true
|
||||
}
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let output = Command::new(sb_binary())
|
||||
.arg("validate")
|
||||
.arg(dir.path())
|
||||
.output()
|
||||
.expect("Failed to execute sb validate");
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"Character without species should validate (species is optional). Stderr: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(stdout.contains("✓ Validation successful"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_species_with_templates() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
|
||||
fs::write(
|
||||
dir.path().join("species_and_templates.sb"),
|
||||
r#"
|
||||
species Human {
|
||||
lifespan: 70..90
|
||||
}
|
||||
|
||||
template Hero {
|
||||
courage: 0.9
|
||||
strength: 0.8
|
||||
}
|
||||
|
||||
character Alice: Human from Hero {
|
||||
age: 7
|
||||
name: "Alice"
|
||||
}
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let output = Command::new(sb_binary())
|
||||
.arg("validate")
|
||||
.arg(dir.path())
|
||||
.output()
|
||||
.expect("Failed to execute sb validate");
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"Character with both species and templates should validate. Stderr: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(stdout.contains("✓ Validation successful"));
|
||||
assert!(stdout.contains("Characters: 1"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user