Let's build an Othello AI - part 3B - Wrap-up and Interactivity (16.4.2017)
In the series:
- Structures and Basic Functionality
- Advanced functions, and UI Scaffolding
- Wrapping up and adding interactivity
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:
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
- Improved AI; the one presented here, as I stated, is one of the simplest possible. There could be marked improvements using alpha-beta pruning, which does more analysis to see if a branch is worth investigating. There's also the possibility of making a more advanced library with strategies and opening sets.. but that's beyond the scope of this series.
- Improved user interface - the one here is markedly simple, and should be fairly easy to replace, considering the whole game itself is constructed using pure functions, not requiring IO monads.
- Randomness: as someone with a keen eye must have certainly noticed, there's absolutely no randomness whatsoever in the AI. Adding such randomness would require either using IO, or carrying a RNG state through functions; both undesirable for this post due to the desire for simplicity.
- Comedic changes: during testing, I once changed the AI so that it plays as poorly as possible. Try to do the same - it is easier than you may think ;)
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