8
\$\begingroup\$

Here's a simple snake game I implemented in Python, which works in a console. It's not complete yet (doesn't generate food yet, and no scoring or difficulty levels), but it works. I would be really grateful if you give me any suggestions on how I can improve my code. Note that I did some of the things I did in a really complicated manner, just to learn new tricks, like the needlessly complicated binary manipulations, but suggestions would be helpful anyway.

All kinds of suggestions would be welcome, but what I really want to know right now, would be how I should accept user input. Should I use threads? If so, how should I implement it? Or should I just poll for input in a for loop?

import math
import time
import msvcrt
import ctypes
import random
from ctypes import wintypes
 ####### ## # ## # ## #######
 # # # # # # # ## #
 # # # # # # # ## #
 ####### # # # ######## #### ##### 
 # # # # # # # ## #
 # # ## # # # ## #
 ####### # # # # # ## #######
 #*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*#
 # A simple command line snake game in Python. #
 #*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*#
########################################################
########################################################
DUMP = False
class GameState:
 field_width = None
 field_height = None
 field_state = []
 snake_length = 0
 snake_head = [0, 0]
 # dir codes for movement
 # ** up **
 # 8
 #
 # ** left ** 4 6 ** right ** 
 #
 # 2
 # ** down **
 head_dir = 4
 game_lost = False
 snakebody = []
 defaults = {}
 map = None
 def initialize(settings):
 # Initialize functions
 try:
 for prop in settings:
 if not callable(getattr(GameState, prop)):
 setattr(GameState, prop, settings[prop])
 else:
 raise AttributeError
 except AttributeError as e:
 e.args = ("Invalid game setting '{}' does not exist.".format(prop),)
 raise
 GameState.snakebody = [list(GameState.snake_head)] * GameState.snake_length
 GameState.defaults = settings
 GameState.loadmap()
 # Make a copy of snake_head
 GameState.snake_head = list(GameState.snake_head)
 def reset():
 GameState.initialize(GameState.defaults)
 GameState.game_lost = False
 def loadmap(file=None):
 if file:
 if not hasattr(file, 'read'):
 file = open(file, 'r')
 GameState.map = [int(line, base=2) for line in file]
 else:
 GameState.map = GameState.map or [0] * GameState.field_height
 GameState.field_state = list(GameState.map)
 if len(GameState.field_state) != GameState.field_height:
 raise Exception("field map is invalid")
WIN32 = ctypes.WinDLL("kernel32")
stdout = WIN32.GetStdHandle(-11) # stdout code = -11 from header files
def HideCursor():
 class _CONSOLE_CURSOR_INFO(ctypes.Structure):
 _fields_ = [('dwSize', wintypes.DWORD),
 ('bVisible', ctypes.c_bool)]
 hidden_cursor = _CONSOLE_CURSOR_INFO()
 hidden_cursor.bVisible = False
 WIN32.SetConsoleCursorInfo(stdout, ctypes.byref(hidden_cursor))
HideCursor()
# Useful class
COORD = wintypes._COORD
# Box drawing chars
box_tl = "\u250C"
box_tr = "\u2510"
box_bl = "\u2514"
box_br = "\u2518"
vborder = "\u2500"
hborder = "\u2502"
# Helper functions
def move_cursor(pos=(0,0)):
 WIN32.SetConsoleCursorPosition(stdout, COORD(*pos))
def change_dir(dir):
 # Prevent reverse gear
 if (GameState.head_dir + dir) == 10: return
 GameState.head_dir = dir
def move_snake():
 # Default changes
 inc = 1
 index = 0
 wrap = GameState.field_width
 dir = GameState.head_dir
 if dir in [4, 8]:
 # to go either left or up, there must be negetive increment
 inc = -1
 if dir in [2, 8]:
 # vertical movement, so change y pos, and wrap vertically
 index = 1
 wrap = GameState.field_height
 GameState.snake_head[index] += inc
 GameState.snake_head[index] %= wrap
 if has_obstacle(GameState.snake_head):
 GameState.game_lost = True 
 GameState.snakebody.insert(0, list(GameState.snake_head))
 tail = GameState.snakebody.pop()
 # update field state
 # -------------------
 # remove tail first
 tx, ty = tail
 GameState.field_state[ty] -= GameState.field_state[ty] & (1 << tx)
 hx, hy = GameState.snake_head
 GameState.field_state[hy] |= (1 << hx)
def has_obstacle(pos):
 x, y = pos
 return bool(GameState.field_state[y] & (1 << x))
def draw_field():
 w = GameState.field_width
 h = GameState.field_height
 # Move cursor to stating point (for overwritting)
 move_cursor()
 # Draw the field
 print(box_tl, vborder * w, box_tr, sep='')
 for i in range(h):
 scanline = GameState.field_state[i]
 screen_line = [" "] * w
 for pix in range(w):
 if scanline & 1: screen_line[pix] = "#"
 scanline >>= 1
 screen_line = "".join(screen_line)
 print(hborder, end='')
 print(screen_line, end='')
 print(hborder)
 print(box_bl, vborder * w, box_br, sep='')
 if DUMP:
 for i in GameState.field_state:
 x = bin(i)[2:]
 print("0b","0"*(w-len(x)),x, sep='')
def transition():
 w = GameState.field_width
 h = GameState.field_height
 # Transition effect: diamond-in
 move_cursor()
 width_band = 10
 inner_offset = [-i for i in range(h//2)]
 inner_offset += [(i - h) for i in range(h//2, h)]
 outer_offset = [k-width_band for k in inner_offset]
 last = min(outer_offset)
 pow2 = (1 << w) - 1
 while last < (w//2 + 1):
 for i in range(h):
 in_pos = inner_offset[i]
 out_pos = outer_offset[i]
 r_in = w - in_pos - 1
 r_out = w - out_pos - 1
 gm_st = GameState.field_state[i]
 # Inner left side
 if in_pos >= 0: gm_st |= 1 << in_pos
 # Inner Right side
 if 0 < r_in < w:
 gm_st |= 1 << r_in
 # Outer left side
 if out_pos >= 0: 
 x = gm_st & ( (1 << (out_pos+1)) - 1 )
 gm_st -= x
 # Outer right side
 if 0 < r_out < w:
 x = pow2 - ( (1 << (r_out+1)) - 1 )
 gm_st -= (gm_st & x)
 GameState.field_state[i] = gm_st
 inner_offset[i] += 1
 outer_offset[i] += 1
 last += 1
 draw_field()
 time.sleep(0.01 if not DUMP else 1)
def draw_lostScreen():
 w = GameState.field_width
 h = GameState.field_height
 text = ["GAME OVER",
 "Want to try again?",
 "Enter y to continue, any other button to quit"]
 v_offset = (h - len(text)) // 2
 transition()
 move_cursor()
 print(box_tl, vborder * w, box_tr, sep='')
 for i in range(h):
 print(hborder, end='')
 print(" "*w, end='')
 print(hborder)
 print(box_bl, vborder * w, box_br, sep='')
 for i, t in enumerate(text):
 h_offset = (w - len(t)) // 2
 move_cursor([h_offset, i + v_offset])
 print(t)
 move_cursor([w // 2, i + v_offset + 1])
 return input().lower() == 'y'
GameState.initialize({
 'field_width' : 75,
 'field_height' : 20,
 'snake_length' : 10,
 'head_dir' : 4,
 'snake_head' : [75//2, 20//2]
})
# GameState.loadmap('map.txt')
try:
 while True:
 while not GameState.game_lost:
 move_snake()
 draw_field()
 if msvcrt.kbhit():
 dir = msvcrt.getch()
 while msvcrt.kbhit():
 dir = msvcrt.getch()
 dir = ord(dir)
 if dir == 72 or dir == 56: dir = 8
 elif dir == 80 or dir == 50: dir = 2
 elif dir == 75 or dir == 52: dir = 4
 elif dir == 77 or dir == 54: dir = 6
 else:
 continue
 change_dir(dir)
 time.sleep(0.03)
 time.sleep(1)
 retry = draw_lostScreen()
 GameState.reset()
 if not retry:
 break
except Exception as e:
 print(e)
input()
200_success
145k22 gold badges190 silver badges478 bronze badges
asked Sep 12, 2015 at 17:58
\$\endgroup\$
1
  • \$\begingroup\$ With normal console fonts, where the character is higher than wide, it makes playing this a bit awkward ^^" \$\endgroup\$ Commented Sep 12, 2015 at 20:11

2 Answers 2

4
\$\begingroup\$

Refactoring GameState

The GameState class feels a little odd to me. Is there a reason why it's a static class? This class would be much more useful if you could instantiate it and create multiple, varying GameStates, rather than changing the static variables contained inside the static GameState class.

The first thing we need to do is add a "magic method" named __init__. This "magic method" is called whenever an instance of the specified class is created, in essence, the constructor. This means that'd you'd define an __init__ method that looks something like this:

It was a little hard to tell which variables should be included in the parameters, so I took an educated guess.

class GameState:
 def __init__(self, field_width, field_height, field_state, snake_length, snake_head, head_dir):
 ...
 ...

This isn't all though, next, we need to initialize the class attributes, and set them to the values of the parameters. Our __init__ method now becomes this:

class GameState:
 def __init__(self, field_width, field_height, field_state, snake_length, snake_head, head_dir):
 self.field_width = field_width
 self.field_height = field_height
 self.field_state = field_state
 self.snake_length = snake_length
 self.snake_head = snake_head
 self.head_dir = head_dir
 ...
 ...

Now that we've done this, all of the GameState. prefixes in your GameState classmethods should be prefixed with self. instead.

Now that you've done this, you should be able to just create a new instance of GameState for each individual game, like this:

current_game_state = GameState( ... )

Defining a main function

Right now, you just have a top-level while loop that runs your code. No main function, and no if __name__ == "__main__" guards. The best thing to do would be to define a main function, and the run it underneath a guard, like this:

if __name__ == "__main__":
 main()

You may think that this isn't very important, but it does do some important things, namely preventing main from running if your file is imported, rather than run directly. You can see more on this subject matter here.


Style

To start off, it's worth mentioning that Python, unlike many other languages, comes with an official style guide, PEP8. It might be worth your time to take a quick peek at that whenever you have a chance.

To start off, some of your naming is incorrect. Names in Python should follow the following conventions:

  • Variables should be in snake_case.
  • Constants should be in UPPER_SNAKE_CASE.
  • Function names should be in snake_case.
  • Class names should be in PascalCase.

Secondly, in Python, there's no sort of value alignment, like you've done here:

'field_width' : 75,
'field_height' : 20,
'snake_length' : 10,
'head_dir' : 4,
'snake_head' : [75//2, 20//2]

And here:

WIN32 = ctypes.WinDLL("kernel32")
stdout = WIN32.GetStdHandle(-11)

There is no need to align the values with extra spaces.

There should also be two blank lines between top-level code/function/class definitions, like this:

class Spam:
 ...
def eggs():
 ...

And not like this:

class Spam:
 ...
def eggs():
 ...

Finally, please indent code contained in if/while/def/etc. blocks like this:

if spam:
 eggs

And not like this:

if spam: eggs

It helps clear up readability a lot. If you're just using the if statement to assign a new value to a variable as well, you can just use a ternary, like this:

spam = eggs if foo else bar
answered Sep 25, 2015 at 15:58
\$\endgroup\$
1
\$\begingroup\$

GameState is a rather weird class. You're actually not using it as a class, but as a namespace. Basically, you have a bunch of global variables that don't look global because they are prefixed by GameState..

You typically don't need to put ...State in the names of classes, either. The whole point of object-oriented programming is to have stateful objects. Instead of naming your class Game, though, I'd just call it SnakeGame.

Calling GameState.initialize() with a dictionary feels more like a JavaScript calque than Python.

A more idiomatic outline of the code would be...

class SnakeGame:
 def __init__(self, **settings):
 self.field_width = None
 self.field_height = None
 self.field_state = []
 ...
 try:
 for k, v in settings.values():
 setattr(self, k, v)
 ...

... to be called like

game = SnakeGame(field_width=75, field_height=20, ...)

But then, some of those parameters aren't actually optional. You should either make them mandatory, or better yet, provide sensible defaults.

class SnakeGame:
 def __init__(self, field_width=75, field_height=20, snake_length=10, head_dir=4, snake_head=None):
 self.snake_head = snake_head or (field_width // 2, field_height // 2)
 # Use a tuple, not a list -------^^^
 ...

To answer your question about threads... that would be totally overkill for this kind of game. Polling should be adequate. Write a function that does msvcrt.getch() with a timeout.

answered Sep 25, 2015 at 18:03
\$\endgroup\$

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.