Spaces:
Runtime error
Runtime error
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
|
| 10 |
-
``resource_reward``
|
|
|
|
| 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 |
-
|
| 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 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
"""
|
| 82 |
post = _post_focal(scenario, game, focal_before, action, blocked)
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
r -= weights.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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
|