488 lines
16 KiB
Python
488 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 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_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()
|
|
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()
|