python练手项目:2048实现与总结

python练手项目:利用curses界面对2048的实现与总结

  • 涉及的知识点
  • 基本实现
    • UI展示
      • curses的初始化方法:wrapper
      • curses的屏幕展示语句:addscr
      • curses正式开画
    • 游戏运行引擎
      • 引擎分析
      • 从“程序运行”到“状态名称”
      • 从“状态名称”到“程序运行”
    • 游戏数据的生成
      • 数据框架的初始化
      • 棋盘主数据初始化生成
      • 随机生成功能
    • 游戏数据的演变
      • 左、右、上、下的移动
      • 用户的输入
      • 游戏胜负的判断
      • 能否移动的判断
  • 全部代码

本练手项目参考自实验楼:200 行 Python 代码实现 2048

涉及的知识点

  1. curses模块
    curses是一个python模块,它需要额外下载。
    这个模块可以实现文本展示。本程序的2048界面就是依赖curses“画”出来的。
    参考:Python Curses
  2. random模块
    random是python自带的模块。
    这个模块可以生成随机整数(randint)、随机小数(uniform)、在递增数列中随机选数(randrange)、在序列中选取随机元素(choice)、随机取序列的子集(sample)等。本例中使用是randrange、choice两个方法。
    参考:Python random模块(获取随机数)常用方法和使用例子
  3. collections模块
    collections是Python内建的一个集合模块,提供了许多有用的集合类。你可以理解成一个杂货铺。
    这个模块包括设置字典默认值(defaultdict)、字符计数(Counter)、双链表(deque)等方法。本例中使用了defaultdict方法
    参考:Python中collections的用法
  4. 状态机类程序的设计与实现
  5. lambda匿名函数
    lambda和普通的函数相比,就是省去了函数名称而已。好处是:
    (1) 省去定义函数的过程、例代码精简。
    (2) 对于不需要重用的函数,一次性定义,反而不用考虑如何命名,即时性很好。
    参考:关于Python中的lambda
  6. for遍历的灵活使用。
  7. 列表生成式: 列表生成式可以十分简便地生成一个序列,如一条语句生成平方序列:[x*x for x in range(1 , 11)]
  8. python切片的使用: b = a[::-1]实现了序列a的逆序排列。
  9. 其它函数
    – ord(‘a’): 将字符串转化为acsii码对应数字
    – zip(list1, list2): 将列表list1、list2中的元素分别对应取出,构造成元组。即映射。本例中,将zip巧用,“分别取出”即实现了由行取列,矩阵的转置也随即完成。
    – range(): range(5) <==>range(0,5,1).即以0开始,以小于5结束,以步长为1取值。
    – any(): 全或判断,即,对于any(iterable) ,如果iterable 中有一个为 True,则返回 True,只有在全部为 False时返回 False。

基本实现

通过分析,实现一个2048游戏,一共包含UI展示、游戏运行引擎、游戏数据的生成、游戏数据的演变等4个部分。

UI展示

本实验中,UI界面是通过curses模块实现的。

curses的初始化方法:wrapper

经常在导入curses模块以后,使用initscr()方法进行初始化。下面是初始化举例。

import curses
stdscr = curses.initscr()
# stdscr指代的就是显示器本器了。

完成初始化之后,还应当使用init_pair()等方法进行屏幕背景颜色、字体阴影颜色的设定。不过,若是不准备对curses界面进行精确控制、采用默认颜色即可的话,可以使用:wrapper方法。看如下对比:

import curses
def main1(stdscr)
    pass
curses.wrapper(main1)

# 上面的语句等价于:
import curses
def main2(stdscr)
    pass
stdscr = curses.initscr()
main2(stdscr)

结论:wrapper(main1)函数不仅会执行一次main1函数、完成curses的初始化,还会将初始化得到的stdscr强行传递给main1作为main1的参数。

curses的屏幕展示语句:addscr

我们的程序,主要是靠addscr这一方法来“绘制”,它的基本语法是:

stdscr.addscr(y, x, str or ch, attr)
# y,x 代表绘制的坐标。可省略
# str代表绘制的内容
# attr代表绘制时指定的属性。可省略。

理解基本的语法后,可以试着展示一条语句:、

import curses
def main1(stdscr):
    stdscr.addstr('hello world')
    ch=stdscr.getch() # 本句是为了使画面暂停。
curses.wrapper(main1)

输出如下:
在这里插入图片描述

curses正式开画

界面可分为得分、棋盘、信息提示,其中棋盘又包括画横线、画竖线。

  1. 准备函数
    screen即stdscr。screen.addstr()太过冗长,故重写,以简化。

    def cast(string):
        # 对addstr操作作简化
        screen.addstr(string + '\n')
    
  2. 得分

    screen.clear() # 清屏
    cast('SCORE: ' + str(self.score))
    cast('HIGHSCORE: ' + str(self.highscore))
    
  3. 棋盘
    画分割线:
    本例所画侵害线并非必须。简单地,我们可以有:

    def draw_hor_separator():
        line = ('+-----' * self.width + '+')
        cast(line)
    

    实验中给出的代码较复杂,可以作参考:

    def draw_hor_separator():
        line = '+' + ('+------' * self.width + '+')[1:]
        	#[1:]是python切片的使用,表示从1开始,这里的作用是省略掉'++---+--..'中的第一个'+'。
        	#对于x='abced',有:x[0]=='a'、x[1]=='b'
        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
    

    画一行:
    对于已经给定信息的每一行,我们需要的内容包括竖分割线、给定的数字。欲实现这些内容,都回归到“是填充数字还是填充空白”这一问题上来。
    这里,利用str.format()及类似{:^10d}的语句(居中并占10个空)来处理数字与空白间的关系。

    def draw_row(row):
        s = ''
        for i in row:
            if i > 0:
                s += '|{: ^6}'.format(i)
            else:
                s += '|      '
        s += '|' # 收尾分割符
        cast(s)
    

    当然,为了更加pythonic,我们可以将if-else合并:

    def draw_row(row):
        s = ''
        for i in row:
            s += '|{: ^6}'.format(i) if i != 0 else '|      '
        s += '|' # 收尾分割符
        cast(s)
    

    还有更pythonic的可能:join()方法与列表生成式的结合使用。

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

    画出整个二维棋盘
    拥有画出一行的能力以后,让这个画的动作遍历棋盘

    for row in self.field:
        draw_hor_separator()
        draw_row(row)
    draw_hor_separator() # 补最后一行分割线。
    
  4. 信息提示
    提示用户可执行的操作和当前状态。在不同情况下有不同显示内容,伪代码如下:

    如果胜利(附带判断):
      输出“你赢了“
    如果失败(附带判断):
      输出”你输了“
    如果正常进行游戏(既非赢也非输):
      输出游戏操作提示
    
    显示一些需要常态显示的提示
    

    代码实现如下(胜负判断稍后完成):

    help_string1 = '(W)Up (S)Down (A)Left (D)Right'
    help_string2 = '     (R)Restart  (Q)Exit'
    gameover_string = '            GAME OVER'
    win_string = '            YOU WIN!'
    
    if self.is_win():           # 如果胜利(附带判断):
        cast(win_string)        #    输出“你赢了”
    elif self.is_gameover():    # 如果失败(附带判断):
        cast(gameover_string)   #    输出“你输了”
    else:                       # 既非赢也非输:
        cast(help_string1)      #    输出游戏操作提示
    
    cast(help_string2)          # 需要常态显示的提示
    

游戏运行引擎

引擎分析

从打开这个2048游戏的角度来讲,游戏共区别为初始化(Init)、游戏中(Game)、游戏胜利(Win)、游戏失败(Gameover)4个状态。它们可以如下图相互转化:
python练手项目:2048实现与总结_第1张图片
实现这样的转化,我们既要这个状态的名称、又要这个状态能通过某种方式执行。字典的特性完美地帮助了我们。

    state_actions = {
            'Init': init,
            'Win': lambda: not_game('Win'),
            'Gameover': lambda: not_game('Gameover'),
            'Game': game,
            }

字典的“键”以字符串的形式存储了状态名称,字典的“健值”甚至可以存储函数名。
配合函数的返回功能,状态机就在“程序运行”-“状态名称”-“程序运行”间相互转换。

从“程序运行”到“状态名称”

这一步,由return来实现,我们需要确保的,就是return返回的字符串刚好是state_actions 里面的键值。

    def init():
        # 重置
        print("do init.")
        return 'Game'
        # 上面的状态图告诉我们,这里只会返回到Game状态

根据状态图,我们可以有以下程序。

    def game():
        # 展示出当前画面,接收用户输入,并根据用户输入,反馈出继续游戏(Game)、游戏胜利(Win)、游戏失败(Gameover)三个结果。
        # 画出当前画面
        print("do draw")
        # 读取用户输入得到action
        action = print("get user's action")
        
        if action == 'Restart':
            return 'Init'
        elif action == 'Exit':
            return 'Exit'
        elif game_field.move(action):
        	# 执行一次移动并成功,判断是否胜利
            if game_field.is_win() :
                return 'Win'
            if game_field.is_gameover() :
                return 'Gameover'
        else:
        	# 移不动,保持现有原状
        	return 'Game'

从“状态名称”到“程序运行”

有了状态名称,执行程序只需要一步:读取state_actions状态名称对应的键值:

    state = 'Init' # 初始化语句
	while state != 'Exit':
        state = state_actions[state]()

游戏数据的生成

游戏数据包括以下内容:
1.棋盘的数据框架:棋盘大小定义、棋盘主数据(4X4数组)、起始与结束分数、最高分。
2.棋盘主数据:带有数值的4X4二维数组(列表)。
3.随机生成功能:初始时,某2个位置填充2或4。

数据框架的初始化

    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)]
        assert_equal(self.field[1][1], 0)
        assert_equal(type(self.field[0][0]), type(0))
        # 2个随机位置的数的生成
        self.spawn()
        self.spawn()

随机生成功能

随机生成功能有2部分:

  1. 生成数:生成2或4。
  2. 生成位置:位置随机,且不能有其它数占用。
    def spawn(self):
        ''' 生成随机数 '''
        # 生成数:以0.1的概率生成4,以0.9概率生成2,并放入随机位置
        if randrange(100) > 89:
            new_element = 4
        else:
            new_element = 2
        # 上面的语句也可以这样更加pythonic:            
        # new_element = 4 if randrange(100) > 89 else 2

        # 数的安置:
        i = randrange(self.width)
        j = randrange(self.height)
        while self.field[i][j] != 0:
            i = randrange(self.width)
            j = randrange(self.height)
        # 上面的语句也可以这样更加pythonic:
        # (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

游戏数据的演变

游戏数据的演变包括:
一、左、右、上、下的移动、相加合成。
二、用户的输入。
三、游戏胜负的判断。

左、右、上、下的移动

一、典型动作的分析

这里不妨用典型动作,一列向左,来分析。
数据的移动可以用三个动作实现。一是紧凑,二是相加,三是再紧凑。
1.紧凑
在紧凑过程中,可以构造空的一列,遇到有意义的数字,就将旧列赋值给新列。否则,跳过新列的当前元素,判断下一个。最后,再对新列填空,以使新旧2个列长度相等。

def move(self, direction):
	def move_row_left(row):
		def tighten(row):
	    	''' 数据紧凑 '''
		    new_row = []
		    # 对有意义的数字赋值:
		    for i in row:
		        if i != 0:
		            new_row.append(i)
		        else:
		            pass
		    # 对无意义的数字填空:
		    for i in range(len(row) - len(new_row)):
		    	new_row.append(0)
	        
			# 有更加pythonic的语句吗?答案是有的:
		    # new_row = [i for i in row if i != 0]
		    # new_row += [0 for i in (len(row) - len(new_row))]
		    
		    return new_row

2.相加
紧凑完成之后,如果相信两个数字相同,则后一数字加至前一数字上,且后一数字置0.

def move(self, direction):
	def move_row_left(row):
		def merge(row):
			for i in range(len(row)):
				if row[i] == row[i - 1] and i > 0:
					row[i - 1] += row[i]
					row[i] = 0
					self.score += row[i - 1]
			return row

3.完成向左移动

def move(self, direction):
	def move_row_left(row):
		return tighten(merge(tighten(row)))

二、向右移动的转化
对一列作向右移动,可看作对一列逆序排列后,进行向左转化,即:取逆还原(向左移动(取逆(row)))。直观地,为实现取逆我们会想到从最后一个元素开始读取,依次赋值给新列。python的切片操作能够减少这一过程的代码量:

	row = [1,2,3]
	new_row = row[::-1]
	print(new_row)
	输出为:[3,2,1]

对于整个filed,可以有:

def invert(field):
	new_field = [row[::-1] for row in filed]
	# 记住我们在def reset(self)中定义的,field是一个4X4二维列表
	return new_field

三、向上、下移动的转化
上、下移动,可以想办法将矩阵转置,进而上、下移动的问题变成左右移动的问题。刚刚好,zip(list1, list2)函数为我们提供的解决方案。
zip函数的举例:

a = [1,2,3]
b = [4,5,6]
c = [7,8,9,10,11]

# 将d中的元素(3个元组)转化为列表,整个f形成一个二维列表
f = []
for i in zip(a,b,c):
    f.append(list(i)) # 如果不加list(i),将只能输出元组
for i in f:
    print(i)
# f:
# [1, 4, 7]
# [2, 5, 8]
# [3, 6, 9]

# 将二维列表解压
h = zip(*f)
print('\nh:')
for i in h:
    print(i) # 这次没有加list(i)
# h:
# (1, 2, 3)
# (4, 5, 6)
# (7, 8, 9)

转置函数:

def transpose(field):
	''' 转置函数 从列表到列表 '''
	new_field = [list(row) for row in zip(*field)]
	return new_field

四、具体移动动作

class GameField(object):
	def move(self, direction):
	    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)))

用户的输入

用户的输入必须在有限状态内进行。如果在有限状态内,则执行,否则就放弃。

actions = ['Up', 'Left', 'Down', 'Right', 'Restart', 'Exit']
letter_codes = [ord(ch) for ch in 'WASDRQwasdrq']
action_dict = dict(zip(letter_codes, actions *2))
def get_user_action(keyboard):
    char = 'N'
    while char not in action_dict:
        char = keyboard.getch()
    return action_dict[char]
class GameField(object):
    def move(self, direction):
        if direction in moves:
            if self.move_is_possible(direction):
                self.field = moves[direction](self.field)
                self.spawn()
                return True
            else:
                return False

游戏胜负的判断

若整个棋盘内出现2048这个数字,即胜。
若整个棋盘不能再移动,即负。
对整个棋盘作判断,可由any()函数帮助我们遍历和判断。

class GameField(object):
	def is_win():
		return any(any(i >= self.win_value for i in row)for row in self.filed)
	def is_gameover():
	    return not any(各方向移动情况)

这里,各方向移动情况,还需要我们完成“能否移动”的判断

能否移动的判断

依旧分析向“左”移动。一列能否移动,主要看其能否相加、是否有0位。所以我们可以有:

class GameField(object):
	def row_is_left_movable(row):
		for i in range(len(row)):
			if row[i] == 0:
				return True
            elif i > 0 and row[i] == row[i-1]:
                return True
        return False

原实验提供了以下代码,以供比较、参考:

class GameField(object):
	def row_is_left_movable(row):
	     def change(i):
	         if row[i] == 0 and row[i + 1] != 0:
	             return True
	         elif row[i] != 0 and row[i + 1] == row[i]:
	             return True
	         else:
	             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))

理解程序整体结构之后,再将前方中伪代码部分进行实现,程序即完成。

全部代码

import curses
# from nose.tools import *
from random import randrange, choice
from collections import defaultdict

actions = ['Up', 'Left', 'Down', 'Right', 'Restart', 'Exit']
letter_codes = [ord(ch) for ch in 'WASDRQwasdrq']
action_dict = dict(zip(letter_codes, actions *2))

def main(stdscr):
    def init():
        # 重置
        game_field.reset()
        return 'Game'

    def not_game(state):
        # 给出画面 Gameover 或 Win
        game_field.draw(stdscr)
        # 读取用户输入,给出正常游戏还是结束游戏
        action = get_user_action(stdscr)
        responses = defaultdict(lambda: state)
        # 对于lambda,直接去掉可以不?responses = defaultdict(lambda: state)
          # 默认为当前状态

        responses['Restart'], responses['Exit'] = 'Init', 'Exit'
        return responses[action]

    def game():
        # 展示出当前画面,接收用户输入,并根据用户输入,反馈出继续游戏(Game)、游戏胜利(Win)、游戏失败(Gameover)三个结果。n
        # 画出当前画面
        game_field.draw(stdscr)
        # 读取用户输入
        action = get_user_action(stdscr)
        if action == 'Restart':
            return 'Init'
        elif action == 'Exit':
            return 'Exit'

        elif game_field.move(action):# 执行一次移动并成功
            if game_field.is_win() :
                return 'Win'
            if game_field.is_gameover() :
                return 'Gameover'
        return 'Game'

    state_actions = {
            'Init': init,
            'Win': lambda: not_game('Win'),
            'Gameover': lambda: not_game('Gameover'),
            'Game': game,
            }

    curses.use_default_colors()
    game_field = GameField(win = 2048)

    state = 'Init'

    # 状态机开始
    while state != 'Exit':
        state = state_actions[state]()

def get_user_action(keyboard):
    char = 'N'
    while char not in action_dict:
        char = keyboard.getch()
    return action_dict[char]

def transpose(field):
    # 矩阵行转列
    # 输入field 4*4的矩阵,输出一个列表,这个列表中每个元素,都是一个元组。再将里面的每一个元组,转化为列表。
    return [list(row) for row in zip(*field)]

def invert(field):
    # 矩阵逆转置
    return [row[::-1] for row in field]

class GameField(object):
    """ GameField."""
    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 spawn(self):
        ''' 生成随机数 '''
        # 数的生成:以0.1的概率生成4,以0.9概率生成2,并放入随机位置
        if randrange(100) > 50:
            new_element = 4
        else:
            new_element = 2
        # 上面的语句也可以这样更加pythonic:
        # new_element = 4 if randrange(100) > 89 else 2

        # 数的安置:
        i = randrange(self.width)
        j = randrange(self.height)
        while self.field[i][j] != 0:
            i = randrange(self.width)
            j = randrange(self.height)
        # 上面的语句也可以这样更加pythonic:
        # (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 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)]
        # 生成两个数
        self.spawn()
        self.spawn()

    def is_win(self):
        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 move(self, direction):
        def move_row_left(row):
            def tighten(row):
                ''' 数据紧凑 '''
                new_row = []
                # 对有意义的数字赋值:
                for i in row:
                    if i != 0:
                        new_row.append(i)
                    else:
                        pass
                # 对无意义的数字填空:
                for i in range(len(row) - len(new_row)):
                    new_row.append(0)

                # 有更加pythonic的语句吗?答案是有的:
                # new_row = [i for i in row if i != 0]
                # new_row += [0 for i in (len(row) - len(new_row))]
                return new_row

            def merge(row):
                # 可以合并的,进行合并
                for i in range(len(row)):
                    if row[i] == row[i - 1] and i > 0:
                        row[i - 1] += row[i]
                        row[i] = 0
                        self.score += row[i - 1]
                return row

            # 先挤,后合并,再挤
            return 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:
            if self.move_is_possible(direction):
                self.field = moves[direction](self.field)
                self.spawn()
                return True
            else:
                return False

    def move_is_possible(self, direction):
        def row_is_left_movable(row):
            for i in range(len(row)):
                if row[i] == 0:
                    return True
                elif i > 0 and row[i] == row[i-1]:
                    return True
            return False
        # def row_is_left_movable(row):
        #     def change(i):
        #         if row[i] == 0 and row[i + 1] != 0:
        #             return True
        #         elif row[i] != 0 and row[i + 1] == row[i]:
        #             return True
        #         else:
        #             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 draw(self, screen):
        def cast(string):
            # 对addstr操作作简化
            screen.addstr(string + '\n')

        def draw_hor_separator():
            line = '+------' * self.width + '+'
            cast(line)

        # 以下是 draw_hor_separator 的另一种实现
        # def draw_hor_separator():
        #     line = '+------' * self.width + '+'
        #     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):
            s = ''
            for i in row:
                if i > 0:
                    s += '|{: ^6}'.format(i)
                else:
                    s += '|      '
            s += '|'
            cast(s)

        # draw_row 的另外2种实现:
        # def draw_row(row):
        #     s = ''
        #     for i in row:
        #         s += '|{: ^6}'.format(i) if i != 0 else '|      '
        #         cast(s)
        #
        # def draw_row(row):
        #     cast(''.join('|{: ^5}'.format(num) if num > 0 else '|     ' for num in row) + '|' + '\n')

        help_string1 = '(W)Up (S)Down (A)Left (D)Right'
        help_string2 = '     (R)Restart  (Q)Exit'
        gameover_string = '         GAME OVER'
        win_string = '         YOU WIN!'

        screen.clear()

        cast('SCORE: ' + str(self.score))
        cast('HIGHSCORE: ' + str(self.highscore))

        for row in self.field:
            draw_hor_separator()
            draw_row(row)
        draw_hor_separator()

        if self.is_win():
            cast(win_string)
        elif self.is_gameover():
            cast(gameover_string)
        else:
            cast(help_string1)
        cast(help_string2)
curses.wrapper(main)

你可能感兴趣的:(python笔记)