From 5cf1d8e77c7dd051a47f29136150fc613f6f78ae Mon Sep 17 00:00:00 2001 From: Geoffrey Allott Date: Sun, 25 Dec 2022 19:37:29 +0000 Subject: [PATCH] refactor for up to 4 players --- snake.py | 427 ++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 281 insertions(+), 146 deletions(-) diff --git a/snake.py b/snake.py index 79bdd2d..c4cd0aa 100644 --- a/snake.py +++ b/snake.py @@ -1,3 +1,4 @@ +import argparse import curses from enum import Enum import os @@ -11,58 +12,203 @@ class ExitGameException(BaseException): 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: @@ -74,38 +220,37 @@ class GameArea: 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'): @@ -118,38 +263,41 @@ class GameArea: (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) @@ -183,75 +331,58 @@ class GameArea: 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() @@ -261,16 +392,20 @@ def setup(): 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: -- 2.34.1