mettagrid 0.2__cp311-none-macosx_11_0_arm64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of mettagrid might be problematic. Click here for more details.

Files changed (137) hide show
  1. metta/mettagrid/__init__.py +61 -0
  2. metta/mettagrid/action_handler.hpp +137 -0
  3. metta/mettagrid/actions/__init__.py +0 -0
  4. metta/mettagrid/actions/attack.hpp +235 -0
  5. metta/mettagrid/actions/change_color.hpp +40 -0
  6. metta/mettagrid/actions/change_glyph.hpp +54 -0
  7. metta/mettagrid/actions/get_output.hpp +88 -0
  8. metta/mettagrid/actions/move.hpp +80 -0
  9. metta/mettagrid/actions/noop.hpp +24 -0
  10. metta/mettagrid/actions/orientation.hpp +128 -0
  11. metta/mettagrid/actions/place_box.hpp +41 -0
  12. metta/mettagrid/actions/put_recipe_items.hpp +56 -0
  13. metta/mettagrid/actions/rotate.hpp +49 -0
  14. metta/mettagrid/actions/swap.hpp +42 -0
  15. metta/mettagrid/builder/__init__.py +7 -0
  16. metta/mettagrid/builder/building.py +52 -0
  17. metta/mettagrid/builder/empty_converters.py +85 -0
  18. metta/mettagrid/builder/envs.py +207 -0
  19. metta/mettagrid/char_encoder.py +55 -0
  20. metta/mettagrid/config.py +106 -0
  21. metta/mettagrid/core.py +291 -0
  22. metta/mettagrid/episode_stats_db.py +157 -0
  23. metta/mettagrid/event.hpp +87 -0
  24. metta/mettagrid/grid.hpp +221 -0
  25. metta/mettagrid/grid_object.hpp +84 -0
  26. metta/mettagrid/grid_object_formatter.py +95 -0
  27. metta/mettagrid/gym_env.py +127 -0
  28. metta/mettagrid/gym_wrapper.py +63 -0
  29. metta/mettagrid/hash.hpp +130 -0
  30. metta/mettagrid/map_builder/__init__.py +10 -0
  31. metta/mettagrid/map_builder/ascii.py +38 -0
  32. metta/mettagrid/map_builder/map_builder.py +147 -0
  33. metta/mettagrid/map_builder/maze.py +122 -0
  34. metta/mettagrid/map_builder/perimeter_incontext.py +101 -0
  35. metta/mettagrid/map_builder/random.py +95 -0
  36. metta/mettagrid/map_builder/utils.py +94 -0
  37. metta/mettagrid/mapgen/__init__.py +0 -0
  38. metta/mettagrid/mapgen/load.py +35 -0
  39. metta/mettagrid/mapgen/mapgen.py +322 -0
  40. metta/mettagrid/mapgen/mapgen_ascii.py +51 -0
  41. metta/mettagrid/mapgen/random/float.py +99 -0
  42. metta/mettagrid/mapgen/random/int.py +50 -0
  43. metta/mettagrid/mapgen/scene.py +323 -0
  44. metta/mettagrid/mapgen/scenes/ascii.py +41 -0
  45. metta/mettagrid/mapgen/scenes/auto.py +149 -0
  46. metta/mettagrid/mapgen/scenes/bsp.py +417 -0
  47. metta/mettagrid/mapgen/scenes/convchain.py +180 -0
  48. metta/mettagrid/mapgen/scenes/copy_grid.py +36 -0
  49. metta/mettagrid/mapgen/scenes/grid_altars.py +118 -0
  50. metta/mettagrid/mapgen/scenes/inline_ascii.py +42 -0
  51. metta/mettagrid/mapgen/scenes/layout.py +31 -0
  52. metta/mettagrid/mapgen/scenes/make_connected.py +163 -0
  53. metta/mettagrid/mapgen/scenes/maze.py +212 -0
  54. metta/mettagrid/mapgen/scenes/mean_distance.py +48 -0
  55. metta/mettagrid/mapgen/scenes/mirror.py +120 -0
  56. metta/mettagrid/mapgen/scenes/multi_left_and_right.py +117 -0
  57. metta/mettagrid/mapgen/scenes/nop.py +15 -0
  58. metta/mettagrid/mapgen/scenes/radial_maze.py +50 -0
  59. metta/mettagrid/mapgen/scenes/random.py +65 -0
  60. metta/mettagrid/mapgen/scenes/random_dcss_scene.py +48 -0
  61. metta/mettagrid/mapgen/scenes/random_objects.py +35 -0
  62. metta/mettagrid/mapgen/scenes/random_scene.py +31 -0
  63. metta/mettagrid/mapgen/scenes/random_yaml_scene.py +32 -0
  64. metta/mettagrid/mapgen/scenes/remove_agents.py +26 -0
  65. metta/mettagrid/mapgen/scenes/room_grid.py +77 -0
  66. metta/mettagrid/mapgen/scenes/spiral.py +104 -0
  67. metta/mettagrid/mapgen/scenes/transplant_scene.py +42 -0
  68. metta/mettagrid/mapgen/scenes/varied_terrain.py +376 -0
  69. metta/mettagrid/mapgen/scenes/wfc.py +269 -0
  70. metta/mettagrid/mapgen/scenes/yaml.py +22 -0
  71. metta/mettagrid/mapgen/tools/dcss_import.py +123 -0
  72. metta/mettagrid/mapgen/tools/gen.py +94 -0
  73. metta/mettagrid/mapgen/tools/gen_scene.py +51 -0
  74. metta/mettagrid/mapgen/tools/view.py +25 -0
  75. metta/mettagrid/mapgen/types.py +81 -0
  76. metta/mettagrid/mapgen/utils/ascii_grid.py +55 -0
  77. metta/mettagrid/mapgen/utils/draw.py +32 -0
  78. metta/mettagrid/mapgen/utils/make_scene_config.py +36 -0
  79. metta/mettagrid/mapgen/utils/pattern.py +181 -0
  80. metta/mettagrid/mapgen/utils/s3utils.py +44 -0
  81. metta/mettagrid/mapgen/utils/show.py +21 -0
  82. metta/mettagrid/mapgen/utils/storable_map.py +117 -0
  83. metta/mettagrid/mapgen/utils/storable_map_index.py +92 -0
  84. metta/mettagrid/mapgen/utils/thumbnail.py +360 -0
  85. metta/mettagrid/mettagrid_c.cpp +973 -0
  86. metta/mettagrid/mettagrid_c.hpp +179 -0
  87. metta/mettagrid/mettagrid_c.pyi +230 -0
  88. metta/mettagrid/mettagrid_c.so +0 -0
  89. metta/mettagrid/mettagrid_c_config.py +256 -0
  90. metta/mettagrid/mettagrid_config.hpp +122 -0
  91. metta/mettagrid/mettagrid_config.py +214 -0
  92. metta/mettagrid/mettagrid_env.py +265 -0
  93. metta/mettagrid/objects/__init__.py +0 -0
  94. metta/mettagrid/objects/agent.hpp +251 -0
  95. metta/mettagrid/objects/agent_config.hpp +99 -0
  96. metta/mettagrid/objects/box.hpp +51 -0
  97. metta/mettagrid/objects/constants.hpp +106 -0
  98. metta/mettagrid/objects/converter.hpp +218 -0
  99. metta/mettagrid/objects/converter_config.hpp +91 -0
  100. metta/mettagrid/objects/has_inventory.hpp +39 -0
  101. metta/mettagrid/objects/production_handler.hpp +40 -0
  102. metta/mettagrid/objects/wall.hpp +58 -0
  103. metta/mettagrid/observation_encoder.hpp +98 -0
  104. metta/mettagrid/packed_coordinate.hpp +173 -0
  105. metta/mettagrid/pettingzoo_env.py +213 -0
  106. metta/mettagrid/profiling/__init__.py +1 -0
  107. metta/mettagrid/profiling/memory_monitor.py +242 -0
  108. metta/mettagrid/profiling/stopwatch.py +711 -0
  109. metta/mettagrid/profiling/system_monitor.py +355 -0
  110. metta/mettagrid/puffer_base.py +143 -0
  111. metta/mettagrid/py.typed +0 -0
  112. metta/mettagrid/renderer/hermes.cpp +1242 -0
  113. metta/mettagrid/renderer/hermes.hpp +50 -0
  114. metta/mettagrid/renderer/hermes.py +13 -0
  115. metta/mettagrid/renderer/miniscope.py +138 -0
  116. metta/mettagrid/renderer/nethack.py +101 -0
  117. metta/mettagrid/replay_writer.py +136 -0
  118. metta/mettagrid/stats_tracker.hpp +165 -0
  119. metta/mettagrid/stats_writer.py +55 -0
  120. metta/mettagrid/test_support/__init__.py +7 -0
  121. metta/mettagrid/test_support/actions.py +426 -0
  122. metta/mettagrid/test_support/mapgen.py +77 -0
  123. metta/mettagrid/test_support/observation_helper.py +50 -0
  124. metta/mettagrid/test_support/orientation.py +100 -0
  125. metta/mettagrid/test_support/token_types.py +18 -0
  126. metta/mettagrid/types.hpp +62 -0
  127. metta/mettagrid/util/__init__.py +0 -0
  128. metta/mettagrid/util/debug.py +501 -0
  129. metta/mettagrid/util/dict_utils.py +10 -0
  130. metta/mettagrid/util/diversity.py +71 -0
  131. metta/mettagrid/util/file.py +569 -0
  132. metta/mettagrid/util/module.py +12 -0
  133. mettagrid-0.2.dist-info/METADATA +256 -0
  134. mettagrid-0.2.dist-info/RECORD +137 -0
  135. mettagrid-0.2.dist-info/WHEEL +5 -0
  136. mettagrid-0.2.dist-info/licenses/LICENSE +21 -0
  137. mettagrid-0.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,61 @@
1
+ """
2
+ MettaGrid - Multi-agent reinforcement learning grid environments.
3
+
4
+ This module provides various environment adapters for different RL frameworks:
5
+ - MettaGridCore: Core C++ wrapper (no training features)
6
+ - MettaGridEnv: Training environment (PufferLib-based with stats/replay)
7
+ - MettaGridGymEnv: Gymnasium adapter
8
+ - MettaGridPettingZooEnv: PettingZoo adapter
9
+
10
+ All adapters inherit from MettaGridCore and provide framework-specific interfaces.
11
+ For PufferLib integration, use PufferLib's MettaPuff wrapper directly.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ # Import environment classes
17
+ from metta.mettagrid.core import MettaGridCore
18
+
19
+ # Import other commonly used classes
20
+ from metta.mettagrid.gym_env import MettaGridGymEnv
21
+ from metta.mettagrid.map_builder.map_builder import GameMap
22
+
23
+ # Import data types from C++ module (source of truth)
24
+ from metta.mettagrid.mettagrid_c import (
25
+ dtype_actions,
26
+ dtype_masks,
27
+ dtype_observations,
28
+ dtype_rewards,
29
+ dtype_success,
30
+ dtype_terminals,
31
+ dtype_truncations,
32
+ )
33
+ from metta.mettagrid.mettagrid_config import MettaGridConfig
34
+ from metta.mettagrid.mettagrid_env import MettaGridEnv
35
+ from metta.mettagrid.pettingzoo_env import MettaGridPettingZooEnv
36
+ from metta.mettagrid.replay_writer import ReplayWriter
37
+ from metta.mettagrid.stats_writer import StatsWriter
38
+
39
+ __all__ = [
40
+ # Config
41
+ "MettaGridConfig",
42
+ # Core classes
43
+ "MettaGridCore",
44
+ # Main environment (backward compatible)
45
+ "MettaGridEnv",
46
+ # Environment adapters
47
+ "MettaGridGymEnv",
48
+ "MettaGridPettingZooEnv",
49
+ # Data types
50
+ "dtype_actions",
51
+ "dtype_observations",
52
+ "dtype_rewards",
53
+ "dtype_terminals",
54
+ "dtype_truncations",
55
+ "dtype_masks",
56
+ "dtype_success",
57
+ # Supporting classes
58
+ "GameMap",
59
+ "ReplayWriter",
60
+ "StatsWriter",
61
+ ]
@@ -0,0 +1,137 @@
1
+ // action_handler.hpp
2
+ #ifndef ACTION_HANDLER_HPP_
3
+ #define ACTION_HANDLER_HPP_
4
+
5
+ #include <pybind11/pybind11.h>
6
+ #include <pybind11/stl.h>
7
+
8
+ #include <map>
9
+ #include <string>
10
+ #include <vector>
11
+
12
+ #include "grid.hpp"
13
+ #include "grid_object.hpp"
14
+ #include "objects/agent.hpp"
15
+ #include "objects/constants.hpp"
16
+ #include "types.hpp"
17
+
18
+ struct ActionConfig {
19
+ std::map<InventoryItem, InventoryQuantity> required_resources;
20
+ std::map<InventoryItem, InventoryQuantity> consumed_resources;
21
+
22
+ ActionConfig(const std::map<InventoryItem, InventoryQuantity>& required_resources = {},
23
+ const std::map<InventoryItem, InventoryQuantity>& consumed_resources = {})
24
+ : required_resources(required_resources), consumed_resources(consumed_resources) {}
25
+
26
+ virtual ~ActionConfig() {}
27
+ };
28
+
29
+ class ActionHandler {
30
+ public:
31
+ unsigned char priority;
32
+ Grid* _grid{};
33
+
34
+ ActionHandler(const ActionConfig& cfg, const std::string& action_name)
35
+ : priority(0),
36
+ _action_name(action_name),
37
+ _required_resources(cfg.required_resources),
38
+ _consumed_resources(cfg.consumed_resources) {
39
+ for (const auto& [item, amount] : _required_resources) {
40
+ if (amount < _consumed_resources[item]) {
41
+ throw std::runtime_error("Required resources must be greater than or equal to consumed resources");
42
+ }
43
+ }
44
+ }
45
+
46
+ virtual ~ActionHandler() {}
47
+
48
+ void init(Grid* grid) {
49
+ this->_grid = grid;
50
+ }
51
+
52
+ bool handle_action(GridObjectId actor_object_id, ActionArg arg) {
53
+ Agent* actor = static_cast<Agent*>(_grid->object(actor_object_id));
54
+
55
+ // Handle frozen status
56
+ if (actor->frozen != 0) {
57
+ actor->stats.incr("status.frozen.ticks");
58
+ actor->stats.incr("status.frozen.ticks." + actor->group_name);
59
+ if (actor->frozen > 0) {
60
+ actor->frozen -= 1;
61
+ }
62
+ return false;
63
+ }
64
+
65
+ bool has_needed_resources = true;
66
+ for (const auto& [item, amount] : _required_resources) {
67
+ if (actor->inventory[item] < amount) {
68
+ has_needed_resources = false;
69
+ break;
70
+ }
71
+ }
72
+
73
+ // Execute the action
74
+ bool success = has_needed_resources && _handle_action(actor, arg);
75
+
76
+ // The intention here is to provide a metric that reports when an agent has stayed in one location for a long
77
+ // period, perhaps spinning in circles. We think this could be a good indicator that a policy has collapsed.
78
+ if (actor->location == actor->prev_location) {
79
+ actor->steps_without_motion += 1;
80
+ if (actor->steps_without_motion > actor->stats.get("status.max_steps_without_motion")) {
81
+ actor->stats.set("status.max_steps_without_motion", actor->steps_without_motion);
82
+ }
83
+ } else {
84
+ actor->steps_without_motion = 0;
85
+ }
86
+
87
+ // Update tracking for this agent
88
+ actor->prev_action_name = _action_name;
89
+ actor->prev_location = actor->location;
90
+
91
+ // Track success/failure
92
+ if (success) {
93
+ actor->stats.incr("action." + _action_name + ".success");
94
+ for (const auto& [item, amount] : _consumed_resources) {
95
+ [[maybe_unused]] InventoryDelta delta = actor->update_inventory(item, -amount);
96
+ // We consume resources after the action succeeds, but in the future we might have an action that uses the
97
+ // resource. This check will catch that.
98
+ assert(delta == -amount);
99
+ }
100
+ } else {
101
+ actor->stats.incr("action." + _action_name + ".failed");
102
+ actor->stats.incr("action.failure_penalty");
103
+ *actor->reward -= actor->action_failure_penalty;
104
+ }
105
+
106
+ return success;
107
+ }
108
+
109
+ virtual unsigned char max_arg() const {
110
+ return 0;
111
+ }
112
+
113
+ std::string action_name() const {
114
+ return _action_name;
115
+ }
116
+
117
+ protected:
118
+ virtual bool _handle_action(Agent* actor, ActionArg arg) = 0;
119
+
120
+ std::string _action_name;
121
+ std::map<InventoryItem, InventoryQuantity> _required_resources;
122
+ std::map<InventoryItem, InventoryQuantity> _consumed_resources;
123
+ };
124
+
125
+ namespace py = pybind11;
126
+
127
+ inline void bind_action_config(py::module& m) {
128
+ py::class_<ActionConfig, std::shared_ptr<ActionConfig>>(m, "ActionConfig")
129
+ .def(py::init<const std::map<InventoryItem, InventoryQuantity>&,
130
+ const std::map<InventoryItem, InventoryQuantity>&>(),
131
+ py::arg("required_resources") = std::map<InventoryItem, InventoryQuantity>(),
132
+ py::arg("consumed_resources") = std::map<InventoryItem, InventoryQuantity>())
133
+ .def_readwrite("required_resources", &ActionConfig::required_resources)
134
+ .def_readwrite("consumed_resources", &ActionConfig::consumed_resources);
135
+ }
136
+
137
+ #endif // ACTION_HANDLER_HPP_
File without changes
@@ -0,0 +1,235 @@
1
+ #ifndef ACTIONS_ATTACK_HPP_
2
+ #define ACTIONS_ATTACK_HPP_
3
+
4
+ #include <pybind11/pybind11.h>
5
+ #include <pybind11/stl.h>
6
+
7
+ #include <map>
8
+ #include <string>
9
+ #include <vector>
10
+
11
+ #include "action_handler.hpp"
12
+ #include "grid_object.hpp"
13
+ #include "mettagrid_config.hpp"
14
+ #include "objects/agent.hpp"
15
+ #include "objects/constants.hpp"
16
+ #include "types.hpp"
17
+
18
+ // Attack takes an argument 0-8, which is the index of the target agent to attack.
19
+ // Target agents are those found in a 3x3 grid in front of the agent, indexed in scan order.
20
+ // If the argument (agent index to attack) > num_agents, the last agent is attacked.
21
+ struct AttackActionConfig : public ActionConfig {
22
+ std::map<InventoryItem, InventoryQuantity> defense_resources;
23
+
24
+ AttackActionConfig(const std::map<InventoryItem, InventoryQuantity>& required_resources,
25
+ const std::map<InventoryItem, InventoryQuantity>& consumed_resources,
26
+ const std::map<InventoryItem, InventoryQuantity>& defense_resources)
27
+ : ActionConfig(required_resources, consumed_resources), defense_resources(defense_resources) {}
28
+ };
29
+
30
+ class Attack : public ActionHandler {
31
+ public:
32
+ explicit Attack(const AttackActionConfig& cfg,
33
+ const GameConfig* game_config,
34
+ const std::string& action_name = "attack")
35
+ : ActionHandler(cfg, action_name), _defense_resources(cfg.defense_resources), _game_config(game_config) {
36
+ priority = 1;
37
+ }
38
+
39
+ unsigned char max_arg() const override {
40
+ return 8;
41
+ }
42
+
43
+ protected:
44
+ std::map<InventoryItem, InventoryQuantity> _defense_resources;
45
+ const GameConfig* _game_config;
46
+
47
+ bool _handle_action(Agent* actor, ActionArg arg) override {
48
+ Agent* last_agent = nullptr;
49
+ short num_skipped = 0;
50
+
51
+ // Attack pattern depends on diagonal support
52
+ if (!_game_config->allow_diagonals) {
53
+ // Original 3x3 grid pattern for cardinal-only movement
54
+ // 7 6 8 (3 cells forward)
55
+ // 4 3 5 (2 cells forward)
56
+ // 1 0 2 (1 cell forward)
57
+ // . A . (Agent position)
58
+
59
+ static constexpr short COL_OFFSETS[3] = {0, -1, 1};
60
+
61
+ for (short distance = 1; distance <= 3; distance++) {
62
+ for (short offset : COL_OFFSETS) {
63
+ GridLocation target_loc = _grid->relative_location(actor->location, actor->orientation, distance, offset);
64
+ target_loc.layer = GridLayer::AgentLayer;
65
+
66
+ Agent* target_agent = static_cast<Agent*>(_grid->object_at(target_loc));
67
+ if (target_agent) {
68
+ last_agent = target_agent;
69
+ if (num_skipped == arg) {
70
+ return _handle_target(*actor, *target_agent);
71
+ }
72
+ num_skipped++;
73
+ }
74
+ }
75
+ }
76
+ } else {
77
+ // Diagonal attack pattern when diagonals are enabled
78
+ // Agent facing NE example:
79
+ // . 4 6 8
80
+ // . 1 3 7
81
+ // . 0 2 5
82
+ // A . . .
83
+
84
+ // Define the 9 positions in order (0-8)
85
+ static constexpr struct {
86
+ short forward;
87
+ short lateral;
88
+ } DIAGONAL_POSITIONS[9] = {
89
+ {1, 0}, // 0: directly forward
90
+ {2, -1}, // 1: forward-left
91
+ {1, 1}, // 2: forward-right
92
+ {2, 0}, // 3: 2 forward
93
+ {3, -2}, // 4: far forward-left
94
+ {1, 2}, // 5: right-forward
95
+ {3, -1}, // 6: far forward-left-center
96
+ {2, 1}, // 7: forward-right-center
97
+ {3, 0} // 8: 3 forward
98
+ };
99
+
100
+ for (const auto& pos : DIAGONAL_POSITIONS) {
101
+ GridLocation target_loc =
102
+ _grid->relative_location(actor->location, actor->orientation, pos.forward, pos.lateral);
103
+ target_loc.layer = GridLayer::AgentLayer;
104
+
105
+ Agent* target_agent = static_cast<Agent*>(_grid->object_at(target_loc));
106
+ if (target_agent) {
107
+ last_agent = target_agent;
108
+ if (num_skipped == arg) {
109
+ return _handle_target(*actor, *target_agent);
110
+ }
111
+ num_skipped++;
112
+ }
113
+ }
114
+ }
115
+
116
+ // If we got here, it means we skipped over all the targets. Attack the last one.
117
+ if (last_agent) {
118
+ return _handle_target(*actor, *last_agent);
119
+ }
120
+
121
+ return false;
122
+ }
123
+
124
+ bool _handle_target(Agent& actor, Agent& target) {
125
+ bool was_already_frozen = target.frozen > 0;
126
+
127
+ // Check if target can defend
128
+ if (!_defense_resources.empty()) {
129
+ bool target_can_defend = _check_defense_capability(target);
130
+
131
+ if (target_can_defend) {
132
+ _consume_defense_resources(target);
133
+ _log_blocked_attack(actor, target);
134
+ return true;
135
+ }
136
+ }
137
+
138
+ // Attack succeeds
139
+ target.frozen = target.freeze_duration;
140
+
141
+ if (!was_already_frozen) {
142
+ _steal_resources(actor, target);
143
+ _log_successful_attack(actor, target);
144
+ } else {
145
+ // Track wasted attacks on already-frozen targets
146
+ const std::string& actor_group = actor.group_name;
147
+ actor.stats.incr(_action_prefix(actor_group) + "wasted_on_frozen");
148
+ }
149
+ return true;
150
+ }
151
+
152
+ private:
153
+ bool _check_defense_capability(const Agent& target) const {
154
+ for (const auto& [item, amount] : _defense_resources) {
155
+ auto it = target.inventory.find(item);
156
+ if (it == target.inventory.end() || it->second < amount) {
157
+ return false;
158
+ }
159
+ }
160
+ return true;
161
+ }
162
+
163
+ void _consume_defense_resources(Agent& target) {
164
+ for (const auto& [item, amount] : _defense_resources) {
165
+ [[maybe_unused]] InventoryDelta delta = target.update_inventory(item, -amount);
166
+ assert(delta == -amount);
167
+ }
168
+ }
169
+
170
+ void _steal_resources(Agent& actor, Agent& target) {
171
+ // Create snapshot to avoid iterator invalidation
172
+ std::vector<std::pair<InventoryItem, InventoryQuantity>> snapshot;
173
+ snapshot.reserve(target.inventory.size());
174
+ for (const auto& [item, amount] : target.inventory) {
175
+ snapshot.emplace_back(item, amount);
176
+ }
177
+
178
+ // Transfer resources
179
+ for (const auto& [item, amount] : snapshot) {
180
+ InventoryDelta stolen = actor.update_inventory(item, amount);
181
+ target.update_inventory(item, -stolen);
182
+
183
+ if (stolen > 0) {
184
+ _log_resource_theft(actor, target, item, stolen);
185
+ }
186
+ }
187
+ }
188
+
189
+ std::string _action_prefix(const std::string& group) const {
190
+ return "action." + _action_name + "." + group + ".";
191
+ }
192
+
193
+ void _log_blocked_attack(Agent& actor, const Agent& target) const {
194
+ const std::string& actor_group = actor.group_name;
195
+ const std::string& target_group = target.group_name;
196
+
197
+ actor.stats.incr(_action_prefix(actor_group) + "blocked_by." + target_group);
198
+ }
199
+
200
+ void _log_successful_attack(Agent& actor, Agent& target) const {
201
+ const std::string& actor_group = actor.group_name;
202
+ const std::string& target_group = target.group_name;
203
+ bool same_team = (actor_group == target_group);
204
+
205
+ if (same_team) {
206
+ actor.stats.incr(_action_prefix(actor_group) + "friendly_fire");
207
+ } else {
208
+ actor.stats.incr(_action_prefix(actor_group) + "hit." + target_group);
209
+ target.stats.incr(_action_prefix(target_group) + "hit_by." + actor_group);
210
+ }
211
+ }
212
+
213
+ void _log_resource_theft(Agent& actor, Agent& target, InventoryItem item, InventoryDelta amount) const {
214
+ const std::string& actor_group = actor.group_name;
215
+ const std::string& target_group = target.group_name;
216
+ const std::string item_name = actor.stats.resource_name(item);
217
+
218
+ actor.stats.add(_action_prefix(actor_group) + "steals." + item_name + ".from." + target_group, amount);
219
+ }
220
+ };
221
+
222
+ namespace py = pybind11;
223
+
224
+ inline void bind_attack_action_config(py::module& m) {
225
+ py::class_<AttackActionConfig, ActionConfig, std::shared_ptr<AttackActionConfig>>(m, "AttackActionConfig")
226
+ .def(py::init<const std::map<InventoryItem, InventoryQuantity>&,
227
+ const std::map<InventoryItem, InventoryQuantity>&,
228
+ const std::map<InventoryItem, InventoryQuantity>&>(),
229
+ py::arg("required_resources") = std::map<InventoryItem, InventoryQuantity>(),
230
+ py::arg("consumed_resources") = std::map<InventoryItem, InventoryQuantity>(),
231
+ py::arg("defense_resources") = std::map<InventoryItem, InventoryQuantity>())
232
+ .def_readwrite("defense_resources", &AttackActionConfig::defense_resources);
233
+ }
234
+
235
+ #endif // ACTIONS_ATTACK_HPP_
@@ -0,0 +1,40 @@
1
+ #ifndef ACTIONS_CHANGE_COLOR_HPP_
2
+ #define ACTIONS_CHANGE_COLOR_HPP_
3
+
4
+ #include <string>
5
+
6
+ #include "action_handler.hpp"
7
+ #include "objects/agent.hpp"
8
+ #include "types.hpp"
9
+
10
+ class ChangeColor : public ActionHandler {
11
+ public:
12
+ explicit ChangeColor(const ActionConfig& cfg) : ActionHandler(cfg, "change_color") {}
13
+
14
+ unsigned char max_arg() const override {
15
+ return 3; // support fine and coarse adjustment
16
+ }
17
+
18
+ protected:
19
+ bool _handle_action(Agent* actor, ActionArg arg) override {
20
+ // Note: 'color' is uint8_t which naturally wraps at 256.
21
+ // This could be interpreted as circular hue behavior (red -> orange -> ... -> violet -> red)
22
+
23
+ // Calculate step size once (integer division is intentional)
24
+ const uint8_t step_size = static_cast<uint8_t>(255 / (max_arg() + 1));
25
+
26
+ if (arg == 0) { // Increment
27
+ actor->color = static_cast<uint8_t>(actor->color + 1);
28
+ } else if (arg == 1) { // Decrement
29
+ actor->color = static_cast<uint8_t>(actor->color - 1);
30
+ } else if (arg == 2) { // Large increment
31
+ actor->color = static_cast<uint8_t>(actor->color + step_size);
32
+ } else if (arg == 3) { // Large decrement
33
+ actor->color = static_cast<uint8_t>(actor->color - step_size);
34
+ }
35
+
36
+ return true;
37
+ }
38
+ };
39
+
40
+ #endif // ACTIONS_CHANGE_COLOR_HPP_
@@ -0,0 +1,54 @@
1
+ #ifndef ACTIONS_CHANGE_GLYPH_HPP_
2
+ #define ACTIONS_CHANGE_GLYPH_HPP_
3
+
4
+ #include <pybind11/pybind11.h>
5
+ #include <pybind11/stl.h>
6
+
7
+ #include <string>
8
+
9
+ #include "action_handler.hpp"
10
+ #include "objects/agent.hpp"
11
+ #include "types.hpp"
12
+
13
+ struct ChangeGlyphActionConfig : public ActionConfig {
14
+ const ObservationType number_of_glyphs;
15
+
16
+ ChangeGlyphActionConfig(const std::map<InventoryItem, InventoryQuantity>& required_resources,
17
+ const std::map<InventoryItem, InventoryQuantity>& consumed_resources,
18
+ const ObservationType number_of_glyphs)
19
+ : ActionConfig(required_resources, consumed_resources), number_of_glyphs(number_of_glyphs) {}
20
+ };
21
+
22
+ class ChangeGlyph : public ActionHandler {
23
+ public:
24
+ explicit ChangeGlyph(const ChangeGlyphActionConfig& cfg)
25
+ : ActionHandler(cfg, "change_glyph"), _number_of_glyphs(cfg.number_of_glyphs) {}
26
+
27
+ unsigned char max_arg() const override {
28
+ // Return number_of_glyphs - 1 since args are 0-indexed
29
+ return _number_of_glyphs > 0 ? _number_of_glyphs - 1 : 0;
30
+ }
31
+
32
+ protected:
33
+ const ObservationType _number_of_glyphs;
34
+
35
+ bool _handle_action(Agent* actor, ActionArg arg) override {
36
+ actor->glyph = static_cast<ObservationType>(arg); // ActionArg is int32 for puffer compatibility
37
+ return true;
38
+ }
39
+ };
40
+
41
+ namespace py = pybind11;
42
+
43
+ inline void bind_change_glyph_action_config(py::module& m) {
44
+ py::class_<ChangeGlyphActionConfig, ActionConfig, std::shared_ptr<ChangeGlyphActionConfig>>(m,
45
+ "ChangeGlyphActionConfig")
46
+ .def(py::init<const std::map<InventoryItem, InventoryQuantity>&,
47
+ const std::map<InventoryItem, InventoryQuantity>&,
48
+ const int>(),
49
+ py::arg("required_resources") = std::map<InventoryItem, InventoryQuantity>(),
50
+ py::arg("consumed_resources") = std::map<InventoryItem, InventoryQuantity>(),
51
+ py::arg("number_of_glyphs"))
52
+ .def_readonly("number_of_glyphs", &ChangeGlyphActionConfig::number_of_glyphs);
53
+ }
54
+ #endif // ACTIONS_CHANGE_GLYPH_HPP_
@@ -0,0 +1,88 @@
1
+ #ifndef ACTIONS_GET_OUTPUT_HPP_
2
+ #define ACTIONS_GET_OUTPUT_HPP_
3
+
4
+ #include <string>
5
+
6
+ #include "action_handler.hpp"
7
+ #include "grid.hpp"
8
+ #include "grid_object.hpp"
9
+ #include "objects/agent.hpp"
10
+ #include "objects/converter.hpp"
11
+ #include "types.hpp"
12
+
13
+ class GetOutput : public ActionHandler {
14
+ public:
15
+ explicit GetOutput(const ActionConfig& cfg) : ActionHandler(cfg, "get_items") {}
16
+
17
+ unsigned char max_arg() const override {
18
+ return 0;
19
+ }
20
+
21
+ protected:
22
+ bool _handle_action(Agent* actor, ActionArg /*arg*/) override {
23
+ GridLocation target_loc = _grid->relative_location(actor->location, static_cast<Orientation>(actor->orientation));
24
+ target_loc.layer = GridLayer::ObjectLayer;
25
+ // get_output only works on Converters, since only Converters have an output.
26
+ // Once we generalize this to `get`, we should be able to get from any HasInventory object, which
27
+ // should include agents. That's (e.g.) why we're checking inventory_is_accessible.
28
+ Converter* converter = dynamic_cast<Converter*>(_grid->object_at(target_loc));
29
+ Box* box = dynamic_cast<Box*>(_grid->object_at(target_loc));
30
+ if (!converter && !box) {
31
+ return false;
32
+ }
33
+ // If converter, get output from converter
34
+ if (converter) {
35
+ if (!converter->inventory_is_accessible()) {
36
+ return false;
37
+ }
38
+
39
+ // Actions is only successful if we take at least one item.
40
+ bool resources_taken = false;
41
+
42
+ for (const auto& [item, _] : converter->output_resources) {
43
+ if (converter->inventory.count(item) == 0) {
44
+ continue;
45
+ }
46
+ InventoryDelta resources_available = converter->inventory[item];
47
+
48
+ InventoryDelta taken = actor->update_inventory(item, resources_available);
49
+
50
+ if (taken > 0) {
51
+ actor->stats.add(actor->stats.resource_name(item) + ".get", taken);
52
+ converter->update_inventory(item, -taken);
53
+ resources_taken = true;
54
+ }
55
+ }
56
+ return resources_taken;
57
+ }
58
+
59
+ // If box, pick up box if allowed
60
+ if (box) {
61
+ if (actor->agent_id == box->creator_agent_id) {
62
+ // Creator cannot open their own box
63
+ return false;
64
+ }
65
+ // If the creator of the box is an agent, return blue battery to creator and penalize creator
66
+ if (box->creator_agent_id != 255) {
67
+ Agent* creator = dynamic_cast<Agent*>(_grid->object(box->creator_agent_object_id));
68
+ if (!creator) {
69
+ return false;
70
+ }
71
+ // Return required resources to create box to creator inventory
72
+ for (const auto& [item, amount] : box->returned_resources) {
73
+ if (amount > 0) {
74
+ creator->update_inventory(item, amount);
75
+ }
76
+ }
77
+ }
78
+
79
+ // Reward the agent for opening the box and teleport back to top-left corner
80
+ _grid->ghost_move_object(box->id, GridLocation(0, 0, GridLayer::ObjectLayer));
81
+ actor->stats.add("box.opened", 1.0f);
82
+ return true;
83
+ }
84
+ return false;
85
+ }
86
+ };
87
+
88
+ #endif // ACTIONS_GET_OUTPUT_HPP_