packages feed

minisat-solver-0.1: examples/Sudoku.hs

-- Copyright (C) 2016 Peter Selinger.
--
-- This file is free software and may be distributed under the terms
-- of the MIT license. Please see the file LICENSE for details.

-- | A program to solve Sudoku puzzles. It illustrates how to use the
-- MiniSat solver.

module Main where

import SAT.MiniSat

import qualified Data.Map as Map
import Data.Map (Map)
import Data.List
import Data.Char
import System.IO
import System.Environment

-- ----------------------------------------------------------------------
-- * The rules of Sudoku as a SAT problem

-- | 'Cell' /i/ /j/ /n/ is a boolean variable expressing that the cell
-- at row /i/ and column /j/ contains the number /n/.
data Cell = Cell Int Int Int
            deriving (Eq, Ord, Show)

-- | Like the 'Cell' constructor, but return a formula instead of a
-- variable. For convenience.
cell :: Int -> Int -> Int -> Formula Cell
cell i j n = Var (Cell i j n)

-- | The general-purpose Sudoku rules. They state that each cell 
-- contain exactly one number, and each number occurs exactly once in
-- each row, column, and 3x3 square.
rules :: Formula Cell
rules = cells :&&: rows :&&: columns :&&: squares
  where
    cells = All [ ExactlyOne [ cell i j n | n <- [1..9] ] | i <- [1..9], j <- [1..9] ]
    rows = All [ ExactlyOne [ cell i j n | j <- [1..9] ] | i <- [1..9], n <- [1..9] ]
    columns = All [ ExactlyOne [ cell i j n | i <- [1..9] ] | j <- [1..9], n <- [1..9] ]
    squares = All [ ExactlyOne [ cell i j n | i <- [i'..i'+2], j <- [j'..j'+2] ] | i' <- [1,4,7], j' <- [1,4,7], n <- [1..9] ]

-- ----------------------------------------------------------------------
-- * Sudoku solver

-- | A datatype for a partially filled Sudoku.
type Sudoku = Map (Int, Int) Int

-- | Turn a 'Sudoku' into a formula.
formula_of_sudoku :: Sudoku -> Formula Cell
formula_of_sudoku s = All [ cell i j n | ((i,j),n) <- Map.toList s ]

-- | Turn a solution into a 'Sudoku'.
sudoku_of_solution :: Map Cell Bool -> Sudoku
sudoku_of_solution m = Map.fromList [ ((i,j),n) | (Cell i j n, True) <- Map.toList m ] 

-- | Solve the Sudoku. Return all solutions.
solve_sudoku :: Sudoku -> [Sudoku]
solve_sudoku s = map sudoku_of_solution (solve_all (rules :&&: formula_of_sudoku s))

-- ----------------------------------------------------------------------
-- * Pretty-printing

-- | Print a 'Sudoku'.
show_sudoku :: Sudoku -> String
show_sudoku s =
  divider
  ++ concat [ row i | i <- [1,2,3] ] 
  ++ divider
  ++ concat [ row i | i <- [4,5,6] ]
  ++ divider
  ++ concat [ row i | i <- [7,8,9] ]
  ++ divider
  where
    divider = "+-------+-------+-------+\n"
    row i =
      "| "
      ++ intercalate " " [ entry i j | j <- [1, 2, 3]]
      ++ " | "
      ++ intercalate " " [ entry i j | j <- [4, 5, 6]]
      ++ " | "
      ++ intercalate " " [ entry i j | j <- [7, 8, 9]]
      ++ " |\n"
    entry i j = case Map.lookup (i,j) s of
      Nothing -> " "
      Just n -> show n

-- ----------------------------------------------------------------------
-- * Parsing

-- | Create a Sudoku from a list of 81 numbers (using 0 as blank).
sudoku_of_list :: [Int] -> Sudoku
sudoku_of_list xs =
  Map.fromList [ ((i,j),n) | ((i,j),n) <- zip coords xs, 1 <= n && n <= 9 ]
  where
    coords = [ (i,j) | i <- [1..9], j <- [1..9] ]

-- | Read a Sudoku from a string. This accepts the format produced by
-- 'show_sudoku', or a simple list of 81 numbers with 0 representing a
-- blank.
read_sudoku :: String -> Sudoku
read_sudoku s = sudoku_of_list (aux s)
  where
    aux [] = []
    aux (' ':' ':cs) = 0 : aux cs
    aux (c:cs)
      | '0' <= c && c <= '9' = (ord c - ord '0') : aux cs
      | otherwise = aux cs

-- ----------------------------------------------------------------------
-- * Main

-- | Print usage information.
usage :: IO ()
usage = do
  putStrLn "Usage: Sudoku [option]"
  putStrLn "Options:"
  putStrLn " -h, --help  - print usage info and exit"
  putStrLn " -p          - solve a predefined Sudoku (default)"
  putStrLn " -r          - read a Sudoku from stdin and solve"
  putStrLn ""
  putStrLn "The input can be specified in one of two formats:"
  putStrLn "Format 1:"
  putStrLn "+-------+-------+-------+"
  putStrLn "|       |   1 4 |   9   |"
  putStrLn "|   4 7 |     2 |     8 |"
  putStrLn "|   6   |     9 | 2     |"
  putStrLn "+-------+-------+-------+"
  putStrLn "|       |       | 7 6 9 |"
  putStrLn "| 7     |       |     3 |"
  putStrLn "| 5 8 6 |       |       |"
  putStrLn "+-------+-------+-------+"
  putStrLn "|     8 | 2     |   3   |"
  putStrLn "| 6     | 5     | 9 7   |"
  putStrLn "|   7   | 1 4   |       |"
  putStrLn "+-------+-------+-------+"
  putStrLn ""
  putStrLn "Format 2:"
  putStrLn "0 0 0 0 1 4 0 9 0"
  putStrLn "0 4 7 0 0 2 0 0 8"
  putStrLn "0 6 0 0 0 9 2 0 0"
  putStrLn "0 0 0 0 0 0 7 6 9"
  putStrLn "7 0 0 0 0 0 0 0 3"
  putStrLn "5 8 6 0 0 0 0 0 0"
  putStrLn "0 0 8 2 0 0 0 3 0"
  putStrLn "6 0 0 5 0 0 9 7 0"
  putStrLn "0 7 0 1 4 0 0 0 0"


-- | Make the sure the entire string is strictly read before
-- continuing the IO action.
strictly_read :: String -> IO ()
strictly_read [] = return ()
strictly_read (h:t) = strictly_read t

-- | A predefined Sudoku, so the user doesn't have to enter one.
predefined = sudoku_of_list
  [0,0,0,0,1,4,0,9,0,
   0,4,7,0,0,2,0,0,8,
   0,6,0,0,0,9,2,0,0,
   0,0,0,0,0,0,7,6,9,
   7,0,0,0,0,0,0,0,3,
   5,8,6,0,0,0,0,0,0,
   0,0,8,2,0,0,0,3,0,
   6,0,0,5,0,0,9,7,0,
   0,7,0,1,4,0,0,0,0]

-- | The main function.
main :: IO ()
main = do
  args <- getArgs
  case args of
   "--help" : _ -> usage
   "-h" : _ -> usage
   "-r" : _ -> do
     str <- getContents
     strictly_read str
     let s = read_sudoku str
     main_with s
   "-p" : _ -> main_with predefined
   [] -> main_with predefined
   o@('-':_) : _ -> do
     hPutStrLn stderr ("Unrecognized option -- " ++ o)
     hPutStrLn stderr "Try --help for more info."
   _ -> do
     hPutStrLn stderr "Invalid command line. Try --help for more info."

-- | Main-like function for solving the given Sudoku.
main_with :: Sudoku -> IO ()
main_with s = do
  putStrLn "Sudoku:"
  putStr (show_sudoku s)
  case solve_sudoku s of
   [] -> do
     putStrLn "No solution."
   [h] -> do
     putStrLn "Unique solution:"
     putStr (show_sudoku h)
   h:t -> do
     putStrLn "Non-unique solution:"
     putStr (show_sudoku h)
  return ()