# Creating Bingo Cards with Python and ReportLab

Imagine the following scenario:

You are about to host a gathering of some sort and for whatever reason you can't come up with better entertainment than to have your guests play bingo. Being the Python hacking, bingo playing weirdo that you are, you'll clearly have create the bingo cards yourself.

So you set out to write a program creating an arbitrary number of bingo cards neatly contained inside a single PDF file.

Where there are random numbers, the `random` module can't be far:

```import random
```

For PDF creation we will use the frightingly powerful ReportLab library. If you haven't already installed it you can do so using pip (`pip install reportlab`).

We will only need a few of ReportLab's many (many) components:

```from reportlab.platypus import (
BaseDocTemplate,
PageTemplate,
Frame,
Table,
TableStyle,
PageBreak
)
from reportlab.lib.colors import lightgrey, black
from reportlab.lib.units import cm
```

First, let's create the random numbers needed to fill up the columns of a bingo card. This will be achieved by the function `list_of_columns` and involves a number of steps:

1. We split the list of available numbers (1 through 75, typically) into sublists from which the columns of our bingo cards will be fed. The length of each slice is the total number of numbers (75) divided by the number of columns (5). Since we want all slices to be of the same length, we perform a floor division.
2. From each slice we pick a random sample of n numbers where n is the number of rows (5).
3. Lastly, we sort every sample so the numbers of each column will appear on the card in ascending order.

Weave those steps together into a list comprehension and you get:

```def list_of_columns(
numbers=range(1, 76),
num_of_columns=5,
num_of_rows=5
):
slice_length = len(numbers) // num_of_columns
return [
sorted(
random.sample(
numbers[i * slice_length: (i + 1) * slice_length],
num_of_rows
)
)
for i in range(num_of_columns)
]
```

Isn't it beautiful? The function `list_of_columns` gives us all the numbers for a bingo card, neatly arranged in a list of columns.

Unfortunately this still isn't exactly what we need: To be able to later fill them into a table, we will need the numbers to be arranged by rows. How to we go about that?

Let's interpret our list of columns as a matrix. Then the list of rows is the transpose of that matrix.

So, next we define a function `list_of_rows` that takes as its only argument a matrix (the list of columns) and returns its transpose:

```def list_of_rows(list_of_columns):
return [list(row) for row in zip(*list_of_columns)]
```

Later on, we will want a free space to appear in the center of the card. So next, we write a function `insert_free_spaces` which takes the following arguments:

• `numbers` (a matrix i.e. a list of lists)
• `coords` (a list of 2-tuples)

The function's return value is a new matrix where the numbers at all given coordinates have been replaced with `None`.

If the second argument is omitted `insert_free_spaces` will replace the third number of the third row, which translates to the coordinates `(2, 2)`.

```def insert_free_spaces(numbers, coords=[(2, 2)]):
return [
[
n if not (x, y) in coords else None
for x, n in enumerate(numbers[y])
]
for y in range(len(numbers))
]
```

The next function `prepend_title_row` is quite simple. It receives a matrix and returns a new one where a row of characters spelling 'BINGO' has been prepended. This will be used to add title rows to our bingo cards.

```def prepend_title_row(numbers):
return [['B', 'I', 'N', 'G', 'O']] + numbers
```

Phew! Now let's put it all together with the help of a new function called `card_data`.

It doesn't take any arguments and returns a list of lists (rows) containing all the data we need to create a bingo card.

```def card_data():
return prepend_title_row(
insert_free_spaces(
list_of_rows(
list_of_columns()
)
)
)
```

It's time to create the PDF document! Every page contains what is essentially a nicely styled table. Let's do the styling first:

• We set the font size for all cells to a generous 28 points.
• Leading equals the font size which is unusual but will help with vertical alignment.
• For the cells of the title row we use the bold version of the Helvetica font.
• We add a thin black inner grid and slighty thicker lines to frame the table.
• The title row also receives a more visible border.
• All cell content should be centered horizontally and vertically.
• Finally, let's change the background color of the free space, as well as the title row, to a light gray.

All of these rules are stored in a `TableStyle` and will later be requested by calling the function `stylesheet` which returns a dictionary of styles. Well, in this case its just one style...

```def stylesheet():
return {
'bingo': TableStyle(
[
('FONTSIZE', (0, 0), (-1, -1), 28),
('LEADING', (0, 0), (-1, -1), 28),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('INNERGRID', (0, 0), (-1, -1), 0.25, black),
('BOX', (0, 0), (-1, 0), 2.0, black),
('BOX', (0, 1), (-1, -1), 2.0, black),
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
('BACKGROUND', (2, 3), (2, 3), lightgrey),
('BACKGROUND', (0, 0), (-1, 0), lightgrey),
]
),
}
```

Next we create the pages of the document. Every page contains a table which, in turn, contains the data created by `card_data`.

Since we want exactly one table per page we need to insert a page break after each table. This is achieved by zipping the list of tables with an equally long list of page breaks.

The function `pages` takes as its arguments the number of pages that are to be created and a stylesheet like the one we've just built. The return value is a list of pages.

```def pages(number_of_pages, stylesheet):
pages = zip(
[
Table(
card_data(),
5 * [2.5 * cm],  # column widths
6 * [2.5 * cm],  # row heights
style=stylesheet['bingo']
)
for i in range(number_of_pages)
],
[
PageBreak()
] * number_of_pages
)
return [e for p in pages for e in p]
```

The last function is `build_pdf` which does what its name implies - it builds the PDF document and writes it to the file specified in the first argument.

As a second argument `build_pdf` expects a list of pages as created by the function `pages`.

The body of `build_pdf` contains mostly boilerplate which you will find useful for most ReportLab projects.

```def build_pdf(filename, pages):
doc = BaseDocTemplate(filename)
[
PageTemplate(
frames=[
Frame(
doc.leftMargin,
doc.bottomMargin,
doc.width,
doc.height,
id=None
),
]
),
]
)
doc.build(pages)
```

And that's it! The only thing left to do is to actually call `build_pdf` and produce the desired number of bingo cards — 1000 in this example.

```build_pdf('1000-bingo-cards.pdf', pages(1, stylesheet()))
```

What's next? A few suggestions: