summaryrefslogtreecommitdiff
path: root/GameEngine
diff options
context:
space:
mode:
authorbd-912 <bdunahu@gmail.com>2023-11-12 20:10:57 -0700
committerbd-912 <bdunahu@gmail.com>2023-11-12 20:26:49 -0700
commita2b56742da7b30afa00f33c9a806fa6031be68a5 (patch)
tree94acd653183c0cc57e0434f39f5d3917eb99fdc0 /GameEngine
parentfa75138690814ad7a06194883a12f25c3936a15e (diff)
Added initial files
Diffstat (limited to 'GameEngine')
-rw-r--r--GameEngine/GoalCollection.py33
-rw-r--r--GameEngine/PlayersCollection.py158
-rw-r--r--GameEngine/__init__.py0
-rw-r--r--GameEngine/multiplayer.py170
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()