| from __future__ import annotations | |
| import argparse | |
| import json | |
| import sys | |
| from collections import Counter | |
| from pathlib import Path | |
| from time import perf_counter | |
| sys.path.insert(0, str(Path(__file__).resolve().parents[1])) | |
| from tower_game.ai import LocalOpenAIGateway, MockGateway, load_model_config | |
| from tower_game.engine import TowerGame | |
| def prepared_state(game: TowerGame, seed: int = 7001): | |
| state, _ = game.new_character(seed=seed) | |
| game.confirm_allocation(state, [6, 6, 6, 6]) | |
| game.prepare_run(state) | |
| game.choose_starter_skill(state, 0) | |
| return state | |
| def record(rows, operation, gateway, started, fallback=False): | |
| metrics = dict(gateway.agent.last_metrics) | |
| rows.append( | |
| { | |
| "operation": operation, | |
| "latency_ms": round((perf_counter() - started) * 1000), | |
| "fallback": fallback, | |
| "last_error": getattr(gateway, "_last_error", ""), | |
| **metrics, | |
| } | |
| ) | |
| def repeated_verification(gateway: LocalOpenAIGateway, repetitions: int): | |
| game = TowerGame(gateway) | |
| state = prepared_state(game) | |
| state["current_floor"] = 10 | |
| state["boss_deck"] = [ | |
| move.model_dump() | |
| for move in MockGateway().boss_deck(state, final=True).moves | |
| ] | |
| rows = [] | |
| for index in range(repetitions): | |
| operations = [ | |
| ("run_setup", lambda: gateway.run_setup(state)), | |
| ("class_evolution", lambda: gateway.evolution(state, 1 + index % 2)), | |
| ( | |
| "boss_package", | |
| lambda: gateway.boss_package( | |
| state, game.assets.ids("boss"), final=bool(index % 2) | |
| ), | |
| ), | |
| ("boss_adjustment", lambda: gateway.boss_adjustment(state)), | |
| ("boss_turn_decision", lambda: gateway.boss_turn_decision(state)), | |
| ("ascension", lambda: gateway.ascension(state)), | |
| ] | |
| for name, operation in operations: | |
| started = perf_counter() | |
| operation() | |
| record(rows, name, gateway, started, bool(getattr(gateway, "_last_error", ""))) | |
| return rows | |
| def full_run(gateway: LocalOpenAIGateway): | |
| print("AI Ascension: generating run setup...", flush=True) | |
| game = TowerGame(gateway) | |
| state = prepared_state(game, seed=8101) | |
| guard = 0 | |
| last_phase = "" | |
| while state["game_phase"] != "ascension" and guard < 200: | |
| guard += 1 | |
| phase = state["game_phase"] | |
| if phase != last_phase: | |
| print( | |
| f"AI Ascension: {phase} / floor {state.get('current_floor')}", | |
| flush=True, | |
| ) | |
| last_phase = phase | |
| if phase == "combat": | |
| state["enemy_evasion"] = 0 | |
| state["enemy_hp"] = 1 | |
| game.act(state, "strike") | |
| if state.get("boss_thinking") and state["game_phase"] == "combat": | |
| decision, status = game.generate_boss_decision(state) | |
| game.apply_boss_decision(state, decision, status) | |
| elif phase == "victory": | |
| if state["victory_step"] == "level_skill": | |
| game.choose_level_skill(state, 0) | |
| if state["game_phase"] == "skill_replacement": | |
| game.replace_skill(state, 0) | |
| elif state["victory_step"] == "loot": | |
| while not state["proceed_ready"]: | |
| index = next( | |
| i | |
| for i, reward in enumerate(state["pending_loot"]) | |
| if not reward.get("claimed") | |
| ) | |
| game.choose_loot(state, index) | |
| game.proceed(state) | |
| else: | |
| game.proceed(state) | |
| elif phase == "evolution_loading": | |
| evolution, status = game.generate_evolution(state) | |
| game.complete_evolution_generation(state, evolution, status) | |
| elif phase == "evolution_reveal": | |
| game.embrace_evolution(state) | |
| elif phase == "evolution_healing": | |
| game.heal_choice(state, True) | |
| elif phase in {"skill_replacement", "evolution_skill_replace"}: | |
| game.replace_skill(state, 0) | |
| elif phase == "boss_loading": | |
| package, decision, status = game.generate_boss_intro(state) | |
| game.complete_boss_intro(state, package, decision, status) | |
| elif phase == "ascension_loading": | |
| passive, status = game.generate_ascension(state) | |
| game.complete_ascension(state, passive, status) | |
| elif phase == "defeat": | |
| raise RuntimeError("AI-enabled verification run was defeated") | |
| else: | |
| raise RuntimeError(f"unexpected phase during verification: {phase}") | |
| if state["game_phase"] != "ascension": | |
| raise RuntimeError("AI-enabled run did not reach Ascension") | |
| return { | |
| "floor": state["current_floor"], | |
| "ascension": state["ascension_level"], | |
| "fallbacks": state["agent_status"]["fallback_count"], | |
| } | |
| def main(): | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument("--repetitions", type=int, default=20) | |
| parser.add_argument("--skip-full-run", action="store_true") | |
| parser.add_argument( | |
| "--output", type=Path, default=Path(".local/llama.cpp/verification.json") | |
| ) | |
| args = parser.parse_args() | |
| config = load_model_config() | |
| config["backend"] = "local_openai" | |
| gateway = LocalOpenAIGateway(config) | |
| rows = repeated_verification(gateway, args.repetitions) | |
| summary = { | |
| "calls": len(rows), | |
| "fallbacks": sum(1 for row in rows if row["fallback"]), | |
| "operations": dict(Counter(row["operation"] for row in rows)), | |
| "average_latency_ms": round( | |
| sum(row["latency_ms"] for row in rows) / max(1, len(rows)) | |
| ), | |
| "results": rows, | |
| } | |
| if not args.skip_full_run: | |
| summary["full_run"] = full_run(gateway) | |
| args.output.parent.mkdir(parents=True, exist_ok=True) | |
| args.output.write_text(json.dumps(summary, indent=2), encoding="utf-8") | |
| print(json.dumps({key: value for key, value in summary.items() if key != "results"}, indent=2)) | |
| if __name__ == "__main__": | |
| main() | |