Srishti280992 commited on
Commit
4a336a4
·
verified ·
1 Parent(s): e445842

Update llm/world_generator.py

Browse files
Files changed (1) hide show
  1. llm/world_generator.py +587 -78
llm/world_generator.py CHANGED
@@ -431,8 +431,12 @@ class DeterministicWorldSpecBuilder:
431
  """Build a valid generic fallback ``WorldSpec`` without an SLM.
432
 
433
  This fallback exists so the Gradio demo can still run when no SLM client is
434
- configured or when the model returns invalid JSON. It does not replace the
435
- SLM; it provides a safe baseline.
 
 
 
 
436
  """
437
 
438
  default_steps: int = 60
@@ -448,56 +452,52 @@ class DeterministicWorldSpecBuilder:
448
  """Build a deterministic world spec from prompt keywords."""
449
 
450
  domain = self._infer_domain(prompt)
 
 
 
 
 
451
  world_id = _slugify(f"{domain}_world")
452
- roles = self._roles_for_domain(domain)[: max(1, int(self.max_agents))]
453
- resources = self._resources_for_domain(domain)
454
- goals = self._goals_for_domain(domain)
455
 
456
  agents = tuple(
457
  self._agent_spec(
458
  index=index,
459
  role=role,
460
  domain=domain,
461
- goals=goals,
462
- total_agents=len(roles),
463
  )
464
  for index, role in enumerate(roles)
465
  )
466
 
467
  resource_specs = tuple(
468
  ResourceSpec(
469
- id=f"{resource_type}_{index + 1}",
470
  type=resource_type,
471
  amount=float(10 + index * 4),
472
  position=(float(2 + index * 2), float(7 - index % 4)),
473
- regeneration_rate=0.1 if index == 0 else 0.0,
474
  max_amount=float(30 + index * 5),
475
- metadata={"domain": domain},
 
 
 
 
476
  )
477
  for index, resource_type in enumerate(resources)
478
  )
479
 
480
- events = (
481
- EventSpec(
482
- id="midpoint_pressure",
483
- name="midpoint_pressure",
484
- trigger_step=max(1, self.default_steps // 2),
485
- payload={
486
- "description": "A generic pressure event that can be narrated later.",
487
- "domain": domain,
488
- },
489
- enabled=True,
490
- metadata={"generated_by": "fallback_builder"},
491
- ),
492
  )
493
 
494
- description = self._description(prompt, domain, constraints)
495
-
496
  return WorldSpec(
497
  schema_version="1.0",
498
  id=world_id,
499
  name=_title_from_domain(domain),
500
- description=description,
501
  simulation=SimulationSpec(
502
  steps=max(1, int(self.default_steps)),
503
  seed=self.default_seed,
@@ -524,6 +524,8 @@ class DeterministicWorldSpecBuilder:
524
  "generated_by": "deterministic_fallback",
525
  "domain": domain,
526
  "source_prompt": prompt,
 
 
527
  },
528
  )
529
 
@@ -533,18 +535,13 @@ class DeterministicWorldSpecBuilder:
533
  index: int,
534
  role: str,
535
  domain: str,
536
- goals: Sequence[str],
537
- total_agents: int,
538
  ) -> AgentSpec:
539
  """Create one generic fallback agent spec."""
540
 
541
- x = 1.0 + (index % 3) * 3.0
542
- y = 1.0 + (index // 3) * 3.0
543
- agent_id = f"{role}_{index + 1}"
544
-
545
- target_agent_id = None
546
- if total_agents > 1:
547
- target_agent_id = f"{self._roles_for_domain(domain)[(index + 1) % total_agents]}_{((index + 1) % total_agents) + 1}"
548
 
549
  goal_records = [
550
  {
@@ -556,22 +553,49 @@ class DeterministicWorldSpecBuilder:
556
  for goal_index, goal in enumerate(goals)
557
  ]
558
 
 
 
 
559
  behaviors = [
560
  BehaviorSpec(
561
- name="remember",
562
- params={
563
- "category": "initial_context",
564
- "content": {
565
- "role": role,
566
- "domain": domain,
567
- "note": "Seed memory created by fallback generator.",
568
- },
569
- "tags": ["initial", domain],
570
  },
571
- ),
572
- BehaviorSpec(name="evaluate", params={"category": "strategy"}),
573
- BehaviorSpec(name="prioritize", params={"source_path": "memory.goals"}),
574
- BehaviorSpec(name="choose_goal", params={}),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
575
  BehaviorSpec(
576
  name="communicate",
577
  params={
@@ -580,64 +604,549 @@ class DeterministicWorldSpecBuilder:
580
  "max_recipients": 1,
581
  }
582
  if target_agent_id is not None
583
- else {"message": f"{role}_status_update", "max_recipients": 1},
 
 
 
584
  ),
585
  BehaviorSpec(
586
  name="cooperate",
587
  params={
588
  "target_agent_id": target_agent_id,
589
  "goal": goals[0],
590
- "effort": 1.0,
591
  }
592
  if target_agent_id is not None
593
- else {"goal": goals[0], "effort": 1.0},
 
 
 
 
 
 
 
 
 
 
 
 
 
594
  ),
595
  ]
596
 
597
- policy = PolicySpec(
598
- type="rule_policy",
599
- params={
600
- "selection_strategy": "highest_score",
601
- "rules": [
602
- {"behavior_name": "choose_goal", "score_delta": 4.0, "priority_delta": 1.0},
603
- {"behavior_name": "prioritize", "score_delta": 3.0},
604
- {"behavior_name": "communicate", "score_delta": 2.0},
605
- {"behavior_name": "cooperate", "score_delta": 1.5},
606
- {"behavior_name": "remember", "score_delta": 0.5},
607
- {"behavior_name": "evaluate", "score_delta": 0.25},
608
- ],
609
- },
610
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
611
 
612
  return AgentSpec(
613
  id=agent_id,
614
  type=role,
615
  position=(x, y),
616
- state={
617
- "energy": float(10 - index),
618
- "credits": float(5 + index),
619
- "influence": float(index + 1),
620
- "domain_pressure": 0.2,
621
- },
622
  memory={
623
  "goals": goal_records,
624
- "known_options": {
625
- "cooperate": 1.0,
626
- "communicate": 0.8,
627
- "evaluate": 0.6,
628
- },
629
  "relationships": {},
 
630
  },
631
  goals=goal_records,
632
  behaviors=tuple(behaviors),
633
- policy=policy,
 
 
 
 
 
 
634
  alive=True,
635
  metadata={
636
  "domain": domain,
637
  "generated_by": "deterministic_fallback",
 
638
  },
639
  )
640
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
641
  @staticmethod
642
  def _infer_domain(prompt: str) -> str:
643
  """Infer a broad fallback domain from prompt keywords."""
@@ -645,7 +1154,7 @@ class DeterministicWorldSpecBuilder:
645
  text = prompt.lower()
646
 
647
  keyword_domains = (
648
- ("farm", ("farm", "crop", "soil", "harvest", "animal", "pasture")),
649
  ("medieval", ("medieval", "kingdom", "castle", "guild", "village", "empire")),
650
  ("research", ("research", "scientist", "paper", "lab", "study", "knowledge")),
651
  ("startup", ("startup", "founder", "investor", "customer", "market", "product")),
@@ -729,7 +1238,7 @@ class DeterministicWorldSpecBuilder:
729
  constraint_text = f" Constraints: {constraints}"
730
 
731
  return (
732
- f"A deterministic fallback {domain} world generated from the prompt: "
733
  f"{prompt.strip()[:240]}{constraint_text}"
734
  )
735
 
 
431
  """Build a valid generic fallback ``WorldSpec`` without an SLM.
432
 
433
  This fallback exists so the Gradio demo can still run when no SLM client is
434
+ configured or when the model returns invalid JSON.
435
+
436
+ Unlike the earliest fallback version, this builder preserves important
437
+ prompt entities when possible. For example, a prompt mentioning sheep,
438
+ wolves, a hunter, and grass will produce sheep agents, wolf agents, a
439
+ hunter agent, and grass resources instead of generic farm-support roles.
440
  """
441
 
442
  default_steps: int = 60
 
452
  """Build a deterministic world spec from prompt keywords."""
453
 
454
  domain = self._infer_domain(prompt)
455
+ max_agents = self._max_agents_from_constraints(constraints) or self.max_agents
456
+ max_agents = max(1, int(max_agents))
457
+
458
+ roles = self._roles_for_prompt(prompt, domain, max_agents=max_agents)
459
+ resources = self._resources_for_prompt(prompt, domain)
460
  world_id = _slugify(f"{domain}_world")
 
 
 
461
 
462
  agents = tuple(
463
  self._agent_spec(
464
  index=index,
465
  role=role,
466
  domain=domain,
467
+ roles=roles,
 
468
  )
469
  for index, role in enumerate(roles)
470
  )
471
 
472
  resource_specs = tuple(
473
  ResourceSpec(
474
+ id=self._resource_id(resource_type, index),
475
  type=resource_type,
476
  amount=float(10 + index * 4),
477
  position=(float(2 + index * 2), float(7 - index % 4)),
478
+ regeneration_rate=self._resource_regeneration_rate(resource_type),
479
  max_amount=float(30 + index * 5),
480
+ metadata={
481
+ "domain": domain,
482
+ "generated_by": "deterministic_fallback",
483
+ "prompt_entity": resource_type in self._prompt_entity_resource_names(prompt),
484
+ },
485
  )
486
  for index, resource_type in enumerate(resources)
487
  )
488
 
489
+ events = self._events_for_prompt(
490
+ prompt=prompt,
491
+ domain=domain,
492
+ roles=roles,
493
+ resources=resources,
 
 
 
 
 
 
 
494
  )
495
 
 
 
496
  return WorldSpec(
497
  schema_version="1.0",
498
  id=world_id,
499
  name=_title_from_domain(domain),
500
+ description=self._description(prompt, domain, constraints),
501
  simulation=SimulationSpec(
502
  steps=max(1, int(self.default_steps)),
503
  seed=self.default_seed,
 
524
  "generated_by": "deterministic_fallback",
525
  "domain": domain,
526
  "source_prompt": prompt,
527
+ "entity_aware_fallback": True,
528
+ "constraints": constraints if isinstance(constraints, Mapping) else str(constraints or ""),
529
  },
530
  )
531
 
 
535
  index: int,
536
  role: str,
537
  domain: str,
538
+ roles: Sequence[str],
 
539
  ) -> AgentSpec:
540
  """Create one generic fallback agent spec."""
541
 
542
+ agent_id = self._agent_id_for_roles(roles, index)
543
+ target_agent_id = self._next_agent_id_for_roles(roles, index)
544
+ goals = self._goals_for_role(role, domain)
 
 
 
 
545
 
546
  goal_records = [
547
  {
 
553
  for goal_index, goal in enumerate(goals)
554
  ]
555
 
556
+ x, y = self._position_for_index(index)
557
+ role_state = self._state_for_role(role, domain, index)
558
+
559
  behaviors = [
560
  BehaviorSpec(
561
+ name="remember",
562
+ params={
563
+ "category": "initial_context",
564
+ "content": {
565
+ "role": role,
566
+ "domain": domain,
567
+ "note": f"Seed memory for {role} created by entity-aware fallback generator.",
 
 
568
  },
569
+ "tags": ["initial", domain, role],
570
+ },
571
+ ),
572
+ BehaviorSpec(
573
+ name="evaluate",
574
+ params={
575
+ "category": f"{role}_strategy",
576
+ "candidate_options": self._options_for_role(role),
577
+ "known_options_path": "memory.known_options",
578
+ "adoption_threshold": 0.7,
579
+ "write_decision_path": "memory.latest_strategy_decision",
580
+ },
581
+ ),
582
+ BehaviorSpec(
583
+ name="prioritize",
584
+ params={
585
+ "source_path": "memory.goals",
586
+ "item_attribute_weights": {
587
+ "importance": 1.0,
588
+ "score": 1.0,
589
+ },
590
+ },
591
+ ),
592
+ BehaviorSpec(
593
+ name="choose_goal",
594
+ params={
595
+ "source_path": "memory.latest_priorities.items",
596
+ "current_goal_path": "state.current_goal",
597
+ },
598
+ ),
599
  BehaviorSpec(
600
  name="communicate",
601
  params={
 
604
  "max_recipients": 1,
605
  }
606
  if target_agent_id is not None
607
+ else {
608
+ "message": f"{role}_status_update",
609
+ "max_recipients": 1,
610
+ },
611
  ),
612
  BehaviorSpec(
613
  name="cooperate",
614
  params={
615
  "target_agent_id": target_agent_id,
616
  "goal": goals[0],
617
+ "effort": self._effort_for_role(role),
618
  }
619
  if target_agent_id is not None
620
+ else {
621
+ "goal": goals[0],
622
+ "effort": self._effort_for_role(role),
623
+ },
624
+ ),
625
+ BehaviorSpec(
626
+ name="update_belief",
627
+ params={
628
+ "belief_id": f"{role}_situation_belief",
629
+ "proposition": self._belief_for_role(role),
630
+ "evidence_path": self._evidence_path_for_role(role),
631
+ "learning_rate": 0.25,
632
+ "write_belief_path": "memory.latest_situation_belief",
633
+ },
634
  ),
635
  ]
636
 
637
+ if role in {"hunter", "farmer"}:
638
+ behaviors.append(
639
+ BehaviorSpec(
640
+ name="recommend",
641
+ params={
642
+ "target_agent_id": target_agent_id,
643
+ "recommendation": "protect_flock_and_monitor_wolves",
644
+ "confidence": 0.75,
645
+ }
646
+ if target_agent_id is not None
647
+ else {
648
+ "recommendation": "protect_flock_and_monitor_wolves",
649
+ "confidence": 0.75,
650
+ },
651
+ )
652
+ )
653
+
654
+ if role == "wolf":
655
+ behaviors.append(
656
+ BehaviorSpec(
657
+ name="share",
658
+ params={
659
+ "target_agent_id": target_agent_id,
660
+ "content_path": "state.pack_signal",
661
+ "share_key": "pack_signal_share",
662
+ "relationship_delta": 0.05,
663
+ }
664
+ if target_agent_id is not None
665
+ else {
666
+ "content_path": "state.pack_signal",
667
+ "share_key": "pack_signal_share",
668
+ "relationship_delta": 0.05,
669
+ },
670
+ )
671
+ )
672
+
673
+ policy_rules = [
674
+ {"behavior_name": "choose_goal", "score_delta": 4.0, "priority_delta": 1.0},
675
+ {"behavior_name": "prioritize", "score_delta": 3.0},
676
+ {"behavior_name": "communicate", "score_delta": 2.0},
677
+ {"behavior_name": "cooperate", "score_delta": 1.5},
678
+ {"behavior_name": "evaluate", "score_delta": 1.0},
679
+ {"behavior_name": "update_belief", "score_delta": 0.8},
680
+ {"behavior_name": "remember", "score_delta": 0.5},
681
+ ]
682
+
683
+ if role in {"hunter", "farmer"}:
684
+ policy_rules.append({"behavior_name": "recommend", "score_delta": 1.2})
685
+
686
+ if role == "wolf":
687
+ policy_rules.append({"behavior_name": "share", "score_delta": 1.2})
688
 
689
  return AgentSpec(
690
  id=agent_id,
691
  type=role,
692
  position=(x, y),
693
+ state=role_state,
 
 
 
 
 
694
  memory={
695
  "goals": goal_records,
696
+ "known_options": self._known_options_for_role(role),
 
 
 
 
697
  "relationships": {},
698
+ "source_role": role,
699
  },
700
  goals=goal_records,
701
  behaviors=tuple(behaviors),
702
+ policy=PolicySpec(
703
+ type="rule_policy",
704
+ params={
705
+ "selection_strategy": "highest_score",
706
+ "rules": policy_rules,
707
+ },
708
+ ),
709
  alive=True,
710
  metadata={
711
  "domain": domain,
712
  "generated_by": "deterministic_fallback",
713
+ "prompt_role": role,
714
  },
715
  )
716
 
717
+ def _roles_for_prompt(
718
+ self,
719
+ prompt: str,
720
+ domain: str,
721
+ *,
722
+ max_agents: int,
723
+ ) -> tuple[str, ...]:
724
+ """Return role list that preserves important prompt entities."""
725
+
726
+ text = prompt.lower()
727
+ roles: list[str] = []
728
+
729
+ if domain == "farm":
730
+ roles.append("farmer")
731
+
732
+ has_sheep = self._contains_any(text, ("sheep", "lamb", "flock"))
733
+ has_wolf = self._contains_any(text, ("wolf", "wolves", "pack"))
734
+ has_hunter = self._contains_any(text, ("hunter", "ranger", "shepherd"))
735
+
736
+ if has_sheep:
737
+ roles.append("sheep")
738
+
739
+ if has_wolf:
740
+ roles.append("wolf")
741
+
742
+ if has_hunter:
743
+ roles.append("hunter")
744
+
745
+ if has_sheep and len(roles) < max_agents:
746
+ roles.append("sheep")
747
+
748
+ if has_wolf and len(roles) < max_agents:
749
+ roles.append("wolf")
750
+
751
+ if not has_sheep and not has_wolf and not has_hunter:
752
+ roles.extend(self._roles_for_domain(domain))
753
+
754
+ return tuple(roles[:max_agents])
755
+
756
+ roles.extend(self._roles_for_domain(domain))
757
+ return tuple(roles[:max_agents])
758
+
759
+ def _resources_for_prompt(self, prompt: str, domain: str) -> tuple[str, ...]:
760
+ """Return resource types that preserve important prompt entities."""
761
+
762
+ text = prompt.lower()
763
+
764
+ if domain == "farm":
765
+ resources: list[str] = []
766
+
767
+ if self._contains_any(text, ("grass", "pasture")):
768
+ resources.append("grass")
769
+ else:
770
+ resources.append("food")
771
+
772
+ if self._contains_any(text, ("sheep", "flock", "lamb")):
773
+ resources.append("flock_safety")
774
+ resources.append("shelter")
775
+
776
+ if self._contains_any(text, ("wolf", "wolves", "pack", "hunter")):
777
+ resources.append("predator_pressure")
778
+
779
+ resources.append("water")
780
+
781
+ if "soil_health" not in resources:
782
+ resources.append("soil_health")
783
+
784
+ return tuple(dict.fromkeys(resources))
785
+
786
+ return self._resources_for_domain(domain)
787
+
788
+ def _events_for_prompt(
789
+ self,
790
+ *,
791
+ prompt: str,
792
+ domain: str,
793
+ roles: Sequence[str],
794
+ resources: Sequence[str],
795
+ ) -> tuple[EventSpec, ...]:
796
+ """Return domain events that preserve important prompt entities."""
797
+
798
+ text = prompt.lower()
799
+ events: list[EventSpec] = []
800
+
801
+ wolf_ids = [
802
+ self._agent_id_for_roles(roles, index)
803
+ for index, role in enumerate(roles)
804
+ if role == "wolf"
805
+ ]
806
+ sheep_ids = [
807
+ self._agent_id_for_roles(roles, index)
808
+ for index, role in enumerate(roles)
809
+ if role == "sheep"
810
+ ]
811
+ hunter_ids = [
812
+ self._agent_id_for_roles(roles, index)
813
+ for index, role in enumerate(roles)
814
+ if role == "hunter"
815
+ ]
816
+ farmer_ids = [
817
+ self._agent_id_for_roles(roles, index)
818
+ for index, role in enumerate(roles)
819
+ if role == "farmer"
820
+ ]
821
+
822
+ resource_ids = {
823
+ resource_type: self._resource_id(resource_type, index)
824
+ for index, resource_type in enumerate(resources)
825
+ }
826
+
827
+ if domain == "farm" and self._contains_any(text, ("wolf", "wolves", "pack")):
828
+ events.append(
829
+ EventSpec(
830
+ id="wolf_pack_enters",
831
+ name="wolf_pack_enters",
832
+ trigger_step=max(1, self.default_steps // 4),
833
+ payload={
834
+ "description": "A pack of wolves enters the farm and increases pressure on the flock.",
835
+ "signals": {
836
+ "predator_pressure": 0.35,
837
+ "flock_fear": 0.25,
838
+ },
839
+ },
840
+ enabled=True,
841
+ target_agent_ids=tuple(wolf_ids + sheep_ids + farmer_ids),
842
+ target_resource_ids=tuple(
843
+ resource_id
844
+ for key, resource_id in resource_ids.items()
845
+ if key in {"predator_pressure", "flock_safety", "grass"}
846
+ ),
847
+ metadata={
848
+ "generated_by": "entity_aware_fallback",
849
+ "narrative_hint": "Predator pressure tests flock safety and farm coordination.",
850
+ },
851
+ )
852
+ )
853
+
854
+ if domain == "farm" and self._contains_any(text, ("hunter", "ranger")):
855
+ events.append(
856
+ EventSpec(
857
+ id="hunter_enters",
858
+ name="hunter_enters",
859
+ trigger_step=max(2, self.default_steps // 3),
860
+ payload={
861
+ "description": "A hunter enters the farm area and changes the balance between wolves and sheep.",
862
+ "signals": {
863
+ "protection_pressure": 0.3,
864
+ "wolf_caution": 0.2,
865
+ },
866
+ },
867
+ enabled=True,
868
+ target_agent_ids=tuple(hunter_ids + wolf_ids + farmer_ids + sheep_ids),
869
+ target_resource_ids=tuple(
870
+ resource_id
871
+ for key, resource_id in resource_ids.items()
872
+ if key in {"predator_pressure", "flock_safety"}
873
+ ),
874
+ metadata={
875
+ "generated_by": "entity_aware_fallback",
876
+ "narrative_hint": "Hunter arrival adds a stabilizing or disruptive force.",
877
+ },
878
+ )
879
+ )
880
+
881
+ if not events:
882
+ events.append(
883
+ EventSpec(
884
+ id="midpoint_pressure",
885
+ name="midpoint_pressure",
886
+ trigger_step=max(1, self.default_steps // 2),
887
+ payload={
888
+ "description": "A generic pressure event that can be narrated later.",
889
+ "domain": domain,
890
+ },
891
+ enabled=True,
892
+ metadata={"generated_by": "fallback_builder"},
893
+ )
894
+ )
895
+
896
+ return tuple(events)
897
+
898
+ def _state_for_role(self, role: str, domain: str, index: int) -> dict[str, Any]:
899
+ """Return role-specific initial state."""
900
+
901
+ base_state: dict[str, Any] = {
902
+ "energy": float(max(3, 10 - index)),
903
+ "credits": float(5 + index),
904
+ "influence": float(index + 1),
905
+ "domain_pressure": 0.2,
906
+ "inventory": {},
907
+ }
908
+
909
+ if role == "sheep":
910
+ base_state.update(
911
+ {
912
+ "hunger": 0.4,
913
+ "fear": 0.35,
914
+ "herd_signal": 0.75,
915
+ "grazing_need": 0.8,
916
+ "inventory": {"wool": 1},
917
+ }
918
+ )
919
+ return base_state
920
+
921
+ if role == "wolf":
922
+ base_state.update(
923
+ {
924
+ "hunger": 0.85,
925
+ "pack_signal": 0.8,
926
+ "caution": 0.3,
927
+ "predator_intent": 0.9,
928
+ "inventory": {},
929
+ }
930
+ )
931
+ return base_state
932
+
933
+ if role == "hunter":
934
+ base_state.update(
935
+ {
936
+ "tracking_skill": 0.8,
937
+ "protection_signal": 0.7,
938
+ "caution": 0.55,
939
+ "inventory": {"tools": 2, "supplies": 2},
940
+ }
941
+ )
942
+ return base_state
943
+
944
+ if role == "farmer":
945
+ base_state.update(
946
+ {
947
+ "flock_care": 0.8,
948
+ "grass_management": 0.65,
949
+ "protection_signal": 0.5,
950
+ "inventory": {"hay": 3, "tools": 2},
951
+ }
952
+ )
953
+ return base_state
954
+
955
+ base_state.update(
956
+ {
957
+ "role_signal": 0.6,
958
+ "inventory": {},
959
+ }
960
+ )
961
+ return base_state
962
+
963
+ def _goals_for_role(self, role: str, domain: str) -> tuple[str, ...]:
964
+ """Return role-specific fallback goal ids."""
965
+
966
+ if role == "sheep":
967
+ return ("graze_on_grass", "stay_with_flock", "avoid_wolves")
968
+
969
+ if role == "wolf":
970
+ return ("coordinate_pack", "seek_food_pressure", "avoid_hunter")
971
+
972
+ if role == "hunter":
973
+ return ("protect_flock", "track_wolves", "avoid_unnecessary_harm")
974
+
975
+ if role == "farmer":
976
+ return ("protect_sheep", "maintain_grass", "coordinate_hunter")
977
+
978
+ return self._goals_for_domain(domain)
979
+
980
+ def _known_options_for_role(self, role: str) -> dict[str, Any]:
981
+ """Return role-specific known options."""
982
+
983
+ if role == "sheep":
984
+ return {
985
+ "graze": {"score": 1.0, "energy": 0.8},
986
+ "flock_together": {"score": 1.2, "safety": 0.9},
987
+ "avoid_predator": {"score": 1.4, "safety": 1.0},
988
+ }
989
+
990
+ if role == "wolf":
991
+ return {
992
+ "pack_coordination": {"score": 1.3, "success": 0.8},
993
+ "cautious_entry": {"score": 1.0, "safety": 0.7},
994
+ "pressure_flock": {"score": 1.2, "food": 0.8},
995
+ }
996
+
997
+ if role == "hunter":
998
+ return {
999
+ "track_wolves": {"score": 1.2, "protection": 0.9},
1000
+ "coordinate_farmer": {"score": 1.1, "trust": 0.8},
1001
+ "guard_pasture": {"score": 1.0, "safety": 0.9},
1002
+ }
1003
+
1004
+ if role == "farmer":
1005
+ return {
1006
+ "protect_flock": {"score": 1.2, "safety": 0.9},
1007
+ "maintain_pasture": {"score": 1.0, "grass": 0.8},
1008
+ "coordinate_hunter": {"score": 1.1, "protection": 0.8},
1009
+ }
1010
+
1011
+ return {
1012
+ "cooperate": 1.0,
1013
+ "communicate": 0.8,
1014
+ "evaluate": 0.6,
1015
+ }
1016
+
1017
+ def _options_for_role(self, role: str) -> list[str]:
1018
+ """Return candidate option ids for evaluation behavior."""
1019
+
1020
+ return list(self._known_options_for_role(role).keys())
1021
+
1022
+ def _belief_for_role(self, role: str) -> str:
1023
+ """Return a role-specific belief proposition."""
1024
+
1025
+ if role == "sheep":
1026
+ return "The flock can stay safe while grazing."
1027
+
1028
+ if role == "wolf":
1029
+ return "The pack can coordinate under farm and hunter pressure."
1030
+
1031
+ if role == "hunter":
1032
+ return "The hunter can reduce wolf pressure without destabilizing the farm."
1033
+
1034
+ if role == "farmer":
1035
+ return "The farm can protect sheep while maintaining grass resources."
1036
+
1037
+ return "The current situation is manageable."
1038
+
1039
+ def _evidence_path_for_role(self, role: str) -> str:
1040
+ """Return a role-specific evidence path."""
1041
+
1042
+ if role == "sheep":
1043
+ return "state.herd_signal"
1044
+
1045
+ if role == "wolf":
1046
+ return "state.pack_signal"
1047
+
1048
+ if role == "hunter":
1049
+ return "state.protection_signal"
1050
+
1051
+ if role == "farmer":
1052
+ return "state.flock_care"
1053
+
1054
+ return "state.domain_pressure"
1055
+
1056
+ def _effort_for_role(self, role: str) -> float:
1057
+ """Return role-specific cooperation effort."""
1058
+
1059
+ if role in {"hunter", "farmer", "wolf"}:
1060
+ return 1.0
1061
+
1062
+ if role == "sheep":
1063
+ return 0.7
1064
+
1065
+ return 0.8
1066
+
1067
+ def _position_for_index(self, index: int) -> tuple[float, float]:
1068
+ """Return deterministic 2D position for agent index."""
1069
+
1070
+ positions = (
1071
+ (1.0, 1.0),
1072
+ (3.0, 6.5),
1073
+ (7.5, 6.5),
1074
+ (8.5, 2.0),
1075
+ (4.0, 7.5),
1076
+ (6.5, 2.5),
1077
+ (2.0, 4.0),
1078
+ (8.0, 8.0),
1079
+ )
1080
+ return positions[index % len(positions)]
1081
+
1082
+ def _agent_id_for_roles(self, roles: Sequence[str], index: int) -> str:
1083
+ """Return stable id for an agent in a possibly duplicated role list."""
1084
+
1085
+ role = roles[index]
1086
+ ordinal = sum(1 for candidate in roles[: index + 1] if candidate == role)
1087
+ return f"{role}_{ordinal}"
1088
+
1089
+ def _next_agent_id_for_roles(self, roles: Sequence[str], index: int) -> str | None:
1090
+ """Return next cyclic target agent id."""
1091
+
1092
+ if len(roles) <= 1:
1093
+ return None
1094
+
1095
+ next_index = (index + 1) % len(roles)
1096
+ return self._agent_id_for_roles(roles, next_index)
1097
+
1098
+ def _resource_id(self, resource_type: str, index: int) -> str:
1099
+ """Return stable resource id."""
1100
+
1101
+ return f"{_slugify(resource_type)}_{index + 1}"
1102
+
1103
+ def _resource_regeneration_rate(self, resource_type: str) -> float:
1104
+ """Return resource-specific regeneration rate."""
1105
+
1106
+ if resource_type in {"grass", "food", "water", "soil_health"}:
1107
+ return 0.2
1108
+
1109
+ if resource_type in {"flock_safety", "predator_pressure"}:
1110
+ return 0.05
1111
+
1112
+ return 0.0
1113
+
1114
+ def _max_agents_from_constraints(
1115
+ self,
1116
+ constraints: Mapping[str, Any] | str | None,
1117
+ ) -> int | None:
1118
+ """Extract max_agents from parsed constraints when available."""
1119
+
1120
+ if not isinstance(constraints, Mapping):
1121
+ return None
1122
+
1123
+ value = constraints.get("max_agents")
1124
+ if value is None:
1125
+ return None
1126
+
1127
+ try:
1128
+ return max(1, int(value))
1129
+ except (TypeError, ValueError):
1130
+ return None
1131
+
1132
+ def _prompt_entity_resource_names(self, prompt: str) -> set[str]:
1133
+ """Return resource names explicitly suggested by the prompt."""
1134
+
1135
+ text = prompt.lower()
1136
+ names: set[str] = set()
1137
+
1138
+ if self._contains_any(text, ("grass", "pasture")):
1139
+ names.add("grass")
1140
+
1141
+ return names
1142
+
1143
+ @staticmethod
1144
+ def _contains_any(text: str, words: Sequence[str]) -> bool:
1145
+ """Return whether any word-like string appears in text."""
1146
+
1147
+ lowered = text.lower()
1148
+ return any(word in lowered for word in words)
1149
+
1150
  @staticmethod
1151
  def _infer_domain(prompt: str) -> str:
1152
  """Infer a broad fallback domain from prompt keywords."""
 
1154
  text = prompt.lower()
1155
 
1156
  keyword_domains = (
1157
+ ("farm", ("farm", "crop", "soil", "harvest", "animal", "pasture", "sheep", "wolf", "wolves", "grass")),
1158
  ("medieval", ("medieval", "kingdom", "castle", "guild", "village", "empire")),
1159
  ("research", ("research", "scientist", "paper", "lab", "study", "knowledge")),
1160
  ("startup", ("startup", "founder", "investor", "customer", "market", "product")),
 
1238
  constraint_text = f" Constraints: {constraints}"
1239
 
1240
  return (
1241
+ f"A deterministic entity-aware fallback {domain} world generated from the prompt: "
1242
  f"{prompt.strip()[:240]}{constraint_text}"
1243
  )
1244