- 修复 AI decide 方法参数类型为 Array[Card] - 添加 cards_played 和 player_passed 信号 - 出牌时在桌面中央显示排序后的卡牌 - 过牌时清除桌面牌 - 添加延迟动画效果(0.8秒显示,0.5秒清除) - 添加 TableLabel 作为出牌显示区域 - 出牌按 rank 排序显示 🤖 Generated with [Qoder][https://qoder.com]
254 lines
9.0 KiB
Python
254 lines
9.0 KiB
Python
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]) |