Haskell IO,在同一行获取两个输入并进行验证

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

我是 Haskell 的初学者,我正在开发一款小型 TicTacToe 游戏。这是我用来让玩家输入符号坐标的函数,比如 2 2 (这意味着符号放置在中心),他们想要放入.我想在上面添加一些验证功能。到目前为止,它只能处理超出范围的输入,如 12 2,并避免覆盖已占用的网格。但我想做更多。例如,2(只有 1 个输入),1 2 xy(xy 不应该在这里),以及 abcde(随机输入没有意义)。我想让程序也能处理这些无效输入。

作为一般性建议,如果我们将验证与用户交互分开,通常会更简洁。我们可以为验证结果使用自定义类型。

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

使用上面的内容,我们可以定义一个自定义函数来进行验证。 (下面,为了简单起见,我利用了 Text.Read.readMaybe,但是 Prelude 中的 reads 也可以在稍作改动后使用。)

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

以上利用了我们在下面定义的自定义网格访问函数:

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

之后,我们可以在执行用户交互时利用我们的验证:

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