前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >OpenCV-Python实战(3) —— OpenCV的绘图功能实现【小游戏2048】

OpenCV-Python实战(3) —— OpenCV的绘图功能实现【小游戏2048】

作者头像
Rattenking
发布2022-11-07 14:48:01
1.6K0
发布2022-11-07 14:48:01
举报
文章被收录于专栏:Rattenking

1. 预览

输入图片说明
输入图片说明

2. 实现思路

  1. 通过二位列表,确定每个数字所在的位置;
  2. 通过字典的引用变量,直接改变字典中的数;
  3. 将二维列表变成一维列表抽取随机位置;
  4. 使用random产生随机的数字2或者4;
  5. OpenCV 的 cv.waitKey 获取键盘按键的 key。

3. 依赖引入

代码语言:javascript
复制
import cv2 as cv
import numpy as np
import random

4. 代码解析

4.0 初始化参数
  1. 初始化画布得宽高和网格数量boardNum*boardNum
  2. 计算每个格子得宽高
  3. 初始化游戏是否结束和记分器归0
  4. 初始化网格列表init_board
代码语言:javascript
复制
def __init__(self, width=340, height=340, boardNum = 4):
    # 初始化参数
    self.width = width
    self.height = height
    self.cellspace = 10
    self.boardNum = boardNum
    self.cellw = (width - self.cellspace * (boardNum + 1)) / boardNum
    self.cellh = self.cellw
    self.score = 0
    self.is_game_over = False
    # 初始化格子
    self.init_board()
4.1 将十六进制颜色转 OpenCV 的 BGR 颜色值
代码语言:javascript
复制
# 将16进制颜色转成opencv可以使用BGR颜色值
def Hex_to_BGR(hex):
  hex = hex[1:]
  r = int(hex[0:2],16)
  g = int(hex[2:4],16)
  b = int(hex[4:6], 16)
  bgr = (b,g,r)
  return bgr
4.2 设置不同数字得背景字典
代码语言:javascript
复制
# 不同文字对应的背景颜色
  def get_board_bg(self, num):
    return Hex_to_BGR({
      "0": "#cdc1b3",
      "2": "#eee4da",
      "4": "#eee1c9",
      "8": "#f3b27a",
      "16": "#f69664",
      "32": "#f77c5f",
      "64": "#f75f3b",
      "128": "#edd073",
      "256": "#edcc62",
      "512": "#edc950",
      "1024": "#edc53f",
      "2048": "#edc22e",
      "4096": "#eee4da"
    }[f'{num}'])
4.3 设置不同数字得颜色字典
代码语言:javascript
复制
# 不同文本的字体颜色
  def get_board_text_color(self, num):
    return Hex_to_BGR({
      "0": "#cdc1b3",
      "2": "#796d65",
      "4": "#796d65",
      "8": "#ffffff",
      "16": "#ffffff",
      "32": "#ffffff",
      "64": "#ffffff",
      "128": "#ffffff",
      "256": "#ffffff",
      "512": "#ffffff",
      "1024": "#ffffff",
      "2048": "#ffffff",
      "4096": "#ffffff"
    }[f'{num}'])
4.4 初始化2048网格
  1. 初始化二维列表self.board
  2. x,y是格子在界面的坐标
  3. num 是对应格子的数字
  4. merge 确定当前格式是否允许合并
代码语言:javascript
复制
# 初始化2048网格
  def init_board(self):
    self.board = [[{'x': i, 'y': j, 'num': 0, 'merge': True} for j in range(self.boardNum)] for i in range(self.boardNum)]
4.5 将二维列表转一维列表

使用 numpy 模块将二维列表转一维列表;

代码语言:javascript
复制
# 获取全部格子得一维列表
  def get_flat_board(self):
    return np.array(self.board).flatten()
4.6 生成随机数2或者4

生成的随机数小于0.9返回2否则返回4

代码语言:javascript
复制
# 产生随机值2或者4
  def get_random(self):
    if random.random() < 0.9:
      return 2
    else:
      return 4
4.7 随机位置填写随机变量
  1. 循环获取网格中是0的字典
  2. 将获取的字典随机一个位置的num赋值获取随机变量
代码语言:javascript
复制
# 随机位置填写随机变量
  def get_random_board(self):
    filters = [item for item in self.get_flat_board() if item["num"] == 0]
    filters[random.randint(0,len(filters) - 1)]["num"] = self.get_random()
4.8 绘制2048UI界面
  1. 更新界面的记分器
  2. 循环网格,绘制网格对应的背景颜色 get_board_bg 获取背景色
  3. 判断对应网格字典的num是否为0
  4. 当字典的num不是0时,将数字绘制到对应的网格 get_board_text_color 获取文字颜色
代码语言:javascript
复制
# 绘制2048UI界面
  def draw(self):
    self.label.config(text=f'SCPRE\n{self.score}')
    for item in self.get_flat_board():
      self.draw_rect(item["x"], item["y"], self.cellw, self.cellh, self.get_board_bg(item["num"]))
      if(item["num"] != 0):
        self.draw_text(item["x"], item["y"], item["num"], self.get_board_text_color(item["num"]))
4.9 绘制网格矩形
  1. 计算网格矩形的起始坐标x0,y0
  2. 计算网格矩形的右下角坐标x1,y1
  3. cv.rectangle 绘制网格背景
代码语言:javascript
复制
# 绘制矩形
  def draw_rect(self, x, y, width, height, color):
    x0 = int(self.cellspace * (x + 1) + self.cellw * x + int((400 - self.width) / 2))
    y0 = int(self.cellspace * (y + 1) + self.cellw * y + int((400 - self.height) / 2))
    x1 = int(x0 + width)
    y1 = int(y0 + height)
    cv.rectangle(self.game2048, (x0, y0), (x1, y1), color, -1)
4.10 绘制网格中文本
  1. 计算网格文本的坐标x,y
  2. cv.putText 绘制每一个字典对应的文本
代码语言:javascript
复制
# 绘制文本
  def draw_text(self, x, y, text, color):
    x = int(self.cellspace * (x + 1) + self.cellw * x + self.cellw / 2 + int((400 - self.width) / 2))
    y = int(self.cellspace * (y + 1) + self.cellw * y + self.cellw / 2 + int((400 - self.height) / 2))
    (fw,fh),dh = cv.getTextSize(str(text), cv.FONT_HERSHEY_DUPLEX, 1, 1)
    x = int(x - fw / 2)
    y = int(y + fh / 2)
    cv.putText(self.game2048, str(text), (x, y), cv.FONT_HERSHEY_DUPLEX, 1, color,1,cv.LINE_AA)
4.11 使用 OpenCV-Python 实现UI界面
代码语言:javascript
复制
# 初始化canvas,绘制
  def render(self):
    self.game2048 = np.zeros((400,400,3),np.uint8)
    self.game2048[:] = 255
    self.copy_game2048 = np.copy(self.game2048)
    x,y = (int((400 - self.width) / 2), int((400 - self.height) / 2))
    cv.rectangle(self.game2048, (x,y),(x + self.width, y + self.height),(158, 175, 193),-1)
    # 绘制2048UI界面
    self.draw()
    while True:
      cv.imshow("GAME 2048", self.game2048)
      key = cv.waitKey(0)
      if key == 27:
        # ESC退出程序
        break
      elif key == 119:
        # W 向上
        self.up()
      elif key == 115:
        # S 向下
        self.down()
      elif key == 97:
        # A 向左
        self.left()
      elif key == 100:
        # D 向右
        self.right()
      elif key == 114:
        # R 重新开始
        self.reset()
      else:
        pass
    cv.destroyAllWindows()
4.12 其他按钮事件的实现
  1. 重新开始游戏按钮【R】事件实现
  2. 清空图像
  3. 重置结束游戏参数
  4. 重置当前盘游戏记分
  5. 初始化格子
  6. 绘制2048UI界面
代码语言:javascript
复制
def reset(self):
    self.game2048[:] = self.copy_game2048
    self.is_game_over = False
    self.score = 0
    # 初始化格子
    self.init_board()
    self.draw()
  
  # 上下左右按钮执行事件
  def up(self):
    if self.is_game_over == False:
     self.move("Up")
  def down(self):
    if self.is_game_over == False:
     self.move("Down")
  def left(self):
    if self.is_game_over == False:
     self.move("Left")
  def right(self):
    if self.is_game_over == False:
     self.move("Right")
4.13 移动UI界面对应元素
  1. 将界面不为0的字典筛选出来
  2. 判断方向如果是[“Left”,“Up”],从左上角开始循环移动;
  3. 判断方向如果是[“Down”,“Right”],从右下角开始循环移动;注意:右下角就是将列表反转循环查询
  4. 使用 item_move 对每一个元素进行移动
  5. 移动完成,设置所有的字典可以再次允许移动
  6. 判断游戏是否结束 has_game_over
  7. 游戏未结束,生成随机数绘制新的UI界面
  8. 游戏结束,先绘制结束时的UI界面,再绘制游戏结束界面注意:此处本准备绘制一个半透明背景,但是由于没找到方法,如果有知道的大佬,请指正
代码语言:javascript
复制
# 移动元素
  def move(self, direction):
    filters = [item for item in self.get_flat_board() if item["num"] != 0]
    if direction in ["Left","Up"]:
      for item in filters:
        self.item_move(item, direction)
    elif direction in ["Down","Right"]:
      for item in list(reversed(filters)):
        self.item_move(item, direction)

    # 移动完成,设置所有元素允许再次合并
    for item in self.get_flat_board():
      item["merge"] = True

    # 判断游戏是否结束
    self.has_game_over()
    if self.is_game_over == False:
      # 生成随机数
      self.get_random_board()
      # 移动完成,生成随机数完成,绘制新的矩阵
      self.draw()
    else:
      self.draw()
      # 生成游戏结束界面
      self.draw_game_over()
4.14 单个元素的移动
  1. 获取当前位置要移动方向的第一个元素 get_current_item_side
  2. 如果返回的是 False,说明当前元素是移动方向的边界元素,不需要移动操作
  3. 如果 item_side 字典的num是0,说明移动方向是空位,需要将当前元素移动到旁边元素
  4. 移动实现就是将当前的值赋值给旁边的值
  5. 注意:需要查询当前元素是否还允许合并,如果不允许,同样需要将合并状态转移到旁边元素!!!
  6. 再次以旁边元素为基点,向旁边移动!
  7. 如果当前字典的数和旁边字典的数不同,不进行移动操作
  8. 如果当前字典的数和旁边字典的数相同,并且两个字典都未曾合并过,进行合并操作
  9. 旁边字典数和当前数合并
  10. 记分器记分
  11. 当前数归0,当前可再次合并,旁边字典不可合并
代码语言:javascript
复制
# 单个元素的移动
  def item_move(self, item, direction):
    item_side = self.get_current_item_side(item, direction)
    
    if item_side == False:
      # 边界不操作
      pass
    elif item_side["num"] == 0:
      # 说明移动方向旁边位置为空,将其移动到旁边位置
      item_side["num"] = item["num"]
      item["num"] = 0
      if item["merge"] == False:
        item_side["merge"] = False
        item["merge"] = True
      self.item_move(item_side, direction)
    elif item_side["num"] != item["num"]:
      # 说明当前和旁边元素不同,不进行移动
      pass
    elif item_side["num"] == item["num"] and item_side["merge"] == True and item["merge"] == True:
      # 说明当前和旁边元素相同,将当前数给旁边元素,当前元素置为0
      item_side["num"] = item_side["num"] * 2
      self.score += item_side["num"]
      item["num"] = 0
      item_side["merge"] = False
      item["merge"] = True
    else:
      return
4.15 获取当前元素旁边元素的值
  1. 计算移动方向的下一个字典的坐标x,y
  2. 判断x,y是否越界,如果没有越界,就返回x,y的字典
  3. 发生越界,返回 False
代码语言:javascript
复制
# 获取当前元素旁边元素的值
  def get_current_item_side(self, item, direction):
    x = item["x"]
    y = item["y"]
    if direction in ["Left"]:
      x = x - 1
    if direction in ["Right"]:
      x = x + 1
    if direction in ["Up"]:
      y = y  - 1
    if direction in ["Down"]:
      y = y + 1
      
    if x >= 0 and x < self.boardNum and y >= 0 and y < self.boardNum:
      return self.board[x][y]
    else:
      return False
4.16 游戏是否结束
  1. 如果网格中存在2048,就游戏结束
  2. 如果网格中不存在空位,循环全部网格
  3. 查找循环的当前字典的上下左右旁边的元素
  4. 对比旁边的元素的数字和当前数字是否相等
  5. 存在相等,游戏未结束
  6. 网格存在空位,游戏未结束
代码语言:javascript
复制
# 是否游戏结束
  def has_game_over(self):
    filters = self.get_flat_board()
    if 2048 in [item["num"] for item in filters]:
      self.is_game_over = True
    elif len([item["num"] for item in filters if item["num"] > 0]) == len(filters):
      for item in filters:
        item_left = self.get_current_item_side(item, 'Left')
        item_right = self.get_current_item_side(item, 'Right')
        item_up = self.get_current_item_side(item, 'Up')
        item_down = self.get_current_item_side(item, 'Down')
        if item_left != False and item_left["num"] == item["num"]:
          return
        elif item_right != False and item_right["num"] == item["num"]:
          return
        elif item_up != False and item_up["num"] == item["num"]:
          return
        elif item_down != False and item_down["num"] == item["num"]:
          return
    else:
      return
    self.is_game_over = True

5. 完整代码

代码语言:javascript
复制
import cv2 as cv
import numpy as np
import random

class G2048():
  def __init__(self, width=340, height=340, boardNum = 4):
    # 初始化参数
    self.width = width
    self.height = height
    self.cellspace = 10
    self.boardNum = boardNum
    self.cellw = (width - self.cellspace * (boardNum + 1)) / boardNum
    self.cellh = self.cellw
    self.score = 0
    self.is_game_over = False
    # 初始化格子
    self.init_board()
    

  # 不同文字对应的背景颜色
  def get_board_bg(self, num):
    return Hex_to_BGR({
      "0": "#cdc1b3",
      "2": "#eee4da",
      "4": "#eee1c9",
      "8": "#f3b27a",
      "16": "#f69664",
      "32": "#f77c5f",
      "64": "#f75f3b",
      "128": "#edd073",
      "256": "#edcc62",
      "512": "#edc950",
      "1024": "#edc53f",
      "2048": "#edc22e",
      "4096": "#eee4da"
    }[f'{num}'])

  # 不同文本的字体颜色
  def get_board_text_color(self, num):
    return Hex_to_BGR({
      "0": "#cdc1b3",
      "2": "#796d65",
      "4": "#796d65",
      "8": "#ffffff",
      "16": "#ffffff",
      "32": "#ffffff",
      "64": "#ffffff",
      "128": "#ffffff",
      "256": "#ffffff",
      "512": "#ffffff",
      "1024": "#ffffff",
      "2048": "#ffffff",
      "4096": "#ffffff"
    }[f'{num}'])

  # 初始化2048网格
  def init_board(self):
    self.board = [[{'x': i, 'y': j, 'num': 0, 'merge': True} for j in range(self.boardNum)] for i in range(self.boardNum)]
    # 生成随机位置的随机数
    self.get_random_board()
    self.get_random_board()

  # 获取全部格子得一维列表
  def get_flat_board(self):
    return np.array(self.board).flatten()

  # 产生随机值2或者4
  def get_random(self):
    if random.random() < 0.9:
      return 2
    else:
      return 4

  # 随机位置填写随机变量
  def get_random_board(self):
    filters = [item for item in self.get_flat_board() if item["num"] == 0]
    filters[random.randint(0,len(filters) - 1)]["num"] = self.get_random()

  # 绘制2048UI界面
  def draw(self):
    for item in self.get_flat_board():
      self.draw_rect(item["x"], item["y"], self.cellw, self.cellh, self.get_board_bg(item["num"]))
      if(item["num"] != 0):
        self.draw_text(item["x"], item["y"], item["num"], self.get_board_text_color(item["num"]))

  # 绘制矩形
  def draw_rect(self, x, y, width, height, color):
    x0 = int(self.cellspace * (x + 1) + self.cellw * x + int((400 - self.width) / 2))
    y0 = int(self.cellspace * (y + 1) + self.cellw * y + int((400 - self.height) / 2))
    x1 = int(x0 + width)
    y1 = int(y0 + height)
    cv.rectangle(self.game2048, (x0, y0), (x1, y1), color, -1)

  # 绘制文本
  def draw_text(self, x, y, text, color):
    x = int(self.cellspace * (x + 1) + self.cellw * x + self.cellw / 2 + int((400 - self.width) / 2))
    y = int(self.cellspace * (y + 1) + self.cellw * y + self.cellw / 2 + int((400 - self.height) / 2))
    (fw,fh),dh = cv.getTextSize(str(text), cv.FONT_HERSHEY_DUPLEX, 1, 1)
    x = int(x - fw / 2)
    y = int(y + fh / 2)
    cv.putText(self.game2048, str(text), (x, y), cv.FONT_HERSHEY_DUPLEX, 1, color,1,cv.LINE_AA)

  def reset(self):
    self.game2048[:] = self.copy_game2048
    self.is_game_over = False
    self.score = 0
    # 初始化格子
    self.init_board()
    self.draw()
  
  # 上下左右按钮执行事件
  def up(self):
    if self.is_game_over == False:
     self.move("Up")
  def down(self):
    if self.is_game_over == False:
     self.move("Down")
  def left(self):
    if self.is_game_over == False:
     self.move("Left")
  def right(self):
    if self.is_game_over == False:
     self.move("Right")

  # 初始化canvas,绘制
  def render(self):
    self.game2048 = np.zeros((400,400,3),np.uint8)
    self.game2048[:] = 255
    self.copy_game2048 = np.copy(self.game2048)
    x,y = (int((400 - self.width) / 2), int((400 - self.height) / 2))
    cv.rectangle(self.game2048, (x,y),(x + self.width, y + self.height),(158, 175, 193),-1)
    # 绘制2048UI界面
    self.draw()
    while True:
      cv.imshow("GAME 2048", self.game2048)
      key = cv.waitKey(0)
      if key == 27:
        # ESC退出程序
        break
      elif key == 119:
        # W 向上
        self.up()
      elif key == 115:
        # S 向下
        self.down()
      elif key == 97:
        # A 向左
        self.left()
      elif key == 100:
        # D 向右
        self.right()
      elif key == 114:
        # R 重新开始
        self.reset()
      else:
        pass
    cv.destroyAllWindows()

  # 单个元素的移动
  def item_move(self, item, direction):
    item_side = self.get_current_item_side(item, direction)
    
    if item_side == False:
      # 边界不操作
      pass
    elif item_side["num"] == 0:
      # 说明移动方向旁边位置为空,将其移动到旁边位置
      item_side["num"] = item["num"]
      item["num"] = 0
      if item["merge"] == False:
        item_side["merge"] = False
        item["merge"] = True
      self.item_move(item_side, direction)
    elif item_side["num"] != item["num"]:
      # 说明当前和旁边元素不同,不进行移动
      pass
    elif item_side["num"] == item["num"] and item_side["merge"] == True and item["merge"] == True:
      # 说明当前和旁边元素相同,将当前数给旁边元素,当前元素置为0
      item_side["num"] = item_side["num"] * 2
      self.score += item_side["num"]
      item["num"] = 0
      item_side["merge"] = False
      item["merge"] = True
    else:
      return

  # 获取当前元素旁边元素的值
  def get_current_item_side(self, item, direction):
    x = item["x"]
    y = item["y"]
    if direction in ["Left"]:
      x = x - 1
    if direction in ["Right"]:
      x = x + 1
    if direction in ["Up"]:
      y = y  - 1
    if direction in ["Down"]:
      y = y + 1
      
    if x >= 0 and x < self.boardNum and y >= 0 and y < self.boardNum:
      return self.board[x][y]
    else:
      return False
    
  # 移动元素
  def move(self, direction):
    filters = [item for item in self.get_flat_board() if item["num"] != 0]
    if direction in ["Left","Up"]:
      for item in filters:
        self.item_move(item, direction)
    elif direction in ["Down","Right"]:
      for item in list(reversed(filters)):
        self.item_move(item, direction)

    # 移动完成,设置所有元素允许再次合并
    for item in self.get_flat_board():
      item["merge"] = True

    # 判断游戏是否结束
    self.has_game_over()
    if self.is_game_over == False:
      # 生成随机数
      self.get_random_board()
      # 移动完成,生成随机数完成,绘制新的矩阵
      self.draw()
    else:
      self.draw()
      # 生成游戏结束界面
      self.draw_game_over()

    
  # 生成游戏结束界面
  def draw_game_over(self):
    (fw,fh),dh = cv.getTextSize("Game Over!", cv.FONT_HERSHEY_DUPLEX, 1.2, 1)
    x = int(200 - fw / 2)
    y = int(200 + fh / 2)
    cv.putText(self.game2048, "Game Over!", (x, y), cv.FONT_HERSHEY_DUPLEX, 1.2, (255,255,255),1,cv.LINE_AA)

  # 是否游戏结束
  def has_game_over(self):
    filters = self.get_flat_board()
    if 2048 in [item["num"] for item in filters]:
      self.is_game_over = True
    elif len([item["num"] for item in filters if item["num"] > 0]) == len(filters):
      for item in filters:
        item_left = self.get_current_item_side(item, 'Left')
        item_right = self.get_current_item_side(item, 'Right')
        item_up = self.get_current_item_side(item, 'Up')
        item_down = self.get_current_item_side(item, 'Down')
        if item_left != False and item_left["num"] == item["num"]:
          return
        elif item_right != False and item_right["num"] == item["num"]:
          return
        elif item_up != False and item_up["num"] == item["num"]:
          return
        elif item_down != False and item_down["num"] == item["num"]:
          return
    else:
      return
    self.is_game_over = True

# 将16进制颜色转成opencv可以使用BGR颜色值
def Hex_to_BGR(hex):
  hex = hex[1:]
  r = int(hex[0:2],16)
  g = int(hex[2:4],16)
  b = int(hex[4:6], 16)
  bgr = (b,g,r)
  return bgr

    
if __name__ == '__main__':
  g2048 = G2048()
  g2048.render()
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-10-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 预览
  • 2. 实现思路
  • 3. 依赖引入
  • 4. 代码解析
    • 4.0 初始化参数
      • 4.1 将十六进制颜色转 OpenCV 的 BGR 颜色值
        • 4.2 设置不同数字得背景字典
          • 4.3 设置不同数字得颜色字典
            • 4.4 初始化2048网格
              • 4.5 将二维列表转一维列表
                • 4.6 生成随机数2或者4
                  • 4.7 随机位置填写随机变量
                    • 4.8 绘制2048UI界面
                      • 4.9 绘制网格矩形
                        • 4.10 绘制网格中文本
                          • 4.11 使用 OpenCV-Python 实现UI界面
                            • 4.12 其他按钮事件的实现
                              • 4.13 移动UI界面对应元素
                                • 4.14 单个元素的移动
                                  • 4.15 获取当前元素旁边元素的值
                                    • 4.16 游戏是否结束
                                    • 5. 完整代码
                                    领券
                                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档