PREVIOUS PART: Artificial Intelligence

Part 3B - Wrapping Up and Adding Interactivity

Now the UI shall change things!

We have all this fancy scaffolding, but no way to use it? That won’t do. Let’s first add a helper function to the UI code, to locate our clicks on the grid

-- Tries to locate the coordinates the mouse did click. If available, return Just it, otherwise Nothing
getClickTarget :: (Float, Float) -> Maybe Coordinate
getClickTarget (clickX, clickY)
    | dividedX < 0 || dividedX >= gameGridSize = Nothing -- Invalid coordinates
    | dividedY < 0 || dividedY >= gameGridSize = Nothing
    
    | otherwise = Just (dividedX, dividedY)
    
    where
        -- dividedX, dividedY should directly correspond to grid coordinates
        dividedX :: Int
        dividedX = (translatedClickX) `div` gridBoxSize -- Div is an integer division; Haskell is remarkably strict about types, so we need to explicitly accept the loss of precision associated
        dividedY :: Int
        dividedY = (gameGridSize-1) - (translatedClickY `div` gridBoxSize)
        translatedClickX :: Int
        translatedClickX = (round clickX) - centreAdjustmentX - gridAbsoluteLeftX
        translatedClickY :: Int
        translatedClickY = (round clickY) - centreAdjustmentY - gridAbsoluteLeftY

Also, remember the handleEvent and timerTick functions? It is now their time to shine! Replace those with these functions, which actually change the state of the world in response to events and timer ticks

handleEvent :: Event -> GameWorld -> GameWorld
handleEvent (EventKey (MouseButton RightButton) Down _ _) _ = initialWorld
handleEvent (EventKey (MouseButton LeftButton) Down _ clickPos) world
    -- Both players have passed, so the game's over
    | bothStalled world = world 
    --  AI plays this turn
    | elem (playerTurn world) (aiPlays) = world
    -- No valid position
    | Nothing <- possibleClickPos = world 
    -- We have a position,  evaluate it
    | Just coordinate <- possibleClickPos = evaluatePlayerTurn world coordinate
    where
        evaluatePlayerTurn :: GameWorld -> Coordinate -> GameWorld
        evaluatePlayerTurn wrld crd
            -- No valid moves
            | null (moveOnPoint) = wrld
            -- Apply a move and change the turn to the opposing player; also reset any pass counters and the ticker
            | otherwise = World (appliedBoard) (opposingPlayer (playerTurn wrld)) False False 0 
            where
                appliedBoard = applyMove (gameBoard wrld) (playerTurn wrld) moveOnPoint
                moveOnPoint = getMovesOnPoint (gameBoard wrld) (playerTurn wrld) crd 
        possibleClickPos = getClickTarget clickPos
-- Rest do not affect the world
handleEvent _ world = world 

timerTick :: Float -> GameWorld -> GameWorld
timerTick tick_diff (World board turn passedOnLast bothStalled curTicks)
    | tick_diff+curTicks < 1.0 = World board turn passedOnLast bothStalled (curTicks+tick_diff)
    | otherwise = evaluateTickTurn 
    where
        evaluateTickTurn = case () of _
                                         -- Both have stalled, do not do anything
                                        | bothStalled -> tickResetWorld
                                         -- Pass, unable to make a turn
                                        | null (movesAvailableForPlayer board turn) -> World board (opposingPlayer turn) True (passedOnLast) 0
                                         -- AI does not play this turn
                                        | notElem turn (aiPlays) -> tickResetWorld
                                        | otherwise -> World (applyMove board turn possibleAIturn) (opposingPlayer turn) False False 0
        
        -- No changes apart from the ticks resetting on tickResetWorld
        tickResetWorld = World board turn passedOnLast bothStalled 0
        possibleAIturn = getAIsMove board turn

And we’re actually done now. Build the program as described in the end of the previous part (2B), and start it. What you should have now is a functioning Othello game, ready for your tinkering and development :)

Wrapping it all up.

What should you have now

Revisiting the first post, this is approximately what you should have now:

Screenshot of the almost final product

Apologies, the picture seems to have a 10x10 grid instead of 8x8 you get

What one should have got out of this?

I think this set of posts demonstrates one of the more interesting aspects of Haskell; for games which are highly deterministic, Haskell can very nicely express the logic required to alter the state in a concise form. While UI rendering could certainly be less messy, the core game logic (in my opinion) is still very straightforward and expressed well in Haskell. The AI is also reasonably fast, being able to analyze a fair depth even using the simplest of methods.

Also, you now have a perfectly functional game whose internals you can study and alter at will to your interests :)

Possible improvements and modifications

The End

Again, thank for your interest - I hope this was as interesting to follow for you as it was to make for me. Be sure to comment!

The canonical version with the full source is available on GitLab