from collections import defaultdict, Counter import heapq import math from aoe.exceptions import * from aoe.costs import * from aoe.dependencies import * from aoe.gather import * from aoe.housing import * from aoe.production import * from aoe.research import * from aoe.times import * from aoe.types import * class World(): # Creating a unit is split into three stages: # queuing the unit # building the unit # adding the unit to the world # # Before queuing a thing, we: # check its dependencies (research, age, etc.) # pay its cost # Once a thing is queued, it waits to be built. # # Before building a thing, we # check for housing capacity # check for available production buildings # Once a thing is building, it waits to be added. # # We assume for now that anything that has built can be added. # # Note: queuing a unit is itself an event that gets added to the queue, # and, if executed successfully, it adds another event to the queue that # actually adds the unit to the world # For now, we assume just 1 villager can work a farm at a time. # This shouldn't be a huge deal to change later. # We also assume that a player makes optimal choices about farms -- # villagers are removed from farms with the lowest capacity first and added # to farms with the highest capacity. # We keep track of only the closest upcoming farm deletion. # TODO fishing ships # TODO relics # TODO trade carts # TODO allow automation: # reseeding of farms # queuing of villagers # possibly other things # TODO gathering boars and hunting # spend villager time in exchange for adding gatherable resource # TODO food spoilage # TODO use market for buy/sell MAX_WORLD_TIME = 3*60*60 # 3 hours def add_resources(self, gathered): for i in range(len(self.resources)): self.resources[i] += gathered[i] def pay_cost(self, thing): spent = self.costs.compute_cost(thing) for i in range(len(self.resources)): if self.resources[i] < spent[i]: raise NotEnoughResourcesException for i in range(len(self.resources)): self.resources[i] -= spent[i] def _remove_event_at_time(self, removed_event, scheduled_time): if not scheduled_time: return for i, event in enumerate(self.events[scheduled_time]): if event[0] == removed_event: del self.events[scheduled_time][i] break def _schedule_next_gather_rate_decrease(self, task): if task in [Tasks.IDLE, Tasks.BUILD]: return # remove old decrease self._remove_event_at_time(self._decrease_gather_rate_by_overuse, self.rate_decrease_event_times[task]) rate = self.gather.gather_rates[task] if task == Tasks.FARM: available = self.farms[0] villagers = min(self.villager_tasks[task], 1) else: available = self.gather.gatherable_amounts[task] villagers = self.villager_tasks[task] try: next_decrease = self.current_time + available/(villagers*rate) except ZeroDivisionError: # either villagers or rate was 0, so no next decrease should # be scheduled self.rate_decrease_event_times[task] = None else: # rounding error will give 1 extra second of gathering next_decrease = math.ceil(self.current_time + next_decrease) self.rate_decrease_event_times[task] = next_decrease if task == Tasks.FARM: self.events[next_decrease].append((self._remove_farm, ())) else: self.events[next_decrease].append((self._decrease_gather_rate_by_overuse, (task,))) def _decrease_gather_rate_by_overuse(self, task): new_rate = self.gather.decrease_gather_rate_by_overuse(task) if new_rate > 0: self._schedule_next_gather_rate_decrease(task) else: self.assign_villager(task, Tasks.IDLE, self.villager_tasks[task]) def _improve_gather_rate_by_building(self, building, tasks): tasks_improved = self.gather.improve_gather_rate_by_building(building, tasks) for task in tasks_improved: self._schedule_next_gather_rate_decrease(task) def permanently_modify_gather_rate(self, task, multiplier): self.gather.permanently_modify_gather_rate(task, multiplier) self._schedule_next_gather_rate_decrease(task) def permanently_modify_farm_capacity(self, increase): self._permanently_modify_consumable_capacity(Buildings.FARM, Tasks.FARM, increase) def _permanently_modify_consumable_capacity(self, building, task, increase): multiplier = self.gather.permanently_modify_consumable_capacity(building, task, increase) if building == Buildings.FARM: used = self.farms unused = self.unused_farms for i in range(len(used)): used[i] += used[i]*multiplier for i in range(len(unused)): unused[i] += unused[i]*multiplier self._schedule_next_gather_rate_decrease(task) def assign_villager(self, previous_task, new_task, count=1): # new_task of None means kill the villager # villager_tasks must be modified only in this function # if it is changed elsewhere, the rate-decrease schedule will be wrong if self.villager_tasks[previous_task] < count: raise IllegalVillagerMoveException if new_task: self.gather.check_gather(self, new_task, count) self.villager_tasks[new_task] += count else: self.villagers -= 1 self.villager_tasks[previous_task] -= count if previous_task == Tasks.FARM: # remove villagers from farms with lowest remaining capacity for i in range(count): heapq.heappush_max(self.unused_farms, heapq.heappop(self.farms)) if new_task == Tasks.FARM: # add villagers to farms with highest remaining capacity for i in range(count): heapq.heappush(self.farms, heapq.heappop_max(self.unused_farms)) self._schedule_next_gather_rate_decrease(previous_task) if new_task: self._schedule_next_gather_rate_decrease(new_task) def _check_villager_move(self, villagers_from_count): for task, count in villagers_from_count.items(): if self.villager_tasks[task] < count: raise IllegalVillagerMoveException def _move_villagers_to_build(self, villagers_from_count): for task, count in villagers_from_count.items(): self.assign_villager(task, Tasks.BUILD, count) def build(self, building, villagers_from, villagers_to, building_tasks=None): if type(villagers_from) == Tasks: villagers_from = [villagers_from] if type(villagers_to) == Tasks: villagers_to = [villagers_to] if len(villagers_from) != len(villagers_to): raise ConservationOfVillagersException villagers_from_count = Counter(villagers_from) self._check_villager_move(villagers_from_count) self.dependencies.check_dependencies(self, building) self.pay_cost(building) self._move_villagers_to_build(villagers_from_count) villagers_to_count = Counter(villagers_to) num_builders = len(villagers_from) build_time = self.build_times.compute_build_time(building, num_builders) update_time = self.current_time + build_time self.events[update_time].append((self._add_building, (building, villagers_to_count, building_tasks,))) def _add_building(self, building, villagers_to, tasks_improved): self.buildings.append(building) self._free_production_building(building) if building == Buildings.HOUSE: self._free_housing() if building == Buildings.FARM: self._add_farm() else: self._improve_gather_rate_by_building(building, tasks_improved) for task, count in villagers_to.items(): self.assign_villager(Tasks.BUILD, task, count) def _add_farm(self): self.buildings.append(Buildings.FARM) new_farm_value = self.gather.buildings_gather_increase[Buildings.FARM][Tasks.FARM] heapq.heappush(self.unused_farms, new_farm_value) def _remove_farm(self): self.buildings.remove(Buildings.FARM) heapq.heappop(self.farms) if self.villager_tasks[Tasks.FARM] > len(self.farms): self.assign_villager(Tasks.FARM, Tasks.IDLE) def advance_by_time(self, duration): self._run_clock_until(self.time + duration) def advance_until_time(self, end_time): self._run_clock_until_time(end_time) def _get_next_event_time(self): try: next_event_time = min(self.events.keys()) except ValueError: next_event_time = World.MAX_WORLD_TIME return next_event_time def _run_clock_without_events(self, end_time): duration = end_time - self.current_time gathered = self.gather.compute_gathered_resources_for_duration(self.villager_tasks, duration) self.add_resources(gathered) self.current_time = end_time def _run_clock_until_time(self, end_time): while self.current_time < end_time: next_event_time = self._get_next_event_time() self._run_clock_without_events(min(next_event_time, end_time)) self._handle_events() def get_population_space(self): space = sum([self.housing.space[building] for building in self.buildings]) return min(space, 200) def get_effective_population(self): return self.population + self.units_being_built def _free_housing(self): pop_space = self.get_population_space() i = 0 while (self.get_effective_population() < pop_space and i < len(self.production_queue)): thing = self.production_queue[i] if self.housing.check_housing(self, thing) and \ self.production.check_production(self, thing): self._start_producing(thing) del self.production_queue[i] else: i += 1 def _free_production_building_of_thing(self, thing): building = self.production.building[thing] self._free_production_building(building) def _free_production_building(self, building): # This will not respect the order of a queue in a particular building. # For example, if there is a TC with the queue [villager, loom] and # there is no available housing, the TC will research loom. In the # actual game, the villager would block the researching of loom. # There is no easy way around this as long as we are treating the # production queue as a shared pool, and I don't really care. self.production_available[building] += 1 i = 0 while i < len(self.production_queue): thing = self.production_queue[i] if self.production.building[thing] == building and \ self.housing.check_housing(self, thing): self._start_producing(thing) del self.production_queue[i] break i += 1 def _use_housing_and_production(self, thing): self.housing.check_housing(self, thing) self.production.check_production(self, thing) building = self.production.building[thing] if building is not None: self.production_available[building] -= 1 self.units_being_built += self.housing.required[thing] def _kill_unit(self, unit, task=None): if unit == Units.VILLAGER and not task: raise IllegalVillagerMoveException self.population -= self.housing.required(unit) if unit == Units.VILLAGER: self.assign_villager(task, None) self._free_housing() def _add_thing(self, thing, initial_task): self.units_being_built -= self.housing.required[thing] self.population += self.housing.required[thing] if thing in Research: self.research.append(thing) ResearchEffects.complete_research(self, thing) self._free_production_building_of_thing(thing) if thing == Units.VILLAGER: self.villagers += 1 self.villager_tasks[Tasks.IDLE] += 1 self.assign_villager(Tasks.IDLE, initial_task) def _start_producing(self, thing, initial_task): try: self._use_housing_and_production(thing) except ProductionBuildingNotAvailableException: self.production_queue.append(thing) else: update_time = self.current_time + self.build_times.build_times[thing] self.events[update_time].append((self._add_thing, (thing, initial_task,))) def _queue_thing(self, thing, initial_task=None): self.dependencies.check_dependencies(self, thing) self.costs.compute_cost(thing) self.pay_cost(thing) self._start_producing(thing, initial_task) def queue_research(self, research): self._queue_thing(research) def queue_unit(self, unit): self._queue_thing(unit) def queue_villager(self, initial_task): self._queue_thing(Units.VILLAGER, initial_task) def _handle_events(self): for func, args in self.events[self.current_time]: func(*args) del self.events[self.current_time] def _update_age(self, new_age): self.age = new_age def increase_age(self): if self.age == Ages.IMPERIAL: raise Exception("no.") new_age = self.age + 1 self.dependencies.check_dependencies(new_age) self.pay_cost(new_age) update_time = self.current_time + self.build_times.build_times[new_age] self.events[update_time].append((self._update_age, (new_age,))) def _deal_with_civ(self): pass # deal with any civ-specific changes def __init__(self, civ=None): self.civ = civ self.age = Ages.DARK self.villagers = 3 self.population = 4 self.resources = [200, 200, 100, 200] self.research = [] self.buildings = [Buildings.TOWNCENTER] self.farms = [] self.unused_farms = [] self.villager_tasks = {t: 0 for t in Tasks} self.villager_tasks[Tasks.IDLE] = self.villagers self.rate_decrease_event_times = {t: None for t in Tasks} self.units_being_built = 0 self.production_available = {b: 0 for b in Buildings} self.production_available[Buildings.TOWNCENTER] = 1 self.production_queue = [] self.current_time = 0 self.events = defaultdict(list) self.build_times = Times() self.costs = Costs() self.dependencies = Dependencies() self.housing = Housing() self.production = Production() self.gather = Gather() self._deal_with_civ()