4
\$\begingroup\$

I've written a simple text-based (can be run in terminal) snake game in python. This is a personal project and also one of my very first projects in python. I appreciate any advice or suggestion that make my code more efficient and professional.

main.py

from game import Game
if __name__=="__main__":
 game = Game()
 game.new_game([15,15])
 game.run()

game.py

from time import sleep
from threading import Thread
from snake import Snake
from board import Board
class Game:
 
 def __init__(self):
 pass
 def new_game(self, board_dim = [10, 10], level = "EASY"):
 self.snake = Snake(2, [[2,2],[2,3]], "UP", board_dim)
 self.board = Board(board_dim[0], board_dim[1], self.snake.pos)
 self.thrd = Thread(target=self.snake.check_arrow_keys)
 self.score = 0
 def finish_game(self):
 print(f"Your score is {self.score}")
 self.thrd.join()
 exit()
 
 def run(self):
 self.thrd.start()
 while True:
 print('033円c')
 if not self.snake.status:
 break
 self.board.food_process(self.snake.normalize_pos())
 if self.board.eaten:
 self.score += 1
 self.snake.move_toward_direction(increment_size=True)
 else:
 self.snake.move_toward_direction()
 self.board.board_init(self.snake.normalize_pos())
 self.board.show_board(self.snake)
 print(f"score:{self.score}")
 sleep(.2)
 self.finish_game()

board.py

from random import randint
class Board:
 
 def __init__(self, columns, rows, fill):
 self.board = [[0 for j in range(columns)] for i in range(rows)]
 self.fill = fill
 self.col = columns
 self.rows = rows
 self.first = fill[-1]
 self.put_food(fill)
 
 
 def board_init(self, fill):
 self.board = [[0 for j in range(self.col)] for i in range(self.rows)]
 self.fill = fill
 self.first = fill[-1]
 
 for i in self.fill:
 if i == self.first:
 self.board[i[0]%self.rows][i[1]%self.col] = 2
 else:
 self.board[i[0]%self.rows][i[1]%self.col] = 1
 self.board[self.food[0]][self.food[1]] = 3
 
 
 def food_process(self, fill):
 
 if self.check_food(fill):
 self.eaten = True
 self.put_food(fill)
 else:
 self.eaten = False
 
 def normalize_fill(self, fill):
 return [[i[0]%self.rows, i[1]%self.col] for i in fill]
 
 def check_food(self, fill):
 if self.food in self.normalize_fill(fill):
 return True
 return False
 
 def put_food(self, fill):
 
 while True:
 x,y = randint(0,self.col-1), randint(0, self.rows-1) 
 if [x,y] not in self.normalize_fill(fill):
 self.board[x][y] = 3
 self.food = [x,y]
 return
 
 
 def show_board(self, snake):
 
 board_ = ""
 for i in self.board:
 for j in i:
 if j==1:
 board_ += "@|"
 elif j==2:
 if snake.dir == "UP":
 board_ += "^|"
 elif snake.dir == "LEFT":
 board_ += "<|"
 elif snake.dir == "RIGHT":
 board_ += ">|"
 elif snake.dir == "DOWN":
 board_ += "˅|"
 elif j==3:
 board_ += "*|"
 else:
 board_ += " |"
 board_ += "\n"
 board_ += "".join(["_ "*self.col])
 board_ += "\n"
 
 print(board_)

snake.py

from random import choice
from threading import Thread
import sys
import select
import tty
import termios
class Snake:
 
 def __init__(self, length, pos, direction, board_size):
 
 if length != len(pos):
 raise Exception("Length is not equal to the size of `pos`")
 self.len = length
 self.pos = pos
 self.dir = direction
 self.last = pos[-1]
 self.first = pos[0]
 self.columns= board_size[0]
 self.rows = board_size[1]
 self.init_l = length
 self.status = True
 def normalize_pos(self):
 return [ [p[0]%self.rows, p[1]%self.columns] for p in self.pos]
 def move_toward_direction(self, step = 1, increment_size=False):
 temp = self.last[:]
 if self.dir.upper() == "UP":
 temp[0] -= 1
 if self.check(temp):
 self.pos.append(temp)
 else:
 self.__lost()
 elif self.dir.upper() == "DOWN":
 temp[0] += 1
 if self.check(temp):
 self.pos.append(temp)
 else:
 self.__lost()
 elif self.dir.upper() == "RIGHT":
 temp[1] += 1
 if self.check(temp):
 self.pos.append(temp)
 else:
 self.__lost()
 elif self.dir.upper() == "LEFT":
 temp[1] -= 1
 if self.check(temp):
 self.pos.append(temp)
 else:
 self.__lost()
 else:
 raise Exception(f"Direction not correct!: {self.dir}")
 if not increment_size :
 self.pos.remove(self.first)
 self.first = self.pos[0]
 else:
 self.len += 1
 self.first = self.pos[0]
 self.last = self.pos[-1]
 
 
 def check(self, tmp):
 
 if tmp not in self.normalize_pos() and tmp not in self.pos:
 return True
 else:
 return False
 def rand_direction(self):
 
 counter = 0
 while True:
 
 tmp = choice(["UP","RIGHT","LEFT","DOWN"])
 #chcs = [i for i in ["UP","RIGHT","LEFT","DOWN"] if self.check(i)]
 temp = self.last[:]
 if tmp == "UP" :
 temp[0] -= 1
 elif tmp == "DOWN" :
 temp[0] += 1
 elif tmp == "RIGHT":
 temp[1] += 1
 elif tmp == "LEFT" :
 temp[1] -= 1
 else:
 raise Exception(f"Direction not correct!: {tmp}")
 
 if self.check(temp):
 self.dir = tmp
 return
 counter += 1
 if counter > 32:
 raise Exception("No movement is possible")
 def check_arrow_keys(self):
 old_settings = termios.tcgetattr(sys.stdin)
 try:
 tty.setcbreak(sys.stdin.fileno())
 while 1:
 if self.__isData() or self.status:
 c = sys.stdin.read(3)
 if c == '\x1b[A': 
 if self.dir != "DOWN":
 self.dir = "UP"
 elif c == '\x1b[B':
 if self.dir != "UP":
 self.dir = "DOWN"
 elif c == '\x1b[D':
 if self.dir != "RIGHT":
 self.dir = "LEFT"
 elif c == '\x1b[C':
 if self.dir != "LEFT":
 self.dir = "RIGHT"
 else:
 pass
 else:
 return
 finally:
 termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings)
 
 def __isData(self):
 return select.select([sys.stdin], [], [], 0) == ([sys.stdin], [], [])
 def __lost(self):
 self.status = False

also this is link of project on github. If you enjoyed the project, please give me star on github :D

Edit: I noticed that if I run the program on the tmux, it will looks very better (It does not blink).

dariosicily
4,0561 gold badge11 silver badges22 bronze badges
asked Jun 28, 2021 at 9:11
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

First off, nice game! It plays pretty smoothly. The code looks decent as-is, which is why I'm going more in depth--high level, no improvements are really needed.

I'll go through by file, since it's split up.

General comments

  • Read PEP 8.
  • Be consistent about using blank lines. Don't leave a blank after a function def.
  • Add type annotations
  • Remove debugging code and commented-out code.
  • I had trouble figuring out what functions did and didn't handle--I had to guess too much. Partly this is naming, partly it's lack of docstring or comments, and partly it's file organization. Also, it's just hard for a medium-sized game like this.
  • Add docstrings to your methods to explain what each one does. Remember to explain the method in terms of "when would I want to call this?" rather than "what does it do inside?"
  • In a couple places, you used while True loops. Try to use a more specific condition if you can, because then the reader can immediately read it and say "oh, the loop ends when..."

User Testing

I tested this out. Some notes:

  • Very clean visuals (although not all the characters display in my font).
  • Movement feels silky smooth on my computer.
  • It seems like the heart is drawn over the snake for a while after you eat it
  • Once you die, you have to press a key to exit.
  • I managed to wiggle the snake around and get a score of 0 somehow at the start. There may be race conditions in the threading.
  • Overall looks cool!

main.py

  • Looks good.

game.py

  • Pass the same parameters you would to new_game to __init__ instead, and remove new_game.
  • Take the entire contents of the run loop. Split these into get_input, do_tick, and update_display. Right now there is no input processing in game.py which is a little confusing for someone trying to understand how it gets done.
  • Have board.show_board return a board, and print it here. Try to put all your print statements in one file.
  • Change the run loop to while self.snake.alive for clarity (change from .status to .alive for this reason)

board.py

  • I would say this is the file that needs the most improvement, but I don't have great answers as to how.
  • Generally, this could use more separation between displaying the board, vs tracking the game state.
  • Add a docstring to each method. In particular: board_init, food_process, normalize_fill, check_food, put_food all need one to figure out what they do. Even show_board is a little fuzzy in that I don't know if it will clear the screen or print the score
  • There are too many blank lines
  • Don't use hardcoded values 1 to 3 for board contents. Instead, use an enum
  • Initialization:
    • I don't know what board.fill is based on the name, or board.first.
    • Rename board.food to board.food_pos
  • board_init sounds like it should be called exactly once, at startup, but instead it's called every game tick. I'm not sure what it does.
  • I don't know what board.normalize_fill or snake.normalize_pos do based on the name.
  • Rename check_food to is_food_on_board. Make it a one-liner.
  • Rename put_food to place_random_food. This is a good use of while True.
  • In show_board, use the names x,y or row,column instead of i,j.

snake.py

  • Define an ENUM of directions
  • Your initialization process is good. The variables are clear, but I think they could be trimmed or renamed in some cases.
    • You don't need self.init_l.
    • Change snake.status to snake.alive.
    • Change first and last to head and tail, which are clear for a snake. Then delete head and tail, because it's bad to have two variables that can be out of sync--if you want, @property might be a good fit instead.
    • Change pos to something like occupied_squares because it's not clear it's a list from the name.
  • Add a docstring to normalize_pos and check, and __isData.
  • move_toward_direction is too repetitive, shrink it. Remove the unused step parameter as well.
  • Rename self.__lost() to self.lose() or self.alive = False. Prefer present tense action verbs for method names.
  • Remove rand_direction, which looks like unused testing code.
  • Move check_arrow_keys elsewhere, such as into game.py.
answered Oct 23, 2021 at 8:31
\$\endgroup\$
1
  • \$\begingroup\$ Thanks for your points. Very nice comments. \$\endgroup\$ Commented Jan 6, 2022 at 17:17

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.