diff --git a/Tiles/a3_support.py b/Tiles/a3_support.py new file mode 100644 index 0000000..6f93168 --- /dev/null +++ b/Tiles/a3_support.py @@ -0,0 +1,1265 @@ +# VERSION 1.0.1 + +import itertools +import bisect +import random +from ee import * +import tkinter as tk + +# Number of rows/columns in the grid +GRID_SIZE = 6 + +GRID_CELL_WIDTH = GRID_CELL_HEIGHT = 80 # Pixel width/height of grid cell +GRID_PADDING = 14 # Pixel width/height to use as padding between cells. + +# Pixel width/height of grid +GRID_WIDTH = 550 # (GRID_SIZE - 1) * GRID_PADDING + GRID_SIZE * GRID_CELL_WIDTH +GRID_HEIGHT = 550 # (GRID_SIZE - 1) * GRID_PADDING + GRID_SIZE * GRID_CELL_HEIGHT + +# The size of the player/enemy +CHARACTER_WIDTH = 200 +CHARACTER_HEIGHT = GRID_HEIGHT + +# A run must have at least this many cells in a straight line to be removed +MINIMUM_STRAIGHT_RUN = 3 + +PERFORMANCE_SCALE = 400 # For CSSE7030 students +SLOWNESS = 1 # A positive integer to increase the slowness of animation + +DROP_TIME = 250 * SLOWNESS # Time to take dropping a cell by one row +DROP_STEPS = 5 # Number of steps to take while dropping a tile +# Time to wait before dropping tile another step +DROP_TIME_STEP = DROP_TIME // DROP_STEPS + +# Time to wait between removing next run (prior to dropping new tiles) +RUN_REMOVE_STEP = 250 * SLOWNESS + +# Direction labels/deltas +NORTH = 'n' +EAST = 'e' +SOUTH = 's' +WEST = 'w' + +DIRECTION_DELTAS = { + NORTH: (-1, 0), + EAST: (0, 1), + SOUTH: (1, 0), + WEST: (0, -1) +} + +# Task 1 format strings +SCORE_FORMAT = "Score: {: >9}" +SWAPS_FORMAT = "{} swap{} made" + +# Task 2 format strings +SWAPS_LEFT_FORMAT = "{} swap{} left" +HEALTH_FORMAT = "Health: {:>6}" +LEVEL_FORMAT = "Level {}" + +# Colours for each tile +TILE_COLOURS = { + 'fire': 'red', + 'poison': 'green', + 'water': 'blue', + 'coin': 'gold', + 'psychic': 'purple', + 'ice': 'light sky blue' +} + +# Ratio of probabilities that a tile of this type will be generated. +TILE_PROBABILITIES = { + 'fire': 20, + 'poison': 20, + 'water': 20, + 'coin': 10, + 'psychic': 20, + 'ice': 20 +} + +# Ratio of probabilities that an enemy of this type will be generated. +ENEMY_PROBABILITIES = { + 'fire': 1, + 'water': 1, + 'poison': 1, + 'psychic': 1, + 'ice': 1 +} + +# Player defaults +SWAPS_PER_TURN = 5 +PLAYER_BASE_HEALTH = 500 +PLAYER_BASE_ATTACK = 10 + +# Values used to generate enemies +ENEMY_BASE_HEALTH = 500 +ENEMY_BASE_ATTACK = 100 +ENEMY_ATTACK_RANGE = .8, 1.25 +ENEMY_HEALTH_RANGE = .8, 1.25 + +ENEMY_HEALTH_DELTA = 200 +ENEMY_ATTACK_DELTA = 40 + + +def generate_enemy_stats(level): + """ + Generates health and attack stats for an enemy found on a given level. + + generate_enemy_stats(int) -> (int, (int, int)) + """ + health_factor = (level + 1) // 2 # Boost attack on odd levels + attack_factor = level // 2 # Boost attack on even levels + + health = ENEMY_BASE_HEALTH + health_factor * ENEMY_HEALTH_DELTA + attack = ENEMY_BASE_ATTACK + attack_factor * ENEMY_ATTACK_DELTA + + min_factor, max_factor = ENEMY_ATTACK_RANGE + attack = int(min_factor * attack), int(max_factor * attack) + + health *= random.uniform(*ENEMY_HEALTH_RANGE) + + return int(health), attack + + +class WeightedTable: + """Provides random choice between multiple items, according to their + relative probability weightings.""" + + def __init__(self, items): + """ + Constructor(dict(*: num)) + + Precondition: each value in items is >= 0 + """ + + self._values, self._weights = zip(*items) + cumsum = list(itertools.accumulate(self._weights)) + total = cumsum[-1] + self._p = [i / total for i in cumsum] + + def choose(self): + """ + Returns a random option, based upon the probability weights. + """ + i = bisect.bisect(self._p, random.random()) + return self._values[i] + + def clone(self, removed=None, added=None): + """ + Clones this WeightedTable, and removes/adds certain items. + + WeightedTable.clone(WeightedTable, set, dict) -> WeightedTable + + :param removed: An iterable of items to remove from the cloned table. + Defaults to None. + :param added: A dictionary of {items: weights} of new items to add to + the cloned table. Defaults to None. + """ + + if not removed: + removed = set() + else: + removed = set(removed) + + items = [(value, weight) for value, weight in + zip(self._values, self._weights) if + value not in removed] + + if added: + items += added + + return WeightedTable(items) + + def __repr__(self): + return "WeightedTable({!r})".format( + list(zip(self._values, self._weights))) + + +class Tile: + "Represents a tile in the game." + + def __init__(self, type): + """ + Constructor(str) + :param type: The type of this tile. + """ + self._type = type + self._selected = False + + def get_type(self): + """ + Returns the type of this tile. + + Tile.get_type(Tile) -> str + """ + return self._type + + def set_type(self, type): + """ + Sets the type of this tile. + + Tile.set_type(Tile, str) + """ + self._type = type + + def get_selected(self): + """ + Returns whether this tile is selected. + + Tile.get_selected(Tile) -> bool + """ + return self._selected + + def set_selected(self, selected): + """ + Sets whether this tile is selected. + + Tile.get_selected(Tile) -> bool + """ + self._selected = selected + + def get_colour(self): + """ + Returns the colour of this tile. + Tile.get_colour(Tile) -> str + """ + return TILE_COLOURS[self._type] + + def __eq__(self, other): + "Returns True iff this tile is equal to the other." + return self._type == other._type + + def equivalent_to(self, other): + "Returns True if this tile is equivalent to the other." + return self._type == other._type + + def __repr__(self): + "Returns Pythonic representation of this object." + return "Tile({!r})".format(self._type) + + +class GridManager: + "Manages positions in a grid, with methods for validation and navigation." + + def __init__(self, size=None): + """ + Constructor(GridManager, (int, int)) + Constructor(GridManager) + + :param size: The size of the grid, in (rows, columns). Defaults to None. + """ + + self._size = size + + def set_size(self, size): + """ + Sets the size of the grid to the given (row, column) pair. + + GridManager.set_size(GridManager, (int, int)) -> None + """ + self._size = size + + def is_cell_position_valid(self, position): + """ + Returns True iff the given position is valid for the grid. + + Precondition: size has been set to a (row, column) pair. + + GridManager.is_cell_position_valid(GridManager, (int, int)) -> bool + + :param position: (row, column) position to check. + """ + row, column = position + rows, columns = self._size + + return 0 <= row < rows and 0 <= column < columns + + def move(self, position, direction): + """ + Returns new position after moving in direction from position. + + Precondition: direction in DIRECTION_DELTAS + + :param position: (row, column) position. + :param direction: Direction to move. One of NORTH, SOUTH, EAST, WEST. + """ + + row, column = position + drow, dcolumn = DIRECTION_DELTAS[direction] + + return row + drow, column + dcolumn + + def get_valid_neighbours(self, position, directions=None): + """ + Yields sequence of valid neighbours surrounding position. Excludes + neighbours that are out of bounds. + + :param position: Position to find neighbours of. + :param directions: Sequence of directions to move. + Defaults to DIRECTION_DELTAS. + """ + if not directions: + directions = DIRECTION_DELTAS + + for neighbour in self.get_neighbours(position, directions): + if self.is_cell_position_valid(neighbour): + yield neighbour + + def get_neighbours(self, pos, directions=None): + """ + Yields sequence of neighbours surrounding position. Includes neighbours + without valid positions (i.e. out of bounds). + + :param position: Position to find neighbours of. + :param directions: Sequence of directions to move. + Defaults to DIRECTION_DELTAS. + """ + for direction in directions: + yield self.move(pos, direction) + + def explore(self, position, *directions, include=None): + """ + Yields every neighbour of position in given directions. + + :param position: Position to explore from. + :param directions: Sequence of directions to move. + :param include: Function that accepts a (row, column) pair as an + argument and returns True iff the given position should + be included, else False. If this function returns False, + exploration in the current direction ceases. + """ + for direction in directions: + if not include: + include = lambda position: True + + while True: + position = self.move(position, direction) + if not include(position): + return + yield position + + +class Span: + "A span has distances to four corners (of the compass)." + + def __init__(self, distances): + """ + Constructor(Span, {str: num}) + + :param distances: Dictionary with keys NORTH, SOUTH, EAST, WEST, and values that + are distances from a centre point. + """ + self._distances = distances + + def get_dimensions(self): + """ + Returns the dimensions of the span, in the form: + ((width, height), (horizontal skew), (vertical skew)) + + get_dimensions(Span) -> ((int, int), (int, int)) + """ + v_skew = abs(self._distances[NORTH] - self._distances[SOUTH]) + h_skew = abs(self._distances[EAST] - self._distances[WEST]) + + width = self._distances[NORTH] + self._distances[SOUTH] + height = self._distances[EAST] + self._distances[WEST] + + return (width, height), (h_skew, v_skew) + + def dominates(self, other): + """ + Returns True if span dominates other. + + dominates(Span, Span) -> bool + """ + (width, height), (h_skew, v_skew) = self.get_dimensions() + (o_width, o_height), (o_h_skew, o_v_skew) = other.get_dimensions() + + # Span dominated by other (not necessarily absolutely) + if width < o_width or height < o_height: + return False + + # Equally dominant, but with better skews + if width == o_width and height == o_height: + return h_skew >= o_h_skew and v_skew >= o_v_skew + + # Span absolutely dominates other + return width >= o_width and height >= o_height + + +class Run: + """ + Represents a run of connected cells. + """ + + def __init__(self, cells): + """ + Constructor() + """ + self._cells = cells + self._pm = GridManager() + + self._calculate_dimensions() + + @staticmethod + def from_set(cells_set, tile=None): + """ + Builds a run from a set of cell positions. + + Run.from_set(set) -> Run + Run.from_set(set, tile|None) -> Run + """ + return Run(dict.fromkeys(cells_set, tile)) + + def _calculate_dimensions(self): + """ + Calculates the dimensions of this run, in the form: + (longest horizontal, longest vertical) + """ + width = height = 0 + pm = self._pm + cells = self._cells + include = lambda position: position in cells + + ns_visited = set() + ew_visited = set() + + for cell in self._cells: + ns_visited.add(cell) + ns_run = {cell} + + # North/South + for pos in pm.explore(cell, NORTH, SOUTH, include=include): + ns_visited.add(pos) + ns_run.add(pos) + + height = max(height, len(ns_run)) + + ew_visited.add(cell) + ew_run = {cell} + + # East/West + for pos in pm.explore(cell, EAST, WEST, include=include): + ew_visited.add(pos) + ew_run.add(pos) + + width = max(width, len(ew_run)) + + self._dimensions = width, height + return self._dimensions + + def find_dominant_cell(self): + """ + Returns the dominant cell of this run. + + A dominant cell is the one in the centre of the longest combined + horizontal and vertical runs, with ties being decided by the cell + closest to the centre. + """ + dominant_span = Span({d: 0 for d in DIRECTION_DELTAS}) + dominant_cell = None + pm = self._pm + cells = self._cells + include = lambda position: position in cells + + for cell in self._cells: + distances = {} + # Explore all directions + for direction in DIRECTION_DELTAS: + distance = 0 + for pos in pm.explore(cell, direction, include=include): + distance += 1 + distances[direction] = distance + + span = Span(distances) + + if span.dominates(dominant_span): + dominant_span = span + dominant_cell = cell + + return dominant_cell + + def remove(self, cell): + """ + Removes a cell from this Run. + + Run.remove((int, int)) -> None + """ + self._cells.pop(cell) + + self._calculate_dimensions() + + def get_dimensions(self): + """ + Returns a pair of longest straight paths in this run, of the form: + (horizontal, vertical) + + Run.get_dimensions() -> (int, int) + """ + return self._dimensions + + def get_max_dimension(self): + """ + Returns the size of the longest straight path in this run. + + Run.get_max_dimension() -> int + """ + return max(self._dimensions) + + def __len__(self): + """ + Returns the number of cells in this Run. + """ + return len(self._cells) + + def __getitem__(self, cell): + """ + Returns the value associated with cell. + + Raises KeyError if cell is not in this run. + """ + return self._cells[cell] + + def __setitem__(self, cell, value): + """ + Sets the value associated with cell. + """ + self._cells[cell] = value + + def __delitem__(self, cell): + """ + Removes a cell from this run. + """ + return self.remove(cell) + + def __iter__(self): + """ + Returns an iterator over all cells in this Run. + """ + return iter(self._cells) + + def items(self): + """ + Returns an iterator over all cell, value pairs in this Run. + """ + return self._cells.items() + + def __repr__(self): + return "Run({!r})".format(set(self._cells.keys())) + + +class TileGrid(EventEmitter): + """" + Models a tile grid. + + Emits: + - swap (from_pos, to_pos): When a swap is initiated. + - swap_resolution (from_pos, to_pos): When a swap has been completely + resolved. + - run (run_number, runs): When a group of runs has been detected and is + about to be removed. + """ + + def __init__(self, types, rows=GRID_SIZE, columns=GRID_SIZE): + """ + Constructor(WeightedTable, int, int) + :param types: An iterable (i.e. list) containing + (type, probability of generating) pairs. + :param rows: The number of rows in the grid. + :param columns: The number of columns in this grid. + """ + super().__init__() + + self._cells = [[None for j in range(columns)] for i in range(rows)] + self._types = WeightedTable(types.items()) + + self._size = rows, columns + + self._grid_manager = GridManager(self._size) + + self.generate() + + def generate(self): + """ + Populates the TileGrid with tiles. + + TileGrid.generate(TileGrid) -> None + """ + rows, columns = self._size + + for i in reversed(range(rows)): + for j in range(columns): + self._cells[i][j] = self.generate_cell_at(j) + + # Remove runs + for run in self.find_runs(): + # print("----RUN----") + # print(run) + while run.get_max_dimension() >= MINIMUM_STRAIGHT_RUN: + dominant = run.find_dominant_cell() + dominant_tile = self[dominant] + old_type = dominant_tile.get_type() + + # print( + # "{} is dominant in {}: {}".format(dominant, old_type, run)) + + types = {old_type} + + for position in self._grid_manager.get_valid_neighbours( + dominant): + types.add(self[position].get_type()) + + types = self._types.clone(types) + new_type = types.choose() + # print( + # "Changing from {} to {}, with {}".format(old_type, new_type, + # types)) + self[dominant].set_type(new_type) + + run.remove(dominant) + + def generate_cell_at(self, column): + """ + Returns a randomly generated tile. + + TileGrid.generate_cell_at(TileGrid, int) -> Tile + :param column: The column the cell will be generated at. + """ + return Tile(self._types.choose()) + + def get_grid_manager(self): + """ + Returns the grid manager used by this TileGrid. + + TileGrid.get_grid_manager(TileGrid) -> GridManager + """ + return self._grid_manager + + def get_size(self): + """ + Returns the (row, column) size of this TileGrid. + + TileGrid.get_size(TileGrid) -> (int, int) + """ + return self._size + + def __getitem__(self, position): + """ + Returns the cell at (row, column) position. + + Enables tile_grid[(row, column)] syntax. + """ + row, column = position + return self._cells[row][column] + + def __setitem__(self, position, tile): + """ + Sets the cell at (row, column) position to tile. + + Enables tile_grid[(row, column)] = tile syntax. + """ + row, column = position + self._cells[row][column] = tile + + def __contains__(self, position): + """ + Returns True iff position corresponds to a (row, column) position on this TileGrid and + there exists a tile at that position. + + Enables position in tile_grid syntax + """ + return position >= (0, 0) and self[position] is not None + + def pop(self, position): + """ + Removes and returns the tile at the given (row, column) position from + this TileGrid. + + TileGrid.pop(TileGrid, (int, int)) -> Tile + """ + tile = self[position] + self[position] = None + return tile + + def __iter__(self): + """ + Returns an iterator over all tiles in this TileGrid. + + Enables for cell in tile_grid: pass syntax. + :return: + """ + rows, columns = self._size + for i in range(rows): + for j in range(columns): + yield ((i, j), self._cells[i][j]) + + def generate_refills(self, empty_columns): + """ + Generates cells to refill empty columns. + + :param empty_columns: + :return: 2D list, list at index i holds the refills for column i. Inner + list contains Tile instances according to length of + empty_columns i. Tiles generated earlier are earlier in list. + """ + ref = [ + list(( + [self.generate_cell_at(column) for i in range(removed)])) + for column, removed in enumerate(empty_columns)] + + return ref + + def swap(self, from_pos, to_pos): + """ + Swaps the cells at the given (row, column) positions. + + Yields changes that occur as a result. + """ + tmp = self[from_pos] + self[from_pos] = self[to_pos] + self[to_pos] = tmp + + self.emit('swap', from_pos, to_pos) + + def run_detector(): + # repeatedly detect runs + for runs_num in itertools.count(): + runs = self.find_runs() + + empty = [0 for i in range(self._size[1])] + + results = [] + + deleted_per_col = [[] for i in range(self._size[1])] + + # remove cells in runs + for run in runs: + for cell in run: + row, column = cell + bisect.insort(deleted_per_col[column], row) + tile = self.pop(cell) + empty[column] += 1 + + result = run + results.append(result) + + # print("Need to generate: {}".format(empty)) + + new_per_col = self.generate_refills(empty) + + # drop cells + drops = {} + for column, rows in enumerate(deleted_per_col): + rows = rows[:] + + minimum = -len(rows) + replacements = [] + + new_per_col[column].reverse() + + if minimum < 0: + replacements = [i for i in range(minimum, rows[-1] + 1) + if i not in rows] + # Clean up + for i in replacements: + if i not in rows: + bisect.insort(rows, i) + + replacements.reverse() + rows.reverse() + + for to_row, from_row in zip(rows, replacements): + to_cell = to_row, column + from_cell = from_row, column + + if from_row >= 0: + self[to_cell] = self.pop(from_cell) + else: + self[to_cell] = new_per_col[column][from_row] + + # print("{} moved to {}".format(from_cell, to_cell)) + + deleted_per_col[column] = zip(rows, replacements) + + if not len(results): + break + + self.emit('runs', runs_num, runs) + + yield results, deleted_per_col, new_per_col + + self.emit('swap_resolution', from_pos, to_pos) + + return run_detector() + + # def _move_in_dir(self, position, run, direction, path=None): + # + # if not path: + # path = set() + # + # last_position = position + # while True: + # last_position = self._grid_manager.move(last_position, + # direction) + # + # if last_position not in run: + # break + # + # path.add(last_position) + # + # return path + + def find_runs(self, positions=None, validator=lambda cell: True): + """ + Finds runs in this TileGrid that start at each position in positions. + If position is None, all positions in this grid are considered. + + TileGrid.find_runs(TileGrid[, set((int, int))) -> list(Run) + """ + runs = [] + + rows, columns = self._size + + if not positions: + positions = set(itertools.product(range(rows), range(columns))) + + # Implementation of repeated depth-first search to find potential runs + while len(positions): + v = positions.pop() + root = self[v] + + S = set() + S.add(v) + + run = set() + + while len(S): + v = S.pop() + + if not validator(v): + continue + + if v not in run: + run.add(v) + for w in self._grid_manager.get_valid_neighbours(v): + neighbour = self[w] + if root == neighbour: + S.add(w) + + if len(run) >= MINIMUM_STRAIGHT_RUN: + run = Run({cell: self[cell] for cell in run}) + + for pos in run: + try: + positions.remove(pos) + except KeyError: + pass + + if max(run.get_dimensions()) >= MINIMUM_STRAIGHT_RUN: + runs.append(run) + + return runs + + +class TileGridView(tk.Canvas): + "Visual representation of a TileGrid." + + def __init__(self, master, grid, *args, width=GRID_WIDTH, + height=GRID_HEIGHT, + cell_width=GRID_CELL_WIDTH, cell_height=GRID_CELL_HEIGHT, + **kwargs): + """ + Constructor(tk.Frame, TileGrid, *, int, int, int, int, *) + + :param master: The tkinter master widget/window. + :param width: Total width of the grid. + :param height: Total height of the grid. + :param cell_width: Width of each cell. + :param cell_height: Height of each cell. + """ + + super().__init__(master, width=width, height=height, **kwargs) + + self._master = master + + self._resolving = False + + self._sprites = {} + + self._selected = [] + + self._enabled = True + + self._grid = grid + + self._width = width + self._height = height + + self._cell_width = cell_width + self._cell_height = cell_height + + rows, columns = self._grid.get_size() + + self._x_padding = (self._width - columns * (self._cell_width)) // ( + columns - 1) + self._y_padding = (self._height - rows * (self._cell_height)) // ( + rows - 1) + + self._x_width = cell_width + self._x_padding + self._y_height = cell_height + self._y_padding + + self._calculate_positions() + + self.draw() + + self.bind('', self._click) + self.bind('', self._release) + + def is_resolving(self): + """Returns True iff this TileGridView is currently resolving a swap. + + TileGridView.is_resolving(TildGridView)""" + return self._resolving + + def xy_to_rc(self, xy_pos): + """ + Converts an (x, y) position to (row, column) position on this TileGrid, + else None. + + TileGridView.xy_to_rc(TileGridView, (int, int)) -> (int, int) + TileGridView.xy_to_rc(TileGridView, (int, int)) -> None + """ + x, y = xy_pos + x_rem = x % self._x_width + x_on = x_rem <= self._cell_width + + if not x_on: + return + + y_rem = y % self._y_height + y_on = y_rem <= self._cell_height + + if not y_on: + return + + return y // self._y_height, x // self._x_width + + def rc_to_xy_centre(self, rc_pos): + """ + Converts a (row, column) position on this TileGrid to the (x, y) + position of the cell's centre. + + TileGridView.rc_to_xy_centre(TileGridView, (int, int)) -> None + """ + row, column = rc_pos + + return self._xs[column], self._ys[row] + + def undraw_tile_sprite(self, position): + """Undraws the sprite for the tile at given (row, column) position. + + TileGridView.undraw_tile_sprite(TileGridView, (int, int)) -> None""" + if position in self._sprites: + self.delete(self._sprites[position]) + self._sprites[position] = None + + def draw_tile_sprite(self, xy_pos, tile, selected): + """Draws the sprite for the given tile at given (x, y) position. + + TileGridView.undraw_tile_sprite(TileGridView, (int, int), Tile, bool) + -> None""" + colour = tile.get_colour() + + width, height = self._calculate_tile_size(xy_pos, selected) + x, y = xy_pos + return self.create_rectangle( + x - width, y - height, x + width, y + height, fill=colour) + + def redraw_tile(self, rc_pos, selected=False, tile=None, offset=(0, 0)): + """Redraws the sprite for the tile at given (row, column) position. + + TileGridView.undraw_tile_sprite(TileGridView, (int, int)) -> None""" + if not tile: + tile = self._grid[rc_pos] + + self.undraw_tile_sprite(rc_pos) + + x, y = self.rc_to_xy_centre(rc_pos) + + x += offset[0] + y += offset[1] + + self._sprites[rc_pos] = self.draw_tile_sprite((x, y), tile, selected) + + def _calculate_tile_size(self, pos, selected): + """Calculates and returns the size of a tile depending upon whether + it is selected.""" + if selected: + return (self._cell_width + self._x_padding) // 2, ( + self._cell_height + self._y_padding) // 2 + else: + return self._cell_width // 2, self._cell_height // 2 + + def draw(self): + """Draws every cell in this TileGridView.""" + self.delete(tk.ALL) + + for pos, cell in self._grid: + self.redraw_tile(pos, selected=False, tile=cell) + + def disable(self): + """Disables this TileGridView.""" + self._enabled = False + + def enable(self): + """Enables this TileGridView.""" + self._enabled = True + + def _click(self, ev): + """Handles left mouse click.""" + if self._resolving or not self._enabled: + return + + pos = self.xy_to_rc((ev.x, ev.y)) + + if pos is None: + return + + selected = self._selected == [] or pos not in self._selected + + self.redraw_tile(pos, selected=selected) + + if selected: + self._selected.append(pos) + else: + self._selected.remove(pos) + + def _release(self, ev=None): + """Handles left mouse release.""" + if self._resolving or not self._enabled: + return + + if len(self._selected) == 2: + from_pos, to_pos = self._selected + self.swap(from_pos, to_pos) + self._selected = [] + + ########################################################################### + ######################## BEGIN COMPLICATED METHODS ######################## + ################### (Students needn't understand these) ################### + ########################################################################### + + def _calculate_positions(self): + + half_width = self._cell_width // 2 + half_height = self._cell_height // 2 + + rows, columns = self._grid.get_size() + + self._positions = [[None for j in range(columns)] for i in range(rows)] + + xs = [half_width + i * (self._cell_width + self._x_padding) for i in + range(columns)] + ys = [half_height + j * (self._cell_height + self._y_padding) for j in + range(rows)] + + self._xs = xs + self._ys = ys + + for i in range(rows): + for j in range(columns): + self._positions[i][j] = xs[j], ys[i] + + def _animate_drops_step(self, cell_steps): + while len(cell_steps): + for key in list(cell_steps.keys()): + to_row, column = key + from_row, steps = cell_steps[key] + + to_cell = to_row, column + + steps -= 1 + + # get tile + tile = self._grid[to_cell] + + # redraw with offset + dy = -self._y_height * steps / DROP_STEPS + + self.redraw_tile(to_cell, tile=tile, + offset=(0, dy)) + + if steps == 0: + cell_steps.pop((to_row, column)) + continue + + cell_steps[key] = from_row, steps + + yield + + def _animate_runs_step(self, runs): + for run in runs: + for cell in run: + self.undraw_tile_sprite(cell) + if cell not in self._grid: + continue + + yield + + def _create_animation_stepper(self, steps, delay, callback=None): + def stepper(): + try: + next(steps) + if delay is not None: + self._master.after(delay, stepper) + except StopIteration: + if callback: + callback() + + return stepper + + def swap(self, from_pos, to_pos): + if self._resolving: + return + + self._resolving = True + run_steps = self._grid.swap(from_pos, to_pos) + self.redraw_tile(from_pos, selected=False) + self.redraw_tile(to_pos, selected=False) + + # this is a crap fest + def process_run_string(done): + + data = {} + + fns = { + 'remove_runs': lambda data, + callback: self._create_animation_stepper( + self._animate_runs_step(data['changes']), + RUN_REMOVE_STEP, + callback), + 'drop_tiles': lambda data, + callback: self._create_animation_stepper( + self._animate_drops_step( + data['cell_steps']), DROP_TIME_STEP, callback) + } + + # process runs + for changes, deleted_per_col, new in run_steps: + data['changes'] = changes + processed_runs = self._animate_runs_step(changes) + + def callback(): + cell_steps = {} + for column, row_changes in enumerate(deleted_per_col): + for to_row, from_row in row_changes: + cell_steps[(to_row, column)] = from_row, ( + to_row - from_row) * DROP_STEPS + + if from_row >= 0: + self.undraw_tile_sprite((from_row, column)) + + data['cell_steps'] = cell_steps + + fns['drop_tiles'](data, done)() + + fns['remove_runs'](data, callback)() + + yield + + def done(): + self._resolving = False + + callback = lambda: self._master.after(500, stepper) + steps = process_run_string(callback) + stepper = self._create_animation_stepper(steps, None, + done) + + stepper() + + ########################################################################### + ######################### END COMPLICATED METHODS ######################### + ################### (Students needn't understand these) ################### + ########################################################################### + + +class SimpleGame(EventEmitter): + """" + Models a simple game. + + Emits: + - swap (from_pos, to_pos): + When a swap is initiated. + - swap_resolution (from_pos, to_pos): + When a swap has been completely resolved. + - run (run_number, runs): + When a group of runs has been detected and is about to be removed. + - score (score): + When a score has been earned. + """ + + def __init__(self): + """ + Constructor() + """ + super().__init__() + + self._grid = TileGrid(TILE_PROBABILITIES) + self._grid.on('runs', self._handle_runs) + self._grid.on('swap', self._handle_swap) + self._grid.on('swap_resolution', self._handle_swap_resolution) + + self._resolving = False + + def get_grid(self): + """ + Returns the TileGrid for this SimpleGame. + + SimpleGame.get_grid(SimpleGame) -> TileGrid + """ + return self._grid + + def _handle_swap(self, from_pos, to_pos): + """ + Handles the initiation of a swap (before all runs have been resolved). + + Emits swap. + + SimpleGame._handle_swap(SimpleGame, (int, int), (int, int)) -> None + """ + self.emit('swap', from_pos, to_pos) + + def _handle_swap_resolution(self, from_pos, to_pos): + """ + Handles the resolution of a swap (after all runs have been resolved). + + Emits swap_resolution. + + SimpleGame._handle_swap_resolution(SimpleGame, (int, int), (int, int)) + -> None + """ + self.emit('swap_resolution', from_pos, to_pos) + + def _handle_runs(self, runs_number, runs): + """ + Converts runs into score. + + Emits score, runs. + + SimpleGame._handle_runs(SimpleGame, int, list(Runs)) -> None + """ + score = 0 + for run in runs: + score += (len(run) * run.get_max_dimension()) * 10 + + score *= (runs_number + 1) + + self.emit('score', score) + self.emit('runs', runs) + + def reset(self): + """ + Resets this SimpleGame. + + SimpleGame.reset(SimpleGame) -> None + """ + self._grid.generate() diff --git a/Tiles/a3_tiles.py b/Tiles/a3_tiles.py new file mode 100644 index 0000000..cb58566 --- /dev/null +++ b/Tiles/a3_tiles.py @@ -0,0 +1,234 @@ +#!/usr/bin/env python3 +################################################################################ +# +# CSSE1001/7030 - Assignment 3 +# +# Student Username: s4318841 +# +# Student Name: Alexander Wang +# +################################################################################ + +# VERSION 1.0.1 + +################################################################################ +# +# The following is support code. DO NOT CHANGE. + +from a3_support import * + + +# End of support code +################################################################################ +# Write your code below +################################################################################ + +# Write your classes here (including import statements, etc.) + + +class SimpleTileApp(object): + def __init__(self, master): + """ + Constructor(SimpleTileApp, tk.Frame) + """ + self._master = master + + self._game = SimpleGame() + + self._game.on('swap', self._handle_swap) + self._game.on('score', self._handle_score) + + self._player = SimplePlayer() + + + self._grid_view = TileGridView( + master, self._game.get_grid(), + width=GRID_WIDTH, height=GRID_HEIGHT, bg='black') + self._grid_view.pack(side=tk.TOP, expand=True, fill=tk.BOTH) + + #Creates a status bar + self._status = SimpleStatusBar(master, self._player) + self._status.pack(side=tk.BOTTOM, fill=tk.X) + + # Create reset button + Button1=tk.Button(text = "Reset", command = self._reset).pack(side = tk.BOTTOM) + + def _reset(self): + print("Reset") + + # Add your code here + + def _handle_swap(self, from_pos, to_pos): + """ + Run when a swap on the grid happens. + """ + print("SimplePlayer made a swap from {} to {}!".format( + from_pos, to_pos)) + + def _handle_score(self, score): + """ + Run when a score update happens. + """ + print("SimplePlayer scored {}!".format(score)) + + def add_score(self, score): + self._score=str(score) + return self._score + + def get_score(self): + return self._score + + + def reset_score(self): + pass + + def record_swap(self): + pass + + def get_swaps(self): + pass + + def reset_swaps(self): + pass + + def _handle_swap(self, from_pos, to_pos): + """ + Run when a swap on the grid happens. + """ + print("SimplePlayer made a swap from {} to {}!".format( + from_pos, to_pos)) + + def _handle_score(self, score): + """ + Run when a score update happens. + """ + print("SimplePlayer scored {}!".format(score)) + +class SimplePlayer(object): + def __init__(self): + self._score=0 + self._scorecount=0 + self._swapscount=0 + + def add_score(self, score): + self._score+=score + return self._score + + def get_score(self): + return self._score + + def reset_score(self): + self._score=0 + + def record_swap(self): + self._swapscount+=1 + + def get_swaps(self): + return self._swapscount + + def reset_swaps(self): + self._swapscount=0 + +class SimpleStatusBar(tk.Frame): + def __init__(self, master, player): + + super().__init__(master) + + self._player = player +## self._simpleplayer=SimplePlayer() + swaps_label=tk.Label(master, text= self._player.get_swaps(), bd=1, relief=tk.SUNKEN) + swaps_label.pack(side=tk.LEFT) + score_label=tk.Label(master, text= self._player.get_score(), bd=1, relief=tk.SUNKEN) + score_label.pack(side=tk.LEFT) + + + ## # Reset button# + + def resetStatus(): + player.reset_score() + player.reset_swaps() + reset_button=tk.Button(root, text='Reset Status', command=resetStatus) + reset_button.pack() + +class Character(object): + def __init__(self, max_health): + self._max_health= max_health + self._health=max_health + + def get_max_health(self): + return self._max_health + + def get_health(self): + return self._health + + def lose_health(self, amount): + if self._health-amount>=0: + self._health-=amount + else: + self._health=0 + + def gain_health(self, amount): + if self._health+amount<=self._max_health: + self._health+=amount + else: + self._health=self._max_health + + def reset_health(self): + self._health=self._max_health + +class Enemy(Character): + def __init__(self, type, max_health, attack): + self._type=type + self._attack=attack + + def get_type(self): + return self._type + + def attack(self): + return random.randint(self._attack[0],self._attack[1]) + + +class Player(Character): + def __init__(self, max_health, swaps_per_turn, base_attack): +## super().init(self, max_health) + self._type= type + self.base_attack=base_attack + self._swaps=swaps_per_turn + + def record_swap(self): + if self._swaps-1>=0: + self._swaps-=1 + else: + self._swaps=0 + return self._swaps + + def get_swaps(self): + return self._swaps + + def attack(self, runs, defender_type): + pass + +def task1(): + master=tk.Tk() + SimpleTileApp(master) + master.mainloop() + + + +def task2(): + # Add task 2 GUI instantiation code here + pass + + +def task3(): + # Add task 3 GUI instantiation code here + pass + + +def main(): + # Choose relevant task to run + task1() + + +if __name__ == '__main__': + main() diff --git a/Tiles/a3_tilesadvanced.py b/Tiles/a3_tilesadvanced.py new file mode 100644 index 0000000..74a7574 --- /dev/null +++ b/Tiles/a3_tilesadvanced.py @@ -0,0 +1,740 @@ +#!/usr/bin/env python3 +################################################################################ +# +# CSSE1001/7030 - Assignment 3 +# +# Student Username: s4318841 +# +# +# +################################################################################ + +# VERSION 1.0.1 + +################################################################################ +# +# The following is support code. DO NOT CHANGE. + +from a3_support import * + + +# End of support code +################################################################################ +# Write your code below +################################################################################ + +# Write your classes here (including import statements, etc.) + + +class SimpleTileApp(object): + def __init__(self, master): + """ + Constructor(SimpleTileApp, tk.Frame) + """ + self._master = master + + self._game = SimpleGame() + + self._game.on('swap', self._handle_swap) + self._game.on('score', self._handle_score) + + self._player = SimplePlayer() + + self._grid_view = TileGridView( + master, self._game.get_grid(), + width=GRID_WIDTH, height=GRID_HEIGHT, bg='black') + self._grid_view.pack(side=tk.TOP, expand=True, fill=tk.BOTH) + + #Creates a status bar + self._status = SimpleStatusBar(master, self._player) + self._status.pack(side=tk.BOTTOM) + + # Create reset button + Button1=tk.Button(master,text = "Reset status", command = self._reset).pack(side = tk.BOTTOM) + + #create a file menu + self._filemenu=Filemenu(master, self._player) + + menubar=tk.Menu(self._master) + self._master.config(menu=menubar) + filemenu=tk.Menu(menubar) + + menubar.add_cascade(label="File", menu=filemenu) + filemenu.add_command(label="New Game", command=self.new_game) + filemenu.add_command(label="Exit", command=self.exit_game) + + def new_game(self): + """ Attempts to start a new game, which involves resetting the current swaps and score status, + if grid is being resolved it will raise an error. + """ + + if self._grid_view.is_resolving()==True: + new_game_messagebox=tk.messagebox.showerror('Error:', 'Cannot quit when grid is resolving, please wait ') + + else: + self._game.reset() + self._grid_view.draw() + self._player.reset_score() + self._status.score() + self._player.reset_swaps() + self._status.swaps() + + def exit_game(self): + """ Exits the game + """ + self._master.destroy() + + + def _reset(self): + """ + Resets the player and status bar score and swaps + """ + + self._player.reset_score() + self._status.score() + self._player.reset_swaps() + self._status.swaps() + + def _handle_swap(self, from_pos, to_pos): + """ + Run when a swap on the grid happens. + """ + print("SimplePlayer made a swap from {} to {}!".format( + from_pos, to_pos)) + + + def _handle_score(self, score): + """ + Run when a score update happens. + """ + print("SimplePlayer scored {}!".format(score)) + + def add_score(self, score): + """ + Adds a score + """ + self._score=str(score) + return self._score + + def get_score(self): + """ + Returns the current score + """ + return self._score + + + def reset_score(self): + """ + Resets the players score and score status + """ + self._player.reset_score + self._status.score() + + def record_swap(self): + """ + Records swaps + """ + pass + + def get_swaps(self): + """ + Returns swaps + """ + pass + + def reset_swaps(self): + """ + Resets the players swap count + """ + self._player.reset_swaps + self._status.swaps() + + + def get_health(self): + """ + prints the players health + """ + print("SimplePlayer health: {}".format(get_health())) + + + def _handle_swap(self, from_pos, to_pos): + """ + Run when a swap on the grid happens. + """ + print("SimplePlayer made a swap from {} to {}!".format( + from_pos, to_pos)) + self._player.record_swap() + self._status.swaps() + + def _handle_score(self, score): + """ + Run when a score update happens. + """ + print("SimplePlayer scored {}!".format(score)) + self._player.add_score(10) + self._status.score() + +class SimplePlayer(object): + """ + Creates a SimplePlayer + """ + def __init__(self): + """ + Constructor() + """ + self._score=0 + self._scorecount=0 + self._swapscount=0 + + def add_score(self, score): + """ + Adds a score to the player scorecount + Simpleplayer.add_score(int) -> int + """ + self._score+=score + return self._score + + def get_score(self): + """ + Returns the players scorecount + SimplePlayer.get_score() -> int + """ + return self._score + + def reset_score(self): + """ + Resets the player's scorecount + SimplePlayer.reset_score() -> None + """ + self._score=0 + + def record_swap(self): + """ + Adds 1 to the player's swapcount + SimplePlayer.record_swap() -> None + """ + self._swapscount+=1 + + def get_swaps(self): + """ + Returns the player's swapcount + SimplePlayer.get_swaps() -> Int + """ + return self._swapscount + + def reset_swaps(self): + """ + Resets the player's swapcount + SimplePlayer.reset_swaps()-> Int + """ + self._swapscount=0 + +class SimpleStatusBar(tk.Frame): + """ + Represents the player's status: displays the number of swaps the player has made and player's score. + """ + def __init__(self, master, player): + """ + Constructor: SimpleStatusBar(tk.Frame,player)->GUI + """ + super().__init__(master) + master.title('Simpletile Game') + frame1=tk.Frame(master) + frame2=tk.Frame(master) + + frame1.pack(side=tk.BOTTOM, expand=True, fill=tk.X) + self._player = player + + self._swapstext=tk.Label(frame1, text='Swaps:') + self._swapstext.pack(side=tk.LEFT, padx=20) + self._swaps_label=tk.Label(frame1, text=str(0), bd=1, relief=tk.SUNKEN) + self._swaps_label.pack(side=tk.LEFT) + + self._score_label=tk.Label(frame1,text=str(0), bd=1, relief=tk.SUNKEN) + self._score_label.pack(side=tk.RIGHT, padx=20) + self._scoretext=tk.Label(frame1, text='Score:') + self._scoretext.pack(side=tk.RIGHT) + + + def swaps(self): + """ + Configures the swaps_label to display the player's swap count + SimpleStatusBar.swaps() -> tk.Label + """ + + self._swaps_label.config(text=str(self._player.get_swaps())) + + + def score(self): + """ + Configures the score_label to display the player's score + SimpleStatusBar.score() -> tk.Label + """ + + self._score_label.config(text=str(self._player.get_score())) + +class VersusStatusBar(tk.Frame): + """ + Reprsents the status of the game, player and enemy. It displays the current level, player's health, + enemy's healh and the number of swaps made. + + """ + def __init__(self, master, player): + super().__init__(master) + master.title('Tile Game') + + self._player = player + + + self._width=110 + self._changewidth=self._width-10 + + + self._lbl = tk.Label(master, text="LEVEL 1") + self._lbl.pack(side=tk.TOP, expand='100') + + self._frame1=tk.Frame(master) + self._frame2=tk.Frame(master) + self._frame1.pack(side=tk.TOP, expand=True, fill=tk.X) + self._frame2.pack(side=tk.TOP, expand=True, fill=tk.X) + + + self._health_player=tk.Label(self._frame1, text='Health:') + self._health_player.pack(side=tk.LEFT) + self._health_label=tk.Label(self._frame1, text=str(PLAYER_BASE_HEALTH)) + self._health_label.pack(side=tk.LEFT, padx=50) + + self._health_label2=tk.Label(self._frame1, text=str(PLAYER_BASE_HEALTH)) + self._health_label2.pack(side=tk.RIGHT) + self._health_enemy=tk.Label(self._frame1, text='Health:') + self._health_enemy.pack(side=tk.RIGHT, padx=48) + + + self._canvas1=tk.Canvas(self._frame2, bg='blue', width= self._width, height=15) + self._canvas1.pack(side=tk.LEFT, padx=2, expand=True, anchor=tk.W) + + self._canvas2=tk.Canvas(self._frame2, bg='red', width= self._width, height=15) + self._canvas2.pack(side=tk.LEFT, expand=True, anchor=tk.E) + +## def damage_enemy(self): +#### if self._player.get_swaps(): +## self._versusbar.config(width=self._changewidth) +## +## def damage_player(self): +## self._versusbar.config(width=self._canvaswidth) + +class Filemenu(tk.Frame): + """ + Creates a file menu with the follow items: New Game- which resets the tiles and the SimpleStatusbar, + Exit- which terminates the game + """ + def __init__(self, master, player): + """ + Constructor:Filemenu(tk.Frame, player)-> GUI + """ + super().__init__(master) + self._master=master + + self._frame=tk.Frame(master, bg='yellow') + self._frame.pack(side=tk.BOTTOM) + + #File menu + menubar=tk.Menu(self._master) + self._master.config(menu=menubar) + filemenu=tk.Menu(menubar) + + +class ImageTileGridView(TileGridView): + """ + Visual representation of a TileGrid." + """ + + def __init__(self, master, grid, *args, width=GRID_WIDTH, + height=GRID_HEIGHT, cell_width=GRID_CELL_WIDTH, + cell_height=GRID_CELL_HEIGHT, **kwargs): + """ + Constructor(tk.Frame, TileGrid, *, int, int, int, int, *) + + :param master: The tkinter master widget/window. + :param width: Total width of the grid. + :param height: Total height of the grid. + :param cell_width: Width of each cell. + :param cell_height: Height of each cell. + """ + + + self._tile_images={'red':tk.PhotoImage(file='fire.gif'), 'green':tk.PhotoImage(file='poison.gif'), + 'blue':tk.PhotoImage(file='water.gif'),'gold':tk.PhotoImage(file='coin.gif'), + 'purple':tk.PhotoImage(file='psychic.gif'),'light sky blue':tk.PhotoImage(file='ice.gif')} + + + super().__init__(master, grid, *args, width=width, + height=height, cell_width=cell_width, + cell_height=cell_height, **kwargs) + + def draw_tile_sprite(self, xy_pos, tile, selected): + """Draws the sprite for the given tile at given (x, y) position. + ImageTileGridView.draw_tile_sprite(TileGridView, (int, int), Tile, bool) + -> None""" + + colour = tile.get_colour() + width, height = self._calculate_tile_size(xy_pos, selected) + x, y = xy_pos + + return self.create_image(x,y,image=self._tile_images[colour]) + + +class Character(object): + """ + Represents the basic functionality of a player or enemy within the game. + """ + + def __init__(self, max_health): + """ + Constructor: Character(int) + """ + self._max_health= max_health + self._health=max_health + + def get_max_health(self): + """ + Returns the max health of the character + Character.get_max_health()-> int + """ + return self._max_health + + def get_health(self): + """ + Returns the health of the character + Character.get_health()-> Int + """ + return self._health + + def lose_health(self, amount): + """ + Decreases the health of the character by amount + Character.lose_health(int) -> None + """ + if self._health-amount>=0: + self._health-=amount + else: + self._health=0 + + def gain_health(self, amount): + """ + Increases health of the character by amount + Character.gain_health(int)-> None + """ + if self._health+amount<=self._max_health: + self._health+=amount + else: + self._health=self._max_health + + def reset_health(self): + """ + Resets the health of character + Character.reset_health()-> None + """ + self._health=self._max_health + + + +class Enemy(Character): + """ + Represents the enemy character + """ + + def __init__(self, type, max_health, attack): + """ + Constructor: Enemy(str, int, (int,int)) + """ + self._type=type + self._attack=attack + self._damage=0 + + def get_type(self): + """ + Returns the enemy type + Enemy.get_type() -> str + """ + return self._type + + def attack(self): + """ + Returns a random integer in the enemy's attack range + Enemy.attack() -> int + """ + return random.randint(self._attack[0],self._attack[1]) + + +class Player(Character): + def __init__(self, max_health, swaps_per_turn, base_attack): + """ Constructs the player, max_health is an integer represnting the health of the player, + swaps_per_turn is an integer representing no. of swaps a player can make each turn, + base_attack is the player's base attack + Constructor: Player(int, int, int) + """ + +## super().init(self, max_health) + self._type=[] + self._swapscount=0 + self._base_attack=base_attack + self._swaps=swaps_per_turn + self._attack=[] + self._damage=[] + self._max=0 + self._length=0 + + def record_swap(self): + """ + Adds a swap to swapcount + Player.record_swap() -> None + """ + + if self._swaps-1>=0: + self._swaps-=1 + else: + self._swaps=0 + + def get_swaps(self): + """ + Returns player's swap count + Player.get_swaps()-> int + """ + + return self._swaps + + def attack(self, runs, defender_type): + """Takes a list of Run instances and a defender type, then returns a list of pairs + of the form (tile,damage) + Player.attack(list(runs), str) -> [(str, int), (str, int)] + """ + for i in runs: + self._max+=i.get_max_dimension() + self._length+=i.__len__() + A=i.__getitem__(i.find_dominant_cell()) + self._type.append(A.get_type()) + self._damage= self._base_attack*self._length*self._max + (x,y)=(A.get_type(),self._damage) + self._attack.append((x,y)) + return(self._attack) + + def reset_swaps(self): + """ + Resets the player's swap count + Player.reset_swaps() -> none + """ + self._swapscount=0 + + +class SinglePlayerTileApp(SimpleTileApp): + """ + SingleplayerTileapp is responsible for displaying the top-level GUI, it has the follow features: + ImageTileGRidView, VersusStatusBar, graphics and file menu + """ + + def __init__(self, master): + """ + Constructor: SinglePLayerTileApp(tk.Frame) + """ + self._master = master + self._game = SimpleGame() + + self._game.on('swap', self._handle_swap) + self._game.on('score', self._handle_score) + + self._player = SimplePlayer() + self._char_images={} + self._level=1 + + self._player_swaps_per_turn=SWAPS_PER_TURN + self._player_health=PLAYER_BASE_HEALTH + self._player_base_attack=PLAYER_BASE_ATTACK + + self._enemy_stats=(generate_enemy_stats(self._level)) + self._enemy_health=self._enemy_stats[0] + self._enemy_attackrange=self._enemy_stats[1] + + #Creates a status bar + self._status = SimpleStatusBar(master, self._player) + self._status.pack(side=tk.BOTTOM) + + #Creates a versus bar + self._versusstatus=VersusStatusBar(master, self._player) + self._versusstatus._lbl.config(text='Level: {}'.format(self._level)) + self._versusstatus._health_label.config(text=self._player_health) + self._versusstatus._health_label2.config(text=self._enemy_health) + self._versusstatus.pack(side=tk.TOP) + + + #Creates character images + self._canvas= tk.Canvas(master, width = 100, height = 150) + self._canvas.pack(side=tk.TOP,expand=1, fill=tk.X) + + self._char_images={'player':tk.PhotoImage(file='player.gif'), + 'enemy1':tk.PhotoImage(file='enemy_1.gif'), + 'enemy2':tk.PhotoImage(file='enemy_2.gif'), + 'enemy3':tk.PhotoImage(file='enemy_3.gif'), + 'enemy4':tk.PhotoImage(file='enemy_4.gif'), + 'enemy5':tk.PhotoImage(file='enemy_5.gif')} + + #creates a dictionary to store image + self._canvas.create_image(50,100,image=self._char_images['player']) + self._canvas.create_image(500,100,image=self._char_images['enemy{}'.format(self._level)]) + + self._image_grid_view=ImageTileGridView(master, self._game.get_grid(), + width=GRID_WIDTH, height=GRID_HEIGHT, bg='black') + self._image_grid_view.pack(side=tk.TOP, expand=True, fill=tk.BOTH) + + #create a file menu + self._filemenu=Filemenu(master, self._player) + + menubar=tk.Menu(self._master) + self._master.config(menu=menubar) + filemenu=tk.Menu(menubar) + + menubar.add_cascade(label="File", menu=filemenu) + filemenu.add_command(label="New Game", command=self.new_game) + filemenu.add_command(label="Exit", command=self.exit_game) + + def new_game(self): + """ Attempts to create a new game: if image grid is resolving an error will be raised, + otherwise, a confirmation message box will be raised: if Yes is selected the game will reset, + else nothing will happen + SinglePlayerTileApp() + + """ + + if self._image_grid_view.is_resolving()==True: + new_game_messagebox=tk.messagebox.showerror('Error:', 'Cannot quit when grid is resolving, please wait.. ') + + else: + confirmation_messagebox=tk.messagebox.askyesno('New Game', 'Are you sure you want to start a new game?') + if confirmation_messagebox: + self._game.reset() + self._image_grid_view.draw() + self._player.reset_score() + self._status.score() + self._player.reset_swaps() + self._status.swaps() + else: + pass + + def exit_game(self): + self._master.destroy() + + + def _reset(self): + """ + Resets the player and status bar score and swaps + """ + self._player.reset_score() + self._status.score() + self._player.reset_swaps() + self._status.swaps() + + + def _handle_swap(self, from_pos, to_pos): + """ + Run when a swap on the grid happens. + """ + print("SimplePlayer made a swap from {} to {}!".format( + from_pos, to_pos)) + + + def _handle_score(self, score): + """ + Run when a score update happens. + + """ + self._player.add_score(10) + self._status.score() + A=self._enemy_health-self._player_base_attack + + if A>0: + self._enemy_health-=self._player_base_attack + self._versusstatus._health_label2.config(text=self._enemy_health) + + else: + self._level+=1 + self._enemy_health=0 + self._versusstatus._health_label2.config(text=self._enemy_health) + self._image_grid_view.draw() + + self._canvas.create_image(500,100,image=self._char_images['enemy{}'.format(self._level)]) + self._player.reset_score() + self._status.score() + self._player.reset_swaps() + self._status.swaps() + self._enemy_health=self._enemy_stats[0] + + print("SimplePlayer scored {}!".format(score)) + + def add_score(self, score): + """ + Adds a score + """ + self._score=str(score) + return self._score + + def get_score(self): + """ + Returns the current score + """ + return self._score + + + def reset_score(self): + """ + Resets the players score and score status + """ + self._player.reset_score + self._status.score() + + def record_swap(self): + pass + + def get_swaps(self): + pass + + def reset_swaps(self): + self._player.reset_swaps + self._status.swaps() + + + def get_health(self): + print("SimplePlayer health: {}".format(get_health())) + + def _handle_swap(self, from_pos, to_pos): + """ + Run when a swap on the grid happens. + """ + print("SimplePlayer made a swap from {} to {}!".format( + from_pos, to_pos)) + self._player.record_swap() + self._status.swaps() + + +############################################################################################################### +def task1(): + root=tk.Tk() + SimpleTileApp(root) + root.mainloop() + + +def task2(): + root=tk.Tk() + SinglePlayerTileApp(root) + root.mainloop() + + +def task3(): + # Add task 3 GUI instantiation code here + pass + +def main(): + # Choose relevant task to run + task2() + +if __name__ == '__main__': + main() diff --git a/Tiles/coin.gif b/Tiles/coin.gif new file mode 100644 index 0000000..a1c1cc0 Binary files /dev/null and b/Tiles/coin.gif differ diff --git a/Tiles/ee.py b/Tiles/ee.py new file mode 100644 index 0000000..97bdc6e --- /dev/null +++ b/Tiles/ee.py @@ -0,0 +1,317 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + + +""" +pymitter +Python port of the extended Node.js EventEmitter 2 approach providing +namespaces, wildcards and TTL. +""" + + +__author__ = "Marcel Rieger" +__copyright__ = "Copyright 2014, Marcel Rieger" +__credits__ = ["Marcel Rieger"] +__license__ = "MIT" +__maintainer__ = "Marcel Rieger" +__status__ = "Development" +__version__ = "0.2.3" +__all__ = ["EventEmitter"] + + +# python imports +from time import time + + +class EventEmitter(object): + + __CBKEY = "__callbacks" + __WCCHAR = "*" + + def __init__(self, **kwargs): + """ EventEmitter(wildcard=False, delimiter=".", new_listener=False, + max_listeners=-1) + The EventEmitter class. + Please always use *kwargs* in the constructor. + - *wildcard*: When *True*, wildcards are used. + - *delimiter*: The delimiter to seperate event namespaces. + - *new_listener*: When *True*, the "new_listener" event is emitted every + time a new listener is registered with arguments *(func, event=None)*. + - *max_listeners*: Maximum number of listeners per event. Negativ values + mean infinity. + """ + super(EventEmitter, self).__init__() + + self.wildcard = kwargs.get("wildcard", False) + self.__delimiter = kwargs.get("delimiter", ".") + self.new_listener = kwargs.get("new_listener", False) + self.max_listeners = kwargs.get("max_listeners", -1) + + self.__tree = self.__new_branch() + + @property + def delimiter(self): + """ + *delimiter* getter. + """ + return self.__delimiter + + @classmethod + def __new_branch(cls): + """ + Returns a new branch. Basically, a branch is just a dictionary with + a special item *__CBKEY* that holds registered functions. All other + items are used to build a tree structure. + """ + return { cls.__CBKEY: [] } + + def __find_branch(self, event): + """ + Returns a branch of the tree stucture that matches *event*. Wildcards + are not applied. + """ + parts = event.split(self.delimiter) + + if self.__CBKEY in parts: + return None + + branch = self.__tree + for p in parts: + if p not in branch: + return None + branch = branch[p] + + return branch + + @classmethod + def __remove_listener(cls, branch, func): + """ + Removes a listener given by its function from a branch. + """ + listeners = branch[cls.__CBKEY] + + indexes = [i for i, l in enumerate(listeners) if l.func == func] + indexes.reverse() + + for i in indexes: + listeners.pop(i) + + def on(self, event, func=None, ttl=-1): + """ + Registers a function to an event. When *func* is *None*, decorator + usage is assumed. *ttl* defines the times to listen. Negative values + mean infinity. Returns the function. + """ + def _on(func): + if not hasattr(func, "__call__"): + return func + + parts = event.split(self.delimiter) + + if self.__CBKEY in parts: + return func + + branch = self.__tree + for p in parts: + branch = branch.setdefault(p, self.__new_branch()) + + listeners = branch[self.__CBKEY] + + if 0 <= self.max_listeners <= len(listeners): + return func + + listener = Listener(func, event, ttl) + listeners.append(listener) + + if self.new_listener: + self.emit("new_listener", func, event) + + return func + + if func is not None: + return _on(func) + else: + return _on + + def once(self, *args, **kwargs): + """ + Registers a function to an event with *ttl = 1*. See *on*. Returns the + function. + """ + if len(args) == 3: + args[2] = 1 + else: + kwargs["ttl"] = 1 + return self.on(*args, **kwargs) + + def on_any(self, func=None): + """ + Registers a function that is called every time an event is emitted. + When *func* is *None*, decorator usage is assumed. Returns the function. + """ + def _on_any(func): + if not hasattr(func, "__call__"): + return func + + listeners = self.__tree[self.__CBKEY] + + if 0 <= self.max_listeners <= len(listeners): + return func + + listener = Listener(func, None, -1) + listeners.append(listener) + + if self.new_listener: + self.emit("new_listener", func) + + return func + + if func is not None: + return _on_any(func) + else: + return _on_any + + def off(self, event, func=None): + """ + Removes a function that is registered to an event. When *func* is + *None*, decorator usage is assumed. Returns the function. + """ + def _off(func): + branch = self.__find_branch(event) + if branch is None: + return func + + self.__remove_listener(branch, func) + + return func + + if func is not None: + return _off(func) + else: + return _off + + def off_any(self, func=None): + """ + Removes a function that was registered via *on_any*. When *func* is + *None*, decorator usage is assumed. Returns the function. + """ + def _off_any(func): + self.__remove_listener(self.__tree, func) + + return func + + if func is not None: + return _off_any(func) + else: + return _off_any + + def off_all(self): + """ + Removes all registerd functions. + """ + del self.__tree + self.__tree = self.__new_branch() + + def listeners(self, event): + """ + Returns all functions that are registered to an event. Wildcards are not + applied. + """ + branch = self.__find_branch(event) + if branch is None: + return [] + + return [l.func for l in branch[self.__CBKEY]] + + def listeners_any(self): + """ + Returns all functions that were registered using *on_any*. + """ + return [l.func for l in self.__tree[self.__CBKEY]] + + def listeners_all(self): + """ + Returns all registered functions. + """ + listeners = self.__tree[self.__CBKEY][:] + + branches = self.__tree.values() + for b in branches: + if not isinstance(b, dict): + continue + + branches.extend(b.values()) + + listeners.extend(b[self.__CBKEY]) + + return [l.func for l in listeners] + + def emit(self, event, *args, **kwargs): + """ + Emits an event. All functions of events that match *event* are invoked + with *args* and *kwargs* in the exact order of their registration. + Wildcards might be applied. + """ + parts = event.split(self.delimiter) + + if self.__CBKEY in parts: + return + + listeners = self.__tree[self.__CBKEY][:] + + branches = [self.__tree] + + for p in parts: + _branches = [] + for branch in branches: + for k, b in branch.items(): + if k == self.__CBKEY: + continue + if k == p: + _branches.append(b) + elif self.wildcard: + if p == self.__WCCHAR or k == self.__WCCHAR: + _branches.append(b) + branches = _branches + + for b in branches: + listeners.extend(b[self.__CBKEY]) + + listeners.sort(key=lambda l: l.time) + + remove = [l for l in listeners if not l(*args, **kwargs)] + + for l in remove: + self.off(l.event, func=l.func) + + +class Listener(object): + + def __init__(self, func, event, ttl): + """ + The Listener class. + Listener instances are simple structs to handle functions and their ttl + values. + """ + super(Listener, self).__init__() + + self.func = func + self.event = event + self.ttl = ttl + + self.time = time() + + def __call__(self, *args, **kwargs): + """ + Invokes the wrapped function. If the ttl value is non-negative, it is + decremented by 1. In this case, returns *False* if the ttl value + approached 0. Returns *True* otherwise. + """ + self.func(*args, **kwargs) + + if self.ttl > 0: + self.ttl -= 1 + if self.ttl == 0: + return False + + return True \ No newline at end of file diff --git a/Tiles/enemy_1.gif b/Tiles/enemy_1.gif new file mode 100644 index 0000000..156c32d Binary files /dev/null and b/Tiles/enemy_1.gif differ diff --git a/Tiles/enemy_1.zip b/Tiles/enemy_1.zip new file mode 100644 index 0000000..e3e7e5f Binary files /dev/null and b/Tiles/enemy_1.zip differ diff --git a/Tiles/enemy_2.gif b/Tiles/enemy_2.gif new file mode 100644 index 0000000..8e197c0 Binary files /dev/null and b/Tiles/enemy_2.gif differ diff --git a/Tiles/enemy_3.gif b/Tiles/enemy_3.gif new file mode 100644 index 0000000..ffe4dae Binary files /dev/null and b/Tiles/enemy_3.gif differ diff --git a/Tiles/enemy_4.gif b/Tiles/enemy_4.gif new file mode 100644 index 0000000..1284578 Binary files /dev/null and b/Tiles/enemy_4.gif differ diff --git a/Tiles/enemy_5.gif b/Tiles/enemy_5.gif new file mode 100644 index 0000000..e39036b Binary files /dev/null and b/Tiles/enemy_5.gif differ diff --git a/Tiles/fire.gif b/Tiles/fire.gif new file mode 100644 index 0000000..d2fedc2 Binary files /dev/null and b/Tiles/fire.gif differ diff --git a/Tiles/ice.gif b/Tiles/ice.gif new file mode 100644 index 0000000..eaaf2fd Binary files /dev/null and b/Tiles/ice.gif differ diff --git a/Tiles/player.gif b/Tiles/player.gif new file mode 100644 index 0000000..ab58244 Binary files /dev/null and b/Tiles/player.gif differ diff --git a/Tiles/poison.gif b/Tiles/poison.gif new file mode 100644 index 0000000..86cacff Binary files /dev/null and b/Tiles/poison.gif differ diff --git a/Tiles/psychic.gif b/Tiles/psychic.gif new file mode 100644 index 0000000..58c0c0e Binary files /dev/null and b/Tiles/psychic.gif differ diff --git a/Tiles/water.gif b/Tiles/water.gif new file mode 100644 index 0000000..f2a70a3 Binary files /dev/null and b/Tiles/water.gif differ