diff options
author | bd-912 <bdunahu@gmail.com> | 2023-11-12 20:10:57 -0700 |
---|---|---|
committer | bd-912 <bdunahu@gmail.com> | 2023-11-12 20:26:49 -0700 |
commit | a2b56742da7b30afa00f33c9a806fa6031be68a5 (patch) | |
tree | 94acd653183c0cc57e0434f39f5d3917eb99fdc0 /GameEngine | |
parent | fa75138690814ad7a06194883a12f25c3936a15e (diff) |
Added initial files
Diffstat (limited to 'GameEngine')
-rw-r--r-- | GameEngine/GoalCollection.py | 33 | ||||
-rw-r--r-- | GameEngine/PlayersCollection.py | 158 | ||||
-rw-r--r-- | GameEngine/__init__.py | 0 | ||||
-rw-r--r-- | GameEngine/multiplayer.py | 170 |
4 files changed, 361 insertions, 0 deletions
diff --git a/GameEngine/GoalCollection.py b/GameEngine/GoalCollection.py new file mode 100644 index 0000000..17e9721 --- /dev/null +++ b/GameEngine/GoalCollection.py @@ -0,0 +1,33 @@ +#+AUTHOR: bdunahu +#+TITLE: multiplayer.py +#+DESCRIPTION: goal object for multiagent snake + +import pygame as pg +from random import randint +from collections import namedtuple + +Point = namedtuple('Point', 'x, y') +GREEN = (0,128,43) + +class Goal(): + def __init__(self, display, window_width=640, window_height=480, game_units=40): + ''' create initial location ''' + self.location = None + + self.display = display + self.window_width = window_width + self.window_height = window_height + self.game_units = game_units + + def reset(self, hazards=[]): + ''' generate new coordinates for goal ''' + x = randint(0, (self.window_width-self.game_units )//self.game_units )*self.game_units + y = randint(0, (self.window_height-self.game_units )//self.game_units )*self.game_units + self.location = Point(x, y) + if self.location in hazards: + self.reset(hazards) + + def draw(self): + ''' draw rectangle directly on field ''' + pg.draw.rect(self.display, GREEN, pg.Rect(self.location.x, self.location.y, + self.game_units, self.game_units)) diff --git a/GameEngine/PlayersCollection.py b/GameEngine/PlayersCollection.py new file mode 100644 index 0000000..10fa990 --- /dev/null +++ b/GameEngine/PlayersCollection.py @@ -0,0 +1,158 @@ +#+AUTHOR: bdunahu +#+TITLE: PlayersCollection.py +#+DESCRIPTION: methods for handling and querying snake objects + +import pygame as pg +from random import randint +from collections import namedtuple +from enum import Enum + +class Direction(Enum): + UP = 0 + RIGHT = 1 + DOWN = 2 + LEFT = 3 + +Point = namedtuple('Point', 'x, y') + +YELLOW = (255,255,0) +RED = (255,0,0) +PURPLE = (204,51,255) +WHITE = (255,255,255) +PLAYER_COLOR = [YELLOW, RED, PURPLE, WHITE] + +WINDOW_WIDTH = 640 +WINDOW_HEIGHT = 480 +GAME_UNITS = 40 +DISPLAY = None + +class Players(): + def __init__(self, snake_size, num_players, display, window_width=640, window_height=480, game_units=40): + ''' define array list of new Snake objects ''' + global WINDOW_WIDTH + global WINDOW_HEIGHT + global GAME_UNITS + global DISPLAY + + WINDOW_WIDTH = window_width + WINDOW_HEIGHT = window_height + GAME_UNITS = game_units + DISPLAY = display + + self._index = 0 + self.num_players = num_players + + self.players = [Snake(snake_size, player_id) + for player_id in range(num_players)] + + def __iter__(self): + return iter(self.players) + + def __getitem__(self, index): + return self.players[index] + + def full_reset(self): + ''' reset every snake position ''' + # map(lambda player:player.reset(), self.players) + for player in self.players: + player.reset() + + def move_all(self): + ''' move all snakes ''' + # map(lambda player:player.move(), self.players) + for player in self.players: + player.move() + + def reward_killer(self, player): + ''' split play length up against killing snakes ''' + killer = self._point_lookup(player.head) + if not killer == None and not killer == player: + rewards = player.score.curr_score + player.size + killer.deficit += rewards + killer.score.curr_score += rewards + killer.score.total_score += rewards + + def _point_lookup(self, head): + for player in self.players: + if head in player.snake: + return player + return None + + def draw(self): + ''' draw all snakes ''' + # map(lambda player:player.draw(), self.players) + for player in self.players: + player.draw() + +class Snake(): + def __init__(self, initial_size, player_id): + ''' define initial size (length), direction, and position ''' + self.player_id = player_id + self.size = initial_size + self.direction = None + self.head = None + self.snake = [] + self.score = Score(self.player_id) + self.deficit = 0 # for how many moves does this snake need to grow? + + def reset(self): + self.score.reset() + self.deficit = 0 + self.direction = Direction.RIGHT.value + x = randint(0, (WINDOW_WIDTH-GAME_UNITS )//GAME_UNITS )*GAME_UNITS + y = randint(0, (WINDOW_HEIGHT-GAME_UNITS )//GAME_UNITS )*GAME_UNITS + self.head = Point(x,y) + self.snake = [self.head] + for seg in range(self.size-1): + self.snake.append(Point(self.head.x-(seg*GAME_UNITS), self.head.y)) + + def move(self): + ''' update snake coordinates by inserting new head ''' + x = self.head.x + y = self.head.y + if self.direction == Direction.RIGHT.value: + x += GAME_UNITS + if self.direction == Direction.LEFT.value: + x -= GAME_UNITS + if self.direction == Direction.DOWN.value: + y += GAME_UNITS + if self.direction == Direction.UP.value: + y -= GAME_UNITS + + self.head = Point(x, y) + self.snake.insert(0,self.head) + + def in_wall(self): + return True if (self.head.x > WINDOW_WIDTH - GAME_UNITS or + self.head.x < 0 or + self.head.y > WINDOW_HEIGHT - GAME_UNITS or + self.head.y < 0) else False + + def draw(self): + ''' draw rectangle(s) directly on field ''' + for seg in self.snake: # see explanation in engine.org + pg.draw.rect(DISPLAY, PLAYER_COLOR[self.player_id], pg.Rect(seg.x, seg.y, + GAME_UNITS, GAME_UNITS)) + self.score.draw() + +class Score(): + def __init__(self, player_id): + ''' initialize score counter ''' + self.player_id = player_id + self.font = pg.font.SysFont("monospace", 16) + self.curr_score = 0 + self.total_score = 0 + self.deaths = 0 + self.kills = 0 + + def scored(self): + self.curr_score += 1 + self.total_score += 1 + + def reset(self): + self.curr_score = 0 + + def draw(self): + ''' draw score on top left ''' + score_surf = self.font.render(f'Current: {self.curr_score} Total: {self.total_score}', True, PLAYER_COLOR[self.player_id]) + DISPLAY.blit(score_surf, (0, 0+24*self.player_id)) diff --git a/GameEngine/__init__.py b/GameEngine/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/GameEngine/__init__.py diff --git a/GameEngine/multiplayer.py b/GameEngine/multiplayer.py new file mode 100644 index 0000000..9f6b651 --- /dev/null +++ b/GameEngine/multiplayer.py @@ -0,0 +1,170 @@ +#+AUTHOR: bdunahu +#+TITLE: multiplayer.py +#+DESCRIPTION game engine for multiagent snake + +from enum import Enum +import pygame as pg +import numpy as np + +from GameEngine import PlayersCollection +from GameEngine import GoalCollection + +BLACK = (0, 0, 0) + +class CollisionType(Enum): # each of these means different outcome for model + DEATH = 0 + GOAL = 1 + NONE = 2 + + +class Playfield: + def __init__(self, window_width=640, window_height=480, units=40, g_speed=25, s_size=3): + ''' initialize pygame modules, snake, goal, and score objects ''' + global DISPLAY + + self._g_speed = g_speed # game speed + self._s_size = s_size # initial snake size + self._units = units + self._window_width = window_width + self._window_height = window_height + self._player_count = -1 # number of registered players + ''' for human feedback ''' + self._game_state = False # false is game over + self._draw_on = True + self._clock = pg.time.Clock() + + ''' objects ''' + pg.init() + self.display = pg.display.set_mode( + [self._window_width, self._window_height], + pg.HWSURFACE) # display object (see explanation in engine.org) + self._players = None + self._goal = None + + def add_player(self): + ''' + Returns the player's number to the callee. + If the player count is over four, returns None + ''' + if self._player_count < 4 or self._game_state == True: + self._player_count += 1 + return self._player_count + return None + + def get_heads_tails_and_goal(self): + ''' + Returns an array of heads, an array of tail positions, + and the goal position + ''' + heads = [] + for player in self._players.players: + heads.append(player.head) + return heads, self._get_player_bodies(), self._goal.location + + def get_viable_actions(self, player_id): + ''' + Given a player's id, + returns a list of actions that does + not result in immediate death + ''' + head = self._players.players[player_id].head + tail = self._players.players[player_id].snake + danger_array = np.array([ + head.y-self._units < 0 or PlayersCollection.Point(head.x, head.y-self._units) in tail[1:], # up + head.x+self._units >= self._window_width or PlayersCollection.Point(head.x+self._units, head.y) in tail[1:], # right + head.y+self._units >= self._window_height or PlayersCollection.Point(head.x, head.y+self._units) in tail[1:], # down + head.x-self._units < 0 or PlayersCollection.Point(head.x-self._units, head.y) in tail[1:], # left + ]) + + return np.where(danger_array == False)[0] + + def start_game(self): + ''' + Initializes player objects, starts the game + ''' + self._players = PlayersCollection.Players(self._s_size, self._player_count+1, self.display, game_units=self._units) + self._goal = GoalCollection.Goal(self.display, game_units=self._units) + self._reset() + print(f'Game starting with {self._player_count+1} players.') + + def stop_game(self): + ''' + Restarts the game, allows adding/removing players + ''' + self._game_state = False + print(f'Game over!') + + def cleanup(self): + ''' end game session ''' + pg.quit() + + def player_advance(self, actions, noise=0.0): + ''' given a list of snake actions ''' + ''' return a list of results, and ''' + ''' update game state ''' + + for player_id, action in enumerate(actions): + if np.random.uniform < noise: + # random action (noise) + random_choice = [0, 1, 2, 3] + random_choice.remove(self._players[player_id].direction) + self._players[player_id].direction = np.random.choice(random_choice) + else: + self._players[player_id].direction = action + + self._players.move_all() + collisions = self._check_player_collisions() + if self._draw_on: + self._update_ui() + return collisions + + def toggle_draw(self): + ''' turns off and on UI ''' + self._draw_on = not self._draw_on + print(f'Draw is now {self._draw_on}.') + + def _check_player_collisions(self): + results = [] + for player in self._players: + ''' determine what obstacle was hit ''' + hazards = self._get_player_bodies() + hazards.remove(player.head) + if (player.head in hazards or player.in_wall()): + self._players.reward_killer(player) + player.reset() + results.append(CollisionType.DEATH) + elif player.head == self._goal.location: + player.score.curr_score += 1 + player.score.total_score += 1 + self._goal.reset(hazards) + results.append(CollisionType.GOAL) + else: + if player.deficit <= 0: + player.snake.pop() + else: + player.deficit -= 1 + results.append(CollisionType.NONE) + return results + + def _get_player_bodies(self): + ''' return an array of all tail coordinates ''' + tails = [0] + for player in self._players: + tails += player.snake + return tails + + def _update_ui(self): + ''' flush new positions to screen ''' + self.display.fill(BLACK) + self._players.draw() + self._goal.draw() + + pg.display.flip() # full screen update + self._clock.tick(self._g_speed) + + def _reset(self): + ''' reset game state ''' + ''' required by training ''' + self._game_state = True # false is game over + self._goal.reset() + self._players.full_reset() |