1. 预览
2. 实现思路
- 通过二位列表,确定每个数字所在的位置;
- 通过字典的引用变量,直接改变字典中的数;
- 将二维列表变成一维列表抽取随机位置;
- 使用random产生随机的数字2或者4;
- OpenCV 的 cv.waitKey 获取键盘按键的 key。
3. 依赖引入
代码语言:javascript复制import cv2 as cv
import numpy as np
import random
4. 代码解析
4.0 初始化参数
- 初始化画布得宽高和网格数量boardNum*boardNum
- 计算每个格子得宽高
- 初始化游戏是否结束和记分器归0
- 初始化网格列表init_board
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网格
- 初始化二维列表self.board
- x,y是格子在界面的坐标
- num 是对应格子的数字
- merge 确定当前格式是否允许合并
# 初始化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 将二维列表转一维列表
代码语言:javascript复制使用 numpy 模块将二维列表转一维列表;
# 获取全部格子得一维列表
def get_flat_board(self):
return np.array(self.board).flatten()
4.6 生成随机数2或者4
代码语言:javascript复制生成的随机数小于0.9返回2否则返回4
# 产生随机值2或者4
def get_random(self):
if random.random() < 0.9:
return 2
else:
return 4
4.7 随机位置填写随机变量
- 循环获取网格中是0的字典
- 将获取的字典随机一个位置的num赋值获取随机变量
# 随机位置填写随机变量
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界面
- 更新界面的记分器
- 循环网格,绘制网格对应的背景颜色 get_board_bg 获取背景色
- 判断对应网格字典的num是否为0
- 当字典的num不是0时,将数字绘制到对应的网格 get_board_text_color 获取文字颜色
# 绘制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 绘制网格矩形
- 计算网格矩形的起始坐标x0,y0
- 计算网格矩形的右下角坐标x1,y1
- cv.rectangle 绘制网格背景
# 绘制矩形
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 绘制网格中文本
- 计算网格文本的坐标x,y
- cv.putText 绘制每一个字典对应的文本
# 绘制文本
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 其他按钮事件的实现
- 重新开始游戏按钮【R】事件实现
- 清空图像
- 重置结束游戏参数
- 重置当前盘游戏记分
- 初始化格子
- 绘制2048UI界面
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界面对应元素
- 将界面不为0的字典筛选出来
- 判断方向如果是[“Left”,“Up”],从左上角开始循环移动;
- 判断方向如果是[“Down”,“Right”],从右下角开始循环移动;注意:右下角就是将列表反转循环查询
- 使用 item_move 对每一个元素进行移动
- 移动完成,设置所有的字典可以再次允许移动
- 判断游戏是否结束 has_game_over
- 游戏未结束,生成随机数绘制新的UI界面
- 游戏结束,先绘制结束时的UI界面,再绘制游戏结束界面注意:此处本准备绘制一个半透明背景,但是由于没找到方法,如果有知道的大佬,请指正
# 移动元素
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 单个元素的移动
- 获取当前位置要移动方向的第一个元素 get_current_item_side
- 如果返回的是 False,说明当前元素是移动方向的边界元素,不需要移动操作
- 如果 item_side 字典的num是0,说明移动方向是空位,需要将当前元素移动到旁边元素
- 移动实现就是将当前的值赋值给旁边的值
- 注意:需要查询当前元素是否还允许合并,如果不允许,同样需要将合并状态转移到旁边元素!!!
- 再次以旁边元素为基点,向旁边移动!
- 如果当前字典的数和旁边字典的数不同,不进行移动操作
- 如果当前字典的数和旁边字典的数相同,并且两个字典都未曾合并过,进行合并操作
- 旁边字典数和当前数合并
- 记分器记分
- 当前数归0,当前可再次合并,旁边字典不可合并
# 单个元素的移动
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 获取当前元素旁边元素的值
- 计算移动方向的下一个字典的坐标x,y
- 判断x,y是否越界,如果没有越界,就返回x,y的字典
- 发生越界,返回 False
# 获取当前元素旁边元素的值
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 游戏是否结束
- 如果网格中存在2048,就游戏结束
- 如果网格中不存在空位,循环全部网格
- 查找循环的当前字典的上下左右旁边的元素
- 对比旁边的元素的数字和当前数字是否相等
- 存在相等,游戏未结束
- 网格存在空位,游戏未结束
# 是否游戏结束
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()