3
\$\begingroup\$

I am making a super simple and dumb terminal text game to try out unittest in python. I have ran into a problem. When I run the test, the game loop is ran, and I have to manually insert input to continue to the tests.

Is there a way to not run the game loop when I run my tests, so I can test the class methods only?

Any other advice on the code in general would be helpful appreciated as well.

Game concept: Player is prompted to build, gather, recruit, or attack every turn and when someone's castle health is <= 0, game is over.

import os, random

class Player():
    def __init__(self):
        self.castle = 1
        self.villagers = 1
        self.warriors = 0
        self.food = 0
        self.stone = 0

    def takeTurn(self):
        action = 0
        action = int(input("1) Build\n2) Gather\n3) Recruit\n4) Attack\n"))
        if action == 1:
            self.build()
        elif action == 2:
            self.gather()
        elif action == 3:
            self.recruit()
        elif action == 4:
            self.attack()
        else:
            self.takeTurn()

    def build(self):
        if self.stone >= 1:
                self.castle += 1
                self.stone -= 1
        else:
            os.system('clear')
            if self.__class__.__name__ == "Player":
                print("You don't have enough stone!\n")
                printMenu()
            self.takeTurn()

    def gather(self):
        self.food += self.villagers
        self.stone += self.villagers

    def recruit(self):
        if self.food >= 1:
                self.warriors += 1
                self.food -= 1
        else:
            os.system('clear')
            if self.__class__.__name__ == "Player":
                print("You don't have enough food!\n")
                printMenu()
            self.takeTurn()

    def attack(self):
        if self.warriors >= 1:
            if self.__class__.__name__ == "Player":
                ai.castle -= self.warriors
            elif self.__class__.__name__ == "AI":
                player.castle -= self.warriors


class AI(Player):
    def __init__(self):
        super().__init__()

    def makeMove(self):
        action = 0
        # Win if player castle weak
        if player.castle == self.warriors:
            player.castle -= self.warriors
        # Build if castle weak
        elif self.castle == player.warriors:
            if self.stone >= 1:
                self.castle += 1
            else:
                action = random.randint(2, 4)
        else:
            action = random.randint(1, 4)

        if action == 1:
            super().build()
        if action == 2:
            super().gather()
        if action == 3:
            super().recruit()
        if action == 4:
            super().attack()


player = Player()
ai = AI()


def printMenu():
    print("You:\nCastle: " + str(player.castle) + " Food: " + str(player.food) + " Stone: " + str(player.stone) + " Villagers: " + str(player.villagers) + " Warriors: " + str(player.warriors) + "\n")
    print("Computer:\nCastle: " + str(ai.castle) + " Food: " + str(ai.food) + " Stone: " + str(ai.stone) + " Villagers: " + str(ai.villagers) + " Warriors: " + str(ai.warriors) + "\n")


def gameLoop():
    # MAIN LOOP
    playing = False
    if playing:
        printMenu()

        player.takeTurn()    
        ai.makeMove()   

        if ai.castle <= 0:
            playing = False
            print("You Win!\n")
        elif player.castle <= 0:
            playing = False
            print("You Lose!")
        else:
            os.system('clear')


gameLoop()
import unittest

from game.main import Player


class TestPlayer(unittest.TestCase):
    def setUp(self):
        self.player = Player()

    def test_build(self):
        # Should fail 
        self.player.castle = 2
        self.player.stone = 1
        self.player.build()
        self.assertEqual(self.player.castle, 1)

if __name__ == '__main__':
    unittest.main()
\$\endgroup\$
1
  • 7
    \$\begingroup\$ Put your gameloop() call inside a if __name__ == '__main__': block so it doesn't run when you import the module. \$\endgroup\$
    – Samwise
    Mar 25, 2020 at 14:46

1 Answer 1

4
\$\begingroup\$

You already have the answer to your "bug" given in the comments, but I will try to give you a more in-depth review. Nevertheless, I will repeat the aforementioned solution: to avoid the program auto-running on import, but your main call inside of a if __name__ == "__main__" (read more here if interested).

For your __init__ method, having your method initializers as (default) arguments tend to give more flexibility down the road as the project grows:

    def __init__(self, castle=1, villagers=1, warriors=0, food=0, stone=0):
        self.castle = castle
        self.villagers = villagers
        self.warriors = warriors
        self.food = food
        self.stone = stone

For your first real method, I would recommend staying with the Python standard of snake_case over camelCase. Furthermore, initialising the variable action to 0 doesn't really do you anything since you overwrite it in the following line. And since you rely on that input I think you should figure out what to do when you don't get an int, or if you get something like 11 (because 11 here would likely be a typo, given the options).

This, quite naturally, leads on to the purpose of testing (yay!). A nice thing about tests is that they allow you to think about what exactly you want to happen when you create an object or call on a function. For your takeTurns function, that would maybe be that you're only allowed to enter digits, and if you don't enter a single digit in the range 0–5 it throws an exception or it asks for another input. You would then basically implement your function (after the tests) to meet all the requirements you set forth in the beginning. You may then also find that maybe you were thinking about it the wrong way; in this case it could be that maybe take_turns (I renamed it for you...) shouldn't be asking for input() but rather be accepting an argument take_turns(user_choice).

To finalise, I'll add some smaller pieces of advice.

  • Instead of self.__class__.__name__ == "Player": you could (and should) use isinstance(self, Player).

  • Instead of action = random.randint(1, 4) coupled with 4 if-statements, there's a handy little function in random called choice (have a look here). You could also make your AI class much smaller by rethinking how the AI differs (or doesn't differ) from a Player.

  • f-strings are super nice. Instead of print("You:\nCastle: " + str(player.castle) + " Food: " + str(player.food) + " Stone: " + str(player.stone) + " Villagers: " + str(player.villagers) + " Warriors: " + str(player.warriors) + "\n"), you could have (and notice that I removed str() since you don't need it):

    print(f"""You:
    Castle: {player.castle}
    Food: {player.food}
    Stone: {player.stone}
    Villagers: {player.villagers}
    Warriors: {player.warriors}""")
    
\$\endgroup\$
3
  • 3
    \$\begingroup\$ There's a typo here but it's not enough characters to submit an edit: instanceof should be isinstance \$\endgroup\$ Mar 26, 2020 at 20:49
  • 1
    \$\begingroup\$ Corrected, thank you \$\endgroup\$
    – ades
    Mar 27, 2020 at 9:52
  • \$\begingroup\$ Thank you very much! Lots of good advice. \$\endgroup\$
    – CliffJ
    Mar 27, 2020 at 22:09

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.