repl.it
Python

A port of the classic Snake game to Python. Based on work by: https://github.com/ahirapatel/python-snake-cli

fork
loading
Files
  • main.py
  • board.py
  • constants.py
  • snake.py
main.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
'''
A port of the classic Snake game to Python.
Based on work by Ahira Patel: https://github.com/ahirapatel/python-snake-cli
'''


import fcntl
import os
import select
import signal
import sys
import termios
import threading
import time
import tty

from random import randint

from board import Board
from constants import Symbols
from snake import Snake


class Game(object):
    '''
    Manages gameplay.
    '''

    def __init__(self):
        self.board = Board(get_terminal_dimensions())
        self.key_quit = False
        self.num_food = 25
        self.over = False
        self.sig_quit = False
        self.snake = Snake((self.board.height() // 2, self.board.width() // 2))

        self.board.set(self.snake.get_head(), Symbols.SNAKE.value)
        self.board.draw_initial_board()

        self.spawn_new_food()

    def play(self):
        time.sleep(2)
        while True:
            exit_as_needed(self)
            self.update_game_board()
            self.draw_game_board()
            time.sleep(.2)

    def spawn_new_food(self):
        spawned = False

        while not spawned:
            r = randint(0, self.board.height() - 1)
            c = randint(0, self.board.width() - 1)
            coord = (r,c)

            if not self.board.is_valid_coord(coord) or self.board.get(coord) in [Symbols.SNAKE.value, Symbols.WALL.value]:
                continue

            self.board.set(coord, Symbols.FOOD.value)
            self.board.draw(coord, Symbols.FOOD.value)
            spawned = True

    def draw_game_board(self):
        if self.snake.is_hungry():
            self.board.draw(self.snake.get_old_tail(), Symbols.GRID.value)
        self.board.draw(self.snake.get_head(), Symbols.SNAKE.value)

    def update_game_board(self):
        # Move the head in the right direction, then keep going if food is present.
        new_head = self.snake.move()

        self.board.set(self.snake.get_old_tail(), Symbols.GRID.value)
        self.snake.consume(self.board.get(new_head))
        self.board.set(new_head, Symbols.SNAKE.value)

        # Check if snake is within the board, and if it collides with itself.
        if self.board.is_valid_coord(new_head) and not self.snake.is_dead():
            if not self.snake.is_hungry():
                self.num_food -= 1
                self.spawn_new_food()
        else:
            self.over = True


def movement_listener(game):
    arrow_key_start = '\x1b'
    # The bytes that match up with the paired movement.
    bytes_to_movement_dict = { '\x1b[A' : 'up','\x1b[B' : 'down','\x1b[C' : 'right','\x1b[D' : 'left' }

    fd = sys.stdin.fileno()
    # Store settings for stdin, because we have to restore them later.
    orig_term_settings = termios.tcgetattr(fd)
    quit.orig_term_settings = orig_term_settings
    # No echo and have stdin work on a char-by-char basis.
    tty.setraw(fd)
    # Keep options for stdin, then add the nonblocking flag to it.
    orig_flags = fcntl.fcntl(sys.stdin, fcntl.F_GETFL)
    quit.orig_flags = orig_flags
    fcntl.fcntl(sys.stdin, fcntl.F_SETFL, orig_flags | os.O_NONBLOCK)

    p = select.poll()
    p.register(fd, select.POLLIN)
    try:
        while True:
            res = p.poll(100)
            ch = sys.stdin.read(1) if res else None
            if (ch and ord(ch) == 3): # Ctrl-c/game ended.
                game.key_quit = True
                break
            elif game.over:
                break

            if arrow_key_start == ch:
                try:
                    ch += sys.stdin.read(2)
                except IOError:
                    continue # No more data to read.
                game.snake.set_movement(bytes_to_movement_dict.get(ch))
    finally:
        # Restore our old settings for the terminal.
        termios.tcsetattr(fd, termios.TCSADRAIN, orig_term_settings)
        fcntl.fcntl(sys.stdin, fcntl.F_SETFL, orig_flags)

def get_terminal_dimensions():
    rows, columns = os.popen('stty size', 'r').read().split()
    return (int(rows) - 2, int(columns) - 20)

# This is used to print output to the alternative screen buffer.
# Programs like 'man' and 'tmux' print to it, making it so that
# when you leave them, their output is gone and you are back to
# the output from before running those commands.
def start_alternate_screen():
    sys.stdout.write("\033[?1049h\033[H")
    sys.stdout.flush()

def end_alternate_screen():
    sys.stdout.write("\033[?1049l")
    sys.stdout.flush()

def exit_as_needed(game):
    if game.over:
        quit(game, message='Game Over!')
    elif game.num_food == 0:
        quit(game, message='You win!')
    elif game.sig_quit:
        quit(game, kill_all=True, message='Process was politely terminated.')
    elif game.key_quit:
        quit(game, message='You quit!')

def signal_handler(signal, frame, game):
    game.sig_quit = True

def quit(game, kill_all=None, message=''):
    game.over = True

    # Restore terminal settings to how they were before.
    end_alternate_screen()
    termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, quit.orig_term_settings)
    fcntl.fcntl(sys.stdin, fcntl.F_SETFL, quit.orig_flags)

    if message:
        sys.stdout.write(message + '\n')

    sys.exit(0)


if __name__ == '__main__':
    num_rows = (os.get_terminal_size().lines // 2) - 5
    num_cols = os.get_terminal_size().columns

    for i in range(num_rows):
        print(' ' * num_cols)
    for i in ['3...', '2...', '1...', 'GO GO GO!']:
        print(' ' * ((num_cols - 6) // 2) + i, flush=True)
        time.sleep(1)
    for i in range(num_rows):
        print(' ' * num_cols)

    start_alternate_screen()
    signal.signal(signal.SIGINT, signal_handler)
    signal.signal(signal.SIGTERM, signal_handler)

    game = Game()

    key_listener = threading.Thread(target = movement_listener, args=(game,))
    key_listener.start()

    game.play()