Balance fixes: - Discord scene 1: normal → let (was 0% success) - Angel scene 4: svaert → normal (too hard for kids) - Rarity scene 4: normal → svaert (was 100% win) - Apple scene 1, Twilight scene 1, Angel scene 1: let → normal - Overall win rate: 47% (target 40-60%) Frontend improvements: - Encouraging failure narration for kids - Stats dashboard page with backend integration - Stats button on home page - API loadStats() function Evaluation: - Exhaustive mode: all 32 pony×theme combos - Per-scene success rate tracking - JSON output for automated analysis
383 lines
13 KiB
Python
383 lines
13 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Evaluere MLP Pony-spillet som en 4-årig.
|
||
|
||
Simulerer spil-gennemgange og producerer rapporter med:
|
||
- gennemsnitlig score per tema
|
||
- succesrate per scene
|
||
- sværeste/letteste scener
|
||
- pony-type balance
|
||
- generel balance-vurdering
|
||
|
||
Brug:
|
||
cd /home/alex/pony/web
|
||
python3 evaluate_game.py # 100 tilfældige spil
|
||
python3 evaluate_game.py --exhaustive # alle 32 pony×tema kombinationer
|
||
"""
|
||
|
||
import argparse
|
||
import json
|
||
import os
|
||
import random
|
||
import statistics
|
||
import sys
|
||
|
||
sys.path.insert(0, os.path.dirname(__file__))
|
||
|
||
from app.data.themes import THEMAER
|
||
from app.game.pony import PONITYPER, PONYNAMNE
|
||
from app.game.dice import roll_d6, DIFFICULTY_PENALTY
|
||
from app.game.state import create_game
|
||
from app.services.game_service import roll_scene, resolve_scene_interaction, format_scene_data
|
||
|
||
|
||
NUM_RUNS = 100
|
||
RNG = random.Random(42) # reproducibelt
|
||
|
||
|
||
def pick_pony():
|
||
return RNG.randint(0, len(PONITYPER) - 1)
|
||
|
||
|
||
def pick_theme():
|
||
return RNG.randint(0, len(THEMAER) - 1)
|
||
|
||
|
||
def play_one_game():
|
||
"""Play a full game from start to finish. Return a report dict."""
|
||
pony_idx = pick_pony()
|
||
tema_idx = pick_theme()
|
||
game = create_game(pony_idx, tema_idx)
|
||
|
||
pony_type = PONITYPER[pony_idx]
|
||
tema = THEMAER[tema_idx]
|
||
scenes = tema.get("scener", [])
|
||
|
||
results = []
|
||
|
||
while not game.get("færdig"):
|
||
scn_idx = game["scene"]
|
||
if scn_idx >= len(scenes):
|
||
game["færdig"] = True
|
||
break
|
||
|
||
scn = scenes[scn_idx]
|
||
interaction = scn.get("interaction", {"type": "dice"})
|
||
|
||
if interaction["type"] == "dice":
|
||
game = roll_scene(game)
|
||
else:
|
||
options = interaction.get("options", [])
|
||
if not options:
|
||
game = roll_scene(game)
|
||
else:
|
||
target = interaction.get("target")
|
||
if interaction["type"] in ("color", "number", "memory") and target:
|
||
target_opt = next((o for o in options if o.get("id") == target), None)
|
||
selection = target_opt["id"] if target_opt else RNG.choice([o["id"] for o in options])
|
||
elif interaction["type"] == "choice":
|
||
selection = RNG.choice([o["id"] for o in options])
|
||
else:
|
||
selection = RNG.choice([o["id"] for o in options])
|
||
|
||
game, _ = resolve_scene_interaction(game, selection)
|
||
|
||
if game["historie"]:
|
||
results.append(game["historie"][-1])
|
||
|
||
fmt = format_scene_data(game)
|
||
|
||
return {
|
||
"pony_type": pony_type["navn"],
|
||
"pony_idx": pony_idx,
|
||
"tema": tema["titel"],
|
||
"tema_idx": tema_idx,
|
||
"stats": {
|
||
"krop": game["pony"]["krop"],
|
||
"sind": game["pony"]["sind"],
|
||
"charme": game["pony"]["charme"],
|
||
"talent": game["pony"]["talent"],
|
||
},
|
||
"succeser": game["succeser"],
|
||
"fiaskoer": game["fiaskoer"],
|
||
"total_scenes": len(scenes),
|
||
"victory": fmt.get("victory"),
|
||
"mixed": fmt.get("mixed"),
|
||
"defeat": fmt.get("defeat"),
|
||
"results": results,
|
||
}
|
||
|
||
|
||
def play_game_deterministic(pony_idx, tema_idx):
|
||
"""Play a full game with deterministic correct choices. Return report dict."""
|
||
game = create_game(pony_idx, tema_idx)
|
||
tema = THEMAER[tema_idx]
|
||
scenes = tema.get("scener", [])
|
||
results = []
|
||
|
||
while not game.get("færdig"):
|
||
scn_idx = game["scene"]
|
||
if scn_idx >= len(scenes):
|
||
game["færdig"] = True
|
||
break
|
||
|
||
scn = scenes[scn_idx]
|
||
interaction = scn.get("interaction", {"type": "dice"})
|
||
|
||
if interaction["type"] == "dice":
|
||
game = roll_scene(game)
|
||
else:
|
||
options = interaction.get("options", [])
|
||
if not options:
|
||
game = roll_scene(game)
|
||
continue
|
||
# For color/number/memory: pick the correct target
|
||
target = interaction.get("target")
|
||
if target and interaction["type"] in ("color", "number", "memory"):
|
||
target_opt = next((o for o in options if o.get("id") == target), None)
|
||
selection = target_opt["id"] if target_opt else options[0]["id"]
|
||
elif interaction["type"] == "choice":
|
||
selection = options[0]["id"] # any choice works
|
||
else:
|
||
selection = options[0]["id"]
|
||
|
||
game, _ = resolve_scene_interaction(game, selection)
|
||
|
||
if game["historie"]:
|
||
results.append(game["historie"][-1])
|
||
|
||
fmt = format_scene_data(game)
|
||
return {
|
||
"pony_type": PONITYPER[pony_idx]["navn"],
|
||
"pony_idx": pony_idx,
|
||
"tema": tema["titel"],
|
||
"tema_id": tema["id"],
|
||
"tema_idx": tema_idx,
|
||
"stats": {
|
||
"krop": game["pony"]["krop"],
|
||
"sind": game["pony"]["sind"],
|
||
"charme": game["pony"]["charme"],
|
||
"talent": game["pony"]["talent"],
|
||
},
|
||
"succeser": game["succeser"],
|
||
"fiaskoer": game["fiaskoer"],
|
||
"total_scenes": len(scenes),
|
||
"victory": fmt.get("victory"),
|
||
"mixed": fmt.get("mixed"),
|
||
"defeat": fmt.get("defeat"),
|
||
"results": results,
|
||
"scene_results": [
|
||
{
|
||
"scene": i,
|
||
"action": scn["aktion"],
|
||
"stat": scn["stat"],
|
||
"difficulty": scn["svaer"],
|
||
"success": res.get("succes", False) if res else None,
|
||
}
|
||
for i, (scn, res) in enumerate(zip(scenes, results))
|
||
],
|
||
}
|
||
|
||
|
||
def evaluate_exhaustive():
|
||
"""Run every pony x theme combination once."""
|
||
all_runs = []
|
||
total = len(PONITYPER) * len(THEMAER)
|
||
print(f"Kører {len(PONITYPER)} x {len(THEMAER)} = {total} kombinationer...")
|
||
for pony_idx in range(len(PONITYPER)):
|
||
for tema_idx in range(len(THEMAER)):
|
||
try:
|
||
run = play_game_deterministic(pony_idx, tema_idx)
|
||
all_runs.append(run)
|
||
except Exception as e:
|
||
print(f" Fejl: pony {pony_idx} x tema {tema_idx}: {e}")
|
||
return all_runs
|
||
|
||
|
||
def report(runs):
|
||
total = len(runs)
|
||
|
||
avg_succeser = statistics.mean([r["succeser"] for r in runs])
|
||
avg_fiaskoer = statistics.mean([r["fiaskoer"] for r in runs])
|
||
victory_count = sum(1 for r in runs if r.get("victory"))
|
||
mixed_count = sum(1 for r in runs if r.get("mixed"))
|
||
defeat_count = sum(1 for r in runs if r.get("defeat"))
|
||
|
||
print("=" * 60)
|
||
print(" EVALUERINGSRAPPORT — My Little Pony: Tails of Equestria")
|
||
print("=" * 60)
|
||
print(f"\nSamlet: {total} spil")
|
||
print(f" Gennemsnitlige succeser: {avg_succeser:.1f} / {runs[0]['total_scenes']}")
|
||
print(f" Gennemsnitlige fiaskoer: {avg_fiaskoer:.1f}")
|
||
print(f" Sejr: {victory_count} ({victory_count/total*100:.0f}%)")
|
||
print(f" Blandet: {mixed_count} ({mixed_count/total*100:.0f}%)")
|
||
print(f" Nederlag: {defeat_count} ({defeat_count/total*100:.0f}%)")
|
||
|
||
# --- Per tema ---
|
||
print("\n--- Per Tema ---")
|
||
themes = {}
|
||
for r in runs:
|
||
t = r["tema"]
|
||
if t not in themes:
|
||
themes[t] = {"succeser": [], "fiaskoer": [], "victories": 0, "count": 0}
|
||
themes[t]["succeser"].append(r["succeser"])
|
||
themes[t]["fiaskoer"].append(r["fiaskoer"])
|
||
themes[t]["count"] += 1
|
||
if r.get("victory"):
|
||
themes[t]["victories"] += 1
|
||
|
||
for t, d in themes.items():
|
||
avg_s = statistics.mean(d["succeser"])
|
||
v_rate = d["victories"] / d["count"] * 100
|
||
print(f" {t}:")
|
||
print(f" Spil: {d['count']}, Gns. succeser: {avg_s:.1f}, Sejr-rate: {v_rate:.0f}%")
|
||
|
||
# --- Per pony type ---
|
||
print("\n--- Per Pony Type ---")
|
||
ponies = {}
|
||
for r in runs:
|
||
p = r["pony_type"]
|
||
if p not in ponies:
|
||
ponies[p] = {"succeser": [], "victories": 0, "count": 0}
|
||
ponies[p]["succeser"].append(r["succeser"])
|
||
ponies[p]["count"] += 1
|
||
if r.get("victory"):
|
||
ponies[p]["victories"] += 1
|
||
|
||
for p, d in ponies.items():
|
||
avg_s = statistics.mean(d["succeser"])
|
||
v_rate = d["victories"] / d["count"] * 100
|
||
print(f" {p}: {d['count']} spil, Gns. succeser: {avg_s:.1f}, Sejr-rate: {v_rate:.0f}%")
|
||
|
||
# --- Per scene ---
|
||
print("\n--- Scene Analyse (alle temaer samlet) ---")
|
||
scene_data = {}
|
||
for r in runs:
|
||
tema = r["tema"]
|
||
for i, res in enumerate(r["results"]):
|
||
key = f"{tema} - Scene {i+1}"
|
||
if key not in scene_data:
|
||
scene_data[key] = {"successes": 0, "total": 0}
|
||
scene_data[key]["total"] += 1
|
||
if res["succes"]:
|
||
scene_data[key]["successes"] += 1
|
||
|
||
sorted_scenes = sorted(scene_data.items(), key=lambda x: x[1]["successes"] / x[1]["total"])
|
||
print("\n Sværeste scener (laveste succesrate):")
|
||
for key, d in sorted_scenes[:5]:
|
||
rate = d["successes"] / d["total"] * 100
|
||
print(f" {key}: {rate:.0f}% ({d['successes']}/{d['total']})")
|
||
|
||
print("\n Letteste scener (højeste succesrate):")
|
||
for key, d in sorted_scenes[-5:][::-1]:
|
||
rate = d["successes"] / d["total"] * 100
|
||
print(f" {key}: {rate:.0f}% ({d['successes']}/{d['total']})")
|
||
|
||
# --- Balance assessment ---
|
||
print("\n--- Balance Vurdering ---")
|
||
overall_rate = avg_succeser / runs[0]["total_scenes"]
|
||
if overall_rate >= 0.8:
|
||
print(" Spillet er FOR LET — de fleste passer næsten alle scener")
|
||
elif overall_rate >= 0.6:
|
||
print(" God balance — børn vil føle succes men også møde udfordring")
|
||
elif overall_rate >= 0.4:
|
||
print(" Spillet er MELLEMSVÆRT — kan være frustrerende for 4-årige")
|
||
else:
|
||
print(" Spillet er FOR SVÆRT — 4-årige vil blive frustrerede")
|
||
|
||
if victory_count / total > 0.7:
|
||
print(" Sejr-rate over 70% — børnespil skal have ~50-60% sejr")
|
||
elif victory_count / total < 0.3:
|
||
print(" Sejr-rate under 30% — for svært for målgruppen")
|
||
else:
|
||
print(f" Sejr-rate {victory_count/total*100:.0f}% — inden for rimeligt område")
|
||
|
||
# --- Stats balance ---
|
||
print("\n--- Stat Balance ---")
|
||
stat_rolls = {}
|
||
for r in runs:
|
||
for res in r["results"]:
|
||
stat = res.get("stat", "?")
|
||
if stat not in stat_rolls:
|
||
stat_rolls[stat] = []
|
||
stat_rolls[stat].append(1 if res["succes"] else 0)
|
||
|
||
for stat, vals in sorted(stat_rolls.items()):
|
||
rate = statistics.mean(vals) * 100
|
||
count = len(vals)
|
||
emoji = {"krop": "💪", "sind": "🧠", "charme": "💕"}.get(stat, "")
|
||
print(f" {emoji} {stat}: {rate:.0f}% succes over {count} kast")
|
||
|
||
# --- Difficulty distribution ---
|
||
print("\n--- Sværhedsfordeling ---")
|
||
diff_counts = {"let": 0, "normal": 0, "svaert": 0}
|
||
for tema in THEMAER:
|
||
for scn in tema.get("scener", []):
|
||
diff_counts[scn.get("svaer", "normal")] += 1
|
||
total_scenes = sum(diff_counts.values())
|
||
for diff, count in diff_counts.items():
|
||
pct = count / total_scenes * 100
|
||
label = {"let": "Nem", "normal": "Normal", "svaert": "Svær"}.get(diff, diff)
|
||
print(f" {label}: {count} scener ({pct:.0f}%)")
|
||
|
||
print("\n" + "=" * 60)
|
||
|
||
|
||
def report_exhaustive(runs):
|
||
"""Report for exhaustive mode — includes per-scene breakdown and JSON output."""
|
||
report(runs)
|
||
|
||
# Per-scene success rates across all pony types
|
||
print("\n--- Per-Scene Success Rates (exhaustive) ---")
|
||
for scene_num in range(5):
|
||
scene_data = []
|
||
for r in runs:
|
||
for sr in r.get("scene_results", []):
|
||
if sr["scene"] == scene_num:
|
||
scene_data.append(sr)
|
||
if scene_data:
|
||
success_rate = sum(1 for s in scene_data if s["success"]) / len(scene_data) * 100
|
||
print(f" Scene {scene_num+1}: {success_rate:.0f}% succes ({len(scene_data)} kast)")
|
||
|
||
# Identify problem areas
|
||
print("\n--- Balance-Problemer ---")
|
||
for r in runs:
|
||
if r["fiaskoer"] >= 4:
|
||
print(f" FOR SVÆRT: {r['pony_type']} x {r['tema']} — {r['fiaskoer']} fiaskoer")
|
||
if r["succeser"] == r["total_scenes"] and r["fiaskoer"] == 0:
|
||
print(f" FOR LET: {r['pony_type']} x {r['tema']} — perfekt run")
|
||
|
||
# Output JSON for automated analysis
|
||
json_output = {
|
||
"total": len(runs),
|
||
"victory_count": sum(1 for r in runs if r.get("victory")),
|
||
"win_rate": round(sum(1 for r in runs if r.get("victory")) / len(runs) * 100, 1) if runs else 0,
|
||
"runs": runs,
|
||
}
|
||
print(f"\n--- JSON Output ({len(json.dumps(json_output))} chars) ---")
|
||
print(json.dumps(json_output, indent=2))
|
||
|
||
|
||
def evaluate():
|
||
all_runs = []
|
||
|
||
print(f"Kører {NUM_RUNS} spil-gennemgange...")
|
||
for i in range(NUM_RUNS):
|
||
try:
|
||
all_runs.append(play_one_game())
|
||
except Exception as e:
|
||
print(f" Fejl i løb {i}: {e}")
|
||
|
||
print(f"Klar! {len(all_runs)} gennemgange færdige.\n")
|
||
report(all_runs)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
parser = argparse.ArgumentParser()
|
||
parser.add_argument("--exhaustive", action="store_true", help="Run all pony×theme combinations")
|
||
args = parser.parse_args()
|
||
if args.exhaustive:
|
||
runs = evaluate_exhaustive()
|
||
report_exhaustive(runs)
|
||
else:
|
||
evaluate() |