Turing Machine is a board game that uses punch cards: cards with holes punched in them. Each punch card has a colored label with a single digit on it. Blue labels correspond to the first digit of a three-digit number, yellow labels to the middle (second) digit, and purple labels to the last (third) digit. For example, these are the three punch cards that correspond to the number 421.

If you stack these three punch cards on top of each other, only a single hole remains.

The punch cards used in the Turing Machine game are designed so that each combination of three cards corresponding to a three-digit number has exactly one hole at the same position. In addition, the holes are spread out sufficiently across the punch cards so that the cards do not lose their strength.
Define a class PunchCard that can be used to represent punch cards with a rectangular $$m \times n$$ grid with $$m$$ rows and $$n$$ columns. A hole can be punched in each cell of the grid. Unlike in the Turing Machine game, punch cards (PunchCard) do not necessarily have to be square, and they do not have a label. For example, this is a punch card with a $$12 \times 12$$ grid in which 25 holes have been punched.

The rows of the grid are numbered from top to bottom, and the columns from left to right, starting from zero. This allows us to represent the position of a cell in the grid as a tuple $$(r, k)$$, where $$r$$ (int) is the row number and $$k$$ (int) is the column number where the cell is located.
When creating a new punch card (PunchCard), the number of rows $$m \in \mathbb{N}$$ (int) and the number of columns $$n \in \mathbb{N}$$ (int) of the grid must be specified. Optionally, a collection (list, tuple, or set) can be passed as a third argument, containing positions (tuple) where holes are punched in the card. If no collection is passed, no holes are punched in the card. If the punch card does not have at least two rows and two columns, or if a position is passed that is outside the grid, an AssertionError must be raised with the message invalid punch card.
If a punch card $$p$$ (PunchCard) is passed to the built-in function repr, the function must return a string representation (str) of punch card $$p$$. This representation must read like a Python expression that creates a new punch card (PunchCard) with the same dimensions and holes as punch card $$p$$. In addition, a list must always be passed as the third argument, containing the positions (tuple) of the holes listed in reading order: from left to right and from top to bottom.
If a punch card $$p$$ (PunchCard) is passed to the built-in function str, the function must return a string representation (str) of punch card $$p$$. In this representation, the rows are represented as consecutive lines (listed from top to bottom). Each line consists of a representation of the individual cells in the corresponding row (listed from left to right): a capital letter O if there is a hole in the cell or a hash (#) if there is no hole in the cell.
A punch card $$p$$ (PunchCard) with $$m$$ rows and $$n$$ columns must support at least the following methods:
A method punch that takes a row number $$r$$ (int) and a column number $$k$$ (int). If $$(r, k)$$ does not indicate the position of a cell in the grid of punch card $$p$$ ($$0 \leq r < m$$ and $$0 \leq k < n$$), an AssertionError must be raised with message invalid position. If punch card $$p$$ already has a hole at position $$(r, k)$$, an AssertionError must be raised with message card already punched at position (r, k), where the fragments in italics are filled in with row number $$r$$ and column number $$k$$, respectively. Otherwise, the method must make a hole at position $$(r, k)$$ in the grid of punch card $$p$$ and return a reference to punch card $$p$$.
A method fill that takes a row number $$r$$ (int) and a column number $$k$$ (int). If $$(r, k)$$ does not indicate the position of a cell in the grid of punch card $$p$$ ($$0 \leq r < m$$ and $$0 \leq k < n$$), an AssertionError must be raised with message invalid position. If punch card $$p$$ does not have a hole at position $$(r, k)$$, an AssertionError must be raised with message card not punched at position (r, k), where the fragments in italics are filled in with row number $$r$$ and column number $$k$$, respectively. Otherwise, the method must remove the hole at position $$(r, k)$$ in the grid of punch card $$p$$ and return a reference to punch card $$p$$.
A method issturdy that takes no arguments. The method must return a Boolean value (bool) indicating whether punch card $$p$$ is sufficiently sturdy. This is the case if each hole has at most one other hole in its orthogonally adjacent cells and no other holes in its diagonally adjacent cells. The orthogonally adjacent cells of a cell are the cells that share a side with that cell (i.e., the cells above, below, to the left, and to the right of the cell). The diagonally adjacent cells of a cell are the cells that share a corner with that cell (i.e., the cells at its top left, bottom left, top right and bottom right).

If the operator == is used to compare two punch cards $$p$$ and $$q$$ (PunchCard) with each other (p == q), they must be equal if and only if they have the same dimensions and holes in the same positions.
If the operator + is used to add two punch cards $$p$$ and $$q$$ (PunchCard) together (p + q), the two punch cards must have the same dimensions. If this is not the case, an AssertionError must be raised with message punch cards are not the same size. Otherwise, the sum must result in a new punch card (PunchCard) with the same dimensions as punch cards $$p$$ and $$q$$, and holes in positions where both punch card $$p$$ and punch card $$q$$ have a hole.
>>> card = PunchCard(4, 8)
>>> card
PunchCard(4, 8, [])
>>> print(card)
########
########
########
########
>>> card.issturdy()
True
>>> card = PunchCard(1, 2)
Traceback (most recent call last):
AssertionError: invalid punch card
>>> card = PunchCard(4, 8, {(1, 2), (3, 6), (5, 12)})
Traceback (most recent call last):
AssertionError: invalid punch card
>>> card = PunchCard(4, 8, ((1, 2), (2, 7), (3, 6)))
>>> card
PunchCard(4, 8, [(1, 2), (2, 7), (3, 6)])
>>> print(card)
########
##O#####
#######O
######O#
>>> card.issturdy()
False
>>> card.punch(0, 5)
PunchCard(4, 8, [(0, 5), (1, 2), (2, 7), (3, 6)])
>>> print(card)
#####O##
##O#####
#######O
######O#
>>> card.issturdy()
False
>>> card.punch(0, 1).punch(0, 4).punch(3, 5)
PunchCard(4, 8, [(0, 1), (0, 4), (0, 5), (1, 2), (2, 7), (3, 5), (3, 6)])
>>> print(card)
#O##OO##
##O#####
#######O
#####OO#
>>> card.issturdy()
False
>>> card.punch(5, 10)
Traceback (most recent call last):
AssertionError: invalid position
>>> card.punch(0, 4)
Traceback (most recent call last):
AssertionError: card already punched at position (0, 4)
>>> card.fill(2, 7)
PunchCard(4, 8, [(0, 1), (0, 4), (0, 5), (1, 2), (3, 5), (3, 6)])
>>> print(card)
#O##OO##
##O#####
########
#####OO#
>>> card.issturdy()
False
>>> card.fill(1, 2).fill(3, 5).fill(0, 4)
PunchCard(4, 8, [(0, 1), (0, 5), (3, 6)])
>>> print(card)
#O###O##
########
########
######O#
>>> card.issturdy()
True
>>> card.fill(5, 10)
Traceback (most recent call last):
AssertionError: invalid position
>>> card.fill(0, 4)
Traceback (most recent call last):
AssertionError: card not punched at position (0, 4)
>>> PunchCard(4, 8, [(0, 1), (0, 5), (1, 2)]) == PunchCard(4, 8, ((1, 2), (0, 1), (0, 5)))
True
>>> PunchCard(4, 8, [(0, 1), (0, 5), (1, 2)]) == PunchCard(4, 8, [(3, 5), (1, 2), (3, 6), (0, 4)])
False
>>> PunchCard(4, 8, [(0, 1), (0, 5), (1, 2)]) == PunchCard(5, 9, ((1, 2), (0, 1), (0, 5)))
False
>>> PunchCard(4, 8, [(0, 1), (0, 5), (1, 2)]) + PunchCard(4, 8, [(3, 5), (1, 2), (3, 6), (0, 4)])
PunchCard(4, 8, [(1, 2)])
>>> print(PunchCard(4, 8, [(0, 1), (0, 5), (1, 2)]) + PunchCard(4, 8, [(3, 5), (1, 2), (3, 6), (0, 4)]))
########
##O#####
########
########
>>> PunchCard(4, 8, [(0, 1), (0, 5), (1, 2)]) + PunchCard(5, 9, [(3, 5), (1, 2), (3, 6), (0, 4)])
Traceback (most recent call last):
AssertionError: punch cards are not the same size