What secret message is hidden in this grid?

message
What secret message is hidden in this grid?

The message is "East, west, home's best.". A so-called grille is used to decode the message: a pierced sheet (of paper or cardboard or similar) with a square grid into which some apertures are pierced. In this case, a $$5 \times 5$$ grid into which six apertures are pierced according to the following pattern.

grille
Grille with apertures at positions (0, 0), (1, 0), (1, 2), (1, 4), (3, 3) and (4, 2).

If the grille is placed on top of the square grid, the text "East, " can be read through the apertures (in reading order from left to right, and from top to bottom). This is the first fragment of the hidden message.

grille op boodschap
Als de grille op het rooster gelegd wordt, dan kan doorheen de openingen de tekst "East, " uitgelezen worden (van links naar rechts, en van boven naar onder).

By rotating the grille 90° clockwise, the second fragment of the hidden message can be read: "West, ". After that, the grille can be rotated 90° clockwise twice more to read the final two fragments of the hidden message: "home's" and " best."

rotations
The entire message can be read from the four possible rotations of the grille.

The earliest known description of this technique is due to the Italian polymath Girolamo Cardano1 in 1550. For that reason, grilles are sometimes called Cardan grilles.

Assignment

In order to be able to refer to the cells of a grid (both for a grid with characters in which a secret message is hidden and for a grid with the apertures of a grille), the rows are numbered from top to bottom, and the columns from left to right, starting at zero in both cases. As a result, the position of a cell can be represented as a tuple $$(r, c)$$, with $$r \in \mathbb{N}$$ (int) indicating the row index and $$c \in \mathbb{N}$$ (int) indicating the column index.

An $$n \times n$$ ($$n \in \mathbb{N}_0$$) grid that hides a secret message is stored in a text file. Each row of the grid is on a separate line, so that the file has $$n$$ lines that each contain $$n$$ characters. For example, the file containing the $$5 \times 5$$ grid that hides the secret message from the introduction, looks like this:

E hWe
aosbt
seVts
m,e,'
t.  s

Define a class Grille to represent grilles that can be used to decode secret messages. Two arguments must be passed when creating a grille (Grille): i) the number of rows/columns (int) of the square grid on the grille and ii) a collection (list, tuple or set) with the positions of the apertures in the grille. A grille $$g$$ (Grille) must at least have the following properties:

In addition, a grille $$g$$ (Grille) must at least support the following methods:

If a grille (Grille) is passed to the built-in function str, a string representation (str) of its grid must be returned. Each row of the grid is on a separate line, cells with apertures are represented by the capital O, and cells without apertures are represented by a hash symbol (#).

Two grilles $$r$$ and $$s$$ (Grille) are equal (r == s) if they have the same number of rows/columns and if they can be rotated (multiples of 90°) such that all their apertures are at the same positions.

The addition (r + s) of two grilles $$r$$ and $$s$$ (Grille) produces a new grille $$t$$ (Grille) that looks as if both grilles $$r$$ and $$s$$ are put on top of each other. In other words, the apertures in $$t$$ are at positions where there are apertures in both $$r$$ and $$s$$. The addition is only defined for grilles $$r$$ and $$s$$ that have the same number of rows/columns. If this is not the case, an AssertionError must be raised with the message invalid operation.

Example

In the following interactive session we assume the text file message.txt2 to be located in the current directory.

>>> grille = Grille(5, [(0, 0), (1, 0), (1, 2), (1, 4), (3, 3), (4, 2)])
>>> grille.dimension
5
>>> grille.apertures
{(1, 2), (0, 0), (3, 3), (1, 4), (4, 2), (1, 0)}
>>> print(grille)
O####
O#O#O
#####
###O#
##O##
>>> grille.decode('message.txt3')
'East, '
>>> grille.rotate().apertures
{(3, 1), (2, 0), (2, 3), (4, 3), (0, 4), (0, 3)}
>>> print(grille)
###OO
#####
O##O#
#O###
###O#
>>> grille.decode('message.txt4')
'West, '
>>> grille.rotate().apertures
{(3, 2), (3, 0), (4, 4), (1, 1), (3, 4), (0, 2)}
>>> print(grille)
##O##
#O###
#####
O#O#O
####O
>>> grille.decode('message.txt5')
"home's"
>>> grille.rotate().apertures
{(0, 1), (1, 3), (2, 1), (4, 1), (2, 4), (4, 0)}
>>> print(grille)
#O###
###O#
#O##O
#####
OO###
>>> grille.decode('message.txt6')
' best.'
>>> grille.rotate(clockwise=False).apertures
{(3, 2), (3, 0), (4, 4), (1, 1), (3, 4), (0, 2)}
>>> print(grille)
##O##
#O###
#####
O#O#O
####O
>>> grille.decode('message.txt7')
"home's"

>>> grille1 = Grille(4, ((1, 3), (0, 0), (2, 3), (1, 1)))
>>> grille2 = Grille(4, {(2, 0), (1, 0), (3, 3), (2, 2)})
>>> grille3 = Grille(4, [(3, 0), (2, 3), (2, 0), (1, 1)])
>>> grille4 = Grille(5, ((1, 3), (0, 0), (2, 3), (1, 1)))
>>> grille1 == grille2
True
>>> grille1 == grille3
False
>>> grille1 == grille4
False

>>> grille5 = grille1 + grille3
>>> grille5.dimension
4
>>> grille5.apertures
{(2, 3), (1, 1)}
>>> print(grille5)
####
#O##
###O
####
>>> grille1 + grille4
Traceback (most recent call last):
AssertionError: invalid operation