cogames 0.3.49__py3-none-any.whl → 0.3.64__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. cogames/cli/client.py +60 -6
  2. cogames/cli/docsync/__init__.py +0 -0
  3. cogames/cli/docsync/_nb_md_directive_processing.py +180 -0
  4. cogames/cli/docsync/_nb_md_sync.py +103 -0
  5. cogames/cli/docsync/_nb_py_sync.py +122 -0
  6. cogames/cli/docsync/_three_way_sync.py +115 -0
  7. cogames/cli/docsync/_utils.py +76 -0
  8. cogames/cli/docsync/docsync.py +156 -0
  9. cogames/cli/leaderboard.py +112 -28
  10. cogames/cli/mission.py +64 -53
  11. cogames/cli/policy.py +46 -10
  12. cogames/cli/submit.py +268 -67
  13. cogames/cogs_vs_clips/cog.py +79 -0
  14. cogames/cogs_vs_clips/cogs_vs_clips_mapgen.md +19 -16
  15. cogames/cogs_vs_clips/cogsguard_reward_variants.py +153 -0
  16. cogames/cogs_vs_clips/cogsguard_tutorial.py +56 -0
  17. cogames/cogs_vs_clips/evals/README.md +10 -16
  18. cogames/cogs_vs_clips/evals/cogsguard_evals.py +81 -0
  19. cogames/cogs_vs_clips/evals/diagnostic_evals.py +49 -444
  20. cogames/cogs_vs_clips/evals/difficulty_variants.py +13 -326
  21. cogames/cogs_vs_clips/evals/integrated_evals.py +5 -45
  22. cogames/cogs_vs_clips/evals/spanning_evals.py +9 -180
  23. cogames/cogs_vs_clips/mission.py +187 -146
  24. cogames/cogs_vs_clips/missions.py +46 -137
  25. cogames/cogs_vs_clips/procedural.py +8 -8
  26. cogames/cogs_vs_clips/sites.py +107 -3
  27. cogames/cogs_vs_clips/stations.py +198 -186
  28. cogames/cogs_vs_clips/tutorial_missions.py +1 -1
  29. cogames/cogs_vs_clips/variants.py +25 -476
  30. cogames/device.py +13 -1
  31. cogames/{policy/scripted_agent/README.md → docs/SCRIPTED_AGENT.md} +82 -58
  32. cogames/evaluate.py +18 -30
  33. cogames/main.py +1434 -243
  34. cogames/maps/canidate1_1000.map +1 -1
  35. cogames/maps/canidate1_1000_stations.map +2 -2
  36. cogames/maps/canidate1_500.map +1 -1
  37. cogames/maps/canidate1_500_stations.map +2 -2
  38. cogames/maps/canidate2_1000.map +1 -1
  39. cogames/maps/canidate2_1000_stations.map +2 -2
  40. cogames/maps/canidate2_500.map +1 -1
  41. cogames/maps/canidate2_500_stations.map +2 -2
  42. cogames/maps/canidate3_1000.map +1 -1
  43. cogames/maps/canidate3_1000_stations.map +2 -2
  44. cogames/maps/canidate3_500.map +1 -1
  45. cogames/maps/canidate3_500_stations.map +2 -2
  46. cogames/maps/canidate4_500.map +1 -1
  47. cogames/maps/canidate4_500_stations.map +2 -2
  48. cogames/maps/cave_base_50.map +2 -2
  49. cogames/maps/diagnostic_evals/diagnostic_agile.map +2 -2
  50. cogames/maps/diagnostic_evals/diagnostic_agile_hard.map +2 -2
  51. cogames/maps/diagnostic_evals/diagnostic_charge_up.map +2 -2
  52. cogames/maps/diagnostic_evals/diagnostic_charge_up_hard.map +2 -2
  53. cogames/maps/diagnostic_evals/diagnostic_chest_navigation1.map +2 -2
  54. cogames/maps/diagnostic_evals/diagnostic_chest_navigation1_hard.map +2 -2
  55. cogames/maps/diagnostic_evals/diagnostic_chest_navigation2.map +2 -2
  56. cogames/maps/diagnostic_evals/diagnostic_chest_navigation2_hard.map +2 -2
  57. cogames/maps/diagnostic_evals/diagnostic_chest_navigation3.map +2 -2
  58. cogames/maps/diagnostic_evals/diagnostic_chest_navigation3_hard.map +2 -2
  59. cogames/maps/diagnostic_evals/diagnostic_chest_near.map +2 -2
  60. cogames/maps/diagnostic_evals/diagnostic_chest_search.map +2 -2
  61. cogames/maps/diagnostic_evals/diagnostic_chest_search_hard.map +2 -2
  62. cogames/maps/diagnostic_evals/diagnostic_extract_lab.map +2 -2
  63. cogames/maps/diagnostic_evals/diagnostic_extract_lab_hard.map +2 -2
  64. cogames/maps/diagnostic_evals/diagnostic_memory.map +2 -2
  65. cogames/maps/diagnostic_evals/diagnostic_memory_hard.map +2 -2
  66. cogames/maps/diagnostic_evals/diagnostic_radial.map +2 -2
  67. cogames/maps/diagnostic_evals/diagnostic_radial_hard.map +2 -2
  68. cogames/maps/diagnostic_evals/diagnostic_resource_lab.map +2 -2
  69. cogames/maps/diagnostic_evals/diagnostic_unclip.map +2 -2
  70. cogames/maps/evals/eval_balanced_spread.map +9 -5
  71. cogames/maps/evals/eval_clip_oxygen.map +9 -5
  72. cogames/maps/evals/eval_collect_resources.map +9 -5
  73. cogames/maps/evals/eval_collect_resources_hard.map +9 -5
  74. cogames/maps/evals/eval_collect_resources_medium.map +9 -5
  75. cogames/maps/evals/eval_divide_and_conquer.map +9 -5
  76. cogames/maps/evals/eval_energy_starved.map +9 -5
  77. cogames/maps/evals/eval_multi_coordinated_collect_hard.map +9 -5
  78. cogames/maps/evals/eval_oxygen_bottleneck.map +9 -5
  79. cogames/maps/evals/eval_single_use_world.map +9 -5
  80. cogames/maps/evals/extractor_hub_100x100.map +9 -5
  81. cogames/maps/evals/extractor_hub_30x30.map +9 -5
  82. cogames/maps/evals/extractor_hub_50x50.map +9 -5
  83. cogames/maps/evals/extractor_hub_70x70.map +9 -5
  84. cogames/maps/evals/extractor_hub_80x80.map +9 -5
  85. cogames/maps/machina_100_stations.map +2 -2
  86. cogames/maps/machina_200_stations.map +2 -2
  87. cogames/maps/machina_200_stations_small.map +2 -2
  88. cogames/maps/machina_eval_exp01.map +2 -2
  89. cogames/maps/machina_eval_template_large.map +2 -2
  90. cogames/maps/machinatrainer4agents.map +2 -2
  91. cogames/maps/machinatrainer4agentsbase.map +2 -2
  92. cogames/maps/machinatrainerbig.map +2 -2
  93. cogames/maps/machinatrainersmall.map +2 -2
  94. cogames/maps/planky_evals/aligner_avoid_aoe.map +28 -0
  95. cogames/maps/planky_evals/aligner_full_cycle.map +28 -0
  96. cogames/maps/planky_evals/aligner_gear.map +24 -0
  97. cogames/maps/planky_evals/aligner_hearts.map +24 -0
  98. cogames/maps/planky_evals/aligner_junction.map +26 -0
  99. cogames/maps/planky_evals/exploration_distant.map +28 -0
  100. cogames/maps/planky_evals/maze.map +32 -0
  101. cogames/maps/planky_evals/miner_best_resource.map +26 -0
  102. cogames/maps/planky_evals/miner_deposit.map +24 -0
  103. cogames/maps/planky_evals/miner_extract.map +26 -0
  104. cogames/maps/planky_evals/miner_full_cycle.map +28 -0
  105. cogames/maps/planky_evals/miner_gear.map +24 -0
  106. cogames/maps/planky_evals/multi_role.map +28 -0
  107. cogames/maps/planky_evals/resource_chain.map +30 -0
  108. cogames/maps/planky_evals/scout_explore.map +32 -0
  109. cogames/maps/planky_evals/scout_gear.map +24 -0
  110. cogames/maps/planky_evals/scrambler_full_cycle.map +28 -0
  111. cogames/maps/planky_evals/scrambler_gear.map +24 -0
  112. cogames/maps/planky_evals/scrambler_target.map +26 -0
  113. cogames/maps/planky_evals/stuck_corridor.map +32 -0
  114. cogames/maps/planky_evals/survive_retreat.map +26 -0
  115. cogames/maps/training_facility_clipped.map +2 -2
  116. cogames/maps/training_facility_open_1.map +2 -2
  117. cogames/maps/training_facility_open_2.map +2 -2
  118. cogames/maps/training_facility_open_3.map +2 -2
  119. cogames/maps/training_facility_tight_4.map +2 -2
  120. cogames/maps/training_facility_tight_5.map +2 -2
  121. cogames/maps/vanilla_large.map +2 -2
  122. cogames/maps/vanilla_small.map +2 -2
  123. cogames/pickup.py +183 -0
  124. cogames/play.py +166 -33
  125. cogames/policy/chaos_monkey.py +54 -0
  126. cogames/policy/nim_agents/__init__.py +27 -10
  127. cogames/policy/nim_agents/agents.py +121 -60
  128. cogames/policy/nim_agents/thinky_eval.py +35 -222
  129. cogames/policy/pufferlib_policy.py +67 -32
  130. cogames/policy/starter_agent.py +184 -0
  131. cogames/policy/trainable_policy_template.py +4 -1
  132. cogames/train.py +51 -13
  133. cogames/verbose.py +2 -2
  134. cogames-0.3.64.dist-info/METADATA +1842 -0
  135. cogames-0.3.64.dist-info/RECORD +159 -0
  136. cogames-0.3.64.dist-info/licenses/LICENSE +21 -0
  137. cogames-0.3.64.dist-info/top_level.txt +2 -0
  138. metta_alo/__init__.py +0 -0
  139. metta_alo/job_specs.py +17 -0
  140. metta_alo/policy.py +16 -0
  141. metta_alo/pure_single_episode_runner.py +75 -0
  142. metta_alo/py.typed +0 -0
  143. metta_alo/rollout.py +322 -0
  144. metta_alo/scoring.py +168 -0
  145. cogames/maps/diagnostic_evals/diagnostic_assembler_near.map +0 -49
  146. cogames/maps/diagnostic_evals/diagnostic_assembler_search.map +0 -49
  147. cogames/maps/diagnostic_evals/diagnostic_assembler_search_hard.map +0 -89
  148. cogames/policy/nim_agents/common.nim +0 -887
  149. cogames/policy/nim_agents/install.sh +0 -1
  150. cogames/policy/nim_agents/ladybug_agent.nim +0 -984
  151. cogames/policy/nim_agents/nim_agents.nim +0 -55
  152. cogames/policy/nim_agents/nim_agents.nims +0 -14
  153. cogames/policy/nim_agents/nimby.lock +0 -3
  154. cogames/policy/nim_agents/racecar_agents.nim +0 -884
  155. cogames/policy/nim_agents/random_agents.nim +0 -68
  156. cogames/policy/nim_agents/test_agents.py +0 -53
  157. cogames/policy/nim_agents/thinky_agents.nim +0 -717
  158. cogames/policy/scripted_agent/baseline_agent.py +0 -1049
  159. cogames/policy/scripted_agent/demo_policy.py +0 -244
  160. cogames/policy/scripted_agent/pathfinding.py +0 -126
  161. cogames/policy/scripted_agent/starter_agent.py +0 -136
  162. cogames/policy/scripted_agent/types.py +0 -235
  163. cogames/policy/scripted_agent/unclipping_agent.py +0 -476
  164. cogames/policy/scripted_agent/utils.py +0 -385
  165. cogames-0.3.49.dist-info/METADATA +0 -406
  166. cogames-0.3.49.dist-info/RECORD +0 -136
  167. cogames-0.3.49.dist-info/top_level.txt +0 -1
  168. {cogames-0.3.49.dist-info → cogames-0.3.64.dist-info}/WHEEL +0 -0
  169. {cogames-0.3.49.dist-info → cogames-0.3.64.dist-info}/entry_points.txt +0 -0
@@ -52,7 +52,7 @@ class MachinaArenaConfig(SceneConfig):
52
52
  #### Building placement ####
53
53
 
54
54
  # How much of the map is covered by buildings
55
- building_coverage: float = 0.0175
55
+ building_coverage: float = 0.05
56
56
  # Resource placement (building-based API)
57
57
  # Defines the set of buildings that can be placed on the map
58
58
  building_names: list[str] | None = None
@@ -113,8 +113,8 @@ class MachinaArena(Scene[MachinaArenaConfig]):
113
113
  # Building weights
114
114
  default_building_weights = {
115
115
  "chest": 0.0,
116
- "charger": 0.6,
117
- "germanium_extractor": 0.5,
116
+ "junction": 0.7,
117
+ "germanium_extractor": 0.3,
118
118
  "silicon_extractor": 0.3,
119
119
  "oxygen_extractor": 0.3,
120
120
  "carbon_extractor": 0.3,
@@ -372,11 +372,11 @@ class SequentialMachinaArena(Scene[SequentialMachinaArenaConfig]):
372
372
  base_cfg: SceneConfig = BaseCfgModel.model_validate(cfg.base_biome_config or {})
373
373
  default_building_weights = {
374
374
  "chest": 0.0,
375
- "charger": 0.6,
376
- "germanium_extractor": 0.5,
377
- "silicon_extractor": 0.3,
378
- "oxygen_extractor": 0.3,
379
- "carbon_extractor": 0.3,
375
+ "junction": 0.6,
376
+ "germanium_extractor": 0.2,
377
+ "silicon_extractor": 0.2,
378
+ "oxygen_extractor": 0.2,
379
+ "carbon_extractor": 0.2,
380
380
  }
381
381
  weights_dict: dict[str, float] = {str(k): v for k, v in (cfg.building_weights or {}).items()}
382
382
  if not weights_dict:
@@ -1,10 +1,12 @@
1
1
  """Central site definitions shared across mission modules."""
2
2
 
3
+ from typing import cast
4
+
3
5
  from cogames.cogs_vs_clips.mission import Site
4
6
  from cogames.cogs_vs_clips.mission_utils import get_map
5
7
  from cogames.cogs_vs_clips.procedural import MachinaArena, RandomTransform, SequentialMachinaArena
6
- from mettagrid.mapgen.mapgen import MapGen
7
- from mettagrid.mapgen.scenes.base_hub import BaseHub
8
+ from mettagrid.mapgen.mapgen import MapGen, MapGenConfig
9
+ from mettagrid.mapgen.scenes.base_hub import BaseHub, BaseHubConfig
8
10
 
9
11
  TRAINING_FACILITY = Site(
10
12
  name="training_facility",
@@ -46,6 +48,22 @@ MACHINA_1 = Site(
46
48
  max_cogs=20,
47
49
  )
48
50
 
51
+
52
+ def _cogsguard_hub_config() -> BaseHubConfig:
53
+ return BaseHubConfig(
54
+ corner_bundle="extractors",
55
+ cross_bundle="none",
56
+ cross_distance=7,
57
+ stations=[
58
+ "aligner_station",
59
+ "scrambler_station",
60
+ "miner_station",
61
+ "scout_station",
62
+ "chest",
63
+ ],
64
+ )
65
+
66
+
49
67
  # Evals site used by diagnostic evaluation missions
50
68
  # Note: Individual diagnostic missions override this with their own specific maps
51
69
  EVALS = Site(
@@ -56,9 +74,95 @@ EVALS = Site(
56
74
  max_cogs=8,
57
75
  )
58
76
 
59
- SITES = [
77
+
78
+ def make_cogsguard_arena_site(num_agents: int = 10) -> Site:
79
+ """Create a CogsGuard arena site with configurable agent count."""
80
+ map_builder = MapGen.Config(
81
+ width=50,
82
+ height=50,
83
+ instance=MachinaArena.Config(
84
+ spawn_count=num_agents,
85
+ building_coverage=0.1,
86
+ hub=_cogsguard_hub_config(),
87
+ ),
88
+ )
89
+ return Site(
90
+ name="cogsguard_arena",
91
+ description="CogsGuard arena map",
92
+ map_builder=map_builder,
93
+ min_cogs=num_agents,
94
+ max_cogs=num_agents,
95
+ )
96
+
97
+
98
+ def _build_cogsguard_machina1_map_builder(spawn_count: int) -> MapGenConfig:
99
+ map_builder = cast(MapGenConfig, MACHINA_1.map_builder).model_copy(deep=True)
100
+ instance = map_builder.instance
101
+ assert instance is not None
102
+ return map_builder.model_copy(
103
+ update={
104
+ "instance": instance.model_copy(
105
+ update={
106
+ "spawn_count": spawn_count,
107
+ "hub": _cogsguard_hub_config(),
108
+ }
109
+ ),
110
+ }
111
+ )
112
+
113
+
114
+ def make_cogsguard_machina1_site(num_agents: int = 10) -> Site:
115
+ """Create a CogsGuard Machina1 site with configurable agent count."""
116
+ return Site(
117
+ name="cogsguard_machina_1",
118
+ description="CogsGuard Machina1 layout with gear stations.",
119
+ map_builder=_build_cogsguard_machina1_map_builder(num_agents),
120
+ min_cogs=num_agents,
121
+ max_cogs=num_agents,
122
+ )
123
+
124
+
125
+ # Default CogsGuard Machina1 site with flexible agent count
126
+ COGSGUARD_MACHINA_1 = Site(
127
+ name="cogsguard_machina_1",
128
+ description="CogsGuard Machina1 layout with gear stations.",
129
+ map_builder=_build_cogsguard_machina1_map_builder(20),
130
+ min_cogs=1,
131
+ max_cogs=20,
132
+ )
133
+
134
+ # Default CogsGuard arena site with flexible agent count
135
+ COGSGUARD_ARENA = Site(
136
+ name="cogsguard_arena",
137
+ description="CogsGuard arena - compact training map with gear stations.",
138
+ map_builder=MapGen.Config(
139
+ width=50,
140
+ height=50,
141
+ instance=MachinaArena.Config(
142
+ spawn_count=20,
143
+ building_coverage=0.1,
144
+ hub=_cogsguard_hub_config(),
145
+ ),
146
+ ),
147
+ min_cogs=1,
148
+ max_cogs=20,
149
+ )
150
+
151
+
152
+ # Feature flag: Set to True to include legacy (pre-CogsGuard) sites in the CLI.
153
+ # To enable, add TRAINING_FACILITY, HELLO_WORLD, MACHINA_1 to SITES below.
154
+ # Also set _INCLUDE_LEGACY_MISSIONS = True in missions.py.
155
+ _INCLUDE_LEGACY_SITES = False
156
+
157
+ _LEGACY_SITES = [
60
158
  TRAINING_FACILITY,
61
159
  HELLO_WORLD,
62
160
  MACHINA_1,
161
+ ]
162
+
163
+ SITES = [
164
+ COGSGUARD_MACHINA_1,
165
+ COGSGUARD_ARENA,
63
166
  EVALS,
167
+ *(_LEGACY_SITES if _INCLUDE_LEGACY_SITES else []),
64
168
  ]
@@ -1,13 +1,33 @@
1
+ from typing import Optional
2
+
1
3
  from pydantic import Field
2
4
 
3
5
  from mettagrid.base_config import Config
4
6
  from mettagrid.config import vibes
7
+ from mettagrid.config.handler_config import (
8
+ AOEConfig,
9
+ ClearInventoryMutation,
10
+ EntityTarget,
11
+ Handler,
12
+ actorCollectiveHas,
13
+ actorHas,
14
+ alignToActor,
15
+ collectiveDeposit,
16
+ collectiveWithdraw,
17
+ isAlignedToActor,
18
+ isEnemy,
19
+ isNeutral,
20
+ removeAlignment,
21
+ targetCollectiveHas,
22
+ updateActor,
23
+ updateTarget,
24
+ updateTargetCollective,
25
+ withdraw,
26
+ )
5
27
  from mettagrid.config.mettagrid_config import (
6
- AssemblerConfig,
7
28
  ChestConfig,
8
29
  GridObjectConfig,
9
30
  InventoryConfig,
10
- ProtocolConfig,
11
31
  WallConfig,
12
32
  )
13
33
 
@@ -24,11 +44,35 @@ resources = [
24
44
  "scrambler",
25
45
  ]
26
46
 
47
+ # CogsGuard constants
48
+ GEAR = ["aligner", "scrambler", "miner", "scout"]
49
+ ELEMENTS = ["oxygen", "carbon", "germanium", "silicon"]
27
50
 
28
- class CvCStationConfig(Config):
29
- start_clipped: bool = Field(default=False)
30
- clip_immune: bool = Field(default=False)
31
51
 
52
+ HEART_COST = {e: 10 for e in ELEMENTS}
53
+ COGSGUARD_ALIGN_COST = {"heart": 1}
54
+ COGSGUARD_SCRAMBLE_COST = {"heart": 1}
55
+
56
+ GEAR_COSTS = {
57
+ "aligner": {"carbon": 3, "oxygen": 1, "germanium": 1, "silicon": 1},
58
+ "scrambler": {"carbon": 1, "oxygen": 3, "germanium": 1, "silicon": 1},
59
+ "miner": {"carbon": 1, "oxygen": 1, "germanium": 3, "silicon": 1},
60
+ "scout": {"carbon": 1, "oxygen": 1, "germanium": 1, "silicon": 3},
61
+ }
62
+
63
+ GEAR_SYMBOLS = {
64
+ "aligner": "🔗",
65
+ "scrambler": "🌀",
66
+ "miner": "⛏️",
67
+ "scout": "🔭",
68
+ }
69
+
70
+
71
+ def _neg(recipe: dict[str, int]) -> dict[str, int]:
72
+ return {k: -v for k, v in recipe.items()}
73
+
74
+
75
+ class CvCStationConfig(Config):
32
76
  def station_cfg(self) -> GridObjectConfig:
33
77
  raise NotImplementedError("Subclasses must implement this method")
34
78
 
@@ -38,203 +82,171 @@ class CvCWallConfig(CvCStationConfig):
38
82
  return WallConfig(name="wall", render_symbol=vibes.VIBE_BY_NAME["wall"].symbol)
39
83
 
40
84
 
41
- class ExtractorConfig(CvCStationConfig):
42
- """Base class for all extractor configs."""
43
-
44
- # How much this extractor produces relative to its default.
45
- # Efficiency outside of this range won't technically break things, but they'll be far enough from the
46
- # expectations that we don't want to go beyond them without some thought.
47
- efficiency: int = Field(ge=20, le=500, default=100)
48
- # How much additional agents increase production.
49
- # Scaled so 0 means none and 100 means some version of "twice as much".
50
- synergy: int = Field(default=0)
51
- max_uses: int = Field()
52
-
53
-
54
- class ChargerConfig(ExtractorConfig):
55
- max_uses: int = 0 # unlimited uses
56
-
57
- def station_cfg(self) -> AssemblerConfig:
58
- output = 50 * self.efficiency // 100
59
- return AssemblerConfig(
60
- name="charger",
61
- render_symbol=vibes.VIBE_BY_NAME["charger"].symbol,
62
- # Protocols
63
- allow_partial_usage=True, # can use it while its on cooldown
64
- max_uses=self.max_uses,
65
- protocols=[
66
- ProtocolConfig(
67
- min_agents=(additional_agents + 1) if additional_agents >= 1 else 0,
68
- output_resources={"energy": output * (100 + additional_agents * self.synergy) // 100},
69
- cooldown=10,
70
- )
71
- for additional_agents in range(4)
72
- ],
73
- # Clipping
74
- start_clipped=self.start_clipped,
75
- clip_immune=self.clip_immune,
76
- )
85
+ # ==============================================================================
86
+ # CogsGuard Station Configs
87
+ # ==============================================================================
77
88
 
78
89
 
79
- # Time consuming but easy to mine.
80
- class CarbonExtractorConfig(ExtractorConfig):
81
- max_uses: int = Field(default=25)
82
-
83
- def station_cfg(self) -> AssemblerConfig:
84
- output = 2 * self.efficiency // 100
85
- return AssemblerConfig(
86
- name="carbon_extractor",
87
- render_symbol=vibes.VIBE_BY_NAME["carbon_a"].symbol,
88
- max_uses=self.max_uses,
89
- protocols=[
90
- ProtocolConfig(
91
- min_agents=(additional_agents + 1) if additional_agents >= 1 else 0,
92
- output_resources={"carbon": output * (100 + additional_agents * self.synergy) // 100},
93
- cooldown=0,
94
- )
95
- for additional_agents in range(4)
96
- ],
97
- # Clipping
98
- start_clipped=self.start_clipped,
99
- clip_immune=self.clip_immune,
100
- )
90
+ class SimpleExtractorConfig(CvCStationConfig):
91
+ """Simple resource extractor with inventory that transfers resources to actors."""
101
92
 
93
+ resource: str = Field(description="The resource to extract")
94
+ initial_amount: int = Field(default=100, description="Initial amount of resource in extractor")
95
+ small_amount: int = Field(default=1, description="Amount extracted without mining equipment")
96
+ large_amount: int = Field(default=10, description="Amount extracted with mining equipment")
102
97
 
103
- # Accumulates over time.
104
- class OxygenExtractorConfig(ExtractorConfig):
105
- max_uses: int = Field(default=5)
106
-
107
- def station_cfg(self) -> AssemblerConfig:
108
- # efficiency impacts cooldown, not output
109
- output = 10
110
- return AssemblerConfig(
111
- name="oxygen_extractor",
112
- render_symbol=vibes.VIBE_BY_NAME["oxygen_a"].symbol,
113
- max_uses=self.max_uses,
114
- allow_partial_usage=True, # can use it while its on cooldown
115
- protocols=[
116
- ProtocolConfig(
117
- min_agents=(additional_agents + 1) if additional_agents >= 1 else 0,
118
- output_resources={"oxygen": output * (100 + additional_agents * self.synergy) // 100},
119
- cooldown=int(10_000 / self.efficiency),
120
- )
121
- for additional_agents in range(4)
122
- ],
123
- # Clipping
124
- start_clipped=self.start_clipped,
125
- clip_immune=self.clip_immune,
98
+ def station_cfg(self) -> ChestConfig:
99
+ return ChestConfig(
100
+ name=f"{self.resource}_extractor",
101
+ map_name=f"{self.resource}_extractor",
102
+ render_symbol="📦",
103
+ on_use_handlers={
104
+ # Order matters: miner first so agents with miner gear get the bonus
105
+ "miner": Handler(
106
+ filters=[actorHas({"miner": 1})],
107
+ mutations=[withdraw({self.resource: self.large_amount}, remove_when_empty=True)],
108
+ ),
109
+ "extract": Handler(
110
+ filters=[],
111
+ mutations=[withdraw({self.resource: self.small_amount}, remove_when_empty=True)],
112
+ ),
113
+ },
114
+ inventory=InventoryConfig(initial={self.resource: self.initial_amount}),
126
115
  )
127
116
 
128
117
 
129
- # Rare regenerates slowly. More cogs increase the amount extracted.
130
- class GermaniumExtractorConfig(ExtractorConfig):
131
- max_uses: int = Field(default=5)
132
- synergy: int = 50
133
-
134
- def station_cfg(self) -> AssemblerConfig:
135
- # efficiency impacts cooldown, not output
136
- output = 2
137
- return AssemblerConfig(
138
- name="germanium_extractor",
139
- render_symbol=vibes.VIBE_BY_NAME["germanium_a"].symbol,
140
- max_uses=self.max_uses,
141
- protocols=[
142
- ProtocolConfig(
143
- min_agents=(additional_agents + 1) if additional_agents >= 1 else 0,
144
- output_resources={"germanium": output * (100 + additional_agents * self.synergy) // 100},
145
- cooldown=int(20_000 / self.efficiency),
146
- )
147
- for additional_agents in range(4)
148
- ],
149
- # Clipping
150
- start_clipped=self.start_clipped,
151
- clip_immune=self.clip_immune,
118
+ class JunctionConfig(CvCStationConfig):
119
+ """Supply depot that receives element resources via default vibe into collective."""
120
+
121
+ map_name: str = Field(description="Map name for this junction")
122
+ team: Optional[str] = Field(default=None, description="Team/collective this junction belongs to")
123
+ aoe_range: int = Field(default=10, description="Range for AOE effects")
124
+ influence_deltas: dict[str, int] = Field(default_factory=lambda: {"influence": 10, "energy": 100, "hp": 100})
125
+ attack_deltas: dict[str, int] = Field(default_factory=lambda: {"hp": -1, "influence": -100})
126
+ elements: list[str] = Field(default_factory=lambda: ELEMENTS)
127
+ align_cost: dict[str, int] = Field(default_factory=lambda: COGSGUARD_ALIGN_COST)
128
+ scramble_cost: dict[str, int] = Field(default_factory=lambda: COGSGUARD_SCRAMBLE_COST)
129
+
130
+ def station_cfg(self) -> GridObjectConfig:
131
+ return GridObjectConfig(
132
+ name="junction",
133
+ map_name=self.map_name,
134
+ render_symbol="📦",
135
+ collective=self.team,
136
+ aoes={
137
+ "influence": AOEConfig(
138
+ radius=self.aoe_range,
139
+ filters=[isAlignedToActor()],
140
+ mutations=[updateTarget(self.influence_deltas)],
141
+ ),
142
+ "attack": AOEConfig(
143
+ radius=self.aoe_range,
144
+ filters=[isEnemy()],
145
+ mutations=[updateTarget(self.attack_deltas)],
146
+ ),
147
+ },
148
+ on_use_handlers={
149
+ "deposit": Handler(
150
+ filters=[isAlignedToActor()],
151
+ mutations=[collectiveDeposit({resource: 100 for resource in self.elements})],
152
+ ),
153
+ "align": Handler(
154
+ filters=[isNeutral(), actorHas({"aligner": 1, "influence": 1, **self.align_cost})],
155
+ mutations=[updateActor(_neg(self.align_cost)), alignToActor()],
156
+ ),
157
+ "scramble": Handler(
158
+ filters=[isEnemy(), actorHas({"scrambler": 1, **self.scramble_cost})],
159
+ mutations=[removeAlignment(), updateActor(_neg(self.scramble_cost))],
160
+ ),
161
+ },
152
162
  )
153
163
 
154
164
 
155
- # Bulky and energy intensive.
156
- class SiliconExtractorConfig(ExtractorConfig):
157
- max_uses: int = Field(default=10)
158
-
159
- def station_cfg(self) -> AssemblerConfig:
160
- output = 15 * self.efficiency // 100
161
- return AssemblerConfig(
162
- name="silicon_extractor",
163
- render_symbol=vibes.VIBE_BY_NAME["silicon_a"].symbol,
164
- max_uses=self.max_uses,
165
- protocols=[
166
- ProtocolConfig(
167
- min_agents=(additional_agents + 1) if additional_agents >= 1 else 0,
168
- input_resources={"energy": 20},
169
- output_resources={"silicon": output * (100 + additional_agents * self.synergy) // 100},
170
- )
171
- for additional_agents in range(4)
172
- ],
173
- # Clipping
174
- start_clipped=self.start_clipped,
175
- clip_immune=self.clip_immune,
165
+ class HubConfig(JunctionConfig):
166
+ """Main hub with influence AOE effect. A junction without align/scramble handlers."""
167
+
168
+ def station_cfg(self) -> GridObjectConfig:
169
+ return GridObjectConfig(
170
+ name="hub",
171
+ map_name=self.map_name,
172
+ render_name="hub",
173
+ render_symbol="📦",
174
+ collective=self.team,
175
+ aoes={
176
+ "influence": AOEConfig(
177
+ radius=self.aoe_range,
178
+ filters=[isAlignedToActor()],
179
+ mutations=[updateTarget(self.influence_deltas)],
180
+ ),
181
+ "attack": AOEConfig(
182
+ radius=self.aoe_range,
183
+ filters=[isEnemy()],
184
+ mutations=[updateTarget(self.attack_deltas)],
185
+ ),
186
+ },
187
+ on_use_handlers={
188
+ "deposit": Handler(
189
+ filters=[isAlignedToActor()],
190
+ mutations=[collectiveDeposit({resource: 100 for resource in self.elements})],
191
+ ),
192
+ },
176
193
  )
177
194
 
178
195
 
179
- class CvCChestConfig(CvCStationConfig):
180
- initial_inventory: dict[str, int] = Field(default={}, description="Initial inventory for each resource type")
196
+ class CogsGuardChestConfig(CvCStationConfig):
197
+ """Chest for heart management in CogsGuard."""
181
198
 
182
- def station_cfg(self) -> ChestConfig:
183
- # Use map_name/name "chest" so maps and procedural builders that place
184
- # "chest" resolve to this config. The specific CvC type remains a label.
185
- return ChestConfig(
186
- render_symbol=vibes.VIBE_BY_NAME["chest"].symbol,
187
- vibe_transfers={
188
- "default": {"heart": 255, "carbon": 255, "oxygen": 255, "germanium": 255, "silicon": 255},
189
- "heart_a": {"heart": 0},
190
- "heart_b": {"heart": 1},
191
- "carbon_a": {"carbon": -10},
192
- "carbon_b": {"carbon": 10},
193
- "oxygen_a": {"oxygen": -10},
194
- "oxygen_b": {"oxygen": 10},
195
- "germanium_a": {"germanium": -1},
196
- "germanium_b": {"germanium": 1},
197
- "silicon_a": {"silicon": -25},
198
- "silicon_b": {"silicon": 25},
199
+ collective: str = Field(default="cogs", description="Collective this chest belongs to")
200
+ heart_cost: dict[str, int] = Field(default_factory=lambda: HEART_COST)
201
+
202
+ def station_cfg(self) -> GridObjectConfig:
203
+ return GridObjectConfig(
204
+ name="chest",
205
+ map_name="chest",
206
+ render_symbol="📦",
207
+ collective=self.collective,
208
+ on_use_handlers={
209
+ "get_heart": Handler(
210
+ filters=[isAlignedToActor(), targetCollectiveHas({"heart": 1})],
211
+ mutations=[collectiveWithdraw({"heart": 1})],
212
+ ),
213
+ "make_heart": Handler(
214
+ filters=[isAlignedToActor(), targetCollectiveHas(self.heart_cost)],
215
+ mutations=[
216
+ updateTargetCollective(_neg(self.heart_cost)),
217
+ updateActor({"heart": 1}),
218
+ ],
219
+ ),
199
220
  },
200
- inventory=InventoryConfig(initial=self.initial_inventory),
201
221
  )
202
222
 
203
223
 
204
- class CvCAssemblerConfig(CvCStationConfig):
205
- # These could be "fixed_cost" and "variable_cost" instead, but we're more likely to want to read them like this.
206
- first_heart_cost: int = Field(default=10)
207
- additional_heart_cost: int = Field(default=5)
208
-
209
- def station_cfg(self) -> AssemblerConfig:
210
- gear = [("carbon", "decoder"), ("oxygen", "modulator"), ("germanium", "scrambler"), ("silicon", "resonator")]
211
- return AssemblerConfig(
212
- name="assembler",
213
- render_symbol=vibes.VIBE_BY_NAME["assembler"].symbol,
214
- clip_immune=True,
215
- protocols=[
216
- ProtocolConfig(
217
- vibes=["heart_a"] * (i + 1),
218
- input_resources={
219
- "carbon": self.first_heart_cost + self.additional_heart_cost * i,
220
- "oxygen": self.first_heart_cost + self.additional_heart_cost * i,
221
- "germanium": max(1, (self.first_heart_cost + self.additional_heart_cost * i) // 5),
222
- "silicon": 3 * (self.first_heart_cost + self.additional_heart_cost * i),
223
- },
224
- output_resources={"heart": i + 1},
225
- )
226
- for i in range(4)
227
- ]
228
- + [
229
- # Specific gear protocols: ['gear', 'resource'] -> gear_item
230
- # Agent must have the specific resource AND use gear vibe
231
- ProtocolConfig(
232
- vibes=["gear", f"{gear[i][0]}_a"],
233
- input_resources={gear[i][0]: 1},
234
- output_resources={gear[i][1]: 1},
235
- )
236
- for i in range(len(gear))
237
- ],
238
- # Note: Generic ['gear'] protocol is added dynamically by clipping variants
239
- # C++ only allows ONE protocol per unique vibe list, so we can't pre-add all 4 here
224
+ class GearStationConfig(CvCStationConfig):
225
+ """Gear station that clears all gear and adds the specified gear type."""
226
+
227
+ gear_type: str = Field(description="Type of gear this station provides")
228
+ collective: str = Field(default="cogs", description="Collective this station belongs to")
229
+ gear_costs: dict[str, dict[str, int]] = Field(default_factory=lambda: GEAR_COSTS)
230
+
231
+ def station_cfg(self) -> GridObjectConfig:
232
+ cost = self.gear_costs.get(self.gear_type, {})
233
+ return GridObjectConfig(
234
+ name=f"{self.gear_type}_station",
235
+ map_name=f"{self.gear_type}_station",
236
+ render_symbol=GEAR_SYMBOLS.get(self.gear_type, "⚙️"),
237
+ collective=self.collective,
238
+ on_use_handlers={
239
+ "keep_gear": Handler(
240
+ filters=[isAlignedToActor(), actorHas({self.gear_type: 1})],
241
+ mutations=[],
242
+ ),
243
+ "change_gear": Handler(
244
+ filters=[isAlignedToActor(), actorCollectiveHas(cost)],
245
+ mutations=[
246
+ ClearInventoryMutation(target=EntityTarget.ACTOR, limit_name="gear"),
247
+ updateTargetCollective(_neg(cost)),
248
+ updateActor({self.gear_type: 1}),
249
+ ],
250
+ ),
251
+ },
240
252
  )
@@ -10,7 +10,7 @@ class TutorialVariant(MissionVariant):
10
10
 
11
11
  @override
12
12
  def modify_mission(self, mission: Mission) -> None:
13
- mission.energy_regen_amount = 1
13
+ mission.cog.energy_regen = 1
14
14
 
15
15
  @override
16
16
  def modify_env(self, mission: Mission, env) -> None: