packages feed

wordify-0.7.0.0: src/Wordify/Rules/Move.hs

module Wordify.Rules.Move
  ( Move (PlaceTiles, Exchange, Pass),
    GameTransition (MoveTransition, ExchangeTransition, PassTransition, GameFinished),
    makeMove,
    newGame,
  )
where

import Control.Applicative
import Control.Arrow
import Control.Error
import Control.Error.Util
import Control.Monad
import Data.Map
import qualified Data.Map as M
import qualified Data.Map as Map
import Wordify.Rules.Board
import Wordify.Rules.Dictionary
import Wordify.Rules.FormedWord
import Wordify.Rules.Game.Internal
import Wordify.Rules.LetterBag
import Wordify.Rules.Player
import Wordify.Rules.Pos
import Wordify.Rules.WordifyError
import Wordify.Rules.Tile

data GameTransition
  = -- | The new player (with their updated letter rack and score), new game state, and the words formed by the move
    MoveTransition Player Game FormedWords
  | -- | The new game state, the player with their rack before and after the exchange respectively.and the exchanged tiles
    ExchangeTransition Game Player Player [Tile]
  | -- | The new game state with the opportunity to play passed on to the next player.
    PassTransition Game
  | -- |
    --                          The game has finished. The final game state, and the final words formed (if the game was ended by a
    --                          player placing their final tiles.) The players before their scores were increased or decreased is also
    --                          given.
    GameFinished Game (Maybe FormedWords)

-- |
--    Transitiions the game to the next state. If the move places tiles, the player must have the tiles to place and
--    place the tiles legally. If the move exchanges tiles, the bag must not be empty and the player must have the
--    tiles to exchange. A WordifyError is returned if these condtions are not the case.
makeMove :: Game -> Move -> Either WordifyError GameTransition
makeMove game move
  | gameStatus game /= InProgress = Left GameNotInProgress
  | otherwise = flip addMoveToHistory move <$> gameTransition
  where
    gameTransition = case move of
      PlaceTiles placed -> makeBoardMove game placed
      Exchange exchanged -> exchangeMove game exchanged
      Pass -> (Right . passMove) game

makeBoardMove :: Game -> M.Map Pos Tile -> Either WordifyError GameTransition
makeBoardMove game placed =
  do
    validPlacedTiles <- validateTiles (validLetters letterBag) placed
    let playedTiles = Map.elems validPlacedTiles
    formed <- formedWords
    (overallScore, _) <- scoresIfWordsLegal dict formed
    nextBoard <- newBoard currentBoard validPlacedTiles
    intermediatePlayer <- removeLettersandGiveScore player playedTiles overallScore

    if hasEmptyRack intermediatePlayer && (bagSize letterBag == 0)
      then do
        let beforeFinalisingGame = updateGame game intermediatePlayer nextBoard letterBag
        let finalisedGame = finaliseGame beforeFinalisingGame
        return $ GameFinished finalisedGame (Just formed)
      else do
        let (newPlayer, newBag) = updatePlayerRackAndBag intermediatePlayer letterBag (Map.size validPlacedTiles)
        let updatedGame = updateGame game newPlayer nextBoard newBag
        return $ MoveTransition newPlayer updatedGame formed
  where
    player = currentPlayer game
    currentBoard = board game
    dict = dictionary game
    letterBag = bag game
    validTiles = validLetters letterBag

    formedWords =
      if any isPlaceMove (movesMade game)
        then wordsFormedMidGame currentBoard placed
        else wordFormedFirstMove currentBoard placed

    isPlaceMove mv = case mv of
      PlaceTiles _ -> True
      _ -> False

exchangeMove :: Game -> [Tile] -> Either WordifyError GameTransition
exchangeMove game exchangedTiles =
  let exchangeOutcome = exchangeLetters (bag game) exchangedTiles
   in case exchangeOutcome of
        Nothing -> Left CannotExchangeWhenNoLettersInBag
        Just (givenTiles, newBag) ->
          let newPlayer = exchange player exchangedTiles givenTiles
           in maybe
                (Left $ PlayerCannotExchange (tilesOnRack player) exchangedTiles)
                ( \exchangedPlayer ->
                    let gameState = updateGame game exchangedPlayer (board game) newBag
                     in Right $ ExchangeTransition gameState player exchangedPlayer exchangedTiles
                )
                newPlayer
  where
    player = currentPlayer game

passMove :: Game -> GameTransition
passMove game =
  let gameState = pass game
   in if gameFinished
        then GameFinished (finaliseGame gameState) Nothing
        else PassTransition gameState
  where
    numPasses = passes game + 1
    gameFinished = numPasses == numberOfPlayers game * 2


validateTiles :: ValidTiles -> M.Map Pos Tile -> Either WordifyError (M.Map Pos Tile)
validateTiles validTiles placed = fromList <$> mapM (validateTilePlacement validTiles) (toList placed)
  where
    validateTilePlacement :: ValidTiles -> (Pos, Tile) -> Either WordifyError (Pos, Tile)
    validateTilePlacement validTiles (pos, Letter letters x) = (,) pos <$> note (InvalidTileLetters pos letters) (Map.lookup letters validTiles)
    validateTilePlacement validTiles (pos, Blank (Just assigned)) =
      note (NotAssignableToBlank pos assigned validTileStrings) (Map.lookup assigned validTiles) >>= \x -> Right (pos, Blank (Just assigned))
    validateTilePlacement validTiles (pos, Blank Nothing) = Left (CannotPlaceBlankWithoutLetter pos)

    validTileStrings = Map.keys validTiles

newGame :: GameTransition -> Game
newGame (MoveTransition _ game _) = game
newGame (ExchangeTransition game _ _ _) = game
newGame (PassTransition game) = game
newGame (GameFinished game _) = game

addMoveToHistory :: GameTransition -> Move -> GameTransition
addMoveToHistory (MoveTransition player game formedWords) move = MoveTransition player (updateHistory game move) formedWords
addMoveToHistory (ExchangeTransition game oldPlayer newPlayer exchangedTiles) move = ExchangeTransition (updateHistory game move) oldPlayer newPlayer exchangedTiles
addMoveToHistory (PassTransition game) move = PassTransition (updateHistory game move)
addMoveToHistory (GameFinished game wordsFormed) move = GameFinished (updateHistory game move) wordsFormed

finaliseGame :: Game -> Game
finaliseGame game
  | gameStatus game == Finished = game
  | otherwise = game {player1 = play1, player2 = play2, optionalPlayers = optionals, gameStatus = Finished, moveNumber = pred moveNo}
  where
    unplayedValues = Prelude.sum $ Prelude.map tileValues allPlayers
    allPlayers = players game
    moveNo = moveNumber game

    play1 = finalisePlayer (player1 game)
    play2 = finalisePlayer (player2 game)
    optionals =
      optionalPlayers game
        >>= ( \(player3, maybePlayer4) ->
                Just (finalisePlayer player3, finalisePlayer <$> maybePlayer4)
            )

    finalisePlayer player =
      if hasEmptyRack player
        then giveEndWinBonus player unplayedValues
        else giveEndLosePenalty player (tileValues player)

updatePlayerRackAndBag :: Player -> LetterBag -> Int -> (Player, LetterBag)
updatePlayerRackAndBag player letterBag numPlayed
  | tilesInBag == 0 = (player, letterBag)
  | tilesInBag >= numPlayed =
    maybe (player, letterBag) (first (giveTiles player)) $ takeLetters letterBag numPlayed
  | otherwise = maybe (player, letterBag) (first (giveTiles player)) $ takeLetters letterBag tilesInBag
  where
    tilesInBag = bagSize letterBag

newBoard :: Board -> M.Map Pos Tile -> Either WordifyError Board
newBoard currentBoard placed = foldM (\oldBoard (pos, tile) -> newBoardIfUnoccupied oldBoard pos tile) currentBoard $ Map.toList placed
  where
    newBoardIfUnoccupied brd pos tile = note (PlacedTileOnOccupiedSquare pos tile) $ placeTile brd tile pos

removeLettersandGiveScore :: Player -> [Tile] -> Int -> Either WordifyError Player
removeLettersandGiveScore player playedTiles justScored = do
  let newPlayer = flip increaseScore justScored <$> removePlayedTiles player playedTiles
   in note (PlayerCannotPlace (tilesOnRack player) playedTiles) newPlayer

scoresIfWordsLegal :: Dictionary -> FormedWords -> Either WordifyError (Int, [(String, Int)])
scoresIfWordsLegal dict formedWords =
  let strings = wordStrings formedWords
   in case invalidWords dict strings of
        [] -> Right $ wordsWithScores formedWords
        xs -> Left $ WordsNotInDictionary xs