solver-0h-h1/sources/solver_0hh1.py

580 lines
18 KiB
Python

# 0h h1 Solver. Solves grids of 0h h1 game.
# Copyright (C) 2015 Gabriel Augendre <gabriel@augendre.info>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
__author__ = 'gaugendre'
import sys
def string_from_list(line):
"""
Makes a string from a line of squares.
:param line: A line (list) of squares to make a string from.
:type line: list
:return: A string containing all the states of the squares in the list.
:rtype: str
.. warning:: The items of the list must be squares or have an attribute
called 'state'.
"""
string = ""
for square in line:
string += square.state
return string
def non_space_element(line):
"""
Returns the number of non space characters in a string.
:param line: The line where to count characters.
:type line: str
:return: The number of non space characters.
:rtype: str
:Example:
>>> non_space_element('Ceci est un test')
13
"""
return len(line.replace(' ', ''))
class Square:
"""
Represents a square in the grid.
A square can be either Red, Blue, or Nothing, depending on the text
written in it and displayed ('R', 'B' or ' ').
"""
def __init__(self, grid, vert, horiz, state=' ', base=False):
self.horiz = horiz
self.vert = vert
self.switched = False
self.base = base
if not grid or isinstance(grid, Grid):
self.grid = grid
else:
print("Warning : Attribute grid not instance of Grid",
file=sys.stderr)
self.grid = None
if state in (' ', 'R', 'B'):
self._state = state
else:
print("Warning : Attribute state not in ('R', 'B', ' ')",
file=sys.stderr)
self._state = ' '
def next_horiz(self):
"""
A method to get the next square horizontally.
:return: The next square, horizontally.
:rtype: Square
.. warning:: The square must be part of a grid
"""
if not self.grid or self.horiz == self.grid.size - 1:
return None
return self.grid.square(self.horiz + 1, self.vert)
def prev_horiz(self):
"""
A method to get the previous square horizontally.
:return: The previous square, horizontally.
:rtype: Square
.. warning:: The square must be part of a grid
"""
if not self.grid or self.horiz == 0:
return None
return self.grid.square(self.horiz - 1, self.vert)
def next_vert(self):
"""
A method to get the next square vertically.
:return: The next square, vertically.
:rtype: Square
.. warning:: The square must be part of a grid
"""
if not self.grid or self.vert == self.grid.size - 1:
return None
return self.grid.square(self.horiz, self.vert + 1)
def prev_vert(self):
"""
A method to get the previous square vertically.
:return: The previous square, vertically.
:rtype: Square
.. warning:: The square must be part of a grid
"""
if not self.grid or self.vert == 0:
return None
return self.grid.square(self.horiz, self.vert - 1)
def __eq__(self, other):
if other is None or not isinstance(other, Square):
return False
else:
return self.__hash__() == other.__hash__()
def __hash__(self):
return hash((self.horiz, self.vert, self.grid))
def __repr__(self):
return "({}, {}) : '{}'".format(self.horiz, self.vert, self.state)
@property
def state(self):
"""
Allow to get square state.
:return: The square state. Either ' ', 'R' or 'B'
"""
return self._state
@state.setter
def state(self, new_state):
"""
Changes square state. Accepts only 'R', 'B', or ' '.
Other values are not accepted and the square is not modified.
"""
if new_state in ('R', 'B', ' '):
self._state = new_state
self.switched = True
else:
print("Error :", new_state, "not in ('R', 'B', ' ').")
def opposite_state(self):
"""
Returns the opposite state of the current Square.
The opposite state of 'R' is 'B', and vice-versa.
The opposite state of ' ' is ' '.
:return: The opposite state of the current square.
:rtype: str
:Example:
>>> Square(None, 0, 0, 'R').opposite_state()
'B'
>>> Square(None, 0, 0, ' ').opposite_state()
' '
"""
if self.state == 'R':
return 'B'
elif self.state == 'B':
return 'R'
else:
return ' '
def is_empty(self):
"""
Simply tells if the square contains nothing or not.
:return: True if the square contains ' ', else False.
:rtype: bool
:Example:
>>> Square(None, 0, 0, ' ').is_empty()
True
>>> Square(None, 0, 0, 'R').is_empty()
False
"""
return self.state == ' '
def all_prev_horiz(self):
"""
Get the list of all previous squares, horizontally.
:return: A list containing all the previous squares horizontally.
:rtype: list
.. seealso:: prev_horiz()
.. warning:: The square must be part of a grid.
"""
h_prev = self.prev_horiz()
all_prev_horiz_list = []
while h_prev:
all_prev_horiz_list.append(h_prev)
h_prev = h_prev.prev_horiz()
return all_prev_horiz_list
def all_next_horiz(self):
"""
Get the list of all next squares, horizontally.
:return: A list containing all the next squares horizontally.
:rtype: list
.. seealso:: next_horiz()
.. warning:: The square must be part of a grid.
"""
h_next = self.next_horiz()
all_next_horiz_list = []
while h_next:
all_next_horiz_list.append(h_next)
h_next = h_next.next_horiz()
return all_next_horiz_list
def all_prev_vert(self):
"""
Get the list of all previous squares, vertically.
:return: A list containing all the previous squares vertically.
:rtype: list
.. seealso:: prev_vert()
.. warning:: The square must be part of a grid.
"""
v_prev = self.prev_vert()
all_prev_vert_list = []
while v_prev:
all_prev_vert_list.append(v_prev)
v_prev = v_prev.prev_vert()
return all_prev_vert_list
def all_next_vert(self):
"""
Get the list of all next squares, vertically.
:return: A list containing all the next squares vertically.
:rtype: list
.. seealso:: next_vert()
.. warning:: The square must be part of a grid.
"""
v_next = self.next_vert()
all_next_vert_list = []
while v_next:
all_next_vert_list.append(v_next)
v_next = v_next.next_vert()
return all_next_vert_list
def same_line(self):
"""
List of squares in the same line.
Does not include the considered square.
:return: The list of the squares in the same line.
.. seealso:: all_prev_horiz(), all_next_horiz()
.. warning:: The square must be part of a grid.
"""
line_list = []
line_list.extend(self.all_prev_horiz())
line_list.append(self)
line_list.extend(self.all_next_horiz())
return line_list
def same_column(self):
"""
List of squares in the same column.
Does not include the considered square.
:return: The list of the squares in the same column.
.. seealso:: all_prev_vert(), all_next_vert()
.. warning:: The square must be part of a grid.
"""
line_list = []
line_list.extend(self.all_prev_vert())
line_list.append(self)
line_list.extend(self.all_next_vert())
return line_list
def solve_three_square(square):
"""
Prevent 'three in a row'.
Checks before and after the square if there are two squares of the
same color in order to prevent 'three in a row'.
:param square: The Square to check
:type square: Square
:return: A boolean : True if something has been done, else False.
:rtype: bool
"""
solved = False
if square.is_empty():
v_prev = square.prev_vert()
v_next = square.next_vert()
h_prev = square.prev_horiz()
h_next = square.next_horiz()
if not square.switched and v_prev and not v_prev.is_empty():
v_p_prev = v_prev.prev_vert()
if v_p_prev and v_p_prev.state == v_prev.state:
square.state = v_prev.opposite_state()
v_p_p_prev = v_p_prev.prev_vert()
if v_p_p_prev:
solve_three_square(v_p_p_prev)
elif v_next and v_next.state == v_prev.state:
square.state = v_prev.opposite_state()
if not square.switched and v_next and not v_next.is_empty():
v_n_next = v_next.next_vert()
if v_n_next and v_n_next.state == v_next.state:
square.state = v_next.opposite_state()
v_n_n_next = v_n_next.next_vert()
if v_n_n_next:
solve_three_square(v_n_n_next)
if not square.switched and h_prev and not h_prev.is_empty():
h_p_prev = h_prev.prev_horiz()
if h_p_prev and h_p_prev.state == h_prev.state:
square.state = h_prev.opposite_state()
h_p_p_prev = h_p_prev.prev_horiz()
if h_p_p_prev:
solve_three_square(h_p_p_prev)
elif h_next and h_next.state == h_prev.state:
square.state = h_prev.opposite_state()
if not square.switched and h_next and not h_next.is_empty():
h_n_next = h_next.next_horiz()
if h_n_next and h_n_next.state == h_next.state:
square.state = h_next.opposite_state()
h_n_n_next = h_n_next.next_horiz()
if h_n_n_next:
solve_three_square(h_n_n_next)
if square.switched:
solved = True
return solved
class Grid:
"""
A Grid is a square array containing Squares.
"""
def __init__(self, size, array=None):
"""
Instantiate a grid from a size and maybe an array of characters.
If an array is provided, the grid will be filled with squares
with state corresponding to the character in the array.
:param size: The size of the grid (either width or length).
:type size: int
:param array: The array used to fill the grid.
:type array: list
"""
_squares = []
square_list = []
squares_to_modify = []
i = 0
while i < size:
_squares.append([])
j = 0
while j < size:
if array:
value = array[i][j]
if value != ' ':
_squares[i].append(Square(self, i, j, value, True))
else:
_squares[i].append(Square(self, i, j))
squares_to_modify.append(_squares[i][j])
else:
_squares[i].append(Square(self, i, j))
squares_to_modify.append(_squares[i][j])
j += 1
square_list.extend(_squares[i])
i += 1
self._squares = _squares
self.size = size
self.square_list = square_list
self.squares_to_modify = squares_to_modify
@property
def squares(self):
"""
A method to get the squares in the grid.
:return: The squares in the grid.
:rtype: list
"""
return self._squares
@squares.setter
def squares(self, array):
"""
Replace the squares in the grid with the one provided in the array.
:param array: The array to replace the squares.
:type array: list
.. warning:: The array must be a list containing
lists of characters (square array).
"""
squares = []
i = 0
size = len(array)
while i < size:
squares.append([])
j = 0
while j < size:
squares[i].append(Square(self, i, j, array[i][j]))
j += 1
i += 1
self._squares = squares
def square(self, horiz, vert):
"""
Used to get a specific square in the grid.
:param horiz: The horizontal position of the square to get.
:type horiz: int
:param vert: The vertical position of the square to get.
:type vert: int
:return: The square at the given position
:rtype: Square
"""
return self._squares[vert][horiz]
def __repr__(self):
representation = ""
for line in self._squares:
for square in line:
to_print = square.state
if to_print == ' ':
to_print = '_'
representation += to_print + ' '
representation += "\n"
return representation
def squares_on_line(self, line_number):
"""
Returns the squares on a line specified by the number
(starting from zero).
:param line_number: The line to get.
:type line_number: int
:return: The list containing the squares on the required line.
:rtype: list
"""
return self.squares[line_number]
def squares_on_column(self, col_number):
"""
Returns the squares on a column specified by the number
(starting from zero).
:param col_number: The column to get.
:type col_number: int
:return: The list containing the squares on the required column.
:rtype: list
"""
col = []
for line in self.squares:
col.append(line[col_number])
return col
def solve_threes(self):
"""
Solves the grid recursively to prevent 'three in a row'.
:return: True if a square has been modified, else False.
:rtype: bool
"""
solved = False
for square in self.squares_to_modify:
if solve_three_square(square):
solved = True
return solved
def solve(self):
"""
Solves the grid using 'three in a row', 'same number of red and blue
on the same line or column' and, later, 'no identical line or column'.
"""
solved = True
while solved:
if not self.solve_threes():
solved = False
if self.solve_same_number():
solved = True
# self.solve_different_lines_or_columns()
def solve_same_number(self):
"""
Solves the grid implementing the fact that there is always the same
number of red and blue on the same line or column.
:return: True if a square has been modified, else False.
:rtype: bool
"""
solved = False
for square in self.square_list:
if square.is_empty():
same_line = square.same_line()
count_red = 0
count_blue = 0
for line_square in same_line:
if line_square.state == 'B':
count_blue += 1
elif line_square.state == 'R':
count_red += 1
if count_red == self.size / 2:
square.state = 'B'
elif count_blue == self.size / 2:
square.state = 'R'
if not square.switched:
same_column = square.same_column()
count_red = 0
count_blue = 0
for line_square in same_column:
if line_square.state == 'B':
count_blue += 1
elif line_square.state == 'R':
count_red += 1
if count_red == self.size / 2:
square.state = 'B'
elif count_blue == self.size / 2:
square.state = 'R'
if square.switched:
solved = True
return solved
def solve_different_lines_or_columns(self):
"""
Solves the grid implementing the fact that there isn't two identical
lines or columns.
:return: True if a square has been modified, else False.
:rtype: bool
.. warning:: Function still not finished. DOESN'T WORK.
"""
for square in self.square_list:
line = string_from_list(square.same_line())
col = string_from_list(square.same_column())
colored_line = line.replace(' ', '')
if len(colored_line) >= self.size - 2:
for i in range(0, self.size, 1):
if i != square.vert:
line_comp = string_from_list(self.squares_on_line(i))
col_comp = string_from_list(self.squares_on_column(i))