1

I'm very new at Python, and I've searched for this answer for a few days now and I'm not seeing an answer. If I missed a post, I apologize and will happily go read that instead. This is more of a conceptual question, so just skimming for answers hasn't gotten me far yet.

I'm trying to use a pen-and-paper RPG system I know as a springboard for learning Python, and I want to divide up turns not just by player order, but by character speed as well.

sample code (there's more, but this is the problem part):

class char:
    def __init__(self,name,side,Spd):
    self.name=name
    self.side=side
    self.Spd=Spd

hero=char("Jimbo","good",4)
helplessSidekick=char("Timmy","good",2)
thug1="Crusher","evil",3)
thug2="Bruiser","evil",3)

So Jimbo spd 4, Timmy spd 2, Crusher spd 3, Bruiser spd 3. For the listed characters, I'd want the turn order for one round of combat to be: J,C,B,T,J,C,B,T,J,C,B,J sort of counting down each one until they're all out of speed. I can use a for loop to generate the initial turn order, no problem. But I need a while loop to actually run this because one of them can be defeated and thus no longer in the turn order, natch--and of course that turn order can fall apart if Jimbo KOs Crusher on his first move.

I've been staring at this for a few days and I'm stumped. I apologize in advance for the beginner nature of the question; I've been having a ton of fun with this, but I definitely have a lot to learn. Thank you!

3 Answers 3

1

I like @constantstranger's answer but another simple way to achieve this would be to just drop each player from a list as the game progresses using list.remove or list.pop.

class Char:
    def __init__(self, name, side, Spd):
        self.name=name
        self.side=side
        self.Spd=Spd

def take_turn(char):
    print(f"{char.name}'s turn...")
    char.Spd -= 1

hero = Char("Jimbo","good",4)
helplessSidekick = Char("Timmy","good",2)
thug1 = Char("Crusher","evil",3)
thug2 = Char("Bruiser","evil",3)

in_game_chars = [hero, thug1, thug2, helplessSidekick]
i = 0  # index of character to go first
while len(in_game_chars) > 0:
    i = i % len(in_game_chars)
    char = in_game_chars[i]
    take_turn(char)
    if char.Spd < 1:
        in_game_chars.pop(i)
    else:
        i += 1
print("Game over")

Output:

Jimbo's turn...
Crusher's turn...
Bruiser's turn...
Timmy's turn...
Jimbo's turn...
Crusher's turn...
Bruiser's turn...
Timmy's turn...
Jimbo's turn...
Crusher's turn...
Bruiser's turn...
Jimbo's turn...
Game over

The standard way to iterate indefinitely over a list of items in Python is the cycle iterable. Unfortunately, it does not allow items to be removed from the cycle after instantiation. So you have to rebuild the cycle iterator every time you change the list. And then you need a way to start the cycle at a specified index to make sure the order of the players is not disrupted:

from itertools import cycle, islice

in_game_chars = [hero, thug1, thug2, helplessSidekick]
i = 0  # index of character to go first
while len(in_game_chars) > 0:
    for char in islice(cycle(in_game_chars), i, None):
        take_turn(char)
        if char.Spd < 1:
            i = in_game_chars.index(char)
            # Remove player
            in_game_chars.pop(i)
            break
print("Game over")

Finally, you could also use filter or filterfalse which conditionally include/drop items in the list:

in_game_chars = [hero, thug1, thug2, helplessSidekick]
while len(in_game_chars) > 0:
    in_game_chars = list(filter(lambda x: x.Spd > 0, in_game_chars))
    for char in in_game_chars:
        take_turn(char)
print("Game over")
Sign up to request clarification or add additional context in comments.

6 Comments

Thank you! That gets me closer to the "multiple actions per turn" aspect I was aiming for. This is actually what I was initially trying, but I just didn't (/don't lol) know enough to make it work correctly. If you'd forgive a follow-up question, what does i = i % len do? I get the len part, but I don't know the % operator here. (And searching for % signs on search engines is turning out to be just as much fun as you'd expect...)
In Python % is the modulo operator. a % b gives the remainder after a is divided by b. In this case I am using it to make sure i does not exceed the length of in_game_chars when it is incremented or when a char is removed.
Thank you again! I'm learning a ton here tonight.
@Bill You have addressed the speed issue in OP's question, but it's not clear how to modify the code in your examples to handle the aspect of the question regarding premature removal of a character such as Crusher getting KO'ed by Jimbo. Put another way, take_turn() can have side effects (i.e., can either mutate the in_game_chars list, accelerate a defeated character's Spd attribute to 0, or take some other effectively equivalent action to remove a char from the game). Would this break your %, cycle and/or filter examples?
Yeah I didn't realize that. None of these methods work if take_turn(char) reduces the speed of other characters as well as char. In that case you would have to adapt these methods to check the speed attributes of every char each iteration or during take_turn.
|
1

Here's one way to do it, with the collection of characters remaining unchanged, but a hash table (a set in Python) of inactive characters growing as characters are killed off.

For illustrative purposes, I've hardcoded a game with 5 rounds and a predestined future in which Crusher meets their demise in turn 2 at the hands of Jimbo.

        class char:
            def __init__(self,name,side,Spd):
                self.name=name
                self.side=side
                self.Spd=Spd

        hero=char("Jimbo","good",4)
        helplessSidekick=char("Timmy","good",2)
        thug1=char("Crusher","evil",3)
        thug2=char("Bruiser","evil",3)
        
        global inactiveChars
        inactiveChars = set()
        global turns
        turns = 5
        def gameOver():
            global turns
            over = turns == 0
            if turns > 0:
                turns -= 1
            return over
        def killChar(name):
            global inactiveChars
            inactiveChars.add(name)
            print(f"Oof! {name} was KO'ed")
        def takeTurn(turnNumber, charName):
            if turnNumber == 2 and charName == "Jimbo":
                killChar("Crusher")

        chars = [hero, helplessSidekick, thug1, thug2]
        chars.sort(key=lambda character:character.Spd, reverse=True)
        curTurn = 0
        while not gameOver():
            curTurn += 1
            print(f"======== Turn number {curTurn}:")
            for ch in chars:
                if ch.name not in inactiveChars:
                    print(f"It's {ch.name}'s turn")
                    takeTurn(curTurn, ch.name)

Sample output:

======== Turn number 1:
It's Jimbo's turn
It's Crusher's turn
It's Bruiser's turn
It's Timmy's turn
======== Turn number 2:
It's Jimbo's turn
Oof! Crusher was KO'ed
It's Bruiser's turn
It's Timmy's turn
======== Turn number 3:
It's Jimbo's turn
It's Bruiser's turn
It's Timmy's turn
======== Turn number 4:
It's Jimbo's turn
It's Bruiser's turn
It's Timmy's turn
======== Turn number 5:
It's Jimbo's turn
It's Bruiser's turn
It's Timmy's turn

5 Comments

Thank you! That's marvelous. I wasn't looking at it that way at all; I'm glad I posted here. Thank you again!
@JoelRasdall, great to hear. Do you need any more help with your question?
That gives me a couple of conceptual points I didn't have; I'm playing with them now. I wanted to have the characters each take a number of turns relative to their speed as well, so like Jimbo didn't just go first, he got four moves to Timmy's two. (Turn order J,C,B,T,J,C,B,T,J,C,B,J = Jimbo goes four times, the thugs go three times each and poor Timmy only goes twice in a round.) I'm trying a "while Done != 0" and then keep a character-specific running tally for Done and zero them out as they get there, which isn't quite working but the logic seems good. Thanks again!
@JoelRasdall You're welcome - definitely seems like a fun way to learn a new language. If you found the answer helpful, you can mark it as "accepted".
Hah! Thank you, I knew I was supposed to do that but missed the checkpoint. Happy Sunday evening to you.
0

Your indentations are invalid syntax but I'm assuming this is just code formatting typos.

In any case you aren't using idiomatic capitalization conventions. In particular the class name should be upper case leading letter. Why is only Spd upper case first letter and not the other parameters?

I suspect the substance of your query is about editing a container whilst iterating over it. Naively doing this, the iterator won't be privy to the changes you make to the container whilst iterating over it. Often best practice is to iterate over a copy of the container whilst editing the original.

1 Comment

Sorry, yep, indentations issues are just code formatting issues here; this is my very first question on S.O. Capitalization conventions: I didn't actually know about those. I basically just downloaded IDLE and a very-beginner book and started fiddling around about a week ago. Iterate a copy of the container while editing the original: so I'd have two identical while loops running, with one changing the other? I don't understand entirely what you mean.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.