docker000 / game /sim /attract.py
seachen's picture
Deploy Quiet Town Corner Shop Docker demo (Gradio + Python sim) (part 5)
3831e97 verified
Raw
History Blame Contribute Delete
3.6 kB
"""Attract / match scoring (R4, R5) — pure functions."""
from __future__ import annotations
from typing import Dict, Iterable, Mapping, Optional, Sequence, Set
def clamp(x: float, lo: float = 0.0, hi: float = 1.0) -> float:
return max(lo, min(hi, x))
def l1_normalize(vec: Mapping[str, float]) -> Dict[str, float]:
cleaned = {k: max(0.0, float(v)) for k, v in vec.items()}
s = sum(cleaned.values())
if s <= 0:
return {k: 0.0 for k in cleaned}
return {k: v / s for k, v in cleaned.items()}
def preference_match(
preferred_atmosphere: Mapping[str, float],
shop_atmosphere_snapshot: Mapping[str, float],
) -> float:
"""dot(L1_norm(pref), shop_snapshot) clamped — shop dims NOT renormalized."""
pref = l1_normalize(preferred_atmosphere)
keys = set(pref) | set(shop_atmosphere_snapshot)
total = 0.0
for k in keys:
total += pref.get(k, 0.0) * float(shop_atmosphere_snapshot.get(k, 0.0))
return clamp(total)
def shop_product_match(desired_tags: Sequence[str], displayed_product_tags: Iterable[str]) -> float:
desired = set(desired_tags)
if not desired:
return 0.0
disp = set(displayed_product_tags)
return len(desired & disp) / max(1, len(desired))
def item_match(desired_tags: Sequence[str], product_tags: Sequence[str]) -> float:
desired = set(desired_tags)
if not desired:
return 0.0
return len(desired & set(product_tags)) / max(1, len(desired))
def layout_comfort(has_free_browse_spot: bool, current_customers: int, max_customers: int = 5) -> float:
free = 1.0 if has_free_browse_spot else 0.3
crowding = (current_customers / max(1, max_customers)) if max_customers else 0.0
return clamp(0.5 * free + 0.5 * (1.0 - crowding))
def relationship_pull(stage: str, table: Optional[Mapping[str, float]] = None) -> float:
default = {
"STRANGER": 0.0,
"ACQUAINTANCE": 0.25,
"FAMILIAR": 0.45,
"TRUSTED": 0.65,
"BOND": 0.85,
}
t = table or default
return float(t.get(stage, 0.0))
def compute_attract(
*,
preferred_atmosphere: Mapping[str, float],
shop_atmosphere: Mapping[str, float],
desired_tags: Sequence[str],
displayed_tags: Iterable[str],
has_free_browse_spot: bool,
current_customers: int,
stage: str = "STRANGER",
weights: Optional[Mapping[str, float]] = None,
rel_table: Optional[Mapping[str, float]] = None,
max_customers: int = 5,
weather_time_mod: float = 0.0,
) -> float:
w = {
"w_atm": 0.35,
"w_prod": 0.30,
"w_layout": 0.15,
"w_rel": 0.15,
"w_weather": 0.0,
}
if weights:
w.update({k: float(v) for k, v in weights.items()})
pref = preference_match(preferred_atmosphere, shop_atmosphere)
prod = shop_product_match(desired_tags, displayed_tags)
lay = layout_comfort(has_free_browse_spot, current_customers, max_customers)
rel = relationship_pull(stage, rel_table)
return (
w["w_atm"] * pref
+ w["w_prod"] * prod
+ w["w_layout"] * lay
+ w["w_rel"] * rel
+ w["w_weather"] * weather_time_mod
)
def displayed_tags_from_state(state: dict, products: Mapping[str, dict]) -> Set[str]:
tags: Set[str] = set()
for slot in state.get("shop", {}).get("displays", {}).values():
if not slot:
continue
pid = slot.get("product_id")
qty = int(slot.get("qty") or 0)
if not pid or qty <= 0:
continue
p = products.get(pid) or {}
tags.update(p.get("tags") or [])
return tags