《2048》作为一款风靡全球的益智类数字小游戏,应该大部分朋友都有玩过。《2048》是20岁的Gabriele Cirulli开发的一款数字游戏。初衷就是觉得好玩,在将其开源版本放到Github上后,意外走红。这款游戏的玩法很简单,每次可以选择上下左右滑动,每滑动一次,所有的数字方块都会往滑动的方向靠拢,系统也会在空白的地方乱数出现一个数字方块,相同数字的方块在靠拢、相撞时会相加。不断的叠加最终拼凑出2048这个数字就算成功。
官方2048网址:http://2048game.com/
我们将要实现的2048操作基本和原版类似,只不过我们会用上下左右键替换原版的上下左右滑动。
1.一开始方格内会出现2或者4这两个小数字;玩家只需要上下左右其中一个方向来移动出现的数字,所有的数字就会向对应的方向靠拢,而剩余的空白方块就会随机出现一个数字(2或4);
2.相同的数字相撞时会叠加靠拢,然后一直这样,不断的叠加最终拼凑出2048这个数字就算成功。
3.如果向上向下向左向右均不可移动,那么Game Over!否则游戏将继续执行!
接下来就开始分开多个步骤模块进行游戏的编写。
import random
# 1.生成4*4的方格数据
# 方格宽度的变量值
width = 4
# 循环width(4)次, 生成列表, 列表元素为width(4)个
data = [[0 for j in range(width)] for i in range(width)]
# 2. 开始游戏时,随机生成2或者4, 2出现的几率比较大
def random_create():
while True:
# 获取随机修改信息的行数;
row = random.randint(0, 3)
# 获取随机修改信息的列数
column = random.randint(0, 3)
# 生成随机填充的值;2出现的几率比较大
value = random.choice([2, 2, 2, 2, 2, 4])
# 判断方格指定位置原先是否有数据, 如果有, 则继续随机获取行或者列; 如果没有, 填充信息;
if data[row][column] == 0:
data[row][column] = value
break
# 3. 绘制方格的分隔符
def draw_sep():
"""+----+----+----+----+"""
print("+----" * 4 + "+")
# 4. 绘制每一行的数据
def draw_one_row(row):
# [0, 0, 2, 2], 如果有值, 内容填充
# | | | 2 | 2 |
for item in row:
if item == 0:
print('| ', end='')
else:
print('|' + str(item).center(4), end='')
print('|')
if __name__ == '__main__':
random_create()
random_create()
print("开始游戏时, 方格的数据显示:")
draw_sep()
for row in data:
draw_one_row(row)
draw_sep()
运行后,可以看到游戏的方格已经画好了,如下:
在移动的时候,我们可能会想到下面几个问题:
问题一:如何判断方格中数字是否可以向左移动?
只要方格的任意一行可以数字可以向左移动, 就返回True;
问题二:如何判断方格的一行数字是否可以向左移动?
只要这一行的任意两个元素可以向左移动, 则返回True;
问题三: 如何判断两个元素可以向左移动?
- 如果第一个数值为0, 第二个数值不为0, 则说明可以向左移动; eg:0 2,0 4 ,0 8
- 如果第一个数值不为0, 第二个数值与第一个元素相等, 则说明可以向左移动;eg: 4 4,2 2
def is_row_left(row: list) -> bool:
"""用户传入一行数据, 判断是否可以向左移动, 返回Bool"""
# 任意两个元素是否可以向左移动?
def is_item_left(index): # index指的是索引值
"""判断当前索引值和下一个索引值, 是否可以向左移动"""
# - 如果第一个数值为0, 第二个数值不为0, 则说明可以向左移动; eg: 0 2 , 0 4 , 0 8
if row[index] == 0 and row[index + 1] != 0:
return True
# - 如果第一个数值不为0, 第二个数值与第一个元素相等, 则说明可以向左移动;eg: 4 4, 2 2,
if row[index] != 0 and row[index] == row[index+1]:
return True
return False
# 只要这一行的任意两个元素可以向左移动, 认为这一行可以向左移动, 返回True;
# any(): 传递可迭代对象, 如果有任意一个为True, 则返回True;
# all(): 传递可迭代对象, 如果有任意一个为False, 则返回False;
return any([is_item_left(index) for index in range(3)])
def is_move_left(data):
"""判断方格数字是否可以向左移动"""
return any([is_row_left(row) for row in data])
def invert(data):
"""
:param data: 方格中的数据
:return: 反转后的方格数据
"""
return [row[::-1] for row in data]
def transpose(data):
"""
实现矩阵的转置
"""
return [list(row) for row in zip(*data)]
加入这两个操作可以大大节省我们的代码量,减少重复劳动。我们只需要编写数字向左移动的代码,在需要数字向右移动时,先将方格中的数字反转,再将其向左移动就会实现数字向右移动了;在需要数字向上移动时,先将方格中的数字进行转置,再将其向左移动就会实现数字向上移动了。
def is_move_right(data):
"""
判断2048能否向右移动
"""
invertData = invert(data)
return is_move_left(invertData)
def is_move_up(data):
"""
判断2048能否向上移动
"""
transposeData = transpose(data)
return is_move_left(transposeData)
def is_move_down(data):
"""
判断2048能否向下移动
源数据:
[2, 0, 4, 2],
[4, 0, 2, 2],
[0, 0, 4, 2],
[0, 0, 4, 2],
处理之后: 判断是否可以向右移动, 也就是判断源数据是否可以向下移动.
[2, 4, 0, 0],
[0, 0, 0, 0],
[4, 2, 4, 4],
[2, 2, 2, 2]
"""
transposeData = transpose(data)
return is_move_right(transposeData)
def tight(row):
"""将这一行所有的非0数字向前放, 0向后放."""
return sorted(row, key=lambda x: 1 if x == 0 else 0)
def merge(row):
"""
依次循环判断两个数是否相等, 如果相等, 第一个元素*2, 第二个元素归0; [4 0 4 0]
"""
# 比较的次数
for index in range(3):
# 如果相等, 第一个元素*2, 第二个元素归0;
if row[index] == row[index + 1]:
# row[index] = row[index] * 2
row[index] *= 2
row[index + 1] = 0
return row
def row_left(row):
"""将某一行数据向左移动"""
# 三部曲
return tight(merge(tight(row)))
def move_left(data):
"""2048整体向左移动"""
return [row_left(row) for row in data]
def move_right(data): # 反转====反转回来
"""
2048整体向右移动
源数据:
[2 0 2 0]
[0 0 0 0]
[0 0 0 0]
[0 0 0 0]
反转
[0 2 0 2]
[0 0 0 0]
[0 0 0 0]
[0 0 0 0]
向左移动
[4 0 0 0]
[0 0 0 0]
[0 0 0 0]
[0 0 0 0]
反转:
[0 0 0 4]
[0 0 0 0]
[0 0 0 0]
[0 0 0 0]
"""
invertData = invert(data)
return invert(move_left(invertData))
def move_up(data):
"""2048整体向上移动"""
transposeData = transpose(data)
return transpose(move_left(transposeData))
def move_down(data):
"""2048整体向下移动"""
transposeData = transpose(data)
return transpose(move_right(transposeData))
问题一:何时用户游戏胜利?
当棋盘中出现num=2048时, 则代表用户胜利。
问题二:何时game over?
当用户在任何方向都不能移动时, 则代表游戏结束, 用户失败。
from itertools import chain
def is_win(data):
"""
2048里面有一个数字大于或者等于2048, 游戏胜利
"""
# 将嵌套的元素连成一个列表
# print(*data) # 解包
# print(list(chain(*data)))
# maxNum = max(chain(*data))
# print(maxNum)
return max(chain(*data)) >= 2048
# 只要有任意一个方向可以移动, 那就没有结束
def is_gameover(data):
return not any([is_move_left(data), is_move_right(data),
is_move_up(data), is_move_down(data)])
利用cursor模块中的getch()方法实现键盘输入的获取。
def get_user_action(stdscr):
"""获取用户输入的函数封装"""
action = stdscr.getch()
if action == curses.KEY_UP:
return 'Up'
elif action == curses.KEY_DOWN:
return 'Down'
elif action == curses.KEY_LEFT:
return 'Left'
elif action == curses.KEY_RIGHT:
return 'Right'
# ord方法查看字母对应的ACII
elif action == ord('r'):
return 'Restart'
elif action == ord('e'):
return 'Exit'
以上就是所有的2048游戏基本的步骤模块了,接下来,再通过主程序的编写和所有模块的连接,就可以实现最终的2048游戏了,拼接后的完整代码如下:
import random
import curses
from itertools import chain
class GameField(object):
"""游戏类"""
# 初始化信息
def __init__(self, width=4, height=4, win_value=2048):
self.width = width # 方格的宽度
self.height = height
self.win_value = win_value
self.score = 0 # 当前得分, 默认为0
self.highscore = 0 # 最高分
self.is_moves = {
'Up': self.is_move_up,
'Down': self.is_move_down,
'Left': self.is_move_left,
'Right': self.is_move_right
}
self.moves = {
'Up': self.move_up,
'Down': self.move_down,
'Left': self.move_left,
'Right': self.move_right
}
def random_create(self):
"""在随机位置生成2或者4"""
while True:
# 获取随机修改信息的行数;
row = random.randint(0, self.height - 1)
# 获取随机修改信息的列数
column = random.randint(0, self.width - 1)
# 生成随机填充的值;2出现的几率比较大
value = random.choice([2, 2, 2, 2, 2, 4])
# 判断方格指定位置原先是否有数据, 如果有, 则继续随机获取行或者列; 如果没有, 填充信息;
if self.data[row][column] == 0:
self.data[row][column] = value
break
def reset(self): # 重新开始2048游戏
# 更新最高分
if self.score > self.highscore:
self.highscore = self.score
self.score = 0
self.data = [[0 for j in range(self.width)] for i in range(self.height)]
self.random_create()
self.random_create()
def draw(self, stdscr):
"""接收一个参数stdscr, 标准屏幕, 将来在该屏幕上绘制数据"""
# . 绘制棋盘的分隔符
def draw_sep():
"""+----+----+----+----+"""
stdscr.addstr("+----" * 4 + "+" + '\n')
# . 绘制每一行的数据
def draw_one_row(row):
# [0, 0, 2, 2], 如果有值, 内容填充
# | | | 2 | 2 |
for item in row:
if item == 0:
stdscr.addstr('| ')
else:
stdscr.addstr('|' + str(item).center(4))
stdscr.addstr('|\n')
# *** 清空窗口绘制的图形信息
stdscr.clear()
stdscr.addstr("2048游戏".center(40, '*') + '\n')
# 绘制方格
draw_sep()
for row in self.data:
draw_one_row(row)
draw_sep()
# 游戏的相关信息
stdscr.addstr("\n当前分数: %s" % (self.score))
stdscr.addstr("\n当前最高分数: %s" % (self.highscore))
stdscr.addstr("\n游戏帮助: 上下左右键 E(xit) R(estart)")
def is_win(self):
"""
2048里面有一个数字大于或者等于2048, 游戏胜利
"""
return max(chain(*self.data)) >= 2048
def is_gameover(self):
return not any([self.is_move_left(self.data), self.is_move_right(self.data),
self.is_move_up(self.data), self.is_move_down(self.data)])
# [0, 2, 0, 2] ==>01 12 23
@staticmethod
def is_row_left(row: list) -> bool:
"""用户传入一行数据, 判断是否可以向左移动, 返回Bool"""
# 任意两个元素是否可以向左移动?
def is_item_left(index): # index指的是索引值
"""判断当前索引值和下一个索引值, 是否可以向左移动"""
# - 如果第一个数值为0, 第二个数值不为0, 则说明可以向左移动; eg: 0 2 , 0 4 , 0 8
if row[index] == 0 and row[index + 1] != 0:
return True
# - 如果第一个数值不为0, 第二个数值与第一个元素相等, 则说明可以向左移动;eg: 4 4, 2 2,
if row[index] != 0 and row[index] == row[index + 1]:
return True
return False
# 只要这一行的任意两个元素可以向左移动, 认为这一行可以向左移动, 返回True;
# any(): 传递可迭代对象, 如果有任意一个为True, 则返回True;
# all(): 传递可迭代对象, 如果有任意一个为False, 则返回False;
return any([is_item_left(index) for index in range(3)])
def is_move_left(self, data):
"""判断方格数字是否可以向左移动"""
return any([self.is_row_left(row) for row in data])
@staticmethod
def invert(data):
"""
:param data: 方格的数据
:return: 反转后的方格数据
"""
return [row[::-1] for row in data]
@staticmethod
def transpose(data):
"""
实现矩阵的转置
"""
return [list(row) for row in zip(*data)]
def is_move_right(self, data):
"""
判断2048能否向右移动
"""
invertData = self.invert(data)
return self.is_move_left(invertData)
def is_move_up(self, data):
"""
判断2048能否向上移动
"""
transposeData = self.transpose(data)
return self.is_move_left(transposeData)
def is_move_down(self, data):
"""
判断2048能否向下移动
"""
transposeData = self.transpose(data)
return self.is_move_right(transposeData)
def move_left(self, data):
# 判断是否可以移动
def tight(row):
"""将这一行所有的非0数字向前放, 0向后放."""
return sorted(row, key=lambda x: 1 if x == 0 else 0)
def merge(row):
"""
依次循环判断两个数是否相等, 如果相等, 第一个元素*2, 第二个元素归0; [4 0 4 0]
"""
# 比较的次数
for index in range(3):
# 如果相等, 第一个元素*2, 第二个元素归0;
if row[index] == row[index + 1]:
# row[index] = row[index] * 2
row[index] *= 2
row[index + 1] = 0
self.score += row[index] #
return row
def row_left(row):
"""将某一行数据向左移动"""
# 三部曲
return tight(merge(tight(row)))
return [row_left(row) for row in data]
def move_right(self, data): # 反转====反转回来
invertData = self.invert(data)
return self.invert(self.move_left(invertData))
def move_up(self, data):
"""2048整体向上移动"""
transposeData = self.transpose(data)
return self.transpose(self.move_left(transposeData))
def move_down(self, data):
"""2048整体向下移动"""
transposeData = self.transpose(data)
return self.transpose(self.move_right(transposeData))
def move(self, direction):
"""
1. 判断这个方向是否可以移动?
2. 执行移动操作;
3. 再随机生成一个2或者4
:param direction:
:return:
"""
if direction in self.moves:
if self.is_moves[direction](self.data):
self.data = self.moves[direction](self.data)
self.random_create()
# PEP8
def main(stdscr):
gameObj = GameField(win_value=32)
def get_user_action(stdscr):
"""获取用户输入的函数封装"""
action = stdscr.getch()
if action == curses.KEY_UP:
return 'Up'
elif action == curses.KEY_DOWN:
return 'Down'
elif action == curses.KEY_LEFT:
return 'Left'
elif action == curses.KEY_RIGHT:
return 'Right'
# ord方法查看字母对应的ACII
elif action == ord('r'):
return 'Restart'
elif action == ord('e'):
return 'Exit'
def init():
"""初始化函数, 初始化结束后, 游戏进入‘Game’状态"""
gameObj.reset()
gameObj.draw(stdscr)
return 'Game'
def game():
gameObj.draw(stdscr)
action = get_user_action(stdscr)
if action == 'Restart':
return 'Init'
elif action == 'Exit':
return 'Exit'
else:
if gameObj.move(action):
# 判断win or gameover?
if gameObj.is_win():
return 'Win'
elif gameObj.is_gameover():
return 'GameOver'
return 'Game'
def not_game():
"""当游戏状态为Win或者GameOver时执行的逻辑"""
action = get_user_action(stdscr)
if action == 'Restart':
return 'Init'
elif action == 'Exit':
return 'Exit'
state_dict = {
'Init': init,
'Game': game,
'Win': not_game,
'GameOver': not_game,
'Exit': exit
}
state = 'Init' # 刚开始玩游戏, 游戏状态为Init
while True:
state = state_dict.get(state)()
curses.wrapper(main)
由于curses模块只能获取在命令窗口的键盘输入,所以程序最终需要在Terminal中运行:
[kiosk@foundation ~]$ cd /home/kiosk/Desktop
[kiosk@foundation Desktop]$ python3 2048.py
运行之后,界面如下:
按上键,向上移动:
按下键 ,向下移动:
按 左键,向左移动:
按右键,向右移动:
按 r 键,重新开始游戏:
需要退出时,就按下 e 键,即可退出。
一个粗糙的2048游戏就完成了,但是基本功能很完整,若发现有什么需要改进的地方,欢迎大家在评论区评论。