It might make logical sense. Honestly, I'm just tired of the code for World being so long.
478 lines
16 KiB
Python
478 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.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()
|
|
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.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):
|
|
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 _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._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()
|