📄 config.py
All constants — scoring caps, session weights, formation quotas, selection thresholds
MODEL = "claude-haiku-4-5-20251001"

# Scoring maximums
MAX_INDIVIDUAL = 35
MAX_CHEMISTRY  = 50
MAX_UNIVERSAL  = 15
MAX_TOTAL      = 100

# Session weights (must sum to 1.0)
SESSION_WEIGHTS = {
    "Monday":    0.15,
    "Wednesday": 0.25,
    "Thursday":  0.25,
    "Friday":    0.35,
}

# Session focus: weights for (individual, chemistry, universal) scoring
SESSION_FOCUS = {
    "Monday":    {"individual": 0.30, "chemistry": 0.10, "universal": 0.60},
    "Wednesday": {"individual": 0.70, "chemistry": 0.10, "universal": 0.20},
    "Thursday":  {"individual": 0.10, "chemistry": 0.70, "universal": 0.20},
    "Friday":    {"individual": 0.333, "chemistry": 0.333, "universal": 0.334},
}

FORMATIONS = {
    "4-3-3": {"GK": 1, "DEF": 4, "MID": 3, "FWD": 3},
    "4-4-2": {"GK": 1, "DEF": 4, "MID": 4, "FWD": 2},
    "3-5-2": {"GK": 1, "DEF": 3, "MID": 5, "FWD": 2},
}

THRESHOLD_LINEUP = 65
THRESHOLD_BENCH  = 50
THRESHOLD_UNUSED = 40
📄 coach.py
Position-specific AI coach personas — calls Claude Haiku to evaluate each player in each training session
import os, json, random
import anthropic
from config import MODEL, MAX_INDIVIDUAL, MAX_CHEMISTRY, MAX_UNIVERSAL, SESSION_FOCUS

client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))

POSITION_PERSONAS = {
    "GK":  "You are a goalkeeping coach. Evaluate goalkeeper performance on shot-stopping, "
           "command of area, distribution, communication, and organising the defensive line.",
    "DEF": "You are a defensive coach. Evaluate defenders on positioning, tackling, "
           "aerial duels, pressing discipline, and how well they hold their shape.",
    "MID": "You are a midfield coach. Evaluate midfielders on passing range, pressing, "
           "defensive cover, attacking transitions, and combination play with teammates.",
    "FWD": "You are a forward coach. Evaluate forwards on finishing, movement, "
           "pressing triggers, hold-up play, and runs in behind the defensive line.",
}

def evaluate_player(player, session, session_desc):
    focus  = SESSION_FOCUS[session]
    prompt = f"""Evaluate {player['name']}, a {player['position']} (age {player['age']}).
Style: {player['style']}
Strength: {player['strength']}
Weakness: {player['weakness']}

Session: {session} — {session_desc}
Focus: Individual {int(focus['individual']*100)}%, Chemistry {int(focus['chemistry']*100)}%, Universal {int(focus['universal']*100)}%

Scoring:
- individual_score: 0-{MAX_INDIVIDUAL} (position-specific technical performance)
- chemistry_score:  0-{MAX_CHEMISTRY}  (selflessness, support runs, pressing coordination)
- universal_score:  0-{MAX_UNIVERSAL}  (fitness, attitude, discipline)

Respond ONLY with valid JSON:
{{"individual_score": int, "chemistry_score": int, "universal_score": int,
  "highlight": "one sentence", "concern": "one sentence or No concerns today"}}"""

    response = client.messages.create(
        model=MODEL, max_tokens=300,
        system=POSITION_PERSONAS[player["position"]],
        messages=[{"role": "user", "content": prompt}],
    )
    result = json.loads(response.content[0].text.strip())
    result["individual_score"] = max(0, min(MAX_INDIVIDUAL, int(result["individual_score"])))
    result["chemistry_score"]  = max(0, min(MAX_CHEMISTRY,  int(result["chemistry_score"])))
    result["universal_score"]  = max(0, min(MAX_UNIVERSAL,  int(result["universal_score"])))
    result["total_score"] = (
        result["individual_score"] + result["chemistry_score"] + result["universal_score"]
    )
    return result
📄 simulation.py
Runs 4 training sessions for all 22 players, computes weighted weekly score and rolling form average
from config import SESSION_WEIGHTS, SESSION_DESCRIPTIONS, FORM_THIS_WEEK, FORM_LAST_WEEK
from coach import evaluate_player

SESSIONS_ORDER = ["Monday", "Wednesday", "Thursday", "Friday"]

def run_week(players, last_week_scores=None):
    results = {}
    for player in players:
        pid = player["id"]
        session_results = {}
        for session in SESSIONS_ORDER:
            session_results[session] = evaluate_player(
                player, session, SESSION_DESCRIPTIONS[session]
            )

        # Weighted weekly score
        weekly_score = sum(
            session_results[s]["total_score"] * SESSION_WEIGHTS[s]
            for s in SESSIONS_ORDER
        )

        # Rolling form: this week 60% + last week 40%
        if last_week_scores and pid in last_week_scores:
            form_score = weekly_score * FORM_THIS_WEEK + last_week_scores[pid] * FORM_LAST_WEEK
        else:
            form_score = weekly_score

        results[pid] = {
            "name": player["name"], "position": player["position"],
            "sessions": session_results,
            "weekly_score": round(weekly_score, 1),
            "form_score": round(form_score, 1),
            "highlight": session_results["Friday"]["highlight"],
            "concern": session_results["Friday"]["concern"],
        }
    return results
📄 report.py
Formation-aware selection engine + hybrid formation advisor that scores fit across all formations
from config import FORMATIONS, THRESHOLD_LINEUP, THRESHOLD_BENCH

def score_formation_fit(week_results, formation):
    """Sum of top-N form scores per position slot — higher = better natural fit."""
    quotas = FORMATIONS[formation]
    by_pos = {"GK": [], "DEF": [], "MID": [], "FWD": []}
    for d in week_results.values():
        by_pos[d["position"]].append(d["form_score"])
    for pos in by_pos:
        by_pos[pos].sort(reverse=True)
    return round(sum(sum(by_pos[p][:n]) for p, n in quotas.items()), 1)

def recommend_formation(week_results):
    fit_scores = {f: score_formation_fit(week_results, f) for f in FORMATIONS}
    best = max(fit_scores, key=fit_scores.get)
    return best, fit_scores

def select_squad(week_results, formation, transfer_risk_ids):
    quotas = FORMATIONS[formation]
    by_pos = {pos: [] for pos in quotas}
    for pid, d in week_results.items():
        by_pos[d["position"]].append({**d, "id": pid})
    for pos in by_pos:
        by_pos[pos].sort(key=lambda x: x["form_score"], reverse=True)

    lineup, remaining = [], {pos: [] for pos in quotas}
    for pos, count in quotas.items():
        eligible = [p for p in by_pos[pos] if p["form_score"] >= THRESHOLD_LINEUP]
        selected = (eligible + [p for p in by_pos[pos] if p not in eligible])[:count]
        lineup.extend(selected)
        remaining[pos] = [p for p in by_pos[pos] if p not in selected]

    # Bench: 1 GK + best 4 outfield
    bench = []
    if remaining["GK"]:
        bench.append(remaining["GK"].pop(0))
    outfield = sorted(
        [p for pos in ["DEF","MID","FWD"] for p in remaining[pos]],
        key=lambda x: x["form_score"], reverse=True
    )
    bench.extend(outfield[:5 - len(bench)])

    used_ids  = {p["id"] for p in lineup + bench}
    all_ids   = set(week_results.keys())
    unused_ids = all_ids - used_ids
    unused = sorted(
        [{**week_results[pid], "id": pid} for pid in unused_ids],
        key=lambda x: x["form_score"], reverse=True
    )
    transfer_shortlist = [p for p in unused if p["id"] in transfer_risk_ids]
    return {"formation": formation, "quotas": quotas,
            "lineup": lineup, "bench": bench,
            "unused": unused, "transfer_shortlist": transfer_shortlist}