DetectiveShadow commited on
Commit
0b62116
·
verified ·
1 Parent(s): 99198b1

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +306 -0
app.py ADDED
@@ -0,0 +1,306 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os, json, hashlib
2
+ from pathlib import Path
3
+ from typing import Dict, List, Tuple, Optional
4
+ import gradio as gr
5
+
6
+ DATA_PATH = os.environ.get("DATA_PATH", "skills_dataset.jsonl")
7
+
8
+ # -----------------------------
9
+ # Utilities
10
+ # -----------------------------
11
+ def _hash_id(name: str) -> str:
12
+ # stable, short-ish ID for generated skills
13
+ h = hashlib.md5(name.encode("utf-8")).hexdigest()[:8].upper()
14
+ return f"GEN-{h}"
15
+
16
+ def load_dataset(path: str) -> List[dict]:
17
+ p = Path(path)
18
+ if p.exists():
19
+ rows = []
20
+ with p.open("r", encoding="utf-8") as f:
21
+ for line in f:
22
+ line = line.strip()
23
+ if line:
24
+ rows.append(json.loads(line))
25
+ return rows
26
+ # Minimal fallback dataset if no file present
27
+ return [
28
+ {
29
+ "skill_id":"S001","name":"Slash","category":"Combat","element":"None","tier":1,
30
+ "description":"A quick melee cut.","base_power":30,"cost_mana":0,"cooldown_s":2,
31
+ "status_effects":[],"scales_with":["strength"],"prerequisites":[],
32
+ "unlock_methods":["starter"],"rarity":"common",
33
+ "evolves_to":[{"to_skill_id":"S101","condition":"Use Slash 200 times"}],
34
+ "combines_with":[]
35
+ },
36
+ {
37
+ "skill_id":"S101","name":"Cleave","category":"Combat","element":"None","tier":2,
38
+ "description":"A wide, sweeping strike.","base_power":55,"cost_mana":0,"cooldown_s":4,
39
+ "status_effects":["bleed_small"],"scales_with":["strength"],"prerequisites":["S001"],
40
+ "unlock_methods":["use_mastery"],"rarity":"uncommon","evolves_to":[],"combines_with":[]
41
+ },
42
+ {
43
+ "skill_id":"S002","name":"Fireball","category":"Magic","element":"Fire","tier":1,
44
+ "description":"Hurl a small fire orb.","base_power":40,"cost_mana":20,"cooldown_s":5,
45
+ "status_effects":["burn"],"scales_with":["intelligence"],"prerequisites":[],
46
+ "unlock_methods":["starter"],"rarity":"common","evolves_to":[],"combines_with":[]
47
+ }
48
+ ]
49
+
50
+ def index_dataset(rows: List[dict]):
51
+ by_id = {r["skill_id"]: r for r in rows}
52
+ by_name = {}
53
+ for r in rows:
54
+ # disambiguate duplicate names by appending (ID)
55
+ key = r["name"]
56
+ if key in by_name:
57
+ key = f'{r["name"]} ({r["skill_id"]})'
58
+ by_name[key] = r["skill_id"]
59
+ # Build combo map from skill-> list of {with, result, rule}
60
+ combos = {}
61
+ for r in rows:
62
+ for c in r.get("combines_with", []):
63
+ combos.setdefault(r["skill_id"], []).append(c)
64
+ # Evolutions from skill_id -> list of {"to_skill_id":..., "condition":...}
65
+ evol = {}
66
+ for r in rows:
67
+ for e in r.get("evolves_to", []):
68
+ evol.setdefault(r["skill_id"], []).append(e)
69
+ return by_id, by_name, combos, evol
70
+
71
+ # Rule packs for element infusion (extend as you like)
72
+ ELEMENT_PACK = {
73
+ "Fire": {"status_add":["burn"], "power_mult":1.15, "cost_add":+4, "desc_suffix":"Ignites targets."},
74
+ "Ice": {"status_add":["slow"], "power_mult":1.10, "cost_add":+4, "desc_suffix":"Chills on hit."},
75
+ "Poison":{"status_add":["poison"], "power_mult":1.12, "cost_add":+3, "desc_suffix":"Applies toxin."},
76
+ "Lightning":{"status_add":["shock"], "power_mult":1.14, "cost_add":+5, "desc_suffix":"Electrifies foes."},
77
+ "Wind": {"status_add":["knockback"], "power_mult":1.08, "cost_add":+2, "desc_suffix":"Carries force."},
78
+ "Earth": {"status_add":["stagger"], "power_mult":1.12, "cost_add":+2, "desc_suffix":"Hits heavy."},
79
+ "Light": {"status_add":["blind_short"], "power_mult":1.10, "cost_add":+4, "desc_suffix":"Radiant flare."},
80
+ "Shadow":{"status_add":["invisible_short"],"power_mult":1.06,"cost_add":+3,"desc_suffix":"Cloaked strike."},
81
+ "Water": {"status_add":["soak"], "power_mult":1.08, "cost_add":+3, "desc_suffix":"Drenches."},
82
+ "Arcane":{"status_add":["silence_short"],"power_mult":1.10,"cost_add":+5, "desc_suffix":"Raw surge."},
83
+ }
84
+
85
+ def generate_infused_skill(base: dict, element: str) -> dict:
86
+ pack = ELEMENT_PACK[element]
87
+ name = f"{element} {base['name']}" if element not in (base.get("element") or "") else base["name"]
88
+ new = dict(base) # shallow copy
89
+ new["name"] = name
90
+ new["element"] = element if base.get("element", "None") in ("None", "", None) else f"{base['element']}/{element}"
91
+ new["tier"] = max(1, int(base.get("tier", 1))) # same tier
92
+ new["base_power"] = int(round(base.get("base_power", 0) * pack["power_mult"]))
93
+ new["cost_mana"] = int(base.get("cost_mana", 0) + pack["cost_add"])
94
+ new["cooldown_s"] = base.get("cooldown_s", 0) # keep
95
+ new_effects = list(dict.fromkeys((base.get("status_effects") or []) + pack["status_add"]))
96
+ new["status_effects"] = new_effects
97
+ new["description"] = (base.get("description") or "") + " " + pack["desc_suffix"]
98
+ new["rarity"] = base.get("rarity", "common")
99
+ new["skill_id"] = _hash_id(f"{name}|{new['element']}|{new['base_power']}")
100
+ new["prerequisites"] = list(base.get("prerequisites") or [])
101
+ new["unlock_methods"] = list(base.get("unlock_methods") or []) + [f"infusion:{element.lower()}"]
102
+ # Generated skills have no baked edges; they can still be combined via rules
103
+ new["evolves_to"] = []
104
+ new["combines_with"] = []
105
+ return new
106
+
107
+ def dataset_combo_result(current: dict, with_id: str, by_id: Dict[str,dict], dataset_combos: Dict[str, List[dict]]) -> Optional[dict]:
108
+ # If dataset has an explicit combo from current->with_id, use that result skill definition
109
+ for c in dataset_combos.get(current["skill_id"], []):
110
+ if c["with_skill_id"] == with_id:
111
+ res_id = c["result_skill_id"]
112
+ return by_id.get(res_id)
113
+ # also check reverse
114
+ for c in dataset_combos.get(with_id, []):
115
+ if c["with_skill_id"] == current["skill_id"]:
116
+ res_id = c["result_skill_id"]
117
+ return by_id.get(res_id)
118
+ return None
119
+
120
+ def rule_combo_generate(a: dict, b: dict) -> dict:
121
+ # Simple generative fallback: merge name pieces, elements, and effects
122
+ el_a = a.get("element","None")
123
+ el_b = b.get("element","None")
124
+ merged_el = "None"
125
+ if el_a == "None": merged_el = el_b
126
+ elif el_b == "None": merged_el = el_a
127
+ else: merged_el = f"{el_a}/{el_b}"
128
+
129
+ name = f"{a['name']} + {b['name']}"
130
+ desc = f"Fusion of {a['name']} and {b['name']}."
131
+ base_power = int(round(a.get("base_power",0)*0.6 + b.get("base_power",0)*0.6)) + 8
132
+ cost = int(round(a.get("cost_mana",0)*0.5 + b.get("cost_mana",0)*0.5))
133
+ cd = max(a.get("cooldown_s",0), b.get("cooldown_s",0))
134
+ effects = list(dict.fromkeys((a.get("status_effects") or []) + (b.get("status_effects") or [])))
135
+
136
+ res = {
137
+ "skill_id": _hash_id(f"{name}|{merged_el}|{base_power}"),
138
+ "name": name,
139
+ "category": a.get("category","Combat"),
140
+ "element": merged_el,
141
+ "tier": max(a.get("tier",1), b.get("tier",1)) + 1,
142
+ "description": desc,
143
+ "base_power": base_power,
144
+ "cost_mana": cost,
145
+ "cooldown_s": cd,
146
+ "status_effects": effects,
147
+ "scales_with": sorted(list(set((a.get("scales_with") or []) + (b.get("scales_with") or [])))),
148
+ "prerequisites": [a["skill_id"], b["skill_id"]],
149
+ "unlock_methods": ["fusion_discovery"],
150
+ "rarity": "rare",
151
+ "evolves_to": [],
152
+ "combines_with": []
153
+ }
154
+ return res
155
+
156
+ def dataset_evolve(current: dict, evol_map: Dict[str,List[dict]], by_id: Dict[str,dict]) -> Optional[dict]:
157
+ # For the Space prototype, we "assume" conditions are met and take the first evolution
158
+ candidates = evol_map.get(current["skill_id"], [])
159
+ if not candidates: return None
160
+ to_id = candidates[0]["to_skill_id"]
161
+ return by_id.get(to_id)
162
+
163
+ def summarize(skill: dict) -> str:
164
+ lines = [
165
+ f"**{skill['name']}** (`{skill['skill_id']}`)",
166
+ f"- Tier: {skill.get('tier','?')} | Category: {skill.get('category','?')} | Element: {skill.get('element','None')}",
167
+ f"- Power: {skill.get('base_power',0)} | Cost: {skill.get('cost_mana',0)} | Cooldown: {skill.get('cooldown_s',0)}s",
168
+ f"- Effects: {', '.join(skill.get('status_effects') or []) or '—'}",
169
+ f"- Scales With: {', '.join(skill.get('scales_with') or []) or '—'}",
170
+ f"- Rarity: {skill.get('rarity','?')}",
171
+ f"- Desc: {skill.get('description','')}"
172
+ ]
173
+ return "\n".join(lines)
174
+
175
+ # -----------------------------
176
+ # Load / Index dataset
177
+ # -----------------------------
178
+ DATA_ROWS = load_dataset(DATA_PATH)
179
+ BY_ID, BY_NAME, COMBOS, EVOLS = index_dataset(DATA_ROWS)
180
+ START_SKILLS = sorted(BY_NAME.keys()) # for dropdown
181
+ ELEMENT_CHOICES = ["Fire","Ice","Poison","Lightning","Wind","Earth","Light","Shadow","Water","Arcane"]
182
+
183
+ # -----------------------------
184
+ # Gradio App State Machine
185
+ # -----------------------------
186
+ def new_session():
187
+ return {"history": [], "current": None}
188
+
189
+ def start_skill(select_name: str, state):
190
+ sid = BY_NAME[select_name]
191
+ base = BY_ID[sid]
192
+ state = state or new_session()
193
+ state["history"] = [("Start", base)]
194
+ state["current"] = base
195
+ return summarize(base), state, gr.update(choices=_combine_choices(state["current"]))
196
+
197
+ def _combine_choices(current: Optional[dict]) -> List[str]:
198
+ if not current: return []
199
+ # offer dataset combos and also all dataset skills as free-form for rule fusion
200
+ names = []
201
+ # dataset-defined combos:
202
+ for c in COMBOS.get(current["skill_id"], []):
203
+ with_id = c["with_skill_id"]
204
+ target = BY_ID.get(with_id)
205
+ if target:
206
+ label = f"{target['name']} ({with_id})"
207
+ names.append(label)
208
+ # also allow any skill in the dataset as a free-form partner (dedupe later)
209
+ for sid, row in BY_ID.items():
210
+ label = f"{row['name']} ({sid})"
211
+ names.append(label)
212
+ return sorted(list(dict.fromkeys(names)))
213
+
214
+ def apply_infusion(element: str, state):
215
+ state = state or new_session()
216
+ if not state["current"]:
217
+ return "Pick a start skill first.", state
218
+ infused = generate_infused_skill(state["current"], element)
219
+ state["history"].append((f"Infuse {element}", infused))
220
+ state["current"] = infused
221
+ return summarize(infused), state
222
+
223
+ def apply_combo(partner_label: str, state):
224
+ state = state or new_session()
225
+ if not state["current"]:
226
+ return "Pick a start skill first.", state, gr.update()
227
+ # partner label is like "Fireball (S002)"
228
+ if "(" in partner_label and partner_label.endswith(")"):
229
+ partner_id = partner_label.split("(")[-1][:-1]
230
+ else:
231
+ # fallback name lookup (rare)
232
+ partner_id = BY_NAME.get(partner_label, None)
233
+ if partner_id is None or partner_id not in BY_ID:
234
+ return "Invalid partner.", state, gr.update()
235
+
236
+ partner = BY_ID[partner_id]
237
+ res = dataset_combo_result(state["current"], partner_id, BY_ID, COMBOS)
238
+ if res is None:
239
+ res = rule_combo_generate(state["current"], partner)
240
+
241
+ state["history"].append((f"Combine with {partner['name']}", res))
242
+ state["current"] = res
243
+ return summarize(res), state, gr.update(choices=_combine_choices(state["current"]))
244
+
245
+ def apply_dataset_evolve(state):
246
+ state = state or new_session()
247
+ if not state["current"]:
248
+ return "Pick a start skill first.", state, gr.update()
249
+ evo = dataset_evolve(state["current"], EVOLS, BY_ID)
250
+ if evo is None:
251
+ return "No dataset evolution available for this skill.", state, gr.update()
252
+ state["history"].append(("Evolve (dataset)", evo))
253
+ state["current"] = evo
254
+ return summarize(evo), state, gr.update(choices=_combine_choices(state["current"]))
255
+
256
+ def render_history(state):
257
+ if not state or not state.get("history"):
258
+ return "—"
259
+ out = []
260
+ for step, s in state["history"]:
261
+ out.append(f"### {step}\n" + summarize(s))
262
+ return "\n\n".join(out)
263
+
264
+ def export_build(state):
265
+ if not state or not state.get("history"):
266
+ return json.dumps({"build":[]}, indent=2)
267
+ return json.dumps(
268
+ {"build":[{"step":step, "skill":skill} for step, skill in state["history"]]},
269
+ indent=2
270
+ )
271
+
272
+ # -----------------------------
273
+ # Gradio UI
274
+ # -----------------------------
275
+ with gr.Blocks(title="Skill Forge: Evolve & Combine") as demo:
276
+ gr.Markdown("# 🛠️ Skill Forge\nPick a base skill, infuse an element, evolve, and combine—step by step.")
277
+
278
+ with gr.Row():
279
+ with gr.Column():
280
+ start_dd = gr.Dropdown(choices=START_SKILLS, label="Start Skill", value=START_SKILLS[0] if START_SKILLS else None)
281
+ start_btn = gr.Button("Start Build", variant="primary")
282
+
283
+ infuse_dd = gr.Dropdown(choices=ELEMENT_CHOICES, label="Infuse Element")
284
+ infuse_btn = gr.Button("Apply Infusion")
285
+
286
+ combo_dd = gr.Dropdown(choices=[], label="Combine With (dataset or free-form)")
287
+ combo_btn = gr.Button("Combine")
288
+
289
+ evo_btn = gr.Button("Evolve (use dataset edge if available)")
290
+ export_btn = gr.Button("Export Build JSON")
291
+
292
+ with gr.Column():
293
+ current_md = gr.Markdown("Current Skill will appear here.")
294
+ history_md = gr.Markdown("—")
295
+
296
+ state = gr.State(new_session())
297
+
298
+ # Wire events
299
+ start_btn.click(start_skill, inputs=[start_dd, state], outputs=[current_md, state, combo_dd])
300
+ infuse_btn.click(apply_infusion, inputs=[infuse_dd, state], outputs=[current_md, state]).then(render_history, inputs=state, outputs=history_md)
301
+ combo_btn.click(apply_combo, inputs=[combo_dd, state], outputs=[current_md, state, combo_dd]).then(render_history, inputs=state, outputs=history_md)
302
+ evo_btn.click(apply_dataset_evolve, inputs=[state], outputs=[current_md, state, combo_dd]).then(render_history, inputs=state, outputs=history_md)
303
+ export_btn.click(export_build, inputs=[state], outputs=[gr.Code(label="Build JSON", language="json")])
304
+
305
+ if __name__ == "__main__":
306
+ demo.launch()