标签:
2048这个游戏应该是没几个人不知道吧... 今天去实验楼学了一下这个游戏的终端版本, 大概讲一下我对这个游戏的开发思路的理解.
实现为了实现2048, 我们需要用到3个模块, 分别是curses(用于终端界面交互程序开发的库, 可以解决屏幕打印以及按键处理等方面的问题), random, 以及collections 中的 defaultdict.
第一个库比较复杂, 我之前也没接触过, 不过隐隐感觉是一个功能强大的库, 我之后会专门研究它的官方文档, 目前暂且放在一边, 所幸2048中对这个库用的也不多, 所以也不用太担心. 第二个库是随机函数库, 我们只用其中的两个函数, 一个是randrange(), choice(), 这两个函数在我之前的文章中应该有提到, 相当简单, 这里不再赘述. 最后一个是collections 中的 defaultdict, 根据官方文档, 这是dict的一个子类, 主要的不同就在于这个类的初始化需要传递一个工厂函数给他(也就是constructor), 对于一般的字典, 当你试图查询一个不存在的‘key‘的value时, 会出现错误, 而对于该类对象, 则会调用工厂函数产生对应工厂类的对象...
比如 :
1 import collections 2 s = [(‘yellow‘, 1), (‘blue‘, 2), (‘yellow‘, 3), (‘blue‘, 4), (‘red‘, 1)] 3 d = collections.defaultdict(list) 4 for k, v in s: 5 d[k].append(v) 6 print(list(d.items())) 7 print(d[‘1234‘])
注意看第5行, 它使用了d[k].append(v), 也就是说, 一开始d为空, ‘yellow‘的值显然是不存在的, 所以直接调用list(), 所以一开始其实d[‘yellow‘]的值是[], 第7行也正好验证了第五行的猜想, 结果如下 :
[(‘red‘, [1]), (‘yellow‘, [1, 3]), (‘blue‘, [2, 4])] []
接下来我们第一件事应该是理清程序的主要逻辑, 一般来讲, 就像编译器分析源代码一样, 我们会引入状态机的概念来对游戏的逻辑进行分析, 对于这道题 , 引用实验楼里的状态分析图:
简单来讲, 我们可以这么分析 :
1. 当开始运行程序, 程序init之后进入game状态.
2. 在game状态, 用户可以选择重新开始游戏或者退出游戏, 也可以选择正常的上下左右移动进行游戏
3. 如果选择重新开始游戏则重新开始, 实际还是init之后又进入game状态, 而选择退出则退出游戏, 最后如果上下左右达成游戏结束条件(win或者gameover), 那么就进入了not_game状态, 此时只能选择重新开始或者退出.
所以经过分析可以发现其实该程序只有2个状态, 一个是正在游戏, 一个是不在游戏(游戏结束之后但又没有进行任何操作的那个状态), 而退出游戏和初始化游戏更准确来说的应该是一个即时动作, 并不作为状态可以长期持续.
根据分析我们可以先大概写出程序的主框架 :
1 import curses 2 from collections import defaultdict 3 4 5 def main(stdscr): 6 def init(): 7 #Todo : 初始化游戏画面 8 return ‘Gaming‘ 9 10 def game(): 11 #Todo : 绘制游戏图象 12 action = #Todo : 根据用户的输入转化为相应的动作 13 14 if action == ‘Restart‘: 15 return ‘Init‘ 16 if action == ‘Quit‘: 17 return ‘Quit‘ 18 if #正常移动: 19 #执行移动动作 20 if 胜利条件: 21 return ‘Win‘ 22 if 失败条件: 23 return ‘Fail‘ 24 25 return ‘Gaming‘ # 说明这一步移动并不会导致游戏结束, 那么继续游戏. 26 def not_game(state): 27 #Todo : 根据状态绘制游戏图象(表现出胜利或者失败的信息) 28 action = #Todo : 根据用户的输入转化为相应的动作 29 30 #下面这两行的作用就是, 除非action动作是restart或者init, 其他的任何动作都不会对当前状态造成任何影响. 31 response = defaultdict(lambda : state) 32 response[‘Restart‘], response[‘Quit‘] = ‘Init‘, ‘Quit‘ 33 34 return response[action] 35 36 37 #根据不同的状态返回相应的函数(对应做出相应的动作) 38 state_action = { 39 ‘Init‘: init, 40 ‘Win‘: lambda : not_game(‘Win‘), 41 ‘GameOver‘: lambda :not_game(‘GameOver‘), 42 ‘Gaming‘: game 43 } 44 45 state = ‘Init‘ 46 while state != ‘Quit‘: 47 state =state_action[state]()
接下来我们可以进一步来考虑按键与相应动作的对应, 我个人倾向于使用vim式的移动, 所以我准备设置的键位 : 左(h), 下(j), 上(k), 右(l), 退出(q), 重启(r)...
1 actions = [‘Left‘, ‘Down‘, ‘Up‘, ‘Right‘, ‘Quit‘, "Restart"] 2 key = [ord(ch) for ch in ‘hjklqrHJKLQR‘] 3 KeyActionMap = dict(zip(key, actions * 2)) 4 5 def getUserAction(keyboard): 6 char = "N" 7 while char not in KeyActionMap: 8 char = keyboard.getch() 9 return KeyActionMap[char]
然后是棋盘对象 : 首先基本属性当然是棋盘的宽(x), 高(y), 胜利分数, 当前分数, 当然原实验中还加入了一个历史最高分, 所以初始化函数是这样的...
1 class Board(): 2 def __init__(self, width=4, height=4, goal=2048): 3 self.width = width 4 self.height = height 5 self.goal = goal 6 self.curScore = 0 7 self.topScore = 0 8 self.reset()
既然是初始化棋盘, 最后一个reset() 自然是初始化棋盘中每个小格子的数值, 同时考虑到reset其实有可能是游戏胜利后重新开始(可能需要用当前分数刷新最高分数) :
1 def reset(self): 2 self.topScore = self.curScore if self.curScore > self.topScore else self.topScore 3 self.curScore = 0 4 self.board = [[0 for i in range(self.width)] for j in range(self.height)] 5 self.spawn() 6 self.spawn()
最后两次调用spawn(), 意图很明显了, 就是要产生两个非零的格子作为开始的两个格子 :
def reset(self): self.topScore = self.curScore if self.curScore > self.topScore else self.topScore self.curScore = 0 self.board = [[0 for i in range(self.width)] for j in range(self.height)] self.spawn() self.spawn()
我们可以捋一捋目前的思路, 进入游戏是一个状态机, 通过init之后现在是游戏状态, 显然我们现在需要考虑的一个问题是格子的移动了, 也就是说玩家是通过移动格子来保证游戏的进展的. 我们先假设我们要向左移动, 接着我们先只考虑一行, 怎么样算是完成了一次向左移动 ? 如果将左移动作分解的话, 其实是先将所有的非0格子靠左, 接着对于从左往右找到的第一对响铃的等值格子进行合并, 之后再将合并之后出现的空缺(0格子)用其右边的非0格子进行填充, 也就是说这个动作总体可以分为三步 :
1. 非0格子的做贴紧
2. 最左边的等值格子的合并(如果存在的话)
3. 重复第一步]
到这里我们可以先把这个思路用代码表示出来 :
1 def moveRowLeft(row): 2 def tighten(row): 3 newRow = [i for i in row if i != 0] 4 newRow += [0 for i in range(len(row) - len(newRow))] 5 return newRow 6 7 def merge(row): 8 pair = False 9 newRow = [] 10 for i in range(len(row)): 11 if pair: 12 newRow.append(0) 13 pair = False 14 else: 15 if i + 1 < len(row) and row[i] == row[i+1]: 16 self.curScore += 2 * row[i] 17 newRow.append(2 * row[i]) 18 pair = True 19 else : 20 newRow.append(0) 21 return newRow 22 23 return tighten(merge(tighten(row)))
要进行整个棋盘的左移到这里已经显而易见了, 就是对于每一行的都调用一次moveRowLeft, 这时当然可以用相同的思路考虑右移动和上下移动, 但是思路相同不同方向的代码实现起来却很麻烦, 好在实验中为我们提供了更加简单的方法, 这里我们先引入基本的数学两个概念, 一个是置换(transpose), 一个是逆转(invert).
transpose :
[[1, 2, 3], [4, 5, 6]] ----> [[1, 4], [2, 5], [3, 6]]
invert :
[[1, 2, 3], [4, 5, 6]] ----> [[3, 2, 1], [6, 5, 4]]
这两个操作非常关键, 我们先用代码实现他们 :
def transpose(board): return [list(row) for row in zip(*board)] def invert(board): return [row[::-1] for row in board]
这两个操作有什么作用呢? 你可以这么想, 如果我们将棋盘进行逆转操作之后, 再对其进行左移操作, 再逆转一次, 是不是等效于完成了右移动? 如果我们将棋盘进行置换操作后, 对其进行左移操作, 再置换一次, 是不是等效于完成了上移动? 按照这个思路, 我们可以利用左移动来完成上下左右移动:
1 def move(self, direction): 2 def moveRowLeft(row): 3 def tighten(row): 4 newRow = [i for i in row if i != 0] 5 newRow += [0 for i in range(len(row) - len(newRow))] 6 return newRow 7 8 def merge(row): 9 pair = False 10 newRow = [] 11 for i in range(len(row)): 12 if pair: 13 newRow.append(0) 14 pair = False 15 else: 16 if i + 1 < len(row) and row[i] == row[i+1]: 17 self.curScore += 2 * row[i] 18 newRow.append(2 * row[i]) 19 pair = True 20 else : 21 newRow.append(0) 22 return newRow 23 24 return tighten(merge(tighten(row))) 25 26 moves = {} 27 moves[‘Left‘] = lambda board: [moveRowLeft(row) for row in board] 28 moves[‘Right‘] = lambda board: invert(moves[‘Left‘](invert(board))) 29 moves[‘Up‘] = lambda board: transpose(moves[‘Left‘](transpose(board))) 30 moves[‘Down‘] = lambda board: transpose(moves[‘Right‘](transpose(board))) 31 32 return moves[direction](self.board)
上面的代码看似是完成了, 但是转念一想其实又不对, 并不是每一次移动用户想要移动都能够完成移动, 什么意思呢? 比如说, 此时棋盘的16个格子全都非0, 同时在玩家想要移动的方向上并没有合并的地方, 那么此时不应该再移动棋盘, 而应该保持该状态(如果出现4个方向都无法合并, 在上一次移动结束之后就应该判定为游戏结束, 所以在此处不应该出现四个方向都不能合并的情况 ), 所以此时应该有一个判断条件.
1 def canMove(self, direction): 2 def canMoveLeft(row): 3 def change(i): 4 if row[i] == 0 and row[i + 1] != 0: 5 return True 6 if row[i] != 0 and row[i + 1] == row[i]: 7 return True 8 return False 9 return any(change(i) for i in range(len(row) - 1)) 10 11 check = {} 12 check[‘Left‘] = lambda board: any(canMoveLeft(row) for row in board) 13 check[‘Right‘] = lambda board: check[‘Left‘](invert(board)) 14 check[‘Up‘] = lambda board: check[‘Left‘](transpose(board)) 15 check[‘Down‘] = lambda board: check[‘Right‘](transpose(board)) 16 17 return check[direction](self.board)
那么之前的move()函数就应该这么写 :
1 def move(self, direction): 2 def moveRowLeft(row): 3 def tighten(row): 4 newRow = [i for i in row if i != 0] 5 newRow += [0 for i in range(len(row) - len(newRow))] 6 return newRow 7 8 def merge(row): 9 pair = False 10 newRow = [] 11 for i in range(len(row)): 12 if pair: 13 newRow.append(0) 14 pair = False 15 else: 16 if i + 1 < len(row) and row[i] == row[i+1]: 17 self.curScore = 2 * row[i] 18 newRow.append(self.curScore) 19 pair = True 20 else : 21 newRow.append(0) 22 return newRow 23 24 return tighten(merge(tighten(row))) 25 26 moves = {} 27 moves[‘Left‘] = lambda board: [moveRowLeft(row) for row in board] 28 moves[‘Right‘] = lambda board: invert(moves[‘Left‘](invert(board))) 29 moves[‘Up‘] = lambda board: transpose(moves[‘Left‘](transpose(board))) 30 moves[‘Down‘] = lambda board: transpose(moves[‘Right‘](transpose(board))) 31 32 if self.canMove(direction): 33 self.board = moves[direction](self.board) 34 self.spawn() 35 return True 36 else: 37 return False
最后我们还必须完成两个判断函数用来判断游戏结束和游戏胜利 :
def isWin(self): return self.curScore >= self.goal def isGameOver(self): return not any([self.canMove(direction) for direction in actions[:4]])
完成了这一部分之后, 游戏的内在执行已经是实现了, 最后一个就是画面的绘制, 这一部分有些地方我自己也不太懂, 不过不重要, 这道题的精华在于之前一步一步解决问题的思路, 所以我认为这一部分不太清除也无关紧要...
1 def draw(self, screen): 2 helpString1 = ‘(K)Up (J)Down (H)Left (L)Right‘ 3 helpString2 = ‘ (R)Restart (Q)Exit ‘ 4 gameOverString = ‘ Game Over !!!‘ 5 winString = ‘ YOU WIN!‘ 6 7 def cast(string): 8 screen.addstr(string + ‘\n‘) 9 10 def draw_hor_separator(): 11 line = ‘+‘ + (‘+------‘ * self.width + ‘+‘)[1:] 12 separator = defaultdict(lambda : line) 13 14 if not hasattr(draw_hor_separator, "counter"): 15 draw_hor_separator.counter = 0 16 cast(separator[draw_hor_separator.counter]) 17 draw_hor_separator.counter += 1 18 19 def draw_row(row): 20 cast("".join(‘|{: ^5} ‘.format(num) if num > 0 else ‘| ‘ for num in row) + ‘|‘) 21 22 screen.clear() 23 cast(‘SCORE: ‘ + str(self.curScore)) 24 if 0 != self.topScore: 25 cast(‘TOPSCORE: ‘ + str(self.highscore)) 26 27 for row in self.board: 28 draw_hor_separator() 29 draw_row(row) 30 31 draw_hor_separator() 32 33 if self.isWin(): 34 cast(winString) 35 else: 36 if self.isGameOver(): 37 cast(gameOverString) 38 else: 39 cast(helpString1) 40 cast(helpString2)
最后就是讲之前搭好的框架填充起来 :
def main(stdscr): def init(): #Todo : 初始化游戏画面 --> 完成 board.reset() return ‘Gaming‘ def game(): #Todo : 绘制游戏图象 --> 完成 board.draw(stdscr) action = getUserAction(stdscr)#Todo : 根据用户的输入转化为相应的动作 --> 完成 if action == ‘Restart‘: return ‘Init‘ if action == ‘Quit‘: return ‘Quit‘ if board.move(action): #执行移动动作 if board.isWin(): return ‘Win‘ if board.isGameOver(): return ‘Fail‘ return ‘Gaming‘ # 说明这一步移动并不会导致游戏结束, 那么继续游戏. def not_game(state): board.draw(stdscr) #Todo : 根据状态绘制游戏图象(表现出胜利或者失败的信息) --> 完成 action = getUserAction(stdscr)#Todo : 根据用户的输入转化为相应的动作 #下面这两行的作用就是, 除非action动作是restart或者init, 其他的任何动作都不会对当前状态造成任何影响. response = defaultdict(lambda : state) response[‘Restart‘], response[‘Quit‘] = ‘Init‘, ‘Quit‘ return response[action] #根据不同的状态返回相应的函数(对应做出相应的动作) stateAction = { ‘Init‘: init, ‘Win‘: lambda : not_game(‘Win‘), ‘GameOver‘: lambda :not_game(‘GameOver‘), ‘Gaming‘: game } curses.use_default_colors() #先测试一下得分到达32时会不会触发胜利条件 board = Board(goal=32) state = ‘Init‘ while state != ‘Quit‘: state = stateAction[state]() curses.wrapper(main)
然后是完整代码 :
import curses import random from collections import defaultdict actions = [‘Left‘, ‘Down‘, ‘Up‘, ‘Right‘, ‘Quit‘, "Restart"] key = [ord(ch) for ch in ‘hjklqrHJKLQR‘] KeyActionMap = dict(zip(key, actions * 2)) def getUserAction(keyboard): char = "N" while char not in KeyActionMap: char = keyboard.getch() return KeyActionMap[char] def transpose(board): return [list(row) for row in zip(*board)] def invert(board): return [row[::-1] for row in board] class Board(): def __init__(self, width=4, height=4, goal=1024): self.width = width self.height = height self.goal = goal self.curScore = 0 self.topScore = 0 self.reset() def reset(self): self.topScore = self.curScore if self.curScore > self.topScore else self.topScore self.curScore = 0 self.board = [[0 for i in range(self.width)] for j in range(self.height)] self.spawn() self.spawn() def spawn(self): new_grid = 4 if random.randrange(100) > 89 else 2 #决定是出现2还是4, 这个决定规则估计是固定的... (x, y) = random.choice([(x, y) for x in range(self.width) for y in range(self.height) if self.board[x][y] == 0]) self.board[x][y] = new_grid def move(self, direction): def moveRowLeft(row): def tighten(row): newRow = [i for i in row if i != 0] newRow += [0 for i in range(len(row) - len(newRow))] return newRow def merge(row): pair = False newRow = [] for i in range(len(row)): if pair: newRow.append(0) pair = False else: if i + 1 < len(row) and row[i] == row[i+1]: self.curScore += 2 * row[i] newRow.append(row[i] * 2) pair = True else : newRow.append(row[i]) return newRow return tighten(merge(tighten(row))) moves = {} moves[‘Left‘] = lambda board: [moveRowLeft(row) for row in board] moves[‘Right‘] = lambda board: invert(moves[‘Left‘](invert(board))) moves[‘Up‘] = lambda board: transpose(moves[‘Left‘](transpose(board))) moves[‘Down‘] = lambda board: transpose(moves[‘Right‘](transpose(board))) if self.canMove(direction): self.board = moves[direction](self.board) self.spawn() return True else: return False def canMove(self, direction): def canMoveLeft(row): def change(i): if row[i] == 0 and row[i + 1] != 0: return True if row[i] != 0 and row[i + 1] == row[i]: return True return False return any(change(i) for i in range(len(row) - 1)) check = {} check[‘Left‘] = lambda board: any(canMoveLeft(row) for row in board) check[‘Right‘] = lambda board: check[‘Left‘](invert(board)) check[‘Up‘] = lambda board: check[‘Left‘](transpose(board)) check[‘Down‘] = lambda board: check[‘Right‘](transpose(board)) return check[direction](self.board) def isWin(self): return self.curScore >= self.goal def isGameOver(self): return not any([self.canMove(direction) for direction in actions[:4]]) def draw(self, screen): helpString1 = ‘(K)Up (J)Down (H)Left (L)Right‘ helpString2 = ‘ (R)Restart (Q)Quit ‘ gameOverString = ‘ Game Over !!!‘ winString = ‘ YOU WIN!‘ def cast(string): screen.addstr(string + ‘\n‘) def draw_hor_separator(): line = ‘+‘ + (‘+------‘ * self.width + ‘+‘)[1:] 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): cast("".join(‘|{: ^5} ‘.format(num) if num > 0 else ‘| ‘ for num in row) + ‘|‘) screen.clear() cast(‘SCORE: ‘ + str(self.curScore)) if 0 != self.topScore: cast(‘TOPSCORE: ‘ + str(self.topScore)) for row in self.board: draw_hor_separator() draw_row(row) draw_hor_separator() if self.isWin(): cast(winString) else: if self.isGameOver(): cast(gameOverString) else: cast(helpString1) cast(helpString2) def main(stdscr): def init(): #Todo : 初始化游戏画面 --> 完成 board.reset() return ‘Gaming‘ def game(): #Todo : 绘制游戏图象 --> 完成 board.draw(stdscr) action = getUserAction(stdscr)#Todo : 根据用户的输入转化为相应的动作 --> 完成 if action == ‘Restart‘: return ‘Init‘ if action == ‘Quit‘: return ‘Quit‘ if board.move(action): #执行移动动作 if board.isWin(): return ‘Win‘ if board.isGameOver(): return ‘Fail‘ return ‘Gaming‘ # 说明这一步移动并不会导致游戏结束, 那么继续游戏. def not_game(state): board.draw(stdscr) #Todo : 根据状态绘制游戏图象(表现出胜利或者失败的信息) --> 完成 action = getUserAction(stdscr)#Todo : 根据用户的输入转化为相应的动作 #下面这两行的作用就是, 除非action动作是restart或者init, 其他的任何动作都不会对当前状态造成任何影响. response = defaultdict(lambda : state) response[‘Restart‘], response[‘Quit‘] = ‘Init‘, ‘Quit‘ return response[action] #根据不同的状态返回相应的函数(对应做出相应的动作) stateAction = { ‘Init‘: init, ‘Win‘: lambda : not_game(‘Win‘), ‘GameOver‘: lambda :not_game(‘GameOver‘), ‘Gaming‘: game } curses.use_default_colors() #先测试一下得分到达32时会不会触发胜利条件 board = Board(goal=32) state = ‘Init‘ while state != ‘Quit‘: state = stateAction[state]() curses.wrapper(main)
测试结果 ...
标签:
原文地址:http://www.cnblogs.com/nzhl/p/5602060.html