[Python3] 2048小游戏

代码来源:https://www.shiyanlou.com/courses/368/labs/1172/document

小白,最近看到实验楼的2048的小游戏,在代码上带了自己的注释。

前期准备:

  1. 有限状态机      ##https://www.cnblogs.com/21207-iHome/p/6085334.html
  2. defaultdict()    ##https://blog.csdn.net/real_ray/article/details/17919289
  3. zip()    ##zip() 函数用于将可迭代的对象作为参数,将对象中对应的元素打包成一个个元组,然后返回由这些元组组成的列表。
  4. assert  ##在开发一个程序时候,与其让它运行时崩溃,不如在它出现错误条件时就崩溃(返回错误)。
  5. randrange()  ##randrange() 方法返回指定递增基数集合中的一个随机数,基数缺省值为1。
  6. any()   ##any() 函数用于判断给定的可迭代参数 iterable 是否全部为空对象,如果都为空、0、false,则返回 False,如果不都为空、0、false,则返回 True。
  7. curses()   //关于curses的使用与安装,将会另外开一篇文章来说
#-*- coding:utf-8 -*-
import curses
from random import randrange, choice
from collections import defaultdict


letter_codes = [ord(ch) for ch in 'WASDRQwasdrq']   #ord函数是把字符转换成对应的数字
actions = ['Up','Left','Down','Right','Restart','Exit']
actions_dict = dict(zip(letter_codes,actions * 2))  #zip是把元组的数值对应起来

def get_user_action(keyboard):
    char = "N"    ##初始值为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)]       ##相当于旋转90度。zip后加*,是把行变成列,把列变成行

def invert(field):
    return [row[::-1] for row in field]   #步长为-1。相当于是调转180,把前后列表调转


##Merge and tighten,移动方向时候应该有的逻辑判断, 整个游戏的画面绘制,规则定制等。
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
        self.spawn()    #调用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))]   #其余位置用0fill in
                return new_row

            def merge(row):
                pair = False
                new_row = []
                for i in range(len(row)):    #在格子里循环
                    if pair:    #如果pair为真
                        new_row.append(2 * row[i])   #乘以2的值追加到new_row                        self.score += 2 * row[i]
                        pair = False
                    else:
                        if i + 1 < len(row) and row[i] == row[i+1]:  #如果pair为假,然后i+1还没到边界,并且row[i]=row[i+1]
                            pair = True              ##那么运行上面的pair为真的部分
                            new_row.append(0)        #追加0
                        else:
                            new_row.append(row[i])   #pair为假,不能合并,超出边界,多加row[i]
                assert len(new_row) == len(row)    ##报错提醒
                return new_row
            return tighten(merge(tighten(row)))    #先挤在一起,然后合并,然后再挤在一起

        moves = {}

        #将只有一个方向左,旋转90度成为向上(transpose(field)),利用左方向调转(invert(field))成为右方向。因此拥有四个方向
        moves['Left'] = lambda field: [move_row_left(row) for row in field]
        moves['Right'] = lambda field: invert(moves['Left'](invert(field)))        ##moves['Left']已经定义了mergetighten的操作
        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 is_win(self):
        return any(any(i >= self.win_value for i in row) for row in self.field)   #i在格子中,并且i的值超出了2048

    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='        Congratulation! You Win!'
        def cast(string):
            screen.addstr(string +'\n')

        def draw_hor_separator():                 ##横坐标分割线
            line = '+' + ('+------' * self.width + '+')[1:]
            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):
            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():
            cast(win_string)
        else:
            if self.is_gameover():
                cast(gameover_string)
            else:
                cast(help_string1)
        cast(help_string2)

##随机生成2或者4
    def spawn(self):
        new_element = 4 if randrange(100) > 89 else 2   #100次有89次出现4
        (i,j) = choice([(i,j) for i in range(self.width) for j in range(self.height) if self.field[i][j]==0])
        ##就是2,4随机出现的区域,要在widthheight的里面。前提self.field[i][j]==0
        self.field[i][j] = new_element   ##那么, 2,4出现的要获取他的横纵坐标

##判断是否可移动
    def move_is_possible(self, direction):
        def row_is_left_movable(row):
            def change(i):    ##True if there will be change in i-th tile
                if row[i] == 0 and row[i+1] != 0: ##有空位可以移动
                    return True
                if row[i] != 0 and row[i+1] == row[i]: ##没有空位,但是二者数值相同可以合并处理及移动
                    return True
                return False
            return any(change(i) for i in range(len(row)-1))   ##any()为一个函数,全为0false

        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

##Main Logic
def main(stdsrc):
    def init():
        #重置游戏棋盘
        game_field.reset()
        return 'Game'

    def not_game(state):
        #画出GameOver或者Win的界面
        game_field.draw(stdsrc)
        #读取用户输入得到action,判断是重启游戏还是结束游戏
        action = get_user_action(stdsrc)

        responses = defaultdict(lambda:state) #默认当前状态,没有行为就会在当前界面循环
        responses['Restart'],responses['Exit'] = 'Init','Exit' #对应不同的行为转换到不同的状态
        return responses[action]

    def game():
        ##画出当前棋盘状态
        game_field.draw(stdsrc)
        #Getthe action
        action = get_user_action(stdsrc)

        if action == 'Restart':
            return 'Init'
        if action == 'Exit':
            return 'Exit'
        if 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()  ##Color

    #设置终结状态最大数值为32
    game_field = GameField(win=32)

    state='Init'

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

curses.wrapper(main)


##Once the callable returns, wrapper() will restore the original state of the terminal.

##运行结果

SCORE: 0
+------+------+------+------+
|      |      |      |      |
+------+------+------+------+
|      |  2   |      |      |
+------+------+------+------+
|      |      |      |  4   |
+------+------+------+------+
|      |      |      |      |
+------+------+------+------+
(W)Up (S)Down (A)Left (D)Right
      (R)Restart (Q)Exit


一路上遇到的问题:

  1. Redirection is not supported.    >>>>把pycharm改成cmd来运行
  2. import curses的问题

懒,决定在这里也讲述如何在Windows上安装curses。 Curses库在Windows下没有直接的安装的,有大神搞了在windows下的wheel版。

  •  查看自己的pip支持什么格式内容

E:\MyDownloads\Download>python
Python 3.6.3 (v3.6.3:2c5fed8, Oct  3 2017, 18:11:49) [MSC v.1900 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import pip
>>> print(pip.pep425tags.get_supported())
[('cp36', 'cp36m', 'win_amd64'), ('cp36', 'none', 'win_amd64'), ('py3', 'none', 'win_amd64'), ('cp36', 'none', 'any'), ('cp3', 'non
e', 'any'), ('py36', 'none', 'any'), ('py3', 'none', 'any'), ('py35', 'none', 'any'), ('py34', 'none', 'any'), ('py33', 'none', 'an
y'), ('py32', 'none', 'any'), ('py31', 'none', 'any'), ('py30', 'none', 'any')]

>>> ^Z

  • 从以下网站中download wheel文件。

从上面可以得知, 我的pip支持的是cp36的。于是从以下网址下载cp36的wheel

https://www.lfd.uci.edu/~gohlke/pythonlibs/#curses

  • 一键安装  
E:\MyDownloads\Download>python -m pip install Scripts/curses-2.2-cp36-cp36m-win_amd64.whl
Processing e:\mydownloads\download\scripts\curses-2.2-cp36-cp36m-win_amd64.whl
Installing collected packages: curses

Successfully installed curses-2.2

以后目标:

1. 改变颜色跟边框曲线
2. 把数字改变为朝代。相同朝代碰撞能变为下一个朝代。

猜你喜欢

转载自blog.csdn.net/cyx441984694/article/details/79745404