Initial commit

This commit is contained in:
Nate Bowman
2026-07-26 20:26:36 -04:00
commit 4a014aeb50
12 changed files with 1058 additions and 0 deletions

46
aoe/costs.py Normal file
View File

@@ -0,0 +1,46 @@
from aoe.types import *
class Costs():
def __init__(self):
self.costs = {
Ages.DARK: (0, 0, 0, 0),
Ages.FEUDAL: (500, 0, 0, 0),
Ages.CASTLE: (800, 0, 200, 0),
Ages.IMPERIAL: (1000, 0, 800, 0),
Buildings.HOUSE: (0, 25, 0, 0),
Buildings.LUMBERCAMP: (0, 100, 0, 0),
Buildings.MILL: (0, 100, 0, 0),
Buildings.FARM: (0, 60, 0, 0),
Buildings.MININGCAMP: (0, 100, 0, 0),
Buildings.DOCK: (0, 150, 0, 0),
Buildings.BARRACKS: (0, 175, 0, 0),
Buildings.PALISADEWALL: (0, 3, 0, 0),
Buildings.PALISADEGATE: (0, 30, 0, 0),
Buildings.TOWNCENTER: (0, 275, 0, 100),
Buildings.BLACKSMITH: (0, 200, 0, 0),
Buildings.WATCHTOWER: (0, 35, 0, 125),
Buildings.STONEWALL: (0, 0, 0, 5),
Buildings.GATE: (0, 25, 0, 0),
Buildings.DONJON: (0, 50, 0, 175),
Buildings.FISHTRAP: (0, 100, 0, 0),
Buildings.MARKET: (0, 175, 0, 0),
Buildings.ARCHERYRANGE: (0, 175, 0, 0),
Buildings.STABLE: (0, 175, 0, 0),
Units.VILLAGER: (50, 0, 0, 0),
Units.TRADECART: (0, 100, 50, 0),
Units.SPEARMAN: (35, 25, 0, 0),
Units.MILITIA: (60, 0, 20, 0),
Units.SCOUT: (80, 0, 0, 0),
Units.KNIGHT: (60, 0, 75, 0),
Units.SKIRMISHER: (25, 35, 0, 0),
Units.ARCHER: (0, 25, 45, 0),
}
#def update_research(self, research):
#case research:
#match Bowsaw:

96
aoe/dependencies.py Normal file
View File

@@ -0,0 +1,96 @@
from aoe.types import *
class BuildingDependencies():
def __init__(self):
self.dependencies = {
Buildings.HOUSE: [Ages.DARK],
Buildings.LUMBERCAMP: [Ages.DARK],
Buildings.MILL: [Ages.DARK],
Buildings.FARM: [Ages.DARK, Buildings.MILL],
Buildings.MININGCAMP: [Ages.DARK],
Buildings.DOCK: [Ages.DARK],
Buildings.BARRACKS: [Ages.DARK],
Buildings.PALISADEWALL: [Ages.DARK],
Buildings.PALISADEGATE: [Ages.DARK],
Buildings.TOWNCENTER: [Ages.DARK],
Buildings.BLACKSMITH: [Ages.FEUDAL],
Buildings.WATCHTOWER: [Ages.FEUDAL],
Buildings.STONEWALL: [Ages.FEUDAL],
Buildings.GATE: [Ages.FEUDAL],
Buildings.DONJON: [Ages.FEUDAL],
Buildings.FISHTRAP: [Ages.FEUDAL],
Buildings.MARKET: [Ages.FEUDAL, Buildings.MILL],
Buildings.ARCHERYRANGE: [Ages.FEUDAL, Buildings.BARRACKS],
Buildings.STABLE: [Ages.FEUDAL, Buildings.BARRACKS],
}
class AgeDependencies():
def __init__(self):
self.dependencies = {
Ages.FEUDAL: {
Buildings.BARRACKS: 1,
Buildings.MILL: 1,
Buildings.LUMBERCAMP: 1,
Buildings.MININGCAMP: 1,
Buildings.DOCK: 1,
#Buildings.MULECART: 1,
#Buildings.FOLWARK: 1,
#Buildings.SETTLEMENT: 2,
},
Ages.CASTLE: {
Buildings.ARCHERYRANGE: 1,
Buildings.STABLE: 1,
Buildings.MARKET: 1,
Buildings.BLACKSMITH: 1,
#Buildings.SIEGEWORKSHOP: 1,
},
Ages.IMPERIAL: {
#Buildings.MONASTERY: 1,
#Buildings.FORTIFIEDCHURCH: 1,
#Buildings.UNIVERSITY: 1,
#Buildings.SIEGEWORKSHOP: 1,
#Buildings.CASTLE: 2,
#Buildings.KREPOST: 2,
},
}
self.dependencies_counts = {
Ages.FEUDAL: 2,
Ages.CASTLE: 2,
Ages.IMPERIAL: 2,
}
class ResearchDependencies():
def __init__(self):
self.dependencies = {
Research.DOUBLEBITAXE: [Ages.FEUDAL, Buildings.LUMBERCAMP],
Research.BOWSAW: [Ages.CASTLE, Buildings.LUMBERCAMP, Research.DOUBLEBITAXE],
Research.TWOMANSAW: [Ages.IMPERIAL, Buildings.LUMBERCAMP, Research.BOWSAW],
Research.HORSECOLLAR: [Ages.FEUDAL, Buildings.MILL],
Research.HEAVYPLOW: [Ages.CASTLE, Buildings.MILL, Research.HORSECOLLAR],
Research.CROPCYCLE: [Ages.IMPERIAL, Buildings.MILL, Research.HEAVYPLOW],
Research.GOLDMINING: [Ages.FEUDAL, Buildings.MININGCAMP],
Research.GOLDSHAFTMINING: [Ages.CASTLE, Buildings.MININGCAMP, Research.GOLDMINING],
Research.STONEMINING: [Ages.FEUDAL, Buildings.MININGCAMP],
Research.STONESHAFTMINING: [Ages.CASTLE, Buildings.MININGCAMP, Research.STONEMINING],
}
class UnitDependencies():
def __init__(self):
self.dependencies = {
Units.VILLAGER: [Buildings.TOWNCENTER],
Units.TRADECART: [Buildings.MARKET],
Units.SPEARMAN: [Ages.FEUDAL, Buildings.BARRACKS],
Units.MILITIA: [Buildings.BARRACKS],
Units.SCOUT: [Ages.FEUDAL, Buildings.STABLE],
Units.KNIGHT: [Ages.CASTLE, Buildings.STABLE],
Units.SKIRMISHER: [Ages.FEUDAL, Buildings.ARCHERYRANGE],
Units.ARCHER: [Ages.FEUDAL, Buildings.ARCHERYRANGE],
}

37
aoe/exceptions.py Normal file
View File

@@ -0,0 +1,37 @@
class CannotChangeThePastException(Exception):
pass
class ConservationOfVillagersException(Exception):
pass
class GatherBuildingNotAvailableException(Exception):
pass
class IllegalVillagerMoveException(Exception):
pass
class MissingDependencyException(Exception):
pass
class NotEnoughResourcesException(Exception):
pass
class ProductionBuildingNotAvailableException(Exception):
pass
class YallGotHousedException(Exception):
pass

85
aoe/gameplay.py Normal file
View File

@@ -0,0 +1,85 @@
from collections import defaultdict
from aoe.exceptions import *
from aoe.types import *
from aoe.world import *
class Gameplay():
def print_status(self):
print(f'Current time: {self.world.current_time}')
print()
print(f'Civ: {self.world.civ}')
print(f'Age: {self.world.age}')
print(f'Population: {self.world.population}')
print()
print(f'Villagers: {self.world.villagers}')
print(f'Tasks: {[(t, c) for t, c in self.world.villager_tasks.items() if c != 0]}')
print()
print(f'Resources: {[int(r) for r in self.world.resources]}')
print()
print(f'Buildings: {self.world.buildings}')
print()
print(f'Research: {self.world.research}')
def build(self, action_time, building, villagers_from, villagers_to):
self._check_time(action_time)
self.actions[action_time].append((self.world.build,
(building,
villagers_from,
villagers_to,)))
def queue_villager(self, action_time, initial_task):
self._check_time(action_time)
self.actions[action_time].append((self.world.queue_villager,
(initial_task,)))
def assign_villager(self, action_time, old_task, new_task, count=1):
self._check_time(action_time)
self.actions[action_time].append((self.world.assign_villager,
(old_task, new_task, count,)))
def _check_time(self, action_time):
# TODO will it work if we do something at current time?
if self.world.current_time > action_time:
raise CannotChangeThePastException
def _get_next_action_time(self):
try:
next_action_time = min(self.actions.keys())
except ValueError:
next_action_time = self.end_time
return next_action_time
def _play_without_actions(self, end_time):
self.world.advance_until_time(end_time)
def _handle_actions(self):
for func, args in self.actions[self.world.current_time]:
func(*args)
del self.actions[self.world.current_time]
def play_until(self, end_time):
self.end_time = end_time
while self.world.current_time < self.end_time:
next_action_time = self._get_next_action_time()
self._play_without_actions(min(next_action_time, self.end_time))
self._handle_actions()
def __init__(self):
self.world = World()
self.actions = defaultdict(list)
self.end_time = 0

59
aoe/gather.py Normal file
View File

@@ -0,0 +1,59 @@
from aoe.types import *
class Gather():
def __init__(self):
self.gather_rates = {
Tasks.BERRIES: {Resources.FOOD: 18.6/60},
Tasks.SHEEP: {Resources.FOOD: 19.8/60},
Tasks.BOAR: {Resources.FOOD: 24.6/60},
Tasks.HUNT: {Resources.FOOD: 24.6/60},
Tasks.FARM: {Resources.FOOD: 20/60},
Tasks.FISH_VILLAGER: {Resources.FOOD: 25.8/60},
Tasks.FISH_SHIP_SHORE: {Resources.FOOD: 16.8/60},
Tasks.FISH_SHIP_DEEP: {Resources.FOOD: 29.4/60},
Tasks.FISHTRAP: {Resources.FOOD: 21/60},
Tasks.WOOD: {Resources.WOOD: 23.4/60},
Tasks.GOLD: {Resources.GOLD: 22.8/60},
Tasks.TRADECART: {Resources.GOLD: 25.9/60},
Tasks.TRADECOG: {Resources.GOLD: 25.9/60},
Tasks.RELIC: {Resources.GOLD: 30/60},
Tasks.STONE: {Resources.STONE: 21.6/60},
Tasks.BUILD: {},
Tasks.IDLE: {},
}
self.required_gather_buildings = {
Tasks.BERRIES: [set([Buildings.MILL]),
set([Buildings.TOWNCENTER])],
Tasks.SHEEP: [set([Buildings.MILL]),
set([Buildings.TOWNCENTER])],
Tasks.BOAR: [set([Buildings.MILL]),
set([Buildings.TOWNCENTER])],
Tasks.HUNT: [set([Buildings.MILL]),
set([Buildings.TOWNCENTER])],
Tasks.FARM: [set([Buildings.FARM, Buildings.MILL]),
set([Buildings.FARM, Buildings.TOWNCENTER])],
Tasks.FISH_VILLAGER: [set([Buildings.DOCK]),
set([Buildings.MILL]),
set([Buildings.TOWNCENTER])],
Tasks.FISH_SHIP_SHORE: [set([Buildings.DOCK])],
Tasks.FISH_SHIP_DEEP: [set([Buildings.DOCK])],
Tasks.FISHTRAP: [set([Buildings.DOCK, Buildings.FISHTRAP])],
Tasks.WOOD: [set([Buildings.LUMBERCAMP]),
set([Buildings.TOWNCENTER])],
Tasks.GOLD: [set([Buildings.MININGCAMP]),
set([Buildings.TOWNCENTER])],
Tasks.TRADECART: [set([Buildings.MARKET])],
Tasks.TRADECOG: [set([Buildings.DOCK])],
#Tasks.RELIC: [set([Buildings.MONASTERY])],
Tasks.STONE: [set([Buildings.MININGCAMP]),
set([Buildings.TOWNCENTER])],
Tasks.BUILD: [],
Tasks.IDLE: [],
}
self.improved_gather_buildings = {}

14
aoe/housing.py Normal file
View File

@@ -0,0 +1,14 @@
from collections import defaultdict
from aoe.types import *
class Housing():
def __init__(self):
self.space = defaultdict(int, {
Buildings.HOUSE: 5,
Buildings.TOWNCENTER: 5,
})

19
aoe/production.py Normal file
View File

@@ -0,0 +1,19 @@
from aoe.types import *
class Production():
# assumes for now each unit can be built in only one type of building
def __init__(self):
self.building = {
Units.VILLAGER: Buildings.TOWNCENTER,
Units.TRADECART: Buildings.MARKET,
Units.SPEARMAN: Buildings.BARRACKS,
Units.MILITIA: Buildings.BARRACKS,
Units.SCOUT: Buildings.STABLE,
Units.KNIGHT: Buildings.STABLE,
Units.SKIRMISHER: Buildings.ARCHERYRANGE,
Units.ARCHER: Buildings.ARCHERYRANGE,
}

45
aoe/times.py Normal file
View File

@@ -0,0 +1,45 @@
from aoe.types import *
class Times():
def __init__(self):
self.build_times = {
Ages.FEUDAL: 130,
Ages.CASTLE: 160,
Ages.IMPERIAL: 190,
Buildings.HOUSE: 25,
Buildings.LUMBERCAMP: 35,
Buildings.MILL: 35,
Buildings.FARM: 15,
Buildings.MININGCAMP: 35,
Buildings.DOCK: 35,
Buildings.BARRACKS: 50,
Buildings.PALISADEWALL: 7,
Buildings.PALISADEGATE: 30,
Buildings.TOWNCENTER: 150,
Buildings.BLACKSMITH: 40,
Buildings.WATCHTOWER: 80,
Buildings.STONEWALL: 10,
Buildings.GATE: 70,
Buildings.DONJON: 83,
Buildings.FISHTRAP: 40,
Buildings.MARKET: 60,
Buildings.ARCHERYRANGE: 50,
Buildings.STABLE: 50,
Units.VILLAGER: 25,
Units.TRADECART: 50,
Units.SPEARMAN: 22,
Units.MILITIA: 21,
Units.SCOUT: 30,
Units.KNIGHT: 30,
Units.SKIRMISHER: 22,
Units.ARCHER: 35,
}
self.exist_duration = {
Buildings.FARM: 0,
Buildings.FISHTRAP: 0,
}

95
aoe/types.py Normal file
View File

@@ -0,0 +1,95 @@
from enum import Enum, IntEnum, auto
class Ages(IntEnum):
DARK = 0
FEUDAL = 1
CASTLE = 2
IMPERIAL = 3
class Buildings(Enum):
HOUSE = auto()
LUMBERCAMP = auto()
MILL = auto()
FARM = auto()
MININGCAMP = auto()
DOCK = auto()
BARRACKS = auto()
STABLE = auto()
ARCHERYRANGE = auto()
PALISADEWALL = auto()
PALISADEGATE = auto()
TOWNCENTER = auto()
BLACKSMITH = auto()
WATCHTOWER = auto()
STONEWALL = auto()
GATE = auto()
DONJON = auto()
FISHTRAP = auto()
MARKET = auto()
class Tasks(Enum):
BERRIES = auto()
SHEEP = auto()
BOAR = auto()
HUNT = auto()
FARM = auto()
FISH_VILLAGER = auto()
FISH_SHIP_SHORE = auto()
FISH_SHIP_DEEP = auto()
FISHTRAP = auto()
WOOD = auto()
GOLD = auto()
RELIC = auto()
TRADECART = auto()
TRADECOG = auto()
STONE = auto()
BUILD = auto()
IDLE = auto()
class Research(Enum):
DOUBLEBITAXE = auto()
BOWSAW = auto()
TWOMANSAW = auto()
HORSECOLLAR = auto()
HEAVYPLOW = auto()
CROPCYCLE = auto()
GOLDMINING = auto()
GOLDSHAFTMINING = auto()
STONEMINING = auto()
STONESHAFTMINING = auto()
'''
BLACKSMITH = 11
WATCHTOWER = 12
STONEWALL = 13
GATE = 14
DONJON = 15
FISHTRAP = 16
'''
class Resources(IntEnum):
FOOD = 0
WOOD = 1
GOLD = 2
STONE = 3
class Units(Enum):
VILLAGER = auto()
TRADECART = auto()
SPEARMAN = auto()
MILITIA = auto()
SCOUT = auto()
KNIGHT = auto()
SKIRMISHER = auto()
ARCHER = auto()

425
aoe/world.py Normal file
View File

@@ -0,0 +1,425 @@
from collections import defaultdict, Counter
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.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
# TODO farms, reseeding, etc.
# _check_resource_building, _use_resource_building, _free_resource_building
# TODO sheep should die like farms
# TODO need some warning, penalty, etc. if villager is assigned to task
# without appropriate building (e.g., mining with no camp)
# TODO need efficiency penalties on, e.g., lumber camp
# too many vils/camp decreases rate
# "stale" (old) camp decreases rate
# TODO research
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 _check_research_dependencies(self, research):
self._check_dependencies(self.research_deps.dependencies[research])
def _check_building_dependencies(self, building):
self._check_dependencies(self.building_deps.dependencies[building])
def _check_unit_dependencies(self, unit):
self._check_dependencies(self.unit_deps.dependencies[unit])
def _check_dependencies(self, dependencies):
for dep in dependencies:
if dep not in self.buildings and \
dep not in self.research and \
not self._is_sufficient_age(dep):
raise MissingDependencyException
def _is_sufficient_age(self, age):
if type(age) == Ages and self.age >= age:
return True
return False
def _check_age_dependencies(self, age):
total_buildings = 0
for building in self.age_deps.dependencies[age].keys():
if self.has_building(building):
total_buildings += self.age_deps.dependencies[age][building]
if total_buildings < self.age_deps.dependencies_counts[age]:
raise MissingDependencyException
def buy_research(self, research):
self._check_research_dependencies(research)
self._pay_cost(research)
self._update_costs(research)
def _update_costs(self, research):
pass
def _update_gather_rate(self, task):
pass # TODO
#num_farms = self.buildings.count(Buildings.FARM)
#num_workers = self.villager_tasks[task]
#villager_room =
def assign_villager(self, previous_task, new_task, count=1):
if self.villager_tasks[previous_task] < count:
raise IllegalVillagerMoveException
self._check_gather(new_task, count)
self.villager_tasks[previous_task] -= count
self.villager_tasks[new_task] += count
self._update_gather_rate(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.villager_tasks[task] -= count
self.villager_tasks[Tasks.BUILD] += count
def build(self, building, villagers_from, villagers_to):
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._check_building_dependencies(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,)))
def _add_building(self, building, villagers_to):
self.buildings.append(building)
self._free_production_building(building)
for task, count in villagers_to.items():
self.villager_tasks[Tasks.BUILD] -= count
self.villager_tasks[task] += count
if building == Buildings.HOUSE:
self._free_housing()
if building == Buildings.FARM:
# assumes a farm is worked nonstop after being built
update_time = self.current_time + times.exist_duration[Buildings.FARM]
self.events[update_time].append((self._remove_farm, ()))
def _remove_farm(self):
self.buildings.remove(Buildings.FARM)
num_farms = self.buildings.count(Buildings.FARM)
if self.villager_tasks[Tasks.FARM] > num_farms:
self.villager_tasks[Tasks.FARM] -= 1
self.villager_tasks[Tasks.IDLE] += 1
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):
return sum([self.housing.space[building] for building in self.buildings])
def _get_effective_population(self):
return self.population + self.units_being_built
def _free_housing(self):
pop_space = self._get_population_space()
original_queue_len = len(self.production_queue)
# Assumes each unit costs 1 pop
new_builds = []
i = 0
while (self._get_effective_population() < pop_space and
i < original_queue_len):
unit = self.production_queue[i]
if self._check_production(unit):
self._build_unit(unit)
new_builds.append(i)
i += 1
self.production_queue = [self.production_queue[i]
for i in range(original_queue_len)
if i not in new_builds]
def _free_production_building_of_unit(self, unit):
building = self.production.building[unit]
self._free_production_building(building)
def _free_production_building(self, building):
self.production_available[building] += 1
# Start the next queued unit if one exists
ready_index = None
ready_unit = None
for i in range(len(self.production_queue)):
unit = self.production_queue[i]
if self.production.building[unit] == building:
ready_index = i
ready_unit = unit
break
if ready_index:
self.production_queue.remove(i)
self._build_unit(unit)
def _check_housing(self, unit):
# assumes for now that all units cost 1 pop
pop_space = sum([self.housing.space[building] for building in self.buildings])
if self._get_effective_population() >= pop_space:
raise YallGotHousedException
def _check_gather(self, task, count=1):
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:
num_farms = self.buildings.count(Buildings.FARM)
if self.villager_tasks[task] + count > num_farms:
raise GatherBuildingNotAvailableException
def _check_production(self, unit):
building = self.production.building[unit]
if self.production_available[building] < 1:
raise ProductionBuildingNotAvailableException
def _use_housing_and_production(self, unit):
self._check_housing(unit)
self._check_production(unit)
building = self.production.building[unit]
self.production_available[building] -= 1
self.units_being_built += 1
def _kill_unit(self, unit, task=None):
if unit == Units.VILLAGER and not task:
raise IllegalVillagerMoveException
# assumes population of 1 for every unit
self.population -= 1
if unit == Units.VILLAGER:
self.villagers -= 1
self.villager_tasks[task] -= 1
self._update_gather_rate(task)
self._free_housing()
def _add_unit(self, unit, initial_task):
# assumes population of 1 for every unit
self.units_being_built -= 1
self.population += 1
self._free_production_building_of_unit(unit)
if unit == Units.VILLAGER:
self.villagers += 1
self.villager_tasks[Tasks.IDLE] += 1
self.assign_villager(Tasks.IDLE, initial_task)
def _build_unit(self, unit, initial_task):
try:
self._use_housing_and_production(unit)
except ProductionBuildingNotAvailableException:
self.production_queue.append(unit)
except YallGotHousedException:
pass
else:
update_time = self.current_time + self.build_times.build_times[unit]
self.events[update_time].append((self._add_unit, (unit, initial_task,)))
def queue_unit(self, unit, initial_task=None):
self._check_unit_dependencies(unit)
self._pay_cost(unit)
self._build_unit(unit, initial_task)
def queue_villager(self, initial_task):
self.queue_unit(Units.VILLAGER, initial_task)
def _gather_resources_for_duration(self, duration):
for task, count in self.villager_tasks.items():
gather_rates = self.gather.gather_rates[task]
for resource, rate in gather_rates.items():
self.resources[resource] += count*rate*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.villager_tasks = {t: 0 for t in Tasks}
self.villager_tasks[Tasks.IDLE] = self.villagers
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.building_deps = BuildingDependencies()
self.age_deps = AgeDependencies()
self.research_deps = ResearchDependencies()
self.unit_deps = UnitDependencies()
self.housing = Housing()
self.production = Production()
self.gather = Gather()
self._deal_with_civ()

77
description.md Normal file
View File

@@ -0,0 +1,77 @@
# TODO
* Get aoe info
* API for it somewhere?
* Write simple code
* Write tests
* Write GUI
Deal with getting housed
Put villagers on resource sources
* what to do about farms?
* one villager per
* run out occaisionally and must be rebuilt
# Outline
Timeline of villager production
* should be able to duplicate them easily and make modifications to compare
* throws an error if a task is impossible and a warning if you get idle time
# Design
Was originally doing object-centric:
each thing contained all information about itself.
For example,
a Blacksmith was a thing (class) that had fields representing
a cost, a time to build, an Age requirement, etc.
However, this led to circular dependencies.
For example, a Blacksmith required Feudal Age,
and Castle Age optionally required a Blacksmith.
This made it difficult in Python to have Ages and Buildings in separate files.
They had to import each other,
which is not possible in any simple way in Python.
Instead, data is stored in an attribute-centric way.
Costs is a class,
and it holds costs for Blacksmith, Feudal Age, Militia, and everything else.
Dependencies is another class,
and Times (i.e., build or unit production times) is yet another,
and others are added as needed.
If you think of the information needed as residing in a table
with the rows representing entities (House, Villager, etc.)
and the columns representing attributes (Cost, Build Time, etc.),
we are storing the data by column rather than by row.
As an aside,
this is similar to what I've seen described as the fundamental difference
between object-oriented (row-based) and functional (column-based) programming,
though I'm still using classes and objects.
# Scratch
```
def create(self):
self.
# Assume entries sorted by time
# Not necessarily from user, but from queue_villager and move_villager
```
```
Class Task():
requires = {"wood": "lumber camp"}
# what if more villagers than farms?
```
# Assumptions
* Fixed gather rate for each resource
* what about food -- hunt vs farm?
* At least one TC always exists
* Using no civ bonuses for now

60
runner.py Normal file
View File

@@ -0,0 +1,60 @@
from aoe.exceptions import *
from aoe.types import *
from aoe.gameplay import *
#import ipdb; breakpoint()
# for testing
# TODO need to go back through old tasks when new housing is added
# TODO wherever we test against Age, it's probably wrong
# TODO add building to production_available when finished
# TODO need some warning, penalty, etc. if villager is assigned to task
# without appropriate building (e.g., mining with no camp)
# TODO building time assumes just 1 vil is working for now
# Set up world
game = Gameplay()
# should work even before we do anything
#game.print_status()
game.queue_villager(5, Tasks.WOOD) # first number is time in seconds
# should generate exception
#game.assign_villager(40, Tasks.SHEEP, Tasks.WOOD)
game.assign_villager(0, Tasks.IDLE, Tasks.SHEEP)
game.assign_villager(2, Tasks.IDLE, Tasks.SHEEP, 2)
# should generate exception
#game.assign_villager(0, Tasks.IDLE, Tasks.SHEEP)
# should generate exception
#game.build(45, Buildings.STABLE, Tasks.WOOD, Tasks.IDLE)
#game.build(45, Buildings.LUMBERCAMP, Tasks.WOOD, Tasks.IDLE)
game.build(45, Buildings.LUMBERCAMP, Tasks.WOOD, Tasks.WOOD)
game.queue_villager(45, Tasks.WOOD)
game.queue_villager(45, Tasks.WOOD)
game.build(100, Buildings.HOUSE, [Tasks.SHEEP, Tasks.WOOD], [Tasks.IDLE]*2)
#game.assign_villager(Build.Stable(), "food", wait_until_finished=True)
'''
game.queue_unit(s1, Scout)
'''
# should generate exception
#game.build(200, Buildings.FARM, [Tasks.SHEEP], [Tasks.WOOD])
game.build(200, Buildings.MILL, [Tasks.SHEEP], [Tasks.WOOD])
game.build(250, Buildings.FARM, [Tasks.SHEEP, Tasks.WOOD], [Tasks.IDLE])
# Evaluate game
game.play_until(800)
game.print_status()
# should generate exception
#game.assign_villager(0, Tasks.SHEEP, Tasks.WOOD)