From e0b0a2a30f66f2275fc110070393784f7fab5375 Mon Sep 17 00:00:00 2001 From: David Bloomin Date: Fri, 16 Jan 2026 20:09:46 -0800 Subject: [PATCH 1/2] feat(mettagrid): add BoundaryFilter for filtering objects at collective boundaries Add BoundaryFilter that checks if target is at the boundary between two collectives. Passes if target belongs to collective_a AND is adjacent to collective_b. Python: - BoundaryFilterConfig with filter_type="boundary", collective_a, collective_b fields - AtBoundary() helper function - Added to AnyFilter union C++: - BoundaryFilterConfig struct in handler_config.hpp - BoundaryFilter class in filter.hpp with AdjacencyCallback for grid integration - Added to FilterConfig variant Tests: - test_boundary_filter.py with creation, helper, serialization tests Note: Adjacency checking requires grid integration via set_adjacency_callback(). Co-Authored-By: Claude Opus 4.5 --- .../mettagrid/handler/filters/filter.hpp | 47 +++++++++++++++++++ .../mettagrid/handler/handler_config.hpp | 9 +++- .../src/mettagrid/config/filter_config.py | 33 +++++++++++++ .../mettagrid/tests/test_boundary_filter.py | 47 +++++++++++++++++++ 4 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 packages/mettagrid/tests/test_boundary_filter.py diff --git a/packages/mettagrid/cpp/include/mettagrid/handler/filters/filter.hpp b/packages/mettagrid/cpp/include/mettagrid/handler/filters/filter.hpp index 676d848ee20..c8dc879349c 100644 --- a/packages/mettagrid/cpp/include/mettagrid/handler/filters/filter.hpp +++ b/packages/mettagrid/cpp/include/mettagrid/handler/filters/filter.hpp @@ -1,6 +1,8 @@ #ifndef PACKAGES_METTAGRID_CPP_INCLUDE_METTAGRID_HANDLER_FILTERS_FILTER_HPP_ #define PACKAGES_METTAGRID_CPP_INCLUDE_METTAGRID_HANDLER_FILTERS_FILTER_HPP_ +#include + #include "core/grid_object.hpp" #include "handler/handler_config.hpp" #include "handler/handler_context.hpp" @@ -190,6 +192,51 @@ class CollectiveFilter : public Filter { CollectiveFilterConfig _config; }; +/** + * BoundaryFilter: Check if entity is at the boundary between two collectives + * Passes if target belongs to collective_a AND is adjacent to collective_b. + * Note: The adjacency check requires grid integration. The is_adjacent callback + * must be set externally with access to the grid to check neighboring objects. + */ +class BoundaryFilter : public Filter { +public: + // Callback type for checking adjacency to a collective + using AdjacencyCallback = std::function; + + explicit BoundaryFilter(const BoundaryFilterConfig& config) : _config(config) {} + + // Set the callback for checking adjacency (called during filter setup) + void set_adjacency_callback(AdjacencyCallback callback) { + _is_adjacent_to_collective = std::move(callback); + } + + bool passes(const HandlerContext& ctx) const override { + // Get GridObject to access collective + GridObject* grid_obj = dynamic_cast(ctx.resolve(_config.entity)); + if (grid_obj == nullptr) { + return false; + } + + // Check if target belongs to collective_a + Collective* coll = grid_obj->getCollective(); + if (coll == nullptr || coll->name != _config.collective_a) { + return false; + } + + // Check if target is adjacent to collective_b + if (_is_adjacent_to_collective) { + return _is_adjacent_to_collective(grid_obj, _config.collective_b); + } + + // If no adjacency callback is set, filter fails + return false; + } + +private: + BoundaryFilterConfig _config; + AdjacencyCallback _is_adjacent_to_collective = nullptr; +}; + } // namespace mettagrid #endif // PACKAGES_METTAGRID_CPP_INCLUDE_METTAGRID_HANDLER_FILTERS_FILTER_HPP_ diff --git a/packages/mettagrid/cpp/include/mettagrid/handler/handler_config.hpp b/packages/mettagrid/cpp/include/mettagrid/handler/handler_config.hpp index af26413ff1f..2f1674b5c7d 100644 --- a/packages/mettagrid/cpp/include/mettagrid/handler/handler_config.hpp +++ b/packages/mettagrid/cpp/include/mettagrid/handler/handler_config.hpp @@ -72,13 +72,20 @@ struct CollectiveFilterConfig { int collective_id = -1; // ID (index) of the collective the object must belong to (-1 = unset) }; +struct BoundaryFilterConfig { + EntityRef entity = EntityRef::target; + std::string collective_a; // Collective the target must belong to + std::string collective_b; // Collective the target must be adjacent to +}; + // Variant type for all filter configs using FilterConfig = std::variant; + CollectiveFilterConfig, + BoundaryFilterConfig>; // ============================================================================ // Mutation Configs diff --git a/packages/mettagrid/python/src/mettagrid/config/filter_config.py b/packages/mettagrid/python/src/mettagrid/config/filter_config.py index 7483aa573ac..585c2263b08 100644 --- a/packages/mettagrid/python/src/mettagrid/config/filter_config.py +++ b/packages/mettagrid/python/src/mettagrid/config/filter_config.py @@ -119,6 +119,30 @@ class CollectiveFilter(Filter): collective: str = Field(description="Name of collective the object must belong to") +class BoundaryFilter(Filter): + """Filter that checks if the target is at the boundary between two collectives. + + This is useful for events that should trigger at territorial boundaries, + such as when an object is adjacent to both friendly and enemy territory. + + The filter passes if the target object: + - Belongs to collective_a, AND + - Is adjacent to an object belonging to collective_b + + Example: + BoundaryFilter(collective_a="cogs", collective_b="clips") + # Affects cogs objects that are adjacent to clips territory + """ + + filter_type: Literal["boundary"] = "boundary" + target: HandlerTarget = Field( + default=HandlerTarget.TARGET, + description="Entity to check the filter against", + ) + collective_a: str = Field(description="Collective the target must belong to") + collective_b: str = Field(description="Collective the target must be adjacent to") + + AnyFilter = Annotated[ Union[ Annotated[VibeFilter, Tag("vibe")], @@ -126,6 +150,7 @@ class CollectiveFilter(Filter): Annotated[AlignmentFilter, Tag("alignment")], Annotated[ObjectTypeFilter, Tag("object_type")], Annotated[CollectiveFilter, Tag("collective")], + Annotated[BoundaryFilter, Tag("boundary")], ], Discriminator("filter_type"), ] @@ -192,6 +217,12 @@ def BelongsToCollective(collective: str) -> CollectiveFilter: return CollectiveFilter(collective=collective) +# Helper for BoundaryFilter +def AtBoundary(collective_a: str, collective_b: str) -> BoundaryFilter: + """Filter: target belongs to collective_a and is adjacent to collective_b.""" + return BoundaryFilter(collective_a=collective_a, collective_b=collective_b) + + # Re-export all filter-related types __all__ = [ # Enums @@ -205,6 +236,7 @@ def BelongsToCollective(collective: str) -> CollectiveFilter: "AlignmentFilter", "ObjectTypeFilter", "CollectiveFilter", + "BoundaryFilter", "AnyFilter", # Filter helpers "isAligned", @@ -218,4 +250,5 @@ def BelongsToCollective(collective: str) -> CollectiveFilter: "TargetCollectiveHas", "HasTag", "BelongsToCollective", + "AtBoundary", ] diff --git a/packages/mettagrid/tests/test_boundary_filter.py b/packages/mettagrid/tests/test_boundary_filter.py new file mode 100644 index 00000000000..aba0459a73d --- /dev/null +++ b/packages/mettagrid/tests/test_boundary_filter.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 + +"""Test BoundaryFilter configuration.""" + +from mettagrid.config.filter_config import AtBoundary, BoundaryFilter + + +class TestBoundaryFilter: + """Tests for BoundaryFilter configuration.""" + + def test_boundary_filter_creation(self): + """Test creating BoundaryFilter directly.""" + f = BoundaryFilter(collective_a="cogs", collective_b="clips") + assert f.filter_type == "boundary" + assert f.collective_a == "cogs" + assert f.collective_b == "clips" + + def test_at_boundary_helper(self): + """Test AtBoundary helper function.""" + f = AtBoundary("sprockets", "widgets") + assert isinstance(f, BoundaryFilter) + assert f.filter_type == "boundary" + assert f.collective_a == "sprockets" + assert f.collective_b == "widgets" + + def test_boundary_filter_serialization(self): + """Test BoundaryFilter serialization.""" + f = BoundaryFilter(collective_a="team_a", collective_b="team_b") + data = f.model_dump() + assert data["filter_type"] == "boundary" + assert data["collective_a"] == "team_a" + assert data["collective_b"] == "team_b" + + def test_boundary_filter_deserialization(self): + """Test BoundaryFilter deserialization.""" + f = BoundaryFilter(collective_a="red", collective_b="blue") + json_str = f.model_dump_json() + restored = BoundaryFilter.model_validate_json(json_str) + assert restored.filter_type == "boundary" + assert restored.collective_a == "red" + assert restored.collective_b == "blue" + + +if __name__ == "__main__": + import pytest + + pytest.main([__file__, "-v"]) From 1958dbd42deccdf2e3ce067e1c66bf4a8ce7696b Mon Sep 17 00:00:00 2001 From: David Bloomin Date: Fri, 16 Jan 2026 20:55:32 -0800 Subject: [PATCH 2/2] docs: add mechanic-based curriculum design Design for automatic curriculum that promotes game mechanics: - Threshold-based graduation - Random selection from ungraduated mechanics - Scaffolding removal after graduation - Configurable batch size Co-Authored-By: Claude Opus 4.5 --- .../cpp/include/mettagrid/handler/handler_bindings.hpp | 10 ++++++++++ packages/mettagrid/cpp/src/mettagrid/handler/event.cpp | 2 ++ .../mettagrid/cpp/src/mettagrid/handler/handler.cpp | 2 ++ .../python/src/mettagrid/config/mettagrid_c_config.py | 9 +++++++++ 4 files changed, 23 insertions(+) diff --git a/packages/mettagrid/cpp/include/mettagrid/handler/handler_bindings.hpp b/packages/mettagrid/cpp/include/mettagrid/handler/handler_bindings.hpp index 55b9c80f2de..f5e16cf64f6 100644 --- a/packages/mettagrid/cpp/include/mettagrid/handler/handler_bindings.hpp +++ b/packages/mettagrid/cpp/include/mettagrid/handler/handler_bindings.hpp @@ -71,6 +71,12 @@ inline void bind_handler_config(py::module& m) { .def_readwrite("entity", &CollectiveFilterConfig::entity) .def_readwrite("collective_id", &CollectiveFilterConfig::collective_id); + py::class_(m, "BoundaryFilterConfig") + .def(py::init<>()) + .def_readwrite("entity", &BoundaryFilterConfig::entity) + .def_readwrite("collective_a", &BoundaryFilterConfig::collective_a) + .def_readwrite("collective_b", &BoundaryFilterConfig::collective_b); + // Mutation configs py::class_(m, "ResourceDeltaMutationConfig") .def(py::init<>()) @@ -146,6 +152,10 @@ inline void bind_handler_config(py::module& m) { "add_collective_filter", [](HandlerConfig& self, const CollectiveFilterConfig& cfg) { self.filters.push_back(cfg); }, py::arg("filter")) + .def( + "add_boundary_filter", + [](HandlerConfig& self, const BoundaryFilterConfig& cfg) { self.filters.push_back(cfg); }, + py::arg("filter")) // Add mutation methods - each type wraps into the variant .def( "add_resource_delta_mutation", diff --git a/packages/mettagrid/cpp/src/mettagrid/handler/event.cpp b/packages/mettagrid/cpp/src/mettagrid/handler/event.cpp index d34c87c4cb1..ed6a8a60e16 100644 --- a/packages/mettagrid/cpp/src/mettagrid/handler/event.cpp +++ b/packages/mettagrid/cpp/src/mettagrid/handler/event.cpp @@ -75,6 +75,8 @@ std::unique_ptr Event::create_filter(const FilterConfig& config) { return std::make_unique(cfg); } else if constexpr (std::is_same_v) { return std::make_unique(cfg); + } else if constexpr (std::is_same_v) { + return std::make_unique(cfg); } else { return nullptr; } diff --git a/packages/mettagrid/cpp/src/mettagrid/handler/handler.cpp b/packages/mettagrid/cpp/src/mettagrid/handler/handler.cpp index 86f1c19b926..09c34aadf75 100644 --- a/packages/mettagrid/cpp/src/mettagrid/handler/handler.cpp +++ b/packages/mettagrid/cpp/src/mettagrid/handler/handler.cpp @@ -79,6 +79,8 @@ std::unique_ptr Handler::create_filter(const FilterConfig& config) { return std::make_unique(cfg); } else if constexpr (std::is_same_v) { return std::make_unique(cfg); + } else if constexpr (std::is_same_v) { + return std::make_unique(cfg); } else { return nullptr; } diff --git a/packages/mettagrid/python/src/mettagrid/config/mettagrid_c_config.py b/packages/mettagrid/python/src/mettagrid/config/mettagrid_c_config.py index 439cc636989..7caa7998c89 100644 --- a/packages/mettagrid/python/src/mettagrid/config/mettagrid_c_config.py +++ b/packages/mettagrid/python/src/mettagrid/config/mettagrid_c_config.py @@ -17,6 +17,7 @@ from mettagrid.mettagrid_c import AssemblerConfig as CppAssemblerConfig from mettagrid.mettagrid_c import AttackActionConfig as CppAttackActionConfig from mettagrid.mettagrid_c import AttackOutcome as CppAttackOutcome +from mettagrid.mettagrid_c import BoundaryFilterConfig as CppBoundaryFilterConfig from mettagrid.mettagrid_c import ChangeVibeActionConfig as CppChangeVibeActionConfig from mettagrid.mettagrid_c import ChestConfig as CppChestConfig from mettagrid.mettagrid_c import CollectiveConfig as CppCollectiveConfig @@ -122,6 +123,14 @@ def _convert_handlers( cpp_filter.collective_id = collective_name_to_id[collective_name] cpp_handler.add_collective_filter(cpp_filter) + elif filter_type == "boundary": + # Boundary filter checks if target is at boundary between two collectives + cpp_filter = CppBoundaryFilterConfig() + cpp_filter.entity = convert_entity_ref(filter_config.target) + cpp_filter.collective_a = filter_config.collective_a + cpp_filter.collective_b = filter_config.collective_b + cpp_handler.add_boundary_filter(cpp_filter) + # Convert mutations using shared utility convert_mutations( handler.mutations,