diff --git a/examples/father.py b/examples/father.py index 655b5c0..7ce5a96 100644 --- a/examples/father.py +++ b/examples/father.py @@ -21,7 +21,6 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from __future__ import print_function from pyswip import * diff --git a/examples/register_foreign_simple.py b/examples/register_foreign_simple.py index edb818e..31ee425 100644 --- a/examples/register_foreign_simple.py +++ b/examples/register_foreign_simple.py @@ -23,7 +23,6 @@ # Demonstrates registering a Python function as a Prolog predicate through SWI-Prolog's FFI. -from __future__ import print_function from pyswip.prolog import Prolog from pyswip.easy import registerForeign diff --git a/examples/sudoku/sudoku.py b/examples/sudoku/sudoku.py index 7108bfa..1efb67f 100644 --- a/examples/sudoku/sudoku.py +++ b/examples/sudoku/sudoku.py @@ -21,9 +21,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from __future__ import print_function from pyswip.prolog import Prolog -from pyswip.easy import * _ = 0 @@ -61,7 +59,7 @@ def pretty_print(table): def solve(problem): - prolog.consult("sudoku.pl") + prolog.consult("sudoku.pl", relative_to=__file__) p = str(problem).replace("0", "_") result = list(prolog.query("L=%s,sudoku(L)" % p, maxresult=1)) if result: diff --git a/examples/sudoku/sudoku_daily.py b/examples/sudoku/sudoku_daily.py index 3ebf852..c2908ea 100644 --- a/examples/sudoku/sudoku_daily.py +++ b/examples/sudoku/sudoku_daily.py @@ -25,14 +25,11 @@ # Sudoku auto-solver. Get today's sudoku at http://www.sudoku.org.uk/daily.asp # and solve it -from __future__ import print_function -from pyswip.prolog import Prolog -from pyswip.easy import * - from html.parser import HTMLParser - import urllib.request as urllib_request +from pyswip.prolog import Prolog + class DailySudokuPuzzle(HTMLParser): def __init__(self): @@ -75,7 +72,7 @@ def get_daily_sudoku(url): def solve(problem): - prolog.consult("sudoku.pl") + prolog.consult("sudoku.pl", relative_to=__file__) p = str(problem).replace("0", "_") result = list(prolog.query("Puzzle=%s,sudoku(Puzzle)" % p, maxresult=1)) if result: @@ -94,7 +91,7 @@ def solve(problem): print("-- PUZZLE --") pretty_print(puzzle) print() - print(" -- SOLUTION --") + print("-- SOLUTION --") solution = solve(puzzle) if solution: pretty_print(solution) diff --git a/src/pyswip/examples/__init__.py b/src/pyswip/examples/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/pyswip/examples/sudoku.pl b/src/pyswip/examples/sudoku.pl new file mode 100644 index 0000000..d572c20 --- /dev/null +++ b/src/pyswip/examples/sudoku.pl @@ -0,0 +1,26 @@ + +% Prolog Sudoku Solver (C) 2007 Markus Triska (triska@gmx.at) +% Public domain code. + +:- use_module(library(bounds)). + +% Pss is a list of lists representing the game board. + +sudoku(Pss) :- + flatten(Pss, Ps), + Ps in 1..9, + maplist(all_different, Pss), + Pss = [R1,R2,R3,R4,R5,R6,R7,R8,R9], + columns(R1, R2, R3, R4, R5, R6, R7, R8, R9), + blocks(R1, R2, R3), blocks(R4, R5, R6), blocks(R7, R8, R9), + label(Ps). + +columns([], [], [], [], [], [], [], [], []). +columns([A|As],[B|Bs],[C|Cs],[D|Ds],[E|Es],[F|Fs],[G|Gs],[H|Hs],[I|Is]) :- + all_different([A,B,C,D,E,F,G,H,I]), + columns(As, Bs, Cs, Ds, Es, Fs, Gs, Hs, Is). + +blocks([], [], []). +blocks([X1,X2,X3|R1], [X4,X5,X6|R2], [X7,X8,X9|R3]) :- + all_different([X1,X2,X3,X4,X5,X6,X7,X8,X9]), + blocks(R1, R2, R3). diff --git a/src/pyswip/examples/sudoku.py b/src/pyswip/examples/sudoku.py new file mode 100644 index 0000000..050e02a --- /dev/null +++ b/src/pyswip/examples/sudoku.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- + +# pyswip -- Python SWI-Prolog bridge +# Copyright (c) 2007-2024 YĆ¼ce Tekol +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import sys +from typing import List, Union, Literal +from io import StringIO + +from pyswip.prolog import Prolog + + +__all__ = "Matrix", "solve" + +DIMENSION = 9 + + +Prolog.consult("sudoku.pl", relative_to=__file__) + + +class Matrix: + + def __init__(self, matrix: List[List[int]]) -> None: + if not matrix: + raise ValueError("matrix must be given") + if len(matrix) != DIMENSION: + raise ValueError("Matrix dimension must be 9") + self._dimension = len(matrix) + self._validate(self._dimension, matrix) + self.matrix = matrix + + @classmethod + def from_text(cls, text: str) -> "Matrix": + lines = text.strip().split("\n") + dimension = len(lines) + rows = [] + for i, line in enumerate(lines): + cols = line.split() + if len(cols) != dimension: + raise ValueError(f"All rows must have {dimension} columns, line {i+1} has {len(cols)}") + rows.append([0 if x == "." else int(x) for x in cols]) + return cls(rows) + + @classmethod + def _validate(cls, dimension: int, matrix: List[List[int]]): + if len(matrix) != dimension: + raise ValueError(f"Matrix must have {dimension} rows, it has {len(matrix)}") + for i, row in enumerate(matrix): + if len(row) != dimension: + raise ValueError(f"All rows must have {dimension} columns, row {i+1} has {len(row)}") + + def __len__(self) -> int: + return self._dimension + + def __str__(self) -> str: + sio = StringIO() + self.pretty_print(file=sio) + return sio.getvalue() + + def __repr__(self) -> str: + return str(self.matrix) + + def pretty_print(self, *, file=sys.stdout) -> None: + for row in self.matrix: + row = " ".join(str(x or ".") for x in row) + print(row, file=file) + + +def solve(matrix: Matrix) -> Union[Matrix, Literal[False]]: + """ + Solves the given Sudoku puzzle + + Parameters: + matrix (Matrix): The matrix that contains the Sudoku puzzle + + Returns: + Matrix: Solution matrix + False: If no solutions was found + """ + p = repr(matrix).replace("0", "_") + result = list(Prolog.query(f"L={p},sudoku(L)", maxresult=1)) + if not result: + return False + result = result[0].get("L") + if not result: + return False + return Matrix(result) + + +def main(): + puzzle = Matrix.from_text(""" +. 6 . 1 . 4 . 5 . +. . 8 3 . 5 6 . . +2 . . . . . . . 1 +8 . . 4 . 7 . . 6 +. . 6 . . . 3 . . +7 . . 9 . 1 . . 4 +5 . . . . . . . 2 +. . 7 2 . 6 9 . . +. 4 . 5 . 8 . 7 . +""") + print("-- PUZZLE --") + puzzle.pretty_print() + print(" -- SOLUTION --") + solution = solve(puzzle) + if solution: + solution.pretty_print() + print(repr(solution)) + else: + print("This puzzle has no solutions. Is it valid?") + + +if __name__ == "__main__": + main() diff --git a/tests/examples/__init__.py b/tests/examples/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/examples/test_sudoku.py b/tests/examples/test_sudoku.py new file mode 100644 index 0000000..7f1fdec --- /dev/null +++ b/tests/examples/test_sudoku.py @@ -0,0 +1,55 @@ +import unittest + +from pyswip.examples.sudoku import Matrix, solve + + +class MatrixTestCase(unittest.TestCase): + + FIXTURE = """ +. 6 . 1 . 4 . 5 . +. . 8 3 . 5 6 . . +2 . . . . . . . 1 +8 . . 4 . 7 . . 6 +. . 6 . . . 3 . . +7 . . 9 . 1 . . 4 +5 . . . . . . . 2 +. . 7 2 . 6 9 . . +. 4 . 5 . 8 . 7 . + """ + + def test_matrix_from_text(self): + got = Matrix.from_text(self.FIXTURE) + target = [ + [0, 6, 0, 1, 0, 4, 0, 5, 0], + [0, 0, 8, 3, 0, 5, 6, 0, 0], + [2, 0, 0, 0, 0, 0, 0, 0, 1], + [8, 0, 0, 4, 0, 7, 0, 0, 6], + [0, 0, 6, 0, 0, 0, 3, 0, 0], + [7, 0, 0, 9, 0, 1, 0, 0, 4], + [5, 0, 0, 0, 0, 0, 0, 0, 2], + [0, 0, 7, 2, 0, 6, 9, 0, 0], + [0, 4, 0, 5, 0, 8, 0, 7, 0], + ] + self.assertListEqual(target, got.matrix) + + def test_solve_success(self): + puzzle = Matrix.from_text(self.FIXTURE) + solution = solve(puzzle) + target = [ + [9, 6, 3, 1, 7, 4, 2, 5, 8], + [1, 7, 8, 3, 2, 5, 6, 4, 9], + [2, 5, 4, 6, 8, 9, 7, 3, 1], + [8, 2, 1, 4, 3, 7, 5, 9, 6], + [4, 9, 6, 8, 5, 2, 3, 1, 7], + [7, 3, 5, 9, 6, 1, 8, 2, 4], + [5, 8, 9, 7, 1, 3, 4, 6, 2], + [3, 1, 7, 2, 4, 6, 9, 8, 5], + [6, 4, 2, 5, 9, 8, 1, 7, 3], + ] + self.assertEqual(target, solution.matrix) + + def test_solve_failure(self): + fixture = " 8" + self.FIXTURE[2:] + puzzle = Matrix.from_text(fixture) + solution = solve(puzzle) + self.assertFalse(solution)