board-games 0.3 → 0.4
raw patch · 19 files changed
+1293/−234 lines, 19 filesdep +boxesdep +combinatorialdep +doctest-exitcode-stdiodep ~containersdep ~criteriondep ~enummapset
Dependencies added: boxes, combinatorial, doctest-exitcode-stdio, doctest-lib, explicit-exception, haha, semigroups, shell-utility
Dependency ranges changed: containers, criterion, enummapset, network-uri, random, transformers
Files
- Changes.md +4/−0
- Makefile +25/−1
- board-games.cabal +24/−8
- demo/Server.hs +5/−3
- demo/Server/Option.hs +10/−24
- private/Game/Utility.hs +30/−6
- src/Game/Labyrinth.hs +948/−0
- src/Game/Mastermind.hs +43/−38
- src/Game/Mastermind/CodeSet.hs +1/−1
- src/Game/Mastermind/CodeSet/Tree.hs +1/−3
- src/Game/Mastermind/HTML.hs +9/−8
- src/Game/VierGewinnt/HTML.hs +5/−4
- src/Game/ZeilenSpalten/HTML.hs +9/−8
- test-module.list +3/−0
- test/Test.hs +9/−10
- test/Test/Game/Labyrinth.hs +52/−0
- test/Test/Game/Mastermind.hs +83/−0
- test/Test/Game/Utility.hs +27/−0
- test/Test/Mastermind.hs +5/−120
Changes.md view
@@ -1,5 +1,9 @@ # Change log for the `board-games` package +## 0.4++ * `Labyrinth`: New game "Das verrückte Labyrinth".+ ## 0.3 * `Mastermind.CodeSet`:
Makefile view
@@ -1,12 +1,36 @@+FLATPAK = $$FLATPAKBUILD+ ghci: ghci -Wall -i:src src/Game/Mastermind.hs -run-test:+run-test: update-test runhaskell Setup configure --user \ --enable-tests --enable-benchmarks -fbuildExamples runhaskell Setup build runhaskell Setup haddock ./dist/build/board-games-test/board-games-test +update-test:+ doctest-extract-0.1 -i src/ -i private/ -o test/ --executable-main=Test.hs $$(cat test-module.list)++ criterion.html: ./dist/build/board-games-benchmark/board-games-benchmark $< --output=$@ $(patsubst %.html, %.csv, --csv=$@)+++dist-newstyle/cache/plan.json:+ cabal new-build --dry-run --disable-benchmarks --disable-tests++flatpak.json: flatpak.cabal.json dist-newstyle/cache/plan.json+ cabal-flatpak --cabal-install --arch x86_64 --arch i386 $< $@++repo-%: flatpak.json+ flatpak-builder --force-clean --arch=$* --repo=$(FLATPAK)/repository \+ --state-dir=$(FLATPAK)/builder/ $(FLATPAK)/build/games $<+ touch $@++board-games.%.flatpak: repo-%+ flatpak build-bundle --arch=$* $(FLATPAK)/repository $@ com.github.thielema.board-games \+ --runtime-repo=https://flathub.org/repo/flathub.flatpakrepo++flatpak-all: board-games.x86_64.flatpak board-games.i386.flatpak
board-games.cabal view
@@ -1,5 +1,5 @@ Name: board-games-Version: 0.3+Version: 0.4 License: GPL License-File: LICENSE Author: Henning Thielemann <haskell@henning-thielemann.de>@@ -9,7 +9,8 @@ Synopsis: Three games for inclusion in a web server Description: Three games that might run as CGI script in a web server:- Connect Four, Rows&Columns, Mastermind+ Connect Four, Rows&Columns, Mastermind.+ Additionally there is a CLI-only game: Das verrueckte Labyrinth. . Check running versions at <http://www.henning-thielemann.de/VierGewinnt> and@@ -29,18 +30,21 @@ Currently the games use German texts. I wanted to use gettext, but this is not thread-safe. Tested-With: GHC==6.4.1, GHC==6.8.2, GHC==6.12.3+Tested-With: GHC==8.6.5+Tested-With: GHC==9.2.5, GHC==9.4.4 Cabal-Version: 1.14 Build-Type: Simple Extra-Source-Files: Makefile Changes.md+ test-module.list Source-Repository head type: darcs location: http://code.haskell.org/~thielema/games/ Source-Repository this- tag: 0.3+ tag: 0.4 type: darcs location: http://code.haskell.org/~thielema/games/ @@ -53,16 +57,21 @@ Library Build-Depends: html >=1.0 && <1.1,+ boxes >=0.1.5 && <0.2,+ haha >=0.3.1 && <0.4, cgi >=3001.1 && <3002, non-empty >=0.2 && <0.4,+ explicit-exception >=0.1.7 && <0.3,+ semigroups >=0.18 && <0.21, utility-ht >=0.0.3 && <0.1,- transformers >=0.2.2 && <0.6,- enummapset >=0.1 && <0.7,+ combinatorial >=0.1 && <0.2,+ transformers >=0.2.2 && <0.7,+ enummapset >=0.1 && <0.8, QuickCheck >2.0 && <3.0 If flag(splitBase) Build-Depends: containers >=0.2 && <0.7,- random >=1.0 && <1.2,+ random >=1.0 && <1.3, array >=0.1 && <0.6, base >= 2 && <5 Else@@ -84,6 +93,7 @@ Game.Mastermind.CodeSet.Union Game.Mastermind.CodeSet.Tree Game.Mastermind.NonEmptyEnumSet+ Game.Labyrinth Other-Modules: Game.Utility @@ -97,9 +107,10 @@ Build-Depends: board-games, httpd-shed >=0.4 && <0.5,- network-uri >=2.6 && <2.7,+ network-uri >=2.6 && <2.8, html, cgi,+ shell-utility >=0.0 && <0.2, non-empty, utility-ht, transformers,@@ -117,10 +128,15 @@ Hs-Source-Dirs: test, private Main-Is: Test.hs Other-Modules:+ Test.Game.Labyrinth+ Test.Game.Utility+ Test.Game.Mastermind Test.Mastermind Game.Utility Build-Depends: board-games,+ doctest-exitcode-stdio >=0.0 && <0.1,+ doctest-lib >=0.1 && <0.2, QuickCheck, non-empty >=0.3.1, utility-ht,@@ -159,7 +175,7 @@ Main-Is: MastermindSpeed.hs Build-Depends: board-games,- criterion >=0.6 && <1.6,+ criterion >=0.6 && <1.7, enummapset, containers, non-empty,
demo/Server.hs view
@@ -1,6 +1,7 @@ module Main where import qualified Server.Option as Option+import qualified Shell.Utility.Verbosity as Verbosity import qualified Game.ZeilenSpalten.HTML as ZeilenSpalten import qualified Game.VierGewinnt.HTML as VierGewinnt@@ -29,10 +30,11 @@ opt <- Option.get HTTPd.initServer (Option.port opt) $ \ req -> do -- FixMe: should check for HTTP method here- Option.printVerbose opt 1 req+ Option.printVerbose opt Verbosity.verbose req let uri = HTTPd.reqURI req- Option.printVerbose opt 2 $ uriQuery uri- Option.printVerbose opt 2 $ HTTPd.queryToArguments $ uriQuery uri+ Option.printVerbose opt Verbosity.deafening $ uriQuery uri+ Option.printVerbose opt Verbosity.deafening $+ HTTPd.queryToArguments $ uriQuery uri case uriPath uri of "/" -> return $ HTTPd.Response 200 headers $
demo/Server/Option.hs view
@@ -4,18 +4,22 @@ printVerbose, ) where +import qualified Shell.Utility.Verbosity as Verbosity+import Shell.Utility.Verbosity (Verbosity)+import Shell.Utility.ParseArgument (parseNumber)+import Shell.Utility.Exit (exitFailureMsg)+ import System.Console.GetOpt (getOpt, ArgOrder(..), OptDescr(..), ArgDescr(..), usageInfo, ) import System.Environment (getArgs, getProgName, )-import System.Exit (exitSuccess, exitFailure, )-import qualified System.IO as IO+import System.Exit (exitSuccess, ) import Control.Monad (when, ) data T = Cons {- verbosity :: Int,+ verbosity :: Verbosity, port :: Int } deriving (Show)@@ -24,30 +28,13 @@ deflt :: T deflt = Cons {- verbosity = 0,+ verbosity = Verbosity.normal, port = 8080 -- other options might control maximum values for some dimensions in the games } -exitFailureMsg :: String -> IO a-exitFailureMsg msg =- IO.hPutStrLn IO.stderr msg >> exitFailure--parseNumber ::- (Read a) =>- String -> (a -> Bool) -> String -> String -> IO a-parseNumber name constraint constraintName str =- case reads str of- [(n, "")] ->- if constraint n- then return n- else exitFailureMsg $ name ++ " must be " ++ constraintName- _ ->- exitFailureMsg $ name ++ " must be a number, but is '" ++ str ++ "'"---printVerbose :: (Show a) => T -> Int -> a -> IO ()+printVerbose :: (Show a) => T -> Verbosity -> a -> IO () printVerbose opt verb a = when (verb <= verbosity opt) $ print a @@ -67,8 +54,7 @@ "show options" : Option ['v'] ["verbose"] (flip ReqArg "LEVEL" $ \str flags ->- fmap (\verb -> flags{verbosity = fromInteger verb}) $- parseNumber "verbosity" (\n -> 0<=n && n<=2) "from the range from 0 to 2" str)+ fmap (\verb -> flags{verbosity = verb}) $ Verbosity.parse str) "level of verbosity" : Option ['p'] ["port"] (flip ReqArg "NUMBER" $ \str flags ->
private/Game/Utility.hs view
@@ -14,18 +14,16 @@ import qualified Test.QuickCheck as QC -readMaybe :: (Read a) => String -> Maybe a-readMaybe str =- case reads str of- [(a,"")] -> Just a- _ -> Nothing+{- $setup+>>> import Game.Utility (Choice, mergeChoice, noChoice)+-} + nullToMaybe :: [a] -> Maybe [a] nullToMaybe [] = Nothing nullToMaybe s = Just s -- candidate for random-utility, cf. module htam:Election, markov-chain--- for Sets it would be more efficient to use Set.elemAt randomSelect :: (Rnd.RandomGen g, Monad m) => [a] -> MS.StateT g m a randomSelect items = liftM (items!!) $ MS.state $ Rnd.randomR (0, length items-1)@@ -56,6 +54,11 @@ noChoice = Choice EnumMap.empty 0 -- it is hard to test whether fullEval absorbs+{- |+prop> \a -> a == mergeChoice noChoice (a :: Choice Char)+prop> \a -> a == mergeChoice a (noChoice :: Choice Char)+prop> \a b -> mergeChoice a b == mergeChoice b (a :: Choice Char)+-} mergeChoice :: (Enum a) => Choice a -> Choice a -> Choice a mergeChoice (Choice symbolsA countA) (Choice symbolsB countB) = Choice@@ -63,3 +66,24 @@ (countA + countB - min (min countA countB) (Fold.sum (EnumMap.intersectionWith min symbolsA symbolsB)))++{-+Unfortunately, this does not apply:++*Test.Mastermind EnumMap> let a = Choice (EnumMap.singleton 'x' 1) 1+*Test.Mastermind EnumMap> let b = Choice (EnumMap.singleton 'x' 1) 0+*Test.Mastermind EnumMap> let c = Choice (EnumMap.singleton 'y' 1) 1+*Test.Mastermind EnumMap> mergeChoice (mergeChoice a b) c+Choice (fromList [('x',1),('y',1)]) 2+*Test.Mastermind EnumMap> mergeChoice a (mergeChoice b c)+Choice (fromList [('x',1),('y',1)]) 1+*Test.Mastermind EnumMap> mergeChoice a b+Choice (fromList [('x',1)]) 1+*Test.Mastermind EnumMap> mergeChoice b c+Choice (fromList [('x',1),('y',1)]) 1+-}+_choiceAssociative :: Choice Char -> Choice Char -> Choice Char -> Bool+_choiceAssociative a b c =+ mergeChoice (mergeChoice a b) c+ ==+ mergeChoice a (mergeChoice b c)
+ src/Game/Labyrinth.hs view
@@ -0,0 +1,948 @@+module Game.Labyrinth where++import qualified System.Random as Rnd+import Game.Utility (randomSelect)++import qualified Graphics.Ascii.Haha.Terminal as ANSI+import qualified Text.PrettyPrint.Boxes as Box+import Text.PrettyPrint.Boxes (Box)+import Text.Printf (printf)++import qualified Test.QuickCheck as QC++import qualified Control.Monad.Exception.Synchronous as ME+import qualified Control.Monad.Trans.State as MS+import qualified Control.Applicative.HT as App+import qualified Control.Functor.HT as FuncHT+import Control.Monad (mfilter)+import Control.Applicative (Applicative, pure, (<*>), (<$>))++import qualified Data.Traversable as Trav+import qualified Data.Foldable as Fold+import qualified Data.Monoid.HT as Mn+import qualified Data.NonEmpty.Class as NonEmptyC+import qualified Data.NonEmpty as NonEmpty+import qualified Data.List.Match as Match+import qualified Data.List.HT as ListHT+import qualified Data.List as List+import qualified Data.EnumMap as EnumMap+import qualified Data.EnumSet as EnumSet+import qualified Data.Map as Map+import qualified Data.Sequence as Seq+import qualified Data.Bits as Bits+import qualified Data.Char as Char+import Data.EnumMap (EnumMap)+import Data.EnumSet (EnumSet)+import Data.Map (Map)+import Data.Sequence (Seq)+import Data.Bits ((.&.), (.|.))+import Data.Word (Word64)+import Data.Traversable (Traversable, sequenceA)+import Data.Foldable (Foldable, foldMap)+import Data.NonEmpty ((!:))+import Data.List (zipWith4)+import Data.Monoid (Monoid, mconcat, mappend, mempty)+import Data.Semigroup (Semigroup, (<>))+import Data.Tuple.HT (mapPair, mapFst, mapSnd)+import Data.Ord.HT (comparing)+++{- $setup+>>> import qualified Game.Labyrinth as Labyrinth+-}++newtype Board = Board Word64+ deriving (Eq)++instance Show Board where+ showsPrec p (Board bits) =+ showParen (p>=10) $+ showString "Board " . showString (printf "0x%016x" bits)++instance Semigroup Board where+ Board x <> Board y = Board (x.|.y)++instance Monoid Board where+ mempty = Board 0+ mappend = (<>)++instance QC.Arbitrary Board where+ arbitrary = Board . (boardMask .&.) <$> QC.arbitrary+++data Coord = C0 | C1 | C2 | C3 | C4 | C5 | C6+ deriving (Eq, Ord, Enum, Bounded, Show)++instance QC.Arbitrary Coord where+ arbitrary = QC.arbitraryBoundedEnum++boardIndex :: (Coord,Coord) -> Int+boardIndex (row,column) = fromEnum row * 8 + fromEnum column++isCellFixed :: (Coord,Coord) -> Bool+isCellFixed (row,column) = even (fromEnum row) && even (fromEnum column)++boardGet :: Board -> (Coord,Coord) -> Bool+boardGet (Board bits) pos = Bits.testBit bits $ boardIndex pos++boardSet :: Bool -> (Coord,Coord) -> Board+boardSet b pos = Board $ fromIntegral (fromEnum b) `Bits.shiftL` boardIndex pos++singleCell :: (Coord,Coord) -> Board+singleCell = Board . Bits.bit . boardIndex++listsFromBoard :: Board -> [[Bool]]+listsFromBoard board =+ ListHT.outerProduct (curry $ boardGet board) allEnums allEnums++allEnums :: (Enum a, Bounded a) => [a]+allEnums = [minBound .. maxBound]++rowMask, columnMask, boardMask :: Word64+rowMask = 0x7F+columnMask = 0x1010101010101+boardMask = rowMask*columnMask++rowKMask, columnKMask :: Coord -> Word64+rowKMask pos = rowMask `Bits.shiftL` (8 * fromEnum pos)+columnKMask pos = columnMask `Bits.shiftL` fromEnum pos++infixl 7 .-.++(.-.) :: Bits.Bits a => a -> a -> a+a.-.b = a .&. Bits.complement b++shiftBoardLeft :: Board -> Board+shiftBoardLeft (Board bits) =+ Board $ (bits .-. columnKMask C0) `Bits.shiftR` 1++shiftBoardRight :: Board -> Board+shiftBoardRight (Board bits) =+ Board $ (bits .-. columnKMask C6) `Bits.shiftL` 1++shiftBoardUp :: Board -> Board+shiftBoardUp (Board bits) =+ Board $ (bits .-. rowKMask C0) `Bits.shiftR` 8++shiftBoardDown :: Board -> Board+shiftBoardDown (Board bits) =+ Board $ (bits .-. rowKMask C6) `Bits.shiftL` 8+++splitBoard :: Board -> Board -> (Board,Board)+splitBoard (Board mask) (Board board) =+ (Board (board.&.mask), Board (board.-.mask))++{- |+prop> \k b -> b == Labyrinth.shiftRowLeft k (Labyrinth.shiftRowRight k b)+prop> \k b -> b == Labyrinth.shiftRowRight k (Labyrinth.shiftRowLeft k b)+-}+shiftRowLeft :: Coord -> (Bool, Board) -> (Bool, Board)+shiftRowLeft r (extra,board) =+ let (row, remainder) = splitBoard (Board $ rowKMask r) board+ in (boardGet row (r,C0),+ remainder <> shiftBoardLeft row <> boardSet extra (r,C6))++shiftRowRight :: Coord -> (Bool, Board) -> (Bool, Board)+shiftRowRight r (extra,board) =+ let (row, remainder) = splitBoard (Board $ rowKMask r) board+ in (boardGet row (r,C6),+ remainder <> shiftBoardRight row <> boardSet extra (r,C0))++{- |+prop> \k b -> b == Labyrinth.shiftColumnUp k (Labyrinth.shiftColumnDown k b)+prop> \k b -> b == Labyrinth.shiftColumnDown k (Labyrinth.shiftColumnUp k b)+-}+shiftColumnUp :: Coord -> (Bool, Board) -> (Bool, Board)+shiftColumnUp c (extra,board) =+ let (column, remainder) = splitBoard (Board $ columnKMask c) board+ in (boardGet column (C0,c),+ remainder <> shiftBoardUp column <> boardSet extra (C6,c))++shiftColumnDown :: Coord -> (Bool, Board) -> (Bool, Board)+shiftColumnDown c (extra,board) =+ let (column, remainder) = splitBoard (Board $ columnKMask c) board+ in (boardGet column (C6,c),+ remainder <> shiftBoardDown column <> boardSet extra (C0,c))+++cycleBoardLeft :: Board -> Board+cycleBoardLeft (Board bits) =+ Board $+ (bits .-. columnKMask C0) `Bits.shiftR` 1+ .|.+ (bits .&. columnKMask C0) `Bits.shiftL` 6++cycleBoardRight :: Board -> Board+cycleBoardRight (Board bits) =+ Board $+ (bits .-. columnKMask C6) `Bits.shiftL` 1+ .|.+ (bits .&. columnKMask C6) `Bits.shiftR` 6++cycleBoardUp :: Board -> Board+cycleBoardUp (Board bits) =+ Board $+ (bits .-. rowKMask C0) `Bits.shiftR` 8+ .|.+ (bits .&. rowKMask C0) `Bits.shiftL` (8*6)++cycleBoardDown :: Board -> Board+cycleBoardDown (Board bits) =+ Board $+ (bits .-. rowKMask C6) `Bits.shiftL` 8+ .|.+ (bits .&. rowKMask C6) `Bits.shiftR` (8*6)++cycleGen :: (Board -> Board) -> (Coord -> Word64) -> Coord -> Board -> Board+cycleGen cycleBoard mask coord =+ uncurry mappend . mapFst cycleBoard . splitBoard (Board $ mask coord)++{- |+prop> \k b -> b == Labyrinth.cycleRowLeft k (Labyrinth.cycleRowRight k b)+prop> \k b -> b == Labyrinth.cycleRowRight k (Labyrinth.cycleRowLeft k b)+-}+cycleRowLeft :: Coord -> Board -> Board+cycleRowLeft = cycleGen cycleBoardLeft rowKMask++cycleRowRight :: Coord -> Board -> Board+cycleRowRight = cycleGen cycleBoardRight rowKMask++{- |+prop> \k b -> b == Labyrinth.cycleColumnUp k (Labyrinth.cycleColumnDown k b)+prop> \k b -> b == Labyrinth.cycleColumnDown k (Labyrinth.cycleColumnUp k b)+-}+cycleColumnUp :: Coord -> Board -> Board+cycleColumnUp = cycleGen cycleBoardUp columnKMask++cycleColumnDown :: Coord -> Board -> Board+cycleColumnDown = cycleGen cycleBoardDown columnKMask++cycleStripe :: Border -> Coord -> Board -> Board+cycleStripe b =+ case b of+ West -> cycleRowRight+ East -> cycleRowLeft+ South -> cycleColumnUp+ North -> cycleColumnDown++++rotateMasked ::+ (Directions Board -> Directions Board) ->+ Board -> Directions Board -> Directions Board+rotateMasked rotate mask board =+ case FuncHT.unzip (splitBoard mask <$> board) of+ (masked, remaining) -> rotate masked <> remaining++rotateDir90, rotateDir180 :: Directions a -> Directions a+rotateDir90 (Directions n w s e) = Directions e n w s+rotateDir180 (Directions n w s e) = Directions s e n w++randomRotate ::+ (Rnd.RandomGen g) => Directions Board -> MS.State g (Directions Board)+randomRotate board =+ ($ board) <$>+ App.lift2 (.)+ (rotateMasked rotateDir180 . Board <$> MS.state Rnd.random)+ (rotateMasked rotateDir90 . Board <$> MS.state Rnd.random)+++data Directions a = Directions {north, west, south, east :: a}+ deriving (Eq)++instance Semigroup a => Semigroup (Directions a) where+ (<>) = App.lift2 (<>)++instance Monoid a => Monoid (Directions a) where+ mempty = pure mempty+ mappend = App.lift2 mappend++instance Functor Directions where+ fmap f (Directions n w s e) = Directions (f n) (f w) (f s) (f e)++instance Foldable Directions where+ foldMap f (Directions n w s e) =+ f n `mappend` f w `mappend` f s `mappend` f e++instance Traversable Directions where+ sequenceA (Directions n w s e) = App.lift4 Directions n w s e++instance Applicative Directions where+ pure a = Directions a a a a+ Directions fn fw fs fe <*> Directions n w s e =+ Directions (fn n) (fw w) (fs s) (fe e)++transposeDirections :: Directions [[a]] -> [[Directions a]]+transposeDirections (Directions n w s e) =+ zipWith4 (zipWith4 Directions) n w s e+++boolChar :: Char -> Bool -> Char+boolChar c b = if b then c else ' '++formatBoard :: Board -> String+formatBoard = unlines . map (map (boolChar '*')) . listsFromBoard++wallChar :: Char+wallChar = '\x2588'++wayChar :: Char+wayChar = '.'++selectWallWayChar :: Bool -> Bool -> Char+selectWallWayChar wall connect =+ case (wall, connect) of+ (False, False) -> ' '+ (True, False) -> wallChar+ (False, True) -> wayChar+ (True, True) -> error "character cannot be both wall and way"++boxTable :: [[Box]] -> Box+boxTable = Box.vcat Box.left . map (Box.hcat Box.top)++formatCell ::+ EnumSet Player -> Directions Bool -> Char -> Directions Bool -> Box+formatCell players connects center walls =+ let plChar p =+ if EnumSet.member p players then playerChar True p else wallChar+ in case App.lift2 selectWallWayChar walls connects of+ Directions n w s e ->+ boxTable $ map (map Box.char) $+ [plChar Player0, n, plChar Player1] :+ [w, center, e] :+ [plChar Player2, s, plChar Player3] :+ []++format :: Directions Board -> Box+format =+ boxTable .+ map (map (formatCell EnumSet.empty (pure False) ' ')) .+ transposeDirections . fmap listsFromBoard++formatWays :: Directions Board -> Board -> Box+formatWays walls reachable =+ boxTable $+ zipWith3+ (zipWith3 (formatCell EnumSet.empty))+ (transposeDirections $ fmap listsFromBoard $+ connectReachable walls reachable)+ (map (map (boolChar wayChar)) $ listsFromBoard reachable)+ (transposeDirections $ fmap listsFromBoard walls)++connectReachable :: Directions Board -> Board -> Directions Board+connectReachable walls reachable =+ let blockWall (Board mask) (Board bits) = Board $ bits.-.mask+ shiftAndBlock block1 shift block0 =+ blockWall+ (block1 walls <> shift (block0 walls))+ (reachable <> shift reachable)+ in Directions {+ north = shiftAndBlock north shiftBoardDown south,+ south = shiftAndBlock south shiftBoardUp north,+ west = shiftAndBlock west shiftBoardRight east,+ east = shiftAndBlock east shiftBoardLeft west+ }++arrows :: Directions Char+arrows =+ Directions {+ north = '\x21D3', west = '\x21D2', south = '\x21D1', east = '\x21D0'+ }++arrowFrame :: Maybe (Border, Coord) -> Box -> Box+arrowFrame forbidden box =+ let rowBox arrow c = Box.text [' ', arrow, c]+ columnBox c0 c1 = Box.alignVert Box.center1 3 $ Box.text [c0, c1]+ makeBoxes =+ Directions {+ north = rowBox, west = flip columnBox,+ south = rowBox, east = columnBox+ }+ horizVert h v = Directions {north = h, south = h, west = v, east = v}+ labels =+ maybe id+ (\(b,c) -> singleBorder b $ Map.insert c)+ forbidden (pure $ const id)+ <*>+ horizVert (Box.emptyBox 1 3) (Box.emptyBox 3 1)+ <*>+ App.lift2 fmap (makeBoxes <*> arrows) shiftCharMap+ labelBoxes =+ horizVert (Box.hsep 3 Box.top) (Box.vsep 3 Box.center1)+ <*>+ ((Box.nullBox :) . Map.elems <$> labels)+ emptyBox = Box.emptyBox 1 2+ in boxTable $+ [emptyBox, north labelBoxes, emptyBox] :+ [west labelBoxes, box, east labelBoxes] :+ [emptyBox, south labelBoxes, emptyBox] :+ []++constMap :: (Ord k) => [k] -> a -> Map k a+constMap keys a = Map.fromList $ map (flip (,) a) keys++shiftCharMap :: Directions (Map Coord Char)+shiftCharMap =+ flip MS.evalState 'A' $ Trav.traverse Trav.sequence $+ pure $ constMap [C1,C3,C5] $ MS.state $ \c -> (c, succ c)++arrowMap :: Map Char (Border,Coord)+arrowMap =+ Fold.fold $+ App.lift2+ (\b -> Map.fromList . map (\(c,char) -> (char,(b,c))) . Map.toList)+ borderSet shiftCharMap+++coordinateFrame :: Box -> Box+coordinateFrame box =+ let row =+ Box.emptyBox 1 1 Box.<>+ (Box.hsep 2 Box.top $ map Box.char $ Map.keys columnMap)+ column =+ Box.emptyBox 1 1 Box.//+ (Box.vsep 2 Box.top $ map Box.char $ Map.keys rowMap)+ emptyBox = Box.emptyBox 1 2+ in boxTable $+ [emptyBox, row, emptyBox] :+ [column Box.<> Box.emptyBox 1 1, box, Box.emptyBox 1 1 Box.<> column] :+ [emptyBox, row, emptyBox] :+ []++rowMap, columnMap :: Map Char Coord+rowMap = Map.fromList $ zip ['A'..] allEnums+columnMap = Map.fromList $ zip ['0'..] allEnums+++data Shape = I | L | T+ deriving (Eq, Ord, Enum, Show)++type InitTile = (Shape, Maybe Char)++data Rotation = R0 | R1 | R2 | R3+ deriving (Eq, Ord, Enum, Bounded)+++dirsFromShape :: Shape -> Directions Bool+dirsFromShape shape =+ case shape of+ I -> (pure True) {north = False, south = False}+ L -> (pure True) {north = False, east = False}+ T -> (pure True) {west = False, east = False, south = False}++defaultBoardDirs :: [[Directions Bool]]+defaultBoardDirs =+ let f :: Rotation -> Shape -> Directions Bool+ f rot =+ (if Bits.testBit (fromEnum rot) 1 then rotateDir180 else id) .+ (if Bits.testBit (fromEnum rot) 0 then rotateDir90 else id) .+ dirsFromShape+ r0 = f R0; r1 = f R1; r2 = f R2; r3 = f R3+ in [r3 L, r0 T, r0 T, r2 L] :+ [r1 T, r1 T, r0 T, r3 T] :+ [r1 T, r2 T, r3 T, r3 T] :+ [r0 L, r2 T, r2 T, r1 L] :+ []++defaultBoard :: Directions Board+defaultBoard =+ let evenCoords = [C0, C2 .. C6]+ in mconcat $+ zipWith+ (\row ->+ mconcat .+ zipWith (\col -> fmap (flip Mn.when (singleCell (row,col))))+ evenCoords)+ evenCoords defaultBoardDirs+++randomChoose :: (Rnd.RandomGen g) => MS.StateT [a] (MS.State g) a+randomChoose = MS.StateT $ randomSelect . ListHT.removeEach++{-+ total fixed symbol+ position+I: 13 0 0+T: 18 12 18+L: 19 4 10++symbols: 24 12+-}+shuffle :: (Rnd.RandomGen g) => MS.State g (InitTile, [InitTile])+shuffle =+ let (symbolsL, symbolsT) = splitAt 6 moveableSymbols+ xs =+ replicate 13 (I, Nothing) +++ map (\s -> (T, Just s)) symbolsT +++ replicate 9 (L, Nothing) +++ map (\s -> (L, Just s)) symbolsL+ in flip MS.evalStateT xs $ App.lift2 (,) randomChoose $+ sequence $ Match.replicate (drop 1 xs) randomChoose++singleCellShape :: (Coord, Coord) -> Shape -> Directions Board+singleCellShape pos shape =+ flip Mn.when (singleCell pos) <$> dirsFromShape shape++grid :: [a] -> [(a,a)]+grid xs = App.lift2 (,) xs xs++layoutMoveableCells :: [InitTile] -> (Directions Board, SymbolMap)+layoutMoveableCells =+ mapPair (Fold.fold . Map.mapWithKey singleCellShape, Map.mapMaybe id) .+ FuncHT.unzip . Map.fromList .+ zip (filter (not . isCellFixed) $ grid allEnums)++randomBoard ::+ (Rnd.RandomGen g) => MS.State g (InitTile, (Directions Board, SymbolMap))+randomBoard = do+ (extra,shapes) <- shuffle+ let (board,symbolMap) = layoutMoveableCells shapes+ moveable <- randomRotate board+ return (extra, (defaultBoard <> moveable, symbolMap))++shuffleTargets :: (Rnd.RandomGen g) => MS.State g [Char]+shuffleTargets =+ let xs = moveableSymbols ++ fixedSymbols+ in flip MS.evalStateT xs $ sequence $ Match.replicate xs randomChoose++associateTargets ::+ NonEmpty.T [] Player -> [Char] ->+ NonEmpty.T Seq (Player, NonEmpty.T [] Char)+associateTargets players =+ NonEmpty.mapTail Seq.fromList .+ NonEmptyC.zipWith+ (\player targets ->+ (player, NonEmpty.snoc targets $ playerChar False player))+ players .+ maybe (error "empty players list") id .+ NonEmpty.fetch . ListHT.sliceHorizontal (1 + length (NonEmpty.tail players))+++bfsStep :: Directions Board -> Board -> Board+bfsStep walls board =+ let blockWall (Board mask) (Board bits) = Board $ bits.-.mask+ shiftAndBlock block1 shift block0 =+ blockWall (block1 walls) (shift (blockWall (block0 walls) board))+ in board+ <>+ shiftAndBlock east shiftBoardLeft west+ <>+ shiftAndBlock west shiftBoardRight east+ <>+ shiftAndBlock north shiftBoardDown south+ <>+ shiftAndBlock south shiftBoardUp north++bfs :: Directions Board -> Board -> Board+bfs walls =+ snd . head . dropWhile (not . fst) .+ ListHT.mapAdjacent (\x y -> (x==y, x)) . iterate (bfsStep walls)+++data Player = Player0 | Player1 | Player2 | Player3+ deriving (Eq, Ord, Enum, Bounded, Show)++playerChar :: Bool -> Player -> Char+playerChar b p =+ case p of+ Player0 -> if b then '\x2666' else '\x2662'+ Player1 -> if b then '\x2665' else '\x2661'+ Player2 -> if b then '\x2660' else '\x2664'+ Player3 -> if b then '\x2663' else '\x2667'++playerSymbols, fixedSymbols, moveableSymbols :: [Char]+playerSymbols = map (playerChar False) allEnums+(fixedSymbols, moveableSymbols) = splitAt 12 $ take 24 ['\x2600' ..]++type SymbolMap = Map (Coord,Coord) Char++fixedSymbolMap :: SymbolMap+fixedSymbolMap =+ let corners = Map.fromList $ zip (grid [C0,C6]) playerSymbols+ in Map.union corners $ Map.fromList $+ zip (filter (flip Map.notMember corners) $ grid [C0, C2 .. C6])+ fixedSymbols++randomSymbolMap :: (Rnd.RandomGen g) => MS.State g SymbolMap+randomSymbolMap =+ Map.fromList <$>+ MS.evalStateT+ (mapM (\symbol -> flip (,) symbol <$> randomChoose) moveableSymbols)+ (filter (flip Map.notMember fixedSymbolMap) $ grid [C0 .. C6])++removeFromMap :: (Ord k) => k -> Map k a -> (Maybe a, Map k a)+removeFromMap = Map.updateLookupWithKey (\ _k _a -> Nothing)++insertMaybeMap :: (Ord k) => k -> Maybe a -> Map k a -> Map k a+insertMaybeMap k = maybe id (Map.insert k)++shiftRowLeftMap :: Coord -> (Maybe Char, SymbolMap) -> (Maybe Char, SymbolMap)+shiftRowLeftMap rowPos (extra, m) =+ let (row, remainder) = Map.partitionWithKey (\(r,_c) _ -> r == rowPos) m+ (x,xs) = removeFromMap (rowPos,minBound) row+ in (x,+ remainder <>+ insertMaybeMap (rowPos,maxBound) extra+ (Map.mapKeysMonotonic (\(r,c) -> (r, pred c)) xs))++shiftRowRightMap :: Coord -> (Maybe Char, SymbolMap) -> (Maybe Char, SymbolMap)+shiftRowRightMap rowPos (extra, m) =+ let (row, remainder) = Map.partitionWithKey (\(r,_c) _ -> r == rowPos) m+ (x,xs) = removeFromMap (rowPos,maxBound) row+ in (x,+ remainder <>+ insertMaybeMap (rowPos,minBound) extra+ (Map.mapKeysMonotonic (\(r,c) -> (r, succ c)) xs))++shiftColumnUpMap ::+ Coord -> (Maybe Char, SymbolMap) -> (Maybe Char, SymbolMap)+shiftColumnUpMap columnPos (extra, m) =+ let (column, remainder) =+ Map.partitionWithKey (\(_r,c) _ -> c == columnPos) m+ (x,xs) = removeFromMap (minBound,columnPos) column+ in (x,+ remainder <>+ insertMaybeMap (maxBound,columnPos) extra+ (Map.mapKeysMonotonic (\(r,c) -> (pred r, c)) xs))++shiftColumnDownMap ::+ Coord -> (Maybe Char, SymbolMap) -> (Maybe Char, SymbolMap)+shiftColumnDownMap columnPos (extra, m) =+ let (column, remainder) =+ Map.partitionWithKey (\(_r,c) _ -> c == columnPos) m+ (x,xs) = removeFromMap (maxBound,columnPos) column+ in (x,+ remainder <>+ insertMaybeMap (minBound,columnPos) extra+ (Map.mapKeysMonotonic (\(r,c) -> (succ r, c)) xs))+++formatWithSymbols :: BoardState -> Board -> Box+formatWithSymbols s reachable =+ boxTable $+ zipWith4+ (zipWith4+ (\pos connects reach ->+ formatCell+ (EnumMap.keysSet $ EnumMap.filter (pos==) $ statePlayerMap s)+ connects+ (Map.findWithDefault (boolChar wayChar reach) pos $+ stateSymbolMap s)))+ (ListHT.outerProduct (,) allEnums allEnums)+ (transposeDirections $ fmap listsFromBoard $+ connectReachable (stateBoard s) reachable)+ (listsFromBoard reachable)+ (transposeDirections $ fmap listsFromBoard $ stateBoard s)+++type PlayerMap = EnumMap Player (Coord,Coord)++shiftPlayersGen ::+ Eq a => (b -> a) -> (b -> b) -> a -> EnumMap k b -> EnumMap k b+shiftPlayersGen select update coord =+ EnumMap.map (\pos -> if select pos == coord then update pos else pos)++shiftRowLeftPlayers, shiftRowRightPlayers,+ shiftColumnUpPlayers, shiftColumnDownPlayers ::+ Coord -> PlayerMap -> PlayerMap+shiftRowLeftPlayers = shiftPlayersGen fst (mapSnd cyclicPred)+shiftRowRightPlayers = shiftPlayersGen fst (mapSnd cyclicSucc)+shiftColumnUpPlayers = shiftPlayersGen snd (mapFst cyclicPred)+shiftColumnDownPlayers = shiftPlayersGen snd (mapFst cyclicSucc)+++data BoardState =+ BoardState {+ stateBoard :: Directions Board,+ stateSymbolMap :: SymbolMap,+ statePlayerMap :: PlayerMap+ }++type Tile = (Directions Bool, Maybe Char)++cyclicPred :: (Eq a, Enum a, Bounded a) => a -> a+cyclicPred x = if x==minBound then maxBound else pred x++cyclicSucc :: (Eq a, Enum a, Bounded a) => a -> a+cyclicSucc x = if x==maxBound then minBound else succ x++shiftStateGen ::+ (Coord -> (Bool, Board) -> (Bool, Board)) ->+ (Coord -> (Maybe Char, SymbolMap) -> (Maybe Char, SymbolMap)) ->+ (Coord -> PlayerMap -> PlayerMap) ->+ Coord -> (Tile, BoardState) -> (Tile, BoardState)+shiftStateGen shiftBoard shiftMap shiftPlayers rowPos ((tile,symbol), state) =+ let (newExtra, newSymbolMap) =+ shiftMap rowPos (symbol, stateSymbolMap state)+ (newTile, newBoard) =+ FuncHT.unzip $ fmap (shiftBoard rowPos) $+ App.lift2 (,) tile (stateBoard state)+ newPlayerMap = shiftPlayers rowPos $ statePlayerMap state+ in ((newTile, newExtra),+ BoardState {+ stateBoard = newBoard,+ stateSymbolMap = newSymbolMap,+ statePlayerMap = newPlayerMap+ })++data Border = North | West | South | East+ deriving (Eq, Ord, Enum, Bounded, Show)++borderSet :: Directions Border+borderSet = Directions {north = North, west = West, south = South, east = East}++singleBorder :: Border -> a -> Directions a -> Directions a+singleBorder b a m =+ case b of+ North -> m{north = a}+ South -> m{south = a}+ East -> m{east = a}+ West -> m{west = a}++lookupBorder :: Border -> Directions a -> a+lookupBorder b =+ case b of+ North -> north+ South -> south+ East -> east+ West -> west++flipBorder :: Border -> Border+flipBorder b =+ case b of+ North -> South+ South -> North+ East -> West+ West -> East++shiftState :: Border -> Coord -> (Tile, BoardState) -> (Tile, BoardState)+shiftState b =+ case b of+ West -> shiftStateGen shiftRowRight shiftRowRightMap shiftRowRightPlayers+ East -> shiftStateGen shiftRowLeft shiftRowLeftMap shiftRowLeftPlayers+ South -> shiftStateGen shiftColumnUp shiftColumnUpMap shiftColumnUpPlayers+ North ->+ shiftStateGen shiftColumnDown shiftColumnDownMap shiftColumnDownPlayers+++reachableFromPlayer :: Player -> BoardState -> Board+reachableFromPlayer player state =+ bfs (stateBoard state) $ singleCell $+ EnumMap.findWithDefault (error "player not in playerMap") player $+ statePlayerMap state++shapeRotations :: Eq a => Directions a -> [(Rotation, Directions a)]+shapeRotations shape =+ zip (if rotateDir180 shape == shape then [R0,R1] else allEnums) $+ iterate rotateDir90 shape++reachable1Single :: Board -> (Border, Coord) -> Tile -> BoardState -> Bool+reachable1Single cloud0 (b,pos) tile state0 =+ let state1 = snd $ shiftState b pos (tile, state0)+ cloud1 = bfs (stateBoard state1) $ cycleStripe b pos cloud0+ in any (boardGet cloud1) $ Map.keys $ stateSymbolMap state1++reachable1 :: (Tile, BoardState) -> [((Border, Coord), ([Rotation], Bool))]+reachable1 ((shape,symbol),state) =+ filter (not . null . fst . snd) $+ flip map (App.lift2 (,) allEnums [C1,C3,C5]) $ \shift ->+ (shift,+ mapPair (map fst, null) $+ ListHT.partition+ (\(_rot,rotShape) ->+ reachable1Single+ (Fold.foldMap singleCell $ statePlayerMap state)+ shift (rotShape,symbol) state) $+ shapeRotations shape)++reachable2Single :: Board -> (Border, Coord) -> Tile -> BoardState -> Int+reachable2Single cloud0 (b,pos) tile state0 =+ let (tile1,state1) = shiftState b pos (tile, state0)+ cloud1 = bfs (stateBoard state1) $ cycleStripe b pos cloud0+ in length $+ filter+ (\shift ->+ reachable1Single cloud1 shift (pure True, snd tile1) state1) $+ filter ((flipBorder b, pos) /=) $+ App.lift2 (,) allEnums [C1,C3,C5]++reachable2 :: (Tile, BoardState) -> [(((Border, Coord), Rotation), Int)]+reachable2 ((shape,symbol),state) =+ map+ (\(shift, (rot,rotShape)) ->+ ((shift, rot),+ reachable2Single+ (Fold.foldMap singleCell $ statePlayerMap state)+ shift (rotShape,symbol) state)) $+ App.lift2 (,)+ (App.lift2 (,) allEnums [C1,C3,C5])+ (shapeRotations shape)+++formatRotation :: Rotation -> String+formatRotation = show . fromEnum++formatShift :: ((Border, Coord), Rotation) -> String+formatShift ((b, pos), rot) =+ Map.findWithDefault (error "forbidden coordinate")+ pos (lookupBorder b shiftCharMap)+ :+ formatRotation rot++formatShifts :: ((Border, Coord), ([Rotation], Bool)) -> String+formatShifts ((b, pos), (rot,complete)) =+ Map.findWithDefault (error "forbidden coordinate")+ pos (lookupBorder b shiftCharMap)+ :+ if complete then "*" else concatMap formatRotation rot++parseShift ::+ Maybe (Border, Coord) -> String ->+ ME.Exceptional String ((Border, Coord), Rotation)+parseShift forbidden input =+ case input of+ [arrowChar, rotation] ->+ App.lift2 (,)+ (do+ pos <-+ ME.fromMaybe "not an arrow letter" $+ Map.lookup (Char.toUpper arrowChar) arrowMap+ if Just pos == forbidden+ then ME.throw "reversing the last shift is forbidden"+ else return pos)+ (case rotation of+ '0' -> return R0+ '1' -> return R1+ '2' -> return R2+ '3' -> return R3+ _ -> ME.throw "not a rotation number from 0-3")+ _ -> ME.throw "need two input characters"++parsePosition :: Board -> String -> ME.Exceptional String (Coord, Coord)+parsePosition reachable input =+ case input of+ [rowChar, columnChar] -> do+ pos <-+ App.lift2 (,)+ (ME.fromMaybe "row letter out of range" $+ Map.lookup (Char.toUpper rowChar) rowMap)+ (ME.fromMaybe "column number out of range" $+ Map.lookup columnChar columnMap)+ if boardGet reachable pos+ then return pos+ else ME.throw "position unreachable"+ _ -> ME.throw "need two input characters"++inputLoop :: (String -> ME.Exceptional String a) -> IO a+inputLoop parse =+ ME.switch (\msg -> putStrLn msg >> inputLoop parse) return . parse+ =<< getLine++playerCharSet :: EnumSet Char+playerCharSet = EnumSet.fromList $ map (playerChar True) allEnums++inversePlayerChar :: Char -> String+inversePlayerChar c =+ if EnumSet.member c playerCharSet+ then ANSI.esc "" (ANSI.reverse []) "m" ++ c : ANSI.reset+ else [c]++printBox :: Box -> IO ()+printBox = putStr . concatMap inversePlayerChar . Box.render++main :: IO ()+main = do+ let gameLoop forbiddenShift (shape0, msymbol0) state0+ (NonEmpty.Cons (player,targets) remPlayers) = do+ let rotatedShapes =+ EnumMap.fromList $ zip allEnums $ iterate rotateDir90 shape0+ let symbol0 = maybe ' ' id msymbol0+ printBox $ Box.hsep 5 Box.top $+ arrowFrame forbiddenShift (formatWithSymbols state0 mempty)+ :+ (Box.vcat Box.center1 $ Fold.fold $+ EnumMap.mapWithKey+ (\k sh ->+ [Box.text $ formatRotation k,+ formatCell EnumSet.empty (pure False) symbol0 sh,+ Box.emptyBox 1 0])+ rotatedShapes)+ :+ []+ let NonEmpty.Cons target remTargets = targets+ let state0Target =+ state0{+ stateSymbolMap =+ Map.filter (target==) $ stateSymbolMap state0,+ statePlayerMap =+ EnumMap.intersectionWith const (statePlayerMap state0)+ (EnumMap.singleton player ())+ }+ let msymbol0Target = mfilter (target==) msymbol0+ let isAllowed (shift,_) = forbiddenShift /= Just shift+ case splitAt 5 $ filter isAllowed $+ reachable1 ((shape0,msymbol0Target),state0Target) of+ ([], _) -> do+ let xs =+ take 5 $ List.sortBy (flip $ comparing snd) $+ filter (isAllowed . fst) $+ reachable2 ((shape0,msymbol0Target),state0Target)+ putStrLn $ "promising: " +++ List.intercalate ", "+ (map (\(x,l) -> printf "%s(%d)" (formatShift x) l) xs+ ++ ["..."])+ (xs, ys) ->+ putStrLn $ "hint: " +++ List.intercalate ", "+ (map formatShifts xs ++ take 1 (map (const "...") ys))+ let goal = playerChar True player : '\x2192' : target : []+ putStr $ goal ++ " Enter position and rotation of inserted piece: "+ ((border,coord), rotation) <- inputLoop $ parseShift forbiddenShift+ let (shape1,state1) =+ shiftState border coord+ ((rotatedShapes EnumMap.! rotation, msymbol0), state0)+ let reachable = reachableFromPlayer player state1+ printBox $ coordinateFrame $ formatWithSymbols state1 reachable+ putStrLn ""+ putStr $ goal ++ " Enter coordinates of new player position: "+ pos <- inputLoop $ parsePosition reachable+ mNewPlayers <-+ if Just target /= Map.lookup pos (stateSymbolMap state1)+ then return $ Just $ NonEmpty.snoc remPlayers (player,targets)+ else do+ putStrLn $ "found target: " ++ target : ""+ case NonEmpty.fetch remTargets of+ Nothing -> do+ putStrLn $ "All targets found!"+ return $ NonEmpty.fetch remPlayers+ Just remTargetsNE@(NonEmpty.Cons newTarget _) -> do+ putStrLn $+ "new target: " ++ newTarget :+ ", remaining: " ++ show (length remTargets)+ return $ Just $+ NonEmpty.snoc remPlayers (player, remTargetsNE)+ case mNewPlayers of+ Nothing -> putStrLn "All players found all their targets!"+ Just newPlayers ->+ gameLoop (Just (flipBorder border, coord)) shape1+ state1{statePlayerMap =+ EnumMap.insert player pos $ statePlayerMap state1}+ newPlayers++ let (((shape,msymbol),(board,symbolMap)), shuffledTargets) =+ MS.evalState (App.lift2 (,) randomBoard shuffleTargets) $+ Rnd.mkStdGen 42+ let targets = associateTargets (Player1 !: Player2 : []) shuffledTargets+ let playerMap =+ EnumMap.intersectionWith (const id)+ (EnumMap.fromList $ Fold.toList targets) $+ EnumMap.fromList $ zip allEnums $ grid [C0,C6]+ gameLoop Nothing (dirsFromShape shape, msymbol)+ (BoardState board (fixedSymbolMap <> symbolMap) playerMap)+ targets++{-+let board = snd $ MS.evalState randomBoard $ Rnd.mkStdGen 42+printBox $ formatWays board $ bfs board $ singleCell (C1,C6)+-}
src/Game/Mastermind.hs view
@@ -47,15 +47,31 @@ import Control.Monad.IO.Class (liftIO) import Control.Monad (guard, when, replicateM, liftM2, ) +import qualified Combinatorics as Combi+ import qualified System.Random as Rnd import qualified System.IO as IO +{- $setup+>>> import qualified Test.Mastermind as TestMM+>>> import Test.Mastermind (CodeSetInt, alphabet, Code(Code), CodePair(CodePair), forAllEval)+>>> import qualified Game.Mastermind.CodeSet.Tree as CodeSetTree+>>> import qualified Game.Mastermind.CodeSet as CodeSet+>>> import qualified Game.Mastermind as MM+>>> import qualified Data.EnumSet as EnumSet+>>> import Game.Mastermind (Eval(Eval))+>>> import Control.Monad (replicateM)+>>> import Data.Function.HT (compose2)+-}+ data Eval = Eval Int Int deriving (Eq, Ord, Show) {- | Given the code and a guess, compute the evaluation.++prop> \(CodePair secret attempt) -> MM.evaluate secret attempt == MM.evaluate attempt secret -} evaluate :: (Enum a) => [a] -> [a] -> Eval evaluate code attempt =@@ -68,14 +84,7 @@ partition (uncurry $ equating fromEnum) $ zip code attempt -{--*Game.Mastermind> filter ((Eval 2 0 ==) . evaluate "aabbb") $ replicateM 5 ['a'..'c']-["aaaaa","aaaac","aaaca","aaacc","aacaa","aacac","aacca","aaccc","acbcc","accbc","acccb","cabcc","cacbc","caccb","ccbbc","ccbcb","cccbb"]-*Game.Mastermind> CodeSet.flatten $ matching (EnumSet.fromList ['a'..'c']) "aabbb" (Eval 2 0)-["aaaaa","aaaac","aaaca","aaacc","aacaa","aacac","aacca","aaccc","acbcc","accbc","acccb","cabcc","cacbc","caccb","ccbbc","ccbcb","cccbb"]--} - bagFromList :: (Enum a) => [a] -> EnumMap a Int bagFromList = EnumMap.fromListWith (+) . map (\a -> (a,1)) @@ -106,26 +115,7 @@ then EnumSet.singleton symbol else EnumSet.delete symbol alphabet) code) $- possibleRightPlaces (length code) rightPlaces---- ToDo: import from combinatorial-{- |-Combinatorical \"choose k from n\".--}-possibleRightPlaces :: Int -> Int -> [[Bool]]-possibleRightPlaces n rightPlaces =- if n < rightPlaces- then []- else- if n==0- then [[]]- else- (guard (rightPlaces>0) >>- (map (True:) $- possibleRightPlaces (n-1) (rightPlaces-1)))- ++- (map (False:) $- possibleRightPlaces (n-1) rightPlaces)+ Combi.choose (length code) rightPlaces {- | Given a code and an according evaluation,@@ -134,6 +124,19 @@ The Game.Mastermind game consists of collecting pairs of codes and their evaluations. The searched code is in the intersection of all corresponding code sets.++>>> filter ((MM.Eval 2 0 ==) . MM.evaluate "aabbb") $ replicateM 5 ['a'..'c']+["aaaaa","aaaac","aaaca","aaacc","aacaa","aacac","aacca","aaccc","acbcc","accbc","acccb","cabcc","cacbc","caccb","ccbbc","ccbcb","cccbb"]+>>> CodeSet.flatten (MM.matching (EnumSet.fromList ['a'..'c']) "aabbb" (Eval 2 0) :: CodeSetTree.T Char)+["aaaaa","aaaac","aaaca","aaacc","aacaa","aacac","aacca","aaccc","acbcc","accbc","acccb","cabcc","cacbc","caccb","ccbbc","ccbcb","cccbb"]++prop> \(CodePair secret attempt) -> CodeSetTree.member secret $ MM.matching alphabet attempt (MM.evaluate secret attempt)+prop> \(CodePair secret attempt) -> forAllEval secret $ \eval -> (eval == MM.evaluate secret attempt) == CodeSetTree.member secret (MM.matching alphabet attempt eval)+prop> \(Code attempt) -> forAllEval attempt $ \eval0 -> forAllEval attempt $ \eval1 -> eval0 == eval1 || CodeSetTree.null (compose2 CodeSetTree.intersection (MM.matching alphabet attempt) eval0 eval1)+prop> \(Code attempt) -> forAllEval attempt $ \eval -> all ((eval ==) . MM.evaluate attempt) $ take 100 $ CodeSet.flatten $ (MM.matching alphabet attempt eval :: CodeSetInt)+prop> \(Code attempt) -> forAllEval attempt $ \eval -> let set :: CodeSetInt; set = MM.matching alphabet attempt eval in map (CodeSet.select set) [0 .. min 100 (CodeSet.size set) - 1] == take 100 (CodeSet.flatten set)+prop> TestMM.intersections+prop> TestMM.solve -} matching :: (CodeSet.C set, Enum a) => EnumSet a -> [a] -> Eval -> set a matching alphabet =@@ -164,9 +167,18 @@ let patternCode = zip pattern code in findCodes patternCode rightSymbols $ bagFromList $ map snd $ filter (not . fst) patternCode) $- possibleRightPlaces (length code) rightPlaces+ Combi.choose (length code) rightPlaces +{- |+A more precise test would be to check+that for different numbers of rightPlace and rightSymbol+the codesets are disjoint+and their union is the set of all possible codes.+To this end we need a union with simplification or a subset test.++prop> \(Code attempt) -> fromIntegral (EnumSet.size alphabet) ^ length attempt == sum (map snd (MM.partitionSizes alphabet attempt))+-} partitionSizes :: (Enum a) => EnumSet a -> [a] -> [(Eval, Integer)] partitionSizes alphabet code = map (\eval -> (eval, CodeSetTree.size $ matching alphabet code eval)) $@@ -234,6 +246,9 @@ in EnumSet.union symbols $ EnumSet.fromList $ take 1 $ EnumSet.toList $ EnumSet.difference alphabet symbols +{- |+prop> TestMM.bestSeparatingCode+-} bestSeparatingCode :: (CodeSet.C set, Enum a) => Int -> set a -> NonEmpty.T [] [a] -> [a] bestSeparatingCode n set =@@ -394,16 +409,6 @@ nextSymbols return $ Just $ map snd $ take n $ List.sortBy (comparing fst) $ zip keys nextSymbols-{-- if count>=n || EnumSet.size unusedSymbols <= n- then randomizedAttempt n set- else do- let nextSymbols = EnumSet.toList unusedSymbols- keys <-- mapM (const $ MS.state $ Rnd.randomR (0,1::Double)) nextSymbols- return $ map snd $ take n $- List.sortBy (comparing fst) $ zip keys nextSymbols--} mainRandom :: NonEmptySet.T Char -> Int -> IO ()
src/Game/Mastermind/CodeSet.hs view
@@ -63,7 +63,7 @@ intersections :: (C set, Enum a) => NonEmpty.T [] (set a) -> set a intersections = NonEmpty.foldl1 intersection . nonEmptySortKey size --- ToDo: import from NonEmptyC+-- cannot be easily generalized for inclusion in non-empty package nonEmptySortKey :: (Ord b) => (a -> b) -> NonEmpty.T [] a -> NonEmpty.T [] a nonEmptySortKey f = fmap snd . NonEmptyC.sortBy (comparing fst) . fmap (\x -> (f x, x))
src/Game/Mastermind/CodeSet/Tree.hs view
@@ -63,14 +63,13 @@ Map.toList xps --- ToDo: sizeLimitted max - return size only if it is at most 'max' size :: T a -> Integer size End = 1 size (Products xs) = sum $ map (\(a,b) -> fromIntegral (NonEmptySet.size a) * size b) $ Map.toList xs --- FixMe: somehow inefficient, because the sizes of subsets are recomputed several times+-- somehow inefficient, because the sizes of subsets are recomputed several times select :: (Enum a) => T a -> Integer -> [a] select End n = case compare n 0 of@@ -166,7 +165,6 @@ compare (Indexable x) (Indexable y) = case (x,y) of (End,End) -> EQ- -- maybe should be even an error (End,Products _) -> LT (Products _,End) -> GT (Products xs, Products ys) -> comparing (fmap Indexable) xs ys
src/Game/Mastermind/HTML.hs view
@@ -9,10 +9,11 @@ import qualified Game.Mastermind.CodeSet as CodeSet import qualified Game.Mastermind.NonEmptyEnumSet as NonEmptySet import qualified Game.Mastermind as MM-import Game.Utility (readMaybe, nullToMaybe, randomSelect, )+import Game.Utility (nullToMaybe, randomSelect) -import Text.Html((<<), (+++), concatHtml, toHtml) import qualified Text.Html as Html+import Text.Html((<<), (+++), concatHtml, toHtml)+import Text.Read.HT (maybeRead) import qualified Network.CGI as CGI @@ -249,9 +250,9 @@ parseQuery :: String -> Maybe (Config, Maybe String) parseQuery query = let pairs = CGI.formDecode query- in do width <- readMaybe =<< List.lookup "width" pairs+ in do width <- maybeRead =<< List.lookup "width" pairs alphabet <- NonEmpty.fetch =<< List.lookup "alphabet" pairs- seed <- readMaybe =<< List.lookup "seed" pairs+ seed <- maybeRead =<< List.lookup "seed" pairs mMoves <- maybe (Just Nothing) (fmap Just .@@ -260,15 +261,15 @@ [code,rightPlacesText,rightSymbolsText] -> fmap ((,) code) $ liftM2 MM.Eval- (readMaybe rightPlacesText)- (readMaybe rightSymbolsText)+ (maybeRead rightPlacesText)+ (maybeRead rightSymbolsText) _ -> Nothing) . words) $ List.lookup "moves" pairs let mAttempt0 = List.lookup "attempt" pairs- mRightPlaces = fmap readMaybe $ List.lookup "rightplaces" pairs- mRightSymbols = fmap readMaybe $ List.lookup "rightsymbols" pairs+ mRightPlaces = fmap maybeRead $ List.lookup "rightplaces" pairs+ mRightSymbols = fmap maybeRead $ List.lookup "rightsymbols" pairs (moves,mAttempt) <- case mMoves of Nothing -> Just (Nothing, Nothing)
src/Game/VierGewinnt/HTML.hs view
@@ -8,11 +8,12 @@ import Game.VierGewinnt (Spieler(..), Zug, Spielstand, grundstellung, brettVon, wertung, anfangundzuege, moeglicheZuege, berechneSpielstand, istMatt, )-import Game.Utility (readMaybe, nullToMaybe, )+import Game.Utility (nullToMaybe) -import Text.Html((<<), (+++), noHtml, spaceHtml, concatHtml, renderHtml, toHtml, ) import qualified Text.Html as Html import qualified Data.List as List+import Text.Html((<<), (+++), noHtml, spaceHtml, concatHtml, renderHtml, toHtml)+import Text.Read.HT (maybeRead) import Data.Array((!)) import qualified Network.CGI as CGI@@ -115,14 +116,14 @@ interpretiereAnfrage anfrage = let paare = CGI.formDecode anfrage in do anfangText <- List.lookup "start" paare- anfang <- readMaybe anfangText+ anfang <- maybeRead anfangText zuege <- case List.lookup "zuege" paare of Nothing -> Just [] Just zuegeText -> mapM (\zugText -> case zugText of- [_] -> readMaybe zugText+ [_] -> maybeRead zugText _ -> Nothing) $ words zuegeText return (anfang, zuege)
src/Game/ZeilenSpalten/HTML.hs view
@@ -7,10 +7,11 @@ import Game.ZeilenSpalten hiding (spiel) import qualified Game.Tree as GameTree-import Game.Utility (readMaybe, nullToMaybe, )+import Game.Utility (nullToMaybe) -import Text.Html((<<), (+++), concatHtml, toHtml) import qualified Text.Html as Html+import Text.Html((<<), (+++), concatHtml, toHtml)+import Text.Read.HT (maybeRead) import qualified Network.CGI as CGI @@ -143,11 +144,11 @@ interpretiereAnfrage :: String -> Maybe Beschreibung interpretiereAnfrage anfrage = let paare = CGI.formDecode anfrage- in do breite <- readMaybe =<< List.lookup "breite" paare- hoehe <- readMaybe =<< List.lookup "hoehe" paare- saat <- readMaybe =<< List.lookup "saat" paare- orient <- readMaybe =<< List.lookup "orient" paare- gegenzug <- readMaybe =<< List.lookup "gegenzug" paare+ in do breite <- maybeRead =<< List.lookup "breite" paare+ hoehe <- maybeRead =<< List.lookup "hoehe" paare+ saat <- maybeRead =<< List.lookup "saat" paare+ orient <- maybeRead =<< List.lookup "orient" paare+ gegenzug <- maybeRead =<< List.lookup "gegenzug" paare zuege <- case List.lookup "zuege" paare of Nothing -> Just []@@ -155,7 +156,7 @@ mapM (\zugText -> case zugText of _:_:_:_ -> Nothing- _ -> readMaybe zugText) $+ _ -> maybeRead zugText) $ words zuegeText return ((breite,hoehe), saat, orient, gegenzug, zuege)
+ test-module.list view
@@ -0,0 +1,3 @@+Game.Labyrinth+Game.Mastermind+Game.Utility
test/Test.hs view
@@ -1,15 +1,14 @@+-- Do not edit! Automatically created with doctest-extract. module Main where -import qualified Test.Mastermind as MM-+import qualified Test.Game.Labyrinth+import qualified Test.Game.Mastermind+import qualified Test.Game.Utility -prefix :: String -> [(String, IO ())] -> [(String, IO ())]-prefix msg =- map (\(str,test) -> (msg ++ "." ++ str, test))+import qualified Test.DocTest.Driver as DocTest main :: IO ()-main =- mapM_ (\(msg,io) -> putStr (msg++": ") >> io) $- concat $- prefix "Game.Mastermind" MM.tests :- []+main = DocTest.run $ do+ Test.Game.Labyrinth.test+ Test.Game.Mastermind.test+ Test.Game.Utility.test
+ test/Test/Game/Labyrinth.hs view
@@ -0,0 +1,52 @@+-- Do not edit! Automatically created with doctest-extract from src/Game/Labyrinth.hs+{-# LINE 50 "src/Game/Labyrinth.hs" #-}++module Test.Game.Labyrinth where++import qualified Test.DocTest.Driver as DocTest++{-# LINE 51 "src/Game/Labyrinth.hs" #-}+import qualified Game.Labyrinth as Labyrinth++test :: DocTest.T ()+test = do+ DocTest.printPrefix "Game.Labyrinth:137: "+{-# LINE 137 "src/Game/Labyrinth.hs" #-}+ DocTest.property+{-# LINE 137 "src/Game/Labyrinth.hs" #-}+ (\k b -> b == Labyrinth.shiftRowLeft k (Labyrinth.shiftRowRight k b))+ DocTest.printPrefix "Game.Labyrinth:138: "+{-# LINE 138 "src/Game/Labyrinth.hs" #-}+ DocTest.property+{-# LINE 138 "src/Game/Labyrinth.hs" #-}+ (\k b -> b == Labyrinth.shiftRowRight k (Labyrinth.shiftRowLeft k b))+ DocTest.printPrefix "Game.Labyrinth:153: "+{-# LINE 153 "src/Game/Labyrinth.hs" #-}+ DocTest.property+{-# LINE 153 "src/Game/Labyrinth.hs" #-}+ (\k b -> b == Labyrinth.shiftColumnUp k (Labyrinth.shiftColumnDown k b))+ DocTest.printPrefix "Game.Labyrinth:154: "+{-# LINE 154 "src/Game/Labyrinth.hs" #-}+ DocTest.property+{-# LINE 154 "src/Game/Labyrinth.hs" #-}+ (\k b -> b == Labyrinth.shiftColumnDown k (Labyrinth.shiftColumnUp k b))+ DocTest.printPrefix "Game.Labyrinth:202: "+{-# LINE 202 "src/Game/Labyrinth.hs" #-}+ DocTest.property+{-# LINE 202 "src/Game/Labyrinth.hs" #-}+ (\k b -> b == Labyrinth.cycleRowLeft k (Labyrinth.cycleRowRight k b))+ DocTest.printPrefix "Game.Labyrinth:203: "+{-# LINE 203 "src/Game/Labyrinth.hs" #-}+ DocTest.property+{-# LINE 203 "src/Game/Labyrinth.hs" #-}+ (\k b -> b == Labyrinth.cycleRowRight k (Labyrinth.cycleRowLeft k b))+ DocTest.printPrefix "Game.Labyrinth:212: "+{-# LINE 212 "src/Game/Labyrinth.hs" #-}+ DocTest.property+{-# LINE 212 "src/Game/Labyrinth.hs" #-}+ (\k b -> b == Labyrinth.cycleColumnUp k (Labyrinth.cycleColumnDown k b))+ DocTest.printPrefix "Game.Labyrinth:213: "+{-# LINE 213 "src/Game/Labyrinth.hs" #-}+ DocTest.property+{-# LINE 213 "src/Game/Labyrinth.hs" #-}+ (\k b -> b == Labyrinth.cycleColumnDown k (Labyrinth.cycleColumnUp k b))
+ test/Test/Game/Mastermind.hs view
@@ -0,0 +1,83 @@+-- Do not edit! Automatically created with doctest-extract from src/Game/Mastermind.hs+{-# LINE 56 "src/Game/Mastermind.hs" #-}++module Test.Game.Mastermind where++import Test.DocTest.Base+import qualified Test.DocTest.Driver as DocTest++{-# LINE 57 "src/Game/Mastermind.hs" #-}+import qualified Test.Mastermind as TestMM+import Test.Mastermind (CodeSetInt, alphabet, Code(Code), CodePair(CodePair), forAllEval)+import qualified Game.Mastermind.CodeSet.Tree as CodeSetTree+import qualified Game.Mastermind.CodeSet as CodeSet+import qualified Game.Mastermind as MM+import qualified Data.EnumSet as EnumSet+import Game.Mastermind (Eval(Eval))+import Control.Monad (replicateM)+import Data.Function.HT (compose2)++test :: DocTest.T ()+test = do+ DocTest.printPrefix "Game.Mastermind:74: "+{-# LINE 74 "src/Game/Mastermind.hs" #-}+ DocTest.property+{-# LINE 74 "src/Game/Mastermind.hs" #-}+ (\(CodePair secret attempt) -> MM.evaluate secret attempt == MM.evaluate attempt secret)+ DocTest.printPrefix "Game.Mastermind:133: "+{-# LINE 133 "src/Game/Mastermind.hs" #-}+ DocTest.property+{-# LINE 133 "src/Game/Mastermind.hs" #-}+ (\(CodePair secret attempt) -> CodeSetTree.member secret $ MM.matching alphabet attempt (MM.evaluate secret attempt))+ DocTest.printPrefix "Game.Mastermind:134: "+{-# LINE 134 "src/Game/Mastermind.hs" #-}+ DocTest.property+{-# LINE 134 "src/Game/Mastermind.hs" #-}+ (\(CodePair secret attempt) -> forAllEval secret $ \eval -> (eval == MM.evaluate secret attempt) == CodeSetTree.member secret (MM.matching alphabet attempt eval))+ DocTest.printPrefix "Game.Mastermind:135: "+{-# LINE 135 "src/Game/Mastermind.hs" #-}+ DocTest.property+{-# LINE 135 "src/Game/Mastermind.hs" #-}+ (\(Code attempt) -> forAllEval attempt $ \eval0 -> forAllEval attempt $ \eval1 -> eval0 == eval1 || CodeSetTree.null (compose2 CodeSetTree.intersection (MM.matching alphabet attempt) eval0 eval1))+ DocTest.printPrefix "Game.Mastermind:136: "+{-# LINE 136 "src/Game/Mastermind.hs" #-}+ DocTest.property+{-# LINE 136 "src/Game/Mastermind.hs" #-}+ (\(Code attempt) -> forAllEval attempt $ \eval -> all ((eval ==) . MM.evaluate attempt) $ take 100 $ CodeSet.flatten $ (MM.matching alphabet attempt eval :: CodeSetInt))+ DocTest.printPrefix "Game.Mastermind:137: "+{-# LINE 137 "src/Game/Mastermind.hs" #-}+ DocTest.property+{-# LINE 137 "src/Game/Mastermind.hs" #-}+ (\(Code attempt) -> forAllEval attempt $ \eval -> let set :: CodeSetInt; set = MM.matching alphabet attempt eval in map (CodeSet.select set) [0 .. min 100 (CodeSet.size set) - 1] == take 100 (CodeSet.flatten set))+ DocTest.printPrefix "Game.Mastermind:138: "+{-# LINE 138 "src/Game/Mastermind.hs" #-}+ DocTest.property+{-# LINE 138 "src/Game/Mastermind.hs" #-}+ (TestMM.intersections)+ DocTest.printPrefix "Game.Mastermind:139: "+{-# LINE 139 "src/Game/Mastermind.hs" #-}+ DocTest.property+{-# LINE 139 "src/Game/Mastermind.hs" #-}+ (TestMM.solve)+ DocTest.printPrefix "Game.Mastermind:128: "+{-# LINE 128 "src/Game/Mastermind.hs" #-}+ DocTest.example+{-# LINE 128 "src/Game/Mastermind.hs" #-}+ (filter ((MM.Eval 2 0 ==) . MM.evaluate "aabbb") $ replicateM 5 ['a'..'c'])+ [ExpectedLine [LineChunk "[\"aaaaa\",\"aaaac\",\"aaaca\",\"aaacc\",\"aacaa\",\"aacac\",\"aacca\",\"aaccc\",\"acbcc\",\"accbc\",\"acccb\",\"cabcc\",\"cacbc\",\"caccb\",\"ccbbc\",\"ccbcb\",\"cccbb\"]"]]+ DocTest.printPrefix "Game.Mastermind:130: "+{-# LINE 130 "src/Game/Mastermind.hs" #-}+ DocTest.example+{-# LINE 130 "src/Game/Mastermind.hs" #-}+ (CodeSet.flatten (MM.matching (EnumSet.fromList ['a'..'c']) "aabbb" (Eval 2 0) :: CodeSetTree.T Char))+ [ExpectedLine [LineChunk "[\"aaaaa\",\"aaaac\",\"aaaca\",\"aaacc\",\"aacaa\",\"aacac\",\"aacca\",\"aaccc\",\"acbcc\",\"accbc\",\"acccb\",\"cabcc\",\"cacbc\",\"caccb\",\"ccbbc\",\"ccbcb\",\"cccbb\"]"]]+ DocTest.printPrefix "Game.Mastermind:180: "+{-# LINE 180 "src/Game/Mastermind.hs" #-}+ DocTest.property+{-# LINE 180 "src/Game/Mastermind.hs" #-}+ (\(Code attempt) -> fromIntegral (EnumSet.size alphabet) ^ length attempt == sum (map snd (MM.partitionSizes alphabet attempt)))+ DocTest.printPrefix "Game.Mastermind:250: "+{-# LINE 250 "src/Game/Mastermind.hs" #-}+ DocTest.property+{-# LINE 250 "src/Game/Mastermind.hs" #-}+ (TestMM.bestSeparatingCode)
+ test/Test/Game/Utility.hs view
@@ -0,0 +1,27 @@+-- Do not edit! Automatically created with doctest-extract from private/Game/Utility.hs+{-# LINE 17 "private/Game/Utility.hs" #-}++module Test.Game.Utility where++import qualified Test.DocTest.Driver as DocTest++{-# LINE 18 "private/Game/Utility.hs" #-}+import Game.Utility (Choice, mergeChoice, noChoice)++test :: DocTest.T ()+test = do+ DocTest.printPrefix "Game.Utility:58: "+{-# LINE 58 "private/Game/Utility.hs" #-}+ DocTest.property+{-# LINE 58 "private/Game/Utility.hs" #-}+ (\a -> a == mergeChoice noChoice (a :: Choice Char))+ DocTest.printPrefix "Game.Utility:59: "+{-# LINE 59 "private/Game/Utility.hs" #-}+ DocTest.property+{-# LINE 59 "private/Game/Utility.hs" #-}+ (\a -> a == mergeChoice a (noChoice :: Choice Char))+ DocTest.printPrefix "Game.Utility:60: "+{-# LINE 60 "private/Game/Utility.hs" #-}+ DocTest.property+{-# LINE 60 "private/Game/Utility.hs" #-}+ (\a b -> mergeChoice a b == mergeChoice b (a :: Choice Char))
test/Test/Mastermind.hs view
@@ -1,24 +1,21 @@-module Test.Mastermind (tests) where+module Test.Mastermind where import qualified Game.Mastermind.CodeSet.Tree as CodeSetTree -- import qualified Game.Mastermind.CodeSet.Union as CodeSetUnion import qualified Game.Mastermind.CodeSet as CodeSet import qualified Game.Mastermind.NonEmptyEnumSet as NonEmptySet import qualified Game.Mastermind as MM-import Game.Utility (Choice, mergeChoice, noChoice) -import Control.Monad (liftM2, )-import Control.Applicative ((<$>), )+import Control.Applicative (liftA2, (<$>)) import qualified Data.NonEmpty.Class as NonEmptyC import qualified Data.NonEmpty as NonEmpty import qualified Data.Traversable as Trav-import qualified Data.EnumSet as EnumSet import Data.EnumSet (EnumSet) import Data.NonEmpty ((!:)) import qualified Test.QuickCheck as QC-import Test.QuickCheck (Property, Arbitrary(arbitrary), quickCheck, (==>), )+import Test.QuickCheck (Property, Arbitrary(arbitrary), (==>), ) alphabet :: EnumSet Int@@ -50,20 +47,14 @@ genCodePair :: Int -> QC.Gen CodePair genCodePair width =- liftM2- (\(Code xs) (Code ys) ->- uncurry CodePair $ unzip $ zip xs ys)+ liftA2+ (\(Code xs) (Code ys) -> uncurry CodePair $ unzip $ zip xs ys) (genCode width) (genCode width) instance Arbitrary CodePair where arbitrary = genCodePair 5 -matchingMember :: CodePair -> Bool-matchingMember (CodePair secret attempt) =- CodeSetTree.member secret $- MM.matching alphabet attempt (MM.evaluate secret attempt)- genEval :: Int -> QC.Gen MM.Eval genEval size = do total <- QC.frequency $ map (\k -> (k+1, return k)) [1 .. size]@@ -73,63 +64,10 @@ forAllEval :: QC.Testable prop => [a] -> (MM.Eval -> prop) -> Property forAllEval code = QC.forAll (genEval (length code)) -matchingNotMember :: CodePair -> Property-matchingNotMember (CodePair secret attempt) =- forAllEval secret $ \eval ->- (eval == MM.evaluate secret attempt)- ==- (CodeSetTree.member secret $ MM.matching alphabet attempt eval) -matchingDisjoint :: Code -> Property-matchingDisjoint (Code attempt) =- forAllEval attempt $ \eval0 ->- forAllEval attempt $ \eval1 ->- let matching0 = MM.matching alphabet attempt eval0- matching1 = MM.matching alphabet attempt eval1- in eval0 == eval1 ||- CodeSetTree.null (CodeSetTree.intersection matching0 matching1)--evaluateCommutative :: CodePair -> Bool-evaluateCommutative (CodePair secret attempt) =- MM.evaluate secret attempt- ==- MM.evaluate attempt secret-- type CodeSetInt = CodeSetTree.T Int -evaluateMatching :: Code -> Property-evaluateMatching (Code attempt) =- forAllEval attempt $ \eval ->- all ((eval ==) . MM.evaluate attempt) $- take 100 $- CodeSet.flatten $- (MM.matching alphabet attempt eval :: CodeSetInt) -{--A more precise test would be to check-that for different numbers of rightPlace and rightSymbol-the codesets are disjoint-and their union is the set of all possible codes.-To this end we need a union with simplification or a subset test.--}-partitionSizes :: Code -> Bool-partitionSizes (Code attempt) =- fromIntegral (EnumSet.size alphabet) ^ length attempt- ==- sum (map snd (MM.partitionSizes alphabet attempt))---selectFlatten :: Code -> Property-selectFlatten (Code attempt) =- forAllEval attempt $ \eval ->- let set :: CodeSetInt- set = MM.matching alphabet attempt eval- in map (CodeSet.select set) [0 .. min 100 (CodeSet.size set) - 1]- ==- take 100 (CodeSet.flatten set)-- genFixedLengthCodes :: (NonEmptyC.Gen f) => Int -> QC.Gen (f [Int]) genFixedLengthCodes width = NonEmptyC.genOf $ QC.vectorOf width genElement @@ -179,56 +117,3 @@ check member against intersection with singleton -}---choiceLeftIdentity :: Choice Char -> Bool-choiceLeftIdentity a =- a == mergeChoice noChoice a--choiceRightIdentity :: Choice Char -> Bool-choiceRightIdentity a =- a == mergeChoice a noChoice--choiceCommutative :: Choice Char -> Choice Char -> Bool-choiceCommutative a b =- mergeChoice a b == mergeChoice b a--{--Unfortunately, this does not apply:--*Test.Mastermind EnumMap> let a = Choice (EnumMap.singleton 'x' 1) 1-*Test.Mastermind EnumMap> let b = Choice (EnumMap.singleton 'x' 1) 0-*Test.Mastermind EnumMap> let c = Choice (EnumMap.singleton 'y' 1) 1-*Test.Mastermind EnumMap> mergeChoice (mergeChoice a b) c-Choice (fromList [('x',1),('y',1)]) 2-*Test.Mastermind EnumMap> mergeChoice a (mergeChoice b c)-Choice (fromList [('x',1),('y',1)]) 1-*Test.Mastermind EnumMap> mergeChoice a b-Choice (fromList [('x',1)]) 1-*Test.Mastermind EnumMap> mergeChoice b c-Choice (fromList [('x',1),('y',1)]) 1--}-_choiceAssociative :: Choice Char -> Choice Char -> Choice Char -> Bool-_choiceAssociative a b c =- mergeChoice (mergeChoice a b) c- ==- mergeChoice a (mergeChoice b c)---tests :: [(String, IO ())]-tests =- ("matchingMember", quickCheck matchingMember) :- ("matchingNotMember", quickCheck matchingNotMember) :- ("matchingDisjoint", quickCheck matchingDisjoint) :- ("evaluateCommutative", quickCheck evaluateCommutative) :- ("evaluateMatching", quickCheck evaluateMatching) :- ("partitionSizes", quickCheck partitionSizes) :- ("selectFlatten", quickCheck selectFlatten) :- ("bestSeparatingCode", quickCheck bestSeparatingCode) :- ("intersections", quickCheck intersections) :- ("solve", quickCheck solve) :- ("choiceLeftIdentity", quickCheck choiceLeftIdentity) :- ("choiceRightIdentity", quickCheck choiceRightIdentity) :- ("choiceCommutative", quickCheck choiceCommutative) :- -- ("choiceAssociative", quickCheck choiceAssociative) :- []