Learning Goals
3 minBy the end of this lesson you can:
- Use
random.randint(low, high)to roll a random whole-number damage value, inclusive of both ends. - Write a fight loop with
while hero_hp > 0 and dragon_hp > 0— and explain why both conditions appear. - Let the inventory shape the fight: equipping a sword from Part 2 should add to the player's attack roll.
Warm-Up
5 minYou already know random.choice(list) from PY-L1-19. Today we need the sibling — random.randint(a, b) — that returns a whole number between a and b inclusive:
import random print(random.randint(1, 6)) # like rolling a die — 1, 2, 3, 4, 5, or 6 print(random.randint(5, 10)) # 5, 6, 7, 8, 9 or 10 print(random.randint(0, 1)) # 0 or 1
Quick prediction game. What range of values can random.randint(3, 8) produce?
Show the answer
3, 4, 5, 6, 7, or 8 — six possible values
Both ends are inclusive. That's different from range(3, 8), which stops before 8. randint can land on 8.
random.choice(list) picks one item from a list. random.randint(a, b) picks one number from a range. Together they cover almost every "pick something random" need in a game.
New Concept · The Fight Loop
14 minTwo HPs, one loop
A combat round goes: hero acts (attack or heal), if dragon still standing it attacks back. Repeat until one of them is dead.
hero_hp = 30 dragon_hp = 40 while hero_hp > 0 and dragon_hp > 0: # 1. ask hero what they want to do # 2. apply hero's action # 3. if dragon still alive, dragon attacks back
The and in the condition is essential. As soon as either HP hits 0, the loop ends. That covers both endings — "you win" and "you die".
Hero's action — attack or heal
action = input("ATTACK or HEAL? ").strip().lower() if action == "attack": damage = random.randint(4, 9) dragon_hp = dragon_hp - damage print("You strike for", damage, "damage. Dragon HP:", dragon_hp) elif action == "heal": heal = random.randint(3, 7) hero_hp = hero_hp + heal print("You patch yourself up for", heal, ". Hero HP:", hero_hp) else: print("You hesitate. The dragon notices.")
Three branches. Attack rolls damage on the dragon. Heal rolls HP back to the hero. Anything else wastes a turn — the dragon still gets to strike.
Dragon's counter-attack
After the hero's turn, check if the dragon is still up. If so, it hits back:
if dragon_hp > 0: bite = random.randint(3, 6) hero_hp = hero_hp - bite print("Dragon bites for", bite, ". Hero HP:", hero_hp)
That single if is what makes the fight feel fair. If your attack killed the dragon, you don't take a final hit on the way out — the dragon's already dead.
Decide who won after the loop
if hero_hp > 0: print("🏆 You defeated the dragon!") else: print("💀 The dragon ate you. The end.")
The loop ended because one of them dropped. Whichever one's HP is still above 0 is the winner.
Inventory shapes the fight
This is the cool moment — if the player picked up a sword in an earlier scene, attacks should hurt more. Just check the bag:
damage = random.randint(4, 9) if "sword" in inventory: damage = damage + 5 # bonus from the sword print("(Your sword bites deep!)")
Part 2's inventory finally pays off. Players who explored more of the world before this scene get a meaningful combat advantage.
A turn-based fight is one while loop with a combined-condition. Inside: one branch per action, then a follow-up that lets the opponent react only if still alive.
Worked Example · The Dragon in the Cave
14 minAdd the boss to the adventure
We'll change the cave scene from Part 2: instead of immediately winning, the cave now leads into a dragon fight. The fight is a separate function. Save as the latest adventure.py:
Code
# adventure.py — Part 3: dragon fight import random def fight_dragon(inventory): print() print("== DRAGON LAIR ==") print("A red dragon coils on its hoard. It opens one ember-yellow eye.") hero_hp = 30 dragon_hp = 40 while hero_hp > 0 and dragon_hp > 0: print() print("Hero HP:", hero_hp, " | Dragon HP:", dragon_hp) action = input("ATTACK or HEAL? ").strip().lower() # hero's turn if action == "attack": damage = random.randint(4, 9) if "sword" in inventory: damage = damage + 5 print("(Your sword bites deep!)") dragon_hp = dragon_hp - damage print("You strike for", damage, "damage.") elif action == "heal": heal = random.randint(3, 7) hero_hp = hero_hp + heal print("You patch yourself for", heal, "HP.") else: print("You hesitate, distracted by the heat.") # dragon's turn — only if it's still alive if dragon_hp > 0: bite = random.randint(3, 6) hero_hp = hero_hp - bite print("The dragon bites for", bite, "damage.") print() if hero_hp > 0: print("🏆 The dragon collapses. You loot the hoard!") inventory.append("dragon_hoard") return "end" else: print("💀 The dragon's flames consume you. THE END.") return "end" # (the other scenes from Part 2 stay the same; the cave now leads to the fight) def scene_cave(inventory): print() print("== CAVE INTERIOR ==") if "torch" not in inventory: print("Too dark. You stumble back.") return "forest" print("You walk deeper, torch held high. A great red shape stirs ahead...") return "fight" # main program (snippet — full dispatcher includes village/forest from Part 2) inventory = [] location = "village" while True: if location == "village": location = scene_village(inventory) elif location == "forest": location = scene_forest(inventory) elif location == "cave": location = scene_cave(inventory) elif location == "fight": location = fight_dragon(inventory) elif location == "end": print() print("Final bag:", inventory) break elif location == "quit": print("Goodbye.") break
(Re-use your scene_village and scene_forest from Part 2. The cave function changed, and a new fight function plus dispatcher branch are added.)
Sample run · a swordless victory (lucky)
== DRAGON LAIR == A red dragon coils on its hoard. It opens one ember-yellow eye. Hero HP: 30 | Dragon HP: 40 ATTACK or HEAL? attack You strike for 7 damage. The dragon bites for 4 damage. Hero HP: 26 | Dragon HP: 33 ATTACK or HEAL? attack You strike for 9 damage. The dragon bites for 6 damage. ... a few more turns ... Hero HP: 8 | Dragon HP: 4 ATTACK or HEAL? attack You strike for 6 damage. 🏆 The dragon collapses. You loot the hoard!
Sample run · sword bonus
Hero HP: 30 | Dragon HP: 40 ATTACK or HEAL? attack (Your sword bites deep!) You strike for 13 damage.
Things to notice
- The hero acts first, then the dragon — only if alive. That order makes the fight winnable. If you let the dragon act before the "is hero dead?" check, you can get a phantom hit after you should've already died.
- HP can go above the starting max. The lesson code doesn't cap healing. If you want a cap ("max 30 HP"), use
hero_hp = min(hero_hp + heal, 30). Try it as a stretch. - The fight function returns
"end"either way. The dispatcher doesn't need to know whether you won or lost — that decision was made and announced insidefight_dragon.
A real turn-based RPG fight, with stats, a combat verb system, randomness, and equipment that matters. Less than 40 lines of code, and a complete loop you'll see at the heart of every roguelike. You have a Level 1 adventure with combat. Brag.
Try It Yourself
14 minThree tasks. Build a tiny fight, then connect it back to the world.
Open fight_test.py. Hard-code hero_hp = 20 and dragon_hp = 25. Loop while both are above 0. On each turn, attack the dragon for random.randint(3, 7) damage; the dragon hits back for random.randint(2, 5). Print HP after every turn. After the loop, announce who won.
Hint
import random hero_hp = 20 dragon_hp = 25 while hero_hp > 0 and dragon_hp > 0: damage = random.randint(3, 7) dragon_hp = dragon_hp - damage print("You hit for", damage, "→ dragon HP:", dragon_hp) if dragon_hp > 0: bite = random.randint(2, 5) hero_hp = hero_hp - bite print("Dragon bites for", bite, "→ hero HP:", hero_hp) if hero_hp > 0: print("Hero wins!") else: print("Dragon wins!")
Auto-attack version — no input, just two stat blocks bashing each other. Run it three times. Different outcomes each run? Good — that's random doing its job.
Make the hero choose. input for attack or heal. Heal restores random.randint(3, 6) HP. The dragon still bites every turn. Add a print for the hero's chosen action.
Hint
while hero_hp > 0 and dragon_hp > 0: action = input("ATTACK or HEAL? ").strip().lower() if action == "attack": d = random.randint(3, 7) dragon_hp = dragon_hp - d print("Hit for", d) elif action == "heal": h = random.randint(3, 6) hero_hp = hero_hp + h print("Healed for", h) else: print("Wasted turn.") if dragon_hp > 0: b = random.randint(2, 5) hero_hp = hero_hp - b print("Bitten for", b)
Try the "heal every turn" strategy. Notice you can't actually win that way — the dragon never goes down. Combat needs offence eventually.
Open your adventure.py. Add the fight_dragon(inventory) function from the worked example. Change scene_cave so that, with torch in hand, it returns "fight". Add an elif location == "fight": branch in the dispatcher. Play the full game start-to-finish.
Hint
# in scene_cave, after torch check: print("You step deeper. A dragon stirs ahead!") return "fight" # in main loop: elif location == "fight": location = fight_dragon(inventory)
Three changes — new function, return value tweaked in one scene, one new dispatcher branch. That's how an entire combat system gets bolted into a game without rewriting it.
Mini-Challenge · Yuki's Immortal Dragon
8 minYuki's dragon never dies, even when its HP goes negative. Find the two bugs.
# yuki_fight.py — buggy
hero_hp = 30
dragon_hp = 40
while hero_hp > 0 or dragon_hp > 0: # bug
damage = random.randint(4, 9)
dragon_hp = dragon_hp - damage
print("Hit. Dragon:", dragon_hp)
bite = random.randint(3, 6) # bug: no "still alive?" check
hero_hp = hero_hp - bite
print("Bitten. Hero:", hero_hp)
print("Done.")- Bug 1. The loop condition uses
orinstead ofand. Withor, the loop continues as long as either HP is positive — so the hero (or dragon) keeps getting beaten well past death. - Bug 2. The dragon always attacks, even if its HP already dropped to 0 or below. Wrap the bite in
if dragon_hp > 0:.
Show the fix
while hero_hp > 0 and dragon_hp > 0: # AND, not OR damage = random.randint(4, 9) dragon_hp = dragon_hp - damage print("Hit. Dragon:", dragon_hp) if dragon_hp > 0: # only if still alive bite = random.randint(3, 6) hero_hp = hero_hp - bite print("Bitten. Hero:", hero_hp)
The and/or mix-up is one of the easiest English-to-code mistakes. "keep fighting while both are alive" in English is and in code — both conditions have to be true together. or would mean "keep fighting while at least one is alive", which is a fight that never properly ends.
Recap
3 minThe adventure is complete. A boss fight is one while loop with a combined condition: keep going while both fighters have HP. Each iteration the hero acts (attack or heal), the action's effect is rolled with random.randint, and if the dragon survives it counter-attacks. The combat hooks into the inventory — a sword in the bag adds damage, so all the exploration from Part 2 actually mattered. After the loop one quick if picks the right ending. Tonight you can wire all three parts together into one continuous game. Next lesson kicks off the three-part Code Wars: bug hunts, output prediction, and a speed round.
Vocabulary Card
random.randint(a, b)- Random whole number from
atob, both ends inclusive. - combined loop condition
while X and Y— keep going only while both are true. The fight loop ends the moment either side hits 0.- turn order
- The fixed sequence of actions in a round: hero acts, then dragon acts (if alive). Reversing it changes the game.
- stat block
- The small set of numbers describing a character — HP, damage range, etc. Lives in the function or main program.
- equipment bonus
- Damage or HP changes based on what's in the inventory. The way exploration translates into combat power.
Homework
4 minFinish the full adventure tonight.
- Combine your Part 1 + Part 2 + Part 3 work into one
adventure.pythat plays from start to finish — village, forest (with torch), market (with key and sword), cave fight, gate. - Add a
scene_marketthat lets the player pick up a sword. Wire the fight to use it (the worked example already does). - Cap the hero's heal at a max of 30 HP — use
min(hero_hp + heal, 30). - Add a small visible "health bar" before each round — e.g.
Hero: [#####.....] 25/30. Hint: the bar string is"#" * hero_hp + "." * (30 - hero_hp).
Bring the whole game next class. PY-L1-42 starts the Code Wars trilogy.
Sample · fight_dragon with health bar & cap
def health_bar(hp, max_hp): full = max(hp, 0) empty = max_hp - full return "[" + "#" * full + "." * empty + "] " + str(hp) + "/" + str(max_hp) def fight_dragon(inventory): print("\n== DRAGON LAIR ==") hero_hp = 30 dragon_hp = 40 while hero_hp > 0 and dragon_hp > 0: print() print("Hero ", health_bar(hero_hp, 30)) print("Dragon", health_bar(dragon_hp, 40)) action = input("ATTACK or HEAL? ").strip().lower() if action == "attack": d = random.randint(4, 9) if "sword" in inventory: d = d + 5 print("(Sword bonus!)") dragon_hp = dragon_hp - d print("You hit for", d) elif action == "heal": h = random.randint(3, 7) hero_hp = min(hero_hp + h, 30) print("Healed for", h) else: print("You hesitate.") if dragon_hp > 0: b = random.randint(3, 6) hero_hp = hero_hp - b print("Dragon bites for", b) print() if hero_hp > 0: print("🏆 Dragon defeated!") inventory.append("dragon_hoard") else: print("💀 The dragon wins.") return "end"
Three nice touches in the sample. health_bar is its own helper function — keeps the fight loop readable. max(hp, 0) guards against negative HP in the bar so you never get a bar with negative #s. And min(hero_hp + heal, 30) is the single-line cap — never go above the max, never below 0 with the bar guard.