def __init__(self, filename: str, pos: tuple, height: float) -> None: """Initialise a new `Image` object. pos: a pair of floats that describe this image's position on the screen. The floats must be normalized, e.g. a value of (0.3, 0.6) means 30% of the screen width and 60% of the screen height. (0, 0) is the top left corner. height: the normalized height of the image. """ validate(self.__init__, locals()) surface_w, surface_h = pygame.display.get_surface().get_size() pos_x, pos_y = pos raw_img = pygame.image.load(filename) # When resizing the image to fit `height`, the image ratio must # be preserved. height_px = surface_h * height width_px = height_px / (raw_img.get_height() / raw_img.get_width()) self.top_left = ( surface_w*pos_x - width_px/2, surface_h*pos_y - height_px/2 ) self.image = pygame.transform.scale( raw_img, (int(width_px), int(height_px)) )
def __init__( self, pos: tuple, height: float, text: str, font_type: str, alignment: str, colour: tuple) -> None: """Initialise a new `Label` object. pos: a pair of floats that describe this label's position on the screen. The floats must be normalized, e.g. a value of (0.3, 0.6) means 30% of the screen width and 60% of the screen height. (0, 0) is the top left corner. height: the normalized height of the label. font_type: the font family of the text. alignment: the alignment of the text in relation to the position. Valid values are 'centre' and 'left'. """ validate(self.__init__, locals()) surface_w, surface_h = pygame.display.get_surface().get_size() pos_x, pos_y = pos self.pos = (surface_w * pos_x, surface_h * pos_y) self.font = Font( pygame.font.match_font(font_type), int(surface_h * height) ) self.text = text self.alignment = alignment self.colour = colour
def point_to_direction(point: tuple) -> Direction: """Converts a vector to a `Direction`.""" validate(point_to_direction, locals()) if point == (0, 1): return NORTH elif point == (1, 1): return NORTHEAST elif point == (1, 0): return EAST elif point == (1, -1): return SOUTHEAST elif point == (0, -1): return SOUTH elif point == (-1, -1): return SOUTHWEST elif point == (-1, 0): return WEST elif point == (-1, 1): return NORTHWEST else: raise ValueError('point {} has incorrect form'.format(point))
def check_event(self, event: EventType) -> None: """Update this component using `event`.""" validate(self.check_event, locals()) level_length = len(self.world.current_level.tiles) # If there is a mouse motion or click event, set `self.hover` # and `self.pressed` to the correct values. if event.type == pygame.MOUSEMOTION: for x in range(level_length): for y in range(level_length): if self.tile_rects[x][y].collidepoint(event.pos): self.hover = (x, y) return self.hover = None elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1: for x in range(level_length): for y in range(level_length): if self.tile_rects[x][y].collidepoint(event.pos): self.pressed = (x, y) self.pressed_this_step = True return
def check_fill_rect( level: Level, point: tuple, width: int, height: int, type_: TileType) -> None: """Fills a rectangular area on `level`. The area is only filled if every tile in the area has a type of `SOLID_EARTH`. Returns true if successful, false otherwise. """ validate(check_fill_rect, locals()) x, y = point top_right = (x + width, y + height) level_length = len(level.tiles) # The bounds of the area need to be checked to ensure the whole # rectangle fits on the level. if (not point_within(level_length, point) or not point_within(level_length, top_right)): return False for i in range(x, x + width): for j in range(y, y + height): if level.tiles[i][j].type != SOLID_EARTH: return False for i in range(x, x + width): for j in range(y, y + height): level.tiles[i][j] = new_tile(type_) return True
def direction_to_point(direction: Direction) -> tuple: """Converts a `Direction` to a vector.""" validate(direction_to_point, locals()) if direction is NORTH: return (0, 1) elif direction is NORTHEAST: return (1, 1) elif direction is EAST: return (1, 0) elif direction is SOUTHEAST: return (1, -1) elif direction is SOUTH: return (0, -1) elif direction is SOUTHWEST: return (-1, -1) elif direction is WEST: return (-1, 0) else: return (-1, 1)
def remove_entity(level: Level, point: tuple) -> None: """Removes the entity on `level` at `point`.""" validate(remove_entity, locals()) x, y = point level.tiles[x][y].entity = None level.entities[level.entities.index(point)] = None
def render(self, surface: Surface) -> None: """Render this display to the given surface.""" validate(self.render, locals()) level_length = len(self.world.current_level.tiles) # Render each tile in the level using the appropriate icon. for x in range(level_length): for y in range(level_length): tile = self.world.current_level.tiles[x][y] rect = self.tile_rects[x][y] if tile.entity is not None: icon = self.icons[tile.entity.icon_name].copy() else: icon = self.icons[tile.type.name].copy() # If the tile is being hovered over, highlight it. if self.hover == (x, y): colourise(icon, (64, 64, 64)) surface.blit(icon, rect)
def render(self, surface: Surface) -> None: """Render this element to the given surface.""" validate(self.render, locals()) # Any messages that are over the character limit must be split # onto multiple lines. self.messages = [ str_ for msg in self.messages for str_ in textwrap.wrap(msg, width=self.chars_per_line) ] # If there are too many lines of messages, remove the ones at # the beginning of the list. while len(self.messages) > self.lines: del self.messages[0] # The colour of each line must be toggled between white and # grey to allow each line to be more easily distinguished. colours = itertools.cycle([(255, 255, 255), (190, 190, 190)]) seq = zip(self.messages, self.y_positions, colours) for msg, y_pos, colour in seq: label = Label( pos=(self.left_pos, y_pos), height=self.font_height, text=msg, font_type='mono', alignment='left', colour=colour ) label.render(surface)
def path_to(self, world: World, point: tuple) -> None: """ Generate the necessary actions to move the hero to `point`. """ validate(self.path_to, locals()) x, y = point tile = world.current_level.tiles[x][y] if not tile.type.passable and tile.type is not CLOSED_DOOR: self.add_message('You cannot move there.') return elif world.hero == point: self.wait() return directions = renethack.entity.find_path(world.hero, point, world.current_level) moves = [Move(d) for d in directions] # If the target tile type is special, convert the last action # to a `Use`. if (tile.type is UP_STAIRS or tile.type is DOWN_STAIRS or tile.type is OPEN_DOOR): self.actions = moves[:-1] self.actions.append(Use(directions[-1])) else: self.actions = moves
def __init__(self, name: str, hit_points: int, defence: int, speed: int, strength: int) -> None: """Initialise a new entity. name: display name hit_points: health defence: damage reduction speed: energy gain per turn strength: damage """ validate(self.__init__, locals()) self.name = name self.hit_points = hit_points self.max_hit_points = hit_points self.defence = defence self.speed = speed self.strength = strength self.level = 1 self.experience = 0 self.score = 0 self.energy = 0 self.hp_counter = 0 self.actions = [] self.messages = [] self.icon_name = 'Hero'
def __init__(self, pos: tuple, height: float) -> None: """Initialise a new `TextBox` object. pos: a pair of floats that describe this text box's position on the screen. The floats must be normalized, e.g. a value of (0.3, 0.6) means 30% of the screen width and 60% of the screen height. (0, 0) is the top left corner. height: the normalized height of the text box. """ validate(self.__init__, locals()) surface_w, surface_h = pygame.display.get_surface().get_size() pos_x, pos_y = pos width = height*5 left_pos = pos_x - width/2 top_pos = pos_y - height/2 self.underline_rect = Rect( surface_w * left_pos, surface_h * (top_pos + height*1.2), surface_w * width, surface_h * height * 0.05 ) # The text of the label must be updated each step, so # initially it is set empty. self.label = Label( pos=pos, height=height*0.8, text='', font_type='sans', alignment='centre', colour=(255, 255, 255) )
def new_tile(type_: TileType) -> Tile: """Returns a new, empty `Tile` object.""" validate(new_tile, locals()) return Tile( type_, entity=None )
def rand_hero(name: str) -> Hero: """Returns a new hero with random stats.""" validate(rand_hero, locals()) return Hero(name=name, hit_points=random.randint(7, 10), defence=random.randint(0, 1), speed=random.randint(50, 75), strength=random.randint(1, 2))
def point_within(length: int, point: tuple) -> bool: """ Check whether a point is within a square grid of length `length`. """ validate(point_within, locals()) x, y = point return 0 <= x < length and 0 <= y < length
def __init__(self, type: TileType, entity) -> None: """Initialise a new `Tile` object. `entity` can either be a `Monster` or a `Hero`. """ validate(self.__init__, locals()) self.type = type self.entity = entity
def render(self, surface: Surface) -> None: """Render this label to the given surface.""" validate(self.render, locals()) for c in self.components: c.render(surface) for x in range(self.bars): surface.fill((255, 255, 255), self.rects[x])
def __init__(self, tiles: list, entities: list) -> None: """Initialises a new `Level` object. tiles: a 2D grid of tiles that describe the layout of the level. entities: the list of points that currently contain entities on the level. """ validate(self.__init__, locals()) self.tiles = tiles self.entities = entities
def __init__(self, levels: list, hero: tuple) -> None: """Initialise a new `World` object. levels: the list of levels to use. hero: the point on the first level that the hero is at. """ validate(self.__init__, locals()) self.upper_levels = [] self.current_level = levels[0] self.lower_levels = levels[1:] self.hero = hero
def get_tiles(level: Level, type_: TileType) -> list: """Returns the list of points on `level` with tile type `type_`.""" validate(get_tiles, locals()) level_length = len(level.tiles) return [ (x, y) for x in range(level_length) for y in range(level_length) if level.tiles[x][y].type is type_ ]
def __init__(self, parent, point: tuple, target_point: tuple) -> None: validate(self.__init__, locals()) self.parent = parent self.point = point x, y = self.point target_x, target_y = target_point self.cost = 0 if parent is None else parent.cost + 1 self.remaining_cost = abs(x - target_x) + abs(y - target_y) self.final_cost = self.cost + self.remaining_cost
def apply(config: Config) -> Surface: """Apply the given config and return the resulting surface object. `pygame.display.set_mode` is used to apply the config. """ validate(apply, locals()) flag = (pygame.FULLSCREEN | pygame.HWSURFACE | pygame.DOUBLEBUF if config.fullscreen else 0) pygame.mixer.music.set_volume(config.volume) return pygame.display.set_mode(config.resolution, flag)
def __init__(self, pos: tuple, width: float, hero: Hero) -> None: """Initialise a new `StatusDisplay` object. pos: a pair of floats that describe this display's position on the screen. The floats must be normalized, e.g. a value of (0.3, 0.6) means 30% of the screen width and 60% of the screen height. (0, 0) is the top left corner. width: the normalized width of the display. """ validate(self.__init__, locals()) surface_w, surface_h = pygame.display.get_surface().get_size() pos_x, pos_y = pos height = width*1.25 top_pos = pos_y - height/2 left_pos = pos_x - width/2 text_height = height*0.1 # Generate a sequence of labels, each with the same x position # and a different y position. labels = ( Label( pos=(left_pos, top_pos + height*x), height=text_height, text='', font_type='mono', alignment='left', colour=(255, 255, 255) ) for x in xrange(1/14, 1, 1/7) ) self.name_label = next(labels) self.score_label = next(labels) self.level_label = next(labels) self.hp_label = next(labels) self.defence_label = next(labels) self.speed_label = next(labels) self.strength_label = next(labels) self.components = [ self.name_label, self.score_label, self.level_label, self.hp_label, self.defence_label, self.speed_label, self.strength_label ] self.hero = hero
def vol_update(self, volume: float) -> float: """ Update this component using `volume`. Returns the new volume. """ validate(self.vol_update, locals()) if self.left_button.pressed: volume = min_clamp(volume-0.1, 0.0) elif self.right_button.pressed: volume = max_clamp(volume+0.1, 1.0) self.bars = round(volume * 10) return volume
def __init__(self, pos: tuple, height: float) -> None: """Initialise a new `VolumeDisplay` object. pos: a pair of floats that describe this display's position on the screen. The floats must be normalized, e.g. a value of (0.3, 0.6) means 30% of the screen width and 60% of the screen height. (0, 0) is the top left corner. height: the normalized height of the display. """ validate(self.__init__, locals()) surface_w, surface_h = pygame.display.get_surface().get_size() pos_x, pos_y = pos width = height*6 button_width = 0.15 bar_width = (1 - 2*button_width)/10 bar_rect_width = bar_width*0.5 self.bars = 0 self.left_button = Button( pos=(pos_x - width*(0.5 - button_width/2), pos_y), width=width*button_width, height=height, text='<' ) self.right_button = Button( pos=(pos_x + width*(0.5 - button_width/2), pos_y), width=width*button_width, height=height, text='>' ) self.components = [self.left_button, self.right_button] # Generate a list of rectangles that represent the current # volume, depending on which rectangles are rendered. self.rects = [ Rect( surface_w * (pos_x - width/2 + width*x), surface_h * (pos_y - height/2), surface_w * width * bar_rect_width, surface_h * height ) for x in xrange( button_width + (bar_width - bar_rect_width)/2, 1-button_width, bar_width ) ]
def step(self, ms_per_step: float) -> None: """Update this element.""" validate(self.step, locals()) self.name_label.text = self.hero.name self.score_label.text = 'Score: {}'.format(self.hero.score) self.level_label.text = 'Level {}'.format(self.hero.level) self.hp_label.text = 'Hit Points: {}/{}'.format( self.hero.hit_points, self.hero.max_hit_points) self.defence_label.text = 'Defence: {}'.format(self.hero.defence) self.speed_label.text = 'Speed: {}'.format(self.hero.speed) self.strength_label.text = 'Strength: {}'.format(self.hero.strength)
def check_event(self, event: EventType) -> None: """Check `event` with this element.""" validate(self.check_event, locals()) # If backspace is pressed, the last character must be removed. # If any other key is pressed, add its character to the label. if event.type == pygame.KEYDOWN: if event.key == pygame.K_BACKSPACE: self.label.text = self.label.text[:-1] else: self.label.text += event.unicode
def step(self, ms_per_step: float) -> None: """Step this component by `ms_per_step`.""" validate(self.step, locals()) # Only set `self.pressed` to `None` if the click event happened # during the last step, not this one. if self.pressed is not None: if self.pressed_this_step: self.pressed_this_step = False else: self.pressed = None del self.pressed_this_step
def step(self, ms_per_step: float) -> None: """Step this button by `ms_per_step`.""" validate(self.step, locals()) # Only set `self.pressed` to `False` if the click event # happened during the last step, not this one. if self.pressed: if self.pressed_this_step: self.pressed_this_step = False else: self.pressed = False del self.pressed_this_step
def __init__(self, pos: tuple, height: float, score: Score) -> None: """Initialise a new `ScoreDisplay` object. pos: a pair of floats that describe this display's position on the screen. The floats must be normalized, e.g. a value of (0.3, 0.6) means 30% of the screen width and 60% of the screen height. (0, 0) is the top left corner. height: the normalized height of the display. """ validate(self.__init__, locals()) pos_x, pos_y = pos line_height = height/3 text_height = line_height*0.8 self.components = [] self.components.append( Label( pos=(pos_x, pos_y - line_height), height=text_height, text='Name: {}'.format(score.name), font_type='sans', alignment='left', colour=(255, 255, 255) ) ) self.components.append( Label( pos=pos, height=text_height, text='Level: {}'.format(score.level), font_type='sans', alignment='left', colour=(255, 255, 255) ) ) self.components.append( Label( pos=(pos_x, pos_y + line_height), height=text_height, text='Score: {}'.format(score.score), font_type='sans', alignment='left', colour=(255, 255, 255) ) )