目录
概述
使用Python语言开发的蜘蛛纸牌游戏, 与蜘蛛纸牌游戏的功能基本一致(目前缺少"撤销"操作, 后续可能会添加).
代码可从我的资源下载, 该资源为付费资源(!!创作不易, 敬请谅解!!)
特色
- 界面基于
tkinter
模块; - 支持游戏的保存和导入;
- 纸牌的移动和发牌通过鼠标操作.
功能演示
- 移动纸牌演示:
- 发牌演示
- 保存与导入演示.
详细说明
CardSuit(纸牌花色枚举)
CardSuit
枚举类用于表示纸牌的花色, 分别为: 红桃(Hearts
), 黑桃(Spades
), 方片(Diamonds
), 梅花(Clubs
). 目前游戏只用到了黑桃. 后续可以扩充为使用 两种花色 和 四种花色.
class CardSuit(Enum):
Hearts = 0
Spades = 1
Diamonds = 2,
Clubs = 3
Card(纸牌类)
Card
类用于表示纸牌. 纸牌的属性包括:
- 点数(
points: int
), - 花色(
suit: CardSuit
), - 是否正面朝上(
isfront: bool
), - 在
canvas
中对应的项目ID - 正面图像(
_imfront: ImageTk.PhotoImage
), - 反面图像(
_imback: ImageTk.PhotoImage
)
image()
方法用于返回要显示的图像. isfront==True
时, 返回正面图像, 否则返回反面图像.
class Card:
def __init__(self, points: int, suit: CardSuit,
imfront: ImageTk.PhotoImage,
imback: ImageTk.PhotoImage,
isfront: bool = False) -> None:
self.points = points
self.suit = suit
self._imfront = imfront
self._imback = imback
self.isfront = isfront
self.itemId = None
def image(self):
if self.isfront:
return self._imfront
else:
return self._imback
...
SpiderSolitaire(蜘蛛纸牌类)
SpiderSolitaire
为蜘蛛纸牌类, 包含整个游戏的界面的显示, 事件响应, 游戏逻辑等.
SpiderSolitaire
类继承自Mainwindow
.
class SpiderSolitaire(Mainwindow):
"""
蜘蛛纸牌
"""
def __init__(self, master: Tk, **kw):
super().__init__(master, **kw)
self.createMenu()
self.canvas = Canvas(self.mainframe, width=CANVAS_WIDTH, height=CANVAS_HEIGHT, background='gray55')
self.canvas.grid(row=0, column=0, sticky=NS)
...
# set window's title.
self.master.title('Spider Solitaire')
# set window's geometry size
self.master.geometry(f'{CANVAS_WIDTH+10}x{CANVAS_HEIGHT+10}')
self.showmessage('Ready.')
常量
以下常量在SpiderSolitaire
类中被用到.
# 纸牌的宽度和高度
CARD_WIDTH = 68
CARD_HEIGHT = 103
# 列宽. COL_WIDTH > CARD_WIDTH
COL_WIDTH = 90
# 行宽
ROW_WIDTH = 25
# 左侧空白宽度
COL_PAD = (COL_WIDTH - CARD_WIDTH) // 2
# 列数
COL_NUM = 10
# 待发牌堆数, 每堆 COL_NUM 张牌
HEAP_NUM = 5
# 牌的组数. 牌的总数为: PACK_NUM * 13
PACK_NUM = 8
# 右下角牌堆显示的宽度
HEAP_WIDTH = 20
# 待发牌堆的水平偏移和垂直偏移
HEAP_OS_H = COL_WIDTH*COL_NUM - HEAP_WIDTH * HEAP_NUM - CARD_WIDTH
HEAP_OS_V = ROW_WIDTH * 25
# 显示文字的水平偏移和垂直偏移
TEXT_OS_H = COL_WIDTH*COL_NUM//2
TEXT_OS_V = ROW_WIDTH * 25 + CARD_HEIGHT//2
# CANVAS的宽高
CANVAS_WIDTH = COL_WIDTH*COL_NUM
CANVAS_HEIGHT = HEAP_OS_V + CARD_HEIGHT + 25
抽象数据结构(ADT)
- 纸牌队列
游戏界面中每一列纸牌(card_queue
), 用一个列表(list
)表示. 列表元素为Card
. 所有纸牌列用一个列表(list
)表示.
class SpiderSolitaire(Mainwindow):
...
def start(self):
...
self.card_queues = [[] for k in range(COL_NUM)]
while len(cards) > 0:
for k in range(COL_NUM):
self.card_queues[k].append(cards.pop())
if len(cards) <= 0:
break
- 待发纸牌
待发纸牌(card_heaps
)分成5组, 每组10张(对应纸牌的列数). 每组纸牌(card_heap
)用一个列表(list
)表示, 所有待发的纸牌构成一个列表(list
)
class SpiderSolitaire(Mainwindow):
...
def start(self):
...
self.card_heaps = []
for k in range(HEAP_NUM):
self.card_heaps.append([cards.pop() for i in range(COL_NUM)])
- 分数
分数(score
)用int
表示, 初始分数为500, 每移动一组纸牌, 分数-1. 完成一组序列(从"K"到"A"连续递减), 分数+100. - 操作计数
操作计数(moves
)用int
表示, 初始为0, 每移动一组纸牌, 操作计数+1.
翻牌的实现
每一张在纸牌队列和待发纸牌中的纸牌, 在canvas
中都对应一个image
项, 该项通过调用self.canvas.create_image()
创建.
翻牌动作通过删除该纸牌对应的image
项( self.canvas.delete(card.itemId)
), 然后创建新的image
项实现.
def flopTop(self, col: int):
"""
翻最上面的一张牌
"""
if len(self.card_queues[col]) == 0:
return False
card: Card = self.card_queues[col][-1]
card.isfront = True
self.canvas.delete(card.itemId)
card.itemId = self.canvas.create_image(
self.col_from*COL_WIDTH+COL_PAD,
(len(self.card_queues[self.col_from])-1)*ROW_WIDTH,
image=card.image(), anchor='nw')
return True
纸牌移动的实现
- 纸牌的移动用到了鼠标左键的"按下(
<ButtonPress-1>
)", “拖动(<B1-Motion>
)”, "释放(<ButtonRelease-1>
)"三个预定义事件.分别绑定self.buttone1Press()
,self.button1Motion()
,self.button1Release()
三个函数.
class SpiderSolitaire(Mainwindow):
def __init__(self, master: Tk, **kw):
...
self.canvas.bind("<ButtonPress-1>", self.buttone1Press)
self.canvas.bind("<ButtonRelease-1>", self.button1Release)
self.canvas.bind("<B1-Motion>", self.button1Motion)
buttone1Press()
- 在
buttone1Press()
函数中, 获取当前点击的纸牌(card
)所在的行和列, 并判断该纸牌是否正面朝上(card.isfront == True
), 且其上的所有要移动的纸牌(self.select_cards
)是否满足连续递减的条件 (该函数同时需要判断点击区域是否为待发牌区域). - 因为
Canvas
中的所有项目是按照创建先后进行堆叠的, 如果只是单纯移动项目的位置, 可能导致意想不到的遮盖混乱. 因此需要调用self.canvas.lift(card.itemId)
函数, 将要移动的纸牌的堆叠顺序,移动到最上层. - 因为可能出现移动失败的情况(移动到的列跟移动的纸牌不能衔接, 或者将移动头纸牌的点数不是13的纸牌放置到空列, 则移动失败), 因此需要保存移动的起始列(
self.col_from
) - 为了在
button1Motion()
函数中, 纸牌能够跟着鼠标移动, 需要保存移动纸牌与鼠标的相对位置(self.offset_x
,self.offset_y
)
def selectCheck(self, col: int, row: int):
card_queue = self.card_queues[col]
if card_queue[row].isfront == False:
return False
for i in range(row+1, len(card_queue)):
if card_queue[i-1].points - card_queue[i].points != 1:
return False
return True
....
def buttone1Press(self, event):
"""
鼠标左键按下事件.
判断选中的卡片. 或者发牌.
"""
# 发牌
if event.x >= HEAP_OS_H and event.y >= HEAP_OS_V:
self.dealCard()
col = (event.x - COL_PAD) // COL_WIDTH
row = event.y // ROW_WIDTH
self.offset_x = event.x - col * COL_WIDTH + COL_PAD
self.offset_y = event.y - row * ROW_WIDTH
queue_len = len(self.card_queues[col])
if row >= queue_len:
if event.y < (queue_len - 1) * ROW_WIDTH + CARD_HEIGHT:
row = queue_len - 1
else:
return
if self.selectCheck(col, row) == False:
self.select_cards = []
return
self.select_cards = self.card_queues[col][row:]
for card in self.select_cards:
card: Card
self.canvas.lift(card.itemId)
self.col_from = col
del self.card_queues[col][row:]
button1Motion()
button1Motion()
函数的功能很简单, 只要调用self.canvas.coords()
修改所有要移动的纸牌(self.select_cards
)的坐标值即可
def button1Motion(self, event):
col = event.x // COL_WIDTH
for row, card in enumerate(self.select_cards):
self.canvas.coords(card.itemId, event.x - self.offset_x,
event.y - self.offset_y + row * ROW_WIDTH)
button1Release()
button1Release()
函数是纸牌移动的最终过程, 需要判断移动是否符合条件;- 如果移动成功, 将移动的纸牌添加到该列纸牌的末尾, 并翻转移动纸牌的起始列的最上面的一张牌;
- 如果移动失败, 将移动的纸牌归还给起始列;
- 同时, 需要判断是否构成收牌条件(从"K"到"A"依次递减), 如果符合条件, 需要删除响应的纸牌, 修改分数.
def button1Release(self, event):
if len(self.select_cards) == 0:
return
col = event.x // COL_WIDTH
if self.moveCheck(col) == False:
col = self.col_from
for row, card in enumerate(self.select_cards, len(self.card_queues[col])):
self.canvas.coords(card.itemId, col * COL_WIDTH + COL_PAD,
row * ROW_WIDTH)
self.card_queues[col].extend(self.select_cards)
self.select_cards.clear()
self.moves += 1
self.score -= 1
self.canvas.itemconfig(self.text_item,
text=f'分数: {self.score}\n操作: {self.moves}')
ok = self.tryTakeup(col)
if ok:
self.flopTop(col)
if col != self.col_from:
# 翻转最后一张牌
if len(self.card_queues[self.col_from]) > 0:
self.flopTop(self.col_from)
全部代码请从我的资源下载, 该资源为付费资源(!!创作不易, 敬请谅解!!)