├── .gitignore ├── .python-version ├── LICENSE ├── README.md ├── gupb ├── __init__.py ├── __main__.py ├── controller │ ├── __init__.py │ ├── keyboard.py │ └── random.py ├── default_config.py ├── logger │ ├── __init__.py │ └── core.py ├── model │ ├── __init__.py │ ├── arenas.py │ ├── characters.py │ ├── consumables.py │ ├── coordinates.py │ ├── effects.py │ ├── games.py │ ├── profiling.py │ ├── tiles.py │ └── weapons.py ├── runner.py ├── scripts │ ├── __init__.py │ ├── arena_generator.py │ └── result_parser.py ├── together_config.py └── view │ ├── __init__.py │ └── render.py ├── profiling.md ├── requirements.txt ├── resources ├── arenas │ ├── archipelago.gupb │ ├── dungeon.gupb │ ├── fisher_island.gupb │ ├── island.gupb │ ├── isolated_shrine.gupb │ ├── lone_sanctum.gupb │ ├── mini.gupb │ ├── ordinary_chaos.gupb │ └── wasteland.gupb ├── fonts │ └── whitrabt.ttf └── images │ ├── characters │ ├── champion_blue.png │ ├── champion_brown.png │ ├── champion_green.png │ ├── champion_grey.png │ ├── champion_lime.png │ ├── champion_orange.png │ ├── champion_pink.png │ ├── champion_red.png │ ├── champion_stripped.png │ ├── champion_turquoise.png │ ├── champion_violet.png │ ├── champion_white.png │ └── champion_yellow.png │ ├── consumables │ └── potion.png │ ├── effects │ ├── blood.png │ ├── fire.png │ └── mist.png │ ├── tiles │ ├── forest.png │ ├── land.png │ ├── menhir.png │ ├── sea.png │ └── wall.png │ └── weapons │ ├── amulet.png │ ├── axe.png │ ├── bow.png │ ├── knife.png │ ├── scroll.png │ └── sword.png └── screenshots └── gupb_sample_state.png /.gitignore: -------------------------------------------------------------------------------- 1 | .dmypy.json 2 | .idea/ 3 | *.pyc 4 | venv/ 5 | .run/ 6 | results/ 7 | *.DS_STORE 8 | .vscode/ 9 | env 10 | resources/arenas/generated_*.gupb 11 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.11.4 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Radosław Łazarz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GUPB 2 | 3 | Gra Udająca Prawdziwe Battle-Royale (polish for "Game Pretending to be a True Battle-Royale"). 4 | 5 | A simplified action game used for teaching machine learning courses at AGH University of Science and Technology. 6 | 7 | ![Sample GUPB State](https://github.com/Prpht/GUPB/blob/master/screenshots/gupb_sample_state.png?raw=true) 8 | 9 | ## Requirements 10 | 11 | The projects requires Python 3.11 or higher. 12 | 13 | ## Installation 14 | 15 | When in project root directory install the requirements using the following command. 16 | Using `virtualenv` is recommended, as it will allow to isolate the installed dependencies from main environment. 17 | ``` 18 | pip install -r requirements.txt 19 | ``` 20 | 21 | ## Usage 22 | 23 | To run the game type `python -m gupb` while in root directory. 24 | Additional options are covered in the help excerpt below. 25 | ``` 26 | Usage: python -m gupb [OPTIONS] 27 | 28 | Options: 29 | -c, --config_path PATH The path to run configuration file. 30 | -i, --inquiry Whether to configure the runner interactively on 31 | start. 32 | 33 | -l, --log_directory PATH The path to log storage directory. 34 | --help Show this message and exit. 35 | ``` 36 | When no configuration file provided, `gupb\default_config.py` is used instead. 37 | Options selected as default in interactive mode are based on chosen configuration. 38 | Log are stored in `results` directory by default. 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /gupb/__init__.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | import selectors 4 | 5 | selector = selectors.SelectSelector() 6 | loop = asyncio.SelectorEventLoop(selector) 7 | asyncio.set_event_loop(loop) 8 | 9 | os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = '1' 10 | os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3' 11 | -------------------------------------------------------------------------------- /gupb/__main__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations, unicode_literals 2 | from datetime import datetime 3 | from functools import lru_cache 4 | import glob 5 | import importlib 6 | import importlib.util 7 | import logging 8 | import os 9 | import pathlib 10 | import pkgutil 11 | import sys 12 | from typing import Any, Union 13 | 14 | import click 15 | import questionary 16 | 17 | from gupb import controller 18 | from gupb import runner 19 | 20 | 21 | # noinspection PyUnresolvedReferences 22 | @lru_cache() 23 | def possible_controllers() -> list[controller.Controller]: 24 | controllers = [] 25 | pkg_path = os.path.dirname(controller.__file__) 26 | names = [name for _, name, _ in pkgutil.iter_modules(path=[pkg_path], prefix=f"{controller.__name__}.")] 27 | for name in names: 28 | module = importlib.import_module(name) 29 | controllers.extend(module.POTENTIAL_CONTROLLERS) 30 | return controllers 31 | 32 | 33 | # noinspection PyUnresolvedReferences 34 | def load_initial_config(config_path: str) -> dict[str, Any]: 35 | spec = importlib.util.spec_from_file_location("config_module", config_path) 36 | config_module = importlib.util.module_from_spec(spec) 37 | sys.modules[spec.name] = config_module 38 | spec.loader.exec_module(config_module) 39 | return config_module.CONFIGURATION 40 | 41 | 42 | # noinspection PyUnresolvedReferences 43 | def possible_arenas() -> set[str]: 44 | paths = glob.glob("resources/arenas/*.gupb") 45 | return set(pathlib.Path(path).stem for path in paths) 46 | 47 | 48 | def configuration_inquiry(initial_config: dict[str, Any]) -> dict[str, Any]: 49 | def when_show_sight(current_answers: dict[str, Any]) -> bool: 50 | chosen_controllers.extend([ 51 | { 52 | 'name': possible_controller.name, 53 | 'value': possible_controller, 54 | } 55 | for possible_controller in current_answers['controllers'] 56 | ]) 57 | default_controller = [c for c in chosen_controllers if c['value'] == initial_config['show_sight']] 58 | other_controllers = [c for c in chosen_controllers if c['value'] != initial_config['show_sight']] 59 | chosen_controllers.clear() 60 | chosen_controllers.extend(default_controller) 61 | chosen_controllers.extend(other_controllers) 62 | return current_answers['visualise'] 63 | 64 | def validate_runs_no(runs_no: str) -> Union[bool, str]: 65 | try: 66 | int(runs_no) 67 | return True 68 | except ValueError: 69 | return "The number of games should be a valid integer!" 70 | 71 | chosen_controllers = [ 72 | { 73 | 'name': 'None', 74 | 'value': None, 75 | }, 76 | ] 77 | questions = [ 78 | { 79 | 'type': 'checkbox', 80 | 'name': 'arenas', 81 | 'message': 'Which arenas should be used in this run?', 82 | 'choices': [ 83 | { 84 | 'name': possible_arena, 85 | 'checked': possible_arena in initial_config['arenas'] 86 | } 87 | for possible_arena in possible_arenas() 88 | ], 89 | }, 90 | { 91 | 'type': 'checkbox', 92 | 'name': 'controllers', 93 | 'message': 'Which controllers should participate in this run?', 94 | 'choices': [ 95 | { 96 | 'name': possible_controller.name, 97 | 'value': possible_controller, 98 | 'checked': possible_controller in initial_config['controllers'], 99 | } 100 | for possible_controller in possible_controllers() 101 | ], 102 | }, 103 | { 104 | 'type': 'confirm', 105 | 'name': 'visualise', 106 | 'message': 'Show the live game visualisation?', 107 | 'default': initial_config['visualise'], 108 | }, 109 | { 110 | 'type': 'select', 111 | 'name': 'show_sight', 112 | 'message': 'Which controller should have its sight visualised?', 113 | 'when': when_show_sight, 114 | 'choices': chosen_controllers, 115 | 'filter': lambda result: None if result == 'None' else result, 116 | 'default': initial_config['show_sight'], 117 | }, 118 | { 119 | 'type': 'input', 120 | 'name': 'runs_no', 121 | 'message': 'How many games should be played?', 122 | 'validate': validate_runs_no, 123 | 'filter': int, 124 | 'default': str(initial_config['runs_no']), 125 | }, 126 | { 127 | 'type': 'confirm', 128 | 'name': 'start_balancing', 129 | 'message': 'Repeat positions until every controller tried each at least once?', 130 | 'default': initial_config['start_balancing'], 131 | }, 132 | ] 133 | answers = questionary.prompt(questions) 134 | return answers 135 | 136 | 137 | def configure_logging(log_directory: str) -> None: 138 | logging_dir_path = pathlib.Path(log_directory) 139 | logging_dir_path.mkdir(parents=True, exist_ok=True) 140 | logging_dir_path.chmod(0o777) 141 | time = datetime.now().strftime('%Y_%m_%d_%H_%M_%S') 142 | 143 | verbose_logger = logging.getLogger('verbose') 144 | verbose_logger.propagate = False 145 | verbose_file_path = logging_dir_path / f'gupb__{time}.log' 146 | verbose_file_handler = logging.FileHandler(verbose_file_path.as_posix()) 147 | verbose_formatter = logging.Formatter( 148 | '%(asctime)s | %(levelname)s | %(module)s.%(funcName)s:%(lineno)d | %(message)s' 149 | ) 150 | verbose_file_handler.setFormatter(verbose_formatter) 151 | verbose_logger.addHandler(verbose_file_handler) 152 | verbose_logger.setLevel(logging.DEBUG) 153 | 154 | json_logger = logging.getLogger('json') 155 | json_logger.propagate = False 156 | json_file_path = logging_dir_path / f'gupb__{time}.json' 157 | json_file_handler = logging.FileHandler(json_file_path.as_posix()) 158 | json_formatter = logging.Formatter( 159 | '{"time_stamp": "%(asctime)s",' 160 | ' "severity": "%(levelname)s",' 161 | ' "line": "%(module)s.%(funcName)s:%(lineno)d",' 162 | ' "type": "%(event_type)s",' 163 | ' "value": %(message)s}' 164 | ) 165 | json_file_handler.setFormatter(json_formatter) 166 | json_logger.addHandler(json_file_handler) 167 | json_logger.setLevel(logging.DEBUG) 168 | 169 | 170 | @click.command() 171 | @click.option('-c', '--config_path', default='gupb/default_config.py', 172 | type=click.Path(exists=True), help="The path to run configuration file.") 173 | @click.option('-i', '--inquiry', 174 | is_flag=True, help="Whether to configure the runner interactively on start.") 175 | @click.option('-l', '--log_directory', default='results', 176 | type=click.Path(exists=False), help="The path to log storage directory.") 177 | def main(config_path: str, inquiry: bool, log_directory: str) -> None: 178 | configure_logging(log_directory) 179 | current_config = load_initial_config(config_path) 180 | current_config = configuration_inquiry(current_config) if inquiry else current_config 181 | game_runner = runner.Runner(current_config) 182 | game_runner.run() 183 | game_runner.print_scores() 184 | 185 | 186 | if __name__ == '__main__': 187 | main(prog_name='python -m gupb') 188 | -------------------------------------------------------------------------------- /gupb/controller/__init__.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | from dataclasses import dataclass 3 | from typing import Protocol 4 | 5 | from gupb.logger import core as logger_core 6 | from gupb.model import arenas 7 | from gupb.model import characters 8 | 9 | 10 | class Controller(Protocol): 11 | 12 | @abstractmethod 13 | def decide(self, knowledge: characters.ChampionKnowledge) -> characters.Action: 14 | raise NotImplementedError 15 | 16 | @abstractmethod 17 | def praise(self, score: int) -> None: 18 | raise NotImplementedError 19 | 20 | @abstractmethod 21 | def reset(self, game_no: int, arena_description: arenas.ArenaDescription) -> None: 22 | raise NotImplementedError 23 | 24 | @property 25 | @abstractmethod 26 | def name(self) -> str: 27 | raise NotImplementedError 28 | 29 | @property 30 | @abstractmethod 31 | def preferred_tabard(self) -> characters.Tabard: 32 | raise NotImplementedError 33 | 34 | 35 | @dataclass(frozen=True) 36 | class ControllerExceptionReport(logger_core.LoggingMixin): 37 | controller_name: str 38 | exception: str 39 | -------------------------------------------------------------------------------- /gupb/controller/keyboard.py: -------------------------------------------------------------------------------- 1 | from queue import SimpleQueue 2 | 3 | import pygame 4 | 5 | from gupb import controller 6 | from gupb.model import arenas 7 | from gupb.model import characters 8 | 9 | 10 | # noinspection PyUnusedLocal 11 | # noinspection PyMethodMayBeStatic 12 | class KeyboardController(controller.Controller): 13 | def __init__(self): 14 | self.action_queue: SimpleQueue[characters.Action] = SimpleQueue() 15 | 16 | def __eq__(self, other: object) -> bool: 17 | if isinstance(other, KeyboardController): 18 | return True 19 | return False 20 | 21 | def __hash__(self) -> int: 22 | return 42 23 | 24 | def decide(self, knowledge: characters.ChampionKnowledge) -> characters.Action: 25 | if self.action_queue.empty(): 26 | return characters.Action.DO_NOTHING 27 | else: 28 | return self.action_queue.get() 29 | 30 | def praise(self, score: int) -> None: 31 | pass 32 | 33 | def reset(self, game_no: int, arena_description: arenas.ArenaDescription) -> None: 34 | pass 35 | 36 | def register(self, key): 37 | if key == pygame.K_UP: 38 | self.action_queue.put(characters.Action.STEP_FORWARD) 39 | elif key == pygame.K_DOWN: 40 | self.action_queue.put(characters.Action.STEP_BACKWARD) 41 | elif key == pygame.K_LEFT: 42 | self.action_queue.put(characters.Action.STEP_LEFT) 43 | elif key == pygame.K_RIGHT: 44 | self.action_queue.put(characters.Action.STEP_RIGHT) 45 | elif key == pygame.K_z: 46 | self.action_queue.put(characters.Action.TURN_LEFT) 47 | elif key == pygame.K_x: 48 | self.action_queue.put(characters.Action.TURN_RIGHT) 49 | elif key == pygame.K_SPACE: 50 | self.action_queue.put(characters.Action.ATTACK) 51 | 52 | @property 53 | def name(self) -> str: 54 | return 'KeyboardController' 55 | 56 | @property 57 | def preferred_tabard(self) -> characters.Tabard: 58 | return characters.Tabard.BLUE 59 | 60 | 61 | POTENTIAL_CONTROLLERS = [ 62 | KeyboardController(), 63 | ] 64 | -------------------------------------------------------------------------------- /gupb/controller/random.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from gupb import controller 4 | from gupb.model import arenas 5 | from gupb.model import characters 6 | 7 | POSSIBLE_ACTIONS = [ 8 | characters.Action.TURN_LEFT, 9 | characters.Action.TURN_RIGHT, 10 | characters.Action.STEP_FORWARD, 11 | characters.Action.ATTACK, 12 | ] 13 | 14 | 15 | # noinspection PyUnusedLocal 16 | # noinspection PyMethodMayBeStatic 17 | class RandomController(controller.Controller): 18 | def __init__(self, first_name: str): 19 | self.first_name: str = first_name 20 | 21 | def __eq__(self, other: object) -> bool: 22 | if isinstance(other, RandomController): 23 | return self.first_name == other.first_name 24 | return False 25 | 26 | def __hash__(self) -> int: 27 | return hash(self.first_name) 28 | 29 | def decide(self, knowledge: characters.ChampionKnowledge) -> characters.Action: 30 | return random.choice(POSSIBLE_ACTIONS) 31 | 32 | def praise(self, score: int) -> None: 33 | pass 34 | 35 | def reset(self, game_no: int, arena_description: arenas.ArenaDescription) -> None: 36 | pass 37 | 38 | @property 39 | def name(self) -> str: 40 | return f'RandomController{self.first_name}' 41 | 42 | @property 43 | def preferred_tabard(self) -> characters.Tabard: 44 | return characters.Tabard.WHITE 45 | 46 | 47 | POTENTIAL_CONTROLLERS = [ 48 | RandomController("Alice"), 49 | RandomController("Bob"), 50 | RandomController("Cecilia"), 51 | RandomController("Darius"), 52 | ] 53 | -------------------------------------------------------------------------------- /gupb/default_config.py: -------------------------------------------------------------------------------- 1 | from gupb.controller import keyboard 2 | from gupb.controller import random 3 | 4 | 5 | keyboard_controller = keyboard.KeyboardController() 6 | 7 | CONFIGURATION = { 8 | 'arenas': [ 9 | 'ordinary_chaos' 10 | ], 11 | 'controllers': [ 12 | keyboard_controller, 13 | random.RandomController("Alice"), 14 | random.RandomController("Bob"), 15 | random.RandomController("Cecilia"), 16 | random.RandomController("Darius"), 17 | ], 18 | 'start_balancing': False, 19 | 'visualise': True, 20 | 'show_sight': keyboard_controller, 21 | 'runs_no': 1, 22 | 'profiling_metrics': [], 23 | } 24 | -------------------------------------------------------------------------------- /gupb/logger/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prpht/GUPB/d70903656b53a4682e025fea87048234a686b388/gupb/logger/__init__.py -------------------------------------------------------------------------------- /gupb/logger/core.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | 4 | from dataclasses_json import DataClassJsonMixin 5 | 6 | json_logger = logging.getLogger('json') 7 | 8 | 9 | class LoggingMixin(DataClassJsonMixin): 10 | def log(self, level: int) -> None: 11 | json_logger.log(level=level, msg=json.dumps(self.to_dict()), extra={'event_type': self.__class__.__name__}) 12 | -------------------------------------------------------------------------------- /gupb/model/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prpht/GUPB/d70903656b53a4682e025fea87048234a686b388/gupb/model/__init__.py -------------------------------------------------------------------------------- /gupb/model/arenas.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from dataclasses import dataclass 3 | import logging 4 | import os.path 5 | import random 6 | from enum import Enum, member 7 | from typing import Dict, NamedTuple, Optional 8 | 9 | import bresenham 10 | 11 | from gupb.logger import core as logger_core 12 | from gupb.model import characters 13 | from gupb.model import coordinates 14 | from gupb.model import effects 15 | from gupb.model import tiles 16 | from gupb.model import weapons 17 | 18 | verbose_logger = logging.getLogger('verbose') 19 | 20 | TILE_ENCODING = { 21 | '=': tiles.Sea, 22 | '.': tiles.Land, 23 | '@': tiles.Forest, 24 | '#': tiles.Wall, 25 | } 26 | 27 | WEAPON_ENCODING = { 28 | 'K': weapons.Knife, 29 | 'S': weapons.Sword, 30 | 'A': weapons.Axe, 31 | 'B': weapons.Bow, 32 | 'M': weapons.Amulet, 33 | 'C': weapons.Scroll, 34 | } 35 | 36 | FIXED_MENHIRS = { 37 | 'isolated_shrine': coordinates.Coords(9, 9), 38 | 'lone_sanctum': coordinates.Coords(9, 9), 39 | } 40 | 41 | Terrain = Dict[coordinates.Coords, tiles.Tile] 42 | 43 | 44 | # noinspection PyMethodParameters 45 | class StepDirection(Enum): 46 | @member 47 | def FORWARD(facing: characters.Facing) -> characters.Facing: 48 | return facing 49 | 50 | @member 51 | def BACKWARD(facing: characters.Facing) -> characters.Facing: 52 | return facing.opposite() 53 | 54 | @member 55 | def LEFT(facing: characters.Facing) -> characters.Facing: 56 | return facing.turn_left() 57 | 58 | @member 59 | def RIGHT(facing: characters.Facing) -> characters.Facing: 60 | return facing.turn_right() 61 | 62 | 63 | class ArenaDescription(NamedTuple): 64 | name: str 65 | 66 | 67 | class Arena: 68 | def __init__(self, name: str, terrain: Terrain) -> None: 69 | self.name = name 70 | self.terrain: Terrain = terrain 71 | self.tiles_with_instant_effects: set[tiles.Tile] = set() 72 | self.size: tuple[int, int] = terrain_size(self.terrain) 73 | self.menhir_position: Optional[coordinates.Coords] = None 74 | self.mist_radius = int(self.size[0] * 2 ** 0.5) + 1 75 | self.no_of_champions_alive: int = 0 76 | 77 | @staticmethod 78 | def load(name: str) -> Arena: 79 | terrain = dict() 80 | arena_file_path = os.path.join('resources', 'arenas', f'{name}.gupb') 81 | with open(arena_file_path) as file: 82 | for y, line in enumerate(file.readlines()): 83 | for x, character in enumerate(line): 84 | if character != '\n': 85 | position = coordinates.Coords(x, y) 86 | if character in TILE_ENCODING: 87 | terrain[position] = TILE_ENCODING[character]() 88 | elif character in WEAPON_ENCODING: 89 | terrain[position] = tiles.Land() 90 | terrain[position].loot = WEAPON_ENCODING[character]() 91 | return Arena(name, terrain) 92 | 93 | def description(self) -> ArenaDescription: 94 | return ArenaDescription(self.name) 95 | 96 | def empty_coords(self) -> list[coordinates.Coords]: 97 | return sorted(set(coords for coords, tile in self.terrain.items() if tile.empty)) 98 | 99 | def visible_coords(self, champion: characters.Champion) -> set[coordinates.Coords]: 100 | def estimate_border_point() -> tuple[coordinates.Coords, int]: 101 | if champion.facing == characters.Facing.UP: 102 | return coordinates.Coords(champion.position.x, 0), champion.position[1] 103 | elif champion.facing == characters.Facing.RIGHT: 104 | return coordinates.Coords(self.size[0] - 1, champion.position.y), self.size[0] - champion.position[0] 105 | elif champion.facing == characters.Facing.DOWN: 106 | return coordinates.Coords(champion.position.x, self.size[1] - 1), self.size[1] - champion.position.y 107 | elif champion.facing == characters.Facing.LEFT: 108 | return coordinates.Coords(0, champion.position.y), champion.position[0] 109 | 110 | def champion_left_and_right() -> list[coordinates.Coords]: 111 | if champion.facing == characters.Facing.UP or champion.facing == characters.Facing.DOWN: 112 | return [ 113 | coordinates.Coords(champion.position.x + 1, champion.position.y), 114 | coordinates.Coords(champion.position.x - 1, champion.position.y), 115 | ] 116 | elif champion.facing == characters.Facing.LEFT or champion.facing == characters.Facing.RIGHT: 117 | return [ 118 | coordinates.Coords(champion.position.x, champion.position.y + 1), 119 | coordinates.Coords(champion.position.x, champion.position.y - 1), 120 | ] 121 | 122 | visible = set() 123 | visible.add(champion.position) 124 | prescience = champion.weapon.prescience(champion.position, champion.facing) 125 | if len(prescience) > 0: 126 | for coords in prescience: 127 | if coords in self.terrain: 128 | visible.add(coords) 129 | else: 130 | border, distance = estimate_border_point() 131 | left = champion.facing.turn_left().value 132 | targets = [border + coordinates.Coords(i * left.x, i * left.y) for i in range(-distance, distance + 1)] 133 | for coords in targets: 134 | ray = bresenham.bresenham(champion.position.x, champion.position.y, coords[0], coords[1]) 135 | next(ray) 136 | for ray_coords in ray: 137 | if ray_coords not in self.terrain: 138 | break 139 | visible.add(ray_coords) 140 | if not self.terrain[ray_coords].transparent: 141 | break 142 | visible.update(champion_left_and_right()) 143 | return visible 144 | 145 | def visible_tiles(self, champion: characters.Champion) -> dict[coordinates.Coords, tiles.TileDescription]: 146 | return {coords: self.terrain[coords].description() for coords in self.visible_coords(champion)} 147 | 148 | def step(self, champion: characters.Champion, step_direction: StepDirection) -> None: 149 | new_position = champion.position + step_direction.value(champion.facing).value 150 | if self.terrain[new_position].passable: 151 | self.terrain[champion.position].leave(champion) 152 | champion.position = new_position 153 | self.terrain[champion.position].enter(champion) 154 | verbose_logger.debug(f"Champion {champion.controller.name} entered tile {new_position}.") 155 | ChampionEnteredTileReport(champion.controller.name, new_position).log(logging.DEBUG) 156 | 157 | def stay(self, champion: characters.Champion) -> None: 158 | self.terrain[champion.position].stay() 159 | 160 | def spawn_menhir(self, new_position: Optional[coordinates.Coords] = None) -> None: 161 | if self.menhir_position: 162 | self.terrain[self.menhir_position] = tiles.Land() 163 | new_position = random.sample(self.empty_coords(), 1)[0] if new_position is None else new_position 164 | new_position = FIXED_MENHIRS[self.name] if self.name in FIXED_MENHIRS else new_position 165 | self.menhir_position = new_position 166 | self.terrain[self.menhir_position] = tiles.Menhir() 167 | verbose_logger.debug(f"Menhir spawned at {self.menhir_position}.") 168 | MenhirSpawnedReport(self.menhir_position).log(logging.DEBUG) 169 | 170 | def spawn_champion_at(self, coords: coordinates.Coords) -> characters.Champion: 171 | champion = characters.Champion(coords, self) 172 | self.terrain[coords].character = champion 173 | self.no_of_champions_alive += 1 174 | return champion 175 | 176 | def increase_mist(self) -> None: 177 | self.mist_radius -= 1 if self.mist_radius > 0 else self.mist_radius 178 | if self.mist_radius: 179 | verbose_logger.debug(f"Radius of mist-free space decreased to {self.mist_radius}.") 180 | MistRadiusReducedReport(self.mist_radius).log(logging.DEBUG) 181 | for coords in self.terrain: 182 | distance = int(((coords.x - self.menhir_position.x) ** 2 + 183 | (coords.y - self.menhir_position.y) ** 2) ** 0.5) 184 | if distance == self.mist_radius: 185 | self.register_effect(effects.Mist(), coords) 186 | 187 | def register_effect(self, effect: effects.Effect, coords: coordinates.Coords) -> None: 188 | tile = self.terrain[coords] 189 | tile.effects.add(effect) 190 | if effect.lifetime() == effects.EffectLifetime.INSTANT: 191 | self.tiles_with_instant_effects.add(tile) 192 | 193 | def trigger_instants(self) -> None: 194 | for tile in self.tiles_with_instant_effects: 195 | tile.instant() 196 | self.tiles_with_instant_effects = set() 197 | 198 | 199 | def terrain_size(terrain: Terrain) -> tuple[int, int]: 200 | estimated_x_size, estimated_y_size = max(terrain) 201 | return estimated_x_size + 1, estimated_y_size + 1 202 | 203 | 204 | @dataclass(frozen=True) 205 | class ChampionEnteredTileReport(logger_core.LoggingMixin): 206 | controller_name: str 207 | tile_coords: coordinates.Coords 208 | 209 | 210 | @dataclass(frozen=True) 211 | class MenhirSpawnedReport(logger_core.LoggingMixin): 212 | position: coordinates.Coords 213 | 214 | 215 | @dataclass(frozen=True) 216 | class MistRadiusReducedReport(logger_core.LoggingMixin): 217 | mist_radius: int 218 | -------------------------------------------------------------------------------- /gupb/model/characters.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from dataclasses import dataclass 3 | from enum import Enum 4 | from functools import partial 5 | import logging 6 | import random 7 | from typing import NamedTuple, Optional, Dict 8 | 9 | from gupb import controller 10 | from gupb.logger import core as logger_core 11 | from gupb.model import arenas 12 | from gupb.model import coordinates 13 | from gupb.model import consumables 14 | from gupb.model import tiles 15 | from gupb.model import weapons 16 | 17 | verbose_logger = logging.getLogger('verbose') 18 | 19 | CHAMPION_STARTING_HP: int = 8 20 | PENALISED_IDLE_TIME = 16 21 | IDLE_DAMAGE_PENALTY = 1 22 | 23 | class ChampionKnowledge(NamedTuple): 24 | position: coordinates.Coords 25 | no_of_champions_alive: int 26 | visible_tiles: Dict[coordinates.Coords, tiles.TileDescription] 27 | 28 | 29 | class ChampionDescription(NamedTuple): 30 | controller_name: str 31 | health: int 32 | weapon: weapons.WeaponDescription 33 | facing: Facing 34 | 35 | 36 | class Tabard(Enum): 37 | BLUE = 'Blue' 38 | BROWN = 'Brown' 39 | GREY = 'Grey' 40 | GREEN = 'Green' 41 | LIME = 'Lime' 42 | ORANGE = 'Orange' 43 | PINK = 'Pink' 44 | RED = 'Red' 45 | STRIPPED = 'Stripped' 46 | TURQUOISE = 'Turquoise' 47 | VIOLET = 'Violet' 48 | WHITE = 'White' 49 | YELLOW = 'Yellow' 50 | ALPHA = "AlphaGUPB" 51 | ANCYMON = "Ancymon" 52 | ARAGORN = 'Aragorn' 53 | FROG = 'Frog' 54 | KROMBOPULOS = 'Krombopulos' 55 | MONGOL = 'Mongolek' 56 | PIKACHU = 'Pikachu' 57 | R2D2 = 'R2D2' 58 | 59 | 60 | class Champion: 61 | def __init__(self, starting_position: coordinates.Coords, arena: arenas.Arena) -> None: 62 | self.facing: Facing = Facing.random() 63 | self.weapon: weapons.Weapon = weapons.Knife() 64 | self.health: int = CHAMPION_STARTING_HP 65 | self.position: coordinates.Coords = starting_position 66 | self.arena: arenas.Arena = arena 67 | self.controller: Optional[controller.Controller] = None 68 | self.tabard: Optional[Tabard] = None 69 | self.previous_facing: Facing = self.facing 70 | self.previous_position: coordinates.Coords = self.position 71 | self.time_idle: int = 0 72 | 73 | def assign_controller(self, assigned_controller: controller.Controller) -> None: 74 | self.controller = assigned_controller 75 | self.tabard = self.controller.preferred_tabard 76 | 77 | def description(self) -> ChampionDescription: 78 | return ChampionDescription(self.controller.name, self.health, self.weapon.description(), self.facing) 79 | 80 | def verbose_name(self) -> str: 81 | return self.controller.name if self.controller else "NULL_CONTROLLER" 82 | 83 | def act(self) -> None: 84 | if self.alive: 85 | verbose_logger.debug(f"Champion {self.verbose_name()} starts acting.") 86 | self.store_previous_state() 87 | action = self.pick_action() 88 | verbose_logger.debug(f"Champion {self.verbose_name()} picked action {action}.") 89 | ChampionPickedActionReport(self.verbose_name(), action.name).log(logging.DEBUG) 90 | action(self) 91 | self.arena.stay(self) 92 | self.assess_idle_penalty() 93 | 94 | def store_previous_state(self) -> None: 95 | self.previous_position = self.position 96 | self.previous_facing = self.facing 97 | 98 | def assess_idle_penalty(self) -> None: 99 | if self.position == self.previous_position and self.previous_facing == self.facing: 100 | self.time_idle += 1 101 | else: 102 | self.time_idle = 0 103 | if self.time_idle >= PENALISED_IDLE_TIME: 104 | verbose_logger.debug(f"Champion {self.verbose_name()} penalised for idle time.") 105 | IdlePenaltyReport(self.verbose_name()).log(logging.DEBUG) 106 | self.damage(IDLE_DAMAGE_PENALTY) 107 | 108 | # noinspection PyBroadException 109 | def pick_action(self) -> Action: 110 | if self.controller: 111 | visible_tiles = self.arena.visible_tiles(self) 112 | knowledge = ChampionKnowledge(self.position, self.arena.no_of_champions_alive, visible_tiles) 113 | try: 114 | action = self.controller.decide(knowledge) 115 | if action is None: 116 | verbose_logger.warning(f"Controller {self.verbose_name()} returned a non-action.") 117 | controller.ControllerExceptionReport(self.verbose_name(), "a non-action returned").log(logging.WARN) 118 | return Action.DO_NOTHING 119 | return action 120 | except Exception as e: 121 | verbose_logger.warning(f"Controller {self.verbose_name()} throw an unexpected exception: {repr(e)}. {e.__traceback__}") 122 | controller.ControllerExceptionReport(self.verbose_name(), repr(e)).log(logging.WARN) 123 | return Action.DO_NOTHING 124 | else: 125 | verbose_logger.warning(f"Controller {self.verbose_name()} was non-existent.") 126 | controller.ControllerExceptionReport(self.verbose_name(), "controller non-existent").log(logging.WARN) 127 | return Action.DO_NOTHING 128 | 129 | def turn_left(self) -> None: 130 | self.facing = self.facing.turn_left() 131 | verbose_logger.debug(f"Champion {self.controller.name} is now facing {self.facing}.") 132 | ChampionFacingReport(self.controller.name, self.facing.value).log(logging.DEBUG) 133 | 134 | def turn_right(self) -> None: 135 | self.facing = self.facing.turn_right() 136 | verbose_logger.debug(f"Champion {self.controller.name} is now facing {self.facing}.") 137 | ChampionFacingReport(self.controller.name, self.facing.value).log(logging.DEBUG) 138 | 139 | def step_forward(self) -> None: 140 | self.arena.step(self, arenas.StepDirection.FORWARD) 141 | 142 | def step_backward(self) -> None: 143 | self.arena.step(self, arenas.StepDirection.BACKWARD) 144 | 145 | def step_left(self) -> None: 146 | self.arena.step(self, arenas.StepDirection.LEFT) 147 | 148 | def step_right(self) -> None: 149 | self.arena.step(self, arenas.StepDirection.RIGHT) 150 | 151 | def attack(self) -> None: 152 | self.weapon.cut(self.arena, self.position, self.facing) 153 | verbose_logger.debug(f"Champion {self.controller.name} attacked with its {self.weapon.description().name}.") 154 | ChampionAttackReport(self.controller.name, self.weapon.description().name).log(logging.DEBUG) 155 | 156 | def do_nothing(self) -> None: 157 | pass 158 | 159 | def damage(self, wounds: int) -> None: 160 | self.health -= wounds 161 | self.health = self.health if self.health > 0 else 0 162 | verbose_logger.debug(f"Champion {self.controller.name} took {wounds} wounds, it has now {self.health} hp left.") 163 | ChampionWoundsReport(self.controller.name, wounds, self.health).log(logging.DEBUG) 164 | if not self.alive: 165 | self.die() 166 | 167 | def die(self) -> None: 168 | self.arena.terrain[self.position].character = None 169 | self.arena.terrain[self.position].consumable = consumables.Potion() 170 | self.arena.terrain[self.position].loot = self.weapon if self.weapon.droppable() else None 171 | verbose_logger.debug(f"Champion {self.controller.name} died.") 172 | ChampionDeathReport(self.controller.name).log(logging.DEBUG) 173 | 174 | die_callable = getattr(self.controller, "die", None) 175 | if die_callable and callable(die_callable): 176 | die_callable() 177 | 178 | @property 179 | def alive(self) -> bool: 180 | return self.health > 0 181 | 182 | 183 | class Facing(Enum): 184 | UP = coordinates.Coords(0, -1) 185 | DOWN = coordinates.Coords(0, 1) 186 | LEFT = coordinates.Coords(-1, 0) 187 | RIGHT = coordinates.Coords(1, 0) 188 | 189 | @staticmethod 190 | def random() -> Facing: 191 | return random.choice([Facing.UP, Facing.DOWN, Facing.LEFT, Facing.RIGHT]) 192 | 193 | def turn_left(self) -> Facing: 194 | if self == Facing.UP: 195 | return Facing.LEFT 196 | elif self == Facing.LEFT: 197 | return Facing.DOWN 198 | elif self == Facing.DOWN: 199 | return Facing.RIGHT 200 | elif self == Facing.RIGHT: 201 | return Facing.UP 202 | 203 | def turn_right(self) -> Facing: 204 | if self == Facing.UP: 205 | return Facing.RIGHT 206 | elif self == Facing.RIGHT: 207 | return Facing.DOWN 208 | elif self == Facing.DOWN: 209 | return Facing.LEFT 210 | elif self == Facing.LEFT: 211 | return Facing.UP 212 | 213 | def opposite(self) -> Facing: 214 | if self == Facing.UP: 215 | return Facing.DOWN 216 | elif self == Facing.RIGHT: 217 | return Facing.LEFT 218 | elif self == Facing.DOWN: 219 | return Facing.UP 220 | elif self == Facing.LEFT: 221 | return Facing.RIGHT 222 | 223 | 224 | class Action(Enum): 225 | TURN_LEFT = partial(Champion.turn_left) 226 | TURN_RIGHT = partial(Champion.turn_right) 227 | STEP_FORWARD = partial(Champion.step_forward) 228 | STEP_BACKWARD = partial(Champion.step_backward) 229 | STEP_LEFT = partial(Champion.step_left) 230 | STEP_RIGHT = partial(Champion.step_right) 231 | ATTACK = partial(Champion.attack) 232 | DO_NOTHING = partial(Champion.do_nothing) 233 | 234 | def __call__(self, *args): 235 | self.value(*args) 236 | 237 | 238 | @dataclass(frozen=True) 239 | class ChampionPickedActionReport(logger_core.LoggingMixin): 240 | controller_name: str 241 | action_name: str 242 | 243 | 244 | @dataclass(frozen=True) 245 | class ChampionFacingReport(logger_core.LoggingMixin): 246 | controller_name: str 247 | facing_value: coordinates.Coords 248 | 249 | 250 | @dataclass(frozen=True) 251 | class ChampionAttackReport(logger_core.LoggingMixin): 252 | controller_name: str 253 | weapon_name: str 254 | 255 | 256 | @dataclass(frozen=True) 257 | class ChampionWoundsReport(logger_core.LoggingMixin): 258 | controller_name: str 259 | wounds: int 260 | rest_health: int 261 | 262 | 263 | @dataclass(frozen=True) 264 | class ChampionDeathReport(logger_core.LoggingMixin): 265 | controller_name: str 266 | 267 | 268 | @dataclass(frozen=True) 269 | class IdlePenaltyReport(logger_core.LoggingMixin): 270 | controller_name: str 271 | -------------------------------------------------------------------------------- /gupb/model/consumables.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from abc import ABC, abstractmethod 3 | from typing import NamedTuple 4 | 5 | from gupb.model import characters 6 | 7 | POTION_RESTORED_HP: int = 5 8 | 9 | 10 | class ConsumableDescription(NamedTuple): 11 | name: str 12 | 13 | 14 | class Consumable(ABC): 15 | def description(self) -> ConsumableDescription: 16 | return ConsumableDescription(self.__class__.__name__.lower()) 17 | 18 | @classmethod 19 | @abstractmethod 20 | def apply_to(cls, champion: characters.Champion): 21 | raise NotImplementedError 22 | 23 | 24 | class Potion(Consumable): 25 | @classmethod 26 | def apply_to(cls, champion: characters.Champion): 27 | champion.health += POTION_RESTORED_HP 28 | -------------------------------------------------------------------------------- /gupb/model/coordinates.py: -------------------------------------------------------------------------------- 1 | from typing import NamedTuple 2 | 3 | Coords = NamedTuple('Coords', [('x', int), ('y', int)]) 4 | 5 | 6 | def add_coords(self: Coords, other: Coords) -> Coords: 7 | return Coords(self[0] + other[0], self[1] + other[1]) 8 | 9 | 10 | def sub_coords(self: Coords, other: Coords) -> Coords: 11 | return Coords(self[0] - other[0], self[1] - other[1]) 12 | 13 | 14 | def mul_coords(self: Coords, other) -> Coords: 15 | if isinstance(other, int): 16 | return Coords(self[0] * other, self[1] * other) 17 | elif isinstance(other, float): 18 | return Coords(int(self[0] * other), int(self[1] * other)) 19 | else: 20 | raise NotImplementedError 21 | 22 | 23 | Coords.__add__ = add_coords 24 | Coords.__sub__ = sub_coords 25 | Coords.__mul__ = mul_coords 26 | -------------------------------------------------------------------------------- /gupb/model/effects.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from abc import ABC, abstractmethod 3 | from dataclasses import dataclass 4 | import logging 5 | from enum import auto, StrEnum 6 | import functools 7 | from typing import NamedTuple 8 | 9 | from gupb.logger import core as logger_core 10 | from gupb.model import characters 11 | 12 | verbose_logger = logging.getLogger('verbose') 13 | 14 | CUT_DAMAGE: int = 2 15 | MIST_DAMAGE: int = 1 16 | FIRE_DAMAGE: int = 3 17 | 18 | 19 | class EffectDescription(NamedTuple): 20 | type: str 21 | 22 | 23 | class EffectLifetime(StrEnum): 24 | INSTANT = auto() 25 | ETERNAL = auto() 26 | 27 | 28 | @functools.total_ordering 29 | class Effect(ABC): 30 | order: int = 0 31 | 32 | def description(self) -> EffectDescription: 33 | return EffectDescription(self.__class__.__name__.lower()) 34 | 35 | def __lt__(self, other): 36 | return self.order < other.order 37 | 38 | @staticmethod 39 | @abstractmethod 40 | def instant(champion: characters.Champion) -> None: 41 | raise NotImplementedError 42 | 43 | @staticmethod 44 | @abstractmethod 45 | def stay(champion: characters.Champion) -> None: 46 | raise NotImplementedError 47 | 48 | @staticmethod 49 | @abstractmethod 50 | def lifetime() -> EffectLifetime: 51 | raise NotImplementedError 52 | 53 | 54 | class Mist(Effect): 55 | @staticmethod 56 | def instant(champion: characters.Champion) -> None: 57 | pass 58 | 59 | @staticmethod 60 | def stay(champion: characters.Champion) -> None: 61 | if champion: 62 | verbose_logger.debug(f"Champion {champion.controller.name} was damaged by deadly mist.") 63 | ChampionDamagedByMistReport(champion.controller.name, MIST_DAMAGE).log(logging.DEBUG) 64 | champion.damage(MIST_DAMAGE) 65 | 66 | @staticmethod 67 | def lifetime() -> EffectLifetime: 68 | return EffectLifetime.ETERNAL 69 | 70 | 71 | class WeaponCut(Effect): 72 | def __init__(self, damage: int = CUT_DAMAGE): 73 | self.damage: int = damage 74 | 75 | def instant(self, champion: characters.Champion) -> None: 76 | if champion: 77 | verbose_logger.debug(f"Champion {champion.controller.name} was damaged by weapon cut.") 78 | ChampionDamagedByWeaponCutReport(champion.controller.name, self.damage).log(logging.DEBUG) 79 | champion.damage(self.damage) 80 | 81 | @staticmethod 82 | def stay(champion: characters.Champion) -> None: 83 | pass 84 | 85 | @staticmethod 86 | def lifetime() -> EffectLifetime: 87 | return EffectLifetime.INSTANT 88 | 89 | 90 | class Fire(Effect): 91 | @staticmethod 92 | def burn(champion: characters.Champion) -> None: 93 | if champion: 94 | verbose_logger.debug(f"Champion {champion.controller.name} was damaged by fire.") 95 | ChampionDamagedByFireReport(champion.controller.name, FIRE_DAMAGE).log(logging.DEBUG) 96 | champion.damage(FIRE_DAMAGE) 97 | 98 | @staticmethod 99 | def instant(champion: characters.Champion) -> None: 100 | Fire.burn(champion) 101 | 102 | @staticmethod 103 | def stay(champion: characters.Champion) -> None: 104 | Fire.burn(champion) 105 | 106 | @staticmethod 107 | def lifetime() -> EffectLifetime: 108 | return EffectLifetime.ETERNAL 109 | 110 | 111 | @dataclass(frozen=True) 112 | class ChampionDamagedByMistReport(logger_core.LoggingMixin): 113 | controller_name: str 114 | damage: int 115 | 116 | 117 | @dataclass(frozen=True) 118 | class ChampionDamagedByWeaponCutReport(logger_core.LoggingMixin): 119 | controller_name: str 120 | damage: int 121 | 122 | 123 | @dataclass(frozen=True) 124 | class ChampionDamagedByFireReport(logger_core.LoggingMixin): 125 | controller_name: str 126 | damage: int 127 | 128 | 129 | EFFECTS_ORDER = { 130 | Mist, 131 | WeaponCut, 132 | Fire, 133 | } 134 | for i, effect in enumerate(EFFECTS_ORDER): 135 | effect.order = i 136 | -------------------------------------------------------------------------------- /gupb/model/games.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from dataclasses import dataclass 3 | import logging 4 | import random 5 | from typing import Iterator, NamedTuple, Optional 6 | 7 | # noinspection PyPackageRequirements 8 | import statemachine 9 | 10 | from gupb import controller 11 | from gupb.logger import core as logger_core 12 | from gupb.model import arenas 13 | from gupb.model import characters 14 | from gupb.model import coordinates 15 | 16 | verbose_logger = logging.getLogger('verbose') 17 | 18 | MIST_TTH_PER_CHAMPION: int = 2 19 | 20 | ChampionDeath = NamedTuple('ChampionDeath', [('champion', characters.Champion), ('episode', int)]) 21 | 22 | 23 | class Game(statemachine.StateMachine): 24 | actions_done = statemachine.State('ActionsDone', value=9, initial=True) 25 | instants_triggered = statemachine.State('InstantsTriggered', value=1) 26 | 27 | cycle = actions_done.to(instants_triggered) | instants_triggered.to(actions_done) 28 | 29 | def __init__( 30 | self, 31 | game_no: int, 32 | arena_name: str, 33 | to_spawn: list[controller.Controller], 34 | menhir_position: Optional[coordinates.Coords] = None, 35 | initial_champion_positions: Optional[list[coordinates.Coords]] = None, 36 | ) -> None: 37 | self.game_no: int = game_no 38 | self.arena: arenas.Arena = arenas.Arena.load(arena_name) 39 | self.arena.spawn_menhir(menhir_position) 40 | self._prepare_controllers(to_spawn) 41 | self.initial_champion_positions: Optional[list[coordinates.Coords]] = initial_champion_positions 42 | self.champions: list[characters.Champion] = self._spawn_champions(to_spawn) 43 | self.action_queue: list[characters.Champion] = [] 44 | self.episode: int = 0 45 | self.episodes_since_mist_increase: int = 0 46 | self.deaths: list[ChampionDeath] = [] 47 | self.finished = False 48 | super().__init__() 49 | 50 | def on_enter_actions_done(self) -> None: 51 | if not self.action_queue: 52 | self._environment_action() 53 | else: 54 | self._champion_action() 55 | 56 | def on_enter_instants_triggered(self): 57 | self.arena.trigger_instants() 58 | 59 | def score(self) -> dict[controller.Controller, int]: 60 | if not self.finished: 61 | raise RuntimeError("Attempted to score an unfinished game!") 62 | return {death.champion.controller: score for death, score in zip(self.deaths, self._fibonacci())} 63 | 64 | def _prepare_controllers(self, to_spawn: list[controller.Controller]): 65 | for controller_to_spawn in to_spawn: 66 | controller_to_spawn.reset(self.game_no, self.arena.description()) 67 | 68 | def _spawn_champions( 69 | self, 70 | to_spawn: list[controller.Controller], 71 | ) -> list[characters.Champion]: 72 | champions = [] 73 | if self.initial_champion_positions is None: 74 | self.initial_champion_positions = random.sample(self.arena.empty_coords(), len(to_spawn)) 75 | if len(to_spawn) != len(self.initial_champion_positions): 76 | raise RuntimeError("Unable to spawn champions: not enough positions!") # TODO: remove if works 77 | for controller_to_spawn, coords in zip(to_spawn, self.initial_champion_positions): 78 | champion = self.arena.spawn_champion_at(coords) 79 | champion.assign_controller(controller_to_spawn) 80 | champions.append(champion) 81 | verbose_logger.debug(f"{champion.tabard.value} champion for {controller_to_spawn.name}" 82 | f" spawned at {coords} facing {champion.facing}.") 83 | ChampionSpawnedReport(controller_to_spawn.name, coords, champion.facing.value).log(logging.DEBUG) 84 | return champions 85 | 86 | def _environment_action(self) -> None: 87 | self._clean_dead_champions() 88 | self.action_queue = self.champions.copy() 89 | self.episode += 1 90 | self.episodes_since_mist_increase += 1 91 | verbose_logger.debug(f"Starting episode {self.episode}.") 92 | EpisodeStartReport(self.episode).log(logging.DEBUG) 93 | if self.episodes_since_mist_increase >= MIST_TTH_PER_CHAMPION * len(self.champions): 94 | self.arena.increase_mist() 95 | self.episodes_since_mist_increase = 0 96 | 97 | def _clean_dead_champions(self): 98 | alive = [] 99 | for champion in self.champions: 100 | if champion.alive: 101 | alive.append(champion) 102 | else: 103 | death = ChampionDeath(champion, self.episode) 104 | self.deaths.append(death) 105 | self.arena.no_of_champions_alive -= 1 106 | self.champions = alive 107 | if len(self.champions) == 1: 108 | verbose_logger.debug(f"Champion {self.champions[0].controller.name} was the last one standing.") 109 | LastManStandingReport(self.champions[0].controller.name).log(logging.DEBUG) 110 | champion = self.champions.pop() 111 | death = ChampionDeath(champion, self.episode) 112 | self.deaths.append(death) 113 | 114 | win_callable = getattr(champion.controller, "win", None) 115 | if win_callable and callable(win_callable): 116 | win_callable() 117 | 118 | if not self.champions: 119 | self.finished = True 120 | 121 | def _champion_action(self) -> None: 122 | champion = self.action_queue.pop() 123 | champion.act() 124 | 125 | @staticmethod 126 | def _fibonacci() -> Iterator[int]: 127 | yield 1 128 | yield 2 129 | a = 3 130 | b = 4 131 | while True: 132 | yield int(a) 133 | a, b = b, (a / 2.2) + b 134 | 135 | 136 | @dataclass(frozen=True) 137 | class ChampionSpawnedReport(logger_core.LoggingMixin): 138 | controller_name: str 139 | coords: coordinates.Coords 140 | facing_value: coordinates.Coords 141 | 142 | 143 | @dataclass(frozen=True) 144 | class EpisodeStartReport(logger_core.LoggingMixin): 145 | episode_number: int 146 | 147 | 148 | @dataclass(frozen=True) 149 | class LastManStandingReport(logger_core.LoggingMixin): 150 | controller_name: str 151 | -------------------------------------------------------------------------------- /gupb/model/profiling.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | PROFILE_RESULTS = {} 4 | 5 | 6 | def profile(_func=None, name=None): 7 | """ Profiling decorator. """ 8 | 9 | def decorator(func): 10 | def wrapper(*args, **kw): 11 | start_time = time.time() 12 | 13 | result = func(*args, **kw) 14 | 15 | elapsed_time = time.time() - start_time 16 | key = name if name else func.__qualname__ 17 | PROFILE_RESULTS.setdefault(key, []).append(elapsed_time) 18 | return result 19 | 20 | return wrapper 21 | 22 | return decorator(_func) if _func else decorator 23 | 24 | 25 | def humanize_time(time_diff_secs): 26 | intervals = [('s', 1000), ('m', 60), ('h', 60)] 27 | 28 | unit, number = 'ms', abs(time_diff_secs) * 1000 29 | for new_unit, ratio in intervals: 30 | new_number = float(number) / ratio 31 | if new_number < 2: 32 | break 33 | unit, number = new_unit, new_number 34 | shown_num = number 35 | return '{:.2f} {}'.format(shown_num, unit) 36 | 37 | 38 | # noinspection PyShadowingBuiltins 39 | def print_stats(function_name, all=False, total=True, avg=True): 40 | if function_name not in PROFILE_RESULTS: 41 | print("{!r} wasn't profiled, nothing to display.".format(function_name)) 42 | else: 43 | runtimes = PROFILE_RESULTS[function_name] 44 | total_runtime = sum(runtimes) 45 | average = total_runtime / len(runtimes) 46 | print('Stats for function: {!r}'.format(function_name)) 47 | if all: 48 | print(' run times: {}'.format([humanize_time(run_time) for run_time in runtimes])) 49 | if total: 50 | print(' total run time: {}'.format(humanize_time(total_runtime))) 51 | if avg: 52 | print(' average run time: {}'.format(humanize_time(average))) 53 | -------------------------------------------------------------------------------- /gupb/model/tiles.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from abc import ABC, abstractmethod 3 | from dataclasses import dataclass 4 | import logging 5 | from typing import NamedTuple, Optional, List 6 | 7 | import sortedcontainers 8 | 9 | from gupb.logger import core as logger_core 10 | from gupb.model import effects 11 | from gupb.model import characters 12 | from gupb.model import consumables 13 | from gupb.model import weapons 14 | 15 | verbose_logger = logging.getLogger('verbose') 16 | 17 | 18 | class TileDescription(NamedTuple): 19 | type: str 20 | loot: Optional[weapons.WeaponDescription] 21 | character: Optional[characters.ChampionDescription] 22 | consumable: Optional[consumables.ConsumableDescription] 23 | effects: List[effects.EffectDescription] 24 | 25 | 26 | class Tile(ABC): 27 | def __init__(self): 28 | self.loot: Optional[weapons.Weapon] = None 29 | self.consumable: Optional[consumables.Consumable] = None 30 | self.character: Optional[characters.Champion] = None 31 | self.effects: sortedcontainers.SortedList[effects.Effect] = sortedcontainers.SortedList() 32 | 33 | def description(self) -> TileDescription: 34 | return TileDescription( 35 | self.__class__.__name__.lower(), 36 | self.loot.description() if self.loot else None, 37 | self.character.description() if self.character else None, 38 | self.consumable.description() if self.consumable else None, 39 | [effect.description() for effect in self.effects], 40 | ) 41 | 42 | @property 43 | def passable(self) -> bool: 44 | return self.terrain_passable() and not self.character 45 | 46 | @staticmethod 47 | @abstractmethod 48 | def terrain_passable() -> bool: 49 | raise NotImplementedError 50 | 51 | @property 52 | def transparent(self) -> bool: 53 | return self.terrain_transparent() and not self.character 54 | 55 | @staticmethod 56 | @abstractmethod 57 | def terrain_transparent() -> bool: 58 | raise NotImplementedError 59 | 60 | @property 61 | def empty(self) -> bool: 62 | return self.passable and not self.loot and not self.character 63 | 64 | def enter(self, champion: characters.Champion) -> None: 65 | self.character = champion 66 | if self.loot: 67 | champion.weapon, self.loot = self.loot, champion.weapon if champion.weapon.droppable() else None 68 | verbose_logger.debug( 69 | f"Champion {champion.controller.name} picked up a {champion.weapon.description().name}.") 70 | ChampionPickedWeaponReport(champion.controller.name, champion.weapon.description().name).log(logging.DEBUG) 71 | if self.consumable: 72 | self.consumable.apply_to(champion) 73 | verbose_logger.debug( 74 | f"Champion {champion.controller.name} consumed a {self.consumable.description().name}.") 75 | ChampionConsumableReport(champion.controller.name, self.consumable.description().name).log(logging.DEBUG) 76 | self.consumable = None 77 | 78 | # noinspection PyUnusedLocal 79 | def leave(self, champion: characters.Champion) -> None: 80 | self.character = None 81 | 82 | def stay(self) -> None: 83 | self._activate_effects('stay') 84 | 85 | def instant(self) -> None: 86 | self._activate_effects('instant') 87 | self.effects = sortedcontainers.SortedList( 88 | effect for effect in self.effects if effect.lifetime() != effects.EffectLifetime.INSTANT 89 | ) 90 | 91 | def _activate_effects(self, activation: str) -> None: 92 | if self.character: 93 | if self.effects: 94 | for effect in self.effects: 95 | getattr(effect, activation)(self.character) 96 | 97 | 98 | class Land(Tile): 99 | @staticmethod 100 | def terrain_passable() -> bool: 101 | return True 102 | 103 | @staticmethod 104 | def terrain_transparent() -> bool: 105 | return True 106 | 107 | 108 | class Sea(Tile): 109 | @staticmethod 110 | def terrain_passable() -> bool: 111 | return False 112 | 113 | @staticmethod 114 | def terrain_transparent() -> bool: 115 | return True 116 | 117 | 118 | class Wall(Tile): 119 | @staticmethod 120 | def terrain_passable() -> bool: 121 | return False 122 | 123 | @staticmethod 124 | def terrain_transparent() -> bool: 125 | return False 126 | 127 | 128 | class Forest(Tile): 129 | @staticmethod 130 | def terrain_passable() -> bool: 131 | return True 132 | 133 | @staticmethod 134 | def terrain_transparent() -> bool: 135 | return False 136 | 137 | 138 | class Menhir(Tile): 139 | @staticmethod 140 | def terrain_passable() -> bool: 141 | return True 142 | 143 | @staticmethod 144 | def terrain_transparent() -> bool: 145 | return True 146 | 147 | 148 | @dataclass(frozen=True) 149 | class ChampionPickedWeaponReport(logger_core.LoggingMixin): 150 | controller_name: str 151 | weapon_name: str 152 | 153 | 154 | @dataclass(frozen=True) 155 | class ChampionConsumableReport(logger_core.LoggingMixin): 156 | controller_name: str 157 | consumable_name: str 158 | -------------------------------------------------------------------------------- /gupb/model/weapons.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from abc import ABC, abstractmethod 4 | import math 5 | from typing import NamedTuple, List 6 | 7 | from gupb.model import arenas 8 | from gupb.model import characters 9 | from gupb.model import coordinates 10 | from gupb.model import effects 11 | 12 | 13 | class WeaponDescription(NamedTuple): 14 | name: str 15 | 16 | 17 | class Weapon(ABC): 18 | def description(self) -> WeaponDescription: 19 | return WeaponDescription(self.__class__.__name__.lower()) 20 | 21 | @classmethod 22 | @abstractmethod 23 | def cut_positions( 24 | cls, 25 | terrain: arenas.Terrain, 26 | position: coordinates.Coords, 27 | facing: characters.Facing 28 | ) -> List[coordinates.Coords]: 29 | raise NotImplementedError 30 | 31 | @abstractmethod 32 | def cut(self, arena: arenas.Arena, position: coordinates.Coords, facing: characters.Facing) -> None: 33 | raise NotImplementedError 34 | 35 | @classmethod 36 | def prescience(cls, position: coordinates.Coords, facing: characters.Facing) -> list[coordinates.Coords]: 37 | return [] 38 | 39 | @classmethod 40 | def droppable(cls) -> bool: 41 | return True 42 | 43 | @classmethod 44 | def cut_transparent(cls, arena: arenas.Arena, position: coordinates.Coords) -> None: 45 | if position in arena.terrain and arena.terrain[position].terrain_transparent(): 46 | arena.register_effect(cls.cut_effect(), position) 47 | 48 | @staticmethod 49 | def cut_effect() -> effects.Effect: 50 | return effects.WeaponCut() 51 | 52 | 53 | class LineWeapon(Weapon, ABC): 54 | @staticmethod 55 | @abstractmethod 56 | def reach() -> int: 57 | raise NotImplementedError 58 | 59 | @classmethod 60 | def cut_positions( 61 | cls, 62 | terrain: arenas.Terrain, 63 | position: coordinates.Coords, 64 | facing: characters.Facing 65 | ) -> List[coordinates.Coords]: 66 | cut_positions = [] 67 | cut_position = position 68 | for _ in range(cls.reach()): 69 | cut_position += facing.value 70 | if cut_position not in terrain: 71 | break 72 | cut_positions.append(cut_position) 73 | if not terrain[cut_position].transparent: 74 | break 75 | return cut_positions 76 | 77 | def cut(self, arena: arenas.Arena, position: coordinates.Coords, facing: characters.Facing) -> None: 78 | for cut_position in self.cut_positions(arena.terrain, position, facing): 79 | self.cut_transparent(arena, cut_position) 80 | 81 | 82 | class PropheticWeapon(Weapon, ABC): 83 | @classmethod 84 | def prescience(cls, position: coordinates.Coords, facing: characters.Facing) -> list[coordinates.Coords]: 85 | radius = cls.prescience_radius() 86 | visible_coordinates = [] 87 | for x in range(position.x - radius, position.x + radius + 1): 88 | for y in range(position.y - radius, position.y + radius + 1): 89 | if math.sqrt((x - position.x) ** 2 + (y - position.y) ** 2) <= radius: 90 | visible_coordinates.append(coordinates.Coords(x, y)) 91 | return visible_coordinates 92 | 93 | @staticmethod 94 | @abstractmethod 95 | def prescience_radius() -> int: 96 | raise NotImplementedError 97 | 98 | 99 | class Knife(LineWeapon): 100 | @staticmethod 101 | def reach() -> int: 102 | return 1 103 | 104 | @classmethod 105 | def droppable(cls) -> bool: 106 | return False 107 | 108 | 109 | class Sword(LineWeapon): 110 | @staticmethod 111 | def reach() -> int: 112 | return 3 113 | 114 | 115 | class Bow(LineWeapon): 116 | def __init__(self): 117 | self.ready: bool = False 118 | 119 | def description(self) -> WeaponDescription: 120 | return WeaponDescription(f"{self.__class__.__name__.lower()}_{'loaded' if self.ready else 'unloaded'}") 121 | 122 | @staticmethod 123 | def reach() -> int: 124 | return 50 125 | 126 | @staticmethod 127 | def cut_effect() -> effects.Effect: 128 | return effects.WeaponCut(3) 129 | 130 | def cut(self, arena: arenas.Arena, position: coordinates.Coords, facing: characters.Facing) -> None: 131 | if self.ready: 132 | super().cut(arena, position, facing) 133 | self.ready = False 134 | else: 135 | self.ready = True 136 | 137 | 138 | class Axe(Weapon): 139 | @classmethod 140 | def cut_positions( 141 | cls, 142 | terrain: arenas.Terrain, 143 | position: coordinates.Coords, 144 | facing: characters.Facing 145 | ) -> List[coordinates.Coords]: 146 | centre_position = position + facing.value 147 | left_position = centre_position + facing.turn_left().value 148 | right_position = centre_position + facing.turn_right().value 149 | return [left_position, centre_position, right_position] 150 | 151 | @staticmethod 152 | def cut_effect() -> effects.Effect: 153 | return effects.WeaponCut(3) 154 | 155 | def cut(self, arena: arenas.Arena, position: coordinates.Coords, facing: characters.Facing) -> None: 156 | for cut_position in self.cut_positions(arena.terrain, position, facing): 157 | self.cut_transparent(arena, cut_position) 158 | 159 | 160 | class Amulet(PropheticWeapon, Weapon): 161 | @staticmethod 162 | def prescience_radius() -> int: 163 | return 3 164 | 165 | @classmethod 166 | def cut_positions( 167 | cls, 168 | terrain: arenas.Terrain, 169 | position: coordinates.Coords, 170 | facing: characters.Facing 171 | ) -> List[coordinates.Coords]: 172 | return [ 173 | coordinates.Coords(*position + (1, 1)), 174 | coordinates.Coords(*position + (-1, 1)), 175 | coordinates.Coords(*position + (1, -1)), 176 | coordinates.Coords(*position + (-1, -1)), 177 | coordinates.Coords(*position + (2, 2)), 178 | coordinates.Coords(*position + (-2, 2)), 179 | coordinates.Coords(*position + (2, -2)), 180 | coordinates.Coords(*position + (-2, -2)), 181 | ] 182 | 183 | def cut(self, arena: arenas.Arena, position: coordinates.Coords, facing: characters.Facing) -> None: 184 | for cut_position in self.cut_positions(arena.terrain, position, facing): 185 | self.cut_transparent(arena, cut_position) 186 | 187 | 188 | class Scroll(LineWeapon): 189 | def __init__(self): 190 | self.charges: int = 5 191 | 192 | @staticmethod 193 | def reach() -> int: 194 | return 1 195 | 196 | @classmethod 197 | def droppable(cls) -> bool: 198 | return False 199 | 200 | @staticmethod 201 | def cut_effect() -> effects.Effect: 202 | return effects.Fire() 203 | 204 | def cut(self, arena: arenas.Arena, position: coordinates.Coords, facing: characters.Facing) -> None: 205 | if self.charges > 0: 206 | super().cut(arena, position, facing) 207 | self.charges -= 1 208 | -------------------------------------------------------------------------------- /gupb/runner.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import collections 3 | from dataclasses import dataclass 4 | import logging 5 | import random 6 | from typing import Any, List, Optional 7 | 8 | from tqdm import trange 9 | 10 | from gupb import controller 11 | from gupb.controller import keyboard 12 | from gupb.model.profiling import PROFILE_RESULTS, print_stats 13 | from gupb.logger import core as logger_core 14 | from gupb.model import coordinates 15 | from gupb.model import games 16 | from gupb.view import render 17 | 18 | verbose_logger = logging.getLogger('verbose') 19 | 20 | class Runner: 21 | def __init__(self, config: dict[str, Any]) -> None: 22 | self.arenas: list[str] = config['arenas'] 23 | self.controllers: list[controller.Controller] = config['controllers'] 24 | self.keyboard_controller: Optional[keyboard.KeyboardController] = next( 25 | (c for c in self.controllers if isinstance(c, keyboard.KeyboardController)), None 26 | ) 27 | self.show_sight: Optional[controller.Controller] = config['show_sight'] if 'show_sight' in config else None 28 | self.renderer: Optional[render.Renderer] = render.Renderer() if config['visualise'] else None 29 | self.runs_no: int = config['runs_no'] 30 | self.start_balancing: bool = config['start_balancing'] 31 | self.scores: dict[str, int] = collections.defaultdict(int) 32 | self.profiling_metrics = config['profiling_metrics'] if 'profiling_metrics' in config else None 33 | self._last_arena: Optional[str] = None 34 | self._last_menhir_position: Optional[coordinates.Coords] = None 35 | self._last_initial_positions: Optional[list[coordinates.Coords]] = None 36 | 37 | def run(self) -> None: 38 | for i in trange(self.runs_no, desc="Playing games"): 39 | verbose_logger.info(f"Starting game number {i + 1}.") 40 | GameStartReport(i + 1).log(logging.INFO) 41 | self.run_game(i) 42 | 43 | # noinspection PyBroadException 44 | def run_game(self, game_no: int) -> None: 45 | arena = random.choice(self.arenas) 46 | verbose_logger.debug(f"Randomly picked arena: {arena}.") 47 | RandomArenaPickReport(arena).log(logging.DEBUG) 48 | if not self.start_balancing or game_no % len(self.controllers) == 0: 49 | random.shuffle(self.controllers) 50 | game = games.Game( 51 | game_no=game_no, 52 | arena_name=arena, 53 | to_spawn=self.controllers, 54 | ) 55 | else: 56 | self.controllers = self.controllers[1:] + [self.controllers[0]] 57 | game = games.Game( 58 | game_no=game_no, 59 | arena_name=self._last_arena, 60 | to_spawn=self.controllers, 61 | menhir_position=self._last_menhir_position, 62 | initial_champion_positions=self._last_initial_positions, 63 | ) 64 | self._last_arena = game.arena.name 65 | self._last_menhir_position = game.arena.menhir_position 66 | self._last_initial_positions = game.initial_champion_positions 67 | show_sight = next((c for c in game.champions if c.controller == self.show_sight), None) 68 | if self.renderer: 69 | self.renderer.run(game, show_sight, self.keyboard_controller) 70 | else: 71 | self.run_in_memory(game) 72 | for dead_controller, score in game.score().items(): 73 | verbose_logger.info(f"Controller {dead_controller.name} scored {score} points.") 74 | ControllerScoreReport(dead_controller.name, score).log(logging.INFO) 75 | try: 76 | dead_controller.praise(score) 77 | except Exception as e: 78 | verbose_logger.warning(f"Controller {dead_controller.name} throw an unexpected exception: {repr(e)}.") 79 | controller.ControllerExceptionReport(dead_controller.name, repr(e)).log(logging.WARN) 80 | self.scores[dead_controller.name] += score 81 | 82 | def print_scores(self) -> None: 83 | verbose_logger.info(f"Final scores.") 84 | scores_to_log = [] 85 | for i, (name, score) in enumerate(sorted(self.scores.items(), key=lambda x: x[1], reverse=True)): 86 | score_line = f"{int(i) + 1}. {name}: {score}." 87 | verbose_logger.info(score_line) 88 | scores_to_log.append(ControllerScoreReport(name, score)) 89 | print(score_line) 90 | FinalScoresReport(scores_to_log).log(logging.INFO) 91 | 92 | if self.profiling_metrics: 93 | for func in PROFILE_RESULTS.keys(): 94 | print_stats(func, **{m: True for m in self.profiling_metrics}) 95 | 96 | @staticmethod 97 | def run_in_memory(game: games.Game) -> None: 98 | while not game.finished: 99 | game.cycle() 100 | 101 | 102 | @dataclass(frozen=True) 103 | class GameStartReport(logger_core.LoggingMixin): 104 | game_number: int 105 | 106 | 107 | @dataclass(frozen=True) 108 | class RandomArenaPickReport(logger_core.LoggingMixin): 109 | arena_name: str 110 | 111 | 112 | @dataclass(frozen=True) 113 | class ControllerScoreReport(logger_core.LoggingMixin): 114 | controller_name: str 115 | score: int 116 | 117 | 118 | @dataclass(frozen=True) 119 | class FinalScoresReport(logger_core.LoggingMixin): 120 | scores: List[ControllerScoreReport] 121 | -------------------------------------------------------------------------------- /gupb/scripts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prpht/GUPB/d70903656b53a4682e025fea87048234a686b388/gupb/scripts/__init__.py -------------------------------------------------------------------------------- /gupb/scripts/arena_generator.py: -------------------------------------------------------------------------------- 1 | from itertools import repeat 2 | import random 3 | from typing import Callable, Iterator 4 | 5 | import networkx as nx 6 | import perlin_noise 7 | import scipy.stats as scp_stats 8 | from tqdm import tqdm 9 | 10 | import gupb.model.arenas as arenas 11 | 12 | ArenaDefinition = list[list[str]] 13 | 14 | DEFAULT_WIDTH = 24 15 | DEFAULT_HEIGHT = 24 16 | MIN_SIZE = 28 17 | MAX_SIZE = 42 18 | PERLIN_NOISE_OCTAVES = 4 19 | REQUIRED_AREA_COEFFICIENT = 0.4 20 | WEAPONS_PER_BUILDING = 2 21 | BUILDINGS_PER_ARENA = 10 22 | MAX_BUILDING_SIZE = 10 23 | 24 | WEAPONS = ['S', 'A', 'B', 'M', 'C'] 25 | 26 | 27 | def forest_probability(intensity: float) -> float: 28 | return scp_stats.logistic.cdf(intensity, loc=0.35, scale=0.05) - mountain_probability(intensity) 29 | 30 | 31 | def mountain_probability(intensity: float) -> float: 32 | return scp_stats.logistic.cdf(intensity, loc=0.50, scale=0.05) 33 | 34 | 35 | def sea_probability(intensity: float) -> float: 36 | return 1.0 - scp_stats.logistic.cdf(intensity, loc=-0.05, scale=0.05) 37 | 38 | 39 | def empty_arena(width: int, height: int) -> ArenaDefinition: 40 | return [['='] * width for _ in range(height)] 41 | 42 | 43 | def perlin_landscape_arena(width: int, height: int) -> ArenaDefinition: 44 | def potentially_resolve_coin(roll: float, probability_eater: Callable[[float], float], mark: str): 45 | roll -= probability_eater(noise_picture[i][j]) 46 | if roll < 0.0: 47 | arena[i + 1][j + 1] = mark 48 | return True 49 | return False 50 | 51 | arena = empty_arena(width, height) 52 | perlin_width, perlin_height = width - 2, height - 2 53 | noise = perlin_noise.PerlinNoise(octaves=PERLIN_NOISE_OCTAVES) 54 | noise_picture = [ 55 | [noise([i / perlin_width, j / perlin_height]) for j in range(perlin_width)] 56 | for i in range(perlin_height) 57 | ] 58 | 59 | for i in range(perlin_height): 60 | for j in range(perlin_width): 61 | coin = random.random() 62 | if potentially_resolve_coin(coin, mountain_probability, '#'): 63 | continue 64 | if potentially_resolve_coin(coin, sea_probability, '='): 65 | continue 66 | if potentially_resolve_coin(coin, forest_probability, '@'): 67 | continue 68 | arena[i + 1][j + 1] = '.' 69 | return arena 70 | 71 | 72 | def arena_dimensions(arena: ArenaDefinition) -> tuple[int, int]: 73 | width, height = len(arena[0]), len(arena) 74 | return width, height 75 | 76 | 77 | def add_buildings(arena: ArenaDefinition) -> None: 78 | width, height = arena_dimensions(arena) 79 | for _ in range(BUILDINGS_PER_ARENA): 80 | building_width, building_height = random.randint(3, MAX_BUILDING_SIZE), random.randint(3, MAX_BUILDING_SIZE) 81 | building_j = random.randint(1, width - building_width - 1) 82 | building_i = random.randint(1, height - building_height - 1) 83 | for i in range(building_height): 84 | for j in range(building_width): 85 | arena[building_i + i][building_j + j] = '#' 86 | for i in range(building_height - 2): 87 | for j in range(building_width - 2): 88 | arena[building_i + i + 1][building_j + j + 1] = '.' 89 | 90 | doors_no = random.randint(2, 4) 91 | locations = ['top', 'bottom', 'left', 'right'] 92 | door_locations = random.sample(locations, doors_no) 93 | if 'top' in door_locations: 94 | door_shift = random.randint(1, building_width - 2) 95 | arena[building_i][building_j + door_shift] = '.' 96 | if 'bottom' in door_locations: 97 | door_shift = random.randint(1, building_width - 2) 98 | arena[building_i + building_height - 1][building_j + door_shift] = '.' 99 | if 'left' in door_locations: 100 | door_shift = random.randint(1, building_height - 2) 101 | arena[building_i + door_shift][building_j] = '.' 102 | if 'right' in door_locations: 103 | door_shift = random.randint(1, building_height - 2) 104 | arena[building_i + door_shift][building_j + building_width - 1] = '.' 105 | 106 | treasure_no = random.randint(1, min(WEAPONS_PER_BUILDING, (building_width - 2) * (building_height - 2))) 107 | for _ in range(treasure_no): 108 | treasure_i, treasure_j = random.randint(1, building_height - 2), random.randint(1, building_width - 2) 109 | treasure_type = random.choice(WEAPONS) 110 | arena[building_i + treasure_i][building_j + treasure_j] = treasure_type 111 | 112 | 113 | def is_passable(field: str) -> bool: 114 | return field in WEAPONS or arenas.TILE_ENCODING[field].terrain_passable() 115 | 116 | 117 | def create_arena_graph(arena: ArenaDefinition) -> nx.Graph: 118 | def add_passable_edge(i_target: int, j_target: int): 119 | if 0 <= i_target < height and 0 <= j_target < width and is_passable(arena[i_target][j_target]): 120 | arena_graph.add_edge((i, j), (i_target, j_target)) 121 | 122 | arena_graph = nx.Graph() 123 | width, height = arena_dimensions(arena) 124 | for i in range(height): 125 | for j in range(width): 126 | if is_passable(arena[i][j]): 127 | arena_graph.add_node((i, j)) 128 | add_passable_edge(i + 1, j) 129 | add_passable_edge(i - 1, j) 130 | add_passable_edge(i, j + 1) 131 | add_passable_edge(i, j - 1) 132 | return arena_graph 133 | 134 | 135 | def remove_disconnected_islands(arena: ArenaDefinition) -> int: 136 | arena_graph = create_arena_graph(arena) 137 | connected_components = [c for c in sorted(nx.connected_components(arena_graph), key=len, reverse=True)] 138 | for component in connected_components[1:]: 139 | component_size = len(component) 140 | # noinspection PyTypeChecker 141 | for i, j in component: 142 | if component_size > 5: 143 | arena[i][j] = '=' 144 | else: 145 | arena[i][j] = '#' 146 | return len(connected_components[0]) 147 | 148 | 149 | def generate_arena(width: int, height: int) -> ArenaDefinition: 150 | required_area = int(width * height * REQUIRED_AREA_COEFFICIENT) 151 | while True: 152 | arena = perlin_landscape_arena(width, height) 153 | add_buildings(arena) 154 | playable_area = remove_disconnected_islands(arena) 155 | if playable_area > required_area: 156 | break 157 | return arena 158 | 159 | 160 | def save_arena(arena: ArenaDefinition, file_name: str) -> None: 161 | def write_arena() -> None: 162 | with open(file_path, 'w') as file: 163 | for line in arena: 164 | for character in line: 165 | file.write(character) 166 | file.write('\n') 167 | 168 | def remove_last_new_line() -> None: 169 | with open(file_path, 'r+') as file: 170 | content = file.read() 171 | content = content.rstrip('\n') 172 | file.seek(0) 173 | file.write(content) 174 | file.truncate() 175 | 176 | file_path = f"./resources/arenas/{file_name}.gupb" 177 | write_arena() 178 | remove_last_new_line() 179 | 180 | 181 | def generate_arenas( 182 | how_many: int, 183 | size_generator: Iterator[tuple[int, int]] = repeat((DEFAULT_WIDTH, DEFAULT_HEIGHT)) 184 | ) -> list[str]: 185 | arena_names = [f"generated_{i}" for i in range(how_many)] 186 | for name in tqdm(arena_names, desc="Generating arenas"): 187 | arena = generate_arena(*next(size_generator)) 188 | save_arena(arena, name) 189 | return arena_names 190 | 191 | 192 | def random_size_generator() -> Iterator[tuple[int, int]]: 193 | while True: 194 | size = random.randint(MIN_SIZE, MAX_SIZE) 195 | yield size, size 196 | 197 | 198 | def main() -> None: 199 | generate_arenas(10) 200 | 201 | 202 | if __name__ == '__main__': 203 | main() 204 | -------------------------------------------------------------------------------- /gupb/scripts/result_parser.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import json 3 | 4 | 5 | def aggregate_scores(log: str, max_games_no: int) -> dict[str, int]: 6 | i = 0 7 | scores = collections.defaultdict(int) 8 | with open(f"../../results/together/{log}.json") as file: 9 | for line in file.readlines(): 10 | data = json.loads(line) 11 | if data['type'] == 'GameStartReport': 12 | i += 1 13 | if i > max_games_no: 14 | break 15 | elif data['type'] == 'ControllerScoreReport': 16 | scores[data['value']['controller_name']] += data['value']['score'] 17 | return dict(sorted(scores.items(), key=lambda x: x[1])) 18 | 19 | 20 | def main() -> None: 21 | result = aggregate_scores("gupb__2022_01_09_01_46_55", 365) 22 | print(result) 23 | 24 | 25 | if __name__ == '__main__': 26 | main() 27 | -------------------------------------------------------------------------------- /gupb/together_config.py: -------------------------------------------------------------------------------- 1 | from gupb.controller import random 2 | from gupb.scripts import arena_generator 3 | 4 | CONFIGURATION = { 5 | 'arenas': arena_generator.generate_arenas(10, arena_generator.random_size_generator()), 6 | 'controllers': [ 7 | random.RandomController("Alice"), 8 | random.RandomController("Bob"), 9 | random.RandomController("Cecilia"), 10 | random.RandomController("Darius"), 11 | ], 12 | 'start_balancing': False, 13 | 'visualise': False, 14 | 'show_sight': False, 15 | 'runs_no': 100, 16 | } 17 | -------------------------------------------------------------------------------- /gupb/view/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prpht/GUPB/d70903656b53a4682e025fea87048234a686b388/gupb/view/__init__.py -------------------------------------------------------------------------------- /gupb/view/render.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import os 3 | import itertools 4 | from typing import Any, Optional, TypeVar, Tuple 5 | 6 | import pygame 7 | import pygame.freetype 8 | 9 | from gupb.controller import keyboard 10 | from gupb.model import characters 11 | from gupb.model import consumables 12 | from gupb.model import effects 13 | from gupb.model import games 14 | from gupb.model import tiles 15 | from gupb.model import weapons 16 | 17 | pygame.init() 18 | 19 | Sprite = TypeVar('Sprite') 20 | 21 | INIT_TILE_SIZE = 32 22 | KEEP_TILE_RATIO = False 23 | 24 | HEALTH_BAR_HEIGHT = 4 25 | HEALTH_BAR_UNIT_WIDTH = 2 26 | HEALTH_BAR_FULL_COLOR = (250, 0, 0) 27 | HEALTH_BAR_OVERFILL_COLOR = (150, 0, 0) 28 | HEALTH_BAR_EMPTY_COLOR = (0, 0, 0) 29 | 30 | BLACK = pygame.Color('black') 31 | WHITE = pygame.Color('white') 32 | GAME_FONT = pygame.freetype.Font("resources/fonts/whitrabt.ttf", 24) 33 | 34 | 35 | def load_sprite(group: str, name: str, transparent: pygame.Color = None) -> Sprite: 36 | path = os.path.join('resources', 'images', group, f'{name}.png') 37 | sprite = pygame.image.load(path).convert() 38 | if sprite.get_size() is not (INIT_TILE_SIZE, INIT_TILE_SIZE): 39 | sprite = pygame.transform.scale(sprite, (INIT_TILE_SIZE, INIT_TILE_SIZE)) 40 | if transparent: 41 | sprite.set_colorkey(transparent) 42 | return sprite 43 | 44 | 45 | class SpriteRepository: 46 | def __init__(self) -> None: 47 | self.size = (INIT_TILE_SIZE, INIT_TILE_SIZE) 48 | self.sprites: dict[Any, Sprite] = { 49 | tiles.Sea: load_sprite('tiles', 'sea'), 50 | tiles.Land: load_sprite('tiles', 'land'), 51 | tiles.Forest: load_sprite('tiles', 'forest'), 52 | tiles.Wall: load_sprite('tiles', 'wall'), 53 | tiles.Menhir: load_sprite('tiles', 'menhir'), 54 | 55 | weapons.Knife: load_sprite('weapons', 'knife', BLACK), 56 | weapons.Sword: load_sprite('weapons', 'sword', BLACK), 57 | weapons.Axe: load_sprite('weapons', 'axe', BLACK), 58 | weapons.Bow: load_sprite('weapons', 'bow', BLACK), 59 | weapons.Amulet: load_sprite('weapons', 'amulet', BLACK), 60 | weapons.Scroll: load_sprite('weapons', 'scroll', BLACK), 61 | 62 | consumables.Potion: load_sprite('consumables', 'potion', BLACK), 63 | 64 | characters.Tabard.BLUE: load_sprite('characters', 'champion_blue', BLACK), 65 | characters.Tabard.BROWN: load_sprite('characters', 'champion_brown', BLACK), 66 | characters.Tabard.GREY: load_sprite('characters', 'champion_grey', BLACK), 67 | characters.Tabard.GREEN: load_sprite('characters', 'champion_green', BLACK), 68 | characters.Tabard.LIME: load_sprite('characters', 'champion_lime', BLACK), 69 | characters.Tabard.ORANGE: load_sprite('characters', 'champion_orange', BLACK), 70 | characters.Tabard.PINK: load_sprite('characters', 'champion_pink', BLACK), 71 | characters.Tabard.RED: load_sprite('characters', 'champion_red', BLACK), 72 | characters.Tabard.STRIPPED: load_sprite('characters', 'champion_stripped', BLACK), 73 | characters.Tabard.TURQUOISE: load_sprite('characters', 'champion_turquoise', BLACK), 74 | characters.Tabard.VIOLET: load_sprite('characters', 'champion_violet', BLACK), 75 | characters.Tabard.WHITE: load_sprite('characters', 'champion_white', BLACK), 76 | characters.Tabard.YELLOW: load_sprite('characters', 'champion_yellow', BLACK), 77 | 78 | effects.Mist: load_sprite('effects', 'mist', BLACK), 79 | effects.WeaponCut: load_sprite('effects', 'blood', BLACK), 80 | effects.Fire: load_sprite('effects', 'fire', WHITE), 81 | } 82 | self.rotation_values: dict[characters.Facing, int] = { 83 | characters.Facing.RIGHT: 0, 84 | characters.Facing.UP: 90, 85 | characters.Facing.LEFT: 180, 86 | characters.Facing.DOWN: 270, 87 | } 88 | self.champion_sprites: dict[tuple[characters.Tabard, characters.Facing], Sprite] = { 89 | (tabard, facing): pygame.transform.rotate(self.sprites[tabard], self.rotation_values[facing]) 90 | for tabard, facing in itertools.product( 91 | [ 92 | characters.Tabard.BLUE, 93 | characters.Tabard.BROWN, 94 | characters.Tabard.GREY, 95 | characters.Tabard.GREEN, 96 | characters.Tabard.LIME, 97 | characters.Tabard.ORANGE, 98 | characters.Tabard.PINK, 99 | characters.Tabard.RED, 100 | characters.Tabard.STRIPPED, 101 | characters.Tabard.TURQUOISE, 102 | characters.Tabard.VIOLET, 103 | characters.Tabard.WHITE, 104 | characters.Tabard.YELLOW, 105 | ], 106 | [ 107 | characters.Facing.RIGHT, 108 | characters.Facing.UP, 109 | characters.Facing.LEFT, 110 | characters.Facing.DOWN, 111 | ] 112 | ) 113 | } 114 | 115 | self._sprites = self.sprites.copy() 116 | self._champion_sprites = self.champion_sprites.copy() 117 | 118 | def match_sprite(self, element: Any) -> Sprite: 119 | if isinstance(element, characters.Champion): 120 | return self.champion_sprites[(element.tabard, element.facing)] 121 | else: 122 | return self.sprites[type(element)] 123 | 124 | @staticmethod 125 | def scale_sprite(sprite: Sprite, size: Tuple[int, int]) -> Sprite: 126 | return pygame.transform.scale(sprite, size) 127 | 128 | def scale_sprites(self, window_size: Tuple[int, int], arena_size: Tuple[int, int]) -> Tuple[int, int]: 129 | self.size = (int(window_size[0] / arena_size[0]), int(window_size[1] / arena_size[1])) 130 | 131 | if KEEP_TILE_RATIO: 132 | self.size = (min(self.size), min(self.size)) 133 | 134 | for sprite in self.sprites: 135 | self.sprites[sprite] = self.scale_sprite(self._sprites[sprite], self.size) 136 | 137 | for sprite in self.champion_sprites: 138 | self.champion_sprites[sprite] = self.scale_sprite(self._champion_sprites[sprite], self.size) 139 | 140 | return self.size[0] * arena_size[0], self.size[1] * arena_size[1] 141 | 142 | 143 | class Renderer: 144 | def __init__(self, ms_per_time_unit: int = 5): 145 | pygame.display.set_caption('GUPB') 146 | self.screen = pygame.display.set_mode((500, 500), pygame.RESIZABLE) 147 | self.sprite_repository = SpriteRepository() 148 | self.clock = pygame.time.Clock() 149 | self.time_passed = 0 150 | self.ms_per_time_unit = ms_per_time_unit 151 | 152 | def run( 153 | self, 154 | game: games.Game, 155 | show_sight: Optional[characters.Champion] = None, 156 | keyboard_controller: Optional[keyboard.KeyboardController] = None, 157 | ) -> None: 158 | self.screen = self._resize_window(game) 159 | self._render_starting_screen() 160 | 161 | time_to_cycle = self._time_to_cycle(game) 162 | self.clock.tick() 163 | while not game.finished: 164 | self.time_passed += self.clock.tick() 165 | if self.time_passed >= time_to_cycle: 166 | self.time_passed -= time_to_cycle 167 | game.cycle() 168 | self._render(game, show_sight) 169 | time_to_cycle = self._time_to_cycle(game) 170 | 171 | for event in pygame.event.get(): 172 | if event.type == pygame.QUIT: 173 | return 174 | elif event.type == pygame.KEYDOWN and keyboard_controller: 175 | keyboard_controller.register(event.key) 176 | if event.type == pygame.VIDEORESIZE: 177 | new_size = self.sprite_repository.scale_sprites((event.w, event.h), game.arena.size) 178 | self.screen = pygame.display.set_mode(new_size, pygame.RESIZABLE) 179 | 180 | def _resize_window(self, game: games.Game) -> pygame.Surface: 181 | arena_x_size, arena_y_size = game.arena.size 182 | window_size = self.sprite_repository.size[0] * arena_x_size, self.sprite_repository.size[1] * arena_y_size 183 | return pygame.display.set_mode(window_size, pygame.RESIZABLE) 184 | 185 | def _time_to_cycle(self, game: games.Game) -> int: 186 | return self.ms_per_time_unit * game.current_state.value 187 | 188 | def _render_starting_screen(self): 189 | wait_for_start_key = True 190 | while wait_for_start_key: 191 | GAME_FONT.render_to(self.screen, (70, 180), "Press X to start..!", (250, 250, 250)) 192 | pygame.display.flip() 193 | 194 | for event in pygame.event.get(): 195 | if event.type == pygame.QUIT: 196 | pygame.quit() 197 | if event.type == pygame.KEYDOWN and event.key == pygame.K_x: 198 | wait_for_start_key = False 199 | 200 | def _render(self, game: games.Game, show_sight: Optional[characters.Champion]) -> None: 201 | background = pygame.Surface(self.screen.get_size()) 202 | background.convert() 203 | self._render_arena(game, background) 204 | if show_sight: 205 | self._render_sight(game, show_sight, background) 206 | self.screen.blit(background, (0, 0)) 207 | pygame.display.flip() 208 | 209 | def _render_arena(self, game: games.Game, background: pygame.Surface) -> None: 210 | def render_character() -> None: 211 | def prepare_heath_bar_rect(health: int) -> pygame.Rect: 212 | return pygame.Rect( 213 | blit_destination[0], 214 | blit_destination[1] - HEALTH_BAR_HEIGHT - 1, 215 | health * HEALTH_BAR_UNIT_WIDTH, 216 | HEALTH_BAR_HEIGHT 217 | ) 218 | 219 | character_sprite = self.sprite_repository.match_sprite(tile.character) 220 | background.blit(character_sprite, blit_destination) 221 | pygame.draw.rect( 222 | background, 223 | HEALTH_BAR_OVERFILL_COLOR, 224 | prepare_heath_bar_rect(tile.character.health) 225 | ) 226 | pygame.draw.rect( 227 | background, 228 | HEALTH_BAR_EMPTY_COLOR, 229 | prepare_heath_bar_rect(characters.CHAMPION_STARTING_HP) 230 | ) 231 | pygame.draw.rect( 232 | background, 233 | HEALTH_BAR_FULL_COLOR, 234 | prepare_heath_bar_rect(min(characters.CHAMPION_STARTING_HP, tile.character.health)) 235 | ) 236 | 237 | for i, j in game.arena.terrain: 238 | blit_destination = (i * self.sprite_repository.size[0], j * self.sprite_repository.size[1]) 239 | tile = game.arena.terrain[i, j] 240 | tile_sprite = self.sprite_repository.match_sprite(tile) 241 | background.blit(tile_sprite, blit_destination) 242 | if tile.loot: 243 | loot_sprite = self.sprite_repository.match_sprite(tile.loot) 244 | background.blit(loot_sprite, blit_destination) 245 | if tile.consumable: 246 | consumable_sprite = self.sprite_repository.match_sprite(tile.consumable) 247 | background.blit(consumable_sprite, blit_destination) 248 | if tile.character: 249 | render_character() 250 | if tile.effects: 251 | for effect in tile.effects: 252 | effect_sprite = self.sprite_repository.match_sprite(effect) 253 | background.blit(effect_sprite, blit_destination) 254 | 255 | def _render_sight(self, game: games.Game, show_sight: characters.Champion, background: pygame.Surface) -> None: 256 | if show_sight in game.champions: 257 | darken_percent = 0.5 258 | dark = pygame.Surface(self.sprite_repository.size, pygame.SRCALPHA) 259 | dark.fill((0, 0, 0, int(darken_percent * 255))) 260 | visible = game.arena.visible_coords(show_sight) 261 | for i, j in game.arena.terrain: 262 | if (i, j) not in visible: 263 | blit_destination = (i * self.sprite_repository.size[0], j * self.sprite_repository.size[1]) 264 | background.blit(dark, blit_destination) 265 | -------------------------------------------------------------------------------- /profiling.md: -------------------------------------------------------------------------------- 1 | 2 | ## Profiling 3 | 4 | ### Zbieranie czasów wykonań danej funkcji 5 | ```python 6 | import gupb.model.profiling.profile 7 | 8 | class RandomController: 9 | 10 | @profile 11 | def decide(cls, knowledge: characters.ChampionKnowledge) -> characters.Action: 12 | return random.choice(POSSIBLE_ACTIONS) 13 | ``` 14 | 15 | Kolejne czasy wykonania funkcji zbierane są w globalnym dictionary `gupb.model.profiling.PROFILE_RESULTS`, 16 | z defaulta jest to `__qualname__` danej funkcji, czyli w tym przypadku `RandomController.decide`. 17 | Można natomiast podać własną nazwę w dekoratorze `@profile(name="MyFunction"))`. 18 | Należy zwrócić uwagę na to, że w przypadku wystąpienia tych samych nazw czasy będą zapisywane pod tą samą nazwą. 19 | 20 | 21 | ### Konfiguracja wypisywanych metryk 22 | Brak wartości w konfiguracji lub pusta lista spowoduje niewypisanie metryk na koniec gry. 23 | Możemy podać które metryki chcemy wypisać na koniec, np. pomijając osobne czasy każdego wykonania. 24 | ```python 25 | CONFIGURATION = { 26 | 'profiling_metrics': ['total', 'avg'], # possible metrics ['all', 'total', 'avg'] 27 | } 28 | ``` 29 | Metryk wypisywane są na koniec wszystkich gier w metodzie `print_scores` w klasie `Runner` 30 | 31 | ```python 32 | def print_scores(cls) -> None: 33 | ... 34 | if cls.profiling_metrics: 35 | for func in PROFILE_RESULTS.keys(): 36 | print_stats(func, **{m: True for m in cls.profiling_metrics}) 37 | ``` 38 | 39 | 40 | ### Przykład zebranych metryk 41 | ```text 42 | Stats for function: 'RandomController.decide' 43 | run times: ['1.31 ms', '0.75 ms', '0.78 ms', '0.71 ms', '4.20 ms', '1.34 ms', '1.01 ms', '0.45 ms', '0.45 ms', '3.07 ms', '0.57 ms', '0.44 ms', '18.73 ms', '0.46 ms', '3.55 ms', '0.47 ms', '0.44 ms', '0.69 ms', '1.23 ms', '8.26 ms', '1.10 ms', '0.90 ms', '0.67 ms', '0.78 ms', '4.21 ms', '1.16 ms', '0.92 ms', '0.75 ms', '0.74 ms', '3.17 ms', '0.55 ms', '0.53 ms', '0.75 ms', '0.74 ms', '4.04 ms', '0.85 ms', '1.03 ms', '1.12 ms', '1.04 ms', '6.56 ms', '1.22 ms', '0.77 ms', '0.78 ms', '0.72 ms', '3.39 ms', '0.80 ms', '0.76 ms', '0.69 ms', '0.63 ms', '3.63 ms', '0.76 ms', '0.70 ms', '0.61 ms', '0.88 ms', '3.66 ms', '0.71 ms', '0.59 ms', '0.77 ms', '0.76 ms', '3.53 ms', '0.70 ms', '1.17 ms', '0.97 ms', '0.75 ms', '3.33 ms', '0.88 ms', '0.75 ms', '0.67 ms', '0.63 ms', '3.64 ms', '0.90 ms', '0.88 ms', '0.91 ms', '1.50 ms', '11.34 ms', '3.13 ms', '2.71 ms', '3.45 ms', '1.46 ms', '4.20 ms', '0.64 ms', '0.79 ms', '0.78 ms', '0.70 ms', '3.32 ms', '0.84 ms', '0.87 ms', '0.77 ms', '0.70 ms', '4.24 ms', '0.98 ms', '1.03 ms', '0.99 ms', '1.46 ms', '6.82 ms', '1.36 ms', '1.20 ms', '1.51 ms', '1.64 ms', '8.38 ms', '1.39 ms', '1.75 ms', '1.80 ms', '1.84 ms', '10.01 ms', '3.68 ms', '3.35 ms', '2.20 ms', '1.93 ms', '12.29 ms', '2.49 ms', '2.23 ms', '2.68 ms', '3.27 ms', '10.14 ms', '1.01 ms', '0.77 ms', '0.96 ms', '0.89 ms', '4.01 ms', '0.76 ms', '1.01 ms', '0.80 ms', '0.82 ms', '3.42 ms', '0.69 ms', '0.68 ms', '0.72 ms', '0.61 ms', '3.80 ms', '0.95 ms', '0.79 ms', '0.74 ms', '0.91 ms', '3.99 ms', '0.74 ms', '0.69 ms', '0.92 ms', '0.88 ms', '3.67 ms', '0.62 ms', '0.79 ms', '0.72 ms', '0.68 ms', '3.31 ms', '0.86 ms', '0.72 ms', '0.70 ms', '0.63 ms', '3.60 ms', '0.76 ms', '0.69 ms', '0.59 ms', '0.76 ms', '3.45 ms', '0.68 ms', '0.54 ms', '0.74 ms', '0.67 ms', '3.26 ms', '0.53 ms', '0.71 ms', '0.70 ms', '0.66 ms', '3.26 ms', '0.74 ms', '0.67 ms', '0.65 ms', '0.54 ms', '3.32 ms', '0.64 ms', '0.64 ms', '0.60 ms', '0.68 ms', '3.32 ms', '0.74 ms', '0.57 ms', '0.75 ms', '0.70 ms', '3.32 ms', '0.61 ms', '0.76 ms', '0.69 ms', '0.59 ms', '3.23 ms', '0.76 ms', '0.64 ms', '0.61 ms', '0.58 ms', '3.35 ms', '0.70 ms', '0.59 ms', '0.56 ms', '0.75 ms', '3.32 ms', '0.60 ms', '0.58 ms', '0.73 ms', '0.67 ms', '3.37 ms', '0.55 ms', '0.76 ms', '0.67 ms', '0.60 ms', '3.28 ms', '0.80 ms', '0.96 ms', '0.70 ms', '0.63 ms', '3.53 ms', '0.76 ms', '0.79 ms', '0.57 ms', '0.79 ms', '3.47 ms', '0.71 ms', '0.62 ms', '0.85 ms', '0.79 ms', '3.37 ms', '0.58 ms', '0.83 ms', '0.81 ms', '0.68 ms', '3.37 ms', '0.78 ms', '0.72 ms', '0.59 ms', '0.59 ms', '3.40 ms', '0.75 ms', '0.64 ms', '0.58 ms', '0.77 ms', '3.31 ms', '0.58 ms', '0.60 ms', '0.75 ms', '0.75 ms', '3.27 ms', '0.55 ms', '0.78 ms', '0.68 ms', '0.67 ms', '3.43 ms', '0.82 ms', '0.67 ms', '0.68 ms', '0.61 ms', '3.39 ms', '0.72 ms', '0.68 ms', '0.69 ms', '0.71 ms', '3.35 ms', '18.92 ms'] 44 | total run time: 445.20 ms 45 | average run time: 1.74 ms 46 | ``` 47 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | bresenham==0.2.1 2 | click==8.1.7 3 | dataclasses-json==0.6.1 4 | gymnasium==0.29.1 5 | stable-baselines3==2.1.0 6 | matplotlib==3.8.0 7 | networkx==3.2.1 8 | numpy==1.26.0 9 | pathfinding==1.0.4 10 | perlin-noise==1.12 11 | pygame==2.5.2 12 | python-statemachine==2.1.1 13 | questionary==2.0.1 14 | scikit-guess==0.0.1a0 15 | scikit-learn==1.3.1 16 | scipy==1.11.3 17 | sortedcontainers==2.4.0 18 | tqdm==4.66.1 19 | -------------------------------------------------------------------------------- /resources/arenas/archipelago.gupb: -------------------------------------------------------------------------------- 1 | ================================================== 2 | ===B======================...=========..========== 3 | ===..===..============.......======........======= 4 | ===..=....=...========.........====............B== 5 | ===............======...........===........===.=== 6 | ==....#####................................===.=== 7 | ==....#A........=====.....==..............====.=== 8 | ==....#.........============..##..####========.=== 9 | ==....#.........=============.#.....A#========.=== 10 | ==....#...#........==========.#......=========.=== 11 | ===...#...#....===..==========================.=== 12 | ===............===..==========================.=== 13 | ====...........===.....========....===========.=== 14 | ===........=.=.===......=..==......======......=== 15 | ===........===.===.................=====..======== 16 | ===........===.=====...............=====....====== 17 | ======...=====.=====...............=====.....===== 18 | ======..======.=====...######....=............==== 19 | ======.=======.........#S........=====........==== 20 | ===....==========......#....#....====#........==== 21 | ===.=============......#...S#....====#A.......==== 22 | ===....==========......#.####....====####.....==== 23 | ======.==========................====#.....=..==== 24 | ==B....===========.==.......==...====#.....==.==== 25 | ===.=================.......==..======.....======= 26 | ===.=========.=.=====...........======.....======= 27 | ===..====.....=....=====........======.==.======== 28 | ====.====.....=....=====........=========.======== 29 | ====..=............=========..===========.======== 30 | ====....###........=========..===========.======== 31 | ====......#........==========.===========.======== 32 | ===....##.#......=.==========.===========.======== 33 | ====...#A.#......============.===========...====== 34 | ====...#..#......==========...==========.....===== 35 | ===....#.##......==========.....=========#...===== 36 | ===..............=========.........=======...===== 37 | ===..............=======.............====.....==== 38 | ===........=...=.===...=...##.####...=====..====== 39 | ===.......======.....=..........A#...======.====== 40 | ====......=============....#.....#...======.====== 41 | ====..=.=.=============....#.###............====== 42 | ========================..........=.=======B====== 43 | ===========================....=..================ 44 | ================================================== 45 | =============###================================== 46 | =============#M#================================== 47 | =============###================================== 48 | ================================================== 49 | ================================================== 50 | ================================================== -------------------------------------------------------------------------------- /resources/arenas/dungeon.gupb: -------------------------------------------------------------------------------- 1 | ################################################## 2 | #..................................#.........#..A# 3 | #....#S..#..#..#.###.#.#####.##.#..#.........#..A# 4 | #....#####..#..#.#......#.......#.....#..#...#...# 5 | #....#S..#..#..#.#..............#.....#..#...#...# 6 | ###......#.....#.#..###...###...#..#.....#.......# 7 | #K#..###########.#..#...#...#...#..#.....#.......# 8 | #S#..............#..#.......#...#..#############.# 9 | #A#..###########.#..#..###..#...#..........====#.# 10 | #B#..#........A#.#..#...S...#...#...........S==#.# 11 | #M#..#......#.A#.#..#..#S#..#...#..#.........S=#.# 12 | ###..#...#..#..#.#..#..#S#..#...#..#....===...=#.# 13 | #....#...#.....#.#..#...S...#...#..#....===....#.# 14 | #....#...#######.#..#..###..#...#..#.....==....#.# 15 | #....#...#.....#.#..#.......#...#..####........#.# 16 | #..............#.#..###...###...#..#==#..........# 17 | #..............#.#..............#..#==#..........# 18 | #######.#.######.######...#######..#############.# 19 | #................................................# 20 | ##...###########.................................# 21 | #.....#...#....#.################..#####......#### 22 | #..=..#........#...................#.............# 23 | #..=..#........#.#..............#..#.#.#.#.#.#.#.# 24 | #..=..#...#....#.#...###..###...#..#.............# 25 | #..=......#....#.#...#......#...#..#.#.#.#.#.#.#.# 26 | #..=......#....#.#...#..==..#...#..#.............# 27 | #..=..#...#....#.#...#MM==MM#...#..#.#.#.#.#.#.#.# 28 | #.....#...#....#.#...########...#..#.............# 29 | ################.#..............#....#.#.#.#.#.#.# 30 | #..............#.#..............#..#.............# 31 | #..............#.#..###..##..#..#..##..####B##..## 32 | #..=====.......#.#..###..##..#..#..#.....####....# 33 | #...=======....#.#..............#..#..=..####..=.# 34 | #....=======...#.#..............#..#..=..####..=.# 35 | #......====....#.#..##..##..##..#..#..=..####..=.# 36 | #...======.....#.#..##..##..##..#..#..=..####..=.# 37 | #....======....#.#..............#..#..=..####..=.# 38 | #.=====B=====..#.#..............#..#.....####....# 39 | #....======....#.#..#..##..###..#..####..#B##..### 40 | #..=======.....#.#..#..##..###..#................# 41 | #..=======.....#.#..............#..####..####...## 42 | #...========...#.####..##..######..#.....#AA#....# 43 | #...=======....#...................#.....#..#....# 44 | #..............#.################..#..#..#..#....# 45 | #..............#...................#..#..#..#....# 46 | #..............#...................#..#..#..#....# 47 | ####.......#####....=.===.===.=....#..#..#..#....# 48 | #B...............#..===.===.===.#..#..#.....#....# 49 | #B...............#..............#..#........#....# 50 | ################################################## -------------------------------------------------------------------------------- /resources/arenas/fisher_island.gupb: -------------------------------------------------------------------------------- 1 | ================================================== 2 | ==============......============================== 3 | ===========.......=======....===================== 4 | ============......==#####....#####===..=========== 5 | ==========.......###S...#....#.........=========== 6 | ==========.......#...##.#....#...#.....=========== 7 | =======..........#.###.......###.#........======== 8 | ========..#####...........................======== 9 | ==========#............====.............========== 10 | ==========#.........=====...######....####======== 11 | =========.#...#......======.#.........#....======= 12 | ====..==###..##...========..#....#.........======= 13 | ===.....#.....#.....=======......#....##.#======== 14 | ====....#.....#.....=========.####........======== 15 | =====...##..###...======..................======== 16 | =====....#...#.....======.........##..##..======== 17 | =======..#....#..======...........#S..A#.========= 18 | =======..###.A#...................######========== 19 | =====.......###.................===========.====== 20 | ======....................##........=====...====== 21 | =====....................#####................==== 22 | =====....................######..............===== 23 | =====.......................###....#####......==== 24 | =======....=.....................#########...===== 25 | ========..=====..........=.........##S####..====== 26 | =================.....====........#######....===== 27 | ===========.======....=.............###......===== 28 | ==========.....========......................===== 29 | ==========....................................==== 30 | ========...........................=======....==== 31 | ========..=......................===========...=== 32 | ===..====..=............==.......====..=========== 33 | ===...............==....=.....=====....###======== 34 | ====..===...=..==..=...==....======......#======== 35 | ====......=.=...=.....=.......=====..==#.#======== 36 | ====.==..==...=.....===.......==========...======= 37 | ===..........==..=..=........===========....====== 38 | ===..=....=........==.........=....====......===== 39 | ====B...=...==...====.......................====== 40 | =====...===..=...=..........................====== 41 | ===========.....==.........................======= 42 | ==================...........###.#.........======= 43 | ================.....==......#...#.........======= 44 | ==============......====.....#...###.#....======== 45 | ==============.=....====.....###.#M..#...========= 46 | ==============........==.......#.....#============ 47 | ================...............#######============ 48 | ===================............=================== 49 | ================================================== 50 | ================================================== 51 | -------------------------------------------------------------------------------- /resources/arenas/island.gupb: -------------------------------------------------------------------------------- 1 | ==================================================================================================== 2 | ==================================================================================================== 3 | ==================================================================================================== 4 | ==================================================================================================== 5 | ==================================================================================================== 6 | ================.....................===============================..............================== 7 | ============...................................................................===================== 8 | ==========.......................................................................=================== 9 | ===============...................................................................================== 10 | ==================..................................................................================ 11 | ==============.........................................................................============= 12 | ============....................................................................==================== 13 | ==========........................................................................=====...========== 14 | ==========................................................................................========== 15 | ==========................................................................................========== 16 | ==========...................................=========....................................========== 17 | ==========...............S...................=========....................................========== 18 | ==========....................................========....................................========== 19 | ==========.......................................===========..............................========== 20 | =============.....................................=============.....................S.....========== 21 | ===========....................................================...........................========== 22 | ================..................................============............................========== 23 | ============.........................................===========..........................========== 24 | ==========.............................................=============......................========== 25 | ==========..........................................================......................========== 26 | ==========.............................................=========================..........========== 27 | ==========................................................................................========== 28 | ==========................................................................................========== 29 | ==========........................................###########.............................========== 30 | ==========................................................................................========== 31 | ==========.....................................................#..........................========== 32 | ==========..................................#####..............#..........................========== 33 | ==========..................................#..................#..........................========== 34 | ==========..................................#####..............#..........................========== 35 | ===========================....................................#...................================= 36 | ===========================....................................#...................================= 37 | ===========================....................................#...................================= 38 | ===========================....................................#...................================= 39 | ===========================........................................................================= 40 | ==========................................................................................========== 41 | ==========................................................................................========== 42 | ==========................................................................................========== 43 | ==========..................................#########################.....................========== 44 | ==========..................................#.......................#.....................========== 45 | ==========..................................#.............................................========== 46 | ==========..................................#.............................................========== 47 | ===========================.................#......................................================= 48 | ===========================.................#.......................#..............================= 49 | ==========..................................#.......................#.....................========== 50 | ==========..................................#.......................#.....................========== 51 | ===========================.................#.......................#..............================= 52 | ==========..................................#########################.....................========== 53 | ==========................................................................................========== 54 | ==========................................................................................========== 55 | ===========================........................................................================= 56 | ===========================........................................................================= 57 | ==========................................................................................========== 58 | ==========................................................................................========== 59 | ==========................................................................................========== 60 | ==========................................................................................========== 61 | ==========.............................................##########.........................========== 62 | ==========.............................................#........#.........................========== 63 | ==========.............................................##.======#.........................========== 64 | ==========..........................B...............##..#.======.............B............========== 65 | ===========================..........................#.M#..........................================= 66 | ===========================........................######..........................================= 67 | ===========================........................#..#............................================= 68 | ===========================........................................................================= 69 | ===========================........................................................================= 70 | ===========================........................................................================= 71 | ===========================........................................................================= 72 | ==========............................................S...................................========== 73 | ==========................................................................................========== 74 | ==========................................................................................========== 75 | ==========............................................................A...................========== 76 | ==========................................................................................========== 77 | ==========................................................................................========== 78 | ==========................................................................................========== 79 | ==========................................................................................========== 80 | ==========...............................=========================================================== 81 | ==========...............................=========================================================== 82 | ==========...............................=========================================================== 83 | ==========...............................=========================================================== 84 | ==========........................A......=========================================================== 85 | ==========...............................=========================================================== 86 | ===========================........................................................================= 87 | ===========================........................................................================= 88 | ===========================........................................................================= 89 | ==========...............................=========================================================== 90 | ==========...............................=========================================================== 91 | ==========.........B.....................=========================================================== 92 | ==========...............................=========================================================== 93 | ==========....................A..........=========================================================== 94 | ==========...............................=========================================================== 95 | ==========...............................=========================================================== 96 | ==================================================================================================== 97 | ==================================================================================================== 98 | ==================================================================================================== 99 | ==================================================================================================== 100 | ==================================================================================================== -------------------------------------------------------------------------------- /resources/arenas/isolated_shrine.gupb: -------------------------------------------------------------------------------- 1 | =================== 2 | =M...............M= 3 | =.=..====B==.S===.= 4 | =.==A...=#=..===..= 5 | =.=====.....===A..= 6 | =.S==##.....##=.=.= 7 | =...=#..###..#=.=.= 8 | =.=......#......=.= 9 | =.==..#.....#..==.= 10 | =.B#..##...##..#B.= 11 | =.==..#.....#..==.= 12 | =.=......#......=.= 13 | =.=.=#..###..#=...= 14 | =.=.=##.....##==S.= 15 | =..A===.....=====.= 16 | =..===..=#=...A==.= 17 | =.===S.==B====..=.= 18 | =M...............M= 19 | =================== -------------------------------------------------------------------------------- /resources/arenas/lone_sanctum.gupb: -------------------------------------------------------------------------------- 1 | =================== 2 | =AA.#...===...#.BB= 3 | =A..#...==....#..B= 4 | =........=........= 5 | =####.........####= 6 | =..........==.....= 7 | =....========.....= 8 | =...........=.....= 9 | ==#.#==...#.==#.#== 10 | ===.===.....===.=== 11 | ==#.#==.#...==#.#== 12 | =.....=..........== 13 | =.....========...== 14 | =.....==..........= 15 | =####.........####= 16 | =........=........= 17 | =S..#....==...#..M= 18 | =SS.#...===...#.MM= 19 | =================== -------------------------------------------------------------------------------- /resources/arenas/mini.gupb: -------------------------------------------------------------------------------- 1 | ========== 2 | =.#####..= 3 | =.#...#..= 4 | =.#..##..= 5 | =...====.= 6 | =KSABM==.= 7 | =....==..= 8 | =.#...==.= 9 | =........= 10 | ========== -------------------------------------------------------------------------------- /resources/arenas/ordinary_chaos.gupb: -------------------------------------------------------------------------------- 1 | ======================== 2 | =####.##=C...====....=== 3 | ==#A...#==....====..=@@= 4 | ==.S....==...@==......@= 5 | ==#....#===..@@@.=.....= 6 | =##....#====..@..#.#...= 7 | =####.##====......M#=..= 8 | ==.....=====.....#.....= 9 | ==.###.@.#.==..#.#.#...= 10 | ===#.#....@==....#.#...= 11 | =....#...@@.......===..= 12 | =###..B...@.=...====.=.= 13 | =#...##.##..===...==...= 14 | =#...#==.#.===........#= 15 | =#..S#=.....==........@= 16 | =#...#==...===##.###...= 17 | =#.###=..#..==#....#==.= 18 | =....=.......=#BA...==== 19 | ==..==....@........#==== 20 | ==.....@.@#@..##.###==== 21 | =====..#@@#....M..====== 22 | =.....@###....##.#====== 23 | =C====@@....=.....====== 24 | ======================== -------------------------------------------------------------------------------- /resources/arenas/wasteland.gupb: -------------------------------------------------------------------------------- 1 | ================================================== 2 | =.#M.......=...###......#.....................M..= 3 | =..............###.............====...........#..= 4 | =...#####.......###...................=...#......= 5 | =...#..................###...............KA#.....= 6 | =...#...#....=.....#.....###.............##....#.= 7 | =...###.#.........................#......###.....= 8 | =#..#SK...........................#..............= 9 | =....#........#...#....=.....=....#...#..........= 10 | =........=....#..............................##..= 11 | =..=..........#.................#..........###...= 12 | =..................#.................=.....#.....= 13 | =.....#............#....#####..............#.....= 14 | =........#...=.....###..#...#####................= 15 | =......##....=.........................=.........= 16 | =..=...........=....######.######................= 17 | =..............=.........#.......................= 18 | =...........==.=.........#.#.........#......###..= 19 | =..##....................#.#.........#......KKB#.= 20 | =.#BKK....=....#....#...........=....#......####.= 21 | =..###..............#.......................#S#..= 22 | =....#..............#...#M..................#K#..= 23 | =....#.....#........#...#................=..#....= 24 | =..........###.....######.....###................= 25 | =..........#.##........#......##...#.............= 26 | =.............#........#.......######......=.....= 27 | =......=........#......#.........#####...........= 28 | =..#........#...##.........=....###..#...#.....#.= 29 | =..#.............#..............=................= 30 | =..##.............##......#................=.....= 31 | =...####...........#......##.....................= 32 | =...#...#....=......##....##....#............#...= 33 | =..........................#........##.......#...= 34 | =................................#...#.......#...= 35 | =........###...#.....................#...........= 36 | =.##................=.............###............= 37 | =..###...................#....=..................= 38 | =.........=..............##.................=....= 39 | =............####....#....#...........#..........= 40 | =........=......#.................#####....=.....= 41 | =.#.#...........#............=........#..........= 42 | =.#.#.........#####...................#........#.= 43 | =##.##.................##........................= 44 | =#...#..........................##..........#K#..= 45 | =#...#........=....#...#......###.........###K#..= 46 | =#.#.#....#..................#............KA#B#..= 47 | =#...#.....#K#..#..........................#.#...= 48 | =#M..##....#A#...................=...##..........= 49 | =##........######.####..#...#........##.......=M.= 50 | ================================================== -------------------------------------------------------------------------------- /resources/fonts/whitrabt.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prpht/GUPB/d70903656b53a4682e025fea87048234a686b388/resources/fonts/whitrabt.ttf -------------------------------------------------------------------------------- /resources/images/characters/champion_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prpht/GUPB/d70903656b53a4682e025fea87048234a686b388/resources/images/characters/champion_blue.png -------------------------------------------------------------------------------- /resources/images/characters/champion_brown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prpht/GUPB/d70903656b53a4682e025fea87048234a686b388/resources/images/characters/champion_brown.png -------------------------------------------------------------------------------- /resources/images/characters/champion_green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prpht/GUPB/d70903656b53a4682e025fea87048234a686b388/resources/images/characters/champion_green.png -------------------------------------------------------------------------------- /resources/images/characters/champion_grey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prpht/GUPB/d70903656b53a4682e025fea87048234a686b388/resources/images/characters/champion_grey.png -------------------------------------------------------------------------------- /resources/images/characters/champion_lime.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prpht/GUPB/d70903656b53a4682e025fea87048234a686b388/resources/images/characters/champion_lime.png -------------------------------------------------------------------------------- /resources/images/characters/champion_orange.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prpht/GUPB/d70903656b53a4682e025fea87048234a686b388/resources/images/characters/champion_orange.png -------------------------------------------------------------------------------- /resources/images/characters/champion_pink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prpht/GUPB/d70903656b53a4682e025fea87048234a686b388/resources/images/characters/champion_pink.png -------------------------------------------------------------------------------- /resources/images/characters/champion_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prpht/GUPB/d70903656b53a4682e025fea87048234a686b388/resources/images/characters/champion_red.png -------------------------------------------------------------------------------- /resources/images/characters/champion_stripped.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prpht/GUPB/d70903656b53a4682e025fea87048234a686b388/resources/images/characters/champion_stripped.png -------------------------------------------------------------------------------- /resources/images/characters/champion_turquoise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prpht/GUPB/d70903656b53a4682e025fea87048234a686b388/resources/images/characters/champion_turquoise.png -------------------------------------------------------------------------------- /resources/images/characters/champion_violet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prpht/GUPB/d70903656b53a4682e025fea87048234a686b388/resources/images/characters/champion_violet.png -------------------------------------------------------------------------------- /resources/images/characters/champion_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prpht/GUPB/d70903656b53a4682e025fea87048234a686b388/resources/images/characters/champion_white.png -------------------------------------------------------------------------------- /resources/images/characters/champion_yellow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prpht/GUPB/d70903656b53a4682e025fea87048234a686b388/resources/images/characters/champion_yellow.png -------------------------------------------------------------------------------- /resources/images/consumables/potion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prpht/GUPB/d70903656b53a4682e025fea87048234a686b388/resources/images/consumables/potion.png -------------------------------------------------------------------------------- /resources/images/effects/blood.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prpht/GUPB/d70903656b53a4682e025fea87048234a686b388/resources/images/effects/blood.png -------------------------------------------------------------------------------- /resources/images/effects/fire.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prpht/GUPB/d70903656b53a4682e025fea87048234a686b388/resources/images/effects/fire.png -------------------------------------------------------------------------------- /resources/images/effects/mist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prpht/GUPB/d70903656b53a4682e025fea87048234a686b388/resources/images/effects/mist.png -------------------------------------------------------------------------------- /resources/images/tiles/forest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prpht/GUPB/d70903656b53a4682e025fea87048234a686b388/resources/images/tiles/forest.png -------------------------------------------------------------------------------- /resources/images/tiles/land.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prpht/GUPB/d70903656b53a4682e025fea87048234a686b388/resources/images/tiles/land.png -------------------------------------------------------------------------------- /resources/images/tiles/menhir.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prpht/GUPB/d70903656b53a4682e025fea87048234a686b388/resources/images/tiles/menhir.png -------------------------------------------------------------------------------- /resources/images/tiles/sea.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prpht/GUPB/d70903656b53a4682e025fea87048234a686b388/resources/images/tiles/sea.png -------------------------------------------------------------------------------- /resources/images/tiles/wall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prpht/GUPB/d70903656b53a4682e025fea87048234a686b388/resources/images/tiles/wall.png -------------------------------------------------------------------------------- /resources/images/weapons/amulet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prpht/GUPB/d70903656b53a4682e025fea87048234a686b388/resources/images/weapons/amulet.png -------------------------------------------------------------------------------- /resources/images/weapons/axe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prpht/GUPB/d70903656b53a4682e025fea87048234a686b388/resources/images/weapons/axe.png -------------------------------------------------------------------------------- /resources/images/weapons/bow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prpht/GUPB/d70903656b53a4682e025fea87048234a686b388/resources/images/weapons/bow.png -------------------------------------------------------------------------------- /resources/images/weapons/knife.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prpht/GUPB/d70903656b53a4682e025fea87048234a686b388/resources/images/weapons/knife.png -------------------------------------------------------------------------------- /resources/images/weapons/scroll.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prpht/GUPB/d70903656b53a4682e025fea87048234a686b388/resources/images/weapons/scroll.png -------------------------------------------------------------------------------- /resources/images/weapons/sword.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prpht/GUPB/d70903656b53a4682e025fea87048234a686b388/resources/images/weapons/sword.png -------------------------------------------------------------------------------- /screenshots/gupb_sample_state.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prpht/GUPB/d70903656b53a4682e025fea87048234a686b388/screenshots/gupb_sample_state.png --------------------------------------------------------------------------------