Python小项目三:用curses实现2048

来源:互联网 发布:少儿编程培训内容 编辑:程序博客网 时间:2024/06/11 03:37

本博文是对实验楼课程学习的笔记,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_actiongame():    执行操作,    判断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('字符串' + '\n')来绘制。

    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] + '\n')                        draw_line.counter += 1            #screen.addstr('+' + "------+" * 4 + '\n')                 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) + '|'  + '\n')                        #开始draw        screen.clear()                screen.addstr('SCORE: 0' +  '\n')                for row in self.field:                        draw_line()            draw_nums(row)                draw_line()                if self.win ==1:                        screen.addstr(win_string + '\n')                elif self.gameover == 1:                        screen.addstr(gameover_string + '\n')                    else:                        screen.addstr(help_string1 +  '\n')                screen.addstr(help_string2 +  '\n')
绘制前应先得到期盼数字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 npimport cursesfrom random import randrange, choicefrom 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] + '\n')                        draw_line.counter += 1            #screen.addstr('+' + "------+" * 4 + '\n')                 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) + '|'  + '\n')                        #开始draw        screen.clear()                screen.addstr('SCORE: 0' +  '\n')                for row in self.field:                        draw_line()            draw_nums(row)                draw_line()                if self.win ==1:                        screen.addstr(win_string + '\n')                elif self.gameover == 1:                        screen.addstr(gameover_string + '\n')                    else:                        screen.addstr(help_string1 +  '\n')                screen.addstr(help_string2 +  '\n')                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下载。