Skip to content

Commit ff5c7e0

Browse files
committed
cp
1 parent 2679b18 commit ff5c7e0

File tree

15 files changed

+270
-61
lines changed

15 files changed

+270
-61
lines changed

packages/mettagrid/cpp/bindings/mettagrid_c.cpp

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1024,8 +1024,19 @@ PYBIND11_MODULE(mettagrid_c, m) {
10241024
.def_readonly("resource_names", &MettaGrid::resource_names)
10251025
.def("set_inventory", &MettaGrid::set_inventory, py::arg("agent_id"), py::arg("inventory"));
10261026

1027+
// Bind DemolishConfig for building destruction
1028+
py::class_<DemolishConfig>(m, "DemolishConfig")
1029+
.def(py::init<>())
1030+
.def(py::init<const std::unordered_map<InventoryItem, InventoryQuantity>&,
1031+
const std::unordered_map<InventoryItem, InventoryQuantity>&>(),
1032+
py::arg("cost") = std::unordered_map<InventoryItem, InventoryQuantity>(),
1033+
py::arg("scrap") = std::unordered_map<InventoryItem, InventoryQuantity>())
1034+
.def_readwrite("cost", &DemolishConfig::cost)
1035+
.def_readwrite("scrap", &DemolishConfig::scrap);
1036+
10271037
// Expose this so we can cast python WallConfig / AgentConfig to a common GridConfig cpp object.
1028-
py::class_<GridObjectConfig, std::shared_ptr<GridObjectConfig>>(m, "GridObjectConfig");
1038+
py::class_<GridObjectConfig, std::shared_ptr<GridObjectConfig>>(m, "GridObjectConfig")
1039+
.def_readwrite("demolish", &GridObjectConfig::demolish);
10291040

10301041
bind_wall_config(m);
10311042

packages/mettagrid/cpp/include/mettagrid/actions/attack.hpp

Lines changed: 67 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -93,32 +93,84 @@ class Attack : public ActionHandler {
9393
bool try_attack(Agent& actor, GridObject* target_object) {
9494
if (!target_object) return false;
9595

96+
// First try to attack as agent
9697
Agent* target = dynamic_cast<Agent*>(target_object);
97-
if (!target) return false; // Can only attack agents
98+
if (target) {
99+
// Don't attack already frozen agents - let move handler swap instead
100+
if (target->frozen > 0) return false;
98101

99-
// Don't attack already frozen agents - let move handler swap instead
100-
if (target->frozen > 0) return false;
102+
// Check if actor has required resources for attack
103+
for (const auto& [item, amount] : _consumed_resources) {
104+
if (actor.inventory.amount(item) < amount) {
105+
return false; // Can't afford attack
106+
}
107+
}
108+
109+
bool success = _handle_target(actor, *target);
110+
111+
// Consume resources on success
112+
if (success) {
113+
for (const auto& [item, amount] : _consumed_resources) {
114+
if (amount > 0) {
115+
InventoryDelta delta = static_cast<InventoryDelta>(-static_cast<int>(amount));
116+
actor.inventory.update(item, delta);
117+
}
118+
}
119+
}
120+
return success;
121+
}
122+
123+
// Not an agent - try to demolish if target has demolish config
124+
return try_demolish(actor, target_object);
125+
}
126+
127+
// Attempt to demolish a building
128+
bool try_demolish(Agent& actor, GridObject* target) {
129+
if (!target || !target->demolish_config) {
130+
return false; // No demolish config, can't demolish
131+
}
132+
133+
const DemolishConfig& demolish = *target->demolish_config;
101134

102-
// Check if actor has required resources for attack
103-
for (const auto& [item, amount] : _consumed_resources) {
135+
// Check if actor has required resources for demolition
136+
for (const auto& [item, amount] : demolish.cost) {
104137
if (actor.inventory.amount(item) < amount) {
105-
return false; // Can't afford attack
138+
const std::string& actor_group = actor.group_name;
139+
actor.stats.incr(_action_prefix(actor_group) + "demolish.failed.insufficient_resources");
140+
return false;
106141
}
107142
}
108143

109-
bool success = _handle_target(actor, *target);
144+
// Pay the demolition cost
145+
for (const auto& [item, amount] : demolish.cost) {
146+
if (amount > 0) {
147+
InventoryDelta delta = static_cast<InventoryDelta>(-static_cast<int>(amount));
148+
actor.inventory.update(item, delta);
149+
}
150+
}
110151

111-
// Consume resources on success
112-
if (success) {
113-
for (const auto& [item, amount] : _consumed_resources) {
114-
if (amount > 0) {
115-
InventoryDelta delta = static_cast<InventoryDelta>(-static_cast<int>(amount));
116-
actor.inventory.update(item, delta);
117-
}
152+
// Log demolition BEFORE removing (target will be deleted)
153+
const std::string& actor_group = actor.group_name;
154+
const std::string target_type_name = target->type_name; // Copy before removal
155+
actor.stats.incr(_action_prefix(actor_group) + "demolish." + target_type_name);
156+
actor.stats.incr("demolish." + target_type_name);
157+
158+
// Call on_demolish before removing
159+
target->on_demolish();
160+
161+
// Give scrap resources to actor
162+
for (const auto& [item, amount] : demolish.scrap) {
163+
if (amount > 0) {
164+
actor.inventory.update(item, amount);
118165
}
119166
}
120167

121-
return success;
168+
// Remove the object from grid (this deletes the object)
169+
if (_grid) {
170+
_grid->remove_object(*target);
171+
}
172+
173+
return true;
122174
}
123175

124176
protected:

packages/mettagrid/cpp/include/mettagrid/core/grid.hpp

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,27 @@ class Grid {
9393
return true;
9494
}
9595

96+
// Remove an object from the grid. Returns true if successful.
97+
// Note: The object's ID slot in the objects vector becomes null but is not reused,
98+
// keeping all other IDs stable.
99+
inline bool remove_object(GridObject& obj) {
100+
if (!is_valid_location(obj.location)) {
101+
return false;
102+
}
103+
if (grid[obj.location.r][obj.location.c] != &obj) {
104+
return false; // Object not at expected location
105+
}
106+
107+
// Clear the grid cell
108+
grid[obj.location.r][obj.location.c] = nullptr;
109+
110+
// Release the object (unique_ptr becomes null but slot remains)
111+
if (obj.id < objects.size()) {
112+
objects[obj.id].reset();
113+
}
114+
return true;
115+
}
116+
96117
inline GridObject* object(GridObjectId obj_id) const {
97118
assert(obj_id < objects.size() && "Invalid object ID");
98119
return objects[obj_id].get();

packages/mettagrid/cpp/include/mettagrid/core/grid_object.hpp

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
#define PACKAGES_METTAGRID_CPP_INCLUDE_METTAGRID_CORE_GRID_OBJECT_HPP_
33

44
#include <cstdint>
5+
#include <optional>
56
#include <span>
67
#include <string>
8+
#include <unordered_map>
79
#include <vector>
810

911
#include "core/types.hpp"
@@ -47,14 +49,26 @@ class GridLocation {
4749
}
4850
};
4951

52+
// Configuration for demolishing a building via attack
53+
struct DemolishConfig {
54+
std::unordered_map<InventoryItem, InventoryQuantity> cost; // Resources required to demolish
55+
std::unordered_map<InventoryItem, InventoryQuantity> scrap; // Resources returned after demolish
56+
57+
DemolishConfig() = default;
58+
DemolishConfig(const std::unordered_map<InventoryItem, InventoryQuantity>& cost,
59+
const std::unordered_map<InventoryItem, InventoryQuantity>& scrap)
60+
: cost(cost), scrap(scrap) {}
61+
};
62+
5063
struct GridObjectConfig {
5164
TypeId type_id;
5265
std::string type_name;
5366
std::vector<int> tag_ids;
5467
ObservationType initial_vibe;
68+
std::optional<DemolishConfig> demolish; // If set, object can be demolished
5569

5670
GridObjectConfig(TypeId type_id, const std::string& type_name, ObservationType initial_vibe = 0)
57-
: type_id(type_id), type_name(type_name), tag_ids({}), initial_vibe(initial_vibe) {}
71+
: type_id(type_id), type_name(type_name), tag_ids({}), initial_vibe(initial_vibe), demolish(std::nullopt) {}
5872

5973
virtual ~GridObjectConfig() = default;
6074
};
@@ -66,21 +80,27 @@ class GridObject : public HasVibe {
6680
TypeId type_id{};
6781
std::string type_name;
6882
std::vector<int> tag_ids;
83+
const DemolishConfig* demolish_config = nullptr; // Optional demolish config for buildings
6984

7085
virtual ~GridObject() = default;
7186

7287
void init(TypeId object_type_id,
7388
const std::string& object_type_name,
7489
const GridLocation& object_location,
7590
const std::vector<int>& tags,
76-
ObservationType object_vibe = 0) {
91+
ObservationType object_vibe = 0,
92+
const DemolishConfig* demolish = nullptr) {
7793
this->type_id = object_type_id;
7894
this->type_name = object_type_name;
7995
this->location = object_location;
8096
this->tag_ids = tags;
8197
this->vibe = object_vibe;
98+
this->demolish_config = demolish;
8299
}
83100

101+
// Called when this object is demolished. Override for cleanup.
102+
virtual void on_demolish() {}
103+
84104
virtual std::vector<PartialObservationToken> obs_features() const {
85105
return {}; // Default: no observable features
86106
}

packages/mettagrid/cpp/include/mettagrid/objects/assembler.hpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,8 @@ class Assembler : public GridObject, public Usable {
266266
allow_partial_usage(cfg.allow_partial_usage),
267267
chest_search_distance(cfg.chest_search_distance),
268268
clipper_ptr(nullptr) {
269-
GridObject::init(cfg.type_id, cfg.type_name, GridLocation(r, c), cfg.tag_ids, cfg.initial_vibe);
269+
const DemolishConfig* demolish = cfg.demolish.has_value() ? &cfg.demolish.value() : nullptr;
270+
GridObject::init(cfg.type_id, cfg.type_name, GridLocation(r, c), cfg.tag_ids, cfg.initial_vibe, demolish);
270271
}
271272
virtual ~Assembler() = default;
272273

packages/mettagrid/cpp/include/mettagrid/objects/chest.hpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@ class Chest : public GridObject, public Usable, public HasInventory {
7878
vibe_transfers(cfg.vibe_transfers),
7979
stats_tracker(stats_tracker),
8080
grid(nullptr) {
81-
GridObject::init(cfg.type_id, cfg.type_name, GridLocation(r, c), cfg.tag_ids, cfg.initial_vibe);
81+
const DemolishConfig* demolish = cfg.demolish.has_value() ? &cfg.demolish.value() : nullptr;
82+
GridObject::init(cfg.type_id, cfg.type_name, GridLocation(r, c), cfg.tag_ids, cfg.initial_vibe, demolish);
8283
// Set initial inventory for all configured resources (ignore limits for initial setup)
8384
for (const auto& [resource, amount] : cfg.initial_inventory) {
8485
if (amount > 0) {

packages/mettagrid/cpp/include/mettagrid/objects/protocol.hpp

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
#include <pybind11/pybind11.h>
55
#include <pybind11/stl.h>
66

7+
#include <algorithm>
8+
#include <cmath>
79
#include <memory>
810
#include <unordered_map>
911

@@ -16,13 +18,40 @@ class Protocol {
1618
std::unordered_map<InventoryItem, InventoryQuantity> input_resources;
1719
std::unordered_map<InventoryItem, InventoryQuantity> output_resources;
1820
unsigned short cooldown;
21+
float slope; // Linear component: adds slope * activation_count to the multiplier
22+
float exponent; // Exponential component: multiplies by (1 + exponent)^activation_count
23+
24+
// Track activation count for inflation calculation
25+
mutable unsigned int activation_count;
1926

2027
Protocol(unsigned short min_agents = 0,
2128
const std::vector<ObservationType>& vibes = {},
2229
const std::unordered_map<InventoryItem, InventoryQuantity>& inputs = {},
2330
const std::unordered_map<InventoryItem, InventoryQuantity>& outputs = {},
24-
unsigned short cooldown = 0)
25-
: min_agents(min_agents), vibes(vibes), input_resources(inputs), output_resources(outputs), cooldown(cooldown) {}
31+
unsigned short cooldown = 0,
32+
float slope = 0.0f,
33+
float exponent = 0.0f)
34+
: min_agents(min_agents),
35+
vibes(vibes),
36+
input_resources(inputs),
37+
output_resources(outputs),
38+
cooldown(cooldown),
39+
slope(slope),
40+
exponent(exponent),
41+
activation_count(0) {}
42+
43+
// Calculate cost multiplier based on activation count
44+
// Formula: max(0, 1 + slope * n) * (1 + exponent)^n
45+
// - slope < 0: discounting (starts at 1, decreases linearly, floors at 0)
46+
// - slope > 0: linear cost increase
47+
// - exponent > 0: exponential cost increase
48+
// - exponent < 0: exponential cost decrease
49+
float get_cost_multiplier() const {
50+
float n = static_cast<float>(activation_count);
51+
float linear_component = std::max(0.0f, 1.0f + slope * n);
52+
float exponential_component = std::pow(1.0f + exponent, n);
53+
return linear_component * exponential_component;
54+
}
2655
};
2756

2857
inline void bind_protocol(py::module& m) {
@@ -32,7 +61,10 @@ inline void bind_protocol(py::module& m) {
3261
.def_readwrite("vibes", &Protocol::vibes)
3362
.def_readwrite("input_resources", &Protocol::input_resources)
3463
.def_readwrite("output_resources", &Protocol::output_resources)
35-
.def_readwrite("cooldown", &Protocol::cooldown);
64+
.def_readwrite("cooldown", &Protocol::cooldown)
65+
.def_readwrite("slope", &Protocol::slope)
66+
.def_readwrite("exponent", &Protocol::exponent)
67+
.def_readwrite("activation_count", &Protocol::activation_count);
3668
}
3769

3870
#endif // PACKAGES_METTAGRID_CPP_INCLUDE_METTAGRID_OBJECTS_PROTOCOL_HPP_

packages/mettagrid/cpp/include/mettagrid/objects/wall.hpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ struct WallConfig : public GridObjectConfig {
2020
class Wall : public GridObject {
2121
public:
2222
Wall(GridCoord r, GridCoord c, const WallConfig& cfg) {
23-
GridObject::init(cfg.type_id, cfg.type_name, GridLocation(r, c), cfg.tag_ids, cfg.initial_vibe);
23+
const DemolishConfig* demolish = cfg.demolish.has_value() ? &cfg.demolish.value() : nullptr;
24+
GridObject::init(cfg.type_id, cfg.type_name, GridLocation(r, c), cfg.tag_ids, cfg.initial_vibe, demolish);
2425
}
2526

2627
std::vector<PartialObservationToken> obs_features() const override {
2.35 MB
Binary file not shown.

packages/mettagrid/nim/mettascope/src/mettascope/replays.nim

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ type
9595
# Computed fields.
9696
gainMap*: seq[seq[ItemAmount]]
9797
isAgent*: bool
98+
removedAtStep*: int # Step when object was removed, -1 if not removed
9899

99100
Replay* = ref object
100101
version*: int
@@ -737,6 +738,7 @@ proc loadReplayString*(jsonData: string, fileName: string): Replay =
737738
inventory: inventory,
738739
inventoryMax: obj.getInt("inventory_max", 0),
739740
color: obj.getExpandedIntSeq("color", replay.maxSteps),
741+
removedAtStep: -1, # Not removed
740742
)
741743
entity.groupId = getInt(obj, "group_id", 0)
742744

@@ -861,10 +863,17 @@ proc loadReplay*(fileName: string): Replay =
861863
proc apply*(replay: Replay, step: int, objects: seq[ReplayEntity]) =
862864
## Apply a replay step to the replay.
863865
const agentTypeName = "agent"
866+
867+
# Track which object IDs are present in this step
868+
var presentIds: set[uint16]
869+
for obj in objects:
870+
if obj.id >= 0 and obj.id < 65536:
871+
presentIds.incl(obj.id.uint16)
872+
864873
for obj in objects:
865874
let index = obj.id - 1
866875
while index >= replay.objects.len:
867-
replay.objects.add(Entity(id: obj.id))
876+
replay.objects.add(Entity(id: replay.objects.len + 1, removedAtStep: -1))
868877

869878
let entity = replay.objects[index]
870879
doAssert entity.id == obj.id, "Object id mismatch"
@@ -908,6 +917,24 @@ proc apply*(replay: Replay, step: int, objects: seq[ReplayEntity]) =
908917
entity.allowPartialUsage = obj.allowPartialUsage
909918
entity.protocols = obj.protocols
910919

920+
# Check for removed objects (existed before but not in current step)
921+
# Only mark as removed if:
922+
# - Not already removed
923+
# - Not a wall (walls are optimized out after step 0)
924+
# - Not an agent (agents are never removed)
925+
# - Has location data (was actually initialized with real data)
926+
# - Not in current step's data
927+
for entity in replay.objects:
928+
if entity.removedAtStep < 0 and # Not already removed
929+
entity.typeName != "wall" and # Skip walls (static, optimized out)
930+
entity.typeName != "agent" and # Skip agents (never removed)
931+
entity.typeName.len > 0 and # Has a type (was initialized)
932+
entity.location.len > 0 and # Has been seen before (has location data)
933+
entity.id >= 0 and entity.id < 65536 and
934+
entity.id.uint16 notin presentIds:
935+
# This object was not in the step data - it was removed
936+
entity.removedAtStep = step
937+
911938
# Extend the max steps.
912939
replay.maxSteps = max(replay.maxSteps, step + 1)
913940

0 commit comments

Comments
 (0)