Function still running after using os._exit() command

I’m making a text-based rpg right now, and I have just gotten completely stumped from my main menu quit option just completely not working, and I cannot find a single reason why, I slammed it into chatgpt like 20 times to see if it could find the issue, and it said its unusual. It also specifically happens after you’ve gone through a battle atleast one time, but when you have yet to play the game once, it quits as intended. Also probably worth noting is that this is using the default python from replit.com

the loading_screen() function referenced in the main menu just uses the time.sleep() function to do a “…” after you enter the parameter for a quote

loading_screen() function, in case it finds itself being a problem

def loading_screen(seconds, quote):
  print(quote, end="", flush=True)
  for _ in range(seconds):
    time.sleep(0.5)
    print(".", end="", flush=True)
  print()

savegame() function

def savegame():
  global damageThisBattle
  global itemsUsed
  global playerExperience
  global experienceToNextLevel
  global currentLevel
  global ogresKilled
  global skeletonsKilled
  saveFile = open("savestates.txt", "r+")
  lines = saveFile.readlines()
  saveFile.seek(0)  
  # Move the cursor to the beginning of the file

  ogresKilled = ogresKilled + int(lines[0].strip())
  skeletonsKilled = skeletonsKilled + int(lines[1].strip())
  totalDamage = int(lines[2].strip()) + damageThisBattle
  totalItemsUsed = int(lines[3].strip()) + itemsUsed

  # Update the lines with the new values
  saveFile.write(str(ogresKilled) + "\n")
  saveFile.write(str(skeletonsKilled) + "\n")
  saveFile.write(str(totalDamage) + "\n")
  saveFile.write(str(totalItemsUsed) + "\n")
  saveFile.write(str(currentExperience) + "\n")
  saveFile.write(str(currentLevel) + "\n")

  # Truncate the file in case the new content is smaller     
  # than the old content
  saveFile.truncate()

  saveFile.close()

Main menu code ran in a loop where the other condition is (if inBattle == True:)

    while playInput not in validPlayInputs:
      printMainMenu()
      playInput=input("\n1. Play\n2. Check Save\n3. Wipe save"
                      "\n4. Quit\n")
      if playInput=="1":
        loadgame()
        if newGame==False:
          loading_screen(3,"Loading Save")
          loading_screen(3,"Starting game")
          print("")
          print("")
        else:
          loading_screen(3,"Starting New Game")
  
      elif playInput=="2":
        checkSave()
      elif playInput=="3":
        wipeSave()
      elif playInput=="4":
        loading_screen(3,"Quitting")
        os._exit(0)
      

The entire code itself:

import random
import math
import sys
import subprocess
import time
import os


def restart_program():
    python = sys.executable
    subprocess.call([python] + sys.argv)

# Lots of variables
quit_game=False
inBattle=False
experienceToNextLevel=0
currentExperience=0
experienceDefault=30
currentLevel=0
levelMultiplier=1.3
ogresKilled=0
skeletonsKilled=0
newGame=False
lineNo=0
turnCount=1
backToSelection=False
actionInput=""
skillInput=""
weaponSelect=""
itemUse=""
itemsUsed=0
totalItemsUsed=0
totalDamage=0
damageThisBattle=0
playInput=""
areaSelect=""
saveGameInput=""
validSaveInputs=["Y","N"]
turnsOnFire=0
playerHealth=20
playerMaxHealth=20
playerMana=10
playerMaxMana=10
playerExperience=0
battleExperience=0
deathPrompt="You died!"
missPrompt="The attack missed!"
# Enemy Stat Variables
enemy_type=""
enemyDamage=0
enemyHealth=0
enemyMaxHealth=0
enemyHealthBarFilled=0
specificAttack=0
attackPrompt1,attackPrompt2="",""
attack1Low,attack1High=0,0
attack2Low,attack2High=0,0
enemyOnFire=False
enemyDefeated=True
enemyKillsThisBattle=0
playerExperience=""
# 
weaponDamage=0
weaponAccuracy=0
skillAccuracy=0
playerAttack=0
defended=False
validCommands=["1","SKILL","2","GUARD","3","ITEM","4","RUN"]
validItemCommands=["1","HEALTH","2","MANA","3"]
validWeaponCommands=["1","2","3","4"]
validPlayInputs=["1","3"]
validSkillCommands=["1","2","3","4","5"]
validAreas=["1","2","FOREST","DESERT"]
healthPotions=2
manaPotions=1
seconds=0
quote=""
area=""
# Dictionaries for data storage and calling
# The Chance of the attack is added up from the previous num to 100,
# So if attack 1 goes from 0 to 80 its an 80% chance of happening,
# And attack 2 goes from 81 to 100 its a 20% chance of happening
enemy_types = {
"Ogre": {
    "attack_prompt1": "The Ogre swings his club at you!",
    "attack_prompt2": "The Ogre charges at you!",
    "attack1_accuracy": 90,
    "attack2_accuracy": 60,
    "attack1_damageRange": (4, 6),
    "attack2_damageRange": (4, 8),
    "attack1_chance": 80,
    "attack2_chance": 81,
    "max_health": 20,
    "Experience_reward": 15},
"Skeleton": {
    "attack_prompt1": "The Skeleton swings his sword at you!",
    "attack_prompt2": "The Skeleton tries to deal a devastating strike!",
    "attack1_accuracy": 85,
    "attack2_accuracy": 25,
    "attack1_damageRange": (7,9),
    "attack2_damageRange": (10,15),
    "attack1_chance": 90,
    "attack2_chance": 91,
    "max_health": 35,
    "Experience_reward": 40}
}
# Defining of functions
# Experience functions
def calculateExperienceRequired():
  global levelMultiplier
  global currentLevel
  global currentExperience
  global experienceDefault
  return int(experienceDefault * (levelMultiplier ** currentLevel))
def currentPlayerLevel():
  global currentLevel
  global currentExperience
  global battleExperience
  global experienceToNextLevel
  currentExperience+=battleExperience
  while currentExperience >= calculateExperienceRequired():
    currentExperience=currentExperience - calculateExperienceRequired()
    currentLevel+=1
    print(f"You leveled up to level {currentLevel}!")
  battleExperience=0
  experienceToNextLevel=calculateExperienceRequired()-currentExperience

def currentPlayerLevelDebug():
  global currentLevel
  global currentExperience
  global battleExperience
  print("current experience:",currentExperience)
  currentExperience+=battleExperience
  print("current experience:",currentExperience)
  if currentExperience >= calculateExperienceRequired():
    print("current level",currentLevel)
    currentLevel+=1
    print("You leveled up to level:",currentLevel)
    print("current level",currentLevel)
  currentExperience=currentExperience%calculateExperienceRequired()
  print("current experience:",currentExperience)
  
# Loading screen function, quitting screen, rebooting screen, etc...
def loading_screen(seconds, quote):
  print(quote, end="", flush=True)
  for _ in range(seconds):
    time.sleep(0.5)
    print(".", end="", flush=True)
  print()
# Check save function
def checkSave():
    try:
        loading_screen(3,"Checking save")
        saveFile = open("savestates.txt", "r")
        lines = saveFile.readlines()
        if len(lines) >= 6 and all(line.strip().isdigit() for line in lines):
            print("Save data found.")
            print("Total Ogres defeated:", lines[0])
            print("Total Skeletons defeated:", lines[1])
            print("Total damage dealt:", lines[2])
            print("Total items used:", lines[3])
            print("Experience:", lines[4])
            print("Level:", lines[5])
        else:
            loading_screen(3,"Invalid format, initializing save file")
            saveFile = open("savestates.txt", "w")
            saveFile.write("0\n0\n0\n0\n0\n0")
            saveFile.close()
            print("Save file initialized.")
    except Exception as e:
        print("Error:", e)
    finally:
        saveFile.close()


# Wipe save function
def wipeSave():
  saveFile=open("savestates.txt","w")
  saveFile.write("0\n0\n0\n0\n0\n0")
  loading_screen(3,"Rebooting")
  saveFile.close()
  restart_program()
# Saving function
def savegame():
  global damageThisBattle
  global itemsUsed
  global playerExperience
  global experienceToNextLevel
  global currentLevel
  global ogresKilled
  global skeletonsKilled
  saveFile = open("savestates.txt", "r+")
  lines = saveFile.readlines()
  saveFile.seek(0)  
  # Move the cursor to the beginning of the file

  ogresKilled = ogresKilled + int(lines[0].strip())
  skeletonsKilled = skeletonsKilled + int(lines[1].strip())
  totalDamage = int(lines[2].strip()) + damageThisBattle
  totalItemsUsed = int(lines[3].strip()) + itemsUsed

  # Update the lines with the new values
  saveFile.write(str(ogresKilled) + "\n")
  saveFile.write(str(skeletonsKilled) + "\n")
  saveFile.write(str(totalDamage) + "\n")
  saveFile.write(str(totalItemsUsed) + "\n")
  saveFile.write(str(currentExperience) + "\n")
  saveFile.write(str(currentLevel) + "\n")

  # Truncate the file in case the new content is smaller     
  # than the old content
  saveFile.truncate()

  saveFile.close()
# Loading Function
def loadgame():
  global playerExperience
  global currentLevel
  global newGame
  global experienceToNextLevel
  global currentExperience
  try:
    saveFile = open("savestates.txt", "r")
    lines = saveFile.readlines()
    if len(lines) >= 6 and all(value.strip().isdigit() and int(value.strip()) \
      == 0 for value in lines):
      print("New save detected!")
      currentLevel+=1
      newGame=True
    elif len(lines) >= 6 and all(value.strip().isdigit() for value in lines[:6]):
      print("Save File Found!")
      newGame=False
      currentExperience = int(lines[4].strip())
      currentLevel = int(lines[5].strip())
    saveFile.close()
  except Exception as e:
    print("No save detected, creating new save file.")

# Player Statistic Initialization based on level
def playerStats():
  global playerMaxHealth
  global playerHealth
  global playerMaxMana
  global playerMana
  global weaponDamage
  playerMaxHealth=(currentLevel-1)*3+20
  playerHealth=playerMaxHealth
  playerMaxMana=(currentLevel-1)*2+10
  playerMana=playerMaxMana
  weaponDamage=math.ceil(weaponDamage+(weaponDamage*currentLevel-1)*0.1)
# Enemy Type Finding for Statistic Updates
def enemyTypeKilled():
  global enemy_type
  global ogresKilled
  global skeletonsKilled
  if enemy_type == "Ogre":
    ogresKilled+=1
  elif enemy_type == "Skeleton":
    skeletonsKilled+=1
# Enemy Initialization Upon Choosing Area
def initializeEnemy(area):
  global enemy_type, enemyMaxHealth, enemyHealth, enemy_types
  if area == "Forest":
    enemy_type = "Ogre"
  elif area == "Desert":
    enemy_type = "Skeleton"
  enemyMaxHealth = enemy_types[enemy_type]["max_health"]
  enemyHealth = enemyMaxHealth
# enemyAttack(s) function(s)
def enemyAttack(playerHealth,defended,enemy_type):
  attackPrompt1 = enemy_types[enemy_type]["attack_prompt1"]
  attackPrompt2 = enemy_types[enemy_type]["attack_prompt2"]
  attack1Accuracy = enemy_types[enemy_type]["attack1_accuracy"]
  attack2Accuracy = enemy_types[enemy_type]["attack2_accuracy"]
  attack1Low, attack1High = enemy_types[enemy_type]["attack1_damageRange"]
  attack2Low, attack2High = enemy_types[enemy_type]["attack2_damageRange"]
  attack1Chance = enemy_types[enemy_type]["attack1_chance"]
  attack2Chance = enemy_types[enemy_type]["attack2_chance"]
  if playerHealth<=0:
    print(deathPrompt)
  specificAttack=random.randint(1,100)
  if specificAttack<=attack1Chance:
    print(attackPrompt1)
    if random.randint(1,100)<=attack1Accuracy:
      enemyDamage=random.randint(attack1Low,attack1High)
      if defended==True:
        enemyDamage=math.ceil(enemyDamage/2)
      print("Damage taken:",enemyDamage)
      playerHealth=playerHealth-enemyDamage
    else:
      print(missPrompt)
  elif specificAttack>=attack2Chance:
    print(attackPrompt2)
    if random.randint(1,100)<=attack2Accuracy:
      enemyDamage=random.randint(attack1Low,attack2High)
      if defended==True:
        enemyDamage=math.ceil(enemyDamage/2)
      playerHealth=playerHealth-enemyDamage
      print("Damage taken:",enemyDamage)
    else:
      print(missPrompt)
  
  return playerHealth
# Main Menu Function
def printMainMenu():
  print("╔══════════════════════════════════════╗\n"
  "║         Epic Battle Simulator        ║\n"
  "╚══════════════════════════════════════╝")
# Player Status Bars Function
def playerStatusBars():
  global playerHealth
  global playerMaxHealth
  global playerMaxMana
  global playerMana
  global currentExperience
  global experienceToNextLevel
  playerHealthRatio = playerHealth / playerMaxHealth
  playerManaRatio = playerMana / playerMaxMana
  try:
    playerExpRatio=currentExperience / calculateExperienceRequired()
  except ZeroDivisionError:
      playerExpRatio=0
  barLength = 20
  healthBarFilled = int(barLength * playerHealthRatio)
  manaBarFilled = int(barLength * playerManaRatio)
  expBarFilled= int(barLength*playerExpRatio)
  

  healthBar = "[" + "■" * healthBarFilled + " " * (barLength - healthBarFilled) + "]"
  manaBar = "[" + "■" * manaBarFilled + " " * (barLength - manaBarFilled) + "]"
  expBar = "[" + "■" * expBarFilled + " " * (barLength - expBarFilled) + "]"

  print(f"Health: {healthBar} {playerHealth}/{playerMaxHealth}")
  print(f"  Mana: {manaBar} {playerMana}/{playerMaxMana}")
  print(f"   Exp: {expBar} {currentExperience}/{calculateExperienceRequired()}")
# Enemy Status Bar Function
def enemyStatusBar(enemyHealth, enemyMaxHealth):
  enemyHealthRatio = enemyHealth / enemyMaxHealth
  barLength = 20
  enemyHealthBarFilled = int(barLength * enemyHealthRatio)

  enemyHealthBar = "[" + "■" * enemyHealthBarFilled + " " * (barLength - enemyHealthBarFilled) + "]"
  print(f"Enemy:  {enemyHealthBar} {enemy_type}")





###########################
# Game Loop
###########################

while True:
  if quit_game:
    loading_screen(3, "Quitting")
    sys.exit()
  while inBattle==True:
    if playInput=="4":
      sys.exit()
    actionInput=""
    defended=False
    backToSelection=False
    print("--------------")
    print("Current turn: %d" % turnCount)
    enemyStatusBar(enemyHealth,enemyMaxHealth)
    playerStatusBars()
    print("--------------")
    while actionInput not in validCommands:
      actionInput=input("1. Skill\n2. Guard\n3. Item\n4. Run\n--------------\n")
      if actionInput not in validCommands:
        print("Valid Commands =", validCommands)
  # Function 1: Skill
    skillInput=""
    if actionInput.upper()=="SKILL" or actionInput.upper()=="1":
      while skillInput not in validSkillCommands or skillInput=="4":
        skillInput=input("--------------\n"
                          "1. Basic attack M:0\n2. Strong attack M:5\n3. Blazing "
                          "Slash M:10\n4. Attack Descriptions\n5. Back to selection"
                          "\n--------------\n")
        if skillInput=="1":
          print("--------------")
          print("You attack the enemy!")
          playerAttack=random.randint(weaponDamage-1,weaponDamage+1)
          if random.randint(1,100)<=weaponAccuracy:
            print("You hit the enemy!")
            print("Damage dealt:",playerAttack)
            enemyHealth=enemyHealth-playerAttack
            damageThisBattle+=playerAttack
          else:
            print("You missed!")
            playerAttack=0
          if enemyHealth<0:
            enemyHealth=0
        elif skillInput=="2" and playerMana >=5:
          if playerMana<5:
            backToSelection=True
            print("Not enough mana")
          playerMana-=5
          print("--------------")
          print("You use a strong attack!")
          playerAttack=random.randint(weaponDamage-1,weaponDamage+1)
          if random.randint(1,100)<=weaponAccuracy:
            playerAttack=math.ceil(playerAttack*1.30)
            print("You hit the enemy!")
            print("Damage dealt:",playerAttack)
            enemyHealth=enemyHealth-playerAttack
            damageThisBattle+=playerAttack
          else:
            print("You missed!")
            playerAttack=0
          if enemyHealth<0:
            enemyHealth=0
        elif skillInput=="3" and playerMana>=10:
          if playerMana<10:
            backToSelection=True
            print("Not enough mana")
          playerMana-=10
          enemyOnFire=True
          turnsOnFire=3
          print("--------------")
          print("You use blazing slash!")
          print("Enemy now burning!")
          playerAttack=random.randint(weaponDamage-1,weaponDamage+1)
          playerAttack=math.ceil(playerAttack*1.25)
          print("Damage dealt:",playerAttack)
          enemyHealth=enemyHealth-playerAttack
          damageThisBattle+=playerAttack
        if enemyHealth<0:
          enemyHealth=0
        elif skillInput=="4":
          print("Basic and Strong attacks have a chance to miss"
                " based on weapon accuracy.\n--------------")
          print("Basic attack: Deals damage based on your weapon damage +/- 1. Mana:0"
                "\n--------------")
          print("Strong attack: Deals 30% more damage,"
                " rounded up, than a regular attack. Mana:5\n--------------")
          print("Blazing Slash: Guaranteed hit, Deals 25% more damage,"
                " rounded up, than a regular attack, and "
                "lights the enemy on fire, dealing 2 damage per turn"
                " for 3 turns. Mana:10"
                "\n--------------")
        elif skillInput=="5":
          backToSelection=True
      
          
  # Function 2: Guard
    elif actionInput.upper()=="GUARD" or actionInput.upper()=="2":
      print("You guard!")
      print("You regenerated 4 mana!")
      playerMana+=4
      if playerMana>playerMaxMana:
        playerMana=playerMaxMana
      defended=True
      
  # Function 3: Item
    elif actionInput.upper()=="ITEM" or actionInput.upper()=="3":
      itemUse=""
      if healthPotions<=0 and manaPotions<=0:
        print("You have no items remaining!")
        continue
      while itemUse not in validItemCommands:
        itemUse=input("What item do you want to use?\n1. Health Potion (%d)"
                      "\n2. Mana Potion (%d)\n"
                      "3. Back to selection"
                      "\n--------------\n" % (healthPotions,manaPotions))
      if itemUse.upper() in validItemCommands:
        if itemUse.upper()=="1" or itemUse.upper()=="HEALTH":
          if healthPotions<=0:
            print("None left!")
            validItemCommands.remove("1")
            validItemCommands.remove("HEALTH")
            continue
          print("You use a health potion!")
          itemsUsed+=1
          playerHealth+=10
          if playerHealth>playerMaxHealth:
            playerHealth=playerMaxHealth
          healthPotions-=1
        elif itemUse.upper()=="2" or itemUse.upper()=="MANA":
          if manaPotions<=0:
            print("None left!")
            validItemCommands.remove("2")
            validItemCommands.remove("MANA")
            continue
          print("You use a mana potion!")
          itemsUsed+=1
          playerMana+=10
          if playerMana>playerMaxMana:
            playerMana=playerMaxMana
          manaPotions-=1
        elif itemUse.upper()=="3":
          backToSelection=True
  
  
    
  # Function 4: Run
    elif actionInput.upper()=="RUN" or actionInput.upper()=="4":
      print("Coward!")
      break
      
    # Final Statements
    if backToSelection==True:
      continue
    turnCount+=1
    if enemyOnFire==True:
      enemyHealth-=2
      damageThisBattle+=2
      print("The enemy takes 2 fire damage!")
      turnsOnFire-=1
      if turnsOnFire<=0:
        print("Enemy stopped burning")
        enemyOnFire=False
    if enemyHealth<=0:
      inBattle=False
      area=""
      areaSelect=""
      print("Enemy defeated!")
      enemyDefeated=True
      battleExperience=enemy_types[enemy_type]["Experience_reward"]
      currentPlayerLevel()
      enemyTypeKilled()
      print("Turns:", turnCount)
      while saveGameInput not in validSaveInputs and playInput!=4:
        saveGameInput=input("Do you want to save your game? (y/n)\n")
        if saveGameInput.upper()=="Y":
          savegame()
          loading_screen(3,"Saving and returning to main menu")
          restart_program()
        else:
          saveGameInput=""
          while saveGameInput not in validSaveInputs:
            saveGameInput=input("Do you want to quit without saving? (y/n)\n")
            if saveGameInput.upper()=="Y":
              loading_screen(3,"Returning to main menu")
              restart_program()
            else:
              savegame()
              loading_screen(3,"Saving and returning to main menu")
              restart_program()
              break
    else:
      playerHealth=enemyAttack(playerHealth,defended,enemy_type)
    if playerHealth<=0:
      loading_screen(3,"You died! Returning to main menu")
      restart_program()
      
  else:
    # Actual Main Menu
    while playInput not in validPlayInputs:
      printMainMenu()
      playInput=input("\n1. Play\n2. Check Save\n3. Wipe save"
                      "\n4. Quit\n")
      if playInput=="1":
        loadgame()
        if newGame==False:
          loading_screen(3,"Loading Save")
          loading_screen(3,"Starting game")
          print("")
          print("")
        else:
          loading_screen(3,"Starting New Game")
  
      elif playInput=="2":
        checkSave()
      elif playInput=="3":
        wipeSave()
      elif playInput=="4":
        loading_screen(3,"Quitting")
        os._exit(0)
      
  
    # Weapon selection prior to fight  
    while weaponSelect not in validWeaponCommands or weaponSelect=="4":
      weaponSelect=input("--------------\nWhat weapon do you want to use?"
                         " \n1. Sword\n2. Greatsword\n3. Shortsword"
                         "\n4. Weapon stats\n--------------\n")
      if weaponSelect.upper()=="1":
        weaponDamage=6
        weaponAccuracy=80
      elif weaponSelect.upper()=="2":
        weaponDamage=10
        weaponAccuracy=50
      elif weaponSelect.upper()=="3":
        weaponDamage=5
        weaponAccuracy=100
      elif weaponSelect.upper()=="4":
        print("Sword\n--------\nDamage: 6\nAccuracy: 80\n"
              "A low risk and medium reward weapon with good accuracy and damage\n"
              "Tip: A versatile weapon for any playstyle"
              "\n\nGreatsword\n--------\nDamage: 10\nAccuracy: 50"
              "\nA high risk high reward weapon with low accuracy and high damage\n"
              "Tip: Good for powerful guaranteed hit skills"
              "\n\nShortsword\n--------\nDamage: 5\nAccuracy: 100\n"
              "A zero risk and low reward weapon with perfect accuracy and low damage\n"
              "Tip: Good for repeated strong attacks\n")
  
    while areaSelect not in validAreas:
      areaSelect=input("--------------\nWhere do you want to go?\n"
                       "1. Forest\n2. Desert\n").upper()
      if areaSelect == "1" or areaSelect == "FOREST":
        loading_screen(3,"Entering Forest")
        area="Forest"
        inBattle=True
        initializeEnemy(area)
        playerStats()
        damageThisBattle=0
      elif areaSelect=="DESERT" or areaSelect=="2":
        loading_screen(3,"Entering Desert")
        area="Desert"
        inBattle=True
        initializeEnemy(area)
        playerStats()
        damageThisBattle=0
      else:
        area="Desert"

UPDATE:
I’ve run the code in PyCharm community edition as well to make sure it wasn’t replit.com causing the issue, and it happens on PyCharm as well. It is running python version: 3.11

UPDATE 2:
I believe I have solved the issue, and it was caused by the restart_program() function, which I’ve now updated to instead actually return the player to the main menu through breaking of the loop.

the new code in which the updated lines are relatively close to the bottom, before the main menu loop:

import random
import math
import sys
import subprocess
import time
import os


def restart_program():
    python = sys.executable
    subprocess.call([python] + sys.argv)

# Lots of variables
quit_game=False
inBattle=False
experienceToNextLevel=0
currentExperience=0
experienceDefault=30
currentLevel=0
levelMultiplier=1.3
ogresKilled=0
skeletonsKilled=0
newGame=False
lineNo=0
turnCount=1
backToSelection=False
actionInput=""
skillInput=""
weaponSelect=""
itemUse=""
itemsUsed=0
totalItemsUsed=0
totalDamage=0
damageThisBattle=0
playInput=""
areaSelect=""
saveGameInput=""
validSaveInputs=["Y","N"]
turnsOnFire=0
playerHealth=20
playerMaxHealth=20
playerMana=10
playerMaxMana=10
playerExperience=0
battleExperience=0
deathPrompt="You died!"
missPrompt="The attack missed!"
# Enemy Stat Variables
enemy_type=""
enemyDamage=0
enemyHealth=0
enemyMaxHealth=0
enemyHealthBarFilled=0
specificAttack=0
attackPrompt1,attackPrompt2="",""
attack1Low,attack1High=0,0
attack2Low,attack2High=0,0
enemyOnFire=False
enemyDefeated=True
enemyKillsThisBattle=0
playerExperience=""
# 
weaponDamage=0
weaponAccuracy=0
skillAccuracy=0
playerAttack=0
defended=False
validCommands=["1","SKILL","2","GUARD","3","ITEM","4","RUN"]
validItemCommands=["1","HEALTH","2","MANA","3"]
validWeaponCommands=["1","2","3","4"]
validPlayInputs=["1","3"]
validSkillCommands=["1","2","3","4","5"]
validAreas=["1","2","FOREST","DESERT"]
healthPotions=2
manaPotions=1
seconds=0
quote=""
area=""
# Dictionaries for data storage and calling
# The Chance of the attack is added up from the previous num to 100,
# So if attack 1 goes from 0 to 80 its an 80% chance of happening,
# And attack 2 goes from 81 to 100 its a 20% chance of happening
enemy_types = {
"Ogre": {
    "attack_prompt1": "The Ogre swings his club at you!",
    "attack_prompt2": "The Ogre charges at you!",
    "attack1_accuracy": 90,
    "attack2_accuracy": 60,
    "attack1_damageRange": (4, 6),
    "attack2_damageRange": (4, 8),
    "attack1_chance": 80,
    "attack2_chance": 81,
    "max_health": 20,
    "Experience_reward": 15},
"Skeleton": {
    "attack_prompt1": "The Skeleton swings his sword at you!",
    "attack_prompt2": "The Skeleton tries to deal a devastating strike!",
    "attack1_accuracy": 85,
    "attack2_accuracy": 25,
    "attack1_damageRange": (7,9),
    "attack2_damageRange": (10,15),
    "attack1_chance": 90,
    "attack2_chance": 91,
    "max_health": 35,
    "Experience_reward": 40}
}
# Defining of functions
# Experience functions
def calculateExperienceRequired():
  global levelMultiplier
  global currentLevel
  global currentExperience
  global experienceDefault
  return int(experienceDefault * (levelMultiplier ** currentLevel))
def currentPlayerLevel():
  global currentLevel
  global currentExperience
  global battleExperience
  global experienceToNextLevel
  currentExperience+=battleExperience
  while currentExperience >= calculateExperienceRequired():
    currentExperience=currentExperience - calculateExperienceRequired()
    currentLevel+=1
    print(f"You leveled up to level {currentLevel}!")
  battleExperience=0
  experienceToNextLevel=calculateExperienceRequired()-currentExperience

def currentPlayerLevelDebug():
  global currentLevel
  global currentExperience
  global battleExperience
  print("current experience:",currentExperience)
  currentExperience+=battleExperience
  print("current experience:",currentExperience)
  if currentExperience >= calculateExperienceRequired():
    print("current level",currentLevel)
    currentLevel+=1
    print("You leveled up to level:",currentLevel)
    print("current level",currentLevel)
  currentExperience=currentExperience%calculateExperienceRequired()
  print("current experience:",currentExperience)
  
# Loading screen function, quitting screen, rebooting screen, etc...
def loading_screen(seconds, quote):
  print(quote, end="", flush=True)
  for _ in range(seconds):
    time.sleep(0.5)
    print(".", end="", flush=True)
  print()
# Check save function
def checkSave():
    try:
        loading_screen(3,"Checking save")
        saveFile = open("savestates.txt", "r")
        lines = saveFile.readlines()
        if len(lines) >= 6 and all(line.strip().isdigit() for line in lines):
            print("Save data found.")
            print("Total Ogres defeated:", lines[0])
            print("Total Skeletons defeated:", lines[1])
            print("Total damage dealt:", lines[2])
            print("Total items used:", lines[3])
            print("Experience:", lines[4])
            print("Level:", lines[5])
        else:
            loading_screen(3,"Invalid format, initializing save file")
            saveFile = open("savestates.txt", "w")
            saveFile.write("0\n0\n0\n0\n0\n0")
            saveFile.close()
            print("Save file initialized.")
    except Exception as e:
        print("Error:", e)
    finally:
        saveFile.close()


# Wipe save function
def wipeSave():
  saveFile=open("savestates.txt","w")
  saveFile.write("0\n0\n0\n0\n0\n0")
  loading_screen(3,"Rebooting")
  saveFile.close()
  restart_program()
# Saving function
def savegame():
  global damageThisBattle
  global itemsUsed
  global playerExperience
  global experienceToNextLevel
  global currentLevel
  global ogresKilled
  global skeletonsKilled
  saveFile = open("savestates.txt", "r+")
  lines = saveFile.readlines()
  saveFile.seek(0)  
  # Move the cursor to the beginning of the file

  ogresKilled = ogresKilled + int(lines[0].strip())
  skeletonsKilled = skeletonsKilled + int(lines[1].strip())
  totalDamage = int(lines[2].strip()) + damageThisBattle
  totalItemsUsed = int(lines[3].strip()) + itemsUsed

  # Update the lines with the new values
  saveFile.write(str(ogresKilled) + "\n")
  saveFile.write(str(skeletonsKilled) + "\n")
  saveFile.write(str(totalDamage) + "\n")
  saveFile.write(str(totalItemsUsed) + "\n")
  saveFile.write(str(currentExperience) + "\n")
  saveFile.write(str(currentLevel) + "\n")

  # Truncate the file in case the new content is smaller     
  # than the old content
  saveFile.truncate()

  saveFile.close()
# Loading Function
def loadgame():
  global playerExperience
  global currentLevel
  global newGame
  global experienceToNextLevel
  global currentExperience
  try:
    saveFile = open("savestates.txt", "r")
    lines = saveFile.readlines()
    if len(lines) >= 6 and all(value.strip().isdigit() and int(value.strip()) \
      == 0 for value in lines):
      print("New save detected!")
      currentLevel+=1
      newGame=True
    elif len(lines) >= 6 and all(value.strip().isdigit() for value in lines[:6]):
      print("Save File Found!")
      newGame=False
      currentExperience = int(lines[4].strip())
      currentLevel = int(lines[5].strip())
    saveFile.close()
  except Exception as e:
    print("No save detected, creating new save file.")
    wipesave()

# Player Statistic Initialization based on level
def playerStats():
  global playerMaxHealth
  global playerHealth
  global playerMaxMana
  global playerMana
  global weaponDamage
  playerMaxHealth=(currentLevel-1)*3+20
  playerHealth=playerMaxHealth
  playerMaxMana=(currentLevel-1)*2+10
  playerMana=playerMaxMana
  weaponDamage=math.ceil(weaponDamage+(weaponDamage*currentLevel-1)*0.1)
# Enemy Type Finding for Statistic Updates
def enemyTypeKilled():
  global enemy_type
  global ogresKilled
  global skeletonsKilled
  if enemy_type == "Ogre":
    ogresKilled+=1
  elif enemy_type == "Skeleton":
    skeletonsKilled+=1
# Enemy Initialization Upon Choosing Area
def initializeEnemy(area):
  global enemy_type, enemyMaxHealth, enemyHealth, enemy_types
  if area == "Forest":
    enemy_type = "Ogre"
  elif area == "Desert":
    enemy_type = "Skeleton"
  enemyMaxHealth = enemy_types[enemy_type]["max_health"]
  enemyHealth = enemyMaxHealth
# enemyAttack(s) function(s)
def enemyAttack(playerHealth,defended,enemy_type):
  attackPrompt1 = enemy_types[enemy_type]["attack_prompt1"]
  attackPrompt2 = enemy_types[enemy_type]["attack_prompt2"]
  attack1Accuracy = enemy_types[enemy_type]["attack1_accuracy"]
  attack2Accuracy = enemy_types[enemy_type]["attack2_accuracy"]
  attack1Low, attack1High = enemy_types[enemy_type]["attack1_damageRange"]
  attack2Low, attack2High = enemy_types[enemy_type]["attack2_damageRange"]
  attack1Chance = enemy_types[enemy_type]["attack1_chance"]
  attack2Chance = enemy_types[enemy_type]["attack2_chance"]
  if playerHealth<=0:
    print(deathPrompt)
  specificAttack=random.randint(1,100)
  if specificAttack<=attack1Chance:
    print(attackPrompt1)
    if random.randint(1,100)<=attack1Accuracy:
      enemyDamage=random.randint(attack1Low,attack1High)
      if defended==True:
        enemyDamage=math.ceil(enemyDamage/2)
      print("Damage taken:",enemyDamage)
      playerHealth=playerHealth-enemyDamage
    else:
      print(missPrompt)
  elif specificAttack>=attack2Chance:
    print(attackPrompt2)
    if random.randint(1,100)<=attack2Accuracy:
      enemyDamage=random.randint(attack1Low,attack2High)
      if defended==True:
        enemyDamage=math.ceil(enemyDamage/2)
      playerHealth=playerHealth-enemyDamage
      print("Damage taken:",enemyDamage)
    else:
      print(missPrompt)
  
  return playerHealth
# Main Menu Function
def printMainMenu():
  print("╔══════════════════════════════════════╗\n"
  "║         Epic Battle Simulator        ║\n"
  "╚══════════════════════════════════════╝")
# Player Status Bars Function
def playerStatusBars():
  global playerHealth
  global playerMaxHealth
  global playerMaxMana
  global playerMana
  global currentExperience
  global experienceToNextLevel
  playerHealthRatio = playerHealth / playerMaxHealth
  playerManaRatio = playerMana / playerMaxMana
  try:
    playerExpRatio=currentExperience / calculateExperienceRequired()
  except ZeroDivisionError:
      playerExpRatio=0
  barLength = 20
  healthBarFilled = int(barLength * playerHealthRatio)
  manaBarFilled = int(barLength * playerManaRatio)
  expBarFilled= int(barLength*playerExpRatio)
  

  healthBar = "[" + "■" * healthBarFilled + " " * (barLength - healthBarFilled) + "]"
  manaBar = "[" + "■" * manaBarFilled + " " * (barLength - manaBarFilled) + "]"
  expBar = "[" + "■" * expBarFilled + " " * (barLength - expBarFilled) + "]"

  print(f"Health: {healthBar} {playerHealth}/{playerMaxHealth}")
  print(f"  Mana: {manaBar} {playerMana}/{playerMaxMana}")
  print(f"   Exp: {expBar} {currentExperience}/{calculateExperienceRequired()}")
# Enemy Status Bar Function
def enemyStatusBar(enemyHealth, enemyMaxHealth):
  enemyHealthRatio = enemyHealth / enemyMaxHealth
  barLength = 20
  enemyHealthBarFilled = int(barLength * enemyHealthRatio)

  enemyHealthBar = "[" + "■" * enemyHealthBarFilled + " " * (barLength - enemyHealthBarFilled) + "]"
  print(f"Enemy:  {enemyHealthBar} {enemy_type}")





###########################
# Game Loop
###########################

while True:
  if quit_game:
    loading_screen(3, "Quitting")
    sys.exit()
  while inBattle==True:
    if playInput=="4":
      sys.exit()
    actionInput=""
    defended=False
    backToSelection=False
    print("--------------")
    print("Current turn: %d" % turnCount)
    enemyStatusBar(enemyHealth,enemyMaxHealth)
    playerStatusBars()
    print("--------------")
    while actionInput not in validCommands:
      actionInput=input("1. Skill\n2. Guard\n3. Item\n4. Run\n--------------\n")
      if actionInput not in validCommands:
        print("Valid Commands =", validCommands)
  # Function 1: Skill
    skillInput=""
    if actionInput.upper()=="SKILL" or actionInput.upper()=="1":
      while skillInput not in validSkillCommands or skillInput=="4":
        skillInput=input("--------------\n"
                          "1. Basic attack M:0\n2. Strong attack M:5\n3. Blazing "
                          "Slash M:10\n4. Attack Descriptions\n5. Back to selection"
                          "\n--------------\n")
        if skillInput=="1":
          print("--------------")
          print("You attack the enemy!")
          playerAttack=random.randint(weaponDamage-1,weaponDamage+1)
          if random.randint(1,100)<=weaponAccuracy:
            print("You hit the enemy!")
            print("Damage dealt:",playerAttack)
            enemyHealth=enemyHealth-playerAttack
            damageThisBattle+=playerAttack
          else:
            print("You missed!")
            playerAttack=0
          if enemyHealth<0:
            enemyHealth=0
        elif skillInput=="2" and playerMana >=5:
          if playerMana<5:
            backToSelection=True
            print("Not enough mana")
          playerMana-=5
          print("--------------")
          print("You use a strong attack!")
          playerAttack=random.randint(weaponDamage-1,weaponDamage+1)
          if random.randint(1,100)<=weaponAccuracy:
            playerAttack=math.ceil(playerAttack*1.30)
            print("You hit the enemy!")
            print("Damage dealt:",playerAttack)
            enemyHealth=enemyHealth-playerAttack
            damageThisBattle+=playerAttack
          else:
            print("You missed!")
            playerAttack=0
          if enemyHealth<0:
            enemyHealth=0
        elif skillInput=="3" and playerMana>=10:
          if playerMana<10:
            backToSelection=True
            print("Not enough mana")
          playerMana-=10
          enemyOnFire=True
          turnsOnFire=3
          print("--------------")
          print("You use blazing slash!")
          print("Enemy now burning!")
          playerAttack=random.randint(weaponDamage-1,weaponDamage+1)
          playerAttack=math.ceil(playerAttack*1.25)
          print("Damage dealt:",playerAttack)
          enemyHealth=enemyHealth-playerAttack
          damageThisBattle+=playerAttack
        if enemyHealth<0:
          enemyHealth=0
        elif skillInput=="4":
          print("Basic and Strong attacks have a chance to miss"
                " based on weapon accuracy.\n--------------")
          print("Basic attack: Deals damage based on your weapon damage +/- 1. Mana:0"
                "\n--------------")
          print("Strong attack: Deals 30% more damage,"
                " rounded up, than a regular attack. Mana:5\n--------------")
          print("Blazing Slash: Guaranteed hit, Deals 25% more damage,"
                " rounded up, than a regular attack, and "
                "lights the enemy on fire, dealing 2 damage per turn"
                " for 3 turns. Mana:10"
                "\n--------------")
        elif skillInput=="5":
          backToSelection=True
      
          
  # Function 2: Guard
    elif actionInput.upper()=="GUARD" or actionInput.upper()=="2":
      print("You guard!")
      print("You regenerated 4 mana!")
      playerMana+=4
      if playerMana>playerMaxMana:
        playerMana=playerMaxMana
      defended=True
      
  # Function 3: Item
    elif actionInput.upper()=="ITEM" or actionInput.upper()=="3":
      itemUse=""
      if healthPotions<=0 and manaPotions<=0:
        print("You have no items remaining!")
        continue
      while itemUse not in validItemCommands:
        itemUse=input("What item do you want to use?\n1. Health Potion (%d)"
                      "\n2. Mana Potion (%d)\n"
                      "3. Back to selection"
                      "\n--------------\n" % (healthPotions,manaPotions))
      if itemUse.upper() in validItemCommands:
        if itemUse.upper()=="1" or itemUse.upper()=="HEALTH":
          if healthPotions<=0:
            print("None left!")
            validItemCommands.remove("1")
            validItemCommands.remove("HEALTH")
            continue
          print("You use a health potion!")
          itemsUsed+=1
          playerHealth+=10
          if playerHealth>playerMaxHealth:
            playerHealth=playerMaxHealth
          healthPotions-=1
        elif itemUse.upper()=="2" or itemUse.upper()=="MANA":
          if manaPotions<=0:
            print("None left!")
            validItemCommands.remove("2")
            validItemCommands.remove("MANA")
            continue
          print("You use a mana potion!")
          itemsUsed+=1
          playerMana+=10
          if playerMana>playerMaxMana:
            playerMana=playerMaxMana
          manaPotions-=1
        elif itemUse.upper()=="3":
          backToSelection=True
  
  
    
  # Function 4: Run
    elif actionInput.upper()=="RUN" or actionInput.upper()=="4":
      print("Coward!")
      break
      
    # Final Statements
    if backToSelection==True:
      continue
    turnCount+=1
    if enemyOnFire==True:
      enemyHealth-=2
      damageThisBattle+=2
      print("The enemy takes 2 fire damage!")
      turnsOnFire-=1
      if turnsOnFire<=0:
        print("Enemy stopped burning")
        enemyOnFire=False
    if enemyHealth<=0:
      inBattle=False
      area=""
      areaSelect=""
      weaponSelect=""
      playInput=""
      print("Enemy defeated!")
      enemyDefeated=True
      battleExperience=enemy_types[enemy_type]["Experience_reward"]
      currentPlayerLevel()
      enemyTypeKilled()
      print("Turns:", turnCount)
      while saveGameInput not in validSaveInputs:
        saveGameInput=input("Do you want to save your game? (y/n)\n")
        if saveGameInput.upper()=="Y":
          savegame()
          loading_screen(3,"Saving and returning to main menu")
          break
        else:
          saveGameInput=""
          while saveGameInput not in validSaveInputs:
            saveGameInput=input("Do you want to quit without saving? (y/n)\n")
            if saveGameInput.upper()=="Y":
              loading_screen(3,"Returning to main menu")
              break
            else:
              savegame()
              loading_screen(3,"Saving and returning to main menu")
              break
    else:
      playerHealth=enemyAttack(playerHealth,defended,enemy_type)
    if playerHealth<=0:
      loading_screen(3,"You died! Returning to main menu")
      restart_program()
      
  else:
    # Actual Main Menu
    while playInput not in validPlayInputs:
      printMainMenu()
      playInput=input("\n1. Play\n2. Check Save\n3. Wipe save"
                      "\n4. Quit\n")
      if playInput=="1":
        loadgame()
        if newGame==False:
          loading_screen(3,"Loading Save")
          loading_screen(3,"Starting game")
          print("")
          print("")
        else:
          loading_screen(3,"Starting New Game")
  
      elif playInput=="2":
        checkSave()
      elif playInput=="3":
        wipeSave()
      elif playInput=="4":
        loading_screen(3,"Quitting")
        sys.exit()
      
  
    # Weapon selection prior to fight  
    while weaponSelect not in validWeaponCommands or weaponSelect=="4":
      weaponSelect=input("--------------\nWhat weapon do you want to use?"
                         " \n1. Sword\n2. Greatsword\n3. Shortsword"
                         "\n4. Weapon stats\n--------------\n")
      if weaponSelect.upper()=="1":
        weaponDamage=6
        weaponAccuracy=80
      elif weaponSelect.upper()=="2":
        weaponDamage=10
        weaponAccuracy=50
      elif weaponSelect.upper()=="3":
        weaponDamage=5
        weaponAccuracy=100
      elif weaponSelect.upper()=="4":
        print("Sword\n--------\nDamage: 6\nAccuracy: 80\n"
              "A low risk and medium reward weapon with good accuracy and damage\n"
              "Tip: A versatile weapon for any playstyle"
              "\n\nGreatsword\n--------\nDamage: 10\nAccuracy: 50"
              "\nA high risk high reward weapon with low accuracy and high damage\n"
              "Tip: Good for powerful guaranteed hit skills"
              "\n\nShortsword\n--------\nDamage: 5\nAccuracy: 100\n"
              "A zero risk and low reward weapon with perfect accuracy and low damage\n"
              "Tip: Good for repeated strong attacks\n")
  
    while areaSelect not in validAreas:
      areaSelect=input("--------------\nWhere do you want to go?\n"
                       "1. Forest\n2. Desert\n").upper()
      if areaSelect == "1" or areaSelect == "FOREST":
        loading_screen(3,"Entering Forest")
        area="Forest"
        inBattle=True
        initializeEnemy(area)
        playerStats()
        damageThisBattle=0
      elif areaSelect=="DESERT" or areaSelect=="2":
        loading_screen(3,"Entering Desert")
        area="Desert"
        inBattle=True
        initializeEnemy(area)
        playerStats()
        damageThisBattle=0
      else:
        area="Desert"

That’s your problem (also in your updated code - though I didn’t look at that carefully). First of all, starting a subprocess call of python inside a python script - that’s a big red flag. I cannot think of any scenario – in normal straight-forward programs like this – where this would ever be necessary. (Btw if you do call subprocess you should also at least check its exit code instead of ignoring that.) Second, calling os._exit is a yellow flag. If you want to exit, you should normally use sys.exit, since os._exit is a hard exit that doesn’t do any resource cleanup (even if really needed).
Then, the way the program exits is wrong when used in combination with restart:

elif playInput=="4":
        loading_screen(3,"Quitting")
        os._exit(0)

This will just exit from the subprocess, so the main python will keep running! You can debug and verify this by a skeleton version of your program:

import subprocess
import os
import sys

def restart():
    python = sys.executable
    subprocess.call([python] + sys.argv)

print("Running", os.getpid())

while True:
    cmd = input(">>> ")
    if cmd == "q":
        print("Exiting", os.getpid())
        sys.exit(0)
    else:
        restart()

Now, I can see why you added that restart function. The reason is that you have all those globals which need to be reset. Using such a ton of globals is also a pretty big red flag in the code.

So to fix I suggest:

  • get completely rid of restart
  • either: get rid of all globals, replace them by objects and object attributes(instances of custom classes like Player, Weapon, and attributes of given objects) that track the game state (objects can easily be re-created or reset when you want to restart); or keep the globals, but add a proper initiailization/reset function to reset them to defaults when a restart is needed; instead of restarting with a new process, you would then just reset the state – staying in the same process

Using an OO (object oriented)-approach (with custom classes) will make your script way more readable and maintainable and will also make it a lot easier to change it into a real multi-player kind of game for instance.

2 Likes

Here are a few assignments that you might try to improve the code. I think you’ll be very happy with the results; it should make the code much easier to follow and understand - and the user interface will look more consistent, too.

First, try using more functions to split up the work in the main loop. For example, try making a function for the entire while inBattle==True: loop, and figure out what information needs to be passed and returned to make that work. Try to use functions/arguments and the return value, instead of global variables.


Try making a different function to print whatever text for the menu header. Have it take two parameters: the middle text (here, that would be "Epic Battle Simulator"), and a box width (40 in this case). Hint: with the .format method to format strings, you can tell it to “center-justify” text within a field, and set a field width dynamically:

>>> '{:10}'.format('text') # the 10 is a width
'text      '
>>> '{:^10}'.format('text') # the ^ means to center
'   text   '
>>> '{:^{}}'.format('text', 10) # nest the {} to give the width separately
'   text   '

(Bonus: instead of just taking a string to center, have it take a list of strings, that will show up one per line. Keep in mind that the calling code will have to know how to handle that. You could also try passing the lines as separate arguments, using *args.)


Try making a function that creates a “generic” bar, by telling it a “label” as well as the current and maximum values. (You’re almost there with enemyStatusBar already). Try making it return the bar text, instead of printing it, and let the calling code do the printing instead. Use this three times to create the bars for playerStatusBars instead of repeating the logic, and have it return either a list of the bars to print, or a single string (joined up with newlines). Let the calling code do the printing for that, too.


Try making a generic function for a menu, like so (I’ve used comments and a docstring to explain what it needs to do):

def do_menu(header, options):
    """Repeatedly show the menu and ask for an input
    until something valid is chosen. Returns an integer for the choice.
    header -> a string to show before the options
    options -> a list of strings for menu options, without number labels
    """
    # Have an outer loop that will repeat until the input is valid
    # Your choice: show the header only once, or at the top of the loop
    # Have an inner loop that shows each option, one per line, and numbers them
    # Prompt for input and test it:
    # if it can be converted to an integer in range, return that
    # if it matches one of the options, return the corresponding integer
    # (hint: try using another inner loop to check the option text)
    # otherwise, keep trying

Then see about using that to replace the other parts of the code that handle menus.

(Bonus: if you made the box-printing function from above, try using it to put the header in a box always.)

1 Like

run read_var.py :roll_eyes:

set_var.py

a = 0

def set_var():
    global a
    for x in range(10):
        a += x
        print(a)

set_var()

read_var.py

import asyncio
import sys
import os

async def show_var():
    proc = await asyncio.create_subprocess_exec(
        sys.executable, 'set_var.py',
        stdout=asyncio.subprocess.PIPE)
    print(f' set_var pid {proc.pid}\n')

    print(f'show_var pid {os.getpid()}')
    data = '#'
    while data:
        data = await proc.stdout.readline()
        if data:
            print(data.decode("ascii").rstrip())

    # Wait for the subprocess exit.
    await proc.wait()
    print(f"\n retcode of pid {proc.pid} is {proc.returncode}")

async def main():
    async with asyncio.TaskGroup() as tg:
        tg.create_task(show_var())

asyncio.run(main())