refactor: modular backend - dice engine, pony types, game state, API routes, tests
This commit is contained in:
521
web/app.py
521
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)
|
||||
3
web/app/__init__.py
Normal file
3
web/app/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
My Little Pony: Tails of Equestria - Flask Application Factory
|
||||
"""
|
||||
34
web/app/config.py
Normal file
34
web/app/config.py
Normal 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
3
web/app/data/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Game data: pony types, themes, scenes, achievements.
|
||||
"""
|
||||
223
web/app/data/themes.py
Normal file
223
web/app/data/themes.py
Normal 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
3
web/app/game/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Game engine modules: dice, pony types, adventure system.
|
||||
"""
|
||||
88
web/app/game/dice.py
Normal file
88
web/app/game/dice.py
Normal 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
80
web/app/game/pony.py
Normal 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
59
web/app/game/state.py
Normal 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,
|
||||
}
|
||||
3
web/app/routes/__init__.py
Normal file
3
web/app/routes/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
API and page routes.
|
||||
"""
|
||||
47
web/app/routes/api.py
Normal file
47
web/app/routes/api.py
Normal 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
57
web/app/routes/pages.py
Normal 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"))
|
||||
3
web/app/services/__init__.py
Normal file
3
web/app/services/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Service layer: game logic orchestration.
|
||||
"""
|
||||
142
web/app/services/game_service.py
Normal file
142
web/app/services/game_service.py
Normal 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
3
web/tests/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Unit tests for MLP Pony: Tails of Equestria.
|
||||
"""
|
||||
113
web/tests/test_dice.py
Normal file
113
web/tests/test_dice.py
Normal 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)
|
||||
69
web/tests/test_integration.py
Normal file
69
web/tests/test_integration.py
Normal 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
68
web/tests/test_pony.py
Normal 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
|
||||
Reference in New Issue
Block a user