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)
```
asked Apr 23, 2020 at 9:30
\$\endgroup\$

1 Answer 1

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
answered Apr 23, 2020 at 12:53
\$\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\$ Commented May 3, 2020 at 10:25

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.