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)三个结果。