用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__" 来在运行该文件时运行程序
'''

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


import tkinter
import pygame
import tkinter.messagebox

from pygameWidgets import *

from GameEngine import *
import time

class singleGUI:
    def __init__(self, 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))

        self.Game = Game
        self.Game.p2 = AI(self.Game.p2.name)
        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}
        self.player = self.Game.p1
        self.objs = []
        self.cardDict = {}
        self.selectedCards = []
        self.chosenLandlord = False
        self.prevPlayTime = time.time()
        pygame.init()
        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 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)
            self.selectedCards = []
            mod = self.Game.checkWin()
            if mod == 1:
                tkinter.Tk().wm_withdraw()
                tkinter.messagebox.showinfo(
                    f'Winner is: {self.Game.prevPlayer}', 'CONGRATS PROFESSOR! KEEP OPPRESSING YOUR STUDENTS!')
                pygame.quit()
            elif mod == 2:
                tkinter.Tk().wm_withdraw()
                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.messagebox.showwarning('Warning', 'Invalid play!')

    def passCard(self):

        if self.selectedCards == []:
            self.Game.makePlay([])
            self.prevPlayTime = time.time()
            self.updateScreen()

    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:

                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

        xStart = 230
        cardCnt = len(self.Game.prevPlay[1])
        if cardCnt != 0:
            for card in self.Game.prevPlay[1]:

                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

        text = f"Current playing: {self.Game.currentPlayer}"

        currentPlayerTextObj = Text(self.screen, text, 230, 120, 350, 50)
        self.objs.append(currentPlayerTextObj)

        myPos = self.Game.playOrder.index(self.name)

        if myPos == 0:

            self.prevPlayer = Player(
                self.screen, self.Game.playerDict[self.Game.playOrder[2]], 50, 70, 60, 20, self.chosenLandlord)
            self.objs.append(self.prevPlayer)

            self.nextPlayer = Player(
                self.screen, self.Game.playerDict[self.Game.playOrder[1]], 700, 70, 60, 20, self.chosenLandlord)
            self.objs.append(self.nextPlayer)

            self.myPlayer = Player(
                self.screen, self.Game.playerDict[self.Game.playOrder[0]], 370, 570, 60, 20, self.chosenLandlord)
            self.objs.append(self.myPlayer)

            self.prevImg = Img(
                self.screen, self.Game.playerDict[self.Game.playOrder[2]], 50, 10, 60, 60)
            self.objs.append(self.prevImg)

            self.afterImg = Img(
                self.screen, self.Game.playerDict[self.Game.playOrder[1]], 700, 10, 60, 60)
            self.objs.append(self.afterImg)

            self.myImg = Img(
                self.screen, self.Game.playerDict[self.Game.playOrder[0]], 370, 510, 60, 60)
            self.objs.append(self.myImg)

            self.prevCardCnt = Text(
                self.screen, str(len(self.Game.playerDict[self.Game.playOrder[2]].cards)), 70, 90, 20, 20)
            self.objs.append(self.prevCardCnt)

            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:
            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:

            self.passCardButton = Button(
                self.screen, 100, 350, 100, 50, 'Pass turn', self.passCard)
            self.objs.append(self.passCardButton)

            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:

            self.passButton = Button(
                self.screen, 100, 350, 100, 50, 'Pass', self.passIdentity)
            self.objs.append(self.passButton)

            self.confirmButton = Button(
                self.screen, 600, 350, 100, 50, 'Be Professor', self.confirmIdentity)
            self.objs.append(self.confirmButton)

    def initGUI(self):

        self.clock = pygame.time.Clock()
        self.prevPlayTime = time.time()
        self.screen = pygame.display.set_mode((self.width, self.height))
        self.screen.fill(self.bgColor)
        pygame.display.set_caption(self.title)

        self.Game.assignPlayOrder()
        self.Game.shuffleDeck()
        self.Game.dealCard()

        self.updateScreen()

    def run(self):
        self.initGUI()
        playing = True
        while playing:

            self.screen.fill(self.bgColor)
            self.screen.blit(self.bg, (0, 0))
            self.clock.tick(self.fps)
            self.updateScreen()
            if time.time() - self.prevPlayTime > 3:
                if self.Game.currentPlayer == 'AI1':
                    if self.chosenLandlord:
                        self.Game.AIMakePlay('AI1', self.chosenLandlord)
                    else:
                        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()
            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:
                obj.process()
            pygame.display.flip()
        pygame.quit()
        exit()

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

if __name__ == "__main__":
    wnd = tkinter.Tk()
    wnd.geometry("800x600")
    wnd.title("Fight the Professor!")
    wnd.resizable(0, 0)
    game = Game('human', 'AI1', 'AI2')
    singleGUIObj = singleGUI(game)
    wnd.mainloop()

刚才还有提到过很多次博主自己写的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):

        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))
        self.buttonRect = pygame.Rect(self.x, self.y, self.width, self.height)
        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):
            self.buttonSurface.fill(self.fillColors['hover'])
            if pygame.mouse.get_pressed(num_buttons=3)[0]:
                self.buttonSurface.fill(self.fillColors['pressed'])
                if self.onePress:
                    self.onclickFunction()
                elif not self.alreadyPressed:
                    self.onclickFunction()
                    self.alreadyPressed = True
            else:
                self.alreadyPressed = False

        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
        ])

        self.screen.blit(self.buttonSurface, self.buttonRect)

        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,
        }
        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))
        self.cardRect = pygame.Rect(self.x, self.y, self.width, self.height)

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

        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
        ])

        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游戏的引擎。感兴趣的小伙伴可以去了解一下。
不出意外的话本博客就是该系列的最后一篇博客了,希望对各位有帮助!有各种问题和见解欢迎评论或者私信!

Original: https://blog.csdn.net/EricFrenzy/article/details/128132574
Author: EricFrenzy
Title: 用python写一个有AI的斗地主游戏(三)——简述前端设计和实现

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/783554/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球