Declarative Project 1
===
###### tags: `declarative` `comp30020`
```haskell=
-- File : Proj1.hs
-- Author : Austen McClernon
-- Origin : Thursday Sep 5 09:30:02 2019
-- Purpose : Project 1 Submission, COMP30020, Declarative Programming.
--
-- This file implements functions that define an Agent that plays 'CardGuesser'
--
-- Card Guesser is a game where an 'answer' is selected. The 'answer' is a list
-- of distinct western playing cards. The cards are taken from a deck of 52
-- cards, where jokers are not included (e.g. '4C' four clubs,'TS' ten spades).
-- The 'guesser' then acts to correctly guess the answer via repeated guesses,
-- informed by feedback in the form of a tuple. The 'feedback' function details
-- this.
--
-- This guess function solves this problem via generating every single possible
-- 'answer' for a given size & pruning this list via selecting guesses that
-- would prune the GameState as much as possible. This is explained in the
-- functions 'bestGuess' and 'guessScore'
module Proj1 (feedback, initialGuess, nextGuess, GameState) where
import Data.List
import Data.Ord
import Card
-- | GameState represents a list of remaining possible answers
-- Feedback represents the scoring format defined by the project
-- specification.
newtype GameState = GameState [[Card]]
type Feedback = (Int, Int, Int, Int, Int)
-- | Takes a target & guess containing a list of cards of equal length,
-- returns tuple of five integers:
-- 1. size of intersection between target & guess
-- 2. number of cards with a rank strictly less than the lowest ranked card
-- in guess, from target
-- 3. size of intersection between target (rank only) & guess (rank only)
-- 4. number of cards with a rank strictly greater than the highest ranked
-- card in guess, from target
-- 5. size of intersection between target (suit only) & guess (suit only)
feedback :: [Card] -> [Card] -> Feedback
feedback target guess = (cardOverlap, cardsLesser, rankOverlap,
cardsGreater, suitOverlap)
where
cardOverlap = l (target `intersect` guess)
cardsLesser = l (filter (< minimum (map rank guess)) (map rank target))
rankOverlap = l (map rank target \\ (map rank target \\ map rank guess))
cardsGreater = l (filter (> maximum (map rank guess)) (map rank target))
suitOverlap = l (map suit target \\ (map suit target \\ map suit guess))
l = length
-- | Generates an initial guess, and a new game state enumerating every
-- possible answer. The first guess is a logical spread of both Rank & Suit
-- to eliminate the most possible answers (on average)
initialGuess :: Int -> ([Card],GameState)
initialGuess n = (cardSelection, makeGameState)
where
cardSelection = combineSelect n
makeGameState = GameState (combsOfSizeK n cardSet)
-- | Generates a list of cards, where rank is equally distant to both
-- the first and last rank (2, Ace) & the other ranks selected. Suit,
-- is then selected for minimal overlap. Refer to spec for +1 divison
--
combineSelect :: Int -> [Card]
combineSelect n = zipWith (\x y -> cardSet !! (x * numRanks + y))
selectSuits selectRank
where
selectRank = [ x * (numRanks `div` (n + 1)) | x <- [1..n] ]
selectSuits = [ x `mod` numSuits | x <- [1..n]]
numRanks = length ([minBound..maxBound]::[Rank])
numSuits = length ([minBound..maxBound]::[Suit])
-- | Prunes the game state such that only the guesses that would give the
-- same feedback, if we were to consider them the answer & using our
-- previous guess. Based on this, the right guess is generated.
nextGuess :: ([Card],GameState)->Feedback->([Card],GameState)
nextGuess (prevGuess, GameState possible) res = (reduction, state)
where
state = reduce (prevGuess, GameState possible) res
reduction = selectMethod state state
-- | Filter's the candidate answers according to Knuth's Mastermind Algorithm.
-- Where we reduce the pool of possible answers, by comparing the feedback
-- received from the previous guess against using each of the candidate's
-- remaining as the target. Where any candidate that does not give the same
-- feedback as that which we received in the previous guess, cannot be a
-- candidate answer.
-- https://bit.ly/2lUClFL for reference
reduce :: ([Card], GameState) -> (Int, Int, Int, Int, Int)-> GameState
reduce (guess, GameState possible) res
= GameState [ hand | hand <- possible, res == feedback hand guess]
-- | Selects the guessing method to be used, if the number of candidate
-- answers is less than ~ 1400 then 'bestGuess' is possible - see function
-- below for details. Otherwise, it is too computationally expensive and
-- the first possibility is selected
--
-- Further:
-- The figure 1400, was settled on via automated testing & timing on a
-- single thread modern processor, in many cases didn't conclude reliably
-- in under 10 seconds given the algorithm for finding the best guess is
-- O(n^2(logn)).
selectMethod :: GameState -> (GameState -> [Card])
selectMethod (GameState gs)
| length gs < 1400 = bestGuess
| otherwise = getHead
-- | Generates the 'best' possible guess, given a game's state
-- (candidate answers remaining) 'Best' is defined as the guess that when
-- chosen will, on average result in the lowest number of remaining
-- candidates, if it's incorrect. To clarify, it reduces the number of
-- candidate answers the most, therefore increasing the probability of the
-- actual answer being selected in a future guess.
bestGuess :: GameState -> [Card]
bestGuess (GameState gs) = minimumBy(comparing $ guessScore $ GameState gs) gs
-- | Calculates the average number of candidates that will remain, assuming
-- the guess is incorrect. Hence providing a 'score' of how effective is at
-- reducing the size of the candidate pool.
--
-- Process:
-- Given a possible guess, we compare that guess against every other
-- 'candidate' within the game state we use the feedback function, where
-- guess is compared to every other 'candidate' as the target. We then map
-- the feedback of using this guess, over the candidates & group identical
-- feedback's together. The guess is then scored according to:
-- sum (group_size^2) / sum (group_size), where a lower score corresponds to
-- a higher reduction in candidates, on average if that guess were used.
guessScore :: GameState -> [Card] -> Double
guessScore (GameState gs) guess
= fromIntegral (sum $ map (^ 2) groupSizes)
where
groupSizes = map length $ (group . sort) $ map (`feedback` guess) gs
-- | Helper Function that returns the first group of cards
-- in the game state of candidate answers
getHead:: GameState -> [Card]
getHead (GameState state) = head state
-- | Helper function to returns the set of cards [2..A, C..D]
cardSet :: [Card]
cardSet = [minBound..maxBound] :: [Card]
-- | Returns all combinations of a list, given a selection size using the
-- results from right twice, to increase performance above that of the
-- naive solution. solution is inspired by analysis of combinations
-- see this link for further information -- https://bit.ly/2lTogbE
combsOfSizeK :: Int -> [a] -> [[a]]
combsOfSizeK k set = let len = length set
in if k>len then [] else combsOfSizeK set !! (len-k)
where
combsOfSizeK [] = [[[]]]
combsOfSizeK (x:set) = let right = combsOfSizeK set
in zipWith (++) ([]:right) (map (map (x:)) right ++ [[]])
```