Learning Python with a Terminal Game
Tags: beginners, python, codenewbie
Published: 2023-07-10 18:18:38

For CodeCademy I was tasked with creating a Terminal game. I knew it was coming and had been preparing for weeks for it as I plowed through the Computer Science course, fully confident that I would tackle this project with ease.

I think I stared at my IDE for about 2 hours before I figured out how to get started. I began building a class I simply called "Human". This was gong to represent both myself, and the NPCs in my little text-based world. Oh the grand ideas I had. Hours blew by and I typed away.

Before I realized what was going on, I had overloaded myself. Every time I had a though, I'd insert blocks of code to make that a reality and the project blew up beyond a simple little terminal game. I could have saved myself hours if I had had a little humility about my actual capabilities. Instead hours turned into days, and then a week later I have about 400 lines of code and none of it works.

I had managed to exhaustively iron out the exception-causing bugs, but nothing behaved as I intended. NPCs would get stuck in one room, weapons (the second class I created to satisfy the '2 Classes' Requirement of the project) weren't being properly equipped. Combat was hilariously broken, and I, as the player, couldn't even leave the room I'd crafted to enter one of the other two.

So today I saved the project as a WIP to visit later with better tools and reduced it significantly to you and your opponent. No great fanfare of worldbuilding, no trying to move around from room to room, not even the complex turn-based battle system I'd dreamed up.

You pick a name, you pick a weapon, you enter the main loop, and you and an NPC whack at eachother (or defend) until one of you falls unconscious.

I did get to keep one fun little toy I'd dreamed up. I envisioned a mace as a special weapon, one that could immediately end the fight by knocking your opponent unconscious. I reduced the chance of this to nearly nothing, and was treated on my first playtest to being immediately defeated in the first hit by a mace wielding NPC.

Unfortunately I didn't think to document my journey with screenshots, so all I have is post-completion. The final product. Behold the culmination of a week and a half of free-time consuming effort:

{% spoiler A printout of the game runtime %}You enter the arena to the roar of the crowd. A man in the emperor's box with a booming voice shouts, "Welcome, one and all, back to The Arena!"

The crowd roars again. After the din dies down, the announcer continues. "Today, we see battle between two fierce competitors! First, we have Leonius Libo using a Dagger!"

Once more the arena erupts in cheers, some even chanting Leonius Libo's name.

"And second, we have...

Enter your name: 

Hathrel using a...


Select a weapon from the following: DAGGER, GREATSWORD, LONGSWORD, MACE 

Mace!" the crowd erupts into cheers again. Some chanting your name this time.

"Without further ado ladies and gentlemen, let the battle BEGIN!" he shouts, the crowd erupts in roars and your opponent rushes you!
What do you want to do? ATTACK, DEFEND, or CHECK HEALTH? Hathrel's attack missed!
Leonius Libo's weapon lands a blow! Hathrel takes 10 damage!
What do you want to do? ATTACK, DEFEND, or CHECK HEALTH? Hathrel's attack missed!
Leonius Libo prepares for an attack...
What do you want to do? ATTACK, DEFEND, or CHECK HEALTH? Hathrel's attack missed!
Leonius Libo's weapon lands a blow! Hathrel takes 8 damage!
What do you want to do? ATTACK, DEFEND, or CHECK HEALTH? Leonius Libo partially deflected Hathrel's attack! Leonius Libo only takes 0 damage!
Leonius Libo prepares for an attack...
What do you want to do? ATTACK, DEFEND, or CHECK HEALTH? Hathrel prepares for an attack...
Hathrel partially deflected Leonius Libo's attack! Hathrel only takes 1 damage!
What do you want to do? ATTACK, DEFEND, or CHECK HEALTH? Leonius Libo partially deflected Hathrel's attack! Leonius Libo only takes 0 damage!
Leonius Libo's weapon lands a blow! Hathrel takes 8 damage!
What do you want to do? ATTACK, DEFEND, or CHECK HEALTH? Hathrel's attack missed!
Leonius Libo's weapon lands a blow! Hathrel takes 7 damage!
What do you want to do? ATTACK, DEFEND, or CHECK HEALTH? Hathrel's weapon lands a blow! Leonius Libo takes 4 damage!
Leonius Libo's weapon lands a blow! Hathrel takes 3 damage!
What do you want to do? ATTACK, DEFEND, or CHECK HEALTH? Hathrel's weapon lands a blow! Leonius Libo takes 12 damage!
Leonius Libo's weapon lands a blow! Hathrel takes 6 damage!
What do you want to do? ATTACK, DEFEND, or CHECK HEALTH? Hathrel's weapon lands a blow! Leonius Libo takes 12 damage!
Leonius Libo's weapon lands a blow! Hathrel takes 11 damage!
What do you want to do? ATTACK, DEFEND, or CHECK HEALTH? Hathrel's attack missed!
Leonius Libo prepares for an attack...
What do you want to do? ATTACK, DEFEND, or CHECK HEALTH? Hathrel's attack missed!
Leonius Libo's weapon lands a blow! Hathrel takes 7 damage!
What do you want to do? ATTACK, DEFEND, or CHECK HEALTH? You have 39 health left.
What do you want to do? ATTACK, DEFEND, or CHECK HEALTH? Leonius Libo partially deflected Hathrel's attack! Leonius Libo only takes 1 damage!
Leonius Libo's weapon lands a blow! Hathrel takes 4 damage!
What do you want to do? ATTACK, DEFEND, or CHECK HEALTH? Hathrel's weapon lands a blow! Leonius Libo takes 10 damage!
Leonius Libo's weapon lands a blow! Hathrel takes 11 damage!
What do you want to do? ATTACK, DEFEND, or CHECK HEALTH? Hathrel's attack missed!
Leonius Libo's weapon lands a blow! Hathrel takes 5 damage!
What do you want to do? ATTACK, DEFEND, or CHECK HEALTH? Hathrel's weapon lands a blow! Leonius Libo takes 5 damage!
Leonius Libo's weapon lands a blow! Hathrel takes 7 damage!
What do you want to do? ATTACK, DEFEND, or CHECK HEALTH? Hathrel's weapon lands a blow! Leonius Libo takes 9 damage!
Leonius Libo's weapon lands a blow! Hathrel takes 12 damage!
You have been defeated. Game Over...
The thread 'MainThread' (0x1) has exited with code 0 (0x0).
The program 'python.exe' has exited with code 0 (0x0).{% endspoiler %}

{% spoiler And here's the code that makes it work. I'm not stingy about it, I doubt I've done anything groundbreaking. %}```Python
import random as r

#I'm lazy and don't want to type r.randinit every time I want to roll a number
def roll(num1, num2):
    roll = r.randint(num1, num2)
    return roll


class Human:
  def __init__(self, name, weapon, player=False):
    self.name = name
    #Randomize stats to make each fight truly unique. You might be a buff god made of iron, or a weak worm made of paper. Same goes for the NPC
    self.strength = roll(1, 10)
    self.defense = roll(1, 10)
    self.health = 100
    self.weapon = weapon
    self.player = player
    self.conscious = True
    self.is_defending = False
  def __repr__(self):
      return "A Human"
  
  #This took an absurd amount of my time, both in the original script and in this one
  def attack(self, target=None):
        if self.conscious and target.conscious:
            attack_roll = roll(1, 20) + self.weapon.attack
            defense_roll = roll(1, 20)
            if defense_roll + target.defense >= attack_roll + self.strength:
                print("{attacker}'s attack missed!".format(attacker=self.name))
            elif target.is_defending:
                block_chance = roll(1, 100) + target.defense
                if block_chance >= 90:
                    print("{defender} blocked the attack!".format(defender=target.name))
                else:
                    damage_roll = roll(1, 10) + self.weapon.damage - target.defense
                    if damage_roll < 0:
                        damage_roll = 0
                    print(
                        "{defender} partially deflected {attacker}'s attack! {defender} only takes {damage_roll} damage!".format(
                            defender=target.name, attacker=self.name, damage_roll=damage_roll
                        )
                    )
                    target.health -= damage_roll
                target.is_defending = False
            else:
                damage_roll = roll(1, 10) + self.weapon.damage
                #Go ahead and create a roll for the knockout check
                knockout_roll = roll(1, 100)
                #Check if you or the NPC has a mace, if so, here's the chance to crush their skull
                if self.weapon.make == "Mace" and knockout_roll >= 95:
                    target.conscious = False
                    print(
                        "{attacker}'s Mace lands on {defender}'s head with a loud crunch! {defender} has been knocked out!".format(
                            attacker=self.name, defender=target.name
                        )
                    )
                else:
                    target.health -= damage_roll
                    print(
                        "{attacker}'s weapon lands a blow! {defender} takes {damage} damage!".format(
                            attacker=self.name, defender=target.name, damage=damage_roll
                        )
                    )
        elif not target.conscious:
            print("{target} is already unconscious!".format(target=target.name))

  def defend(self):
    if self.conscious and self.is_defending == False:
      self.is_defending = True
      print("{defender} prepares for an attack...".format(defender = self.name))
    
    elif self.conscious and self.is_defending and self.player:
      print("{defender} can't be any more prepared for an attack!")

class Weapon:
    def __init__(self, make):
        self.make = make
        #kinda proud of this. I didn't do it alone, it was built on a suggestion by ChatGPT, but the values and use are mine
        self.stats = {
            "Longsword": {"damage": 5, "attack": 10},
            "Mace": {"damage": 3, "attack": 3},
            "Dagger": {"damage": 2, "attack": 15},
            "Greatsword": {"damage": 15, "attack": 5}
        }

        if self.make in self.stats:
            self.damage = self.stats[self.make]["damage"]
            self.attack = self.stats[self.make]["attack"]
        else:
            self.damage = 0
            self.attack = 0

    def __repr__(self):
        return self.make

longsword = Weapon("Longsword")
mace = Weapon("Mace")
dagger = Weapon("Dagger")
greatsword = Weapon("Greatsword")
weapon_rack = [longsword, mace, dagger, greatsword]

#Nailed this one on the first try. I think I'm most proud of this function, out of this entire script, small, but it was a victory for me
def assign_name():
    first_names = ["Nicostratus", "Sostratus", "Patricius", "Leonius", "Beringarius", "Nerva", "Saul", "Demetrius", "Viggo", "Philandrus", "Philocrates", "Ladislaus", "Amatus", "Fidelis"]
    last_names = ["Procillus", "Cossus", "Clineas", "Blasio", "Laevinus", "Regillensis", "Pictor", "Libo", "Spinther", "Rutilus", "Mocilla", "Sacerdos", "Sisenna", "Crus", "Potitus"]
    name = first_names[roll(0, len(first_names) - 1)] + " " + last_names[roll(0, len(last_names) - 1)]
    return name

npc = Human(assign_name(), weapon = weapon_rack[roll(0, len(weapon_rack) - 1)])

#No matter how much I stripped it down, I couldn't help but put in a little bit of fanfare
print("""

You enter the arena to the roar of the crowd. A man in the emperor's box with a booming voice shouts, "Welcome, one and all, back to The Arena!"

The crowd roars again. After the din dies down, the announcer continues. "Today, we see battle between two fierce competitors! First, we have {npc} using a {weapon1}!"

Once more the arena erupts in cheers, some even chanting {npc}'s name.

"And second, we have...
""".format(npc = npc.name, weapon1 = npc.weapon))

player_name = input("Enter your name: ")

print("""

{player} using a...

""".format(player = player_name))

weapon = ""
while weapon == "":
    weapon_choice = input("Select a weapon from the following: DAGGER, GREATSWORD, LONGSWORD, MACE ")
    clean_weapon = weapon_choice.lower()
    if clean_weapon == "dagger":
        weapon = dagger
    elif clean_weapon == "greatsword" or clean_weapon == "great sword":
        weapon = greatsword
    elif clean_weapon == "longsword" or clean_weapon == "long sword":
        weapon = longsword
    elif clean_weapon == "mace":
        weapon = mace
    else:
        print("{choice} is not a valid choice".format(choice = weapon_choice))

print("""

{player_weapon}!" the crowd erupts into cheers again. Some chanting your name this time.

"Without further ado ladies and gentlemen, let the battle BEGIN!" he shouts, the crowd erupts in roars and your opponent rushes you!

""".format(player_weapon = weapon))
player = Human(player_name, weapon, True)

#Flags to make sure neither the user nor the NPC gets unlimited turns
player_turn = True
npc_turn = False

#Main game loop. If you or the NPC gets knocked out, game over
while player.conscious and npc.conscious:
    while player_turn == True and npc_turn == False:
        player_action = input("What do you want to do? ATTACK, DEFEND, or CHECK HEALTH? ").lower()
        if player_action == "attack":
            player.attack(npc)
            player_turn = False
            npc_turn = True
        elif player_action == "defend":
            player.defend()
            player_turn = False
            npc_turn = True
        elif player_action == "check health":
            print("You have {health} health left.".format(health = player.health))
        else:
            print("You can't do that!")
        #Admittedly this was annoying difficult. Hours upon hours figuring out why the loop wouldn't break appropriately or return the
        #right output. All because I declared this and the NPC unconscious condition at the top of the game loop
        if npc.health <= 0:

            print("You have won! Congratulations! The crowd roars and chants your name over and over.")
            npc.conscious = False

    #NPC Logic. Not smart enough to be called AI. Ensures you get a unique fight every game load.
    while npc_turn == True and player_turn == False:
        action_choice = roll(1,100)
        if action_choice >= 80:
            npc.defend()
            npc_turn = False
            player_turn = True
        else:
            npc.attack(player)
            npc_turn = False
            player_turn = True
        if player.health <= 0:
            print("You have been defeated. Game Over...")
            player.conscious = False
```{% endspoiler %}