Let's build an Othello AI - part 3B - Wrap-up and Interactivity (16.4.2017)

In the series:

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 final product

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 leave a comment - and if you are inspired to make further developments, be sure to drop a link here so we can see them :)

The full source is available also on GitHub

And again: the canonical version is available on GitHub