zoukankan      html  css  js  c++  java
  • Python小项目三:用curses实现2048 标签: 2048python实验楼游戏 2017-07-01 22:12 50人阅读 评

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

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

  • 相关阅读:
    C++ 虚函数表解析
    Zend Studio使用
    Java的位运算符具体解释实例——与(&amp;)、非(~)、或(|)、异或(^)
    史上最简单的Hibernate入门简单介绍
    Ubuntu下很给力的下载工具
    hdu1698 Just a Hook 线段树:成段替换,总区间求和
    STL vector使用方法介绍
    Linux守护进程的编程实现
    PDO--PHP Data Objects
    经常使用的android弹出对话框
  • 原文地址:https://www.cnblogs.com/helay/p/7133936.html
Copyright © 2011-2022 走看看