Files
aoe2-resource-simulator/aoe/world.py
2026-07-29 20:18:05 -04:00

509 lines
16 KiB
Python

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()