-- runghc Sudoku.hs http://localhost:3000 -- GHC bundled libraries + curl; no Hackage packages required. -- x = column, y = row (0–8); n = value (1–9); board game !! y !! x. -- Functional client: use the Game returned by move. module Main where import Data.Char (isSpace) import System.Environment (getArgs) import System.Exit (ExitCode(..)) import System.Process (readProcessWithExitCode) import Text.ParserCombinators.ReadP -- A local snapshot of a server-owned game. data Game = Game { gameId :: String, board :: [[Int]], baseUrl :: String } deriving Show request :: String -> String -> String -> Maybe String -> IO String request base path method body = do let root = reverse (dropWhile (== '/') (reverse base)) args = ["--silent", "--show-error", "--fail-with-body", "--max-time", "10", "--request", method, "--header", "Content-Type: application/json"] ++ maybe [] (\value -> ["--data-raw", value]) body ++ ["--url", root ++ path] (code, output, err) <- readProcessWithExitCode "curl" args "" case code of ExitSuccess -> pure output ExitFailure n -> ioError (userError ("curl " ++ show n ++ ": " ++ err ++ output)) -- The API returns single-field objects: {"id": "UUID"} or {"board": [[...]]}. -- UUID strings and integer arrays share their syntax with Haskell Read. field :: Read a => String -> String -> IO a field name input = case [value | (value, "") <- readP_to_S parser input] of [value] -> pure value _ -> ioError (userError ("Unexpected API response: " ++ input)) where ws = skipMany (satisfy isSpace) parser = do ws; _ <- char '{'; ws; _ <- string (show name); ws; _ <- char ':'; ws value <- readS_to_P reads ws; _ <- char '}'; ws; eof pure value newGame :: IO Game newGame = newGameAt "http://localhost:3000" newGameAt :: String -> IO Game newGameAt base = do ident <- request base "/board" "POST" Nothing >>= field "id" grid <- request base ("/board/" ++ ident) "GET" Nothing >>= field "board" pure (Game ident grid base) move :: Game -> Int -> Int -> Int -> IO Game move game x y n | x < 0 || x > 8 || y < 0 || y > 8 || n < 1 || n > 9 = ioError (userError "x and y must be 0–8; n must be 1–9") | otherwise = do let payload = "{\"row\":" ++ show y ++ ",\"col\":" ++ show x ++ ",\"value\":" ++ show n ++ "}" grid <- request (baseUrl game) ("/board/" ++ gameId game ++ "/turn") "PATCH" (Just payload) >>= field "board" pure game { board = grid } main :: IO () main = do args <- getArgs game <- case args of base : _ -> newGameAt base [] -> newGame updated <- move game 2 1 5 print (board updated)