packages feed

scrabble-bot-0.1.0.0: PlayGen.hs

{-# LANGUAGE LambdaCase, TupleSections, RecordWildCards, BangPatterns #-}

module PlayGen (genAllPlays, showPlay) where

import qualified Data.DAWG.Packed as D
import Data.Int                     
import Data.Array.Unboxed                                            
import Data.List                    
import Data.List.Split           
import Data.Char                                       
import Control.Arrow               
import Control.Parallel.Strategies
import Data.Ord                    
import Data.Tuple                  
import Text.Printf            
import GameData

maxRackSize = 7
maxWcards = 2

genPlays :: Direction -> [String] -> D.Node -> String -> [Play]
genPlays dir tbl dawg rck | length rck > maxRackSize = error $ printf "Rack too big, limit is %d" maxRackSize
                          | wcardnum   > maxWcards   = error $ printf "Too many wildcards, limit is %d" maxWcards
                          | otherwise = concat $ parMap rdeepseq (getScores . genPlaysAt) (range tableBounds) where


    table = parseTable tbl dawg 
    (wcardnum, rack) = first length $ partition (==wildcard) rck 

    maybeDel :: String -> D.Node -> Maybe (D.Node, String)
    maybeDel []     n = Nothing
    maybeDel (x:xs) n | x == D.value n = Just (n, xs)
                      | otherwise  = second (x:) `fmap` maybeDel xs n

    leftParts :: [PrefixData]
    leftParts = (dawg, "", (wcardnum, rack)): next dawg rack wcardnum where

        next n r w = concat $ no_wcard ++ with_wcard where
            chs        = D.getChildren n 
            no_wcard   = [go id ch r' w | not (null r), Just (ch, r') <- map (maybeDel r) chs]
            with_wcard = [go toLower ch r (w - 1) | w /= 0, ch <- chs]

        go f n r w = (n, [c], (w, r)): [(n, c:p, wr) | (n, p, wr) <- next n r w] where
            c = f (D.value n)


    leftPartMemo :: Array Int [PrefixData]
    leftPartMemo = accumArray (flip (:)) [] (0, maxRackSize) [(length pref, a) | a@(_, pref, _) <- leftParts]


    getLeftParts :: Int -> [PrefixData]
    getLeftParts l = [0..l] >>= (leftPartMemo!)


    getRightParts :: CellIndex -> PrefixData -> [(String, String)]
    getRightParts i (n, pref, (w, r)) = map (pref,) $ concat $ next n r w i (table ! i) where

        next n r w (i,j) (Filled c) = [go (const c) ch r w (i, j + 1) | ch <- D.getChildren n, D.value ch == toUpper c]
        next n r w (i,j) cell       = no_wcard ++ with_wcard where
            i'         = (i, j + 1)
            chs        = (if isEmpty cell then id else filter (hasLetter (lset cell) . D.value)) (D.getChildren n)
            no_wcard   = [go id ch r' w i' | not (null r), Just (ch, r') <- map (maybeDel r) chs]
            with_wcard = [go toLower ch r (w - 1) i' | w /= 0, ch <- chs]

        go f n r w i = word ++ rest where
            c    = f (D.value n)
            cell = table ! i
            inb  = inBounds i
            word = [[c] | D.endOfWord n, (not inb) || (not $ isFilled cell)]
            rest = [c:x | inb, x <- concat $ next n r w i cell]


    genPlaysAt :: CellIndex -> (CellIndex, [(String, String)])
    genPlaysAt i = (i, plays) where

        plays | not $ isAnchor $ table ! i = []
              | not $ null leftWord        = getRightParts i (leftNode, leftWord, (wcardnum, rack))
              | otherwise                  = getLeftParts prefLen >>= getRightParts i

        goLeft   = map (table!) $ drop 1 $ stepLeft i
        leftWord = reverse $ map (\(Filled c) -> c) $ takeWhile isFilled goLeft
        leftNode = maybe (error "invalid word in scrabble table") id (D.lookupPrefix leftWord dawg)
        prefLen  = min maxRackSize (length $ takeWhile isEmpty $ goLeft)


    getScores :: (CellIndex, [(String, String)]) -> [Play]
    getScores ((i, j), playwords) = map go playwords where

        go (a, b) = Play dir start' totalScore word where
            start   = (i, j - (fromIntegral $ length a))
            cells   = map (table!) (stepRight start)
            bonuses = map (bonusTable!) (stepRight start)
            word    = a ++ b
            start'  = (if dir == H then id else swap) start

            wordMod s = \case
                WS2 -> 2 * s
                WS3 -> 3 * s
                _   -> s 

            process (!wsc, !csc, !wmods, !bingo) (char, cell, bonus) = let
                notfill = not $ isFilled cell
                ps      = pieceScore char
                cscore  = crossScore cell

                lscore | not notfill  = ps
                       | bonus == LS2 = 2*ps
                       | bonus == LS3 = 3*ps
                       | otherwise    = ps

                csc' = csc + (if isAnchor cell && cscore /= 0
                                then wordMod (lscore + cscore) bonus
                                else 0)

                wmods' = (if elem bonus [WS2, WS3] && notfill then (bonus:) else id) wmods
                in (wsc + lscore, csc', wmods', bingo + fromEnum notfill)

            (wsc, csc, wmods, bingo) = foldl' process (0, 0, [], 0) (zip3 word cells bonuses)
            totalScore = csc + foldl' wordMod wsc wmods + 50 * fromEnum (bingo == 7)


genAllPlays :: [String] -> D.Node -> String -> [Play]
genAllPlays table dawg rack = sortBy (flip $ comparing score) plays where
    plays = zip [H, V] [table, transpose table] >>= \(d, t) -> genPlays d t dawg rack


showPlay :: [String] -> Play -> IO ()
showPlay table p@(Play d l s w) = let
    as = zip ((if d == H then stepRight else stepDown) l) w
    bs = zip (range tableBounds) (concat table)

    insert ((i, a):as) ((j, b):bs)
        | i == j    = a: insert as bs
        | otherwise = b: insert ((i, a):as) bs
    insert a b = map snd a ++ map snd b

    in do putStrLn (show p)
          mapM_ print $ chunksOf (length table) (insert as bs)