终端游戏开发 : 开发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)

测试结果 ...

原文地址:https://www.cnblogs.com/nzhl/p/5602060.html