tags:

views:

73

answers:

2

I am trying to write a Python script that finds combinations of armor items from a game that match certain criteria. I have an object that has keys for each item slot (ie. head, chest, waist, etc.) and a list of all the items that can fit in that slot with their stats in each key. There are 10 slots and many items for each up to a total of 88 or so items.

My question is: Is there some kind of algorithm already used to do stuff like this? An example of what I would want to do is find the combination of armor pieces that gives me stat1 < 35 that has the highest stat2+3+4.

I don't believe brute forcing it would be practical because it would take ages (correct me if I'm wrong). Any help would be appreciated!

Edit - More details:

Data sample: http://pastebin.com/rTH3Q5Sj The first tuple is 2 head slot items, 2nd tuple is 2 chest slot items.

One thing I might want to do with the sample data is get the combination of helm and chest that has the highest slashing/bludgeoning/piercing total but less than 12 encumbrance total.

+4  A: 

It sounds like the elegant solution to this is linear programming. I can help with that if you provide more details.

with only 88 items spread out amongst ten slots, brute forcing it wouldn't be terrible either. Some combination of the two might be the easiest.

update:

based on the update you gave, I think linear programming is overkill (and difficult to apply). I wrote you this fairly general solution. Study it and understand it. If anybody has any corrections or improvements, I'd love to hear them.

from itertools import ifilter, product

# Definition of ITEMS cut for brevity. See below.    

def find_best(slots, maximize, constraints):
    """example call:
         find_best(['helm', 'chest'], ['slashing', 'bludgeon'],
                   {'encumbrance': 12})
    """
    # save the slot names to construct a nice return value
    slot_names = slots

    # pull the actual lists of items for each slot out of the global dict
    slots = [ITEMS[slot] for slot in slots]

    # this function calculates the value of a solution
    value = lambda solution: sum(float(slot[attr]) for attr in maximize
                                                   for slot in solution)

    # replace our constraints with functions to check solutions
    constraints = [lambda solution:
                       sum(float(slot[attr]) for slot in solution) < constraint
                 for attr, limit in constraints.items()]

    # start with all possible combinations
    solutions = product(*slots)

    # chain together ifilters to weed out the ones that fail any of the
    # constraints. Note that for optimum performance, you should place the
    # constraints in descending order of probability to fail
    for constraint in constraints:
        solutions = ifilter(constraint, solutions)

    # We're going to do decorate, max, undecorate
    solutions = ((value(solution), solution) for solution in solutions)
    value, solution = max(solutions)

    # get the indexes and return
    return dict((name, slot.index(item)) for name, slot, item
                in zip(slot_names, slots, solution))

Note that you should be storing the values as floats and not as strings! It's easier (because it's often automatic when you need it) to cast to string than a float. Then you can take the ugly casts out of my code. I was just to lazy to do it for you. Note that you can call with as many constraints as you want but only one set of attributes to maximize. This made sense to me. If you study the code and understand it, you should be able to modify it to suit your tastes.

Here's how I modified your data structure.

ITEMS = { 'helm': [{'Acid':' 2.71',
                        'Bludgeoning': '1.04',
                        'Cold': '2.71',
                        'Encumbrance': '8.00',
                        'Fire': '2.71',
                        'Holy': '2.71',
                        'Impact': '1.30',
                        'Lightning': '2.00',
                        'Name': 'Plate Helm',
                        'Piercing': '1.17',
                        'Slashing': '1.30',
                        'Unholy': '2.71'},

                       {'Acid': '2.18',
                        'Bludgeoning': '0.92',
                        'Cold': '2.18',
                        'Encumbrance': '7.00',
                        'Fire': '2.18',
                        'Holy': '2.18',
                        'Impact': '1.15',
                        'Lightning': '1.65',
                        'Name': 'Scale Helm',
                        'Piercing': '1.03',
                        'Slashing': '1.15',
                        'Unholy': '2.18'}],

              'chest':[{'Acid': '5.47',
                        'Bludgeoning': '2.05',
                        'Cold': '5.47',
                        'Encumbrance': '32.00',
                        'Fire': '5.47',
                        'Holy': '5.47',
                        'Impact': '2.57',
                        'Lightning': '4.06',
                        'Name': 'Plate Chest',
                        'Piercing': '2.31',
                        'Slashing': '2.57',
                        'Unholy': '5.47'},

                       {'Acid': '4.45',
                        'Bludgeoning': '1.84',
                        'Cold': '4.45',
                        'Encumbrance': '28.00',
                        'Fire': '4.45',
                        'Holy': '4.45',
                        'Impact': '2.30',
                        'Lightning': '3.31',
                        'Name': 'Scale Cuirass',
                        'Piercing': '2.07',
                        'Slashing': '2.30',
                        'Unholy': '4.45'}]}

note that the values of the outermost dictionary are lists and not tuples as you said. There is a huge distinction!

aaronasterling
I updated it with more details. I looked at the linear programming page and that sounds exactly like the concept I need but I'm not sure how it is implemented in code.
Barakat
Thank you very much for taking the time to write that up! I learned a lot from the code and have implemented it in my script. I have never heard of itertools, so I'm glad I found out about it from you. I need to look at all the standard libraries for Python @_@. Thank's again!
Barakat
@Barakat. Glad I could help. Note that the cool thing about the way it's written is that because it's all generators, it actually runs backwards. no work is done until `max` is called to consume the generator that we build in previous steps.
aaronasterling
Is there a way I could have it update me on it's progress? And would it also be possible to split the work so I can run it on multiple cores?
Barakat
@Barakat. No and No. (at least not with this method) How many attributes are you using it on? in the example, you gave three. What sort of times are you getting?
aaronasterling
There are 10 slots with many parts in each, and if I understand correctly it would have to go through 1.4 billion possibilities. I've been running it for 30 minutes now on a 3.0ghz core.
Barakat
@Barakat. 1.4 billion sounds about right. I thought you only wanted to do a few slots at a time based on your update. itertools is pretty fast so if this is just a one time thing, then let it run. If you need this to be part of a public interface, then there are better solutions.
aaronasterling
I will just have it calculate all the inputs I want over time and store them. I wanted to do this to learn something new and figure out a few armor combinations for darkfall online at the same time :). Thanks!
Barakat
ahh, I see now. I misread your update where you specified 'with the _sample_ data'. at least you learned about itertools. you should probably uncheck this answer to open the question back up.
aaronasterling
@Barakat. I sincerely hope that you tested this on smaller inputs before you dove right in ;) I did only have that small test set.
aaronasterling
No worries, your answer was what I was looking for. It is working, I tested it with only a few slots at a time, it just takes a lot more time to do more than 6 or 7 slots. The data does not need to be calculated more than a few times though so that is all I need. :)
Barakat
+1  A: 

Seems like a variation of the Knapsack problem. Dynamic programming should be good enough if your weight (stats?) limit is not too big.

Edit:

Here's a prototype in Java of the dynamic programming solution:

public static int getBestAcidSum(int[][] acid, int[][] fire, int maxFire) {
    int slots = acid.length;
    int[][] acidSum = new int[slots][maxFire + 1];

    for (int j = 0; j < acid[0].length; j++)
        acidSum[0][fire[0][j]] = Math.max(acidSum[0][fire[0][j]], acid[0][j]);

    for (int i = 1; i < slots; i++)
        for (int j = 0; j < acid[i].length; j++)
            for (int fireSum = fire[i][j]; fireSum <= maxFire; fireSum++)
                if (acidSum[i-1][fireSum - fire[i][j]] > 0)
                    acidSum[i][fireSum] = Math.max(acidSum[i][fireSum], acidSum[i-1][fireSum - fire[i][j]] + acid[i][j]);

    int ret = 0;
    for (int fireSum = 0; fireSum <= maxFire; fireSum++)
        ret = Math.max(ret, acidSum[slots - 1][fireSum]);
    return ret;
}

public static void main(String[] args) {
    System.out.println(getBestAcidSum(new int[][] {{271, 218},  {547, 445}}, new int[][] {{271, 218}, {547, 445}}, 800));
}

The algorithm is O(N*C) where N is the total number of items and C is the constraint ("maxFire" in the example). Should work instantaneously with the quantities you've been mentioning.

The code is very simplified but it maintains the fundamental algorithmic difficulty. It only returns the optimal sum that fulfills the constraints. Should be easily modified to return the actual combination. Summing more than one stat together should be easily incorporated as well. Values were converted to integers by multiplying by 100.

Sheldon L. Cooper
Thank you, I'm studying the code right now.
Barakat