Files
pony/web/evaluate_game.py
alexpolo 48b84a6e98 feat: game balance improvements, stats dashboard, and encouraging narration
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
2026-08-09 22:28:27 +00:00

383 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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()