Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 <functional>

#include "core/grid_object.hpp"
#include "handler/handler_config.hpp"
#include "handler/handler_context.hpp"
Expand Down Expand Up @@ -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<bool(GridObject*, const std::string&)>;

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<GridObject*>(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_
Original file line number Diff line number Diff line change
Expand Up @@ -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_<BoundaryFilterConfig>(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_<ResourceDeltaMutationConfig>(m, "ResourceDeltaMutationConfig")
.def(py::init<>())
Expand Down Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<VibeFilterConfig,
ResourceFilterConfig,
AlignmentFilterConfig,
TagFilterConfig,
ObjectTypeFilterConfig,
CollectiveFilterConfig>;
CollectiveFilterConfig,
BoundaryFilterConfig>;

// ============================================================================
// Mutation Configs
Expand Down
2 changes: 2 additions & 0 deletions packages/mettagrid/cpp/src/mettagrid/handler/event.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ std::unique_ptr<Filter> Event::create_filter(const FilterConfig& config) {
return std::make_unique<ObjectTypeFilter>(cfg);
} else if constexpr (std::is_same_v<T, CollectiveFilterConfig>) {
return std::make_unique<CollectiveFilter>(cfg);
} else if constexpr (std::is_same_v<T, BoundaryFilterConfig>) {
return std::make_unique<BoundaryFilter>(cfg);
} else {
return nullptr;
}
Expand Down
2 changes: 2 additions & 0 deletions packages/mettagrid/cpp/src/mettagrid/handler/handler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ std::unique_ptr<Filter> Handler::create_filter(const FilterConfig& config) {
return std::make_unique<ObjectTypeFilter>(cfg);
} else if constexpr (std::is_same_v<T, CollectiveFilterConfig>) {
return std::make_unique<CollectiveFilter>(cfg);
} else if constexpr (std::is_same_v<T, BoundaryFilterConfig>) {
return std::make_unique<BoundaryFilter>(cfg);
} else {
return nullptr;
}
Expand Down
33 changes: 33 additions & 0 deletions packages/mettagrid/python/src/mettagrid/config/filter_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,13 +119,38 @@ 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")],
Annotated[ResourceFilter, Tag("resource")],
Annotated[AlignmentFilter, Tag("alignment")],
Annotated[ObjectTypeFilter, Tag("object_type")],
Annotated[CollectiveFilter, Tag("collective")],
Annotated[BoundaryFilter, Tag("boundary")],
],
Discriminator("filter_type"),
]
Expand Down Expand Up @@ -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
Expand All @@ -205,6 +236,7 @@ def BelongsToCollective(collective: str) -> CollectiveFilter:
"AlignmentFilter",
"ObjectTypeFilter",
"CollectiveFilter",
"BoundaryFilter",
"AnyFilter",
# Filter helpers
"isAligned",
Expand All @@ -218,4 +250,5 @@ def BelongsToCollective(collective: str) -> CollectiveFilter:
"TargetCollectiveHas",
"HasTag",
"BelongsToCollective",
"AtBoundary",
]
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
47 changes: 47 additions & 0 deletions packages/mettagrid/tests/test_boundary_filter.py
Original file line number Diff line number Diff line change
@@ -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"])
Loading