| 
									
										
										
										
											2021-04-10 03:03:46 +02:00
										 |  |  | from __future__ import annotations | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | # Factorio technologies are imported from a .json document in /data | 
					
						
							| 
									
										
										
										
											2021-04-09 22:10:04 +02:00
										 |  |  | from typing import Dict, Set, FrozenSet | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | import json | 
					
						
							|  |  |  | import Utils | 
					
						
							| 
									
										
										
										
											2021-04-09 22:10:04 +02:00
										 |  |  | import logging | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-05 15:37:15 +02:00
										 |  |  | factorio_id = 2 ** 17 | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | source_file = Utils.local_path("data", "factorio", "techs.json") | 
					
						
							| 
									
										
										
										
											2021-04-08 19:53:24 +02:00
										 |  |  | recipe_source_file = Utils.local_path("data", "factorio", "recipes.json") | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | with open(source_file) as f: | 
					
						
							|  |  |  |     raw = json.load(f) | 
					
						
							| 
									
										
										
										
											2021-04-08 19:53:24 +02:00
										 |  |  | with open(recipe_source_file) as f: | 
					
						
							|  |  |  |     raw_recipes = json.load(f) | 
					
						
							| 
									
										
										
										
											2021-05-03 18:06:21 +02:00
										 |  |  | tech_table: Dict[str, int] = {} | 
					
						
							| 
									
										
										
										
											2021-04-24 01:16:49 +02:00
										 |  |  | technology_table: Dict[str, Technology] = {} | 
					
						
							| 
									
										
										
										
											2021-04-05 15:37:15 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-10 00:08:59 +02:00
										 |  |  | always = lambda state: True | 
					
						
							| 
									
										
										
										
											2021-04-05 15:37:15 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-10 03:03:46 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-05 15:37:15 +02:00
										 |  |  | class Technology():  # maybe make subclass of Location? | 
					
						
							| 
									
										
										
										
											2021-04-24 01:16:49 +02:00
										 |  |  |     def __init__(self, name, ingredients, factorio_id): | 
					
						
							| 
									
										
										
										
											2021-04-05 15:37:15 +02:00
										 |  |  |         self.name = name | 
					
						
							|  |  |  |         self.factorio_id = factorio_id | 
					
						
							|  |  |  |         self.ingredients = ingredients | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-24 01:16:49 +02:00
										 |  |  |     def build_rule(self, player: int): | 
					
						
							| 
									
										
										
										
											2021-04-09 22:10:04 +02:00
										 |  |  |         logging.debug(f"Building rules for {self.name}") | 
					
						
							| 
									
										
										
										
											2021-04-08 19:53:24 +02:00
										 |  |  |         ingredient_rules = [] | 
					
						
							|  |  |  |         for ingredient in self.ingredients: | 
					
						
							| 
									
										
										
										
											2021-04-24 01:16:49 +02:00
										 |  |  |             logging.debug(f"Building rules for ingredient {ingredient}") | 
					
						
							|  |  |  |             technologies = required_technologies[ingredient]  # technologies that unlock the recipes | 
					
						
							|  |  |  |             if technologies: | 
					
						
							|  |  |  |                 logging.debug(f"Required Technologies: {technologies}") | 
					
						
							|  |  |  |                 ingredient_rules.append( | 
					
						
							|  |  |  |                     lambda state, technologies=technologies: all(state.has(technology.name, player) | 
					
						
							|  |  |  |                                                                  for technology in technologies)) | 
					
						
							| 
									
										
										
										
											2021-04-10 00:08:59 +02:00
										 |  |  |         if ingredient_rules: | 
					
						
							|  |  |  |             ingredient_rules = frozenset(ingredient_rules) | 
					
						
							|  |  |  |             return lambda state: all(rule(state) for rule in ingredient_rules) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return always | 
					
						
							| 
									
										
										
										
											2021-04-08 19:53:24 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-10 03:03:46 +02:00
										 |  |  |     def get_prior_technologies(self, allowed_packs) -> Set[Technology]: | 
					
						
							|  |  |  |         """Get Technologies that have to precede this one to resolve tree connections.""" | 
					
						
							|  |  |  |         technologies = set() | 
					
						
							|  |  |  |         for ingredient in self.ingredients: | 
					
						
							|  |  |  |             if ingredient in allowed_packs: | 
					
						
							|  |  |  |                 technologies |= required_technologies[ingredient]  # technologies that unlock the recipes | 
					
						
							|  |  |  |         return technologies | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-05 15:37:15 +02:00
										 |  |  |     def __hash__(self): | 
					
						
							|  |  |  |         return self.factorio_id | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-09 22:10:04 +02:00
										 |  |  |     def __repr__(self): | 
					
						
							|  |  |  |         return f"{self.__class__.__name__}({self.name})" | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-24 01:16:49 +02:00
										 |  |  |     def get_custom(self, world, allowed_packs: Set[str], player: int) -> CustomTechnology: | 
					
						
							|  |  |  |         return CustomTechnology(self, world, allowed_packs, player) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class CustomTechnology(Technology): | 
					
						
							|  |  |  |     """A particularly configured Technology for a world.""" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def __init__(self, origin: Technology, world, allowed_packs: Set[str], player: int): | 
					
						
							|  |  |  |         ingredients = origin.ingredients & allowed_packs | 
					
						
							|  |  |  |         self.player = player | 
					
						
							|  |  |  |         if world.random_tech_ingredients[player]: | 
					
						
							|  |  |  |             ingredients = list(ingredients) | 
					
						
							|  |  |  |             ingredients.sort() # deterministic sample | 
					
						
							|  |  |  |             ingredients = world.random.sample(ingredients, world.random.randint(1, len(ingredients))) | 
					
						
							|  |  |  |         super(CustomTechnology, self).__init__(origin.name, ingredients, origin.factorio_id) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-09 22:10:04 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-08 19:53:24 +02:00
										 |  |  | class Recipe(): | 
					
						
							|  |  |  |     def __init__(self, name, category, ingredients, products): | 
					
						
							|  |  |  |         self.name = name | 
					
						
							|  |  |  |         self.category = category | 
					
						
							| 
									
										
										
										
											2021-04-09 22:10:04 +02:00
										 |  |  |         self.ingredients = ingredients | 
					
						
							|  |  |  |         self.products = products | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def __repr__(self): | 
					
						
							|  |  |  |         return f"{self.__class__.__name__}({self.name})" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def unlocking_technologies(self) -> Set[Technology]: | 
					
						
							|  |  |  |         """Unlocked by any of the returned technologies. Empty set indicates a starting recipe.""" | 
					
						
							|  |  |  |         return {technology_table[tech_name] for tech_name in recipe_sources.get(self.name, ())} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | # recipes and technologies can share names in Factorio | 
					
						
							| 
									
										
										
										
											2021-04-05 15:37:15 +02:00
										 |  |  | for technology_name in sorted(raw): | 
					
						
							|  |  |  |     data = raw[technology_name] | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |     factorio_id += 1 | 
					
						
							| 
									
										
										
										
											2021-04-05 15:37:15 +02:00
										 |  |  |     current_ingredients = set(data["ingredients"]) | 
					
						
							| 
									
										
										
										
											2021-04-24 01:16:49 +02:00
										 |  |  |     technology = Technology(technology_name, current_ingredients, factorio_id) | 
					
						
							|  |  |  |     factorio_id += 1 | 
					
						
							| 
									
										
										
										
											2021-04-05 15:37:15 +02:00
										 |  |  |     tech_table[technology_name] = technology.factorio_id | 
					
						
							|  |  |  |     technology_table[technology_name] = technology | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-09 22:10:04 +02:00
										 |  |  | recipe_sources: Dict[str, str] = {}  # recipe_name -> technology source | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | for technology, data in raw.items(): | 
					
						
							| 
									
										
										
										
											2021-04-09 22:10:04 +02:00
										 |  |  |     for recipe_name in data["unlocks"]: | 
					
						
							|  |  |  |         recipe_sources.setdefault(recipe_name, set()).add(technology) | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-05 15:37:15 +02:00
										 |  |  | del (raw) | 
					
						
							|  |  |  | lookup_id_to_name: Dict[int, str] = {item_id: item_name for item_name, item_id in tech_table.items()} | 
					
						
							| 
									
										
										
										
											2021-04-08 19:53:24 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-10 02:33:54 +02:00
										 |  |  | all_product_sources: Dict[str, Set[Recipe]] = {} | 
					
						
							| 
									
										
										
										
											2021-04-08 19:53:24 +02:00
										 |  |  | for recipe_name, recipe_data in raw_recipes.items(): | 
					
						
							|  |  |  |     # example: | 
					
						
							|  |  |  |     # "accumulator":{"ingredients":["iron-plate","battery"],"products":["accumulator"],"category":"crafting"} | 
					
						
							| 
									
										
										
										
											2021-04-09 22:10:04 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     recipe = Recipe(recipe_name, recipe_data["category"], set(recipe_data["ingredients"]), set(recipe_data["products"])) | 
					
						
							| 
									
										
										
										
											2021-05-10 02:33:54 +02:00
										 |  |  |     if recipe.products != recipe.ingredients and "empty-barrel" not in recipe.products:  # prevents loop recipes like uranium centrifuging | 
					
						
							| 
									
										
										
										
											2021-04-09 22:10:04 +02:00
										 |  |  |         for product_name in recipe.products: | 
					
						
							| 
									
										
										
										
											2021-05-10 02:33:54 +02:00
										 |  |  |             all_product_sources.setdefault(product_name, set()).add(recipe) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-09 22:10:04 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | # build requirements graph for all technology ingredients | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | all_ingredient_names: Set[str] = set() | 
					
						
							|  |  |  | for technology in technology_table.values(): | 
					
						
							|  |  |  |     all_ingredient_names |= technology.ingredients | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def recursively_get_unlocking_technologies(ingredient_name, _done=None) -> Set[Technology]: | 
					
						
							|  |  |  |     if _done: | 
					
						
							|  |  |  |         if ingredient_name in _done: | 
					
						
							|  |  |  |             return set() | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             _done.add(ingredient_name) | 
					
						
							|  |  |  |     else: | 
					
						
							| 
									
										
										
										
											2021-04-10 00:21:56 +02:00
										 |  |  |         _done = {ingredient_name} | 
					
						
							| 
									
										
										
										
											2021-05-10 02:33:54 +02:00
										 |  |  |     recipes = all_product_sources.get(ingredient_name) | 
					
						
							|  |  |  |     if not recipes: | 
					
						
							| 
									
										
										
										
											2021-04-09 22:10:04 +02:00
										 |  |  |         return set() | 
					
						
							| 
									
										
										
										
											2021-05-10 02:33:54 +02:00
										 |  |  |     current_technologies = set() | 
					
						
							|  |  |  |     for recipe in recipes: | 
					
						
							|  |  |  |         current_technologies |= recipe.unlocking_technologies | 
					
						
							|  |  |  |         for ingredient_name in recipe.ingredients: | 
					
						
							|  |  |  |             current_technologies |= recursively_get_unlocking_technologies(ingredient_name, _done) | 
					
						
							| 
									
										
										
										
											2021-04-09 22:10:04 +02:00
										 |  |  |     return current_technologies | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | required_technologies: Dict[str, FrozenSet[Technology]] = {} | 
					
						
							|  |  |  | for ingredient_name in all_ingredient_names: | 
					
						
							|  |  |  |     required_technologies[ingredient_name] = frozenset(recursively_get_unlocking_technologies(ingredient_name)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | advancement_technologies: Set[str] = set() | 
					
						
							|  |  |  | for technologies in required_technologies.values(): | 
					
						
							|  |  |  |     advancement_technologies |= {technology.name for technology in technologies} |