Skip to content

brayvid/skyrim-alchemy-optimizer

Repository files navigation

Skyrim Alchemy Optimizer

Make the most of the ingredients you have. Maximize total magnitude (essentially in-game value) with integer linear programming in scipy.

import numpy as np
import pandas as pd
from scipy.optimize import milp, Bounds, LinearConstraint

Read in ingredients and recipes

Uses local files "ingredients_have.csv" and "recipes_can_make.csv"

I made my CSVs using this helpful spreadsheet: https://docs.google.com/spreadsheets/d/1010C6ltqv7apuBoNYuFIFSBZER4YI03Y54kIsoKs5RI/edit?usp=sharing

# Ingredients we have with quantity on hand
ingredients = pd.read_csv('ingredients_have.csv');ingredients
Ingredient Quantity
0 Blisterwort 4
1 Blue Butterfly Wing 4
2 Blue Dartwing 1
3 Blue Mountain Flower 24
4 Bone Meal 5
5 Butterfly Wing 6
6 Canis Root 2
7 Creep Cluster 1
8 Deathbell 6
9 Dragons Tongue 5
10 Ectoplasm 5
11 Elves Ear 10
12 Fire Salts 1
13 Fly Amanita 1
14 Frost Mirriam 3
15 Garlic 7
16 Giant Lichen 2
17 Glow Dust 2
18 Hagraven Feathers 2
19 Histcarp 2
20 Honeycomb 4
21 Ice Wraith Teeth 2
22 Imp Stool 2
23 Lavender 17
24 Luna Moth Wing 4
25 Mora Tapinella 2
26 Mudcrab Chitin 2
27 Nightshade 7
28 Nirnroot 3
29 Nordic Barnacle 2
30 Orange Dartwing 2
31 Purple Mountain Flower 15
32 Red Mountain Flower 1
33 River Betty 2
34 Rock Warbler Egg 3
35 Salt Pile 14
36 Scaly Pholiota 1
37 Skeever Tail 2
38 Slaughterfish Scales 3
39 Snowberries 6
40 Spider Egg 8
41 Spriggan Sap 3
42 Swamp Fungal Pod 2
43 Taproot 2
44 Thistle Branch 2
45 Torchbug Thorax 3
46 Troll Fat 3
47 Tundra Cotton 7
48 Vampire Dust 1
49 Void Salts 1
50 White Cap 6
# Potions list with magnitude and ingredient names (1,2 + optional 3rd)
recipes = pd.read_csv('recipes_can_make.csv')
recipes = recipes[recipes['Magnitude'] > 0]
recipes[['Magnitude','Ingredient 1','Ingredient 2','Ingredient 3','MyPotionID']].head(50)
Magnitude Ingredient 1 Ingredient 2 Ingredient 3 MyPotionID
0 159 Blue Dartwing Blue Mountain Flower Glow Dust 3028
1 156 Blue Dartwing Blue Mountain Flower Nightshade 3037
2 156 Blue Dartwing Blue Mountain Flower Spider Egg 3045
3 156 Blue Dartwing Blue Mountain Flower Spriggan Sap 3046
4 113 Blisterwort Blue Butterfly Wing Blue Mountain Flower 2130
5 113 Blue Butterfly Wing Blue Mountain Flower Rock Warbler Egg 2680
6 112 Frost Mirriam Histcarp Purple Mountain Flower 10371
7 110 Blue Butterfly Wing Blue Mountain Flower Butterfly Wing 2666
8 110 Blue Butterfly Wing Blue Mountain Flower Imp Stool 2677
9 110 Blue Butterfly Wing Blue Mountain Flower Swamp Fungal Pod 2684
10 110 Glow Dust Nightshade River Betty 11628
11 109 Blisterwort Blue Mountain Flower Spriggan Sap 2210
12 109 Blue Butterfly Wing Bone Meal Spriggan Sap 2703
13 109 Butterfly Wing Glow Dust Nightshade 4738
14 109 Creep Cluster Ectoplasm Histcarp 6302
15 109 Creep Cluster Histcarp Red Mountain Flower 6550
16 109 Creep Cluster River Betty Skeever Tail 6725
17 109 Nightshade River Betty Spriggan Sap 14461
18 108 Blisterwort Blue Butterfly Wing Spriggan Sap 2154
19 108 Blisterwort Blue Mountain Flower Spider Egg 2209
20 108 Blue Butterfly Wing Blue Mountain Flower Bone Meal 2665
21 108 Blue Butterfly Wing Blue Mountain Flower Canis Root 2667
22 108 Blue Butterfly Wing Blue Mountain Flower Nirnroot 2679
23 108 Blue Butterfly Wing Blue Mountain Flower Spider Egg 2682
24 108 Blue Butterfly Wing Bone Meal Glow Dust 2693
25 108 Blue Butterfly Wing Bone Meal Nightshade 2700
26 108 Blue Butterfly Wing Bone Meal Spider Egg 2702
27 108 Blue Butterfly Wing Glow Dust Hagraven Feathers 2860
28 108 Blue Butterfly Wing Hagraven Feathers Spider Egg 2908
29 108 Blue Butterfly Wing Lavender Spider Egg 2969
30 108 Blue Dartwing Glow Dust Nightshade 3203
31 108 Blue Mountain Flower Bone Meal Spider Egg 3416
32 108 Blue Mountain Flower Butterfly Wing Glow Dust 3429
33 108 Blue Mountain Flower Glow Dust Hagraven Feathers 3628
34 108 Blue Mountain Flower Glow Dust Swamp Fungal Pod 3643
35 108 Blue Mountain Flower Rock Warbler Egg Spider Egg 3780
36 108 Glow Dust Hagraven Feathers Nightshade 11513
37 108 Glow Dust Luna Moth Wing Nightshade 11598
38 108 Glow Dust Nightshade Nordic Barnacle 11624
39 108 Glow Dust Nightshade Snowberries 11631
40 108 Glow Dust Nightshade Swamp Fungal Pod 11632
41 107 Blisterwort Spider Egg Spriggan Sap 2648
42 107 Blue Butterfly Wing Canis Root Spider Egg 2724
43 107 Blue Mountain Flower Imp Stool Nightshade 3716
44 107 Canis Root Spider Egg Spriggan Sap 5163
45 107 Creep Cluster Ectoplasm Skeever Tail 6318
46 107 Creep Cluster Frost Mirriam Purple Mountain Flower 6392
47 107 Creep Cluster Frost Mirriam Red Mountain Flower 6393
48 107 Creep Cluster Frost Mirriam Taproot 6402
49 107 Creep Cluster Frost Mirriam White Cap 6406

Create recipe matrix A in Ax <= b

One row for each ingredient, one column for each potion. "1" indicates the ingredient is used in the potion.

# Boolean matrix A says what ingredients are in what recipes
A = pd.DataFrame(0, index=range(len(ingredients)),columns=range(len(recipes)))
for i in range(len(recipes)):
  if ingredients.iloc[ingredients["Ingredient"].str.find(recipes.loc[i, "Ingredient 1"]).idxmax()]["Quantity"] > 0:
    A.iloc[ingredients["Ingredient"].str.find(recipes.loc[i, "Ingredient 1"]).idxmax(), i] = 1
  if ingredients.iloc[ingredients["Ingredient"].str.find(recipes.loc[i, "Ingredient 2"]).idxmax()]["Quantity"] > 0:
    A.iloc[ingredients["Ingredient"].str.find(recipes.loc[i, "Ingredient 2"]).idxmax(), i] = 1
  if not pd.isnull(recipes.loc[i, "Ingredient 3"]):
    if ingredients.iloc[ingredients["Ingredient"].str.find(recipes.loc[i, "Ingredient 3"]).idxmax()]["Quantity"] > 0:
      A.iloc[ingredients["Ingredient"].str.find(recipes.loc[i, "Ingredient 3"]).idxmax(), i] = 1
A
0 1 2 3 4 5 6 7 8 9 ... 2375 2376 2377 2378 2379 2380 2381 2382 2383 2384
0 0 0 0 0 1 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
1 0 0 0 0 1 1 0 1 1 1 ... 0 0 0 0 0 0 0 0 0 0
2 1 1 1 1 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
3 1 1 1 1 1 1 0 1 1 1 ... 0 0 0 0 0 0 0 0 0 0
4 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
5 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
6 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
7 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
8 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
9 0 0 0 0 0 0 0 0 0 0 ... 1 0 0 0 0 0 0 0 0 0
10 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
11 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
12 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
13 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
14 0 0 0 0 0 0 1 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
15 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
16 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
17 1 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
18 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
19 0 0 0 0 0 0 1 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
20 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
21 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 1 0 0 0
22 0 0 0 0 0 0 0 0 1 0 ... 0 0 0 0 0 0 0 0 0 0
23 0 0 0 0 0 0 0 0 0 0 ... 0 1 1 1 0 0 0 1 0 0
24 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 1 0
25 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
26 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
27 0 1 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
28 0 0 0 0 0 0 0 0 0 0 ... 0 1 0 0 1 0 1 1 1 1
29 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
30 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
31 0 0 0 0 0 0 1 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
32 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
33 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
34 0 0 0 0 0 1 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
35 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
36 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
37 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
38 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
39 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 1 0 0 0 0
40 0 0 1 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
41 0 0 0 1 0 0 0 0 0 0 ... 0 0 0 0 0 1 0 0 0 0
42 0 0 0 0 0 0 0 0 0 1 ... 0 0 0 0 0 0 0 0 0 0
43 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
44 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
45 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
46 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
47 0 0 0 0 0 0 0 0 0 0 ... 1 0 1 0 1 0 1 0 1 1
48 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 1 0 1
49 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 1 0 0 0 0 0 0
50 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0

51 rows × 2385 columns

Set up optimization variables

find x to minimize f.x with Ax <= b, x >= lb

f = -1 * magnitude, b = qty of each ingredient on hand

# Objective function f.x to minimize
f = np.array(-1 * recipes['Magnitude'],dtype=int); # f = -1*value so that minimizing f.x maximizes total value
# Bounds
b_max = np.array(ingredients['Quantity'],dtype=int) # Cannot use more than we have on hand
x_lb = np.zeros(shape=len(recipes)) # Cannot use less than 0
# milp parameters
bounds = Bounds(lb=x_lb)
constraint = LinearConstraint(A, ub=b_max)
integrality = np.ones(shape=len(recipes),dtype=int) # All x should be integers

Perform optimization with scipy.optimize.milp

# Perform optimization
res = milp(c=f, integrality=integrality, bounds=bounds, constraints=constraint)

Display recommended potions to make

# Display the potions we should make to maximize magnitude where the last column is quantity to make
total_magnitude = int(-res.fun)
num_potions = int(sum(res.x))
indices_to_make = np.nonzero(res.x > 0)
to_make_df = recipes.iloc[indices_to_make].copy()
to_make_df.loc[:,'QtyToMake'] = res.x[indices_to_make].astype(int)
to_make_df[['Magnitude','Type','Ingredient 1','Ingredient 2','Ingredient 3','MyPotionID','QtyToMake']].head(50)
Magnitude Type Ingredient 1 Ingredient 2 Ingredient 3 MyPotionID QtyToMake
6 112 Mixed Frost Mirriam Histcarp Purple Mountain Flower 10371 2
7 110 Mixed Blue Butterfly Wing Blue Mountain Flower Butterfly Wing 2666 4
11 109 Mixed Blisterwort Blue Mountain Flower Spriggan Sap 2210 3
19 108 Mixed Blisterwort Blue Mountain Flower Spider Egg 2209 1
31 108 Mixed Blue Mountain Flower Bone Meal Spider Egg 3416 4
33 108 Mixed Blue Mountain Flower Glow Dust Hagraven Feathers 3628 1
34 108 Mixed Blue Mountain Flower Glow Dust Swamp Fungal Pod 3643 1
35 108 Mixed Blue Mountain Flower Rock Warbler Egg Spider Egg 3780 1
45 107 Mixed Creep Cluster Ectoplasm Skeever Tail 6318 1
57 107 Mixed Frost Mirriam Purple Mountain Flower Skeever Tail 10519 1
103 105 Mixed Blue Mountain Flower Lavender Nightshade 3749 7
104 105 Mixed Blue Mountain Flower Lavender Spider Egg 3755 2
303 59 Potion Blue Dartwing Swamp Fungal Pod NaN 3388 1
334 57 Mixed Deathbell Salt Pile Taproot 8001 1
361 55 Poison River Betty Salt Pile Troll Fat 15006 2
367 53 Poison Deathbell Nirnroot Salt Pile 7937 2
370 53 Poison Deathbell Salt Pile Troll Fat 8004 1
379 50 Poison Deathbell Salt Pile NaN 8006 2
384 17 Mixed Elves Ear Fire Salts Salt Pile 8930 1
391 16 Potion Dragons Tongue Fly Amanita Scaly Pholiota 8094 1
397 15 Potion Garlic Taproot Vampire Dust 11060 1
400 14 Mixed Ectoplasm Giant Lichen Void Salts 8576 1
443 12 Mixed Canis Root Imp Stool Rock Warbler Egg 5088 2
461 12 Mixed Luna Moth Wing Nordic Barnacle Orange Dartwing 14094 1
465 12 Potion Dragons Tongue Elves Ear Mora Tapinella 8058 2
468 12 Potion Honeycomb Purple Mountain Flower Slaughterfish Scales 12905 1
469 12 Potion Mudcrab Chitin Purple Mountain Flower Thistle Branch 14343 2
470 11 Mixed Ectoplasm Red Mountain Flower NaN 8873 1
493 11 Mixed Dragons Tongue Elves Ear White Cap 8067 2
516 11 Mixed Elves Ear Snowberries White Cap 9130 2
582 10 Mixed Elves Ear Ice Wraith Teeth White Cap 9037 2
658 9 Mixed Bone Meal Lavender Nirnroot 4095 1
704 9 Mixed Purple Mountain Flower Snowberries Torchbug Thorax 14933 3
760 9 Potion Ectoplasm Elves Ear Tundra Cotton 8459 1
765 9 Potion Ectoplasm Giant Lichen Tundra Cotton 8575 1
804 9 Potion Garlic Lavender Luna Moth Wing 10953 1
806 9 Potion Garlic Lavender Salt Pile 10961 5
855 9 Potion Honeycomb Purple Mountain Flower Tundra Cotton 12911 3
879 8 Mixed Luna Moth Wing Nordic Barnacle NaN 14098 1
1017 8 Mixed Hagraven Feathers Lavender Luna Moth Wing 12101 1
1144 8 Potion Orange Dartwing Purple Mountain Flower Snowberries 14612 1
1443 7 Potion Purple Mountain Flower Slaughterfish Scales Tundra Cotton 14922 2
print(f"To maximize magnitude and therefore value, create {num_potions} potions of the {len(to_make_df)} unique types listed above for a total magnitude of {total_magnitude}.")
To maximize magnitude and therefore value, create 76 potions of the 42 unique types listed above for a total magnitude of 3905.