+import argparse
import curses
from enum import Enum
import os
class RestartGameException(BaseException):
pass
-class TileContents(Enum):
- Empty = "Empty"
- Wall = "Wall"
- Snake = "Snake"
- Fruit = "Fruit"
- TurboFruit = "TurboFruit"
- PortalFruit = "PortalFruit"
- Dead = "Dead"
-
- def char(self, *, turbo: bool):
- if self is TileContents.Empty:
- return ' '
- elif self is TileContents.Wall:
- return '#'
- elif self is TileContents.Snake:
- return '*' if turbo else '§'
- 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):
+ def opposite(self):
+ if self == Direction.Left: return Direction.Right
+ if self == Direction.Right: return Direction.Left
+ if self == Direction.Up: return Direction.Down
+ if self == Direction.Down: return Direction.Up
+
+class Action:
+ def is_teleport(self) -> bool:
+ return False
+
+ def is_score(self) -> bool:
+ return False
+
+ def is_turbo(self) -> bool:
+ return False
+
+ def is_death(self) -> bool:
+ return False
+
+class NullAction(Action):
+ pass
+
+class DeathAction(Action):
+ def is_death(self) -> bool:
+ return True
+
+class ScoreAction(Action):
+ def is_score(self) -> bool:
+ return True
+
+class TurboAction(ScoreAction):
+ def is_turbo(self) -> bool:
+ return True
+
+class TeleportAction(ScoreAction):
+ def __init__(self, row, column):
self.row = row
self.column = column
+
+ def is_teleport(self) -> bool:
+ return True
+
+class Tile:
+ def char(self) -> str:
+ raise NotImplementedError
+
+ def color(self) -> int:
+ return curses.color_pair(0)
+
+ def action(self) -> Action:
+ raise NotImplementedError
+
+ def is_empty(self) -> bool:
+ return False
+
+ def is_fruit(self) -> bool:
+ return False
+
+class EmptyTile(Tile):
+ def char(self) -> str:
+ return ' '
+
+ def action(self) -> Action:
+ return NullAction()
+
+ def is_empty(self) -> bool:
+ return True
+
+class WallTile(Tile):
+ def char(self) -> str:
+ return '#'
+
+ def action(self) -> Action:
+ return DeathAction()
+
+class Snake:
+ def __init__(self, segments, color, dir):
+ self.segments = segments
+ self.turbo = 0
+ self.color = color
+ self.alive = True
+ self.prev_dir = dir
+ self.dir = dir
+
+ def len(self):
+ return len(self.segments)
+
+ def set_dir(self, dir):
+ if self.prev_dir == dir.opposite():
+ curses.beep()
+ elif self.dir != self.prev_dir:
+ pass
+ else:
+ self.dir = dir
+
+ def slither(self):
+ if self.dir == Direction.Left:
+ self.segments.append((self.segments[-1][0], self.segments[-1][1] - 1))
+ if self.dir == Direction.Right:
+ self.segments.append((self.segments[-1][0], self.segments[-1][1] + 1))
+ if self.dir == Direction.Up:
+ self.segments.append((self.segments[-1][0] - 1, self.segments[-1][1]))
+ if self.dir == Direction.Down:
+ self.segments.append((self.segments[-1][0] + 1, self.segments[-1][1]))
+ self.prev_dir = self.dir
+
+ def shrink(self):
+ self.segments = self.segments[1:]
+
+class SnakeTile(Tile):
+ def __init__(self, snake: Snake):
+ self.snake = snake
+
+ def char(self) -> str:
+ return '*' if self.snake.turbo else '§'
+
+ def color(self) -> int:
+ return self.snake.color
+
+ def action(self) -> Action:
+ return DeathAction()
+
+class DeadTile(Tile):
+ def char(self) -> str:
+ return 'X'
+
+ def action(self) -> Action:
+ return DeathAction()
+
+class FruitTile(Tile):
+ def __init__(self, life):
self.life = life
- self.id = id
+
+ def char(self) -> str:
+ return '@'
+
+ def action(self) -> Action:
+ return ScoreAction()
+
+ def is_fruit(self) -> bool:
+ return True
+
+class TurboFruitTile(FruitTile):
+ def char(self) -> str:
+ return '»'
+
+ def action(self) -> Action:
+ return TurboAction()
+
+class PortalFruitTile(FruitTile):
+ def __init__(self, life: int, other_end_row: int, other_end_column: int):
+ self.life = life
+ self.other_end_row = other_end_row
+ self.other_end_column = other_end_column
+
+ def char(self) -> str:
+ return 'O'
+
+ def action(self) -> Action:
+ return TeleportAction(self.other_end_row, self.other_end_column)
class GameArea:
- def __init__(self, *, win: "curses window", highscore_file: str):
+ def __init__(self, *, win: "curses window", highscore_file: str, players: int):
self.win = win
self.height, self.width = win.getmaxyx()
self.tiles = [
- [TileContents.Wall for _ in range(self.width)],
- *[[TileContents.Wall] + [TileContents.Empty for _ in range(self.width - 2)] + [TileContents.Wall] for _ in range(self.height - 3)],
- [TileContents.Wall for _ in range(self.width)],
+ [WallTile() for _ in range(self.width)],
+ *[[WallTile()] + [EmptyTile() for _ in range(self.width - 2)] + [WallTile()] for _ in range(self.height - 3)],
+ [WallTile() for _ in range(self.width)],
]
- self.snake = [(self.height // 2, self.width // 2)]
- self.fruit = []
- self.tiles[self.snake[0][0]][self.snake[0][1]] = TileContents.Snake
- self.dir = Direction.Right
- self.turbo = 0
+ self.frame = 0
+ player_layout = {
+ 1: [0],
+ 2: [1, 0],
+ 3: [1, 2, 0],
+ 4: [1, 3, 2, 0],
+ }[players]
+ self.snakes = [
+ Snake(
+ [(
+ self.height // 2,
+ (self.width * (1+player_layout[i]*2)) // 2 // players
+ )],
+ color=curses.color_pair(i+1),
+ dir=[Direction.Left, Direction.Right][i%2]
+ )
+ for i in range(players)
+ ]
+ for snake in self.snakes:
+ for row, column in snake.segments:
+ self.tiles[row][column] = SnakeTile(snake)
self.random = random.Random()
self.highscore_file = highscore_file
try:
self.add_fruit()
def score(self):
- return len(self.snake) - 1
+ return max(snake.len() - 1 for snake in self.snakes)
def paint(self):
for i, row in enumerate(self.tiles):
for j, tile in enumerate(row):
- self.win.addch(i, j, tile.char(turbo=self.turbo))
+ self.win.addch(i, j, tile.char(), tile.color())
self.win.addstr(i + 1, 2, f'{self.highscore:04}')
- self.win.addstr(i + 1, j - 6, f'{self.score():04}')
+ self.win.addstr(i + 1, j - 1 - 5 * len(self.snakes), ' '.join(f'{snake.len() - 1:04}' for snake in self.snakes))
self.win.refresh()
def getch(self):
ch = self.win.getch()
- if ch == curses.KEY_LEFT or ch == ord('h'):
- if self.dir == Direction.Right:
- curses.beep()
- else:
- self.dir = Direction.Left
- if ch == curses.KEY_RIGHT or ch == ord('l'):
- if self.dir == Direction.Left:
- curses.beep()
- else:
- self.dir = Direction.Right
- if ch == curses.KEY_UP or ch == ord('k'):
- if self.dir == Direction.Down:
- curses.beep()
- else:
- self.dir = Direction.Up
- if ch == curses.KEY_DOWN or ch == ord('j'):
- if self.dir == Direction.Up:
- curses.beep()
- else:
- self.dir = Direction.Down
+ if ch == curses.KEY_LEFT: self.snakes[0].set_dir(Direction.Left)
+ if ch == curses.KEY_RIGHT: self.snakes[0].set_dir(Direction.Right)
+ if ch == curses.KEY_UP: self.snakes[0].set_dir(Direction.Up)
+ if ch == curses.KEY_DOWN: self.snakes[0].set_dir(Direction.Down)
+ if len(self.snakes) >= 2:
+ if ch == ord('a'): self.snakes[1].set_dir(Direction.Left)
+ if ch == ord('d'): self.snakes[1].set_dir(Direction.Right)
+ if ch == ord('w'): self.snakes[1].set_dir(Direction.Up)
+ if ch == ord('s'): self.snakes[1].set_dir(Direction.Down)
+ if len(self.snakes) >= 3:
+ if ch == ord('j'): self.snakes[2].set_dir(Direction.Left)
+ if ch == ord('l'): self.snakes[2].set_dir(Direction.Right)
+ if ch == ord('i'): self.snakes[2].set_dir(Direction.Up)
+ if ch == ord('k'): self.snakes[2].set_dir(Direction.Down)
+ if len(self.snakes) >= 4:
+ if ch == ord('v'): self.snakes[3].set_dir(Direction.Left)
+ if ch == ord('n'): self.snakes[3].set_dir(Direction.Right)
+ if ch == ord('g'): self.snakes[3].set_dir(Direction.Up)
+ if ch == ord('b'): self.snakes[3].set_dir(Direction.Down)
if ch == 27 or ch == ord('p'):
self.pause()
if ch == ord('q'):
(r, c)
for r, row in enumerate(self.tiles)
for c, tile in enumerate(row)
- if tile is TileContents.Empty
+ if tile.is_empty()
])
+ def two_random_empty_tiles(self):
+ return self.random.sample([
+ (r, c)
+ for r, row in enumerate(self.tiles)
+ for c, tile in enumerate(row)
+ if tile.is_empty()
+ ], k=2)
+
def add_fruit(self):
- r, c = self.random_empty_tile()
fruit_type = self.random.choices(
- [TileContents.Fruit, TileContents.TurboFruit, TileContents.PortalFruit],
+ ['fruit', 'turbo_fruit', 'portal_fruit'],
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))
+ if fruit_type == 'fruit':
+ r, c = self.random_empty_tile()
+ self.tiles[r][c] = FruitTile(life=100)
+ elif fruit_type == 'turbo_fruit':
r, c = self.random_empty_tile()
- self.tiles[r][c] = fruit_type
- self.fruit.append(Fruit(r, c, life, id))
+ self.tiles[r][c] = TurboFruitTile(life=50)
+ elif fruit_type == 'portal_fruit':
+ life = 150
+ (r1, c1), (r2, c2) = self.two_random_empty_tiles()
+ self.tiles[r1][c1] = PortalFruitTile(life=150, other_end_row=r2, other_end_column=c2)
+ self.tiles[r2][c2] = PortalFruitTile(life=150, other_end_row=r1, other_end_column=c1)
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
- ]
+ for r, row in enumerate(self.tiles):
+ for c, tile in enumerate(row):
+ if tile.is_fruit():
+ tile.life -= 1
+ if tile.life == 0:
+ self.tiles[r][c] = EmptyTile()
def write_highscore(self):
os.makedirs(os.path.dirname(self.highscore_file), exist_ok=True)
while True:
ch = self.win.getch()
if ch == 27 or ch == ord('q'):
- break
+ raise ExitGameException()
if ch == ord('r'):
raise RestartGameException()
- raise ExitGameException()
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, fruit.column) != (r, c)]
- self.tiles[r][c] = TileContents.Snake
- elif self.tiles[r][c] is TileContents.TurboFruit:
- self.fruit = [fruit for fruit in self.fruit if (fruit.row, fruit.column) != (r, c)]
- self.tiles[r][c] = TileContents.Snake
- self.turbo = 100
- elif self.tiles[r][c] is TileContents.PortalFruit:
- portal_id = [fruit.id for fruit in self.fruit if (fruit.row, fruit.column) == (r, 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, fruit.column) != (r, c)
- ]
- self.fruit = [fruit for fruit in self.fruit if (fruit.row, fruit.column) != (r, 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:
- 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.snake = self.snake[1:]
- self.game_over()
- elif self.tiles[r][c] is TileContents.Wall:
- self.tiles[r][c] = TileContents.Dead
- self.snake = self.snake[1:]
- 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() > self.highscore:
- self.highscore = self.score()
- self.write_highscore()
- if not self.turbo:
+ self.frame += 1
+ for snake in self.snakes:
+ if not snake.alive: continue
+ if not snake.turbo and self.frame % 2 == 0: continue
+ snake.slither()
+ r, c = snake.segments[-1]
+ action = self.tiles[r][c].action()
+ if action.is_teleport():
+ self.tiles[r][c] = EmptyTile()
+ r, c = snake.segments[-1] = action.row, action.column
+ self.tiles[r][c] = SnakeTile(snake)
+ if action.is_turbo():
+ snake.turbo = 100
+ if not action.is_score():
+ r0, c0 = snake.segments[0]
+ self.tiles[r0][c0] = EmptyTile()
+ snake.shrink()
+ if action.is_death():
+ self.tiles[r][c] = DeadTile()
+ snake.alive = False
+ if not any(snake.alive for snake in self.snakes):
+ self.game_over()
+ if snake.turbo:
+ snake.turbo -= 1
+ if any(snake.turbo for snake in self.snakes):
self.decay_fruit()
- else:
- self.turbo -= 1
+ elif self.frame % 2 == 0:
+ return
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():
+def setup(players: int):
win = curses.initscr()
+ curses.start_color()
+ curses.use_default_colors()
+ if players == 1:
+ curses.init_pair(1, -1, -1)
+ else:
+ curses.init_pair(1, curses.COLOR_RED, -1)
+ curses.init_pair(2, curses.COLOR_GREEN, -1)
+ curses.init_pair(3, curses.COLOR_BLUE, -1)
+ curses.init_pair(4, curses.COLOR_CYAN, -1)
+ curses.init_pair(5, curses.COLOR_MAGENTA, -1)
+ curses.init_pair(6, curses.COLOR_BLACK, -1)
curses.cbreak()
curses.noecho()
curses.nonl()
return win
if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--players', '-p', default=1, type=int, help='number of players')
+ args = parser.parse_args()
+
+ if args.players < 1 or args.players > 4:
+ raise Exception('Number of players must be between 1 and 4')
+
while True:
- win = setup()
- gamearea = GameArea(win=win, highscore_file=os.path.expanduser('~/.local/snake/highscore'))
+ win = setup(args.players)
+ gamearea = GameArea(win=win, highscore_file=os.path.expanduser('~/.local/snake/highscore'), players=args.players)
try:
while True:
gamearea.paint()
- if not gamearea.turbo:
- sleep(0.1)
- else:
- sleep(0.05)
+ sleep(0.05)
gamearea.getch()
gamearea.update()
except ExitGameException: