rngoptimizer 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,86 @@
1
+ Metadata-Version: 2.3
2
+ Name: rngoptimizer
3
+ Version: 0.1.1
4
+ Summary: Optimize your Tycoon RNG:Optimizer factory
5
+ Requires-Python: >=3.14
6
+ Description-Content-Type: text/markdown
7
+
8
+ # Tycoon RNG : Refinery Optimizer
9
+ TRRO is a tool developed to help you optimize your setup to maximize profit in the game Tycoon RNG: Refinery.
10
+ It uses an asynchronous Genetic Algorithm to find the best value without much computing power and without having to check all 60 quadrillion combinations.
11
+ ## Features
12
+ - Random runs to avoid shallow bests
13
+ - A comprehensive and easy way to pass in available buildings
14
+ - An estimated value
15
+ - Handling of complex tag interactions, rarity multipliers, and more through an in-depth class system
16
+ - Future-proof with clear syntax and buildings interactions. Adding a building is done with a single line!
17
+
18
+ ## Installation and use
19
+ 1. Download the source code by clicking the green **Code** button on the top right and **Download ZIP**.
20
+ 2. Unzip the file.
21
+ 3. Write your available buildings in available_buildings.txt or another text file in the same directory using the following syntax:
22
+
23
+ **RARITY1 RARITY2 RARITY3 - BUILDING NAME**
24
+
25
+ Capitalization is important. For example:
26
+ - **Negative Overpowered - Slime Duper** is correct
27
+ - **OP H - Ancient Ruins** is correct
28
+ - **Hyperspace Electric Dropper** is incorrect
29
+ - **negative magical tree** is incorrect
30
+ 4. run *runme.py*. Make sure you have python installed.
31
+ 5. When prompted, enter the number of droppers slots you have on your base, the number of upgraders, and the preset. To use the default value, press enter. *(Default value can also be set in available buildings.txt)*
32
+ 6. Wait for the program to finish.
33
+ 7. The last line will be the best found buildings and the estimated value produced. It will also be saved to *optimized_buildings.txt*.
34
+ 8. Done!
35
+
36
+ ## Frequently Asked Questions
37
+
38
+ <details>
39
+ <summary><b>Is this safe?</b></summary>
40
+ <br>
41
+
42
+ Yes. It does not read from any file other than available_buildings.txt and does not write to any file other than optimized_buildings.txt. This is a fully open-source project, and anyone with Python experience can confirm this program does what it says it does.
43
+
44
+ </details>
45
+
46
+
47
+ <details>
48
+ <summary><b>Is this linked to my Roblox Account?</b></summary>
49
+ <br>
50
+
51
+ No. It requires you to manually add in your own buildings by writing them down. It does not have access to your Roblox account, and requires no login.
52
+
53
+ </details>
54
+
55
+
56
+ <details>
57
+ <summary><b>I can't open runme.py!</b></summary>
58
+ <br>
59
+
60
+ Make sure you have Python 3.14 installed. you can find it [here.](https://www.python.org/downloads/)
61
+
62
+ </details>
63
+
64
+ <details>
65
+ <summary><b>&lt;Building&gt; isn't recognized!</b></summary>
66
+ <br>
67
+
68
+ The building might've been added in a recent update. You can notify me on the TR:R Discord or DM me: **@Elie07**
69
+
70
+ </details>
71
+
72
+ <details>
73
+ <summary><b>The factory found is worse than the one I have right now!</b></summary>
74
+ <br>
75
+
76
+ The algorithm isn't perfect, and is by definition not very intelligent. It's basically a better trial-and-error. I'm working on upgrading the algorithm to be more performant.
77
+
78
+ </details>
79
+
80
+ <details>
81
+ <summary><b>I built the recommended factory, but it doesn't perform the same!</b></summary>
82
+ <br>
83
+
84
+ I'm not affiliated with the TR:R developement team and have no way to check if my calculations are accurate. If you run into this issue, please send me your available buildings, the setup the algorithm gave you and what you're getting in game via discord: **@Elie07**
85
+
86
+ </details>
@@ -0,0 +1,79 @@
1
+ # Tycoon RNG : Refinery Optimizer
2
+ TRRO is a tool developed to help you optimize your setup to maximize profit in the game Tycoon RNG: Refinery.
3
+ It uses an asynchronous Genetic Algorithm to find the best value without much computing power and without having to check all 60 quadrillion combinations.
4
+ ## Features
5
+ - Random runs to avoid shallow bests
6
+ - A comprehensive and easy way to pass in available buildings
7
+ - An estimated value
8
+ - Handling of complex tag interactions, rarity multipliers, and more through an in-depth class system
9
+ - Future-proof with clear syntax and buildings interactions. Adding a building is done with a single line!
10
+
11
+ ## Installation and use
12
+ 1. Download the source code by clicking the green **Code** button on the top right and **Download ZIP**.
13
+ 2. Unzip the file.
14
+ 3. Write your available buildings in available_buildings.txt or another text file in the same directory using the following syntax:
15
+
16
+ **RARITY1 RARITY2 RARITY3 - BUILDING NAME**
17
+
18
+ Capitalization is important. For example:
19
+ - **Negative Overpowered - Slime Duper** is correct
20
+ - **OP H - Ancient Ruins** is correct
21
+ - **Hyperspace Electric Dropper** is incorrect
22
+ - **negative magical tree** is incorrect
23
+ 4. run *runme.py*. Make sure you have python installed.
24
+ 5. When prompted, enter the number of droppers slots you have on your base, the number of upgraders, and the preset. To use the default value, press enter. *(Default value can also be set in available buildings.txt)*
25
+ 6. Wait for the program to finish.
26
+ 7. The last line will be the best found buildings and the estimated value produced. It will also be saved to *optimized_buildings.txt*.
27
+ 8. Done!
28
+
29
+ ## Frequently Asked Questions
30
+
31
+ <details>
32
+ <summary><b>Is this safe?</b></summary>
33
+ <br>
34
+
35
+ Yes. It does not read from any file other than available_buildings.txt and does not write to any file other than optimized_buildings.txt. This is a fully open-source project, and anyone with Python experience can confirm this program does what it says it does.
36
+
37
+ </details>
38
+
39
+
40
+ <details>
41
+ <summary><b>Is this linked to my Roblox Account?</b></summary>
42
+ <br>
43
+
44
+ No. It requires you to manually add in your own buildings by writing them down. It does not have access to your Roblox account, and requires no login.
45
+
46
+ </details>
47
+
48
+
49
+ <details>
50
+ <summary><b>I can't open runme.py!</b></summary>
51
+ <br>
52
+
53
+ Make sure you have Python 3.14 installed. you can find it [here.](https://www.python.org/downloads/)
54
+
55
+ </details>
56
+
57
+ <details>
58
+ <summary><b>&lt;Building&gt; isn't recognized!</b></summary>
59
+ <br>
60
+
61
+ The building might've been added in a recent update. You can notify me on the TR:R Discord or DM me: **@Elie07**
62
+
63
+ </details>
64
+
65
+ <details>
66
+ <summary><b>The factory found is worse than the one I have right now!</b></summary>
67
+ <br>
68
+
69
+ The algorithm isn't perfect, and is by definition not very intelligent. It's basically a better trial-and-error. I'm working on upgrading the algorithm to be more performant.
70
+
71
+ </details>
72
+
73
+ <details>
74
+ <summary><b>I built the recommended factory, but it doesn't perform the same!</b></summary>
75
+ <br>
76
+
77
+ I'm not affiliated with the TR:R developement team and have no way to check if my calculations are accurate. If you run into this issue, please send me your available buildings, the setup the algorithm gave you and what you're getting in game via discord: **@Elie07**
78
+
79
+ </details>
@@ -0,0 +1,14 @@
1
+ [project]
2
+ name = "rngoptimizer"
3
+ version = "0.1.1"
4
+ description = "Optimize your Tycoon RNG:Optimizer factory"
5
+ readme = "README.md"
6
+ requires-python = ">=3.14"
7
+ dependencies = []
8
+
9
+ [project.scripts]
10
+ rngoptimizer = "rngoptimizer.cli:main_cli"
11
+
12
+ [build-system]
13
+ requires = ["uv_build>=0.12.12,<0.13.0"]
14
+ build-backend = "uv_build"
@@ -0,0 +1,14 @@
1
+ [project]
2
+ name = "rngoptimizer"
3
+ version = "0.1.1"
4
+ description = "Optimize your Tycoon RNG:Optimizer factory"
5
+ readme = "README.md"
6
+ requires-python = ">=3.14"
7
+ dependencies = []
8
+
9
+ [project.scripts]
10
+ rngoptimizer = "rngoptimizer.cli:main_cli"
11
+
12
+ [build-system]
13
+ requires = ["uv_build>=0.12.12,<0.13.0"]
14
+ build-backend = "uv_build"
File without changes
@@ -0,0 +1,5 @@
1
+ if __name__ == "__main__":
2
+ from rngoptimizer.engine import main
3
+ import multiprocessing
4
+ multiprocessing.freeze_support()
5
+ main()
@@ -0,0 +1,140 @@
1
+ Bonuses = ["Fire", "Wet", "Aired", "Magnetic", "Ancient", "Slimed", "Acid", "Nuclear", "Fueled", "Nebula"]
2
+ Destroy_timers = {"Fire": 7.5, "Acid":8.0, "Nuclear":7.5}
3
+ Rarities = {"Standard": 1, "Overpowered": 2, "Negative": 3.5, "Hyperspace": 6, "":1, "OP": 2, "N": 3.5, "H": 6}
4
+
5
+ ##TODO: Instead of handling tags, vulns, and raritites as strings, handle them as ints for faster runtime
6
+
7
+ def apply_rarities(rarities, value):
8
+ for rarity in rarities.split(" "):
9
+ value *= Rarities[rarity]
10
+ return value
11
+
12
+
13
+ class Item:
14
+ def __init__(self, bonuses, effects, vulnerabilities, immunities, value, destroy_timers=None):
15
+ self.bonuses = dict(bonuses)
16
+ self.effects = list(effects)
17
+ self.vulnerabilities = list(vulnerabilities)
18
+ self.immunities = list(immunities)
19
+ self.value = value
20
+ self.destroy_timers = {} if destroy_timers is None else dict(destroy_timers)
21
+
22
+ #In between machines: removes effects it's immune to (fallback), applies destroy timers, and applies native bonuses (from the dropper)
23
+ def update(self, time):
24
+ for effect in self.effects:
25
+ if effect in self.immunities:
26
+ self.effects[:] = [active_effect for active_effect in self.effects if active_effect != effect]
27
+ continue
28
+ if effect in Destroy_timers:
29
+ if effect not in self.destroy_timers:
30
+ self.destroy_timers[effect] = Destroy_timers[effect] - time
31
+ else:
32
+ self.destroy_timers[effect] -= time
33
+ if self.destroy_timers[effect] <= 0:
34
+ self.value = 0
35
+ if effect in self.bonuses:
36
+ self.value *= self.bonuses[effect]
37
+ self.bonuses.pop(effect)
38
+ return self
39
+
40
+
41
+ class Dropper:
42
+ def __init__(self, name, eff, rarity="Standard", bonus=None, vulnerabilities=None, immunities=None):
43
+ self.name = name
44
+ self.efficiency = eff
45
+ self.rarity = rarity
46
+ self.bonus = {} if bonus is None else bonus
47
+ self.vulnerabilities = [] if vulnerabilities is None else vulnerabilities
48
+ self.immunities = [] if immunities is None else immunities
49
+
50
+ #Returns an Item object with the dropper's bonuses, vulnerabilities, immunities, and value (efficiency * rarity multiplier)
51
+ def drop(self):
52
+ value = apply_rarities(self.rarity, self.efficiency)
53
+ return Item(self.bonus, [], self.vulnerabilities, self.immunities, value)
54
+
55
+ class Upgrader:
56
+ def __init__(self, name, mult, rarity="Standard", effect_add=None, effect_remove=None, cond_effect_add=None, trans_effect=None, bonuses=None, destroy_chance=0.0):
57
+ self.rarity = rarity
58
+ self.name = name
59
+ self.mult = mult
60
+ self.effect_add = [] if effect_add is None else effect_add
61
+ self.effect_remove = [] if effect_remove is None else effect_remove
62
+ self.cond_effect_add = {} if cond_effect_add is None else cond_effect_add
63
+ self.trans_effect = {} if trans_effect is None else trans_effect
64
+ self.bonuses = {} if bonuses is None else bonuses
65
+ self.destroy_chance = destroy_chance
66
+ def upgrade(self, item):
67
+
68
+ #Multiply the item's value by the upgrader's multiplier and apply rarity multiplier
69
+ item.value *= self.mult
70
+ item.value = apply_rarities(self.rarity, item.value)
71
+
72
+
73
+ #Remove all effects from effect_remove
74
+ if "All" in self.effect_remove:
75
+ removed_effects = set(item.effects)
76
+ item.effects.clear()
77
+ for effect in removed_effects:
78
+ item.destroy_timers.pop(effect, None)
79
+ else:
80
+ removed_effects = set(self.effect_remove).intersection(item.effects)
81
+ item.effects[:] = [effect for effect in item.effects if effect not in self.effect_remove]
82
+ for effect in removed_effects:
83
+ item.destroy_timers.pop(effect, None)
84
+
85
+ #Apply bonuses to the item's value if the item has the corresponding effect
86
+ for bonus in self.bonuses:
87
+ if bonus in item.effects:
88
+ item.value *= self.bonuses[bonus]
89
+
90
+ #Add effects from effect_add to the item, and add vulnerabilities if the effect starts with "V-"
91
+ for effect in self.effect_add:
92
+ if effect.startswith("V-"):
93
+ item.vulnerabilities.append(effect[2:])
94
+ else:
95
+ item.effects.append(effect)
96
+
97
+ #Add conditional effects based on the item's current effects (1 per effect)
98
+ for effect, additions in self.cond_effect_add.items():
99
+ for _ in range(item.effects.count(effect)):
100
+ item.effects.extend(additions)
101
+
102
+ #Transform effects based on trans_effect mapping (1 per effect)
103
+ for index, effect in enumerate(item.effects):
104
+ if effect in self.trans_effect:
105
+ item.effects[index] = self.trans_effect[effect]
106
+
107
+ #Multiply the item's value by (1 - destroy_chance) to simulate the chance of destruction
108
+ if self.destroy_chance > 0:
109
+ item.value *= (1 - self.destroy_chance)
110
+
111
+ return item
112
+
113
+ class Processor:
114
+ def __init__(self, name, mult, rarity="Standard", bonus={}, onlyaccept=[], refuse=[]):
115
+ self.name = name
116
+ self.mult = mult
117
+ self.rarity = rarity
118
+ self.bonus = bonus
119
+ self.onlyaccept = onlyaccept
120
+ self.refuse = refuse
121
+
122
+ def process(self, item):
123
+
124
+ #If the processor has onlyaccept effects and the item does not have any of those effects, set the item's value to 0
125
+ if self.onlyaccept and not any(effect in item.effects for effect in self.onlyaccept):
126
+ item.value = 0
127
+
128
+ #If the processor has refuse effects and the item has any of those effects, set the item's value to 0
129
+ if any(effect in item.effects for effect in self.refuse):
130
+ item.value = 0
131
+
132
+ #Multiply the item's value by the processor's multiplier and apply rarity multiplier
133
+ item.value *= self.mult
134
+ item.value = apply_rarities(self.rarity, item.value)
135
+
136
+ #Apply bonuses to the item's value if the item has the corresponding effect
137
+ for effect in self.bonus:
138
+ if effect in item.effects:
139
+ item.value *= self.bonus[effect]
140
+ return item
@@ -0,0 +1,32 @@
1
+
2
+ import argparse
3
+ from rngoptimizer.engine import run
4
+
5
+ def main_cli():
6
+
7
+ parser = argparse.ArgumentParser(description="Run the building optimization engine.")
8
+ parser.add_argument("-p","--path", type=str, default="available_buildings.txt", help="Path to available buildings file")
9
+ parser.add_argument("-r", "--runs", type=int, default=1, help="Number of runs")
10
+ parser.add_argument("-d", "--droppers", type=int, default=0, help="Number of droppers")
11
+ parser.add_argument("-u", "--upgraders", type=int, default=0, help="Number of upgraders")
12
+
13
+ args = parser.parse_args()
14
+ if args.path == "":
15
+ args.path = "available_buildings.txt"
16
+ if args.runs < 1:
17
+ args.runs = 1
18
+ args.droppers = abs(args.droppers)
19
+ args.upgraders = abs(args.upgraders)
20
+
21
+ print("Calculating...")
22
+
23
+ result = run(args.path, args.droppers, args.upgraders, args.runs)
24
+ print("Optimized buildings:", result)
25
+ with open("optimized_buildings.txt", "w") as f:
26
+ f.write(str(result))
27
+ input("Press Enter to exit...")
28
+
29
+ if __name__ == "__main__":
30
+ import multiprocessing
31
+ multiprocessing.freeze_support()
32
+ main_cli()
@@ -0,0 +1,63 @@
1
+ try:
2
+ from . import buildings
3
+ except ImportError:
4
+ import rngoptimizer.buildings as buildings
5
+ Droppers = []
6
+ Upgraders = []
7
+ Processors = []
8
+
9
+ Droppers.append(buildings.Dropper(name="Basic Dropper", eff=1))
10
+ Droppers.append(buildings.Dropper(name="Advanced Dropper", eff=2))
11
+ Droppers.append(buildings.Dropper(name="Poisoned Dripper", eff=3, bonus={"Acid": 2}, vulnerabilities=["Wet"]))
12
+ Droppers.append(buildings.Dropper(name="Coal Drill", eff=5, immunities=["Fire"]))
13
+ Droppers.append(buildings.Dropper(name="Electric Dropper", eff=10, vulnerabilities=["Wet"]))
14
+ Droppers.append(buildings.Dropper(name="Ancient Ruins", eff=16, bonus={"Ancient": 1.2}, immunities=["Acid"]))
15
+ Droppers.append(buildings.Dropper(name="Slime Duper", eff=20, bonus={"Slimed": 1.3}))
16
+ Droppers.append(buildings.Dropper(name="Flower Forest", eff=25, bonus={"Nebula": 0.9, "Ice": 0.1}, vulnerabilities=["Fire"]))
17
+ Droppers.append(buildings.Dropper(name="Sea Dropper", eff=37, bonus={"Wet": 1.5}, immunities=["Fire"]))
18
+ Droppers.append(buildings.Dropper(name="Ice Dropper", eff=41))
19
+ Droppers.append(buildings.Dropper(name="Dupe Error", eff=75, bonus={"Nuclear": 1.5}, vulnerabilities=["Acid"]))
20
+ Droppers.append(buildings.Dropper(name="Timmy the Tree", eff=100, bonus={"Aired": 1.2}))
21
+ Droppers.append(buildings.Dropper(name="Shard Harvester", eff=119, bonus={"Nebula": 1.3}, immunities=["Nuclear"]))
22
+ Droppers.append(buildings.Dropper(name="Steampunk Terminator", eff=132, bonus={"Fueled": 1.7}, vulnerabilities=["Acid"]))
23
+ Droppers.append(buildings.Dropper(name="Magical Tree", eff=150, bonus={"Aired": 1.45}, immunities=["Nuclear"]))
24
+ Droppers.append(buildings.Dropper(name="Aegis", eff=183.3333333, bonus={"Nebula": 2}, immunities=["Nuclear"]))
25
+
26
+ Upgraders.append(buildings.Upgrader(name="Copper Upgrader", mult=1.5))
27
+ Upgraders.append(buildings.Upgrader(name="Ore Scanner", mult=2, bonuses={"Aired": 1.5}))
28
+ Upgraders.append(buildings.Upgrader(name="Fire Laser", mult=2.5, effect_remove=["Wet"], trans_effect={"Wet": "Fire"}))
29
+ Upgraders.append(buildings.Upgrader(name="Blue Simulation", mult=7, effect_add=["Wet"]))
30
+ Upgraders.append(buildings.Upgrader(name="Coral Sea", mult=10, effect_add=["Wet", "Wet"], effect_remove=["Fire"]))
31
+ Upgraders.append(buildings.Upgrader(name="Ice Wind", mult=12, effect_add=["Ice"]))
32
+ Upgraders.append(buildings.Upgrader(name="Nuclear Waste", mult=13, effect_add=["Nuclear"], destroy_chance=0.2))
33
+ Upgraders.append(buildings.Upgrader(name="Surge Light", mult=15, bonuses={"Aired": 1.3}))
34
+ Upgraders.append(buildings.Upgrader(name="Mystical Swamp", mult=17, effect_add=["Slimed"], bonuses={"Slimed": 1.3}, destroy_chance=0.01))
35
+ Upgraders.append(buildings.Upgrader(name="Magnetic Fuser", mult=18, effect_add=["Magnetic", "Nuclear"], cond_effect_add={"Nuclear": ["Nebula"]}))
36
+ Upgraders.append(buildings.Upgrader(name="Sandy Valley", mult=21, effect_add=["Ancient"]))
37
+ Upgraders.append(buildings.Upgrader(name="Water Mountain", mult=25, effect_add=["Wet"], effect_remove=["Fire"], destroy_chance=0.05))
38
+ Upgraders.append(buildings.Upgrader(name="Acid Leak", mult=31, effect_add=["Acid"]))
39
+ Upgraders.append(buildings.Upgrader(name="Northern Lights", mult=33, effect_add=["Aired", "Ice"], effect_remove=["Fire"], bonuses={"Nuclear": 0.5}))
40
+ Upgraders.append(buildings.Upgrader(name="Temple of Silence", mult=25, effect_remove=["All"]))
41
+ Upgraders.append(buildings.Upgrader(name="Death Waste", mult=35, effect_add=["V-Wet"], bonuses={"Nuclear": 2}))
42
+ Upgraders.append(buildings.Upgrader(name="Sunstone", mult=37, effect_add=["Fire", "Fire", "Fire", "Fire", "Fire"]))
43
+ Upgraders.append(buildings.Upgrader(name="Nuclear Lab", mult=43, trans_effect={"Acid": "Nuclear"}, bonuses={"Nuclear": 1.5}))
44
+ Upgraders.append(buildings.Upgrader(name="Amethyst Singularity", mult=50, bonuses={"Nebula": 2}))
45
+ Upgraders.append(buildings.Upgrader(name="Creations Studio", mult=55, effect_add=["Ice", "Magnetic", "Aired"]))
46
+ Upgraders.append(buildings.Upgrader(name="Spring Sunshine", mult=60, effect_remove=["Wet", "Nuclear"]))
47
+ Upgraders.append(buildings.Upgrader(name="Storming Summit", mult=64, effect_add=["Magnetic"], effect_remove=["Acid"], bonuses={"Magnetic": 1.2}, destroy_chance=0.25))
48
+ Upgraders.append(buildings.Upgrader(name="Shinigami Realm", mult=72, bonuses={"Ancient": 1.6}))
49
+ Upgraders.append(buildings.Upgrader(name="Aegis Upgrader", mult=89, effect_add=["Nebula"], bonuses={"Nuclear": 2}))
50
+ Upgraders.append(buildings.Upgrader(name="Infernal Realm", mult=99, effect_add=["Fire", "Fire", "Fire"], effect_remove=["Wet"], bonuses={"Fire": 1.66}))
51
+ Upgraders.append(buildings.Upgrader(name="Ancient Gate", mult=120, effect_add=["Fire"], bonuses={"Ancient": 1.3}))
52
+
53
+ Processors.append(buildings.Processor(name="Copper Processor", mult=1.5))
54
+ Processors.append(buildings.Processor(name="Cardboard Boxes", mult=2, refuse=["Acid"]))
55
+ Processors.append(buildings.Processor(name="Golden Forge", mult=3, bonus={"Fire": 1.5}))
56
+ Processors.append(buildings.Processor(name="Magnet Staff", mult=5, bonus={"Magnetic": 1.5}))
57
+ Processors.append(buildings.Processor(name="Space Processor", mult=12, bonus={"Nebula": 1.5}, onlyaccept=["Nebula"]))
58
+ Processors.append(buildings.Processor(name="Mushroom Spores", mult=22, bonus={"Acid": 2.5}, onlyaccept=["Acid"]))
59
+ Processors.append(buildings.Processor(name="Vortex Shrine", mult=25, bonus={"Nuclear": 2.5},))
60
+ Processors.append(buildings.Processor(name="Subzero", mult=27, bonus={"Wet": 2}, refuse=["Fire"]))
61
+ Processors.append(buildings.Processor(name="Flying Isles", mult=41, bonus={"Aired": 2.5}))
62
+ Processors.append(buildings.Processor(name="Portal of Blocks", mult=63, bonus={"Fire": 2}, refuse=["Wet"]))
63
+ Processors.append(buildings.Processor(name="Hand of Ancient", mult=91, bonus={"Ancient": 1.5}, refuse=["Fire"]))
@@ -0,0 +1,316 @@
1
+ import asyncio
2
+ import copy
3
+ import os
4
+ import random
5
+ from concurrent.futures import ProcessPoolExecutor
6
+
7
+ from . import buildings
8
+ from . import definitions
9
+
10
+ #Gets the time between upgraders for destruction effects
11
+ def get_timings(upgrader_count):
12
+ timings = [2.5, 1.7, 3.1, 3.6, 2.5, 2.1, 2.5, 2.9, 3.3, 2.9, 2.5, 2.5, 2.4, 2.8, 5.2, 4.2, 1.0]
13
+ if upgrader_count <= 0:
14
+ return []
15
+ return timings[-upgrader_count:]
16
+
17
+ #Evaluates the estimated revenue/sec of a given layout
18
+ def calculate_factory(droppers, upgraders, processor):
19
+ time_table = get_timings(len(upgraders))
20
+ final_value = 0
21
+
22
+ for dropper in droppers:
23
+ item = dropper.drop()
24
+
25
+ for step, upgrader in enumerate(upgraders):
26
+ item = upgrader.upgrade(item)
27
+ item = item.update(time_table[step])
28
+ if item is None or item.value <= 0:
29
+ break
30
+
31
+ if item is not None and item.value > 0:
32
+ item = processor.process(item)
33
+ final_value += item.value
34
+
35
+ return final_value
36
+
37
+ #Returns the building object based on the name of the building
38
+ def get_building_by_name(name):
39
+ for dropper in definitions.Droppers:
40
+ if dropper.name == name:
41
+ return dropper
42
+ for upgrader in definitions.Upgraders:
43
+ if upgrader.name == name:
44
+ return upgrader
45
+ for processor in definitions.Processors:
46
+ if processor.name == name:
47
+ return processor
48
+ return None
49
+
50
+ #Gets buildings from raw text
51
+ def get_available_buildings(building_list):
52
+ #Words before - are the rarities
53
+ #Words after - is the building name
54
+ #\ns are the building list
55
+ available_droppers = []
56
+ available_upgraders = []
57
+ available_processors = []
58
+ dropper_count = 0
59
+ upgrader_count = 0
60
+ for building in building_list.split("\n"):
61
+ if building.startswith("#"):
62
+ continue
63
+ if building.startswith("@"):
64
+ count = building.split(" ")[-1]
65
+ if building.startswith("@Dr"):
66
+ dropper_count = int(count)
67
+ elif building.startswith("@Up"):
68
+ upgrader_count = int(count)
69
+ building = building.strip()
70
+ if building:
71
+ parts = building.split("-")
72
+ if len(parts) == 2:
73
+ rarity, name = (part.strip() for part in parts)
74
+ else:
75
+ rarity = "Standard"
76
+ name = parts[0].strip()
77
+ building_obj = get_building_by_name(name)
78
+ if building_obj:
79
+ building_obj = copy.deepcopy(building_obj)
80
+ building_obj.rarity = rarity
81
+ if isinstance(building_obj, buildings.Dropper):
82
+ available_droppers.append(building_obj)
83
+ elif isinstance(building_obj, buildings.Upgrader):
84
+ available_upgraders.append(building_obj)
85
+ elif isinstance(building_obj, buildings.Processor):
86
+ available_processors.append(building_obj)
87
+ return available_droppers, available_upgraders, available_processors, dropper_count, upgrader_count
88
+
89
+
90
+ #---GENETIC ALGORITHM---
91
+
92
+ def _random_candidate(droppers, upgraders, processors, dropper_count, upgrader_count):
93
+ if not processors:
94
+ return None
95
+
96
+ return (
97
+ tuple(random.sample(droppers, min(dropper_count, len(droppers)))),
98
+ tuple(random.sample(upgraders, min(upgrader_count, len(upgraders)))),
99
+ random.choice(processors),
100
+ )
101
+
102
+ def _evaluate(candidate):
103
+ droppers, upgraders, processor = candidate
104
+ return calculate_factory(droppers, upgraders, processor)
105
+
106
+ def _evaluate_batch(candidates):
107
+ return [_evaluate(candidate) for candidate in candidates]
108
+
109
+ def _mutate(candidate, droppers, upgraders, processors):
110
+ selected_droppers, selected_upgraders, processor = candidate
111
+ selected_droppers = list(selected_droppers)
112
+ selected_upgraders = list(selected_upgraders)
113
+
114
+ if selected_droppers and random.random() < 0.35:
115
+ index = random.randrange(len(selected_droppers))
116
+ available = [
117
+ dropper
118
+ for dropper in droppers
119
+ if dropper not in selected_droppers or dropper is selected_droppers[index]
120
+ ]
121
+ if available:
122
+ selected_droppers[index] = random.choice(available)
123
+
124
+ if selected_upgraders and random.random() < 0.35:
125
+ index = random.randrange(len(selected_upgraders))
126
+ available = [
127
+ upgrader
128
+ for upgrader in upgraders
129
+ if upgrader not in selected_upgraders or upgrader is selected_upgraders[index]
130
+ ]
131
+ if available:
132
+ selected_upgraders[index] = random.choice(available)
133
+
134
+ if len(selected_upgraders) > 1 and random.random() < 0.5:
135
+ first, second = random.sample(range(len(selected_upgraders)), 2)
136
+ selected_upgraders[first], selected_upgraders[second] = selected_upgraders[second], selected_upgraders[first]
137
+
138
+ if random.random() < 0.2:
139
+ processor = random.choice(processors)
140
+
141
+ return tuple(selected_droppers), tuple(selected_upgraders), processor
142
+
143
+ def _describe(candidate, value):
144
+ selected_droppers, selected_upgraders, processor = candidate
145
+ return {
146
+ "droppers": [
147
+ f"{dropper.rarity} {dropper.name}"
148
+ for dropper in selected_droppers
149
+ ],
150
+ "upgraders": [
151
+ f"{upgrader.rarity} {upgrader.name}"
152
+ for upgrader in selected_upgraders
153
+ ],
154
+ "processor": f"{processor.rarity} {processor.name}",
155
+ "value": value,
156
+ }
157
+
158
+ async def optimize_buildings(
159
+ droppers,
160
+ upgraders,
161
+ processors,
162
+ dropper_count=None,
163
+ upgrader_count=None,
164
+ generations=100,
165
+ population_size=64,
166
+ elite_size=8,
167
+ seed=None,
168
+ random_fraction=0.1,
169
+ stagnation_patience=10,
170
+ max_random_fraction=0.5,
171
+ progress_every=None,
172
+ target_value=None,
173
+ ):
174
+ if not droppers or not upgraders or not processors:
175
+ raise ValueError("at least one dropper, upgrader, and processor is required")
176
+ if dropper_count is None:
177
+ dropper_count = len(droppers)
178
+ if upgrader_count is None:
179
+ upgrader_count = len(upgraders)
180
+ if not 0 <= random_fraction <= 1:
181
+ raise ValueError("random_fraction must be between 0 and 1")
182
+ if stagnation_patience < 1:
183
+ raise ValueError("stagnation_patience must be positive")
184
+ if not random_fraction <= max_random_fraction <= 1:
185
+ raise ValueError("max_random_fraction must be between random_fraction and 1")
186
+ if seed is not None:
187
+ random.seed(seed)
188
+
189
+ population = [
190
+ _random_candidate(droppers, upgraders, processors, dropper_count, upgrader_count)
191
+ for _ in range(max(population_size, elite_size))
192
+ ]
193
+ best_candidate = None
194
+ best_value = float("-inf")
195
+ stagnant_generations = 0
196
+ immigrant_fraction = random_fraction
197
+ score_cache = {}
198
+
199
+ loop = asyncio.get_running_loop()
200
+ with ProcessPoolExecutor() as process_executor:
201
+ for generation in range(1, generations + 1):
202
+ uncached_candidates = list(dict.fromkeys(population))
203
+ uncached_candidates = [
204
+ candidate for candidate in uncached_candidates
205
+ if candidate not in score_cache
206
+ ]
207
+ if uncached_candidates:
208
+ worker_count = min(len(uncached_candidates), os.cpu_count() or 1)
209
+ chunk_size = max(
210
+ 1,
211
+ (len(uncached_candidates) + worker_count * 2 - 1) // (worker_count * 2),
212
+ )
213
+ candidate_batches = [
214
+ uncached_candidates[start:start + chunk_size]
215
+ for start in range(0, len(uncached_candidates), chunk_size)
216
+ ]
217
+ tasks = [
218
+ loop.run_in_executor(process_executor, _evaluate_batch, batch)
219
+ for batch in candidate_batches
220
+ ]
221
+ score_batches = await asyncio.gather(*tasks)
222
+ uncached_scores = [score for batch in score_batches for score in batch]
223
+ score_cache.update(zip(uncached_candidates, uncached_scores))
224
+
225
+ scores = [score_cache[candidate] for candidate in population]
226
+
227
+ ranked = sorted(zip(scores, population), key=lambda result: result[0], reverse=True)
228
+ if ranked[0][0] > best_value:
229
+ best_value, best_candidate = ranked[0]
230
+ stagnant_generations = 0
231
+ immigrant_fraction = random_fraction
232
+ else:
233
+ stagnant_generations += 1
234
+ if stagnant_generations >= stagnation_patience:
235
+ immigrant_fraction = min(max_random_fraction, immigrant_fraction + 0.1)
236
+
237
+ if progress_every and (generation % progress_every == 0 or generation == 1):
238
+ print(
239
+ f"Generation {generation}: best value = {best_value} "
240
+ f"(immigrants: {immigrant_fraction:.0%})"
241
+ )
242
+ print(_describe(best_candidate, best_value))
243
+
244
+ if target_value is not None and best_value >= target_value:
245
+ break
246
+
247
+ elites = [candidate for _, candidate in ranked[:max(1, elite_size)]]
248
+ population = elites[:]
249
+
250
+ while len(population) < population_size:
251
+ if random.random() < immigrant_fraction:
252
+ population.append(
253
+ _random_candidate(droppers, upgraders, processors, dropper_count, upgrader_count)
254
+ )
255
+ else:
256
+ parent = random.choice(elites)
257
+ population.append(_mutate(parent, droppers, upgraders, processors))
258
+
259
+ await asyncio.sleep(0)
260
+
261
+ return _describe(best_candidate, best_value)
262
+
263
+ #----------//-----------
264
+
265
+
266
+ #Runs the optimization process with the given parameters and returns the best result
267
+ def run(path, dropper_count, upgrader_count, runs=10, run_stagnation_patience=8):
268
+ with open(path, "r") as f:
269
+ building_list = f.read()
270
+ droppers, upgraders, processors, dropper_count_from_txt, upgrader_count_from_txt = get_available_buildings(building_list)
271
+ if not droppers or not upgraders or not processors:
272
+ raise ValueError(
273
+ "available buildings must contain at least one dropper, upgrader, and processor"
274
+ )
275
+ dropper_count = dropper_count_from_txt if dropper_count == 0 else int(dropper_count)
276
+ upgrader_count = upgrader_count_from_txt if upgrader_count == 0 else int(upgrader_count)
277
+
278
+ runs = abs(int(runs))
279
+ if run_stagnation_patience < 1:
280
+ raise ValueError("run_stagnation_patience must be positive")
281
+
282
+ best_result = None
283
+ stagnant_runs = 0
284
+ for run_number in range(1, runs + 1):
285
+ result = asyncio.run(
286
+ optimize_buildings(
287
+ droppers,
288
+ upgraders,
289
+ processors,
290
+ dropper_count=dropper_count,
291
+ upgrader_count=upgrader_count,
292
+ generations=150,
293
+ population_size=64,
294
+ elite_size=8,
295
+ random_fraction=0.1,
296
+ progress_every=0,
297
+ )
298
+ )
299
+ if best_result is None or result["value"] > best_result["value"]:
300
+ best_result = result
301
+ stagnant_runs = 0
302
+ else:
303
+ stagnant_runs += 1
304
+ print(f"Run {run_number}/{runs}: {result['value']}")
305
+ if stagnant_runs >= run_stagnation_patience:
306
+ print(f"Stopping after {stagnant_runs} runs without improvement.")
307
+ break
308
+
309
+ return best_result
310
+
311
+ if __name__ == "__main__":
312
+ AVAILABLE_BUILDINGS_PATH = "available_buildings.txt"
313
+ DROPPER_COUNT = 5
314
+ UPGRADER_COUNT = 7
315
+ RUNS = 20
316
+ print(run(AVAILABLE_BUILDINGS_PATH, DROPPER_COUNT, UPGRADER_COUNT, RUNS))