本博文是对实验楼课程学习的笔记,https://www.shiyanlou.com/courses/368/labs/1172/document。与实验楼不同之处在于我这里是使用的python3进行的实现。
2048小游戏大家都很熟悉,我们要做的就是通过对这个游戏的逻辑和状态进行建模,来实现该游戏。
程序以不同状态作为条件,执行对应的操作(逻辑)。而逻辑操作执行时要考虑用户的输入。除了退出状态,其他状态执行完应返回新得到的状态。以下是状态逻辑转化图。我们设计游戏基本实现下面的图即可。
由上述的状态逻辑转换图,我们可以得到以下伪代码:
init() while(state !='Exit'): op = get_action() if op =='quit': state = 'Exit' elif op == 'Restart': state = 'init' else state = 'game' #调用状态类 state_class[state]()
#相关函数 init() get_action game(): 执行操作, 判断is_win? 判断is_lose? 返回状态注意到,由于游戏胜利或gameover后与正常游戏操作中的输入操作不同(赢了输了只能重开或者退出),所以要区分这些,if-elif-else语句就会很复杂,这里我们采用条件类,将不同的状态作为条件写在字典中,每次循环即可。
state_functions = {'Init':init, 'win': lambda:not_game('Win') 'lose': lambda:not_game('gameover') 'game': game} def main(): state = 'init' while(state !='Exit'): state_functions[state]()这时,在主函数中定义not_game()、game()即可:
def not_game(): get_action() 执行action的操作 def game(): get_action() 判断该方向是否可以移动: 可以-->执行-->得到新棋盘-->判断输赢: win-->return ‘win’ lose --> return 'lose' else --> return 'game' 不可以--> pass -->还是旧棋盘 -->原路返回
当了解了游戏状态与逻辑的设计时,我们一步一步地进行实现:
1.curses窗口设置与使用
我们在curses窗口(控制字符界面)下设计这个棋盘。
使用curses.wrapper()函数可以更容易的进行调试:wrapper会将界面变量screen传递给main函数,而一旦main函数执行完毕,则自动退出该控制字符界面。
def main(screen): while(): ...... curses.wrapper(main)这段代码进入curses界面,对main函数赋参整个窗口对象screen。接着执行main函数里的内容。
2. 绘制游戏界面
我们需要画出这个框,同时需要填入数字。
绘制语句用screen.addstr('字符串' + ' ')来绘制。
def draw(self, screen): help_string1 = 'W(up) S(down) A(left) D(right)' help_string2 = ' R(restart) Q(exit)' gameover_string = ' GAME OVER' win_string = ' You Win' def draw_line(): line = '+' + ('+------' * self.width + '+')[1:] separator = defaultdict(lambda : line) if not hasattr(draw_line, "counter"): draw_line.counter = 0 screen.addstr(separator[draw_line.counter] + ' ') draw_line.counter += 1 #screen.addstr('+' + "------+" * 4 + ' ') def draw_nums(row_num): #给定一行(默认4个)数字,如[0, 0, 0, 4],以列表存放,该函数将其画在screen上 screen.addstr(''.join('|{: ^5} '.format(num) if num > 0 else '| ' for num in row_num) + '|' + ' ') #开始draw screen.clear() screen.addstr('SCORE: 0' + ' ') for row in self.field: draw_line() draw_nums(row) draw_line() if self.win ==1: screen.addstr(win_string + ' ') elif self.gameover == 1: screen.addstr(gameover_string + ' ') else: screen.addstr(help_string1 + ' ') screen.addstr(help_string2 + ' ')绘制前应先得到期盼数字filed(以array形式存储4*4矩阵)
class GameField(object): def __init__(self, height = 4, width = 4, win_value = 2048): self.height = height self.width = width self.score = 0 self.highscore = 0 self.win_value = win_value self.win = 0 self.gameover = 0 self.field = [[0 for i in range(self.height) ] for j in range(self.width)]
3. 处理输入和状态转移
这就和上面讲的是一样的了,具体来讲就是要实现上面提到的各种函数,如game()、not_game()、is_win()、is_lose()、get_action()、is_move_possible()、move()
调试时应先尝试实现curses下绘出正确的初始随机棋盘(step1),之后尝试有限次的操作移动,看看是否有效且无bug(step2),最后在写成while循环的形式。
容易出现的一个问题是:当可移动但是某个方向不能移动时,这时如果代码不进行上述情况的判断,强行在某个方向上进行移动会出错。这时编程者需要注意这种情况的出现。如还未gameover但是向左无法移动,这时接收到left的操作应该什么都不做,返回原状态。
-----------------------------------------------------------------------------------------------------------------------
整体代码如下:
# -*- coding: utf-8 -*- """ Created on Wed Jun 28 00:33:41 2017 @author: dc """ import numpy as np import curses from random import randrange, choice from collections import defaultdict # 建立输入-动作映射表 actions = ['Up', 'Left', 'Down', 'Right', 'Restart', 'Exit'] letter_codes = [ord(ch) for ch in 'WASDRQwasdrq' ] actions_dict = dict(zip(letter_codes,actions * 2)) def invert(qipan): return [row[::-1] for row in qipan] def tran(qipan): return list(np.array(qipan).T) class GameField(object): def __init__(self, height = 4, width = 4, win_value = 2048): self.height = height self.width = width self.score = 0 self.highscore = 0 self.win_value = win_value self.win = 0 self.gameover = 0 self.field = [[0 for i in range(self.height) ] for j in range(self.width)] def spawn(self): new_element = 4 if randrange(100) > 89 else 2 (ii,jj) = choice([(i,j) for i in range(self.height) for j in range(self.width) if self.field[i][j] == 0]) self.field[ii][jj] = new_element def get_field(self): #计算得到随机产生的初始状态下的field self.field = [[0 for i in range(self.width) ] for j in range(self.height)] num1 = 4 if randrange(1,100) > 89 else 2 num2 = 2 if num1 == 4 else 4 (i1, j1) = choice([(i, j) for i in range(self.height) for j in range(self.width) if self.field[i][j]==0]) (i2, j2) = choice([(i, j) for i in range(self.height) for j in range(self.width) if self.field[i][j]==0]) self.field[i1][j1] = num1 self.field[i2][j2] = num2 def draw(self, screen): help_string1 = 'W(up) S(down) A(left) D(right)' help_string2 = ' R(restart) Q(exit)' gameover_string = ' GAME OVER' win_string = ' You Win' def draw_line(): line = '+' + ('+------' * self.width + '+')[1:] separator = defaultdict(lambda : line) if not hasattr(draw_line, "counter"): draw_line.counter = 0 screen.addstr(separator[draw_line.counter] + ' ') draw_line.counter += 1 #screen.addstr('+' + "------+" * 4 + ' ') def draw_nums(row_num): #给定一行(默认4个)数字,如[0, 0, 0, 4],以列表存放,该函数将其画在screen上 screen.addstr(''.join('|{: ^5} '.format(num) if num > 0 else '| ' for num in row_num) + '|' + ' ') #开始draw screen.clear() screen.addstr('SCORE: 0' + ' ') for row in self.field: draw_line() draw_nums(row) draw_line() if self.win ==1: screen.addstr(win_string + ' ') elif self.gameover == 1: screen.addstr(gameover_string + ' ') else: screen.addstr(help_string1 + ' ') screen.addstr(help_string2 + ' ') return True def get_action(self, keyboard): char = 'N' while char not in actions_dict: char = keyboard.getch() return actions_dict[char] def is_move_possible(self, move): def left_row_move_possible(row): def point_changeable(i): if i+1<len(row) and row[i] == row[i+1]: return True if row[i] == 0: return True else: return False return any([point_changeable(i) for i in range(len(row))]) Changeable_dict = {} Changeable_dict['Left'] = lambda field : any([left_row_move_possible(row) for row in field]) Changeable_dict['Right'] = lambda field : Changeable_dict['Left'](invert(field)) Changeable_dict['Up'] = lambda field: Changeable_dict['Left'](tran(field)) Changeable_dict['Down'] = lambda field : Changeable_dict['Up'](invert(field)) if move in Changeable_dict: return Changeable_dict[move](self.field) return False def move(self, direction): def left_row_move(row): def squeeze(row): newrow = [i for i in row if i !=0] newrow += [0 for i in row if i == 0] return newrow def merge(row): pair = False newrow = [] for i in range(len(row)): if pair == True: newrow.append(row[i] *2) pair = False else: if i+1 < len(row) and row[i] == row[i+1]: pair = True newrow.append(0) else: newrow.append(row[i]) assert len(newrow) == len(row) return newrow return squeeze(merge(squeeze(row))) #建立操作为key,对应函数输出为值的字典 moves = {} moves['Left'] = lambda field : [left_row_move(row) for row in field] moves['Right'] = lambda field : invert(moves['Left'](invert(field))) moves['Up'] = lambda field : tran(moves['Left'](tran(field))) moves['Down'] = lambda field : invert(moves['Up'](invert(field))) if direction in moves: if self.is_move_possible(direction): self.field = moves[direction](self.field) #操作完后要加入新的两个随机的2或4? try: self.spawn() self.spawn() except IndexError: return False return True else: return False return False def is_win(self): return any(any(num >= self.win_value for num in row) for row in self.field) def is_lose(self): return not any(self.is_move_possible(move) for move in actions) def main(screen): def init(): field.get_field() #field.draw(screen) return "Game" #注意在main函数中实例化一个field之后,main函数中再定义的函数就可以用field这个变量了。 def not_game(state): if state == 'Win': field.win = 1 if state == 'Gameover': field.gameover = 1 #else: #视作游戏崩溃,crash #field.gameover = 1 field.draw(screen) notgame_action = field.get_action(screen) responses = defaultdict(lambda: state) responses['Restart'], responses['Exit'] = 'Init', 'Exit' return responses[notgame_action] def game(): field.draw(screen) game_action = field.get_action(screen) #每一次game()处理先获取操作并根据操作来执行 if game_action == 'Restart': return 'Init' if game_action == 'Exit': return 'Exit' if field.move(game_action): if field.is_win(): return 'Win' if field.is_lose(): return 'Gameover' else: if field.is_lose(): return 'Gameover' return 'Game' #main()函数开始: field = GameField() # 建立状态-操作字典 state_actions = { 'Init' : init, 'Win' : lambda: not_game('Win'), 'Gameover': lambda: not_game('Gameover'), 'Game': game } state = 'Init' curses.use_default_colors() while(state != 'Exit'): state = state_actions[state]() curses.wrapper(main)
--------------------------------------------------------------------------------------------------------------
一些新的语句的总结:
1. choice(来自random库),参数为列表,功能从列表中随机选取。
2. defaultdict(工厂函数function_factory),来自collections,除了在key不存在时返回默认值外,defaultdict其他行为和dict一样,而dict会抛出keyError。
如separator = defaultdict(lambda :line) : key自行确定赋值,values是工厂函数的类示例。
工厂函数(实际为类):调用它们时,实际是生成了该类型的一个实例。
3. ord(字符):返回ascii字符对应的十进制整数。
一些漂亮操作的总结:
1.实现矩阵转置:
用zip将一系列可迭代对象中的元素打包为元祖,之后将这些元祖放置在列表中,两步加起来等价于行列转置。
2.矩阵翻转
取出每行的元素,逆序索引遍历 = 左右翻转。
示例代码同样可在此处http://download.csdn.net/detail/u010103202/9882485下载。