Optimize resources to level up data python

Viewed 289

I have a list of items e.g

  • Springfield Squad * 10 (ItemA1)
  • Tommy gun squad * 5 (ItemA6)
  • QF6-Pounder Mk IV * 2 (ItemB2)
  • A13 Covenanter Mk III * 1 (ItemC2)

.... ....

Resource list I have is represented as:

resources = {
    'itema1':0,'itema2': 4, 'itema3': 1, 'itema4': 0, 'itema5': 0, 'itema6': 0,'itema7': 0, 'itema8': 1,'itema9': 0,
    'itemb1':2,'itemb2': 7, 'itemb3': 0, 'itemb4': 0, 'itemb5': 0, 'itemb6': 0,'itemb7': 0, 'itemb8': 0,'itemb9': 0,
    'itemc1':2,'itemc2': 3, 'itemc3': 0, 'itemc4': 1, 'itemc5': 0, 'itemc6': 0,'itemc7': 2, 'itemc8': 0,'itemc9': 0
}

My data sheet is as below

https://docs.google.com/spreadsheets/d/1S5QdxQongeVjEaAZp0lDbI6VMnfWle0nRRPdpW7cYv0/edit?usp=sharing

Rules:

  1. Level 1,Level 7 -- > 3 Similar items on combine to make level 2 item( 3 * ItemA1 = ItemA2 , 3*ItemA7 = ItemA8 )

  2. Level 2,4,6,8 --> any 3 items can combine to make next level item (i.e at level2 ItemA2ItemB2ItemC2 = ItemA3)

  3. Level 3,5 --> 2 similar items can combine to make next level item ( e.g 2 * ItemA3 = ItemA4)

Problem I am trying to solve: What is the quickest way to reach level 9 of any item type (a to h) based on the items I have?

For example based on the resources above logic is what I'd try to use.

I will first attempt to take item type A to level as I already have a level 8 resource there. If I can't make it reach level 9, I will ensure that the rest of resources use maximum permutation for type A to reach as high level as possible to ensure that when I have enough resources I can progress sit to level 9.

2 Answers

Try this with your list of resources.

The format of resources I choose is a dict as it is easier to process, but other formats can be converted tell me if you have. Example.

# Input resources data
resources1 = {
    'itema1':10, 'itema6': 5,
    'itemb2': 2,
    'itemc2': 1
}

resources2 = {
    'itema1':6, 'itema2': 2, 'itema3': 5, 'itema6': 5,
    'itemb1': 2,'itemb2': 2, 'itemb3': 2,
    'itemc1': 1, 'itemc2': 2
}

resources3 = {
    'itema1':10, 'itema2': 4, 'itema3': 5, 'itema4': 2, 'itema5': 5, 'itema6': 3,
    'itemb1': 2,'itemb2': 4, 'itemb3': 2, 'itemb4': 2, 'itemb5': 2,
    'itemc1': 1, 'itemc2': 3, 'itemc3': 2
}

In the main(), we will read this input and convert to a list then process it.

def main():
    # Add resources into a list
    myresources = []
    myresources.append(resources1)
    myresources.append(resources2)
    myresources.append(resources3)

    # Loop on each resource from the myresources.
    for i, r in enumerate(myresources):
        r = build_missing_items(r)

        max_level, resources_remaining = level_progress(r)

        # Print summary of the given resources r
        print(f'Input resources # {i+1}')
        print(f'max level            : {max_level}')
        print(f'resources remaining  : {get_dict_with_value(resources_remaining)}')
        print(f'resources original   : {get_dict_with_value(r)}')
        print()

The build_missing_items() is just a function that takes a resource and supply missing items but with value zero.

def build_missing_items(res):
    """Read resources res and if there are missing items add
    the item with zero value. This useful when we create items
    as we progress in levels.
    
    Example:
    if itema7 is missing, just add {"itema7: 0}
    """
...

The level_progress() here it checks the resource in every levels.

def level_progress(r):
    """Returns max level reached and the remaining resources info"""
    max_level_reach = 1

    res = r.copy()

    status, res = level1(res)  # Create/subract resources at this level
    if status:  # if True it can advance to level 2
        max_level_reach += 1

    if status:
        status, res = level2(res)
        if status:
            max_level_reach += 1

    if status:
        status, res = level3(res)
        if status:
            max_level_reach += 1
...

There are 8 level functions level1(res), level2(res) ... In these levels we check from available resources if we can create an item for next level with the application of the rules. I maximize the creation of items for next level see the comments in the code below. So the idea is to create an item for next level and then subtract what was used in the creation. If we succeed in creating an item for the next level we set the status to True meaning we can advance to next level. We return this status along the updated resources for the next level check. level checking starts at level1, level2 ... level8.

def level1(par):
    """Create resources for next level from the given initial resources par"""
    status = False
    itema = par.get('itema1', 0)  # Get the count of itema1
    itemb = par.get('itemb1', 0)
    itemc = par.get('itemc1', 0)

    ok = 0  # if ok is 1 then we can go to the next level or level 2
    if itema >= 3:
        par['itema2'] += itema // 3  # maximize conversion, if there are 6 itema1 then we will create 6/3 or 2 itema2
        par['itema1'] -= 3 * (itema // 3)  # reduce the original value
        ok += 1
    if itemb >= 3:
        par['itemb2'] += itemb // 3
        par['itemb1'] -= 3 * (itemb // 3)
        ok += 1
    if itemc >= 3:
        par['itemc2'] += itemc // 3
        par['itemc1'] -= 3 * (itemc // 3)
        ok += 1

    status = True if ok else False

    return status, par

Output

With those 3 resources at the top, this is the result.

Intepretation in resources #1 result.

It reaches level 3. itema1 starts at 10 but now it is only 1 because we created 3 itema2 which consumes 9 itema1. 10-9 is 1 that 1 is the remaining itema1 count. At level 2, we need a2, b2 and c2 to advance to level 3. We have a2, we have b2 and we also have c2 that is why we reached level 3. But we cannot advance to level 4 because it requires at least 2 a3 or 2 b3 or 2 c3 and we cannot satisfy that conditions see the resources remaining we only have 1 a3.

Input resources # 1
max level            : 3
resources remaining  : {'itema1': 1, 'itema6': 5, 'itemb2': 1, 'itema2': 2, 'itema3': 1}
resources original   : {'itema1': 10, 'itema6': 5, 'itemb2': 2, 'itemc2': 1}

Input resources # 2
max level            : 4
resources remaining  : {'itema2': 2, 'itema3': 1, 'itema6': 5, 'itemb1': 2, 'itemc1': 1, 'itema4': 3, 'itemb4': 1}
resources original   : {'itema1': 6, 'itema2': 2, 'itema3': 5, 'itema6': 5, 'itemb1': 2, 'itemb2': 2, 'itemb3': 2, 'itemc1': 1, 'itemc2': 2}

Input resources # 3
max level            : 6
resources remaining  : {'itema1': 1, 'itema2': 4, 'itema4': 5, 'itema6': 6, 'itemb1': 2, 'itemb2': 1, 'itemb4': 2, 'itemc1': 1, 'itemb6': 1}
resources original   : {'itema1': 10, 'itema2': 4, 'itema3': 5, 'itema4': 2, 'itema5': 5, 'itema6': 3, 'itemb1': 2, 'itemb2': 4, 'itemb3': 2, 'itemb4': 2, 'itemb5': 2, 'itemc1': 1, 'itemc2': 3, 'itemc3': 2}   

Full code

def level8(par):
    """Create resources for next level from the given initial resources par"""
    status = False
    itema = par.get('itema8', 0)
    itemb = par.get('itemb8', 0)
    itemc = par.get('itemc8', 0)

    # Maximize creations.
    ok = 0
    while True:
        if itema and itemb and itemc:
            ok += 1
            par['itema9'] += 1  # create item for next level
            par['itema8'] -= 1  # reduce item on current level
            par['itemb8'] -= 1
            par['itemc8'] -= 1
            itema -= 1
            itemb -= 1
            itemc -= 1
        else:
            break

    status = True if ok else False

    return status, par


def level7(par):
    """Create resources for next level from the given initial resources par"""
    status = False
    itema = par.get('itema7', 0)
    itemb = par.get('itemb7', 0)
    itemc = par.get('itemc7', 0)

    ok = 0
    if itema >= 3:
        par['itema8'] += itema // 3  # maximize conversion
        par['itema7'] -= 3 * (itema // 3)  # reduce the original value
        ok += 1
    if itemb >= 3:
        par['itemb8'] += itemb // 3
        par['itemb7'] -= 3 * (itemb // 3)
        ok += 1
    if itemc >= 3:
        par['itemc8'] += itemc // 3
        par['itemc7'] -= 3 * (itemc // 3)
        ok += 1

    status = True if ok else False

    return status, par


def level6(par):
    """Create resources for next level from the given initial resources par"""
    status = False
    itema = par.get('itema6', 0)
    itemb = par.get('itemb6', 0)
    itemc = par.get('itemc6', 0)

    # Maximize creations.
    ok = 0
    while True:
        if itema and itemb and itemc:
            ok += 1
            par['itema7'] += 1  # create item for next level
            par['itema6'] -= 1  # reduce item on current level
            par['itemb6'] -= 1
            par['itemc6'] -= 1
            itema -= 1
            itemb -= 1
            itemc -= 1
        else:
            break

    status = True if ok else False

    return status, par


def level5(par):
    """Create resources for next level from the given initial resources par"""
    status = False
    itema = par.get('itema5', 0)
    itemb = par.get('itemb5', 0)
    itemc = par.get('itemc5', 0)

    ok = 0
    if itema >= 2:
        par['itema6'] += itema // 2  # maximize conversion
        par['itema5'] -= 2 * (itema // 2)  # reduce the original value
        ok += 1
    if itemb >= 2:
        par['itemb6'] += itemb // 2
        par['itemb5'] -= 2 * (itemb // 2)
        ok += 1
    if itemc >= 2:
        par['itemc6'] += itemc // 2
        par['itemc5'] -= 2 * (itemc // 2)
        ok += 1

    status = True if ok else False

    return status, par


def level4(par):
    """Create resources for next level from the given initial resources par"""
    status = False
    itema = par.get('itema4', 0)
    itemb = par.get('itemb4', 0)
    itemc = par.get('itemc4', 0)

    # Maximize creations.
    ok = 0
    while True:
        if itema and itemb and itemc:
            ok += 1
            par['itema5'] += 1  # create item for next level
            par['itema4'] -= 1  # reduce item on current level
            par['itemb4'] -= 1
            par['itemc4'] -= 1
            itema -= 1
            itemb -= 1
            itemc -= 1
        else:
            break

    status = True if ok else False

    return status, par


def level3(par):
    """Create resources for next level from the given initial resources par"""
    status = False
    itema = par.get('itema3', 0)
    itemb = par.get('itemb3', 0)
    itemc = par.get('itemc3', 0)

    ok = 0
    if itema >= 2:
        par['itema4'] += itema // 2  # maximize conversion
        par['itema3'] -= 2 * (itema // 2)  # reduce the original value
        ok += 1
    if itemb >= 2:
        par['itemb4'] += itemb // 2
        par['itemb3'] -= 2 * (itemb // 2)
        ok += 1
    if itemc >= 2:
        par['itemc4'] += itemc // 2
        par['itemc3'] -= 2 * (itemc // 2)
        ok += 1

    status = True if ok else False

    return status, par


def level2(par):
    """Create resources for next level from the given initial resources par"""
    status = False
    itema = par.get('itema2', 0)
    itemb = par.get('itemb2', 0)
    itemc = par.get('itemc2', 0)

    # Maximize creations.
    ok = 0
    while True:
        if itema and itemb and itemc:
            ok += 1
            par['itema3'] += 1  # create item for next level
            par['itema2'] -= 1  # reduce item on current level
            par['itemb2'] -= 1
            par['itemc2'] -= 1
            itema -= 1
            itemb -= 1
            itemc -= 1
        else:
            break

    status = True if ok else False

    return status, par


def level1(par):
    """Create resources for next level from the given initial resources par"""
    status = False
    itema = par.get('itema1', 0)  # Get the count of itema1
    itemb = par.get('itemb1', 0)
    itemc = par.get('itemc1', 0)

    ok = 0  # if ok is 1 then we can go to the next level or level 2
    if itema >= 3:
        par['itema2'] += itema // 3  # maximize conversion, if there are 6 itema1 then we will create 6/3 or 2 itema2
        par['itema1'] -= 3 * (itema // 3)  # reduce the original value
        ok += 1
    if itemb >= 3:
        par['itemb2'] += itemb // 3
        par['itemb1'] -= 3 * (itemb // 3)
        ok += 1
    if itemc >= 3:
        par['itemc2'] += itemc // 3
        par['itemc1'] -= 3 * (itemc // 3)
        ok += 1

    status = True if ok else False

    return status, par


def get_dict_with_value(res):
    """Convert res into a new dict that has values"""
    ret = {}
    for k, v in res.items():
        if v == 0:
            continue
        ret.update({k: v})

    return ret


def level_progress(r):
    """Returns max level reached and the remaining resources info"""
    max_level_reach = 1

    res = r.copy()

    status, res = level1(res)  # Create/subract resources at this level
    if status:  # if True it can advance to level 2
        max_level_reach += 1

    if status:
        status, res = level2(res)
        if status:
            max_level_reach += 1

    if status:
        status, res = level3(res)
        if status:
            max_level_reach += 1

    if status:
        status, res = level4(res)
        if status:
            max_level_reach += 1

    if status:
        status, res = level5(res)
        if status:
            max_level_reach += 1

    if status:
        status, res = level6(res)
        if status:
            max_level_reach += 1

    if status:
       status, res = level7(res)
       if status:
           max_level_reach += 1

    if status:
       status, res = level8(res)
       if status:
           max_level_reach += 1

    return max_level_reach, res


def build_missing_items(res):
    """Read resources res and if there are missing items add
    the item with zero value. This useful when we create items
    as we progress in levels.
    
    Example:
    if itema7 is missing, just add {"itema7: 0}
    """
    for c in ['a', 'b', 'c']:
        for i in range(1, 10):
            found = False
            itm = f'item{c}{i}'
            for k, v in res.items():
                if itm == k:
                    found = True
                    break
                    
            if not found:  # add
                res.update({itm: 0})

    return res


# Input resources data
resources1 = {
    'itema1':10, 'itema6': 5,
    'itemb2': 2,
    'itemc2': 1
}

resources2 = {
    'itema1':6, 'itema2': 2, 'itema3': 5, 'itema6': 5,
    'itemb1': 2,'itemb2': 2, 'itemb3': 2,
    'itemc1': 1, 'itemc2': 2
}

resources3 = {
    'itema1':10, 'itema2': 4, 'itema3': 5, 'itema4': 2, 'itema5': 5, 'itema6': 3,
    'itemb1': 2,'itemb2': 4, 'itemb3': 2, 'itemb4': 2, 'itemb5': 2,
    'itemc1': 1, 'itemc2': 3, 'itemc3': 2
}


def main():
    # Add resources into a list
    myresources = []
    myresources.append(resources1)
    myresources.append(resources2)
    myresources.append(resources3)

    # Loop on each resource from the myresources.
    for i, r in enumerate(myresources):
        r = build_missing_items(r)

        max_level, resources_remaining = level_progress(r)

        # Print summary of the given resources r
        print(f'Input resources # {i+1}')
        print(f'max level            : {max_level}')
        print(f'resources remaining  : {get_dict_with_value(resources_remaining)}')
        print(f'resources original   : {get_dict_with_value(r)}')
        print()


# Test
main()


We can use a bit of recursion to simplify the problem. We know what's needed to move from one level to the next, so we can pretty simply write a function that gets how many level 1 items are required to make a given item. E.g., as you said:

A_2 = 3 * A_1
A_3 = 1 * A_2 + 2 * (A|B|C)_2
...

We have to be careful about the 2 * (A|B|C)_2. For level 2 > 3, we really need 1 * (A|B|C)_2 + 1 * (A|B|C)_2, but to create C_2 (e.g.) we need 3 C_1. So we need some way to represent that while 3 items may have any time, all 3 must have the same type.

In this code, progression is the requirements to progress to a given level in terms of the previous level. E.g., to level A_2 to A_3, we need 1 A_2 and 2 (A|B|C)_3. Thus, progression[3] = (1, 2).

The function you'd actually use is creatable_from, which determines whether the target item can be created from the given list of resources. You can pass in Item(ItemType.A, 7), for example, to check whether A_7 is possible with your resources, or you can pass in Item(ItemType.ANY, 7) to check whether any level 7 item is possible.

from collections import Counter
from dataclasses import dataclass, field, replace
from enum import Flag, auto

progression = {
    2: (3, 0),
    3: (1, 2),
    4: (1, 1),
    5: (1, 2),
    6: (1, 1),
    7: (1, 2),
    8: (3, 0),
    9: (1, 2),
}


class ItemType(Flag):
    """
    Whatever item types there are.
    """

    A = auto()
    B = auto()
    C = auto()
    ANY = A | B | C


@dataclass(frozen=True)
class Item:
    """
    An item is defined by its type and level
    """

    type_: ItemType = field(default=ItemType.ANY)
    level: int = field(default=1)
    # `group` field maintains the connection between
    # sets of items that must all be the same type,
    # though that could be anything.
    group: int = field(default=-1, repr=False)


def cost(item):
    """
    Calculate the base-level resources needed to construct a given item.
    """
    if item.level == 1:
        return Counter({item: 1})

    same_type_coeff, any_type_coeff = progression[item.level]

    # this isn't pretty. better way?
    components = [replace(item, level=item.level - 1)] * same_type_coeff
    for i in range(any_type_coeff):
        components.append(Item(level=item.level - 1, group=i))

    return sum((cost(component) for component in components), start=Counter())


def creatable_from(target, resources_possessed):
    """
    Determine if `target` can be created from the given resources.

    If you have a `list` of `Item`s instead of a `Counter`, convert with:

        resources = sum((cost(res) for res in resources), start=Counter())
    """
    resources_required = cost(target)
    resources_possessed = sum(
        (
            cost(item)
            for item, count in resources_possessed.items()
            for _ in range(count)
        ),
        start=Counter(),
    )

    for res_req, n_required in list(resources_required.items()):
        for res_pos, n_possessed in list(resources_possessed.items()):
            if res_pos.type_ & res_req.type_ and n_possessed >= n_required:
                resources_possessed.subtract({res_pos: n_required})
                del resources_required[res_req]
                break

    if resources_required:
        return False

    return True

Example:

resources = Counter(
    {
        Item(ItemType.A, 1): 5,
        Item(ItemType.B, 1): 3,
        Item(ItemType.C, 1): 1,
    }
)
creatable_from(Item(ItemType.ANY, 3), resources)
# False

#%%

resources = Counter(
    {
        Item(ItemType.A, 1): 6,
        Item(ItemType.B, 1): 3,
    }
)
creatable_from(Item(ItemType.A, 3), resources)
# True

Edit:

Loading your data

Converting from your dictionary of resources to a Counter of Items is simple enough. Here's an example implementation. If you wanted to keep your code as is, you could modify my code to work with the dictionary as it is too, but with a bit more effort.

# if your data is literally {"itema1": 3 ...}, this works as is. 
# otherwise, you'd need to modify the parsing a bit

resources = {
    'itema1':0,'itema2': 4, 'itema3': 1, 'itema4': 0, 'itema5': 0, 'itema6': 0,'itema7': 0, 'itema8': 1,'itema9': 0,
    'itemb1':2,'itemb2': 7, 'itemb3': 0, 'itemb4': 0, 'itemb5': 0, 'itemb6': 0,'itemb7': 0, 'itemb8': 0,'itemb9': 0,
    'itemc1':2,'itemc2': 3, 'itemc3': 0, 'itemc4': 1, 'itemc5': 0, 'itemc6': 0,'itemc7': 2, 'itemc8': 0,'itemc9': 0
}

# The ItemType[...] bit gets the correct enum flag value from the item type.
# e.g. using the "a" from "itema1" we get ItemType["A"], which returns ItemType.A 
resources_counter = Counter(
    {
        Item(ItemType[key[-2].upper()], int(key[-1])): value
        for key, value in resources.items()
    }
)

Your modifications would need to be made in the ItemType enum, where you'd change the values A, B, C, etc to the real item types, as well as in the parsing bit. If your data was "infantry1" for example, instead of key[-2] you'd have key[:-2] and the corresponding enum value would be INFANTRY = auto().

Finding max level

This is pretty simple to do too. I think the easiest solution would be to just loop through all the levels and check if it can be made.

for level in range(9, 0, -1):
    if creatable_from(Item(ItemType.ANY, level), resources):
        print(f"max level: {level}")
        break
Related