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.
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