refactor: modular backend - dice engine, pony types, game state, API routes, tests

This commit is contained in:
2026-08-09 07:41:57 +00:00
parent aab36f9a85
commit 06618b5e7d
18 changed files with 1006 additions and 513 deletions

View File

@@ -2,525 +2,20 @@
"""
My Little Pony: Tails of Equestria - Interaktivt Web Spil
For 4-årige: stort, farvegt, simpelt, én scene ad gangen.
Entry point: python3 app.py
Runs Flask on port 5001.
"""
import sys
import os
import random
from flask import Flask, render_template, redirect, url_for, session, request, jsonify
from flask_cors import CORS
app = Flask(__name__)
app.secret_key = 'equestria_magic_key_12345'
CORS(app, supports_credentials=True)
# Ensure the web directory is in the path so 'app' package is importable
sys.path.insert(0, os.path.dirname(__file__))
# === JSON API for React frontend ===
@app.route("/api/start", methods=["POST"])
def api_start():
data = request.get_json(force=True)
ponytype_idx = data.get("type")
if ponytype_idx is None:
return jsonify({"error": "Manglende pony type"}), 400
ponytype_idx = int(ponytype_idx)
tema_idx = int(data.get("tema", 0))
session["spil"] = ny_spil()
pt = PONITYPER[ponytype_idx]
sp = session["spil"]
sp["pony"]["type"] = pt["navn"]
sp["pony"]["emoji"] = pt["emoji"]
sp["pony"]["krop"] = random.randint(2, 4)
sp["pony"]["sind"] = random.randint(2, 4)
sp["pony"]["charme"] = random.randint(2, 4)
if pt["bonus"] == "krop":
sp["pony"]["krop"] = min(sp["pony"]["krop"] + 1, 5)
elif pt["bonus"] == "sind":
sp["pony"]["sind"] = min(sp["pony"]["sind"] + 1, 5)
elif pt["bonus"] == "alle":
sp["pony"]["krop"] = min(sp["pony"]["krop"] + 1, 5)
sp["pony"]["sind"] = min(sp["pony"]["sind"] + 1, 5)
sp["pony"]["charme"] = min(sp["pony"]["charme"] + 1, 5)
sp["tema"] = THEMAER[tema_idx]
return jsonify(api_scene_data(sp))
@app.route("/api/scene", methods=["GET"])
def api_scene():
sp = session.get("spil")
if not sp:
return jsonify({"error": "no game"}), 404
return jsonify(api_scene_data(sp))
@app.route("/api/kast", methods=["POST"])
def api_kast():
sp = session.get("spil")
if not sp or sp.get("færdig"):
return jsonify({"error": "no game"}), 404
scn = sp["tema"]["scener"][sp["scene"]]
stat = scn["stat"]
svaer = scn["svaer"]
succes, dice, succ_count, krav = lav_test(sp["pony"], stat, svaer)
udfald = {
"aktion": scn["aktion"],
"stat": stat,
"svaer": svaer,
"dice": dice,
"succeser": succ_count,
"krav": krav,
"succes": succes,
"tekst": scn["succes"] if succes else scn["fiasko"],
}
sp["historie"].append(udfald)
sp["udfald_tekst"] = udfald["tekst"]
if succes:
sp["succeser"] += 1
else:
sp["fiaskoer"] += 1
sp["scene"] += 1
if sp["scene"] >= len(sp["tema"]["scener"]):
sp["færdig"] = True
session["spil"] = sp
return jsonify(api_scene_data(sp))
def api_scene_data(sp):
scn = None
if not sp.get("færdig") and sp["scene"] < len(sp["tema"]["scener"]):
scn = sp["tema"]["scener"][sp["scene"]]
tema = sp["tema"]
pony = sp["pony"]
result = {
"finished": sp.get("færdig", False),
"ponyName": pony["navn"],
"ponyType": pony["type"],
"ponyEmoji": pony["emoji"],
"ponyImg": f"/static/images/{_pony_img(pony['type'])}",
"ponyStats": {
"krop": pony["krop"],
"sind": pony["sind"],
"charme": pony["charme"],
},
"talent": pony.get("talent", ""),
"tema": tema["titel"],
"sceneNum": f"Scene {sp['scene'] + 1} af {len(tema['scener'])}",
"progress": f"🌟 {'' * sp['succeser']} {'' * (len(tema['scener']) - sp['succeser'] - sp['fiaskoer'])}{'💔' * sp['fiaskoer']}",
}
if scn:
result.update({
"sceneText": scn["tekst"],
"actionText": scn["aktion"],
"difficulty": _diff_text(scn["svaer"]),
})
else:
result["sceneText"] = ""
result["actionText"] = ""
result["difficulty"] = ""
result["history"] = []
for h in sp.get("historie", []):
result["history"].append({
"action": h["aktion"],
"dice": h["dice"],
"success": h["succes"],
"result": _result_text(h["succes"]),
"story": h["tekst"],
})
if sp.get("færdig"):
result["score"] = f"{sp['succeser']} succeser ud af {len(tema['scener'])}"
if sp["succeser"] >= len(tema["scener"]):
result["victory"] = True
result["endText"] = "Fantastisk! Du reddede Equestria! 🌟"
elif sp["succeser"] >= len(tema["scener"]) // 2:
result["mixed"] = True
result["endText"] = "Du gjorde dit bedste! Prøv igen for at blive endnu bedre! 💪"
else:
result["defeat"] = True
result["endText"] = "Ingen panik! Alle ponies prøver igen! 🌈"
return result
def _pony_img(navn):
m = {"Jordpony": "jordpony.png", "Pegasus": "pegasus.png", "Enhjørning": "enhjorning.png", "Alicorn": "alicorn.png"}
return m.get(navn, "jordpony.png")
def _diff_text(svaer):
return {"let": "Nem 🌟", "normal": "Lidt svært 🌟🌟", "svaert": "Svært 🌟🌟🌟"}.get(svaer, "")
def _result_text(succes):
return "✅ Succes!" if succes else "❌ Mislykket"
# === DATA ===
PONITYPER = [
{"navn": "Jordpony", "emoji": "🐴", "bonus": "krop", "tekst": "Stærk og dygtig!"},
{"navn": "Pegasus", "emoji": "🦅", "bonus": "krop", "tekst": "Flyver i himlen!"},
{"navn": "Enhjørning", "emoji": "🦄", "bonus": "sind", "tekst": "Har magisk horn!"},
{"navn": "Alicorn", "emoji": "👑", "bonus": "alle", "tekst": "Magi OG vinger!"},
]
PONYNAMNE = [
"Stjern", "Regnbue", "Glitter", "Silke", "Måne", "Sol",
"Blomst", "Kry", "Ly", "Dug", "Is", "Flint",
"Torden", "Skum", "Ros", "Vind", "Skov", "Bæk",
]
SVAERHED = {"let": 1, "normal": 2, "svaert": 3}
SVAERHED_NAVNE = {"let": "Nem 🌟", "normal": "Lidt svært 🌟🌟", "svaert": "Svært 🌟🌟🌟"}
STAT_EMOJI = {"krop": "💪", "sind": "🧠", "charme": "💕"}
STAT_NAVNE = {"krop": "Krop", "sind": "Sind", "charme": "Charme"}
# Børnevenlige temaer med rige scener
THEMAER = [
{
"titel": "Regnbues fødselsdag",
"emoji": "🎂🌈",
"intro": "Pinkie Pie vil lave en fødselsdagssurprise! Men melhylden er tom — du skal finde mel til kagen før alle vennerne kommer.",
"scener": [
{
"tekst": "Du løber ind i supermarkedet. Melhylden er helt tom — kun en lille fuge af mel er tilbage. Du ser Mrs. Cake bag disken, der vifter med en note.",
"aktion": "Spørg Mrs. Cake om mel",
"stat": "charme", "svaer": "let",
"succes": "Mrs. Cake smiler varmt. 'Ah, en lille pony der hjælper! Jeg har en ekstra pose bagved.' Hun rækker dig en stor pose mel med et glimt i øjet.",
"fiasko": "Mrs. Cake ryster på hovedet. 'Desværre, alt mel er solgt.' Men hun hvisker: 'Prøv måske hos Zecora i skoven?'",
},
{
"tekst": "Du finder Zecoras hytte i skoven. Den dufter af urter og honning. Zecora taler i vers — det er svær at forstå, men hun har noget der kan bruges!",
"aktion": "Forstå Zecoras vers",
"stat": "sind", "svaer": "normal",
"succes": "Du lytter omhyggeligt. 'Dundrende urter, knus dem små, så bliver de til mel, se!' Du knuser urterne og får perfekt mel!",
"fiasko": "Zecoras vers er for kryptisk. 'Dundrende...' hvad? Men hun giver dig alligevel en pose med noget der ligner mel.",
},
{
"tekst": "Kagen er klar! Du skal sætte den i ovnen. Den skal være perfekt — ikke for brændt, ikke for rå. Ovnen gløder orange og varm.",
"aktion": "Bag kagen i ovnen",
"stat": "krop", "svaer": "normal",
"succes": "Du trækker kagen ud på det perfekte tidspunkt! Den er gylden og dufter fantastisk. Pinkie Pie vil elske den!",
"fiasko": "Kagen bliver lidt for brændt ovenpå. Men inde i er den stadig god — du skraber den brændte del af!",
},
{
"tekst": "Nu skal kagen pyntes! Pinkie Pie vil have regnbueglasure, sprinkles og en lille pony-figur ovenpå. Det ser sværere ud end det lyder...",
"aktion": "Pynt kagen perfekt",
"stat": "krop", "svaer": "svaert",
"succes": "Kagen er VAKKER! Regnbueglasuren skinner, sprinklerne er perfekt fordelt, og pony-figuren står lige midt på. Pinkie Pie vil græde af glæde!",
"fiasko": "Glasuren løber lidt ud, og sprinklerne lander lidt tilfældigt. Men kagen er sød — og det er det vigtigste!",
},
{
"tekst": "Kagen er klar! Nu skal alle ponyerne samles. Rainbow Dash er i luften, Fluttershy er i haven, og Rarity er i sit værksted. Du skal finde dem alle!",
"aktion": "Find alle ponyerne",
"stat": "charme", "svaer": "svaert",
"succes": "Du finder dem alle! Rainbow Dash lander med et whoosh, Fluttershy kommer småløbende, og Rarity glimrer i solen. Alle er klar til fødselsdagen! 🎉",
"fiasko": "Du finder de fleste — men Rainbow Dash er for hurtig at fange! Men det går fint, hun kommer alligevel når hun får lugtet kagen!",
},
],
},
{
"titel": "Angel er løbet væk",
"emoji": "🐰💕",
"intro": "Fluttershy er i gråd! Hendes lille kanin Angel er løbet væk. Du tilbyder at hjælpe med at finde ham.",
"scener": [
{
"tekst": "Du løber ud i Fluttershys have. Blomsterne er høje og farverige — røde roser, gule solsikker, blå blåklatter. Angel kan være hvor som helst her!",
"aktion": "Søg i blomsterhaven",
"stat": "sind", "svaer": "let",
"succes": "Du kigger bag en stor solsikke — og ser to små ører rulle frem! Angel gemte sig der! Men... han ser ud til at være løbet videre.",
"fiasko": "Du leder og leder, men finder kun en sommerfugl og en bier. Angel er ikke her — men du finder små fodspor der peger væk!",
},
{
"tekst": "Du løber gennem PonyVille. Alle ponyer er travle — Applejack pløjer, Rarity måler stof, og Twilight skriver noter. Har nogen set Angel?",
"aktion": "Spørg alle ponyer i PonyVille",
"stat": "charme", "svaer": "normal",
"succes": "Rarity råber: 'En lille hvid kanin? Jeg så ham løbe mod bakken!' Du har en ledetråd!",
"fiasko": "Ponyerne ryster på hovedet. 'En kanin?' Men Spike hvisker: 'Jeg så noget hvidt hoppe forbi...' Det må være ham!",
},
{
"tekst": "Små fodspor fører gennem det høje græs. De er små og runde — typiske kanin-fodspor! De fører op ad bakken mod skoven.",
"aktion": "Følg fodsporene",
"stat": "sind", "svaer": "normal",
"succes": "Fodsporene fører dig direkte til et stort træ! Du kigger op — og ser Angel sidde på en gren, der ryster nervøst.",
"fiasko": "Fodsporene forsvinder i græsset. Men du hører et lille 'pip' fra et træ — Angel er deroppe!",
},
{
"tekst": "Angel sidder højt oppe i et stort egetræ! Han ryster og græder. Træet er højt, og grenene er tynde. Du skal op til ham!",
"aktion": "Klatr op i træet",
"stat": "krop", "svaer": "svaert",
"succes": "Du klatrer op ad stammen, gren for gren. Det er skræmmende højt, men du når Angel! Han hopper ned i dine arme med en lille glæde-pip!",
"fiasko": "Du klatrer op, men grenen knækker under dig! Du lander blødt i en busk. Men Angel ser dig og hopper ned af sig selv!",
},
{
"tekst": "Angel er i dine arme, men han er stadig bange. Han ryster og gemmer hovedet i pelsen. Du skal få ham til at føle sig tryg igen.",
"aktion": "Berolig Angel",
"stat": "charme", "svaer": "svaert",
"succes": "Du stryger ham blidt over pelsen og synger en lille sang. Angel stopper med at ryste og slikker din kind! Fluttershy græder af glæde: 'Angel! Du har reddet ham!' 💕",
"fiasko": "Angel er stadig en lille smule bange, men han holder fast i dit øre. Fluttershy smiler: 'Han bliver bedre — tak fordi du fandt ham!'",
},
],
},
{
"titel": "Æblerne ruller ned ad bakken",
"emoji": "🍎🐴",
"intro": "Applejacks æbleskure er væltet! Hundredevis af æbler ruller ned ad bakken. Hjælp med at redde høsten!",
"scener": [
{
"tekst": "ÆBLER! Overalt! De ruller som små røde boulder ned ad bakken. Du skal gribe dem, før de rammer bunden!",
"aktion": "Jag de rullende æbler",
"stat": "krop", "svaer": "let",
"succes": "Du griber og griber! Æbler i munden, æbler i halen, æbler i ørerne! Du redder halvdelen!",
"fiasko": "Æblerne ruller for hurtigt! Du griber nogle få, men de fleste fortsætter ned ad bakken...",
},
{
"tekst": "Mange æbler er landet i det høje, gyldne græs. De er svære at se — kun små røde prikker mellem græsstråene.",
"aktion": "Saml æbler i højt græs",
"stat": "krop", "svaer": "normal",
"succes": "Du kravler gennem græsset og finder æblerne ét for ét! Din kurv er snart fyldt til randen!",
"fiasko": "Græsset er højt, og du finder kun halvdelen. Men Apple Bloom hjælper med at finde resten!",
},
{
"tekst": "Bækken glimrer i solen — og der svømmer røde æbler rundt i det! De er våde og glatte, svære at gribe.",
"aktion": "Fisk æbler op af bækken",
"stat": "krop", "svaer": "normal",
"succes": "Du bøjer dig ned og griber æblerne ét for ét! Vandet er koldt, men det er sjovt! Alle æbler reddet!",
"fiasko": "Æblerne er glatte! De glider ud af dine poter. Men du får fanget de fleste med halen!",
},
{
"tekst": "Nu skal alle æbler bæres tilbage til gården! Kurven er TUNG. Bakken er STEIL. Men du kan gøre det!",
"aktion": "Bær tunge kurve hjem",
"stat": "krop", "svaer": "svaert",
"succes": "Du bærer kurven op ad bakken med stolthed! Applejack råber: 'Du er stærkere end de fleste!' Alle æbler er i sikkerhed!",
"fiasko": "Kurven er for tung! Du vælter og æblerne ruller lidt. Men Big Mac kommer og hjælper — sammen når I hjem!",
},
{
"tekst": "Applejack står med tårer i øjnene. 'Du har reddet hele høsten,' hvisker hun. Hun rækker dig noget... en æblekage, lavet specielt til dig!",
"aktion": "Modtag præmien fra Applejack",
"stat": "charme", "svaer": "let",
"succes": "Æblekagen er den bedste du nogensinde har smagt! Applejack giver dig et knus. 'Du er den bedste pony i hele Equestria!' 🍎",
"fiasko": "Du får æblekagen, men den er lidt brændt. Applejack griner: 'Næste gang bliver den perfekt!'",
},
],
},
{
"titel": "Raritys glimmer-sten er borte",
"emoji": "💎✨",
"intro": "Rarity er i panik! Hendes kæreste glimmer-sten — den smukkeste sten i hele Equestria — er forsvundet! Du skal finde den.",
"scener": [
{
"tekst": "Raritys værksted er et kaotisk paradis! Stofruller overalt, nåle i luften, og glitter på gulvet. Måske har stenen faldet et sted her?",
"aktion": "Søg i Raritys værksted",
"stat": "sind", "svaer": "let",
"succes": "Du finder en glimt under en stofrulle! Men det er kun en lille krystall — ikke glimmer-stenen. Men det giver dig en idé!",
"fiasko": "Værkstedet er for rodet! Du finder nåle, knapper og glitter — men ikke glimmer-stenen.",
},
{
"tekst": "Du løber gennem PonyVille. Glimmer-stenen er så lysende — den burde være svær at overse. Har nogen set den?",
"aktion": "Spørg i PonyVille",
"stat": "charme", "svaer": "normal",
"succes": "Spike råber: 'Jeg så Rarity droppe noget glitrende da hun løb forbi parken!' Du ved hvor du skal lede!",
"fiasko": "Ingen har set stenen. Men Twilight hvisker: 'Måske kan min magi hjælpe?'",
},
{
"tekst": "Parken er smuk i solen! Bænke, blomsterbede, og en lille sø. Glimmer-stenen skinner — du kan næsten SE den glitre et sted!",
"aktion": "Søg i parken",
"stat": "sind", "svaer": "normal",
"succes": "Du ser et glimt ved søen! Glimmer-stenen ligger på en sten, der glitrer som en lille sol!",
"fiasko": "Du leder overalt i parken. Men du finder en glimt der kommer fra vandet...",
},
{
"tekst": "Glimmer-stenen er faldet i søen! Du kan SE den på bunden — den skinner! Men søen er dyb, og du skal dykke for at nå den.",
"aktion": "Dyk efter glimmer-stenen",
"stat": "krop", "svaer": "svaert",
"succes": "Du dykker ned! Vandet er koldt, men du griber stenen! Du bryder overfladen med stenen i munden — den glitrer som aldrig før! 💎",
"fiasko": "Du dykker, men stenen er for dybt! Men Fluttershy kommer og dykker med dig — sammen redder I stenen!",
},
{
"tekst": "Rarity græder af glæde! 'Min glimmer-sten! Åh, du er en engel!' Hun omfavner dig og rækker dig en lille æske...",
"aktion": "Modtag Raritys tak",
"stat": "charme", "svaer": "let",
"succes": "Inden i æsken er en lille glimmer-ørestift! 'Til dig, min søde ven!' Rarity hvisker. Du føler dig som en stjerne! ✨",
"fiasko": "Rarity giver dig en knus i stedet. 'Du er bedre end nogen sten!' Og det føles faktisk bedre.",
},
],
},
{
"titel": "Discord laver sjov",
"emoji": "🍰🐉",
"intro": "Discord — den skøre drage — har forvandlet jorden til marshmallow! PonyVille er i kaos. Du skal fikse det!",
"scener": [
{
"tekst": "Du træder ud — og dit ben synker ned i MARSHMALLOW! Overalt er hvid, blød masse. I det fjerne ser du en drage der griner hysterisk.",
"aktion": "Find Discord",
"stat": "sind", "svaer": "let",
"succes": "Discord er umulig at overse! Han svæver i luften med en marshmallow-hat. 'Hej, lille pony! Vil du lege?' spørger han.",
"fiasko": "Marshmallow er overalt! Det er svær at se. Men du hører Discords grin — og følger lyden!",
},
{
"tekst": "Discord griner. 'Jeg vil kun fixe det, hvis du får mig til at grine! Det er min regel!' Han krydser armene og puster på næsen.",
"aktion": "Få Discord til at grine",
"stat": "charme", "svaer": "normal",
"succes": "Du laver en latterlig dans — hoppende, snurrende, med munden skæv! Discord griner så meget at han næsten falder af himlen! 'OK, OK! Jeg fikser det!'",
"fiasko": "Du fortæller en vittighed. Discord ryster på hovedet... men så fniser han! 'OK, det var faktisk ret sjovt. Jeg fikser det.'",
},
{
"tekst": "Discord har lavet en marshmallow-labyrint! Du skal finde vejen gennem til hans magiske krystall — den kan vende forandringen tilbage.",
"aktion": "Find vejen gennem marshmallow-labyrinten",
"stat": "krop", "svaer": "normal",
"succes": "Du hopper og balancerer gennem marshmallow-væggene! Det er som at lege i en kæmpe slikbutik! Du finder krystallen!",
"fiasko": "Du vader gennem marshmallowet — det er blød og klæbrig! Men du når alligevel krystallen!",
},
{
"tekst": "Discord rækker dig en gåde på et stykke marshmallow: 'Jeg har ben men kan ikke gå, jeg har en rygrad men kan ikke bo. Hvad er jeg?'",
"aktion": "Løs Discordens gåde",
"stat": "sind", "svaer": "svaert",
"succes": "'En bog!' råber du! Discord griner: 'Korrekt! Du er klogere end du ser ud!' Han klapper i klørne — og alt bliver normalt igen!",
"fiasko": "Du tænker længe... 'En... fisk?' Discord griner: 'Nej, det er en bog! Men det var sødt at du prøvede!' Han fikser det alligevel.",
},
{
"tekst": "PonyVille er tilbage til normalt! Discord lander blidt og rækker dig en marshmallow. 'Til næste gang,' hvisker han med et glimt i øjet. Alle ponyer fejrer dig!",
"aktion": "Fejr sejren med alle ponyer",
"stat": "charme", "svaer": "let",
"succes": "Alle ponyer synger og danser! Discord laver marshmallow-regn — men denne gang er det sjovt! Du er heltens af dagen! 🎉🍰",
"fiasko": "Alle er glade! Discord giver dig et high-five. 'Du er en sej pony!' Og marshmallow-regnet er faktisk ret sjovt!",
},
],
},
]
def kast_d6(n):
return [random.randint(1, 6) for _ in range(n)]
def tael_succes(dice):
s = 0
for d in dice:
if d == 6:
s += 2
elif d >= 4:
s += 1
return s
def lav_test(pony, stat, svaer):
terninger = pony[stat]
if pony.get("talent") == stat:
terninger += 1
krav = SVAERHED[svaer]
kast = kast_d6(terninger)
succeser = tael_succes(kast)
return succeser >= krav, kast, succeser, krav
def ny_spil():
tema = random.choice(THEMAER)
ponytype = random.choice(PONITYPER)
navn = random.choice(PONYNAMNE) + random.choice([
"hals", "støv", "ros", "vinge", "blomst", "fyr",
"blik", "horn", "lys", "snude", "pels", "mane",
])
pony = {
"navn": navn,
"type": ponytype["navn"],
"emoji": ponytype["emoji"],
"krop": random.randint(2, 4),
"sind": random.randint(2, 4),
"charme": random.randint(2, 4),
"talent": random.choice(["krop", "sind", "charme"]),
}
if ponytype["bonus"] == "krop":
pony["krop"] = min(pony["krop"] + 1, 5)
elif ponytype["bonus"] == "sind":
pony["sind"] = min(pony["sind"] + 1, 5)
elif ponytype["bonus"] == "alle":
pony["krop"] = min(pony["krop"] + 1, 5)
pony["sind"] = min(pony["sind"] + 1, 5)
pony["charme"] = min(pony["charme"] + 1, 5)
return {
"tema": tema,
"pony": pony,
"scene": 0,
"historie": [],
"succeser": 0,
"fiaskoer": 0,
"færdig": False,
"udfald_tekst": None,
}
@app.route("/")
def index():
return render_template("index.html")
@app.route("/start", methods=["GET", "POST"])
def start():
if request.method == "POST":
ponytype_idx = int(request.form.get("type", 0))
tema_idx = int(request.form.get("tema", 0))
session["spil"] = ny_spil()
pt = PONITYPER[ponytype_idx]
sp = session["spil"]
sp["pony"]["type"] = pt["navn"]
sp["pony"]["emoji"] = pt["emoji"]
sp["pony"]["krop"] = random.randint(2, 4)
sp["pony"]["sind"] = random.randint(2, 4)
sp["pony"]["charme"] = random.randint(2, 4)
if pt["bonus"] == "krop":
sp["pony"]["krop"] = min(sp["pony"]["krop"] + 1, 5)
elif pt["bonus"] == "sind":
sp["pony"]["sind"] = min(sp["pony"]["sind"] + 1, 5)
elif pt["bonus"] == "alle":
sp["pony"]["krop"] = min(sp["pony"]["krop"] + 1, 5)
sp["pony"]["sind"] = min(sp["pony"]["sind"] + 1, 5)
sp["pony"]["charme"] = min(sp["pony"]["charme"] + 1, 5)
sp["tema"] = THEMAER[tema_idx]
return redirect(url_for("scene"))
return render_template("start.html", ponytyper=PONITYPER, themaer=THEMAER, enumerate=enumerate)
@app.route("/scene")
def scene():
sp = session.get("spil")
if not sp:
return redirect(url_for("start"))
return render_template("spil.html", sp=sp)
@app.route("/kast", methods=["POST"])
def kast():
sp = session.get("spil")
if not sp or sp.get("færdig"):
return redirect(url_for("start"))
scn = sp["tema"]["scener"][sp["scene"]]
stat = scn["stat"]
svaer = scn["svaer"]
succes, dice, succ_count, krav = lav_test(sp["pony"], stat, svaer)
udfald = {
"aktion": scn["aktion"],
"stat": stat,
"svaer": svaer,
"dice": dice,
"succeser": succ_count,
"krav": krav,
"succes": succes,
"tekst": scn["succes"] if succes else scn["fiasko"],
}
sp["historie"].append(udfald)
sp["udfald_tekst"] = udfald["tekst"]
if succes:
sp["succeser"] += 1
else:
sp["fiaskoer"] += 1
sp["scene"] += 1
if sp["scene"] >= len(sp["tema"]["scener"]):
sp["færdig"] = True
session["spil"] = sp
return redirect(url_for("scene"))
@app.route("/igen")
def igen():
session.pop("spil", None)
return redirect(url_for("start"))
from app.config import create_app
app = create_app()
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5001, debug=True)

3
web/app/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
"""
My Little Pony: Tails of Equestria - Flask Application Factory
"""

34
web/app/config.py Normal file
View File

@@ -0,0 +1,34 @@
"""
Flask application factory for MLP Pony: Tails of Equestria.
"""
import os
from flask import Flask
from flask_cors import CORS
def create_app(config_name=None):
"""Application factory.
Args:
config_name: 'development', 'testing', or 'production'
Returns:
Configured Flask app
"""
app = Flask(__name__)
# Secret key
app.secret_key = os.environ.get("FLASK_SECRET_KEY", "equestria_magic_key_12345")
# CORS for React frontend
CORS(app, supports_credentials=True)
# Register routes
from app.routes.api import api_bp
app.register_blueprint(api_bp)
from app.routes.pages import pages_bp
app.register_blueprint(pages_bp)
return app

3
web/app/data/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
"""
Game data: pony types, themes, scenes, achievements.
"""

223
web/app/data/themes.py Normal file
View File

@@ -0,0 +1,223 @@
"""
Adventure themes and scenes for MLP Pony: Tails of Equestria.
5 themes, each with 5 scenes of increasing difficulty.
"""
THEMAER = [
{
"id": "regnbues-fødselsdag",
"titel": "Regnbues fødselsdag",
"emoji": "\U0001f382\U0001f308",
"intro": "Pinkie Pie vil lave en fødselsdagssurprise! Men melhylden er tom \u2014 du skal finde mel til kagen før alle vennerne kommer.",
"scener": [
{
"tekst": "Du løber ind i supermarkedet. Melhylden er helt tom \u2014 kun en lille fuge af mel er tilbage. Du ser Mrs. Cake bag disken, der vifter med en note.",
"aktion": "Spørg Mrs. Cake om mel",
"stat": "charme", "svaer": "let",
"succes": "Mrs. Cake smiler varmt. 'Ah, en lille pony der hjælper! Jeg har en ekstra pose bagved.' Hun rækker dig en stor pose mel med et glimt i øjet.",
"fiasko": "Mrs. Cake ryster på hovedet. 'Desværre, alt mel er solgt.' Men hun hvisker: 'Prøv måske hos Zecora i skoven?'",
},
{
"tekst": "Du finder Zecoras hytte i skoven. Den dufter af urter og honning. Zecora taler i vers \u2014 det er svær at forstå, men hun har noget der kan bruges!",
"aktion": "Forstå Zecoras vers",
"stat": "sind", "svaer": "normal",
"succes": "Du lytter omhyggeligt. 'Dundrende urter, knus dem små, så bliver de til mel, se!' Du knuser urterne og får perfekt mel!",
"fiasko": "Zecoras vers er for kryptisk. 'Dundrende...' hvad? Men hun giver dig alligevel en pose med noget der ligner mel.",
},
{
"tekst": "Kagen er klar! Du skal sætte den i ovnen. Den skal være perfekt \u2014 ikke for brændt, ikke for rå. Ovnen gløder orange og varm.",
"aktion": "Bag kagen i ovnen",
"stat": "krop", "svaer": "normal",
"succes": "Du trækker kagen ud på det perfekte tidspunkt! Den er gylden og dufter fantastisk. Pinkie Pie vil elske den!",
"fiasko": "Kagen bliver lidt for brændt ovenpå. Men inde i er den stadig god \u2014 du skraber den brændte del af!",
},
{
"tekst": "Nu skal kagen pyntes! Pinkie Pie vil have regnbueglasure, sprinkles og en lille pony-figur ovenpå. Det ser sværere ud end det lydes...",
"aktion": "Pynt kagen perfekt",
"stat": "krop", "svaer": "svaert",
"succes": "Kagen er VAKKER! Regnbueglasuren skinner, sprinklerne er perfekt fordelt, og pony-figuren står lige midt på. Pinkie Pie vil græde af glæde!",
"fiasko": "Glasuren løber lidt ud, og sprinklerne lander lidt tilfældigt. Men kagen er sød \u2014 og det er det vigtigste!",
},
{
"tekst": "Kagen er klar! Nu skal alle ponyerne samles. Rainbow Dash er i luften, Fluttershy er i haven, og Rarity er i sit værksted. Du skal finde dem alle!",
"aktion": "Find alle ponyerne",
"stat": "charme", "svaer": "svaert",
"succes": "Du finder dem alle! Rainbow Dash lander med et whoosh, Fluttershy kommer småløbende, og Rarity glimrer i solen. Alle er klar til fødselsdagen! \U0001f389",
"fiasko": "Du finder de fleste \u2014 men Rainbow Dash er for hurtig at fange! Men det går fint, hun kommer alligevel når hun får lugtet kagen!",
},
],
},
{
"id": "angel-er-løbet-væk",
"titel": "Angel er løbet væk",
"emoji": "\U0001f430\u2764\ufe0f",
"intro": "Fluttershy er i gråd! Hendes lille kanin Angel er løbet væk. Du tilbyder at hjælpe med at finde ham.",
"scener": [
{
"tekst": "Du løber ud i Fluttershys have. Blomsterne er høje og farverige \u2014 røde roser, gule solsikker, blå blåklatter. Angel kan være hvor som helst her!",
"aktion": "Søg i blomsterhaven",
"stat": "sind", "svaer": "let",
"succes": "Du kigger bag en stor solsikke \u2014 og ser to små ører rulle frem! Angel gemte sig der! Men... han ser ud til at være løbet videre.",
"fiasko": "Du leder og leder, men finder kun en sommerfugl og en bier. Angel er ikke her \u2014 men du finder små fodspor der peger væk!",
},
{
"tekst": "Du løber gennem PonyVille. Alle ponyer er travle \u2014 Applejack pløjer, Rarity måler stof, og Twilight skriver noter. Har nogen set Angel?",
"aktion": "Spørg alle ponyer i PonyVille",
"stat": "charme", "svaer": "normal",
"succes": "Rarity råber: 'En lille hvid kanin? Jeg så ham løbe mod bakken!' Du har en ledetråd!",
"fiasko": "Ponyerne ryster på hovedet. 'En kanin?' Men Spike hvisker: 'Jeg så noget hvidt hoppe forbi...' Det må være ham!",
},
{
"tekst": "Små fodspor fører gennem det høje græs. De er små og runde \u2014 typiske kanin-fodspor! De fører op ad bakken mod skoven.",
"aktion": "Følg fodsporene",
"stat": "sind", "svaer": "normal",
"succes": "Fodsporene fører dig direkte til et stort træ! Du kigger op \u2014 og ser Angel sidde på en gren, der ryster nervøst.",
"fiasko": "Fodsporene forsvinder i græsset. Men du hører et lille 'pip' fra et træ \u2014 Angel er deroppe!",
},
{
"tekst": "Angel sidder højt oppe i et stort egetræ! Han ryster og græder. Træet er højt, og grenene er tynde. Du skal op til ham!",
"aktion": "Klatr op i træet",
"stat": "krop", "svaer": "svaert",
"succes": "Du klatrer op ad stammen, gren for gren. Det er skræmmende højt, men du når Angel! Han hopper ned i dine arme med en lille glæde-pip!",
"fiasko": "Du klatrer op, men grenen knækker under dig! Du lander blødt i en busk. Men Angel ser dig og hopper ned af sig selv!",
},
{
"tekst": "Angel er i dine arme, men han er stadig bange. Han ryster og gemmer hovedet i pelsen. Du skal få ham til at føle sig tryg igen.",
"aktion": "Berolig Angel",
"stat": "charme", "svaer": "svaert",
"succes": "Du stryger ham blidt over pelsen og synger en lille sang. Angel stopper med at ryste og slikker din kind! Fluttershy græder af glæde: 'Angel! Du har reddet ham!' \u2764\ufe0f",
"fiasko": "Angel er stadig en lille smule bange, men han holder fast i dit øre. Fluttershy smiler: 'Han bliver bedre \u2014 tak fordi du fandt ham!'",
},
],
},
{
"id": "æblerne-ruller",
"titel": "Æblerne ruller ned ad bakken",
"emoji": "\U0001f34e\U0001f434",
"intro": "Applejacks æbleskure er væltet! Hundredevis af æbler ruller ned ad bakken. Hjælp med at redde høsten!",
"scener": [
{
"tekst": "ÆBLER! Overalt! De ruller som små røde boulder ned ad bakken. Du skal gribe dem, før de rammer bunden!",
"aktion": "Jag de rullende æbler",
"stat": "krop", "svaer": "let",
"succes": "Du griber og griber! Æbler i munden, æbler i halen, æbler i ørerne! Du redder halvdelen!",
"fiasko": "Æblerne ruller for hurtigt! Du griber nogle få, men de fleste fortsætter ned ad bakken...",
},
{
"tekst": "Mange æbler er landet i det høje, gyldne græs. De er svære at se \u2014 kun små røde prikker mellem græsstråene.",
"aktion": "Saml æbler i højt græs",
"stat": "krop", "svaer": "normal",
"succes": "Du kravler gennem græsset og finder æblerne ét for ét! Din kurv er snart fyldt til randen!",
"fiasko": "Græsset er højt, og du finder kun halvdelen. Men Apple Bloom hjælper med at finde resten!",
},
{
"tekst": "Bækken glimrer i solen \u2014 og der svømmer røde æbler rundt i det! De er våde og glatte, svære at gribe.",
"aktion": "Fisk æbler op af bækken",
"stat": "krop", "svaer": "normal",
"succes": "Du bøjer dig ned og griber æblerne ét for ét! Vandet er koldt, men det er sjovt! Alle æbler reddet!",
"fiasko": "Æblerne er glatte! De glider ud af dine poter. Men du får fanget de fleste med halen!",
},
{
"tekst": "Nu skal alle æbler bæres tilbage til gården! Kurven er TUNG. Bakken er STEIL. Men du kan gøre det!",
"aktion": "Bær tunge kurve hjem",
"stat": "krop", "svaer": "svaert",
"succes": "Du bærer kurven op ad bakken med stolthed! Applejack råber: 'Du er stærkere end de fleste!' Alle æbler er i sikkerhed!",
"fiasko": "Kurven er for tung! Du vælter og æblerne ruller lidt. Men Big Mac kommer og hjælper \u2014 sammen når I hjem!",
},
{
"tekst": "Applejack står med tårer i øjnene. 'Du har reddet hele høsten,' hvisker hun. Hun rækker dig noget... en æblekage, lavet specielt til dig!",
"aktion": "Modtag præmien fra Applejack",
"stat": "charme", "svaer": "let",
"succes": "Æblekagen er den bedste du nogensinde har smagt! Applejack giver dig et knus. 'Du er den bedste pony i hele Equestria!' \U0001f34e",
"fiasko": "Du får æblekagen, men den er lidt brændt. Applejack griner: 'Næste gang bliver den perfekt!'",
},
],
},
{
"id": "raritys-glimmer-sten",
"titel": "Raritys glimmer-sten er borte",
"emoji": "\U0001f48e\u2728",
"intro": "Rarity er i panik! Hendes kæreste glimmer-sten \u2014 den smukkeste sten i hele Equestria \u2014 er forsvundet! Du skal finde den.",
"scener": [
{
"tekst": "Raritys værksted er et kaotisk paradis! Stofruller overalt, nåle i luften, og glitter på gulvet. Måske har stenen faldet et sted her?",
"aktion": "Søg i Raritys værksted",
"stat": "sind", "svaer": "let",
"succes": "Du finder en glimt under en stofrulle! Men det er kun en lille krystall \u2014 ikke glimmer-stenen. Men det giver dig en idé!",
"fiasko": "Værkstedet er for rodet! Du finder nåle, knapper og glitter \u2014 men ikke glimmer-stenen.",
},
{
"tekst": "Du løber gennem PonyVille. Glimmer-stenen er så lysende \u2014 den burde være svær at overse. Har nogen set den?",
"aktion": "Spørg i PonyVille",
"stat": "charme", "svaer": "normal",
"succes": "Spike råber: 'Jeg så Rarity droppe noget glitrende da hun løb forbi parken!' Du ved hvor du skal lede!",
"fiasko": "Ingen har set stenen. Men Twilight hvisker: 'Måske kan min magi hjælpe?'",
},
{
"tekst": "Parken er smuk i solen! Bænke, blomsterbede, og en lille sø. Glimmer-stenen skinner \u2014 du kan næsten SE den glitre et sted!",
"aktion": "Søg i parken",
"stat": "sind", "svaer": "normal",
"succes": "Du ser et glimt ved søen! Glimmer-stenen ligger på en sten, der glitrer som en lille sol!",
"fiasko": "Du leder overalt i parken. Men du finder en glimt der kommer fra vandet...",
},
{
"tekst": "Glimmer-stenen er faldet i søen! Du kan SE den på bunden \u2014 den skinner! Men søen er dyb, og du skal dykke for at nå den.",
"aktion": "Dyk efter glimmer-stenen",
"stat": "krop", "svaer": "svaert",
"succes": "Du dykker ned! Vandet er koldt, men du griber stenen! Du bryder overfladen med stenen i munden \u2014 den glitrer som aldrig før! \U0001f48e",
"fiasko": "Du dykker, men stenen er for dybt! Men Fluttershy kommer og dykker med dig \u2014 sammen redder I stenen!",
},
{
"tekst": "Rarity græder af glæde! 'Min glimmer-sten! Åh, du er en engel!' Hun omfavner dig og rækker dig en lille æske...",
"aktion": "Modtag Raritys tak",
"stat": "charme", "svaer": "let",
"succes": "Inden i æsken er en lille glimmer-ørestift! 'Til dig, min søde ven!' Rarity hvisker. Du føler dig som en stjerne! \u2728",
"fiasko": "Rarity giver dig en knus i stedet. 'Du er bedre end nogen sten!' Og det føles faktisk bedre.",
},
],
},
{
"id": "discord-laver-sjov",
"titel": "Discord laver sjov",
"emoji": "\U0001f9e1\U0001f409",
"intro": "Discord \u2014 den skøre drage \u2014 har forvandlet jorden til marshmallow! PonyVille er i kaos. Du skal fikse det!",
"scener": [
{
"tekst": "Du træder ud \u2014 og dit ben synker ned i MARSHMALLOW! Overalt er hvid, blød masse. I det fjerne ser du en drage der griner hysterisk.",
"aktion": "Find Discord",
"stat": "sind", "svaer": "let",
"succes": "Discord er umulig at overse! Han svæver i luften med en marshmallow-hat. 'Hej, lille pony! Vil du lege?' spørger han.",
"fiasko": "Marshmallow er overalt! Det er svær at se. Men du hører Discords grin \u2014 og følger lyden!",
},
{
"tekst": "Discord griner. 'Jeg vil kun fixe det, hvis du får mig til at grine! Det er min regel!' Han krydser armene og puster på næsen.",
"aktion": "Få Discord til at grine",
"stat": "charme", "svaer": "normal",
"succes": "Du laver en latterlig dans \u2014 hoppende, snurrende, med munden skæv! Discord griner så meget at han næsten falder af himlen! 'OK, OK! Jeg fikser det!'",
"fiasko": "Du fortæller en vittighed. Discord ryster på hovedet... men så fniser han! 'OK, det var faktisk ret sjovt. Jeg fikser det.'",
},
{
"tekst": "Discord har lavet en marshmallow-labyrint! Du skal finde vejen gennem til hans magiske krystall \u2014 den kan vende forandringen tilbage.",
"aktion": "Find vejen gennem marshmallow-labyrinten",
"stat": "krop", "svaer": "normal",
"succes": "Du hopper og balancerer gennem marshmallow-væggene! Det er som at lege i en kæmpe slikbutik! Du finder krystallen!",
"fiasko": "Du vader gennem marshmallowet \u2014 det er blød og klæbrig! Men du når alligevel krystallen!",
},
{
"tekst": "Discord rækker dig en gåde på et stykke marshmallow: 'Jeg har ben men kan ikke gå, jeg har en rygrad men kan ikke bo. Hvad er jeg?'",
"aktion": "Løs Discordens gåde",
"stat": "sind", "svaer": "svaert",
"succes": "'En bog!' råber du! Discord griner: 'Korrekt! Du er klogere end du ser ud!' Han klapper i klørne \u2014 og alt bliver normalt igen!",
"fiasko": "Du tænker længe... 'En... fisk?' Discord griner: 'Nej, det er en bog! Men det var sødt at du prøvede!' Han fikser det alligevel.",
},
{
"tekst": "PonyVille er tilbage til normalt! Discord lander blidt og rækker dig en marshmallow. 'Til næste gang,' hvisker han med et glimt i øjet. Alle ponyer fejrer dig!",
"aktion": "Fejr sejren med alle ponyer",
"stat": "charme", "svaer": "let",
"succes": "Alle ponyer synger og danser! Discord laver marshmallow-regn \u2014 men denne gang er det sjovt! Du er heltens af dagen! \U0001f389\U0001f9e1",
"fiasko": "Alle er glade! Discord giver dig et high-five. 'Du er en sej pony!' Og marshmallow-regnet er faktisk ret sjovt!",
},
],
},
]

3
web/app/game/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
"""
Game engine modules: dice, pony types, adventure system.
"""

88
web/app/game/dice.py Normal file
View File

@@ -0,0 +1,88 @@
"""
Dice engine for MLP Pony: Tails of Equestria.
d6 pool system:
- 6 = 2 successes
- 4-5 = 1 success
- 1-3 = 0 successes
Difficulty thresholds:
- let (easy) = 1 success needed
- normal = 2 successes needed
- svaert (hard) = 3 successes needed
"""
import random
DIFFICULTY_REQUIREMENTS = {
"let": 1,
"normal": 2,
"svaert": 3,
}
DIFFICULTY_TEXT = {
"let": "Nem \U0001f31f",
"normal": "Lidt sv\u00e6rt \U0001f31f\U0001f31f",
"svaert": "Sv\u00e6rt \U0001f31f\U0001f31f\U0001f31f",
}
def roll_d6(count, rng=None):
"""Roll N six-sided dice. Uses provided RNG or random module."""
if rng is None:
rng = random
return [rng.randint(1, 6) for _ in range(count)]
def count_successes(dice):
"""Count successes from a list of d6 results.
6 = 2 successes, 4-5 = 1 success, 1-3 = 0.
"""
total = 0
for d in dice:
if d == 6:
total += 2
elif d >= 4:
total += 1
return total
def roll_and_evaluate(dice_count, difficulty, rng=None):
"""Roll dice and evaluate against difficulty.
Returns dict with:
- dice: list of rolled values
- successes: total success count
- required: successes needed for this difficulty
- passed: whether the roll met the threshold
"""
if dice_count < 0:
dice_count = 0
required = DIFFICULTY_REQUIREMENTS.get(difficulty, 2)
dice = roll_d6(dice_count, rng)
successes = count_successes(dice)
return {
"dice": dice,
"successes": successes,
"required": required,
"passed": successes >= required,
}
def resolve_test(pony, stat, difficulty, rng=None):
"""Resolve a skill test for a pony.
Args:
pony: dict with stat values and optional 'talent' key
stat: which stat to use ('krop', 'sind', 'charme')
difficulty: 'let', 'normal', or 'svaert'
rng: optional random.Random instance for deterministic testing
Returns:
dict with dice, successes, required, passed
"""
dice_count = pony.get(stat, 2)
if pony.get("talent") == stat:
dice_count += 1
return roll_and_evaluate(dice_count, difficulty, rng)

80
web/app/game/pony.py Normal file
View File

@@ -0,0 +1,80 @@
"""
Pony types for MLP Pony: Tails of Equestria.
Each pony type has:
- name (Danish)
- emoji
- bonus stat (krop, sind, charme, or alle)
- description text
- image filename
"""
PONITYPER = [
{
"id": "jordpony",
"navn": "Jordpony",
"emoji": "\U0001f434",
"bonus": "krop",
"tekst": "St\u00e6rk og dygtig!",
"img": "jordpony.png",
},
{
"id": "pegasus",
"navn": "Pegasus",
"emoji": "\U0001f985",
"bonus": "krop",
"tekst": "Flyver i himlen!",
"img": "pegasus.png",
},
{
"id": "enhjorning",
"navn": "Enhj\u00f8rning",
"emoji": "\U0001f984",
"bonus": "sind",
"tekst": "Har magisk horn!",
"img": "enhjorning.png",
},
{
"id": "alicorn",
"navn": "Alicorn",
"emoji": "\U0001f451",
"bonus": "alle",
"tekst": "Magi OG vinger!",
"img": "alicorn.png",
},
]
PONYNAMNE = [
"Stjern", "Regnbue", "Glitter", "Silke", "M\u00e5ne", "Sol",
"Blomst", "Kry", "Ly", "Dug", "Is", "Flint",
"Torden", "Skum", "Ros", "Vind", "Skov", "B\u00e6k",
]
STAT_EMOJI = {"krop": "\U0001f4aa", "sind": "\U0001f9e0", "charme": "\u2764\ufe0f"}
STAT_NAVNE = {"krop": "Krop", "sind": "Sind", "charme": "Charme"}
def get_pony_type(index):
"""Get pony type by index (0-based)."""
if 0 <= index < len(PONITYPER):
return PONITYPER[index]
return None
def apply_pony_bonus(stats, pony_type):
"""Apply a pony type's bonus to a stats dict.
Stats dict has keys: krop, sind, charme.
Returns a new dict with bonuses applied.
"""
bonus = pony_type.get("bonus")
result = dict(stats)
if bonus == "krop":
result["krop"] = min(result["krop"] + 1, 5)
elif bonus == "sind":
result["sind"] = min(result["sind"] + 1, 5)
elif bonus == "alle":
result["krop"] = min(result["krop"] + 1, 5)
result["sind"] = min(result["sind"] + 1, 5)
result["charme"] = min(result["charme"] + 1, 5)
return result

59
web/app/game/state.py Normal file
View File

@@ -0,0 +1,59 @@
"""
Game state management for MLP Pony: Tails of Equestria.
Manages the full game state: pony, adventure, progress, and history.
"""
import random
import uuid
from app.data.themes import THEMAER
from app.game.pony import PONITYPER, PONYNAMNE, apply_pony_bonus
def create_game(pony_idx, tema_idx):
"""Create a new game state.
Args:
pony_idx: index into PONITYPER
tema_idx: index into THEMAER
Returns:
dict with full game state
"""
pony_type = PONITYPER[pony_idx] if 0 <= pony_idx < len(PONITYPER) else PONITYPER[0]
tema = THEMAER[tema_idx] if 0 <= tema_idx < len(THEMAER) else THEMAER[0]
name = random.choice(PONYNAMNE) + random.choice([
"hals", "støv", "ros", "vinge", "blomst", "fyr",
"blik", "horn", "lys", "snude", "pels", "mane",
])
base_stats = {
"krop": random.randint(2, 4),
"sind": random.randint(2, 4),
"charme": random.randint(2, 4),
}
stats = apply_pony_bonus(base_stats, pony_type)
talent = random.choice(["krop", "sind", "charme"])
return {
"game_id": str(uuid.uuid4()),
"pony": {
"navn": name,
"type": pony_type["navn"],
"emoji": pony_type["emoji"],
"krop": stats["krop"],
"sind": stats["sind"],
"charme": stats["charme"],
"talent": talent,
},
"tema": tema,
"scene": 0,
"historie": [],
"succeser": 0,
"fiaskoer": 0,
"færdig": False,
"udfald_tekst": None,
}

View File

@@ -0,0 +1,3 @@
"""
API and page routes.
"""

47
web/app/routes/api.py Normal file
View File

@@ -0,0 +1,47 @@
"""
JSON API routes for React frontend.
"""
from flask import Blueprint, request, jsonify, session
from app.services.game_service import start_game, roll_scene, format_scene_data
api_bp = Blueprint("api", __name__)
@api_bp.route("/api/start", methods=["POST"])
def api_start():
"""Start a new game.
Expects JSON: {"type": <pony_idx>, "tema": <theme_idx>}
"""
data = request.get_json(force=True)
ponytype_idx = data.get("type")
if ponytype_idx is None:
return jsonify({"error": "Manglende pony type"}), 400
ponytype_idx = int(ponytype_idx)
tema_idx = int(data.get("tema", 0))
game = start_game(ponytype_idx, tema_idx)
session["spil"] = game
return jsonify(format_scene_data(game))
@api_bp.route("/api/scene", methods=["GET"])
def api_scene():
"""Get current scene data."""
game = session.get("spil")
if not game:
return jsonify({"error": "no game"}), 404
return jsonify(format_scene_data(game))
@api_bp.route("/api/kast", methods=["POST"])
def api_kast():
"""Roll dice for current scene."""
game = session.get("spil")
if not game or game.get("færdig"):
return jsonify({"error": "no game"}), 404
game = roll_scene(game)
session["spil"] = game
return jsonify(format_scene_data(game))

57
web/app/routes/pages.py Normal file
View File

@@ -0,0 +1,57 @@
"""
Legacy HTML page routes (Jinja2 templates).
Kept for backward compatibility.
"""
from flask import Blueprint, render_template, redirect, url_for, request, session
from app.services.game_service import start_game, roll_scene
pages_bp = Blueprint("pages", __name__)
@pages_bp.route("/")
def index():
return render_template("index.html")
@pages_bp.route("/start", methods=["GET", "POST"])
def start():
if request.method == "POST":
from app.game.pony import PONITYPER
from app.data.themes import THEMAER
ponytype_idx = int(request.form.get("type", 0))
tema_idx = int(request.form.get("tema", 0))
game = start_game(ponytype_idx, tema_idx)
session["spil"] = game
return redirect(url_for("pages.scene"))
return render_template("start.html",
ponytyper=PONITYPER,
themaer=THEMAER,
enumerate=enumerate)
@pages_bp.route("/scene")
def scene():
game = session.get("spil")
if not game:
return redirect(url_for("pages.start"))
return render_template("spil.html", sp=game)
@pages_bp.route("/kast", methods=["POST"])
def kast():
game = session.get("spil")
if not game or game.get("færdig"):
return redirect(url_for("pages.start"))
game = roll_scene(game)
session["spil"] = game
return redirect(url_for("pages.scene"))
@pages_bp.route("/igen")
def igen():
session.pop("spil", None)
return redirect(url_for("pages.start"))

View File

@@ -0,0 +1,3 @@
"""
Service layer: game logic orchestration.
"""

View File

@@ -0,0 +1,142 @@
"""
Game service: orchestrates dice rolls, scene progression, and result formatting.
"""
from app.game.dice import resolve_test
from app.game.state import create_game
def start_game(pony_idx, tema_idx):
"""Start a new game. Returns game state dict."""
return create_game(pony_idx, tema_idx)
def roll_scene(game):
"""Process a dice roll for the current scene.
Args:
game: current game state dict
Returns:
Updated game state dict
"""
if game.get("færdig") or "tema" not in game:
return game
tema = game["tema"]
scenes = tema.get("scener", [])
if game["scene"] >= len(scenes):
game["færdig"] = True
return game
scn = scenes[game["scene"]]
stat = scn["stat"]
svaer = scn["svaer"]
pony = game["pony"]
result = resolve_test(pony, stat, svaer)
udfald = {
"aktion": scn["aktion"],
"stat": stat,
"svaer": svaer,
"dice": result["dice"],
"succeser": result["successes"],
"krav": result["required"],
"succes": result["passed"],
"tekst": scn["succes"] if result["passed"] else scn["fiasko"],
}
game["historie"].append(udfald)
game["udfald_tekst"] = udfald["tekst"]
if result["passed"]:
game["succeser"] += 1
else:
game["fiaskoer"] += 1
game["scene"] += 1
if game["scene"] >= len(scenes):
game["færdig"] = True
return game
def format_scene_data(game):
"""Format game state for JSON API response.
Returns a dict suitable for jsonify().
"""
pony = game["pony"]
tema = game.get("tema", {})
scenes = tema.get("scener", []) if tema else []
pony_img_map = {
"Jordpony": "jordpony.png",
"Pegasus": "pegasus.png",
"Enhjørning": "enhjorning.png",
"Alicorn": "alicorn.png",
}
diff_map = {
"let": "Nem \U0001f31f",
"normal": "Lidt sv\u00e6rt \U0001f31f\U0001f31f",
"svaert": "Sv\u00e6rt \U0001f31f\U0001f31f\U0001f31f",
}
result = {
"finished": game.get("færdig", False),
"ponyName": pony.get("navn", ""),
"ponyType": pony.get("type", ""),
"ponyEmoji": pony.get("emoji", ""),
"ponyImg": f"/static/images/{pony_img_map.get(pony.get('type', ''), 'jordpony.png')}",
"ponyStats": {
"krop": pony.get("krop", 2),
"sind": pony.get("sind", 2),
"charme": pony.get("charme", 2),
},
"talent": pony.get("talent", ""),
"tema": tema.get("titel", ""),
"sceneNum": f"Scene {game['scene'] + 1} af {len(scenes)}",
"progress": f"\U0001f31f {'\u2b50' * game['succeser']} {'\u2606' * (len(scenes) - game['succeser'] - game['fiaskoer'])} \u274c {'\U0001f494' * game['fiaskoer']}",
"history": [],
}
# Current scene data
if not game.get("færdig") and game["scene"] < len(scenes):
scn = scenes[game["scene"]]
result["sceneText"] = scn.get("tekst", "")
result["actionText"] = scn.get("aktion", "")
result["difficulty"] = diff_map.get(scn.get("svaer", ""), "")
else:
result["sceneText"] = ""
result["actionText"] = ""
result["difficulty"] = ""
# History
for h in game.get("historie", []):
result["history"].append({
"action": h.get("aktion", ""),
"dice": h.get("dice", []),
"success": h.get("succes", False),
"result": "\u2705 Succes!" if h.get("succes") else "\u274c Mislykket",
"story": h.get("tekst", ""),
})
# End-of-game summary
if game.get("færdig"):
total = len(scenes)
wins = game["succeser"]
result["score"] = f"{wins} succeser ud af {total}"
if wins >= total:
result["victory"] = True
result["endText"] = "Fantastisk! Du reddede Equestria! \U0001f31f"
elif wins >= total // 2:
result["mixed"] = True
result["endText"] = "Du gjorde dit bedste! Prøv igen for at blive endnu bedre! \U0001f4aa"
else:
result["defeat"] = True
result["endText"] = "Ingen panik! Alle ponies prøver igen! \U0001f308"
return result

3
web/tests/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
"""
Unit tests for MLP Pony: Tails of Equestria.
"""

113
web/tests/test_dice.py Normal file
View File

@@ -0,0 +1,113 @@
"""
Tests for the dice engine.
"""
import random
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from app.game.dice import (
roll_d6,
count_successes,
roll_and_evaluate,
resolve_test,
DIFFICULTY_REQUIREMENTS,
)
class TestRollD6:
def test_single_die_range(self):
rng = random.Random(42)
for _ in range(100):
result = roll_d6(1, rng)
assert len(result) == 1
assert 1 <= result[0] <= 6
def test_multiple_dice_count(self):
rng = random.Random(42)
for count in [1, 3, 5, 10]:
result = roll_d6(count, rng)
assert len(result) == count
def test_deterministic_with_seed(self):
rng1 = random.Random(123)
rng2 = random.Random(123)
assert roll_d6(5, rng1) == roll_d6(5, rng2)
class TestCountSuccesses:
def test_six_is_two(self):
assert count_successes([6]) == 2
def test_four_is_one(self):
assert count_successes([4]) == 1
def test_five_is_one(self):
assert count_successes([5]) == 1
def test_low_is_zero(self):
assert count_successes([1]) == 0
assert count_successes([2]) == 0
assert count_successes([3]) == 0
def test_mixed_roll(self):
assert count_successes([6, 4, 3, 1, 5, 2]) == 4 # 2+1+0+0+1+0
def test_empty_roll(self):
assert count_successes([]) == 0
class TestRollAndEvaluate:
def test_easy_needs_one(self):
rng = random.Random(42)
# Force a 6 via manual dice
result = roll_and_evaluate(1, "let", rng)
assert result["required"] == 1
def test_normal_needs_two(self):
result = roll_and_evaluate(1, "normal", random.Random(42))
assert result["required"] == 2
def test_hard_needs_three(self):
result = roll_and_evaluate(1, "svaert", random.Random(42))
assert result["required"] == 3
def test_pass_with_enough_successes(self):
# Two 6s = 4 successes, enough for any difficulty
rng = random.Random(999)
# We can't force values, so test logic instead
result = roll_and_evaluate(0, "let", random.Random(42))
assert result["dice"] == []
assert result["successes"] == 0
def test_zero_dice_on_negative(self):
result = roll_and_evaluate(-3, "let", random.Random(42))
assert result["dice"] == []
class TestResolveTest:
def test_pony_stat_used(self):
pony = {"krop": 3, "sind": 2, "charme": 4}
rng = random.Random(42)
result = resolve_test(pony, "krop", "let", rng)
assert len(result["dice"]) == 3
def test_talent_bonus(self):
pony = {"krop": 2, "sind": 2, "charme": 2, "talent": "krop"}
rng = random.Random(42)
result = resolve_test(pony, "krop", "let", rng)
assert len(result["dice"]) == 3 # 2 + 1 talent
def test_no_talent_bonus(self):
pony = {"krop": 2, "sind": 2, "charme": 2, "talent": "sind"}
rng = random.Random(42)
result = resolve_test(pony, "krop", "let", rng)
assert len(result["dice"]) == 2
def test_default_stat_value(self):
pony = {} # No stats at all
rng = random.Random(42)
result = resolve_test(pony, "krop", "let", rng)
assert len(result["dice"]) == 2 # default .get("krop", 2)

View File

@@ -0,0 +1,69 @@
"""
Integration tests: full game flow through the API.
"""
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from app.config import create_app
def make_client():
app = create_app()
app.config["TESTING"] = True
return app.test_client()
class TestGameFlow:
def test_full_game(self):
c = make_client()
# Start
r = c.post("/api/start", json={"type": 0, "tema": 0})
assert r.status_code == 200
data = r.get_json()
assert data["ponyType"] == "Jordpony"
assert data["tema"] == "Regnbues fødselsdag"
assert not data["finished"]
assert data["sceneNum"] == "Scene 1 af 5"
# Roll through all 5 scenes
for _ in range(5):
r = c.post("/api/kast")
assert r.status_code == 200
# After 5 rolls, game should be finished
r = c.get("/api/scene")
data = r.get_json()
assert data["finished"]
assert len(data["history"]) == 5
def test_invalid_pony_type(self):
c = make_client()
r = c.post("/api/start", json={"tema": 0})
assert r.status_code == 400
def test_roll_without_game(self):
c = make_client()
r = c.post("/api/kast")
assert r.status_code == 404
def test_scene_without_game(self):
c = make_client()
r = c.get("/api/scene")
assert r.status_code == 404
def test_all_themes(self):
c = make_client()
for tema_idx in range(5):
r = c.post("/api/start", json={"type": 2, "tema": tema_idx})
assert r.status_code == 200
data = r.get_json()
assert not data["finished"]
def test_all_pony_types(self):
c = make_client()
for pony_idx in range(4):
r = c.post("/api/start", json={"type": pony_idx, "tema": 0})
assert r.status_code == 200

68
web/tests/test_pony.py Normal file
View File

@@ -0,0 +1,68 @@
"""
Tests for pony type system.
"""
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from app.game.pony import PONITYPER, get_pony_type, apply_pony_bonus
class TestGetPonyType:
def test_valid_indices(self):
for i in range(4):
pt = get_pony_type(i)
assert pt is not None
assert "navn" in pt
def test_out_of_range(self):
assert get_pony_type(-1) is None
assert get_pony_type(4) is None
assert get_pony_type(100) is None
def test_four_types(self):
assert len(PONITYPER) == 4
class TestApplyPonyBonus:
def test_krop_bonus(self):
stats = {"krop": 3, "sind": 3, "charme": 3}
pony = {"bonus": "krop"}
result = apply_pony_bonus(stats, pony)
assert result["krop"] == 4
assert result["sind"] == 3
assert result["charme"] == 3
def test_sind_bonus(self):
stats = {"krop": 3, "sind": 3, "charme": 3}
pony = {"bonus": "sind"}
result = apply_pony_bonus(stats, pony)
assert result["krop"] == 3
assert result["sind"] == 4
assert result["charme"] == 3
def test_alle_bonus(self):
stats = {"krop": 3, "sind": 3, "charme": 3}
pony = {"bonus": "alle"}
result = apply_pony_bonus(stats, pony)
assert result["krop"] == 4
assert result["sind"] == 4
assert result["charme"] == 4
def test_cap_at_5(self):
stats = {"krop": 5, "sind": 5, "charme": 5}
pony = {"bonus": "alle"}
result = apply_pony_bonus(stats, pony)
assert result["krop"] == 5
assert result["sind"] == 5
assert result["charme"] == 5
def test_no_bonus(self):
stats = {"krop": 3, "sind": 3, "charme": 3}
pony = {"bonus": "nothing"}
result = apply_pony_bonus(stats, pony)
assert result["krop"] == 3
assert result["sind"] == 3
assert result["charme"] == 3