diff --git a/addons/beehave/nodes/composites/randomized_composite.gd b/addons/beehave/nodes/composites/randomized_composite.gd index 6ac32b32..62661187 100644 --- a/addons/beehave/nodes/composites/randomized_composite.gd +++ b/addons/beehave/nodes/composites/randomized_composite.gd @@ -16,7 +16,7 @@ const WEIGHTS_PREFIX = "Weights/" @export var use_weights: bool: set(value): use_weights = value - if use_weights and Engine.is_editor_hint(): + if use_weights: _update_weights(get_children()) _connect_children_changing_signals() notify_property_list_changed() @@ -112,6 +112,7 @@ func _update_weights(children: Array[Node]) -> void: func _on_child_entered_tree(node: Node): +# print('%s has entered the tree' % node.name) _update_weights(get_children()) if not node.renamed.is_connected(_on_child_renamed): @@ -119,6 +120,9 @@ func _on_child_entered_tree(node: Node): func _on_child_exiting_tree(node: Node): + if node.renamed.is_connected(_on_child_renamed): + node.renamed.disconnect(_on_child_renamed) + var children = get_children() children.erase(node) _update_weights(children) diff --git a/addons/beehave/nodes/composites/sequence_random.gd b/addons/beehave/nodes/composites/sequence_random.gd index 458e5814..ab8eb789 100644 --- a/addons/beehave/nodes/composites/sequence_random.gd +++ b/addons/beehave/nodes/composites/sequence_random.gd @@ -5,8 +5,8 @@ @icon("../../icons/sequence_random.svg") class_name SequenceRandomComposite extends RandomizedComposite - -signal reseted(new_order) +# Emitted whenever the children are shuffled. +signal reset(new_order: Array[Node]) ## Whether the sequence should start where it left off after a previous failure. @export var resume_on_failure: bool = false @@ -80,12 +80,13 @@ func _get_reversed_indexes() -> Array[int]: func _reset() -> void: - _children_bag = get_shuffled_children() - reseted.emit(_children_bag) + var new_order = get_shuffled_children() + _children_bag = new_order.duplicate() + _children_bag.reverse() # It needs to run the children in reverse order. + reset.emit(new_order) func get_class_name() -> Array[StringName]: var classes := super() classes.push_back(&"SequenceRandomComposite") return classes - diff --git a/node_2d.gd b/node_2d.gd deleted file mode 100644 index 8eda8135..00000000 --- a/node_2d.gd +++ /dev/null @@ -1,22 +0,0 @@ -extends Node2D - -var results: Dictionary - -var times_reseted: int = 0 - -@onready var label: Label = $Label - -func _on_sequence_random_composite_reseted(new_order): - times_reseted += 1 - var first = new_order[0] - if not results.has(first.name): - results[first.name] = 0 - results[first.name] += 1 - _update_label() - -func _update_label(): - var text = "resets: %d\n" % times_reseted - for node in results.keys(): - var perc = float(results[node]) / float(times_reseted) * 100.0 - text += "%s: %.2f%%\n" % [node, perc] - label.text = text diff --git a/node_2d.tscn b/node_2d.tscn deleted file mode 100644 index 6e952b15..00000000 --- a/node_2d.tscn +++ /dev/null @@ -1,38 +0,0 @@ -[gd_scene load_steps=5 format=3 uid="uid://qjpfc7lbb2a1"] - -[ext_resource type="Script" path="res://node_2d.gd" id="1_bh4q4"] -[ext_resource type="Script" path="res://addons/beehave/nodes/beehave_tree.gd" id="1_ktxiv"] -[ext_resource type="Script" path="res://addons/beehave/nodes/composites/sequence_random.gd" id="2_gocxx"] -[ext_resource type="Script" path="res://test/actions/mock_action.gd" id="3_qrq84"] - -[node name="Node2D" type="Node2D"] -script = ExtResource("1_bh4q4") - -[node name="Label" type="Label" parent="."] -offset_right = 40.0 -offset_bottom = 23.0 - -[node name="BeehaveTree" type="Node" parent="."] -script = ExtResource("1_ktxiv") - -[node name="SequenceRandomComposite" type="Node" parent="BeehaveTree"] -script = ExtResource("2_gocxx") -use_weights = true -Weights/First = 40 -Weights/Second = 30 -Weights/Third = 15 -Weights/Forth = 15 - -[node name="First" type="Node" parent="BeehaveTree/SequenceRandomComposite"] -script = ExtResource("3_qrq84") - -[node name="Second" type="Node" parent="BeehaveTree/SequenceRandomComposite"] -script = ExtResource("3_qrq84") - -[node name="Third" type="Node" parent="BeehaveTree/SequenceRandomComposite"] -script = ExtResource("3_qrq84") - -[node name="Forth" type="Node" parent="BeehaveTree/SequenceRandomComposite"] -script = ExtResource("3_qrq84") - -[connection signal="reseted" from="BeehaveTree/SequenceRandomComposite" to="." method="_on_sequence_random_composite_reseted"] diff --git a/test/nodes/composites/sequence_random_test.gd b/test/nodes/composites/sequence_random_test.gd index 5aefa023..5a716922 100644 --- a/test/nodes/composites/sequence_random_test.gd +++ b/test/nodes/composites/sequence_random_test.gd @@ -21,7 +21,9 @@ var action2: ActionLeaf func before_test() -> void: tree = auto_free(load(__tree).new()) action1 = auto_free(load(__count_up_action).new()) + action1.name = 'Action 1' action2 = auto_free(load(__count_up_action).new()) + action2.name = 'Action 2' sequence = auto_free(load(__source).new()) sequence.random_seed = RANDOM_SEED var actor = auto_free(Node2D.new()) @@ -63,14 +65,45 @@ func test_random_even_execution() -> void: assert_that(action2.count).is_equal(2) +func test_weighted_random_sampling() -> void: + sequence.use_weights = true + sequence._weights[action1.name] = 2 + assert_dict(sequence._weights).contains_key_value(action1.name, 2) + assert_dict(sequence._weights).contains_key_value(action2.name, 1) + + action1.status = BeehaveNode.RUNNING + action2.status = BeehaveNode.RUNNING + + assert_array(sequence._children_bag).is_empty() + + assert_that(tree.tick()).is_equal(BeehaveNode.RUNNING) + + # Children are in reverse order; aka action1 will run first. + assert_array(sequence._children_bag)\ + .contains_exactly([action2, action1]) + + # Only action 1 should have executed. + assert_that(action1.count).is_equal(1) + assert_that(action2.count).is_equal(0) + + action1.status = BeehaveNode.SUCCESS + + assert_that(tree.tick()).is_equal(BeehaveNode.RUNNING) + + assert_that(action1.count).is_equal(2) + assert_that(action2.count).is_equal(1) + + sequence.use_weights = false + + func test_return_failure_of_none_is_succeeding() -> void: action1.status = BeehaveNode.FAILURE action2.status = BeehaveNode.FAILURE assert_that(tree.tick()).is_equal(BeehaveNode.FAILURE) - assert_that(action1.count).is_equal(1) - assert_that(action2.count).is_equal(0) + assert_that(action1.count).is_equal(0) + assert_that(action2.count).is_equal(1) func test_clear_running_child_after_run() -> void: diff --git a/test/randomized_composites/runtime_changes/RuntimeChangesTestScene.gd b/test/randomized_composites/runtime_changes/RuntimeChangesTestScene.gd new file mode 100644 index 00000000..0437baac --- /dev/null +++ b/test/randomized_composites/runtime_changes/RuntimeChangesTestScene.gd @@ -0,0 +1,3 @@ +extends Node2D + +@onready var sequence_random: SequenceRandomComposite = %SequenceRandom diff --git a/test/randomized_composites/runtime_changes/RuntimeChangesTestScene.tscn b/test/randomized_composites/runtime_changes/RuntimeChangesTestScene.tscn new file mode 100644 index 00000000..573dc29a --- /dev/null +++ b/test/randomized_composites/runtime_changes/RuntimeChangesTestScene.tscn @@ -0,0 +1,34 @@ +[gd_scene load_steps=5 format=3 uid="uid://dhhw4ej2jbyha"] + +[ext_resource type="Script" path="res://addons/beehave/nodes/beehave_tree.gd" id="1_10c1m"] +[ext_resource type="Script" path="res://test/randomized_composites/runtime_changes/RuntimeChangesTestScene.gd" id="1_folsk"] +[ext_resource type="Script" path="res://addons/beehave/nodes/composites/sequence_random.gd" id="2_k8ytk"] +[ext_resource type="Script" path="res://test/actions/mock_action.gd" id="3_kqvkq"] + +[node name="RuntimeChangesTestScene" type="Node2D"] +script = ExtResource("1_folsk") + +[node name="BeehaveTree" type="Node" parent="."] +script = ExtResource("1_10c1m") + +[node name="SequenceRandom" type="Node" parent="BeehaveTree"] +unique_name_in_owner = true +script = ExtResource("2_k8ytk") +random_seed = 12345 +use_weights = true +Weights/Idle = 1 +Weights/Run = 1 +"Weights/Attack Meele" = 1 +"Weights/Attack Ranged" = 1 + +[node name="Idle" type="Node" parent="BeehaveTree/SequenceRandom"] +script = ExtResource("3_kqvkq") + +[node name="Run" type="Node" parent="BeehaveTree/SequenceRandom"] +script = ExtResource("3_kqvkq") + +[node name="Attack Meele" type="Node" parent="BeehaveTree/SequenceRandom"] +script = ExtResource("3_kqvkq") + +[node name="Attack Ranged" type="Node" parent="BeehaveTree/SequenceRandom"] +script = ExtResource("3_kqvkq") diff --git a/test/randomized_composites/runtime_changes/runtime_changes_test.gd b/test/randomized_composites/runtime_changes/runtime_changes_test.gd new file mode 100644 index 00000000..6eb40d30 --- /dev/null +++ b/test/randomized_composites/runtime_changes/runtime_changes_test.gd @@ -0,0 +1,105 @@ +# GdUnit generated TestSuite +class_name RuntimeChangesTest +extends GdUnitTestSuite +@warning_ignore("unused_parameter") +@warning_ignore("return_value_discarded") + + +const __source = "res://test/randomized_composites/runtime_changes/RuntimeChangesTestScene.tscn" +const __mock_action = "res://test/actions/mock_action.gd" + +func create_scene() -> Node2D: + var scene = auto_free(load(__source).instantiate()) + return scene + + +func create_new_action(): + var new_action = auto_free(load(__mock_action).new()) + new_action.name = "New Attack" + return new_action + + +func test_add_child() -> void: + var scene = create_scene() + var runner := scene_runner(scene) + + runner.set_time_factor(100.0) + + var weights_before = scene.sequence_random._weights.duplicate() + + runner.simulate_frames(10) + + var new_action = create_new_action() + scene.sequence_random.add_child(new_action) + + # Weights should have a new key with the added child. + assert_dict(scene.sequence_random._weights)\ + .contains_key_value(new_action.name, 1) + + # All other children's weights should be the same. + for node in weights_before.keys(): + assert_dict(scene.sequence_random._weights)\ + .contains_key_value(node, weights_before[node]) + + runner.simulate_frames(10) # Everything should work fine afterwards. + + +func test_remove_child() -> void: + var scene = create_scene() + var runner := scene_runner(scene) + + runner.set_time_factor(100.0) + + var weights_before: Dictionary = scene.sequence_random._weights.duplicate() + + runner.simulate_frames(10) + + var removed_action = runner.find_child(weights_before.keys()[0]) + scene.sequence_random.remove_child(removed_action) + + # Weights should not have that action anymore. + assert_dict(scene.sequence_random._weights)\ + .not_contains_keys([removed_action.name]) + + # All other children's weights should be the same. + var other_children = weights_before.keys()\ + .filter(func(k): return k != removed_action.name) + for node in other_children: + assert_dict(scene.sequence_random._weights)\ + .contains_key_value(node, weights_before[node]) + + removed_action.queue_free() + + runner.simulate_frames(10) # Everything should work fine afterwards. + + +func test_rename_child() -> void: + var scene = create_scene() + var runner := scene_runner(scene) + + runner.set_time_factor(100.0) + + var weights_before: Dictionary = scene.sequence_random._weights.duplicate() + + runner.simulate_frames(10) + + var renamed_action = runner.find_child(weights_before.keys()[0]) + var previous_name = renamed_action.name + renamed_action.name = "Renamed Action" + + # Weights should not have the old action name anymore. + assert_dict(scene.sequence_random._weights)\ + .not_contains_keys([previous_name]) + + # Weights should have the new name with the default weight. + assert_dict(scene.sequence_random._weights)\ + .contains_key_value(renamed_action.name, 1) + + # All other children's weights should be the same. + var other_children = weights_before.keys()\ + .filter(func(k): return k != previous_name) + for node in other_children: + assert_dict(scene.sequence_random._weights)\ + .contains_key_value(node, weights_before[node]) + + runner.simulate_frames(10) # Everything should work fine afterwards. diff --git a/test/randomized_composites/weighted_sampling/WeightedSamplingTestScene.gd b/test/randomized_composites/weighted_sampling/WeightedSamplingTestScene.gd new file mode 100644 index 00000000..32ac2ca4 --- /dev/null +++ b/test/randomized_composites/weighted_sampling/WeightedSamplingTestScene.gd @@ -0,0 +1,30 @@ +extends Node2D + +signal done + +# How many iterations should the test run. +@export var test_sample_count: int = 1_000 + +@onready var sequence_random: SequenceRandomComposite = %SequenceRandom + +var reset_count: int = 0 +var sample_count: Dictionary = {} + + +func _on_sequence_reset(new_order: Array[Node]): + reset_count += 1 + var first = new_order[0] + if not sample_count.has(first.name): + sample_count[first.name] = 0 + sample_count[first.name] += 1 + + if reset_count >= test_sample_count: + done.emit() + + +func get_final_results() -> Dictionary: + var final_results = {} + for node in sample_count.keys(): + var perc = float(sample_count[node]) / float(reset_count) * 100.0 + final_results[node] = perc + return final_results diff --git a/test/randomized_composites/weighted_sampling/WeightedSamplingTestScene.tscn b/test/randomized_composites/weighted_sampling/WeightedSamplingTestScene.tscn new file mode 100644 index 00000000..6a4dd620 --- /dev/null +++ b/test/randomized_composites/weighted_sampling/WeightedSamplingTestScene.tscn @@ -0,0 +1,36 @@ +[gd_scene load_steps=5 format=3 uid="uid://dkaaauniwk8vr"] + +[ext_resource type="Script" path="res://test/randomized_composites/weighted_sampling/WeightedSamplingTestScene.gd" id="1_jp21r"] +[ext_resource type="Script" path="res://addons/beehave/nodes/beehave_tree.gd" id="1_lxyfw"] +[ext_resource type="Script" path="res://addons/beehave/nodes/composites/sequence_random.gd" id="2_rx8pw"] +[ext_resource type="Script" path="res://test/actions/mock_action.gd" id="3_kgdbt"] + +[node name="WeightedSamplingTestScene" type="Node2D"] +script = ExtResource("1_jp21r") + +[node name="BeehaveTree" type="Node" parent="."] +script = ExtResource("1_lxyfw") + +[node name="SequenceRandom" type="Node" parent="BeehaveTree"] +unique_name_in_owner = true +script = ExtResource("2_rx8pw") +random_seed = 12345 +use_weights = true +Weights/Idle = 1 +Weights/Run = 1 +"Weights/Attack Meele" = 1 +"Weights/Attack Ranged" = 1 + +[node name="Idle" type="Node" parent="BeehaveTree/SequenceRandom"] +script = ExtResource("3_kgdbt") + +[node name="Run" type="Node" parent="BeehaveTree/SequenceRandom"] +script = ExtResource("3_kgdbt") + +[node name="Attack Meele" type="Node" parent="BeehaveTree/SequenceRandom"] +script = ExtResource("3_kgdbt") + +[node name="Attack Ranged" type="Node" parent="BeehaveTree/SequenceRandom"] +script = ExtResource("3_kgdbt") + +[connection signal="reset" from="BeehaveTree/SequenceRandom" to="." method="_on_sequence_reset"] diff --git a/test/randomized_composites/weighted_sampling/weighted_sampling_test.gd b/test/randomized_composites/weighted_sampling/weighted_sampling_test.gd new file mode 100644 index 00000000..e79d03c9 --- /dev/null +++ b/test/randomized_composites/weighted_sampling/weighted_sampling_test.gd @@ -0,0 +1,46 @@ +# GdUnit generated TestSuite +class_name WeightedSamplingTest +extends GdUnitTestSuite +@warning_ignore("unused_parameter") +@warning_ignore("return_value_discarded") + + +# TestSuite generated from +const __source = "res://test/randomized_composites/weighted_sampling/WeightedSamplingTestScene.tscn" + +const SAMPLE_SIZE = 1_000 +const ACCEPTABLE_RANGE = 3.0 + + +func create_scene() -> Node2D: + var scene = auto_free(load(__source).instantiate()) + scene.test_sample_count = SAMPLE_SIZE + return scene + + +func test_weights_effecting_sample() -> void: + var scene = create_scene() + var runner := scene_runner(scene) + + runner.set_time_factor(500.0) + + await runner.await_signal("done", [], 5000) + + # Make sure the scene has reset enough times. + assert_int(scene.reset_count).is_greater_equal(SAMPLE_SIZE) + + var result: Dictionary = scene.get_final_results() + var weights: Dictionary = scene.sequence_random._weights + + # Both weights and the results should have the same keys. + assert_array(result.keys()).contains_exactly_in_any_order(weights.keys()) + + var weight_sum: float = weights.values()\ + .reduce(func(acc, n): return acc + n, 0.0) + + # The percentage of a node being the first should be more or less + # the value of the weight relative to the total of weights. + for action in result.keys(): + var normalized_weight: float = (weights[action] / weight_sum) * 100.0 + assert_float(result[action]).is_equal_approx(normalized_weight, ACCEPTABLE_RANGE) +