-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