«sharedTypes imports» {-# LANGUAGE CPP #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE BangPatterns #-} module Calcudoku.SharedTypes where import Data.Text ( Text, empty ) import Data.Data ( Data, Typeable ) #ifdef FAY import Data.Var instance Read Text -- Sadly this was left out of Data.Text in fay-base type World = Ref W type UpdateW a = W -> a -> W #endifI define a couple of type synonyms to make things clearer, and then define the Coord type, which is just the row and column of a cell in the puzzle. The first time I wrote this, I just used a tuple for this, but Fay gave me a lot of trouble with that, (when I tried to encode and decode the data, and also with pattern matching) so I decided it was easier to just define a new data type and let emacs query-replace go to work for me.
«sharedTypes Coord» type Row = Int type Column = Int type Value = Int type Coord = (Row, Column) type Should a = Either Text a cRow :: Coord -> Row cRow (row,_) = row cColumn :: Coord -> Column cColumn (_,col) = col mkC :: Row -> Column -> Coord mkC x1 x2 = (x1,x2)Next are the data types for the operations and the puzzle type. The non commutative operations work as follows. For Minus one of the cells holds the first value and the rest are subtracted from it. Similar for Divide, one if the cells in the Divide region is the dividend, and the rest are the divisors. A Mod region can have two cells, and Id can only be a one cell region. I use Empty internally for a basically non-existant region. The Subscriber PuzzleType constructor was an afterthought, and is not used in the Fay Client portion of the code. It us used in case the user tries to parse a puzzle from the calcudoku.org website that is for subscibers only.
«sharedTypes Region» data Operation = Plus | Minus | Times | Divide | Power | Or | And | Mod | Id | Empty deriving (Data, Eq, Read, Show, Typeable) defaultPuzzleType :: PuzzleType defaultPuzzleType = NullP data PuzzleType = Single | Double | Killer | Subscriber | NullP deriving (Data, Eq, Read, Show, Typeable) data Region = Region { regionResult :: !Value, regionOperation :: !Operation, regionCoords :: ![Coord] } deriving (Data, Eq, Read, Show, Typeable) emptyRegion :: Region emptyRegion = Region 0 Empty []Constraints are just lists of Regions, and a Board is a collection of data that totally defines the puzzle. Board is the important Data type that is shared between the client and the server.
«sharedTypes Board» data Constraints = Constraints { regions :: ![Region] } deriving (Data, Eq, Read, Show, Typeable) data Board = Board { puzzleType :: !PuzzleType, puzzleSide :: !Int, puzzleElementRange :: ![Int], puzzleConstraints :: !(Constraints,Constraints) } deriving (Data, Eq, Read, Show, Typeable) -- puzzleConstraints is a tuple because when we solve a Double PuzzleType -- we use flipConstraints to switch the constraints from one board -- to another. Thus fst puzzleConstraints is the current list of Regions defaultBoard :: Board defaultBoard = Board NullP 0 [] (Constraints [], Constraints [])After running this for a few months, I realized that editing existing puzzle boards was really a pain in the ..., so I impletment different modes to make editing easier. The original (and only mode) became ModeToggleCell. The ModeRemoveCell allows users to remove cells from any region. The ModeSelectRegion allows users to change regions, and finally ModeChangeValue lets users change the operator and/or value associated with a region.
«sharedTypes PuzzleMode» data PuzzleMode = ModeToggleCell | ModeRemoveCell | ModeSelectRegion | ModeChangeValue deriving (Data, Eq, Read, Show, Typeable)Another feature I added is to run the solver in a seperate process from the web server. This allows the solver to run longer and not hang up the user. I need to send the data in the Shared data type to the solver process.
«sharedTypes Shared» data Shared = Shared { sharedBoard :: !Board , sharedEvents :: ![CEvent] , sharedIPAddress :: !Text , sharedURL :: !Text , sharedActualURL :: !Text , sharedBgFile :: !Text , sharedPosted :: !Bool } deriving (Data, Eq, Read, Show, Typeable) -- For some unknown reason, if this is defined outside of main -- the JSON version of this data turns "" of type Text into an -- empty array. defaultText :: Text defaultText = Data.Text.empty defaultShared :: Shared defaultShared = Shared defaultBoard [] defaultText defaultText defaultText defaultText True
«clientTypes World» data W = W { board :: !Board -- The puzzle board , currentRegionIndex :: Maybe Int -- If we have a current region -- this is the index into the -- puzzleConstraints list , editing :: !PuzzleMode -- the current editing mode , cevents :: ![CEvent] -- a list of user generated -- browser events } deriving (Data, Eq, Read, Show, Typeable) defaultW :: W defaultW = W (Board NullP 0 [] (Constraints [],Constraints [])) Nothing ModeToggleCell []
«sharedTypes CEvent» data CEvent = P PuzzleType Int [Int] -- Type Size Range | D Int Int -- Discard region containing x,y | L Int Int -- l on cell x y | M PuzzleMode -- change editing mode | R (Maybe Int) -- change current region | V Operation Int -- change op and value of region | N Int Int Operation Int -- add a new region deriving (Data, Eq, Read, Show, Typeable)
«maps definitions» valueToOperationMap :: [(Text, Operation)] valueToOperationMap = [ ("Id" , Id) , ("Plus" , Plus) , ("Minus" , Minus) , ("Times" , Times) , ("Divide", Divide) , ("Power" , Power) , ("Mod" , Mod) , ("And" , And) , ("Or" , Or) , ("Empty" , Empty) ] valueToOperationTextMap :: [(Text, Text)] valueToOperationTextMap = [ ("", "Select operator for region") , ("Id" , "=") , ("Plus" , "+") , ("Minus" , "-") , ("Times" , "*") , ("Divide" , "/") , ("Power" , "^") , ("Mod" , "mod") , ("And" , "&") , ("Or" , "|") ] operationToTextMap :: [(Operation, Text)] operationToTextMap = [ (Id , "=") , (Plus , "+") , (Minus , "-") , (Times , "*") , (Divide , "/") , (Power , "^") , (Mod , "mod") , (And , "&") , (Or , "|") ] -- valueToOperationTextMap :: [(Text, Text)] -- valueToOperationTextMap = [ -- ("", "Select operator for region") -- , ("Id" , "=") -- , ("Plus" , "+") -- , ("Minus" , "-") -- , ("Times" , "×") -- , ("Divide" , "÷") -- , ("Power" , "^") -- , ("Mod" , "mod") -- , ("And" , "∧") -- , ("Or" , "∨") -- ] -- -- operationToTextMap :: [(Operation, Text)] -- operationToTextMap = [ -- (Id , "=") -- , (Plus , "+") -- , (Minus , "-") -- , (Times , "*") -- , (Divide , "/") -- , (Power , "^") -- , (Mod , "%") -- , (And , "&") -- , (Or , "|") -- ] xoperationToTextMap :: [(Operation,Text)] xoperationToTextMap = let textOps = map (\x -> fromMaybe (error . unpack $ "Missing op") (lookup x valueToOperationTextMap)) (map fst valueToOperationMap) opNames = map snd valueToOperationMap in zip opNames textOpsYou'ld think I'ld need the same three representations for the PuzzleType data type, but I thought I'ld implement it differently by having the option elements already populate the html file. Just like Perl, tmtowtdi. I also have explanations associated with each PuzzleType that are displayed to the user and stored as hidden elements in the html page.
«maps puzzleMaps» puzzleTypeMap :: [(Text, PuzzleType)] puzzleTypeMap = [ ("Single" , Single) , ("Double" , Double) , ("Killer" , Killer) , ("Subscriber" , Subscriber) , ("NullP" , NullP)] puzzleModeMap :: [(Text, PuzzleMode)] puzzleModeMap = [ ( "ModeToggleCell" , ModeToggleCell ) , ( "ModeRemoveCell" , ModeRemoveCell ) , ( "ModeSelectRegion" , ModeSelectRegion ) , ( "ModeChangeValue" , ModeChangeValue ) ] explainMap :: [(PuzzleType, Text)] explainMap = [ (Single , "#explainCreateRegion") , (Double , "#explainCreateRegion") , (Killer , "#explainKillerCreateRegion") ]
«updaters code» updateSide :: UpdateW Int updateSide w v = w { board = (board w) { puzzleSide = v}} updatePuzzleType :: UpdateW PuzzleType updatePuzzleType w v = w { board = (board w) { puzzleType = v}} updateElementRange :: UpdateW [Int] updateElementRange w v = w { board = (board w) { puzzleElementRange = v}} updateLow :: UpdateW Int updateLow w v = w { board = (board w) { puzzleElementRange = [v .. (Prelude.last . puzzleElementRange . board $ w)]}} updateHigh :: UpdateW Int updateHigh w v = w { board = (board w) { puzzleElementRange = [(Prelude.head . puzzleElementRange . board $ w) .. v]}} updateAll w v = w { board = (board w) { puzzleSide = v, puzzleElementRange = [1..v] }} currentRegion :: W -> Region currentRegion w = maybe (tError "currentRegion is Nothing") thisRegion (currentRegionIndex w) where thisRegion i = (regions $ currentConstraints w) !! i currentConstraints :: W -> Constraints currentConstraints w = fst . puzzleConstraints . board $ w currentRegionCoords :: W -> [Coord] currentRegionCoords w = maybe [] theseCoords (currentRegionIndex w) where theseCoords _ = regionCoords . currentRegion $ w updateCurrentRegion :: UpdateW Region updateCurrentRegion w r = let b = board w allRegions = regions . currentConstraints $ w newW = case (currentRegionIndex w) of Nothing -> let newRegions = r : allRegions newC = (Constraints newRegions , snd (puzzleConstraints b)) newB = b { puzzleConstraints = newC } in w { board = newB, currentRegionIndex = Just 0 } Just i -> let regionIsEmpty = Prelude.null . regionCoords (before,after) = splitAt i allRegions newRegions = if regionIsEmpty r then before ++ (Prelude.tail after) else before ++ [r] ++ (Prelude.tail after) newC = (Constraints newRegions , snd (puzzleConstraints b)) newB = b { puzzleConstraints = newC } w1 = if regionIsEmpty r then w { currentRegionIndex = Nothing } else w w2 = w1 { board = newB } in w2 in newW isNewRegion :: W -> Bool isNewRegion w = maybe True (const False) (currentRegionIndex w) removeEmptyRegions :: W -> W removeEmptyRegions w = let b = board w allRegions :: W -> [Region] allRegions = regions . currentConstraints nonEmptyRegions :: W -> [Region] nonEmptyRegions = filter hasCells . allRegions hasCells :: Region -> Bool hasCells = not . Prelude.null . regionCoords newB = b { puzzleConstraints = (Constraints (nonEmptyRegions w) , snd (puzzleConstraints b)) } newW = w { board = newB } in newW
«pickle compat» #ifdef FAY unwords :: [Text] -> Text unwords = ffi "%1.join(\" \")" #else unwords :: [Text] -> Text unwords = Data.Text.unwords #endif decodeBool :: Text -> Bool decodeBool x = x == "True" || x == "true" #ifdef FAY encodeInt :: Int -> Text encodeInt = ffi "JSON.stringify(%1)" decodeInt :: Text -> Int decodeInt = ffi "parseInt(%1)" decodeIntList :: Text -> [Int] decodeIntList = ffi "JSON.parse(%1)" encodeIntList :: [Int] -> Text encodeIntList = ffi "JSON.stringify(%1)" encodeBool :: Bool -> Text encodeBool b = if b then "true" else "false" sortIntList :: [(Int,Int)] -> [(Int,Int)] sortIntList = ffi "%1.sort( (function(x,y) { \ \ if (x[0] == y[0]) {return (x[1]-y[1])} else {return (x[0]-y[0])} ;}))" #else decodeFromText :: Read a => Text -> a decodeFromText = read . unpack encodeInt :: Int -> Text encodeInt = pack . show decodeInt :: Text -> Int decodeInt = decodeFromText decodeIntList :: Text -> [Int] decodeIntList = decodeFromText encodeIntList :: [Int] -> Text encodeIntList = pack . show encodeBool :: Bool -> Text encodeBool = pack . show sortIntList :: [(Int,Int)] -> [(Int,Int)] sortIntList = sort deblank :: Text -> Text deblank = Data.Text.filter nonWhiteSpace where nonWhiteSpace c = not (c `Prelude.elem` whiteSpace) #endifHere we go through each data type to build our way up to the Shared type, which is how the server (ghc) communicates with the broswer (fay). I start with the delimiters that we use.
«pickle chars» comma, underscore, space, newLine :: Text comma = "," underscore = "_" space = " " newLine = "\n" whiteSpace :: [Char] whiteSpace = [ ' ', '\n', '\t' ]I am assuming here that the text I am encoding does NOT contain brackets or commas. Since in my case the text is a bunch of filenames or urls, mostly generated by me, this is a safe assumption.
«pickle text» encodeSafeTextList :: [Text] -> Text encodeSafeTextList = encloseWith "[]" . (Data.Text.intercalate (comma <> newLine)) decodeSafeTextList :: Text -> [Text] decodeSafeTextList = balancedBreakText comma . deEnclose
«pickle code» encloseWith :: Text -> Text -> Text encloseWith brackets txt = Data.Text.head brackets `cons` txt `snoc` Data.Text.last brackets deEnclose :: Text -> Text deEnclose = Data.Text.init . Data.Text.tail deList :: Text -> [Text] deList = Data.Text.splitOn comma . deEnclose encodeIntPair :: (Int,Int) -> Text encodeIntPair (x,y) = encloseWith "()" (encodeInt x <> comma <> encodeInt y) decodeIntPair :: Text -> (Int,Int) decodeIntPair txt = if confirm then (decodeInt i1, decodeInt i2) else errorT "decodeIntPair" where (p1,r1) = maybe (errorT "decodeIntPair") id (uncons txt) (i1,t2) = only2 (splitOn comma r1) i2 = Data.Text.init t2 p2 = Data.Text.last t2 confirm = p1 == '(' && p2 == ')' -- !!! Note !!! the IntPairList that comes out of encode is SORTED encodeIntPairList :: [(Int,Int)] -> Text encodeIntPairList ps = encloseWith "[]" (Data.Text.intercalate comma (Prelude.map encodeIntPair . sortIntList $ ps)) decodeIntPairList :: Text -> [(Int,Int)] decodeIntPairList = Prelude.map decodeIntPair . joinTextPairs . deList where joinTextPairs :: [Text] -> [Text] joinTextPairs txts = go [] txts where go acc [] = Prelude.reverse acc go acc (x:y:rest) = go (x<>comma<>y:acc) rest go acc rest = error . unpack . unwords $ ["joinTextPairs:"] ++ acc ++ rest unEither :: Either c c -> c unEither = either id id unLeft :: Either Text c -> c unLeft = either (\x -> (errorT ("unLeft " <> x))) id balancedBreakText :: Text -> Text -> [Text] balancedBreakText delimiterText text = Prelude.map pack result where result = balancedBreak (unpack delimiterText) (unpack text) encodeOperation :: Operation -> Text encodeOperation = unEither . showLookup "encodeOperation error " valueToOperationMap decodeOperation :: Text -> Operation decodeOperation = unLeft . readLookup "decodeOperation error " valueToOperationMap encodePuzzleType :: PuzzleType -> Text encodePuzzleType = unEither . showLookup "encodePuzzleType error " puzzleTypeMap decodePuzzleType :: Text -> PuzzleType decodePuzzleType = unLeft . readLookup "decodePuzzleType error " puzzleTypeMap encodePuzzleMode :: PuzzleMode -> Text encodePuzzleMode = unEither . showLookup "encodePuzzleMode error " puzzleModeMap decodePuzzleMode :: Text -> PuzzleMode decodePuzzleMode = unLeft . readLookup "decodePuzzleMode error " puzzleModeMap encodeCEvent :: CEvent -> Text encodeCEvent cev = case cev of P x1 x2 x3 -> Data.Text.intercalate underscore [ "P" , encodePuzzleType x1, encodeInt x2, encodeIntList x3 ] L x1 x2 -> encodeWith2Ints "L" x1 x2 D x1 x2 -> encodeWith2Ints "D" x1 x2 M x1 -> "M_" <> encodePuzzleMode x1 R Nothing -> "R_N" R (Just x1) -> "R_J_" <> encodeInt x1 V x1 x2 -> Data.Text.intercalate underscore [ "V" , encodeOperation x1, encodeInt x2] N x1 x2 x3 x4 -> Data.Text.intercalate underscore [ "N", encodeInt x1, encodeInt x2, encodeOperation x3, encodeInt x4 ] where encodeWith2Ints str i1 i2 = Data.Text.intercalate underscore [ str , encodeInt i1, encodeInt i2] cEventMap :: [(Text, [Text] -> CEvent)] cEventMap = [ ("P" , \x -> P (decodePuzzleType (x!!1)) (decodeInt (x!!2)) (decodeIntList (x!!3)) ) , ("L" , \x -> L (decodeInt (x!!1)) (decodeInt (x!!2)) ) , ("D" , \x -> D (decodeInt (x!!1)) (decodeInt (x!!2)) ) , ("M" , \x -> M (decodePuzzleMode (x!!1)) ) , ("R" , \x -> R (if (x!!1) == "N" then Nothing else Just (decodeInt (x!!2))) ) , ("V" , \x -> V (decodeOperation (x!!1)) (decodeInt (x!!2)) ) , ("N" , \x -> N (decodeInt (x!!1)) (decodeInt (x!!2)) (decodeOperation (x!!3)) (decodeInt (x!!4)) ) ] decodeCEvent :: Text -> CEvent decodeCEvent txt = case mbF of Nothing -> errorT ("Nothing in decodeCEvent: " <> txt) Just f -> f splitted where splitted = splitOn underscore txt mbFF [] = errorT ("Null in decodeCEvent: " <> txt) mbFF (x:_) = lookup x cEventMap mbF = mbFF splitted encodeCEvents :: [CEvent] -> Text encodeCEvents = encloseWith "[]" . Data.Text.intercalate comma . (Prelude.map encodeCEvent) decodeCEvents :: Text -> [CEvent] decodeCEvents txt = if cs == [""] then [] else Prelude.map decodeCEvent $ cs where cs = balancedBreakText comma . deEnclose $ txt encodeRegion :: Region -> Text encodeRegion (Region v o cs) = "Region_" <> Data.Text.intercalate underscore [encodeInt v, encodeOperation o, encodeIntPairList cs] decodeRegion :: Text -> Region decodeRegion txt = (confirm region) (decodeInt result) (decodeOperation op) (decodeIntPairList coords) where (region,result,op,coords) = only4 . splitOn underscore $ txt confirm x = if x == "Region" then Region else errorT ("decodeRegion: " <> txt) encodeConstraint :: Constraints -> Text encodeConstraint (Constraints cs) = "Con_" <> encloseWith "[]" (Data.Text.intercalate comma (Prelude.map encodeRegion cs)) decodeConstraint :: Text -> Constraints decodeConstraint txt = (confirm constraint) (Prelude.map decodeRegion regs) where (constraint,rs) = (Data.Text.take 4 txt, Data.Text.drop 4 txt) regs = if rs == "[]" then [] else balancedBreakText comma . deEnclose $ rs confirm x = if x == "Con_" then Constraints else errorT ("decodeConstraints: " <> txt) toTuple :: [a] -> (a,a) toTuple [] = errorT "toTuple got null" toTuple [a] = (a,a) toTuple [a,b] = (a,b) toTuple _ = errorT "toTuple got more than two" encodeConstraints :: (Constraints,Constraints) -> Text encodeConstraints (c1,c2) = encloseWith "()" . (Data.Text.intercalate comma) $ [encodeConstraint c1, encodeConstraint c2] decodeConstraints :: Text -> (Constraints,Constraints) decodeConstraints c1c2 = (decodeConstraint c1, decodeConstraint c2) where cs = balancedBreakText comma . deEnclose $ c1c2 (c1,c2) = toTuple cs encodeBoard :: Board -> Text encodeBoard (Board pt ps pe pc) = encloseWith "[]" . (Data.Text.intercalate comma) $ [ "Bo", encodePuzzleType pt, encodeInt ps, encodeIntList pe, encodeConstraints pc] decodeBoard :: Text -> Board decodeBoard txt = ((confirm bo) (decodePuzzleType pt) (decodeInt ps) (decodeIntList pe) (decodeConstraints pc)) where (bo, pt, ps, pe, pc) = only5 . balancedBreakText comma . deEnclose $ txt confirm x = if x == "Bo" then Board else errorT ("decodeBoard: " <> txt) encodeShared :: Shared -> Text encodeShared (Shared bd cs t1 t2 t3 t4 b) = encodeSafeTextList $ (["Sh", encodeBoard bd, encodeCEvents cs] ++ [encodeSafeTextList [t1,t2,t3,t4]] ++ [encodeBool b]) decodeShared :: Text -> Shared decodeShared txt = (confirm sh) (decodeBoard bd) (decodeCEvents cs) t1 t2 t3 t4 (decodeBool b) where (sh, bd, cs, txts, b) = only5 . decodeSafeTextList . deblank $ txt (t1, t2, t3, t4) = only4 . decodeSafeTextList $ txts confirm x = if x == "Sh" then Shared else errorT ("decodeShared: " <> txt) only2 :: Show b => [b] -> (b, b) only2 [a,b] = (a,b) only2 x = errorT . unwords $ ["only2", "[", pack . show $ x, "]"] only4 :: Show d => [d] -> (d, d, d, d) only4 [a,b,c,d] = (a,b,c,d) only4 x = errorT . unwords $ ["only4", "[", pack . show $ x, "]"] only5 :: Show e => [e] -> (e, e, e, e, e) only5 [a,b,c,d,e] = (a,b,c,d,e) only5 x = errorT . unwords $ ["only5", "[", pack . show $ x, "]"]
«pickle test» r1 = Region 1 Plus [(1,2),(3,4)] r2 = Region 2 Times [(4,5),(6,7)] c1 = Constraints [r1,r2] c2 = Constraints [r2,r1] c3 = (c1,c2) b1 = Board Single 3 [-1,0,1] (c1,c1) b2 = Board Double 3 [-1,0,1] (c1,c2) ce1 = P Single 1 [-1,0,1] ce2 = D 1 2 ce3 = L 3 4 ce4 = M ModeChangeValue ce5 = R (Just 1) ce6 = V Divide 2 ce7 = N 1 2 Minus 3 ces = [ce1, ce2, ce3, ce4, ce5, ce6, ce7] s1 = Shared b1 ces "127.0.0.1" "!^withHat1^" "!^withHat2^" "!^withHat3^" True round1 = decodeRegion . encodeRegion round2 = decodeConstraints . encodeConstraints round3 = decodeBoard . encodeBoard round4 = decodeCEvent . encodeCEvent round5 = decodeCEvents . encodeCEvents round6 = decodeShared . encodeShared #ifdef Fay pickleTest :: Fay () #else pickleTest :: IO () #endif pickleTest = do print $ encodeConstraint c1 print (round1 r1) print (round2 c3) print (round3 b1) print (encodeBoard b1) mapM_ print (Prelude.map encodeCEvent ces) mapM_ print (Prelude.map round4 ces) print (encodeCEvents ces) mapM_ print $ (round5 ces) print (encodeShared s1) print (round6 s1)
«shared colorList» backgroundColorList :: [Text] backgroundColorList = map pack [ "DarkCyan" , "DarkGoldenRod" , "DarkGreen" , "DarkKhaki" , "DarkMagenta" , "DarkOliveGreen" , "Darkorange" , "DarkOrchid" , "DarkRed" , "DarkSalmon" , "DarkSeaGreen" , "DarkSlateBlue" , "DarkSlateGray" , "DarkTurquoise" , "DarkViolet" , "DeepPink" , "DeepSkyBlue" , "FireBrick" , "ForestGreen" , "HotPink" , "IndianRed" , "Indigo" , "Maroon" , "MidnightBlue" , "OrangeRed" , "Peru" , "RoyalBlue" , "SlateGray" ]
«jqbindings exposed» windowConfirm :: Text -> Fay Bool windowConfirm = ffi "window.confirm(%1)" simpleClone :: JQuery -> Fay JQuery simpleClone = ffi "%1['clone']()" stopImmediatePropagation :: Event -> Fay () stopImmediatePropagation = ffi "%1['stopImmediatePropagation']()" jPostBoard :: Text -> Text -> (Text -> Fay ()) -> Fay () jPostBoard = ffi "jQuery.ajax(%1, { data: %2, type: 'POST', processData: false, contentType: 'text/json', success: %3 })"
«shared utils 1» reverseMap :: [(a,b)] -> [(b,a)] reverseMap = Prelude.map (\x -> (snd x, fst x)) readLookup :: Eq a => Text -> [(a,b)] -> a -> Should b readLookup msg l x = maybe (Left msg) Right $ lookup x l showLookup :: Eq b => Text -> [(a,b)] -> b -> Should a showLookup msg l x = maybe (Left msg) Right $ lookup x (reverseMap l) zeroFill :: Show a => Int -> a -> Text zeroFill n x = pack $ replicate k '0' ++ show x where k = n - length (show x) chop :: Int -> [a] -> [[a]] chop _ [] = [] chop n xs = take n xs : chop n (drop n xs) merge2 :: [a] -> [a] -> [a] merge2 = m2 [] where m2 acc x [] = acc ++ x m2 acc [] x = acc ++ x m2 acc (x:xs) (y:ys) = m2 (acc ++ [x,y]) xs ys circularNext :: Eq a => [a] -> a -> [a] circularNext l wanted = cn l [] where cn [] _ = [] cn (x:xs) acc = if x == wanted then xs ++ acc ++ [x] else cn xs (acc ++ [x]) padList :: [[a]] -> [[a]] padList l = let maxLength = maximum . map length $ l pad1 x = if length x < maxLength then take maxLength . concat . repeat $ x else x in map pad1 . filter (not . null) $ l split :: Char -> String -> [String] split c str = words' (dropWhile isC str) where words' [] = [] words' s = case break isC s of (a,b) -> a : (split c) b isC = (==c) isPrefixOf :: (Eq a) => [a] -> [a] -> Bool isPrefixOf [] _ = True isPrefixOf _ [] = False isPrefixOf (x:xs) (y:ys)= x == y && isPrefixOf xs ys isSuffixOf :: (Eq a) => [a] -> [a] -> Bool isSuffixOf x y = reverse x `isPrefixOf` reverse y isInfixOf :: (Eq a) => [a] -> [a] -> Bool isInfixOf needle haystack = any (isPrefixOf needle) (tails haystack) tails :: [a] -> [[a]] tails xs = xs : case xs of [] -> [] _ : xs' -> tails xs' breakNear :: Int -> Char -> String -> [String] breakNear i c str = go str [] where go "" acc = concat . reverse $ acc go s acc = let (before,after) = splitAt i s (extra,rest) = break (== c) after in go rest ([before ++ extra] : acc) errorT :: Text -> a errorT = error . unpack
«utils 1» debug :: JQuery -> Fay () debug x = do d <- selectId "debug" appendTo d x & hvoid hvoid :: a -> Fay () hvoid _ = return ()englishInt does very simple grammatical correctness for nouns whose plural end with s.
«utils 2» idFromCoord :: Coord -> Text idFromCoord (r,c) = "T" <> Data.Text.intercalate "_" [showInt r, showInt c] coordFromId :: Text -> Coord coordFromId str = let digitTexts = split "_" (Data.Text.tail str) ints = Prelude.map readInt digitTexts in ((ints!!0) , (ints!!1))doubleQuote - I got tired of escaping everything, and solved it with this simple function.
«utils 3» setError :: Text -> Fay () setError msg = do statusError <- selectId "statusError" case msg of "" -> Fay.SafeJQuery.empty statusError & removeClass "error" _ -> appendText msg statusError & addClass "error" return () setExplain :: Text -> Fay () setExplain explainId = do explain <- selectId explainId & contents & simpleClone selectId "instructions" & Fay.SafeJQuery.empty & appendJQuery explain & hvoid setStatus :: Text -> Text -> Fay () setStatus statusId msg = do status <- selectId statusId setHtml msg status & hvoid whenTablet :: a -> (a -> Fay ()) -> (a -> Fay ()) -> Fay () whenTablet arg trueFun falseFun = do agent <- windowUserAgent let isTablet = "iPhone" `isInfixOf` agent || "iPad" `isInfixOf` agent if isTablet then trueFun arg else falseFun arg -- Monadic fold over the elements of a structure, -- associating to the left, i.e. from left to right. foldM :: (a -> b -> Fay a) -> a -> [b] -> Fay a foldM f z0 xs = foldr f' return xs z0 where f' x k z = f z x >>= k
«client initialize» type Handler a = World -> a -> Fay () type EventHandler = Handler Event type CellHandler = Handler Coord main :: Fay () main = ready $ newRef defaultW & initialize initialize :: World -> Fay () initialize world = do let version = "Version 2" void $ selectId "version" & setHtml version d <- Calcudoku.Client.isDebug purifyConfig let sizeRange = if d then [2..15] else [4..15] setupSize world sizeRange when d $ do (exposeIds ["debug", "debug-input", "debug-show"]) set world defaultW setupPuzzleMode world setupRange world 2 setupPuzzleTable world setupOperators world exposeIds ["range", "operator", "resultSpan" , "finish", "editingSpan"] isDebug :: Fay Bool isDebug = do url <- windowUrl let debug = T.unpack "debug" queryParms = dropWhile (/= '?') (T.unpack url) isDebug = not (null queryParms) && debug == (take (length debug) . tail $ queryParms) return isDebugAs the COBOL people like to say, numberOfRegionsDefined and numberOfSquaresToGo are self documenting, so I'll just explain what is going on in updateBoard. I wanted to have all singleton entries, ie cells whose operation is id, be the color white, all cells that aren't part of a region, either existing or being formed, be AliceBlue, cells that are part of the region being created be DeepSkyBlue, and the rest, the already existing regions be various different colors as defined by the backgroundColorList. Additionally, the first cell in every region should contain the operation and value for that region.
«client regions» ------------------------------ Regions ------------------------------ numberOfRegionsDefined :: W -> Int numberOfRegionsDefined = length . regions . currentConstraints numberOfSquaresToGo :: W -> Int numberOfSquaresToGo w = total - alreadyDefined where total = (puzzleSide . board $ w)^2 alreadyDefinedList = map (length . regionCoords) . regions . currentConstraints $ w alreadyDefined = if null alreadyDefinedList then 0 else sum alreadyDefinedListThis code divides the regions into the different classes I want to represent with a distinct background colors. rs is a list of all the exsiting regions. c is a list of lists of the co-ordinates of these regions. singletons filters c and keeps just the co-ordinates that have only length 1 regions. These correspond to regions that are associated to the = (id) operation. multiples are the list of co-ordinates with more than 1 cell in their regions. unmarked are cells that are not part of an existing finished region. Finally, beingDefined are the co-ordinates of the region currently being defined. Each of these types of cells are zipped together with their background color as described above. Finally allRegions is a list of all non-empty regions, even the one that is currently being defined and hasn't yet been completed.
«client regions 1» updateBoard :: W -> Fay () updateBoard w = do let rs :: [Region] rs = reverse . regions . currentConstraints $ w c :: [[Coord]] c = map regionCoords rs n = puzzleSide . board $ w allCoords = [ (i, j) | i <- [ 1 .. n] , j <- [1 .. n] ] singleTons :: [[Coord]] singleTons = filter ((== 1) . length) c multiples :: [[Coord]] multiples = filter ((/= 1) . length) c unmarked :: [[Coord]] unmarked = [filter (\x -> x `notElem` (concat c)) allCoords] beingDefined :: [[Coord]] beingDefined = [currentRegionCoords w] white :: [ ([Coord],Text) ] white = zip singleTons (repeat "White") colored :: [ ([Coord],Text) ] colored = zip multiples (concat . repeat $ backgroundColorList) yellow = zip unmarked (repeat "Yellow") red = zip beingDefined (repeat "Red") allRegions :: [Region] allRegions = regions . currentConstraints $ wThis double loop sets the background color of all the regions. It also resets the attributes and the values of each cell. They will be filled back in later, depending on their type.
«client regions 2» forM_ (white ++ colored ++ yellow ++ red) $ \i -> do let cells = fst i color = snd i forM_ cells $ \j -> do cell <- selectCell j let v = T.intercalate "" ["background-color:", color, ";"] setAttr "style" v cell & removeClass "cellConstrained" setVal "" cellThis double loop adds back the cellConstrained class to each region that is already defined and completed.
«client regions 3» forM_ (white ++ colored) $ \i -> do let cells = fst i color = snd i forM_ cells $ \j -> do cell <- selectCell j addClass "cellConstrained" cellThis loop restores the operation and the value for the first cell in every region.
«client regions 4» forM_ allRegions $ \g -> when (not (null (regionCoords g))) $ do firstCell <- selectCell . head . regionCoords $ g let operator = regionOperation g opText = maybe "" id $ lookup operator operationToTextMap value = showInt . regionResult $ g setVal (value <> opText) firstCell & hvoid selectId (encodePuzzleMode (editing w) <> "id") & checked & hvoid return () selectCell :: Coord -> Fay JQuery selectCell c = do let cellId = idFromCoord c selectId cellIddoRegionsExist simple checks whether any regions have been defined or are being defined. resetRegions resets the state of the World back to when no regions are yet defined. This is called if the user changes the size or type of the puzzle.
«client regions 5» doRegionsExist :: World -> Fay Bool doRegionsExist world = do w <- get world return $ numberOfRegionsDefined w > 0 resetRegions :: World -> Fay () resetRegions world = do w <- get world let newW = w { currentRegionIndex = Nothing } set world newW selectId "statusRegion" & empty setupPuzzleTable worldThere is a div in the html file for the current status. This is handy for the user so he can see what is going on, and handy for me to make sure I haven't gone off the rails. rangeStatus display the range of numbers that make up the puzzle, usually from 1 to n. regionStatus displays how many regions have been defined, and how many squares are left to be added to the regions.
«client status» ------------------------------ Status ------------------------------ rangeStatus :: W -> Fay () rangeStatus w = do let msg = Fay.JQUtils.unwords [ "The range runs from" , showInt (head . puzzleElementRange . board $ w) , "to" , showInt (last . puzzleElementRange . board $ w) ] setStatus "statusRange" msg regionStatus :: W -> Fay () regionStatus w = do let d = numberOfRegionsDefined w g = numberOfSquaresToGo w dText = englishInt d "region" gText = englishInt g "square" allText = dText <> " defined, " <> gText <> " remaining." setStatus "statusRegion" allTextThese functions setup the various widgets displayed on the page, and associate them with their handlers. change is a jQuery onchange callback, which is called whenever the status of the widget is changed. There are two little gotchas that are not obvious in this code. One was that to make an option be selected you have to set the selected atrribute to the value "selected." The other is that in the html file, the span associated with the puzzle table must have the attribute contenteditable set to "true."
«client setup» ------------------------------ Setup ------------------------------ setupPuzzleMode :: World -> Fay () setupPuzzleMode world = do editing <- select ("[name=editing]" :: Text) change (handlePuzzleMode world) editing setupSize :: World -> [Int] -> Fay () setupSize world l = do selectId "inputShow " & click (handleInputShow world) selectId "inputEvents" & click (handleInputEvents world) selectId "inputShared" & click (handleInputShared world) size <- selectId "size" change (handleSize world) size forM_ l $ \i -> do option <- select ("<option value=''></option>" :: Text) & setVal (showInt i) appendTo size option setText (showInt i) option ints :: [Int] ints = [0..] setupRange :: World -> Int -> Fay () setupRange world n = do fromSelect <- selectId "from" change (handleFrom world) fromSelect toSelect <- selectId "to" change (handleTo world) toSelect add fromSelect toSelect & empty -- Note: JQuery is not an instance of Eq -- thus the need for the zip junk and the wierd case statement forM_ (zip ints [fromSelect, toSelect]) $ \(j,div) -> forM_ [-n .. n] $ \i -> do option <- select ("<option value=''></option>" :: Text) setVal (showInt i) option let selected :: JQuery -> Fay JQuery selected = case j of 0 -> if i == 1 then setAttr "selected" "selected" else return 1 -> if i == n then setAttr "selected" "selected" else return s <- selected option appendTo div s setText (showInt i) s setupOperators :: World -> Fay () setupOperators world = do operator <- selectId "operator" & empty forM_ valueToOperationTextMap $ \vt -> do option <- select ("<option></option>" :: Text) & appendTo operator setVal (fst vt) option setHtml (snd vt) option setupPuzzleTable :: World -> Fay () setupPuzzleTable world = do w <- get world let n = puzzleSide . board $ w table <- select ("<table border='1' style='float:left'></table>" :: Text) p <- selectId "puzzleTable" empty p & appendJQuery table whenTablet p hvoid (keyup (checkForEnter world)) let rowColumn :: [[[Int]]] rowColumn = [ [ [i,j] | j<-[1..n] ] | i<-[1..n] ] forM_ rowColumn $ \row -> do tr <- select ("<tr></tr>" :: Text) & appendTo table forM_ row $ \ij -> do let c = ((ij!!0) , (ij!!1)) td <- select ("<td></td>" :: Text) & appendTo tr let button = Fay.JQUtils.unwords [ "<input type='button'" , "id='" <> idFromCoord c <> "'" , "class='tableCell'>" ] selectText button & appendTo td & click (handleSquareEvent world) setupPuzzleMode world updateBoard wHere all the various callback handlers are defined. worldChangeHandler takes one of two actions, depending upon whether regions exist or not. This is used when the user decides to change a parameter of the puzzle that would affect the currently existing regions. An alert window is presented to give the user a chance to change his mind. If he goes ahead, all existing regions are reset. backoutWithWorld resets the value of the widget associated with an event to a string that is computed based on the current state of the world.
«client handlers 1» ------------------------------ Handlers ------------------------------ worldChangeHandler :: World -> Event -> EventHandler -> EventHandler -> Fay () worldChangeHandler world e h1 h2 = do ok <- doRegionsExist world handler <- if ok then do msg <- fmap makeSafe $ selectId "worldChange" & getHtml ok <- windowConfirm msg return $ if ok then h1 else h2 else return h1 handler world e backoutWithWorld :: (W -> Text) -> EventHandler backoutWithWorld f world e = do w <- get world let v = f w t <- target e select t & setVal v return ()handleSize takes care of two cases. changeSize is called the first time the size is defined and resetSize is called when the size is changed after some regions have already been defined. Once the size is specified, we are ready to let the user define the range of values to be allowed in the puzzle, so we populate and display the "from" and "to" range select widgets based on the size. At this point we can expose the range widget, the puzzleType widget, and the puzzleTable table. The operation and value remain hidden until the puzzleType is known, since if the puzzleType is Killer thexn there is no need for the operation select widget, as it is always Plus. Also the instructions div set to explain to the user what to do next. The way this works is to copy the contents of a hidden div in the html file to the instructions div. The doParm function handles updating the state of world based on a parser, an updater, and an event.
«client handlers 2» handleSize :: EventHandler handleSize world e = worldChangeHandler world e changeSize resetSize where changeSize world e = do reinitialize <- doRegionsExist world when reinitialize (resetRegions world >> initialize world) w <- doParam world readInt updateAll e let pSize = puzzleSide . board $ w n = showInt pSize msg = "The size of the puzzle is " <> n <> " by " <> n setStatus "statusSize" msg setupRange world pSize puzzleType <- selectId "puzzleType" change (handlePuzzleType world) puzzleType setExplain "explainRange" rangeStatus w exposeIds ["range"] setupPuzzleTable world selectId "size" & setProp "disabled" "disabled" & hvoid resetSize = backoutWithWorld (showInt . puzzleSide . board)handleFrom and handleTo are pretty straightforward. handlePuzzleType is again more complicated because if it is called after regions have been defined, we must warn the user of the consequences of changing the type, namely that his regions will be lost. At this point we set up the Operation widget and if the puzzleType is Killer, we hide it, otherwise we expose it. We also set up the instructions div to tell the user what to do next.
«client handlers 3» handleFrom :: EventHandler handleFrom world e = do doParam world readInt updateLow e get world >>= rangeStatus handleTo :: EventHandler handleTo world e = do doParam world readInt updateHigh e get world >>= rangeStatus handlePuzzleType :: EventHandler handlePuzzleType world e = worldChangeHandler world e changePuzzleType resetPuzzleType where changePuzzleType world e = do reinitialize <- doRegionsExist world when reinitialize (resetRegions world) let parsePuzzle x = maybe (tError ("handlePuzzleType: No such puzzle type " <> x)) id (lookup x puzzleTypeMap) w <- doParam world parsePuzzle updatePuzzleType e let bd = board w logEvent world (P (puzzleType bd) (puzzleSide bd)(puzzleElementRange bd) ) setupOperators world if (puzzleType . board $ w) == Killer then do hideIds ["operator"] setExplain "explainKillerCreateRegion" else do exposeIds ["operator"] setExplain "explainCreateRegion" selectId "finishRegion" & click (handleFinishRegion world) exposeIds ["resultSpan" , "finish", "editingSpan" ] rangeStatus w toPuzzleText x = maybe "single" id (lookup x (reverseMap puzzleTypeMap)) resetPuzzleType = backoutWithWorld (toPuzzleText . puzzleType . board)To make recovering from mistakes easier, I've added the some editing modes. The default mode is to toggle a square's membership in the current region, adding it if it is not a member, and removing it if it is a member. Another mode is delete mode, which simply removes cells from their region. Another mode is to create a new region or make an existing region the current region when you click on a square. Finally in case you discover that you put the wrong operator or value on a region, there is a mode that allows you to change that too. That pretty much covers all the ways you can enter a puzzle and then discover that you made a mistake, and fix it easily. The modes I define are:
«client handlers 4» handlePuzzleMode :: EventHandler handlePuzzleMode world e = do setError "" stopImmediatePropagation e w <- get world modeName <- fmap makeSafe $ target e & select & getVal let newEditState = decodePuzzleMode modeName newW = w { editing = newEditState } set world newW logEvent world $ M newEditState if newEditState == ModeSelectRegion then handleFinishRegion world undefined else return ()handleSquareEvent is called whenever a square is clicked. It just get the co-ordinates of the square and calls handleCell to dispatch on the current mode
«client handlers 5» handleSquareEvent :: EventHandler handleSquareEvent world e = do tableCell <- target e & select coordId <- fmap makeSafe $ getAttr "id" tableCell let thisCoord = coordFromId coordId handleCell world thisCoord handleCell :: CellHandler handleCell world thisCoord = do w <- get world case editing w of ModeToggleCell -> handleModeToggleCell world thisCoord ModeRemoveCell -> handleModeRemoveCell world thisCoord ModeSelectRegion -> handleModeSelectRegion world thisCoord ModeChangeValue -> handleModeChangeValue world thisCoord w <- get world updateBoard whandleModeChangeValue is called when you want to change the operator and/or the value for a particular region. I try to cause the least surprise, so if the user is in this mode and clicks on a cell that is not in an existing region, we just toggle it into the current region. If the cell is in an existing region, we make sure the operator is valid and if it is, update the world global with changeValueEffect to reflect the change. This might be a good time to mention logging. Every event the user generates is logged and displayed to the right of the board. When problems occur, they can copy and past the event log and send it to me via email. I can then replay the events on my machine and where something when wrong.
«client handlers 6» handleModeChangeValue :: CellHandler handleModeChangeValue world thisCoord = do w <- get world let maybeExistingRegion = lookupCellInRegions w thisCoord w1 = w { currentRegionIndex = maybeExistingRegion , editing = ModeToggleCell } case maybeExistingRegion of Nothing -> do -- This cell was not in a region, -- so just proceed to toggle it into -- a new region set world w1 logEvent world $ R maybeExistingRegion logEvent world $ M ModeToggleCell handleCell world thisCoord Just existingRegionIndex -> do -- This cell is in an existing region -- and we want to change its operator -- and value, so make sure a valid -- (not null) operator and value are -- entered valid <- setupNewRegion w case valid of Nothing -> do -- The operator or value wasn't valid -- so change nothing and keep the world -- as it was return () Just r -> do -- The operator and value were valid, -- and there exists a region that contains -- this coord, so change this regions -- operator and value to the new ones let newW = changeValueEffect w1 (regionOperation r) (regionResult r) set world newW logEvent world $ M ModeToggleCell logEvent world $ R maybeExistingRegion logEvent world $ V (regionOperation r) (regionResult r) setError "" changeValueEffect :: W -> Operation -> Value -> W changeValueEffect w o v = let rExisting = currentRegion w newR = rExisting { regionResult = v , regionOperation = o } newW = updateCurrentRegion w newR in newWhandleModeSelectRegion is the handler called when a cell is clicked in the ModeSelectRegion mode. If the cell is in an existing region, it is made current and blinked. If the cell isn't in an existing region, we try to create a new region, which will probably fail unless the user was prescient enough to have selected a new operator and value. This mode is only enabled for one click, after which the mode returns to ModeToggleCell.
«client handlers 7» handleModeSelectRegion :: CellHandler handleModeSelectRegion world thisCoord = do w <- get world -- tPutStrLn $ "In handleModeSelectRegion with " <> (tShow thisCoord) let newW = selectRegionEffect w thisCoord set world newW logCoordEvent world thisCoord -- tPutStrLn $ "maybeExistingRegion is " <> (tShow maybeExistingRegion) maybe (handleCell world thisCoord) (const (selectCell thisCoord & blink "redBackground")) (currentRegionIndex newW) blink :: Text -> JQuery -> Fay () blink = ffi "%2['blink'](%1)" selectRegionEffect w c = w { currentRegionIndex = maybeExistingRegion , editing = ModeToggleCell } where maybeExistingRegion = lookupCellInRegions w chandleModeRemoveCell is the handler called when a cell is clicked in the ModeRemoveCell mode. If no regions exist, the user gets an error message, otherwise the cell is removed from its region. You will notice a worrying head . puzzleConstraints in the definition of removeFromBoard. Recall that puzzleConstraints is never empty. It has one element is the PuzzleType is Single or Killer, and two elements if the PuzzleType is Double. Removing a cell always makes no region current.
«client handlers 8» handleModeRemoveCell :: CellHandler handleModeRemoveCell world thisCoord = do thereAreRegions <- doRegionsExist world if thereAreRegions then do w <- get world let newW = removeCellEffect w thisCoord set world newW logCoordEvent world thisCoord regionStatus newW else do setError "There are no more regions to remove squares from" removeCellEffect :: W -> Coord -> W removeCellEffect w c = removeEmptyRegions $ w { board = newBoard , currentRegionIndex = Nothing} where newBoard = removeFromBoard c (board w) removeFromBoard :: Coord -> Board -> Board removeFromBoard c bd = bd { puzzleConstraints = (newConstraints bd , snd (puzzleConstraints bd)) } where newConstraints :: Board -> Constraints newConstraints = removeFromConstraint c . fst . puzzleConstraints removeFromConstraint :: Coord -> Constraints -> Constraints removeFromConstraint c1 = Constraints . map (removeFromRegion c1) . regions removeFromRegion :: Coord -> Region -> Region removeFromRegion c g = g { regionCoords = filter (/= c) (regionCoords g) }setupNewRegion is called whenever we want to create a new region. It checks that the prerequisites already exist, namely that the puzzle type, region operator, and region value are defined. error message is displayed if any of these are missing. If all is well, a new Region is returned.
«client handlers 9» setupNewRegion :: W -> Fay (Maybe Region) setupNewRegion w = do if pType == NullP then setError "You must select a puzzle type!" >> return Nothing else do operator <- if (puzzleType . board $ w ) == Killer then return "Plus" else fmap makeSafe $ selectId "operator" & getVal let op = lookup operator valueToOperationMap result <- fmap makeSafe $ selectId "result" & getVal setError "" maybe (setError "You must select an operator!" >> return Nothing) (handleOperator result) op where pType = puzzleType . board $ w handleOperator result theOperator = do setError "" if result == "" then setError "You must select a value!" >> return Nothing else do return $ Just (Region (readInt result) theOperator [])handleModeToggleCell is called when a square is clicked and we are in ModeToggleCell mode. If we are in a new region, we make sure its prerequisites are valid, and then toggle this cell into or out of the current region. We are slightly smart about the operator, namely if we know in advance how many cells are needed by a specific operator, we immediately finish that region. For example, if the operator is Id, which means that the specified cell must have this value, then we know the region can only be one cell large. This logic is handled in the runToggle function below. We also check to see of all of the cells have been used, and if so finish the current region.
«client handlers 10» handleModeToggleCell :: CellHandler handleModeToggleCell world thisCoord = do {- When I finally tried this on my ipad, I discovered that every time I touched a square in the puzzle, the keyboard would pop up. I tried not attaching the keyup handler to the table, but that had no effect, so after searching around discovered I could blur the active element and that would push the keyboard back down into its place. -} w <- get world whenTablet () (const hideIpadKeyboard) hvoid if isNewRegion w then do let cellIsAlreadyInARegion = maybe False (const True) (lookupCellInRegions w thisCoord) if cellIsAlreadyInARegion then do setError "This cell is in a finished region, and cannot be toggled" return () else do valid <- setupNewRegion w case valid of Nothing -> updateBoard w Just r -> do logEvent world $ N (cRow thisCoord) (cColumn thisCoord) (regionOperation r) (regionResult r) runToggle world thisCoord r setError "" else do logCoordEvent world thisCoord runToggle world thisCoord (currentRegion w) runToggle :: World -> Coord -> Region -> Fay () runToggle world thisCoord r = do w <- get world let w1 = toggleCellEffect w thisCoord r theOperator = regionOperation r set world w1 case length (regionCoords (currentRegion w1)) of 1 -> when (theOperator == Id) $ handleFinishRegion world undefined 2 -> when (theOperator == Mod) $ handleFinishRegion world undefined otherwise -> return () w3 <- get world regionStatus w3 updateBoard w3 let squaresLeft = numberOfSquaresToGo w3 when (squaresLeft == 0) $ handleFinishRegion world undefined return () toggleCellEffect :: W -> Coord -> Region -> W toggleCellEffect w c r = updateCurrentRegion w (toggleMembership c r) toggleMembership :: Coord -> Region -> Region toggleMembership c g = let isInRegion = c `elem` regionCoords g regionWouldBeEmpty = isInRegion && length (regionCoords g) == 1 result = if regionWouldBeEmpty then g else if isInRegion then removeFromRegion c g else g { regionCoords = regionCoords g ++ [c]} in resulthandleFinishRegion as you might guess, we get here when a region has been finished. We do nothing if there is no current region, otherwise we reset the operator and result fields of the form, set the current region to Nothing, redisplay the board and check if all of the squares have been used. If so we call handleAllCellsDefined which will might send the board to server. handleAllCellsDefined is called whenever there are no more squares left to define. That does not necessarily mean we are completely finished with the puzzle. Some calcudoku puzzles are Doubles, meaning that the same solution must exist for two different puzzles. If the puzzle type is Double and we have only defined one board, we need to go and define the other board. This involves creating a new board with the fst of puzzleConstraints null, and the snd (second element) of puzzleConstraints equal to the just defined set of constraints for the first half of the double puzzle. If the puzzle type isn't Double, or we have finished defining the second half of a Double puzzle, we call postToSnap to send the world to the server. Once the puzzle is defined completely, we set the answer region to a message that the answer should appear here shortly. This is in case the puzzle takes too long to solve. We then run an ajax call to the server with the Board as the posted data. If all goes well, the server responds with a solution which is put into the answer div. The answer returned should be plain text that is stuffed into an pre element. Just for fun, we also display the data sent to the server in the post request in the puzzleData div. I found this useful for debugging, and perhaps if the user is a programmer it will help them understand what is going on.
«client finishRegion» handleFinishRegion :: EventHandler handleFinishRegion world _ = do w1 <- get world when (not $ isNewRegion w1) $ do let g = currentRegion w1 logEvent world (V (regionOperation g) (regionResult g) ) logEvent world (R Nothing) selectId "operator" & setVal "" selectId "result" & setVal "" w2 <- get world let newW = w2 { currentRegionIndex = Nothing } set world newW updateBoard newW let squaresLeft = numberOfSquaresToGo newW regionStatus newW if squaresLeft /= 0 then return () else handleAllCellsDefined world -- tPutStrln $ "In handleFinishRegion2 with " <> (tShow newW) handleAllCellsDefined :: World -> Fay () handleAllCellsDefined world = do w <- get world if (puzzleType . board $ w) == Double then do let weAreDone = not . null . regions . snd . puzzleConstraints . board $ w if weAreDone then setExplain "doubleFinished" >> postToSnap w else do let b = board w newB = b { puzzleConstraints = (Constraints [] , fst . puzzleConstraints $ b)} newW = w { board = newB } setExplain "secondPartOfDouble" hideIds ["size", "puzzleType", "range" ] set world newW setupPuzzleTable world else postToSnap wpostToSnap is called when we have finished defining the puzzle. It tells the user to be patient, and cleans up any empty regions that may have been created. It sets the puzzleData region on the web page to the data that is about to be sent to the server, creates a new Shared data type, and packs it all of to send to the server. The server should reply with a simple html message, which will be displayed in the answer div once it is received.
«client postToSnap» onBoth :: (a -> b) -> (a,a) -> (b,b) onBoth f (x,y) = (f x, f y) postToSnap :: W -> Fay () postToSnap w = do let bd = board w setExplain "puzzleFinished" -- tPutStrLn $ "Board: is done" <> showWorld w let -- cleanConstraints = onBoth removeEmptyRegions (puzzleConstraints bd) -- removeEmptyRegions c = Constraints $ -- filter (\g -> regionOperation g /= Empty) (regions c) -- newB = bd { puzzleConstraints = cleanConstraints } shared = defaultShared { sharedBoard = bd -- newB , sharedEvents = (reverse $ cevents w) , sharedPosted = False } textToPost = "Encode:" <> (encodeShared shared) selectId "puzzleData" & empty & hvoid tellUser textToPost jPostBoard "fayParse" textToPost setAnswer tellUser :: Text -> Fay () tellUser sharedText = do let formatted = T.unlines . map T.pack . breakNear 80 ',' $ (T.unpack sharedText) lengthOfShared = T.length sharedText t1 = "The answer should appear here shortly" t2 = "<hr/><br/>This is the data that is being sent to the server, \ \if you have problems, please copy and paste it in a message to Henry.<br/>" t3 = "<br/>Post length is: " <> showInt lengthOfShared <> " bytes</p>" t4 = T.unlines [t2,"<pre>",formatted,"</pre>",t3] selectId "answer" & empty & setHtml t1 selectId "after-answer" & empty & setHtml t4 return () setAnswer :: Text -> Fay () setAnswer s = do -- tPutStrLn "entered setAnswer" selectId "answer" & empty & Fay.SafeJQuery.append s & hvoidlogEvent and logCoordEvent are called throughout to log the event so that it can be replayed.
«client logging» logCoordEvent :: World -> Coord -> Fay () logCoordEvent world thisCoord = logEvent world (L (cRow thisCoord) (cColumn thisCoord) ) logEvent :: World -> CEvent -> Fay () logEvent world e = do puzzleEvents <- selectId "puzzleEvents" appendText (" " <> encodeCEvent e ) puzzleEvents & hvoid w <- get world let newW = w {cevents = e : cevents w} set world newW -- tPutStrLn $ "logEvent Event" <> showCEvent e -- tPutStrLn $ "logEvent World" <> showWorld newWcheckForEnter is an event handler that checks to see if the enter key has been pressed. Pressing the enter key means that the current region being defined is finished. It is easier to do this rather than clicking on the "finish region" button.
«client checkForEnter» checkForEnter :: EventHandler checkForEnter world e = do code <- which e -- tPutStrLn $ "checkForEnter: " <> (showInt code) when (code == 13) $ handleFinishRegion world esimulateEvents unravels a bunch of space delimited events that were recorded while the user was doing his input. These events are displayed as the user enters them to the right of the puzzle board. If something goes wrong, the user can easily copy and paste these events and send them to me. I can run them through this function and recreate the board. It has come in handy several times now. runEvents calls simulateEvents to create a new world global that gets posted to the server. handleInputEvents is only available in debug mode. It reads worldEvents textbox and runs the events
«client events» simulateEvent :: CEvent -> W -> W simulateEvent cev w = let bd = board w result = case cev of P x1 x2 x3 -> w { board = bd {puzzleType = x1, puzzleSide = x2, puzzleElementRange = x3}} L x1 x2 -> let r = currentRegion w c = mkC x1 x2 mode = editing w newW = case mode of ModeToggleCell -> toggleCellEffect w c r ModeRemoveCell -> removeCellEffect w c ModeSelectRegion -> selectRegionEffect w c ModeChangeValue -> w in newW M x1 -> w { editing = x1 } R x1 -> w {currentRegionIndex = x1 } V x1 x2 -> changeValueEffect w x1 x2 N x1 x2 x3 x4 -> toggleCellEffect w (mkC x1 x2) (Region x4 x3 []) otherwise -> tError $ "simulate events error " <> tShow otherwise in result readEvents :: Text -> Fay [CEvent] readEvents textEvents = do selectId "debug-show" & setHtml textEvents let eventList = decodeCEvents textEvents return eventList runEvents :: Text -> Fay () runEvents textEvents = do selectId "debug-show" & setHtml textEvents events <- readEvents textEvents finalW <- foldM simulate1 defaultW events newWorld <- newRef finalW setupPuzzleTable newWorld let squaresLeft = numberOfSquaresToGo finalW if squaresLeft /= 0 then return () else postToSnap finalW where simulate1 a b = do -- tPutStrLn (showCEvent b) return $ simulateEvent b a handleInputEvents :: EventHandler handleInputEvents _ _ = do inputEvents <- fmap makeSafe $ selectId "inputEvents" & prev & getVal runEvents inputEventsSimilar to handleInputEvents, handleInputShared runs the board defined by the global Shared. handleInputShow expects to receive a Shared that was written by haskell's show instance, suitable for a read. Why do I need two different ways to run the solver? Well, if instead of entering the puzzle via fay, the user uses this solver to grab and parse a calcudoku puzzle from calcudoku.org, then there won't be any events defined. The server will parse the puzzle it finds at the specified url, create a Shared, and send it off to the solver. I get a copy of the Shared data if something goes wrong via email.
«client handleInputShared» handleInputShared :: EventHandler handleInputShared _ _ = do sharedText <- fmap (deblank . makeSafe) $ selectId "inputShared" & prev & getVal let textToPost = "Encode:" <> sharedText tellUser textToPost jPostBoard "fayParse" textToPost setAnswer handleInputShow :: EventHandler handleInputShow _ _ = do tPutStrLn "handleInputShow" showText <- fmap makeSafe $ selectId "inputShow" & prev & getVal let textToPost = "Show:" <> showText tellUser textToPost jPostBoard "fayParse" textToPost setAnswerlookupCellInRegions looks to see if the cell is in an existing region. If so, it returns Just the index of the region in the list of current constraints. If not, it returns Nothing.
«client lookupCellInRegions» lookupCellInRegions :: W -> Coord -> Maybe Int lookupCellInRegions w c = let justRegions = regions . currentConstraints $ w indexedRegions = zip justRegions [0..] go [] = Nothing go (r1:rs) = if c `elem` (regionCoords . fst $ r1) then Just (snd r1) else go rs in go indexedRegionsAs you might guess, we get here when a region has been finished. At this point we have to add the current region to the list of Constraints, and reset the current region to null. We also check to see if there are any more squares left to be defined. If not we need to do more finishing.
«fayJQUtils bind» (&) :: Fay a -> (a -> Fay b) -> Fay b x & y = x >>= y infixl 1 &Next we include some DOM manipulations that we have to do frequently. selectId happens so often, that I got tired of typing the "#" sign. Hopefully they are pretty much self explanatory.
«fayJQUtils dom» selectId :: Text -> Fay JQuery selectId = ffi "jQuery('#'+%1)" selectText :: Text -> Fay JQuery selectText = ffi "window['jQuery'](%1)" appendText :: Text -> JQuery -> Fay JQuery appendText = ffi "%2['append'](%1)" prependText :: Text -> JQuery -> Fay JQuery prependText = ffi "%2['prepend'](%1)" exposeIds :: [Text] -> Fay () exposeIds l = forM_ l $ \i -> selectId i & removeClass "hidden" hideIds :: [Text] -> Fay () hideIds l = forM_ l $ \i -> selectId i & addClass "hidden" enable :: JQuery -> Fay JQuery enable = ffi "%1['prop'](\"disabled\",false)" disable :: JQuery -> Fay JQuery disable = ffi "%1['prop'](\"disabled\",true)" checked :: JQuery -> Fay JQuery checked = ffi "%1['prop'](\"checked\",true)" unchecked :: JQuery -> Fay JQuery unchecked = ffi "%1['prop'](\"checked\",false)" scrollOneLine :: JQuery -> Fay () scrollOneLine = ffi "%1['scroll']()" jsBlur :: JQuery -> Fay JQuery jsBlur = ffi "%1['blur']()"Here are some text and show functions that I need all the time, and for some reason didn't work for me with pack . show when I tried them. Again, hopefully they are pretty much self explanatory.
«fayJQUtils show and text» trim :: Text -> Text trim = ffi "jQuery['trim'](%1)" englishInt :: Int -> Text -> Text englishInt n t = case n of 0 -> "No " <> t <> "s" 1 -> "1 " <> t x -> showInt x <> " " <> t <> "s" showJQuery :: JQuery -> Text showJQuery = ffi "JSON.stringify(%1)" showElement :: Element -> Text showElement = ffi "JSON.stringify(%1)" showEvent :: Event -> Text showEvent = ffi "JSON.stringify(%1)" showList :: [Int] -> Text showList = ffi "JSON.stringify(%1)" showArbitrary :: a -> Text showArbitrary = ffi "JSON.stringify(%1)" doubleQuote :: Text -> Text doubleQuote s = "\"" <> s <> "\"" deblank :: Text -> Text deblank = ffi "%1.replace(/\\s+/g,'')"I wish these had been included in the Prelude defined by fay-base.
«fayJQUtils missing from the prelude» split :: Text -> Text -> [Text] split = ffi "%2.split(%1)" words :: Text -> [Text] words = ffi "%1.split(\" \")" unwords :: [Text] -> Text unwords = ffi "%1.join(\" \")" readBool :: Text -> Bool readBool x = if x == "True" then True else FalseHere are some parsers that didn't work as read . unpack, so I added them here.
«fayJQUtils parsers» readInt :: Text -> Int readInt = ffi "parseInt(%1)" readIntList :: Text -> [Int] readIntList = ffi "JSON.parse(%1)" readDouble :: Int -> Text -> Double readDouble = ffi "parseFloat(%2,%1) || 0"Some miscellaneous functions that come in handy.
«fayJQUtils other» exists :: JQuery -> Bool exists = ffi "%1.length > 0" alert :: Text -> Fay () alert = ffi "alert(%1)" jPost :: Text -> Automatic f -> (Automatic g -> Fay ()) -> Fay () jPost = ffi "jQuery.ajax(%1, { data: JSON.stringify(%2), type: 'POST', processData: false, contentType: 'text/json', success: %3 })" windowUrl :: Fay Text windowUrl = ffi "window.location.href" windowUserAgent :: Fay Text windowUserAgent = ffi "navigator.userAgent" hideIpadKeyboard :: Fay () hideIpadKeyboard = ffi "document.activeElement.blur()" isPrefixOf :: Text -> Text -> Bool isPrefixOf = ffi "%2.indexOf(%1) == 0" isInfixOf :: Text -> Text -> Bool isInfixOf = ffi "%2.indexOf(%1) >= 0"These guys want a string in Haskell, but Fay wants Text.
«fayJQUtils text versions» tError :: Text -> a tError = error . unpack tPutStrLn :: Text -> Fay () tPutStrLn = Data.Text.putStrLn tShow :: Show a => a -> Text tShow = pack . show tPrint :: Show a => a -> Fay () tPrint = tPutStrLn . tShow serialize :: JQuery -> Fay Text serialize = ffi "%1['serialize']()" consoleLog :: JQuery -> Fay () consoleLog = ffi "console['log'](%1)" safeTail :: [a] -> [a] safeTail l = if Prelude.null l then l else Prelude.tail lI implement some Read/Show instances with lookup from the Prelude. Also doParam is a helper that is a little complicated. It take a reference to a variable, usually the global state world, a parser function that converts Text to an internal type, and updating function that modifies the internal structure of the world, and a Javascript event. It decodes the event, and updates the world to the new value. It returns the new world, wrapped in the Fay monad.
«fayJQUtils read show with maps» doParam :: Ref b -> (Text -> a) -> (b -> a -> b) -> Event -> Fay b doParam world parser updateF e = do t <- target e sval <- fmap makeSafe $ select t & getVal w <- get world let newW = updateF w (parser sval) set world newW return newW redirect :: Text -> Fay () redirect = ffi "window.location.href = %1" debug :: Text -> Fay () -- debug _ = return () debug = Data.Text.putStrLn onEvent :: EventType -> (Event -> Fay ()) -> Fay () onEvent = ffi "jQuery(document).bind(%1,%2)" isDebug :: Fay Bool isDebug = do url <- windowUrl let debug = unpack "debug" queryParms = dropWhile (/= '?') (unpack url) debugging = Prelude.not (Prelude.null queryParms) && debug == (Prelude.take (Prelude.length debug) . Prelude.tail $ queryParms) return debugging cloneId :: Text -> Fay JQuery cloneId idText = do selectId idText & clone WithoutDataAndEvents & removeAttr "id" & removeClass "hidden" fmap :: (a -> b) -> Fay a -> Fay b fmap f a = do a1 <- a return (f a1)I need to guard against malicious input, So I downloaded the Dom Purify library to help eliminate cross site scripting attacks.
«fayJQUtils cross site scripting» purifyConfig :: Fay () purifyConfig = ffi "DOMPurify.setConfig({SAFE_FOR_JQUERY: true})"
Sitemap
Go up to Haskell Go up to Home Page of Nadine Loves Henry
Go back to How to use Data.Lens Continue with A Medium Sized Snaplet Example