Random numbers, order and choices

Written on

Python has many useful standard libraries - Today we will look at the random module.

Whether it's generating data to test our code or creating a machine player for a simple game, random numbers end up being rather useful. In this article we'll look at three uses of randomness. Random numbers, randomising order and random choices.

Randomness

Firstly we need to point out that computers do not generate random numbers! Consider the following example

from random import seed, randint

repeats = 3
for _ in range(repeats):
    seed(1)  # arbitrary bad seed
    for _ in range(20):
        print(randint(1, 20), end=", ")
    print("")

For me this outputs

>>>
  5, 19, 3, 9, 4, 16, 15, 16, 13, 7, 4, 16, 1, 13, 14, 20, 1, 15, 9, 8,
  5, 19, 3, 9, 4, 16, 15, 16, 13, 7, 4, 16, 1, 13, 14, 20, 1, 15, 9, 8,
  5, 19, 3, 9, 4, 16, 15, 16, 13, 7, 4, 16, 1, 13, 14, 20, 1, 15, 9, 8,

What's important to spot here, is the repetition each time the seed is reset. This is worth bearing in mind if you desire greater randomness than what is essentially a weird sequence. In Python if no seed is specified then an OS-dependent value is taken. In short - bear in mind there are no true random numbers here but they are typically good enough for many cases we come across!

Random Numbers

To begin let us generate a random number using random(). This will create a random number between 0 and 1.

from random import random
value = random()
print(f"The value to two decimal places is {value:.2f}"

If you're unsure how the format argument works have a look at my post on formatting. Note how we import the function random from the library random. If we wish to see what the library contains we can use

import random
help(random)

This will output a lot of helpful text! If we only want help on a specific function we can

import random
help(random.random)

This will give us the output

>>>
  Help on built-in function random:
  random(...) method of random.Random instance
      random() -> x in the interval [0, 1).

Let's use a different function from random. For example, let's create a random bearing

from random import uniform

random_bearing = uniform(0, 360)
print(f"{random_bearing:05.1f} Degrees")

This may print out

>>>
  004.6 Degrees

Let's say we don't want all numbers, just integers. For that we can use randrange. The arguments are the same as range, so

from random import randrange

randrange(10)  # random integer between 0 and 10
randrange(0, 100, 25)  # chooses randomly from 0,25,50,75

Another similar function is randint. This includes the end value and does not allow us to perform steps. For example

from random import randint

def sum_of_dice_rolls(n=1, lower=1, upper=6):
    """
    Sum of n dice rolls
    """
    if n > 0:
        return sum([randint(lower, upper) for _ in range(n)])
    else:
        raise ValueError('Invalid number of dice rolls!')

for n in (1,2,3):
    print(f"From {n} rolls you rolled a total of {sum_of_dice_rolls(n)}")

may give

>>>
  From 1 rolls you rolled a total of 5
  From 2 rolls you rolled a total of 10
  From 3 rolls you rolled a total of 17

Picking Items Randomly

Let's say we want to pick other things randomly, like say names out of a hat. For this we can use the choice function.

from random import choice

names = ('Bob', 'Jerry', 'Mary', 'Francis', 'Jill', 'Barbara')

random_name = choice(names)
print(f"It is {random_name}'s turn!")

Will potentially give

>>>
  It's Barbara's turn!

If we wish to choose multiple cases we can use

from random import choices

names = ('Bob', 'Jerry', 'Mary', 'Francis', 'Jill', 'Barbara')

for random_name in choices(names, k=6):
    print(f"It is {random_name}'s turn!")

this (potentially!) outputs

>>>
  It is Jerry's turn!
  It is Bob's turn!
  It is Mary's turn!
  It is Jill's turn!
  It is Jerry's turn!
  It is Mary's turn!

Here we can see that some names are repeated. This might be okay for our use case but it won't be helpful if we wish to pick everyone just once! Therefore we can use the sample function. This will pick k options as before but won't repeat.

from random import sample

names = ('Bob', 'Jerry', 'Mary', 'Francis', 'Jill', 'Barbara')

for random_name in sample(names, k=6):
    print(f"It is {random_name}'s turn!")

For further reading, go to the Random reference page. For the remaining part of this article, we will out list a few example uses of the random library.

A Random Password

One common example is to generate random strings. Here we import four groups of symbols and we will randomly select from these. We will randomly choose a group and then randomly choose a symbol from that group. Here is our code.

from string import ascii_lowercase, ascii_uppercase, punctuation, digits
from random import choice
options = (ascii_lowercase, ascii_uppercase, punctuation, digits)

length = 12
new_password = ''
while len(new_password) < length:
    new_password += choice(choice(options))

print(new_password)

will produce something like

>>>
  3}z(vvmeO~L3

extension

  1. Alter the above code to select a specific number of each kind of characters. Eg 3 uppercase, 3 lowercase, 2 numbers and 2 punctuation.
  2. Alter the code to produce passwords based on a pattern. Eg nu3p5* would mean number, uppercase, 3 punctation and 5 anything.

Turtle - Random Motion

Turtle is a great little tool for using Python to move a virtual turtle about. Here's a couple of simple programs!

import turtle
from random import choices

dim = 100
turtle.setworldcoordinates(-dim, -dim, dim, dim)
bob = turtle.Turtle()

while True:
    x,y = choices(range(-dim, dim), k=2)
    bob.goto(x,y)


turtle.exitonclick()

will produce something like

Random motion! Turtle travels to a random location on the screen

Here we have generated random coordinates for the turtle to move to each step. Another example!

import turtle
from random import random

turtle.speed(10)
prob = 0.5
step = 10
angle = 60

while True:
    turtle.forward(step)
    if random() > prob:
        turtle.left(angle)
    else:
        turtle.right(angle)

produces

random motion using Turtle producing a hexagonal pattern across the screen.

Here we have used random() to ensure that we turn left the same number of times as turning right. In this way we produce a randomised honeycomb pattern.

Rock Paper Scissors

I'll discuss implementations of this game in further detail in another post, for now let's look at getting input. We need methods of getting player and computer choices for the game. Here's one way to this

from random import choice

moves = ('rock', 'paper', 'scissors')

def computer_choice():
    global moves
    return choice(moves)

def player_choice():
    global moves

    while True:
        print("Valid moves are", *moves, sep="\n", end="")
        player_move = input("Please enter your move >>").lower()
        if player_move in moves:
            return player_move
        else:
            print(f"{player_move} is an invalid option, try again!")

Here there a few things to note. Firstly see how we have used the choice function to select a random move for the computer. Secondly we have kept moves outside both functions. This is so if we wish to expand the game later, we only have one variable to change. Finally note that we have written the player_choice() function to prevent an invalid option being presented!

Battleships

As a final demonstration, I include a crude example of a computer playing batteships. In battleships we want the computer to find all the ships we have hidden.

Ideally we want the computer to visit every tile just once. Furthermore we want the computer to guess adjacent tiles if the computer does in fact get a hit

Here's a gif showing my implementation

Animation of crude algorithm for battleships computer player. Here we can see that the computer will, once it finds a portion of a ship, prioritise searching adjacent tiles first.

Here red squares indicate misses and green indicate hits.

Extensions

  1. Get the computer to focus on a particular axis (horizontal or vertical) once its clear what direction the ship is facing.
  2. Prevent the code from generating ships that overlap.
  3. Add a human player!
import turtle
from random import shuffle, choice, random

dim = 250  # dimension of our screen
L = 25  # size of a box
turtle.setworldcoordinates(-dim, -dim, dim, dim)


def draw_square(tl, x, y, L, fill=False, col='red'):
    """
    Draws filled square at x,y location with side of L
    if fill is True the box is filled with colour 'col'
    """
    tl.penup()
    tl.goto(x, y)
    tl.pendown()
    if fill:
        tl.color("black", col)
        tl.begin_fill()
    for side in range(4):
        tl.forward(L)
        tl.left(90)
    if fill:
        tl.end_fill()

    tl.penup()
    tl.goto(x,y)

bob = turtle.Turtle()
bob.speed(10)

## Generate all possible box locations
locations = []
for x in range(-dim, dim, L):
    for y in range(-dim, dim, L):
        locations.append((x,y))

shuffle(locations)  # Computer will visit all locations randomly

def new_ship(length=3):
    """
    Generate location of ships
    Code checks if both front and end of randomly placed ship is on screen
    if so it returns this

    TODO : does not currently check if intersection with existing ships
    """
    ship = []
    global locations
    x,y = choice(locations) # Front of ship!
    while len(ship) < length:
        step = choice((-1, 1))
        if random() > 0.5:  # randomly choice horizontal or vertical ship
            end_x = x + length * step * L
            if (end_x, y) in locations:  # Only allow valid end position
                for loc in range(x, end_x + 1, step*L):
                    ship.append((loc, y))
        else:
            end_y = y + length * step * L
            if (x, end_y) in locations:
                for loc in range(y, end_y + 1, step*L):
                    ship.append((x, loc))
    return ship


ships = []
for j in range(2, 8):
    ships += new_ship(j)

while locations:
    x,y = locations.pop(0)  # get next guess from front of queue
    if (x,y) in ships:
        col = 'green'
        to_shift = []
        for index, (new_x, new_y) in enumerate(locations):
            if new_x in range(x-L, x+2*L, L) and new_y in range(y-L, y+2*L, L):
                to_shift.append(index)

        # Places all adjacent tiles at the front of the queue
        for index in to_shift:
            locations.insert(0, locations.pop(index))
    else:
        col = 'red'

    draw_square(bob, x, y, L, True, col)


turtle.exitonclick()

Enjoy!