Skip to content

Commit

Permalink
Added the sudoku example
Browse files Browse the repository at this point in the history
  • Loading branch information
yuce committed Oct 19, 2024
1 parent 660fa89 commit ea1f1df
Show file tree
Hide file tree
Showing 9 changed files with 218 additions and 12 deletions.
1 change: 0 additions & 1 deletion examples/father.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 *


Expand Down
1 change: 0 additions & 1 deletion examples/register_foreign_simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 1 addition & 3 deletions examples/sudoku/sudoku.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
11 changes: 4 additions & 7 deletions examples/sudoku/sudoku_daily.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand All @@ -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)
Expand Down
Empty file added src/pyswip/examples/__init__.py
Empty file.
26 changes: 26 additions & 0 deletions src/pyswip/examples/sudoku.pl
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@

% Prolog Sudoku Solver (C) 2007 Markus Triska ([email protected])
% 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).
132 changes: 132 additions & 0 deletions src/pyswip/examples/sudoku.py
Original file line number Diff line number Diff line change
@@ -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()
Empty file added tests/examples/__init__.py
Empty file.
55 changes: 55 additions & 0 deletions tests/examples/test_sudoku.py
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit ea1f1df

Please sign in to comment.