Last active
August 29, 2025 09:57
-
-
Save horstjens/eb7e1685ef314ed6848bbb6e3413a8cd to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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