# 0h h1 Solver. Solves grids of 0h h1 game. # Copyright (C) 2015 Gabriel Augendre # # 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 . __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))