'''The visualiser of the simulator - the game the user plays
Uses the PyGame library
'''
import os, platform, sys
from time import time
from random import randint
#from audri import Agent
import pygame
from config import SimulatorConfig, GUIConfig
from .vehicles import Obstacle, Car, OBSTACLE_SPRITES, OBSTACLE_SPRITE_PREFIX
from .background import BackgroundPiece
from .util import Actions, loadSprite
sys.path.append('..')
guiConf = GUIConfig()
conf = SimulatorConfig()
# Key names that map on to actions. Keys are key names, values actions
# Key names are found in the left column of the reference table
# https://www.tcl.tk/man/tcl/TkCmd/keysyms.htm
KEYS = {
'Left': Actions.LEFT,
'Right': Actions.RIGHT,
'space': Actions.PAUSE,
}
[docs]class SimulatorVisualiser():
'''The visualiser of the simulator, showing a graphical representation
of the highway.
:param windowID: :class:`str` set as the SDL_WINDOWID environment
variable. Used to embed the pygame window into the GUI.
'''
def __init__(self, windowID):
if windowID:
os.putenv('SDL_WINDOWID', windowID)
# Set SDL video driver for Windows & MacOS
if platform.system() != 'Linux':
os.environ['SDL_VIDEODRIVER'] = \
'windib' if platform.system() == 'Windows' \
else 'Quartz'
pygame.init()
self._mode = 0
self.mode = self._mode
#: (:class:`float`) Timestamp of the last :py:meth:`tick` call
self._lastTick = time()
#: (:class:`float`) Timestamp of the last time the screen was drawn
self._lastDraw = time()
#: :class:`list` storing the active
#: :class:`~visualiser.vehicles.BackgroundPiece`
self._backgrounds = []
#: (:class:`int`) Height of the background sprite
self._bgHeight = loadSprite('background.png', scale=1) \
.get_height()
#: (:class:`int`) Frames per second target
self._fps = 0
self.fps = conf.FPS
#: (:class:`float`) Targeted milliseconds in between screen draws
self._targetDrawDelay = 0
#: (:class:`int`) Last timestamp an obstacle vehicle was spawned
self._lastSpawn = 0
#: :class:`list` storing the currently spawned
#: :class:`~visualiser.vehicles.Obstacle` instances
self._obstacles = []
#: (:class:`boolean`) Whether all sprites have been cached
self._cachedSprites = False
#: A reference to the :class:`~pygame.Surface` for drawing
self.canvas = pygame.display.get_surface()
#: The :class:`~visualiser.vehicles.Car` controlled by the expert
self.car = Car(self)
#: The :class:`~visualiser.vehicles.Car` controlled by the agent
self.agentCar = Car(self)
#: :class:`bool` indicating whether the visualiser is paused
self.pause = False
#: :class:`int` indicating the number of expert collisions in the
#: current run
self.collisions = 0
#: :class:`int` indicating the number of agent collisions in the
#: current run
self.agentCollisions = 0
#: :class:`float` indicating the milliseconds since starting the current
#: run
self.sessionTime = 0
#: :class:`int` indicating the number metres travelled in the current
#: run
self.distanceTravelled = 0
#: (:class:`~visualiser.util.Actions`) last action performed
self._lastAction = Actions.NONE
#: :class:`float` Timestamp when the last action was performed
self.lastActionTime = time()
# cache sprites
for sprite in OBSTACLE_SPRITES:
loadSprite(OBSTACLE_SPRITE_PREFIX + sprite)
# spawn background
totalHeight = -self._bgHeight
while totalHeight <= self.canvas.get_height():
back = BackgroundPiece(self, totalHeight)
totalHeight += self._bgHeight
self._backgrounds.append(back)
@property
def mode(self):
'''The mode of the visualiser. See :py:attr:`~gui.sim.Simulator.mode`
for details.
:getter: Return the current set mode
:setter: Set the current mode. Also sets the correct dimensions of the
display, doubling the width if in compare mode
:type: :class:`int`
'''
return self._mode
@mode.setter
def mode(self, val):
self._mode = val
pygame.display.set_mode((guiConf.VisualiserWidth *(1 if val < 2 else 2),
guiConf.Height), 0, 0)
@property
def fps(self):
'''The current target frames per second
:getter: Get the current FPS
:setter: Set the current FPS. Calculates the necessary target draw
interval
:type: :class:`int`
'''
return self._fps
@fps.setter
def fps(self, val):
self._fps = val
self._targetDrawDelay = 1/val
[docs] def tick(self):
'''This method tries to run at regular intervals of
:py:attr:`~config.SimulatorConfig.TickRate` milliseconds.
Performs update logic of the cars, obstacles, and background using
time since the previous tick.
Stores the current tick timestamp in :py:attr:`_lastTick`
'''
now = time()
# delta time - time in seconds since last tick
dt = now -self._lastTick
self._lastTick = now
if now -self._lastDraw >= self._targetDrawDelay:
self.draw()
self._lastDraw = now
if self.pause:
# don't affect obstacle spawn rate when paused
# TODO remove once spawning is based on probablility
self._lastSpawn += dt
return
self.sessionTime += dt
self.distanceTravelled += self.car.speed *dt
# call tick on vehicles, keep only ones on screen
self.car.tick(dt)
self.agentCar.tick(dt)
self._obstacles = [ob for ob in self._obstacles if ob.tick(dt)]
# spawn obstacles
if now -self._lastSpawn >= conf.ObstacleInterval:
ob = Obstacle(self)
ob.lane = randint(1, 3)
self._obstacles.append(ob)
self._lastSpawn = now
# call tick on backgrounds if they should scroll
if conf.ScrollBackground:
self._backgrounds = [back for back in self._backgrounds
if back.tick(dt)]
# spawn backgrounds if necessary
if conf.ScrollBackground and len(self._backgrounds) < 1 \
or min(bg.pos.y for bg in self._backgrounds) >= -1:
back = BackgroundPiece(self, -self._bgHeight)
self._backgrounds.append(back)
# check for collisions
self.collisions += sum(ob.hasCollided for ob in self._obstacles)
self.agentCollisions += sum(
ob.hasCollidedAgent for ob in self._obstacles)
[docs] def doAct(self, act, agent=False):
'''Perform Action act'''
if act is None or act == Actions.NONE:
return
car = self.agentCar if agent else self.car
if act == Actions.PAUSE:
self.togglePause()
if self.pause:
return
if act == Actions.LEFT:
car.lane -= 1
elif act == Actions.RIGHT:
car.lane += 1
self._lastAction = act if act != Actions.PAUSE else self._lastAction
self.lastActionTime = time()
[docs] def keyPress(self, key):
'''Handle key presses and perform the actions they map onto'''
if key not in KEYS:
return
self.doAct(KEYS[key])
[docs] def draw(self):
'''Updates the canvas
Attempts to achieve target FPS by blocking
As such, it should run in its own thread so other things can be done in
the background
Should run in and endless loop to continuously redraw
'''
# draw the background
for back in self._backgrounds:
back.draw(self.canvas)
# also draw background in second road
if self.mode == 2:
back.rect[0] += guiConf.VisualiserWidth
back.draw(self.canvas)
back.rect[0] -= guiConf.VisualiserWidth
# draw obstacle vehicles
for ob in self._obstacles:
ob.draw(self.canvas)
# also draw obstacle in second road
if self.mode == 2:
ob.rect[0] += guiConf.VisualiserWidth
ob.draw(self.canvas)
ob.rect[0] -= guiConf.VisualiserWidth
# draw user car
if self.mode in [0, 2]: self.car.draw(self.canvas)
# draw agent car
if self.mode > 0:
if self.mode == 2:
self.agentCar.rect[0] += guiConf.VisualiserWidth
self.agentCar.draw(self.canvas)
if self.mode == 2:
self.agentCar.rect[0] -= guiConf.VisualiserWidth
pygame.display.flip()
[docs] def stateVector(self, agent=False):
'''Return a dictionary of features:
{last action, current lane, distance of obstales in three lanes,
offroad}
Some features have been 'pruned' from our descision tree because they
did not affect the accuracy of the tree. These have
been commented out for clarity.
'''
car = self.agentCar if agent else self.car
# indicate distances in each lane, 999 means no obstacles in that lane
distances = [999, 999, 999, 999, 999]
for ob in self._obstacles:
laneDistance = int(car.pos.y - ob.pos.y - ob.rect.size[1])
# ignore if not colliding
if laneDistance < -ob.rect.size[1] -car.rect.size[1]: continue
distances[ob.lane] = min(distances[ob.lane], max(0, laneDistance))
if self._lastAction == Actions.NONE or agent:
lane = car.lane
else:
lane = max(0, min(4,
car.lane +(-1 if self._lastAction == Actions.RIGHT else 1)))
stateVector = {
'aheadDistance': distances[lane],
'currentLane': lane,
}
if not agent:
stateVector['action'] = self._lastAction.value
self._lastAction = Actions.NONE
print('State:', stateVector)
return stateVector
[docs] def togglePause(self):
'''Toggle pause state of the game'''
self.pause = not self.pause
[docs] def reset(self):
'''Restart the visualiser by resetting properties'''
self.collisions = 0
self.agentCollisions = 0
self.sessionTime = 0
self.distanceTravelled = 0
self._obstacles = []
self.car.lane = 2
self.agentCar.lane = 2