Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Y
yugioh-ccb
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Locked Files
Issues
0
Issues
0
List
Boards
Labels
Service Desk
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Security & Compliance
Security & Compliance
Dependency List
License Compliance
Packages
Packages
List
Container Registry
Analytics
Analytics
CI / CD
Code Review
Insights
Issues
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
MyCard
yugioh-ccb
Commits
065c540d
Commit
065c540d
authored
May 01, 2025
by
EN1AK
Committed by
GitHub
May 01, 2025
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add files via upload
parent
86044c2b
Changes
3
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
398 additions
and
0 deletions
+398
-0
guess_card_game.py
guess_card_game.py
+249
-0
templates/index.html
templates/index.html
+130
-0
templates/start.html
templates/start.html
+19
-0
No files found.
guess_card_game.py
0 → 100644
View file @
065c540d
from
flask
import
Flask
,
render_template
,
request
,
redirect
,
url_for
,
session
,
jsonify
import
sqlite3
import
pandas
as
pd
import
random
import
numbers
app
=
Flask
(
__name__
)
db
=
None
target_row
=
None
app
.
secret_key
=
"你自己的随机 Secret Key"
# --- 与原 CLI 版本相同的配置 ---
ATTR_MAP
=
{
0x01
:
"地"
,
0x02
:
"水"
,
0x04
:
"炎"
,
0x08
:
"风"
,
0x10
:
"光"
,
0x20
:
"暗"
,
0x40
:
"神"
}
RACE_MAP
=
{
0x1
:
"战士"
,
0x2
:
"魔法师"
,
0x4
:
"天使"
,
0x8
:
"恶魔"
,
0x10
:
"不死"
,
0x20
:
"机械"
,
0x40
:
"水"
,
0x80
:
"炎"
,
0x100
:
"岩石"
,
0x200
:
"鸟兽"
,
0x400
:
"植物"
,
0x800
:
"昆虫"
,
0x1000
:
"雷"
,
0x2000
:
"龙"
,
0x4000
:
"兽"
,
0x8000
:
"兽战士"
,
0x10000
:
"恐龙"
,
0x20000
:
"鱼"
,
0x40000
:
"海龙"
,
0x80000
:
"爬虫"
,
0x100000
:
"念动力"
,
0x200000
:
"幻神兽"
,
0x400000
:
"创造神"
,
0x800000
:
"幻龙"
,
0x1000000
:
"电子界"
,
0x2000000
:
"幻想魔"
,
}
TYPE_MAP
=
{
0x1
:
"怪兽"
,
0x2
:
"魔法"
,
0x4
:
"陷阱"
,
0x10
:
"通常"
,
0x20
:
"效果"
,
0x40
:
"融合"
,
0x80
:
"仪式"
,
0x100
:
"陷阱怪兽"
,
0x200
:
"灵魂"
,
0x400
:
"同盟"
,
0x800
:
"二重"
,
0x1000
:
"调整"
,
0x2000
:
"同调"
,
0x4000
:
"衍生物"
,
0x10000
:
"速攻"
,
0x20000
:
"永续"
,
0x40000
:
"装备"
,
0x80000
:
"场地"
,
0x100000
:
"反击"
,
0x200000
:
"翻转"
,
0x400000
:
"卡通"
,
0x800000
:
"超量"
,
0x1000000
:
"灵摆"
,
0x2000000
:
"特殊召唤"
,
0x4000000
:
"连接"
}
CATEGORY_TAGS
=
{
1100
:
'魔陷破坏'
,
1101
:
'怪兽破坏'
,
1102
:
'卡片除外'
,
1103
:
'送去墓地'
,
1104
:
'返回手卡'
,
1105
:
'返回卡组'
,
1106
:
'手卡破坏'
,
1107
:
'卡组破坏'
,
1108
:
'抽卡辅助'
,
1109
:
'卡组检索'
,
1110
:
'卡片回收'
,
1111
:
'表示形式'
,
1112
:
'控制权'
,
1113
:
'攻守变化'
,
1114
:
'穿刺伤害'
,
1115
:
'多次攻击'
,
1116
:
'攻击限制'
,
1117
:
'直接攻击'
,
1118
:
'特殊召唤'
,
1119
:
'衍生物'
,
1120
:
'种族相关'
,
1121
:
'属性相关'
,
1122
:
'LP伤害'
,
1123
:
'LP回复'
,
1124
:
'破坏耐性'
,
1125
:
'效果耐性'
,
1126
:
'指示物'
,
1127
:
'幸运'
,
1128
:
'融合相关'
,
1129
:
'同调相关'
,
1130
:
'超量相关'
,
1131
:
'效果无效'
}
def
parse_flags
(
value
,
mapping
):
return
[
name
for
bit
,
name
in
mapping
.
items
()
if
value
&
bit
]
def
parse_category
(
cat
):
return
[
CATEGORY_TAGS
[
1100
+
i
]
for
i
in
range
(
64
)
if
(
cat
>>
i
)
&
1
and
(
1100
+
i
)
in
CATEGORY_TAGS
]
def
load_card_database
(
path
):
conn
=
sqlite3
.
connect
(
path
)
# 先把两个表读进 DataFrame
datas
=
pd
.
read_sql_query
(
"SELECT id, type, atk, def, level, race, attribute, category ,hot FROM datas"
,
conn
,
index_col
=
"id"
)
texts
=
pd
.
read_sql_query
(
"SELECT id, name FROM texts"
,
conn
,
index_col
=
"id"
)
conn
.
close
()
# 合并
df
=
datas
.
join
(
texts
,
how
=
"inner"
)
.
reset_index
()
# 按 id 升序排序,drop_duplicates 保留每个 name 的第一个(即最小 id)
df
=
df
.
sort_values
(
"id"
)
.
drop_duplicates
(
subset
=
"name"
,
keep
=
"first"
)
# 以 id 重新设为索引
df
=
df
.
set_index
(
"id"
)
return
df
def
card_to_tags
(
row
):
return
{
"卡名"
:
row
[
"name"
],
"攻击"
:
row
[
"atk"
],
"守备"
:
row
[
"def"
],
"等级"
:
row
[
"level"
]
&
0xFF
,
"刻度"
:
(
row
[
"level"
]
>>
24
)
&
0xFF
,
"类型"
:
parse_flags
(
row
[
"type"
],
TYPE_MAP
),
"属性"
:
ATTR_MAP
.
get
(
row
[
"attribute"
],
f
"0x{row['attribute']:X}"
),
"种族"
:
RACE_MAP
.
get
(
row
[
"race"
],
f
"0x{row['race']:X}"
),
"效果标签"
:
parse_category
(
row
[
"category"
])
}
def
compare_tags
(
guess_tags
,
answer_tags
):
def
cmp
(
key
,
val1
,
val2
):
# 数值型字段:攻击、守备、等级、刻度
if
isinstance
(
val1
,
numbers
.
Number
):
diff
=
abs
(
val1
-
val2
)
# 先判断完全相等
if
diff
==
0
:
cls
=
"tag-green"
else
:
if
key
in
(
"攻击"
,
"守备"
):
if
diff
<=
500
:
cls
=
"tag-yellow"
else
:
cls
=
"tag-gray"
elif
key
in
(
"等级"
,
"刻度"
):
if
diff
<=
2
:
cls
=
"tag-yellow"
else
:
cls
=
"tag-gray"
else
:
cls
=
"tag-gray"
# 箭头
arrow
=
""
if
diff
==
0
else
(
"↑"
if
val1
<
val2
else
"↓"
)
return
f
'<span class="tag {cls}">{val1}{arrow}</span>'
# 列表型字段:如 类型、效果标签……
elif
isinstance
(
val1
,
list
):
pills
=
[]
for
t
in
val1
:
# 猜的 tag 在目标里才 green,否则 red
cls
=
"tag-green"
if
t
in
val2
else
"tag-red"
pills
.
append
(
f
'<span class="tag {cls}">{t}</span>'
)
return
" "
.
join
(
pills
)
or
'<span class="tag tag-gray">—</span>'
# 其它(字符串等)完全匹配才 green,否则 gray
else
:
cls
=
"tag-green"
if
val1
==
val2
else
"tag-gray"
return
f
'<span class="tag {cls}">{val1}</span>'
return
{
key
:
cmp
(
key
,
guess_tags
[
key
],
answer_tags
[
key
])
for
key
in
guess_tags
}
def
filter_db
(
mode
):
"""
mode: 'monster' | 'spell' | 'trap' | 'hot' | 'all'
"""
if
mode
==
'monster'
:
# 怪兽卡 & 排除通常怪兽
mask
=
((
db
[
'type'
]
&
0x1
)
>
0
)
&
((
db
[
'type'
]
&
0x10
)
==
0
)
return
db
[
mask
]
if
mode
==
'spell'
:
return
db
[(
db
[
'type'
]
&
0x2
)
>
0
]
if
mode
==
'trap'
:
return
db
[(
db
[
'type'
]
&
0x4
)
>
0
]
if
mode
==
'hot'
:
mask
=
((
db
[
'type'
]
&
0x1
)
>
0
)
&
((
db
[
'type'
]
&
0x10
)
==
0
)
&
(
db
[
'hot'
]
==
1
)
return
db
[
mask
]
# all
return
db
@
app
.
route
(
"/"
,
methods
=
[
"GET"
,
"POST"
])
def
start
():
"""游戏开始前,选择卡牌范围"""
if
request
.
method
==
"POST"
:
mode
=
request
.
form
.
get
(
"mode"
)
session
.
clear
()
session
[
'mode'
]
=
mode
# 随机选一个 target_id
pool
=
filter_db
(
mode
)
session
[
'target_id'
]
=
int
(
pool
.
sample
(
1
)
.
index
[
0
])
# 重置本局提示相关状态
session
[
'guess_count'
]
=
0
session
[
'hints_shown'
]
=
[]
return
redirect
(
url_for
(
"game"
))
return
render_template
(
"start.html"
)
@
app
.
route
(
"/game"
,
methods
=
[
"GET"
,
"POST"
])
def
game
():
feedback
=
None
mode
=
session
.
get
(
'mode'
)
if
not
mode
or
'target_id'
not
in
session
:
return
redirect
(
url_for
(
"start"
))
# 确保本局猜测计数与提示列表存在
if
'guess_count'
not
in
session
:
session
[
'guess_count'
]
=
0
session
[
'hints_shown'
]
=
[]
filtered
=
filter_db
(
mode
)
target
=
db
.
loc
[
session
[
'target_id'
]]
history
=
session
.
get
(
'history'
,
[])
if
request
.
method
==
"POST"
:
action
=
request
.
form
.
get
(
"action"
,
"guess"
)
if
action
==
"surrender"
:
# 投降:显示答案并清理本局
feedback
=
{
"giveup"
:
True
,
"answer"
:
target
[
"name"
]}
for
key
in
(
'target_id'
,
'history'
,
'guess_count'
,
'hints_shown'
):
session
.
pop
(
key
,
None
)
elif
action
==
"restart"
:
# 重新开始:回到选择范围页
for
key
in
(
'target_id'
,
'mode'
,
'history'
,
'guess_count'
,
'hints_shown'
):
session
.
pop
(
key
,
None
)
return
redirect
(
url_for
(
"start"
))
else
:
# 普通猜测
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}”的卡片。"
}
else
:
guess
=
match
.
iloc
[
0
]
if
guess
.
name
==
target
.
name
:
# 猜中:恭喜并清理本局
feedback
=
{
"success"
:
f
"🎉 恭喜你猜中了!答案就是【{guess['name']}】"
}
for
key
in
(
'target_id'
,
'history'
,
'guess_count'
,
'hints_shown'
):
session
.
pop
(
key
,
None
)
else
:
# 有效一次猜测
session
[
'guess_count'
]
+=
1
# 在第2次和第5次时给提示
if
session
[
'guess_count'
]
in
(
2
,
5
):
name_chars
=
list
(
target
[
'name'
])
shown
=
session
[
'hints_shown'
]
choices
=
[
c
for
c
in
name_chars
if
c
not
in
shown
]
if
choices
:
hint_char
=
random
.
choice
(
choices
)
shown
.
append
(
hint_char
)
session
[
'hints_shown'
]
=
shown
feedback
=
{
"hint"
:
hint_char
}
# 对比并存入 history
compare
=
compare_tags
(
card_to_tags
(
guess
),
card_to_tags
(
target
))
feedback
=
feedback
or
{}
feedback
.
update
({
"compare"
:
compare
,
"guess_name"
:
guess
[
'name'
]
})
history
.
append
({
"guess_name"
:
guess
[
'name'
],
"compare"
:
compare
})
session
[
'history'
]
=
history
return
render_template
(
"index.html"
,
feedback
=
feedback
,
history
=
history
)
@
app
.
route
(
"/suggest"
)
def
suggest
():
q
=
request
.
args
.
get
(
"q"
,
""
)
.
strip
()
if
not
q
:
return
jsonify
([])
mode
=
session
.
get
(
'mode'
,
'all'
)
pool
=
filter_db
(
mode
)
# ← 改这里
matches
=
pool
[
pool
[
"name"
]
.
str
.
contains
(
q
,
case
=
False
,
na
=
False
)
][
"name"
]
.
tolist
()
return
jsonify
(
matches
)
if
__name__
==
"__main__"
:
db
=
load_card_database
(
"cards.cdb"
)
app
.
run
(
debug
=
True
)
templates/index.html
0 → 100644
View file @
065c540d
<!DOCTYPE html>
<html
lang=
"zh"
>
<head>
<meta
charset=
"UTF-8"
>
<title>
🎴 猜卡游戏
</title>
<style>
.tag
{
display
:
inline-block
;
padding
:
2px
8px
;
margin
:
2px
4px
2px
0
;
border-radius
:
12px
;
font-size
:
0.9em
;
font-weight
:
500
;
vertical-align
:
middle
;
}
.tag-green
{
background
:
#c8e6c9
;
color
:
#1b5e20
;
}
.tag-yellow
{
background
:
#fff9c4
;
color
:
#665c00
;
}
.tag-gray
{
background
:
#e0e0e0
;
color
:
#424242
;
}
.tag-red
{
background
:
#ffcdd2
;
color
:
#b71c1c
;
}
body
{
font-family
:
sans-serif
;
margin
:
2em
;
background
:
#f5f5f5
;
}
input
[
type
=
"text"
]
{
width
:
400px
;
padding
:
10px
;
font-size
:
16px
;
}
ul
#suggestions
{
list-style
:
none
;
margin
:
0
;
padding
:
0
;
width
:
400px
;
border
:
1px
solid
#ccc
;
background
:
#fff
;
position
:
absolute
;
z-index
:
10
;
max-height
:
200px
;
overflow-y
:
auto
;
}
ul
#suggestions
li
{
padding
:
8px
;
cursor
:
pointer
;
}
ul
#suggestions
li
:hover
{
background-color
:
#f0f0f0
;
}
table
{
border-collapse
:
collapse
;
width
:
100%
;
margin-top
:
20px
;
}
th
,
td
{
border
:
1px
solid
#999
;
padding
:
6px
10px
;
text-align
:
center
;
}
.match
{
background-color
:
#c8e6c9
;
}
.partial
{
background-color
:
#fff9c4
;
}
.mismatch
{
background-color
:
#ffcdd2
;
}
button
{
margin-left
:
8px
;
}
</style>
</head>
<body>
<h1>
🎴 猜卡游戏
</h1>
<!-- 历史猜测 -->
{% if history %}
<h2>
历史猜测
</h2>
<table>
<thead>
<tr>
<th>
卡名
</th><th>
攻击
</th><th>
守备
</th><th>
等级
</th><th>
刻度
</th>
<th>
类型
</th><th>
属性
</th><th>
种族
</th><th>
效果标签
</th>
</tr>
</thead>
<tbody>
{% for entry in history %}
<tr>
<td>
{{ entry.guess_name }}
</td>
<td>
{{ entry.compare['攻击']|safe }}
</td>
<td>
{{ entry.compare['守备']|safe }}
</td>
<td>
{{ entry.compare['等级']|safe }}
</td>
<td>
{{ entry.compare['刻度']|safe }}
</td>
<td
style=
"white-space: nowrap;"
>
{{ entry.compare['类型']|safe }}
</td>
<td
style=
"white-space: nowrap;"
>
{{ entry.compare['属性']|safe }}
</td>
<td
style=
"white-space: nowrap;"
>
{{ entry.compare['种族']|safe }}
</td>
<td
style=
"white-space: nowrap;"
>
{{ entry.compare['效果标签']|safe }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
<!-- 输入与按钮 -->
<form
method=
"POST"
autocomplete=
"off"
style=
"position: relative; margin-top: 1em;"
>
<input
type=
"text"
id=
"guess"
name=
"guess"
placeholder=
"输入卡名"
oninput=
"fetchSuggestions()"
onfocus=
"fetchSuggestions()"
/>
<!-- 三个动作按钮 -->
<button
type=
"submit"
name=
"action"
value=
"guess"
>
提交猜测
</button>
<button
type=
"submit"
name=
"action"
value=
"surrender"
>
投降
</button>
<button
type=
"submit"
name=
"action"
value=
"restart"
>
重新开始
</button>
<ul
id=
"suggestions"
hidden
></ul>
</form>
<!-- 本次反馈 -->
{% if feedback %}
{% if feedback.error %}
<p
style=
"color:red; font-weight:bold;"
>
{{ feedback.error }}
</p>
{% elif feedback.success %}
<p
style=
"color:green; font-weight:bold;"
>
{{ feedback.success }}
</p>
{% elif feedback.giveup %}
<p
style=
"color:blue; font-weight:bold;"
>
💡 放弃了!正确答案是:{{ feedback.answer }}
</p>
{% elif feedback.compare %}
{% if feedback.hint %}
<p
style=
"color:blue;"
>
💡 提示:卡名中包含 “{{ feedback.hint }}” 这个字
</p>
{% endif %}
<h3>
你猜的是:
<strong>
{{ feedback.guess_name }}
</strong></h3>
<table>
<thead><tr><th>
属性
</th><th>
结果
</th></tr></thead>
<tbody>
{% for k,v in feedback.compare.items() %}
<tr><td>
{{ k }}
</td><td>
{{ v|safe }}
</td></tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% endif %}
<script>
async
function
fetchSuggestions
()
{
const
input
=
document
.
getElementById
(
"
guess
"
);
const
sug
=
document
.
getElementById
(
"
suggestions
"
);
const
kw
=
input
.
value
.
trim
();
if
(
!
kw
)
{
sug
.
innerHTML
=
''
;
sug
.
hidden
=
true
;
return
;
}
const
resp
=
await
fetch
(
`/suggest?q=
${
encodeURIComponent
(
kw
)}
`
);
const
names
=
await
resp
.
json
();
sug
.
innerHTML
=
''
;
if
(
!
names
.
length
)
{
sug
.
hidden
=
true
;
return
;
}
for
(
const
name
of
names
)
{
const
li
=
document
.
createElement
(
'
li
'
);
li
.
textContent
=
name
;
li
.
onclick
=
()
=>
{
input
.
value
=
name
;
sug
.
hidden
=
true
;
};
sug
.
appendChild
(
li
);
}
sug
.
hidden
=
false
;
}
document
.
addEventListener
(
'
click
'
,
e
=>
{
if
(
!
e
.
target
.
closest
(
'
#guess
'
))
{
document
.
getElementById
(
"
suggestions
"
).
hidden
=
true
;
}
});
</script>
</body>
</html>
templates/start.html
0 → 100644
View file @
065c540d
<!DOCTYPE html>
<html
lang=
"zh-CN"
>
<head>
<meta
charset=
"UTF-8"
>
<title>
开始新游戏
</title>
</head>
<body>
<h1>
选择本次游戏的卡片范围
</h1>
<form
method=
"post"
>
<label><input
type=
"radio"
name=
"mode"
value=
"monster"
required
>
怪兽卡(排除通常怪兽)
</label><br>
<label><input
type=
"radio"
name=
"mode"
value=
"spell"
>
魔法卡
</label><br>
<label><input
type=
"radio"
name=
"mode"
value=
"trap"
>
陷阱卡
</label><br>
<label><input
type=
"radio"
name=
"mode"
value=
"all"
>
全部卡片
</label><br>
<label><input
type=
"radio"
name=
"mode"
value=
"hot"
>
热门怪兽卡
</label><br>
<br>
<button
type=
"submit"
>
开始游戏
</button>
</form>
</body>
</html>
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment