6
\$\begingroup\$

I have written this small game for the "learn python the hard way" book. I ask for some opinions about it and how to improve it.

I know about pygame and others game engines but all the game runs in the terminal. The text file orc.txt, cyclops.txt, etc are ASCII drawings

from PIL import Image
import numpy as np
import msvcrt
from time import sleep
from random import choice, randint

previous = 15 #start position tile

def createmap():
    """
    The map is sketched in bmp, I make a array
    to use it like a tileset
    """
    global bmp
    bmp = Image.open('sketch.bmp')
    bmp_data = bmp.getdata()
    bmp_array = np.array(bmp_data)
    global map
    map = bmp_array.reshape(bmp.size)

def array_to_text_map():
    """ Tileset
    # = Wall
    . = sand
    + = door
    e = enemy
    D = dragon
    $ = gold
    @ = the player

    numbers as due to the 16 color palette used
    in the sketch
    """
    text_map = open('map.txt', 'w')
    for x in map:
        for y in x:
            if y == 0:
                text_map.write('#')
            elif y == 3:
                text_map.write('.')
            elif y == 8:
                text_map.write('+')
            elif y == 9:
                text_map.write('e')
            elif y == 10:
                text_map.write('D')
            elif y == 12:
                text_map.write('$')
            elif y == 13:
                text_map.write('@')
            elif y == 15:
                text_map.write(' ')
        text_map.write('\n')
    text_map.close()

def print_text_map():
    text_map = open('map.txt')
    for x in range(bmp.size[1]):
        print(text_map.readline(), end='')

def move_player_by_key():
    wkey = msvcrt.getch
    if wkey() == b"\xe0":
        key2 = wkey()
        if key2 == b"H":
            move_in_matrix('up')
        elif key2 == b"M":
            move_in_matrix('right')
        elif key2 == b"K":
            move_in_matrix('left')
        elif key2 == b"P":
            move_in_matrix('down')
    else:
        exit()

def position():
    ver = np.where(map == 13)[0][0]
    hor = np.where(map == 13)[1][0]
    return ver, hor

def move_in_matrix(direction):
    ver, hor = position()

    try:
        if direction == 'up':
            analize(ver-1 , hor)
        elif direction == 'right':
            analize(ver , hor+1)
        elif direction == 'left':
            analize(ver , hor-1)
        elif direction == 'down':
            analize(ver+1 , hor)
    except IndexError:
        console.append('Not that way')

def analize(verm, horm):
    """Take decision for each tile
    verm and horm = next position
    ver and hor = actual position
    previous = keeps tile number for later
    """
    global previous
    ver, hor = position()

    def restore(ver, hor, previous):
        """Restore color of the leaved tile"""
        map[ver, hor] = previous

    if map[verm, horm] == 0:
        pass

    elif map[verm, horm] == 3:
        restore(ver, hor, previous)
        previous = map[verm, horm]
        map[verm, horm] = 13

    elif map[verm, horm] == 8:
        print('Open the door (yes/no)?', end='')
        answer = input(' ')
        if door(answer):
            restore(verm, horm, 15)
            console.append('The door was opened')
            console.append(doors_dict[(verm, horm)]) #show the description
                                                     #attached to the door

    elif map[verm, horm] == 9:
        print('Do you want to fight against the monster (yes/no)?', end='')
        answer = input(' ')
        result = fight(answer)
        if result and randint(0,1) == 1:
            restore(verm, horm, 12)
        elif result:
            restore(verm, horm, 15)

    elif map[verm, horm] == 10:
        print('the beast seems asleep, you want to wake her?(yes/no)?', end='')
        answer = input(' ')
        result = fight(answer, dragon=True)
        if result:
            win()

    elif map[verm, horm] == 12:
        print("You want to grab the items in the floor (yes/no)?", end='')
        if input(' ') == 'yes':
            gold()
            restore(verm, horm, 15)

    elif map[verm, horm] == 15:
        restore(ver, hor, previous)
        previous = map[verm, horm]
        map[verm, horm] = 13

def door(answer):
    if answer == 'yes':
        return True
    elif answer == 'no':
        return False
    else:
        console.append('Invalid command')

def identify_doors():
    """Hardcode: Identify each door and zip() it with
    a description stored previously
    """
    doors_array = np.where(map == 8)
    doors = zip(doors_array[0], doors_array[1])
    dict_doors = {}
    narrate = open('narrate.txt')
    for x in doors:
        dict_doors[x] = narrate.readline()
    return dict_doors

def fight(answer, dragon=False):
    if answer == 'yes':
        if dragon == False:
            monster = choice([Orc(), Goblin(), Cyclops()])
        else:
            monster = Dragon()

        monster.show_appearance()
        print('Fighting against', monster.name)
        sleep(2)

        while True:
            monster.show_appearance()
            print(monster.name, 'is attacking you', end = '')

            answer = input('fight (1) or defend (2)? ')
            if answer == '1':
                player.defense = 1 + player.armor
                monster.life = monster.life - player.damage
            elif answer == '2':
                player.defense = 1.5 + player.armor
            else:
                print('Invalid command')
                continue

            print(monster.name, 'counterattack!!')
            player.life = player.life - (monster.damage / player.defense)

            print(monster.life, 'remaining', monster.name, 'life')
            print(player.life, 'remaining Player life')

            if player.life <= 0:
                print('The End')
                exit()

            if monster.life <= 0:
                print('\n' * 5)
                print('Enemy defeated')
                print('You earn', monster.gold, 'gold coins')
                player.gold += monster.gold
                break   
        sleep(3)
        return True

    else:
        return False

def moredamge(val):
    player.damage += val

def morearmor(val):
    player.armor += val

golds = {'Fire sword': (moredamge, 20),
            'Oak shield': (morearmor, 0.1),
            'Anhilation sword': (moredamge, 40),
            'Iron shield': (morearmor, 0.2),
            'Siege shield': (morearmor, 0.5)
            }

def gold():
    bunch = randint(10, 200)
    print('You have found', bunch, 'gold coins')
    player.gold += bunch
    sleep(1)
    print('Is there anything else?')
    if randint(0, 10) > 7:
        obtained = choice(list(golds))
        print('You have get', obtained)
        golds[obtained][0](golds[obtained][1]) #access to key: (function, quantity)
        del golds[obtained]
    else:
        print('Oooohh.. nothing else')
    sleep(2)

def win():
    print('You have win the game')
    print('You get', player.gold, 'gold coins!!')
    exit()

class Orc():
    def __init__(self):
        self.name = 'Orc'
        self.life = 100
        self.damage = 10
        self.defense = 1
        self.gold = 100
        self.appearance = open('orc.txt')

    def show_appearance(self):
        print(self.appearance.read())


class Player(Orc):
    def __init__(self):
        self.life = 1000
        self.damage = 20
        self.gold = 0
        self.appearance = open('knight.txt')
        self.armor = 0

class Goblin(Orc):
    def __init__(self):
        self.name = 'Goblin'
        self.life = 50
        self.damage = 10
        self.gold = 50
        self.appearance = open('goblin.txt')

class Dragon(Orc):
    def __init__(self):
        self.name = 'Dragon'
        self.life = 400
        self.damage = 40
        self.gold = 2000
        self.appearance = open('dragon.txt')

class Cyclops(Orc):
    def __init__(self):
        self.name = 'Cyclops'
        self.life = 150
        self.damage = 20
        self.gold = 120
        self.appearance = open('cyclops.txt')

def presentation():
    print('\n'*2)
    player.show_appearance()
    print('Welcome hero, are you ready for fight?')
    input() 

if __name__ == "__main__":
    player = Player()

    presentation()
    console = []

    createmap()
    array_to_text_map()

    doors_dict = identify_doors()
    print_text_map()

    while True:
        move_player_by_key()
        array_to_text_map()
        print_text_map()
        if console:
            for x in console:
                print(x)
        else:
            print()
        console = []
\$\endgroup\$

1 Answer 1

7
\$\begingroup\$

I'll assume you're fairly new to Python; tell me if you feel this stuff is too basic, and I'll start criticizing you for things that you don't deserve to be criticized for.

State

Ok, so the first thing that made me go "eww" in the code was global. In general, whenever you use global, you're probably doing something wrong. If I was to write your createmap function, it might look something more like this:

def createmap():
    """
    The map is sketched in bmp, I make a array
    to use it like a tileset
    """
    bmp = Image.open('sketch.bmp')
    bmp_data = bmp.getdata()
    bmp_array = np.array(bmp_data)
    map = bmp_array.reshape(bmp.size)
    return bmp, map

Instead of using a global bmp and map variable, I'm returning the non-global bmp and map variables. This means that I have a lot more flexibility in calling the function:

# I can use it to create the old global variables:
global bmp, map
bmp, map = createmap()

# Or some local ones
local_bmp, local_map = createmap()

# But most importantly, I can use it to create multiple sets of them
bmp1, map1 = createmap()
bmp2, map2 = createmap()

The last case is the most important; using your version of createmap(), when I call createmap() for the second time, it overwrites the previous bmp and map variables. This might not seem like a big problem; why would you ever need more than one map? but it makes it much harder to test things when they change global variables.

We can do similar things to your other functions to avoid using global variables. The array_to_text_map function currently uses the map global variable, but we can replace that with a map argument. This way we will be able to convert any map to a text map, not just the one stored in the global map variable.

One downside to this is that it does require more typing; I have to explicitly pass around all of my state. However, one of the core tenets of Python is that explicit is better than implicit, so get used to it ;) I should mention that people often wrap up their program's commonly used variables into classes, though that's probably more complex than you are looking for right now.

Switch Statements

Another thing that makes me feel uncomfortable about the code is when you have long chains of elif statements:

if y == 0:
    text_map.write('#')
elif y == 3:
    text_map.write('.')
elif y == 8:
    text_map.write('+')
elif y == 9:
    text_map.write('e')
elif y == 10:
    text_map.write('D')
elif y == 12:
    text_map.write('$')
elif y == 13:
     text_map.write('@')
elif y == 15:
     text_map.write(' ')

This is ugly, and violates the DRY principle. What you are doing here is looking up a value, based on the value of the variable y. The Pythonic (and sane) way of doing this is to define a dictionary (mapping) from the values of y to the string that should be written to text_map:

TEXT_MAP_CHARS = {
    0: "#",
    3: ".",
    8: "+",
    9: "e",
    10: "D",
    12: "$",
    15: " "
}

We can then look up the correct character, and then write that to the file:

char = TEXT_MAP_CHARS[y]
text_map.write(char)

Which is much neater, and doesn't mix up program data (the mapping from numbers to characters) and program logic (the writing of the character to a file based on the value of y).


With that said, I would praise you for your style. For every problem your code may have semantically, it is formatted very well (hooray for PEP8)

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.