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 _pay_cost(self, purchase): for i in range(len(self.resources)): if self.resources[i] < self.costs.costs[purchase][i]: raise NotEnoughResourcesException for i in range(len(self.resources)): self.resources[i] -= self.costs.costs[purchase][i] def _schedule_next_gather_rate_decrease(self, task): if task in [Tasks.IDLE, Tasks.BUILD]: return # remove old decrease scheduled_decrease = self.rate_decrease_event_times[task] if scheduled_decrease is not None: decrease_index = None for i, event in enumerate(self.events[scheduled_decrease]): if event[0] == self._decrease_gather_rate: decrease_index = i break del self.events[scheduled_decrease][i] 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, (task,))) def _decrease_gather_rate(self, task): # assumes rate decrease should trigger; does not check new_rate = self.gather_decrease_rates[task]*self.gather_rates[task] self.gather_rates[task] = new_rate if new_rate > 0: self._schedule_next_gather_rate_decrease(task) else: self.assign_villager(task, task.IDLE, self.villager_tasks[task]) def _improve_gather_rate(self, building, tasks): if building == Buildings.FARM: raise ValueError('Do not call this routine with a farm') if tasks is None: tasks = self.gather.buildings_gather_increase[building].keys() for task in tasks: amount = self.gather.buildings_gather_increase[building][task] if self.gather.gather_rates[task] < self.gather.original_gather_rates[task]: self.gather.gather_rates[task] = self.gather.original_gather_rates[task] self.gather.gatherable_amounts[task] = amount else: self.gather.gatherable_amounts[task] += amount self._schedule_next_gather_rate_decrease(task) def _permanently_modify_farm_capacity(self, increase): current_initial_farm = self.gather.buildings_gather_increase[Buildings.FARM] for i in range(len(farms)): farms[i] += (farms[i]/current_initial_farm)*increase for i in range(len(unused_farms)): unused_farms[i] += (unused_farms[i]/current_initial_farm)*increase self.gather.buildings_gather_increase[Buildings.FARM] += increase self._schedule_next_gather_rate_decrease(Task.FARM) def _permanently_modify_gather_rate(self, task, rate_multiplier): self.gather.gather_rates[tasks] *= rate_multiplier self.gather.original_gather_rates[tasks] *= rate_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._check_gather(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_villagers_building = len(villagers_from) solo_build_time = self.build_times.build_times[building] build_time = 3*solo_build_time/(num_villagers_building + 2) 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() elif building in self.gather.buildings_gather_increase.keys(): self._improve_gather_rate(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): self._gather_resources_for_duration(end_time - self.current_time) 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._check_housing(thing) and self._check_production(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): self.production_available[building] += 1 # Start the next queued thing if one exists ready_index = None ready_thing = None for i in range(len(self.production_queue)): thing = self.production_queue[i] if self.production.building[thing] == building: ready_index = i ready_thing = thing break if ready_index: self.production_queue.remove(i) self._start_producing(thing) def _check_gather(self, task, count=1): if task in [Tasks.IDLE, Tasks.BUILD]: return required = self.gather.required_gather_buildings[task] have = set(self.buildings) match = any(map(have.issuperset, required)) if not match: raise GatherBuildingNotAvailableException if task == task.FARM: if self.villager_tasks[task] + count > len(self.unused_farms): raise GatherBuildingNotAvailableException def _check_housing(self, thing): required = self.housing.required[thing] if required > 0: pop_space = self._get_population_space() if self._get_effective_population() > pop_space + required: raise YallGotHousedException def _check_production(self, unit): building = self.production.building[unit] if self.production_available[building] < 1: raise ProductionBuildingNotAvailableException def _use_housing_and_production(self, thing): self._check_housing(thing) self._check_production(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._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 _gather_resources_for_duration(self, duration): for task, count in self.villager_tasks.items(): rate = self.gather.gather_rates[task] gathered = self.gather.gathered_resources[task] for resource, mult in gathered.items(): self.resources[resource] += count*rate*mult*duration 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._check_age_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()