What's the most efficient way of generating all possible combinations of skyrim (PC Game) potions?

后端 未结 3 1196
一个人的身影
一个人的身影 2021-01-12 01:38

So each ingredient has 4 effects http://www.uesp.net/wiki/Skyrim:Ingredients

If I combine two ingredients. The potions will have the bonus effects of where the two s

相关标签:
3条回答
  • 2021-01-12 02:18

    Here's some c#.

    It makes a lookup of the ingredient by the name of potential effects. Then it uses that lookup to determine which ingredients can match the current recipe. Finally, it generates recipes and discards duplicates as it generates them by using a hashset.

    Complete code (incomplete ingredient list)

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    
    namespace Combinations
    {
    
        public class Ingredient
        {
            public List<string> Effects { get; set; }
            public string Name { get; set; }
            public Ingredient(string name, params string[] effects)
            { Name = name; Effects = new List<string>(effects); }
        }
    
        public class Recipe
        {
            public List<Ingredient> Ingredients {get;set;}
            public Recipe(IEnumerable<Ingredient> ingredients)
            { Ingredients = ingredients.OrderBy(x => x.Name).ToList(); }
            public override string ToString()
            { return string.Join("|", Ingredients.Select(x => x.Name).ToArray()); }
        }
    
        class Program
        {
            static void Main(string[] args)
            {
                List<Ingredient> source = GetIngredients();
    
                ILookup<string, Ingredient> byEffect = (
                    from i in source
                    from e in i.Effects
                    select new { i, e }
                    ).ToLookup(x => x.e, x => x.i);
    
                List<Recipe> oneIng = source.Select(x => new Recipe(new Ingredient[] { x })).ToList();
                List<Recipe> twoIng = oneIng.SelectMany(r => GenerateRecipes(r, byEffect)).ToList();
                List<Recipe> threeIng = twoIng.SelectMany(r => GenerateRecipes(r, byEffect)).ToList();
    
                Console.WriteLine(twoIng.Count);
                foreach(Recipe r in twoIng) { Console.WriteLine(r); }
                Console.WriteLine(threeIng.Count);
                foreach(Recipe r in threeIng) { Console.WriteLine(r); }
                Console.ReadLine();
            }
    
            static IEnumerable<Recipe> GenerateRecipes(Recipe recipe, ILookup<string, Ingredient> byEffect)
            {
                IEnumerable<string> knownEffects = recipe.Ingredients
                    .SelectMany(i => i.Effects)
                    .Distinct();
    
                IEnumerable<Ingredient> matchingIngredients = knownEffects
                    .SelectMany(e => byEffect[e])
                    .Distinct()
                    .Where(i => !recipe.Ingredients.Contains(i));
    
                foreach(Ingredient i in matchingIngredients)
                {
                    List<Ingredient> newRecipeIngredients = recipe.Ingredients.ToList();
                    newRecipeIngredients.Add(i);
                    Recipe result = new Recipe(newRecipeIngredients);
                    string key = result.ToString();
                    if (!_observedRecipes.Contains(key))
                    {
                        _observedRecipes.Add(key);
                        yield return result;
                    }
                }
            }
    
            static HashSet<string> _observedRecipes = new HashSet<string>();
    
            static List<Ingredient> GetIngredients()
            {
                List<Ingredient> result = new List<Ingredient>()
                {
                    new Ingredient("Abecean Longfin", "Weakness to Frost", "Fortify Sneak", "Weakness to Poison", "Fortify Restoration"),
                    new Ingredient("Bear Claws", "Restore Stamina", "Fortify Health", "Fortify One-handed", "Damage Magicka Regen"),
                    new Ingredient("Bee", "Restore Stamina", "Ravage Stamina", "Regenerate Stamina", "Weakness to Shock"),
                    new Ingredient("Beehive Husk", "Resist Poison", "Fortify Light Armor", "Fortify Sneak", "Fortify Destruction"),
                    new Ingredient("Bleeding Crown", "Weakness to Fire", "Fortify Block", "Weakness to Poison", "Resist Magic"),
                    new Ingredient("Blisterwort", "Damage Stamina", "Frenzy", "Restore Health", "Fortify Smithing"),
                    new Ingredient("Blue Butterfly Wing", "Damage Stamina", "Fortify Conjuration", "Damage Magicka Regen", "Fortify Enchanting"),
                    new Ingredient("Blue Dartwing", "Resist Shock", "Fortify Pickpocket", "Restore Health", "Damage Magicka Regen"),
                    new Ingredient("Blue Mountain Flower", "Restore Health", "Fortify Conjuration", "Fortify Health", "Damage Magicka Regen"),
                    new Ingredient("Bone Meal", "Damage Stamina", "Resist Fire", "Fortify Conjuration", "Ravage Stamina"),
                };
    
                return result;
            }
        }
    }
    
    0 讨论(0)
  • 2021-01-12 02:18

    So I had the thought, "What's the most cost-efficient way to gain all ingredient knowledge?" i.e. I want all the ingredients' effects to be known in game, but I don't want to spend twelve Daedra Hearts to do it.

    If you use a traditional search solution (A*, etc.) the branching factor is horrific (there are 22000ish possible effective potions). I tried an annealing approach but wasn't getting good results. I eventually went with an informed search; it's subobptimal but it'll get the job done.

    Here's the import-and-combinatorize code: puts "Importing ingredients..."

    fd = File::open('ingr_weighted.txt', 'r')
    dbtext = fd.read
    fd.close
    ingredients = []
    cvg = []
    id = 0
    dbtext.each_line { |line|
        infos = line.split("\t")
        ingredients << {:id => id, :name => infos[0], :effects => [infos[2],infos[3],infos[4],infos[5]],
                        :eff1 => infos[2], :eff2 => infos[3], :eff3 => infos[4], :eff4 => infos[5],
                        :weight => infos[6], :cost => infos[7].to_i+1}
        id += 1
        cvg << [false, false, false, false]
    }
    
    
    puts "Building potions..."
    potions = []
    id = 0
    for a in 0..ingredients.length-2
        for b in a+1..ingredients.length-1
            # First try two-ingredient potions
            uses = ingredients[a][:effects] & ingredients[b][:effects]
            cost = ingredients[a][:cost] + ingredients[b][:cost]
            if (uses.length > 0)
                coverage = [ingredients[a][:effects].map{|x| uses.include? x},
                            ingredients[b][:effects].map{|x| uses.include? x}]
                potions << {:id => id, :effects => uses, :coverage => coverage, :ingredients => [a, b], :cost => cost}
                id = id + 1
            end
            # Next create three-ingredient potions
            for c in b+1..ingredients.length-1
                uses =  ingredients[a][:effects] & ingredients[b][:effects] |
                        ingredients[a][:effects] & ingredients[c][:effects] |
                        ingredients[b][:effects] & ingredients[c][:effects]
                cost = ingredients[a][:cost] + ingredients[b][:cost] + ingredients[c][:cost]
                if (uses.length > 0)
                    coverage = [ingredients[a][:effects].map{|x| uses.include? x},
                                ingredients[b][:effects].map{|x| uses.include? x},
                                ingredients[c][:effects].map{|x| uses.include? x}]
                    # Prune potions that contain a superfluous ingredient
                    if (coverage.inject(true) { |cum, cvgn|
                                                cum = cum && cvgn.inject { |cum2,ef| cum2 = cum2 || ef}
                                                } )
                        potions << {:id => id, :effects => uses, :coverage => coverage, :ingredients => [a,b,c], :cost => cost}
                        id = id + 1
                    end
                end
            end
        end
    end
    # 22451
    puts "#{potions.count} potions generated!"
    puts "Searching..."
    

    The input file is copy-pasta'd from one of the wikis, so if you're using a mod or something you can drop right in. From here you have all the data imported and the effective potions generated, so do what you want!

    For my original purpose (efficient "learning"), I used the following code. Basically it starts with the most expensive remaining ingredient, exhausts its effects as cheaply as possible, then moves on down. Some rarer ingredients are cheap (forex. human flesh), so I "goosed" my data file to artificially inflate their value. All told, this program runs in about 45 minutes on my laptop, but it is an interpreted language...

    puts "Searching..."
    
    valueChain = ingredients.sort {|a,b| a[:cost] <=> b[:cost]};
    
    while (valueChain.count > 0)
        # Grab highest-value ingredient left
        ingr = valueChain.pop;
    
        # Initialize the coverage and potion sub-set
        pots = potions.each_with_object([]) { |pot, list| list << pot if pot[:ingredients].include? ingr[:id] }
        puts "#{ingr[:name]}:\t#{pots.count} candidates"
        if (cvg[ingr[:id]].all?)
            puts "Already finished"
            next
        end
    
        # Find the cheapest combination that completes our coverage situation
        sitch = {:coverage => cvg[ingr[:id]].dup, :solution => [], :cost => 0}
        best = nil;
        working = []
        working << sitch
        while (working.count != 0)
            parent = working.shift
            pots.each { |pot|
                node = {:coverage => parent[:coverage].zip(pot[:coverage][pot[:ingredients].index(ingr[:id])]).map {|a,b| a || b},
                        :cost => parent[:cost] + pot[:cost],
                        :solution => parent[:solution].dup << pot[:id]}
    
                # This node is useful if its cost is less than the current-best
                if node[:coverage] == [true,true,true,true]
                    if (!best || best[:cost] > node[:cost])
                        best = node
                    end
                elsif node[:solution].count < 4
                    if (!best || best[:cost] > node[:cost])
                        working << node
                    end
                end
            }
        end
    
        # Merge our selected solution into global coverage
        best[:solution].each{ |pIndex|
            potions[pIndex][:ingredients].each_with_index { |ingID, index|
                cvg[ingID] = cvg[ingID].zip(potions[pIndex][:coverage][index]).map {|x,y| x || y}
            }
        }
    
        # Report the actual potions chosen
        best[:solution].each { |pIndex|
            print "\tPotion #{pIndex}"
            potions[pIndex][:ingredients].each { |iIndex|
                print "\t#{ingredients[iIndex][:name]}"
            }
            print "\n"
        }
    #   IRB.start_session(Kernel.binding)
    end
    
    0 讨论(0)
  • 2021-01-12 02:23

    Sounds like a job for everybody's favorite programming language, R!

    library(XML)
    tables <- readHTMLTable('http://www.uesp.net/wiki/Skyrim:Ingredients', 
        stringsAsFactors=FALSE)
    potions <- tables[[1]]
    twoway <- data.frame(t(combn(potions$Name,2)))
    threeway <- data.frame(t(combn(potions$Name,3)))
    

    BAM!

    > head(twoway)
                   X1                  X2
    1 Abecean Longfin          Bear Claws
    2 Abecean Longfin                 Bee
    3 Abecean Longfin        Beehive Husk
    4 Abecean Longfin      Bleeding Crown
    5 Abecean Longfin         Blisterwort
    6 Abecean Longfin Blue Butterfly Wing
    > head(threeway)
                   X1         X2                  X3
    1 Abecean Longfin Bear Claws                 Bee
    2 Abecean Longfin Bear Claws        Beehive Husk
    3 Abecean Longfin Bear Claws      Bleeding Crown
    4 Abecean Longfin Bear Claws         Blisterwort
    5 Abecean Longfin Bear Claws Blue Butterfly Wing
    6 Abecean Longfin Bear Claws       Blue Dartwing
    

    Use the write.csv command to save the tables as csv files.

    /Edit: To explain what I'm doing: The XML package contains the readHTMLTable function, which pulls all the html tables from a website as data.frames and saves them as a list. The first table in this list is the one we want. The combn function finds all the 2-way, 3-way, and n way combinations of potion names, and returns the result as a matrix. I use the t function to transpose this matrix, so each combination is one row, and then convert it to a data frame. This easily extends to combinations of n ingredients.

    /Edit 2: I wrote a function to save the n-way table to a user-specified csv file. I also re-worked it a bit, because transposing huge matricies is computationally expensive. This version should allow you to calculate the 4-way table, although it takes a long time and I don't know if it's relevant to the game.

    nway <- function(n, filepath, data=potions) {
        nway <- combn(data$Name, n, simplify = FALSE)
        nway <- do.call(rbind,nway)
        write.csv(nway,filepath, row.names=FALSE)
    }
    nway(4,'~/Desktop/4way.csv')
    

    /Edit 3: Here's some code to find the actual working potions. It's not very efficient and can probably be greatly improved:

    #Given an ingredient, lookup effects
    findEffects <- function(Name) { #Given a name, lookup effects
        potions[potions$Name==Name,3:6]
    }
    
    #2-way potions
    intersectTwoEffects <- function(x) {
        Effects1 <- findEffects(x[1])
        Effects2 <- findEffects(x[2])
        Effects <- unlist(intersect(Effects1,Effects2))
        Effects <- c(x[1],x[2],Effects)
        length(Effects) <- 6
        names(Effects) <- NULL
        c(Effects,sum(is.na(Effects)))
    
    }
    twoway <- lapply(twoway,intersectTwoEffects)
    twoway <- do.call(rbind,twoway)
    twoway <- twoway[twoway[,7]<4,-7] #remove combos with no effect
    write.csv(twoway,'~/Desktop/twoway.csv',row.names=FALSE)
    
    #3-way potions
    intersectThreeEffects <- function(x) {
        Effects1 <- findEffects(x[1])
        Effects2 <- findEffects(x[2])
        Effects3 <- findEffects(x[3])
        Effects <- c(intersect(Effects1,Effects2),intersect(Effects1,Effects3),intersect(Effects2,Effects3))
        Effects <- unlist(unique(Effects))
        Effects <- c(x[1],x[2],x[3],Effects)
        length(Effects) <- 8
        names(Effects) <- NULL
        c(Effects,sum(is.na(Effects)))
    
    }
    threeway <- lapply(threeway,intersectThreeEffects)
    threeway <- do.call(rbind,threeway)
    threeway <- threeway[threeway[,9]<5,-9] #remove combos with no effect
    write.csv(threeway,'~/Desktop/threeway.csv',row.names=FALSE)
    
    0 讨论(0)
提交回复
热议问题