Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import sys | |
| from dataclasses import replace | |
| from pathlib import Path | |
| import numpy as np | |
| ROOT = Path(__file__).resolve().parents[1] | |
| if str(ROOT) not in sys.path: | |
| sys.path.insert(0, str(ROOT)) | |
| from life_game.game import GAME_MODES, SANDBOX_MODE, new_game | |
| from life_game.tuning import ( | |
| ALLOWED_DATA_KEYS, | |
| DATA_RANGES, | |
| ModeTuning, | |
| TUNING_SCHEMA, | |
| TuningProfile, | |
| apply_tuning_profile, | |
| load_tuning_profile, | |
| parse_tuning_profile, | |
| tuning_profile_to_dict, | |
| ) | |
| Recipe = str | |
| RECIPE_TARGET_SCALES: dict[Recipe, float] = { | |
| "easier": 0.85, | |
| "harder": 1.15, | |
| "shorter": 0.75, | |
| "longer": 1.25, | |
| } | |
| RECIPE_HEALTH_DELTAS: dict[Recipe, int] = { | |
| "easier": 1, | |
| "harder": -1, | |
| "shorter": 0, | |
| "longer": 0, | |
| } | |
| def main() -> None: | |
| parser = argparse.ArgumentParser(description="Create deterministic Signal Garden tuning profile mutations.") | |
| parser.add_argument("--input", default="", help="Optional base tuning JSON profile.") | |
| parser.add_argument("--output", required=True, help="Destination tuning JSON profile.") | |
| parser.add_argument("--recipe", choices=sorted(RECIPE_TARGET_SCALES), required=True) | |
| parser.add_argument( | |
| "--mode", | |
| action="append", | |
| default=[], | |
| help="Playable mode to tune. Repeat for multiple modes. Defaults to every registered arcade mode.", | |
| ) | |
| parser.add_argument("--size", type=int, default=24, help="Board size used to inspect mode defaults.") | |
| args = parser.parse_args() | |
| profile = load_tuning_profile(args.input) | |
| modes = tuple(args.mode) if args.mode else tuple(mode for mode in GAME_MODES if mode != SANDBOX_MODE) | |
| mutated = mutate_profile(profile, args.recipe, modes, max(12, int(args.size))) | |
| output = Path(args.output).expanduser() | |
| output.parent.mkdir(parents=True, exist_ok=True) | |
| output.write_text(json.dumps(tuning_profile_to_dict(mutated), indent=2, sort_keys=True) + "\n", encoding="utf-8") | |
| print(f"Wrote {output}") | |
| def mutate_profile(profile: TuningProfile, recipe: Recipe, modes: tuple[str, ...], size: int = 24) -> TuningProfile: | |
| if recipe not in RECIPE_TARGET_SCALES: | |
| raise ValueError(f"Unknown recipe: {recipe}") | |
| next_modes = dict(profile.modes) | |
| for index, mode in enumerate(modes): | |
| if mode == SANDBOX_MODE or mode not in GAME_MODES: | |
| raise ValueError(f"Cannot tune unsupported mode: {mode}") | |
| base_game = new_game(size, mode, np.random.default_rng(index + 17)) | |
| base_game = apply_tuning_profile(base_game, profile) | |
| existing = next_modes.get(mode, ModeTuning()) | |
| next_modes[mode] = _mutate_mode(existing, base_game.health, base_game.max_health, dict(base_game.data), recipe) | |
| description = profile.description or f"Generated by mutate_tuning.py recipe={recipe}" | |
| return TuningProfile(modes=next_modes, description=description) | |
| def _mutate_mode(existing: ModeTuning, health: int, max_health: int, data: dict[str, object], recipe: Recipe) -> ModeTuning: | |
| health_delta = RECIPE_HEALTH_DELTAS[recipe] | |
| target_scale = RECIPE_TARGET_SCALES[recipe] | |
| next_max_health = max(1, min(100, int(max_health) + health_delta)) | |
| next_health = max(1, min(next_max_health, int(health) + health_delta)) | |
| if recipe in {"shorter", "longer"}: | |
| next_health = existing.health if existing.health is not None else None | |
| next_max_health = existing.max_health if existing.max_health is not None else None | |
| next_data = dict(existing.data) | |
| for key, value in data.items(): | |
| if key not in ALLOWED_DATA_KEYS or isinstance(value, bool) or not isinstance(value, (int, float)): | |
| continue | |
| low, high = DATA_RANGES[key] | |
| scaled = _scale_value(value, target_scale, low, high) | |
| next_data[key] = scaled | |
| candidate = TuningProfile(modes={"candidate": ModeTuning(health=next_health, max_health=next_max_health, data=next_data)}) | |
| parse_tuning_profile(tuning_profile_to_dict(candidate)) | |
| return replace(existing, health=next_health, max_health=next_max_health, data=next_data) | |
| def _scale_value(value: int | float, scale: float, low: float, high: float) -> int | float: | |
| scaled = max(low, min(high, float(value) * scale)) | |
| if isinstance(value, int): | |
| return int(round(scaled)) | |
| return round(scaled, 3) | |
| if __name__ == "__main__": | |
| main() | |