Mini_game / app.py
DetectiveShadow's picture
Create app.py
0b62116 verified
import os, json, hashlib
from pathlib import Path
from typing import Dict, List, Tuple, Optional
import gradio as gr
DATA_PATH = os.environ.get("DATA_PATH", "skills_dataset.jsonl")
# -----------------------------
# Utilities
# -----------------------------
def _hash_id(name: str) -> str:
# stable, short-ish ID for generated skills
h = hashlib.md5(name.encode("utf-8")).hexdigest()[:8].upper()
return f"GEN-{h}"
def load_dataset(path: str) -> List[dict]:
p = Path(path)
if p.exists():
rows = []
with p.open("r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if line:
rows.append(json.loads(line))
return rows
# Minimal fallback dataset if no file present
return [
{
"skill_id":"S001","name":"Slash","category":"Combat","element":"None","tier":1,
"description":"A quick melee cut.","base_power":30,"cost_mana":0,"cooldown_s":2,
"status_effects":[],"scales_with":["strength"],"prerequisites":[],
"unlock_methods":["starter"],"rarity":"common",
"evolves_to":[{"to_skill_id":"S101","condition":"Use Slash 200 times"}],
"combines_with":[]
},
{
"skill_id":"S101","name":"Cleave","category":"Combat","element":"None","tier":2,
"description":"A wide, sweeping strike.","base_power":55,"cost_mana":0,"cooldown_s":4,
"status_effects":["bleed_small"],"scales_with":["strength"],"prerequisites":["S001"],
"unlock_methods":["use_mastery"],"rarity":"uncommon","evolves_to":[],"combines_with":[]
},
{
"skill_id":"S002","name":"Fireball","category":"Magic","element":"Fire","tier":1,
"description":"Hurl a small fire orb.","base_power":40,"cost_mana":20,"cooldown_s":5,
"status_effects":["burn"],"scales_with":["intelligence"],"prerequisites":[],
"unlock_methods":["starter"],"rarity":"common","evolves_to":[],"combines_with":[]
}
]
def index_dataset(rows: List[dict]):
by_id = {r["skill_id"]: r for r in rows}
by_name = {}
for r in rows:
# disambiguate duplicate names by appending (ID)
key = r["name"]
if key in by_name:
key = f'{r["name"]} ({r["skill_id"]})'
by_name[key] = r["skill_id"]
# Build combo map from skill-> list of {with, result, rule}
combos = {}
for r in rows:
for c in r.get("combines_with", []):
combos.setdefault(r["skill_id"], []).append(c)
# Evolutions from skill_id -> list of {"to_skill_id":..., "condition":...}
evol = {}
for r in rows:
for e in r.get("evolves_to", []):
evol.setdefault(r["skill_id"], []).append(e)
return by_id, by_name, combos, evol
# Rule packs for element infusion (extend as you like)
ELEMENT_PACK = {
"Fire": {"status_add":["burn"], "power_mult":1.15, "cost_add":+4, "desc_suffix":"Ignites targets."},
"Ice": {"status_add":["slow"], "power_mult":1.10, "cost_add":+4, "desc_suffix":"Chills on hit."},
"Poison":{"status_add":["poison"], "power_mult":1.12, "cost_add":+3, "desc_suffix":"Applies toxin."},
"Lightning":{"status_add":["shock"], "power_mult":1.14, "cost_add":+5, "desc_suffix":"Electrifies foes."},
"Wind": {"status_add":["knockback"], "power_mult":1.08, "cost_add":+2, "desc_suffix":"Carries force."},
"Earth": {"status_add":["stagger"], "power_mult":1.12, "cost_add":+2, "desc_suffix":"Hits heavy."},
"Light": {"status_add":["blind_short"], "power_mult":1.10, "cost_add":+4, "desc_suffix":"Radiant flare."},
"Shadow":{"status_add":["invisible_short"],"power_mult":1.06,"cost_add":+3,"desc_suffix":"Cloaked strike."},
"Water": {"status_add":["soak"], "power_mult":1.08, "cost_add":+3, "desc_suffix":"Drenches."},
"Arcane":{"status_add":["silence_short"],"power_mult":1.10,"cost_add":+5, "desc_suffix":"Raw surge."},
}
def generate_infused_skill(base: dict, element: str) -> dict:
pack = ELEMENT_PACK[element]
name = f"{element} {base['name']}" if element not in (base.get("element") or "") else base["name"]
new = dict(base) # shallow copy
new["name"] = name
new["element"] = element if base.get("element", "None") in ("None", "", None) else f"{base['element']}/{element}"
new["tier"] = max(1, int(base.get("tier", 1))) # same tier
new["base_power"] = int(round(base.get("base_power", 0) * pack["power_mult"]))
new["cost_mana"] = int(base.get("cost_mana", 0) + pack["cost_add"])
new["cooldown_s"] = base.get("cooldown_s", 0) # keep
new_effects = list(dict.fromkeys((base.get("status_effects") or []) + pack["status_add"]))
new["status_effects"] = new_effects
new["description"] = (base.get("description") or "") + " " + pack["desc_suffix"]
new["rarity"] = base.get("rarity", "common")
new["skill_id"] = _hash_id(f"{name}|{new['element']}|{new['base_power']}")
new["prerequisites"] = list(base.get("prerequisites") or [])
new["unlock_methods"] = list(base.get("unlock_methods") or []) + [f"infusion:{element.lower()}"]
# Generated skills have no baked edges; they can still be combined via rules
new["evolves_to"] = []
new["combines_with"] = []
return new
def dataset_combo_result(current: dict, with_id: str, by_id: Dict[str,dict], dataset_combos: Dict[str, List[dict]]) -> Optional[dict]:
# If dataset has an explicit combo from current->with_id, use that result skill definition
for c in dataset_combos.get(current["skill_id"], []):
if c["with_skill_id"] == with_id:
res_id = c["result_skill_id"]
return by_id.get(res_id)
# also check reverse
for c in dataset_combos.get(with_id, []):
if c["with_skill_id"] == current["skill_id"]:
res_id = c["result_skill_id"]
return by_id.get(res_id)
return None
def rule_combo_generate(a: dict, b: dict) -> dict:
# Simple generative fallback: merge name pieces, elements, and effects
el_a = a.get("element","None")
el_b = b.get("element","None")
merged_el = "None"
if el_a == "None": merged_el = el_b
elif el_b == "None": merged_el = el_a
else: merged_el = f"{el_a}/{el_b}"
name = f"{a['name']} + {b['name']}"
desc = f"Fusion of {a['name']} and {b['name']}."
base_power = int(round(a.get("base_power",0)*0.6 + b.get("base_power",0)*0.6)) + 8
cost = int(round(a.get("cost_mana",0)*0.5 + b.get("cost_mana",0)*0.5))
cd = max(a.get("cooldown_s",0), b.get("cooldown_s",0))
effects = list(dict.fromkeys((a.get("status_effects") or []) + (b.get("status_effects") or [])))
res = {
"skill_id": _hash_id(f"{name}|{merged_el}|{base_power}"),
"name": name,
"category": a.get("category","Combat"),
"element": merged_el,
"tier": max(a.get("tier",1), b.get("tier",1)) + 1,
"description": desc,
"base_power": base_power,
"cost_mana": cost,
"cooldown_s": cd,
"status_effects": effects,
"scales_with": sorted(list(set((a.get("scales_with") or []) + (b.get("scales_with") or [])))),
"prerequisites": [a["skill_id"], b["skill_id"]],
"unlock_methods": ["fusion_discovery"],
"rarity": "rare",
"evolves_to": [],
"combines_with": []
}
return res
def dataset_evolve(current: dict, evol_map: Dict[str,List[dict]], by_id: Dict[str,dict]) -> Optional[dict]:
# For the Space prototype, we "assume" conditions are met and take the first evolution
candidates = evol_map.get(current["skill_id"], [])
if not candidates: return None
to_id = candidates[0]["to_skill_id"]
return by_id.get(to_id)
def summarize(skill: dict) -> str:
lines = [
f"**{skill['name']}** (`{skill['skill_id']}`)",
f"- Tier: {skill.get('tier','?')} | Category: {skill.get('category','?')} | Element: {skill.get('element','None')}",
f"- Power: {skill.get('base_power',0)} | Cost: {skill.get('cost_mana',0)} | Cooldown: {skill.get('cooldown_s',0)}s",
f"- Effects: {', '.join(skill.get('status_effects') or []) or '—'}",
f"- Scales With: {', '.join(skill.get('scales_with') or []) or '—'}",
f"- Rarity: {skill.get('rarity','?')}",
f"- Desc: {skill.get('description','')}"
]
return "\n".join(lines)
# -----------------------------
# Load / Index dataset
# -----------------------------
DATA_ROWS = load_dataset(DATA_PATH)
BY_ID, BY_NAME, COMBOS, EVOLS = index_dataset(DATA_ROWS)
START_SKILLS = sorted(BY_NAME.keys()) # for dropdown
ELEMENT_CHOICES = ["Fire","Ice","Poison","Lightning","Wind","Earth","Light","Shadow","Water","Arcane"]
# -----------------------------
# Gradio App State Machine
# -----------------------------
def new_session():
return {"history": [], "current": None}
def start_skill(select_name: str, state):
sid = BY_NAME[select_name]
base = BY_ID[sid]
state = state or new_session()
state["history"] = [("Start", base)]
state["current"] = base
return summarize(base), state, gr.update(choices=_combine_choices(state["current"]))
def _combine_choices(current: Optional[dict]) -> List[str]:
if not current: return []
# offer dataset combos and also all dataset skills as free-form for rule fusion
names = []
# dataset-defined combos:
for c in COMBOS.get(current["skill_id"], []):
with_id = c["with_skill_id"]
target = BY_ID.get(with_id)
if target:
label = f"{target['name']} ({with_id})"
names.append(label)
# also allow any skill in the dataset as a free-form partner (dedupe later)
for sid, row in BY_ID.items():
label = f"{row['name']} ({sid})"
names.append(label)
return sorted(list(dict.fromkeys(names)))
def apply_infusion(element: str, state):
state = state or new_session()
if not state["current"]:
return "Pick a start skill first.", state
infused = generate_infused_skill(state["current"], element)
state["history"].append((f"Infuse {element}", infused))
state["current"] = infused
return summarize(infused), state
def apply_combo(partner_label: str, state):
state = state or new_session()
if not state["current"]:
return "Pick a start skill first.", state, gr.update()
# partner label is like "Fireball (S002)"
if "(" in partner_label and partner_label.endswith(")"):
partner_id = partner_label.split("(")[-1][:-1]
else:
# fallback name lookup (rare)
partner_id = BY_NAME.get(partner_label, None)
if partner_id is None or partner_id not in BY_ID:
return "Invalid partner.", state, gr.update()
partner = BY_ID[partner_id]
res = dataset_combo_result(state["current"], partner_id, BY_ID, COMBOS)
if res is None:
res = rule_combo_generate(state["current"], partner)
state["history"].append((f"Combine with {partner['name']}", res))
state["current"] = res
return summarize(res), state, gr.update(choices=_combine_choices(state["current"]))
def apply_dataset_evolve(state):
state = state or new_session()
if not state["current"]:
return "Pick a start skill first.", state, gr.update()
evo = dataset_evolve(state["current"], EVOLS, BY_ID)
if evo is None:
return "No dataset evolution available for this skill.", state, gr.update()
state["history"].append(("Evolve (dataset)", evo))
state["current"] = evo
return summarize(evo), state, gr.update(choices=_combine_choices(state["current"]))
def render_history(state):
if not state or not state.get("history"):
return "—"
out = []
for step, s in state["history"]:
out.append(f"### {step}\n" + summarize(s))
return "\n\n".join(out)
def export_build(state):
if not state or not state.get("history"):
return json.dumps({"build":[]}, indent=2)
return json.dumps(
{"build":[{"step":step, "skill":skill} for step, skill in state["history"]]},
indent=2
)
# -----------------------------
# Gradio UI
# -----------------------------
with gr.Blocks(title="Skill Forge: Evolve & Combine") as demo:
gr.Markdown("# 🛠️ Skill Forge\nPick a base skill, infuse an element, evolve, and combine—step by step.")
with gr.Row():
with gr.Column():
start_dd = gr.Dropdown(choices=START_SKILLS, label="Start Skill", value=START_SKILLS[0] if START_SKILLS else None)
start_btn = gr.Button("Start Build", variant="primary")
infuse_dd = gr.Dropdown(choices=ELEMENT_CHOICES, label="Infuse Element")
infuse_btn = gr.Button("Apply Infusion")
combo_dd = gr.Dropdown(choices=[], label="Combine With (dataset or free-form)")
combo_btn = gr.Button("Combine")
evo_btn = gr.Button("Evolve (use dataset edge if available)")
export_btn = gr.Button("Export Build JSON")
with gr.Column():
current_md = gr.Markdown("Current Skill will appear here.")
history_md = gr.Markdown("—")
state = gr.State(new_session())
# Wire events
start_btn.click(start_skill, inputs=[start_dd, state], outputs=[current_md, state, combo_dd])
infuse_btn.click(apply_infusion, inputs=[infuse_dd, state], outputs=[current_md, state]).then(render_history, inputs=state, outputs=history_md)
combo_btn.click(apply_combo, inputs=[combo_dd, state], outputs=[current_md, state, combo_dd]).then(render_history, inputs=state, outputs=history_md)
evo_btn.click(apply_dataset_evolve, inputs=[state], outputs=[current_md, state, combo_dd]).then(render_history, inputs=state, outputs=history_md)
export_btn.click(export_build, inputs=[state], outputs=[gr.Code(label="Build JSON", language="json")])
if __name__ == "__main__":
demo.launch()