用python实现命令行版2048

😼 Step 1. 看懂源代码

  • 由于是初学python,基础语法了解了一下后想找一个项目做做,但自己又做不出来,只好先模仿别人的项目先学习学习(ง •_•)ง。不料就这200行代码看起来也颇有难度>﹏<,很多用法比如any() assert join() format()都没看到过,最后还是靠着搜索引擎勉强看懂了(实验楼写得好简单粗暴)。我用注释形式大致解释了一下我一开始不懂的地方。

代码来源:实验楼——200行Python代码实现2048

import curses #curses是一个在Linux/Unix(Windows不支持)下广泛应用的图形函数库,作用是可以在终端内绘制简单的图形用户界面,其实就是一个把终端变成互动界面的模块
from random import randrange, choice
from collections import defaultdict

letter_codes = [ord(ch) for ch in 'WASDRQwasdrq'] #ord(ch)是将ch转换成对应的数字(ASCⅡ码)
actions = ['Up', 'Left', 'Down', 'Right', 'Restart', 'Exit']
actions_dict = dict(zip(letter_codes, actions * 2)) #使用zip函数, 把key和value的list组合在一起, 生成一个元组列表,再转成字典(dict)

def get_user_action(keyboard):
    '''将用户按的键转换成对应的action'''
    char = "N"
    while char not in actions_dict: #检查字典里是否包含键
        char = keyboard.getch()
    return actions_dict[char]

def transpose(field):
    return [list(row) for row in zip(*field)]  #将field列表“解压缩”,表现为列表转了90度

def invert(field):
    return [row[::-1] for row in field]  #将列表的每行左右倒一下。列表双冒号的含义:list[start:end:step]

class GameField(object):
    '''与游戏界面有关的类'''
    def __init__(self, height=4, width=4, win=2048): #创建实例时初始化界面属性
        self.height = height
        self.width = width
        self.win_value = win
        self.score = 0
        self.highscore = 0
        self.reset()

    def reset(self):
        if self.score > self.highscore:
            self.highscore = self.score
        self.score = 0
        self.field = [[0 for i in range(self.width)] for j in range(self.height)] #创建二阶列表,每个元素初始化为0
        #随机出现2个数字
        self.spawn() 
        self.spawn()

    def move(self, direction):
        def move_row_left(row):
            def tighten(row): # squeese non-zero elements together
                new_row = [i for i in row if i != 0]
                new_row += [0 for i in range(len(row) - len(new_row))]    #列表相加操作
                return new_row

            def merge(row):   # merge
                pair = False
                new_row = []
                for i in range(len(row)):
                    if pair:
                        new_row.append(2 * row[i])
                        self.score += 2 * row[i]
                        pair = False
                    else:
                        if i + 1 < len(row) and row[i] == row[i + 1]:
                            pair = True
                            new_row.append(0)
                        else:
                            new_row.append(row[i])
                assert len(new_row) == len(row) #检查条件,不符合就终止程序
                return new_row
            return tighten(merge(tighten(row))) #先tighten再merge再tighten,将最终得到的row列表返回

        moves = {} #将上下左右的动作与函数挂钩
        moves['Left']  = lambda field:                              
                [move_row_left(row) for row in field]
        moves['Right'] = lambda field:                              
                invert(moves['Left'](invert(field)))
        moves['Up']    = lambda field:                              
                transpose(moves['Left'](transpose(field)))
        moves['Down']  = lambda field:                              
                transpose(moves['Right'](transpose(field)))

        if direction in moves: #如果用户按的键名称在moves的键中
            if self.move_is_possible(direction): #如果可移动
                self.field = moves[direction](self.field)
                self.spawn()
                return True
            else:
                return False

    def is_win(self):
        #any()函数用于判断给定的可迭代参数iterable(元组或列表)是否全部为False,则返回False,如果有一个为True,则返回True
        return any(any(i >= self.win_value for i in row) for row in self.field)

    def is_gameover(self):
        return not any(self.move_is_possible(move) for move in actions)

    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 cast(string):
            screen.addstr(string + '\n') #screen.addstr()向屏幕输出字符串

        def draw_hor_separator():
            line = '+' + ('+------' * self.width + '+')[1:]
            #当key不存在时可以将value设为默认值而不是引发error,defaultdict()参数为函数或类型(类型其实大多是工厂函数),这里是匿名函数
            separator = defaultdict(lambda: line) 
            if not hasattr(draw_hor_separator, "counter"): #hasattr()函数用于判断对象是否包含对应的属性,再py里一切皆象
                draw_hor_separator.counter = 0
            cast(separator[draw_hor_separator.counter])
            draw_hor_separator.counter += 1

        def draw_row(row):
            #注意join()和format()的用法,大括号中的意思是 中间对齐,宽度为5
            cast(''.join('|{: ^5} '.format(num) if num > 0 else '|      ' for num in row) + '|')

        screen.clear() #绘制前需先清屏(刷新)
        cast('SCORE: ' + str(self.score))
        if 0 != self.highscore:
            cast('HIGHSCORE: ' + str(self.highscore))
        for row in self.field:
            draw_hor_separator()
            draw_row(row)
        draw_hor_separator()
        if self.is_win(): #绘制好之后判断是否win
            cast(win_string)
        else:
            if self.is_gameover():
                cast(gameover_string)
            else:
                cast(help_string1)
        cast(help_string2)

    def spawn(self):
        new_element = 4 if randrange(100) > 89 else 2 #randrange ([start,] stop [,step])从给点范围内返回随即项
        #choice()方法返回一个列表,元组或字符串的随机项,这里是返回元组列表的随机项
        (i,j) = choice([(i,j) for i in range(self.width) for j in range(self.height) if self.field[i][j] == 0])
        self.field[i][j] = new_element

    def move_is_possible(self, direction):
        def row_is_left_movable(row): 
            def change(i): # true if there'll be change in i-th tile
                if row[i] == 0 and row[i + 1] != 0: # Move
                    return True
                if row[i] != 0 and row[i + 1] == row[i]: # Merge
                    return True
                return False
            return any(change(i) for i in range(len(row) - 1))

        check = {}
        check['Left']  = lambda field:                              
                any(row_is_left_movable(row) for row in field)

        check['Right'] = lambda field:                              
                 check['Left'](invert(field))

        check['Up']    = lambda field:                              
                check['Left'](transpose(field))

        check['Down']  = lambda field:                              
                check['Right'](transpose(field))

        if direction in check:
            return check[direction](self.field)
        else:
            return False

def main(stdscr):  #standartinput,draw()和get_user_input()函数会用到
    '''程序主逻辑'''
    def init():
        #重置游戏棋盘
        game_field.reset()
        return 'Game'

    def not_game(state):
        #画出 GameOver 或者 Win 的界面
        game_field.draw(stdscr)
        #读取用户输入得到action,判断是重启游戏还是结束游戏
        action = get_user_action(stdscr)
        responses = defaultdict(lambda: state) #默认是当前状态,没有行为就会一直在当前界面循环
        responses['Restart'], responses['Exit'] = 'Init', 'Exit' #对应不同的行为转换到不同的状态
        return responses[action]

    def game():
        #画出当前棋盘状态
        game_field.draw(stdscr)
        #读取用户输入得到action
        action = get_user_action(stdscr)

        if action == 'Restart':
            return 'Init'
        if action == 'Exit':
            return 'Exit'
        if game_field.move(action): # 根据action值执行相关操作
            if game_field.is_win():
                return 'Win'
            if game_field.is_gameover():
                return 'Gameover'
        return 'Game'

    #将状态与函数挂钩,以免大量用if语句
    state_actions = {
            'Init': init,
            'Win': lambda: not_game('Win'),
            'Gameover': lambda: not_game('Gameover'),
            'Game': game,
        }
    #设置默认的终端背景
    curses.use_default_colors()   
    #创建类GameField的实例,并设置终结状态最大数值为 32
    game_field = GameField(win=32)
    state = 'Init'
    #状态机开始循环
    while state != 'Exit':
        state = state_actions[state]()

curses.wrapper(main) #这段代码进入curses界面,对main函数赋参整个窗口对象screen。接着执行main函数里的内容
  • 程序主要分为监测游戏状态、画出游戏界面、移动、判断是否win和gameover四大部分
  • 有限状态机(Finite-state machine, FSM),又称有限状态自动机,简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。FSM是一种算法思想,简单而言,有限状态机由一组状态、一个初始状态、输入和根据输入及现有状态转换为下一个状态的转换函数组成。

  • curses库到底是什么:https://docs.python.org/dev/howto/curses.html

🏈 Step 2. 自我尝试

  • 看一遍没什么大用,自己敲一遍(当然不是抄一遍)才有些许用场。
import curses
from random import randrange, choice
from collections import defaultdict

key_letters = "WASDQRwasdqr"
key_letter_codes = [ord(c) for c in key_letters]
actions = ['up', 'left', 'down', 'right', 'Exit', 'Restart']
key_actions = dict(zip(key_letter_codes, actions * 2))

def get_user_action(keyboard):
    k = ''
    while k not in key_actions:
        k = keyboard.getch()
    return key_actions[k]

class Settings:
    def __init__(self, win=2048):
        self.width = 4
        self.height = 4
        self.score = 0
        self.highscore = 0
        self.win_value = win
        self.field = [[0 for i in range(self.width)] for j in range(self.height)]

setting = Settings()

def reset():
    if setting.highscore < setting.score:
        setting.highscore = setting.score
    setting.score = 0
    setting.field = [[0 for i in range(setting.width)] for j in range(setting.height)]
    spawn()
    spawn()

def draw(screen):
    gameover_string = '           GAME OVER'
    win_string = '          YOU WIN!'

    def draw_hor_separator():
        screen.addstr('-----' * setting.width + '\n')

    def draw_row(row):
        screen.addstr(''.join('|{:^4}'.format(num) if num > 0 else '|    ' for num in row) + '|' + '\n')

    screen.clear()
    screen.addstr('score: ' + str(setting.score) + '    ' + 'high score: ' + str(setting.score) + '\n')
    for i in range(setting.height):
        draw_hor_separator()
        draw_row(setting.field[i])
    draw_hor_separator()

    if is_win(): screen.addstr(win_string)
    if is_gameover(): screen.addstr(gameover_string)

def spawn():
    new_element = 4 if randrange(100) > 89 else 2
    (i, j) = choice([(i, j) for i in range(setting.width) for j in range(setting.height) if setting.field[i][j] == 0])
    setting.field[i][j] = new_element

def invert(field):
    setting.field = [row[::-1] for row in field]

def transpose(field):
    setting.field = [list(row) for row in zip(*field)]

def move_is_possible(direction):
    def row_is_left_possible(row):
        for i in range(setting.width - 1):
            if row[i] == 0 and row[i + 1] != 0:
                return True
            if row[i] == row[i + 1]:
                return True
        return False

    if direction == 'left':
        return any([row_is_left_possible(row) for row in setting.field])
    if direction == 'right':
        invert(setting.field)
        field = setting.field
        invert(setting.field)
        return any([row_is_left_possible(row) for row in field])
    if direction == 'up':
        transpose(setting.field)
        field = setting.field
        transpose(setting.field)
        return any([row_is_left_possible(row) for row in field])
    if direction == 'down':
        transpose(setting.field)
        invert(setting.field)
        field = setting.field
        invert(setting.field)
        transpose(setting.field)
        return any([row_is_left_possible(row) for row in field])

def move(action):
    def move_left():
        def merge(row_m):
            for i in range(setting.width):
                if i + 1 < 4 and row_m[i] == row_m[i + 1]:
                    row_m[i + 1] = 2 * row[i]
                    row_m[i] = 0

        def tighten(row_t):
            for i in range(setting.width):
                if row_t[i] == 0:
                    next_i = i + 1
                    while next_i < 4:
                        if row_t[next_i] != 0:
                            row_t[i] = row_t[next_i]
                            row_t[next_i] = 0
                            break
                        next_i = next_i + 1

        for row in setting.field:
            tighten(row)
            merge(row)
            tighten(row)

    if action in actions[:4]:
        if move_is_possible(action):
            if action == 'left':
                move_left()
            if action == 'right':
                invert(setting.field)
                move_left()
                invert(setting.field)
            if action == 'up':
                transpose(setting.field)
                move_left()
                transpose(setting.field)
            if action == 'down':
                transpose(setting.field)
                invert(setting.field)
                move_left()
                invert(setting.field)
                transpose(setting.field)
            spawn()
            return True
        else:
            return False

def is_win():
    return any(any(i >= setting.win_value for i in row) for row in setting.field)

def is_gameover():
    return not any(move_is_possible(move_i) for move_i in actions)

def main(stdscr):
    def init():
        reset()
        return 'Game'

    def not_game(state_n):
        draw(stdscr)
        action = get_user_action(stdscr)
        responses = defaultdict(lambda: state_n)
        responses['Exit'], responses['Restart'] = 'Exit', 'Init'
        return responses[action]

    def game():
        draw(stdscr)
        action = get_user_action(stdscr)
        if action == 'Exit':
            return 'Exit'
        if action == 'Restart':
            return 'Init'
        if move(action):
            if is_win():
                return 'Win'
            if is_gameover():
                return 'Gameover'
        return 'Game'

    state = 'Init'
    state_funcs = {
        'Init': init,
        'Game': game,
        'Win': lambda: not_game('Win'),
        'Gameover': lambda: not_game('Gameover'),
    }
    while state != 'Exit':
        state = state_funcs[state]()

if __name__ == '__main__':
    curses.wrapper(main)
  • 有些是照着自己的思路写的,改动了一下,但基本思想和程序流程是一样的

🎂 注:

  • windows上安装curses库:https://www.lfd.uci.edu/~gohlke/pythonlibs/#curses 下载curses-2.2.1+utf8-cp37-cp37m-win32.whl(python版本3.7则选37,虽然我的电脑是64位的,但报错,可能不是amd64的原因),再使用pip install curses-2.2+utf8-cp37-cp37m-win32.whl进行安装即可
  • 无论Linux上还是windows上,pycharm都运行不了,一个是没找到curses库,一个是没找到terminal...命令行下运行就正常了

20190731

results matching ""

    No results matching ""