|
|
|
@@ -47,10 +47,13 @@ class AiHttpGameHandler(GameHandler):
|
|
|
|
|
VALIDATION_DIFFICULTY = 90
|
|
|
|
|
REPLAY_FRAME_DELAY = 0.1
|
|
|
|
|
# Scoring weights TOTAL MUST BE 100
|
|
|
|
|
COINS_WEIGHT = 50
|
|
|
|
|
COINS_WEIGHT = 35
|
|
|
|
|
STEPS_WEIGHT = 10
|
|
|
|
|
DISTANCE_WEIGHT = 10
|
|
|
|
|
WIN_WEIGHT = 30
|
|
|
|
|
WIN_WEIGHT = 45
|
|
|
|
|
PLAY_ALL_MAPS_PER_GENERATION = True # Are all maps played during a generation or only MAPS_PER_GENERATION maps per generation are played
|
|
|
|
|
MAPS_PER_GENERATION = 9 # Evaluate across x maps per generation. Only used if PLAY_ALL_MAPS_PER_GENERATION is False. If True, all maps are played in a generation.
|
|
|
|
|
CONSISTENCY_PENALTY_WEIGHT = 0.4 # Penalty for inconsistent map scores
|
|
|
|
|
|
|
|
|
|
def __init__(self, host: str | None = None, port: int | None = None,
|
|
|
|
|
max_workers: int | None = None):
|
|
|
|
@@ -80,6 +83,9 @@ class AiHttpGameHandler(GameHandler):
|
|
|
|
|
"replay_condition": Condition(),
|
|
|
|
|
"client_connected": True,
|
|
|
|
|
"stagnant_generations": 0,
|
|
|
|
|
"map_round": 0,
|
|
|
|
|
"cumulative_scores": {},
|
|
|
|
|
"last_results": {}
|
|
|
|
|
}
|
|
|
|
|
replay_thread = Thread(target=handler._replay_best_runs, args=(session,))
|
|
|
|
|
replay_thread.start()
|
|
|
|
@@ -132,15 +138,34 @@ class AiHttpGameHandler(GameHandler):
|
|
|
|
|
raise ValueError("Simulation requests must belong to a TCP session.")
|
|
|
|
|
if session.get("simulations"):
|
|
|
|
|
raise ValueError("A simulation is already active in this session.")
|
|
|
|
|
session["generation"] = session.get("generation", 0) + 1
|
|
|
|
|
selected_map = self._select_map(session.get("performance", 0.0), validation)
|
|
|
|
|
session["last_result"] = None
|
|
|
|
|
|
|
|
|
|
# print(
|
|
|
|
|
# f"Selected map for simulation: {selected_map.name} "
|
|
|
|
|
# f"(difficulty: {selected_map.difficulty})",
|
|
|
|
|
# flush=True,
|
|
|
|
|
# )
|
|
|
|
|
# Initialize generation round tracking and select 3 maps upfront if starting fresh
|
|
|
|
|
if session.get("map_round", 0) == 0:
|
|
|
|
|
session["generation"] = session.get("generation", 0) + 1
|
|
|
|
|
session["map_scores"] = {model.get("id"): [] for model in models}
|
|
|
|
|
|
|
|
|
|
# Pre-select the 3 maps for this entire generation based on current performance
|
|
|
|
|
if validation:
|
|
|
|
|
session["generation_maps"] = [
|
|
|
|
|
random.choice(self.validation_maps)
|
|
|
|
|
for _ in range(self.MAPS_PER_GENERATION)
|
|
|
|
|
]
|
|
|
|
|
else:
|
|
|
|
|
perf = session.get("performance", 0.0)
|
|
|
|
|
if self.PLAY_ALL_MAPS_PER_GENERATION:
|
|
|
|
|
session["generation_maps"] = self.map_set.get_maps()
|
|
|
|
|
else:
|
|
|
|
|
available_maps = self.map_set.get_maps_by_difficulty(round(perf), self.MAPS_PER_GENERATION)
|
|
|
|
|
session["generation_maps"] = [
|
|
|
|
|
random.choice(available_maps)
|
|
|
|
|
for _ in range(self.MAPS_PER_GENERATION) # Randomly selects in case there is less maps available than MAPS_PER_GENERATION
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
# Fetch the pre-selected map for the current round
|
|
|
|
|
map_round = session.get("map_round", 0)
|
|
|
|
|
selected_map = session["generation_maps"][map_round]
|
|
|
|
|
|
|
|
|
|
session["last_result"] = None
|
|
|
|
|
|
|
|
|
|
simulations = {}
|
|
|
|
|
for model in models:
|
|
|
|
@@ -150,9 +175,7 @@ class AiHttpGameHandler(GameHandler):
|
|
|
|
|
if model_id in simulations:
|
|
|
|
|
raise ValueError(f"Duplicate model id '{model_id}'.")
|
|
|
|
|
simulation = AiSimulation(selected_map)
|
|
|
|
|
# print(f"Starting simulation for model '{model_id}'", flush=True)
|
|
|
|
|
simulation.start()
|
|
|
|
|
# print(f"Model '{model_id}' is ready for its first move", flush=True)
|
|
|
|
|
simulations[model_id] = simulation
|
|
|
|
|
|
|
|
|
|
session["simulations"] = simulations
|
|
|
|
@@ -169,7 +192,6 @@ class AiHttpGameHandler(GameHandler):
|
|
|
|
|
raise ValueError("No simulation is active. Send a 'simulate' request first.")
|
|
|
|
|
if not session.get("simulations"):
|
|
|
|
|
if session.get("last_result") is not None:
|
|
|
|
|
print("Repeated step after simulation completion; returning final result", flush=True)
|
|
|
|
|
return session["last_result"]
|
|
|
|
|
raise ValueError("No simulation is active. Send a 'simulate' request first.")
|
|
|
|
|
moves = request.get("moves")
|
|
|
|
@@ -204,42 +226,75 @@ class AiHttpGameHandler(GameHandler):
|
|
|
|
|
response = {"ok": True, "type": "move_request", "results": results}
|
|
|
|
|
|
|
|
|
|
if all_finished:
|
|
|
|
|
response["type"] = "simulation_result"
|
|
|
|
|
best_score = max(result["score"] for result in results.values())
|
|
|
|
|
best_model_id = max(results, key=lambda model_id: results[model_id]["score"])
|
|
|
|
|
map_difficulty = session["map"].difficulty # e.g., 20 or 60
|
|
|
|
|
response["score"] = best_score
|
|
|
|
|
session["map_round"] += 1
|
|
|
|
|
|
|
|
|
|
# Accumulate scores for this map round
|
|
|
|
|
for model_id, res in results.items():
|
|
|
|
|
session["map_scores"][model_id].append(res["score"])
|
|
|
|
|
|
|
|
|
|
# --- Curriculum Regression (Fallback) & Performance Update ---
|
|
|
|
|
PASSING_THRESHOLD = 85.0 # Score percentage considered successful completion
|
|
|
|
|
if best_score < PASSING_THRESHOLD:
|
|
|
|
|
session["stagnant_generations"] = session.get("stagnant_generations", 0) + 1
|
|
|
|
|
# Check if we have completed all maps for this generation
|
|
|
|
|
if session["map_round"] < self.MAPS_PER_JSON_ROUND if hasattr(self, 'MAPS_PER_JSON_ROUND') else session["map_round"] < self.MAPS_PER_GENERATION:
|
|
|
|
|
# More maps remaining in this generation! Tell client to simulate next map.
|
|
|
|
|
response["type"] = "map_round_complete"
|
|
|
|
|
response["map_round"] = session["map_round"]
|
|
|
|
|
session["simulations"] = None
|
|
|
|
|
session["last_result"] = response
|
|
|
|
|
else:
|
|
|
|
|
session["stagnant_generations"] = 0 # Reset on breakthrough
|
|
|
|
|
# All 3 maps completed -> finalize generation results (Average scores)
|
|
|
|
|
response["type"] = "simulation_result"
|
|
|
|
|
final_results = {}
|
|
|
|
|
|
|
|
|
|
# Calculate final scores for each model based on their score on every map
|
|
|
|
|
for model_id, scores in session["map_scores"].items():
|
|
|
|
|
# Calculate Mean Squared Error from perfection (100)
|
|
|
|
|
squared_errors = [(100.0 - score) ** 2 for score in scores]
|
|
|
|
|
mse = sum(squared_errors) / len(squared_errors)
|
|
|
|
|
rmse = math.sqrt(mse)
|
|
|
|
|
|
|
|
|
|
# Final score penalizes variance/drops sharply based on RMSE
|
|
|
|
|
final_score = round(max(0.0, min(100.0, 100.0 - rmse)), 2)
|
|
|
|
|
|
|
|
|
|
# Attach final structural properties from the last results set
|
|
|
|
|
final_results[model_id] = deepcopy(results[model_id])
|
|
|
|
|
final_results[model_id]["score"] = final_score
|
|
|
|
|
|
|
|
|
|
# If stuck on a difficulty wall for 15 generations, fallback to an easier map range
|
|
|
|
|
if session["stagnant_generations"] >= 15:
|
|
|
|
|
current_perf = session.get("performance", 10 * math.sqrt(map_difficulty))
|
|
|
|
|
regression_drop = 15.0
|
|
|
|
|
session["performance"] = max(0.0, current_perf - regression_drop)
|
|
|
|
|
print(f"Stuck on difficulty {map_difficulty} for {session['stagnant_generations']} generations. Dropping target performance to {session['performance']:.1f}", flush=True)
|
|
|
|
|
session["stagnant_generations"] = 0 # Reset stagnation counter after fallback
|
|
|
|
|
else:
|
|
|
|
|
difficulty_scaling = 10 * math.sqrt(map_difficulty)
|
|
|
|
|
normalized_score = best_score / 100.0 # Convert 0-100 percentage to 0.0-1.0 scale
|
|
|
|
|
session["performance"] = difficulty_scaling * (0.5 + 0.5 * normalized_score)
|
|
|
|
|
response["results"] = final_results
|
|
|
|
|
best_score = max(res["score"] for res in final_results.values())
|
|
|
|
|
best_model_id = max(final_results, key=lambda mid: final_results[mid]["score"])
|
|
|
|
|
map_difficulty = session["map"].difficulty
|
|
|
|
|
response["score"] = best_score
|
|
|
|
|
|
|
|
|
|
session["last_result"] = response
|
|
|
|
|
session["simulations"] = None
|
|
|
|
|
session["best_run"] = (
|
|
|
|
|
deepcopy(session["map"]),
|
|
|
|
|
list(simulations[best_model_id].moves),
|
|
|
|
|
session["generation"],
|
|
|
|
|
)
|
|
|
|
|
replay_condition = session.get("replay_condition")
|
|
|
|
|
if replay_condition is not None:
|
|
|
|
|
with replay_condition:
|
|
|
|
|
replay_condition.notify_all()
|
|
|
|
|
# --- Curriculum Regression & Performance Update ---
|
|
|
|
|
PASSING_THRESHOLD = 85.0
|
|
|
|
|
if best_score < PASSING_THRESHOLD:
|
|
|
|
|
session["stagnant_generations"] = session.get("stagnant_generations", 0) + 1
|
|
|
|
|
else:
|
|
|
|
|
session["stagnant_generations"] = 0
|
|
|
|
|
|
|
|
|
|
if session["stagnant_generations"] >= 15:
|
|
|
|
|
current_perf = session.get("performance", 10 * math.sqrt(map_difficulty))
|
|
|
|
|
regression_drop = 15.0
|
|
|
|
|
session["performance"] = max(0.0, current_perf - regression_drop)
|
|
|
|
|
print(f"Stuck on difficulty {map_difficulty} for {session['stagnant_generations']} generations. Dropping target performance to {session['performance']:.1f}", flush=True)
|
|
|
|
|
session["stagnant_generations"] = 0
|
|
|
|
|
else:
|
|
|
|
|
difficulty_scaling = 10 * math.sqrt(map_difficulty)
|
|
|
|
|
normalized_score = best_score / 100.0
|
|
|
|
|
session["performance"] = difficulty_scaling * (0.5 + 0.5 * normalized_score)
|
|
|
|
|
|
|
|
|
|
# Reset round counter for the next generation
|
|
|
|
|
session["map_round"] = 0
|
|
|
|
|
session["last_result"] = response
|
|
|
|
|
session["simulations"] = None
|
|
|
|
|
session["best_run"] = (
|
|
|
|
|
deepcopy(session["map"]),
|
|
|
|
|
list(simulations[best_model_id].moves),
|
|
|
|
|
session["generation"],
|
|
|
|
|
)
|
|
|
|
|
replay_condition = session.get("replay_condition")
|
|
|
|
|
if replay_condition is not None:
|
|
|
|
|
with replay_condition:
|
|
|
|
|
replay_condition.notify_all()
|
|
|
|
|
return response
|
|
|
|
|
|
|
|
|
|
def _replay_best_runs(self, session: dict) -> None:
|
|
|
|
|