Python开源项目之蜘蛛纸牌

概述

使用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)

全部代码请从我的资源下载, 该资源为付费资源(!!创作不易, 敬请谅解!!)

  • 2
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

falwat

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值