In the Japanese logic puzzle Akari, you're presented with a grid of black and white squares.

akari puzzle
An Akari puzzle with a $$6 \times 6$$ grid.

The goal is to place light bulbs into white cells such that no two bulbs shine on one another and such that the whole grid is illuminated.

akari solution
The unique solution of the Akari puzzle with a $$6 \times 6$$ grid.

Each light bulb sends out rays of light horizontally and vertically, illuminating its row and column unless a black cell blocks the rays.

light bulb
A light bulb sends out rays of light horizontally and vertically, illuminating its row and column unless a black cell blocks the rays.

Assignment

To refer to the cells in the rectangular grid of an Akari puzzle, the rows are numbered from top to bottom, and the columns from left to right, starting from zero. As such, the position of a cell can be represented as a tuple $$(r, c)$$, where $$r \in \mathbb{N}$$ (int) indicates the row number and $$c \in \mathbb{N}$$ (int) the column number.

Define a class Akari that can be used to represent the grid of Akari puzzles. Three arguments must be passed when creating an Akari puzzle (Akari): i) the number of rows (int) in the grid, ii) the number of columns (int) in the grid, and iii) a collection (list, tuple or set) containing the positions of all black cells in the grid. Each Akari puzzle (Akari) must have at least the following properties:

In addition, Akari puzzles (Akari) must also support at least the following methods:

If an Akari puzzle (Akari) is passed to the built-in function str, a string representation (str) of the grid must be returned. Each row forms a separate line, black cells are represented by a hash sign (#), white cells with a light bulb by an uppercase letter L, white cells that are illuminated by at least one light bulb by an asterisk (*) and all other white cells by an underscore (_).

Example

>>> puzzle = Akari(6, 6, [(1, 1), (2, 3), (2, 5), (3, 0), (3, 2), (4, 4)])
>>> puzzle.black_cells
{(3, 2), (3, 0), (4, 4), (2, 3), (2, 5), (1, 1)}
>>> puzzle.light_bulbs
set()
>>> puzzle.illuminated()
set()
>>> print(puzzle)
______
_#____
___#_#
#_#___
____#_
______
>>> puzzle.issolved()
False
>>> puzzle.rays(1, 1)
{(0, 1), (1, 2), (1, 3), (3, 1), (2, 1), (1, 4), (1, 5), (5, 1), (1, 0), (4, 1)}
>>> puzzle.rays(2, 1)
{(3, 1), (2, 0), (2, 2), (5, 1), (4, 1)}
>>> puzzle.add_light_bulb(2, 1)
>>> puzzle.light_bulbs
{(2, 1)}
>>> puzzle.illuminated()
{(2, 0), (2, 2), (5, 1), (3, 1), (4, 1)}
>>> print(puzzle)
______
_#____
*L*#_#
#*#___
_*__#_
_*____
>>> puzzle.issolved()
False
>>> puzzle.add_light_bulb(3, 6)      # outside grid
Traceback (most recent call last):
AssertionError: invalid position
>>> puzzle.add_light_bulb(1, 1)      # not on white cell
Traceback (most recent call last):
AssertionError: invalid position
>>> puzzle.add_light_bulb(2, 1)      # cell already contains a light bulb
Traceback (most recent call last):
AssertionError: invalid position
>>> puzzle.add_light_bulb(5, 1)      # cell is illuminated by another light bulb
Traceback (most recent call last):
AssertionError: invalid position
>>> puzzle.add_light_bulb(0, 1)
>>> puzzle.add_light_bulb(1, 0)
>>> puzzle.add_light_bulb(1, 2)
>>> puzzle.light_bulbs
{(0, 1), (1, 0), (2, 1), (1, 2)}
>>> puzzle.illuminated()
{(0, 0), (1, 3), (2, 2), (3, 1), (1, 4), (2, 0), (1, 5), (0, 5), (0, 4), (5, 1), (0, 3), (4, 1), (0, 2)}
>>> print(puzzle)
*L****
L#L***
*L*#_#
#*#___
_*__#_
_*____
>>> puzzle.issolved()
False
>>> puzzle.remove_light_bulb(1, 2)
>>> puzzle.light_bulbs
{(0, 1), (1, 0), (2, 1)}
>>> print(puzzle)
*L****
L#____
*L*#_#
#*#___
_*__#_
_*____
>>> puzzle.add_light_bulb(1, 2)
>>> print(puzzle)
*L****
L#L***
*L*#_#
#*#___
_*__#_
_*____
>>> puzzle.remove_light_bulb(2, 2)
Traceback (most recent call last):
AssertionError: invalid position
>>> puzzle.add_light_bulb(2, 4)
>>> puzzle.add_light_bulb(3, 3)
>>> puzzle.add_light_bulb(4, 0)
>>> puzzle.add_light_bulb(5, 5)
>>> puzzle.light_bulbs
{(0, 1), (1, 2), (3, 3), (5, 5), (2, 1), (1, 0), (2, 4), (4, 0)}
>>> puzzle.illuminated()
{(5, 4), (0, 0), (1, 3), (4, 1), (4, 5), (5, 2), (1, 4), (0, 5), (5, 1), (0, 3), (4, 2), (5, 3), (3, 5), (3, 1), (1, 5), (2, 0), (0, 4), (4, 3), (2, 2), (5, 0), (3, 4), (0, 2)}
>>> print(puzzle)
*L****
L#L***
*L*#L#
#*#L**
L***#*
*****L
>>> puzzle.issolved()
True

Epilogue

In an extension of the puzzle, some black cells may have a number on it from 0 to 4, indicating how many bulbs must be placed adjacent to its four sides. For example, a cell with a 4 must have four bulbs around it, one on each side, and a cell with a 0 cannot have a bulb next to any of its sides. An unnumbered black cell may have any number of light bulbs adjacent to it, or none. Bulbs placed diagonally adjacent to a numbered cell do not contribute to the bulb count.

Here's a moderately difficult puzzle that makes use of this extension. Can you solve it?

akari
A moderately difficult Akari puzzle.
akari
Solution of the moderately difficult Akari puzzle.