Ввод-вывод Haskell, получение двух входных данных в одной строке и выполнение проверки

#validation #parsing #haskell #types #io

#валидация #синтаксический анализ #хаскелл #типы #io

Вопрос:

 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 MOVEn"
    else putStr "X MOVEn" -- 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) amp;amp; (grid !! 1 == sym) amp;amp; (grid !! 2 == sym)
    then True
    else if (grid !! 3 == sym) amp;amp; (grid !! 4 == sym) amp;amp; (grid !! 5 == sym)
      then True
      else if (grid !! 6 == sym) amp;amp; (grid !! 7 == sym) amp;amp; (grid !! 8 == sym)
      then True
        else if (grid !! 0 == sym) amp;amp; (grid !! 3 == sym) amp;amp; (grid !! 6 == sym)
          then True
          else if (grid !! 1 == sym) amp;amp; (grid !! 4 == sym) amp;amp; (grid !! 7 == sym)
            then True
            else if (grid !! 2 == sym) amp;amp; (grid !! 5 == sym) amp;amp; (grid !! 8 == sym)
              then True
              else if (grid !! 0 == sym) amp;amp; (grid !! 4 == sym) amp;amp; (grid !! 8 == sym)
                then True
                else if (grid !! 2 == sym) amp;amp; (grid !! 4 == sym) amp;amp; (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 и работаю над небольшой игрой в крестики-нолики. Это функция, которую я использовал, чтобы заставить игрока ввести координату символа, например 2 2 (это будет означать, что символ, помещенный в центр), в который он хочет вставить. И я хочу добавить к нему некоторую функцию проверки. К настоящему времени он может обрабатывать только входы вне диапазона, такие как 12 2, и избегать перезаписи занятых сеток. Но я хочу сделать больше. Например, 2 (только 1 ввод), 1 2 xy (xy здесь не должно быть) и abcde (случайный ввод, не имеющий смысла). Я хочу, чтобы программа также могла обрабатывать эти недопустимые входные данные.

Комментарии:

1. Можете ли вы включить свои другие функции ( output_terminate , output_grid , main , и check_win ), чтобы мы могли запустить программу? Это очень помогло бы при тестировании.

2. 1. return означает что-то отличное от большинства других языков, оно не завершается досрочно. 2. Анализируйте, не проверяйте. lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate

3. Я обновил код с помощью других функций

Ответ №1:

В качестве общего предложения обычно чище, если мы отделяем проверку от взаимодействия с пользователем. Мы могли бы использовать пользовательский тип для результата проверки.

 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 для простоты, но reads from Prelude также можно использовать с незначительными изменениями.)

 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