Sugutt's picture
Upload Village Gossip Economy Space
ae929d7 verified
Raw
History Blame Contribute Delete
12.7 kB
from __future__ import annotations
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import networkx as nx
from models import VillageState
from simulation import active_rumor
GRAPH_PATH = Path("network_graph.png")
def target_choices(state: VillageState) -> list[tuple[str, str]]:
return [(villager.name, villager.id) for villager in state.villagers.values()]
def current_rumor_text(state: VillageState) -> str:
rumor = active_rumor(state)
return rumor.current_version if rumor else "No rumor started yet."
def current_day_text(state: VillageState) -> str:
return f"Day {min(state.day, 8)}"
def conversations_text(state: VillageState) -> str:
if not state.latest_conversations:
return "No conversations yet."
parts = []
for item in state.latest_conversations:
speaker = state.villagers[item.speaker_id].name
listener = state.villagers[item.listener_id].name
channel = state.channels[item.channel_id].name
parts.append(f"### {speaker} -> {listener} ({item.action}, {channel})\n{item.dialogue}")
return "\n\n".join(parts)
def daily_summary_text(state: VillageState) -> str:
return state.daily_summaries[-1] if state.daily_summaries else "Start a rumor, then simulate a day."
def timeline_text(state: VillageState) -> str:
lines = []
lines.extend(state.daily_summaries)
for event in state.events:
lines.append(f"Day {event.day}: {event.title} - {event.description}")
return "\n\n".join(lines) if lines else "Timeline is waiting for the first spark."
def metrics_markdown(state: VillageState) -> str:
rumor = active_rumor(state)
if not rumor:
return "No village metrics yet."
village_trust = sum(v.trust for v in state.villagers.values()) / len(state.villagers)
chaos = min(100, rumor.intensity * 0.7 + (1 - village_trust / 100) * 30)
total_damage = 0
if rumor.target_id:
rep = state.villagers[rumor.target_id].reputation
total_damage = sum(70 - value for value in rep.model_dump().values())
return (
f"**Rumor intensity:** {rumor.intensity:.1f}\n\n"
f"**Public belief:** {rumor.public_belief:.2f}\n\n"
f"**Village trust:** {village_trust:.1f}\n\n"
f"**Chaos score:** {chaos:.1f}\n\n"
f"**Total reputation damage:** {total_damage:.1f}"
)
def evidence_markdown(state: VillageState) -> str:
rumor = active_rumor(state)
if not rumor:
return "No evidence yet."
items = [item for item in state.evidence.values() if item.rumor_id == rumor.id]
if not items:
return "No evidence has surfaced yet."
lines = []
for item in sorted(items, key=lambda evidence: evidence.reliability, reverse=True):
discoverer = state.villagers[item.discovered_by].name if item.discovered_by in state.villagers else "Unknown"
stance = "supports" if item.supports_rumor else "contradicts"
visibility = "public" if item.public else "private"
lines.append(
f"**{item.description}**\n\n"
f"{stance.title()} rumor | reliability {item.reliability:.2f} | {visibility} | found by {discoverer}"
)
return "\n\n".join(lines)
def belief_positions_markdown(state: VillageState) -> str:
rumor = active_rumor(state)
if not rumor:
return "No belief positions yet."
rows = []
for villager in sorted(state.villagers.values(), key=lambda item: item.private_beliefs.get(rumor.id, 0), reverse=True):
belief = villager.private_beliefs.get(rumor.id, 0.5)
public = villager.public_positions.get(rumor.id, "neutral").replace("_", " ")
rows.append(f"**{villager.name}:** private {belief:.2f} | public {public}")
return "\n\n".join(rows)
def leaderboards_markdown(state: VillageState) -> str:
villagers = list(state.villagers.values())
def top(label: str, key):
villager = max(villagers, key=key)
return f"**{label}:** {villager.name} ({key(villager):.1f})"
biggest_gossip = max(villagers, key=lambda v: v.attention + (100 - v.trust) * 0.2)
most_damaged = min(villagers, key=lambda v: sum(v.reputation.model_dump().values()) / 5)
lines = [
top("Most trusted", lambda v: v.trust),
top("Most influential", lambda v: v.influence),
top("Most attention", lambda v: v.attention),
f"**Most damaged reputation:** {most_damaged.name} ({sum(most_damaged.reputation.model_dump().values()) / 5:.1f})",
f"**Biggest gossip:** {biggest_gossip.name} ({biggest_gossip.attention:.1f} attention)",
top("Most accurate", lambda v: v.accuracy * 100),
]
return "\n\n".join(lines)
def memories_markdown(state: VillageState) -> str:
villagers = sorted(
state.villagers.values(),
key=lambda villager: max([abs(value) for value in villager.strategy_biases.values()] or [0]),
reverse=True,
)
lines = []
for villager in villagers:
if not villager.memories and not villager.strategy_biases:
continue
biases = sorted(villager.strategy_biases.items(), key=lambda item: abs(item[1]), reverse=True)[:3]
bias_text = ", ".join(f"{action}: {value:+.1f}" for action, value in biases) if biases else "no bias yet"
memory_text = " / ".join(memory.summary for memory in villager.memories[-2:]) if villager.memories else "No memories yet."
lines.append(f"**{villager.name}**\n\nBiases: {bias_text}\n\n{memory_text}")
return "\n\n".join(lines) if lines else "No strategy memories yet."
def judge_mode_markdown(state: VillageState) -> str:
rows = [item for item in state.trace if "action_scores" in item]
if not rows:
return (
"**Judge Mode**\n\n"
"Start a rumor and simulate a day to see the decision market. "
"Python will choose actions through scores; the LLM only narrates dialogue, rumor mutations, and summaries."
)
days = sorted({row["day"] for row in rows})
visible_days = days
sections = [
"**Judge Mode: Decision Market**",
"Python controls bids, incentives, state, belief, evidence, memory, and reputation. The LLM only adds language after winners are chosen.",
]
for day in visible_days:
day_rows = [row for row in rows if row["day"] == day]
sections.append(f"### Day {day}")
for rank, row in enumerate(day_rows, start=1):
scores = row.get("action_scores", {})
top_scores = sorted(scores.items(), key=lambda item: item[1], reverse=True)[:4]
score_text = ", ".join(f"{action} {score:.1f}" for action, score in top_scores)
memory = row.get("memory") or {}
economy = row.get("economy_changes") or {}
biases = row.get("strategy_biases") or {}
action = row.get("chosen_action", "")
bias = biases.get(action, 0)
reputation_delta = row.get("reputation_damage") or {}
rep_text = ", ".join(f"{key} {value:+.2f}" for key, value in reputation_delta.items()) or "none"
sections.append(
f"**{rank}. {row['villager']} won `{action}`** "
f"with score **{row['bid_score']:.1f}** against listener **{row['listener']}** via **{row['channel']}**.\n\n"
f"Top scores: {score_text}\n\n"
f"Learned bias on chosen action: {bias:+.1f}\n\n"
f"Economy delta: trust {economy.get('trust_delta', 0):+.1f}, "
f"influence {economy.get('influence_delta', 0):+.1f}, "
f"attention {economy.get('attention_delta', 0):+.1f}, "
f"payoff {economy.get('payoff', 0):+.1f}\n\n"
f"Belief: {row['belief_before']:.2f} -> {row['belief_after']:.2f}; reputation delta: {rep_text}\n\n"
f"Memory formed: {memory.get('summary', 'none')}\n\n"
f"Reason: {row['reason']}"
)
return "\n\n".join(sections)
def target_reputation_markdown(state: VillageState) -> str:
rumor = active_rumor(state)
if not rumor or not rumor.target_id:
return "No target selected yet."
target = state.villagers[rumor.target_id]
values = target.reputation.model_dump()
return "\n".join([f"**{name.title()}:** {score:.1f}" for name, score in values.items()])
def explainability_markdown(state: VillageState) -> str:
if not state.trace:
return "No reputation changes to explain yet."
rows = [item for item in state.trace if "reputation_damage" in item]
if not rows:
return "No reputation changes to explain yet."
row = rows[-1]
rumor = active_rumor(state)
channel = row["channel"]
target = state.villagers[rumor.target_id].name if rumor and rumor.target_id else "target"
deltas = ", ".join(f"{k}: {v}" for k, v in row["reputation_damage"].items()) or "none"
return (
f"**Why did {target}'s reputation change?**\n\n"
f"Rumor severity: {rumor.severity if rumor else 0:.1f}\n\n"
f"Chosen action: {row['chosen_action']}\n\n"
f"Speaker: {row['villager']}\n\n"
f"Listener belief: {row['belief_before']} -> {row['belief_after']}\n\n"
f"Channel: {channel}\n\n"
f"Reputation delta: {deltas}"
)
def network_graph_path(state: VillageState) -> str | None:
villagers = list(state.villagers.values())
if not villagers:
return None
rumor = active_rumor(state)
graph = nx.Graph()
for villager in villagers:
belief = villager.private_beliefs.get(rumor.id, 0.5) if rumor else 0.5
reputation = sum(villager.reputation.model_dump().values()) / 5
graph.add_node(
villager.id,
label=villager.name.replace(" ", "\n"),
influence=villager.influence,
belief=belief,
reputation=reputation,
target=bool(rumor and rumor.target_id == villager.id),
)
for villager in villagers:
for other_id, score in villager.relationships.items():
if other_id in graph and abs(score) >= 10:
graph.add_edge(villager.id, other_id, weight=abs(score), score=score)
if graph.number_of_edges() == 0:
ids = [villager.id for villager in villagers]
graph.add_edges_from(zip(ids, ids[1:]))
fig, ax = plt.subplots(figsize=(9, 6), dpi=140)
fig.patch.set_facecolor("#10151f")
ax.set_facecolor("#10151f")
pos = nx.spring_layout(graph, seed=7, k=0.7)
node_sizes = [420 + graph.nodes[node]["influence"] * 8 for node in graph.nodes]
node_colors = []
for node in graph.nodes:
data = graph.nodes[node]
if data["target"]:
node_colors.append("#f97316")
elif data["belief"] >= 0.65:
node_colors.append("#ef4444")
elif data["belief"] <= 0.35:
node_colors.append("#22c55e")
else:
node_colors.append("#38bdf8")
edge_colors = ["#ef4444" if graph.edges[edge]["score"] < 0 else "#94a3b8" for edge in graph.edges]
edge_widths = [1 + graph.edges[edge]["weight"] / 25 for edge in graph.edges]
nx.draw_networkx_edges(graph, pos, edge_color=edge_colors, width=edge_widths, alpha=0.45, ax=ax)
nx.draw_networkx_nodes(graph, pos, node_size=node_sizes, node_color=node_colors, linewidths=1.5, edgecolors="#f8fafc", ax=ax)
nx.draw_networkx_labels(
graph,
pos,
labels={node: graph.nodes[node]["label"] for node in graph.nodes},
font_size=7,
font_color="#f8fafc",
ax=ax,
)
ax.set_title("Trust, Belief, and Influence Network", color="#f8fafc", fontsize=13, pad=14)
ax.text(
0.01,
0.01,
"Orange target | Red believers | Green skeptics | Blue undecided",
transform=ax.transAxes,
color="#cbd5e1",
fontsize=8,
)
ax.axis("off")
fig.tight_layout()
fig.savefig(GRAPH_PATH, bbox_inches="tight", facecolor=fig.get_facecolor())
plt.close(fig)
return str(GRAPH_PATH.resolve())
def ui_snapshot(state: VillageState) -> tuple:
return (
current_day_text(state),
current_rumor_text(state),
conversations_text(state),
daily_summary_text(state),
timeline_text(state),
state.baraza_output or "The baraza will be called after Day 7.",
metrics_markdown(state),
evidence_markdown(state),
belief_positions_markdown(state),
leaderboards_markdown(state),
memories_markdown(state),
judge_mode_markdown(state),
target_reputation_markdown(state),
explainability_markdown(state),
network_graph_path(state),
)