Life_Quest_RPG / app.py
sanyan-evo's picture
Update app.py
9cafd3d verified
Raw
History Blame Contribute Delete
5.7 kB
import gradio as gr
import json
import os
from openai import OpenAI
def get_client():
return OpenAI(
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
api_key=os.environ.get("DASHSCOPE_API_KEY", ""),
)
SYSTEM = 'Output ONLY valid JSON no markdown: {"name":"epic name","class":"funny class","level":<1-50>,"alignment":"e.g. Chaotic Tired","stats":{"STR":<1-20>,"DEX":<1-20>,"INT":<1-20>,"WIS":<1-20>,"CHA":<1-20>,"LUK":<1-20>},"quest":"one sentence epic quest"}'
COLORS = {"STR":"#e05050","DEX":"#50e080","INT":"#5080e0","WIS":"#a050e0","CHA":"#e050a0","LUK":"#f0c030"}
def roll(text):
if not text.strip():
return None
try:
r = get_client().chat.completions.create(
model="qwen-turbo",
messages=[{"role":"system","content":SYSTEM},{"role":"user","content":text}],
temperature=0.85, max_tokens=300,
)
raw = r.choices[0].message.content.strip()
if "```" in raw:
raw = raw.split("```")[1]
if raw.startswith("json"): raw = raw[4:]
d = json.loads(raw)
except Exception as e:
d = {"name":"Error Knight","class":"Debug Wizard","level":1,"alignment":"True Broken",
"stats":{k:10 for k in COLORS},"quest":str(e)[:80]}
st = d.get("stats", {})
return {
"name": d.get("name","???"),
"cls": d.get("class",""),
"level": d.get("level",1),
"alignment": d.get("alignment",""),
"quest": d.get("quest",""),
"pairs": [{"c":COLORS[k],"l":k,"v":st.get(k,10)} for k in COLORS]
}
CANVAS_HTML = """
<div class="wrap">
<canvas id="dna-canvas" width="180" height="300"></canvas>
<div id="char-card" class="card">
<div style="color:#8b6914;font-style:italic;display:flex;align-items:center;justify-content:center;height:100%;min-height:200px">Roll your character to begin...</div>
</div>
</div>
"""
INIT_JS = """
() => {
window._dnaAnim = null;
window.updateChar = function(data) {
if (!data) return;
const card = document.getElementById('char-card');
if (card) {
const bars = data.pairs.map(p =>
'<div class="sr"><span class="sk">' + p.l + '</span>' +
'<div class="sb"><div style="width:' + Math.round(p.v/20*100) + '%;height:100%;background:' + p.c + ';border-radius:2px"></div></div>' +
'<span class="sv">' + p.v + '</span></div>'
).join('');
card.innerHTML =
'<div class="cn">' + data.name + '</div>' +
'<div class="cc">' + data.cls + ' · Lv.' + data.level + ' · ' + data.alignment + '</div>' +
'<div class="stats">' + bars + '</div>' +
'<div class="quest">📜 ' + data.quest + '</div>';
}
if (window._dnaAnim) cancelAnimationFrame(window._dnaAnim);
const cv = document.getElementById('dna-canvas');
if (!cv) return;
const ctx = cv.getContext('2d');
let t = 0;
function draw() {
ctx.clearRect(0, 0, 180, 300);
data.pairs.forEach(function(e, i) {
const y = 30 + i * 42, a = t + i * 1.1;
const x1 = 90 + Math.sin(a) * 50, x2 = 90 - Math.sin(a) * 50;
ctx.strokeStyle = '#2a3a4a'; ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.moveTo(x1, y); ctx.lineTo(x2, y); ctx.stroke();
[x1, x2].forEach(function(x) {
ctx.beginPath(); ctx.arc(x, y, 9, 0, Math.PI * 2);
ctx.fillStyle = e.c; ctx.shadowColor = e.c; ctx.shadowBlur = 14;
ctx.fill(); ctx.shadowBlur = 0;
ctx.fillStyle = '#fff'; ctx.font = 'bold 8px monospace';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText(e.l, x, y);
});
});
t += 0.025;
window._dnaAnim = requestAnimationFrame(draw);
}
draw();
}
}
"""
CSS = """
body,.gradio-container{background:#08080d!important;color:#e8d5a3!important}
.gradio-container p,.gradio-container h1,.gradio-container label{color:#e8d5a3!important}
.wrap{display:flex;gap:20px;align-items:center;max-width:580px;margin:0 auto;padding:16px}
canvas{flex-shrink:0;filter:drop-shadow(0 0 8px rgba(100,150,255,.4))}
.card{flex:1;color:#e8d5a3;font-family:Georgia,serif}
.cn{font-size:20px;font-weight:bold;color:#ffd700;letter-spacing:1px;margin-bottom:3px}
.cc{font-size:12px;color:#8b6914;margin-bottom:10px}
.stats{margin-bottom:10px}
.sr{display:flex;align-items:center;gap:6px;margin-bottom:5px}
.sk{width:28px;font-size:10px;color:#c0a060}
.sb{flex:1;height:6px;background:#1a1008;border-radius:2px;overflow:hidden}
.sv{width:18px;font-size:10px;color:#e8d5a3;text-align:right}
.quest{font-size:12px;font-style:italic;color:#c0a060;border-left:2px solid #8b6914;padding-left:8px}
"""
with gr.Blocks(css=CSS, title="Life Quest", js=INIT_JS) as demo:
gr.HTML("<h1 style='color:#ffd700;text-align:center;margin-bottom:4px'>⚔️ LIFE QUEST</h1><p style='color:#c0a060;text-align:center;font-style:italic;margin-top:0'>Describe your life. Get your DNA-coded RPG character.</p>")
gr.HTML(CANVAS_HTML)
txt = gr.Textbox(label="Your life situation", placeholder="I'm a grad student surviving on ramen and false hope...", lines=3)
btn = gr.Button("⚔ Roll Character", variant="primary")
data_out = gr.JSON(visible=False)
btn.click(roll, inputs=txt, outputs=data_out).then(
fn=None,
inputs=[data_out],
js="(data) => { if (data && window.updateChar) window.updateChar(data); }"
)
demo.launch()