本博文是对实验楼课程学习的笔记,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('字符串' + '\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 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] + '\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.youkuaiyun.com/detail/u010103202/9882485下载。