"""
这是基于ESP32板,显示于ssd1306屏幕上的贪吃蛇游戏
游玩方法:
1.程序正常运行后,游戏音乐小星星播放。
2.播放完后,可以通过按任意按钮启动游戏。
3.默认情况下,蛇初始时从左到右移动。游戏的目的是收集尽可能多的水果,水果将随机放置。随着每吃一个水果,蛇就会变得更长。
4.当蛇撞到墙上或它自己时,游戏结束,显示Gameover。
5.此时可通过按任意按钮,将游戏还原为起始值,然后触摸按钮即可再次启动游戏。
"""
import random
import time
import utime
from machine import Pin, I2C,PWM
import SSD1306
# 定义音调频率
tones = {'1': 262, '2': 294, '3': 330, '4': 349, '5': 392, '6': 440, '7': 494, '-': 0}
# 定义小星星旋律
melody = "1155665-4433221-5544332-5544332-1155665-4433221"
# 然后通过PWM控制无缘蜂鸣器发声
beeper = PWM(Pin(15, Pin.OUT), freq=1, duty=1000)
for tone in melody:
freq = tones[tone]
if freq:
beeper.init(duty=1000, freq=freq) # 调整PWM的频率,使其发出指定的音调
else:
beeper.duty(0) # 空拍时一样不上电
# 停顿一下 (四四拍每秒两个音,每个音节中间稍微停顿一下)
utime.sleep_ms(400)
beeper.duty(0) # 设备占空比为0,即不上电
utime.sleep_ms(100)
beeper.deinit() # 释放PWM
SCREEN_WIDTH = 128
SCREEN_HEIGHT = 64
# 上下左右引脚, 通过上拉电阻设为高电平
UP_PIN = Pin(15, Pin.IN, Pin.PULL_UP)
DOWN_PIN = Pin(2, Pin.IN, Pin.PULL_UP)
LEFT_PIN = Pin(4, Pin.IN, Pin.PULL_UP)
RIGHT_PIN = Pin(5, Pin.IN, Pin.PULL_UP)
# 定义蛇
SNAKE_PIECE_SIZE = 2 # 蛇的每一格占用3*3个像素
MAX_SNAKE_LENGTH = 130 # 蛇的最长长度
MAP_SIZE_X = 30 # 活动范围
MAP_SIZE_Y = 30
START_SNAKE_SIZE = 5 # 蛇的初始长度
SNAKE_MOVE_DELAY = 20 # 移动延时
# game config
class State(object):
START = 0
RUNNING = 1
GAMEOVER = 2
@classmethod
def setter(cls, state):
if state == cls.START:
return cls.START
elif state == cls.RUNNING:
return cls.RUNNING
elif state == cls.GAMEOVER:
return cls.GAMEOVER
class Direction(object):
# 注意顺序(上左下右)
UP = 0
LEFT = 1
DOWN = 2
RIGHT = 3
@classmethod
def setter(cls, dirc):
if dirc == cls.UP:
return cls.UP
elif dirc == cls.DOWN:
return cls.DOWN
elif dirc == cls.LEFT:
return cls.LEFT
elif dirc == cls.RIGHT:
return cls.RIGHT
i2c = I2C(0)
screen = ssd1306.SSD1306_I2C(SCREEN_WIDTH, SCREEN_HEIGHT, I2C(0))
#贪吃蛇的实现
class Snake(object):
def __init__(self):
self.snake = [] # 蛇的初始位置
self.fruit = [] # 水果的位置
self.snake_length = START_SNAKE_SIZE
self.direction = Direction.RIGHT # 当前前进方向
self.new_direction = Direction.RIGHT # 用户按键后的前进方向
self.game_state = None
self.display = screen
self.setup_game()
def setup_game(self):
#对游戏进行初始化操作
self.game_state = State.START
direction = Direction.RIGHT
new_direction = Direction.RIGHT
self.reset_snake()
self.generate_fruit()
self.display.fill(0)
self.draw_map()
self.show_score()
self.show_press_to_start()
self.display.show()
def reset_snake(self):
#重置蛇的位置
self.snake = [] # 重置
self.snake_length = START_SNAKE_SIZE
for i in range(self.snake_length):
self.snake.append((MAP_SIZE_X // 2 - i, MAP_SIZE_Y // 2))
def check_fruit(self):
#检测蛇是否吃到水果,能否继续吃水果
if self.snake[0][0] == self.fruit[0] and self.snake[0][1] == self.fruit[1]:
if self.snake_length + 1 < MAX_SNAKE_LENGTH:
self.snake_length += 1
# 吃到水果后,将蛇增加一格
self.snake.insert(0, (self.fruit[0], self.fruit[1]))
self.generate_fruit()
def generate_fruit(self):
#随机生成水果位置,不能生成到蛇身上
while True:
self.fruit = [random.randint(1, MAP_SIZE_X - 1), random.randint(1, MAP_SIZE_Y - 1)]
fruit = tuple(self.fruit)
if fruit in self.snake:
# 生成在蛇身上
continue
else:
print('fruit: ', self.fruit)
break
@staticmethod
def button_press():
#是否有按键按下
for pin in UP_PIN, DOWN_PIN, LEFT_PIN, RIGHT_PIN:
if pin.value() == 0: # 低电平表示按下
return True
return False
def read_direction(self):
#读取新的按键方向,不能与当前方向相反,因为相反就会吃到自身
for direction, pin in enumerate((UP_PIN, LEFT_PIN, DOWN_PIN, RIGHT_PIN)):
if pin.value() == 0 and not (direction == (self.direction + 2) % 4):
self.new_direction = Direction.setter(direction)
return
def collection_check(self, x, y):
#检查蛇是否撞到墙或者自身即游戏失败
for i in self.snake:
if x == i[0] and y == i[1]:
return True
if x < 0 or y < 0 or x >= MAP_SIZE_X or y >= MAP_SIZE_Y:
return True
return False
def move_snake(self):
#按照方向键移动蛇,返回能否继续移动的布尔值
x, y = self.snake[0]
new_x, new_y = x, y
if self.direction == Direction.UP:
new_y -= 1
elif self.direction == Direction.DOWN:
new_y += 1
elif self.direction == Direction.LEFT:
new_x -= 1
elif self.direction == Direction.RIGHT:
new_x += 1
if self.collection_check(new_x, new_y): # 不能继续移动
return False
self.snake.pop() # 去除最后一个位置
self.snake.insert(0, (new_x, new_y)) # 在开头添加新位置
return True # 能继续移动
def draw_map(self):
#绘制地图区域: 蛇、水果、边界
offset_map_x = SCREEN_WIDTH - SNAKE_PIECE_SIZE * MAP_SIZE_X - 2
offset_map_y = 2
# 绘制水果
self.display.rect(self.fruit[0] * SNAKE_PIECE_SIZE + offset_map_x,
self.fruit[1] * SNAKE_PIECE_SIZE + offset_map_y,
SNAKE_PIECE_SIZE, SNAKE_PIECE_SIZE, 1)
# 绘制地图边界, 边界占一个像素,但是绘制时在内侧留一个像素,当蛇头部到达内部一个像素时,即判定为碰撞
self.display.rect(offset_map_x - 2,
0,
SNAKE_PIECE_SIZE * MAP_SIZE_X + 4,
SNAKE_PIECE_SIZE * MAP_SIZE_Y + 4, 1)
# 绘制蛇
for x, y in self.snake:
self.display.fill_rect(x * SNAKE_PIECE_SIZE + offset_map_x,
y * SNAKE_PIECE_SIZE + offset_map_y,
SNAKE_PIECE_SIZE,
SNAKE_PIECE_SIZE, 1)
def show_score(self):
#显示得分
score = self.snake_length - START_SNAKE_SIZE
self.display.text('Score:%d' % score, 0, 2, 1)
def show_press_to_start(self):
#提示按任意键开始游戏
self.display.text('Press', 0, 16, 1)
self.display.text('button', 0, 26, 1)
self.display.text('start!', 0, 36, 1)
def show_game_over(self):
#显示游戏结束
self.display.text('Game', 0, 30, 1)
self.display.text('Over!', 0, 40, 1)
# 循环运行程序 使得游戏能够反复游玩
if __name__ == '__main__':
snake = Snake()
move_time = 0
while True:
if snake.game_state == State.START:
if Snake.button_press():
snake.game_state = State.RUNNING
elif snake.game_state == State.RUNNING:
move_time += 1
snake.read_direction()
if move_time >= SNAKE_MOVE_DELAY:
snake.direction = snake.new_direction
snake.display.fill(0)
if not snake.move_snake():
snake.game_state = State.GAMEOVER
snake.show_game_over()
time.sleep(1)
snake.draw_map()
snake.show_score()
snake.display.show()
snake.check_fruit()
move_time = 0
elif snake.game_state == State.GAMEOVER:
if Snake.button_press():
time.sleep_ms(500)
snake.setup_game()
print('######## new game ########')
snake.game_state = State.START
time.sleep_ms(20)