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.
- rngoptimizer-0.1.1/PKG-INFO +86 -0
- rngoptimizer-0.1.1/README.md +79 -0
- rngoptimizer-0.1.1/pyproject.toml +14 -0
- rngoptimizer-0.1.1/pyproject.toml.orig +14 -0
- rngoptimizer-0.1.1/src/rngoptimizer/__init__.py +0 -0
- rngoptimizer-0.1.1/src/rngoptimizer/__main__.py +5 -0
- rngoptimizer-0.1.1/src/rngoptimizer/buildings.py +140 -0
- rngoptimizer-0.1.1/src/rngoptimizer/cli.py +32 -0
- rngoptimizer-0.1.1/src/rngoptimizer/definitions.py +63 -0
- rngoptimizer-0.1.1/src/rngoptimizer/engine.py +316 -0
|
@@ -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><Building> 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><Building> 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,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))
|