packages feed

MTGBuilder 0.1.0.0 → 0.1.0.1

raw patch · 4 files changed

+214/−2 lines, 4 files

Files

MTGBuilder.cabal view
@@ -2,7 +2,7 @@ -- documentation, see http://haskell.org/cabal/users-guide/  name:                MTGBuilder-version:             0.1.0.0+version:             0.1.0.1 synopsis:            Builds decks out of a meta -- description:          license:             MIT@@ -17,7 +17,10 @@  executable mtg-builder   main-is:             Main.hs-  -- other-modules:       +  other-modules:+    MTGBuilder.Deck+    MTGBuilder.Combination+    MTGBuilder.Parser   -- other-extensions:       build-depends:         base >=4.8 && <4.9
+ src/MTGBuilder/Combination.hs view
@@ -0,0 +1,14 @@+module MTGBuilder.Combination where++import Data.Set (Set)+import qualified Data.Set as Set++combinations :: Ord a => Int -> Set a -> Set (Set a)+combinations k xs = combinations' (Set.size xs) k xs+    where combinations' n k' s+            | k' == 0   = Set.singleton (Set.empty)+            | k' >= n   = Set.singleton (s)+            | null s    = Set.empty+            | otherwise = case Set.minView s of+                Just (y, ys)    -> Set.map (Set.insert y) (combinations' (n-1) (k'-1) ys) `Set.union` combinations' (n-1) k' ys+                Nothing         -> error "Invalid empty set"
+ src/MTGBuilder/Deck.hs view
@@ -0,0 +1,129 @@+{-# LANGUAGE BangPatterns #-}++module MTGBuilder.Deck (+    makeRanking,+    composeDecks,+    dumpDeck,+    dumpRanking,+    Ranking,+    Card(..),+    Deck+) where++import MTGBuilder.Combination+import Data.Set (Set)+import qualified Data.Set as Set+import Data.Map (Map)+import qualified Data.Map as Map+import Data.Maybe+import Data.List+import Control.Monad.Reader+import System.IO++data Card = MkCard {+    name :: String,+    copy :: Int+} deriving (Show, Eq, Ord)++type Deck = Set Card++{-+The ranking is the most important data structure++http://www.channelfireball.com/articles/magic-math-a-new-way-to-determine-an-aggregate-deck-list-rg-dragons/++Frank Karsten's algorithm is an example of building a deck based soley on first order rankings.++An Nth order ranking is the popularty of a particular combination of N cards.+If six of the input decks run Bolt1+Snapcaster1, that combination gets a score of 6.++The interaction field is the map of all combinations found in any input deck,+and the number of decks that have that combination.++The interactionSize field is the maximum order of rank.+This is used to throttle the computational expense, at the cost of precision.+Precision beyond 3rd order likely isn't necessary.+2nd order is enough for most cases.+1st order is exactly Karsten's algorithm, and is only good enough for single-archetype aggregations.+-}++data Ranking = MkRanking {+    interaction :: Map (Set Card) Int,+    interactionSize :: Int+}++-- The return type of this function is a reader over IO so that verbosity can be read, and verbose messages can be printed+makeRanking :: Int -> [(String, Deck)] -> ReaderT Bool IO Ranking+makeRanking size inputDecks = do+    verbose <- ask+    rankDecks MkRanking { interaction=Map.empty, interactionSize=size } inputDecks+    where+        rankDecks :: Ranking -> [(String, Deck)] -> ReaderT Bool IO Ranking+        rankDecks ranking [] = return ranking+        rankDecks ranking ((name, deck):decks) = do+            verbose <- ask+            when verbose (liftIO $ hPutStrLn stderr $ "Ranking " ++ name)+            rankDecks (ranking { interaction=int }) decks+            where+                !int = -- Strict, because it will be fully evaluated anyway, and this provides more realistic verbose messages.+                    let f map x = Map.insertWith (+) x 1 map+                    in  foldl f (interaction ranking) $ Set.unions [combinations n deck | n <- [1..(interactionSize ranking)]]++dumpDeck :: Deck -> String+dumpDeck deck = intercalate "\n" lines+    where+        lines :: [String]+        lines = fmap (\(cardName, count) -> (show count) ++ " " ++ cardName) (Map.assocs getMap)+            where+                getMap :: Map String Int+                getMap = foldl f Map.empty deck+                    where+                        f = (\map card -> Map.insertWith (+) (name card) 1 map)++dumpRanking :: Ranking -> String+dumpRanking ranking = intercalate "\n" lines+    where+        lines :: [String]+        lines = fmap (\(combo, count) -> (show count) ++ " : " ++ (show combo)) $ Map.assocs $ interaction ranking+{-+Composition combines all the input decks.+Note: Although different copies of the same card are treated as different cards in this algorithm,+the same copies of the same card from different decks are treated as the same.+So when we union the decks, we are merely merging all the cards in all decks.+So if two decks each have 4 Bolts, we still only see 4 Bolts in the union.++Composing decks simply sorts the cards in the union by sortWithRanking,+then removes the lowest ranked card, then repeats until the deck is down to the provided size.+-}+composeDecks :: Ranking -> Int -> [Deck] -> ReaderT Bool IO Deck+composeDecks ranking deckSize decks = compose $ Set.unions decks+    where+        compose :: Deck -> ReaderT Bool IO Deck+        compose cards+            | Set.size sorted <= deckSize = return $ Set.map snd sorted+            | otherwise = do+                verbose <- ask+                when verbose $ liftIO $ hPutStrLn stderr (show $ Set.size sorted)+                compose $ Set.map snd $ fromMaybe Set.empty $ fmap snd $ Set.minView sorted+            where sorted = sortWithRanking ranking cards++-- Each card in the set is ranked.+sortWithRanking :: Ranking -> Set Card -> Set (Double, Card)+sortWithRanking ranking deck = Map.foldlWithKey (\set card rank -> Set.insert (rank, card) set) Set.empty rankMap+    where+        rankMap = Map.foldlWithKey rankCombo Map.empty $ interaction ranking+        rankCombo map combo count+            | combo `Set.isSubsetOf` deck = foldl (\m card -> Map.insertWith (+) card rank m) map combo+            | otherwise = map+            where+                {-+                To rank a card, look at each combination in the ranking.+                For each combination that contains the card, and is a subset of the deck,+                add to the card's ranking the following:++                    (popularity of combo) * 1 / (2 ^ order of combo)++                This way, lower orders are considered more important,+                thus the popularity of the card on its own (first order combination) is most important+                -}+                rank = (fromIntegral count) * 1.0 / (2.0 ^ Set.size combo)
+ src/MTGBuilder/Parser.hs view
@@ -0,0 +1,66 @@+module MTGBuilder.Parser (+    deckParser,+    parseDeckString,+    parseDeckFile+) where++import MTGBuilder.Deck+import Control.Monad+import Data.Set+import Text.ParserCombinators.Parsec+import Text.ParserCombinators.Parsec.Language+import qualified Text.ParserCombinators.Parsec.Token as Token+import Text.Parsec.Char+import Text.ParserCombinators.Parsec.Combinator++deckTokens = Token.makeTokenParser $ emptyDef {+    identStart = alphaNum <|> char '_',+    commentLine = "//",+    caseSensitive = False+}++identifier = Token.identifier deckTokens+lexeme = Token.lexeme deckTokens+reserved = Token.reserved deckTokens+symbol = Token.symbol deckTokens+natural = Token.natural deckTokens+brackets = Token.brackets deckTokens+whiteSpace = Token.whiteSpace deckTokens++deckParser :: Parser Deck+deckParser = do+    whiteSpace+    d <- deck+    optionMaybe sideboard+    return d++deck :: Parser Deck+deck = do+    cards <- many cardParser+    return $ unions cards++cardParser :: Parser (Set Card)+cardParser = lexeme (mainboardCard <|> (sideboardCard >> return empty))++mainboardCard :: Parser (Set Card)+mainboardCard = do+    numCopies <- natural+    set <- optionMaybe $ brackets $ optionMaybe identifier+    name <- manyTill anyChar endOfCard+    return $ fromList [MkCard {name=name,copy=fromIntegral n} | n <- [1..numCopies]]+    where endOfCard = (endOfLine >> return ()) <|> eof++sideboardCard :: Parser ()+sideboardCard = do+    symbol "SB:"+    mainboardCard+    return ()++sideboard :: Parser ()+sideboard = do+    reserved "sideboard"+    deck+    return ()++parseDeckString = parse deckParser+parseDeckFile = parseFromFile deckParser