class GUI(): def __init__(self, screen_rect, bg_img): # Setup Screen and background image self.screen = pygame.display.set_mode((screen_rect.w, screen_rect.h)) self.screen_rect = screen_rect self.bg_img = bg_img.convert() # Init starfield (layer above background, slowly scrolls on loop) self.star_field = \ objects.object_types["starField"]( speed = -0.3, surface = self.screen) # Init player ship, add it to shipGroup self.player_ship = objects.object_types["ship"](position=(50, 50), activate=True) ship.Ship.shipGroup.add(self.player_ship) # Init the alien infested asteroid, add to infestedGroup self.infested_asteroid = objects.object_types["infested"]( position=(self.screen_rect.w - 150, 200)) infested.Infested.infestedGroup.add(self.infested_asteroid) # Init space station, add to spaceStation group self.space_station = objects.object_types["spaceStation"]( position=(75, self.screen_rect.h / 2)) spaceStation.SpaceStation.spaceStationGroup.add(self.space_station) # Init scorekeeping, add to score group self.score = objects.object_types["score"]( position=(self.screen_rect.w, 0), time=time.time(), spaceStation=self.space_station) score.Score.scoreGroup.add(self.score) # For the graph section self.paths = None self.graph_timer = Timer(150 / 1000, self.graph_update) # TODO: remove this demo feature self.OPTIONS = 0 def p1_button(self, e, down): ''' Handles user input, allowing player_ship to be piloted Args: event e, a pygame event down, a boolean representing key event type (down==True) ''' if e == K_RIGHT: \ self.player_ship.k_right = down*self.player_ship.turn_speed elif e == K_LEFT: \ self.player_ship.k_left = down*self.player_ship.turn_speed elif e == K_UP: \ self.player_ship.k_up = down*self.player_ship.acceleration elif e == K_DOWN: \ self.player_ship.k_down = down*self.player_ship.acceleration elif e == K_SPACE and down: self.player_ship.fire_missile() elif e == K_TAB and down: self.OPTIONS = (self.OPTIONS + 1) % 4 print(self.OPTIONS) def asteroid_spawn(self): ''' Generates an asteroid offscreen right which will travel towards spacestation. Consecutive asteroids will have semi-uniform motion, defined by the speed and direction. Asteroid spawn positions will be divided into rough 'sections' to help create a uniform asteroid field. ''' # Divide the screen into rough sections (1.25 asteroid height) spacing_height = asteroid.Asteroid.sprite.get_rect().h * 1.25 # Determine how many integer sections fit on screen spacing_number = int(self.screen_rect.h / spacing_height) # The second term positions the asteroid offscreen by some amount x = self.screen_rect.w + random.randrange(10, 50) # First term offsets the spacing so asteroids do not spawn # offscreen in the vertical axis. Second term determines # which 'section' to spawn asteroid in. y = 0.5 * spacing_height + \ spacing_height * random.randint(0,spacing_number) # Field movement properties speed = random.triangular(0.5, 3, 2) direction = random.uniform(170, 190) # Init asteroid object, add it to asteroid group new_asteroid = objects.object_types["asteroid"](position=(x, y), speed=speed, direction=direction) asteroid.Asteroid.asteroidGroup.add(new_asteroid) def alien_spawn(self, QTY): ''' Adds an integer QTY potential aliens to the infested asteroid 'aliens' attribute. This quantity will dictate the amount of aliens which can spawn from the alien_hop method. (I call it 'spawn pool') ''' self.infested_asteroid.aliens += QTY def alien_hop(self): ''' Handles creation of new aliens, movement of existing aliens, and drawing of teleport beams for successful hops. ''' # Only create aliens if there is a path if not self.paths is None: # Send an alien along each path for p in self.paths: if self.infested_asteroid.aliens != 0: new_alien = objects.object_types["alien"]( asteroid=self.infested_asteroid, # Start at source path=p[1:]) #No need to include source in path alien.Alien.alienGroup.add(new_alien) self.infested_asteroid.aliens -= 1 # Decrement 'spawn pool' self.paths = None # Only move if the spacestation is alive if self.space_station.alive(): target = self.space_station for al in alien.Alien.alienGroup: hop = None # Alien movement hop = al.path_hop(target) # Optional alien path drawing if self.OPTIONS == 1 and not al.path is None: self.alien_path_beams(al) # If hop successful, two nodes were returned, draw the beam if not hop is None: beam.Beam.beamGroup.add(\ objects.object_types["beam"](\ obj1 = hop[0], obj2 = hop[1], color = (128,255,0), surface = self.screen)) def offscreen(self): ''' Deactivates objects which go offscreen ''' # Since we don't want asteroids deleting before offscreen, add padding padding = 50 # Asteroids for o in asteroid.Asteroid.asteroidGroup: if o.position[0] < -padding or o.position[0] \ > self.screen_rect.w + padding or \ o.position[1] < -10 or o.position[1] \ > self.screen_rect.h+10: o.deactivate() # Missiles for o in missile.Missile.missileGroup: if o.position[0] < -padding or o.position[0] \ > self.screen_rect.w + padding or \ o.position[1] < -padding or o.position[1] > \ self.screen_rect.h+padding: o.deactivate() # Aliens for o in alien.Alien.alienGroup: if o.position[0] < -padding or o.position[0] \ > self.screen_rect.w + padding or \ o.position[1] < -padding or o.position[1] > \ self.screen_rect.h+padding: o.deactivate() def update(self): ''' Called by game.py, calls the update methods of various groups/objects. Also handles object collisions. ''' self.star_field.update() self.score.update(time.time()) collision.ast_mis(self.screen) collision.ast_ast() # Optional ship collision with asteroids if self.OPTIONS == 2: collision.ship_ast(self.screen) asteroid.Asteroid.asteroidGroup.update() ship.Ship.shipGroup.update() missile.Missile.missileGroup.update() self.graph_timer.update() alien.Alien.alienGroup.update() def draw(self): ''' Draws the background elements and game objects. ''' # Re-draw entire background self.screen.blit(self.bg_img, (0, 0)) self.star_field.draw(self.screen) self.score.draw(self.screen) # Draw the objects infested.Infested.infestedGroup.draw(self.screen) asteroid.Asteroid.asteroidGroup.draw(self.screen) ship.Ship.shipGroup.draw(self.screen) missile.Missile.missileGroup.draw(self.screen) spaceStation.SpaceStation.spaceStationGroup.draw(self.screen) alien.Alien.alienGroup.draw(self.screen) # Draw special effects # Particles draw simple circles on update particle.Particle.particleGroup.update() # Beams draw simple lines on update beam.Beam.beamGroup.update() # Update the screen pygame.display.flip() def graph_update(self): ''' Updates the graph (flow network), modifies the GUI paths attribute to include any paths returned from the max flow algorithm. Everytime this function is called, the graph is created from scratch, and max flow called on this new flow network. ''' # Reset paths self.paths = None # Since the graph creates new source/sink nodes, store these along # with the graph object g, s, t = graph.gen_flow_network( asteroid.Asteroid.asteroidGroup.sprites(), # Nodes self.infested_asteroid, # Source s self.space_station, # Sink t alien.Alien.radius) # The max teleport radius # Optional graph edge drawing if self.OPTIONS == 3: self.graph_beams(g) # Run max flow on the newly created graph object flow = graph.max_flow(g, s, t) if flow > 0: # There is at least 1 valid flow path # Reconstruct flows to yield a list of path lists. self.paths = graph.reconstruct_flows(g, s, t) def graph_beams(self, g): ''' Simply for demoing, draws a line for all edges in the graph object created in graph_update using the beam class. ''' for e in g.edges(): beam.Beam.beamGroup.add(\ objects.object_types["beam"](\ health = 1, obj1 = e[0], obj2 = e[1], color = (255,255,0), surface = self.screen)) def alien_path_beams(self, alien): ''' Simply for demoing, draws a line for all edges in the alien's path. Green means still viable, red means out of range. ''' curr = alien.asteroid for succ in alien.path[1:]: if euclidD(succ.position, curr.position) > alien.radius: color = (255, 153, 153) else: color = (102, 155, 102) beam.Beam.beamGroup.add(\ objects.object_types["beam"](\ health = 40, obj1 = curr, obj2 = succ, color = color, surface = self.screen)) curr = succ
def test_timer(self): timer = Timer(0.05) self.assertFalse(timer.update()) time.sleep(0.05) self.assertTrue(timer.update())
class Tank(entities.Entity, entities.ProjectileCollider, entities.Blocking): def __init__(self, location, graphics, heading=Vector(1, 0)): super().__init__() self.graphics = graphics self.heading = Vector(0, -1) self.direction = Direction.NORTH self.moving = False self.type = type self.maxHitPoints = 10 self.hitpoints = self.maxHitPoints self.movementSpeed = 1 self.scorePoints = 0 self.shielded = False self.shieldEndTime = 0 self.weapon = Weapon(self, level=1) self.controller = None self.controllerTimer = Timer(50) tileBlockedFunction = lambda tile: not tile is None and tile.blocksMovement self.movementHandler = MovementHandler(self, tileBlockedFunction) self.setLocation(location) self.setHeading(heading) self.lastHitTime = None self.lastHitVector = Vector(0, 0) self.destroyCallback = None self.destroyed = False def setScorePoints(self, points): self.scorePoints = points def getScorePoints(self): return self.scorePoints def setMaxHitpoints(self, hitpoints): self.maxHitPoints = hitpoints self.repair() def repair(self): self.hitpoints = self.maxHitPoints def setController(self, controller): self.controller = controller self.playerControlled = isinstance(controller, tankcontroller.PlayerTankController) def getController(self): return self.controller def isPlayerControlled(self): return self.playerControlled def update(self, time, timePassed): if self.controllerTimer.update(time): self.controller.update(time, timePassed) if self.moving: movementVector = self.heading.multiplyScalar( self.movementSpeed * timePassed * 0.05).round() self.movementHandler.moveEntity(movementVector) self.checkIfShieldIsDone(time) self.moving = False pass def render(self, screen, offset, time): extraOffset = Vector(0, 0) if not self.lastHitTime == None and time - self.lastHitTime > 50: extraOffset = self.lastHitVector.multiplyScalar(-1).toUnit() drawOffset = Vector(offset[0], offset[1]) self.graphics.render(screen, drawOffset.add(self.location).add(extraOffset), self.direction) self.controller.render(screen) def moveSingleStep(self, direction): self.setHeading(direction) self.movementHandler.moveEntity(direction.toUnit()) def moveInDirection(self, direction): self.setHeading(direction) self.moving = True def canMoveInDirection(self, direction): return self.movementHandler.canMove(direction) def fire(self, time): if self.weapon.canFire(time): location = self.getProjectileFireLocation() self.weapon.fire(location, self.heading, time) def getProjectileFireLocation(self): halfProjectileSize = Vector(2, 2) location = self.getCenterLocation() return location.subtract(halfProjectileSize) # halfProjectileSize = Vector(2, 2) # location = self.getCenterLocation() # location = location.subtract(halfProjectileSize) # location = location.add(self.heading.toUnit().multiplyScalar(self.size.y / 2)) # return location def hitByProjectile(self, projectile, time): if self.shielded: return self.lastHitTime = time self.lastHitVector = projectile.directionVector self.hitpoints -= projectile.power if self.hitpoints <= 0: self.destroy() def setImage(self, image): self.image = image self.setSize(Vector(self.image.get_width(), self.image.get_height())) def getHeading(self): return self.heading def setHeading(self, newHeading): self.heading = newHeading self.direction = self.getDirectionFromVector(self.heading) self.setGraphics(self.direction) def setGraphics(self, direction): self.setImage(self.graphics.baseImages[direction]) self.turretImage = self.graphics.turretImages[direction] self.turretOffset = self.graphics.turretOffsets[direction] def getWeapon(self): return self.weapon def setWeapon(self, newWeapon): self.weapon = newWeapon def enableShield(self, duration): self.shielded = True self.shieldEndTime = pygame.time.get_ticks() + duration print(f'Shield enabled for {duration} seconds') def checkIfShieldIsDone(self, time): if self.shielded and time >= self.shieldEndTime: self.shielded = False print('Shield has ran out') def setDestroyCallback(self, callback): self.destroyCallback = callback def fireDestroyCallback(self): if self.destroyCallback != None: self.destroyCallback(self) def getHitpoints(self): return self.hitpoints def destroy(self): self.destroyed = True self.createExplosion() self.fireDestroyCallback() self.markDisposable() def createExplosion(self): image = images.get('explosion') entities.manager.add( entities.effect.Effect(image, self.getCenterLocation(), 300)) def isDestroyed(self): return self.destroyed
class AiTankController(TankController): def __init__(self, entity): self.entity = entity self.fireTimer = Timer(500) self.lastMovementTime = pygame.time.get_ticks() self.pendingPathSearch = None self.plannedPath = None self.pathPlanTime = 0 self.searchGridFunction = SearchGridGenerator.getSearchSpaceCellValueForTile self.stepLength = 50 def update(self, time, timePassed): if self.fireTimer.update(time): self.fire(time) if self.isPathPlanningPending() and self.isPathPlanningCompleted(): if self.pendingPathSearch.pathFound(): self.plannedPath = PlannedPath(self.pendingPathSearch.getPath()) self.resetLastMovementTime(pygame.time.get_ticks()) self.pendingPathSearch = None def render(self, screen): pass #self.renderPlannedPath(screen) def renderPlannedPath(self, screen): if self.plannedPath != None: image = images.get('projectile') for step in self.plannedPath.path: screen.blit(image, ((step[0] * 8) + 8, step[1] * 8)) def pathRecalculationNeeded(self, time): return (not self.hasPath() and not self.isPathPlanningPending()) or self.isPlannedPathExpired(time) def isPlannedPathExpired(self, time): return time - self.pathPlanTime > 5000 def canMoveAlongPath(self): return self.hasPath() and not self.plannedPath.targetReached() def moveAlongPath(self, time): movementSteps = int((time - self.lastMovementTime ) / self.stepLength) if movementSteps > 0: for _ in range(movementSteps): self.stepTowardsTarget() if self.plannedPath.targetReached(): break self.resetLastMovementTime(time) def resetLastMovementTime(self, time): self.lastMovementTime = time def stepTowardsTarget(self): targetStep = self.toWorldSpaceTuple(self.plannedPath.getTargetStep()) self.moveTowardsLocation(targetStep) self.plannedPath.moveToNextStepIfCurrentStepIsReached(self.entity.getLocation().toIntTuple()) def fire(self, time): self.entity.fire(time) self.pickRandomFireTime() def pickRandomFireTime(self): self.fireTimer.setInterval(random.randint(400, 600)) def plotPathToLocation(self, targetLocation): searchGrid = SearchGridGenerator.generateSearchGridFromPlayfield(self.searchGridFunction) start = self.toSearchSpaceCoordinateTuple(self.entity.getLocation()) end = self.toSearchSpaceCoordinateTuple(targetLocation) self.pendingPathSearch = PathFindingTask(searchGrid, start, end) PathfinderWorker.queueTask(self.pendingPathSearch) self.pathPlanTime = pygame.time.get_ticks() def isPathPlanningPending(self): return self.pendingPathSearch != None def isPathPlanningCompleted(self): return self.pendingPathSearch != None and self.pendingPathSearch.isCompleted() def hasPath(self): return self.plannedPath != None def moveTowardsLocation(self, targetLocation): location = self.entity.location if location.x < targetLocation[0]: self.entity.moveSingleStep(utilities.vectorRight) elif location.x > targetLocation[0]: self.entity.moveSingleStep(utilities.vectorLeft) elif location.y < targetLocation[1]: self.entity.moveSingleStep(utilities.vectorDown) elif location.y > targetLocation[1]: self.entity.moveSingleStep(utilities.vectorUp) def toSearchSpaceCoordinateTuple(self, coordinates): return (int(coordinates.x / 8), int(coordinates.y / 8)) def toWorldSpaceTuple(self, coordinates): return (int(coordinates[0] * 8), int(coordinates[1] * 8))