From 5f09622d342df7833ccf35124b38b4781b564d0e Mon Sep 17 00:00:00 2001 From: Matt Mower Date: Sat, 2 Mar 2024 19:09:03 +0000 Subject: [PATCH] v1.2.5 Added: ^{} code block syntax. Probably too many ways of doing code now. Fixes: A bunch of issues with behaviour Added: Pass behaviour owner into the behaviour tree Changes: Some RezBehaviour methods are now properties Added: More behaviour docs --- assets/templates/runtime/rez_0prototype.js | 2 +- assets/templates/runtime/rez_behaviour.js | 39 +++++----- assets/templates/stdlib.rez.eex | 73 +++++++++--------- docs/REZ.adoc | 86 ++++++++++++++++++++++ lib/AST/behaviour.ex | 22 ++++-- lib/AST/value_encoder.ex | 4 + lib/parser/value_parsers.ex | 14 ++++ mix.exs | 2 +- 8 files changed, 176 insertions(+), 66 deletions(-) diff --git a/assets/templates/runtime/rez_0prototype.js b/assets/templates/runtime/rez_0prototype.js index 622b19c..5c45598 100644 --- a/assets/templates/runtime/rez_0prototype.js +++ b/assets/templates/runtime/rez_0prototype.js @@ -260,7 +260,7 @@ const basic_object = { const options = tree_spec["options"]; const children = tree_spec["children"].map((spec) => this.instantiateBehaviourTree(spec)); - return behaviour_template.instantiate(options, children); + return behaviour_template.instantiate(this, options, children); }, createTraceryGrammarAttribute(attr_name, value) { diff --git a/assets/templates/runtime/rez_behaviour.js b/assets/templates/runtime/rez_behaviour.js index f780248..cc72652 100644 --- a/assets/templates/runtime/rez_behaviour.js +++ b/assets/templates/runtime/rez_behaviour.js @@ -16,6 +16,18 @@ RezBehaviour.prototype = { __proto__: basic_object, constructor: RezBehaviour, + get firstChild() { + return this.children[0]; + }, + + get secondChild() { + return this.children[1]; + }, + + get childCount() { + return this.children.length; + }, + configure() { const config_fn = this.getAttribute("configure"); if(typeof(config_fn) === "function") { @@ -47,26 +59,10 @@ RezBehaviour.prototype = { this.options[name] = value; }, - firstChild() { - return this.children[0]; - }, - - secondChild() { - return this.children[1]; - }, - - getChild(idx) { + getChildAt(idx) { return this.children[idx]; }, - children() { - return this.children; - }, - - childCount() { - return this.children.length; - }, - result(wmem, success) { return { id: this.id, @@ -78,9 +74,9 @@ RezBehaviour.prototype = { executeBehaviour(wmem) { // By definition this is a function of two attributes // (behaviour, wmem) - const handler = this.getAttribute("execute"); - if(typeof(handler) === "function") { - return handler(this, wmem); + const execute = this.getAttribute("execute"); + if(typeof(execute) === "function") { + return execute(this.owner, this, wmem); } else { return { id: this.id, @@ -91,8 +87,9 @@ RezBehaviour.prototype = { } }, - instantiate(options, children = []) { + instantiate(owner, options, children = []) { const behaviour = this.copyWithAutoId(); + behaviour.owner = owner; behaviour.options = options; behaviour.children = children; behaviour.configure(); diff --git a/assets/templates/stdlib.rez.eex b/assets/templates/stdlib.rez.eex index ce9c47d..665f1a8 100644 --- a/assets/templates/stdlib.rez.eex +++ b/assets/templates/stdlib.rez.eex @@ -974,7 +974,7 @@ options: [] min_children: 2 - execute: (behaviour, wmem) => { + execute: (owner, behaviour, wmem) => { let result = {success: true, wmem: wmem}; for(const child of behaviour.children) { result = child.executeBehaviour(result.wmem); @@ -1003,10 +1003,10 @@ } } - execute: (behaviour, wmem) => { + execute: (owner, behaviour, wmem) => { const p = behaviour.intOption("p"); const die = new RezDie(1, 100, 0); - let result = {success: false, id: behaviour.id, wmem}; + let result = {success: false, wmem}; for(const child of behaviour.children) { if(die.roll() < p) { result = child.executeBehaviour(result.wmem); @@ -1028,11 +1028,11 @@ options: [] min_children: 2 - execute: (behaviour, wmem) => { - let result = {success: true, wmem: wmem}; + execute: (owner, behaviour, wmem) => { + let result; for(const child of behaviour.children) { - result = child.executeBehaviour(result.wmem); - if(result.success) { + result = child.executeBehaviour(wmem); + if(!result.success) { break; } } @@ -1051,11 +1051,11 @@ min_children: 1 max_children: 1 - execute: (behaviour, wmem) => { - const count = behaviour.option("count"); + execute: (owner, behaviour, wmem) => { + const count = behaviour.intOption("count"); let result = {success: false, wmem: wmem}; for(let i=0; i { - const attempts = behaviour.option("attempts"); + execute: (owner, behaviour, wmem) => { + const attempts = behaviour.intOption("attempts"); let result = {success: false, wmem: wmem}; for(let i=0; i { - const p = behaviour.option("p"); + execute: (owner, behaviour, wmem) => { + const p = behaviour.intOption("p"); const die = new RezDie(1, 100, 0); if(die.roll() < p) { - return behaviour.firstChild().executeBehaviour(wmem); + return behaviour.firstChild.executeBehaviour(wmem); } else { return {success: false, id: behaviour.id, error: "Didn't execute"}; } @@ -1124,15 +1124,15 @@ min_children: 2 max_children: 2 - execute: (behaviour, wmem) => { - const p = behaviour.option("p"); + execute: (owner, behaviour, wmem) => { + const p = behaviour.intOption("p"); const die = new RezDie(1, 100, 0); const roll = die.roll(); if(roll < p) { - return behaviour.firstChild().executeBehaviour(wmem); + return behaviour.firstChild.executeBehaviour(wmem); } else { - return behaviour.secondChild().executeBehaviour(wmem); + return behaviour.secondChild.executeBehaviour(wmem); } } } @@ -1146,9 +1146,9 @@ options: [] min_children: 2 - execute: (behaviour, wmem) => { - const die = new RezDie(1, behaviour.childCount(), 0); - const child = behaviour.getChild(die.roll()); + execute: (owner, behaviour, wmem) => { + const die = new RezDie(1, behaviour.childCount, -1); + const child = behaviour.getChildAt(die.roll()); return child.executeBehaviour(wmem); } } @@ -1164,16 +1164,17 @@ options: [] min_children: 2 - execute: (behaviour, wmem) => { - let child_walk = wmem[this]; + execute: (owner, behaviour, wmem) => { + let child_walk = wmem[behaviour.id]; if(typeof(child_walk) == "undefined" || child_walk.length == 0) { - child_walk = Array.from(Array(behaviour.childCount()).keys()).shuffle(); + child_walk = Array.from(Array(behaviour.childCount).keys()).fy_shuffle(); } - const child = child_walk.shift(); - wmem[this] = child_walk; + const child_idx = child_walk.shift(); + wmem[behaviour.id] = child_walk; + const child = behaviour.getChildAt(child_idx); return child.executeBehaviour(wmem); } } @@ -1187,8 +1188,8 @@ min_children: 1 max_children: 1 - execute: (behaviour, wmem) => { - const result = behaviour.firstChild().executeBehaviour(wmem); + execute: (owner, behaviour, wmem) => { + const result = behaviour.firstChild.executeBehaviour(wmem); result.success = true; return result; } @@ -1203,8 +1204,8 @@ min_children: 1 max_children: 1 - execute: (behaviour, wmem) => { - const result = behaviour.firstChild().executeBehaviour(wmem); + execute: (owner, behaviour, wmem) => { + const result = behaviour.firstChild.executeBehaviour(wmem); result.success = false; return result; } @@ -1219,8 +1220,8 @@ min_children: 1 max_children: 1 - execute: (behaviour, wmem) => { - const result = behaviour.firstChild().executeBehaviour(wmem); + execute: (owner, behaviour, wmem) => { + const result = behaviour.firstChild.executeBehaviour(wmem); if(result.success) { result.success = false; result.error = "Inversion"; @@ -1238,7 +1239,7 @@ options: [] max_children: 0 - execute: (behaviour, wmem) => { + execute: (owner, behaviour, wmem) => { return {success: false, wmem: wmem}; } } @@ -1250,7 +1251,7 @@ options: [] max_children: 0 - execute: (behaviour, wmem) => { + execute: (owner, behaviour, wmem) => { return {success: true, wmem: wmem}; } } diff --git a/docs/REZ.adoc b/docs/REZ.adoc index ff8dfe4..530c6e4 100644 --- a/docs/REZ.adoc +++ b/docs/REZ.adoc @@ -1586,6 +1586,92 @@ The Rez stdlib defines a range of xref:behaviour_catalog.adoc[core behaviours] w The core behaviours are intended to provide an overall structure for creating different kinds of behaviour. In particular look at behaviours like xref:behaviour_catalog.adoc#_either[$either], xref:behaviour_catalog.adoc#_random_choice[$random_choice] and xref:behaviour_catalog:[$random_each] which can introduce variability into behaviour patterns. +=== Writing Your Own Behaviours + +The `@behaviour` element allows you to write your own behaviours, typically these will be conditions and actions that query & modify, respectively, your game world. Let's take a look at a query first: + +.... +@behaviour actor_is_armed { + execute: function(owner, behaviour, wmem) { + return { + success: ($player.main_inventory.weapon_contents.length > 0), + wmem: wmem + } + } +} +.... + +First note that the only thing we are required to define is the `execute:` attribute, which must be a regular function (an arrow function will not do). All `execute:` handler functions are required to return an object with two keys: `success` and `wmem`. The value for `success` should a boolean. In this case we return `true` if the players weapon slot has something in it, false if not. The value for `wmem` should be the working memory we were passed in. + +In this example we've assumed the actor is the #player. But we could make it more flexible using an option. + +.... +@behaviour actor_is_armed { + options: ["actor"] + execute: function(owner, behaviour, wmem) { + const actor = $(actor); + return { + success: (actor.main_inventory.weapon_contents.length > 0), + wmem: wmem + } + } +} +.... + +Now we'd specify the behaviour as + +.... +^[actor_is_armed actor=#sam_spade] +.... + +An as long as Sam's `@actor` definition contained a `main_inventory` all would be well. + +Now let's examine how we would define an action: + +.... +@behaviour actor_drinks { + execute: function(owner, behaviour, wmem) { + $player.drunk += 1; + return { + success: true, + wmem: wmem + }; + } +} +.... + +We can see that this is even simpler. Most actions will return a successful result since you have, likely, already queried whether they be executed or not. But if an action can be executed and fail you can return a different results as per the query above. + +Working memory is assumed to be an object that is passed between the different behaviours in the tree. For example one behaviour could store a value that a later behaviour will use. We can see how `actor_sees_item` could return success and store the id of the item in the working memory for `actor_equips_item` to put into their inventory. + +Lastly the owner is the object that owns the behaviour tree. This allows you to make use of the owner's attributes within the behaviour. Here's an example of doing this. Let's create a generic `query` behaviour that lets us test a property of any object and compare it with an attribute of the owning element: + +.... +@behaviour query { + options: ["if", "obj"] + execute: function(owner, behaviour, wmem) { + const f = behaviour.option("if").bind(owner); + const o = $(behaviour.option("obj")); + return { + success: f(o), + wmem: wmem + }; + } +} + +@object foo { + cost: 2 +} + +@object bar { + cash: 10 + + test: ^[query obj="foo" if=^{obj.cost < this.cash}] +} +.... + +Might take you a while to see what is going on here but we are making use of `bind` to make `this` into the owner object within the context of the code block being passed to the `query` via its `if` option. + === Using Behaviour Trees Having defined a behaviour tree, how do you use it? diff --git a/lib/AST/behaviour.ex b/lib/AST/behaviour.ex index b1a17d6..a6bad9f 100644 --- a/lib/AST/behaviour.ex +++ b/lib/AST/behaviour.ex @@ -29,7 +29,11 @@ defimpl Rez.AST.Node, for: Rez.AST.Behaviour do def default_attributes(_behaviour), do: %{ - "$auto_id_idx" => Attribute.number("$auto_id_idx", 0) + "$auto_id_idx" => Attribute.number("$auto_id_idx", 0), + "options" => Attribute.list("options", []), + "min_children" => Attribute.number("min_children", 0), + "max_children" => Attribute.number("max_children", 0), + "owner_id" => Attribute.elem_ref("owner_id", "") } def pre_process(behaviour), do: behaviour @@ -44,24 +48,28 @@ defimpl Rez.AST.Node, for: Rez.AST.Behaviour do def validators(_behaviour) do [ - attribute_present?( + attribute_if_present?( "options", attribute_has_type?( :list, attribute_coll_of?(:keyword) ) ), - attribute_if_present?( - "check_children", - attribute_has_type?(:function) - ), attribute_present?( "execute", attribute_has_type?( :function, - validate_expects_params?(["behaviour", "wmem"]) + validate_expects_params?(["owner", "behaviour", "wmem"]) ) ), + attribute_if_present?( + "min_children", + attribute_has_type?(:number) + ), + attribute_if_present?( + "max_children", + attribute_has_type?(:number) + ), attribute_if_present?( "$js_ctor", attribute_has_type?(:string) diff --git a/lib/AST/value_encoder.ex b/lib/AST/value_encoder.ex index 11e18e0..3309440 100644 --- a/lib/AST/value_encoder.ex +++ b/lib/AST/value_encoder.ex @@ -104,6 +104,10 @@ defmodule Rez.AST.ValueEncoder do encode_function(f) end + def encode_value({:code_block, code}) do + "function(obj) {return (#{code});}" + end + def encode_value({:roll, {count, sides, modifier, rounds}}) do "new RezDieRoll(#{sides}, #{count}, #{modifier}, #{rounds})" end diff --git a/lib/parser/value_parsers.ex b/lib/parser/value_parsers.ex index 407f519..b463e7b 100644 --- a/lib/parser/value_parsers.ex +++ b/lib/parser/value_parsers.ex @@ -281,6 +281,19 @@ defmodule Rez.Parser.ValueParsers do end) end + def code_block_value() do + ParserCache.get_parser("code_block", fn -> + sequence( + [ + lookahead(sequence([caret(), open_brace()])), + ignore(caret()), + text_delimited_by_nested_parsers(open_brace(), close_brace()) + ], + ast: fn [code] -> {:code_block, code} end + ) + end) + end + # Property # ^p{} def property_value() do @@ -543,6 +556,7 @@ defmodule Rez.Parser.ValueParsers do elem_ref_value(), binding_path_value(), keyword_value(), + code_block_value(), function_value(), dynamic_initializer_value(), dynamic_value(), diff --git a/mix.exs b/mix.exs index d170e09..1610dda 100644 --- a/mix.exs +++ b/mix.exs @@ -1,7 +1,7 @@ defmodule Rez.MixProject do use Mix.Project - @version "1.2.4" + @version "1.2.5" def project do [