PREVIOUS PART: Artificial Intelligence
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 :)
Revisiting the first post, this is approximately what you should have now:
Apologies, the picture seems to have a 10x10 grid instead of 8x8 you get
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 :)
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