Commit 167f3101 authored by nanahira's avatar nanahira

Merge branch 'main' of github.com:EN1AK/yugioh-ccb

parents cc628bc8 35c2045f
...@@ -3,7 +3,7 @@ import pandas as pd ...@@ -3,7 +3,7 @@ import pandas as pd
import numbers import numbers
from pathlib import Path from pathlib import Path
import sys import sys
from map import RACE_MAP, TYPE_MAP, CATEGORY_TAGS, TYPE_LINK, LINK_MARKERS, SETNAME_MAP, ATTR_MAP, TYPE_PENDULUM from map import RACE_MAP, TYPE_MAP, CATEGORY_TAGS, TYPE_LINK, LINK_MARKERS, SETNAME_MAP, ATTR_MAP, TYPE_PENDULUM,TYPE_MONSTER
def parse_flags(value, mapping): def parse_flags(value, mapping):
...@@ -91,23 +91,42 @@ def load_card_database(path: str = None) -> pd.DataFrame: ...@@ -91,23 +91,42 @@ def load_card_database(path: str = None) -> pd.DataFrame:
def card_to_tags(row): def card_to_tags(row):
type_names = parse_flags(row["type"], TYPE_MAP)
is_link = bool(row["type"] & TYPE_LINK) is_link = bool(row["type"] & TYPE_LINK)
is_pendulum = bool(row["type"] & TYPE_PENDULUM) is_pendulum = bool(row["type"] & TYPE_PENDULUM)
# 链接怪兽的“守备”清空 is_monster = bool(row["type"] & TYPE_MONSTER)
defense = "" if is_link else row["def"] if not is_monster:
atk_val = ""
def_val = ""
level = ""
scale = ""
attr = ""
race = ""
else:
# 怪兽卡才处理 -2 → “?”
atk_val = "?" if row["atk"] == -2 else row["atk"]
# 链接怪兽没有守备,其它怪兽按 -2 转换
if is_link:
def_val = ""
else:
def_val = "?" if row["def"] == -2 else row["def"]
# 等级/阶级
level = row["level"] & 0xFF
# 刻度只有灵摆怪兽才有
scale = (row["level"] >> 24) & 0xFF if is_pendulum else ""
attr = ATTR_MAP.get(row["attribute"], f"0x{row['attribute']:X}")
race = RACE_MAP.get(row["race"], f"0x{row['race']:X}")
arrows = extract_arrows(row["def"]) if is_link else [] arrows = extract_arrows(row["def"]) if is_link else []
scale = (row["level"] >> 24) & 0xFF if is_pendulum else ""
return { return {
"卡名": row["name"], "卡名": row["name"],
"攻击": row["atk"], "攻击": atk_val,
"守备": defense, "守备": def_val,
"等级/阶级": row["level"] & 0xFF, "等级/阶级": level,
"箭头": arrows, "箭头": arrows,
"刻度": scale, "刻度": scale,
"类型": parse_flags(row["type"], TYPE_MAP), "类型": type_names,
"属性": ATTR_MAP.get(row["attribute"], f"0x{row['attribute']:X}"), "属性": attr,
"种族": RACE_MAP.get(row["race"], f"0x{row['race']:X}"), "种族": race,
"效果标签": parse_category(row["category"]), "效果标签": parse_category(row["category"]),
"系列": parse_setcode(row["setcode"], SETNAME_MAP), "系列": parse_setcode(row["setcode"], SETNAME_MAP),
} }
...@@ -117,9 +136,13 @@ def compare_tags(guess_tags, answer_tags): ...@@ -117,9 +136,13 @@ def compare_tags(guess_tags, answer_tags):
def cmp(key, val1, val2): def cmp(key, val1, val2):
if (val1 == "" or val1 is None) and (val2 == "" or val2 is None): if (val1 == "" or val1 is None) and (val2 == "" or val2 is None):
return '<span class="tag tag-gray">—</span>' return '<span class="tag tag-gray">—</span>'
# 如果其中一个没——黄色“部分” if (val1 == "" or val1 is None) and (val2 != "" or val2 is not None):
if val1 == "" or val1 is None or val2 == "" or val2 is None: return '<span class="tag tag-gray">—</span>'
return '<span class="partial">—</span>' if (val1 != "" or val1 is not None) and (val2 == "" or val2 is None):
num = val1
return f'<span class="tag tag-gray">{num}</span>'
if key == "箭头": if key == "箭头":
pills = [] pills = []
......
...@@ -22,7 +22,6 @@ def filter_db(mode): ...@@ -22,7 +22,6 @@ def filter_db(mode):
mode: 'monster' | 'spell' | 'trap' | 'hot' | 'all' mode: 'monster' | 'spell' | 'trap' | 'hot' | 'all'
""" """
if mode == 'monster': if mode == 'monster':
# 怪兽卡 & 排除通常怪兽
mask = ((db['type'] & 0x1) > 0) & ((db['type'] & 0x10) == 0) mask = ((db['type'] & 0x1) > 0) & ((db['type'] & 0x10) == 0)
return db[mask] return db[mask]
if mode == 'spell': if mode == 'spell':
...@@ -98,6 +97,7 @@ def game(): ...@@ -98,6 +97,7 @@ def game():
session['mode'] = new_mode session['mode'] = new_mode
# 直接把上一行 target_id 删掉,触发上面自动重置 # 直接把上一行 target_id 删掉,触发上面自动重置
session.pop('target_id', None) session.pop('target_id', None)
session.pop('guess_count', None)
return redirect(url_for("game")) return redirect(url_for("game"))
if action == "surrender": if action == "surrender":
...@@ -115,6 +115,7 @@ def game(): ...@@ -115,6 +115,7 @@ def game():
session.pop('history', None) session.pop('history', None)
session.pop('hints', None) session.pop('hints', None)
session.pop('hinted_chars', None) session.pop('hinted_chars', None)
session.pop('guess_count', None)
elif action == "restart": elif action == "restart":
# 重新开始 # 重新开始
...@@ -123,22 +124,35 @@ def game(): ...@@ -123,22 +124,35 @@ def game():
session.pop('history', None) session.pop('history', None)
session.pop('hints', None) session.pop('hints', None)
session.pop('hinted_chars', None) session.pop('hinted_chars', None)
session.pop('guess_count', None)
return redirect(url_for("game")) return redirect(url_for("game"))
else: else:
# 普通猜测 # 普通猜测
guess_count += 1 guess_count = session.get('guess_count', 0) + 1
session['guess_count'] = guess_count session['guess_count'] = guess_count
if guess_count > max_attempts: user_input = request.form.get("guess", "").strip()
feedback = { guess_id = request.form.get("guess_id")
"error": f"😢 猜测次数已用尽!答案是【{target['name']}】", if guess_id:
"giveup": True, try:
"answer": target["name"], guess = db.loc[int(guess_id)]
"hints": hints except Exception:
} guess = None
for key in ('target_id', 'history', 'hints', 'hinted_chars', 'guess_count'): feedback = {"error": "无效的卡片选择。", "hints": hints}
session.pop(key, None) else:
user_input = request.form.get("guess", "").strip()
match = filtered[filtered["name"]
.str.contains(user_input, case=False, na=False)]
if match.empty:
guess = None
feedback = {"error": f"未找到包含“{user_input}”的卡片。", "hints": hints}
else:
guess = match.iloc[0]
# 如果 guess 还是 None,直接跳过下面逻辑
if guess is None:
return render_template( return render_template(
"index.html", "index.html",
feedback=feedback, feedback=feedback,
...@@ -146,17 +160,9 @@ def game(): ...@@ -146,17 +160,9 @@ def game():
hints=hints, hints=hints,
mode=mode, mode=mode,
guess_count=guess_count, guess_count=guess_count,
max_attempts=max_attempts max_attempts=max_attempts,
) )
user_input = request.form.get("guess", "").strip()
match = filtered[filtered["name"].str.contains(user_input, case=False, na=False)]
if match.empty:
feedback = {"error": f"未找到包含“{user_input}”的卡片。", "hints": hints}
else: else:
guess = match.iloc[0]
if guess.name == target.name: if guess.name == target.name:
# 1. 先做一次对比 # 1. 先做一次对比
compare = compare_tags(card_to_tags(guess), card_to_tags(target)) compare = compare_tags(card_to_tags(guess), card_to_tags(target))
...@@ -176,48 +182,59 @@ def game(): ...@@ -176,48 +182,59 @@ def game():
session.pop('history', None) session.pop('history', None)
session.pop('hints', None) session.pop('hints', None)
session.pop('hinted_chars', None) session.pop('hinted_chars', None)
session.pop('guess_count', None)
else: else:
# 对比并入历史 if guess_count >= max_attempts:
compare = compare_tags(card_to_tags(guess), card_to_tags(target)) feedback = {
history.append({ "error": f"😢 猜测次数已用尽!答案是【{target['name']}】",
"guess_name": guess['name'], "giveup": True,
"compare": compare "answer": target["name"],
}) "hints": hints
}
# —— 第二次猜测,给一个新的“效果标签”提示 —— # for key in ('target_id', 'history', 'hints', 'hinted_chars', 'guess_count'):
if len(history) == 2: session.pop(key, None)
target_tags = set(card_to_tags(target)["效果标签"]) else:
guessed_tags = set()
for h in history: compare = compare_tags(card_to_tags(guess), card_to_tags(target))
# history 里保存的 compare 里没有原始 list, history.append({
# 所以直接重新取一次 guess 的原始标签: "guess_name": guess['name'],
row = db[db["name"] == h["guess_name"]].iloc[0] "compare": compare
guessed_tags |= set(card_to_tags(row)["效果标签"]) })
remaining = list(target_tags - guessed_tags)
if remaining: # —— 第二次猜测,给一个新的“效果标签”提示 —— #
tag_hint = random.choice(remaining) if len(history) == 2:
hints.append(f"提示:目标卡有效果标签 “{tag_hint}”") target_tags = set(card_to_tags(target)["效果标签"])
guessed_tags = set()
# —— 第五次猜测,给一个新的名称字符提示 —— # for h in history:
if len(history) == 5: # history 里保存的 compare 里没有原始 list,
name_chars = [c for c in target["name"] if c.strip()] # 所以直接重新取一次 guess 的原始标签:
candidates = [c for c in name_chars if c not in hinted_chars] row = db[db["name"] == h["guess_name"]].iloc[0]
if candidates: guessed_tags |= set(card_to_tags(row)["效果标签"])
char_hint = random.choice(candidates) remaining = list(target_tags - guessed_tags)
hinted_chars.append(char_hint) if remaining:
hints.append(f"提示:目标卡名称中包含 “{char_hint}” 这个字") tag_hint = random.choice(remaining)
hints.append(f"提示:目标卡有效果标签 “{tag_hint}”")
# 更新 session
session['history'] = history # —— 第五次猜测,给一个新的名称字符提示 —— #
session['hints'] = hints if len(history) == 5:
session['hinted_chars'] = hinted_chars name_chars = [c for c in target["name"] if c.strip()]
candidates = [c for c in name_chars if c not in hinted_chars]
feedback = { if candidates:
"compare": compare, char_hint = random.choice(candidates)
"guess_name": guess['name'], hinted_chars.append(char_hint)
"hints": hints hints.append(f"提示:目标卡名称中包含 “{char_hint}” 这个字")
}
# 更新 session
session['history'] = history
session['hints'] = hints
session['hinted_chars'] = hinted_chars
feedback = {
"compare": compare,
"guess_name": guess['name'],
"hints": hints
}
return render_template( return render_template(
"index.html", "index.html",
...@@ -237,11 +254,10 @@ def suggest(): ...@@ -237,11 +254,10 @@ def suggest():
return jsonify([]) return jsonify([])
mode = session.get('mode', 'all') mode = session.get('mode', 'all')
pool = filter_db(mode) pool = filter_db(mode)
matches = pool[ # 只取 name 中包含 q 的行,并把 id 和 name 拼成字典列表
pool["name"].str.contains(q, case=False, na=False) df = pool[pool["name"].str.contains(q, case=False, na=False)][["name"]].reset_index()
]["name"].tolist() records = [{"id": int(r["id"]), "name": r["name"]} for _, r in df.iterrows()]
return jsonify(matches) return jsonify(records)
if __name__ == "__main__": if __name__ == "__main__":
host = "0.0.0.0" host = "0.0.0.0"
......
...@@ -26,6 +26,7 @@ CATEGORY_TAGS = { ...@@ -26,6 +26,7 @@ CATEGORY_TAGS = {
} }
TYPE_LINK = 0x4000000 TYPE_LINK = 0x4000000
TYPE_PENDULUM = 0x1000000 TYPE_PENDULUM = 0x1000000
TYPE_MONSTER = 0x1
LINK_MARKERS = { LINK_MARKERS = {
0x040: "↖", # TOP_LEFT 0x040: "↖", # TOP_LEFT
0x080: "↑", # TOP 0x080: "↑", # TOP
......
...@@ -123,7 +123,11 @@ ...@@ -123,7 +123,11 @@
<!-- 历史猜测 --> <!-- 历史猜测 -->
{% if history %} {% if history %}
<h2>历史猜测</h2> <h2>历史猜测
<small style="font-size:0.8em; color:#666;">
({{ guess_count }} / {{ max_attempts }} )
</small>
</h2>
<table> <table>
<thead> <thead>
<tr> <tr>
...@@ -154,6 +158,7 @@ ...@@ -154,6 +158,7 @@
<!-- 输入与按钮 --> <!-- 输入与按钮 -->
<form method="POST" autocomplete="off" style="position: relative; margin-top: 1em;"> <form method="POST" autocomplete="off" style="position: relative; margin-top: 1em;">
<input type="text" id="guess" name="guess" placeholder="输入卡名" oninput="fetchSuggestions()" onfocus="fetchSuggestions()" /> <input type="text" id="guess" name="guess" placeholder="输入卡名" oninput="fetchSuggestions()" onfocus="fetchSuggestions()" />
<input type="hidden" name="guess_id" id="guess_id" value="">
<!-- 三个动作按钮 --> <!-- 三个动作按钮 -->
<button type="submit" name="action" value="guess">提交猜测</button> <button type="submit" name="action" value="guess">提交猜测</button>
<button type="submit" name="action" value="surrender">投降</button> <button type="submit" name="action" value="surrender">投降</button>
...@@ -193,17 +198,23 @@ ...@@ -193,17 +198,23 @@
<script> <script>
async function fetchSuggestions() { async function fetchSuggestions() {
const input = document.getElementById("guess"); const input = document.getElementById("guess");
const sug = document.getElementById("suggestions"); const hid = document.getElementById("guess_id");
const kw = input.value.trim(); const sug = document.getElementById("suggestions");
if (!kw) { sug.innerHTML=''; sug.hidden=true; return; } const kw = input.value.trim();
if (!kw) { hid.value=""; sug.innerHTML=''; sug.hidden=true; return; }
const resp = await fetch(`/suggest?q=${encodeURIComponent(kw)}`); const resp = await fetch(`/suggest?q=${encodeURIComponent(kw)}`);
const names = await resp.json(); const items = await resp.json();
sug.innerHTML = ''; sug.innerHTML = '';
if (!names.length) { sug.hidden = true; return; } if (!items.length) { sug.hidden = true; return; }
for (const name of names) { for (const item of items) {
const li = document.createElement('li'); const li = document.createElement('li');
li.textContent = name; li.textContent = item.name;
li.onclick = () => { input.value = name; sug.hidden = true; }; li.dataset.id = item.id;
li.onclick = () => {
input.value = item.name;
hid.value = item.id;
sug.hidden = true;
};
sug.appendChild(li); sug.appendChild(li);
} }
sug.hidden = false; sug.hidden = false;
...@@ -214,5 +225,6 @@ ...@@ -214,5 +225,6 @@
} }
}); });
</script> </script>
</body> </body>
</html> </html>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment