Package horizons :: Package ai :: Package aiplayer :: Package goal :: Module improvecollectorcoverage
[hide private]
[frames] | no frames]

Source Code for Module horizons.ai.aiplayer.goal.improvecollectorcoverage

  1  # ################################################### 
  2  # Copyright (C) 2008-2017 The Unknown Horizons Team 
  3  # team@unknown-horizons.org 
  4  # This file is part of Unknown Horizons. 
  5  # 
  6  # Unknown Horizons is free software; you can redistribute it and/or modify 
  7  # it under the terms of the GNU General Public License as published by 
  8  # the Free Software Foundation; either version 2 of the License, or 
  9  # (at your option) any later version. 
 10  # 
 11  # This program is distributed in the hope that it will be useful, 
 12  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
 13  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 14  # GNU General Public License for more details. 
 15  # 
 16  # You should have received a copy of the GNU General Public License 
 17  # along with this program; if not, write to the 
 18  # Free Software Foundation, Inc., 
 19  # 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA 
 20  # ################################################### 
 21   
 22  from collections import deque 
 23   
 24  from horizons.ai.aiplayer.basicbuilder import BasicBuilder 
 25  from horizons.ai.aiplayer.constants import BUILD_RESULT, BUILDING_PURPOSE 
 26  from horizons.ai.aiplayer.goal.settlementgoal import SettlementGoal 
 27  from horizons.ai.aiplayer.roadplanner import RoadPlanner 
 28  from horizons.component.storagecomponent import StorageComponent 
 29  from horizons.constants import BUILDINGS, PRODUCTION, RES 
 30  from horizons.entities import Entities 
 31  from horizons.scheduler import Scheduler 
 32  from horizons.util.shapes import Rect 
 33  from horizons.world.production.producer import Producer 
34 35 36 -class ImproveCollectorCoverageGoal(SettlementGoal):
37 - def get_personality_name(self):
38 return 'ImproveCollectorCoverageGoal'
39 40 @property
41 - def active(self):
42 return super().active and self._is_active
43
45 problematic_buildings = {} 46 for building in self.production_builder.production_buildings: 47 for production in building.get_component(Producer).get_productions(): 48 if production.get_age() < 1.5 * PRODUCTION.STATISTICAL_WINDOW: 49 continue 50 history = production.get_state_history_times(False) 51 # take paused time into account because the AI pauses the production when the output storage is full 52 amount_paused = history[PRODUCTION.STATES.inventory_full.index] + history[PRODUCTION.STATES.paused.index] 53 if amount_paused < self.personality.min_bad_collector_coverage: 54 continue 55 for resource_id in production.get_produced_resources(): 56 if self.settlement.get_component(StorageComponent).inventory.get_free_space_for(resource_id) > self.personality.min_free_space: 57 # this is actually problematic 58 problematic_buildings[building.worldid] = building 59 return list(problematic_buildings.values())
60
61 - def update(self):
62 if self.production_builder.last_collector_improvement_road + self.personality.collector_improvement_road_expires > Scheduler().cur_tick: 63 # skip this goal leave time for the collectors to do their work 64 self._problematic_buildings = None 65 self._is_active = False 66 else: 67 self._problematic_buildings = self._get_problematic_collector_coverage_buildings() 68 self._is_active = bool(self._problematic_buildings)
69
70 - def _build_extra_road_connection(self, building, collector_building):
71 collector_coords = {coords for coords in self.production_builder.iter_possible_road_coords(collector_building.position, collector_building.position)} 72 destination_coords = {coords for coords in self.production_builder.iter_possible_road_coords(building.loading_area, building.position)} 73 pos = building.loading_area 74 beacon = Rect.init_from_borders(pos.left - 1, pos.top - 1, pos.right + 1, pos.bottom + 1) 75 76 path = RoadPlanner()(self.owner.personality_manager.get('RoadPlanner'), collector_coords, 77 destination_coords, beacon, self.production_builder.get_path_nodes()) 78 if path is None: 79 return BUILD_RESULT.IMPOSSIBLE 80 81 cost = self.production_builder.get_road_cost(path) 82 for resource_id, amount in cost.items(): 83 if resource_id == RES.GOLD: 84 if self.owner.get_component(StorageComponent).inventory[resource_id] < amount: 85 return BUILD_RESULT.NEED_RESOURCES 86 elif self.settlement.get_component(StorageComponent).inventory[resource_id] < amount: 87 return BUILD_RESULT.NEED_RESOURCES 88 return BUILD_RESULT.OK if self.production_builder.build_road(path) else BUILD_RESULT.UNKNOWN_ERROR
89
90 - def _build_extra_road(self):
91 """Build an extra road between a storage building and a producer building.""" 92 current_tick = Scheduler().cur_tick 93 94 # which collectors could have actual unused capacity? 95 usable_collectors = [] 96 for building in self.production_builder.collector_buildings: 97 if building.get_utilization_history_length() < 1000 or building.get_collector_utilization() < self.personality.max_good_collector_utilization: 98 usable_collectors.append(building) 99 100 # find possible problematic building to usable collector links 101 potential_road_connections = [] 102 for building in self._problematic_buildings: 103 for collector_building in usable_collectors: 104 distance = building.loading_area.distance(collector_building.position) 105 if distance > collector_building.radius: 106 continue # out of range anyway 107 # TODO: check whether the link already exists 108 potential_road_connections.append((distance * collector_building.get_collector_utilization(), building, collector_building)) 109 110 # try the best link from the above list 111 for _, building, collector_building in sorted(potential_road_connections): 112 result = self._build_extra_road_connection(building, collector_building) 113 if result == BUILD_RESULT.OK: 114 self.production_builder.last_collector_improvement_road = current_tick 115 self.log.info('%s connected %s at %d, %d with %s at %d, %d', self, building.name, building.position.origin.x, 116 building.position.origin.y, collector_building.name, collector_building.position.origin.x, collector_building.position.origin.y) 117 return result 118 self.log.info('%s found no good way to connect buildings that need more collectors to existing collector buildings', self) 119 return BUILD_RESULT.IMPOSSIBLE
120
121 - def _build_extra_storage(self):
122 """Build an extra storage tent to improve collector coverage.""" 123 if not self.production_builder.have_resources(BUILDINGS.STORAGE): 124 return BUILD_RESULT.NEED_RESOURCES 125 126 reachable = dict.fromkeys(self.land_manager.roads) # {(x, y): [(building worldid, distance), ...], ...} 127 for coords, (purpose, _) in self.production_builder.plan.items(): 128 if purpose == BUILDING_PURPOSE.NONE: 129 reachable[coords] = [] 130 for key in reachable: 131 if reachable[key] is None: 132 reachable[key] = [] 133 134 storage_radius = Entities.buildings[BUILDINGS.STORAGE].radius 135 moves = [(-1, 0), (0, -1), (0, 1), (1, 0)] 136 for building in self._problematic_buildings: 137 distance = dict.fromkeys(reachable) 138 queue = deque() 139 for coords in self.production_builder.iter_possible_road_coords(building.loading_area, building.position): 140 if coords in distance: 141 distance[coords] = 0 142 queue.append(coords) 143 144 while queue: 145 x, y = queue.popleft() 146 for dx, dy in moves: 147 coords2 = (x + dx, y + dy) 148 if coords2 in distance and distance[coords2] is None: 149 distance[coords2] = distance[(x, y)] + 1 150 queue.append(coords2) 151 152 for coords, dist in distance.items(): 153 if dist is not None: 154 if building.loading_area.distance(coords) <= storage_radius: 155 reachable[coords].append((building.worldid, dist)) 156 157 options = [] 158 storage_class = Entities.buildings[BUILDINGS.STORAGE] 159 storage_spots = self.island.terrain_cache.get_buildability_intersection(storage_class.terrain_type, 160 storage_class.size, self.settlement.buildability_cache, self.production_builder.buildability_cache) 161 for coords, building_distances in reachable.items(): 162 if coords not in storage_spots: 163 continue 164 builder = BasicBuilder.create(BUILDINGS.STORAGE, coords, 0) 165 166 actual_distance = {} 167 for coords in builder.position.tuple_iter(): 168 for building_worldid, distance in reachable[coords]: 169 if building_worldid not in actual_distance or actual_distance[building_worldid] > distance: 170 actual_distance[building_worldid] = distance 171 if not actual_distance: 172 continue 173 174 usefulness = min(len(actual_distance), self.personality.max_reasonably_served_buildings) 175 for distance in actual_distance.values(): 176 usefulness += 1.0 / (distance + self.personality.collector_extra_distance) 177 178 alignment = 1 179 for tile in self.production_builder.iter_neighbor_tiles(builder.position): 180 coords = (tile.x, tile.y) 181 if coords not in self.production_builder.plan or self.production_builder.plan[coords][0] != BUILDING_PURPOSE.NONE: 182 alignment += 1 183 184 value = usefulness + alignment * self.personality.alignment_coefficient 185 options.append((value, builder)) 186 187 return self.production_builder.build_best_option(options, BUILDING_PURPOSE.STORAGE)
188
189 - def execute(self):
190 result = self._build_extra_road() 191 if result == BUILD_RESULT.IMPOSSIBLE: 192 if self.production_builder.last_collector_improvement_storage + self.personality.collector_improvement_storage_expires <= Scheduler().cur_tick: 193 result = self._build_extra_storage() 194 if result == BUILD_RESULT.OK: 195 self.production_builder.last_collector_improvement_storage = Scheduler().cur_tick 196 self._log_generic_build_result(result, 'storage') 197 return self._translate_build_result(result)
198