Make sure functions run only once

Maybe this is a silly question, and there is probably a better way of doing it, but I’m looking for a way in python, to make sure my functions don’t run multiple times per turn.

So an example, I have 3 functions, mid_defense, left_defense, and right_defense. The issue is the algo calls these based on the amount of “attacks” on that side, so I’ll check the enemy path, based on the previous turn spawn locations, up to 5 previous turns, if there are no spawns in those 5 turns, it’s checks all possible paths, and finds the shortest, obviously this isn’t ideal because there are paths that are the same lengths, I’ve not had time to try and optimize that further. From that it decides to do mid, left or right defense, based on the number of attacks to that side. However, there are obviously some algo’s that deploy info units at different locations, so some can sneak through.

What I would like to do is once it deploys the main defense, I’d like it to check, what side had the next largest amounts of attack, so if mid is deployed, and there is still a certain number of cores left, to move left or right, obviously all of the deploy functions would need to have this, however that could lead to infinite loop, which I would like to avoid, so I’d like a way to mark if a function has been run previously. So each defense function would check to see if it has previously run, then if all of the firewall slots it uses are full, then check which has the second most attacks, and deploy that function. If it’s already been run I would like it to just stop there. so code might look something like this.

    def deploy_right_defense(self, game_state):
        if deploy_right_defense.has_been_called == True:
            return

        deploy_right_defense.has_been_called = True
        if game_state.get_resource(game_state.CORES) > 15 and all_empty_pos < 1:
            left_attacks = self.get_left_attacks(game_state)
            mid_attacks = self.get_mid_attacks(game_state)

            if len(left_attacks) > len(mid_attacks):
                self.deploy_left_defense(game_state)
            elif len(left_attacks) < len(mid_attacks):
                self.deploy_mid_defense(game_state)

I found some stackoverflow posts about this one of the methods looked like this.
def self_aware_function(a, b):
self_aware_function.has_been_called = True
return a + b

    if __name__ == '__main__':
        self_aware_function.has_been_called = False
 
        for i in range(2):
            if self_aware_function.has_been_called:
                print('function already called')
        else:
            print('function not called')
 
        self_aware_function(1, 2)

I was having some issues implementing this, however, I think that’s because I’m doing something wrong, the other ways I found that might work is mock or trace, so I guess I’m wondering if anyone else has done something like this and which way would be the most effective?

Sorry for the long winded first forum post.

Wellcome to advanced defance structures… where the real game (pain?) starts.

Your ideas are interesing but as you can see, going down this path … makes further development chalanging.
If I understand it right, what you try to achieve with run-only-once is to split the defence in to chunks,
and build them in proper order, based on the current defence plan.

In my oppinion, it is better, First to deside tha defence plan, and then execute the build funcitons in order.
Currently in the game there is no penalty if you try to rebuild something that already exists.
To make the build functions a bit “smarter”, you may implement functions like:
build_if_wall_exists
build_if_all_resources_are_available
build_if_x_resources
is_already_build( locations )

To sumerise, instead of solving
“how to call function just once, becaus this will ensure X condtions are met
(enough resources, wall does no exists …)”
Try to solve “how to call this functionl only if X conditions are met”

1 Like

There is definitly a better way to build code logic. At least you can move all logic about choosing next deploy function outside of deploy functions. Having all such logic in one place will make it much easier to add new deploy functions. And certainly you can avoid infinite loops without direct checking if a function has been run previously. But if you still want to do this, you can use decorators and global variables.
Here is an example with counting number of function calls:

decorator_for_counting_calls uses one global variable, so it can track only one function
decorator_for_counting_calls2 uses global dictionary and function names, so it can track several functions

n_calls = 0
n_calls_dict = dict()

def decorator_for_counting_calls(func):

    def wrapper(*args, **kwargs):
        global n_calls
        n_calls += 1
        func(*args, **kwargs)

    return wrapper


def decorator_for_counting_calls2(func):

    def wrapper(*args, **kwargs):
        global n_calls_dict
        n_calls_dict[func.__name__] = n_calls_dict.get(func.__name__, 0) + 1
        func(*args, **kwargs)

    return wrapper

@ decorator_for_counting_calls
def some_function():
    global n_calls
    if n_calls > 2:
        return
    print('doing something')

@ decorator_for_counting_calls2
def other_function():
    pass

if __name__ == '__main__':
    for i in range(42):
        some_function()

    for i in range(22):
        other_function()

    print(n_calls)
    print(n_calls_dict)
1 Like

Thank you for the additional information I will look into decorators more, but I may attempt a different route as suggested by both of you.

@LuckyR’s response is a much cleaner way to do it. I actually have a similar piece of logic in my code for calling a given set of functions only once, in an order that changes from turn to turn. The quick and easy way I did it (not as clean as the above response) is to use flags and sort a list that corresponds to each function.

num_attacks = [left_attacks, middle_attacks, right_attacks]
num_attacks.sort(reverse=True) # descending order

did_left = False
did_middle = False
did_right = False
spent_cores = False
for num in num_attacks:
    if num == left_attacks and not did_left:
        spent_cores = left_defenses()
        did_left = True
    elif #do the others... yada yada
    if spent_cores:
        break

Edit: forgot to actually use the flag

1 Like