Skip to content

Instantly share code, notes, and snippets.

@horstjens
Last active August 29, 2025 09:57
Show Gist options
  • Select an option

  • Save horstjens/eb7e1685ef314ed6848bbb6e3413a8cd to your computer and use it in GitHub Desktop.

Select an option

Save horstjens/eb7e1685ef314ed6848bbb6e3413a8cd to your computer and use it in GitHub Desktop.
import random
class Monster:
"""
a generic monste class for everyone
in the dugeon, inlclding the player!
"""
zoo = {} # number:<class instance>
number = 0
def __init__(self,x,y,z):
self.x = x
self.y = y
self.z = z
self.hitpoints = 0
self.friendly = False # player and allies are friendly
self.message = ""
self.char = "M"
self.sleep_chance = 0
self.blocked_by_wall = True
self.blocked_by_door = True
self.fire_resistance = 0
self.pushed_away_by_foam = False
# --- combat base values ---
self.attack = 5
self.defense = 5
self.damage = 1
self.armor = 0
self.attacks = ["bites"]
# --------------------------
self.number = Monster.number
Monster.number += 1
Monster.zoo[self.number] = self
def stats(self):
return f"hp: {self.hitpoints} att: {self.attack} def: {self.defense} dmg: {self.damage} arm: {self.armor}"
def move(self, dx, dy):
# wall, door ?
new_tile = dungeon[hero.z][self.y+dy][self.x+dx]
if new_tile in "|-": # door
if self.blocked_by_door:
self.message += ", is blocked by door"
dx, dy = 0, 0
else:
self.message += ", goes through a door!"
if new_tile == "#": # wall
if self.blocked_by_wall:
self.message += ", crashes into wall"
dx, dy = 0, 0
else:
self.message += ", floats though a wall!"
# other monster ?
for m in Zombie.zoo.values():
if m.number == self.number: # yourself
continue
if m.z != hero.z: # wrong floor
continue
if m.hitpoints <= 0: # dead monster
continue
if (self.y + dy == m.y) and (self.x + dx == m.x):
dx, dy = 0, 0
if self.friendly == m.friendly:
self.message += " ,bumps into a friend"
else:
self.message += f" ,attacks {m.__class__.__name__}"
fight(self, m)
break
# --- move --
self.x += dx
self.y += dy
def distance_to_player(self):
return ((hero.x-self.x)**2+(hero.y-self.y)**2)**0.5
def action(self):
self.message = ""
if random.random() < self.sleep_chance:
self.sleep = True
self.message += "sleeps."
return
self.sleep = False
dx, dy = 0,0
# sniff player ?
##if hero.z == self.z:
distance = self.distance_to_player()
if distance <= self.sniffrange:
self.status = "hunting"
# jage den spieler
if hero.x < self.x:
dx = -1
if hero.x > self.x:
dx = 1
if hero.y < self.y:
dy = -1
if hero.y > self.y:
dy = 1
else:
self.status = "wandering around"
dx = random.choice((-1,0,1))
dy = random.choice((-1,0,1))
# --- schräge bewegung verbieten ----
if (dx != 0) and (dy != 0):
if random.randint(1,2) == 1:
dx = 0
else:
dy = 0
self.message += self.status
# --- bewegung ----
self.move(dx,dy)
#self.x += dx
#self.y += dy
class Player(Monster):
def __init__(self,x,y,z):
super().__init__(x,y,z)
self.char = "@"
self.hitpoints = 40
self.friendly = True
self.keys = 0
self.matches = 0
self.has_fire_extinguisher = False
self.foam_range = 4 #
# --- overwrite combat base values ---
self.attack = 10
self.defense = 6
self.damage = 4
self.armor = 1
self.attacks = ["punch","kick"]
# --------------------------
def action(self):
pass
def status(self):
return f"z:{hero.z} hp:{hero.hitpoints} §:{hero.keys} i:{hero.matches} fire_res:{hero.fire_resistance}"
def spray_foam(self, direction_key):
"""spray foam into a cardinal direction"""
# spits fire in a thick line towards player
delta = {"W":(0,-1),
"S":(0,1),
"A":(-1,0),
"D":(1,0),
}
dx, dy = delta[direction_key]
# delete fires and pushes monsters away (if possible)
how_many = 0
monstertext = []
push = True
for distance in range(self.foam_range + 1):
# clean fires
try:
tile = dungeon[self.z][self.y+dy * distance][self.x+dx * distance]
except IndexError:
continue
if tile in ("f", "."):
dungeon[self.z][self.y+dy * distance][self.x+dx * distance] = "o" # foam
if tile == "f":
how_many += 1
# push monsters away
if push:
for m in Monster.zoo.values():
if any((m.z != hero.z,
m.number == hero.number,
not m.pushed_away_by_foam,
m.hitpoints <= 0,
m.y != self.y + dy * distance,
m.x != self.x + dx * distance,
)):
continue
# found a monster!
mname = f"{m.__class__.__name__}({m.number})"
#check if there is something blocking behind monster
new_tile = dungeon[self.z][self.y+dy * (distance+1)][self.x+dx * (distance+1)]
if new_tile in "-|":
monstertext.append(f"{mname} is pushed against a door and takes damage")
m.hitpoints -= 1
push = False
elif new_tile == "#":
monstertext.append(f"{mname} is pushed against a wall and takes damage")
m.hitpoints -= 1
push = False
# is there another monster ?
for m2 in Monster.zoo.values():
if any((m2.z != hero.z,
m2.number == hero.number,
m2.number == m.number,
not m2.pushed_away_by_foam,
m2.hitpoints <= 0,
m2.y != self.y + dy * (distance+1),
m2.x != self.x + dx * (distance+1),
)):
continue
# found blocking monster m2
mname2 = f"{m2.__class__.__name__}({m2.number})"
monstertext.append(f"{maname} is pushed into {mname2}, both take damage")
m.hitpoints -= 1
m2.hitpoints -= 1
push = False
# --- pushing still possible ?
if push:
m.x += dx
m.y += dy
messages.append(f"You spray foam and extinguish {how_many} fires")
for m in monstertext:
messages.append(m)
def move(self,dx,dy):
# can open wall with key
new_tile = dungeon[self.z][self.y+dy][self.x+dx]
if new_tile in "-|": # door
if self.keys > 0:
self.keys -= 1
messages.append("player uses key to open a door")
dungeon[self.z][self.y+dy][self.x+dx] = "." # replace door
dx, dy = 0, 0
super().move(dx,dy)
class Zombie(Monster):
def __init__(self,x,y,z):
super().__init__(x,y,z)
self.char = "Z"
self.sleep = False
self.sleep_chance = 0.33
self.sniffrange = 4
self.hitpoints = 20
# --- combat base values ---
self.attack = 5
self.defense = 1
self.damage = 1
self.armor = 0
self.attacks = ["flail","bite"]
# --------------------------
self.pushed_away_by_foam = True
class Dragon(Monster):
def __init__(self,x,y,z):
super().__init__(x,y,z)
self.char = "D"
self.sleep = False
self.sleep_chance = 0.1
self.firebreath_chance = 0.6
self.ignite_chance = 0.8 # chance of a tile to catch fire when dragon spits fire
self.sniffrange = 1
self.spitrange = 7
self.hitpoints = 100
self.fire_resistance = 5
# --- combat base values ---
self.attack = 7
self.defense = 2
self.damage = 6
self.armor = 3
self.attacks = ["bite","tail-sweep", "pounce"]
# --------------------------
def spit_fire(self):
"""spit fire in the general direction of the player"""
# spits fire in a thick line towards player
for x,y in list(bresenham(self.x, self.y, hero.x, hero.y))[1:]: # start one tile after the dragon
for dx in (-1,0,1):
for dy in (-1,0,1):
if random.random() < self.ignite_chance:
# create a flame here
tile = dungeon[hero.z][y+dy][x+dx]
if tile == ".":
dungeon[hero.z][y+dy][x+dx] = "f" # create a flame here
def action(self):
if self.distance_to_player() < self.spitrange:
if random.random() < self.firebreath_chance:
self.message = "spitting fire!"
self.spit_fire()
return
super().action()
def bresenham(x0, y0, x1, y1):
"""Yield integer coordinates on the line from (x0, y0) to (x1, y1).
Input coordinates should be integers.
The result will contain both the start and the end point.
"""
dx = x1 - x0
dy = y1 - y0
xsign = 1 if dx > 0 else -1
ysign = 1 if dy > 0 else -1
dx = abs(dx)
dy = abs(dy)
if dx > dy:
xx, xy, yx, yy = xsign, 0, 0, ysign
else:
dx, dy = dy, dx
xx, xy, yx, yy = 0, ysign, xsign, 0
D = 2*dy - dx
y = 0
for x in range(dx + 1):
yield x0 + x*xx + y*yx, y0 + x*xy + y*yy
if D >= 0:
y += 1
D -= 2*dx
D += 2*dy
def bildschirm_löschen():
for _ in range(0):
print()
def remove_foam():
for y, line in enumerate(dungeon[hero.z]):
for x, char in enumerate(line):
if dungeon[hero.z][y][x] == "o":
dungeon[hero.z][y][x] = "."
def fight(attacker, defender):
# strike and counterstrike
messages.append("==== Strike ====")
messages.append(f"{attacker.__class__.__name__} ({attacker.stats()}) vs. {defender.__class__.__name__} ({defender.stats()})")
#messages.append(f"{attacker.__class__.__name__}: {attacker.stats()}")
#messages.append(f"{defender.__class__.__name__}: {defender.stats()}")
strike(attacker, defender)
if defender.hitpoints > 0:
messages.append("==== Counter-Strike ====")
strike(defender, attacker)
def strike(a, d):
roll_a = random.randint(1,6)
roll_d = random.randint(1,6)
roll_damage = random.randint(1,6)
attack_mode = random.choice(a.attacks)
adam = a.__class__.__name__
bob = d.__class__.__name__
verb1 = attack_mode
verb2 = verb[attack_mode]
# miss
if d.defense + roll_d >= a.attack + roll_a:
text = random.choice((
f"{adam} {verb2} {bob} but misses! ",
f"{adam} fails to {verb1} {bob}",
f"{adam} tries to {verb1} {bob} but is not unsuccessfull",
f"{adam} {verb2} {bob} but {bob} evades the attack! ",
f"{adam}'s {verb1} attack is blocked by {bob}",
))
text += f" ({a.attack}+{roll_a} not > {d.defense}+{roll_d})"
messages.append(text)
return
# hit
text = random.choice((
f"{adam} {verb2} {bob}",
f"{adam} overcomes {bob}'s defense with a {verb1} attack",
f"{adam} {verb2} {bob}, {bob} fails to block the attack",
f"{adam} {verb2} {bob}, right in the face",
f"{adam}'s {verb1} attack is so precise that {bob} can not evade it",
))
text += f" ({a.attack}+{roll_a} > {d.defense}+{roll_d})"
messages.append(text)
damage = a.damage + roll_damage - d.armor
if damage <= 0:
# no damage because of armor
text = f"{bob}'s armor protects from damage ({d.armor} >= {a.damage}+{roll_damage})"
messages.append(text)
else:
text = f"{adam}'s attack penetrates {bob}'s armor, {bob} looses {damage} hp. ({a.damage}+{roll_damage} > {d.armor})"
messages.append(text)
d.hitpoints -= damage
messages.append(f"{d.__class__.__name__} has {d.hitpoints} hp left")
if d.hitpoints <= 0:
messages.append(f"{d.__class__.__name__} dies!")
level1 = """
######################################
#.*iii...............................#
#....ß...............................#
#....................................#
#....................................#
#..............D................>....#
#....................................#
#....................................#
#f.§.....#...§.....|.................#
#.f...f...######-####................#
#.....Z..............................#
#..f.............ZZ..................#
######################################
"""
level2 = """
######################################
#....................................#
#....................................#
#............Z..........############-#
#.......................#............#
#.............Z.........#.......<....#
######################################
"""
hero = Player(1,1,0) # x,y,z
dungeon = []
verb = {"punch":"punches",
"kick" :"kicks",
"bite" :"bites",
"flail":"flails at",
"pounce":"pounces on",
"tail-sweep":"tail-slaps",
}
# dungeon einlesen....
for z, level in enumerate((level1,level2)):
zeilen = [ list(line) for line in level.splitlines()
if len(line) > 0]
# --- zombies erzeugen ---
for y, line in enumerate(zeilen):
for x, char in enumerate(line):
if char == "Z":
Zombie(x,y,z)
zeilen[y][x] = "."
elif char == "D":
Dragon(x,y,z)
zeilen[y][x] = "."
dungeon.append(zeilen)
messages = ["Welcome to the dungeon, hero!"]
while hero.hitpoints > 0:
# ----- graphic engine -----
for y, line in enumerate(dungeon[hero.z]):
for x, char in enumerate(line):
zeichen = char
if (x==hero.x and y == hero.y):
zeichen = hero.char
else:
for monster in Zombie.zoo.values():
if all((monster.x == x,
monster.y == y,
monster.z==hero.z,
monster.hitpoints > 0)):
zeichen = monster.char
break
print(zeichen, end="")
print()
#print(messages)
for m in messages:
print(m)
messages = []
#status = f"x: {hero.x} y: {hero.y} z: {hero.z} keys: {hero.keys}"
#remove foam:
remove_foam()
command = input(f"{hero.status()} >>>")
bildschirm_löschen()
if command == "quit":
break
messages.append("---- hero's action ----")
dx = 0
dy = 0
position = dungeon[hero.z][hero.y][hero.x]
if (command == "down") and (position == ">"):
messages.append("you climb down the stairs...")
hero.z += 1
if (command == "up") and (position == "<"):
messages.append("you climb the stairs up....")
hero.z -= 1
if command == "a":
dx = -1
if command == "d":
dx = 1
if command == "w":
dy = -1
if command == "s":
dy = 1
if command == "fire":
if hero.matches <= 0:
messages.append("you need to find matches (i) to start a fire")
else:
if position == ".":
hero.matches -= 1
messages.append("you start a fire under your feet. move away quickly!")
dungeon[hero.z][hero.y][hero.x] = "f"
else:
messages.append("You can not start a fire here. Find an empty spot (.)")
if command in ("W","A","S","D"):
if not hero.has_fire_extinguisher:
messages.append("You need first to find an fire extinguisher (ß) before you can using one")
else:
hero.spray_foam(command)
hero.move(dx,dy)
# ----- dinge aufsammeln -----
new_pos = dungeon[hero.z][hero.y][hero.x]
# ------------- key ---
if new_pos == "§": # schlüssel
hero.keys += 1
dungeon[hero.z][hero.y][hero.x] = "."
messages.append("You found a key!")
# -------------- fire resistance ---
if new_pos == "*": # improver of fire resistance
hero.fire_resistance += 1
dungeon[hero.z][hero.y][hero.x] = "."
messages.append(f"You improve your fire resistance by +1! You have now a fire resistance value of {hero.fire_resistance}")
# --------------- matches
if new_pos == "i": # box of matches
amount = random.randint(4,8)
hero.matches += amount
messages.append(f"You find a box of {amount} matches! Start a fire at your feet by typing 'fire'")
dungeon[hero.z][hero.y][hero.x] = "."
if new_pos == "ß": # fire_extinguisher
dungeon[hero.z][hero.y][hero.x] = "."
hero.has_fire_extinguisher = True
messages.append(f"You find an fire extinguisher with unlimited capacity! Use bye typing 'W','A','S','D'")
# ----- stiege -------
if new_pos == ">": # stair down
messages.append("type 'down' to climb the stairs down")
elif new_pos == "<": # stair up
messages.append("type 'up' to climb stair up")
# ----- monster action ----
messages.append("----- monster's actions ------")
for monster in Zombie.zoo.values():
if all((monster.hitpoints > 0,
monster.z == hero.z,
monster.number != hero.number,
)):
monster.action()
messages.append(f"{monster.__class__.__name__} {monster.number}: {monster.message}")
# ----- dungeon action ----
# fire damage ?
for m in Monster.zoo.values():
if m.z != hero.z:
continue
if m.hitpoints <= 0:
continue
if dungeon[hero.z][m.y][m.x] == "f": # stays on fire tile
flame_damage = random.randint(1,3)
damage = flame_damage - m.fire_resistance
if damage > 0:
messages.append(f"{m.__class__.__name__} suffers {damage} fire damage")
m.hitpoints -= damage
if m.hitpoints <= 0:
messages.append(f"{m.__class__.__name__} dies in the flames")
# fire goes out?
for y, line in enumerate(dungeon[hero.z]):
for x, char in enumerate(line):
if char == "f": # fire
if random.random() < 0.04:
dungeon[hero.z][y][x] = "." # fire goes out
print("Game Over")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment