diff --git a/web/app.py b/web/app.py index 87d1ef2..ab1dbd3 100644 --- a/web/app.py +++ b/web/app.py @@ -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) \ No newline at end of file diff --git a/web/app/__init__.py b/web/app/__init__.py new file mode 100644 index 0000000..8b739ca --- /dev/null +++ b/web/app/__init__.py @@ -0,0 +1,3 @@ +""" +My Little Pony: Tails of Equestria - Flask Application Factory +""" \ No newline at end of file diff --git a/web/app/config.py b/web/app/config.py new file mode 100644 index 0000000..dd9dfaa --- /dev/null +++ b/web/app/config.py @@ -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 \ No newline at end of file diff --git a/web/app/data/__init__.py b/web/app/data/__init__.py new file mode 100644 index 0000000..6c0fe39 --- /dev/null +++ b/web/app/data/__init__.py @@ -0,0 +1,3 @@ +""" +Game data: pony types, themes, scenes, achievements. +""" \ No newline at end of file diff --git a/web/app/data/themes.py b/web/app/data/themes.py new file mode 100644 index 0000000..3f6db22 --- /dev/null +++ b/web/app/data/themes.py @@ -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!", + }, + ], + }, +] \ No newline at end of file diff --git a/web/app/game/__init__.py b/web/app/game/__init__.py new file mode 100644 index 0000000..f76fb18 --- /dev/null +++ b/web/app/game/__init__.py @@ -0,0 +1,3 @@ +""" +Game engine modules: dice, pony types, adventure system. +""" \ No newline at end of file diff --git a/web/app/game/dice.py b/web/app/game/dice.py new file mode 100644 index 0000000..b48c6cb --- /dev/null +++ b/web/app/game/dice.py @@ -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) \ No newline at end of file diff --git a/web/app/game/pony.py b/web/app/game/pony.py new file mode 100644 index 0000000..dd84585 --- /dev/null +++ b/web/app/game/pony.py @@ -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 \ No newline at end of file diff --git a/web/app/game/state.py b/web/app/game/state.py new file mode 100644 index 0000000..61c0746 --- /dev/null +++ b/web/app/game/state.py @@ -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, + } \ No newline at end of file diff --git a/web/app/routes/__init__.py b/web/app/routes/__init__.py new file mode 100644 index 0000000..c1e63df --- /dev/null +++ b/web/app/routes/__init__.py @@ -0,0 +1,3 @@ +""" +API and page routes. +""" \ No newline at end of file diff --git a/web/app/routes/api.py b/web/app/routes/api.py new file mode 100644 index 0000000..2a6c162 --- /dev/null +++ b/web/app/routes/api.py @@ -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": , "tema": } + """ + 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)) \ No newline at end of file diff --git a/web/app/routes/pages.py b/web/app/routes/pages.py new file mode 100644 index 0000000..6e3b44c --- /dev/null +++ b/web/app/routes/pages.py @@ -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")) \ No newline at end of file diff --git a/web/app/services/__init__.py b/web/app/services/__init__.py new file mode 100644 index 0000000..a6a4f14 --- /dev/null +++ b/web/app/services/__init__.py @@ -0,0 +1,3 @@ +""" +Service layer: game logic orchestration. +""" \ No newline at end of file diff --git a/web/app/services/game_service.py b/web/app/services/game_service.py new file mode 100644 index 0000000..c4299db --- /dev/null +++ b/web/app/services/game_service.py @@ -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 \ No newline at end of file diff --git a/web/tests/__init__.py b/web/tests/__init__.py new file mode 100644 index 0000000..7ec7817 --- /dev/null +++ b/web/tests/__init__.py @@ -0,0 +1,3 @@ +""" +Unit tests for MLP Pony: Tails of Equestria. +""" \ No newline at end of file diff --git a/web/tests/test_dice.py b/web/tests/test_dice.py new file mode 100644 index 0000000..338bcd5 --- /dev/null +++ b/web/tests/test_dice.py @@ -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) \ No newline at end of file diff --git a/web/tests/test_integration.py b/web/tests/test_integration.py new file mode 100644 index 0000000..4ded60e --- /dev/null +++ b/web/tests/test_integration.py @@ -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 \ No newline at end of file diff --git a/web/tests/test_pony.py b/web/tests/test_pony.py new file mode 100644 index 0000000..ad1ecb7 --- /dev/null +++ b/web/tests/test_pony.py @@ -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 \ No newline at end of file