Skip to content
1 change: 1 addition & 0 deletions .obelisk/impl/.attr-cache/command.out
33 changes: 33 additions & 0 deletions src/Display.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
module Display (printBoard, displayHelp) where

import GameLogic (Board, Player(..))

printBoard :: Board -> IO ()
printBoard board = putStrLn $ unlines $ map (unwords . map cellToChar) board
where
cellToChar Nothing = "."
cellToChar (Just X) = "X"
cellToChar (Just O) = "O"

displayHelp :: IO ()
displayHelp = do
putStrLn "Here are the instructions to play the game:"
putStrLn ""
putStrLn "1. How to start the game:"
putStrLn " - Select start"
putStrLn "2. How to make a move:"
putStrLn " - The board is represented as a 3x3 grid with numbered positions:"
putStrLn " 1 | 2 | 3"
putStrLn " ---+---+---"
putStrLn " 4 | 5 | 6"
putStrLn " ---+---+---"
putStrLn " 7 | 8 | 9"
putStrLn " - Enter the number corresponding to the position where you want to place your mark (X or O)."
putStrLn " - For instance when Player X provides 1 1 the first cell will be occupied as follows:"
putStrLn " X | | "
putStrLn " ---+---+---"
putStrLn " | | "
putStrLn " ---+---+---"
putStrLn " | | "
putStrLn ""
putStrLn "Enjoy the game! May the best player win!"
69 changes: 69 additions & 0 deletions src/GameLogic.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
module GameLogic where

data Player = X | O deriving (Eq, Show)
type Position = (Int, Int)
type Board = [[Maybe Player]]

data GameState = InProgress | Draw | Won Player deriving (Show)
data Move = Move Player Position deriving (Show)

data Game = Game {
board :: Board,
currentPlayer :: Player,
state :: GameState
} deriving (Show)

emptyBoard :: Board
emptyBoard = replicate 3 (replicate 3 Nothing)

newGame :: Game
newGame = Game { board = emptyBoard, currentPlayer = X, state = InProgress }

makeMove :: Game -> Move -> Either String Game
makeMove game (Move player (row, col))
| row < 0 || row >= 3 || col < 0 || col >= 3 = Left "Move out of bounds. Try again."
| board game !! row !! col /= Nothing = Left "Cell already occupied. Try again."
| otherwise = Right $ game {
board = updatedBoard,
currentPlayer = nextPlayer (currentPlayer game),
state = newState
}
where
updatedBoard = take row (board game)
++ [take col (board game !! row) ++ [Just player] ++ drop (col + 1) (board game !! row)]
++ drop (row + 1) (board game)
newState = if checkWin updatedBoard player then Won player
else if isDraw updatedBoard then Draw
else InProgress

checkWin :: Board -> Player -> Bool
checkWin board player = any (all (== Just player)) (board ++ transpose board ++ diagonals board)

isDraw :: Board -> Bool
isDraw board = all (/= Nothing) (concat board)

nextPlayer :: Player -> Player
nextPlayer X = O
nextPlayer O = X

diagonals :: Board -> [[Maybe Player]]
diagonals board = [mainDiagonal board, antiDiagonal board]

mainDiagonal :: Board -> [Maybe Player]
mainDiagonal board = [board !! i !! i | i <- [0..n-1]]
where n = length board

antiDiagonal :: Board -> [Maybe Player]
antiDiagonal board = [board !! i !! (n - i - 1) | i <- [0..n-1]]
where n = length board

transpose :: [[a]] -> [[a]]
transpose [] = []
transpose x
| all null x = []
| otherwise = map safeHead x : transpose (map safeTail x)
where
safeHead [] = error "Unexpected empty list in safeHead"
safeHead (y:_) = y
safeTail [] = []
safeTail (_:ys) = ys
63 changes: 57 additions & 6 deletions src/Main.hs
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
module Main where

import System.IO (hFlush, stdout)
import GameLogic
import Display
import Data.Char (isDigit)

main :: IO ()
main = do
putStrLn "Welcome to Tic Tac Toe!"
putStrLn "========================================"
putStrLn " Welcome to Tic Tac Toe game! "
putStrLn " Enjoy to the fullest!!!!! "
putStrLn "========================================"
loop

loop :: IO ()
loop :: IO()
loop = do
putStr "Enter command: "
putStr "Enter the command (start, help, exit): "
hFlush stdout
input <- getLine
isLooping <- handleInput input
Expand All @@ -19,8 +25,53 @@ loop = do

handleInput :: String -> IO Bool
handleInput "exit" = do
putStrLn "Goodbye!"
putStrLn "Goodbye see you next time!"
pure False
handleInput input = do
putStrLn $ "You entered: " ++ input
handleInput "help" = do
Display.displayHelp
pure True
handleInput "start" = do
gameLoop newGame
pure True
handleInput _ = do
putStrLn "Invalid command! Please enter 'start', 'help' or 'exit'."
pure True
gameLoop :: Game -> IO ()
gameLoop game = do
printBoard (board game)
case state game of
InProgress -> do
putStrLn $ "Player " ++ show (currentPlayer game) ++ ", make your move (row and column): "
hFlush stdout
input <- getLine
case parseMove input of
Just (r, c) ->
case makeMove game (Move (currentPlayer game) (r, c)) of
Left err -> putStrLn err >> gameLoop game
Right newGameState -> gameLoop newGameState
Nothing -> putStrLn "Invalid input! Enter a valid input(row and column between 1 to 3)." >> gameLoop game
Draw -> putStrLn "It's a draw!" >> restartGame
Won player -> do
putStrLn $ "Player " ++ show player ++ " wins!"
restartGame


restartGame :: IO ()
restartGame = do
putStrLn "Do you want to play again? (yes/no)"
hFlush stdout
response <- getLine
if response == "yes"
then gameLoop newGame
else if response == "no"
then putStrLn "Thanks for playing, always enjoy to play anytime!"
else do
putStrLn "Invalid choice. Please type 'yes' or 'no'."
restartGame

parseMove :: String -> Maybe Position
parseMove input = case words input of
[r, c] -> if all isDigit r && all isDigit c
then Just (read r - 1, read c - 1)
else Nothing
_ -> Nothing
2 changes: 2 additions & 0 deletions tic-tac-toe.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,7 @@ cabal-version: >=1.10
executable tic-tac-toe
main-is: Main.hs
hs-source-dirs: src
other-modules: Display
GameLogic
default-language: Haskell2010
build-depends: base >=4.7 && <5