0

I've coded a game in oTree for research purposes and the code below does exactly what I intend. The problem is that some games have over 100 rounds so you can see how the code could get ridiculously long. For people who are going to use this code in the future, it is practically unreadable in its current form. Any ideas on how I could condense this into a simpler if statement?

If you are unfamiliar with oTree, the "participant" signifier is essentially a way to pass data across rounds. The "player" reflects the participant specific to a round number. So each round I need to save their current round return (player.r#) and call the returns of all past rounds (participant.r#-1).

if subsession.round_number == 1:
    participant = player.participant
    participant.r1 = round(100 * asset, 3) + 100
    player.r1 = participant.r1
elif subsession.round_number == 2:
    participant = player.participant
    participant.r2 = round(participant.r1 * asset, 3) + participant.r1
    player.r1 = participant.r1
    player.r2 = participant.r2
elif subsession.round_number == 3:
    participant = player.participant
    participant.r3 = round(participant.r2 * asset, 3) + participant.r2
    player.r1 = participant.r1
    player.r2 = participant.r2
    player.r3 = participant.r3
elif subsession.round_number == 4:
    participant = player.participant
    participant.r4 = round(participant.r3 * asset, 3) + participant.r3
    player.r1 = participant.r1
    player.r2 = participant.r2
    player.r3 = participant.r3
    player.r4 = participant.r4
elif subsession.round_number == 5:
    participant = player.participant
    participant.r5 = round(participant.r4 * asset, 3) + participant.r4
    player.r1 = participant.r1
    player.r2 = participant.r2
    player.r3 = participant.r3
    player.r4 = participant.r4
    player.r5 = participant.r5
E_net4
  • 27,810
  • 13
  • 101
  • 139
  • 7
    Using separate variable names for an arbitrary number of values is simply insane. You want a single list, and append another value to it each time. – jasonharper Apr 07 '22 at 16:34
  • 2
    A good rule of thumb is if you see repetition in your code it can be improved, which you have identified. So, look at which sections are being repeated and build a function that you pass parameters to. You are passing a number to each if/elif, so you would pass that number to the function and use the parameter as the value in the function rather than hardcoding the numbers. Does that make sense? – Captain Caveman Apr 07 '22 at 16:37
  • Yes it's definitely insane. Just not smart enough to fix it as of yet. And @CaptainCaveman. I think it makes sense. I guess my confusion comes from having a different length list each round. – Cameron Kormylo Apr 07 '22 at 16:39
  • What exactly are you trying to do? It looks like you should be using the functions built into oTree for this, but I don't have any experience with the library. Specifically, I think `payoffs` and `in_all_rounds()` are relevant. – Layne Bernardo Apr 07 '22 at 16:57
  • 1
    As @CaptainCaveman alludes, you're not following what is known as the ["Don't repeat yourself" (DRY) principle](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) of software development. See the pattern to the repetitious code you're writing? You need to create a function that does that based on arguments passed to it and call it however many times required. – martineau Apr 07 '22 at 17:41

3 Answers3

3

You can use getattr and setattr for this and avoid ifs at all.
getattr(self, name) # equal to: self.name
setattr(self, name, value) # equal to: self.name = value

The final code would be something like:

r_n = subsession.round_number
participant = player.participant

setattr(participant, 'r' + str(r_n), round(getattr(participant, 'r' + str(r_n - 1)) * asset, 3) + getattr(participant, 'r' + str(r_n - 1))

for i in range(1, r_n+1):
    player_id = 'r' + str(i) 
    setattr(player, player_id, getattr(participant, 'r' + str(player_id)))

UPD: Don't forget to add condition for r1. Suddenly there will be if that we tried to avoid :D

Dmitry Barsukoff
  • 152
  • 1
  • 13
1

Here's a function that does the same thing as the code in your if statements. It refers to the last two rounds using Python negative indexing (with a check to handle the first round as a special case). It would only need to be called once for a given round.

def update(player, asset, round_number):
    attrs = tuple(f'r{i}' for i in range(1, round_number+1))  # ('r1', 'r2', 'r3', ...)

    if len(attrs) < 2:
        participant = player.participant
        participant.r1 = round(100 * asset, 3) + 100
        player.r1 = participant.r1
    else:
        participant = player.participant
        prev_particpant = getattr(participant, attrs[-2])
        setattr(participant, attrs[-1], round(prev_particpant*asset, 3) + prev_particpant
        for attr in attrs:
            setattr(player, attr, getattr(participant, attr)

I think it would be better to keep a single list of round information in each player instead of having a separately named instance attribute (i.e. .r1, .r2, .r3, … for each of them. If nothing else it would make copying them all much easier — I also think that is the "X problem" @Dmitry was referring to in his comment.

martineau
  • 119,623
  • 25
  • 170
  • 301
0

I’m know that dictionaries are often used switch statements so that’s some to look into. If you don’t know what switch statements are; they’re similar to if statements that they’re conditional blocks of code. But instead of checking a Boolean they check if a certain variable is a certain case. Python doesn’t have switch statements but dictionaries can be used as a replacement.

  • 1
    Slight correction to that last sentence, 3.10.0 introduced [match](https://peps.python.org/pep-0634/) which is a syntactical equivalent to a switch statement. – BTables Apr 07 '22 at 17:00
  • 1
    Doing this with a switch/match statement (or dictionary for that matter) isn't really practical when there could be as many as 100 "else"s — so this is a totally wrong approach IMO. – martineau Apr 07 '22 at 19:40