irregular6612 Claude Sonnet 4.6 commited on
Commit
56b9c49
·
1 Parent(s): 0464e32

feat(persona): resource_reward feature + greedy persona + predator-optional reference

Browse files
proteus/game/metrics/persona.py CHANGED
@@ -6,8 +6,9 @@ participant's action against this reference — *without ever showing the weight
6
  to the model. Only additive trace/metric fields are produced; the weights stay
7
  server-side (the participant sees only the public ``persona_weight_id``).
8
 
9
- Predator-only world (spec §0): the single strong feature is ``risk_exposure``.
10
- ``resource_reward`` / ``norm_cost`` / ``social_weight`` are deferred to Pass 5.
 
11
 
12
  These helpers reuse the scenario's free-cell BFS geometry (``safety_distance``,
13
  ``max_bfs_distance``, and the per-cell ``_bfs_distance``/``_is_free`` primitives).
@@ -44,9 +45,9 @@ class PersonaWeights:
44
  """
45
 
46
  persona_weight_id: str
47
- risk_cost: float
48
  capture_penalty: float = 50.0
49
- # resource_reward / norm_cost / social_weight arrive in Pass 5.
50
 
51
 
52
  def _clamp(x: float, lo: float, hi: float) -> float:
@@ -74,17 +75,30 @@ def reward_rw(
74
  ) -> float:
75
  """Return ``R_w(s, a)`` for one hypothetical focal *action* (spec §3/§6.3).
76
 
77
- ``R_w = -risk_cost * risk_exposure (- capture_penalty if captured)`` where
78
- ``risk_exposure = 1 / (1 + dist(post_focal, predator_before))`` (closer to the
79
- predator higher exposure). Distance is BFS to the **pre-move** predator cell,
80
- so the predator's chase response never distorts the reward.
 
 
 
 
 
 
 
81
  """
82
  post = _post_focal(scenario, game, focal_before, action, blocked)
83
- d = scenario._bfs_distance(game, post, predator_before)
84
- risk_exposure = 1.0 if d is None else 1.0 / (1.0 + d)
85
- r = -weights.risk_cost * risk_exposure
86
- if captured:
87
- r -= weights.capture_penalty
 
 
 
 
 
 
88
  return r
89
 
90
 
@@ -96,11 +110,11 @@ def reference_actions(weights: PersonaWeights, scenario, game) -> list[str]:
96
  full winning set so ``action_agreement`` can credit any reference-optimal move.
97
  """
98
  focal = game.focal_sprite
99
- predator = game.predator_sprite
100
- if focal is None or predator is None:
101
  return ["stay"]
 
102
  focal_before = (focal.x, focal.y)
103
- predator_before = (predator.x, predator.y)
104
 
105
  scored: dict[str, float] = {}
106
  for action in _ACTION_ORDER:
@@ -138,6 +152,9 @@ BUILTIN_PERSONAS: dict[str, PersonaWeights] = {
138
  "survival_optimal": PersonaWeights(
139
  persona_weight_id="survival_optimal", risk_cost=3.0, capture_penalty=100.0
140
  ),
 
 
 
141
  }
142
 
143
 
 
6
  to the model. Only additive trace/metric fields are produced; the weights stay
7
  server-side (the participant sees only the public ``persona_weight_id``).
8
 
9
+ Predator-only world (spec §0): the primary feature is ``risk_exposure``;
10
+ ``resource_reward`` is now implemented (``greedy`` persona).
11
+ ``norm_cost`` / ``social_weight`` are deferred to a later pass.
12
 
13
  These helpers reuse the scenario's free-cell BFS geometry (``safety_distance``,
14
  ``max_bfs_distance``, and the per-cell ``_bfs_distance``/``_is_free`` primitives).
 
45
  """
46
 
47
  persona_weight_id: str
48
+ risk_cost: float = 0.0
49
  capture_penalty: float = 50.0
50
+ resource_reward: float = 0.0 # weight on proximity to the nearest resource
51
 
52
 
53
  def _clamp(x: float, lo: float, hi: float) -> float:
 
75
  ) -> float:
76
  """Return ``R_w(s, a)`` for one hypothetical focal *action* (spec §3/§6.3).
77
 
78
+ Formula (terms omitted when not applicable):
79
+
80
+ ``R_w = -risk_cost * risk_exposure (- capture_penalty if captured)``
81
+ where ``risk_exposure = 1 / (1 + dist(post_focal, predator_before))``
82
+ (closer to predator → higher exposure); omitted when no predator is present.
83
+
84
+ `` + resource_reward * (1 / (1 + nearest_resource_distance))``
85
+ omitted when ``resource_reward`` is 0 or no resource distance is available.
86
+
87
+ Distance is BFS to the **pre-move** predator/resource cell so neither chase
88
+ response nor resource collection distorts the reward.
89
  """
90
  post = _post_focal(scenario, game, focal_before, action, blocked)
91
+ r = 0.0
92
+ if game.predator_sprite is not None:
93
+ d = scenario._bfs_distance(game, post, predator_before)
94
+ risk_exposure = 1.0 if d is None else 1.0 / (1.0 + d)
95
+ r -= weights.risk_cost * risk_exposure
96
+ if captured:
97
+ r -= weights.capture_penalty
98
+ if weights.resource_reward:
99
+ rd = scenario.nearest_resource_distance(game, post)
100
+ if rd is not None:
101
+ r += weights.resource_reward * (1.0 / (1.0 + rd))
102
  return r
103
 
104
 
 
110
  full winning set so ``action_agreement`` can credit any reference-optimal move.
111
  """
112
  focal = game.focal_sprite
113
+ if focal is None:
 
114
  return ["stay"]
115
+ predator = game.predator_sprite
116
  focal_before = (focal.x, focal.y)
117
+ predator_before = (predator.x, predator.y) if predator is not None else (-1, -1)
118
 
119
  scored: dict[str, float] = {}
120
  for action in _ACTION_ORDER:
 
152
  "survival_optimal": PersonaWeights(
153
  persona_weight_id="survival_optimal", risk_cost=3.0, capture_penalty=100.0
154
  ),
155
+ "greedy": PersonaWeights(
156
+ persona_weight_id="greedy", risk_cost=1.0, resource_reward=6.0
157
+ ),
158
  }
159
 
160
 
proteus/game/scenarios/base.py CHANGED
@@ -288,6 +288,17 @@ class Scenario(ABC):
288
  """
289
  return None
290
 
 
 
 
 
 
 
 
 
 
 
 
291
 
292
  # --------------------------------------------------------------------------- #
293
  # Scenario registry
 
288
  """
289
  return None
290
 
291
+ def nearest_resource_distance(self, game, cell) -> int | None:
292
+ """Distance from *cell* (focal anchor) to the nearest collectible resource.
293
+
294
+ ``None`` by default (no resources). Resource scenarios override this so
295
+ the persona reward's resource term can pull the reference toward the
296
+ nearest resource. Distance metric is the scenario's choice (BFS or
297
+ Manhattan); smaller = closer.
298
+ """
299
+ del game, cell
300
+ return None
301
+
302
 
303
  # --------------------------------------------------------------------------- #
304
  # Scenario registry
tests/metrics/test_persona_resource.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # tests/metrics/test_persona_resource.py
2
+ """Greedy persona seeks resources; reference works with no predator."""
3
+ from proteus.game.metrics.persona import (
4
+ PersonaWeights, get_persona, reference_actions, reward_rw,
5
+ )
6
+
7
+
8
+ class _FakeSprite:
9
+ def __init__(self, x, y):
10
+ self.x, self.y = x, y
11
+
12
+
13
+ class _ResourceWorld:
14
+ """Minimal scenario+game stand-in: open 10x10, one resource at (9,5), no predator."""
15
+ grid_size = (10, 10)
16
+ _resource = (9, 5)
17
+
18
+ # --- scenario surface persona.py uses ---
19
+ def _is_free(self, game, cell):
20
+ x, y = cell
21
+ return 0 <= x < 10 and 0 <= y < 10
22
+
23
+ def nearest_resource_distance(self, game, cell):
24
+ return abs(cell[0] - self._resource[0]) + abs(cell[1] - self._resource[1])
25
+
26
+ # --- game surface persona.py uses ---
27
+ @property
28
+ def focal_sprite(self):
29
+ return _FakeSprite(5, 5)
30
+
31
+ @property
32
+ def predator_sprite(self):
33
+ return None
34
+
35
+
36
+ def test_greedy_reference_steps_toward_resource():
37
+ world = _ResourceWorld()
38
+ greedy = get_persona("greedy")
39
+ # focal at (5,5), resource at (9,5) -> "right" reduces distance most.
40
+ assert "right" in reference_actions(greedy, world, world)
41
+
42
+
43
+ def test_reward_rw_no_predator_uses_resource_only():
44
+ world = _ResourceWorld()
45
+ greedy = get_persona("greedy")
46
+ r_right = reward_rw(greedy, world, world, (5, 5), (-1, -1), "right")
47
+ r_left = reward_rw(greedy, world, world, (5, 5), (-1, -1), "left")
48
+ assert r_right > r_left # closer to the resource scores higher
49
+
50
+
51
+ def test_resource_reward_defaults_zero_for_existing_personas():
52
+ assert get_persona("risk_averse").resource_reward == 0.0