zoukankan      html  css  js  c++  java
  • 终端游戏开发 : 开发2048...

    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 + '
    ')
     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 + '
    ')
    
            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)

    测试结果 ...

  • 相关阅读:
    RabbitMq(四)远程过程调用RPC
    RabbitMq(三)交换机类型
    RabbitMq(二)工作队列
    java基础知识01--JAVA准备
    匿名子类
    网络之Socket详解
    网络之Socket、TCP/IP、Http关系分析
    Eclipse搭建springboot项目(九)常用Starter和整合模板引擎thymeleaf
    Vue学习——Router传参问题
    sql函数——find_in_set()
  • 原文地址:https://www.cnblogs.com/nzhl/p/5602060.html
Copyright © 2011-2022 走看看