예제 #1
0
파일: game.py 프로젝트: lingsond/tota
    def __init__(self,
                 radiant_heroes,
                 dire_heroes,
                 map_file_path,
                 world_size,
                 debug=False,
                 protected=False,
                 drawers=None):
        self.radiant_heroes = radiant_heroes
        self.dire_heroes = dire_heroes
        self.map_file_path = map_file_path
        self.debug = debug
        self.protected = protected
        self.drawers = drawers or []

        self.heroes = []
        self.ancients = {}
        self.towers = {}
        self.events = []
        self.scores = defaultdict(lambda: 0)

        self.world = World(world_size)

        self.initialize_world_map()
        self.cache_structures()
        self.initialize_heroes()
예제 #2
0
    def __init__(self,
                 radiant_heroes,
                 dire_heroes,
                 map_file_path,
                 world_size,
                 debug=False,
                 drawers=None):
        self.radiant_heroes = radiant_heroes
        self.dire_heroes = dire_heroes
        self.map_file_path = map_file_path
        self.debug = debug
        self.drawers = drawers or []

        self.heroes = []
        self.ancients = {}

        self.world = World(world_size, debug=debug)

        self.initialize_world_map()
        self.cache_ancients()
        self.initialize_heroes()
예제 #3
0
파일: game.py 프로젝트: nueces/tota
    def __init__(self, radiant_heroes, dire_heroes, map_file_path, world_size,
                 debug=False, use_basic_icons=False):
        self.radiant_heroes = radiant_heroes
        self.dire_heroes = dire_heroes
        self.map_file_path = map_file_path
        self.debug = debug
        self.use_basic_icons = use_basic_icons

        self.heroes = []
        self.ancients = {}

        self.world = World(world_size, debug=debug)

        self.initialize_world_map()
        self.cache_ancients()
        self.initialize_heroes()
예제 #4
0
파일: game.py 프로젝트: arielrossanigo/tota
    def __init__(self, radiant_heroes, dire_heroes, map_file_path, world_size,
                 debug=False, drawers=None):
        self.radiant_heroes = radiant_heroes
        self.dire_heroes = dire_heroes
        self.map_file_path = map_file_path
        self.debug = debug
        self.drawers = drawers or []

        self.heroes = []
        self.ancients = {}

        self.world = World(world_size, debug=debug)

        self.initialize_world_map()
        self.cache_ancients()
        self.initialize_heroes()
예제 #5
0
파일: game.py 프로젝트: nicoechaniz/tota
    def __init__(self, radiant_heroes, dire_heroes, map_file_path, world_size,
                 debug=False, protected=False, drawers=None):
        self.radiant_heroes = radiant_heroes
        self.dire_heroes = dire_heroes
        self.map_file_path = map_file_path
        self.debug = debug
        self.protected = protected
        self.drawers = drawers or []

        self.heroes = []
        self.ancients = {}
        self.towers = {}
        self.events = []
        self.scores = defaultdict(lambda: 0)

        self.world = World(world_size)

        self.initialize_world_map()
        self.cache_structures()
        self.initialize_heroes()
예제 #6
0
파일: game.py 프로젝트: nueces/tota
class Game:
    """An instance of game controls the flow of the game.

       This includes player and creeps spawning, game main loop, deciding when
       to stop, importing map data, drawing each update, etc.
    """
    def __init__(self, radiant_heroes, dire_heroes, map_file_path, world_size,
                 debug=False, use_basic_icons=False):
        self.radiant_heroes = radiant_heroes
        self.dire_heroes = dire_heroes
        self.map_file_path = map_file_path
        self.debug = debug
        self.use_basic_icons = use_basic_icons

        self.heroes = []
        self.ancients = {}

        self.world = World(world_size, debug=debug)

        self.initialize_world_map()
        self.cache_ancients()
        self.initialize_heroes()

    def initialize_world_map(self):
        with open(self.map_file_path, encoding='utf-8') as map_file:
            map_text = map_file.read()
            self.world.import_map(map_text)

    def cache_ancients(self):
        def get_ancient(team):
            ancients = [thing for thing in self.world.things.values()
                        if isinstance(thing, Ancient) and thing.team == team]
            if not ancients:
                message = "Can't find the ancient for the {} team".format(team)
                raise Exception(message)
            elif len(ancients) > 1:
                message = "Team {} has 2 ancients".format(team)
                raise Exception(message)
            else:
                return ancients[0]

        for team in (settings.TEAM_DIRE, settings.TEAM_RADIANT):
            self.ancients[team] = get_ancient(team)

    def initialize_heroes(self):
        teams = {
            settings.TEAM_DIRE: self.dire_heroes,
            settings.TEAM_RADIANT: self.radiant_heroes,
        }
        for team, heroes in teams.items():
            for hero_name in heroes:
                hero = Hero(name=hero_name,
                            team=team,
                            act_function=get_hero_function(hero_name))
                self.heroes.append(hero)

    def spawn_near_ancient(self, thing):
        """Spawn players or creeps near their ancient."""
        # start searching from the ancient position, outwards, until an empty
        # space is found, using breadth first graph search
        ancient = self.ancients[thing.team]
        spawn_at = closes_empty_position(ancient, self.world)
        if spawn_at:
            self.world.spawn(thing, spawn_at)
        else:
            message = "Can't spawn {} near its ancient".format(thing.name)
            raise Exception(message)

    def position_draw(self, position):
        """Get the string to draw for a given position of the world."""
        # decorations first, then things over them
        thing = self.world.things.get(position)

        if thing is not None:
            if self.use_basic_icons:
                icon = thing.ICON_BASIC
            else:
                icon = thing.ICON
            return colored(icon, settings.TEAM_COLORS[thing.team])
        else:
            return ' '

    def play(self, frames_per_second=2.0):
        """Game main loop, ending in a game result with description."""
        while True:
            # spawn creep wave
            if self.world.t % settings.CREEP_WAVE_COOLDOWN == 0:
                for team in (settings.TEAM_RADIANT, settings.TEAM_DIRE):
                    for i in range(settings.CREEP_WAVE_SIZE):
                        creep = Creep(team)
                        self.spawn_near_ancient(creep)

            self.spawn_heroes()
            self.world.step()
            self.update_experience()
            self.clean_deads()

            self.draw()

            if self.debug:
                input()
            else:
                time.sleep(1.0 / frames_per_second)

            if self.game_ended():
                description = self.game_result()
                print('')
                print(description)

                return description

    def spawn_heroes(self):
        for hero in self.heroes:
            if hero.respawn_at == self.world.t:
                hero.life = hero.max_life
                self.spawn_near_ancient(hero)

    def update_experience(self):
        for thing in list(self.world.things.values()):
            if not thing.alive:
                for hero in self.heroes:
                    if hero.alive and hero.team != thing.team and distance(hero, thing) < settings.XP_DISTANCE:
                        if isinstance(thing, Creep):
                            hero.xp += settings.XP_CREEP_DEAD
                        elif isinstance(thing, Hero):
                            hero.xp += settings.XP_HERO_DEAD
                        elif isinstance(thing, Tower):
                            hero.xp += settings.XP_TOWER_DEAD

    def clean_deads(self):
        """Remove dead things from the world."""
        for thing in list(self.world.things.values()):
            if not thing.alive:
                self.world.destroy(thing)
                if isinstance(thing, Hero):
                    thing.respawn_at = self.world.t + settings.HERO_RESPAWN_COOLDOWN

    def draw(self):
        """Draw the world."""
        screen = ''

        # print the world
        screen += '\n'.join(u''.join(self.position_draw((x, y)) + ' '
                                     for x in range(self.world.size[0]))
                            for y in range(self.world.size[1]))

        # game stats
        screen += '\nticks:{}'.format(self.world.t)

        # print hero stats
        for hero in sorted(self.heroes, key=lambda x: x.name):
            if hero.alive:
                # a small "health bar" with unicode chars, from 0 to 10 chars
                life_chars_count = int((10.0 / hero.max_life) * hero.life)
                life_chars = life_chars_count * '\u2588'
                no_life_chars = (10 - life_chars_count) * '\u2591'
                life_bar = '\u2665 {}{}'.format(life_chars, no_life_chars)
            else:
                life_bar = '\u2620 [dead]'

            hero_template = '{bar}({life}) {name} ({level})'
            hero_stats = hero_template.format(bar=life_bar,
                                              name=hero.name,
                                              life=int(hero.life),
                                              level=hero.level)

            screen += '\n' + colored(hero_stats,
                                     settings.TEAM_COLORS[hero.team])

        # print events (of last step) for debugging
        if self.debug:
            screen += u'\n'
            screen += u'\n'.join([colored('{}: {}'.format(thing.name, event),
                                          settings.TEAM_COLORS[thing.team])
                                  for t, thing, event in self.world.events
                                  if t == self.world.t])
        os.system('clear')
        print(screen)

    def destroyed_ancients(self):
        """Which ancients have been destroyed?"""
        return [ancient for ancient in self.ancients.values()
                if not ancient.alive]

    def game_ended(self):
        """Has the game ended?"""
        return len(self.destroyed_ancients()) > 0

    def game_result(self):
        """Was the game won?"""
        return '\n'.join('Team {} lost!'.format(ancient.team)
                         for ancient in self.destroyed_ancients())
예제 #7
0
파일: game.py 프로젝트: arielrossanigo/tota
class Game:
    """An instance of game controls the flow of the game.

       This includes player and creeps spawning, game main loop, deciding when
       to stop, importing map data, drawing each update, etc.
    """
    def __init__(self, radiant_heroes, dire_heroes, map_file_path, world_size,
                 debug=False, drawers=None):
        self.radiant_heroes = radiant_heroes
        self.dire_heroes = dire_heroes
        self.map_file_path = map_file_path
        self.debug = debug
        self.drawers = drawers or []

        self.heroes = []
        self.ancients = {}

        self.world = World(world_size, debug=debug)

        self.initialize_world_map()
        self.cache_ancients()
        self.initialize_heroes()

    def initialize_world_map(self):
        with open(self.map_file_path, encoding='utf-8') as map_file:
            map_text = map_file.read()
            self.world.import_map(map_text)

    def cache_ancients(self):
        def get_ancient(team):
            ancients = [thing for thing in self.world.things.values()
                        if isinstance(thing, Ancient) and thing.team == team]
            if not ancients:
                message = "Can't find the ancient for the {} team".format(team)
                raise Exception(message)
            elif len(ancients) > 1:
                message = "Team {} has 2 ancients".format(team)
                raise Exception(message)
            else:
                return ancients[0]

        for team in (settings.TEAM_DIRE, settings.TEAM_RADIANT):
            self.ancients[team] = get_ancient(team)

    def initialize_heroes(self):
        teams = {
            settings.TEAM_DIRE: self.dire_heroes,
            settings.TEAM_RADIANT: self.radiant_heroes,
        }
        for team, heroes in teams.items():
            for hero_name in heroes:
                hero = Hero(name=hero_name,
                            team=team,
                            act_function=get_hero_function(hero_name))
                self.heroes.append(hero)

    def spawn_near_ancient(self, thing):
        """Spawn players or creeps near their ancient."""
        # start searching from the ancient position, outwards, until an empty
        # space is found, using breadth first graph search
        ancient = self.ancients[thing.team]
        spawn_at = closes_empty_position(ancient, self.world)
        if spawn_at:
            self.world.spawn(thing, spawn_at)
        else:
            message = "Can't spawn {} near its ancient".format(thing.name)
            raise Exception(message)

    def play(self, frames_per_second=2.0):
        """Game main loop, ending in a game result with description."""
        while True:
            # spawn creep wave
            if self.world.t % settings.CREEP_WAVE_COOLDOWN == 0:
                for team in (settings.TEAM_RADIANT, settings.TEAM_DIRE):
                    for i in range(settings.CREEP_WAVE_SIZE):
                        creep = Creep(team)
                        self.spawn_near_ancient(creep)

            self.spawn_heroes()
            self.world.step()
            self.update_experience()
            self.clean_deads()

            self.draw()

            self.world.effects = {}

            if self.debug:
                input()
            else:
                time.sleep(1.0 / frames_per_second)

            if self.game_ended():
                description = self.game_result()
                print('')
                print(description)

                return description

    def spawn_heroes(self):
        for hero in self.heroes:
            if hero.respawn_at == self.world.t:
                hero.life = hero.max_life
                self.spawn_near_ancient(hero)

    def update_experience(self):
        for thing in list(self.world.things.values()):
            if not thing.alive:
                for hero in self.heroes:
                    if hero.alive and hero.team != thing.team and distance(hero, thing) < settings.XP_DISTANCE:
                        if isinstance(thing, Creep):
                            hero.xp += settings.XP_CREEP_DEAD
                        elif isinstance(thing, Hero):
                            hero.xp += settings.XP_HERO_DEAD
                        elif isinstance(thing, Tower):
                            hero.xp += settings.XP_TOWER_DEAD

    def clean_deads(self):
        """Remove dead things from the world."""
        for thing in list(self.world.things.values()):
            if not thing.alive:
                self.world.destroy(thing)
                if isinstance(thing, Hero):
                    thing.respawn_at = self.world.t + settings.HERO_RESPAWN_COOLDOWN

    def draw(self):
        """Call each drawer instance."""
        for drawer in self.drawers:
            drawer.draw(self)

    def destroyed_ancients(self):
        """Which ancients have been destroyed?"""
        return [ancient for ancient in self.ancients.values()
                if not ancient.alive]

    def game_ended(self):
        """Has the game ended?"""
        return len(self.destroyed_ancients()) > 0

    def game_result(self):
        """Was the game won?"""
        return '\n'.join('Team {} lost!'.format(ancient.team)
                         for ancient in self.destroyed_ancients())
예제 #8
0
class Game:
    """An instance of game controls the flow of the game.

       This includes player and creeps spawning, game main loop, deciding when
       to stop, importing map data, drawing each update, etc.
    """
    def __init__(self,
                 radiant_heroes,
                 dire_heroes,
                 map_file_path,
                 world_size,
                 debug=False,
                 drawers=None):
        self.radiant_heroes = radiant_heroes
        self.dire_heroes = dire_heroes
        self.map_file_path = map_file_path
        self.debug = debug
        self.drawers = drawers or []

        self.heroes = []
        self.ancients = {}

        self.world = World(world_size, debug=debug)

        self.initialize_world_map()
        self.cache_ancients()
        self.initialize_heroes()

    def initialize_world_map(self):
        with open(self.map_file_path, encoding='utf-8') as map_file:
            map_text = map_file.read()
            self.world.import_map(map_text)

    def cache_ancients(self):
        def get_ancient(team):
            ancients = [
                thing for thing in self.world.things.values()
                if isinstance(thing, Ancient) and thing.team == team
            ]
            if not ancients:
                message = "Can't find the ancient for the {} team".format(team)
                raise Exception(message)
            elif len(ancients) > 1:
                message = "Team {} has 2 ancients".format(team)
                raise Exception(message)
            else:
                return ancients[0]

        for team in (settings.TEAM_DIRE, settings.TEAM_RADIANT):
            self.ancients[team] = get_ancient(team)

    def initialize_heroes(self):
        teams = {
            settings.TEAM_DIRE: self.dire_heroes,
            settings.TEAM_RADIANT: self.radiant_heroes,
        }
        for team, heroes in teams.items():
            for hero_name in heroes:
                hero = Hero(name=hero_name,
                            team=team,
                            act_function=get_hero_function(hero_name))
                self.heroes.append(hero)

    def spawn_near_ancient(self, thing):
        """Spawn players or creeps near their ancient."""
        # start searching from the ancient position, outwards, until an empty
        # space is found, using breadth first graph search
        ancient = self.ancients[thing.team]
        spawn_at = closes_empty_position(ancient, self.world)
        if spawn_at:
            self.world.spawn(thing, spawn_at)
        else:
            message = "Can't spawn {} near its ancient".format(thing.name)
            raise Exception(message)

    def play(self, frames_per_second=2.0):
        """Game main loop, ending in a game result with description."""
        while True:
            # spawn creep wave
            if self.world.t % settings.CREEP_WAVE_COOLDOWN == 0:
                for team in (settings.TEAM_RADIANT, settings.TEAM_DIRE):
                    for i in range(settings.CREEP_WAVE_SIZE):
                        creep = Creep(team)
                        self.spawn_near_ancient(creep)

            self.spawn_heroes()
            self.world.step()
            self.update_experience()
            self.clean_deads()

            self.draw()

            self.world.effects = {}

            if self.debug:
                input()
            else:
                time.sleep(1.0 / frames_per_second)

            if self.game_ended():
                description = self.game_result()
                print('')
                print(description)

                return description

    def spawn_heroes(self):
        for hero in self.heroes:
            if hero.respawn_at == self.world.t:
                hero.life = hero.max_life
                self.spawn_near_ancient(hero)

    def update_experience(self):
        for thing in list(self.world.things.values()):
            if not thing.alive:
                for hero in self.heroes:
                    if hero.alive and hero.team != thing.team and distance(
                            hero, thing) < settings.XP_DISTANCE:
                        if isinstance(thing, Creep):
                            hero.xp += settings.XP_CREEP_DEAD
                        elif isinstance(thing, Hero):
                            hero.xp += settings.XP_HERO_DEAD
                        elif isinstance(thing, Tower):
                            hero.xp += settings.XP_TOWER_DEAD

    def clean_deads(self):
        """Remove dead things from the world."""
        for thing in list(self.world.things.values()):
            if not thing.alive:
                self.world.destroy(thing)
                if isinstance(thing, Hero):
                    thing.respawn_at = self.world.t + settings.HERO_RESPAWN_COOLDOWN

    def draw(self):
        """Call each drawer instance."""
        for drawer in self.drawers:
            drawer.draw(self)

    def destroyed_ancients(self):
        """Which ancients have been destroyed?"""
        return [
            ancient for ancient in self.ancients.values() if not ancient.alive
        ]

    def game_ended(self):
        """Has the game ended?"""
        return len(self.destroyed_ancients()) > 0

    def game_result(self):
        """Was the game won?"""
        return '\n'.join('Team {} lost!'.format(ancient.team)
                         for ancient in self.destroyed_ancients())
예제 #9
0
파일: game.py 프로젝트: matuu/tota
class Game:
    """An instance of game controls the flow of the game.

       This includes player and creeps spawning, game main loop, deciding when
       to stop, importing map data, drawing each update, etc.
    """
    def __init__(self, radiant_heroes, dire_heroes, map_file_path, world_size,
                 debug=False, protected=False, drawers=None):
        self.radiant_heroes = radiant_heroes
        self.dire_heroes = dire_heroes
        self.map_file_path = map_file_path
        self.debug = debug
        self.protected = protected
        self.drawers = drawers or []

        self.heroes = []
        self.ancients = {}
        self.events = []

        self.world = World(world_size)

        self.initialize_world_map()
        self.cache_ancients()
        self.initialize_heroes()

    def initialize_world_map(self):
        """Read map and initialize it."""
        with open(self.map_file_path, encoding='utf-8') as map_file:
            map_text = map_file.read()
            self.world.import_map(map_text)

    def cache_ancients(self):
        """
        Find each team's ancient, and store it in a dict for faster access.
        """
        def get_ancient(team):
            ancients = [thing for thing in self.world.things.values()
                        if isinstance(thing, Ancient) and thing.team == team]
            if not ancients:
                message = "Can't find the ancient for the {} team".format(team)
                raise Exception(message)
            elif len(ancients) > 1:
                message = "Team {} has 2 ancients".format(team)
                raise Exception(message)
            else:
                return ancients[0]

        for team in (settings.TEAM_DIRE, settings.TEAM_RADIANT):
            self.ancients[team] = get_ancient(team)

    def initialize_heroes(self):
        """
        Instantiate each hero and add it to the list.
        """
        teams = {
            settings.TEAM_DIRE: self.dire_heroes,
            settings.TEAM_RADIANT: self.radiant_heroes,
        }
        for team, heroes in teams.items():
            for hero_name in heroes:
                hero_function, author = get_hero_implementation(hero_name)
                hero = Hero(name=hero_name,
                            team=team,
                            act_function=hero_function,
                            author=author)
                self.heroes.append(hero)

    def spawn_near_ancient(self, thing):
        """Spawn something near its ancient."""
        # start searching from the ancient position, outwards, until an empty
        # space is found, using breadth first graph search
        ancient = self.ancients[thing.team]
        spawn_at = closes_empty_position(ancient, self.world)
        if spawn_at:
            self.world.spawn(thing, spawn_at)
        else:
            message = "Can't spawn {} near its ancient".format(thing.name)
            raise Exception(message)

    def play(self, frames_per_second=2.0):
        """Game main loop, ending in a game result with description."""
        while True:
            # spawn creep wave
            if self.world.t % settings.CREEP_WAVE_COOLDOWN == 0:
                self.event(self, 'Spawn creep waves')
                for team in (settings.TEAM_RADIANT, settings.TEAM_DIRE):
                    for i in range(settings.CREEP_WAVE_SIZE):
                        creep = Creep(team)
                        self.spawn_near_ancient(creep)

            self.spawn_heroes()
            self.step()
            self.update_experience()
            self.clean_deads()

            self.draw()

            self.world.effects = {}

            if self.debug:
                input()
            else:
                time.sleep(1.0 / frames_per_second)

            if self.game_ended():
                description = self.game_result()
                print('')
                print(description)

                return description

    def step(self):
        """Forward one instant of time."""
        self.world.t += 1
        actions = self.get_actions()
        random.shuffle(actions)
        self.perform_actions(actions)

    def get_actions(self):
        """For each thing, call its act method to get its desired action."""
        actions = []
        actors = [thing for thing in self.world.things.values()
                  if thing.acts]
        for thing in actors:
            if thing.disabled_until > self.world.t:
                message = 'disabled until {}'.format(thing.disabled_until)
                self.event(thing, message)
            else:
                try:
                    act_result = thing.get_action(self.world.things,
                                                  self.world.t)
                    if act_result is None:
                        message = 'is idle'
                    else:
                        action, target_position = act_result
                        if action not in thing.possible_actions:
                            message = 'returned unknown action {}'.format(action)
                        else:
                            actions.append((thing, action, target_position))
                            message = 'wants to {} into {}'.format(action,
                                                                   target_position)
                        self.event(thing, message)
                except Exception as err:
                    message = 'error with act from {}: {}'.format(thing.name,
                                                                  str(err))
                    self.event(thing, message)
                    if not self.protected:
                        raise

        return actions

    def perform_actions(self, actions):
        """Execute actions, and add their results as events."""
        for thing, action, target_position in actions:
            try:
                action_function = thing.possible_actions[action]
                done, event = action_function(thing, self.world, target_position)
                if done:
                    thing.last_uses[action] = self.world.t
                thing.last_action_done = done
                self.event(thing, event)
            except Exception as err:
                message = 'error executing {} action: {}'
                self.event(thing, message.format(action, str(err)))
                if not self.protected:
                    raise

    def event(self, thing, message):
        """Log an event."""
        self.events.append((self.world.t, thing, message))

    def spawn_heroes(self):
        """Spawn heros that need to spawn (dead or start of the game)."""
        for hero in self.heroes:
            if hero.respawn_at == self.world.t:
                hero.life = hero.max_life
                self.event(hero, 'spawned')
                self.spawn_near_ancient(hero)

    def update_experience(self):
        """Add the experience gained for being close to enemy deads."""
        for thing in list(self.world.things.values()):
            if not thing.alive:
                for hero in self.heroes:
                    if hero.alive and hero.team != thing.team and distance(hero, thing) < settings.XP_DISTANCE:
                        xp_gain = 0
                        if isinstance(thing, Creep):
                            xp_gain = settings.XP_CREEP_DEAD
                        elif isinstance(thing, Hero):
                            xp_gain = settings.XP_HERO_DEAD
                        elif isinstance(thing, Tower):
                            xp_gain = settings.XP_TOWER_DEAD
                        hero.xp += xp_gain

                        self.event(hero, 'gained {} xp'.format(xp_gain))

    def clean_deads(self):
        """Remove dead things from the world."""
        for thing in list(self.world.things.values()):
            if not thing.alive:
                self.world.destroy(thing)
                self.event(thing, 'died')
                if isinstance(thing, Hero):
                    thing.respawn_at = self.world.t + settings.HERO_RESPAWN_COOLDOWN

    def draw(self):
        """Call each drawer instance."""
        for drawer in self.drawers:
            drawer.draw(self)

    def destroyed_ancients(self):
        """Which ancients have been destroyed?"""
        return [ancient for ancient in self.ancients.values()
                if not ancient.alive]

    def game_ended(self):
        """Has the game ended?"""
        return len(self.destroyed_ancients()) > 0

    def game_result(self):
        """Was the game won?"""
        return '\n'.join('Team {} lost!'.format(ancient.team)
                         for ancient in self.destroyed_ancients())
예제 #10
0
파일: game.py 프로젝트: lingsond/tota
class Game:
    """An instance of game controls the flow of the game.

       This includes player and creeps spawning, game main loop, deciding when
       to stop, importing map data, drawing each update, etc.
    """
    def __init__(self,
                 radiant_heroes,
                 dire_heroes,
                 map_file_path,
                 world_size,
                 debug=False,
                 protected=False,
                 drawers=None):
        self.radiant_heroes = radiant_heroes
        self.dire_heroes = dire_heroes
        self.map_file_path = map_file_path
        self.debug = debug
        self.protected = protected
        self.drawers = drawers or []

        self.heroes = []
        self.ancients = {}
        self.towers = {}
        self.events = []
        self.scores = defaultdict(lambda: 0)

        self.world = World(world_size)

        self.initialize_world_map()
        self.cache_structures()
        self.initialize_heroes()

    def initialize_world_map(self):
        """Read map and initialize it."""
        with open(self.map_file_path, encoding='utf-8') as map_file:
            map_text = map_file.read()
            self.world.import_map(map_text)

    def cache_structures(self):
        """
        Find each team's structures, and store it dicts for faster access.
        """
        def get_ancient(team):
            ancients = [
                thing for thing in self.world.things.values()
                if isinstance(thing, Ancient) and thing.team == team
            ]
            if not ancients:
                message = "Can't find the ancient for the {} team".format(team)
                raise Exception(message)
            elif len(ancients) > 1:
                message = "Team {} has 2 ancients".format(team)
                raise Exception(message)
            else:
                return ancients[0]

        def get_towers(team):
            return [
                thing for thing in self.world.things.values()
                if isinstance(thing, Tower) and thing.team == team
            ]

        for team in (settings.TEAM_DIRE, settings.TEAM_RADIANT):
            self.ancients[team] = get_ancient(team)
            self.towers[team] = get_towers(team)

    def initialize_heroes(self):
        """
        Instantiate each hero and add it to the list.
        """
        teams = {
            settings.TEAM_DIRE: self.dire_heroes,
            settings.TEAM_RADIANT: self.radiant_heroes,
        }
        for team, heroes in teams.items():
            for hero_name in heroes:
                hero_function, author = get_hero_implementation(hero_name)
                hero = Hero(name=hero_name,
                            team=team,
                            act_function=hero_function,
                            author=author)
                self.heroes.append(hero)

    def spawn_near_ancient(self, thing):
        """Spawn something near its ancient."""
        # start searching from the ancient position, outwards, until an empty
        # space is found, using breadth first graph search
        ancient = self.ancients[thing.team]
        spawn_at = closes_empty_position(ancient, self.world)
        if spawn_at:
            self.world.spawn(thing, spawn_at)
        else:
            message = "Can't spawn {} near its ancient".format(thing.name)
            raise Exception(message)

    def play(self, frames_per_second=2.0):
        """Game main loop, ending in a game result with description."""
        while True:
            # spawn creep wave
            if self.world.t % settings.CREEP_WAVE_COOLDOWN == 0:
                self.event(self, 'Spawn creep waves')
                for team in (settings.TEAM_RADIANT, settings.TEAM_DIRE):
                    for i in range(settings.CREEP_WAVE_SIZE):
                        creep = Creep(team)
                        self.spawn_near_ancient(creep)

            self.spawn_heroes()
            self.step()
            self.update_experience()
            self.clean_deads()

            self.draw()

            self.world.effects = {}

            if self.debug:
                input()
            else:
                time.sleep(1.0 / frames_per_second)

            if self.game_ended():
                description = self.game_result()
                print('')
                print(description)

                return description

    def step(self):
        """Forward one instant of time."""
        self.world.t += 1
        actions = self.get_actions()
        random.shuffle(actions)
        self.perform_actions(actions)

    def get_actions(self):
        """For each thing, call its act method to get its desired action."""
        actions = []
        actors = [thing for thing in self.world.things.values() if thing.acts]
        for thing in actors:
            if thing.disabled_until > self.world.t:
                message = 'disabled until {}'.format(thing.disabled_until)
                self.event(thing, message)
            else:
                try:
                    act_result = thing.get_action(self.world.things,
                                                  self.world.t)
                    if act_result is None:
                        message = 'is idle'
                    else:
                        action, target_position = act_result
                        if action not in thing.possible_actions:
                            message = (
                                'returned unknown action {}'.format(action))
                        else:
                            actions.append((thing, action, target_position))
                            message = ('wants to {} into {}'.format(
                                action, target_position))
                        self.event(thing, message)
                except Exception as err:
                    message = 'error with act from {}: {}'.format(
                        thing.name, str(err))
                    self.event(thing, message)
                    if not self.protected:
                        raise

        return actions

    def perform_actions(self, actions):
        """Execute actions, and add their results as events."""
        for thing, action, target_position in actions:
            try:
                action_function = thing.possible_actions[action]
                done, event = action_function(thing, self.world,
                                              target_position)
                if done:
                    thing.last_uses[action] = self.world.t
                thing.last_action_done = done
                self.event(thing, event)
            except Exception as err:
                message = 'error executing {} action: {}'
                self.event(thing, message.format(action, str(err)))
                if not self.protected:
                    raise

    def event(self, thing, message):
        """Log an event."""
        self.events.append((self.world.t, thing, message))

    def spawn_heroes(self):
        """Spawn heros that need to spawn (dead or start of the game)."""
        for hero in self.heroes:
            if hero.respawn_at == self.world.t:
                hero.life = hero.max_life
                self.event(hero, 'spawned')
                self.spawn_near_ancient(hero)

    def update_experience(self):
        """Add the experience gained for being close to enemy deads."""
        for thing in list(self.world.things.values()):
            if not thing.alive:
                for hero in self.heroes:
                    if (hero.alive and hero.team != thing.team
                            and distance(hero, thing) < settings.XP_DISTANCE):
                        xp_gain = 0
                        if isinstance(thing, Creep):
                            xp_gain = settings.XP_CREEP_DEAD
                        elif isinstance(thing, Hero):
                            xp_gain = settings.XP_HERO_DEAD
                        elif isinstance(thing, Tower):
                            xp_gain = settings.XP_TOWER_DEAD
                        hero.xp += xp_gain

                        self.event(hero, 'gained {} xp'.format(xp_gain))

    def clean_deads(self):
        """Remove dead things from the world."""
        for thing in list(self.world.things.values()):
            if not thing.alive:
                self.world.destroy(thing)
                self.event(thing, 'died')
                if isinstance(thing, Hero):
                    thing.respawn_at = (self.world.t +
                                        settings.HERO_RESPAWN_COOLDOWN)
                if isinstance(thing, (Hero, Tower)):
                    enemy_team = settings.ENEMY_TEAMS[thing.team]
                    self.scores[enemy_team] += 1

    def draw(self):
        """Call each drawer instance."""
        for drawer in self.drawers:
            drawer.draw(self)

    def destroyed_ancients(self):
        """Which ancients have been destroyed?"""
        return [
            ancient for ancient in self.ancients.values() if not ancient.alive
        ]

    def game_ended(self):
        """Has the game ended?"""
        return len(self.destroyed_ancients()) > 0

    def game_result(self):
        """Was the game won?"""
        return '\n'.join('Team {} lost!'.format(ancient.team)
                         for ancient in self.destroyed_ancients())