Search code examples
validationparsinghaskelltypesio

Haskell IO, getting two input in the same line and doing validation


module TicTacToe (tictactoe) where

import Control.Applicative
import Control.Monad
import Control.Monad.State

import Data.Char
import Data.List

import Text.Printf

tictactoe :: IO ()
tictactoe = do
  let grid = [' ',' ',' ',' ',' ',' ',' ',' ',' ']
  let count = 0
  output_grid grid count
  
  
output_grid :: String -> Int -> IO()
output_grid grid count = do
  putStr ".---.---.---.\n"
  printf "| %c | %c | %c |\n" (grid !! 0) (grid !! 1) (grid !! 2)
  putStr ".---.---.---.\n"
  printf "| %c | %c | %c |\n" (grid !! 3) (grid !! 4) (grid !! 5)
  putStr ".---.---.---.\n"
  printf "| %c | %c | %c |\n" (grid !! 6) (grid !! 7) (grid !! 8)
  putStr ".---.---.---.\n" -- output grid
  if count `mod` 2 == 0 
    then putStr "O MOVE\n"
    else putStr "X MOVE\n" -- tell player which to move
  if count `mod` 2 == 0
    then player_input grid 'O' count
    else player_input grid 'X' count


player_input :: String -> Char -> Int -> IO()
player_input grid sym count = do
  inp <- getLine
  let x = (read (takeWhile (/= ' ') inp) :: Int)
  let y = (read (drop 1 (dropWhile (/= ' ') inp)) :: Int)
  if (x < 1) || (x > 3) || (y < 1) || (y > 3)
    then putStr "INVALID POSITION \n"
    else return ()
  if (x < 1) || (x > 3) || (y < 1) || (y > 3)
    then player_input grid sym count
    else return ()
  let target = (x - 1) * 3 + (y - 1)
  if (grid !! target /= ' ')
    then putStr "INVALID POSITION \n"
    else return ()
  if (grid !! target /= ' ')
    then player_input grid sym count
    else return ()
  let new_grid = (take target grid) ++ [sym] ++ (drop (target + 1) grid)
  if (check_win new_grid sym)
    then output_terminate new_grid sym
    else if count == 8
      then output_terminate new_grid 'D'
      else output_grid new_grid (count + 1)


  
check_win :: String -> Char -> Bool
check_win grid sym = do
  if (grid !! 0 == sym) && (grid !! 1 == sym) && (grid !! 2 == sym)
    then True
    else if (grid !! 3 == sym) && (grid !! 4 == sym) && (grid !! 5 == sym)
      then True
      else if (grid !! 6 == sym) && (grid !! 7 == sym) && (grid !! 8 == sym)
      then True
        else if (grid !! 0 == sym) && (grid !! 3 == sym) && (grid !! 6 == sym)
          then True
          else if (grid !! 1 == sym) && (grid !! 4 == sym) && (grid !! 7 == sym)
            then True
            else if (grid !! 2 == sym) && (grid !! 5 == sym) && (grid !! 8 == sym)
              then True
              else if (grid !! 0 == sym) && (grid !! 4 == sym) && (grid !! 8 == sym)
                then True
                else if (grid !! 2 == sym) && (grid !! 4 == sym) && (grid !! 6 == sym)
                  then True
                  else False


output_terminate :: String -> Char -> IO()
output_terminate grid winner = do
  putStr ".---.---.---.\n"
  printf "| %c | %c | %c |\n" (grid !! 0) (grid !! 1) (grid !! 2)
  putStr ".---.---.---.\n"
  printf "| %c | %c | %c |\n" (grid !! 3) (grid !! 4) (grid !! 5)
  putStr ".---.---.---.\n"
  printf "| %c | %c | %c |\n" (grid !! 6) (grid !! 7) (grid !! 8)
  putStr ".---.---.---.\n"
  if winner == 'D'
    then putStr "DRAW \n"
    else printf "%c WINS \n" winner

I am a beginner in Haskell and I am working on a small TicTacToe game. This is the function that I used to get the player to input the coordinate of the symbol, like 2 2 (this will means that the symbol placing at the centre), they want to put into. And I want to add some validation feature on it. By now, it can only handle out of range inputs like 12 2, and avoiding overwriting the occupied grids. But I want to do more. For example, 2 (only 1 input), 1 2 xy (xy not supposed to be here), and abcde (random input not making sense). I want to make the program also able to handle these invalid input.


Solution

  • As a general suggestion, it's usually cleaner if we separate validation from user interaction. We could use a custom type for the validation result.

    data Validation
      = CorrectMove Int Int      -- correct input
      | OutOfBounds              -- off the board
      | NonEmpty                 -- can not play on the same cell twice
      | ParseError               -- input is not two integers
    

    Using the above, we can define a custom function for validation. (Below, I exploit Text.Read.readMaybe for simplicity, but reads from Prelude can also be used with minor changes.)

    import Text.Read (readMaybe)
    
    validate 
       :: String         -- ^ the user input
       -> String         -- ^ the grid (should be its own type)
       -> Validation
    validate input grid = case words input of
       [xStr, yStr] -> -- two words, let's parse them
          case (readMaybe xStr, readMaybe yStr) of
             (Just x, Just y)
                | x < 1 || x > 3 || y < 1 || y > 3 -> OutOfBounds
                | cell grid x y /= ' '             -> NotEmpty
                | otherwise                        -> CorrectMove x y
             _ -> ParseError -- two words, but not two integers
       _ -> ParseError  -- not two words
    

    The above exploits a custom grid-accessing function which we define below:

    -- coordinates must be in-bounds
    cell :: String -> Int -> Int -> Char
    cell grid x y = grid !! ((x - 1) * 3 + y - 1)
    

    After that, we can exploit our validation when we perform user interaction:

    player_input :: String -> Char -> Int -> IO()
    player_input grid sym count = do
      inp <- getLine
      case validate inp grid of
         ParseError  -> putStrLn "Invalid input!"
         NonEmpty    -> putStrLn "Cell not empty!"
         OutOfBounds -> putStrLn "Invalid coordinates!"
         CorrectMove x y -> do
            putStrLn $ "ValidMove in cell " ++ show (x,y)
            -- here we can use x and y, knowing they are valid
            -- and update the game state