cogames 0.3.59.post1.dev2__py3-none-any.whl → 0.3.65__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 (49) hide show
  1. cogames/cli/client.py +12 -0
  2. cogames/cli/leaderboard.py +40 -0
  3. cogames/cli/mission.py +31 -34
  4. cogames/cli/submit.py +1 -1
  5. cogames/cogs_vs_clips/clips.py +86 -0
  6. cogames/cogs_vs_clips/cog.py +14 -7
  7. cogames/cogs_vs_clips/cogsguard_tutorial.py +10 -11
  8. cogames/cogs_vs_clips/config.py +38 -0
  9. cogames/cogs_vs_clips/{cogs_vs_clips_mapgen.md → docs/cogs_vs_clips_mapgen.md} +6 -7
  10. cogames/cogs_vs_clips/evals/README.md +4 -4
  11. cogames/cogs_vs_clips/evals/cogsguard_evals.py +96 -0
  12. cogames/cogs_vs_clips/evals/diagnostic_evals.py +13 -100
  13. cogames/cogs_vs_clips/evals/difficulty_variants.py +9 -18
  14. cogames/cogs_vs_clips/evals/integrated_evals.py +8 -60
  15. cogames/cogs_vs_clips/evals/spanning_evals.py +48 -54
  16. cogames/cogs_vs_clips/mission.py +65 -277
  17. cogames/cogs_vs_clips/missions.py +16 -24
  18. cogames/cogs_vs_clips/sites.py +35 -25
  19. cogames/cogs_vs_clips/stations.py +33 -82
  20. cogames/cogs_vs_clips/team.py +44 -0
  21. cogames/cogs_vs_clips/{procedural.py → terrain.py} +12 -6
  22. cogames/cogs_vs_clips/variants.py +41 -118
  23. cogames/core.py +87 -0
  24. cogames/main.py +5 -1
  25. cogames/maps/evals/eval_balanced_spread.map +7 -3
  26. cogames/maps/evals/eval_clip_oxygen.map +7 -3
  27. cogames/maps/evals/eval_collect_resources.map +7 -3
  28. cogames/maps/evals/eval_collect_resources_hard.map +7 -3
  29. cogames/maps/evals/eval_collect_resources_medium.map +7 -3
  30. cogames/maps/evals/eval_divide_and_conquer.map +7 -3
  31. cogames/maps/evals/eval_energy_starved.map +7 -3
  32. cogames/maps/evals/eval_multi_coordinated_collect_hard.map +7 -3
  33. cogames/maps/evals/eval_oxygen_bottleneck.map +7 -3
  34. cogames/maps/evals/eval_single_use_world.map +7 -3
  35. cogames/maps/evals/extractor_hub_100x100.map +7 -3
  36. cogames/maps/evals/extractor_hub_30x30.map +7 -3
  37. cogames/maps/evals/extractor_hub_50x50.map +7 -3
  38. cogames/maps/evals/extractor_hub_70x70.map +7 -3
  39. cogames/maps/evals/extractor_hub_80x80.map +7 -3
  40. cogames/verbose.py +2 -2
  41. {cogames-0.3.59.post1.dev2.dist-info → cogames-0.3.65.dist-info}/METADATA +19 -3
  42. {cogames-0.3.59.post1.dev2.dist-info → cogames-0.3.65.dist-info}/RECORD +46 -44
  43. cogames/cogs_vs_clips/cogsguard_reward_variants.py +0 -138
  44. cogames/cogs_vs_clips/mission_utils.py +0 -19
  45. cogames/cogs_vs_clips/tutorial_missions.py +0 -25
  46. {cogames-0.3.59.post1.dev2.dist-info → cogames-0.3.65.dist-info}/WHEEL +0 -0
  47. {cogames-0.3.59.post1.dev2.dist-info → cogames-0.3.65.dist-info}/entry_points.txt +0 -0
  48. {cogames-0.3.59.post1.dev2.dist-info → cogames-0.3.65.dist-info}/licenses/LICENSE +0 -0
  49. {cogames-0.3.59.post1.dev2.dist-info → cogames-0.3.65.dist-info}/top_level.txt +0 -0
cogames/cli/client.py CHANGED
@@ -51,6 +51,8 @@ class PoolInfo(BaseModel):
51
51
 
52
52
  class SeasonInfo(BaseModel):
53
53
  name: str
54
+ version: int
55
+ canonical: bool
54
56
  summary: str
55
57
  entry_pool: str | None = None
56
58
  leaderboard_pool: str | None = None
@@ -58,6 +60,13 @@ class SeasonInfo(BaseModel):
58
60
  pools: list[PoolInfo]
59
61
 
60
62
 
63
+ class SeasonVersionInfo(BaseModel):
64
+ version: int
65
+ canonical: bool
66
+ disabled_at: str | None
67
+ created_at: str
68
+
69
+
61
70
  class LeaderboardEntry(BaseModel):
62
71
  rank: int
63
72
  policy: PolicyVersionSummary
@@ -174,6 +183,9 @@ class TournamentServerClient:
174
183
  def get_config(self, config_id: str) -> dict[str, Any]:
175
184
  return self._get(f"/tournament/configs/{config_id}")
176
185
 
186
+ def get_season_versions(self, season_name: str) -> list[SeasonVersionInfo]:
187
+ return self._get(f"/tournament/seasons/{season_name}/versions", list[SeasonVersionInfo])
188
+
177
189
  def get_leaderboard(self, season_name: str, include_hidden_seasons: bool = False) -> list[LeaderboardEntry]:
178
190
  return self._get(
179
191
  f"/tournament/seasons/{season_name}/leaderboard",
@@ -36,6 +36,22 @@ def parse_policy_identifier(identifier: str) -> tuple[str, int | None]:
36
36
  return identifier, None
37
37
 
38
38
 
39
+ def parse_season_ref(season_ref: str) -> tuple[str, int | None]:
40
+ if ":v" in season_ref:
41
+ name, version_str = season_ref.rsplit(":v", 1)
42
+ try:
43
+ return name, int(version_str)
44
+ except ValueError:
45
+ return season_ref, None
46
+ if ":" in season_ref:
47
+ name, version_str = season_ref.rsplit(":", 1)
48
+ try:
49
+ return name, int(version_str)
50
+ except ValueError:
51
+ return season_ref, None
52
+ return season_ref, None
53
+
54
+
39
55
  def _format_timestamp(value: Optional[str]) -> str:
40
56
  """Format ISO timestamps for CLI output."""
41
57
  if not value:
@@ -321,6 +337,8 @@ def leaderboard_cmd(
321
337
 
322
338
 
323
339
  def seasons_cmd(
340
+ season_name: Optional[str] = typer.Argument(None, help="Show versions of a specific season"),
341
+ versions: bool = typer.Option(False, "--versions", "-v", help="List all versions of the season"),
324
342
  login_server: str = typer.Option(
325
343
  DEFAULT_COGAMES_SERVER,
326
344
  "--login-server",
@@ -358,6 +376,28 @@ def seasons_cmd(
358
376
 
359
377
  try:
360
378
  with client:
379
+ if season_name and versions:
380
+ season_versions = client.get_season_versions(season_name)
381
+ if json_output:
382
+ console.print(json.dumps([v.model_dump() for v in season_versions], indent=2))
383
+ return
384
+
385
+ if not season_versions:
386
+ console.print(f"[yellow]No versions found for season '{season_name}'.[/yellow]")
387
+ return
388
+
389
+ table = Table(title=f"Versions: {season_name}", box=box.SIMPLE_HEAVY, show_lines=False, pad_edge=False)
390
+ table.add_column("Version", style="bold cyan")
391
+ table.add_column("Status", style="white")
392
+ table.add_column("Created", style="dim")
393
+
394
+ for v in season_versions:
395
+ status = "[green]canonical[/green]" if v.canonical else "[dim]historical[/dim]"
396
+ table.add_row(f"v{v.version}", status, _format_timestamp(v.created_at))
397
+
398
+ console.print(table)
399
+ return
400
+
361
401
  seasons = client.get_seasons()
362
402
  except httpx.HTTPError as exc:
363
403
  console.print(f"[red]Failed to reach server:[/red] {exc}")
cogames/cli/mission.py CHANGED
@@ -9,53 +9,48 @@ from rich.table import Table
9
9
 
10
10
  from cogames.cli.base import console
11
11
  from cogames.cogs_vs_clips.mission import (
12
- MAP_MISSION_DELIMITER,
13
- AnyMission,
14
- Mission,
15
- MissionVariant,
12
+ CvCMission,
16
13
  NumCogsVariant,
17
- Site,
18
14
  )
19
- from cogames.cogs_vs_clips.procedural import MachinaArena
20
15
  from cogames.cogs_vs_clips.sites import SITES
16
+ from cogames.cogs_vs_clips.terrain import MachinaArena
21
17
  from cogames.cogs_vs_clips.variants import HIDDEN_VARIANTS, VARIANTS
18
+ from cogames.core import (
19
+ MAP_MISSION_DELIMITER,
20
+ CoGameMissionVariant,
21
+ CoGameSite,
22
+ )
22
23
  from cogames.game import load_mission_config, load_mission_config_from_python
23
24
  from mettagrid import MettaGridConfig
24
25
  from mettagrid.mapgen.mapgen import MapGen
25
26
 
26
27
 
27
28
  @lru_cache(maxsize=1)
28
- def _get_core_missions() -> list[AnyMission]:
29
+ def _get_core_missions() -> list[CvCMission]:
29
30
  from cogames.cogs_vs_clips.missions import get_core_missions
30
31
 
31
32
  return get_core_missions()
32
33
 
33
34
 
34
35
  @lru_cache(maxsize=1)
35
- def _get_legacy_missions() -> list[Mission]:
36
- from cogames.cogs_vs_clips.missions import get_legacy_missions
37
-
38
- return get_legacy_missions()
39
-
40
-
41
- @lru_cache(maxsize=1)
42
- def _get_eval_missions_all() -> list[Mission]:
36
+ def _get_eval_missions_all() -> list[CvCMission]:
43
37
  from cogames.cogs_vs_clips.evals.diagnostic_evals import DIAGNOSTIC_EVALS
44
38
  from cogames.cogs_vs_clips.evals.integrated_evals import EVAL_MISSIONS as INTEGRATED_EVAL_MISSIONS
45
39
  from cogames.cogs_vs_clips.evals.spanning_evals import EVAL_MISSIONS as SPANNING_EVAL_MISSIONS
46
40
 
47
- missions: list[Mission] = []
41
+ missions: list[CvCMission] = []
48
42
  missions.extend(INTEGRATED_EVAL_MISSIONS)
49
43
  missions.extend(SPANNING_EVAL_MISSIONS)
50
44
  missions.extend(mission_cls() for mission_cls in DIAGNOSTIC_EVALS) # type: ignore[call-arg]
51
45
  return missions
52
46
 
53
47
 
54
- def load_mission_set(mission_set: str) -> list[AnyMission]:
48
+ def load_mission_set(mission_set: str) -> list[CvCMission]:
55
49
  """Load a predefined set of evaluation missions.
56
50
 
57
51
  Args:
58
52
  mission_set: Name of mission set to load. Options:
53
+ - "cogsguard_evals": CogsGuard evaluation missions (map-based)
59
54
  - "integrated_evals": Integrated evaluation missions
60
55
  - "spanning_evals": Spanning evaluation missions
61
56
  - "diagnostic_evals": Diagnostic evaluation missions
@@ -67,7 +62,7 @@ def load_mission_set(mission_set: str) -> list[AnyMission]:
67
62
  Raises:
68
63
  ValueError: If mission_set name is unknown
69
64
  """
70
- missions_list: list[AnyMission]
65
+ missions_list: list[CvCMission]
71
66
  if mission_set == "all":
72
67
  # All missions: eval missions + integrated + spanning + diagnostic + core missions
73
68
  missions_list = list(_get_eval_missions_all())
@@ -82,6 +77,10 @@ def load_mission_set(mission_set: str) -> list[AnyMission]:
82
77
  from cogames.cogs_vs_clips.evals.diagnostic_evals import DIAGNOSTIC_EVALS
83
78
 
84
79
  missions_list = [mission_cls() for mission_cls in DIAGNOSTIC_EVALS] # type: ignore[call-arg]
80
+ elif mission_set == "cogsguard_evals":
81
+ from cogames.cogs_vs_clips.evals.cogsguard_evals import COGSGUARD_EVAL_MISSIONS
82
+
83
+ missions_list = list(COGSGUARD_EVAL_MISSIONS)
85
84
  elif mission_set == "integrated_evals":
86
85
  from cogames.cogs_vs_clips.evals.integrated_evals import EVAL_MISSIONS as INTEGRATED_EVAL_MISSIONS
87
86
 
@@ -91,20 +90,20 @@ def load_mission_set(mission_set: str) -> list[AnyMission]:
91
90
 
92
91
  missions_list = list(SPANNING_EVAL_MISSIONS)
93
92
  else:
94
- available = "integrated_evals, spanning_evals, diagnostic_evals, all"
93
+ available = "cogsguard_evals, integrated_evals, spanning_evals, diagnostic_evals, all"
95
94
  raise ValueError(f"Unknown mission set: {mission_set}\nAvailable sets: {available}")
96
95
 
97
96
  return missions_list
98
97
 
99
98
 
100
- def parse_variants(variants_arg: Optional[list[str]]) -> list[MissionVariant]:
99
+ def parse_variants(variants_arg: Optional[list[str]]) -> list[CoGameMissionVariant]:
101
100
  """Parse variant specifications from command line.
102
101
 
103
102
  Args:
104
103
  variants_arg: List of variant names like ["solar_flare", "dark_side"]
105
104
 
106
105
  Returns:
107
- List of configured MissionVariant instances
106
+ List of configured CoGameMissionVariant instances
108
107
 
109
108
  Raises:
110
109
  ValueError: If variant name is unknown
@@ -112,11 +111,11 @@ def parse_variants(variants_arg: Optional[list[str]]) -> list[MissionVariant]:
112
111
  if not variants_arg:
113
112
  return []
114
113
 
115
- variants: list[MissionVariant] = []
114
+ variants: list[CoGameMissionVariant] = []
116
115
  all_variants = [*VARIANTS, *HIDDEN_VARIANTS]
117
116
  for name in variants_arg:
118
117
  # Find matching variant class by instantiating and checking the name
119
- variant: MissionVariant | None = None
118
+ variant: CoGameMissionVariant | None = None
120
119
  for v in all_variants:
121
120
  if v.name == name:
122
121
  variant = v
@@ -143,7 +142,7 @@ def get_all_eval_missions() -> list[str]:
143
142
  return [mission.full_name() for mission in _get_eval_missions_all()]
144
143
 
145
144
 
146
- def get_site_by_name(site_name: str) -> Site:
145
+ def get_site_by_name(site_name: str) -> CoGameSite:
147
146
  """Get a site by name.
148
147
 
149
148
  Raises:
@@ -159,7 +158,7 @@ def get_site_by_name(site_name: str) -> Site:
159
158
 
160
159
  def get_mission_name_and_config(
161
160
  ctx: typer.Context, mission_arg: Optional[str], variants_arg: Optional[list[str]] = None, cogs: Optional[int] = None
162
- ) -> tuple[str, MettaGridConfig, Optional[AnyMission]]:
161
+ ) -> tuple[str, MettaGridConfig, Optional[CvCMission]]:
163
162
  if not mission_arg:
164
163
  console.print(ctx.get_help())
165
164
  console.print("[yellow]Missing: --mission / -m[/yellow]\n")
@@ -246,12 +245,10 @@ def find_mission(
246
245
  *,
247
246
  include_evals: bool = False,
248
247
  include_legacy: bool = False,
249
- ) -> AnyMission:
250
- missions: list[AnyMission] = list(_get_core_missions())
248
+ ) -> CvCMission:
249
+ missions: list[CvCMission] = list(_get_core_missions())
251
250
  if include_evals:
252
251
  missions = [*missions, *_get_eval_missions_all()]
253
- if include_legacy:
254
- missions = [*missions, *_get_legacy_missions()]
255
252
 
256
253
  found_site = False
257
254
  for mission in missions:
@@ -281,7 +278,7 @@ def get_mission(
281
278
  variants_arg: Optional[list[str]] = None,
282
279
  cogs: Optional[int] = None,
283
280
  include_legacy: bool = False,
284
- ) -> tuple[str, MettaGridConfig, Optional[AnyMission]]:
281
+ ) -> tuple[str, MettaGridConfig, Optional[CvCMission]]:
285
282
  """Get a specific mission configuration by name or file path.
286
283
 
287
284
  Args:
@@ -291,7 +288,7 @@ def get_mission(
291
288
  include_legacy: Whether to include legacy (pre-CogsGuard) missions
292
289
 
293
290
  Returns:
294
- Tuple of (mission name, MettaGridConfig, Mission or None)
291
+ Tuple of (mission name, MettaGridConfig, CvCMission or None)
295
292
 
296
293
  Raises:
297
294
  ValueError: If mission not found or file cannot be loaded
@@ -323,7 +320,7 @@ def get_mission(
323
320
  else:
324
321
  site_name, mission_name = mission_arg.split(MAP_MISSION_DELIMITER)
325
322
 
326
- mission: AnyMission = find_mission(site_name, mission_name, include_evals=True, include_legacy=include_legacy)
323
+ mission: CvCMission = find_mission(site_name, mission_name, include_evals=True, include_legacy=include_legacy)
327
324
 
328
325
  if variants:
329
326
  mission = mission.with_variants(variants)
@@ -447,7 +444,7 @@ def list_evals() -> None:
447
444
  return
448
445
 
449
446
  # Group missions by site
450
- missions_by_site: dict[str, list[Mission]] = {}
447
+ missions_by_site: dict[str, list[CvCMission]] = {}
451
448
  for m in evals:
452
449
  missions_by_site.setdefault(m.site.name, []).append(m)
453
450
 
@@ -499,7 +496,7 @@ def list_evals() -> None:
499
496
  console.print(" [bold]cogames play[/bold] --mission [blue]evals.divide_and_conquer[/blue]")
500
497
 
501
498
 
502
- def describe_mission(mission_name: str, game_config: MettaGridConfig, mission_cfg: AnyMission | None = None) -> None:
499
+ def describe_mission(mission_name: str, game_config: MettaGridConfig, mission_cfg: CvCMission | None = None) -> None:
503
500
  """Print detailed information about a specific mission.
504
501
 
505
502
  Args:
cogames/cli/submit.py CHANGED
@@ -290,6 +290,7 @@ def validate_policy_in_isolation(
290
290
  "run",
291
291
  "cogames",
292
292
  "validate-policy",
293
+ "--policy",
293
294
  policy_arg,
294
295
  "--season",
295
296
  season,
@@ -298,7 +299,6 @@ def validate_policy_in_isolation(
298
299
  ]
299
300
  if setup_script:
300
301
  validate_cmd.extend(["--setup-script", setup_script])
301
- validate_cmd.extend(["--policy", policy_arg])
302
302
 
303
303
  _run_from_tmp_dir(validate_cmd)
304
304
 
@@ -0,0 +1,86 @@
1
+ """Clips behavior events for CogsGuard missions.
2
+
3
+ Clips are a non-player faction that gradually takes over neutral junctions.
4
+ These events create the spreading/scrambling behavior that pressures players.
5
+ """
6
+
7
+ from pydantic import Field
8
+
9
+ from mettagrid.base_config import Config
10
+ from mettagrid.config.event_config import EventConfig, once, periodic
11
+ from mettagrid.config.filter import isAlignedTo, isNear
12
+ from mettagrid.config.filter.alignment_filter import isNeutral, isNotAlignedTo, isNotNeutral
13
+ from mettagrid.config.mettagrid_config import CollectiveConfig
14
+ from mettagrid.config.mutation import alignTo, removeAlignment
15
+ from mettagrid.config.tag import typeTag
16
+
17
+
18
+ class ClipsConfig(Config):
19
+ """Configuration for clips behavior in CogsGuard game mode."""
20
+
21
+ # Clips Behavior - scramble cogs junctions to neutral
22
+ initial_clips_start: int = Field(default=10)
23
+ initial_clips_spots: int = Field(default=1)
24
+
25
+ scramble_start: int = Field(default=50)
26
+ scramble_interval: int = Field(default=100)
27
+ scramble_radius: int = Field(default=25)
28
+
29
+ # Clips Behavior - align neutral junctions to clips
30
+ align_start: int = Field(default=100)
31
+ align_interval: int = Field(default=100)
32
+ align_radius: int = Field(default=25)
33
+
34
+ def events(self, max_steps: int) -> dict[str, EventConfig]:
35
+ """Create all clips events for a mission.
36
+
37
+ Returns:
38
+ Dictionary of event name to EventConfig.
39
+ """
40
+ return {
41
+ "initial_clips": EventConfig(
42
+ name="initial_clips",
43
+ target_tag=typeTag("junction"),
44
+ timesteps=once(self.initial_clips_start),
45
+ mutations=[alignTo("clips")],
46
+ max_targets=self.initial_clips_spots,
47
+ ),
48
+ "cogs_to_neutral": EventConfig(
49
+ name="cogs_to_neutral",
50
+ target_tag=typeTag("junction"),
51
+ timesteps=periodic(start=self.scramble_start, period=self.scramble_interval, end=max_steps),
52
+ # near a clips-aligned junction
53
+ filters=[
54
+ isNear(typeTag("junction"), [isAlignedTo("clips")], radius=self.scramble_radius),
55
+ isNotAlignedTo("clips"),
56
+ isNotNeutral(),
57
+ ],
58
+ mutations=[removeAlignment()],
59
+ max_targets=1,
60
+ ),
61
+ "neutral_to_clips": EventConfig(
62
+ name="neutral_to_clips",
63
+ target_tag=typeTag("junction"),
64
+ timesteps=periodic(start=self.align_start, period=self.align_interval, end=max_steps),
65
+ # neutral junctions near a clips-aligned junction
66
+ filters=[
67
+ isNear(typeTag("junction"), [isAlignedTo("clips")], radius=self.align_radius),
68
+ isNeutral(),
69
+ ],
70
+ mutations=[alignTo("clips")],
71
+ max_targets=1,
72
+ ),
73
+ # If there are no clips-aligned junctions, re-invade
74
+ "presence_check": EventConfig(
75
+ name="presence_check",
76
+ target_tag=typeTag("junction"),
77
+ timesteps=periodic(start=self.initial_clips_start, period=self.scramble_interval * 2, end=max_steps),
78
+ filters=[isNear(typeTag("junction"), [isAlignedTo("clips")], radius=1000)],
79
+ max_targets=1,
80
+ fallback="initial_clips",
81
+ ),
82
+ }
83
+
84
+ def collective_config(self) -> CollectiveConfig:
85
+ """Create a CollectiveConfig for this clips configuration."""
86
+ return CollectiveConfig(name="clips")
@@ -2,7 +2,9 @@ from __future__ import annotations
2
2
 
3
3
  from pydantic import Field
4
4
 
5
+ from cogames.cogs_vs_clips.config import CvCConfig
5
6
  from mettagrid.base_config import Config
7
+ from mettagrid.config.game_value import stat as game_stat
6
8
  from mettagrid.config.handler_config import Handler
7
9
  from mettagrid.config.mettagrid_config import (
8
10
  AgentConfig,
@@ -10,6 +12,7 @@ from mettagrid.config.mettagrid_config import (
10
12
  ResourceLimitsConfig,
11
13
  )
12
14
  from mettagrid.config.mutation.resource_mutation import updateActor
15
+ from mettagrid.config.reward_config import reward
13
16
 
14
17
 
15
18
  class CogConfig(Config):
@@ -37,25 +40,23 @@ class CogConfig(Config):
37
40
  energy_regen: int = Field(default=1)
38
41
  hp_regen: int = Field(default=-1)
39
42
  influence_regen: int = Field(default=-1)
43
+ action_cost: dict[str, int] = Field(default_factory=lambda: {"energy": 3})
40
44
 
41
- # Movement cost
42
- move_energy_cost: int = Field(default=3)
43
-
44
- def agent_config(self, gear: list[str], elements: list[str]) -> AgentConfig:
45
+ def agent_config(self, team: str, max_steps: int) -> AgentConfig:
45
46
  """Create an AgentConfig for this cog configuration."""
46
47
  return AgentConfig(
47
- collective="cogs",
48
+ collective=team,
48
49
  inventory=InventoryConfig(
49
50
  limits={
50
51
  "hp": ResourceLimitsConfig(min=self.hp_limit, resources=["hp"], modifiers=self.hp_modifiers),
51
52
  # when hp == 0, the cog can't hold gear or hearts
52
- "gear": ResourceLimitsConfig(max=self.gear_limit, resources=gear, modifiers={"hp": 100}),
53
+ "gear": ResourceLimitsConfig(max=self.gear_limit, resources=CvCConfig.GEAR, modifiers={"hp": 100}),
53
54
  "heart": ResourceLimitsConfig(max=self.heart_limit, resources=["heart"], modifiers={"hp": 100}),
54
55
  "energy": ResourceLimitsConfig(
55
56
  min=self.energy_limit, resources=["energy"], modifiers=self.energy_modifiers
56
57
  ),
57
58
  "cargo": ResourceLimitsConfig(
58
- min=self.cargo_limit, resources=elements, modifiers=self.cargo_modifiers
59
+ min=self.cargo_limit, resources=CvCConfig.ELEMENTS, modifiers=self.cargo_modifiers
59
60
  ),
60
61
  "influence": ResourceLimitsConfig(
61
62
  min=self.influence_limit, resources=["influence"], modifiers=self.influence_modifiers
@@ -76,4 +77,10 @@ class CogConfig(Config):
76
77
  ]
77
78
  )
78
79
  },
80
+ rewards={
81
+ "aligned_junction_held": reward(
82
+ game_stat("collective.aligned.junction.held"),
83
+ weight=1.0 / max_steps,
84
+ ),
85
+ },
79
86
  )
@@ -1,12 +1,14 @@
1
1
  """CogsGuard tutorial mission configuration."""
2
2
 
3
- from cogames.cogs_vs_clips.mission import Mission, Site
4
- from cogames.cogs_vs_clips.procedural import MachinaArena
3
+ from cogames.cogs_vs_clips.mission import CvCMission
4
+ from cogames.cogs_vs_clips.team import CogTeam
5
+ from cogames.cogs_vs_clips.terrain import MachinaArena
6
+ from cogames.core import CoGameSite
5
7
  from mettagrid.mapgen.mapgen import MapGen
6
8
  from mettagrid.mapgen.scenes.base_hub import BaseHubConfig
7
9
 
8
10
 
9
- def make_cogsguard_tutorial_site() -> Site:
11
+ def make_cogsguard_tutorial_site() -> CoGameSite:
10
12
  """Create a smaller, simpler CogsGuard arena for the tutorial."""
11
13
  hub_config = BaseHubConfig(
12
14
  corner_bundle="extractors",
@@ -32,7 +34,7 @@ def make_cogsguard_tutorial_site() -> Site:
32
34
  hub=hub_config,
33
35
  ),
34
36
  )
35
- return Site(
37
+ return CoGameSite(
36
38
  name="cogsguard_tutorial",
37
39
  description="CogsGuard tutorial arena - small map for learning",
38
40
  map_builder=map_builder,
@@ -41,16 +43,13 @@ def make_cogsguard_tutorial_site() -> Site:
41
43
  )
42
44
 
43
45
 
44
- CogsGuardTutorialMission = Mission(
46
+ CogsGuardTutorialMission = CvCMission(
45
47
  name="tutorial",
46
48
  description="Learn the basics of CogsGuard: Roles, Resources, and Territory Control.",
47
49
  site=make_cogsguard_tutorial_site(),
48
50
  num_cogs=1,
49
51
  max_steps=2000,
50
- # Generous initial resources for learning
51
- collective_initial_carbon=50,
52
- collective_initial_oxygen=50,
53
- collective_initial_germanium=50,
54
- collective_initial_silicon=50,
55
- collective_initial_heart=10,
52
+ teams={
53
+ "cogs": CogTeam(name="cogs", num_agents=1, wealth=1),
54
+ },
56
55
  )
@@ -0,0 +1,38 @@
1
+ from types import SimpleNamespace
2
+
3
+ from mettagrid.config.vibes import Vibe
4
+
5
+ _GEAR = ["aligner", "scrambler", "miner", "scout"]
6
+ _ELEMENTS = ["oxygen", "carbon", "germanium", "silicon"]
7
+ _VIBES = [
8
+ Vibe("😐", "default"),
9
+ Vibe("❤️", "heart"),
10
+ Vibe("⚙️", "gear"),
11
+ Vibe("🌀", "scrambler"),
12
+ Vibe("🔗", "aligner"),
13
+ Vibe("⛏️", "miner"),
14
+ Vibe("🔭", "scout"),
15
+ ]
16
+
17
+ CvCConfig = SimpleNamespace(
18
+ GEAR=_GEAR,
19
+ ELEMENTS=_ELEMENTS,
20
+ HEART_COST={e: 10 for e in _ELEMENTS},
21
+ ALIGN_COST={"heart": 1},
22
+ SCRAMBLE_COST={"heart": 1},
23
+ GEAR_COSTS={
24
+ "aligner": {"carbon": 3, "oxygen": 1, "germanium": 1, "silicon": 1},
25
+ "scrambler": {"carbon": 1, "oxygen": 3, "germanium": 1, "silicon": 1},
26
+ "miner": {"carbon": 1, "oxygen": 1, "germanium": 3, "silicon": 1},
27
+ "scout": {"carbon": 1, "oxygen": 1, "germanium": 1, "silicon": 3},
28
+ },
29
+ GEAR_SYMBOLS={
30
+ "aligner": "🔗",
31
+ "scrambler": "🌀",
32
+ "miner": "⛏️",
33
+ "scout": "🔭",
34
+ },
35
+ RESOURCES=["energy", "heart", "hp", "influence", *_ELEMENTS, *_GEAR],
36
+ VIBES=_VIBES,
37
+ VIBE_NAMES=[vibe.name for vibe in _VIBES],
38
+ )
@@ -324,7 +324,6 @@ Key design choices:
324
324
 
325
325
  - Pack the base hub lightly (`EmptyBaseVariant`) where appropriate to encourage early exploration without
326
326
  over-constraining.
327
- - Add guidance (`CompassVariant`) on distance-heavy tasks to reduce pure exploration failure modes.
328
327
  - Raise agent caps modestly (`PackRatVariant`) to avoid early inventory stalls but keep routing relevant.
329
328
  - Shape reward on vibe missions (`HeartChorusVariant`) so partial progress is scored.
330
329
  - Keep vibe mechanics intact unless the mission explicitly focuses on vibe manipulation.
@@ -334,9 +333,9 @@ Included missions and variants:
334
333
  - oxygen_bottleneck: `EmptyBaseVariant(missing=["oxygen_extractor"])`, `ResourceBottleneckVariant(["oxygen"])`,
335
334
  `SingleResourceUniformVariant("oxygen_extractor")`, `PackRatVariant`
336
335
  - energy_starved: `EmptyBaseVariant`, `DarkSideVariant`, `PackRatVariant`
337
- - distant_resources: `EmptyBaseVariant`, `CompassVariant`, `DistantResourcesVariant`
338
- - quadrant_buildings: `EmptyBaseVariant`, `QuadrantBuildingsVariant`, `CompassVariant`
339
- - single_use_swarm: `EmptyBaseVariant`, `SingleUseSwarmVariant`, `CompassVariant`, `PackRatVariant`
336
+ - distant_resources: `EmptyBaseVariant`, `DistantResourcesVariant`
337
+ - quadrant_buildings: `EmptyBaseVariant`, `QuadrantBuildingsVariant`
338
+ - single_use_swarm: `EmptyBaseVariant`, `SingleUseSwarmVariant`, `PackRatVariant`
340
339
  - vibe_check: `HeartChorusVariant`, `VibeCheckMin2Variant`
341
340
 
342
341
  Usage example:
@@ -349,6 +348,6 @@ uv run python packages/cogames/scripts/run_evaluation.py \
349
348
  --repeats 2
350
349
  ```
351
350
 
352
- Recommendation: When designing new scorable baselines, combine one shaping variant (e.g., `CompassVariant`,
353
- `HeartChorusVariant`, `PackRatVariant`) with one constraint variant (e.g., `DarkSideVariant`,
354
- `ResourceBottleneckVariant`, `SingleUseSwarmVariant`) to keep tasks legible yet challenging.
351
+ Recommendation: When designing new scorable baselines, combine one "shaping" variant (e.g., `HeartChorusVariant`,
352
+ `PackRatVariant`) with one "constraint" variant (e.g., `DarkSideVariant`, `ResourceBottleneckVariant`,
353
+ `SingleUseSwarmVariant`) to keep tasks legible yet challenging.
@@ -179,11 +179,11 @@ uv run cogames play --mission hello_world.easy_hearts --cogs 2
179
179
  You can apply additional variants on top of the mission's built-in variants:
180
180
 
181
181
  ```bash
182
- # Add compass variant
183
- uv run cogames play --mission hello_world.oxygen_bottleneck --cogs 2 --variant compass
182
+ # Add pack_rat variant
183
+ uv run cogames play --mission hello_world.oxygen_bottleneck --cogs 2 --variant pack_rat
184
184
 
185
185
  # Add multiple variants
186
- uv run cogames play --mission hello_world.energy_starved --cogs 2 --variant compass --variant small_50
186
+ uv run cogames play --mission hello_world.energy_starved --cogs 2 --variant pack_rat --variant small_50
187
187
 
188
188
  # With policy
189
189
  uv run cogames play --mission hello_world.single_use_swarm --cogs 4 -p baseline
@@ -230,7 +230,7 @@ mission_variant_curriculum.train(
230
230
  mission_variant_curriculum.train(
231
231
  base_missions=["oxygen_bottleneck", "energy_starved"],
232
232
  num_cogs=4,
233
- variants=["compass", "pack_rat"]
233
+ variants=["pack_rat", "energized"]
234
234
  )
235
235
  ```
236
236