python练手项目:利用curses界面对2048的实现与总结
本练手项目参考自实验楼:200 行 Python 代码实现 2048
涉及的知识点
- curses模块
curses是一个python模块,它需要额外下载。
这个模块可以实现文本展示。本程序的2048界面就是依赖curses“画”出来的。
参考:Python Curses - random模块
random是python自带的模块。
这个模块可以生成随机整数(randint)、随机小数(uniform)、在递增数列中随机选数(randrange)、在序列中选取随机元素(choice)、随机取序列的子集(sample)等。本例中使用是randrange、choice两个方法。
参考:Python random模块(获取随机数)常用方法和使用例子 - collections模块
collections是Python内建的一个集合模块,提供了许多有用的集合类。你可以理解成一个杂货铺。
这个模块包括设置字典默认值(defaultdict)、字符计数(Counter)、双链表(deque)等方法。本例中使用了defaultdict方法
参考:Python中collections的用法 - 状态机类程序的设计与实现
- lambda匿名函数
lambda和普通的函数相比,就是省去了函数名称而已。好处是:
(1) 省去定义函数的过程、例代码精简。
(2) 对于不需要重用的函数,一次性定义,反而不用考虑如何命名,即时性很好。
参考:关于Python中的lambda - for遍历的灵活使用。
- 列表生成式: 列表生成式可以十分简便地生成一个序列,如一条语句生成平方序列:[x*x for x in range(1 , 11)]
- python切片的使用: b = a[::-1]实现了序列a的逆序排列。
- 其它函数
– ord(‘a’): 将字符串转化为acsii码对应数字
– zip(list1, list2): 将列表list1、list2中的元素分别对应取出,构造成元组。即映射。本例中,将zip巧用,“分别取出”即实现了由行取列,矩阵的转置也随即完成。
– range(): range(5) <==>range(0,5,1).即以0开始,以小于5结束,以步长为1取值。
– any(): 全或判断,即,对于any(iterable) ,如果iterable 中有一个为 True,则返回 True,只有在全部为 False时返回 False。
基本实现
通过分析,实现一个2048游戏,一共包含UI展示、游戏运行引擎、游戏数据的生成、游戏数据的演变等4个部分。
UI展示
本实验中,UI界面是通过curses模块实现的。
curses的初始化方法:wrapper
经常在导入curses模块以后,使用initscr()方法进行初始化。下面是初始化举例。
import curses
stdscr = curses.initscr()
# stdscr指代的就是显示器本器了。
完成初始化之后,还应当使用init_pair()等方法进行屏幕背景颜色、字体阴影颜色的设定。不过,若是不准备对curses界面进行精确控制、采用默认颜色即可的话,可以使用:wrapper方法。看如下对比:
import curses
def main1(stdscr)
pass
curses.wrapper(main1)
# 上面的语句等价于:
import curses
def main2(stdscr)
pass
stdscr = curses.initscr()
main2(stdscr)
结论:wrapper(main1)函数不仅会执行一次main1函数、完成curses的初始化,还会将初始化得到的stdscr强行传递给main1作为main1的参数。
curses的屏幕展示语句:addscr
我们的程序,主要是靠addscr这一方法来“绘制”,它的基本语法是:
stdscr.addscr(y, x, str or ch, attr)
# y,x 代表绘制的坐标。可省略
# str代表绘制的内容
# attr代表绘制时指定的属性。可省略。
理解基本的语法后,可以试着展示一条语句:、
import curses
def main1(stdscr):
stdscr.addstr('hello world')
ch=stdscr.getch() # 本句是为了使画面暂停。
curses.wrapper(main1)
输出如下:

curses正式开画
界面可分为得分、棋盘、信息提示,其中棋盘又包括画横线、画竖线。
-
准备函数
screen即stdscr。screen.addstr()太过冗长,故重写,以简化。def cast(string): # 对addstr操作作简化 screen.addstr(string + '\n') -
得分
screen.clear() # 清屏 cast('SCORE: ' + str(self.score)) cast('HIGHSCORE: ' + str(self.highscore)) -
棋盘
画分割线:
本例所画侵害线并非必须。简单地,我们可以有:def draw_hor_separator(): line = ('+-----' * self.width + '+') cast(line)实验中给出的代码较复杂,可以作参考:
def draw_hor_separator(): line = '+' + ('+------' * self.width + '+')[1:] #[1:]是python切片的使用,表示从1开始,这里的作用是省略掉'++---+--..'中的第一个'+'。 #对于x='abced',有:x[0]=='a'、x[1]=='b' separator = defaultdict(lambda: line) if not hasattr(draw_hor_separator, 'counter'): draw_hor_separator.counter = 0 cast(separator[draw_hor_separator.counter]) draw_hor_separator.counter += 1画一行:
对于已经给定信息的每一行,我们需要的内容包括竖分割线、给定的数字。欲实现这些内容,都回归到“是填充数字还是填充空白”这一问题上来。
这里,利用str.format()及类似{:^10d}的语句(居中并占10个空)来处理数字与空白间的关系。def draw_row(row): s = '' for i in row: if i > 0: s += '|{: ^6}'.format(i) else: s += '| ' s += '|' # 收尾分割符 cast(s)当然,为了更加pythonic,我们可以将if-else合并:
def draw_row(row): s = '' for i in row: s += '|{: ^6}'.format(i) if i != 0 else '| ' s += '|' # 收尾分割符 cast(s)还有更pythonic的可能:join()方法与列表生成式的结合使用。
def draw_row(row): cast(''.join('|{: ^6}'.format(num) if num > 0 else '| ' for num in row) + '|' + '\n')画出整个二维棋盘
拥有画出一行的能力以后,让这个画的动作遍历棋盘for row in self.field: draw_hor_separator() draw_row(row) draw_hor_separator() # 补最后一行分割线。 -
信息提示
提示用户可执行的操作和当前状态。在不同情况下有不同显示内容,伪代码如下:如果胜利(附带判断): 输出“你赢了“ 如果失败(附带判断): 输出”你输了“ 如果正常进行游戏(既非赢也非输): 输出游戏操作提示 显示一些需要常态显示的提示代码实现如下(胜负判断稍后完成):
help_string1 = '(W)Up (S)Down (A)Left (D)Right' help_string2 = ' (R)Restart (Q)Exit' gameover_string = ' GAME OVER' win_string = ' YOU WIN!' if self.is_win(): # 如果胜利(附带判断): cast(win_string) # 输出“你赢了” elif self.is_gameover(): # 如果失败(附带判断): cast(gameover_string) # 输出“你输了” else: # 既非赢也非输: cast(help_string1) # 输出游戏操作提示 cast(help_string2) # 需要常态显示的提示
游戏运行引擎
引擎分析
从打开这个2048游戏的角度来讲,游戏共区别为初始化(Init)、游戏中(Game)、游戏胜利(Win)、游戏失败(Gameover)4个状态。它们可以如下图相互转化:

实现这样的转化,我们既要这个状态的名称、又要这个状态能通过某种方式执行。字典的特性完美地帮助了我们。
state_actions = {
'Init': init,
'Win': lambda: not_game('Win'),
'Gameover': lambda: not_game('Gameover'),
'Game': game,
}
字典的“键”以字符串的形式存储了状态名称,字典的“健值”甚至可以存储函数名。
配合函数的返回功能,状态机就在“程序运行”-“状态名称”-“程序运行”间相互转换。
从“程序运行”到“状态名称”
这一步,由return来实现,我们需要确保的,就是return返回的字符串刚好是state_actions 里面的键值。
def init():
# 重置
print("do init.")
return 'Game'
# 上面的状态图告诉我们,这里只会返回到Game状态
根据状态图,我们可以有以下程序。
def game():
# 展示出当前画面,接收用户输入,并根据用户输入,反馈出继续游戏(Game)、游戏胜利(Win)、游戏失败(Gameover)三个结果。
# 画出当前画面
print("do draw")
# 读取用户输入得到action
action = print("get user's action")
if action == 'Restart':
return 'Init'
elif action == 'Exit':
return 'Exit'
elif game_field.move(action):
# 执行一次移动并成功,判断是否胜利
if game_field.is_win() :
return 'Win'
if game_field.is_gameover() :
return 'Gameover'
else:
# 移不动,保持现有原状
return 'Game'
从“状态名称”到“程序运行”
有了状态名称,执行程序只需要一步:读取state_actions状态名称对应的键值:
state = 'Init' # 初始化语句
while state != 'Exit':
state = state_actions[state]()
游戏数据的生成
游戏数据包括以下内容:
1.棋盘的数据框架:棋盘大小定义、棋盘主数据(4X4数组)、起始与结束分数、最高分。
2.棋盘主数据:带有数值的4X4二维数组(列表)。
3.随机生成功能:初始时,某2个位置填充2或4。
数据框架的初始化
def __init__(self, height = 4, width = 4, win = 2048):
self.height = height #高
self.width = width #宽
self.win_value = win #过关分数
self.score = 0 # 当前分数
self.highscore = 0 # 最高分
self.reset()
棋盘主数据初始化生成
def reset(self):
'''重置棋盘 '''
# 最高分的保留
if self.score > self.highscore:
self.highscore = self.score
# 分数归零
self.score = 0
# 数据全归零。这是[列表生成式]的二维使用:
self.field = [[0 for i in range(self.width)] for j in range(self.height)]
assert_equal(self.field[1][1], 0)
assert_equal(type(self.field[0][0]), type(0))
# 2个随机位置的数的生成
self.spawn()
self.spawn()
随机生成功能
随机生成功能有2部分:
- 生成数:生成2或4。
- 生成位置:位置随机,且不能有其它数占用。
def spawn(self):
''' 生成随机数 '''
# 生成数:以0.1的概率生成4,以0.9概率生成2,并放入随机位置
if randrange(100) > 89:
new_element = 4
else:
new_element = 2
# 上面的语句也可以这样更加pythonic:
# new_element = 4 if randrange(100) > 89 else 2
# 数的安置:
i = randrange(self.width)
j = randrange(self.height)
while self.field[i][j] != 0:
i = randrange(self.width)
j = randrange(self.height)
# 上面的语句也可以这样更加pythonic:
# (i,j) = choice([(i,j) for i in range(self.width) for j in range(self.height) if self.field[i][j] == 0])
# 数、位结合
self.field[i][j] = new_element
游戏数据的演变
游戏数据的演变包括:
一、左、右、上、下的移动、相加合成。
二、用户的输入。
三、游戏胜负的判断。
左、右、上、下的移动
一、典型动作的分析
这里不妨用典型动作,一列向左,来分析。
数据的移动可以用三个动作实现。一是紧凑,二是相加,三是再紧凑。
1.紧凑
在紧凑过程中,可以构造空的一列,遇到有意义的数字,就将旧列赋值给新列。否则,跳过新列的当前元素,判断下一个。最后,再对新列填空,以使新旧2个列长度相等。
def move(self, direction):
def move_row_left(row):
def tighten(row):
''' 数据紧凑 '''
new_row = []
# 对有意义的数字赋值:
for i in row:
if i != 0:
new_row.append(i)
else:
pass
# 对无意义的数字填空:
for i in range(len(row) - len(new_row)):
new_row.append(0)
# 有更加pythonic的语句吗?答案是有的:
# new_row = [i for i in row if i != 0]
# new_row += [0 for i in (len(row) - len(new_row))]
return new_row
2.相加
紧凑完成之后,如果相信两个数字相同,则后一数字加至前一数字上,且后一数字置0.
def move(self, direction):
def move_row_left(row):
def merge(row):
for i in range(len(row)):
if row[i] == row[i - 1] and i > 0:
row[i - 1] += row[i]
row[i] = 0
self.score += row[i - 1]
return row
3.完成向左移动
def move(self, direction):
def move_row_left(row):
return tighten(merge(tighten(row)))
二、向右移动的转化
对一列作向右移动,可看作对一列逆序排列后,进行向左转化,即:取逆还原(向左移动(取逆(row)))。直观地,为实现取逆我们会想到从最后一个元素开始读取,依次赋值给新列。python的切片操作能够减少这一过程的代码量:
row = [1,2,3]
new_row = row[::-1]
print(new_row)
输出为:[3,2,1]
对于整个filed,可以有:
def invert(field):
new_field = [row[::-1] for row in filed]
# 记住我们在def reset(self)中定义的,field是一个4X4二维列表
return new_field
三、向上、下移动的转化
上、下移动,可以想办法将矩阵转置,进而上、下移动的问题变成左右移动的问题。刚刚好,zip(list1, list2)函数为我们提供的解决方案。
zip函数的举例:
a = [1,2,3]
b = [4,5,6]
c = [7,8,9,10,11]
# 将d中的元素(3个元组)转化为列表,整个f形成一个二维列表
f = []
for i in zip(a,b,c):
f.append(list(i)) # 如果不加list(i),将只能输出元组
for i in f:
print(i)
# f:
# [1, 4, 7]
# [2, 5, 8]
# [3, 6, 9]
# 将二维列表解压
h = zip(*f)
print('\nh:')
for i in h:
print(i) # 这次没有加list(i)
# h:
# (1, 2, 3)
# (4, 5, 6)
# (7, 8, 9)
转置函数:
def transpose(field):
''' 转置函数 从列表到列表 '''
new_field = [list(row) for row in zip(*field)]
return new_field
四、具体移动动作
class GameField(object):
def move(self, direction):
moves = {} # 定义一个空字典
moves['Left'] = lambda field: [move_row_left(row) for row in field]
moves['Right'] = lambda field: invert(moves['Left'](invert(field)))
moves['Up'] = lambda field: transpose(moves['Left'](transpose(field)))
moves['Down'] = lambda field: transpose(moves['Right'](transpose(field)))
用户的输入
用户的输入必须在有限状态内进行。如果在有限状态内,则执行,否则就放弃。
actions = ['Up', 'Left', 'Down', 'Right', 'Restart', 'Exit']
letter_codes = [ord(ch) for ch in 'WASDRQwasdrq']
action_dict = dict(zip(letter_codes, actions *2))
def get_user_action(keyboard):
char = 'N'
while char not in action_dict:
char = keyboard.getch()
return action_dict[char]
class GameField(object):
def move(self, direction):
if direction in moves:
if self.move_is_possible(direction):
self.field = moves[direction](self.field)
self.spawn()
return True
else:
return False
游戏胜负的判断
若整个棋盘内出现2048这个数字,即胜。
若整个棋盘不能再移动,即负。
对整个棋盘作判断,可由any()函数帮助我们遍历和判断。
class GameField(object):
def is_win():
return any(any(i >= self.win_value for i in row)for row in self.filed)
def is_gameover():
return not any(各方向移动情况)
这里,各方向移动情况,还需要我们完成“能否移动”的判断
能否移动的判断
依旧分析向“左”移动。一列能否移动,主要看其能否相加、是否有0位。所以我们可以有:
class GameField(object):
def row_is_left_movable(row):
for i in range(len(row)):
if row[i] == 0:
return True
elif i > 0 and row[i] == row[i-1]:
return True
return False
原实验提供了以下代码,以供比较、参考:
class GameField(object):
def row_is_left_movable(row):
def change(i):
if row[i] == 0 and row[i + 1] != 0:
return True
elif row[i] != 0 and row[i + 1] == row[i]:
return True
else:
return False
return any(change(i) for i in range(len(row) - 1))
check = {} # 构造空字典
check['Left'] = lambda field: any(row_is_left_movable(row) for row in field)
check['Right'] = lambda field: check['Left'](invert(field))
check['Up'] = lambda field: check['Left'](transpose(field))
check['Down'] = lambda field: check['Right'](transpose(field))
理解程序整体结构之后,再将前方中伪代码部分进行实现,程序即完成。
全部代码
import curses
# from nose.tools import *
from random import randrange, choice
from collections import defaultdict
actions = ['Up', 'Left', 'Down', 'Right', 'Restart', 'Exit']
letter_codes = [ord(ch) for ch in 'WASDRQwasdrq']
action_dict = dict(zip(letter_codes, actions *2))
def main(stdscr):
def init():
# 重置
game_field.reset()
return 'Game'
def not_game(state):
# 给出画面 Gameover 或 Win
game_field.draw(stdscr)
# 读取用户输入,给出正常游戏还是结束游戏
action = get_user_action(stdscr)
responses = defaultdict(lambda: state)
# 对于lambda,直接去掉可以不?responses = defaultdict(lambda: state)
# 默认为当前状态
responses['Restart'], responses['Exit'] = 'Init', 'Exit'
return responses[action]
def game():
# 展示出当前画面,接收用户输入,并根据用户输入,反馈出继续游戏(Game)、游戏胜利(Win)、游戏失败(Gameover)三个结果。n
# 画出当前画面
game_field.draw(stdscr)
# 读取用户输入
action = get_user_action(stdscr)
if action == 'Restart':
return 'Init'
elif action == 'Exit':
return 'Exit'
elif game_field.move(action):# 执行一次移动并成功
if game_field.is_win() :
return 'Win'
if game_field.is_gameover() :
return 'Gameover'
return 'Game'
state_actions = {
'Init': init,
'Win': lambda: not_game('Win'),
'Gameover': lambda: not_game('Gameover'),
'Game': game,
}
curses.use_default_colors()
game_field = GameField(win = 2048)
state = 'Init'
# 状态机开始
while state != 'Exit':
state = state_actions[state]()
def get_user_action(keyboard):
char = 'N'
while char not in action_dict:
char = keyboard.getch()
return action_dict[char]
def transpose(field):
# 矩阵行转列
# 输入field 4*4的矩阵,输出一个列表,这个列表中每个元素,都是一个元组。再将里面的每一个元组,转化为列表。
return [list(row) for row in zip(*field)]
def invert(field):
# 矩阵逆转置
return [row[::-1] for row in field]
class GameField(object):
""" GameField."""
def __init__(self, height = 4, width = 4, win = 2048):
self.height = height # 高
self.width = width # 宽
self.win_value = win # 过关分数
self.score = 0 # 当前分数
self.highscore = 0 # 最高分
self.reset() # 重置
def spawn(self):
''' 生成随机数 '''
# 数的生成:以0.1的概率生成4,以0.9概率生成2,并放入随机位置
if randrange(100) > 50:
new_element = 4
else:
new_element = 2
# 上面的语句也可以这样更加pythonic:
# new_element = 4 if randrange(100) > 89 else 2
# 数的安置:
i = randrange(self.width)
j = randrange(self.height)
while self.field[i][j] != 0:
i = randrange(self.width)
j = randrange(self.height)
# 上面的语句也可以这样更加pythonic:
# (i,j) = choice([(i,j) for i in range(self.width) for j in range(self.height) if self.field[i][j] == 0])
self.field[i][j] = new_element
def reset(self):
# 重置棋盘
if self.score > self.highscore:
self.highscore = self.score
self.score = 0
self.field = [[0 for i in range(self.width)] for j in range(self.height)]
# 生成两个数
self.spawn()
self.spawn()
def is_win(self):
return any(any(i >= self.win_value for i in row) for row in self.field)
def is_gameover(self):
return not any(self.move_is_possible(move) for move in actions)
def move(self, direction):
def move_row_left(row):
def tighten(row):
''' 数据紧凑 '''
new_row = []
# 对有意义的数字赋值:
for i in row:
if i != 0:
new_row.append(i)
else:
pass
# 对无意义的数字填空:
for i in range(len(row) - len(new_row)):
new_row.append(0)
# 有更加pythonic的语句吗?答案是有的:
# new_row = [i for i in row if i != 0]
# new_row += [0 for i in (len(row) - len(new_row))]
return new_row
def merge(row):
# 可以合并的,进行合并
for i in range(len(row)):
if row[i] == row[i - 1] and i > 0:
row[i - 1] += row[i]
row[i] = 0
self.score += row[i - 1]
return row
# 先挤,后合并,再挤
return tighten(merge(tighten(row)))
moves = {}
moves['Left'] = lambda field: [move_row_left(row) for row in field]
moves['Right'] = lambda field: invert(moves['Left'](invert(field)))
moves['Up'] = lambda field: transpose(moves['Left'](transpose(field)))
moves['Down'] = lambda field: transpose(moves['Right'](transpose(field)))
if direction in moves:
if self.move_is_possible(direction):
self.field = moves[direction](self.field)
self.spawn()
return True
else:
return False
def move_is_possible(self, direction):
def row_is_left_movable(row):
for i in range(len(row)):
if row[i] == 0:
return True
elif i > 0 and row[i] == row[i-1]:
return True
return False
# def row_is_left_movable(row):
# def change(i):
# if row[i] == 0 and row[i + 1] != 0:
# return True
# elif row[i] != 0 and row[i + 1] == row[i]:
# return True
# else:
# return False
# return any(change(i) for i in range(len(row) - 1))
check = {}
check['Left'] = lambda field: any(row_is_left_movable(row) for row in field)
check['Right'] = lambda field: check['Left'](invert(field))
check['Up'] = lambda field: check['Left'](transpose(field))
check['Down'] = lambda field: check['Right'](transpose(field))
if direction in check:
return check[direction](self.field)
else:
return False
# 下面语句在棋盘内定义
def draw(self, screen):
def cast(string):
# 对addstr操作作简化
screen.addstr(string + '\n')
def draw_hor_separator():
line = '+------' * self.width + '+'
cast(line)
# 以下是 draw_hor_separator 的另一种实现
# def draw_hor_separator():
# line = '+------' * self.width + '+'
# separator = defaultdict(lambda: line)
# if not hasattr(draw_hor_separator, 'counter'):
# draw_hor_separator.counter = 0
# cast(separator[draw_hor_separator.counter])
# draw_hor_separator.counter += 1
def draw_row(row):
s = ''
for i in row:
if i > 0:
s += '|{: ^6}'.format(i)
else:
s += '| '
s += '|'
cast(s)
# draw_row 的另外2种实现:
# def draw_row(row):
# s = ''
# for i in row:
# s += '|{: ^6}'.format(i) if i != 0 else '| '
# cast(s)
#
# def draw_row(row):
# cast(''.join('|{: ^5}'.format(num) if num > 0 else '| ' for num in row) + '|' + '\n')
help_string1 = '(W)Up (S)Down (A)Left (D)Right'
help_string2 = ' (R)Restart (Q)Exit'
gameover_string = ' GAME OVER'
win_string = ' YOU WIN!'
screen.clear()
cast('SCORE: ' + str(self.score))
cast('HIGHSCORE: ' + str(self.highscore))
for row in self.field:
draw_hor_separator()
draw_row(row)
draw_hor_separator()
if self.is_win():
cast(win_string)
elif self.is_gameover():
cast(gameover_string)
else:
cast(help_string1)
cast(help_string2)
curses.wrapper(main)
这篇博客详细介绍了使用Python的curses模块和相关库实现2048游戏的过程,包括UI展示、游戏运行引擎、游戏数据生成和演变。通过分析游戏状态机,实现了状态之间的转换,并给出了游戏胜负的判断方法。此外,博客还讨论了lambda匿名函数、for遍历和列表生成式等Python编程技巧。
2186

被折叠的 条评论
为什么被折叠?



