From 135ce55b4bb7c32f9bd12f346a285b7b2fde658d Mon Sep 17 00:00:00 2001 From: David Bloomin Date: Sun, 18 Jan 2026 14:22:42 -0800 Subject: [PATCH] feat(mettagrid): add NearCollective filter and clip takeover events Add NearCollective filter that matches objects NOT aligned to a collective but within a specified radius of objects that ARE aligned to it. Update CogsGuardMission to use the events system for clip territorial expansion: - Every 100 ticks, cogs buildings at boundary with clips become neutral - Every 100 ticks, neutral buildings near clips become aligned to clips Tests: - mettagrid: NearCollective filter config, serialization, event configs - cogames: CogsGuardMission event configuration tests --- .gitignore | 1 + .memory/sessions/2026-01-17.session.jsonl | 73 ------ .../src/cogames/cogs_vs_clips/mission.py | 31 +++ .../cogames/tests/test_cogsguard_events.py | 97 ++++++++ .../.memory/sessions/2026-01-17.session.jsonl | 2 - .../mettagrid/handler/filters/filter.hpp | 45 ++++ .../mettagrid/handler/handler_bindings.hpp | 10 + .../mettagrid/handler/handler_config.hpp | 9 +- .../cpp/src/mettagrid/handler/event.cpp | 2 + .../cpp/src/mettagrid/handler/handler.cpp | 2 + .../src/mettagrid/config/event_config.py | 2 +- .../src/mettagrid/config/filter_config.py | 41 ++++ .../mettagrid/config/mettagrid_c_config.py | 9 + .../tests/test_clip_takeover_events.py | 214 ++++++++++++++++++ .../mettagrid/tests/test_events_config.py | 77 ++++--- uv.lock | 2 + 16 files changed, 504 insertions(+), 113 deletions(-) delete mode 100644 .memory/sessions/2026-01-17.session.jsonl create mode 100644 packages/cogames/tests/test_cogsguard_events.py delete mode 100644 packages/mettagrid/.memory/sessions/2026-01-17.session.jsonl create mode 100644 packages/mettagrid/tests/test_clip_takeover_events.py diff --git a/.gitignore b/.gitignore index 97b6a0860f9..d89f76ad77e 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ resources # Environment and IDE .claude/tasks .claude/analysis +.memory/ .envrc .env .env.* diff --git a/.memory/sessions/2026-01-17.session.jsonl b/.memory/sessions/2026-01-17.session.jsonl deleted file mode 100644 index 188358e2adb..00000000000 --- a/.memory/sessions/2026-01-17.session.jsonl +++ /dev/null @@ -1,73 +0,0 @@ -{"ts": "2026-01-17T20:37:29.084066", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "git branch --show-current", "exit_code": 0} -{"ts": "2026-01-17T20:37:34.944779", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "gt sync --no-interactive 2>&1", "exit_code": 0} -{"ts": "2026-01-17T20:37:40.750067", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "gt restack 2>&1", "exit_code": 0} -{"ts": "2026-01-17T20:37:46.227536", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "git checkout '01-16-feat_mettagrid_add_events_system_core_infrastructure_add_eventconfig_class_for_timestep-based_events_that_apply_mutations_to_filtered_objects._events_can_target_any_gridobject_and_include_-_timesteps_list_of_timesteps_when_event' 2>&1", "exit_code": 0} -{"ts": "2026-01-17T20:37:53.357263", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "gh pr view --json number,url,title,reviews,comments 2>&1", "exit_code": 0} -{"ts": "2026-01-17T20:37:54.194567", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "git remote get-url origin | sed 's/.*github.com[:/]\\(.*\\)\\.git/\\1/' | head -1", "exit_code": 0} -{"ts": "2026-01-17T20:38:13.960327", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "git push --force-with-lease 2>&1", "exit_code": 0} -{"ts": "2026-01-17T20:39:14.342086", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "grep -n \"Event\\|event\" /Users/daveey/code/metta/packages/mettagrid/cpp/bindings/mettagrid_c.cpp 2>/dev/null | head -50", "exit_code": 0} -{"ts": "2026-01-17T20:39:26.307179", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "gt state 2>&1 | head -40", "exit_code": 0} -{"ts": "2026-01-17T20:39:50.192497", "tool": "Write", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "file": "/Users/daveey/code/metta/packages/mettagrid/cpp/include/mettagrid/handler/event_scheduler.hpp", "success": true} -{"ts": "2026-01-17T20:39:58.885009", "tool": "Write", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "file": "/Users/daveey/code/metta/packages/mettagrid/cpp/src/mettagrid/handler/event_scheduler.cpp", "success": true} -{"ts": "2026-01-17T20:40:03.766516", "tool": "Edit", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "file": "/Users/daveey/code/metta/packages/mettagrid/cpp/include/mettagrid/handler/event_scheduler.hpp", "success": true, "change": "#include \n#include \n#include #include \n#include \n#include \n#include \n#include #include \n#include \n#include /**\n * Event processes timestep-based effects thro..."} -{"ts": "2026-01-17T20:40:36.729972", "tool": "Edit", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "file": "/Users/daveey/code/metta/packages/mettagrid/cpp/include/mettagrid/handler/event.hpp", "success": true, "change": " // Get event name\n const std::string& name() co... -> // Get event name\n const std::string& name() co..."} -{"ts": "2026-01-17T20:40:41.740446", "tool": "Edit", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "file": "/Users/daveey/code/metta/packages/mettagrid/cpp/include/mettagrid/handler/event.hpp", "success": true, "change": " std::string _name;\n std::unordered_set _ti... -> std::string _name;\n std::vector Event::Event(const EventConfig& config) : _name(co..."} -{"ts": "2026-01-17T20:42:45.471011", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "uv run pytest packages/mettagrid/tests/ -v --benchmark-disable -x 2>&1 | tail -100", "exit_code": 0} -{"ts": "2026-01-17T20:43:25.600347", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "uv run pytest packages/cogames/tests/ -v --benchmark-disable -x 2>&1 | tail -80", "exit_code": 0} -{"ts": "2026-01-17T20:43:32.723538", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "metta lint 2>&1 | tail -50", "exit_code": 0} -{"ts": "2026-01-17T20:43:39.020826", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "clang-format -i packages/mettagrid/cpp/src/mettagrid/handler/event_scheduler.cpp packages/mettagrid/cpp/include/mettagrid/handler/event_scheduler.hpp 2>&1", "exit_code": 0} -{"ts": "2026-01-17T20:43:56.094521", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "metta lint 2>&1 | tail -20", "exit_code": 0} -{"ts": "2026-01-17T20:44:06.781100", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "git status 2>&1", "exit_code": 0} -{"ts": "2026-01-17T20:44:12.315924", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "git add packages/mettagrid/cpp/include/mettagrid/handler/event.hpp packages/mettagrid/cpp/include/mettagrid/handler/event_scheduler.hpp packages/mettagrid/cpp/src/mettagrid/handler/event.cpp packages/mettagrid/cpp/src/mettagrid/handler/event_scheduler.cpp 2>&1", "exit_code": 0} -{"ts": "2026-01-17T20:44:15.758530", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "git diff --cached 2>&1", "exit_code": 0} -{"ts": "2026-01-17T20:44:51.602420", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "gt submit --no-interactive --force 2>&1", "exit_code": 0} -{"ts": "2026-01-17T20:48:44.685588", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "git checkout '01-16-feat_mettagrid_add_events_system_core_infrastructure_add_eventconfig_class_for_timestep-based_events_that_apply_mutations_to_filtered_objects._events_can_target_any_gridobject_and_include_-_timesteps_list_of_timesteps_when_event' 2>&1", "exit_code": 0} -{"ts": "2026-01-17T20:48:48.931521", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "gt sync --no-interactive 2>&1", "exit_code": 0} -{"ts": "2026-01-17T20:48:59.096066", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "gh pr view --json number,url,title 2>&1", "exit_code": 0} -{"ts": "2026-01-17T20:49:01.460002", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "HEAD_SHA=$(git rev-parse HEAD) && gh api repos/Metta-AI/metta/commits/$HEAD_SHA/check-runs --jq '.check_runs[] | \"\\(.name): \\(.conclusion // .status)\"' 2>&1", "exit_code": 0} -{"ts": "2026-01-17T20:49:37.985088", "tool": "Edit", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "file": "/Users/daveey/code/metta/packages/mettagrid/cpp/include/mettagrid/handler/event_scheduler.hpp", "success": true, "change": "#include \n#include \n#include #include \n#include \n#includ..."} -{"ts": "2026-01-17T20:49:46.769071", "tool": "Edit", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "file": "/Users/daveey/code/metta/packages/mettagrid/cpp/src/mettagrid/handler/event_scheduler.cpp", "success": true, "change": "std::vector EventScheduler::get_events_for... -> int EventScheduler::process_timestep(int timestep,..."} -{"ts": "2026-01-17T20:49:51.855690", "tool": "Edit", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "file": "/Users/daveey/code/metta/packages/mettagrid/cpp/src/mettagrid/handler/event_scheduler.cpp", "success": true, "change": "#include \"handler/event_scheduler.hpp\"\n\n#include <... -> #include \"handler/event_scheduler.hpp\"\n\n#include <..."} -{"ts": "2026-01-17T20:50:02.569298", "tool": "Edit", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "file": "/Users/daveey/code/metta/packages/mettagrid/cpp/include/mettagrid/handler/event.hpp", "success": true, "change": "/**\n * Event processes timestep-based effects thro... -> /**\n * Event processes timestep-based effects thro..."} -{"ts": "2026-01-17T21:01:36.809584", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "uv run pytest packages/mettagrid/tests/test_env_interface.py -v --benchmark-disable -p no:testmon 2>&1 | tail -40", "exit_code": 0} -{"ts": "2026-01-17T21:01:45.678898", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "uv run pytest packages/mettagrid/tests/test_env_interface.py -v --benchmark-disable -p no:testmon -p no:xdist 2>&1 | tail -40", "exit_code": 0} -{"ts": "2026-01-17T21:01:55.312425", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "uv run python -m pytest packages/mettagrid/tests/test_env_interface.py -v --benchmark-disable --no-testmon -n 0 2>&1 | tail -40", "exit_code": 0} -{"ts": "2026-01-17T21:02:05.122600", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "uv run python -m pytest packages/mettagrid/tests/test_actions.py -v --benchmark-disable --no-testmon -n 0 2>&1 | tail -40", "exit_code": 0} -{"ts": "2026-01-17T21:02:19.441454", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "uv run python -m pytest packages/mettagrid/tests/ -v --benchmark-disable --no-testmon -n 0 2>&1 | tail -30", "exit_code": 0} -{"ts": "2026-01-17T21:02:26.444322", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "metta lint 2>&1 | tail -30", "exit_code": 0} -{"ts": "2026-01-17T21:02:30.470850", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "git status 2>&1", "exit_code": 0} -{"ts": "2026-01-17T21:02:34.654947", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "git add packages/mettagrid/cpp/include/mettagrid/handler/event.hpp packages/mettagrid/cpp/include/mettagrid/handler/event_scheduler.hpp packages/mettagrid/cpp/src/mettagrid/handler/event_scheduler.cpp 2>&1", "exit_code": 0} -{"ts": "2026-01-17T21:02:38.688409", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "git diff --cached --stat 2>&1", "exit_code": 0} -{"ts": "2026-01-17T21:03:10.065417", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "gt submit --no-interactive --force 2>&1", "exit_code": 0} -{"ts": "2026-01-17T21:11:46.844301", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "git branch --show-current", "exit_code": 0} -{"ts": "2026-01-17T21:11:48.213224", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "gt state 2>/dev/null | head -150", "exit_code": 0} -{"ts": "2026-01-17T21:12:09.441945", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "git checkout '01-16-refactor_mettagrid_remove_clipper_system' 2>&1", "exit_code": 0} -{"ts": "2026-01-17T21:12:13.273416", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "gt sync --no-interactive 2>&1", "exit_code": 0} -{"ts": "2026-01-17T21:12:21.446704", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "gh pr view --json number,url,title 2>&1", "exit_code": 0} -{"ts": "2026-01-17T21:12:23.963329", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "HEAD_SHA=$(git rev-parse HEAD) && gh api repos/Metta-AI/metta/commits/$HEAD_SHA/check-runs --jq '.check_runs[] | \"\\(.name): \\(.conclusion // .status)\"' 2>&1", "exit_code": 0} -{"ts": "2026-01-17T21:12:43.592724", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "git checkout '01-16-feat_mettagrid_add_events_system_core_infrastructure_add_eventconfig_class_for_timestep-based_events_that_apply_mutations_to_filtered_objects._events_can_target_any_gridobject_and_include_-_timesteps_list_of_timesteps_when_event' 2>&1", "exit_code": 0} -{"ts": "2026-01-17T21:12:46.493298", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "HEAD_SHA=$(git rev-parse HEAD) && gh api repos/Metta-AI/metta/commits/$HEAD_SHA/check-runs --jq '.check_runs[] | \"\\(.name): \\(.conclusion // .status)\"' 2>&1", "exit_code": 0} -{"ts": "2026-01-17T21:12:58.793652", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "grep \"process_timestep\" packages/mettagrid/cpp/include/mettagrid/handler/event_scheduler.hpp 2>/dev/null", "exit_code": 0} -{"ts": "2026-01-17T21:12:58.815526", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "grep -A2 \"events:\" packages/mettagrid/python/src/mettagrid/config/mettagrid_config.py 2>/dev/null | head -5", "exit_code": 0} -{"ts": "2026-01-17T21:13:15.567444", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "git checkout '01-16-feat_mettagrid_add_objecttypefilter_for_filtering_by_object_type_add_objecttypefilter_that_checks_if_target_has_a_specific_tag._used_by_events_to_select_which_objects_to_apply_mutations_to._co-authored-by_claude_opus_4.5_noreply' 2>&1", "exit_code": 0} -{"ts": "2026-01-17T21:14:14.540026", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "git branch --show-current", "exit_code": 0} -{"ts": "2026-01-17T21:14:16.229156", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "gh pr view 4955 --json number,url,title,state", "exit_code": 0} -{"ts": "2026-01-17T21:14:25.126242", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "gt sync --no-interactive 2>&1 | head -50", "exit_code": 0} -{"ts": "2026-01-17T21:14:34.404644", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "HEAD_SHA=$(git rev-parse HEAD) && gh api repos/Metta-AI/metta/commits/$HEAD_SHA/check-runs --jq '.check_runs[] | \"\\(.name): \\(.conclusion // .status)\"' 2>&1 | head -20", "exit_code": 0} -{"ts": "2026-01-17T21:14:40.895437", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "git diff --name-only HEAD~1", "exit_code": 0} -{"ts": "2026-01-17T21:15:14.383238", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "uv run pytest packages/mettagrid/tests/test_object_type_filter.py -v 2>&1 | head -50", "exit_code": 0} -{"ts": "2026-01-17T21:16:02.173255", "tool": "Edit", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "file": "/Users/daveey/code/metta/packages/mettagrid/tests/test_object_type_filter.py", "success": true, "change": "#!/usr/bin/env python3\n\n\"\"\"Test ObjectTypeFilter c... -> #!/usr/bin/env python3\n\n\"\"\"Test ObjectTypeFilter c..."} -{"ts": "2026-01-17T21:16:07.594646", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "uv run pytest packages/mettagrid/tests/test_object_type_filter.py -v 2>&1 | tail -40", "exit_code": 0} -{"ts": "2026-01-17T21:16:12.494707", "tool": "Edit", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "file": "/Users/daveey/code/metta/packages/mettagrid/tests/test_object_type_filter.py", "success": true, "change": "import json\n\nimport pytest... -> import pytest..."} -{"ts": "2026-01-17T21:16:38.044737", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "metta pytest --changed 2>&1 | tail -30", "exit_code": 0} -{"ts": "2026-01-17T21:16:40.740791", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "metta lint 2>&1 | tail -30", "exit_code": 0} -{"ts": "2026-01-17T21:16:45.695165", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "clang-format -i packages/mettagrid/cpp/include/mettagrid/handler/handler_config.hpp", "exit_code": 0} -{"ts": "2026-01-17T21:17:04.807836", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "uv run pytest packages/mettagrid/tests/test_object_type_filter.py -v --no-testmon 2>&1 | tail -20", "exit_code": 0} -{"ts": "2026-01-17T21:17:07.455119", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "metta lint 2>&1 | tail -10", "exit_code": 0} -{"ts": "2026-01-17T21:17:11.706653", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "git add -A && git status", "exit_code": 0} -{"ts": "2026-01-17T21:17:16.454651", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "git restore --staged .memory/sessions/2026-01-17.session.jsonl packages/mettagrid/.memory/sessions/2026-01-17.session.jsonl uv.lock && git status", "exit_code": 0} -{"ts": "2026-01-17T21:17:24.449405", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "gt modify --no-interactive", "exit_code": 0} -{"ts": "2026-01-17T21:17:45.002255", "tool": "Edit", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "file": "/Users/daveey/code/metta/packages/mettagrid/cpp/include/mettagrid/handler/handler_config.hpp", "success": true, "change": "// Variant type for all filter configs\n<<<<<<< HEA... -> // Variant type for all filter configs\nusing Filte..."} diff --git a/packages/cogames/src/cogames/cogs_vs_clips/mission.py b/packages/cogames/src/cogames/cogs_vs_clips/mission.py index 0cd5c6a1f56..d2460b04629 100644 --- a/packages/cogames/src/cogames/cogs_vs_clips/mission.py +++ b/packages/cogames/src/cogames/cogs_vs_clips/mission.py @@ -34,6 +34,8 @@ TransferActionConfig, VibeTransfer, ) +from mettagrid.config.event_config import EventConfig, periodic +from mettagrid.config.filter_config import BelongsToCollective, NearCollective, isNeutral from mettagrid.config.mettagrid_config import ( AgentConfig, AgentRewards, @@ -44,6 +46,7 @@ MettaGridConfig, ResourceLimitsConfig, ) +from mettagrid.config.mutation_config import AlignToCollective, RemoveAlignment from mettagrid.config.vibes import Vibe from mettagrid.map_builder.map_builder import AnyMapBuilderConfig @@ -302,6 +305,12 @@ class CogsGuardMission(Config): collective_initial_silicon: int = Field(default=10) collective_initial_heart: int = Field(default=5) + # Clips Behavior + clips_scramble_interval: int = Field(default=50) + clips_scramble_radius: int = Field(default=10) + clips_align_interval: int = Field(default=100) + clips_align_radius: int = Field(default=10) + # Station configs wall: CvCWallConfig = Field(default_factory=CvCWallConfig) @@ -371,6 +380,28 @@ def make_env(self) -> MettaGridConfig: ), "clips": CollectiveConfig(), }, + events={ + "cogs_to_neutral": EventConfig( + name="cogs_to_neutral", + timesteps=periodic( + start=self.clips_scramble_interval, + period=self.clips_scramble_interval, + end=self.max_steps, + ), + filters=[NearCollective("clips", radius=self.clips_scramble_radius), BelongsToCollective("cogs")], + mutations=[RemoveAlignment()], + ), + "neutral_to_clips": EventConfig( + name="neutral_to_clips", + timesteps=periodic( + start=self.clips_align_interval, + period=self.clips_align_interval, + end=self.max_steps, + ), + filters=[NearCollective("clips", radius=self.clips_align_radius), isNeutral()], + mutations=[AlignToCollective("clips")], + ), + }, ) env = MettaGridConfig(game=game) diff --git a/packages/cogames/tests/test_cogsguard_events.py b/packages/cogames/tests/test_cogsguard_events.py new file mode 100644 index 00000000000..e1566229133 --- /dev/null +++ b/packages/cogames/tests/test_cogsguard_events.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 + +"""Tests for CogsGuardMission clip takeover events. + +These tests verify that CogsGuardMission correctly configures the events +for territorial expansion using NearCollective and alignment mutations. +""" + +import pytest + +from cogames.cogs_vs_clips.missions import make_cogsguard_mission + + +class TestCogsGuardMissionEvents: + """Tests for CogsGuardMission with clip takeover events.""" + + def test_cogsguard_mission_has_events(self): + """Test that CogsGuardMission creates events correctly.""" + mission = make_cogsguard_mission(num_agents=2, max_steps=500) + env = mission.make_env() + + # Verify events are configured + assert len(env.game.events) == 2 + assert "cogs_to_neutral" in env.game.events + assert "neutral_to_clips" in env.game.events + + # Verify cogs_to_neutral event uses NearCollective and BelongsToCollective + e1 = env.game.events["cogs_to_neutral"] + assert len(e1.filters) == 2 + assert e1.filters[0].filter_type == "near_collective" + assert e1.filters[0].collective == "clips" + assert e1.filters[1].filter_type == "collective" + assert e1.filters[1].collective == "cogs" + assert e1.mutations[0].mutation_type == "alignment" + + # Verify neutral_to_clips event + e2 = env.game.events["neutral_to_clips"] + assert e2.filters[0].filter_type == "near_collective" + assert e2.filters[0].collective == "clips" + assert e2.mutations[0].mutation_type == "align_to_collective" + assert e2.mutations[0].collective == "clips" + + def test_cogsguard_mission_event_timesteps(self): + """Test that event timesteps are calculated correctly based on max_steps.""" + mission = make_cogsguard_mission(num_agents=2, max_steps=300) + env = mission.make_env() + + # cogs_to_neutral fires at clips_scramble_interval (default 50): 50, 100, 150, 200, 250, 300 + expected_cogs_to_neutral = [50, 100, 150, 200, 250, 300] + # neutral_to_clips fires at clips_align_interval (default 100): 100, 200, 300 + expected_neutral_to_clips = [100, 200, 300] + assert env.game.events["cogs_to_neutral"].timesteps == expected_cogs_to_neutral + assert env.game.events["neutral_to_clips"].timesteps == expected_neutral_to_clips + + def test_cogsguard_mission_collectives(self): + """Test that CogsGuardMission has cogs and clips collectives (no neutral).""" + mission = make_cogsguard_mission(num_agents=2, max_steps=500) + env = mission.make_env() + + # Should have cogs and clips collectives, but NOT neutral + assert "cogs" in env.game.collectives + assert "clips" in env.game.collectives + assert "neutral" not in env.game.collectives + + def test_cogsguard_event_serialization_roundtrip(self): + """Test that mission events survive serialization/deserialization.""" + from mettagrid.config.mettagrid_config import MettaGridConfig + + mission = make_cogsguard_mission(num_agents=2, max_steps=200) + env = mission.make_env() + + # Serialize and deserialize + json_str = env.model_dump_json() + restored = MettaGridConfig.model_validate_json(json_str) + + # Verify events are preserved + assert len(restored.game.events) == 2 + + # Verify cogs_to_neutral event (scramble_interval=50) + e1 = restored.game.events["cogs_to_neutral"] + assert e1.timesteps == [50, 100, 150, 200] + assert len(e1.filters) == 2 + assert e1.filters[0].filter_type == "near_collective" + assert e1.filters[0].collective == "clips" + assert e1.filters[1].filter_type == "collective" + assert e1.filters[1].collective == "cogs" + + # Verify neutral_to_clips event (align_interval=100) + e2 = restored.game.events["neutral_to_clips"] + assert e2.timesteps == [100, 200] + assert e2.filters[0].filter_type == "near_collective" + assert e2.filters[0].collective == "clips" + assert e2.filters[0].radius == 10 # default clips_align_radius + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/packages/mettagrid/.memory/sessions/2026-01-17.session.jsonl b/packages/mettagrid/.memory/sessions/2026-01-17.session.jsonl deleted file mode 100644 index edd5951631b..00000000000 --- a/packages/mettagrid/.memory/sessions/2026-01-17.session.jsonl +++ /dev/null @@ -1,2 +0,0 @@ -{"ts": "2026-01-17T20:50:34.811575", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "uv run pytest packages/mettagrid/tests/ packages/cogames/tests/ -v --benchmark-disable -x 2>&1 | tail -50", "exit_code": 0} -{"ts": "2026-01-17T21:00:50.082446", "tool": "Bash", "session_id": "72778c8d-f81e-4bbb-9a04-d13c5bd76688", "command": "tail -80 /private/tmp/claude/-Users-daveey-code-metta/tasks/b3cc36a.output 2>/dev/null", "exit_code": 0} diff --git a/packages/mettagrid/cpp/include/mettagrid/handler/filters/filter.hpp b/packages/mettagrid/cpp/include/mettagrid/handler/filters/filter.hpp index 676d848ee20..8e827f3135e 100644 --- a/packages/mettagrid/cpp/include/mettagrid/handler/filters/filter.hpp +++ b/packages/mettagrid/cpp/include/mettagrid/handler/filters/filter.hpp @@ -190,6 +190,51 @@ class CollectiveFilter : public Filter { CollectiveFilterConfig _config; }; +/** + * NearCollectiveFilter: Check if entity is NOT aligned to a collective but near it + * Passes if target is NOT aligned to the specified collective AND is within radius + * of an object that IS aligned to that collective. + * This is useful for "expansion" mechanics where a collective absorbs nearby neutral/enemy objects. + */ +class NearCollectiveFilter : public Filter { +public: + // Callback type for checking if within radius of a collective + // Takes: GridObject* target, collective name, radius + // Returns: true if target is within radius of any object aligned to that collective + using NearCallback = std::function; + + explicit NearCollectiveFilter(const NearCollectiveFilterConfig& config) : _config(config) {} + + // Set the callback for checking proximity (called during filter setup) + void set_near_callback(NearCallback callback) { _is_near_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 is NOT aligned to the specified collective + Collective* coll = grid_obj->getCollective(); + if (coll != nullptr && coll->name == _config.collective) { + // Target is aligned to this collective, so filter fails + return false; + } + + // Check if target is within radius of the collective + if (_is_near_collective) { + return _is_near_collective(grid_obj, _config.collective, _config.radius); + } + + // If no near callback is set, filter fails + return false; + } + +private: + NearCollectiveFilterConfig _config; + NearCallback _is_near_collective = nullptr; +}; } // namespace mettagrid #endif // PACKAGES_METTAGRID_CPP_INCLUDE_METTAGRID_HANDLER_FILTERS_FILTER_HPP_ diff --git a/packages/mettagrid/cpp/include/mettagrid/handler/handler_bindings.hpp b/packages/mettagrid/cpp/include/mettagrid/handler/handler_bindings.hpp index 55b9c80f2de..92219f9e3a3 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, "NearCollectiveFilterConfig") + .def(py::init<>()) + .def_readwrite("entity", &NearCollectiveFilterConfig::entity) + .def_readwrite("collective", &NearCollectiveFilterConfig::collective) + .def_readwrite("radius", &NearCollectiveFilterConfig::radius); + // 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_near_collective_filter", + [](HandlerConfig& self, const NearCollectiveFilterConfig& 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/include/mettagrid/handler/handler_config.hpp b/packages/mettagrid/cpp/include/mettagrid/handler/handler_config.hpp index af26413ff1f..926945448ff 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 NearCollectiveFilterConfig { + EntityRef entity = EntityRef::target; + std::string collective; // Collective to check proximity to + int radius = 1; // Radius (chebyshev distance) to check +}; + // Variant type for all filter configs using FilterConfig = std::variant; + CollectiveFilterConfig, + NearCollectiveFilterConfig>; // ============================================================================ // Mutation Configs diff --git a/packages/mettagrid/cpp/src/mettagrid/handler/event.cpp b/packages/mettagrid/cpp/src/mettagrid/handler/event.cpp index d34c87c4cb1..8662c9d95f0 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..5d2acbbbc6e 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/event_config.py b/packages/mettagrid/python/src/mettagrid/config/event_config.py index b7d8232a3d5..c21cc545f37 100644 --- a/packages/mettagrid/python/src/mettagrid/config/event_config.py +++ b/packages/mettagrid/python/src/mettagrid/config/event_config.py @@ -69,7 +69,7 @@ class EventConfig(Config): Note: Filters target GridObjects, not agents. Use ObjectTypeFilter to select by object type, CollectiveFilter to select by collective membership, - or BoundaryFilter to select objects adjacent to specific collectives. + or NearCollectiveFilter to select objects near specific collectives. """ name: str = Field(description="Unique name for this event") diff --git a/packages/mettagrid/python/src/mettagrid/config/filter_config.py b/packages/mettagrid/python/src/mettagrid/config/filter_config.py index 7483aa573ac..0d511db6843 100644 --- a/packages/mettagrid/python/src/mettagrid/config/filter_config.py +++ b/packages/mettagrid/python/src/mettagrid/config/filter_config.py @@ -119,6 +119,28 @@ class CollectiveFilter(Filter): collective: str = Field(description="Name of collective the object must belong to") +class NearCollectiveFilter(Filter): + """Filter that checks if target is NOT aligned to a collective but near it. + + This is useful for "expansion" mechanics where a collective absorbs nearby + neutral or enemy objects. The filter passes if: + - Target is NOT aligned to the specified collective, AND + - Target is within the specified radius of an object that IS aligned to that collective + + Example: + NearCollectiveFilter(collective="clips", radius=2) + # Affects objects not aligned to clips that are within 2 tiles of clips territory + """ + + filter_type: Literal["near_collective"] = "near_collective" + target: HandlerTarget = Field( + default=HandlerTarget.TARGET, + description="Entity to check the filter against", + ) + collective: str = Field(description="Collective to check proximity to (target must NOT belong to this)") + radius: int = Field(default=1, description="Chebyshev distance (square radius) to check for collective presence") + + AnyFilter = Annotated[ Union[ Annotated[VibeFilter, Tag("vibe")], @@ -126,6 +148,7 @@ class CollectiveFilter(Filter): Annotated[AlignmentFilter, Tag("alignment")], Annotated[ObjectTypeFilter, Tag("object_type")], Annotated[CollectiveFilter, Tag("collective")], + Annotated[NearCollectiveFilter, Tag("near_collective")], ], Discriminator("filter_type"), ] @@ -192,6 +215,22 @@ def BelongsToCollective(collective: str) -> CollectiveFilter: return CollectiveFilter(collective=collective) +# Helper for NearCollectiveFilter +def NearCollective(collective: str, radius: int = 1) -> NearCollectiveFilter: + """Filter: target is NOT aligned to collective but within radius of it. + + This is useful for expansion mechanics where a collective absorbs nearby objects. + The filter passes if: + - Target is NOT aligned to the specified collective, AND + - Target is within radius tiles of an object aligned to that collective + + Args: + collective: Name of the collective to check proximity to + radius: Chebyshev distance (square radius) to check for collective presence + """ + return NearCollectiveFilter(collective=collective, radius=radius) + + # Re-export all filter-related types __all__ = [ # Enums @@ -205,6 +244,7 @@ def BelongsToCollective(collective: str) -> CollectiveFilter: "AlignmentFilter", "ObjectTypeFilter", "CollectiveFilter", + "NearCollectiveFilter", "AnyFilter", # Filter helpers "isAligned", @@ -218,4 +258,5 @@ def BelongsToCollective(collective: str) -> CollectiveFilter: "TargetCollectiveHas", "HasTag", "BelongsToCollective", + "NearCollective", ] 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..e40fb657ce5 100644 --- a/packages/mettagrid/python/src/mettagrid/config/mettagrid_c_config.py +++ b/packages/mettagrid/python/src/mettagrid/config/mettagrid_c_config.py @@ -29,6 +29,7 @@ from mettagrid.mettagrid_c import InventoryConfig as CppInventoryConfig from mettagrid.mettagrid_c import LimitDef as CppLimitDef from mettagrid.mettagrid_c import MoveActionConfig as CppMoveActionConfig +from mettagrid.mettagrid_c import NearCollectiveFilterConfig as CppNearCollectiveFilterConfig from mettagrid.mettagrid_c import ObjectTypeFilterConfig as CppObjectTypeFilterConfig from mettagrid.mettagrid_c import Protocol as CppProtocol from mettagrid.mettagrid_c import ResourceDeltaMutationConfig as CppResourceDeltaMutationConfig @@ -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 == "near_collective": + # Near collective filter checks if target is NOT aligned to collective but near it + cpp_filter = CppNearCollectiveFilterConfig() + cpp_filter.entity = convert_entity_ref(filter_config.target) + cpp_filter.collective = filter_config.collective + cpp_filter.radius = filter_config.radius + cpp_handler.add_near_collective_filter(cpp_filter) + # Convert mutations using shared utility convert_mutations( handler.mutations, diff --git a/packages/mettagrid/tests/test_clip_takeover_events.py b/packages/mettagrid/tests/test_clip_takeover_events.py new file mode 100644 index 00000000000..b128b31f037 --- /dev/null +++ b/packages/mettagrid/tests/test_clip_takeover_events.py @@ -0,0 +1,214 @@ +#!/usr/bin/env python3 + +"""Tests for NearCollective filter and related event configurations. + +These tests verify: +1. NearCollective filter configuration and serialization +2. Event configurations using NearCollective with AlignToCollective +3. Serialization/deserialization of events with these filters +""" + +import pytest + +from mettagrid.config.event_config import EventConfig, periodic +from mettagrid.config.filter_config import NearCollective +from mettagrid.config.mettagrid_config import ( + CollectiveConfig, + MettaGridConfig, +) +from mettagrid.config.mutation_config import AlignToCollective, RemoveAlignment + + +class TestNearCollectiveFilter: + """Tests for NearCollective filter configuration.""" + + def test_near_collective_filter_creation(self): + """Test creating NearCollectiveFilter directly.""" + from mettagrid.config.filter_config import NearCollectiveFilter + + f = NearCollectiveFilter(collective="clips", radius=2) + assert f.filter_type == "near_collective" + assert f.collective == "clips" + assert f.radius == 2 + + def test_near_collective_helper(self): + """Test NearCollective helper function.""" + from mettagrid.config.filter_config import NearCollectiveFilter + + f = NearCollective("cogs", radius=3) + assert isinstance(f, NearCollectiveFilter) + assert f.filter_type == "near_collective" + assert f.collective == "cogs" + assert f.radius == 3 + + def test_near_collective_default_radius(self): + """Test NearCollective with default radius.""" + f = NearCollective("team_a") + assert f.radius == 1 + + def test_near_collective_serialization(self): + """Test NearCollective filter serialization.""" + from mettagrid.config.filter_config import NearCollectiveFilter + + f = NearCollectiveFilter(collective="team_red", radius=5) + data = f.model_dump() + assert data["filter_type"] == "near_collective" + assert data["collective"] == "team_red" + assert data["radius"] == 5 + + def test_near_collective_deserialization(self): + """Test NearCollective filter deserialization.""" + from mettagrid.config.filter_config import NearCollectiveFilter + + f = NearCollectiveFilter(collective="team_blue", radius=3) + json_str = f.model_dump_json() + restored = NearCollectiveFilter.model_validate_json(json_str) + assert restored.collective == "team_blue" + assert restored.radius == 3 + + +class TestRemoveAlignmentMutation: + """Tests for RemoveAlignment mutation configuration.""" + + def test_remove_alignment_creation(self): + """Test creating RemoveAlignment mutation.""" + m = RemoveAlignment() + assert m.mutation_type == "alignment" + assert m.target == "target" + + def test_remove_alignment_serialization(self): + """Test RemoveAlignment mutation serialization.""" + m = RemoveAlignment() + data = m.model_dump() + assert data["mutation_type"] == "alignment" + assert data["align_to"] == "none" + + +class TestTakeoverEventConfigs: + """Tests for event configurations used in territorial takeover mechanics.""" + + def test_event_config_with_near_collective_and_align(self): + """Test EventConfig with NearCollective filter and AlignToCollective mutation.""" + event = EventConfig( + name="expand_territory", + timesteps=periodic(start=100, period=100, end=500), + filters=[NearCollective("team_b", radius=2)], + mutations=[AlignToCollective("team_b")], + ) + assert event.name == "expand_territory" + assert event.timesteps == [100, 200, 300, 400, 500] + assert len(event.filters) == 1 + assert event.filters[0].filter_type == "near_collective" + assert event.filters[0].collective == "team_b" + assert event.filters[0].radius == 2 + assert len(event.mutations) == 1 + assert event.mutations[0].mutation_type == "align_to_collective" + assert event.mutations[0].collective == "team_b" + + def test_game_config_with_takeover_events(self): + """Test GameConfig with territorial takeover events configured.""" + cfg = MettaGridConfig.EmptyRoom(num_agents=1, with_walls=True) + cfg.game.collectives = { + "team_a": CollectiveConfig(), + "team_b": CollectiveConfig(), + } + cfg.game.events = { + "expand_territory": EventConfig( + name="expand_territory", + timesteps=periodic(start=100, period=100, end=500), + filters=[NearCollective("team_b", radius=1)], + mutations=[AlignToCollective("team_b")], + ), + } + assert len(cfg.game.events) == 1 + assert "expand_territory" in cfg.game.events + + def test_events_serialization_roundtrip(self): + """Test that takeover events survive serialization/deserialization.""" + cfg = MettaGridConfig.EmptyRoom(num_agents=1, with_walls=True) + cfg.game.collectives = { + "attackers": CollectiveConfig(), + "defenders": CollectiveConfig(), + } + cfg.game.events = { + "capture_territory": EventConfig( + name="capture_territory", + timesteps=[100, 200], + filters=[NearCollective("attackers", radius=1)], + mutations=[AlignToCollective("attackers")], + ), + } + + # Serialize and deserialize + json_str = cfg.model_dump_json() + restored = MettaGridConfig.model_validate_json(json_str) + + # Verify events are preserved + assert len(restored.game.events) == 1 + + # Verify capture_territory event + e2 = restored.game.events["capture_territory"] + assert e2.timesteps == [100, 200] + assert len(e2.filters) == 1 + assert e2.filters[0].filter_type == "near_collective" + assert e2.filters[0].collective == "attackers" + assert e2.filters[0].radius == 1 + assert len(e2.mutations) == 1 + assert e2.mutations[0].mutation_type == "align_to_collective" + assert e2.mutations[0].collective == "attackers" + + +class TestFilterPolymorphismWithNearCollective: + """Tests for filter type polymorphism including NearCollective.""" + + def test_mixed_filters_with_near_collective(self): + """Test serialization of events with mixed filter types including NearCollective.""" + from mettagrid.config.filter_config import ( + BelongsToCollective, + HasTag, + ) + + event = EventConfig( + name="mixed_filters", + timesteps=[100], + filters=[ + HasTag("building"), + BelongsToCollective("team_a"), + NearCollective("team_b", radius=2), + ], + mutations=[], + ) + data = event.model_dump() + assert len(data["filters"]) == 3 + assert data["filters"][0]["filter_type"] == "object_type" + assert data["filters"][1]["filter_type"] == "collective" + assert data["filters"][2]["filter_type"] == "near_collective" + + def test_mixed_filters_deserialization(self): + """Test deserialization restores correct filter types including NearCollective.""" + from mettagrid.config.filter_config import ( + CollectiveFilter, + NearCollectiveFilter, + ObjectTypeFilter, + ) + + event = EventConfig( + name="mixed_filters", + timesteps=[100], + filters=[ + ObjectTypeFilter(tag="junction"), + CollectiveFilter(collective="cogs"), + NearCollectiveFilter(collective="clips", radius=1), + ], + mutations=[], + ) + json_str = event.model_dump_json() + restored = EventConfig.model_validate_json(json_str) + assert len(restored.filters) == 3 + assert isinstance(restored.filters[0], ObjectTypeFilter) + assert isinstance(restored.filters[1], CollectiveFilter) + assert isinstance(restored.filters[2], NearCollectiveFilter) + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/packages/mettagrid/tests/test_events_config.py b/packages/mettagrid/tests/test_events_config.py index 29ef5f7becc..66642b4528d 100644 --- a/packages/mettagrid/tests/test_events_config.py +++ b/packages/mettagrid/tests/test_events_config.py @@ -4,11 +4,11 @@ from mettagrid.config.event_config import EventConfig, once, periodic from mettagrid.config.filter_config import ( - AtBoundary, BelongsToCollective, - BoundaryFilter, CollectiveFilter, HasTag, + NearCollective, + NearCollectiveFilter, ObjectTypeFilter, ) from mettagrid.config.mettagrid_config import GameConfig, MettaGridConfig @@ -99,31 +99,36 @@ def test_collective_filter_serialization(self): assert data["collective"] == "team_red" -class TestBoundaryFilter: - """Tests for BoundaryFilter configuration.""" +class TestNearCollectiveFilter: + """Tests for NearCollectiveFilter 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_near_collective_filter_creation(self): + """Test creating NearCollectiveFilter directly.""" + f = NearCollectiveFilter(collective="clips", radius=2) + assert f.filter_type == "near_collective" + assert f.collective == "clips" + assert f.radius == 2 + + def test_near_collective_helper(self): + """Test NearCollective helper function.""" + f = NearCollective("cogs", radius=3) + assert isinstance(f, NearCollectiveFilter) + assert f.filter_type == "near_collective" + assert f.collective == "cogs" + assert f.radius == 3 - def test_at_boundary_helper(self): - """Test AtBoundary helper function.""" - f = AtBoundary("allies", "enemies") - assert isinstance(f, BoundaryFilter) - assert f.filter_type == "boundary" - assert f.collective_a == "allies" - assert f.collective_b == "enemies" + def test_near_collective_default_radius(self): + """Test NearCollective with default radius.""" + f = NearCollective("team_a") + assert f.radius == 1 - def test_boundary_filter_serialization(self): - """Test BoundaryFilter serialization.""" - f = BoundaryFilter(collective_a="team_a", collective_b="team_b") + def test_near_collective_filter_serialization(self): + """Test NearCollectiveFilter serialization.""" + f = NearCollectiveFilter(collective="team_a", radius=2) data = f.model_dump() - assert data["filter_type"] == "boundary" - assert data["collective_a"] == "team_a" - assert data["collective_b"] == "team_b" + assert data["filter_type"] == "near_collective" + assert data["collective"] == "team_a" + assert data["radius"] == 2 class TestAlignToCollectiveMutation: @@ -229,16 +234,16 @@ def test_event_config_with_once(self): def test_event_config_serialization(self): """Test EventConfig serialization.""" event = EventConfig( - name="boundary_event", + name="proximity_event", timesteps=[50, 100], - filters=[AtBoundary("cogs", "clips")], - mutations=[LogStat("boundary.touched")], + filters=[NearCollective("cogs", radius=2)], + mutations=[LogStat("proximity.touched")], ) data = event.model_dump() - assert data["name"] == "boundary_event" + assert data["name"] == "proximity_event" assert data["timesteps"] == [50, 100] assert len(data["filters"]) == 1 - assert data["filters"][0]["filter_type"] == "boundary" + assert data["filters"][0]["filter_type"] == "near_collective" assert len(data["mutations"]) == 1 assert data["mutations"][0]["mutation_type"] == "stats" @@ -272,17 +277,17 @@ def test_game_config_with_events(self): filters=[], mutations=[LogStat("tick.marker")], ), - "boundary_check": EventConfig( - name="boundary_check", + "proximity_check": EventConfig( + name="proximity_check", timesteps=once(250), - filters=[AtBoundary("team_a", "team_b")], + filters=[NearCollective("team_a", radius=2)], mutations=[AlignToCollective("neutral")], ), }, ) assert len(config.events) == 2 assert config.events["periodic_stat"].name == "periodic_stat" - assert config.events["boundary_check"].name == "boundary_check" + assert config.events["proximity_check"].name == "proximity_check" def test_game_config_events_serialization(self): """Test GameConfig events serialization.""" @@ -331,7 +336,7 @@ def test_mixed_filters_serialization(self): filters=[ HasTag("charger"), BelongsToCollective("cogs"), - AtBoundary("team_a", "team_b"), + NearCollective("team_b", radius=2), ], mutations=[], ) @@ -339,7 +344,7 @@ def test_mixed_filters_serialization(self): assert len(data["filters"]) == 3 assert data["filters"][0]["filter_type"] == "object_type" assert data["filters"][1]["filter_type"] == "collective" - assert data["filters"][2]["filter_type"] == "boundary" + assert data["filters"][2]["filter_type"] == "near_collective" def test_mixed_filters_deserialization(self): """Test deserialization restores correct filter types.""" @@ -349,7 +354,7 @@ def test_mixed_filters_deserialization(self): filters=[ HasTag("charger"), BelongsToCollective("cogs"), - AtBoundary("team_a", "team_b"), + NearCollective("team_a", radius=1), ], mutations=[], ) @@ -358,7 +363,7 @@ def test_mixed_filters_deserialization(self): assert len(restored.filters) == 3 assert isinstance(restored.filters[0], ObjectTypeFilter) assert isinstance(restored.filters[1], CollectiveFilter) - assert isinstance(restored.filters[2], BoundaryFilter) + assert isinstance(restored.filters[2], NearCollectiveFilter) class TestMutationPolymorphism: diff --git a/uv.lock b/uv.lock index 609aea039f8..e64c4fc743a 100644 --- a/uv.lock +++ b/uv.lock @@ -620,6 +620,7 @@ requires-dist = [ name = "cogames" source = { editable = "packages/cogames" } dependencies = [ + { name = "alo" }, { name = "einops" }, { name = "fastapi" }, { name = "heavyball" }, @@ -637,6 +638,7 @@ dependencies = [ [package.metadata] requires-dist = [ + { name = "alo", editable = "packages/alo" }, { name = "einops", specifier = ">=0.8.0" }, { name = "fastapi", specifier = ">=0.115.0" }, { name = "heavyball", specifier = ">=2.0.0" },