// ============================================================
// Blackboard patches and transactions
//
// Behavior trees frequently evaluate speculative actions before deciding
// whether to keep their side effects. A patch gives save-game systems,
// network replication, and audit logs a small deterministic description of
// the changes between two blackboards. A transaction uses the same diff
// machinery while keeping uncommitted writes isolated from the live state.
// ============================================================
///|
/// A single change needed to transform one blackboard into another.
pub(all) enum BlackboardChange {
Added(String, Value)
Updated(String, Value, Value)
Removed(String, Value)
}
///|
/// Return the key affected by a change.
pub fn BlackboardChange::key(self : BlackboardChange) -> String {
match self {
BlackboardChange::Added(key, _) => key
BlackboardChange::Updated(key, _, _) => key
BlackboardChange::Removed(key, _) => key
}
}
///|
/// Return the previous value, when the key existed before the change.
pub fn BlackboardChange::before(self : BlackboardChange) -> Value? {
match self {
BlackboardChange::Added(_, _) => None
BlackboardChange::Updated(_, value, _) => Some(value)
BlackboardChange::Removed(_, value) => Some(value)
}
}
///|
/// Return the value after applying the change, when the key remains present.
pub fn BlackboardChange::after(self : BlackboardChange) -> Value? {
match self {
BlackboardChange::Added(_, value) => Some(value)
BlackboardChange::Updated(_, _, value) => Some(value)
BlackboardChange::Removed(_, _) => None
}
}
///|
/// Return a stable change category for logs and replication metrics.
pub fn BlackboardChange::kind(self : BlackboardChange) -> String {
match self {
BlackboardChange::Added(_, _) => "added"
BlackboardChange::Updated(_, _, _) => "updated"
BlackboardChange::Removed(_, _) => "removed"
}
}
///|
/// Format one change as a compact audit line.
pub fn BlackboardChange::to_text(self : BlackboardChange) -> String {
match self {
BlackboardChange::Added(key, value) => "added \{key}=\{value.to_text()}"
BlackboardChange::Updated(key, before, after) =>
"updated \{key}: \{before.to_text()} -> \{after.to_text()}"
BlackboardChange::Removed(key, value) => "removed \{key}=\{value.to_text()}"
}
}
///|
/// A deterministic collection of blackboard changes.
pub struct BlackboardPatch {
changes : Array[BlackboardChange]
}
///|
/// Create a patch from an array of changes.
pub fn BlackboardPatch::new(
changes : Array[BlackboardChange],
) -> BlackboardPatch {
{ changes, }
}
///|
/// Return the number of changes in the patch.
pub fn BlackboardPatch::size(self : BlackboardPatch) -> Int {
self.changes.length()
}
///|
/// Return whether applying this patch would leave the target unchanged.
pub fn BlackboardPatch::is_empty(self : BlackboardPatch) -> Bool {
self.changes.length() == 0
}
///|
/// Return a copy of the ordered changes for inspection or persistence.
pub fn BlackboardPatch::changes(
self : BlackboardPatch,
) -> Array[BlackboardChange] {
let result : Array[BlackboardChange] = []
for change in self.changes {
result.push(change)
}
result
}
///|
/// Format the patch as one change per line.
pub fn BlackboardPatch::to_text(self : BlackboardPatch) -> String {
let output = StringBuilder::new()
for change in self.changes {
output.write_string(change.to_text())
output.write_string("\n")
}
output.to_string()
}
///|
/// Compare two supported values without relying on representation details.
pub fn Value::same(self : Value, other : Value) -> Bool {
match self {
Value::Bool(left) =>
match other {
Value::Bool(right) => left == right
_ => false
}
Value::Int(left) =>
match other {
Value::Int(right) => left == right
_ => false
}
Value::Double(left) =>
match other {
Value::Double(right) => left == right
_ => false
}
Value::Str(left) =>
match other {
Value::Str(right) => left == right
_ => false
}
}
}
///|
/// Compute the ordered changes that transform `self` into `target`.
/// Existing keys keep the source insertion order; newly added keys follow the
/// target insertion order. This makes patches stable in logs and fixtures.
pub fn Blackboard::diff(
self : Blackboard,
target : Blackboard,
) -> BlackboardPatch {
let changes : Array[BlackboardChange] = []
for key in self.keys() {
let before = self.get_value(key).unwrap()
match target.get_value(key) {
None => changes.push(BlackboardChange::Removed(key, before))
Some(after) =>
if !before.same(after) {
changes.push(BlackboardChange::Updated(key, before, after))
}
}
}
for key in target.keys() {
if !self.has(key) {
changes.push(BlackboardChange::Added(key, target.get_value(key).unwrap()))
}
}
BlackboardPatch::new(changes)
}
///|
/// Apply a patch and return the number of changes accepted.
pub fn BlackboardPatch::apply(self : BlackboardPatch, bb : Blackboard) -> Int {
let mut applied = 0
for change in self.changes {
match change {
BlackboardChange::Added(key, value) => {
bb.set_value(key, value)
applied = applied + 1
}
BlackboardChange::Updated(key, _, value) => {
bb.set_value(key, value)
applied = applied + 1
}
BlackboardChange::Removed(key, _) =>
if bb.has(key) {
bb.remove(key)
applied = applied + 1
}
}
}
applied
}
///|
/// A live blackboard transaction with isolated writes.
pub struct BlackboardTransaction {
target : Blackboard
original : Blackboard
working : Blackboard
closed : Ref[Bool]
}
///|
/// Begin a transaction. Changes are invisible to the target until commit.
pub fn Blackboard::begin_transaction(
self : Blackboard,
) -> BlackboardTransaction {
{
target: self,
original: self.clone(),
working: self.clone(),
closed: Ref::new(false),
}
}
///|
/// Return the isolated working blackboard for a transaction.
pub fn BlackboardTransaction::blackboard(
self : BlackboardTransaction,
) -> Blackboard {
self.working
}
///|
/// Return the pending changes since the transaction began.
pub fn BlackboardTransaction::changes(
self : BlackboardTransaction,
) -> BlackboardPatch {
self.original.diff(self.working)
}
///|
/// Return whether the transaction can still be committed.
pub fn BlackboardTransaction::is_open(self : BlackboardTransaction) -> Bool {
!self.closed.get()
}
///|
/// Commit all working values atomically and return whether the commit happened.
pub fn BlackboardTransaction::commit(self : BlackboardTransaction) -> Bool {
if self.closed.get() {
return false
}
self.target.clear()
self.target.merge(self.working)
self.closed.set(true)
true
}
///|
/// Discard working values. Rollback is idempotent and never changes the target.
pub fn BlackboardTransaction::rollback(self : BlackboardTransaction) -> Unit {
self.closed.set(true)
}
///|
/// Run an isolated update and commit only when the callback accepts it.
pub fn Blackboard::transactional(
self : Blackboard,
update : (Blackboard) -> Bool,
) -> Bool {
let transaction = self.begin_transaction()
let accepted = update(transaction.blackboard())
if accepted {
transaction.commit()
} else {
transaction.rollback()
false
}
}
///|
/// Apply a patch only when a caller-provided validation callback accepts the
/// resulting state. This is useful for atomic action planning and save games.
pub fn Blackboard::apply_patch_if(
self : Blackboard,
patch : BlackboardPatch,
validate : (Blackboard) -> Bool,
) -> Bool {
self.transactional(fn(working) {
let _ = patch.apply(working)
validate(working)
})
}