full snake game - playable
authorGeoffrey Allott <geoff@Geoffreys-MacBook-Air.local>
Fri, 2 Dec 2022 19:30:17 +0000 (19:30 +0000)
committerGeoffrey Allott <geoff@Geoffreys-MacBook-Air.local>
Fri, 2 Dec 2022 19:30:17 +0000 (19:30 +0000)
snake.py [new file with mode: 0644]

diff --git a/snake.py b/snake.py
new file mode 100644 (file)
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()
+