Untitled
unknown
plain_text
5 months ago
43 kB
2
Indexable
# DO NOT modify or add any import statements import tkinter.filedialog from a2_support import * import tkinter as tk from tkinter import messagebox, filedialog from typing import Optional, Callable # Name: Dzmitry Silchanka # Student Number: s4885581 # ----------- # Write your classes and functions here class Tile: def __init__(self) -> None: pass def __repr__(self) -> str: """ Returns a machine-readable string that could be used to construct an identical instance of the tile. """ return f"{self.__class__.__name__}()" def __str__(self) -> str: """ Returns the character representing the type of the tile. """ return TILE_SYMBOL def get_tile_name(self) -> str: """ Returns the name of the type of the tile. """ return TILE_NAME def is_blocking(self) -> bool: """ Returns True only when the tile is blocking. By default, tiles are not blocking. """ return False class Mountain(Tile): def __init__(self) -> None: super().__init__() def is_blocking(self) -> bool: """ Returns True if the tile is blocking. By default, Mountains are blocking. """ return True def get_tile_name(self) -> str: """ Returns the name of the mountain Tile. """ return MOUNTAIN_NAME def __str__(self) -> str: """ Returns the character representing the type of the tile. """ return MOUNTAIN_SYMBOL class Ground(Tile): def __init__(self) -> None: super().__init__() def get_tile_name(self) -> str: """ Returns the name of the ground Tile. """ return GROUND_NAME def __str__(self) -> str: """ Returns the symbol representing a ground Tile """ return GROUND_SYMBOL class Building(Tile): def __init__(self, initial_health: int) -> None: self.health = initial_health super().__init__() def is_destroyed(self) -> bool: """ Returns true if the building is destroyed and false otherwise. """ if self.health != 0: return False else: return True def damage(self, damage: int): """ Damages the building by the amount specified in "damage". """ if not self.is_destroyed(): self.health = min(9, max(0, self.health - damage)) def is_blocking(self) -> bool: """ Returns if the building is blocking (True) or not (False). """ if self.health != 0: return True else: return False def __str__(self) -> str: """ Returns the health of the building as a string. """ return str(self.health) def get_tile_name(self) -> str: """ Returns the name of the building tile. """ return BUILDING_NAME def __repr__(self) -> str: """ Returns a string that can be used to create another building instance. """ return f"{self.__class__.__name__}("+str(self.health)+")" class Board: def __init__(self, board) -> None: """ Concrete class representing the board on which the game is played Each position in the board is a tuple, where (0,0) is the top left corner """ self.board = self.create_board(board) def create_board(self, board: list[str]) -> list: """ Converts a list of strings representing tiles to their respective Tile objects. """ return [[self.char_to_tile(char) for char in row] for row in board] def char_to_tile(self, char) -> Tile: """ :param char: String representing tile :return: Tile object representing char. """ if char == " ": return Ground() elif char == "M": return Mountain() elif char.isdigit(): return Building(int(char)) def tile_to_char(self, tile) -> str: """ :param tile: Tile Object :return: String Representation of tile. """ if isinstance(tile, Ground): return " " elif isinstance(tile, Mountain): return "M" elif isinstance(tile, Building): return str(tile.health) def __repr__(self) -> str: """ :return: String representing the board """ return f"Board({[[self.tile_to_char(tile) for tile in row] for row in self.board]})" def __str__(self): return "\n".join("".join(str(tile) for tile in row) for row in self.board) def get_dimensions(self) -> tuple[int, int]: """ Returns the (#rows, #columns) dimensions of the board. """ return len(self.board), len(self.board[0]) def get_tile(self, position: tuple[int, int]) -> Tile: """ Returns the tile at the specified position. """ return self.board[position[0]][position[1]] def get_buildings(self) -> dict[tuple[int, int], Building]: """ Returns a dictionary mapping the positions of buildings to the building instances at those positions. """ buildings = {} for row_index, row in enumerate(self.board): for col_index, _ in enumerate(row): tile = self.get_tile((row_index, col_index)) if isinstance(tile, Building): buildings[(row_index, col_index)] = tile return buildings class Entity: Entity_ID_Counter = 0 def __init__(self, position, initial_health, speed, strength) -> None: """ :param position: Position of entity. :param initial_health: Starting health of entity. :param speed: How many tiles the entity can move in a turn. :param strength: How strong the entity's damage or healing is. """ self._id = Entity.Entity_ID_Counter Entity.Entity_ID_Counter += 1 self.position = position self.health = initial_health self.speed = speed self.strength = strength self._name = self.__class__.__name__ def __repr__(self) -> str: """ :return: Comma separated string representing entity. """ return f'{self._name}({self.position}, {self.health}, {self.speed}' \ f', {self.strength})' def __str__(self) -> str: """ :return: Comma seperated string from which an identical entity can be constructed """ return f'{ENTITY_SYMBOL},{self.position[0]},{self.position[1]}' \ f',{self.health},{self.speed},{self.strength}' def get_symbol(self) -> str: """ :return: Symbol representing entity """ return ENTITY_SYMBOL def get_name(self) -> str: """ :return: Name representing entity """ return self._name def get_position(self) -> tuple[int, int]: """ :return: Entity Position - tuple """ return self.position def set_position(self, position: tuple[int, int]) -> None: """ Sets position of entity :param position: Tuple representing postion """ self.position = position def get_health(self) -> int: """ :return: health of entity """ return self.health def get_speed(self) -> int: """ :return: speed of entity """ return self.speed def get_strength(self) -> int: """ :return: strength of entity """ return self.strength def damage(self, damage: int) -> None: """ lowers the enemy's health by the damage dealt :param damage: int of how much damage entity takes """ if self.is_alive(): self.health = max(0, self.health - damage) def is_alive(self) -> bool: """ :return: is the entity is alive (more than 0) health or not """ return self.health > 0 def is_friendly(self) -> bool: """ :return: if the entity is friendly. default, NOT friendly """ return False def get_targets(self) -> list[tuple[int, int]]: """ :return: positions the entity is targeting. default, adjacent squares not inc. diagonals """ return [(self.position[0] + dx, self.position[1] + dy) for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]] def attack(self, entity) -> None: """ :param entity: target entity damages the target entity by the current entity's strength. """ if self.is_alive(): entity.damage(self.strength) def get_display_character(self) -> str: """ :return: The character that will be displayed on the game board """ if self.get_symbol() == SCORPION_SYMBOL: return "\U00010426" elif self.get_symbol() == FIREFLY_SYMBOL: return "\U00000D9E" elif self.get_symbol() == HEAL_SYMBOL: return "\U0001F6E1" elif self.get_symbol() == TANK_SYMBOL: return "\U000023F8" class Mech(Entity): """ Abstract class that inherits from Entity, representing a mech. """ def __init__(self, position, initial_health, speed, strength) -> None: super().__init__(position, initial_health, speed, strength) self._previous_position = position self._is_active = True def is_friendly(self) -> bool: """ :return: True, as all mechs are friendly """ return True def get_symbol(self) -> str: """ :return: Symbol representing a Mech """ return MECH_SYMBOL def get_name(self) -> str: """ :return: Name of mech """ return MECH_NAME def enable(self) -> None: """ Enables the mech to move this turn """ self._is_active = True def disable(self) -> None: """ Disables the mech, preventing it from moving """ self._is_active = False def is_active(self) -> bool: """ Returns bool of if the mech is active or not. """ return self._is_active def set_position(self, position) -> None: """ :param position: Position for the mech to move Moves the mech to the position specified """ self._previous_position = self.get_position() super().set_position(position) def get_previous_position(self) -> tuple[int, int]: """ :return: the previous position of the mech """ return self._previous_position def __str__(self) -> str: """ :return: string that can be used to construct an identical mech """ return f'{MECH_SYMBOL},{self.position[0]},{self.position[1]}' \ f',{self.health},{self.speed},{self.strength}' class TankMech(Mech): """ Concrete class representing a Tank Mech, which is an attacking unit """ def __init__(self, position, initial_health, speed, strength) -> None: super().__init__(position, initial_health, speed, strength) self._previous_position = position def get_symbol(self) -> str: """ :return: Tank symbol """ return TANK_SYMBOL def get_name(self) -> str: """ :return: Tank name """ return TANK_NAME def __str__(self) -> str: """ :return: String representing the tank entity """ return f'{TANK_SYMBOL},{self.position[0]},{self.position[1]}' \ f',{self.health},{self.speed},{self.strength}' def get_targets(self) -> list[tuple[int, int]]: """ :return: List of tuples (positions) that the tank is attacking Tanks attack horizontally from both sides, up to their range """ x, y = self.position targets = [(x, y+i) for i in range(1, (TANK_RANGE+1))] targets += [(x, y-i) for i in range(1, (TANK_RANGE+1))] return targets class HealMech(Mech): """ Concrete class representing a Heal Mech, which is a protective unit """ def __init__(self, position, health, strength, speed) -> None: super().__init__(position, health, strength, speed) def __str__(self) -> str: """ :return: String representing the Heal Mech entity """ return f'{HEAL_SYMBOL},{self.position[0]},{self.position[1]}' \ f',{self.health},{self.speed},{self.strength}' def get_symbol(self) -> str: return HEAL_SYMBOL def get_name(self) -> str: """ :return: Name of heal mech """ return HEAL_NAME def get_strength(self) -> int: """ :return: Strength of HealMech. Negative value as these mechs heal units. """ return -self.strength def attack(self, entity) -> None: """ :param entity: target unit Heals the unit if they are friendly """ if entity.is_friendly(): entity.health += self.strength class Enemy(Entity): """ Abstract class representing an enemy unit """ def __init__(self, position, health, speed, strength) -> None: super().__init__(position, health, speed, strength) self.objective = position def __str__(self) -> str: """ :return: String representing the enemy """ return f'{ENEMY_SYMBOL},{self.position[0]},{self.position[1]}' \ f',{self.health},{self.speed},{self.strength}' def get_symbol(self) -> str: """ :return: Character representing the enemy """ return ENEMY_SYMBOL def get_name(self) -> str: """ :return: enemy name """ return ENEMY_NAME def get_objective(self) -> tuple[int, int]: """ :return: enemy objective (where it wants to go) """ return self.objective def update_objective(self, entities, buildings) -> None: """ default behavior: set objective to current position :param entities: list of entities :param buildings: list of buildings """ self.objective = self.position class Scorpion(Enemy): """ Concrete class representing a Scorpion, which is an enemy type. """ def __init__(self, position, health, speed, strength) -> None: super().__init__(position, health, speed, strength) def get_symbol(self) -> str: """ :return: Symbol representing a Scorpion """ return SCORPION_SYMBOL def get_name(self) -> str: """ :return: Name of Scorpion unit """ return SCORPION_NAME def __str__(self) -> str: """ :return: String representation of a scorpion. """ return f'{SCORPION_SYMBOL},{self.position[0]},' \ f'{self.position[1]},{self.health},{self.speed},{self.strength}' def get_targets(self) -> list[tuple[int, int]]: """ :return: List of tuples (positions) the scorpion is attacking the scorpion attacks vertically and horizontally at a distance equal to their range """ x, y = self.position targets = [(x, y+i) for i in range(-SCORPION_RANGE, 0)]\ + [(x, y+i) for i in range(1, SCORPION_RANGE+1)] targets += [(x+i, y) for i in range(-SCORPION_RANGE, 0)]\ + [(x+i, y) for i in range(1, SCORPION_RANGE+1)] return targets def update_objective(self, entities, buildings) -> None: """ :param entities: list of entities :param buildings: list of buildings Assigns the position of the mech with the highest health as the scorpion's objective """ mechs = [] for e in entities: if isinstance(e, (TankMech, HealMech)): mechs.append(e) if not mechs: return max_health = 0 max_health_mech = None for mech in mechs: health = mech.get_health() if health > max_health: max_health = health max_health_mech = mech self.objective = max_health_mech.get_position() class Firefly(Enemy): """ Concrete class representing a Firefly, which is a hostile unit """ def __init__(self, position, health, speed, strength) -> None: super().__init__(position, health, speed, strength) def get_symbol(self) -> str: """ :return: Symbol representing a firefly """ return FIREFLY_SYMBOL def get_name(self) -> str: """ :return: Name of a Firefly """ return FIREFLY_NAME def __str__(self) -> str: """ :return: string representing a unique firefly entity """ return f'{FIREFLY_SYMBOL},{self.position[0]},{self.position[1]}' \ f',{self.health},{self.speed},{self.strength}' def get_targets(self) -> list[tuple[int, int]]: """ :return: list of tuples representing positions the firefly is targeting The firefly can positions above and below itself equal to its range. """ x, y = self.position targets = [(x+i, y) for i in range(-FIREFLY_RANGE, 0)]\ + [(x+i, y) for i in range(1, FIREFLY_RANGE+1)] return targets def update_objective(self, entities, buildings) -> None: """ :param entities: list of entities :param buildings: list of buildings Sets its objective to the position of the building with the lowest health """ if not buildings: return min_health_building = \ min(buildings, key=lambda x: int(str(buildings[x]))) self.objective = min_health_building class BreachModel: """ Concrete class representing the state of the game Includes the current position of the board, entities, if it is ready to save, moving enemies, making attacks, assigning objectives, and ending turns. Also checks if the player has won or lost. """ def __init__(self, board: Board, entities: list[Entity]) -> None: self.board = board self.entities = entities self.is_ready_to_save = True self.game_end_callback = None def __str__(self) -> str: """ :return: string representation of the board """ return f"{self.board}\n\n" + "\n".join(str(entity) for entity in self.entities) def get_board(self) -> Board: """ :return: the board (object) """ return self.board def get_entities(self) -> list: """ :return: list of entities """ return self.entities def entity_positions(self) -> dict[tuple[int, int], Entity]: """ :return: dictionary containing all entities and their position """ return {entity.get_position(): entity for entity in self.entities} def has_lost(self) -> bool: """ :return: True or False depending on if the current state of the game is lost or not """ buildings = [] mechs = [entity for entity in self.entities if isinstance(entity, Mech)] for building in self.board.get_buildings().values(): buildings.append(building) if all(building.is_destroyed() for building in buildings): return True elif all(entity.health == 0 for entity in mechs): return True else: return False def has_won(self) -> bool: """ :return: True or False depending on if the current state of the game is won or not """ enemies = [entity for entity in self.entities if isinstance(entity, Enemy)] mechs = [entity for entity in self.entities if isinstance(entity, Mech)] all_enemies_destroyed = all(not enemy.is_alive() for enemy in enemies) at_least_one_mech_alive = any(mech.is_alive() for mech in mechs) buildings = [] for building in self.board.get_buildings().values(): buildings.append(building) at_least_one_building_alive = any(not x.is_destroyed() for x in buildings) return all_enemies_destroyed and at_least_one_mech_alive\ and at_least_one_building_alive def get_valid_movement_positions(self, entity: Entity)\ -> list[tuple[int, int]]: """ :param entity: current entity :return: list of positions that the entity can move to in its turn """ board = self.get_board() entity_position = entity.get_position() movement_positions = [] for row in range(board.get_dimensions()[0]): for col in range(board.get_dimensions()[1]): position = (row, col) if position != entity_position and not\ board.get_tile(position).is_blocking(): distance = get_distance(self, entity_position, position) if distance != -1 and distance <= entity.get_speed(): movement_positions.append((distance, position)) movement_positions.sort() return [position for _, position in movement_positions] def attempt_move(self, entity, position) -> None: """ :param entity: current entity :param position: where the entity is trying to move moves the entity to the desired position and disables it """ if entity.is_friendly() and entity.is_active() and position\ in self.get_valid_movement_positions(entity): entity.set_position(position) entity.disable() self.is_ready_to_save = False def assign_objectives(self) -> None: """ assigns all entities their objective based on the state of the game """ for entity in self.get_entities(): if isinstance(entity, Enemy): entity.update_objective(self.get_entities(), self.get_board().get_buildings()) def move_enemies(self) -> None: """ moves all enemies to the correct place on the board considering the state of the game, their speed, and what their objective is """ self.assign_objectives() for entity in self.get_entities(): if isinstance(entity, Enemy): min_distance = float('inf') best_positions = [] for position in self.get_valid_movement_positions(entity): distance = get_distance(self, entity.get_objective() , position) if distance != -1 and (distance < min_distance or distance == min_distance): min_distance = distance if distance == 1 and min_distance == 1: best_positions.append(position) else: best_positions = [position] elif distance == -1: best_positions.append(position) if isinstance(entity, Firefly): print(best_positions, min_distance) if best_positions: if min_distance == 1: best_positions.sort(key=lambda x: (-x[0], x[1])) for position in best_positions: entity.set_position(position) if entity.get_objective() in entity.get_targets(): break else: best_positions.sort(key=lambda x: (x[0], -x[1])) if min_distance != float("inf"): entity.set_position(best_positions[0]) def make_attack(self, entity) -> None: """ :param entity: current entity makes the current entity attack all positions in its range. """ for target in self.entities: if target.position in entity.get_targets(): target.damage(entity.get_strength()) buildings = self.board.get_buildings() for position, building in buildings.items(): if position in entity.get_targets(): building.damage(entity.get_strength()) def end_turn(self) -> None: """ functionality for when the player has ended their turn according to the game rules """ for entity in self.get_entities(): if entity.is_alive(): self.make_attack(entity) if not entity.is_alive(): self.entities.remove(entity) for entity in self.entities: if not entity.is_alive(): self.entities.remove(entity) self.move_enemies() self.is_ready_to_save = True for entity in self.entities: if isinstance(entity, Mech): entity.enable() def ready_to_save(self) -> bool: """ :return: True if the game is in a saveable state, otherwise False """ return self.is_ready_to_save # GUI class GameGrid(AbstractGrid): """ Concrete class representing the grid-like board the player will see when they play. Uses AbstractGrid, a class that provides methods to help use a Tkinter canvas as a grid """ def __init__(self, master, dimensions, size, **kwargs) -> None: super().__init__(master, dimensions, size, **kwargs) def get_cell_size(self) -> tuple[int, int]: """Returns the size of the cells (width, height) in pixels.""" return self._get_cell_size() def redraw(self, board, entities, highlighted=None, movement=False): """Clears the game grid, then redraws it according to the provided information.""" self.clear() for row in range(board.get_dimensions()[0]): for col in range(board.get_dimensions()[1]): tile = board.get_tile((row, col)) if highlighted and (row, col) in highlighted: if movement: colour = MOVE_COLOR else: colour = ATTACK_COLOR elif type(tile) is Building and not tile.is_destroyed(): colour = BUILDING_COLOR elif type(tile) is Mountain: colour = MOUNTAIN_COLOR elif type(tile) is Ground: colour = GROUND_COLOR else: colour = DESTROYED_COLOR self.color_cell((row, col), colour) if colour == BUILDING_COLOR\ or (colour == ATTACK_COLOR and isinstance(tile, Building)): self.annotate_position((row, col), str(tile.health)) for entity in entities: self.annotate_position((entity.position[0], entity.position[1]) , entity.get_display_character()) def bind_click_callback(self, click_callback: Callable[[tuple[int, int]] , None]) -> None: """ :param click_callback: callable function taking a position binds the left and right mouse clicks to a function in the controller class returns a tuple (position) where the player has clicked """ self.bind('<Button-1>', lambda event: click_callback(self.pixel_to_cell(event.x, event.y))) self.bind('<Button-2>', lambda event: click_callback(self.pixel_to_cell(event.x, event.y))) class SideBar(AbstractGrid): """ Concrete class representing the sidebar, which shows the player a list of all entities, their positions, and strength. """ def __init__(self, master, dimensions, size, **kwargs) -> None: super().__init__(master, dimensions, size, **kwargs) self._header_font = SIDEBAR_FONT def display(self, entities) -> None: """ :param entities: list of entities draws the header, and list of entities and their attributes """ self.clear() self._draw_header() self.set_dimensions((len(entities) + 1, 4)) for i, entity in enumerate(entities): self._draw_entity_row(i, entity) def _draw_header(self) -> None: """ annotates each position the header approptiately """ for i, heading in enumerate(SIDEBAR_HEADINGS): self.annotate_position((0, i), heading, font=self._header_font) def _draw_entity_row(self, row, entity) -> None: """ :param row: row in sidebar :param entity: current entity Draws the entities position, health, and strength onto the sidebar. """ symbol = entity.get_display_character() self.annotate_position((row + 1, 0), symbol) self.annotate_position((row + 1, 1), f"({entity.position[0]}" f", {entity.position[1]})") self.annotate_position((row + 1, 2), str(entity.health)) self.annotate_position((row + 1, 3), str(entity.get_strength())) class ControlBar(tk.Frame): """ Concrete class representing the bar housing the three buttons at the bottom of the display, allowing the user to save, load, and end their turn """ def __init__(self, master: tk.Widget , save_callback: Optional[Callable[[], None]] = None , load_callback: Optional[Callable[[], None]] = None , turn_callback: Optional[Callable[[], None]] = None , **kwargs) -> None: super().__init__(master, **kwargs) self.save_callback = save_callback self.load_callback = load_callback self.turn_callback = turn_callback # create widgets and layout here self.save_button = tk.Button(self, text="Save Game" , command=self.on_save) self.save_button.pack(side='left', expand=True) self.load_button = tk.Button(self, text="Load Game" , command=self.on_load) self.load_button.pack(side='left', expand=True) self.turn_button = tk.Button(self, text="End Turn" , command=self.on_turn) self.turn_button.pack(side='left', expand=True) def on_save(self) -> None: """ when save button clicked, callback to controller """ if self.save_callback: self.save_callback() def on_load(self) -> None: """ when load button clicked, callback to controller """ if self.load_callback: self.load_callback() def on_turn(self) -> None: """ when end turn button clicked, callback to controller """ if self.turn_callback: self.turn_callback() class BreachView: """ Concrete class that combines GameGrid, SideBar, and ControlBar to display to the player the current game state. """ def __init__(self, root: tk.Tk, board_dims: tuple[int, int] , save_callback: Optional[Callable[[], None]] = None , load_callback: Optional[Callable[[], None]] = None , turn_callback: Optional[Callable[[], None]] = None , ) -> None: self.root = root self.root.title("Into The Breach") self.board_dims = board_dims self.banner_frame = tk.Frame(self.root, height=BANNER_HEIGHT) self.banner_frame.pack() self.banner = tk.Label(self.banner_frame, text=BANNER_TEXT, font=BANNER_FONT) self.banner.pack(fill='x') self.game_frame = tk.Frame(self.root) self.game_grid = GameGrid(self.game_frame, self.board_dims , (GRID_SIZE, GRID_SIZE)) self.game_grid.pack(side='left') self.side_bar = SideBar(self.game_frame, (1, 4) , (SIDEBAR_WIDTH, GRID_SIZE)) self.side_bar.pack(side='left') self.control_frame = tk.Frame(self.root, height=CONTROL_BAR_HEIGHT , width=SIDEBAR_WIDTH+GRID_SIZE) self.control_bar = ControlBar(self.control_frame) self.control_bar.pack(fill='x', expand=True) self.game_frame.pack() self.control_frame.pack(fill='x', expand=True) def bind_click_callback(self, click_callback: Callable[[tuple[int, int]] , None]) -> None: """ :param click_callback: callable function binds a function in the controller to the callback in the game grid """ self.game_grid.bind_click_callback(click_callback) def redraw(self, board: Board, entities: list[Entity] , highlighted: list[tuple[int, int]] = None, movement: bool = False) -> None: """ :param board: current board :param entities: list of entities :param highlighted: which positions are highlighted :param movement: if the highlight should be shown as movement or attack redraws the view model to the current state of the game. """ self.game_grid.redraw(board, entities, highlighted, movement) self.side_bar.display(entities) class IntoTheBreach: """ Concrete class that acts as the controller to the game Controller gameplay, saving, and loading. """ def __init__(self, root: tk.Tk, game_file: str) -> None: self.board, self.entities = self.initialise_from_file(game_file) self.game_board = Board(self.board) self.root = root self.file_path = game_file self.highlighted = [] self.focussed_entity = None self.model = BreachModel(self.game_board , self.entities) # Initialize BreachModel self.view = BreachView(self.root , self.game_board.get_dimensions() , self.entities) # Initialize BreachView instance self.view.bind_click_callback(self._handle_click) self.view.control_bar.turn_callback = self._end_turn self.view.control_bar.load_callback = self._load_game self.view.control_bar.save_callback = self._save_game self.view.side_bar.display(self.entities) self.redraw() # Redraw the view based on the initial game state def initialise_from_file(self, file_path): """ :param file_path: file path to computer directory :return: tuple containing the board as a list of strings, or none in case of error loading the game. """ try: with open(file_path, 'r') as f: content = f.read() paragraphs = content.split('\n\n') board = [line for line in paragraphs[0].split('\n')] entities = [] for entity_str in paragraphs[1].split('\n'): if entity_str: entity_type, x, y, health, speed, strength\ = entity_str.split(',') x, y, health, speed, strength = int(x), int(y)\ , int(health), int(speed), int(strength) if entity_type == 'T': entities.append(TankMech((x, y), health , speed, strength)) elif entity_type == 'H': entities.append(HealMech((x, y), health , speed, strength)) elif entity_type == 'S': entities.append(Scorpion((x, y), health , speed, strength)) elif entity_type == 'F': entities.append(Firefly((x, y), health , speed, strength)) except ValueError: messagebox.showerror(IO_ERROR_TITLE, IO_ERROR_MESSAGE) return None return board, entities def redraw(self) -> None: """ Redraws the display according to the current game state """ self.view.game_grid.redraw(self.game_board, self.entities) self.view.side_bar.display(self.entities) def set_focussed_entity(self, entity: Optional[Entity]) -> None: """ :param entity: entitiy to focus redraws a grid with the focused entity. focused entities highlight the tiles they are able to move to if they are friendly and haven't moved, or the tiles they will attack if they are unfriendly or have moved. """ if entity: if isinstance(entity, Mech): if entity.is_active(): self.highlighted\ = self.model.get_valid_movement_positions(entity) self.view.redraw(self.game_board, self.entities , self.highlighted, True) else: self.draw_targets(entity) elif not entity.is_friendly(): self.draw_targets(entity) else: self.view.redraw(self.game_board, self.entities) self.highlighted = [] def draw_targets(self, entity) -> None: """ :param entity: current entity :return: highlights the positions that the entity is attacking """ self.highlighted = entity.get_targets() self.view.redraw(self.game_board, self.entities, self.highlighted) self.highlighted = [] def make_move(self, position: tuple[int, int]) -> None: """ :param position: position to move entity to :return: attempts to move the focussed entity to a position """ self.model.attempt_move(self.focussed_entity, position) self.view.redraw(self.game_board, self.entities) def load_model(self, file_path: str) -> None: """ :param file_path: file path to file in directory loads a new game state based on the model in the file """ try: board, entities = self.initialise_from_file(file_path) self.file_path = file_path self.entities = entities self.board = self.game_board self.game_board.board = self.game_board.create_board(board) self.model.entities = self.entities self.model.board = self.game_board self.view.board_dims = self.game_board.get_dimensions() self.view.game_grid.set_dimensions\ (self.game_board.get_dimensions()) self.view.redraw(self.game_board, self.entities) print(self.game_board) except IndexError: tk.messagebox.showerror(IO_ERROR_TITLE , f'{IO_ERROR_MESSAGE}{file_path}') def _save_game(self) -> None: """ Opens a file dialogue where the user is able to save their game to a new file. Raises error if the game state does not allow for a save at the moment """ if self.model.ready_to_save(): file_path = tkinter.filedialog.asksaveasfilename\ (defaultextension='.txt', filetypes=[("Text Files", "*.txt")]) if file_path: with open(file_path, 'w') as file: file.write(str(self.model)) else: tk.messagebox.showerror(INVALID_SAVE_TITLE, INVALID_SAVE_MESSAGE) def _load_game(self) -> None: """ Opens a filedialogue that asks player for a file they wish to open a new level with. """ try: file_path = tkinter.filedialog.askopenfilename\ (defaultextension='.txt') if file_path: self.load_model(file_path) except IOError: messagebox.showerror(IO_ERROR_TITLE, IO_ERROR_MESSAGE) def _end_turn(self) -> None: """ Runs functionality for ending the player's turn and redraws the board. """ self.model.end_turn() self.view.redraw(self.game_board, self.entities) check_play_again = None if self.model.has_won(): check_play_again = messagebox.askquestion\ ("You Win!", "You Win! Would you like to play again?") if self.model.has_lost(): check_play_again = messagebox.askquestion\ ("You Lost!", "You Lost! Would you like to play again?") if check_play_again == "yes": self.load_model(self.file_path) elif check_play_again == "no": self.root.destroy() def _handle_click(self, position: tuple[int, int]) -> None: """ :param position: where the player clicked Handles what occurs when a player clicks the game grid. """ print(f"Clicked on tile at position {position}") if position in self.model.entity_positions(): for entity in self.entities: if entity.position == position: self.focussed_entity = entity self.set_focussed_entity(entity) elif position in self.highlighted: self.make_move(position) else: self.focussed_entity = None self.set_focussed_entity(None) def play_game(root: tk.Tk, file_path: str) -> None: """ :param root: tk root instance :param file_path: path to file in directory initiates a new controller using the given model """ controller = IntoTheBreach(root, file_path) controller.root.mainloop() def main() -> None: """ Is called when the program is run """ root_instance = tk.Tk() play_game(root_instance, "level1.txt") if __name__ == "__main__": main()
Editor is loading...
Leave a Comment