Revamping the Snake
Now that we have a snake that works, it’s time to make the game more complex. The goal is to add random blocks that will act as more wall. If the snake hits any of these blocks, it will die. All we need to do is add these blocks to the game, and check if the snake hits these blocks. Generating theese blocks isn’t difficult, but there are things we have to keep in mind.
- Don’t place blocks in the same spot as other blocks.
- Don’t create an unwinable board. Don’t create a |_| shape where once you enter, you can’t leave.
The first can be accomplished by a simple if statement, however the second one is slightly more complicated.
First, let’s get our general setup for the next game.
#! /usr/bin/env python3
import snake
import pygame
class Game(snake.Game):
pass
class Snake(snake.Snake):
pass
def main():
game = Game(60, 800, 600)
game.play()
if __name__ == "__main__":
main()
Now if we run it, we get our snake game from before
Now that we have that working, we can add the blocks to the game. The basic idea will be to add 10 random blocks. We will iterate 10 times, and each iteration we will try to add a new block randomly on the board. Then, we check if those coordinates have already been used, and we will check if it is safe (a later defined function). Finally, we will return the list.
#Game
def __create_blocks(self):
block_coordinates = []
for i in range(10):
safe = False
while not safe:
block = snake.Block(self.size,
random.randint(0, self.rows) * self.size,
random.randint(0, self.cols) * self.size,
color=(220, 220, 220))
if block in block_coordinates:
safe = False
block_coordinates.append(block)
if self.__isSafe(block_coordinates):
safe = True
else:
block_coordinates.pop()
return block_coordinates
Now we have to check if a block placed into the list is safe.
#Game
def __init__(self, fps, width, height):
super().__init__(fps, width, height)
self.block_width = self.snake.width
self.width = width
self.height = height
self.blocks = self.__create_blocks()
def __create_blocks(self):
block_coordinates = []
for i in range(10):
safe = False
while not safe:
block = snake.Block(self.snake.width,
random.randint(0, self.height/self.snake.width) * self.snake.width,
random.randint(0, self.width/self.snake.width) *
self.snake.width)
if block in block_coordinates:
safe = False
block_coordinates.append(block)
if self.__isSafe(block_coordinates):
safe = True
else:
block_coordinates.pop()
return block_coordinates
def __isSafe(self, blocks):
for block in blocks:
if not self.__blockSafety(block, blocks):
return False
return True
def __mapBlockSurrounding(self, block, blocks):
encoded_map = bitmap.BitMap(8) #8 Surrounding blocks
total_walls = 0
#Make list of all surrounding blocks in board
bx, by = block
surrounding = [(sur_x, sur_y) for sur_x in
range(bx - self.block_width, bx + self.block_width * 2, self.block_width)
for sur_y in range(by - self.block_width, by + self.block_width * 2,
self.block_width)
if sur_x != bx or sur_y != by]
#Encode the surroundings into the bitmap
for index, (x, y) in enumerate(surrounding):
if (snake.Block(self.block_width, x, y) in blocks or x <= 0
or y <= 0 or x >= self.width or y >= self.height
):
encoded_map.set(index)
return encoded_map
def __blockSafety(self, block, blocks):
#Check blocks L R U and D from current block and enocde that area
all_surroundings = [(block.x - block.width, block.y), (block.x +
block.width, block.y), (block.x, block.y - block.width), (block.x,
block.y + block.width)]
#Encode each surrounding
for surrounding in all_surroundings:
encoded_map = self.__mapBlockSurrounding(surrounding, blocks)
#Check for conflict
if encoded_map.count() >= 3: #If it's less, theres no possibility of conflict
#Check for -_- shape (impossible to escape)
if ((encoded_map[3] and encoded_map[4]
and (encoded_map[1] or encoded_map[6]))
or ((encoded_map[1] and encoded_map[6])
and (encoded_map[3] or encoded_map[4]))
):
return False #Not valid, conflict exists
return True
Now that we are sure that all of our blocks are safe, we can display them on the screen
#Game.play
def play(self):
timer = 0
speed = 10
input_buffer = []
moved = False
while not self.done:
for event in pygame.event.get():
if event.type == pygame.QUIT:
self.done = True
if event.type == pygame.KEYDOWN:
if moved:
self.userInput(event.key)
moved = False
else:
input_buffer.append(event.key)
while self.pause:
for event in pygame.event.get():
if event.type == pygame.KEYDOWN:
self.checkPause(event.key)
if input_buffer and moved:
self.userInput(input_buffer.pop(0))
moved = False
if timer * speed > 1:
timer = 0
self.snake.move()
if self.snake.checkDead():
self.gameOver()
moved = True
self.snake.checkEat()
self.screen.fill((0, 0, 0))
self.score.draw()
pygame.draw.rect(self.screen, snake.Snake.color, self.snake)
pygame.draw.rect(self.screen, snake.Food.color, self.food)
for block in self.snake.tail:
pygame.draw.rect(self.screen, snake.Block.color, block)
###Added Lines
for block in self.blocks:
pygame.draw.rect(self.screen, ((220, 220, 220)), block)
###End Added Lines
timer += self.clock.tick(self.fps) / 1000
pygame.display.flip()
Now we just have to make the snake die when it touches a block. This involves minor changes to the Snake class, such as adding a variable (initialized in init), updating move to check if a block is hit, adding a new function to check if a block was hit, and updating the requirements for the snake to die. Overall, this leads to:
class Snake(snake.Snake):
def __init__(self, game, size):
super().__init__(game, size)
self.hit_block = False
def move(self):
super().move()
self.hitBlock()
def hitBlock(self):
for block in self.game.blocks:
if self.colliderect(block):
self.hit_block = True
break
else:
self.hit_block = False
def checkDead(self):
if self.hit_self or self.hit_wall or self.hit_block:
return True
else:
return False
Now we have a new working version of the snake game. Next we have to focus on adding the Q-Learning algorithm. Luckily, much of the heavy lifting is going to be done by the code that has already been written!