From: Geoffrey Allott Date: Fri, 2 Dec 2022 19:30:17 +0000 (+0000) Subject: full snake game - playable X-Git-Url: https://git.pointlesshacks.com/?a=commitdiff_plain;h=5e209102cd280936690077ba15339a156bf1b527;p=snake.git full snake game - playable --- 5e209102cd280936690077ba15339a156bf1b527 diff --git a/snake.py b/snake.py new file mode 100644 index 0000000..b70afa8 --- /dev/null +++ b/snake.py @@ -0,0 +1,257 @@ +import curses +from enum import Enum +import os +import random +import sys +from time import sleep + +class TileContents(Enum): + Empty = "Empty" + Wall = "Wall" + Snake = "Snake" + Fruit = "Fruit" + TurboFruit = "TurboFruit" + PortalFruit = "PortalFruit" + Dead = "Dead" + + def __str__(self): + if self is TileContents.Empty: + return ' ' + elif self is TileContents.Wall: + return '#' + elif self is TileContents.Snake: + return '§' + elif self is TileContents.Fruit: + return '@' + elif self is TileContents.TurboFruit: + return '»' + elif self is TileContents.PortalFruit: + return 'O' + elif self is TileContents.Dead: + return 'X' + +class Direction(Enum): + Left = "Left" + Right = "Right" + Up = "Up" + Down = "Down" + +class Fruit: + def __init__(self, row, column, life, id): + self.row = row + self.column = column + self.life = life + self.id = id + +class GameArea: + def __init__(self, *, win: "curses window", highscore_file: str): + self.win = win + height, width = win.getmaxyx() + self.tiles = [ + [TileContents.Wall for _ in range(width)], + *[[TileContents.Wall] + [TileContents.Empty for _ in range(width - 2)] + [TileContents.Wall] for _ in range(height - 3)], + [TileContents.Wall for _ in range(width)], + ] + self.snake = [(height // 2, width // 2)] + self.fruit = [] + self.tiles[self.snake[0][0]][self.snake[0][1]] = TileContents.Snake + self.dir = Direction.Right + self.turbo = 0 + self.random = random.Random() + self.score = 0 + self.highscore_file = highscore_file + try: + with open(self.highscore_file) as f: + self.highscore = int(f.read()) + except FileNotFoundError: + self.highscore = 0 + self.orig_highscore = self.highscore + self.add_fruit() + + def paint(self): + for i, row in enumerate(self.tiles): + for j, tile in enumerate(row): + if not self.turbo: + self.win.addch(i, j, str(tile)) + else: + self.win.addch(i, j, str(tile).replace('§', '*')) + self.win.addstr(i + 1, 2, f'{self.highscore:04}') + self.win.addstr(i + 1, j - 6, f'{self.score:04}') + self.win.refresh() + + def getch(self): + ch = self.win.getch() + if ch == curses.KEY_LEFT: + if self.dir == Direction.Right: + print('\a', end='') + else: + self.dir = Direction.Left + if ch == curses.KEY_RIGHT: + if self.dir == Direction.Left: + print('\a', end='') + else: + self.dir = Direction.Right + if ch == curses.KEY_UP: + if self.dir == Direction.Down: + print('\a', end='') + else: + self.dir = Direction.Up + if ch == curses.KEY_DOWN: + if self.dir == Direction.Up: + print('\a', end='') + else: + self.dir = Direction.Down + if ch == 27 or ch == ord('q'): + curses.endwin() + sys.exit(0) + + def add_fruit(self): + r, c = self.random.choice([ + (r, c) + for r, row in enumerate(self.tiles) + for c, tile in enumerate(row) + if tile is TileContents.Empty + ]) + fruit_type = self.random.choices( + [TileContents.Fruit, TileContents.TurboFruit, TileContents.PortalFruit], + weights=[0.8, 0.1, 0.1] + )[0] + id = object() + if fruit_type is TileContents.Fruit: + life = 100 + if fruit_type is TileContents.TurboFruit: + life = 50 + if fruit_type is TileContents.PortalFruit: + life = 150 + self.tiles[r][c] = fruit_type + self.fruit.append(Fruit(r, c, life, id)) + r, c = self.random.choice([ + (r, c) + for r, row in enumerate(self.tiles) + for c, tile in enumerate(row) + if tile is TileContents.Empty + ]) + self.tiles[r][c] = fruit_type + self.fruit.append(Fruit(r, c, life, id)) + + def decay_fruit(self): + for fruit in self.fruit: + fruit.life -= 1 + if fruit.life <= 0: + self.tiles[fruit.row][fruit.column] = TileContents.Empty + self.fruit = [ + fruit + for fruit in self.fruit + if fruit.life > 0 + ] + + + def write_highscore(self): + os.makedirs(os.path.dirname(self.highscore_file), exist_ok=True) + with open(self.highscore_file, 'w') as f: + f.write(f'{self.highscore}\n') + + def game_over(self): + self.paint() + height, width = self.win.getmaxyx() + self.win.addstr(height // 2, (width - 9) // 2, 'GAME OVER') + if self.score > self.orig_highscore: + self.win.addstr(height // 2 + 2, (width - 10) // 2, 'HIGH SCORE') + self.win.refresh() + self.win.nodelay(False) + while True: + ch = self.win.getch() + if ch == 27 or ch == ord('q'): + break + curses.endwin() + sys.exit(0) + + def update(self): + if self.dir == Direction.Left: + self.snake.append((self.snake[-1][0], self.snake[-1][1] - 1)) + if self.dir == Direction.Right: + self.snake.append((self.snake[-1][0], self.snake[-1][1] + 1)) + if self.dir == Direction.Up: + self.snake.append((self.snake[-1][0] - 1, self.snake[-1][1])) + if self.dir == Direction.Down: + self.snake.append((self.snake[-1][0] + 1, self.snake[-1][1])) + r, c = self.snake[-1] + if self.tiles[r][c] is TileContents.Fruit: + self.fruit = [fruit for fruit in self.fruit if fruit.row != r or fruit.column != c] + self.tiles[r][c] = TileContents.Snake + self.score += 1 + elif self.tiles[r][c] is TileContents.TurboFruit: + self.fruit = [fruit for fruit in self.fruit if fruit.row != r or fruit.column != c] + self.tiles[r][c] = TileContents.Snake + self.score += 1 + self.turbo = 100 + elif self.tiles[r][c] is TileContents.PortalFruit: + portal_id = [fruit.id for fruit in self.fruit if fruit.row == r or fruit.column == c] + dest_portals = [ + (fruit.row, fruit.column) + for fruit in self.fruit + if fruit.id in portal_id + and self.tiles[fruit.row][fruit.column] is TileContents.PortalFruit + and fruit.row != r + and fruit.column != c + ] + self.fruit = [fruit for fruit in self.fruit if fruit.row != r or fruit.column != c] + if len(dest_portals) == 0: + self.tiles[r][c] = TileContents.Snake + r, c = self.snake[0] + self.tiles[r][c] = TileContents.Empty + self.snake = self.snake[1:] + else: + self.score += 1 + dest_portal = self.random.choice(dest_portals) + self.snake[-1] = dest_portal + self.tiles[r][c] = TileContents.Empty + elif self.tiles[r][c] is TileContents.Snake: + self.tiles[r][c] = TileContents.Dead + self.game_over() + elif self.tiles[r][c] is TileContents.Wall: + self.tiles[r][c] = TileContents.Dead + self.game_over() + else: + self.tiles[r][c] = TileContents.Snake + r, c = self.snake[0] + self.tiles[r][c] = TileContents.Empty + self.snake = self.snake[1:] + if self.score != len(self.snake) - 1: + raise Exception(len(self.snake)) + if self.score > self.highscore: + self.highscore = self.score + self.write_highscore() + if not self.turbo: + self.decay_fruit() + else: + self.turbo -= 1 + if self.random.uniform(0, 1) < 0.05: + self.add_fruit() + + def __str__(self) -> str: + return '\n'.join(''.join(str(tile) for tile in row) for row in self.tiles) + +def setup(): + win = curses.initscr() + curses.cbreak() + curses.noecho() + curses.nonl() + curses.curs_set(0) + win.nodelay(True) + win.keypad(True) + return win + +if __name__ == '__main__': + win = curses.initscr() + win = setup() + gamearea = GameArea(win=win, highscore_file=os.path.expanduser('~/.local/snake/highscore')) + while True: + gamearea.paint() + if not gamearea.turbo: + sleep(0.1) + else: + sleep(0.05) + gamearea.getch() + gamearea.update() +