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

2022-11-07 14:48:01 浏览数 (1)

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'SCPREn{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()

0 人点赞