A small haskell script to solve sudoku puzzles
git clone https://github.com/m-col/sudoku-solver
Files | Refs | Readme

-rwxr-xr-x sudoku.hs


      1 #!/usr/bin/env runhaskell
      2 
      3 {-# LANGUAGE TupleSections #-}
      4 
      5 {-
      6 sudoku-solver script. Copyright Matt Colligan 2022.
      7 -}
      8 
      9 import Control.Applicative (liftA2)
     10 import Data.Array (Array, Ix, assocs, elems, listArray, (!), (//))
     11 import Data.Bool (bool)
     12 import Data.Char (digitToInt)
     13 import Data.List (nub, sort)
     14 import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing, listToMaybe, mapMaybe)
     15 import System.Environment (getArgs)
     16 
     17 {-
     18 Screw dealing with IO when I can use trace. Sorry not sorry.
     19 -}
     20 import Debug.Trace
     21 
     22 puz p = trace (showPuzzle p) p
     23 
     24 type Puzzle = Array Index (Maybe Cell)
     25 
     26 {-
     27 This is the set of possible values, rows and columns.
     28 -}
     29 data Cell = One | Two | Three | Four | Five | Six | Seven | Eight | Nine
     30     deriving (Eq, Ord, Enum, Ix, Show)
     31 
     32 set :: [Cell]
     33 set = [One .. Nine]
     34 
     35 -- An index into the array is (row, col).
     36 type Index = (Cell, Cell)
     37 
     38 {-
     39 Is the given puzzle valid, considering only the cells that do have values? Possible
     40 improvement: don't test every single row/column/box when making a change.
     41 -}
     42 isValid :: Puzzle -> Bool
     43 isValid p = all (goodSet p) blocks
     44   where
     45     goodSet :: Puzzle -> [Index] -> Bool
     46     goodSet p = noRepeats . mapMaybe (p !)
     47 
     48     noRepeats cs = length cs == length (nub cs)
     49 
     50     blocks :: [[Index]]
     51     blocks = concat [rows, cols, boxes]
     52     rows = map (\r -> map (r,) set) set
     53     cols = map (\c -> map (,c) set) set
     54     boxes = [makeBox (r, c) | r <- [One, Four, Seven], c <- [One, Four, Seven]]
     55 
     56     makeBox :: Index -> [Index]
     57     makeBox (r, c) = [(r, c) | r <- [r .. succ . succ $ r], c <- [c .. succ . succ $ c]]
     58 
     59 {-
     60 Solve the puzzle!
     61 -}
     62 solve :: Puzzle -> Maybe Puzzle
     63 solve p = go [(p, (One, One))] (Just (One, One))
     64   where
     65     {-
     66     The first argument here is the stack of steps taken since the beginning. Each
     67     element of the list is the new state of the puzzle, and the index that was filled to
     68     get there. The second argument index is where we are going to be testing this
     69     iteration.
     70     -}
     71     go :: [(Puzzle, Index)] -> Maybe Index -> Maybe Puzzle
     72     -- We are finished when the maybe index is Nothing.
     73     go ((p, _) : _) Nothing = Just p
     74     go pps@((p, pi) : ps) (Just i)
     75         -- This index is already filled - continue.
     76         | isJust (p ! i) = go ((puz p, pi) : ps) (next i)
     77         -- This index is empty - let's find a possibly valid value.
     78         | otherwise =
     79             case getOptions p i of
     80                 [] -> Nothing
     81                 opts -> listToMaybe . mapMaybe (\o -> go ((puz o, i) : pps) (next i)) $ opts
     82 
     83     getOptions :: Puzzle -> Index -> [Puzzle]
     84     getOptions p i = filter isValid . map (\c -> p // [(i, Just c)]) $ set
     85 
     86     -- Maybe-get the next Index, left to right, then top to bottom
     87     next :: Index -> Maybe Index
     88     next (Nine, Nine) = Nothing
     89     next (row, Nine) = Just (succ row, One)
     90     next (row, col) = Just (row, succ col)
     91 
     92 {-
     93 Graphically represent the puzzle layout.
     94 -}
     95 showPuzzle :: Puzzle -> String
     96 showPuzzle = (<>) "\ESC[2J " . unwords . map organise . assocs . fmap render
     97   where
     98     render Nothing = " "
     99     render (Just v) = show . (+) 1 . fromEnum $ v
    100 
    101     -- Check out this ugly stuff. It adds bars and newlines to render the puzzle.
    102     organise ((_, Three), c) = c <> " |"
    103     organise ((_, Six), c) = c <> " |"
    104     organise ((Three, Nine), c) = c <> "\n " <> replicate 21 '-' <> "\n"
    105     organise ((Six, Nine), c) = c <> "\n " <> replicate 21 '-' <> "\n"
    106     organise ((_, Nine), c) = c <> "\n"
    107     organise (_, c) = c <> ""
    108 
    109 {-
    110 Load a puzzle from file.
    111 -}
    112 loadPuzzle :: [Char] -> Puzzle
    113 loadPuzzle = asArray . zip indices . map parse . filter (`elem` chars)
    114   where
    115     chars = [' '] <> ['1' .. '9']
    116     indices = [(r, c) | r <- set, c <- set]
    117 
    118     parse :: Char -> Maybe Cell
    119     parse ' ' = Nothing
    120     parse char = Just . toEnum . subtract 1 . digitToInt $ char
    121 
    122     asArray :: [(Index, Maybe Cell)] -> Puzzle
    123     asArray vals = listArray ((One, One), (Nine, Nine)) (repeat Nothing) // vals
    124 
    125 main :: IO ()
    126 main = do
    127     args <- getArgs
    128     let file = bool (head args) "puzzle_1.txt" (null args)
    129     solved <- solve . loadPuzzle <$> readFile file
    130     putStrLn $
    131         if isNothing solved
    132             then "Failed! Is the puzzle valid?"
    133             else "Completed!"
    134