signal-garden / scripts /mutate_tuning.py
deepmage121's picture
[codex] Prefer Modal llama.cpp endpoint
ddb2889 verified
Raw
History Blame Contribute Delete
4.41 kB
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()