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
167f3101
Commit
167f3101
authored
May 05, 2025
by
nanahira
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'main' of github.com:EN1AK/yugioh-ccb
parents
cc628bc8
35c2045f
Changes
4
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
141 additions
and
89 deletions
+141
-89
data_utils.py
data_utils.py
+38
-15
guess_card_game.py
guess_card_game.py
+81
-65
map.py
map.py
+1
-0
templates/index.html
templates/index.html
+21
-9
No files found.
data_utils.py
View file @
167f3101
...
@@ -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
,
"守备"
:
def
ense
,
"守备"
:
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
=
[]
...
...
guess_card_game.py
View file @
167f3101
...
@@ -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"
...
...
map.py
View file @
167f3101
...
@@ -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
...
...
templates/index.html
View file @
167f3101
...
@@ -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
name
s
=
await
resp
.
json
();
const
item
s
=
await
resp
.
json
();
sug
.
innerHTML
=
''
;
sug
.
innerHTML
=
''
;
if
(
!
name
s
.
length
)
{
sug
.
hidden
=
true
;
return
;
}
if
(
!
item
s
.
length
)
{
sug
.
hidden
=
true
;
return
;
}
for
(
const
name
of
name
s
)
{
for
(
const
item
of
item
s
)
{
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>
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