# Beginner Python Dungeon Crawler RPG

I made a game in python which plays a bit like old terminal based RPGs, though a lot more basic. I had originally intended to implement a weapons and armour system and thus remnants of this can still be seen in the latest version of the code.

Please review this, from the documentation to the actual code.

from random import randrange
critter_names = ["JOHN CENA", "Shrek", "A Troll", "Maymay", "Ur mum", "A Pink guy", "A Roman bust", "A Rampant AI", "A NSA operative", "A Klu Klux Klan Member",
"An iPhone user","A Mac user", "Someone wearing a snapback", "Someone wearing an Unknown Pleasures T-shirt who hasn't even listened to the album",
"Larry Page","Someone who illegitimately won the NCSS", "A Lad", "An Illuminatus"]
character = {"name": "YUNG LEAN", "armour_name": "Boardies", "armour_rating": 1, "weapon_name": "Meaty fists", "weapon_rating" : 10, "hp": 100}

size = input("How big do you want the map? (recomended between 10 and 20): ")
char_xy = [0,0]
critter_list = {}
door = [randrange(0, int(size)), randrange(0, int(size))]
new_level = True
player_in_range = False
level = 0
def attack(name):
critter_list[name][2] = critter_list[name][2] - character["weapon_rating"]
def defend(name):
character["hp"] = character["hp"] - (critter_list[name][3]/character["armour_rating"])
def critter_gen():
for i in critter_names:
if randrange(1, 6) == 3:
critter_list[i] = [randrange(0, int(size)), randrange(0, int(size)), randrange(1, 100), randrange(1, 25)] #[xpos, ypos, hp, attack]
def player_input():
move = input("It's your move! ")
if move == "w" and char_xy[0] > 0:
char_xy[0] -= 1
elif move == "a" and char_xy[1] > 0:
char_xy[1] -= 1
elif move == "s" and char_xy[0] < int(size) - 1:
char_xy[0] += 1
elif move == "d" and char_xy[1] < int(size) - 1:
char_xy[1] += 1
elif move == "e":
if door[0] == char_xy[0] and door[1] == char_xy[1]:
new_level = True
elif move == "f":
for i in critter_list:
if critter_list[i][0] == char_xy[0] and critter_list[i][1] == char_xy[1]:
attack(i)
defend(i)
elif critter_list[i][0] == char_xy[0] + 1 and critter_list[i][1] == char_xy[1] + 1:
attack(i)
elif critter_list[i][0] == char_xy[0] - 1 and critter_list[i][1] == char_xy[1] + 1:
attack(i)
elif critter_list[i][0] == char_xy[0] + 1 and critter_list[i][1] == char_xy[1] - 1:
attack(i)
elif critter_list[i][0] == char_xy[0] - 1 and critter_list[i][1] == char_xy[1] - 1:
attack(i)
elif critter_list[i][0] == char_xy[0] and critter_list[i][1] == char_xy[1] + 1:
attack(i)
elif critter_list[i][0] == char_xy[0] and critter_list[i][1] == char_xy[1] - 1:
attack(i)
elif critter_list[i][0] == char_xy[0] + 1 and critter_list[i][1] == char_xy[1]:
attack(i)
elif critter_list[i][0] == char_xy[0] - 1 and critter_list[i][1] == char_xy[1]:
attack(i)
else:
print("That is not a valid move")
def map_gen():
asc = '-'
gen = [int(size)*[asc] for i in range(int(size))]
gen[char_xy[0]][char_xy[1]] = '@'
for i in critter_list:
if critter_list[i][2] > 0:
gen[critter_list[i][0]][critter_list[i][1]] = "M"
else:
gen[critter_list[i][0]][critter_list[i][1]] = "X"
gen[door[0]][door[1]] = "D"
print('\n'.join(' '.join(row) for row in gen))
def ai():
for i in critter_list:
if critter_list[i][2] > 0:
if critter_list[i][0] == char_xy[0] and critter_list[i][1] == char_xy[1]:
attack(i)
defend(i)
player_in_range = True
elif critter_list[i][0] == char_xy[0] + 1 and critter_list[i][1] == char_xy[1] + 1:
defend(i)
player_in_range = True
elif critter_list[i][0] == char_xy[0] - 1 and critter_list[i][1] == char_xy[1] + 1:
defend(i)
player_in_range = True
elif critter_list[i][0] == char_xy[0] + 1 and critter_list[i][1] == char_xy[1] - 1:
defend(i)
player_in_range = True
elif critter_list[i][0] == char_xy[0] - 1 and critter_list[i][1] == char_xy[1] - 1:
defend(i)
player_in_range = True
elif critter_list[i][0] == char_xy[0] and critter_list[i][1] == char_xy[1] + 1:
defend(i)
player_in_range = True
elif critter_list[i][0] == char_xy[0] and critter_list[i][1] == char_xy[1] - 1:
defend(i)
player_in_range = True
elif critter_list[i][0] == char_xy[0] + 1 and critter_list[i][1] == char_xy[1]:
defend(i)
player_in_range = True
elif critter_list[i][0] == char_xy[0] - 1 and critter_list[i][1] == char_xy[1]:
defend(i)
player_in_range = True
else:
player_in_range = False
if player_in_range == False:
c_move = randrange(1, 6)
if c_move == 1 and critter_list[i][0] < int(size) - 1:
critter_list[i][0] += 1
elif c_move == 2 and critter_list[i][1] < int(size) - 1:
critter_list[i][1] += 1
elif c_move == 4 and critter_list[i][0] > 0:
critter_list[i][0] -= 1
elif c_move == 5 and critter_list[i][1] > 0:
critter_list[i][1] -= 1
while character["hp"] > 0:
if new_level == True:
door = [randrange(0, int(size)), randrange(0, int(size))]
critter_list = {}
critter_gen()
map_gen()
level += 1
new_level = False
elif new_level == False:
print("N A M E:  " + character["name"])
print("H E A L T H:  " + str(character["hp"]))
print("W E A P O N:  " + str(character["weapon_rating"]) + "     " + character["weapon_name"])
print("A R M O U R:  " + str(character["armour_rating"]) + "     " + character["armour_name"])
for i in critter_list:
print("E N E M Y:  " + str(i) + "     " + "H P: " + str(critter_list[i][2]) + "     " + "A T K: " + str(critter_list[i][3]))
player_input()
ai()
map_gen()
if door[0] == char_xy[0] and door[1] == char_xy[1]:
new_level = True

print()
print()
print(" ________________________________________________")
print("|    G    A    M    E        O    V    E    R    |")
print(" IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII")
print()
print("            You Reached L E V E L: " + str(level))


How to use / manual

I SEE CLEARLY IN THE DARK: THE MANUAL

DISPLAY
@ = The Player Character
M = A Monster
D = The Door to the next level
- = An Empty Tile

CONTROLS
{_w_} = This moves the character closer to the top of the map.
{_a_} = This moves the character closer to the right of the map.
{_s_} = This moves the character closer to the bottom of the map.
{_d_} = This moves the character closer to the left of the map.
{_f_} = Attacks all enemies in range.

You must reach the door on each level without being killed by the monsters. Doors are activated on contact so don't worry.

There are no health refills or weapon upgrades in this game, so the primary concern is stealth. Monsters will only notice you if you come into range. If you are in range they WILL attack you and there is no way of telling what Monster you are dealing with until either you or the monster attacks.

This requires careful monitoring of HP

Player's HP:
H E A L T H:  12.0

Monster's stats (HP):
E N E M Y:  An Illuminatus     (H P: 48)     A T K: 2

If you occupy the same tile as a monster, each time you or the enemy attacks both you and the enemy will be dealt damage. This allows for a quick kill if you are cornered but locks you in to a fight you can't avoid.

• Comment on documentation: you seem to have the descriptions of 'a' and 'd' backward (according to your code, and the pretty much universal gaming standard). Nov 21, 2015 at 5:20
• Pretty cool to play congrats Nov 24, 2015 at 23:19

1. Python programmers tend to limit their line length to 79. And so even if you don't follow it every day, it's advisable when uploading to CR.

2. It's unlikely that you as a beginner have heard of JSON, but it's a really nice, and good standard.

Take for example critter_names and character, it'd be good if you could move them out of the python file. And you can! By using JSON.

critter_names.json

["JOHN CENA", "Shrek", "A Troll", "Maymay", "Ur mum", "A Pink guy", "A Roman bust", "A Rampant AI", "A NSA operative", "A Klu Klux Klan Member","An iPhone user","A Mac user", "Someone wearing a snapback", "Someone wearing an Unknown Pleasures T-shirt who hasn't even listened to the album","Larry Page","Someone who illegitimately won the NCSS", "A Lad", "An Illuminatus"]


character.json

{
"name": "YUNG LEAN",
"armour_name": "Boardies",
"armour_rating": 1,
"weapon_name": "Meaty fists",
"weapon_rating" : 10,
"hp": 100
}


Both of the above formats are allowed, one that's easy to edit, and one that's just a dump.
To then use these you can include them.

import json
with open('critter_names.json') as f:
with open('character.json') as f:


The with is a special keyword, that here closes the file, when you are finished with it. without it you should write:

f = open('critter_names.json')
f.close()

3. You do int(size) a lot, instead you could change the definition to:

size = int(input(...))

4. You do randrange(0, int(size)) a lot, you could change it to a function.

def rand_size():
return randrange(0, size)

5. In your player_input, you could change all the move == "w" ... "a" ... into a simple if statement. If we ignore char_xy[0] > 0 and other similar checks, you can make a super simple move function!

if move in "wasd":
index, increment = {
"w": (0, -1),
"a": (1, -1),
"s": (0, 1),
"d": (1, 1),
}(move)
try:
char_xy[index] += increment
except IndexError:
print("That is not a valid move")


You can also change move = "f". Your current code, checks if it's within space of the character. However you can make the code, much simpler!

By making an array, of attack-able critter positions, you can change all the ifs to a simple in. (excluding defend(i))

Here is how you can create the list. The second example is how I would.

attack_pos = []
for x in range(-1, 2):
for y in range(-1, 2):
attack_pos.append((char_xy[0] + x, char_xy[1] + y))

attack_pos = [(char_xy[0] + x, char_xy[1] + y) for x in range(-1, 2) for y in range(-1, 2)]


And how you would use it:

elif move == "f":
attack_pos = [
(char_xy[0] + x, char_xy[1] + y)
for x in range(-1, 2)
for y in range(-1, 2)
]
for critter_name, critter_info in critter_list.items():
if critter_info[:2] in attack_pos:
attack(critter_name)
if critter_info[:2] == char_xy[:2]:
defend(critter_name)

6. Since you seem like a more advanced beginner, you may want to know what the turnery operator is. This is good, as it leads to less repetition, like you have in map_gen.

As en example, I'll show one way to think of the turnery operator, and then how to use it.

for i in critter_list:
if critter_list[i][2] > 0:
icon = "M"
else:
icon = "X"
gen[critter_list[i][0]][critter_list[i][1]] = icon


However, this is quite verbose. Instead you can do:

for critter in critter_list.values():
gen[critter[0]][critter[1]] = "M" if critter[2] > 0 else "X"

7. You can reduce the repetition in ai, the same way you can in player_input. Also, I'm ignoring player_in_range.

def ai():
attack_pos = [
(char_xy[0] + x, char_xy[1] + y)
for x in range(-1, 2)
for y in range(-1, 2)
]
for critter_name, critter_info in critter_list.items():
if critter_info[2] > 0:
if critter_info[:2] in attack_pos:
defend(critter_name)
if critter_info[:2] == char_xy[:2]:
attack(critter_name)


This shows that again, it's a simple task to change the AI. However I didn't add if the player is not in range.

For that I would recommend that we use the same thing as we did for move in "wasd".

index, increment = [
(0, -1),
(1, -1),
(0, 0),
(0, 1),
(1, 1)
](random.randrange(0, 5))
try:
critter_info[index] += increment
except IndexError:
pass


All in all, you can dramatically reduce the size of the code, make it more understandable, and less buggy (later on), by adding a few lists.

Also, now you can change the attack range to two with ease! Time to add a ranger class ay?

attack_pos = [(char_xy[0] + x, char_xy[1] + y) for x in range(-2, 3) for y in range(-2, 3)]


But here's all the changes I made:

import json
from random import randrange

with open('critter_names.json') as f:
with open('character.json') as f:

size = int(input("How big do you want the map? (recomended between 10 and 20): "))
char_xy = [0,0]
critter_list = {}
door = [randrange(0, size), randrange(0, size)]
new_level = True
player_in_range = False
level = 0

def attack(name):
critter_list[name][2] = critter_list[name][2] - character["weapon_rating"]

def defend(name):
character["hp"] = character["hp"] - (critter_list[name][3]/character["armour_rating"])

def critter_gen():
for i in critter_names:
if randrange(1, 6) == 3:
#[xpos, ypos, hp, attack]
critter_list[i] = [randrange(0, size), randrange(0, size), randrange(1, 100), randrange(1, 25)]

def player_input():
move = input("It's your move! ")
if move in "wasd":
index, increment = {
"w": (0, -1),
"a": (1, -1),
"s": (0, 1),
"d": (1, 1),
}(move)
try:
char_xy[index] += increment
except IndexError:
print("That is not a valid move")
elif move == "e":
if door[0] == char_xy[0] and door[1] == char_xy[1]:
new_level = True
elif move == "f":
attack_pos = [
(char_xy[0] + x, char_xy[1] + y)
for x in range(-1, 2)
for y in range(-1, 2)
]
for critter_name, critter_info in critter_list.items():
if critter_info[:2] in attack_pos:
attack(critter_name)
if critter_info[:2] == char_xy[:2]:
defend(critter_name)
else:
print("That is not a valid move")

def map_gen():
asc = '-'
gen = [size * [asc] for i in range(size)]
gen[char_xy[0]][char_xy[1]] = '@'
for critter in critter_list.values():
gen[critter[0]][critter[1]] = "M" if critter[2] > 0 else "X"
gen[door[0]][door[1]] = "D"
print('\n'.join(' '.join(row) for row in gen))

def ai():
attack_pos = [
(char_xy[0] + x, char_xy[1] + y)
for x in range(-1, 2)
for y in range(-1, 2)
]
for critter_name, critter_info in critter_list.items():
if critter_info[2] > 0:
if critter_info[:2] in attack_pos:
defend(critter_name)
if critter_info[:2] == char_xy[:2]:
attack(critter_name)
else:
index, increment = [
(0, -1),
(1, -1),
(0, 0),
(0, 1),
(1, 1)
](random.randrange(0, 5))
try:
critter_info[index] += increment
except IndexError:
pass

while character["hp"] > 0:
if new_level == True:
door = [randrange(0, int(size)), randrange(0, int(size))]
critter_list = {}
critter_gen()
map_gen()
level += 1
new_level = False
elif new_level == False:
print("N A M E:  " + character["name"])
print("H E A L T H:  " + str(character["hp"]))
print("W E A P O N:  " + str(character["weapon_rating"]) + "     " + character["weapon_name"])
print("A R M O U R:  " + str(character["armour_rating"]) + "     " + character["armour_name"])
for i in critter_list:
print("E N E M Y:  " + str(i) + "     " + "H P: " + str(critter_list[i][2]) + "     " + "A T K: " + str(critter_list[i][3]))
player_input()
ai()
map_gen()
if door[0] == char_xy[0] and door[1] == char_xy[1]:
new_level = True

print()
print()
print(" ________________________________________________")
print("|    G    A    M    E        O    V    E    R    |")
print(" IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII")
print()
print("            You Reached L E V E L: " + str(level))


First of all you seem to have made a working game, and there is a certain game logic. But your overall coding style and data structures needs updating. If you haven't done so already, I would strongly suggest reading PEP8.

Actually your style isn't really bad, but the entire game is crippled by no use of vertical spacing. You have mostly good names, albeit some of them are a little short, but you need to add some vertical space in there. And you need to add a little comments here and there.

I suggest the follwing for adding vertical space:

1. Add two newlines before any function, method or class
2. Add newline within functions before logical groups of code, i.e. initialising local variables, if ... elif blocks, for or while loops

Look into using named tuples and/or classes, i.e. your main character could be well served using either. That would allow for coding like while character.hp > 0 or character.move('e'). Which in turn could use a namedtuple with x and y coordinates.

Using a named tuple for coordinates could also make some of your logic easier, as you could use loops to check for all the different positions, instead of large amounts of if ... elif blocks.

You also use some anti-patterns, as they are called. I.e. if new_level == True: followed by elif new_level == False:. First of all this could be written like if new_level: followed by elif not new_level:, but the second part is redundant, as new_level has only two values. So you could and should use if new_level: followed by else:.

A similar anti-pattern is setting the player_in_range = True in each and every if statement. Set it once to True before all the if statements, and let the else: statement reset it to False. Looks nicer, and is easier to follow.

Regarding all those if statements, you could also benefit from combining them using or statements in between. Or even better to make a position class with a class method so that you could do stuff like: if character.position.in_range_of(critter[i]). That would look a lot nicer...

Lastly, I would look into print formatting which could simplify print statements to: print("W E A P O N: {} {}".format(character["weapon_rating"], character["weapon_name"])). Or if using named tuples or classes: ... .format(character.weapon_rating, character.weapon_name)).

The manual seems OK, but I would most likely manually word wrap at around 72 characters, to make it read easier most places. But that kind of depends upon where and you are displaying the text.

This is not a complete review, but you should have some pointers to get you started on refactoring your code. You are very welcome to post a new question with your revised edition of your game.