--- /dev/null
+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()
+