Skip to content

Commit fcb51af

Browse files
committed
Add protocol sigmoid/inflation pricing
Adds dynamic pricing to assembler protocols: - sigmoid: Number of discounted uses (linear 0→1 scaling) - inflation: Compound rate for exponential cost after sigmoid uses
1 parent e49ff8d commit fcb51af

File tree

5 files changed

+98
-24
lines changed

5 files changed

+98
-24
lines changed

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,7 @@
44
#include <pybind11/pybind11.h>
55
#include <pybind11/stl.h>
66

7+
#include <cmath>
78
#include <memory>
89
#include <unordered_map>
910

@@ -16,13 +17,41 @@ class Protocol {
1617
std::unordered_map<InventoryItem, InventoryQuantity> input_resources;
1718
std::unordered_map<InventoryItem, InventoryQuantity> output_resources;
1819
unsigned short cooldown;
20+
unsigned int sigmoid; // Centroid for inflation curve
21+
float inflation; // Compound rate for cost adjustment
22+
23+
// Track activation count for inflation calculation
24+
mutable unsigned int activation_count;
1925

2026
Protocol(unsigned short min_agents = 0,
2127
const std::vector<ObservationType>& vibes = {},
2228
const std::unordered_map<InventoryItem, InventoryQuantity>& inputs = {},
2329
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) {}
30+
unsigned short cooldown = 0,
31+
unsigned int sigmoid = 0,
32+
float inflation = 0.0f)
33+
: min_agents(min_agents),
34+
vibes(vibes),
35+
input_resources(inputs),
36+
output_resources(outputs),
37+
cooldown(cooldown),
38+
sigmoid(sigmoid),
39+
inflation(inflation),
40+
activation_count(0) {}
41+
42+
// Calculate cost multiplier based on activation count
43+
// - Linear phase (0 to sigmoid uses): scales from 0 (free) to 1 (full price)
44+
// - Exponential phase (after sigmoid uses): multiplier = (1 + inflation) ^ (activation_count - sigmoid)
45+
float get_cost_multiplier() const {
46+
// Linear phase: free to full price for first 'sigmoid' uses
47+
if (sigmoid > 0 && activation_count < sigmoid) {
48+
return static_cast<float>(activation_count) / static_cast<float>(sigmoid);
49+
}
50+
// Exponential phase: inflating costs after sigmoid uses
51+
if (inflation == 0.0f) return 1.0f;
52+
int exponent = static_cast<int>(activation_count) - static_cast<int>(sigmoid);
53+
return std::pow(1.0f + inflation, static_cast<float>(exponent));
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("sigmoid", &Protocol::sigmoid)
66+
.def_readwrite("inflation", &Protocol::inflation)
67+
.def_readwrite("activation_count", &Protocol::activation_count);
3668
}
3769

3870
#endif // PACKAGES_METTAGRID_CPP_INCLUDE_METTAGRID_OBJECTS_PROTOCOL_HPP_

packages/mettagrid/python/src/mettagrid/config/mettagrid_c_config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,8 @@ def convert_to_cpp_game_config(mettagrid_config: dict | GameConfig):
286286
cpp_protocol.input_resources = input_res
287287
cpp_protocol.output_resources = output_res
288288
cpp_protocol.cooldown = protocol_config.cooldown
289+
cpp_protocol.sigmoid = protocol_config.sigmoid
290+
cpp_protocol.inflation = protocol_config.inflation
289291
protocols.append(cpp_protocol)
290292

291293
# Convert tag names to IDs
@@ -522,6 +524,8 @@ def process_action_config(action_name: str, action_config):
522524
output_res[key] = val
523525
cpp_protocol.output_resources = output_res
524526
cpp_protocol.cooldown = protocol_config.cooldown
527+
cpp_protocol.sigmoid = protocol_config.sigmoid
528+
cpp_protocol.inflation = protocol_config.inflation
525529
clipper_protocols.append(cpp_protocol)
526530
clipper_config = CppClipperConfig()
527531
clipper_config.unclipping_protocols = clipper_protocols

packages/mettagrid/python/src/mettagrid/config/mettagrid_config.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,16 @@ class ProtocolConfig(Config):
384384
input_resources: dict[str, int] = Field(default_factory=dict)
385385
output_resources: dict[str, int] = Field(default_factory=dict)
386386
cooldown: int = Field(ge=0, default=0)
387+
sigmoid: int = Field(
388+
default=0,
389+
ge=0,
390+
description="Number of discounted uses. Cost scales linearly from 0 (free) to 1 (full price) over these uses.",
391+
)
392+
inflation: float = Field(
393+
default=0.0,
394+
ge=0.0,
395+
description="Compound rate for exponential cost. Cost = base * (1+inflation)^(n - sigmoid).",
396+
)
387397

388398

389399
class AssemblerConfig(GridObjectConfig):

packages/mettagrid/python/src/mettagrid/mettagrid_c.pyi

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,9 @@ class Protocol:
131131
input_resources: dict[int, int]
132132
output_resources: dict[int, int]
133133
cooldown: int
134+
sigmoid: int
135+
inflation: float
136+
activation_count: int
134137

135138
class InventoryConfig:
136139
def __init__(

packages/mettagrid/tests/test_swap_frozen.py

Lines changed: 46 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
AgentConfig,
88
AgentRewards,
99
AttackActionConfig,
10+
AttackOutcome,
11+
ChangeVibeActionConfig,
1012
GameConfig,
1113
InventoryConfig,
1214
MettaGridConfig,
@@ -17,11 +19,20 @@
1719
WallConfig,
1820
)
1921
from mettagrid.simulator import Simulation
20-
from mettagrid.test_support.actions import attack, get_agent_position, move
22+
from mettagrid.test_support.actions import get_agent_position, move
2123
from mettagrid.test_support.map_builders import ObjectNameMapBuilder
2224
from mettagrid.test_support.orientation import Orientation
2325

2426

27+
def get_agent_frozen_status(sim: Simulation, agent_id: int) -> bool:
28+
"""Check if an agent is frozen."""
29+
grid_objects = sim.grid_objects()
30+
for obj in grid_objects.values():
31+
if obj.get("agent_id") == agent_id:
32+
return obj.get("is_frozen", False)
33+
return False
34+
35+
2536
@pytest.fixture
2637
def base_config():
2738
"""Base configuration for swap tests."""
@@ -40,7 +51,12 @@ def base_config():
4051
"west",
4152
]
4253
),
43-
attack=AttackActionConfig(enabled=True, consumed_resources={"laser": 1}, defense_resources={"armor": 1}),
54+
change_vibe=ChangeVibeActionConfig(number_of_vibes=10),
55+
attack=AttackActionConfig(
56+
enabled=False, # Attack triggers via move, not standalone action
57+
vibes=["charger"], # Attack triggers when agent has charger vibe
58+
success=AttackOutcome(freeze=10),
59+
),
4460
),
4561
objects={
4662
"wall": WallConfig(),
@@ -135,26 +151,35 @@ def test_swap_with_frozen_agent(make_sim, adjacent_agents_map):
135151
assert pos_agent0_before == (1, 1), f"Agent 0 should be at (1, 1), got {pos_agent0_before}"
136152
assert pos_agent1_before == (1, 2), f"Agent 1 should be at (1, 2), got {pos_agent1_before}"
137153

138-
# Agent 0 attacks Agent 1 to freeze them
139-
attack_result = attack(sim, target_arg=0, agent_idx=0)
140-
print(f"Attack result: {attack_result}")
141-
assert attack_result["success"], f"Attack should succeed: {attack_result}"
154+
# Verify neither agent is frozen initially
155+
assert not get_agent_frozen_status(sim, 0), "Agent 0 should not start frozen"
156+
assert not get_agent_frozen_status(sim, 1), "Agent 1 should not start frozen"
142157

143-
# Verify agent 1 is frozen
144-
grid_objects = sim.grid_objects()
145-
agent1_obj = None
146-
for _obj_id, obj in grid_objects.items():
147-
if obj["type_name"] == "agent":
148-
pos = (obj["r"], obj["c"])
149-
if pos == pos_agent1_before:
150-
agent1_obj = obj
151-
break
152-
153-
assert agent1_obj is not None, "Should find agent 1"
154-
assert agent1_obj.get("is_frozen", False), f"Agent 1 should be frozen: {agent1_obj}"
155-
print(f"Agent 1 frozen status: {agent1_obj.get('is_frozen')}")
156-
157-
# Now agent 0 tries to move east onto frozen agent 1
158+
# Agent 0 changes vibe to charger (to enable attack on move)
159+
sim.agent(0).set_action("change_vibe_charger")
160+
sim.agent(1).set_action("noop")
161+
sim.step()
162+
163+
# Agent 0 moves east into Agent 1 - should trigger attack due to weapon vibe
164+
sim.agent(0).set_action("move_east")
165+
sim.agent(1).set_action("noop")
166+
sim.step()
167+
168+
# Verify agent 1 is frozen from the attack
169+
assert get_agent_frozen_status(sim, 1), "Agent 1 should be frozen after attack"
170+
print("Agent 1 frozen status: True")
171+
172+
# Verify positions didn't change on first move (attack happened but no swap yet)
173+
pos_agent0_mid = get_agent_position(sim, 0)
174+
pos_agent1_mid = get_agent_position(sim, 1)
175+
print(f"After attack - Agent 0: {pos_agent0_mid}, Agent 1: {pos_agent1_mid}")
176+
177+
# Agent 0 changes vibe back to default so next move doesn't trigger attack
178+
sim.agent(0).set_action("change_vibe_default")
179+
sim.agent(1).set_action("noop")
180+
sim.step()
181+
182+
# Now agent 0 tries to move east onto frozen agent 1 - should swap
158183
move_result = move(sim, Orientation.EAST, agent_idx=0)
159184
print(f"Move result: {move_result}")
160185

0 commit comments

Comments
 (0)