packages feed

wordify-0.3.0.0: src/Wordify/Rules/Player.hs

module Wordify.Rules.Player
  ( Player,
    LetterRack,
    makePlayer,
    name,
    rack,
    tilesOnRack,
    endBonus,
    score,
    increaseScore,
    reduceScore,
    giveEndLosePenalty,
    giveEndWinBonus,
    giveTiles,
    removePlayedTiles,
    removeTiles,
    hasEmptyRack,
    tileValues,
    exchange,
  )
where

import Data.List
import Data.Map (Map)
import qualified Data.Map as Map
import Data.Maybe
import Wordify.Rules.LetterBag
import Wordify.Rules.Tile

type Score = Int

type Name = String

data LetterRack = LetterRack [Tile] deriving (Show, Eq)

data Player = Player
  { name :: Name,
    rack :: LetterRack,
    score :: Score,
    endBonus :: Int
  }
  deriving (Show, Eq)

makePlayer :: String -> Player
makePlayer playerName = Player playerName (LetterRack []) 0 0

tilesOnRack :: Player -> [Tile]
tilesOnRack (Player _ (LetterRack letters) _ _) = letters

increaseScore :: Player -> Int -> Player
increaseScore player justScored = player {score = currentScore + justScored}
  where
    currentScore = score player

reduceScore :: Player -> Int -> Player
reduceScore player removeScore = player {score = currentScore - removeScore}
  where
    currentScore = score player

giveEndLosePenalty :: Player -> Int -> Player
giveEndLosePenalty player penalty = (reduceScore player penalty) {endBonus = - penalty}

giveEndWinBonus :: Player -> Int -> Player
giveEndWinBonus player bonus = (increaseScore player bonus) {endBonus = bonus}

hasEmptyRack :: Player -> Bool
hasEmptyRack player = null $ tilesOnRack player

tileValues :: Player -> Int
tileValues player = sum $ map tileValue (tilesOnRack player)

{-
  Adds tiles to the player's tile rack.
-}
giveTiles :: Player -> [Tile] -> Player
giveTiles player newTiles = player {rack = LetterRack $ newTiles ++ tilesOnRack player}

removeTiles :: Player -> [Tile] -> Player
removeTiles player toRemove = player {rack = LetterRack $ tilesOnRack player \\ toRemove}

-- |
--    Removes played tiles from the player's tile rack, if it was possible for the player
--    to play those tiles in the first place. A player may play a tile on his rack, unless
--    it is a blank, which must first be assigned a letter.
removePlayedTiles :: Player -> [Tile] -> Maybe Player
removePlayedTiles player tiles =
  if playerCanPlace player tiles
    then Just $ player `removedFromRack` tiles
    else Nothing
  where
    removedFromRack playing playedTiles = player {rack = LetterRack (deleteFirstsBy isPlayable (tilesOnRack playing) playedTiles)}

-- |
--    Returns true if the player cannot place any of the given tiles. A player cannot play
--    a Blank tile that they have not given a letter, or a tile not on their rack.
playerCanPlace :: Player -> [Tile] -> Bool
playerCanPlace player played = isNothing $ find isInvalid playedList
  where
    (playedFrequencies, rackFrequencies) = tileFrequencies played (tilesOnRack player)
    playedList = Map.toList playedFrequencies

    isInvalid :: (Tile, Int) -> Bool
    isInvalid (tile, freq) =
      case tile of
        -- Player doesn't have tiles
        Blank _ -> freq > Map.findWithDefault 0 (Blank Nothing) rackFrequencies
        Letter chr val -> freq > Map.findWithDefault 0 (Letter chr val) rackFrequencies

exchange :: Player -> [Tile] -> [Tile] -> Maybe Player
exchange player exchanged received =
  if not (playerCanExchange player exchanged)
    then Nothing
    else Just $ giveTiles (removeTiles player exchanged) received

playerCanExchange :: Player -> [Tile] -> Bool
playerCanExchange (Player _ (LetterRack letterRack) _ _) exchanged =
  isNothing $ find cannotExchange exchangedList
  where
    (exchangedFrequencies, rackFrequencies) = tileFrequencies exchanged letterRack
    exchangedList = Map.toList exchangedFrequencies

    cannotExchange (tile, freq) =
      case tile of
        -- Tried to exchange a blank letter which has been labeled. Client error.
        Blank (Just _) -> False
        -- Player doesn't have tiles
        Blank _ -> freq > Map.findWithDefault 0 (Blank Nothing) rackFrequencies
        Letter chr val -> freq > Map.findWithDefault 0 (Letter chr val) rackFrequencies

tileFrequencies :: [Tile] -> [Tile] -> ((Map.Map Tile Int), (Map.Map Tile Int))
tileFrequencies given letterRack = (givenFrequencies, rackFrequencies)
  where
    buildFrequencies tiles = foldl addFrequency (Map.empty) tiles
    addFrequency dict tile = Map.alter newFrequency tile dict
    newFrequency m = Just $ maybe 1 succ m -- Default freq of one, or inc existing frequency
    givenFrequencies = buildFrequencies given
    rackFrequencies = buildFrequencies letterRack