Snaaake!
So, I was at FOSDEM this last weekend, and had quite a lot of fun. One of the cooler talks was actually Yuri Numerov's talk about making simple console-based games in Python.
I thought hey, I can do that too! and started hacking on a Snake implementation right away. You know, that game you used to waste all your time back in the Nokia 3210 days? Yep, that one.
Turns out, you can do that in less than 100 lines of python. Or in about 250ish lines if you add some more features (like boosters, and increasing difficulty!) and tidying up the code a bit.
So - if you ever thought that making games is hard, think again. And just try to do it in the simplest way possible...
Here's what I came up (hacked in about 1h total):
#!/usr/bin/env python3 # -*- coding: UTF-8 -*- # vim: autoindent expandtab tabstop=4 sw=4 sts=4 filetype=python import os import sys import time import termios import select import fcntl import struct import random from tty import IFLAG, OFLAG, CFLAG, LFLAG, VTIME from tty import BRKINT, ICRNL, INPCK, ISTRIP from tty import IXON, OPOST, CSIZE, PARENB, VMIN from tty import CS8, ECHO, ICANON, IEXTEN, ISIG, CC class Game: snake = {} TURN_TIME = 0.3 SPEEDUP_FACTOR = 0.95 last_move = time.time() boost_val = 0 WIDTH = None HEIGHT = None direction = 'up' head = None length = 5 @classmethod def init(cls): term_width, term_height = cls.term_size() cls.WIDTH = term_width // 2 cls.HEIGHT = term_height cls.head = (cls.HEIGHT // 2, cls.WIDTH // 2) cls.direction = 'up' cls.last_move = time.time() @staticmethod def term_size(): # Stolen from # http://stackoverflow.com/questions/566746/how-to-get-console-window-width-in-python env = os.environ def ioctl_GWINSZ(fd): try: cr = struct.unpack( 'hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, '1234')) except: return return cr cr = ioctl_GWINSZ(0) or ioctl_GWINSZ(1) or ioctl_GWINSZ(2) if not cr: try: fd = os.open(os.ctermid(), os.O_RDONLY) cr = ioctl_GWINSZ(fd) os.close(fd) except: pass if not cr: cr = (env.get('LINES', 25), env.get('COLUMNS', 80)) return int(cr[1]), int(cr[0]) @classmethod def place_star(cls): cls.star = ( random.randint(1, cls.HEIGHT - 2), random.randint(1, cls.WIDTH - 2) ) if cls.star in cls.snake: # collision! cls.place_star() @staticmethod def clearscreen(): # obtained via "clear | hexdump" print('\x5b\x1b\x4a\x33\x5b\x1b\x1b\x48\x32\x5b\x00\x4a') @classmethod def draw(cls): cls.clearscreen() for x in range(0, cls.HEIGHT): line_parts = [] for y in range(0, cls.WIDTH): if (x, y) == cls.head: line_parts.append('OO') elif (x, y) in cls.snake: line_parts.append('##') elif (x, y) == cls.star: line_parts.append('* ') else: line_parts.append(' ') print(''.join(line_parts)) @classmethod def dead(cls, action): print("You %s and DIED (Score: %d)" % (action, cls.length)) sys.exit() @classmethod def evolve(cls): cls.boost_val -= 1 for cell in list(cls.snake.keys()): cls.snake[cell] -= 1 if cls.snake[cell] <= 0: del cls.snake[cell] # Note this is calculating all directions at every move. This is a very # low-performance game, so we're choosing readability over performance # here. cls.head = { 'up': (cls.head[0] - 1, cls.head[1]), 'down': (cls.head[0] + 1, cls.head[1]), 'left': (cls.head[0], cls.head[1] - 1), 'right': (cls.head[0], cls.head[1] + 1), }[cls.direction] if cls.head in cls.snake: # Tail bite! cls.dead("bit yourself") if (cls.head[0] < 0 or cls.head[1] < 0 or cls.head[0] >= cls.HEIGHT or cls.head[1] >= cls.WIDTH): cls.dead("hit a wall") if cls.head == cls.star: cls.length += 1 cls.TURN_TIME = cls.TURN_TIME * cls.SPEEDUP_FACTOR cls.place_star() cls.snake[cls.head] = cls.length @classmethod def update_direction(cls, direction): # don't do 180° turns if any([ direction == 'up' and cls.direction == 'down', direction == 'down' and cls.direction == 'up', direction == 'left' and cls.direction == 'right', direction == 'right' and cls.direction == 'left']): return cls.direction = direction @classmethod def boost(cls): if cls.boost_val < cls.length / 2: cls.boost_val = cls.length * 2 @classmethod def infoscreen(cls): lines = [ "Welcome to this simple snakes game...", "", " h - go left", " j - go down", " k - go up", " l - go right", " <space> - booster", "", " <esc> / q - exit", ] textwidth = max([len(l) for l in lines]) # note: cls.WIDTH is GAME width (double-chars), not terminal width indent = " " * (cls.WIDTH - textwidth // 2) cls.clearscreen() print("\n" * (cls.HEIGHT // 2 - len(lines) // 2)) print("\n".join(['%s%s' % (indent, l) for l in lines])) print("\n" * (cls.HEIGHT // 2 - len(lines) // 2)) time.sleep(4) @classmethod def turn_time_delta(cls): if cls.boost_val < 0: return cls.TURN_TIME return cls.TURN_TIME / 3 @staticmethod def debug_key(key): print("------- unknown key: %s (ord %d)" % (key, ord(key))) sys.exit() @classmethod def wait_and_get_input(cls): fd = sys.stdin.fileno() old_settings = termios.tcgetattr(fd) new_settings = termios.tcgetattr(fd) new_settings[IFLAG] = new_settings[IFLAG] & ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON) # noqa new_settings[OFLAG] = new_settings[OFLAG] & ~(OPOST) new_settings[CFLAG] = new_settings[CFLAG] & ~(CSIZE | PARENB) new_settings[CFLAG] = new_settings[CFLAG] | CS8 new_settings[LFLAG] = new_settings[LFLAG] & ~(ECHO | ICANON | IEXTEN | ISIG) # noqa new_settings[CC][VMIN] = 1 new_settings[CC][VTIME] = 0 try: termios.tcsetattr(fd, termios.TCSADRAIN, new_settings) while time.time() < cls.last_move + cls.turn_time_delta(): i, o, e = select.select( [sys.stdin], [], [], cls.TURN_TIME / 10) for s in i: if s == sys.stdin: char = sys.stdin.read(1) action = { 'h': lambda: cls.update_direction('left'), 'j': lambda: cls.update_direction('down'), 'k': lambda: cls.update_direction('up'), 'l': lambda: cls.update_direction('right'), ' ': lambda: cls.boost(), '\x03': lambda: cls.dead('quit'), # ^C apparently '\x1b': lambda: cls.dead('quit'), # ESC 'q': lambda: cls.dead('quit'), }.get(char, lambda: cls.debug_key(char)) action() cls.last_move = time.time() finally: termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) if __name__ == '__main__': Game.init() Game.infoscreen() Game.place_star() while True: Game.draw() Game.wait_and_get_input() Game.evolve()