Files
game-cards/utility/生成扑克牌.py
xiaji 8c81e0d14b feat: 添加出牌动画和牌型排序显示
- 修复 AI decide 方法参数类型为 Array[Card]
- 添加 cards_played 和 player_passed 信号
- 出牌时在桌面中央显示排序后的卡牌
- 过牌时清除桌面牌
- 添加延迟动画效果(0.8秒显示,0.5秒清除)
- 添加 TableLabel 作为出牌显示区域
- 出牌按 rank 排序显示

🤖 Generated with [Qoder][https://qoder.com]
2026-06-01 23:12:35 +08:00

254 lines
9.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os
import subprocess
import svgwrite
# ========== 卡片基础尺寸 ==========
CARD_W = 63.5 # 毫米(标准桥牌尺寸宽)
CARD_H = 88.9 # 毫米
MM_TO_PX = 3.779527559 # 1mm ≈ 3.78px (96dpi)
W_PX = int(CARD_W * MM_TO_PX)
H_PX = int(CARD_H * MM_TO_PX)
CORNER_RADIUS = 8 # 圆角半径(毫米)
# 花色配置
SUIT_NAME = {'S': 'Spade', 'H': 'Heart', 'C': 'Club', 'D': 'Diamond'}
SUIT_COLOR = {'S': '#000000', 'H': '#C00000', 'C': '#000000', 'D': '#C00000'}
# 点数映射
RANK_NUM = {
'2':2, '3':3, '4':4, '5':5, '6':6, '7':7, '8':8, '9':9, '10':10,
'J':11, 'Q':12, 'K':13, 'A':1
}
# ========== 辅助函数 ==========
def mm_to_px(mm_val):
"""毫米转像素96 DPI"""
return mm_val * MM_TO_PX
def add_corner_indicators(dwg, rank, suit, x_off, y_off, scale=1.0, rotation=0):
"""角标:点数 + 花色字母,无特殊符号"""
group = dwg.g(transform=f"translate({x_off},{y_off}) rotate({rotation})")
rank_font_size = 12 * scale
# 点数
group.add(dwg.text(
rank,
insert=(0, rank_font_size),
font_size=f"{rank_font_size}pt",
font_family="Arial, sans-serif",
font_weight="bold",
fill=SUIT_COLOR[suit]
))
# 花色字母 S/H/C/D
suit_char = suit
suit_font_size = 10 * scale
group.add(dwg.text(
suit_char,
insert=(0, rank_font_size + suit_font_size),
font_size=f"{suit_font_size}pt",
fill=SUIT_COLOR[suit]
))
return group
def draw_suit_symbol(dwg, suit, x, y, size_mm):
"""纯SVG图形绘制四种花色彻底解决方框/乱码"""
g = dwg.g(transform=f"translate({x}, {y})")
s = size_mm / 2.0
color = SUIT_COLOR[suit]
if suit == "H":
# 红桃
path = f"M 0 {0.3*s} C {-s} {-0.3*s}, {-s} {-s}, 0 {-0.5*s} C {s} {-s}, {s} {-0.3*s}, 0 {0.3*s}"
g.add(dwg.path(d=path, fill=color))
elif suit == "D":
# 方块
pts = [(0, -s), (s, 0), (0, s), (-s, 0)]
g.add(dwg.polygon(points=pts, fill=color))
elif suit == "S":
# 黑桃
path = f"M 0 {-s} C {-s} 0, {-s} {0.6*s}, 0 {0.3*s} C {s} {0.6*s}, {s} 0, 0 {-s}"
g.add(dwg.path(d=path, fill=color))
g.add(dwg.rect(insert=(-0.1*s, 0.3*s), size=(0.2*s, 0.7*s), fill=color))
elif suit == "C":
# 梅花
r = s * 0.4
g.add(dwg.circle(center=(-r, -0.5*r), r=r, fill=color))
g.add(dwg.circle(center=(r, -0.5*r), r=r, fill=color))
g.add(dwg.circle(center=(0, 0.2*r), r=r, fill=color))
g.add(dwg.rect(insert=(-0.1*s, 0.6*r), size=(0.2*s, 0.8*s), fill=color))
return g
# ========== 数字牌点阵布局 ==========
LAYOUTS = {
2: [(0.5, 0.25), (0.5, 0.75)],
3: [(0.5, 0.20), (0.5, 0.50), (0.5, 0.80)],
4: [(0.25, 0.25), (0.75, 0.25), (0.25, 0.75), (0.75, 0.75)],
5: [(0.25, 0.25), (0.75, 0.25), (0.5, 0.5), (0.25, 0.75), (0.75, 0.75)],
6: [(0.25, 0.20), (0.75, 0.20), (0.25, 0.50), (0.75, 0.50),
(0.25, 0.80), (0.75, 0.80)],
7: [(0.25, 0.15), (0.75, 0.15), (0.5, 0.35), (0.25, 0.55), (0.75, 0.55),
(0.25, 0.85), (0.75, 0.85)],
8: [(0.25, 0.15), (0.75, 0.15), (0.25, 0.35), (0.75, 0.35),
(0.25, 0.65), (0.75, 0.65), (0.25, 0.85), (0.75, 0.85)],
9: [(0.25, 0.12), (0.5, 0.12), (0.75, 0.12),
(0.25, 0.35), (0.5, 0.35), (0.75, 0.35),
(0.25, 0.65), (0.5, 0.65), (0.75, 0.65)],
10:[(0.25, 0.10), (0.5, 0.10), (0.75, 0.10),
(0.25, 0.30), (0.5, 0.30), (0.75, 0.30),
(0.25, 0.70), (0.5, 0.70), (0.75, 0.70),
(0.5, 0.90)]
}
def add_pips(dwg, suit, rank):
"""绘制中间花色点阵"""
num = RANK_NUM[rank]
if num not in LAYOUTS:
return
points = LAYOUTS[num]
symbol_size_mm = 7.0
for (x_frac, y_frac) in points:
x = x_frac * CARD_W
y = y_frac * CARD_H
dwg.add(draw_suit_symbol(dwg, suit, x, y, symbol_size_mm))
def add_ace_center(dwg, suit):
"""A 牌中心大花色"""
big_size = 25
x = CARD_W / 2
y = CARD_H / 2
dwg.add(draw_suit_symbol(dwg, suit, x, y, big_size))
# ========== 人头牌 J/Q/K 简易图形 ==========
def draw_king(dwg, suit, x_center, y_center):
g = dwg.g()
s = 4
g.add(dwg.polygon([(x_center-s, y_center-s), (x_center, y_center-s-3), (x_center+s, y_center-s)], fill=SUIT_COLOR[suit]))
g.add(dwg.rect(insert=(x_center-s/2, y_center-s), size=(s, s), fill=SUIT_COLOR[suit]))
g.add(dwg.circle(center=(x_center, y_center), r=3, fill=SUIT_COLOR[suit]))
g.add(dwg.rect(insert=(x_center-s/2, y_center+2), size=(s, 5), fill=SUIT_COLOR[suit]))
return g
def draw_queen(dwg, suit, x_center, y_center):
g = dwg.g()
g.add(dwg.circle(center=(x_center, y_center), r=2.5, fill=SUIT_COLOR[suit]))
g.add(dwg.rect(insert=(x_center-3, y_center+2), size=(6, 4), fill=SUIT_COLOR[suit]))
return g
def draw_jack(dwg, suit, x_center, y_center):
g = dwg.g()
g.add(dwg.circle(center=(x_center, y_center), r=2.5, fill=SUIT_COLOR[suit]))
g.add(dwg.line(start=(x_center+2, y_center-2), end=(x_center+5, y_center+3), stroke=SUIT_COLOR[suit], stroke_width=1))
return g
def add_court_card_center(dwg, rank, suit):
x_center = CARD_W / 2
y_center = CARD_H / 2
drawer = {
'K': draw_king,
'Q': draw_queen,
'J': draw_jack
}[rank]
dwg.add(drawer(dwg, suit, x_center, y_center - 8))
bot = drawer(dwg, suit, x_center, y_center + 8)
bot["transform"] = f"translate({x_center},{y_center+8}) scale(1,-1) translate(-{x_center},-{y_center+8})"
dwg.add(bot)
# ========== 生成单张SVG卡牌 ==========
def create_card_svg(rank, suit, output_path):
dwg = svgwrite.Drawing(output_path, size=(f"{CARD_W}mm", f"{CARD_H}mm"), profile='tiny')
dwg.viewbox(width=CARD_W, height=CARD_H)
# 卡片边框
dwg.add(dwg.rect(
insert=(0,0),
size=(CARD_W, CARD_H),
rx=CORNER_RADIUS,
ry=CORNER_RADIUS,
fill='none',
stroke='#333333',
stroke_width=0.5
))
# 左上角标
dwg.add(add_corner_indicators(dwg, rank, suit, 3, 3, scale=1.0, rotation=0))
# 右下角标(倒置)
dwg.add(add_corner_indicators(dwg, rank, suit, CARD_W-3, CARD_H-3, scale=1.0, rotation=180))
# 中心图案
if rank == 'A':
add_ace_center(dwg, suit)
elif rank in ['J','Q','K']:
add_court_card_center(dwg, rank, suit)
else:
add_pips(dwg, suit, rank)
dwg.save()
print(f"SVG 已生成: {output_path}")
def create_joker_svg(joker_type, output_path):
dwg = svgwrite.Drawing(output_path, size=(f"{CARD_W}mm", f"{CARD_H}mm"), profile='tiny')
dwg.viewbox(width=CARD_W, height=CARD_H)
dwg.add(dwg.rect(
insert=(0,0),
size=(CARD_W, CARD_H),
rx=CORNER_RADIUS,
ry=CORNER_RADIUS,
fill='none',
stroke='#333333',
stroke_width=0.5
))
text_color = '#0000FF' if joker_type == 'SJ' else '#FF0000'
cx, cy = CARD_W/2, CARD_H/2
dwg.add(dwg.text("JOKER", insert=(cx, cy-8),
font_size="20pt", font_family="Arial", font_weight="bold",
fill=text_color, text_anchor="middle"))
sub = "Small" if joker_type == 'SJ' else "Big"
dwg.add(dwg.text(sub, insert=(cx, cy+10),
font_size="12pt", fill=text_color, text_anchor="middle"))
dwg.save()
print(f"SVG 已生成: {output_path}")
# ========== 批量生成全部SVG ==========
def generate_all_svgs(output_dir='assets/cards_svg'):
os.makedirs(output_dir, exist_ok=True)
ranks = ['2','3','4','5','6','7','8','9','10','J','Q','K','A']
suits = ['S','H','C','D']
for rank in ranks:
for suit in suits:
fname = f"{rank}{suit}.svg"
full_path = os.path.join(output_dir, fname)
create_card_svg(rank, suit, full_path)
# 大小王
create_joker_svg('SJ', os.path.join(output_dir, 'SJ.svg'))
create_joker_svg('BJ', os.path.join(output_dir, 'BJ.svg'))
# ========== 使用 Inkscape 转 PNG彻底抛弃 cairosvg / cairo ==========
def export_svg_to_png(svg_path, png_path, scale=2):
dpi = 96 * scale
cmd = [
"inkscape",
svg_path,
"--export-type=png",
f"--export-dpi={dpi}",
"-o", png_path
]
subprocess.run(cmd, check=True)
print(f"PNG 已生成: {png_path}")
def batch_export_png(svg_dir='assets/cards_svg', png_dir='assets/cards_png', scales=[2,4]):
os.makedirs(png_dir, exist_ok=True)
for fname in os.listdir(svg_dir):
if fname.lower().endswith('.svg'):
svg_path = os.path.join(svg_dir, fname)
base = fname.replace(".svg", "")
for scale in scales:
png_name = f"{base}@{scale}x.png"
png_path = os.path.join(png_dir, png_name)
export_svg_to_png(svg_path, png_path, scale)
# ========== 主入口 ==========
if __name__ == '__main__':
# 第一步:生成 SVG必跑现在SVG图形完全正常
generate_all_svgs()
# 第二步如需PNG先安装 Inkscape 并加入环境变量,再取消下面注释
# batch_export_png(scales=[2,4])