RayMelius Claude Opus 4.6 commited on
Commit
4ff38f2
Β·
1 Parent(s): 0c73e6f

Add life events, goals, pregnancy system; fix profile editor bugs

Browse files

- Life events: append-only timeline seeded from persona, extended by
LLM reflections and romance milestones (dating/engaged/married/birth)
- Goals: personal aspirations with progress tracking, managed by LLM
reflections (add/complete/progress)
- Pregnancy: married couples can conceive, birth after ~7 sim-days,
baby names, both parents get memories and life events
- Agent context: LLM now sees life story, goals, children, pregnancy
- Fix profile editor: fetch full agent detail instead of WS summary
(occupation, background, personality sliders now load correctly)
- Fix Go dropdown: only rebuild when empty, preserves user selection
- Fix WS state summary: include age, occupation, daily_plan, is_player
- NN self-improve: track accuracy in training_stats.json, compare with
previous model on push

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

scripts/nn_selfimprove.py CHANGED
@@ -465,7 +465,7 @@ def train(epochs: int = 20, batch_size: int = 512, lr: float = 3e-4):
465
  # STEP 3: PUSH β€” Upload improved model to HuggingFace Hub
466
  # ════════════════════════════════════════════════════════════════════════
467
 
468
- def push(repo_id: str = "RayMelius/soci-agent-nn"):
469
  """Push the retrained ONNX model to HuggingFace Hub."""
470
  from huggingface_hub import HfApi, login
471
 
@@ -480,6 +480,21 @@ def push(repo_id: str = "RayMelius/soci-agent-nn"):
480
 
481
  login(token=token)
482
  api = HfApi()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
483
  api.create_repo(repo_id, exist_ok=True)
484
 
485
  # Upload ONNX
@@ -508,6 +523,8 @@ def push(repo_id: str = "RayMelius/soci-agent-nn"):
508
  "model_size_kb": ONNX_PATH.stat().st_size / 1024,
509
  "timestamp": time.strftime("%Y-%m-%dT%H:%M:%S"),
510
  }
 
 
511
  stats_path = MODEL_DIR / "training_stats.json"
512
  stats_path.write_text(json.dumps(stats, indent=2))
513
  api.upload_file(
@@ -911,7 +928,7 @@ async def scheduled(
911
  # 7. Push improved model
912
  if os.environ.get("HF_TOKEN"):
913
  logger.info("Pushing improved model to HF Hub...")
914
- push(repo_id=repo_id)
915
  else:
916
  logger.warning("HF_TOKEN not set β€” skipping push")
917
 
@@ -1022,10 +1039,11 @@ def main():
1022
  asyncio.run(collect(base_url=args.url, duration_minutes=args.minutes))
1023
 
1024
  if args.mode in ("train", "all"):
1025
- train(epochs=args.epochs)
1026
 
1027
  if args.mode in ("push", "all"):
1028
- push(repo_id=args.repo)
 
1029
 
1030
  if args.mode == "scheduled":
1031
  asyncio.run(scheduled(
 
465
  # STEP 3: PUSH β€” Upload improved model to HuggingFace Hub
466
  # ════════════════════════════════════════════════════════════════════════
467
 
468
+ def push(repo_id: str = "RayMelius/soci-agent-nn", accuracy: float = None):
469
  """Push the retrained ONNX model to HuggingFace Hub."""
470
  from huggingface_hub import HfApi, login
471
 
 
480
 
481
  login(token=token)
482
  api = HfApi()
483
+
484
+ # Compare against previous accuracy if available
485
+ try:
486
+ from huggingface_hub import hf_hub_download
487
+ prev_stats_path = hf_hub_download(repo_id=repo_id, filename="training_stats.json", token=token)
488
+ prev_stats = json.loads(open(prev_stats_path).read())
489
+ prev_acc = prev_stats.get("best_accuracy")
490
+ if prev_acc is not None and accuracy is not None:
491
+ delta = accuracy - prev_acc
492
+ symbol = "+" if delta >= 0 else ""
493
+ logger.info(f"Previous accuracy: {prev_acc:.1%} β†’ New: {accuracy:.1%} ({symbol}{delta:.1%})")
494
+ elif prev_acc is not None:
495
+ logger.info(f"Previous accuracy: {prev_acc:.1%} (no new accuracy to compare)")
496
+ except Exception:
497
+ logger.info("No previous training_stats.json found β€” first push")
498
  api.create_repo(repo_id, exist_ok=True)
499
 
500
  # Upload ONNX
 
523
  "model_size_kb": ONNX_PATH.stat().st_size / 1024,
524
  "timestamp": time.strftime("%Y-%m-%dT%H:%M:%S"),
525
  }
526
+ if accuracy is not None:
527
+ stats["best_accuracy"] = round(accuracy, 4)
528
  stats_path = MODEL_DIR / "training_stats.json"
529
  stats_path.write_text(json.dumps(stats, indent=2))
530
  api.upload_file(
 
928
  # 7. Push improved model
929
  if os.environ.get("HF_TOKEN"):
930
  logger.info("Pushing improved model to HF Hub...")
931
+ push(repo_id=repo_id, accuracy=best_acc)
932
  else:
933
  logger.warning("HF_TOKEN not set β€” skipping push")
934
 
 
1039
  asyncio.run(collect(base_url=args.url, duration_minutes=args.minutes))
1040
 
1041
  if args.mode in ("train", "all"):
1042
+ best_acc = train(epochs=args.epochs)
1043
 
1044
  if args.mode in ("push", "all"):
1045
+ acc = best_acc if args.mode == "all" else None
1046
+ push(repo_id=args.repo, accuracy=acc)
1047
 
1048
  if args.mode == "scheduled":
1049
  asyncio.run(scheduled(
src/soci/agents/agent.py CHANGED
@@ -77,6 +77,18 @@ class Agent:
77
  # Romance: current partner ID (dating/engaged/married)
78
  self.partner_id: Optional[str] = None
79
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  @property
81
  def is_busy(self) -> bool:
82
  return self._action_ticks_remaining > 0
@@ -195,6 +207,55 @@ class Agent:
195
  """Reset plan flag for a new day."""
196
  self._has_plan_today = False
197
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
  def build_context(self, tick: int, world_description: str, location_description: str) -> str:
199
  """Build the full context string for LLM prompts."""
200
  parts = [
@@ -207,12 +268,30 @@ class Agent:
207
  f"",
208
  f"WORLD: {world_description}",
209
  f"",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
  f"PEOPLE I KNOW:",
211
  self.relationships.describe_known_people(),
212
  f"",
213
  f"RECENT MEMORIES:",
214
  self.memory.context_summary(tick),
215
- ]
216
  if self.daily_plan:
217
  parts.insert(5, f"- Today's plan: {'; '.join(self.daily_plan)}")
218
  return "\n".join(parts)
@@ -245,6 +324,13 @@ class Agent:
245
  "last_llm_tick": self._last_llm_tick,
246
  "is_player": self.is_player,
247
  "partner_id": self.partner_id,
 
 
 
 
 
 
 
248
  }
249
 
250
  @classmethod
@@ -265,4 +351,11 @@ class Agent:
265
  agent._last_llm_tick = data["last_llm_tick"]
266
  agent.is_player = data["is_player"]
267
  agent.partner_id = data.get("partner_id")
 
 
 
 
 
 
 
268
  return agent
 
77
  # Romance: current partner ID (dating/engaged/married)
78
  self.partner_id: Optional[str] = None
79
 
80
+ # Life history β€” append-only timeline of significant events
81
+ self.life_events: list[dict] = []
82
+ # Personal goals β€” evolving aspirations
83
+ self.goals: list[dict] = []
84
+ self._next_goal_id: int = 0
85
+ # Pregnancy (female agents)
86
+ self.pregnant: bool = False
87
+ self.pregnancy_start_tick: int = 0
88
+ self.pregnancy_partner_id: Optional[str] = None
89
+ # Children (name strings)
90
+ self.children: list[str] = []
91
+
92
  @property
93
  def is_busy(self) -> bool:
94
  return self._action_ticks_remaining > 0
 
207
  """Reset plan flag for a new day."""
208
  self._has_plan_today = False
209
 
210
+ def add_life_event(self, day: int, tick: int, event_type: str, description: str) -> dict:
211
+ """Record a significant life event (promotion, marriage, birth, etc.)."""
212
+ event = {"day": day, "tick": tick, "type": event_type, "description": description}
213
+ self.life_events.append(event)
214
+ return event
215
+
216
+ def add_goal(self, description: str, status: str = "active") -> dict:
217
+ """Add a personal goal."""
218
+ goal = {"id": self._next_goal_id, "description": description, "status": status, "progress": 0.0}
219
+ self._next_goal_id += 1
220
+ self.goals.append(goal)
221
+ return goal
222
+
223
+ def update_goal(self, goal_id: int, status: str = None, progress: float = None) -> None:
224
+ """Update goal status or progress."""
225
+ for g in self.goals:
226
+ if g["id"] == goal_id:
227
+ if status:
228
+ g["status"] = status
229
+ if progress is not None:
230
+ g["progress"] = min(1.0, max(0.0, progress))
231
+ break
232
+
233
+ def seed_biography(self, day: int, tick: int) -> None:
234
+ """Generate initial life events from persona background. Called on sim reset."""
235
+ p = self.persona
236
+ # Origin event
237
+ self.add_life_event(0, 0, "origin", f"Born and raised. {p.background}")
238
+ # Career event
239
+ if p.occupation and p.occupation.lower() not in ("newcomer", "unknown", "unemployed"):
240
+ self.add_life_event(0, 0, "career", f"Works as {p.occupation}")
241
+ # Seed initial goals based on persona
242
+ occ_lower = (p.occupation or "").lower()
243
+ if "student" in occ_lower:
244
+ self.add_goal("Graduate and find a career")
245
+ elif p.age < 30:
246
+ self.add_goal("Advance in my career")
247
+ if p.age >= 20 and p.age < 40:
248
+ self.add_goal("Find meaningful relationships")
249
+ if p.age >= 30:
250
+ self.add_goal("Build a stable and happy life")
251
+
252
+ def biography_summary(self) -> str:
253
+ """Short biography string for LLM context."""
254
+ parts = []
255
+ for e in self.life_events[-10:]:
256
+ parts.append(f"Day {e['day']}: {e['description']}")
257
+ return "\n".join(parts) if parts else "No significant life events yet."
258
+
259
  def build_context(self, tick: int, world_description: str, location_description: str) -> str:
260
  """Build the full context string for LLM prompts."""
261
  parts = [
 
268
  f"",
269
  f"WORLD: {world_description}",
270
  f"",
271
+ f"MY LIFE STORY:",
272
+ self.biography_summary(),
273
+ f"",
274
+ ]
275
+ if self.children:
276
+ parts.append(f"MY CHILDREN: {', '.join(self.children)}")
277
+ parts.append("")
278
+ if self.pregnant:
279
+ parts.append("I am currently pregnant.")
280
+ parts.append("")
281
+ active_goals = [g for g in self.goals if g["status"] == "active"]
282
+ if active_goals:
283
+ parts.append("MY GOALS:")
284
+ for g in active_goals:
285
+ pct = int(g.get("progress", 0) * 100)
286
+ parts.append(f"- {g['description']} ({pct}% progress)")
287
+ parts.append("")
288
+ parts.extend([
289
  f"PEOPLE I KNOW:",
290
  self.relationships.describe_known_people(),
291
  f"",
292
  f"RECENT MEMORIES:",
293
  self.memory.context_summary(tick),
294
+ ])
295
  if self.daily_plan:
296
  parts.insert(5, f"- Today's plan: {'; '.join(self.daily_plan)}")
297
  return "\n".join(parts)
 
324
  "last_llm_tick": self._last_llm_tick,
325
  "is_player": self.is_player,
326
  "partner_id": self.partner_id,
327
+ "life_events": self.life_events,
328
+ "goals": self.goals,
329
+ "_next_goal_id": self._next_goal_id,
330
+ "pregnant": self.pregnant,
331
+ "pregnancy_start_tick": self.pregnancy_start_tick,
332
+ "pregnancy_partner_id": self.pregnancy_partner_id,
333
+ "children": self.children,
334
  }
335
 
336
  @classmethod
 
351
  agent._last_llm_tick = data["last_llm_tick"]
352
  agent.is_player = data["is_player"]
353
  agent.partner_id = data.get("partner_id")
354
+ agent.life_events = data.get("life_events", [])
355
+ agent.goals = data.get("goals", [])
356
+ agent._next_goal_id = data.get("_next_goal_id", 0)
357
+ agent.pregnant = data.get("pregnant", False)
358
+ agent.pregnancy_start_tick = data.get("pregnancy_start_tick", 0)
359
+ agent.pregnancy_partner_id = data.get("pregnancy_partner_id")
360
+ agent.children = data.get("children", [])
361
  return agent
src/soci/api/routes.py CHANGED
@@ -165,6 +165,7 @@ async def get_agent(agent_id: str):
165
  "age": p.age,
166
  "gender": p.gender,
167
  "occupation": p.occupation,
 
168
  "traits": p.trait_summary,
169
  "personality": {
170
  "openness": getattr(p, "openness", 5),
@@ -207,6 +208,10 @@ async def get_agent(agent_id: str):
207
  }
208
  for m in agent.memory.get_recent(10)
209
  ],
 
 
 
 
210
  }
211
 
212
 
 
165
  "age": p.age,
166
  "gender": p.gender,
167
  "occupation": p.occupation,
168
+ "background": p.background,
169
  "traits": p.trait_summary,
170
  "personality": {
171
  "openness": getattr(p, "openness", 5),
 
208
  }
209
  for m in agent.memory.get_recent(10)
210
  ],
211
+ "life_events": agent.life_events[-20:],
212
+ "goals": agent.goals,
213
+ "pregnant": agent.pregnant,
214
+ "children": agent.children,
215
  }
216
 
217
 
src/soci/engine/llm.py CHANGED
@@ -1080,10 +1080,21 @@ Respond with a JSON object:
1080
  {{
1081
  "reflections": ["reflection 1", "reflection 2", ...],
1082
  "mood_shift": -0.3 to 0.3,
1083
- "reasoning": "why your mood shifted this way"
 
 
1084
  }}
1085
 
1086
  Generate 1-3 reflections. Each should be a genuine insight, not just a summary.
 
 
 
 
 
 
 
 
 
1087
  """
1088
 
1089
  CONVERSATION_PROMPT = """\
 
1080
  {{
1081
  "reflections": ["reflection 1", "reflection 2", ...],
1082
  "mood_shift": -0.3 to 0.3,
1083
+ "reasoning": "why your mood shifted this way",
1084
+ "life_event": null,
1085
+ "goal_update": null
1086
  }}
1087
 
1088
  Generate 1-3 reflections. Each should be a genuine insight, not just a summary.
1089
+
1090
+ If something truly significant happened recently (a promotion, finishing a project, personal milestone,
1091
+ making a close friend, learning something important), set life_event to:
1092
+ {{"type": "promotion|graduated|achievement|milestone|new_job|moved|breakup|friendship", "description": "what happened"}}
1093
+ Most reflections should have life_event as null β€” only include when genuinely noteworthy.
1094
+
1095
+ If you want to set a new goal or update progress on an existing one, set goal_update to:
1096
+ {{"action": "add|complete|progress", "description": "goal text", "goal_id": null}}
1097
+ For "complete" or "progress", include the goal_id number. For "add", include description only.
1098
  """
1099
 
1100
  CONVERSATION_PROMPT = """\
src/soci/engine/simulation.py CHANGED
@@ -80,6 +80,9 @@ class Simulation:
80
  """Add an agent to the simulation and place them in the city."""
81
  self.agents[agent.id] = agent
82
  self.city.place_agent(agent.id, agent.location)
 
 
 
83
 
84
  def load_agents_from_yaml(self, path: str) -> None:
85
  """Load all personas from YAML and create agents."""
@@ -338,6 +341,9 @@ class Simulation:
338
  # 9. Romance β€” develop attractions and relationships
339
  self._tick_romance()
340
 
 
 
 
341
  # 10. Advance clock
342
  self.clock.tick()
343
 
@@ -711,6 +717,42 @@ class Simulation:
711
  if reflections:
712
  self._emit(f" [REFLECT] {agent.name}: {reflections[0]}")
713
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
714
  def _tick_romance(self) -> None:
715
  """Develop romantic attractions between compatible agents at the same location."""
716
  agents_list = list(self.agents.values())
@@ -790,6 +832,8 @@ class Simulation:
790
  content=f"I'm now dating {o.name}! I feel excited and nervous.",
791
  importance=9, involved_agents=[o.id],
792
  )
 
 
793
  agent.mood = min(1.0, agent.mood + 0.3)
794
  other.mood = min(1.0, other.mood + 0.3)
795
 
@@ -806,6 +850,8 @@ class Simulation:
806
  content=f"{o.name} and I are engaged! This is the happiest day of my life.",
807
  importance=10, involved_agents=[o.id],
808
  )
 
 
809
  agent.mood = min(1.0, agent.mood + 0.4)
810
  other.mood = min(1.0, other.mood + 0.4)
811
 
@@ -822,6 +868,83 @@ class Simulation:
822
  content=f"I married {o.name} today. I couldn't be happier.",
823
  importance=10, involved_agents=[o.id],
824
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
825
 
826
  def get_state_summary(self) -> dict:
827
  """Get a summary of the current simulation state."""
@@ -832,13 +955,19 @@ class Simulation:
832
  "agents": {
833
  aid: {
834
  "name": a.name,
 
835
  "gender": a.persona.gender,
 
836
  "location": a.location,
837
  "state": a.state.value,
838
  "mood": round(a.mood, 2),
839
  "needs": a.needs.to_dict(),
840
  "action": a.current_action.detail if a.current_action else "idle",
 
841
  "partner_id": a.partner_id,
 
 
 
842
  }
843
  for aid, a in self.agents.items()
844
  },
 
80
  """Add an agent to the simulation and place them in the city."""
81
  self.agents[agent.id] = agent
82
  self.city.place_agent(agent.id, agent.location)
83
+ # Seed biography from persona on first creation
84
+ if not agent.life_events:
85
+ agent.seed_biography(self.clock.day, self.clock.total_ticks)
86
 
87
  def load_agents_from_yaml(self, path: str) -> None:
88
  """Load all personas from YAML and create agents."""
 
341
  # 9. Romance β€” develop attractions and relationships
342
  self._tick_romance()
343
 
344
+ # 9b. Pregnancy β€” check for new pregnancies and births
345
+ self._tick_pregnancy()
346
+
347
  # 10. Advance clock
348
  self.clock.tick()
349
 
 
717
  if reflections:
718
  self._emit(f" [REFLECT] {agent.name}: {reflections[0]}")
719
 
720
+ # Life event from reflection (optional β€” LLM may return null)
721
+ life_event = result.get("life_event")
722
+ if life_event and isinstance(life_event, dict):
723
+ evt_type = life_event.get("type", "milestone")
724
+ evt_desc = life_event.get("description", "")
725
+ if evt_desc:
726
+ agent.add_life_event(self.clock.day, self.clock.total_ticks, evt_type, evt_desc)
727
+ agent.add_observation(
728
+ tick=self.clock.total_ticks, day=self.clock.day,
729
+ time_str=self.clock.time_str,
730
+ content=f"Life milestone: {evt_desc}", importance=9,
731
+ )
732
+ self._emit(f" [LIFE] {agent.name}: {evt_desc}")
733
+
734
+ # Goal updates from reflection (optional)
735
+ goal_update = result.get("goal_update")
736
+ if goal_update and isinstance(goal_update, dict):
737
+ action = goal_update.get("action", "")
738
+ if action == "add" and goal_update.get("description"):
739
+ agent.add_goal(goal_update["description"])
740
+ self._emit(f" [GOAL] {agent.name} new goal: {goal_update['description']}")
741
+ elif action == "complete" and goal_update.get("goal_id") is not None:
742
+ try:
743
+ gid = int(goal_update["goal_id"])
744
+ agent.update_goal(gid, status="completed", progress=1.0)
745
+ self._emit(f" [GOAL] {agent.name} completed a goal!")
746
+ except (TypeError, ValueError):
747
+ pass
748
+ elif action == "progress" and goal_update.get("goal_id") is not None:
749
+ try:
750
+ gid = int(goal_update["goal_id"])
751
+ prog = float(goal_update.get("progress", 0.5))
752
+ agent.update_goal(gid, progress=prog)
753
+ except (TypeError, ValueError):
754
+ pass
755
+
756
  def _tick_romance(self) -> None:
757
  """Develop romantic attractions between compatible agents at the same location."""
758
  agents_list = list(self.agents.values())
 
832
  content=f"I'm now dating {o.name}! I feel excited and nervous.",
833
  importance=9, involved_agents=[o.id],
834
  )
835
+ a.add_life_event(self.clock.day, self.clock.total_ticks,
836
+ "dating", f"Started dating {o.name}")
837
  agent.mood = min(1.0, agent.mood + 0.3)
838
  other.mood = min(1.0, other.mood + 0.3)
839
 
 
850
  content=f"{o.name} and I are engaged! This is the happiest day of my life.",
851
  importance=10, involved_agents=[o.id],
852
  )
853
+ a.add_life_event(self.clock.day, self.clock.total_ticks,
854
+ "engaged", f"Got engaged to {o.name}")
855
  agent.mood = min(1.0, agent.mood + 0.4)
856
  other.mood = min(1.0, other.mood + 0.4)
857
 
 
868
  content=f"I married {o.name} today. I couldn't be happier.",
869
  importance=10, involved_agents=[o.id],
870
  )
871
+ a.add_life_event(self.clock.day, self.clock.total_ticks,
872
+ "married", f"Married {o.name}")
873
+
874
+ def _tick_pregnancy(self) -> None:
875
+ """Handle pregnancy for married couples. Children are born after ~7 sim-days."""
876
+ import random as _rand
877
+ PREGNANCY_DURATION_TICKS = 672 # ~7 days (96 ticks/day at 15-min intervals)
878
+
879
+ for agent in list(self.agents.values()):
880
+ # New pregnancy chance: married female, at home with partner
881
+ if (agent.persona.gender == "female"
882
+ and not agent.pregnant
883
+ and agent.partner_id
884
+ and agent.partner_id in self.agents
885
+ and len(agent.children) < 3): # max 3 children
886
+ partner = self.agents[agent.partner_id]
887
+ rel = agent.relationships.get(partner.id)
888
+ if (rel and rel.relationship_status == "married"
889
+ and agent.location == partner.location
890
+ and agent.location == agent.persona.home_location
891
+ and _rand.random() < 0.002):
892
+ agent.pregnant = True
893
+ agent.pregnancy_start_tick = self.clock.total_ticks
894
+ agent.pregnancy_partner_id = partner.id
895
+ agent.add_life_event(self.clock.day, self.clock.total_ticks,
896
+ "pregnant", f"Expecting a baby with {partner.name}!")
897
+ partner.add_life_event(self.clock.day, self.clock.total_ticks,
898
+ "pregnant", f"{agent.name} and I are expecting a baby!")
899
+ for a in (agent, partner):
900
+ a.add_observation(
901
+ tick=self.clock.total_ticks, day=self.clock.day,
902
+ time_str=self.clock.time_str,
903
+ content=f"We're going to have a baby!",
904
+ importance=10, involved_agents=[partner.id if a == agent else agent.id],
905
+ )
906
+ a.mood = min(1.0, a.mood + 0.4)
907
+ self._emit(f" [LIFE] {agent.name} and {partner.name} are expecting!")
908
+
909
+ # Birth check
910
+ if agent.pregnant:
911
+ elapsed = self.clock.total_ticks - agent.pregnancy_start_tick
912
+ if elapsed >= PREGNANCY_DURATION_TICKS:
913
+ partner = self.agents.get(agent.pregnancy_partner_id)
914
+ # Pick a baby name
915
+ import random as _r
916
+ baby_names_m = ["Oliver", "Liam", "Noah", "Elias", "Lucas", "Theo", "Leo", "Max"]
917
+ baby_names_f = ["Emma", "Olivia", "Sophia", "Mia", "Isabella", "Zoe", "Luna", "Aria"]
918
+ is_girl = _r.random() < 0.5
919
+ pool = baby_names_f if is_girl else baby_names_m
920
+ # Avoid duplicate names
921
+ used = set(agent.children)
922
+ available = [n for n in pool if n not in used]
923
+ baby_name = _r.choice(available) if available else _r.choice(pool)
924
+
925
+ agent.pregnant = False
926
+ agent.children.append(baby_name)
927
+ agent.add_life_event(self.clock.day, self.clock.total_ticks,
928
+ "child_born", f"Gave birth to {baby_name}!")
929
+ agent.add_observation(
930
+ tick=self.clock.total_ticks, day=self.clock.day,
931
+ time_str=self.clock.time_str,
932
+ content=f"Our baby {baby_name} was born today! I'm overwhelmed with joy.",
933
+ importance=10,
934
+ )
935
+ if partner:
936
+ partner.children.append(baby_name)
937
+ partner.add_life_event(self.clock.day, self.clock.total_ticks,
938
+ "child_born", f"{agent.name} and I welcomed {baby_name}!")
939
+ partner.add_observation(
940
+ tick=self.clock.total_ticks, day=self.clock.day,
941
+ time_str=self.clock.time_str,
942
+ content=f"Our baby {baby_name} was born! I'm a parent now!",
943
+ importance=10,
944
+ )
945
+ partner.mood = min(1.0, partner.mood + 0.5)
946
+ agent.mood = min(1.0, agent.mood + 0.5)
947
+ self._emit(f" [LIFE] {agent.name} gave birth to {baby_name}!")
948
 
949
  def get_state_summary(self) -> dict:
950
  """Get a summary of the current simulation state."""
 
955
  "agents": {
956
  aid: {
957
  "name": a.name,
958
+ "age": a.persona.age,
959
  "gender": a.persona.gender,
960
+ "occupation": a.persona.occupation,
961
  "location": a.location,
962
  "state": a.state.value,
963
  "mood": round(a.mood, 2),
964
  "needs": a.needs.to_dict(),
965
  "action": a.current_action.detail if a.current_action else "idle",
966
+ "daily_plan": a.daily_plan,
967
  "partner_id": a.partner_id,
968
+ "is_player": a.is_player,
969
+ "pregnant": a.pregnant,
970
+ "children_count": len(a.children),
971
  }
972
  for aid, a in self.agents.items()
973
  },
web/index.html CHANGED
@@ -2691,12 +2691,14 @@ function showDefaultDetail() {
2691
  const gi = a.gender==='female'?'\uD83D\uDC69':a.gender==='male'?'\uD83D\uDC68':'\uD83E\uDDD1';
2692
  const stateIcon = a.state==='in_conversation'?' \uD83D\uDCAC':(a.state==='sleeping'?' \uD83D\uDCA4':(a.state==='moving'?' \uD83D\uDEB6':''));
2693
  const partner = a.partner_id && agents[a.partner_id] ? ` \u2764\uFE0F ${agents[a.partner_id].name.split(' ')[0]}` : '';
 
 
2694
  return `
2695
  <div class="agent-list-item" onclick="selectedAgentId='${aid}';fetchAgentDetail('${aid}');">
2696
  <span class="agent-dot" style="background:${color}"></span>
2697
  <span style="font-size:13px">${gi}</span>
2698
  <div class="agent-info">
2699
- <div class="agent-name" style="color:${color}">${a.name}${partner}${stateIcon}</div>
2700
  <div class="agent-action">${esc(a.action||'idle')}</div>
2701
  </div>
2702
  </div>`;
@@ -2729,11 +2731,32 @@ function renderAgentDetail(data) {
2729
  const romanceRels=(data.relationships||[]).filter(r=>r.relationship_status&&r.relationship_status!=='none');
2730
  const locName = data.location ? (typeof data.location === 'object' ? data.location.name : data.location) : '?';
2731
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2732
  document.getElementById('agent-detail').innerHTML=`
2733
  <div style="margin-bottom:6px">
2734
  <span style="cursor:pointer;color:#888;font-size:11px" onclick="selectedAgentId=null;showDefaultDetail();">&larr; All Agents</span>
2735
  </div>
2736
- <h2>${gi} ${data.name||'?'}</h2>
2737
  <p class="subtitle">${data.age||'?'} y/o ${data.occupation||''}</p>
2738
  <p class="subtitle" style="color:#888;font-size:10px">${data.traits||''}</p>
2739
  <p class="subtitle" style="color:#e0e0e0">@ ${esc(locName)} &mdash; ${esc(data.action||'idle')}</p>
@@ -2742,6 +2765,10 @@ function renderAgentDetail(data) {
2742
  ${romanceRels.map(r=>`\u2764 ${r.relationship_status} with ${r.name} (${(r.romantic_interest*100).toFixed(0)}%)`).join('<br>')}
2743
  </div>`:''}
2744
 
 
 
 
 
2745
  <div class="bar-container">
2746
  <div class="bar-label"><span>Mood: ${moodLabel}</span><span>${moodPct}%</span></div>
2747
  <div class="bar-bg"><div class="bar-fill ${moodColor}" style="width:${moodPct}%"></div></div>
@@ -2759,6 +2786,35 @@ function renderAgentDetail(data) {
2759
  <div style="font-size:10px;color:#888">Plan: ${esc((data.daily_plan||[]).slice(0,3).join('; ')||'None yet')}</div>
2760
  </div>
2761
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2762
  ${(data.relationships||[]).length>0?`
2763
  <div class="section-header">Relationships</div>
2764
  ${data.relationships.slice(0,8).map(r=>{
@@ -3202,10 +3258,10 @@ function renderPlayerPanel() {
3202
  const locName = agent ? (locations[agent.location]?.name || agent.location || '') : '';
3203
  document.getElementById('pp-loc').textContent = locName ? `@ ${locName}` : '';
3204
 
3205
- // Populate move dropdown from known locations
3206
  const sel = document.getElementById('pp-move-select');
3207
  const locKeys = Object.keys(locations);
3208
- if (locKeys.length > 0) {
3209
  sel.innerHTML = locKeys.map(lid => {
3210
  const ln = locations[lid]?.name || lid;
3211
  const selected = agent && agent.location === lid ? ' selected' : '';
@@ -3226,15 +3282,38 @@ async function playerMove() {
3226
  } catch(e){}
3227
  }
3228
 
3229
- function openProfileEditor() {
3230
  if (!playerAgentId) return;
3231
- const agent = agents[playerAgentId];
3232
- if (agent) {
3233
- document.getElementById('pe-name').value = agent.name || '';
3234
- document.getElementById('pe-age').value = agent.age || 30;
3235
- document.getElementById('pe-occupation').value = agent.occupation || '';
3236
- const gSel = document.getElementById('pe-gender');
3237
- gSel.value = agent.gender || 'unknown';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3238
  }
3239
  document.getElementById('profile-modal').style.display = 'flex';
3240
  }
 
2691
  const gi = a.gender==='female'?'\uD83D\uDC69':a.gender==='male'?'\uD83D\uDC68':'\uD83E\uDDD1';
2692
  const stateIcon = a.state==='in_conversation'?' \uD83D\uDCAC':(a.state==='sleeping'?' \uD83D\uDCA4':(a.state==='moving'?' \uD83D\uDEB6':''));
2693
  const partner = a.partner_id && agents[a.partner_id] ? ` \u2764\uFE0F ${agents[a.partner_id].name.split(' ')[0]}` : '';
2694
+ const pregIcon = a.pregnant ? ' \uD83E\uDD30' : '';
2695
+ const kidIcon = a.children_count > 0 ? ` \uD83D\uDC76${a.children_count}` : '';
2696
  return `
2697
  <div class="agent-list-item" onclick="selectedAgentId='${aid}';fetchAgentDetail('${aid}');">
2698
  <span class="agent-dot" style="background:${color}"></span>
2699
  <span style="font-size:13px">${gi}</span>
2700
  <div class="agent-info">
2701
+ <div class="agent-name" style="color:${color}">${a.name}${partner}${pregIcon}${kidIcon}${stateIcon}</div>
2702
  <div class="agent-action">${esc(a.action||'idle')}</div>
2703
  </div>
2704
  </div>`;
 
2731
  const romanceRels=(data.relationships||[]).filter(r=>r.relationship_status&&r.relationship_status!=='none');
2732
  const locName = data.location ? (typeof data.location === 'object' ? data.location.name : data.location) : '?';
2733
 
2734
+ // Life events
2735
+ const lifeEvents = (data.life_events || []).slice(-10).reverse();
2736
+ const evtIcon = {
2737
+ origin:'\uD83C\uDFE0', career:'\uD83D\uDCBC', dating:'\uD83D\uDC95', engaged:'\uD83D\uDC8D',
2738
+ married:'\uD83D\uDC92', pregnant:'\uD83E\uDD30', child_born:'\uD83D\uDC76',
2739
+ promotion:'\uD83D\uDCC8', graduated:'\uD83C\uDF93', achievement:'\u2B50',
2740
+ milestone:'\uD83C\uDFC6', new_job:'\uD83D\uDCBC', moved:'\uD83D\uDE9A',
2741
+ breakup:'\uD83D\uDC94', friendship:'\uD83E\uDD1D',
2742
+ };
2743
+
2744
+ // Goals
2745
+ const goals = data.goals || [];
2746
+ const activeGoals = goals.filter(g => g.status === 'active');
2747
+ const completedGoals = goals.filter(g => g.status === 'completed');
2748
+
2749
+ // Children & pregnancy
2750
+ const children = data.children || [];
2751
+ const isPregnant = data.pregnant || false;
2752
+ const pregnantBadge = isPregnant ? ' \uD83E\uDD30' : '';
2753
+ const childrenBadge = children.length > 0 ? ` \uD83D\uDC76${children.length}` : '';
2754
+
2755
  document.getElementById('agent-detail').innerHTML=`
2756
  <div style="margin-bottom:6px">
2757
  <span style="cursor:pointer;color:#888;font-size:11px" onclick="selectedAgentId=null;showDefaultDetail();">&larr; All Agents</span>
2758
  </div>
2759
+ <h2>${gi} ${data.name||'?'}${pregnantBadge}${childrenBadge}</h2>
2760
  <p class="subtitle">${data.age||'?'} y/o ${data.occupation||''}</p>
2761
  <p class="subtitle" style="color:#888;font-size:10px">${data.traits||''}</p>
2762
  <p class="subtitle" style="color:#e0e0e0">@ ${esc(locName)} &mdash; ${esc(data.action||'idle')}</p>
 
2765
  ${romanceRels.map(r=>`\u2764 ${r.relationship_status} with ${r.name} (${(r.romantic_interest*100).toFixed(0)}%)`).join('<br>')}
2766
  </div>`:''}
2767
 
2768
+ ${children.length>0?`<div style="margin:4px 0;padding:4px 8px;background:rgba(78,204,163,0.08);border:1px solid rgba(78,204,163,0.2);border-radius:4px;font-size:11px;color:#4ecca3">
2769
+ \uD83D\uDC76 Children: ${children.map(c=>esc(c)).join(', ')}
2770
+ </div>`:''}
2771
+
2772
  <div class="bar-container">
2773
  <div class="bar-label"><span>Mood: ${moodLabel}</span><span>${moodPct}%</span></div>
2774
  <div class="bar-bg"><div class="bar-fill ${moodColor}" style="width:${moodPct}%"></div></div>
 
2786
  <div style="font-size:10px;color:#888">Plan: ${esc((data.daily_plan||[]).slice(0,3).join('; ')||'None yet')}</div>
2787
  </div>
2788
 
2789
+ ${activeGoals.length>0||completedGoals.length>0?`
2790
+ <div class="section-header">Goals</div>
2791
+ ${activeGoals.map(g=>{
2792
+ const pct=Math.round((g.progress||0)*100);
2793
+ return `<div style="margin:3px 0;font-size:11px">
2794
+ <div style="display:flex;justify-content:space-between;color:#e0e0f0">
2795
+ <span>\uD83C\uDFAF ${esc(g.description)}</span><span style="color:#888;font-size:9px">${pct}%</span>
2796
+ </div>
2797
+ <div style="height:3px;background:#1a1a3e;border-radius:2px;margin-top:2px">
2798
+ <div style="height:100%;width:${pct}%;background:#4ecca3;border-radius:2px"></div>
2799
+ </div>
2800
+ </div>`;
2801
+ }).join('')}
2802
+ ${completedGoals.slice(-3).map(g=>`<div style="margin:2px 0;font-size:10px;color:#555">\u2705 ${esc(g.description)}</div>`).join('')}
2803
+ `:''}
2804
+
2805
+ ${lifeEvents.length>0?`
2806
+ <div class="section-header">Life History</div>
2807
+ <div style="max-height:160px;overflow-y:auto">
2808
+ ${lifeEvents.map(e=>{
2809
+ const icon = evtIcon[e.type] || '\uD83D\uDD39';
2810
+ const dayLabel = e.day > 0 ? `Day ${e.day}` : 'Background';
2811
+ return `<div style="margin:3px 0;font-size:11px;display:flex;gap:6px;align-items:start">
2812
+ <span style="flex-shrink:0">${icon}</span>
2813
+ <div><span style="color:#555;font-size:9px">${dayLabel}</span><br><span style="color:#c0c0d0">${esc(e.description)}</span></div>
2814
+ </div>`;
2815
+ }).join('')}
2816
+ </div>`:''}
2817
+
2818
  ${(data.relationships||[]).length>0?`
2819
  <div class="section-header">Relationships</div>
2820
  ${data.relationships.slice(0,8).map(r=>{
 
3258
  const locName = agent ? (locations[agent.location]?.name || agent.location || '') : '';
3259
  document.getElementById('pp-loc').textContent = locName ? `@ ${locName}` : '';
3260
 
3261
+ // Populate move dropdown from known locations (only rebuild if empty)
3262
  const sel = document.getElementById('pp-move-select');
3263
  const locKeys = Object.keys(locations);
3264
+ if (locKeys.length > 0 && sel.options.length === 0) {
3265
  sel.innerHTML = locKeys.map(lid => {
3266
  const ln = locations[lid]?.name || lid;
3267
  const selected = agent && agent.location === lid ? ' selected' : '';
 
3282
  } catch(e){}
3283
  }
3284
 
3285
+ async function openProfileEditor() {
3286
  if (!playerAgentId) return;
3287
+ // Fetch full agent detail to get all fields (WS summary may not have everything)
3288
+ try {
3289
+ const res = await fetch(`${API_BASE}/agents/${playerAgentId}`);
3290
+ if (res.ok) {
3291
+ const data = await res.json();
3292
+ document.getElementById('pe-name').value = data.name || '';
3293
+ document.getElementById('pe-age').value = data.age || 30;
3294
+ document.getElementById('pe-occupation').value = data.occupation || '';
3295
+ document.getElementById('pe-gender').value = data.gender || 'unknown';
3296
+ document.getElementById('pe-background').value = data.background || '';
3297
+ const pers = data.personality || {};
3298
+ const extVal = pers.extraversion || 5;
3299
+ const agrVal = pers.agreeableness || 7;
3300
+ const opnVal = pers.openness || 6;
3301
+ document.getElementById('pe-extraversion').value = extVal;
3302
+ document.getElementById('pe-extra-val').textContent = extVal;
3303
+ document.getElementById('pe-agreeableness').value = agrVal;
3304
+ document.getElementById('pe-agree-val').textContent = agrVal;
3305
+ document.getElementById('pe-openness').value = opnVal;
3306
+ document.getElementById('pe-open-val').textContent = opnVal;
3307
+ }
3308
+ } catch(e) {
3309
+ // Fallback to WS data
3310
+ const agent = agents[playerAgentId];
3311
+ if (agent) {
3312
+ document.getElementById('pe-name').value = agent.name || '';
3313
+ document.getElementById('pe-age').value = agent.age || 30;
3314
+ document.getElementById('pe-occupation').value = agent.occupation || '';
3315
+ document.getElementById('pe-gender').value = agent.gender || 'unknown';
3316
+ }
3317
  }
3318
  document.getElementById('profile-modal').style.display = 'flex';
3319
  }