Trying to generate 5 random items, sometimes generate 4

Viewed 106
import random

twoDimMap = [["H", "-", "-", "-", "-", "-"], ["-", "-", "-", "-", "-", "-"],
             ["-", "-", "-", "-", "-", "-"], ["-", "-", "-", "-", "-", "-"],
             ["-", "-", "-", "-", "-", "-"], ["-", "-", "-", "-", "-", "-"]]

items = 0

while items <= 4:
    test = random.randrange(0, 3)
    if test == 0:
        twoDimMap[random.randrange(0, 5)][random.randrange(0, 5)] = "S"
    if test == 1:
        twoDimMap[random.randrange(0, 5)][random.randrange(0, 5)] = "R"
    if test == 2:
        twoDimMap[random.randrange(0, 5)][random.randrange(0, 5)] = "*"
    #  I couldn't think of an easier way to do this
    if twoDimMap[0][0] != "H":
        twoDimMap[0][0] = "H"
        items -= 1
    items += 1

print(twoDimMap)

Title explains it all pretty much (even though I know it isn't too descriptive :/), I am trying to make a game where the hero starts on the map at position [0],[0]. I can't work out why the hell I'm sometimes generating fewer items than other times.

Edit: Thanks for all of your feedback, and sorry for wasting your time with my stupid mistake :/. I'm going to blame it on the lack of coffee.

6 Answers

You're only checking if the player is overwritten, not if the objects are, you should first get the random coordinates and then check if something's there.

while items <= 4:
    test = random.randrange(0,3)
    x = random.randrange(0, 5)
    y = random.randrange(0, 5)
    if twoDimMap[x][y] == '-':
        if test == 0:
            twoDimMap[x][y] = "S"
        if test ==1:
            twoDimMap[x][y] = "R"
        if test == 2:
            twoDimMap[x][y] = "*"
        items += 1

a more compact solution as suggested in the comments is

while items <= 4:
    x = random.randrange(0, 5)
    y = random.randrange(0, 5)
    if twoDimMap[x][y] == '-':
        twoDimMap[x][y] = random.choice('SR*')
        items += 1

Since the problem space is so small, it's probably not a terrible idea to just generate three of them that are guaranteed unique.

valid_locations = (tup for tup in itertools.product(range(5), repeat=2) if tup != (0, 0))
items = ["S", "R", "*"]
choices = [(random.choice(items), loc) for loc in random.sample(valid_locations, 3)]

for item, (x, y) in choices:
    twoDimMap[y][x] = item

random.sample(collection, n) guarantees n non-duplicated random results from collection. random.choice(collection) gives you a random element from collection.

Sometimes randrange(0, 5) returns the same thing several times, thus some point is reassigned.

This could be solved by generating the coordinates together with the type and only executing the rest of the loop if that point is currently unoccupied. This would also eliminate the need for a separate (0,0) test-case.

To avoid overwriting previously placed items, you can use random.sample, which will pick random items from the input sample without replacement.

There are several other convenient functions for sequences in the random module.

I've rewritten your code and turned it into a function that can generate rectangular maps of any size and number and type of items.

import random

def make_map(width, height, number_of_items, items='SR*'):
    """Create a map with the Hero in top left corner
       and random items spread around"""
    # build an empty map using a nested list comprehension
    game_map = [['-' for x in range(width)] for y in range(height)]
    # place the hero at coordinates 0, 0
    game_map[0][0] = 'H'
    # possible item positions, excluding 0, where the hero is.
    positions = range(1, width * height)
    # loop over n random choices from the available positions
    for pos in random.sample(positions, number_of_items):
        # convert pos to x, y coordinates using modular arithmetic
        x, y = pos % width, pos // width  
        # select a random item to and place it at coordinates x, y
        game_map[y][x] = random.choice(items)

    return game_map

# generate a map. You can change the input arguments to vary size and items
game_map = make_map(6, 6, 5)
# convert the nested list to one string and print
print('\n'.join(''.join(line) for line in game_map))

We use modular arithmetic to convert the position value which is a number in the range 1 to 36 into x,y coordinates on a 6×6 grid. This is a very common and useful operation in computer graphics.

x, y = pos % width, pos // width  

This is such a common operation that python has a built-in function you can use for this exact thing.

y, x = divmod(pos, width)

I will not explain all of the code, but I encourage you to read through the answers and try to understand how every line works. You can learn a lot from reading other people's solutions to the same problem as you have solved yourself.

Less items are generated because there are times where the same coordinates are generated again. One way to avoid this is to check if the location already has an item before assigning it. This way, you will solve the issue of replacing your hero too.

import random

twoDimMap = [["H","-","-","-","-","-"],["-","-","-","-","-","-"],["-","-","-","-","-","-"],["-","-","-","-","-","-"],["-","-","-","-","-","-"],["-","-","-","-","-","-"]]

items = 0
item_list = ['S', 'R', '*']

while items <= 4:
    x = random.randrange(0,5)
    y = random.randrange(0,5)

    if twoDimMap[x][y] == '-':
        twoDimMap[x][y] = item_list[random.randrange(0,3)]
        items += 1

If your map isn't too big, you could use random.sample to Choose at random from combinations.

twoDimMap = [list(line) for line in """\
H-----
------
------
------
------
------""".split("\n")]

width, height = len(twoDimMap[0]), len(twoDimMap)

allLocations = [(x, y) for x in range(width) for y in range(height) if (x, y) != (0, 0)]

for x, y in random.sample(allLocations, 5):
    case = random.randrange(3)
    twoDimMap[y][x] = "SR*"[case]
Related