用python写一个有AI的斗地主游戏(三)——简述前端设计和实现

源码请看我的Github页面
这是我一个课程的学术项目,请不要抄袭,引用时请注明出处。
本专栏系列旨在帮助小白从零开始开发一个项目,同时分享自己写代码时的感想。
请大佬们为我的拙见留情,有不规范之处烦请多多包涵!

开场白

本专栏上一篇博客里介绍了游戏后端/游戏引擎的实现方法。本篇博客讲简要介绍python游戏开发中前端设计和实现的方法。当然,以下内容还是博主自己琢磨的,有遗漏或不足之处请指教!

设计理念

和人一样, 一个游戏受欢迎与否有两个主要因素:一个就是它的灵魂(后端),另一个就是它的脸(前端)。前端对于游戏可玩性、美感、可宣传性、市场竞争力等方面有着重大作用。对于斗地主游戏来说,它的游戏玩法经过历史与文化的堆积,已经非常完美。前端的作用就是要以特别的方式展现游戏内部的价值观。于是,作为一个学术项目,我把我敬爱的教授和热爱的学校放到了游戏里,将斗地主变成了外国友人和苦闷的大学生都易于共情的“斗教授”,并以此为主题设计了游戏的前端。
除了保存基本的游戏玩法外,博主对游戏内装饰性元素如背景和玩家头像进行了基本的更换(换成了教授的头和校徽),把地主改成了教授,把农民换成了学生,等等,以向教授展现学生们在学业压力下的反抗和奋斗精神。

不扯没用的了,下面介绍下tkinterpygame在该项目的一些基础用法。

实现方法

上篇博客讲到,后端已经为游戏逻辑提供了工具,前端负责用这些工具展示一个完整的游戏。博主的单人模式前端代码放在single.pysingleGUI类里。我们需要以下功能:

'''
singleGUI Class: 用来创建、维护、更新游戏窗口并从其中获得并处理输入的类
    __init__: 用Game对象初始化singleGUI类
    confirmIdentity: 叫地主(和按钮对象绑定)
    passIdentity: 不叫地主(和按钮对象绑定)
    selectCard: 选择手牌(和卡牌对象绑定)
    deselectCard: 取消选择手牌(和卡牌对象绑定)
    confirmCard: 确认出牌(和按钮对象绑定)
    passCard: 过牌(和按钮对象绑定)
    updateScreen: 用Game对象更新游戏界面内容
    initGUI: 初始化pygame相关对象和Game对象
    run: 类似于tkinter里的mainloop,以循环的方式运行游戏
我们还可以用一个if __name__ == "__main__" 来在运行该文件时运行程序
'''

single.py

代码的主要逻辑是:实时根据当前游戏状态创建/更新屏幕上的可互动内容(放到一个列表里),并在主循环中遍历并调用每个对象的process函数把它们渲染到屏幕上并可以获取相关输入(即根据其初始化参数和用户操作进行动态更新,后面会介绍这些对象是怎么写的)。代码比较长,博主就不挨个拎出来介绍了,对代码的说明和介绍写在了注释里,建议挑最感兴趣的部分阅读。以下是主要的代码:

# 这三个库用来处理图形窗口
import tkinter
import pygame
import tkinter.messagebox
# pygameWidgets库是博主自己写的一些pygame可以互动的对象,比如按钮和卡牌等,之后会详细介绍
from pygameWidgets import *
# 游戏引擎(后端)
from GameEngine import *
import time


class singleGUI:
    def __init__(self, Game): # 需要传入游戏引擎中的Game对象
        # 初始化变量
        self.name = Game.p1.name # 人类玩家的名字
        self.width = 800 # 窗口宽度
        self.height = 600 # 窗口高度
        self.fps = 20 # 帧率
        self.title = "Fight the Professor! By Eric Gao" # 游戏窗口标题
        self.bgColor = (255, 255, 255) # 窗口默认背景色
        self.bg = pygame.image.load('./imgs/bg/tartanbg.png') # 加载窗口背景图片
        self.bg = pygame.transform.scale(self.bg, (self.width, self.height)) # 预处理窗口背景图片
        # 初始化Game对象
        self.Game = Game # self.Game作为类内变量共享
        self.Game.p2 = AI(self.Game.p2.name) # 两个AI玩家
        self.Game.p3 = AI(self.Game.p3.name)
        self.Game.playerDict = {
    
    self.Game.p1.name: self.Game.p1,
                                self.Game.p2.name: self.Game.p2, self.Game.p3.name: self.Game.p3} # 用名字/id找到相应玩家对象
        self.player = self.Game.p1 # 人类玩家
        self.objs = [] # 记录窗口里的对象,比如按钮、头像、卡牌、文字等等
        self.cardDict = {
    
    } # 卡牌字典,由卡牌的值指向卡牌对象
        self.selectedCards = [] # 已选择的卡牌列表
        self.chosenLandlord = False # 是否已经叫好地主
        self.prevPlayTime = time.time() # 初始化出牌时间
        pygame.init() # 初始化pygame
        self.run() # 运行游戏

    # 叫地主(和按钮对象绑定)
    def confirmIdentity(self):
        self.Game.chooseLandlord(self.name) # 自己叫地主
        self.chosenLandlord = True # 游戏已经产生地主
        self.Game.assignPlayOrder() # 分配游玩顺序
        self.prevPlayTime = time.time() # 记录叫地主的时间
        self.updateScreen() # 更新窗口内容

    # 不叫地主(和按钮对象绑定)
    def passIdentity(self):
        self.Game.makePlay([]) # 相当于过牌
        self.prevPlayTime = time.time() # 记录不叫地主的时间
        self.updateScreen() # 更新窗口内容

    # 选择手牌(和卡牌对象绑定)
    def selectCard(self, cardVal): # 输入卡片的值
    	# 这个if是用来防止玩家再次选择已选择的卡或者选择其它的卡
        if cardVal not in self.selectedCards and cardVal in self.player.cards:
            self.selectedCards.append(cardVal) # 放到列表里

    # 取消选择手牌(和卡牌对象绑定)
    def deSelect(self, cardVal):
        if cardVal in self.selectedCards and cardVal in self.player.cards:
            self.selectedCards.remove(cardVal)

    # 确认出牌(和按钮对象绑定)
    def confirmCard(self):
    	# 这里设置了确定按钮和过牌按钮分开,所以不能不选择牌直接确认
        if self.selectedCards != [] and self.Game.isValidPlay(self.selectedCards):
            self.Game.makePlay(self.selectedCards) # Game对象内出牌
            self.selectedCards = [] # 清空已选择的牌
            mod = self.Game.checkWin() # 看游戏结没结束
            if mod == 1:  # 当前地主玩家没有手牌,地主获胜
                tkinter.Tk().wm_withdraw()  # 隐藏tkinter主窗口
                tkinter.messagebox.showinfo(
                    f'Winner is: {
      
      self.Game.prevPlayer}', 'CONGRATS PROFESSOR! KEEP OPPRESSING YOUR STUDENTS!')
                pygame.quit() # 结束游戏
            elif mod == 2:  # 当前农民玩家没有手牌,农民获胜
                tkinter.Tk().wm_withdraw()  # to hide the main window
                tkinter.messagebox.showinfo(
                    f'Winner is: {
      
      self.Game.prevPlayer}', 'CONGRATS STUDENTS! KILL MORE PROFESSORS!')
                pygame.quit()
            self.prevPlayTime = time.time()
            self.updateScreen()
        elif self.Game.prevPlay == []:  # 不能这么出,显示错误
            tkinter.Tk().wm_withdraw() # 隐藏tkinter主窗口
            tkinter.messagebox.showwarning('Warning', 'Invalid play!')

    # 空过(和按钮对象绑定)
    def passCard(self):
    	# 必须不选择牌才能空过(不是很人性化,但是解决了bug)
        if self.selectedCards == []:
            self.Game.makePlay([])
            self.prevPlayTime = time.time()
            self.updateScreen()

    # 用Game对象更新屏幕
    def updateScreen(self):
        # 清空当前所有屏幕上的对象
        self.objs.clear()
        ################ 更新手牌 ################
        for i in self.cardDict: # 删掉手牌里没有的牌
            if i not in self.player.cards:
                self.cardDict[i] = None
        xStart = 50 # 从屏幕这个位置开始放卡牌对象
        cardCnt = len(self.player.cards)
        for card in self.player.cards: # 遍历自己的卡牌
            if card not in self.cardDict: # 如果看到了新的牌,那就创建它
            	# 这里的Card是博主自己写的pygame组件对象,过后会更详细地介绍
            	# 参数分别是主屏幕对象,卡面数值,x坐标,y坐标,宽,高,被点击时调用的函数
                cardObj = Card(self.screen, card, xStart, 430, 50, 70, lambda x=card: self.selectCard(
                    x), lambda x=card: self.deSelect(x))
                self.cardDict[card] = cardObj
            else: # 如果是已经有的牌,那么只更新它的位置
                cardObj = self.cardDict[card]
                cardObj.x = xStart
            if cardObj not in self.objs: # 如果更新过的牌不在已有对象里,把它放到已有对象里
                self.objs.append(cardObj)
            xStart += 700/cardCnt # 下一张牌的x坐标
        ################ 更新上一个玩家打的牌 ################
        xStart = 230 # 从这个位置开始放牌
        cardCnt = len(self.Game.prevPlay[1]) # 上一个出牌记录牌的数量
        if cardCnt != 0: # 有牌就放牌
            for card in self.Game.prevPlay[1]:
            	# 同样的Card对象,是博主自己写的,稍后会介绍
                prevPlayObj = Card(self.screen, card, xStart, 180, 50, 70)
                self.objs.append(prevPlayObj)
                xStart += 350/cardCnt
        ################ 更新展示的地主牌 ################
        if self.chosenLandlord == True: # 如果已经叫过地主,那就显示地主牌
            xStart = 320
            for card in self.Game.landLordCards:
                landlordCardObj = Card(self.screen, card, xStart, 40, 50, 70)
                self.objs.append(landlordCardObj)
                xStart += 200/3 # 三张牌平均分配到200像素宽
        ################ 更新“当前某某在出牌”文字提示 ################
        text = f"Current playing: {
      
      self.Game.currentPlayer}"
        # Text也是博主自己写的pygame组件,稍后会介绍
        # 需要参数分别是:显示到的窗口,文本内容,x坐标,y坐标,宽,高
        currentPlayerTextObj = Text(self.screen, text, 230, 120, 350, 50)
        self.objs.append(currentPlayerTextObj)
        ################ 更新玩家头像、名称、位置、手牌数量 ################
        myPos = self.Game.playOrder.index(self.name) # 自己的位置
        # if的三种可能性实现原理一样,看一个就好
        if myPos == 0:
        	# 创建上家的Player对象(一个有背景和颜色的名字),Player也是博主自己写的
        	# 需要参数:显示到的窗口,GameEngine里的player对象,x坐标,y坐标,宽,高,是否已叫地主
            self.prevPlayer = Player(
                self.screen, self.Game.playerDict[self.Game.playOrder[2]], 50, 70, 60, 20, self.chosenLandlord)
            self.objs.append(self.prevPlayer)
            # 创建下家的Player对象
            self.nextPlayer = Player(
                self.screen, self.Game.playerDict[self.Game.playOrder[1]], 700, 70, 60, 20, self.chosenLandlord)
            self.objs.append(self.nextPlayer)
            # 创建自己的Player对象
            self.myPlayer = Player(
                self.screen, self.Game.playerDict[self.Game.playOrder[0]], 370, 570, 60, 20, self.chosenLandlord)
            self.objs.append(self.myPlayer)
            # 创建上家的Img对象(图片头像),是博主自己写的pygame组件
            # 需要参数:显示到的屏幕,player对象,x坐标,y坐标,宽,高
            self.prevImg = Img(
                self.screen, self.Game.playerDict[self.Game.playOrder[2]], 50, 10, 60, 60)
            self.objs.append(self.prevImg)
            # 创建下家的Img对象
            self.afterImg = Img(
                self.screen, self.Game.playerDict[self.Game.playOrder[1]], 700, 10, 60, 60)
            self.objs.append(self.afterImg)
            # 创建自己的Img对象
            self.myImg = Img(
                self.screen, self.Game.playerDict[self.Game.playOrder[0]], 370, 510, 60, 60)
            self.objs.append(self.myImg)
            # 创建上家的Text对象(用来记录手牌数量的文字),也是自己写的,后面会详细介绍
            # 需要参数:显示到的屏幕,手牌数量,x坐标,y坐标,宽,高
            self.prevCardCnt = Text(
                self.screen, str(len(self.Game.playerDict[self.Game.playOrder[2]].cards)), 70, 90, 20, 20)
            self.objs.append(self.prevCardCnt)
            # 创建下家的Text对象
            self.afterCardCnt = Text(
                self.screen, str(len(self.Game.playerDict[self.Game.playOrder[1]].cards)), 720, 90, 20, 20)
            self.objs.append(self.afterCardCnt)
        elif myPos == 1: # 这个elif和下面的else类似,只是玩家对象根据顺序进行了变化
            self.prevPlayer = Player(
                self.screen, self.Game.playerDict[self.Game.playOrder[0]], 50, 70, 60, 20, self.chosenLandlord)
            self.objs.append(self.prevPlayer)
            self.nextPlayer = Player(
                self.screen, self.Game.playerDict[self.Game.playOrder[2]], 700, 70, 60, 20, self.chosenLandlord)
            self.objs.append(self.nextPlayer)
            self.myPlayer = Player(
                self.screen, self.Game.playerDict[self.Game.playOrder[1]], 370, 570, 60, 20, self.chosenLandlord)
            self.objs.append(self.myPlayer)
            self.prevImg = Img(
                self.screen, self.Game.playerDict[self.Game.playOrder[0]], 50, 10, 60, 60)
            self.objs.append(self.prevImg)
            self.afterImg = Img(
                self.screen, self.Game.playerDict[self.Game.playOrder[2]], 700, 10, 60, 60)
            self.objs.append(self.afterImg)
            self.myImg = Img(
                self.screen, self.Game.playerDict[self.Game.playOrder[1]], 370, 510, 60, 60)
            self.objs.append(self.myImg)
            self.prevCardCnt = Text(
                self.screen, str(len(self.Game.playerDict[self.Game.playOrder[0]].cards)), 70, 90, 20, 20)
            self.objs.append(self.prevCardCnt)
            self.afterCardCnt = Text(
                self.screen, str(len(self.Game.playerDict[self.Game.playOrder[2]].cards)), 720, 90, 20, 20)
            self.objs.append(self.afterCardCnt)
        else:
            self.prevPlayer = Player(
                self.screen, self.Game.playerDict[self.Game.playOrder[1]], 50, 70, 60, 20, self.chosenLandlord)
            self.objs.append(self.prevPlayer)
            self.nextPlayer = Player(
                self.screen, self.Game.playerDict[self.Game.playOrder[0]], 700, 70, 60, 20, self.chosenLandlord)
            self.objs.append(self.nextPlayer)
            self.myPlayer = Player(
                self.screen, self.Game.playerDict[self.Game.playOrder[2]], 370, 570, 60, 20, self.chosenLandlord)
            self.objs.append(self.myPlayer)
            self.prevImg = Img(
                self.screen, self.Game.playerDict[self.Game.playOrder[1]], 50, 10, 60, 60)
            self.objs.append(self.prevImg)
            self.afterImg = Img(
                self.screen, self.Game.playerDict[self.Game.playOrder[0]], 700, 10, 60, 60)
            self.objs.append(self.afterImg)
            self.myImg = Img(
                self.screen, self.Game.playerDict[self.Game.playOrder[2]], 370, 510, 60, 60)
            self.objs.append(self.myImg)
            self.prevCardCnt = Text(
                self.screen, str(len(self.Game.playerDict[self.Game.playOrder[1]].cards)), 70, 90, 20, 20)
            self.objs.append(self.prevCardCnt)
            self.afterCardCnt = Text(
                self.screen, str(len(self.Game.playerDict[self.Game.playOrder[0]].cards)), 720, 90, 20, 20)
            self.objs.append(self.afterCardCnt)
        ################ 更新按钮对象 ################
        # 如果是自己的回合:
        ##	如果没叫地主,那就显示叫地主和不叫地主的按钮
        ##	否则显示出牌或者过牌的按钮
        if self.chosenLandlord and self.Game.currentPlayer == self.name:
        	# 创建过牌的Button对象(自己写的后面会解释)
        	# 需要参数:显示到的屏幕,x坐标,y坐标,宽,高,显示文字,点击时调用的函数
            self.passCardButton = Button(
                self.screen, 100, 350, 100, 50, 'Pass turn', self.passCard)
            self.objs.append(self.passCardButton)
            # 创建确认出牌的Button对象
            self.confirmCardButton = Button(
                self.screen, 600, 350, 100, 50, 'Confirm Play', self.confirmCard)
            self.objs.append(self.confirmCardButton)
        elif not self.chosenLandlord and self.Game.currentPlayer == self.name:
        	# 创建不叫地主的Button对象
            self.passButton = Button(
                self.screen, 100, 350, 100, 50, 'Pass', self.passIdentity)
            self.objs.append(self.passButton)
            # 创建叫地主的Button对象
            self.confirmButton = Button(
                self.screen, 600, 350, 100, 50, 'Be Professor', self.confirmIdentity)
            self.objs.append(self.confirmButton)

    # 初始化游戏GUI
    def initGUI(self):
        # 设置一些基础变量
        self.clock = pygame.time.Clock() # pygame的计时器
        self.prevPlayTime = time.time() # 上一次出牌的时间
        self.screen = pygame.display.set_mode((self.width, self.height)) # pygame屏幕对象
        self.screen.fill(self.bgColor) # 给屏幕填充初始颜色
        pygame.display.set_caption(self.title) # 给pygame窗口添加标题
        # 初始化Game对象
        self.Game.assignPlayOrder() # 分配叫地主顺序
        self.Game.shuffleDeck() # 创建牌堆并洗牌
        self.Game.dealCard() # 发牌
        # 第一次更新屏幕
        self.updateScreen()

    # 包含主循环的函数
    def run(self):
        self.initGUI() # 初始化游戏GUI
        playing = True # 表示正在游玩的状态
        while playing:
        	# 在每一帧上覆盖前一帧的内容
            self.screen.fill(self.bgColor) # 先用颜色覆盖
            self.screen.blit(self.bg, (0, 0)) # 再用背景图覆盖
            self.clock.tick(self.fps) # 按照帧率跑这个循环(比如每秒这个循环跑20次)
            self.updateScreen() # 每一个循环里要更新屏幕内容
            if time.time() - self.prevPlayTime > 3: # 控制AI出牌,在上一步操作三秒后AI行动
                if self.Game.currentPlayer == 'AI1':
                    if self.chosenLandlord: # AI出牌
                        self.Game.AIMakePlay('AI1', self.chosenLandlord)
                    else: # AI叫地主
                        self.Game.AIMakePlay('AI1', self.chosenLandlord)
                        self.chosenLandlord = True
                    self.prevPlayTime = time.time()
                elif self.Game.currentPlayer == 'AI2':
                    if self.chosenLandlord:
                        self.Game.AIMakePlay('AI2', self.chosenLandlord)
                    else:
                        self.Game.AIMakePlay('AI2', self.chosenLandlord)
                        self.chosenLandlord = True
                    self.prevPlayTime = time.time()
            # 处理用户输入
            for obj in self.objs:
                obj.process() # 每个博主自己写的pygame组件都有一个process函数,调用时可以更新显示并获得用户互动
            for event in pygame.event.get():
                if event.type in [pygame.MOUSEBUTTONDOWN, pygame.MOUSEBUTTONUP]:
                    for obj in self.objs: # 在用户按下和抬起鼠标的时候额外处理一次
                        obj.process()
                if event.type == pygame.QUIT: # 用户退出游戏时结束主循环
                    playing = False
            for obj in self.objs: # 这里多写了一遍为了避免用户一次点击时经过了多个帧出现bug
                obj.process()
            pygame.display.flip() # 把之前生成好的新内容更新到屏幕上
        pygame.quit() # 循环结束后退出游戏
        exit()

以上就是前端代码的主要部分了。
可以在前端代码中加入以下部分来在运行代码时运行程序:

if __name__ == "__main__":
    wnd = tkinter.Tk() # 创建tkinter窗口
    wnd.geometry("800x600") # 设置窗口大小
    wnd.title("Fight the Professor!") # 设置窗口标题
    wnd.resizable(0, 0) # 设置窗口大小为不可更改
    game = Game('human', 'AI1', 'AI2') # 创建Game对象
    singleGUIObj = singleGUI(game) # 创建pygameGUI对象
    wnd.mainloop() # tkinter的主循环

pygameWidgets.py

刚才还有提到过很多次博主自己写的pygame组件,放到了pygameWidgets.py里。我们有以下组件:

'''
pygameWidgets.py描述
Button Class: 用来创建可互动按钮的类
    __init__: 需要:显示到的屏幕, x和y坐标, 宽和高, 按钮文字, 被点击时调用的函数, 是否按钮只能被点一次(这个功能没用到)
    process: 在屏幕上渲染按钮, 检测用户点击, 被点击后调用函数
Card Class: 用来创建可选中/取消选中卡牌的类
    __init__: 需要:显示到的屏幕, x和y坐标, 宽和高, 被选择时调用的函数, 被取消选择(点击第二次)时调用的函数
    process: 在屏幕上渲染卡牌, 检测用户点击, 被选择/取消选择后调用函数
Player Class: 创建可以区分地主和农民身份玩家的类
    __init__: 需要:显示到的屏幕, player对象, x和y坐标, 宽和高, 是否已经叫地主
    process: 根据player对象信息把玩家名字渲染到屏幕上
Text Class: 用来在屏幕上添加文字的类
    __init__: 需要:显示到的屏幕, 要显示的文字, x和y坐标, 宽和高
    process: 把文字渲染到屏幕上
Img Class: 用来在屏幕上显示图片/玩家头像的类
    __init__: 需要:显示到的屏幕, player对象, x和y坐标, 宽和高
    process: 根据玩家身份把头像渲染到屏幕上
'''

以下是pygameWidgets.py代码的主要部分:

import pygame


# 按钮类
class Button():
    def __init__(self, screen, x, y, width, height, buttonText='Button', onclickFunction=lambda: print("Not assigned function"), onePress=False):
    	# 上面的lambda是作为默认函数使用的
    	# 初始化一些变量
        self.screen = screen
        self.x = x
        self.y = y
        self.width = width
        self.height = height
        self.onclickFunction = onclickFunction
        self.onePress = onePress
        self.alreadyPressed = False
        font = pygame.font.SysFont('Arial', 16)

        self.fillColors = {
    
    
            'normal': '#ffffff',
            'hover': '#666666',
            'pressed': '#333333',
        }
        self.buttonSurface = pygame.Surface((self.width, self.height)) # 按钮的Surface
        self.buttonRect = pygame.Rect(self.x, self.y, self.width, self.height) # 按钮的Rect
        self.buttonSurf = font.render(buttonText, True, (20, 20, 20)) # 按钮文字

    def process(self):
        mousePos = pygame.mouse.get_pos() # 获取用户当前鼠标位置
        self.buttonSurface.fill(self.fillColors['normal']) # 填充为“未点击”颜色
        if self.buttonRect.collidepoint(mousePos): # 如果用户鼠标和按钮Rect碰撞
            self.buttonSurface.fill(self.fillColors['hover']) # 填充为“悬停”颜色
            if pygame.mouse.get_pressed(num_buttons=3)[0]: # 如果鼠标左键被点击
                self.buttonSurface.fill(self.fillColors['pressed']) # 填充为“已点击”颜色
                if self.onePress: # 如果只能按一次(这个if没用到)
                    self.onclickFunction() # 那就调用函数一次
                elif not self.alreadyPressed: # 如果没有已经点过
                    self.onclickFunction() # 调用函数一次
                    self.alreadyPressed = True # 已经点击过了(没用到)
            else: # 如果没被点击
                self.alreadyPressed = False # 那就记录没点击过(没用到)
        # 把按钮文字渲染到按钮Surface上
        self.buttonSurface.blit(self.buttonSurf, [
            self.buttonRect.width/2 - self.buttonSurf.get_rect().width/2,
            self.buttonRect.height/2 - self.buttonSurf.get_rect().height/2
        ])
        # 把按钮Surface渲染到屏幕上按钮Rect的位置
        self.screen.blit(self.buttonSurface, self.buttonRect)
        # 在按钮Rect周围画黑色实心边界线
        pygame.draw.rect(self.screen, (0, 0, 0), self.buttonRect, 2)

# 下面几个类非常类似,只会在有区别的地方进行注释说明
# 卡牌类
class Card:
    def __init__(self, screen, cardType, x, y, width, height, onclickFunction=lambda: print("Not assigned function"), cancelFuction=lambda: print("Not assigned function")):
        self.screen = screen
        self.cardType = cardType
        self.x = x
        self.y = y
        self.width = width
        self.height = height
        self.onclickFunction = onclickFunction
        self.cancelFunction = cancelFuction
        self.pressedCnt = 0 # 点击计数
        self.newY = {
    
    
            'normal': self.y,
            'hover': self.y-3,
            'pressed': self.y-5,
        } # 未选中、悬停、选中的y坐标进行上下移动
        self.cardImg = pygame.image.load(f"./imgs/pokers/{
      
      self.cardType}.png") # 加载卡片图片
        self.cardImg = pygame.transform.scale(
            self.cardImg, (self.width, self.height)) # 改变图片大小
        self.cardImg.convert() # 处理图片格式
        self.cardSurface = pygame.Surface((self.width, self.height)) # 卡牌Surface
        self.cardRect = pygame.Rect(self.x, self.y, self.width, self.height) # 卡牌Rect

    def process(self):
        condition = 'normal' # 卡牌目前未选择
        mousePos = pygame.mouse.get_pos()
        if self.cardRect.collidepoint(mousePos):
            condition = 'hover' # 悬停状态
            if pygame.mouse.get_pressed(num_buttons=3)[0]:
                self.pressedCnt = (self.pressedCnt+1) % 2 # 一个小窍门,让计数在0-1-0-1这样循环
                if self.pressedCnt == 1: # 如果从未选择变成已选择,调用选择时的函数
                    self.onclickFunction()
                else: # 否则调用取消选择时的函数
                    self.cancelFunction()
        if self.pressedCnt == 1: # 卡牌已经选择
            condition = 'pressed'
        else: # 卡牌未被选择
            condition = 'normal'
        self.cardRect = pygame.Rect(
            self.x, self.newY[condition], self.width, self.height) # 卡牌Rect
        # 把卡牌图片渲染到卡牌Surface上
        self.cardSurface.blit(self.cardImg, [
            self.cardRect.width/2 - self.cardSurface.get_rect().width/2,
            self.cardRect.height/2 - self.cardSurface.get_rect().height/2
        ])
        # 把卡牌Surface渲染到屏幕上的卡牌Rect位置
        self.screen.blit(self.cardSurface, self.cardRect)
        # 在卡牌周围画边框
        pygame.draw.rect(self.screen, (0, 0, 0), self.cardRect, 2)


# 玩家(名称)类
class Player:
    def __init__(self, screen, playerObj, x, y, width, height, assignedIdentity=False):
        self.player = playerObj
        self.screen = screen
        self.x = x
        self.y = y
        self.width = width
        self.height = height
        font = pygame.font.SysFont('Arial', 18)
        self.playerSurface = pygame.Surface((self.width, self.height))
        self.playerSurface.fill((225, 225, 225))
        self.playerRect = pygame.Rect(self.x, self.y, self.width, self.height)
        if assignedIdentity: # 根据是否选过地主对名字颜色进行更改
            if self.player.identity == 's':
                self.playerSurf = font.render(
                    self.player.name, True, (81, 4, 0))
            else:
                self.playerSurf = font.render(
                    self.player.name, True, (2, 7, 93))
        else:
            self.playerSurf = font.render(self.player.name, True, (80, 80, 80))

    def process(self):
        self.playerSurface.blit(self.playerSurf, [
            self.playerRect.width/2 - self.playerSurf.get_rect().width/2,
            self.playerRect.height/2 - self.playerSurf.get_rect().height/2
        ])
        self.screen.blit(self.playerSurface, self.playerRect)


# 文字类
class Text:
    def __init__(self, screen, text, x, y, width, height):
        self.text = text
        self.screen = screen
        self.x = x
        self.y = y
        self.width = width
        self.height = height
        font = pygame.font.SysFont('Arial', 18)
        self.textSurface = pygame.Surface((self.width, self.height))
        self.textRect = pygame.Rect(self.x, self.y, self.width, self.height)
        self.textSurf = font.render(self.text, True, (255, 255, 255))

    def process(self):
        self.textSurface.blit(self.textSurf, [
            self.textRect.width/2 - self.textSurf.get_rect().width/2,
            self.textRect.height/2 - self.textSurf.get_rect().height/2
        ])
        self.screen.blit(self.textSurface, self.textRect)


# 图片/玩家头像类
class Img:
    def __init__(self, screen, player, x, y, width, height):
        self.screen = screen
        self.x = x
        self.y = y
        self.width = width
        self.height = height
        self.identity = player.identity
        if self.identity == 'p': # 我敬爱的教授的图片(代表地主/教授)
            self.image = pygame.image.load(f"./imgs/professors/saquib.jpg")
        else: # 校徽图片(代表农民/学生)
            self.image = pygame.image.load(f"./imgs/students/tartan.png")
        self.image = pygame.transform.scale(
            self.image, (self.width, self.height))
        self.image.convert()
        self.imgSurface = pygame.Surface((self.width, self.height))
        self.imgRect = pygame.Rect(self.x, self.y, self.width, self.height)

    def process(self):
        self.imgSurface.blit(self.image, [
            self.imgRect.width/2 - self.imgSurface.get_rect().width/2,
            self.imgRect.height/2 - self.imgSurface.get_rect().height/2
        ])
        self.screen.blit(self.imgSurface, self.imgRect)

到这里,游戏的前端部分基本上就完成啦!

结束语

博主没学过设计,游戏界面可能比较丑陋,但是已经按照强迫症标准努力进行了左右对称。
pygame也有一些内置的组件(比如Spire这种功能比较齐全的)。同时,斗地主游戏好像也能只用tkinter实现,因为主要用到的功能基本只有按钮、图片这种tkinter里很强大的部分。pygame强就强在做有镜头感的冒险类游戏(比如上帝视角射击游戏,冰与火之歌游戏等等)。市面上还有很多其它很强大的游戏库,比如Ursina这种能做出很华丽3D游戏的引擎。感兴趣的小伙伴可以去了解一下。
不出意外的话本博客就是该系列的最后一篇博客了,希望对各位有帮助!有各种问题和见解欢迎评论或者私信!

猜你喜欢

转载自blog.csdn.net/EricFrenzy/article/details/128132574