基于Numpy的单人扑克游戏:地城恶棍

[!] 本项目为原创内容,若有错误之处还需批评指正

由于篇幅过长,本文全代码文件位于另一个博客中:传送门


文章目录

  • 基于Numpy的单人扑克游戏:地城恶棍
    • 1.单人扑克牌游戏:地城恶棍
      • 1.1 游戏场景
      • 1.2 游戏规则
      • 1.3 计分规则
      • 1.4 游戏流程
    • 2.制作思路
    • 3. 扑克牌API
      • 3.1 类成员
      • 3.2 索引转换与检索
      • 3.3 洗牌与重置牌库
      • 3.4 取卡牌、插卡牌与丢弃卡牌
      • 3.5 卡牌可视化
      • 3.6 具体展示
    • 4. 游戏本体
      • 4.1 配置文件
      • 4.2 游戏主体
      • 4.3 游戏主循环
      • 4.4 游戏结束与计分
      • 4.5 刷新房间与替换房间
      • 4.6 游戏界面展示
      • 4.7 玩家输入检测
      • 4.8 执行动作
    • 5 游戏展示
      • 5.1 游戏代码

1.单人扑克牌游戏:地城恶棍

  • 翻译自BGG上的扑克游戏,通过一副标准的扑克牌构造一个地下城进行游戏,工具易得,规则简单,携带方便,也有一些RPG要素。(教程来源:传送门)

1.1 游戏场景

  • 取出所有小丑,红色的J,Q,K,A。将它们放在一边,它们不会在这个游戏中使用。

  • 将剩余的卡片洗牌并将正面朝下放在左侧。这个牌堆被称为地下城。


    [借用原链接教程的图,侵删]

1.2 游戏规则

  • 牌堆里的26个草花和黑桃,这些是怪物。他们的伤害等于他们的牌面数字。 (其中10是10,J是11,Q是12,K是13,A是14)
  • 牌堆上的9张方片是武器。每种武器都会造成与对应数字的伤害。 恶棍只能装备一个武器,这意味着如果你选择一个,你必须装备它,并丢弃你以前的武器。
  • 牌堆上的9张红桃是生命药水。每回合你只能使用一个生命药水,即使你拿到两个。你拿的第二个药水被丢弃(放回弃牌堆)。并且你的生命值无法超过20.
  • 您可以在任何地方放置你的弃牌(任何丢弃的牌),但我建议在地城右侧。卡片要面朝下丢弃。
  • 当你的生命达到0或者你穿过整个地牢时,游戏结束。

1.3 计分规则

  • 如果你的生命已经达到零,找到地牢中所有剩余的怪物,并从你的生命中减去它们的值,这个负值就是你的分数。

  • 如果你已经穿过整个地牢,你的分数就应该是你的正值,你的最后一张牌如果是红桃,那就是你最后的财宝,那么你的分数就是你的生命值+财宝价值。

1.4 游戏流程

  • 在你的第一个和之后的每个回合中,一个接一个地翻转卡片顶部的卡片,直到你面前有4张卡片面朝上形成一个房间。

  • 如果您愿意,您可以避开这个房间。如果您选择这样做,请在一次性拿走所有四张卡片,把它们放在地下城的底部。虽然您避免房间的次数不受限制,但您无法避开连续两个房间。

  • 如果您选择选择进入,则必须面对其中包含的四张卡中的3张。

  • 如果选择武器则必须装备它。将它正面朝上放在您和剩余的房卡之间。如果你装备了先前的武器,将它和它上面的任何怪物移动到弃牌牌堆上。

  • 如果选择生命药水则将其增加相应的数目的血量,然后将其放至弃牌堆。您的生命值不得超过20岁,您可能每回合不止一个生命药水。如果你在一个回合中服用两个生命药水,那么第二个就被丢弃,并且不会增加血量。

  • 如果你选择了怪物则可以用徒手或使用装备好的武器进行战斗。一旦你选择了3张牌(只剩下一张牌),轮到你了。将第四张卡面朝上放在您面前,作为下一个房间的一部分。

    • 如果你徒手与怪物战斗,从你的生命中减去它的攻击力,并将怪物移到丢弃牌组。

    • 如果你选择使用装备好的武器对抗怪兽,将怪物面朝上放在武器上方(同时也放在武器上任何其他怪物的顶部。确保错开怪物的位置,不会盖住武器的数字,类似于红心接龙的放置方式)

    • 如果武器的攻击力大于怪物攻击力,怪物被击败且你不会受伤;

    • 如果怪物的攻击力大于你武器的攻击力,你的血量要减去两者的差值;

    • 重要的是要注意,虽然你保留你的武器,直到他们被更换,一旦武器被用于怪物,武器只能用于干掉以前的怪物更低价值(小于等于)的怪物。


2.制作思路

  • 既然是一个扑克牌游戏,首先肯定是要制作一个扑克牌的API

    • 扑克牌可以用字典或者列表来表示,但是这样的话会浪费很多的空间。因此,本项目将使用Numpy来实现,这样的话能保证性能的情况下减少存储成本。
    • 一般情况下,扑克牌应该具有的功能为: 抽牌库顶的牌、洗牌、将牌放进牌库底、丢弃某张牌
  • 其他情况的话,根据游戏规则制作就OKK了
  • 本次制作在Jupyter Notebook中制作,所需要的库如下所示:
    import random as ra
    import numpy as np
    import os
    import re
    import time
    from IPython.display import clear_output #用于清空输出
    

3. 扑克牌API

  • 扑克牌API可以在本人的另一个博客中找到:传送门

3.1 类成员

  • 类成员的话比较简单,主要存储的是牌库以及他们的特殊字符。
  • 这里将特殊字符作为静态成员变量,牌库作为公共成员变量
    class PlayingCard(object):#\033[31m是颜色标识符,让字体变成红色的prefixs = ['\033[31m♦\033[0m','♣','\033[31m♥\033[0m','♠']suffixs = [str(i) for i in range(1,11)] + ['J','Q','K']specials = ["♚","\033[31m♚\033[0m"]def __init__(self) -> None:"""构建牌库[x,y] 获取公式x = 54 // 4y = 54 % 4x \in [0,13],其中0~9代表数值1~10, 10/11/12代表J/Q/K, 13代表特殊牌(即王牌)y \in [0,3],其中0代表方块,1代表梅花,2代表桃心,3代表黑桃[13,0] 小王; [13,1] 大王"""self.library = np.linspace(1,54,54).astype(np.int8) #一个一维向量作为牌库
    

3.2 索引转换与检索

  • 由于我们使用的是一个一维向量来进行存储,因此我们需要计算出一维向量和扑克牌之间的映射关系

    def card2index(self,index) -> tuple:#索引与卡牌的关系assert not int((index-27)/27), "[WARN] Card %d is illegal!" % indexindex -= 1x,y = index//4,index % 4return (x,y)def card2str(self,index) -> str:#将索引转化为文本assert not int((index-27)/27), "[WARN] Card %d is illegal!" % indexx,y = self.card2index(index)return self.prefixs[y] + self.suffixs[x]def getPrefix(self,index) -> int:"""得到扑克牌花色其中0代表方块,1代表梅花,2代表桃心,3代表黑桃"""assert not int((index-27)/27), "[WARN] Card %d is illegal!" % indexreturn (index - 1) % 4def getNumber(self,index) -> int:#得到扑克牌数字assert not int((index-27)/27), "[WARN] Card %d is illegal!" % indexreturn (index - 1) //4
    

3.3 洗牌与重置牌库

  • 由于牌库是一维数组,因此可以直接使用一维数组打乱的方式实现洗牌
  • 对于重置牌库,则可以暴力的重新构造一个有序的一维向量
    def shuffle(self,seed = None) -> np.array:#洗牌if seed is None:seed = ra.randint(0,2^16)ra.seed(seed)ra.shuffle(self.library)return self.librarydef reload(self) -> np.array:#获得一副新牌self.library = np.linspace(1,54,54).astype(np.int8)return self.library
    
  • 注意到这里洗牌方法使用了seed参数,这样可以让用户自己设定随机种子来保证多次洗牌的结果是相同的

3.4 取卡牌、插卡牌与丢弃卡牌

  • 在很多个扑克游戏中,都喜欢在牌库顶抽走卡牌,在牌库底加入卡牌。因此设置这两个方法是有必要的。
  • 需要注意的是,在牌库中随机位置抽卡和随机位置加入卡牌可以通过“取卡/插卡 + 打乱”的组合来实现,因此不需要额外设计。
  • 丢弃卡片则是需要在牌库中丢弃掉某几张不需要的卡,这一点适用于某些规则之中。
  • 由于这几个操作需要判断卡牌是否在牌库中,所以特意加入了一个私有方法来判断卡牌是否在牌库里
    def _inLibrary(self,index) -> bool:#判断是否在卡里?return np.where(self.library == index)[0].size > 0def __len__(self):#重写len方法,即 len() 函数会返回牌库中剩余卡片数return len(self.library)def pop(self) -> int:#取出牌库顶牌if len(self) == 0:return 0pop = self.library[0]self.library = self.library[1:]return popdef insert(self,index:int) -> np.array:#在牌库底插入牌assert not int((index-27)/27), "[WARN] Card %d is illegal!" % indexassert not self._inLibrary(index), "[WARN] Card %d is already in library!" % indexself.library = np.concatenate([self.library,np.array([index])])return self.librarydef drop(self,indexList) -> np.array:#丢弃某张牌for index in indexList if type(indexList) == list else [indexList]:assert self._inLibrary(index), "[WARN] Card %d is not in library!" % indexself.library = np.delete(self.library,np.where(self.library == index))return self.library
  • 由于是一维数组的缘故,因此用户在使用的时候需要先将卡牌转化为索引,随后再输入到drop方法中

3.5 卡牌可视化

  • 由于这个API是用一维数组实现的,因此很难直观地了解到里面到底有什么牌

  • 所以,特意设计了这么一个可视化的方法来进行查看

  • 这个方法通过重写__str__方法来实现,因此可以直接通过对实例使用print()来查看

    def __str__(self):string = "Card Summary: %d\n" % len(self)for x in range(13): #用于显示每一行(本来可以继续压缩的,但是怕可读性不行)string += "\t".join([self.prefixs[y] + self.suffixs[x] for y in range(4) if self._inLibrary(4 * x + y + 1)]) + "\n"string += "\t".join([self.specials[i] for i in [0,1] if self._inLibrary(53 + i)])return string

3.6 具体展示

  • 以下是一个简单的展示环节

    card = PlayingCard() #构建牌库
    print(card) #展示牌库card.shuffle(seed = 42) #随机打乱#抽出牌库顶前18张牌
    for i in range(18):card.pop()
    print(card)
    


4. 游戏本体

4.1 配置文件

  • 作为一个游戏,首先就需要有一个配置文件

    class Config(object):def __init__(self,maxHealth) -> None:self.maxHealth = maxHealth  #最大生命值self.health = maxHealth #现在的生命值self.weapon = 0  #武器self.finalKill = 0 #最后击杀的怪物def configReload(self):#刷新配置,用于重置游戏self.health = self.maxHealth self.weapon = 0self.finalKill = 0print("[INFO] Player Health: %d/%d" % (self.health,self.maxHealth))
    

4.2 游戏主体

  • 构建游戏的主体,用来寄存游戏中出现的变量。
  • 该游戏中设置了保存路径,用于保存每次游戏的得分
  • 该类中内置了reload()方法,来快速重置游戏
    class Game(Config):def __init__(self,maxHealth:int = 20,savePath = './checkpoint',seed = None) -> None:super().__init__(maxHealth)self.card = PlayingCard()self.room = np.zeros(4,dtype = np.int8) #游戏中的地牢房间,0代表空self._step = 0  #游戏回合self.actions = Noneself._isSwitch = False #一个回合中只能刷新一次房间self.playTime = time.strftime("%Y-%m-%d=%H-%M", time.localtime()) #游戏时间self.fileName = os.path.join(savePath,self.playTime) #保存文件的目录if not os.path.exists(savePath):os.mkdir(savePath)with open(self.fileName,'w') as file: pass #创建文件if seed is None:seed = ra.randint(0,2^16)self.reload(seed) #使用随机种子刷新房间def reload(self,seed = None) -> None:"""该游戏不需要红色J,K,Q,A以及小王大王可以设定随机种子来保证公平对战"""if seed is None:seed = ra.randint(0,2^16)self.configReload()self.card.reload()self.card.drop([(2 * i + 1) for i in [0,1,20,21,22,23,24,25]] + [53,54])self.card.shuffle(seed)print("[INFO] The game initialized successfully!")
    

4.3 游戏主循环

  • 这个游戏是一个回合制的游戏,因此可以构建一个 step() 方法,来表示每轮游戏

        def step(self) -> int:result = self._isEnd() #判断游戏是否结束if result != 0:score = self._getScore(result) #如果结束了则开始计分print("[INFO] %s Score: %d" %\("Victory!" if result == 1 else "Defeated.",score))#修改保存的名字os.rename(self.fileName,self.fileName + "=Score%d" % score)return 1 self._updateRoom() #更新房间ans = -1 #记录玩家的输入结果是否合理while(ans == -1):clear_output(wait = True) #清除屏幕print(self)     #显示游戏界面time.sleep(0.2) #等待一段时间,防止输出错位actions = input( "\n1-4: Choose card (e.g. \"1 2 4\") \n0: Switch room\n"+\"\nPlease input the number to choose your actions:")ans = self._decode(actions) #分析玩家的输入#动作0表示逃避房间if self.actions == 0: self._switchRoom()print("[INFO] You switch the room!")input("Press \"Enter\" key to continute")return 0#否则,根据玩家输入顺序来触发动作for action in self.actions:if self.health <= 0:return 0  #已经死亡,退出回合self._action(action)self.room[action - 1] = 0 #将房间情况,表示已完成time.sleep(0.2)#回合数+1,重置刷新次数并退出self._step += 1self._isSwitch = Falseinput("Press \"Enter\" key to continute")return 0
    

4.4 游戏结束与计分

  • 根据游戏规则,游戏结束计分情况如下:

    • 如果你的生命已经达到零,找到地牢中所有剩余的怪物,并从你的生命中减去它们的值,这个负值就是你的分数。
    • 如果你已经穿过整个地牢,你的分数就应该是你的正值,你的最后一张牌如果是红桃,那就是你最后的财宝,那么你的分数就是你的生命值+财宝价值。
  • 因此,可以设计出
        def _isEnd(self) -> int:"""游戏结束机制:1. 卡池只剩一张,返回12. 生命值少于等于0,返回-1"""if len(self.card) == 0: return 1if self.health <= 0: return -1return 0def _getScore(self,index) -> int:if index == 1: #卡尺只剩一张的情况index = self.card.pop()  #抽出来,判断是不是红桃prefix = self.card.getPrefix(index)suffix = self.card.getNumber(index) + 1if prefix == 2:return self.health + suffix #是红桃则作为宝藏else:self._action(index)  #否则,自动执行(可能会挂掉)return self.healthelse:  #自己生命值清空的情况score = self.healthindex = self.card.pop()while(index != 0):  #牌库中的怪物prefix = self.card.getPrefix(index)suffix = self.card.getNumber(index) + 1score -= suffix if prefix %2 == 1 else 0 #减去所有怪物的伤害index = self.card.pop()for index in self.room: #房间中的怪物if index == 0: continueprefix = self.card.getPrefix(index)suffix = self.card.getNumber(index) + 1score -= suffix if prefix %2 == 1 else 0 #减去所有怪物的伤害return score
    

4.5 刷新房间与替换房间

  • 房间由一个长度为4的一维向量构成,其中空房间由0表示,因此可以将牌的索引直接替换0来实现

        def _updateRoom(self) -> None:"""更新地牢房间:在牌库中抽牌,直至四张牌朝上形成一个房间"""for index in np.where(self.room == 0)[0]:self.room[index] = self.card.pop()def _switchRoom(self) -> None:self._isSwitch = Truefor index in self.room:self.card.insert(index)self.room = np.zeros(4,dtype = np.int8)
    

4.6 游戏界面展示

  • 游戏界面同样可以通过重构__str__方法来进行展示

        def __str__(self):string = "Step:%d\t Left: %d" % (len(self),len(self.card)) + "\n" + "=" * 40string += "\nRoom:\t" +"\t".join([self.card.card2str(index) for index in self.room])string += "\n\n\nWeapon:\t%s" % (self.card.card2str(self.weapon) if self.weapon else "None")string += " (" + self.card.card2str(self.finalKill) + ")" if self.finalKill else ""string += "\nHealth:\t%d/%d" % (self.health,self.maxHealth)return string
    
  • 可视化的结果如下:

4.7 玩家输入检测

  • 考虑到玩家输入的情况可能千变万化,因此需要一个比较好的函数来识别玩家的输入
  • 在这里使用了正则表达式来进行动作识别,并判断玩家输入是否合理
        def _decode(self,action):if not action: return -1 #如果没有输入#将字符串中的数字提取出来action = [int(index) for index in re.findall(r"\d",action)]#判断每个数字是否合理for index in action:if index == 0:if not self._isSwitch:self.actions = 0return 1else:print("[WARN] You can't switch room twice in one round!")input("Press \"Enter\" key to continute")return -1if index not in [1,2,3,4]:print("[WARN] Error action %d" % index)input("Press \"Enter\" key to continute")return -1#游戏中一定要选择三张牌,否则报错if len(action) != 3: print("[WARN] The correct action length must be 3! Found %d" % len(action))input("Press \"Enter\" key to continute")return -1#将最后正确的动作返回到类中self.actions = actionreturn 1
    

4.8 执行动作

  • 由于游戏规则的限制,执行动作方法中会有比较多的分支语句

        def _action(self,action):"""其中0代表方块,1代表梅花,2代表桃心,3代表黑桃"""index = self.room[action - 1]prefix = self.card.getPrefix(index)suffix = self.card.getNumber(index) + 1if suffix == 1: suffix = 14  #A代表14if prefix % 2 == 1: #方块和黑桃代表怪物print("[INFO] You meet the monster %s!" % self.card.card2str(index))if self.weapon != 0:damaged = suffix - self.card.getNumber(self.weapon if self.finalKill == 0 else self.finalKill) - 1else:damaged = suffixif damaged > 0:self.health -= damaged #扣除血量print("[INFO] Oh no! You lost %d health! (%d/%d)" %\(damaged,self.health,self.maxHealth))else:self.finalKill = index #成功击杀怪物,但是你的武器会扣除伤害print("[INFO] You successfully kill the monster!")print("[INFO] But your weapon can only kill monsters below the value of %d" % suffix)if prefix == 2: #桃心代表加血self.health = min(self.maxHealth,self.health + suffix)print("[INFO] You meet %s and restored %d health! (%d/%d)" %\(self.card.card2str(index),suffix,self.health,self.maxHealth))if prefix == 0: #方块代表武器print("[INFO] You find the new weapon %s!" % self.card.card2str(index))self.finalKill = 0self.weapon = index  #重新装配武器
    

5 游戏展示

5.1 游戏代码

  • 在三个类的加持下,游戏的主代码非常简单

    game = Game()while(game.step() == 0):pass
    
  • 游戏画面如下:
    • 第一回合

    • 第二回合

    • 第三回合

    • 不小心输错了的情况下,也会有错误反馈

    • 游戏结束(这分惨得很)


单人扑克游戏:地城恶棍的Python实现(附实现代码)相关推荐

  1. python生成一副扑克牌_【扑克游戏基本】用python打造出一副扑克牌并实操大转变...

    今天要用python写一副简单的扑克牌, 我们想用物件导向的概念, 分别定义类别「单张扑克牌」与「一副扑克牌」, 而「一副扑克牌」就由52张「单张扑克牌」所组成 类别- 单张扑克牌 一张扑克牌由「点数 ...

  2. 【2022 Twitter爬虫高级搜索接口分析及代码编写 Python爬虫 附主要代码及解析】

    目录 前言 一.网页分析 二.主要代码 1.请求Json包 2. Guesstoken获取 2.Json文件解析 3.存入xlsx 运行效果 名人信息解析获取 存入excel 总结 前言 最近在帮助做 ...

  3. 【32位win7一键扫雷】32位win7系统自带扫雷游戏逆向分析之一键扫雷(附VS代码工程文件、可执行文件和OD分析缓存文件)

    实现效果 视频地址:https://www.zhihu.com/zvideo/1373742900744974336 附一张扫雷自定义中难度最大时进行一键扫雷的截图,如下,24*30,共668颗雷. ...

  4. 比特币base58源码解析_中本聪源码早期版本流出:区块链原名时间链,比特币内置虚拟扑克游戏...

    本文来自 Bitcoin.com,原文作者:Jamie Redman Odaily 星球日报译者 | Moni 本周,一个比特币源代码早期版本浮出水面,立刻引起了加密货币社区的热议.根据"中 ...

  5. Python实现德州扑克游戏

    这是使用python完成的德州扑克游戏.包括了洗牌,发牌,验证牌型. # 德州扑克 # 设计一个程序: # (1)洗牌功能 # (2)发牌功能 # (3)验证手牌 # 皇家同花顺(同一个花色五张连牌 ...

  6. Python扑克游戏编程---摸大点

    开发环境: IDE:Pycharm OS:mac Monterey version 12.5 游戏说明: 此游戏是一款扑克牌游戏,扑克牌颜色为红桃,黑桃,方块,梅花.牌值为1-13, JQK为牌值0. ...

  7. 扑克游戏的洗牌算法及简单测试

    2019独角兽企业重金招聘Python工程师标准>>> 我在学习<写给大家看的C语言书>这本书时,对书后面附录的一个扑克游戏程序非常感兴趣.源代码在帖子最后. PS:这本 ...

  8. 德州扑克游戏算法讲解

    转载自: https://blog.csdn.net/wojiushi3344/article/details/8967735 德克萨斯扑克全称Texas Hold'em poker,中文简称德州扑克 ...

  9. C++实现德州扑克游戏(和电脑一起玩)

    事先声明,本人坚决反对赌博,对众多程序员助纣为虐,远赴东南亚等地编写赌博网站的行为也很反感,更有甚者,使用python进行黑客行为,非法爬虫,真正实现了"C++从入门到入土",&q ...

最新文章

  1. 微软熊辰炎:如何利用图神经网络解决半结构化数据问题?
  2. Apache Shiro和Spring Security的详细对比
  3. 11步教你选择最稳定的MySQL版本
  4. HDU1237 简单计算器
  5. 工行居逸贷,信贷员说3年利率11.38%!!!
  6. 中国数学会副理事长田刚委员:建议从四个方面加强教师队伍建设
  7. 解决Eclipse报errors running builder ‘javascript validator’ on project
  8. 晚上:上课笔记,听完自己独立完成
  9. grub-install: warning: this GPT partition label contains no BIOS Boot Partition; embedding won’t be
  10. 基于Docker部署Gitlab教程
  11. java swing餐厅订餐系统
  12. android视频裁剪工具类,裁剪切视频工具
  13. 蓝牙通知栏图标不显示的问题解决
  14. 微信小程序之自定义表单组件(radio)
  15. Java 9 模块化(Modularity)
  16. Cloudreve离线下载Aria2安装教程
  17. PowerPMAC技术培训------3、PowerPMAC编程工具-IDE
  18. 判断二叉树是否能成为折半查找判定树
  19. 【转载】广告联盟中CPC、CPS、CPA、CPM、CPV广告有什么区别
  20. 初看一脸问号,看懂直接跪下

热门文章

  1. 手游还能这么玩?电脑控制手机鼠标键盘大屏玩手游了解一下
  2. Windows10家庭版 提升管理员权限
  3. Second season twentieth episode,poor Phoebe
  4. 印象笔记 HTML邮件,#印象笔记#如何使用私有邮箱地址保存内容到印象笔记?
  5. cocos2d 高德地图_高德地图SDK使用经验
  6. 带alpha通道四通道的图片转成rgb三通道
  7. 易语言练习笔记-大叔篇(3)-加减计算器
  8. PTA 7-161 双曲余弦函数(*)
  9. 俄罗斯军事帝国的衰落
  10. dom4j的一些总结