Source code for visualiser.visualiser

'''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