Mini_game / app.py
DetectiveShadow's picture
Create app.py
0b62116 verified
Raw History Blame Contribute Delete
13.9 kB
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()