diff --git a/tictoc.py b/tictoc.py new file mode 100644 index 0000000000000000000000000000000000000000..3881342b84f812e8c6007d410fefd0432715804d --- /dev/null +++ b/tictoc.py @@ -0,0 +1,69 @@ +import time + + +class TicToc: + """ + Class to make the checking of how long times different parts of the program takes. Simply + write tic() where you want to start the timer and toc() when you want to stop timer. Can handle + multiple timers simultaneously by sending a key to tic and toc. Reset everything with reset() + and a specific key by providing thus key + """ + def __init__(self): + self.logs = None + self._time_start = {"main": 0.0} + self.reset() + + def __repr__(self): + """ + How the class should be printed, will print like a normal dictionary + :return: Normal representation of a dictionary + """ + return self.logs.__repr__() + + def __getitem__(self, item): + """ + Getter to retrieve a single timer + :param item: name of the log you want get + :return: Timer of the provided name + """ + return self.logs.get(item) + + def __iter__(self): + """ + Iterator for the times + :return: iterator for the times that are logged + """ + return self.logs.items().__iter__() + + def tic(self, key="main"): + """ + Start the timer for provided key + :param key: Name of the timer, used to save start time in a dict, defaults to "main" + :return: None + """ + if key not in self.logs: + self.logs[key] = 0.0 + self._time_start[key] = time.time() + + def toc(self, key="main"): + """ + Saves the time for provided key. Can be used multiple times from the same tic() then it will do addition + :param key: Name of the timer defaults to "main" + :return: None + """ + try: + self.logs[key] += time.time() - self._time_start[key] + + except KeyError: + "The provided key has not been initialized, need to run tic() before running toc()" + + def reset(self, key=None): + """ + Reset the log for the given key if the key exists. Leave key empty to reset all logs + :param key: what key you want to reset, defaults to None which will reset everything + :return: None + """ + if key is None: + self.logs = {"main": 0.0} + elif key in self.logs: + self.logs[key] = 0.0 diff --git a/visualdebugger/README.md b/visualdebugger/README.md new file mode 100644 index 0000000000000000000000000000000000000000..294b42ceb0acd231d13fb887f3ef461d7bbc7b41 --- /dev/null +++ b/visualdebugger/README.md @@ -0,0 +1,119 @@ +# Visual Debugging +These files provide some useful and hopefully easy to use tools to visualize what data you currently have. + +The debuggers are using pygame to open a new window and draw shapes onto it. Feel free to modify the debugger in +order to suit your needs. + +Also Note that since pygame only can have one window open at a time you are NOT able to run two different +types of debuggers simultaneous. + +## Getting started +In the example below you can see how you get started with the visual debugger. In this example a debugger for a heatmap is set as the debugger. + +```python +from library import * +import numpy as np +import visualdebugger.heat_map_debugger +import visualdebugger.flow_debugger +import visualdebugger.path_debugger + + +class MyAgent(IDABot): + def __init__(self): + IDABot.__init__(self) + + self.debugging = True + self.debugger = None + if self.debugging: + self.set_up_debugging() + + def set_up_debugging(self): + self.debugger = visualdebugger.heat_map_debugger.HeatMapDebugger() + self.debugger.set_color_map({(20, 20): (255, 255, 0)}) + self.debugger.set_update_frequency(0.5) + + def on_game_start(self): + IDABot.on_game_start(self) + self.debugger.on_start() + + def on_step(self): + IDABot.on_step(self) + if self.debugging: + self.debugger.on_step(self.calculate_and_set_heatmap) # OBS function that is sent! NOT function call! + + def calculate_and_set_heatmap(self): + my_units = self.get_my_units() + heat = np.zeros((self.my_bot.map_tools.height, self.my_bot.map_tools.width)) + for unit in my_units: + heat[int(unit.position.y)][int(unit.position.x)] = 20 + self.debugger.set_display_values(heat) # OBS important to set the values + +``` + +1. You are probably best off having a boolean variable to make the switch between having the debugger on or off easy. + +2. Initialize the type of visual debugger you want to use by calling the constructor for that class. In the example +above this is done in the first line of “set_up_debugging()” and this function. + +3. The debugger must set up a window in order to have somewhere to draw onto. This is done in its on_start(). A good +place to call on this function is in on_game_start() just like in the example above. + +4. In order to update what the debugger shows you call its on_step function. This function will only do things as +often as the update frequency is set to (in the example above it is set to 0.5 Hz meaning once every other second). +So you can call this in the agent on_step() just like in the example above. This on step function can also take another +function and call on that function at that same frequency. It can be a good idea to have a function that calls the +.set_display_values() and have that being called this way. + +Note: The .set_display_values() takes different arguments based on what type of debugger you are using and if the +values ain’t correctly formatted a print will occur and say so. Please continue reading this in order to see how to +format the values or take a look directly in the source code. + + +## Structure +In this part you can see how to structure the parameters in order to show the values. + +### All VisualDebuggers: +##### on_start(self) +Sets up a pygame window that the on_step will draw onto. + +##### on_step(self, fun=None) +Will keep track of the time that the last update of the window occured and if it should update it will do so. And will +also call on the function provided at the same time. + +##### set_update_frequency(self, new_frequency) +Set how often you want the on_step to do things. The frequency is in Hertz (updates/second) + +##### set_display_values(self, *args) +Set the values that should be displayed. The different types of debuggers have a bit different criterias for their +arguments so please check below or check the source code to get a better understanding. + +##### set_color_map(self, color_map) +The Heatmap and Path debuggers make it easy to have different colors being displayed based on value. Use this function +to do so by inputting a color_map. The color_map should be a dictionary with tuples of length 2 as keys and length 3 +as values. The key is the interval of which the color should represent. The interval will include the first value and +exclude the last value. And the three long tuples are (r, g, b) values. Note that if the colormap is not correctly +formatted this call won't do anything and print that there was something wrong. Intervals are not allowed to overlap! + +### FlowDebugger +##### .set_display_values(self, map_values) +map_values should be a 2d list or 2d numpy array where every row has the same length. The values have to be integers +or floats between 0 and 2*pi. The values represent the angle in radians and when run, the window will display arrows +pointing in those directions. + +### HeatMapDebugger +##### .set_display_values(self, map_values) +map_values should be a 2d list or 2d numpy array where every row has the same length. The values have to be integers +or floats. You are free to have these values represent whatever you want and based on the color_map you have set, the +color of each square will have that color. + +### PathDebugger +##### .set_display_values(self, relevant_coordinates, map_size=(152, 176)) +This debugger can more or less do the exact same thing as the heatmap debugger but instead of having to provide an +entire 2d array you just need to provide the specific coordinates as the relevant_coordinates parameter. This should +be a dictionary with a tuple of coordinates (x, y) both integer values as key and the value you want that coordinate +to have is the value in the dictionary. You can also provide the size of the map you want as a second tuple. This size +defaults to (152, 176) since that is the size of the normal map used. But keep in mind that all the coordinates must +be in the range of the map_size! + +When using this debugger you can just like with the heatmap set the colormap in order to have different values for +different coordinates display different colors. diff --git a/visualdebugger/flow_debugger.py b/visualdebugger/flow_debugger.py new file mode 100644 index 0000000000000000000000000000000000000000..d6c5ccf2f57e33005c5e7dca54126caf914629fa --- /dev/null +++ b/visualdebugger/flow_debugger.py @@ -0,0 +1,83 @@ +import visualdebugger.visual_debugger +import numpy as np +import pygame +import math + + +class FlowDebugger(visualdebugger.visual_debugger.VisualDebugger): + def __init__(self): + super(FlowDebugger, self).__init__() + self.direction_map = [] + self.arrows = [] + self.arrow_color = (255, 0, 0) + + def on_draw(self): + self.screen.fill(self.background_color) + for arrow in self.arrows: + self.screen.blit(pygame.transform.rotate(arrow[0], math.degrees(arrow[2])), arrow[1]) + + pygame.display.flip() + self.screen.fill(self.background_color) + + def on_step(self, fun=None): + super().on_step(fun) + + if self.should_update: + self.private_on_step() + + def private_on_step(self): + if not self.screen: + self.on_start() + else: + pygame.event.pump() + + self.arrows = [] + # create squares and bind them to a value + for y_pos in range(len(self.direction_map)): + for x_pos in range(len(self.direction_map[y_pos])): + tile_width = int((self.screen.get_width() - + len(self.direction_map[y_pos]) * self.tile_margin) / len(self.direction_map[y_pos])) + tile_height = int((self.screen.get_height() - + len(self.direction_map) * self.tile_margin) / len(self.direction_map)) + + surface = pygame.Surface((tile_width, tile_height)) + + pygame.draw.polygon(surface, self.arrow_color, ((0+tile_width/6, tile_height/3), + (0+tile_width/6, 2*tile_height/3), + (2*tile_width/4, 2*tile_height/3), + (2*tile_width/4, 5*tile_height/6), + (5*tile_width/6, tile_height/2), + (2*tile_width/4, 0+tile_height/6), + (2*tile_width/4, tile_height/3)) + ) + + dest_x = x_pos * (tile_width + self.tile_margin) + self.tile_margin + # y is flipped in order to match sc2 coords + dest_y = (len(self.direction_map) - y_pos - 1) * (tile_height + self.tile_margin) + self.tile_margin + position = (dest_x, dest_y) + self.arrows.append((surface, position, self.direction_map[y_pos][x_pos])) + self.on_draw() + + def set_display_values(self, map_values): + """ + Set what map that should be displayed, it has to be a 2d list + or a 2d numpy array. Where every row in the list has the same length. + The values has to be ints or floats between 0 and 2*pi + :param map_values: [[], []. [], ...] + :return: none, sets the values to display + """ + try: + assert isinstance(map_values, (list, np.ndarray)) and len(map_values) + first_row = map_values[0] + for row in map_values: + assert isinstance(row, (list, np.ndarray)) and len(row) + assert(len(first_row) == len(row)) + for column in row: + assert isinstance(column, (int, float)) + assert 0 <= column <= 2*math.pi + + self.direction_map = map_values + + except AssertionError: + print("The map you set for debugger must be a list or numpy.array. It cant be empty and " + "every row has to have the same length") diff --git a/visualdebugger/heat_map_debugger.py b/visualdebugger/heat_map_debugger.py new file mode 100644 index 0000000000000000000000000000000000000000..0b44532d6eb6d23e479f47e438979b66e040f9e1 --- /dev/null +++ b/visualdebugger/heat_map_debugger.py @@ -0,0 +1,115 @@ +import visualdebugger.visual_debugger +import numpy as np +import pygame + + +class HeatMapDebugger(visualdebugger.visual_debugger.VisualDebugger): + def __init__(self): + super(HeatMapDebugger, self).__init__() + self.map_to_display = [] + self.color_map = {} + self.squares = [] + + def on_draw(self): + self.screen.fill(self.background_color) + for square in self.squares: + pygame.draw.rect(self.screen, self.get_color_of_tile(square[1]), square[0]) + + pygame.display.flip() + self.screen.fill(self.background_color) + + def on_step(self, fun=None): + super().on_step(fun) + + if self.should_update: + self.private_on_step() + + def private_on_step(self): + if not self.screen: + self.on_start() + else: + pygame.event.pump() + + self.squares = [] + # create squares and bind them to a value + for y_pos in range(len(self.map_to_display)): + for x_pos in range(len(self.map_to_display[y_pos])): + tile_val = self.map_to_display[y_pos][x_pos] + + tile_width = int((self.screen.get_width() - len(self.map_to_display[y_pos]) * self.tile_margin) + / len(self.map_to_display[y_pos])) + + tile_height = int((self.screen.get_height() - len(self.map_to_display) * self.tile_margin) + / len(self.map_to_display)) + + rect_pos_x = x_pos * (tile_width + self.tile_margin) + self.tile_margin + rect_pos_y = (len(self.map_to_display) - y_pos - 1) * \ + (tile_height + self.tile_margin) + self.tile_margin + + self.squares.append((pygame.Rect(rect_pos_x, rect_pos_y, tile_width, tile_height), + tile_val)) + + self.on_draw() + + def set_display_values(self, map_values): + """ + Set what map that should be displayed, it has to be a 2d list + or a 2d numpy array. Where every row in the list has the same length + :param map_values: [[], []. [], ...] + :return: + """ + try: + assert isinstance(map_values, (list, np.ndarray)) and len(map_values) + first_row = map_values[0] + for row in map_values: + assert isinstance(row, (list, np.ndarray)) and len(row) + assert(len(first_row) == len(row)) + for number in row: + assert isinstance(number, (int, float)) + self.map_to_display = map_values + + except AssertionError: + print("The map you set for debugger must be a list or numpy.array. It cant be empty and " + "every row has to have the same length and every element must be a int or float") + + def set_color_map(self, color_map): + """ + Setter for the colormap. takes a dict with tuple of interval as key and rgb value it should represent as value. + interval is (include, exclude). + + :param color_map: {interval(include, exclude): color (r, g, b)}. + {(start of interval included, end of interval excluded): (r, g, b)} + :return: None, sets the colormap of the Heatmap + """ + try: + assert isinstance(color_map, dict) and len(color_map) + occupied_intervals = [] + for key in color_map.keys(): + assert isinstance(key, tuple) and isinstance(color_map[key], tuple) and \ + len(key) == 2 and len(color_map[key]) == 3 + + assert key[0] <= key[1] and isinstance(key[0], (int, float)) \ + and isinstance(key[1], (int, float)) + + for interval in occupied_intervals: + if not (key[1] <= interval[0] or key[0] >= interval[1]): + print("interval: " + str(key) + " overlapped with: " + str(interval)) + raise ValueError + + occupied_intervals.append(key) + + for color_val in color_map[key]: + assert 0 <= color_val <= 255 + + self.color_map = color_map + + except AssertionError: + print("The colormap provided was not correctly formatted") + except ValueError: + print("The colormap provided had overlapping intervals") + + def get_color_of_tile(self, tile_value): + for interval in self.color_map.keys(): + if interval[0] <= tile_value < interval[1] or interval[0] == tile_value: + return self.color_map[interval] + return 0, 0, 0 diff --git a/visualdebugger/path_debugger.py b/visualdebugger/path_debugger.py new file mode 100644 index 0000000000000000000000000000000000000000..ab8c5204634bb0eb76beff145692936720ea3155 --- /dev/null +++ b/visualdebugger/path_debugger.py @@ -0,0 +1,122 @@ +import visualdebugger.visual_debugger +import numpy as np +import pygame + + +class PathDebugger(visualdebugger.visual_debugger.VisualDebugger): + def __init__(self): + super(PathDebugger, self).__init__() + self.map_to_display = [] + self.color_map = {} + self.squares = [] + + def on_draw(self): + self.screen.fill(self.background_color) + for square in self.squares: + pygame.draw.rect(self.screen, self.get_color_of_tile(square[1]), square[0]) + + pygame.display.flip() + self.screen.fill(self.background_color) + + def on_step(self, fun=None): + super().on_step(fun) + + if self.should_update: + self.private_on_step() + + def private_on_step(self): + if not self.screen: + self.on_start() # (super) + else: + pygame.event.pump() + + self.squares = [] + # create squares and bind them to a value + for y_pos in range(len(self.map_to_display)): + for x_pos in range(len(self.map_to_display[y_pos])): + tile_val = self.map_to_display[y_pos][x_pos] + tile_width = int((self.screen.get_width() - len(self.map_to_display[y_pos]) * self.tile_margin) + / len(self.map_to_display[y_pos])) + tile_height = int((self.screen.get_height() - len(self.map_to_display) * self.tile_margin) + / len(self.map_to_display)) + rect_pos_x = x_pos * (tile_width + self.tile_margin) + self.tile_margin + rect_pos_y = (len(self.map_to_display) - y_pos - 1) * \ + (tile_height + self.tile_margin) + self.tile_margin + + self.squares.append((pygame.Rect(rect_pos_x, rect_pos_y, tile_width, tile_height), + tile_val)) + self.on_draw() + + def set_display_values(self, relevant_coordinates, map_size=(152, 176)): + """ + Set what map that should be displayed, it has to be a 2d list + or a 2d numpy array. Where every row in the list has the same length + :param relevant_coordinates: {(x, y) : value, (x, y): value, (x, y): value, ...} + :param map_size: (x:int, y:int)=(152, 176) size of map. relevant_coordinates.keys() must be in the range of + these values + :return: + """ + try: + assert isinstance(relevant_coordinates, dict) and len(relevant_coordinates) + + assert (isinstance(map_size, tuple) and len(map_size) == 2) + assert isinstance(map_size[0], int) and isinstance(map_size[1], int) and \ + map_size[0] > 0 and map_size[1] > 0 # check that map size is valid + + for coord in relevant_coordinates.keys(): + assert isinstance(coord, tuple) and len(coord) == 2 and \ + isinstance(coord[0], int) and isinstance(coord[1], int) + assert (0 <= coord[0] < map_size[0] and 0 <= coord[1] < map_size[1]) + assert isinstance(relevant_coordinates[coord], (int, float)) + + # set the map_to_display value of the relevant coords to the value provided. everything else zero. + self.map_to_display = np.zeros((map_size[1], map_size[0])) + for coord in relevant_coordinates.keys(): + self.map_to_display[coord[1]][coord[0]] = relevant_coordinates[coord] + + except AssertionError: + print("The relevant coordinates you set must be a dictionary with a tuple as key (x_pos, y_pos) \n" + "and int or float as value. Coordinates must be in the range of the 0 to map_size-1 and \n" + "the map_size must be a tuple with (width, height) map_size defaults to (152, 176)") + + def set_color_map(self, color_map): + """ + Setter for the colormap. takes a dict with tuple of interval as key and rgb value it should + represent as value. interval is (include, exclude). + + :param color_map: {interval(include, exclude): color (r, g, b)}. + {(start of interval included, end of interval excluded): (r, g, b)} + :return: None, sets the colormap of the Heatmap + """ + try: + assert isinstance(color_map, dict) and len(color_map) + occupied_intervals = [] + for key in color_map.keys(): + assert isinstance(key, tuple) and isinstance(color_map[key], tuple) and \ + len(key) == 2 and len(color_map[key]) == 3 + + assert isinstance(key[0], (int, float)) and isinstance(key[1], (int, float)) and key[0] <= key[1] + + for interval in occupied_intervals: + if not (key[1] <= interval[0] or key[0] >= interval[1]): + print("interval: " + str(key) + " overlapped with: " + str(interval)) + raise ValueError + + occupied_intervals.append(key) + + for color_val in color_map[key]: + assert isinstance(color_val, int) + assert 0 <= color_val <= 255 + + self.color_map = color_map + + except AssertionError: + print("The colormap provided was not correctly formatted") + except ValueError: + print("The colormap provided had overlapping intervals") + + def get_color_of_tile(self, tile_value): + for interval in self.color_map.keys(): + if interval[0] <= tile_value < interval[1] or interval[0] == tile_value: + return self.color_map[interval] + return 0, 0, 0 diff --git a/visualdebugger/visual_debugger.py b/visualdebugger/visual_debugger.py new file mode 100644 index 0000000000000000000000000000000000000000..af93077f593f3fe41b1269c709c5ceef199f8921 --- /dev/null +++ b/visualdebugger/visual_debugger.py @@ -0,0 +1,39 @@ +import time +import pygame + + +class VisualDebugger: + """ + Basic class for a visual debugger, easy to take this and make + a specific type of visual debugger a subclass of this. + Look at the other debuggers if you need guidance. + """ + def __init__(self): + self.default_width = 152*5 + self.default_height = 176*5 + self.tile_margin = 1 + self.background_color = (0, 0, 0) + self.screen = None + self.update_frequency = 0.5 # update frequency in Hz (updates per second) + self.last_update = 0 + self.should_update = True + + def on_start(self): + self.screen = pygame.display.set_mode((self.default_width, self.default_height)) + + def on_step(self, fun=None): + if time.time() - self.last_update > 1/self.update_frequency: # updates if enough time has passed + if callable(fun): + fun() + + self.last_update = time.time() + self.should_update = True + + else: + self.should_update = False + + def set_update_frequency(self, new_frequency): + self.update_frequency = new_frequency + + def set_tile_margin(self, margin): + self.tile_margin = margin