From 4a014aeb50a9f2d85820e218bb554a6d013ea1a0 Mon Sep 17 00:00:00 2001 From: Nate Bowman Date: Sun, 26 Jul 2026 20:26:36 -0400 Subject: [PATCH] Initial commit --- aoe/costs.py | 46 +++++ aoe/dependencies.py | 96 ++++++++++ aoe/exceptions.py | 37 ++++ aoe/gameplay.py | 85 +++++++++ aoe/gather.py | 59 ++++++ aoe/housing.py | 14 ++ aoe/production.py | 19 ++ aoe/times.py | 45 +++++ aoe/types.py | 95 ++++++++++ aoe/world.py | 425 ++++++++++++++++++++++++++++++++++++++++++++ description.md | 77 ++++++++ runner.py | 60 +++++++ 12 files changed, 1058 insertions(+) create mode 100644 aoe/costs.py create mode 100644 aoe/dependencies.py create mode 100644 aoe/exceptions.py create mode 100644 aoe/gameplay.py create mode 100644 aoe/gather.py create mode 100644 aoe/housing.py create mode 100644 aoe/production.py create mode 100644 aoe/times.py create mode 100644 aoe/types.py create mode 100644 aoe/world.py create mode 100644 description.md create mode 100644 runner.py diff --git a/aoe/costs.py b/aoe/costs.py new file mode 100644 index 0000000..87fe7dd --- /dev/null +++ b/aoe/costs.py @@ -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: diff --git a/aoe/dependencies.py b/aoe/dependencies.py new file mode 100644 index 0000000..d8e7f96 --- /dev/null +++ b/aoe/dependencies.py @@ -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], + } diff --git a/aoe/exceptions.py b/aoe/exceptions.py new file mode 100644 index 0000000..46ddc0b --- /dev/null +++ b/aoe/exceptions.py @@ -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 diff --git a/aoe/gameplay.py b/aoe/gameplay.py new file mode 100644 index 0000000..d360c7b --- /dev/null +++ b/aoe/gameplay.py @@ -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 diff --git a/aoe/gather.py b/aoe/gather.py new file mode 100644 index 0000000..7a904a7 --- /dev/null +++ b/aoe/gather.py @@ -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 = {} diff --git a/aoe/housing.py b/aoe/housing.py new file mode 100644 index 0000000..e1172ab --- /dev/null +++ b/aoe/housing.py @@ -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, + }) diff --git a/aoe/production.py b/aoe/production.py new file mode 100644 index 0000000..3a82b9d --- /dev/null +++ b/aoe/production.py @@ -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, + } diff --git a/aoe/times.py b/aoe/times.py new file mode 100644 index 0000000..732f9db --- /dev/null +++ b/aoe/times.py @@ -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, + } diff --git a/aoe/types.py b/aoe/types.py new file mode 100644 index 0000000..529bdb7 --- /dev/null +++ b/aoe/types.py @@ -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() diff --git a/aoe/world.py b/aoe/world.py new file mode 100644 index 0000000..26e3528 --- /dev/null +++ b/aoe/world.py @@ -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() diff --git a/description.md b/description.md new file mode 100644 index 0000000..7a2543d --- /dev/null +++ b/description.md @@ -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 diff --git a/runner.py b/runner.py new file mode 100644 index 0000000..457a193 --- /dev/null +++ b/runner.py @@ -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)