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()