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下载。

原文地址:https://www.cnblogs.com/helay/p/7133936.html