Untitled
unknown
plain_text
a year ago
43 kB
6
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