2
\$\begingroup\$

I've recently wrote snake game. I Wanted to keep it in pure terminal, without libraries like pygame, tkinter or curses. It works just fine, but i know that the code might be improved. I'm rather junior programmer, and it's my first attempt at 'gamedev', because currently i'm learning Django. I would be grateful if you guys could tell me what should i change :) Here is my code:

#!/usr/bin/python3
import random
import time
import keyboard
import collections


class Board:
    def __init__(self, width: int, height: int, speed: float):
        self.WIDTH = width
        self.HEIGHT = height
        self.SPEED = speed
        self.board = self.__create_board()

    def __create_board(self) -> list:
        return [["□" for _ in range(self.WIDTH)] for _ in range(self.HEIGHT)]

    def print_board(self) -> None:
        print('\n')
        for row in self.board:
            print(row)


class Snake:
    def __init__(self, x_pos, y_pos):
        self.x_pos = x_pos
        self.y_pos = y_pos
        self.head_pos = [self.x_pos, self.y_pos]

        self.body_segments = collections.deque()  # doubly end operations, faster than lists
        self.length = 0
        self.apple_pos = [0, 0]
        self.coords = [0, 0]
        self.ate = False
        self.direction = 'up'
        self.spawn_apple()

    def set_empty_square(self) -> None:
        board_obj.board[self.y_pos][self.x_pos] = "□"

    def set_head(self) -> None:
        board_obj.board[self.y_pos][self.x_pos] = "■"

    def draw_body(self) -> None:
        self.body_segments.appendleft(self.coords)
        board_obj.board[self.body_segments[0][1]][self.body_segments[0][0]] = "&"
        if not self.ate:
            board_obj.board[self.body_segments[self.length][1]][self.body_segments[self.length][0]] = "□"
            self.set_head()
            self.body_segments.pop()

    def move(self) -> None:
        self.coords = [self.x_pos, self.y_pos]
        self.set_empty_square()

        if self.direction == "right":
            self.x_pos += 1
        if self.direction == "left":
            self.x_pos -= 1
        if self.direction == "up":
            self.y_pos -= 1
        if self.direction == "down":
            self.y_pos += 1

        self.set_head()
        if self.length >= 1:
            self.draw_body()

        self.head_pos = [self.x_pos, self.y_pos]

    def spawn_apple(self) -> None:
        x = random.randint(0, board_obj.WIDTH-2)
        y = random.randint(0, board_obj.HEIGHT-2)
        apple_pos = [x, y]
        if apple_pos in self.body_segments or apple_pos == self.head_pos:
            self.spawn_apple()  # if apple spawned in snake, then run func again with recursion and quit current func
            return
        self.apple_pos = [x, y]
        board_obj.board[y][x] = "●"

    def eat_apple(self) -> None:
        self.length += 1
        self.ate = True

    def collision(self) -> bool:
        return self.head_pos in self.body_segments or self.y_pos in [board_obj.HEIGHT-1, -1] \
                or self.x_pos in [board_obj.WIDTH-1, -1]


def on_press(_: None):
    for code in keyboard._pressed_events:
        if code == 105:
            snake.direction = 'left'
        elif code == 106:
            snake.direction = 'right'
        elif code == 103:
            snake.direction = 'up'
        elif code == 108:
            snake.direction = 'down'


def start_game(snake: Snake, board_obj: Board):
    snake.set_head()
    board_obj.print_board()
    keyboard.hook(on_press)
    while not snake.collision():
        print("SNAKE: ", snake.head_pos)
        print("BODY: ", snake.body_segments)
        print("APPLE: ", snake.apple_pos)

        snake.move()
        board_obj.print_board()
        snake.ate = False
        if snake.head_pos == snake.apple_pos:
            snake.eat_apple()
            snake.spawn_apple()
        time.sleep(board_obj.SPEED)
    print(f"Game over! Points: {snake.length}")


if __name__ == "__main__":
    board_obj = Board(8, 8, 0.3)  # Set: width, height, speed
    snake = Snake(int(board_obj.WIDTH / 2), int(board_obj.HEIGHT / 2))  # Set: snake x and y position

    start_game(snake, board_obj)

```
\$\endgroup\$
2
\$\begingroup\$

This is good code for a first post. I'm glad to waste time discussing PEP 8 violations, and seeing a sensible use of the collections module. Having chosen good data structures is a good place to start.

For 2d arrays in Python (unless I am doing numeric heavy lifting, when using numpy is the obvious way to go) I prefer using a dictionary to a list of lists. Something like a named tuple can be used as the key. This makes the board initialization look like:

Pos = collections.namedtuple('position', ['x', 'y'])
...
def __create_board(self) -> None:
    self.board = {(Pos(x, y)) for x in range(self.WIDTH) for y in range(self.HEIGHT)}

This is a fairly big change that will ripple through the code but then you won't have to worry about [y][x] indexing or making temporary (x,y) variables.

Onto some more specific things. First, I think I would define a str method here, rather than having a print method. It adds a hidden dependency to sys.stdout in your code, and makes it harder to unit test.

def print_board(self) -> None:
    print('\n')
    for row in self.board:
        print(row)

Here I would lose the recursion and use a while True/break construction.

if apple_pos in self.body_segments or apple_pos == self.head_pos:
    self.spawn_apple()  # if apple spawned in snake, then run func again with recursion 
        return

becomes:

while True:
    ...
    if apple_pos not in self.body_segments and apple_pos != self.head_pos:
        break

I must admit to never having used the keyboard module but _pressed_events doesn't look like it is meant to be part of the API. And I don't know where the magic code numbers come from, or what key (if any) they bind to on my platform.

for code in keyboard._pressed_events:
    if code == 105:
        snake.direction = 'left'

As you never actually use the string directions I might change it to:

for code in keyboard.some_get_event_api_call():
    if code == LEFT_ARROW:
        snake.direction = Pos(-1, 0)

Then you can do the elif thing only once.

Finally:

int(board_obj.WIDTH / 2)

can be just

board_obj.WIDTH // 2
\$\endgroup\$
1
  • \$\begingroup\$ Thank you a lot! I've applied all of you advices, except namedtuple, because there would be a lot of work with this code then.. But i will apply it when i'll have some more time :) Also i've splitted whole game in 3 different files and wrote some tests. Thank you again! \$\endgroup\$
    – regorg
    May 3 '20 at 10:25

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.