packages feed

goatee 0.1.1 → 0.2.0

raw patch · 55 files changed

+7056/−6040 lines, 55 filesdep −test-frameworkdep −test-framework-hunitdep ~template-haskellPVP ok

version bump matches the API change (PVP)

Dependencies removed: test-framework, test-framework-hunit

Dependency ranges changed: template-haskell

API changes (from Hackage documentation)

- Game.Goatee.Common: Seq :: (m ()) -> Seq m
- Game.Goatee.Common: fromLeft :: Either a b -> a
- Game.Goatee.Common: fromRight :: Either a b -> b
- Game.Goatee.Common: instance Monad m => Monoid (Seq m)
- Game.Goatee.Common: listDeleteIndex :: Int -> [a] -> [a]
- Game.Goatee.Common: newtype Seq m
- Game.Goatee.Common: onLeft :: (a -> c) -> Either a b -> Either c b
- Game.Goatee.Common: onRight :: (b -> c) -> Either a b -> Either a c
- Game.Goatee.Sgf.Board: BoardState :: [[CoordState]] -> Bool -> Bool -> ArrowList -> LineList -> LabelList -> Integer -> Color -> Int -> Int -> GameInfo -> BoardState
- Game.Goatee.Sgf.Board: CoordState :: Bool -> Maybe Color -> Maybe Mark -> Bool -> Bool -> CoordState
- Game.Goatee.Sgf.Board: Cursor :: Maybe Cursor -> Int -> Node -> BoardState -> Cursor
- Game.Goatee.Sgf.Board: GameInfo :: RootInfo -> Maybe String -> Maybe String -> Maybe String -> Maybe String -> Maybe String -> Maybe String -> Maybe Ruleset -> Maybe Rational -> Maybe String -> Maybe GameResult -> Maybe String -> Maybe String -> Maybe String -> Maybe String -> Maybe String -> Maybe String -> Maybe String -> Maybe String -> Maybe String -> Maybe String -> Maybe String -> GameInfo
- Game.Goatee.Sgf.Board: RootInfo :: Int -> Int -> VariationMode -> RootInfo
- Game.Goatee.Sgf.Board: boardArrows :: BoardState -> ArrowList
- Game.Goatee.Sgf.Board: boardBlackCaptures :: BoardState -> Int
- Game.Goatee.Sgf.Board: boardCoordState :: Coord -> BoardState -> CoordState
- Game.Goatee.Sgf.Board: boardCoordStates :: BoardState -> [[CoordState]]
- Game.Goatee.Sgf.Board: boardGameInfo :: BoardState -> GameInfo
- Game.Goatee.Sgf.Board: boardHasDimmed :: BoardState -> Bool
- Game.Goatee.Sgf.Board: boardHasInvisible :: BoardState -> Bool
- Game.Goatee.Sgf.Board: boardHeight :: BoardState -> Int
- Game.Goatee.Sgf.Board: boardLabels :: BoardState -> LabelList
- Game.Goatee.Sgf.Board: boardLines :: BoardState -> LineList
- Game.Goatee.Sgf.Board: boardMoveNumber :: BoardState -> Integer
- Game.Goatee.Sgf.Board: boardPlayerTurn :: BoardState -> Color
- Game.Goatee.Sgf.Board: boardWhiteCaptures :: BoardState -> Int
- Game.Goatee.Sgf.Board: boardWidth :: BoardState -> Int
- Game.Goatee.Sgf.Board: colorToMove :: Color -> Coord -> Property
- Game.Goatee.Sgf.Board: coordDimmed :: CoordState -> Bool
- Game.Goatee.Sgf.Board: coordMark :: CoordState -> Maybe Mark
- Game.Goatee.Sgf.Board: coordStar :: CoordState -> Bool
- Game.Goatee.Sgf.Board: coordStone :: CoordState -> Maybe Color
- Game.Goatee.Sgf.Board: coordVisible :: CoordState -> Bool
- Game.Goatee.Sgf.Board: cursorBoard :: Cursor -> BoardState
- Game.Goatee.Sgf.Board: cursorChild :: Cursor -> Int -> Cursor
- Game.Goatee.Sgf.Board: cursorChildCount :: Cursor -> Int
- Game.Goatee.Sgf.Board: cursorChildIndex :: Cursor -> Int
- Game.Goatee.Sgf.Board: cursorChildPlayingAt :: Coord -> Cursor -> Maybe Cursor
- Game.Goatee.Sgf.Board: cursorChildren :: Cursor -> [Cursor]
- Game.Goatee.Sgf.Board: cursorModifyNode :: (Node -> Node) -> Cursor -> Cursor
- Game.Goatee.Sgf.Board: cursorNode :: Cursor -> Node
- Game.Goatee.Sgf.Board: cursorParent :: Cursor -> Maybe Cursor
- Game.Goatee.Sgf.Board: cursorProperties :: Cursor -> [Property]
- Game.Goatee.Sgf.Board: cursorRoot :: Cursor -> Cursor
- Game.Goatee.Sgf.Board: cursorVariations :: VariationModeSource -> Cursor -> [(Coord, Color)]
- Game.Goatee.Sgf.Board: data BoardState
- Game.Goatee.Sgf.Board: data CoordState
- Game.Goatee.Sgf.Board: data Cursor
- Game.Goatee.Sgf.Board: data GameInfo
- Game.Goatee.Sgf.Board: data RootInfo
- Game.Goatee.Sgf.Board: emptyGameInfo :: RootInfo -> GameInfo
- Game.Goatee.Sgf.Board: gameInfoAnnotatorName :: GameInfo -> Maybe String
- Game.Goatee.Sgf.Board: gameInfoBasicTimeSeconds :: GameInfo -> Maybe Rational
- Game.Goatee.Sgf.Board: gameInfoBlackName :: GameInfo -> Maybe String
- Game.Goatee.Sgf.Board: gameInfoBlackRank :: GameInfo -> Maybe String
- Game.Goatee.Sgf.Board: gameInfoBlackTeamName :: GameInfo -> Maybe String
- Game.Goatee.Sgf.Board: gameInfoCopyright :: GameInfo -> Maybe String
- Game.Goatee.Sgf.Board: gameInfoDatesPlayed :: GameInfo -> Maybe String
- Game.Goatee.Sgf.Board: gameInfoEntererName :: GameInfo -> Maybe String
- Game.Goatee.Sgf.Board: gameInfoEvent :: GameInfo -> Maybe String
- Game.Goatee.Sgf.Board: gameInfoGameComment :: GameInfo -> Maybe String
- Game.Goatee.Sgf.Board: gameInfoGameName :: GameInfo -> Maybe String
- Game.Goatee.Sgf.Board: gameInfoOpeningComment :: GameInfo -> Maybe String
- Game.Goatee.Sgf.Board: gameInfoOvertime :: GameInfo -> Maybe String
- Game.Goatee.Sgf.Board: gameInfoPlace :: GameInfo -> Maybe String
- Game.Goatee.Sgf.Board: gameInfoResult :: GameInfo -> Maybe GameResult
- Game.Goatee.Sgf.Board: gameInfoRootInfo :: GameInfo -> RootInfo
- Game.Goatee.Sgf.Board: gameInfoRound :: GameInfo -> Maybe String
- Game.Goatee.Sgf.Board: gameInfoRuleset :: GameInfo -> Maybe Ruleset
- Game.Goatee.Sgf.Board: gameInfoSource :: GameInfo -> Maybe String
- Game.Goatee.Sgf.Board: gameInfoToProperties :: GameInfo -> [Property]
- Game.Goatee.Sgf.Board: gameInfoWhiteName :: GameInfo -> Maybe String
- Game.Goatee.Sgf.Board: gameInfoWhiteRank :: GameInfo -> Maybe String
- Game.Goatee.Sgf.Board: gameInfoWhiteTeamName :: GameInfo -> Maybe String
- Game.Goatee.Sgf.Board: instance Eq RootInfo
- Game.Goatee.Sgf.Board: instance Show ApplyMoveGroup
- Game.Goatee.Sgf.Board: instance Show ApplyMoveParams
- Game.Goatee.Sgf.Board: instance Show BoardState
- Game.Goatee.Sgf.Board: instance Show CoordState
- Game.Goatee.Sgf.Board: instance Show Cursor
- Game.Goatee.Sgf.Board: instance Show GameInfo
- Game.Goatee.Sgf.Board: instance Show RootInfo
- Game.Goatee.Sgf.Board: internalIsGameInfoNode :: Node -> Bool
- Game.Goatee.Sgf.Board: isCurrentValidMove :: BoardState -> Coord -> Bool
- Game.Goatee.Sgf.Board: isValidMove :: BoardState -> Color -> Coord -> Bool
- Game.Goatee.Sgf.Board: mapBoardCoords :: (Int -> Int -> CoordState -> a) -> BoardState -> [[a]]
- Game.Goatee.Sgf.Board: rootBoardState :: Node -> BoardState
- Game.Goatee.Sgf.Board: rootCursor :: Node -> Cursor
- Game.Goatee.Sgf.Board: rootInfoHeight :: RootInfo -> Int
- Game.Goatee.Sgf.Board: rootInfoVariationMode :: RootInfo -> VariationMode
- Game.Goatee.Sgf.Board: rootInfoWidth :: RootInfo -> Int
- Game.Goatee.Sgf.Monad: GoDown :: Int -> Step
- Game.Goatee.Sgf.Monad: GoUp :: Int -> Step
- Game.Goatee.Sgf.Monad: addChild :: MonadGo go => Int -> Node -> go ()
- Game.Goatee.Sgf.Monad: childAddedEvent :: Event go (ChildAddedHandler go)
- Game.Goatee.Sgf.Monad: data Event go h
- Game.Goatee.Sgf.Monad: data GoT m a
- Game.Goatee.Sgf.Monad: data Step
- Game.Goatee.Sgf.Monad: deleteProperty :: (MonadGo go, Descriptor d) => d -> go ()
- Game.Goatee.Sgf.Monad: dropPosition :: MonadGo go => go ()
- Game.Goatee.Sgf.Monad: evalGo :: GoM a -> Cursor -> a
- Game.Goatee.Sgf.Monad: evalGoT :: Monad m => GoT m a -> Cursor -> m a
- Game.Goatee.Sgf.Monad: execGo :: GoM a -> Cursor -> Cursor
- Game.Goatee.Sgf.Monad: execGoT :: Monad m => GoT m a -> Cursor -> m Cursor
- Game.Goatee.Sgf.Monad: fire :: Monad m => Event (GoT m) h -> (h -> GoT m ()) -> GoT m ()
- Game.Goatee.Sgf.Monad: gameInfoChangedEvent :: Event go (GameInfoChangedHandler go)
- Game.Goatee.Sgf.Monad: getCoordState :: MonadGo go => Coord -> go CoordState
- Game.Goatee.Sgf.Monad: getCursor :: MonadGo go => go Cursor
- Game.Goatee.Sgf.Monad: getMark :: MonadGo go => Coord -> go (Maybe Mark)
- Game.Goatee.Sgf.Monad: getProperties :: MonadGo go => go [Property]
- Game.Goatee.Sgf.Monad: getProperty :: (MonadGo go, Descriptor d) => d -> go (Maybe Property)
- Game.Goatee.Sgf.Monad: getPropertyValue :: (MonadGo go, ValuedDescriptor d v) => d -> go (Maybe v)
- Game.Goatee.Sgf.Monad: goDown :: MonadGo go => Int -> go ()
- Game.Goatee.Sgf.Monad: goToGameInfoNode :: MonadGo go => Bool -> go Bool
- Game.Goatee.Sgf.Monad: goToRoot :: MonadGo go => go ()
- Game.Goatee.Sgf.Monad: goUp :: MonadGo go => go ()
- Game.Goatee.Sgf.Monad: instance Eq Step
- Game.Goatee.Sgf.Monad: instance Monad m => Applicative (GoT m)
- Game.Goatee.Sgf.Monad: instance Monad m => Functor (GoT m)
- Game.Goatee.Sgf.Monad: instance Monad m => Monad (GoT m)
- Game.Goatee.Sgf.Monad: instance Monad m => MonadGo (GoT m)
- Game.Goatee.Sgf.Monad: instance MonadIO m => MonadIO (GoT m)
- Game.Goatee.Sgf.Monad: instance MonadTrans GoT
- Game.Goatee.Sgf.Monad: instance MonadWriter w m => MonadWriter w (GoT m)
- Game.Goatee.Sgf.Monad: instance Show (Event go h)
- Game.Goatee.Sgf.Monad: instance Show Step
- Game.Goatee.Sgf.Monad: modifyGameInfo :: MonadGo go => (GameInfo -> GameInfo) -> go GameInfo
- Game.Goatee.Sgf.Monad: modifyMark :: MonadGo go => (Maybe Mark -> Maybe Mark) -> Coord -> go ()
- Game.Goatee.Sgf.Monad: modifyProperties :: MonadGo go => ([Property] -> go [Property]) -> go ()
- Game.Goatee.Sgf.Monad: modifyProperty :: (MonadGo go, Descriptor d) => d -> (Maybe Property -> Maybe Property) -> go ()
- Game.Goatee.Sgf.Monad: modifyPropertyCoords :: (MonadGo go, ValuedDescriptor d CoordList) => d -> ([Coord] -> [Coord]) -> go ()
- Game.Goatee.Sgf.Monad: modifyPropertyString :: (MonadGo go, Stringlike s, ValuedDescriptor d s) => d -> (String -> String) -> go ()
- Game.Goatee.Sgf.Monad: modifyPropertyValue :: (MonadGo go, ValuedDescriptor d v) => d -> (Maybe v -> Maybe v) -> go ()
- Game.Goatee.Sgf.Monad: modifyVariationMode :: MonadGo go => (VariationMode -> VariationMode) -> go ()
- Game.Goatee.Sgf.Monad: navigationEvent :: Event go (NavigationHandler go)
- Game.Goatee.Sgf.Monad: on :: MonadGo go => Event go h -> h -> go ()
- Game.Goatee.Sgf.Monad: popPosition :: MonadGo go => go ()
- Game.Goatee.Sgf.Monad: propertiesModifiedEvent :: Event go (PropertiesModifiedHandler go)
- Game.Goatee.Sgf.Monad: pushPosition :: MonadGo go => go ()
- Game.Goatee.Sgf.Monad: putProperty :: MonadGo go => Property -> go ()
- Game.Goatee.Sgf.Monad: runGo :: GoM a -> Cursor -> (a, Cursor)
- Game.Goatee.Sgf.Monad: runGoT :: Monad m => GoT m a -> Cursor -> m (a, Cursor)
- Game.Goatee.Sgf.Monad: type ChildAddedHandler go = Int -> Cursor -> go ()
- Game.Goatee.Sgf.Monad: type GameInfoChangedHandler go = GameInfo -> GameInfo -> go ()
- Game.Goatee.Sgf.Monad: type GoM = GoT Identity
- Game.Goatee.Sgf.Monad: type NavigationHandler go = Step -> go ()
- Game.Goatee.Sgf.Monad: type PropertiesModifiedHandler go = [Property] -> [Property] -> go ()
- Game.Goatee.Sgf.Monad: type VariationModeChangedHandler go = VariationMode -> VariationMode -> go ()
- Game.Goatee.Sgf.Monad: variationModeChangedEvent :: Event go (VariationModeChangedHandler go)
- Game.Goatee.Sgf.Parser: parseFile :: String -> IO (Either String Collection)
- Game.Goatee.Sgf.Parser: parseString :: String -> Either String Collection
- Game.Goatee.Sgf.Property: AB :: CoordList -> Property
- Game.Goatee.Sgf.Property: AE :: CoordList -> Property
- Game.Goatee.Sgf.Property: AN :: SimpleText -> Property
- Game.Goatee.Sgf.Property: AP :: SimpleText -> SimpleText -> Property
- Game.Goatee.Sgf.Property: AR :: ArrowList -> Property
- Game.Goatee.Sgf.Property: AW :: CoordList -> Property
- Game.Goatee.Sgf.Property: B :: (Maybe Coord) -> Property
- Game.Goatee.Sgf.Property: BM :: DoubleValue -> Property
- Game.Goatee.Sgf.Property: BR :: SimpleText -> Property
- Game.Goatee.Sgf.Property: BT :: SimpleText -> Property
- Game.Goatee.Sgf.Property: C :: Text -> Property
- Game.Goatee.Sgf.Property: CA :: SimpleText -> Property
- Game.Goatee.Sgf.Property: CP :: SimpleText -> Property
- Game.Goatee.Sgf.Property: CR :: CoordList -> Property
- Game.Goatee.Sgf.Property: DD :: CoordList -> Property
- Game.Goatee.Sgf.Property: DM :: DoubleValue -> Property
- Game.Goatee.Sgf.Property: DO :: Property
- Game.Goatee.Sgf.Property: DT :: SimpleText -> Property
- Game.Goatee.Sgf.Property: EV :: SimpleText -> Property
- Game.Goatee.Sgf.Property: FF :: Int -> Property
- Game.Goatee.Sgf.Property: GB :: DoubleValue -> Property
- Game.Goatee.Sgf.Property: GC :: SimpleText -> Property
- Game.Goatee.Sgf.Property: GM :: Int -> Property
- Game.Goatee.Sgf.Property: GN :: SimpleText -> Property
- Game.Goatee.Sgf.Property: GW :: DoubleValue -> Property
- Game.Goatee.Sgf.Property: GameInfoProperty :: PropertyType
- Game.Goatee.Sgf.Property: GeneralProperty :: PropertyType
- Game.Goatee.Sgf.Property: HO :: DoubleValue -> Property
- Game.Goatee.Sgf.Property: IT :: Property
- Game.Goatee.Sgf.Property: KO :: Property
- Game.Goatee.Sgf.Property: LB :: LabelList -> Property
- Game.Goatee.Sgf.Property: LN :: LineList -> Property
- Game.Goatee.Sgf.Property: MA :: CoordList -> Property
- Game.Goatee.Sgf.Property: MN :: Integer -> Property
- Game.Goatee.Sgf.Property: MoveProperty :: PropertyType
- Game.Goatee.Sgf.Property: N :: SimpleText -> Property
- Game.Goatee.Sgf.Property: ON :: SimpleText -> Property
- Game.Goatee.Sgf.Property: OT :: SimpleText -> Property
- Game.Goatee.Sgf.Property: PB :: SimpleText -> Property
- Game.Goatee.Sgf.Property: PC :: SimpleText -> Property
- Game.Goatee.Sgf.Property: PL :: Color -> Property
- Game.Goatee.Sgf.Property: PW :: SimpleText -> Property
- Game.Goatee.Sgf.Property: RE :: GameResult -> Property
- Game.Goatee.Sgf.Property: RO :: SimpleText -> Property
- Game.Goatee.Sgf.Property: RU :: Ruleset -> Property
- Game.Goatee.Sgf.Property: RootProperty :: PropertyType
- Game.Goatee.Sgf.Property: SL :: CoordList -> Property
- Game.Goatee.Sgf.Property: SO :: SimpleText -> Property
- Game.Goatee.Sgf.Property: SQ :: CoordList -> Property
- Game.Goatee.Sgf.Property: ST :: VariationMode -> Property
- Game.Goatee.Sgf.Property: SZ :: Int -> Int -> Property
- Game.Goatee.Sgf.Property: SetupProperty :: PropertyType
- Game.Goatee.Sgf.Property: SomeDescriptor :: a -> SomeDescriptor
- Game.Goatee.Sgf.Property: TE :: DoubleValue -> Property
- Game.Goatee.Sgf.Property: TM :: RealValue -> Property
- Game.Goatee.Sgf.Property: TR :: CoordList -> Property
- Game.Goatee.Sgf.Property: UC :: DoubleValue -> Property
- Game.Goatee.Sgf.Property: US :: SimpleText -> Property
- Game.Goatee.Sgf.Property: UnknownProperty :: String -> UnknownPropertyValue -> Property
- Game.Goatee.Sgf.Property: V :: RealValue -> Property
- Game.Goatee.Sgf.Property: VW :: CoordList -> Property
- Game.Goatee.Sgf.Property: ValuedPropertyInfo :: String -> PropertyType -> Bool -> (Property -> Bool) -> PropertyValueType v -> (Property -> v) -> (v -> Property) -> ValuedPropertyInfo v
- Game.Goatee.Sgf.Property: W :: (Maybe Coord) -> Property
- Game.Goatee.Sgf.Property: WR :: SimpleText -> Property
- Game.Goatee.Sgf.Property: WT :: SimpleText -> Property
- Game.Goatee.Sgf.Property: allDescriptors :: [SomeDescriptor]
- Game.Goatee.Sgf.Property: class Descriptor a
- Game.Goatee.Sgf.Property: class (Descriptor a, Eq v) => ValuedDescriptor a v | a -> v
- Game.Goatee.Sgf.Property: data Property
- Game.Goatee.Sgf.Property: data PropertyInfo
- Game.Goatee.Sgf.Property: data PropertyType
- Game.Goatee.Sgf.Property: data PropertyValueType a
- Game.Goatee.Sgf.Property: data SomeDescriptor
- Game.Goatee.Sgf.Property: data ValuedPropertyInfo v
- Game.Goatee.Sgf.Property: defProperty :: String -> Name -> Bool -> DecsQ
- Game.Goatee.Sgf.Property: defValuedProperty :: String -> Name -> Bool -> Name -> DecsQ
- Game.Goatee.Sgf.Property: descriptorForName :: String -> SomeDescriptor
- Game.Goatee.Sgf.Property: descriptorForName' :: String -> Maybe SomeDescriptor
- Game.Goatee.Sgf.Property: descriptorsByName :: Map String SomeDescriptor
- Game.Goatee.Sgf.Property: markProperty :: Mark -> ValuedPropertyInfo CoordList
- Game.Goatee.Sgf.Property: propertyAB :: ValuedPropertyInfo CoordList
- Game.Goatee.Sgf.Property: propertyAE :: ValuedPropertyInfo CoordList
- Game.Goatee.Sgf.Property: propertyAN :: ValuedPropertyInfo SimpleText
- Game.Goatee.Sgf.Property: propertyAP :: ValuedPropertyInfo (SimpleText, SimpleText)
- Game.Goatee.Sgf.Property: propertyAR :: ValuedPropertyInfo ArrowList
- Game.Goatee.Sgf.Property: propertyAW :: ValuedPropertyInfo CoordList
- Game.Goatee.Sgf.Property: propertyB :: ValuedPropertyInfo (Maybe Coord)
- Game.Goatee.Sgf.Property: propertyBM :: ValuedPropertyInfo DoubleValue
- Game.Goatee.Sgf.Property: propertyBR :: ValuedPropertyInfo SimpleText
- Game.Goatee.Sgf.Property: propertyBT :: ValuedPropertyInfo SimpleText
- Game.Goatee.Sgf.Property: propertyBuilder :: ValuedDescriptor a v => a -> v -> Property
- Game.Goatee.Sgf.Property: propertyC :: ValuedPropertyInfo Text
- Game.Goatee.Sgf.Property: propertyCA :: ValuedPropertyInfo SimpleText
- Game.Goatee.Sgf.Property: propertyCP :: ValuedPropertyInfo SimpleText
- Game.Goatee.Sgf.Property: propertyCR :: ValuedPropertyInfo CoordList
- Game.Goatee.Sgf.Property: propertyDD :: ValuedPropertyInfo CoordList
- Game.Goatee.Sgf.Property: propertyDM :: ValuedPropertyInfo DoubleValue
- Game.Goatee.Sgf.Property: propertyDO :: PropertyInfo
- Game.Goatee.Sgf.Property: propertyDT :: ValuedPropertyInfo SimpleText
- Game.Goatee.Sgf.Property: propertyEV :: ValuedPropertyInfo SimpleText
- Game.Goatee.Sgf.Property: propertyFF :: ValuedPropertyInfo Int
- Game.Goatee.Sgf.Property: propertyGB :: ValuedPropertyInfo DoubleValue
- Game.Goatee.Sgf.Property: propertyGC :: ValuedPropertyInfo SimpleText
- Game.Goatee.Sgf.Property: propertyGM :: ValuedPropertyInfo Int
- Game.Goatee.Sgf.Property: propertyGN :: ValuedPropertyInfo SimpleText
- Game.Goatee.Sgf.Property: propertyGW :: ValuedPropertyInfo DoubleValue
- Game.Goatee.Sgf.Property: propertyHO :: ValuedPropertyInfo DoubleValue
- Game.Goatee.Sgf.Property: propertyIT :: PropertyInfo
- Game.Goatee.Sgf.Property: propertyInfo :: Property -> SomeDescriptor
- Game.Goatee.Sgf.Property: propertyInherited :: Descriptor a => a -> Bool
- Game.Goatee.Sgf.Property: propertyKO :: PropertyInfo
- Game.Goatee.Sgf.Property: propertyLB :: ValuedPropertyInfo LabelList
- Game.Goatee.Sgf.Property: propertyLN :: ValuedPropertyInfo LineList
- Game.Goatee.Sgf.Property: propertyMA :: ValuedPropertyInfo CoordList
- Game.Goatee.Sgf.Property: propertyMN :: ValuedPropertyInfo Integer
- Game.Goatee.Sgf.Property: propertyN :: ValuedPropertyInfo SimpleText
- Game.Goatee.Sgf.Property: propertyName :: Descriptor a => a -> String
- Game.Goatee.Sgf.Property: propertyON :: ValuedPropertyInfo SimpleText
- Game.Goatee.Sgf.Property: propertyOT :: ValuedPropertyInfo SimpleText
- Game.Goatee.Sgf.Property: propertyPB :: ValuedPropertyInfo SimpleText
- Game.Goatee.Sgf.Property: propertyPC :: ValuedPropertyInfo SimpleText
- Game.Goatee.Sgf.Property: propertyPL :: ValuedPropertyInfo Color
- Game.Goatee.Sgf.Property: propertyPW :: ValuedPropertyInfo SimpleText
- Game.Goatee.Sgf.Property: propertyPredicate :: Descriptor a => a -> Property -> Bool
- Game.Goatee.Sgf.Property: propertyRE :: ValuedPropertyInfo GameResult
- Game.Goatee.Sgf.Property: propertyRO :: ValuedPropertyInfo SimpleText
- Game.Goatee.Sgf.Property: propertyRU :: ValuedPropertyInfo Ruleset
- Game.Goatee.Sgf.Property: propertySL :: ValuedPropertyInfo CoordList
- Game.Goatee.Sgf.Property: propertySO :: ValuedPropertyInfo SimpleText
- Game.Goatee.Sgf.Property: propertySQ :: ValuedPropertyInfo CoordList
- Game.Goatee.Sgf.Property: propertyST :: ValuedPropertyInfo VariationMode
- Game.Goatee.Sgf.Property: propertySZ :: ValuedPropertyInfo (Int, Int)
- Game.Goatee.Sgf.Property: propertyTE :: ValuedPropertyInfo DoubleValue
- Game.Goatee.Sgf.Property: propertyTM :: ValuedPropertyInfo RealValue
- Game.Goatee.Sgf.Property: propertyTR :: ValuedPropertyInfo CoordList
- Game.Goatee.Sgf.Property: propertyType :: Descriptor a => a -> PropertyType
- Game.Goatee.Sgf.Property: propertyUC :: ValuedPropertyInfo DoubleValue
- Game.Goatee.Sgf.Property: propertyUS :: ValuedPropertyInfo SimpleText
- Game.Goatee.Sgf.Property: propertyUnknown :: String -> ValuedPropertyInfo UnknownPropertyValue
- Game.Goatee.Sgf.Property: propertyV :: ValuedPropertyInfo RealValue
- Game.Goatee.Sgf.Property: propertyVW :: ValuedPropertyInfo CoordList
- Game.Goatee.Sgf.Property: propertyValue :: ValuedDescriptor a v => a -> Property -> v
- Game.Goatee.Sgf.Property: propertyValueParser :: Descriptor a => a -> Parser Property
- Game.Goatee.Sgf.Property: propertyValueRenderer :: Descriptor a => a -> Property -> Render ()
- Game.Goatee.Sgf.Property: propertyValueRendererPretty :: Descriptor a => a -> Property -> Render ()
- Game.Goatee.Sgf.Property: propertyW :: ValuedPropertyInfo (Maybe Coord)
- Game.Goatee.Sgf.Property: propertyWR :: ValuedPropertyInfo SimpleText
- Game.Goatee.Sgf.Property: propertyWT :: ValuedPropertyInfo SimpleText
- Game.Goatee.Sgf.Property: pvtParser :: PropertyValueType a -> Parser a
- Game.Goatee.Sgf.Property: pvtRenderer :: PropertyValueType a -> a -> Render ()
- Game.Goatee.Sgf.Property: pvtRendererPretty :: PropertyValueType a -> a -> Render ()
- Game.Goatee.Sgf.Property.Parser: colorParser :: Parser Color
- Game.Goatee.Sgf.Property.Parser: compose :: Parser a -> Parser b -> Parser (a, b)
- Game.Goatee.Sgf.Property.Parser: coordElistParser :: Parser CoordList
- Game.Goatee.Sgf.Property.Parser: coordListParser :: Parser CoordList
- Game.Goatee.Sgf.Property.Parser: coordPairListParser :: Parser [(Coord, Coord)]
- Game.Goatee.Sgf.Property.Parser: doubleParser :: Parser DoubleValue
- Game.Goatee.Sgf.Property.Parser: gameResultParser :: Parser GameResult
- Game.Goatee.Sgf.Property.Parser: instance Monoid CoordListMonoid
- Game.Goatee.Sgf.Property.Parser: integralParser :: (Integral a, Read a) => Parser a
- Game.Goatee.Sgf.Property.Parser: labelListParser :: Parser [(Coord, SimpleText)]
- Game.Goatee.Sgf.Property.Parser: line :: Parser Int
- Game.Goatee.Sgf.Property.Parser: moveParser :: Parser (Maybe Coord)
- Game.Goatee.Sgf.Property.Parser: noneParser :: Parser ()
- Game.Goatee.Sgf.Property.Parser: realParser :: Parser RealValue
- Game.Goatee.Sgf.Property.Parser: rulesetParser :: Parser Ruleset
- Game.Goatee.Sgf.Property.Parser: simpleText :: Bool -> Parser SimpleText
- Game.Goatee.Sgf.Property.Parser: simpleTextPairParser :: Parser (SimpleText, SimpleText)
- Game.Goatee.Sgf.Property.Parser: simpleTextParser :: Parser SimpleText
- Game.Goatee.Sgf.Property.Parser: sizeParser :: Parser (Int, Int)
- Game.Goatee.Sgf.Property.Parser: text :: Bool -> Parser String
- Game.Goatee.Sgf.Property.Parser: textParser :: Parser Text
- Game.Goatee.Sgf.Property.Parser: unknownPropertyParser :: Parser UnknownPropertyValue
- Game.Goatee.Sgf.Property.Parser: variationModeParser :: Parser VariationMode
- Game.Goatee.Sgf.Renderer: rendererOf :: Show a => String -> (a -> Render ()) -> a -> Render ()
- Game.Goatee.Sgf.Renderer: runRender :: Render a -> Either String String
- Game.Goatee.Sgf.Renderer: type Render = WriterT String (Either String)
- Game.Goatee.Sgf.Renderer.Tree: renderCollection :: Collection -> Render ()
- Game.Goatee.Sgf.Tree: Collection :: [Node] -> Collection
- Game.Goatee.Sgf.Tree: CollectionWithDeepEquality :: Collection -> CollectionWithDeepEquality
- Game.Goatee.Sgf.Tree: Node :: [Property] -> [Node] -> Node
- Game.Goatee.Sgf.Tree: NodeWithDeepEquality :: Node -> NodeWithDeepEquality
- Game.Goatee.Sgf.Tree: addChild :: Node -> Node -> Node
- Game.Goatee.Sgf.Tree: addChildAt :: Int -> Node -> Node -> Node
- Game.Goatee.Sgf.Tree: addProperty :: Property -> Node -> Node
- Game.Goatee.Sgf.Tree: collectionTrees :: Collection -> [Node]
- Game.Goatee.Sgf.Tree: collectionWithDeepEquality :: CollectionWithDeepEquality -> Collection
- Game.Goatee.Sgf.Tree: data Collection
- Game.Goatee.Sgf.Tree: data Node
- Game.Goatee.Sgf.Tree: emptyNode :: Node
- Game.Goatee.Sgf.Tree: findProperty :: Descriptor a => a -> Node -> Maybe Property
- Game.Goatee.Sgf.Tree: findProperty' :: Descriptor a => a -> [Property] -> Maybe Property
- Game.Goatee.Sgf.Tree: findPropertyValue :: ValuedDescriptor a v => a -> Node -> Maybe v
- Game.Goatee.Sgf.Tree: findPropertyValue' :: ValuedDescriptor a v => a -> [Property] -> Maybe v
- Game.Goatee.Sgf.Tree: instance Eq CollectionWithDeepEquality
- Game.Goatee.Sgf.Tree: instance Eq NodeWithDeepEquality
- Game.Goatee.Sgf.Tree: instance Show Collection
- Game.Goatee.Sgf.Tree: instance Show CollectionWithDeepEquality
- Game.Goatee.Sgf.Tree: instance Show Node
- Game.Goatee.Sgf.Tree: newtype CollectionWithDeepEquality
- Game.Goatee.Sgf.Tree: newtype NodeWithDeepEquality
- Game.Goatee.Sgf.Tree: nodeChildren :: Node -> [Node]
- Game.Goatee.Sgf.Tree: nodeProperties :: Node -> [Property]
- Game.Goatee.Sgf.Tree: nodeWithDeepEquality :: NodeWithDeepEquality -> Node
- Game.Goatee.Sgf.Tree: rootNode :: Maybe (Int, Int) -> Node
- Game.Goatee.Sgf.Tree: validateNode :: Bool -> Bool -> Node -> [String]
- Game.Goatee.Sgf.Types: Black :: Color
- Game.Goatee.Sgf.Types: Double1 :: DoubleValue
- Game.Goatee.Sgf.Types: Double2 :: DoubleValue
- Game.Goatee.Sgf.Types: GameResultDraw :: GameResult
- Game.Goatee.Sgf.Types: GameResultOther :: SimpleText -> GameResult
- Game.Goatee.Sgf.Types: GameResultUnknown :: GameResult
- Game.Goatee.Sgf.Types: GameResultVoid :: GameResult
- Game.Goatee.Sgf.Types: GameResultWin :: Color -> WinReason -> GameResult
- Game.Goatee.Sgf.Types: KnownRuleset :: RulesetType -> Ruleset
- Game.Goatee.Sgf.Types: MarkCircle :: Mark
- Game.Goatee.Sgf.Types: MarkSelected :: Mark
- Game.Goatee.Sgf.Types: MarkSquare :: Mark
- Game.Goatee.Sgf.Types: MarkTriangle :: Mark
- Game.Goatee.Sgf.Types: MarkX :: Mark
- Game.Goatee.Sgf.Types: RulesetAga :: RulesetType
- Game.Goatee.Sgf.Types: RulesetIng :: RulesetType
- Game.Goatee.Sgf.Types: RulesetJapanese :: RulesetType
- Game.Goatee.Sgf.Types: RulesetNewZealand :: RulesetType
- Game.Goatee.Sgf.Types: ShowChildVariations :: VariationModeSource
- Game.Goatee.Sgf.Types: ShowCurrentVariations :: VariationModeSource
- Game.Goatee.Sgf.Types: UnknownRuleset :: String -> Ruleset
- Game.Goatee.Sgf.Types: VariationMode :: VariationModeSource -> Bool -> VariationMode
- Game.Goatee.Sgf.Types: White :: Color
- Game.Goatee.Sgf.Types: WinByForfeit :: WinReason
- Game.Goatee.Sgf.Types: WinByResignation :: WinReason
- Game.Goatee.Sgf.Types: WinByScore :: RealValue -> WinReason
- Game.Goatee.Sgf.Types: WinByTime :: WinReason
- Game.Goatee.Sgf.Types: boardSizeDefault :: Int
- Game.Goatee.Sgf.Types: boardSizeMax :: Int
- Game.Goatee.Sgf.Types: boardSizeMin :: Int
- Game.Goatee.Sgf.Types: buildCoordList :: [Coord] -> CoordList
- Game.Goatee.Sgf.Types: class Stringlike a
- Game.Goatee.Sgf.Types: cnot :: Color -> Color
- Game.Goatee.Sgf.Types: coord1 :: Coord -> CoordList
- Game.Goatee.Sgf.Types: coordListRects :: CoordList -> [(Coord, Coord)]
- Game.Goatee.Sgf.Types: coordListSingles :: CoordList -> [Coord]
- Game.Goatee.Sgf.Types: coords :: [Coord] -> CoordList
- Game.Goatee.Sgf.Types: coords' :: [Coord] -> [(Coord, Coord)] -> CoordList
- Game.Goatee.Sgf.Types: data Color
- Game.Goatee.Sgf.Types: data CoordList
- Game.Goatee.Sgf.Types: data DoubleValue
- Game.Goatee.Sgf.Types: data GameResult
- Game.Goatee.Sgf.Types: data Mark
- Game.Goatee.Sgf.Types: data Ruleset
- Game.Goatee.Sgf.Types: data RulesetType
- Game.Goatee.Sgf.Types: data SimpleText
- Game.Goatee.Sgf.Types: data Text
- Game.Goatee.Sgf.Types: data UnknownPropertyValue
- Game.Goatee.Sgf.Types: data VariationMode
- Game.Goatee.Sgf.Types: data VariationModeSource
- Game.Goatee.Sgf.Types: data WinReason
- Game.Goatee.Sgf.Types: defaultFormatVersion :: Int
- Game.Goatee.Sgf.Types: defaultVariationMode :: VariationMode
- Game.Goatee.Sgf.Types: emptyCoordList :: CoordList
- Game.Goatee.Sgf.Types: expandCoordList :: CoordList -> [Coord]
- Game.Goatee.Sgf.Types: fromRuleset :: Ruleset -> String
- Game.Goatee.Sgf.Types: fromSimpleText :: SimpleText -> String
- Game.Goatee.Sgf.Types: fromText :: Text -> String
- Game.Goatee.Sgf.Types: fromUnknownPropertyValue :: UnknownPropertyValue -> String
- Game.Goatee.Sgf.Types: fromVariationMode :: VariationMode -> Int
- Game.Goatee.Sgf.Types: instance Bounded Mark
- Game.Goatee.Sgf.Types: instance Bounded RulesetType
- Game.Goatee.Sgf.Types: instance Bounded VariationModeSource
- Game.Goatee.Sgf.Types: instance Enum Mark
- Game.Goatee.Sgf.Types: instance Enum RulesetType
- Game.Goatee.Sgf.Types: instance Enum VariationModeSource
- Game.Goatee.Sgf.Types: instance Eq Color
- Game.Goatee.Sgf.Types: instance Eq CoordList
- Game.Goatee.Sgf.Types: instance Eq DoubleValue
- Game.Goatee.Sgf.Types: instance Eq GameResult
- Game.Goatee.Sgf.Types: instance Eq Mark
- Game.Goatee.Sgf.Types: instance Eq Ruleset
- Game.Goatee.Sgf.Types: instance Eq RulesetType
- Game.Goatee.Sgf.Types: instance Eq SimpleText
- Game.Goatee.Sgf.Types: instance Eq Text
- Game.Goatee.Sgf.Types: instance Eq UnknownPropertyValue
- Game.Goatee.Sgf.Types: instance Eq VariationMode
- Game.Goatee.Sgf.Types: instance Eq VariationModeSource
- Game.Goatee.Sgf.Types: instance Eq WinReason
- Game.Goatee.Sgf.Types: instance Show Color
- Game.Goatee.Sgf.Types: instance Show CoordList
- Game.Goatee.Sgf.Types: instance Show DoubleValue
- Game.Goatee.Sgf.Types: instance Show GameResult
- Game.Goatee.Sgf.Types: instance Show Mark
- Game.Goatee.Sgf.Types: instance Show Ruleset
- Game.Goatee.Sgf.Types: instance Show RulesetType
- Game.Goatee.Sgf.Types: instance Show SimpleText
- Game.Goatee.Sgf.Types: instance Show Text
- Game.Goatee.Sgf.Types: instance Show UnknownPropertyValue
- Game.Goatee.Sgf.Types: instance Show VariationMode
- Game.Goatee.Sgf.Types: instance Show VariationModeSource
- Game.Goatee.Sgf.Types: instance Show WinReason
- Game.Goatee.Sgf.Types: instance Stringlike SimpleText
- Game.Goatee.Sgf.Types: instance Stringlike Text
- Game.Goatee.Sgf.Types: instance Stringlike UnknownPropertyValue
- Game.Goatee.Sgf.Types: sgfToString :: Stringlike a => a -> String
- Game.Goatee.Sgf.Types: stringToSgf :: Stringlike a => String -> a
- Game.Goatee.Sgf.Types: supportedFormatVersions :: [Int]
- Game.Goatee.Sgf.Types: supportedGameTypes :: [Int]
- Game.Goatee.Sgf.Types: toRuleset :: String -> Ruleset
- Game.Goatee.Sgf.Types: toSimpleText :: String -> SimpleText
- Game.Goatee.Sgf.Types: toText :: String -> Text
- Game.Goatee.Sgf.Types: toUnknownPropertyValue :: String -> UnknownPropertyValue
- Game.Goatee.Sgf.Types: toVariationMode :: Int -> Maybe VariationMode
- Game.Goatee.Sgf.Types: type ArrowList = [(Coord, Coord)]
- Game.Goatee.Sgf.Types: type Coord = (Int, Int)
- Game.Goatee.Sgf.Types: type LabelList = [(Coord, SimpleText)]
- Game.Goatee.Sgf.Types: type LineList = [(Coord, Coord)]
- Game.Goatee.Sgf.Types: type RealValue = Rational
- Game.Goatee.Sgf.Types: variationModeBoardMarkup :: VariationMode -> Bool
- Game.Goatee.Sgf.Types: variationModeSource :: VariationMode -> VariationModeSource
+ Game.Goatee.Common: listDeleteAt :: Int -> [a] -> [a]
+ Game.Goatee.Common: listInsertAt :: Int -> a -> [a] -> [a]
+ Game.Goatee.Common.Bigfloat: data Bigfloat
+ Game.Goatee.Common.Bigfloat: encode :: Integer -> Int -> Bigfloat
+ Game.Goatee.Common.Bigfloat: exponent :: Bigfloat -> Int
+ Game.Goatee.Common.Bigfloat: fromDouble :: Double -> Bigfloat
+ Game.Goatee.Common.Bigfloat: instance Eq Bigfloat
+ Game.Goatee.Common.Bigfloat: instance Num Bigfloat
+ Game.Goatee.Common.Bigfloat: instance Ord Bigfloat
+ Game.Goatee.Common.Bigfloat: instance Read Bigfloat
+ Game.Goatee.Common.Bigfloat: instance Show Bigfloat
+ Game.Goatee.Common.Bigfloat: significand :: Bigfloat -> Integer
+ Game.Goatee.Common.Bigfloat: toDouble :: Bigfloat -> Double
+ Game.Goatee.Lib.Board: BoardState :: [[CoordState]] -> Bool -> Bool -> ArrowList -> LineList -> LabelList -> Integer -> Color -> Int -> Int -> GameInfo -> BoardState
+ Game.Goatee.Lib.Board: CoordState :: Bool -> Maybe Color -> Maybe Mark -> Bool -> Bool -> CoordState
+ Game.Goatee.Lib.Board: Cursor :: Maybe Cursor -> Int -> Node -> BoardState -> Cursor
+ Game.Goatee.Lib.Board: GameInfo :: RootInfo -> Maybe SimpleText -> Maybe SimpleText -> Maybe SimpleText -> Maybe SimpleText -> Maybe SimpleText -> Maybe SimpleText -> Maybe Ruleset -> Maybe RealValue -> Maybe SimpleText -> Maybe GameResult -> Maybe SimpleText -> Maybe Text -> Maybe SimpleText -> Maybe SimpleText -> Maybe SimpleText -> Maybe SimpleText -> Maybe SimpleText -> Maybe SimpleText -> Maybe SimpleText -> Maybe SimpleText -> Maybe SimpleText -> GameInfo
+ Game.Goatee.Lib.Board: RootInfo :: Int -> Int -> VariationMode -> RootInfo
+ Game.Goatee.Lib.Board: boardArrows :: BoardState -> ArrowList
+ Game.Goatee.Lib.Board: boardBlackCaptures :: BoardState -> Int
+ Game.Goatee.Lib.Board: boardCoordState :: Coord -> BoardState -> CoordState
+ Game.Goatee.Lib.Board: boardCoordStates :: BoardState -> [[CoordState]]
+ Game.Goatee.Lib.Board: boardGameInfo :: BoardState -> GameInfo
+ Game.Goatee.Lib.Board: boardHasDimmed :: BoardState -> Bool
+ Game.Goatee.Lib.Board: boardHasInvisible :: BoardState -> Bool
+ Game.Goatee.Lib.Board: boardHeight :: BoardState -> Int
+ Game.Goatee.Lib.Board: boardLabels :: BoardState -> LabelList
+ Game.Goatee.Lib.Board: boardLines :: BoardState -> LineList
+ Game.Goatee.Lib.Board: boardMoveNumber :: BoardState -> Integer
+ Game.Goatee.Lib.Board: boardPlayerTurn :: BoardState -> Color
+ Game.Goatee.Lib.Board: boardWhiteCaptures :: BoardState -> Int
+ Game.Goatee.Lib.Board: boardWidth :: BoardState -> Int
+ Game.Goatee.Lib.Board: coordDimmed :: CoordState -> Bool
+ Game.Goatee.Lib.Board: coordMark :: CoordState -> Maybe Mark
+ Game.Goatee.Lib.Board: coordStar :: CoordState -> Bool
+ Game.Goatee.Lib.Board: coordStone :: CoordState -> Maybe Color
+ Game.Goatee.Lib.Board: coordVisible :: CoordState -> Bool
+ Game.Goatee.Lib.Board: cursorBoard :: Cursor -> BoardState
+ Game.Goatee.Lib.Board: cursorChild :: Cursor -> Int -> Cursor
+ Game.Goatee.Lib.Board: cursorChildCount :: Cursor -> Int
+ Game.Goatee.Lib.Board: cursorChildIndex :: Cursor -> Int
+ Game.Goatee.Lib.Board: cursorChildPlayingAt :: Maybe Coord -> Cursor -> Maybe Cursor
+ Game.Goatee.Lib.Board: cursorChildren :: Cursor -> [Cursor]
+ Game.Goatee.Lib.Board: cursorModifyNode :: (Node -> Node) -> Cursor -> Cursor
+ Game.Goatee.Lib.Board: cursorNode :: Cursor -> Node
+ Game.Goatee.Lib.Board: cursorParent :: Cursor -> Maybe Cursor
+ Game.Goatee.Lib.Board: cursorProperties :: Cursor -> [Property]
+ Game.Goatee.Lib.Board: cursorRoot :: Cursor -> Cursor
+ Game.Goatee.Lib.Board: cursorVariations :: VariationModeSource -> Cursor -> [(Coord, Color)]
+ Game.Goatee.Lib.Board: data BoardState
+ Game.Goatee.Lib.Board: data CoordState
+ Game.Goatee.Lib.Board: data Cursor
+ Game.Goatee.Lib.Board: data GameInfo
+ Game.Goatee.Lib.Board: data RootInfo
+ Game.Goatee.Lib.Board: emptyGameInfo :: RootInfo -> GameInfo
+ Game.Goatee.Lib.Board: gameInfoAnnotatorName :: GameInfo -> Maybe SimpleText
+ Game.Goatee.Lib.Board: gameInfoBasicTimeSeconds :: GameInfo -> Maybe RealValue
+ Game.Goatee.Lib.Board: gameInfoBlackName :: GameInfo -> Maybe SimpleText
+ Game.Goatee.Lib.Board: gameInfoBlackRank :: GameInfo -> Maybe SimpleText
+ Game.Goatee.Lib.Board: gameInfoBlackTeamName :: GameInfo -> Maybe SimpleText
+ Game.Goatee.Lib.Board: gameInfoCopyright :: GameInfo -> Maybe SimpleText
+ Game.Goatee.Lib.Board: gameInfoDatesPlayed :: GameInfo -> Maybe SimpleText
+ Game.Goatee.Lib.Board: gameInfoEntererName :: GameInfo -> Maybe SimpleText
+ Game.Goatee.Lib.Board: gameInfoEvent :: GameInfo -> Maybe SimpleText
+ Game.Goatee.Lib.Board: gameInfoGameComment :: GameInfo -> Maybe Text
+ Game.Goatee.Lib.Board: gameInfoGameName :: GameInfo -> Maybe SimpleText
+ Game.Goatee.Lib.Board: gameInfoOpeningComment :: GameInfo -> Maybe SimpleText
+ Game.Goatee.Lib.Board: gameInfoOvertime :: GameInfo -> Maybe SimpleText
+ Game.Goatee.Lib.Board: gameInfoPlace :: GameInfo -> Maybe SimpleText
+ Game.Goatee.Lib.Board: gameInfoResult :: GameInfo -> Maybe GameResult
+ Game.Goatee.Lib.Board: gameInfoRootInfo :: GameInfo -> RootInfo
+ Game.Goatee.Lib.Board: gameInfoRound :: GameInfo -> Maybe SimpleText
+ Game.Goatee.Lib.Board: gameInfoRuleset :: GameInfo -> Maybe Ruleset
+ Game.Goatee.Lib.Board: gameInfoSource :: GameInfo -> Maybe SimpleText
+ Game.Goatee.Lib.Board: gameInfoToProperties :: GameInfo -> [Property]
+ Game.Goatee.Lib.Board: gameInfoWhiteName :: GameInfo -> Maybe SimpleText
+ Game.Goatee.Lib.Board: gameInfoWhiteRank :: GameInfo -> Maybe SimpleText
+ Game.Goatee.Lib.Board: gameInfoWhiteTeamName :: GameInfo -> Maybe SimpleText
+ Game.Goatee.Lib.Board: instance Eq RootInfo
+ Game.Goatee.Lib.Board: instance Show ApplyMoveGroup
+ Game.Goatee.Lib.Board: instance Show ApplyMoveParams
+ Game.Goatee.Lib.Board: instance Show BoardState
+ Game.Goatee.Lib.Board: instance Show CoordState
+ Game.Goatee.Lib.Board: instance Show Cursor
+ Game.Goatee.Lib.Board: instance Show GameInfo
+ Game.Goatee.Lib.Board: instance Show RootInfo
+ Game.Goatee.Lib.Board: internalIsGameInfoNode :: Node -> Bool
+ Game.Goatee.Lib.Board: isCurrentValidMove :: BoardState -> Coord -> Bool
+ Game.Goatee.Lib.Board: isValidMove :: BoardState -> Color -> Coord -> Bool
+ Game.Goatee.Lib.Board: mapBoardCoords :: (Int -> Int -> CoordState -> a) -> BoardState -> [[a]]
+ Game.Goatee.Lib.Board: moveToProperty :: Color -> Maybe Coord -> Property
+ Game.Goatee.Lib.Board: rootBoardState :: Node -> BoardState
+ Game.Goatee.Lib.Board: rootCursor :: Node -> Cursor
+ Game.Goatee.Lib.Board: rootInfoHeight :: RootInfo -> Int
+ Game.Goatee.Lib.Board: rootInfoVariationMode :: RootInfo -> VariationMode
+ Game.Goatee.Lib.Board: rootInfoWidth :: RootInfo -> Int
+ Game.Goatee.Lib.Monad: AnyEvent :: (Event go h) -> AnyEvent go
+ Game.Goatee.Lib.Monad: GoDown :: Int -> Step
+ Game.Goatee.Lib.Monad: GoUp :: Int -> Step
+ Game.Goatee.Lib.Monad: NodeDeleteBadIndex :: NodeDeleteResult
+ Game.Goatee.Lib.Monad: NodeDeleteOk :: NodeDeleteResult
+ Game.Goatee.Lib.Monad: NodeDeleteOnPathStack :: NodeDeleteResult
+ Game.Goatee.Lib.Monad: addChild :: MonadGo go => Node -> go ()
+ Game.Goatee.Lib.Monad: addChildAt :: MonadGo go => Int -> Node -> go ()
+ Game.Goatee.Lib.Monad: childAddedEvent :: Event go (ChildAddedHandler go)
+ Game.Goatee.Lib.Monad: childDeletedEvent :: Event go (ChildDeletedHandler go)
+ Game.Goatee.Lib.Monad: class (Functor go, Applicative go, Monad go) => MonadGo go where getCoordState coord = liftM (boardCoordState coord . cursorBoard) getCursor getProperties = liftM cursorProperties getCursor getPropertyValue descriptor = liftM (liftM $ propertyValue descriptor) $ getProperty descriptor putProperty property = modifyProperty (propertyInfo property) $ const $ Just property deleteProperty descriptor = modifyProperty descriptor $ const Nothing modifyPropertyValue descriptor fn = modifyProperty descriptor $ \ old -> propertyBuilder descriptor <$> fn (propertyValue descriptor <$> old) modifyPropertyString descriptor fn = modifyPropertyValue descriptor $ \ value -> case fn (maybe "" sgfToString value) of { "" -> Nothing str -> let sgf = stringToSgf str in if null $ sgfToString sgf then Nothing else Just sgf } modifyPropertyCoords descriptor fn = modifyPropertyValue descriptor $ \ value -> case fn $ maybe [] expandCoordList value of { [] -> Nothing coords -> Just $ buildCoordList coords } getMark = liftM coordMark . getCoordState modifyMark fn coord = do { maybeOldMark <- getMark coord; case (maybeOldMark, fn maybeOldMark) of { (Just oldMark, Nothing) -> remove oldMark (Nothing, Just newMark) -> add newMark (Just oldMark, Just newMark) | oldMark /= newMark -> remove oldMark >> add newMark (Just _, Just _) -> return () (Nothing, Nothing) -> return () } } where remove mark = modifyPropertyCoords (markProperty mark) (delete coord) add mark = modifyPropertyCoords (markProperty mark) (coord :) addChild node = do { childCount <- liftM (length . cursorChildren) getCursor; addChildAt childCount node } on0 event handler = on event $ eventHandlerFromAction event handler
+ Game.Goatee.Lib.Monad: data AnyEvent go
+ Game.Goatee.Lib.Monad: data Event go h
+ Game.Goatee.Lib.Monad: data GoT m a
+ Game.Goatee.Lib.Monad: data NodeDeleteResult
+ Game.Goatee.Lib.Monad: data Step
+ Game.Goatee.Lib.Monad: deleteChildAt :: MonadGo go => Int -> go NodeDeleteResult
+ Game.Goatee.Lib.Monad: deleteProperty :: (MonadGo go, Descriptor d) => d -> go ()
+ Game.Goatee.Lib.Monad: dropPosition :: MonadGo go => go ()
+ Game.Goatee.Lib.Monad: evalGo :: GoM a -> Cursor -> a
+ Game.Goatee.Lib.Monad: evalGoT :: Monad m => GoT m a -> Cursor -> m a
+ Game.Goatee.Lib.Monad: eventHandlerFromAction :: Event go h -> go () -> h
+ Game.Goatee.Lib.Monad: eventName :: Event go h -> String
+ Game.Goatee.Lib.Monad: execGo :: GoM a -> Cursor -> Cursor
+ Game.Goatee.Lib.Monad: execGoT :: Monad m => GoT m a -> Cursor -> m Cursor
+ Game.Goatee.Lib.Monad: fire :: Monad m => Event (GoT m) h -> (h -> GoT m ()) -> GoT m ()
+ Game.Goatee.Lib.Monad: gameInfoChangedEvent :: Event go (GameInfoChangedHandler go)
+ Game.Goatee.Lib.Monad: getCoordState :: MonadGo go => Coord -> go CoordState
+ Game.Goatee.Lib.Monad: getCursor :: MonadGo go => go Cursor
+ Game.Goatee.Lib.Monad: getMark :: MonadGo go => Coord -> go (Maybe Mark)
+ Game.Goatee.Lib.Monad: getProperties :: MonadGo go => go [Property]
+ Game.Goatee.Lib.Monad: getProperty :: (MonadGo go, Descriptor d) => d -> go (Maybe Property)
+ Game.Goatee.Lib.Monad: getPropertyValue :: (MonadGo go, ValuedDescriptor d v) => d -> go (Maybe v)
+ Game.Goatee.Lib.Monad: goDown :: MonadGo go => Int -> go ()
+ Game.Goatee.Lib.Monad: goToGameInfoNode :: MonadGo go => Bool -> go Bool
+ Game.Goatee.Lib.Monad: goToRoot :: MonadGo go => go ()
+ Game.Goatee.Lib.Monad: goUp :: MonadGo go => go ()
+ Game.Goatee.Lib.Monad: instance Bounded NodeDeleteResult
+ Game.Goatee.Lib.Monad: instance Enum NodeDeleteResult
+ Game.Goatee.Lib.Monad: instance Eq (AnyEvent go)
+ Game.Goatee.Lib.Monad: instance Eq (Event go h)
+ Game.Goatee.Lib.Monad: instance Eq NodeDeleteResult
+ Game.Goatee.Lib.Monad: instance Eq Step
+ Game.Goatee.Lib.Monad: instance Monad m => Applicative (GoT m)
+ Game.Goatee.Lib.Monad: instance Monad m => Functor (GoT m)
+ Game.Goatee.Lib.Monad: instance Monad m => Monad (GoT m)
+ Game.Goatee.Lib.Monad: instance Monad m => MonadGo (GoT m)
+ Game.Goatee.Lib.Monad: instance MonadIO m => MonadIO (GoT m)
+ Game.Goatee.Lib.Monad: instance MonadState s m => MonadState s (GoT m)
+ Game.Goatee.Lib.Monad: instance MonadTrans GoT
+ Game.Goatee.Lib.Monad: instance MonadWriter w m => MonadWriter w (GoT m)
+ Game.Goatee.Lib.Monad: instance Ord (AnyEvent go)
+ Game.Goatee.Lib.Monad: instance Ord (Event go h)
+ Game.Goatee.Lib.Monad: instance Show (AnyEvent go)
+ Game.Goatee.Lib.Monad: instance Show (Event go h)
+ Game.Goatee.Lib.Monad: instance Show NodeDeleteResult
+ Game.Goatee.Lib.Monad: instance Show Step
+ Game.Goatee.Lib.Monad: modifyGameInfo :: MonadGo go => (GameInfo -> GameInfo) -> go GameInfo
+ Game.Goatee.Lib.Monad: modifyMark :: MonadGo go => (Maybe Mark -> Maybe Mark) -> Coord -> go ()
+ Game.Goatee.Lib.Monad: modifyProperties :: MonadGo go => ([Property] -> go [Property]) -> go ()
+ Game.Goatee.Lib.Monad: modifyProperty :: (MonadGo go, Descriptor d) => d -> (Maybe Property -> Maybe Property) -> go ()
+ Game.Goatee.Lib.Monad: modifyPropertyCoords :: (MonadGo go, ValuedDescriptor d CoordList) => d -> ([Coord] -> [Coord]) -> go ()
+ Game.Goatee.Lib.Monad: modifyPropertyString :: (MonadGo go, Stringlike s, ValuedDescriptor d s) => d -> (String -> String) -> go ()
+ Game.Goatee.Lib.Monad: modifyPropertyValue :: (MonadGo go, ValuedDescriptor d v) => d -> (Maybe v -> Maybe v) -> go ()
+ Game.Goatee.Lib.Monad: modifyVariationMode :: MonadGo go => (VariationMode -> VariationMode) -> go ()
+ Game.Goatee.Lib.Monad: navigationEvent :: Event go (NavigationHandler go)
+ Game.Goatee.Lib.Monad: on :: MonadGo go => Event go h -> h -> go ()
+ Game.Goatee.Lib.Monad: on0 :: MonadGo go => Event go h -> go () -> go ()
+ Game.Goatee.Lib.Monad: popPosition :: MonadGo go => go ()
+ Game.Goatee.Lib.Monad: propertiesModifiedEvent :: Event go (PropertiesModifiedHandler go)
+ Game.Goatee.Lib.Monad: pushPosition :: MonadGo go => go ()
+ Game.Goatee.Lib.Monad: putProperty :: MonadGo go => Property -> go ()
+ Game.Goatee.Lib.Monad: runGo :: GoM a -> Cursor -> (a, Cursor)
+ Game.Goatee.Lib.Monad: runGoT :: Monad m => GoT m a -> Cursor -> m (a, Cursor)
+ Game.Goatee.Lib.Monad: type ChildAddedHandler go = Int -> go ()
+ Game.Goatee.Lib.Monad: type ChildDeletedHandler go = Cursor -> go ()
+ Game.Goatee.Lib.Monad: type GameInfoChangedHandler go = GameInfo -> GameInfo -> go ()
+ Game.Goatee.Lib.Monad: type GoM = GoT Identity
+ Game.Goatee.Lib.Monad: type NavigationHandler go = Step -> go ()
+ Game.Goatee.Lib.Monad: type PropertiesModifiedHandler go = [Property] -> [Property] -> go ()
+ Game.Goatee.Lib.Monad: type VariationModeChangedHandler go = VariationMode -> VariationMode -> go ()
+ Game.Goatee.Lib.Monad: variationModeChangedEvent :: Event go (VariationModeChangedHandler go)
+ Game.Goatee.Lib.Parser: parseFile :: String -> IO (Either String Collection)
+ Game.Goatee.Lib.Parser: parseString :: String -> Either String Collection
+ Game.Goatee.Lib.Parser: parseSubtree :: RootInfo -> String -> Either String Node
+ Game.Goatee.Lib.Parser: propertyParser :: Parser Property
+ Game.Goatee.Lib.Property: data PropertyValueType a
+ Game.Goatee.Lib.Property: pvtParser :: PropertyValueType a -> Parser a
+ Game.Goatee.Lib.Property: pvtRenderer :: PropertyValueType a -> a -> Render ()
+ Game.Goatee.Lib.Property: pvtRendererPretty :: PropertyValueType a -> a -> Render ()
+ Game.Goatee.Lib.Property.Parser: colorParser :: Parser Color
+ Game.Goatee.Lib.Property.Parser: compose :: Parser a -> Parser b -> Parser (a, b)
+ Game.Goatee.Lib.Property.Parser: coordElistParser :: Parser CoordList
+ Game.Goatee.Lib.Property.Parser: coordListParser :: Parser CoordList
+ Game.Goatee.Lib.Property.Parser: coordPairListParser :: Parser [(Coord, Coord)]
+ Game.Goatee.Lib.Property.Parser: doubleParser :: Parser DoubleValue
+ Game.Goatee.Lib.Property.Parser: gameResultParser :: Parser GameResult
+ Game.Goatee.Lib.Property.Parser: instance Monoid CoordListMonoid
+ Game.Goatee.Lib.Property.Parser: integralParser :: (Integral a, Read a) => Parser a
+ Game.Goatee.Lib.Property.Parser: labelListParser :: Parser [(Coord, SimpleText)]
+ Game.Goatee.Lib.Property.Parser: line :: Parser Int
+ Game.Goatee.Lib.Property.Parser: moveParser :: Parser (Maybe Coord)
+ Game.Goatee.Lib.Property.Parser: noneParser :: Parser ()
+ Game.Goatee.Lib.Property.Parser: realParser :: Parser RealValue
+ Game.Goatee.Lib.Property.Parser: rulesetParser :: Parser Ruleset
+ Game.Goatee.Lib.Property.Parser: simpleText :: Bool -> Parser SimpleText
+ Game.Goatee.Lib.Property.Parser: simpleTextPairParser :: Parser (SimpleText, SimpleText)
+ Game.Goatee.Lib.Property.Parser: simpleTextParser :: Parser SimpleText
+ Game.Goatee.Lib.Property.Parser: sizeParser :: Parser (Int, Int)
+ Game.Goatee.Lib.Property.Parser: text :: Bool -> Parser String
+ Game.Goatee.Lib.Property.Parser: textParser :: Parser Text
+ Game.Goatee.Lib.Property.Parser: unknownPropertyParser :: Parser UnknownPropertyValue
+ Game.Goatee.Lib.Property.Parser: variationModeParser :: Parser VariationMode
+ Game.Goatee.Lib.Renderer: rendererOf :: Show a => String -> (a -> Render ()) -> a -> Render ()
+ Game.Goatee.Lib.Renderer: runRender :: Render a -> Either String String
+ Game.Goatee.Lib.Renderer: type Render = WriterT String (Either String)
+ Game.Goatee.Lib.Renderer.Tree: renderCollection :: Collection -> Render ()
+ Game.Goatee.Lib.Renderer.Tree: renderGameTree :: Node -> Render ()
+ Game.Goatee.Lib.Renderer.Tree: renderProperty :: Property -> Render ()
+ Game.Goatee.Lib.Tree: Collection :: [Node] -> Collection
+ Game.Goatee.Lib.Tree: CollectionWithDeepEquality :: Collection -> CollectionWithDeepEquality
+ Game.Goatee.Lib.Tree: Node :: [Property] -> [Node] -> Node
+ Game.Goatee.Lib.Tree: NodeWithDeepEquality :: Node -> NodeWithDeepEquality
+ Game.Goatee.Lib.Tree: addChild :: Node -> Node -> Node
+ Game.Goatee.Lib.Tree: addChildAt :: Int -> Node -> Node -> Node
+ Game.Goatee.Lib.Tree: addProperty :: Property -> Node -> Node
+ Game.Goatee.Lib.Tree: collectionTrees :: Collection -> [Node]
+ Game.Goatee.Lib.Tree: collectionWithDeepEquality :: CollectionWithDeepEquality -> Collection
+ Game.Goatee.Lib.Tree: data Collection
+ Game.Goatee.Lib.Tree: data Node
+ Game.Goatee.Lib.Tree: deleteChildAt :: Int -> Node -> Node
+ Game.Goatee.Lib.Tree: emptyNode :: Node
+ Game.Goatee.Lib.Tree: findProperty :: Descriptor a => a -> Node -> Maybe Property
+ Game.Goatee.Lib.Tree: findProperty' :: Descriptor a => a -> [Property] -> Maybe Property
+ Game.Goatee.Lib.Tree: findPropertyValue :: ValuedDescriptor a v => a -> Node -> Maybe v
+ Game.Goatee.Lib.Tree: findPropertyValue' :: ValuedDescriptor a v => a -> [Property] -> Maybe v
+ Game.Goatee.Lib.Tree: instance Eq CollectionWithDeepEquality
+ Game.Goatee.Lib.Tree: instance Eq NodeWithDeepEquality
+ Game.Goatee.Lib.Tree: instance Show Collection
+ Game.Goatee.Lib.Tree: instance Show CollectionWithDeepEquality
+ Game.Goatee.Lib.Tree: instance Show Node
+ Game.Goatee.Lib.Tree: newtype CollectionWithDeepEquality
+ Game.Goatee.Lib.Tree: newtype NodeWithDeepEquality
+ Game.Goatee.Lib.Tree: nodeChildren :: Node -> [Node]
+ Game.Goatee.Lib.Tree: nodeProperties :: Node -> [Property]
+ Game.Goatee.Lib.Tree: nodeWithDeepEquality :: NodeWithDeepEquality -> Node
+ Game.Goatee.Lib.Tree: rootNode :: Maybe (Int, Int) -> Node
+ Game.Goatee.Lib.Tree: validateNode :: Bool -> Bool -> Node -> [String]
+ Game.Goatee.Lib.Types: Black :: Color
+ Game.Goatee.Lib.Types: Double1 :: DoubleValue
+ Game.Goatee.Lib.Types: Double2 :: DoubleValue
+ Game.Goatee.Lib.Types: GameResultDraw :: GameResult
+ Game.Goatee.Lib.Types: GameResultOther :: SimpleText -> GameResult
+ Game.Goatee.Lib.Types: GameResultUnknown :: GameResult
+ Game.Goatee.Lib.Types: GameResultVoid :: GameResult
+ Game.Goatee.Lib.Types: GameResultWin :: Color -> WinReason -> GameResult
+ Game.Goatee.Lib.Types: KnownRuleset :: RulesetType -> Ruleset
+ Game.Goatee.Lib.Types: MarkCircle :: Mark
+ Game.Goatee.Lib.Types: MarkSelected :: Mark
+ Game.Goatee.Lib.Types: MarkSquare :: Mark
+ Game.Goatee.Lib.Types: MarkTriangle :: Mark
+ Game.Goatee.Lib.Types: MarkX :: Mark
+ Game.Goatee.Lib.Types: RulesetAga :: RulesetType
+ Game.Goatee.Lib.Types: RulesetIng :: RulesetType
+ Game.Goatee.Lib.Types: RulesetJapanese :: RulesetType
+ Game.Goatee.Lib.Types: RulesetNewZealand :: RulesetType
+ Game.Goatee.Lib.Types: ShowChildVariations :: VariationModeSource
+ Game.Goatee.Lib.Types: ShowCurrentVariations :: VariationModeSource
+ Game.Goatee.Lib.Types: UnknownRuleset :: String -> Ruleset
+ Game.Goatee.Lib.Types: VariationMode :: VariationModeSource -> Bool -> VariationMode
+ Game.Goatee.Lib.Types: White :: Color
+ Game.Goatee.Lib.Types: WinByForfeit :: WinReason
+ Game.Goatee.Lib.Types: WinByResignation :: WinReason
+ Game.Goatee.Lib.Types: WinByScore :: RealValue -> WinReason
+ Game.Goatee.Lib.Types: WinByTime :: WinReason
+ Game.Goatee.Lib.Types: boardSizeDefault :: Int
+ Game.Goatee.Lib.Types: boardSizeMax :: Int
+ Game.Goatee.Lib.Types: boardSizeMin :: Int
+ Game.Goatee.Lib.Types: buildCoordList :: [Coord] -> CoordList
+ Game.Goatee.Lib.Types: class Stringlike a
+ Game.Goatee.Lib.Types: cnot :: Color -> Color
+ Game.Goatee.Lib.Types: convertStringlike :: (Stringlike a, Stringlike b) => a -> b
+ Game.Goatee.Lib.Types: coord1 :: Coord -> CoordList
+ Game.Goatee.Lib.Types: coordListRects :: CoordList -> [(Coord, Coord)]
+ Game.Goatee.Lib.Types: coordListSingles :: CoordList -> [Coord]
+ Game.Goatee.Lib.Types: coords :: [Coord] -> CoordList
+ Game.Goatee.Lib.Types: coords' :: [Coord] -> [(Coord, Coord)] -> CoordList
+ Game.Goatee.Lib.Types: data Color
+ Game.Goatee.Lib.Types: data CoordList
+ Game.Goatee.Lib.Types: data DoubleValue
+ Game.Goatee.Lib.Types: data GameResult
+ Game.Goatee.Lib.Types: data Mark
+ Game.Goatee.Lib.Types: data Ruleset
+ Game.Goatee.Lib.Types: data RulesetType
+ Game.Goatee.Lib.Types: data SimpleText
+ Game.Goatee.Lib.Types: data Text
+ Game.Goatee.Lib.Types: data UnknownPropertyValue
+ Game.Goatee.Lib.Types: data VariationMode
+ Game.Goatee.Lib.Types: data VariationModeSource
+ Game.Goatee.Lib.Types: data WinReason
+ Game.Goatee.Lib.Types: defaultFormatVersion :: Int
+ Game.Goatee.Lib.Types: defaultVariationMode :: VariationMode
+ Game.Goatee.Lib.Types: emptyCoordList :: CoordList
+ Game.Goatee.Lib.Types: expandCoordList :: CoordList -> [Coord]
+ Game.Goatee.Lib.Types: fromRuleset :: Ruleset -> String
+ Game.Goatee.Lib.Types: fromSimpleText :: SimpleText -> String
+ Game.Goatee.Lib.Types: fromText :: Text -> String
+ Game.Goatee.Lib.Types: fromUnknownPropertyValue :: UnknownPropertyValue -> String
+ Game.Goatee.Lib.Types: fromVariationMode :: VariationMode -> Int
+ Game.Goatee.Lib.Types: handicapStones :: Int -> Int -> Int -> Maybe [Coord]
+ Game.Goatee.Lib.Types: instance Bounded Mark
+ Game.Goatee.Lib.Types: instance Bounded RulesetType
+ Game.Goatee.Lib.Types: instance Bounded VariationModeSource
+ Game.Goatee.Lib.Types: instance Enum Mark
+ Game.Goatee.Lib.Types: instance Enum RulesetType
+ Game.Goatee.Lib.Types: instance Enum VariationModeSource
+ Game.Goatee.Lib.Types: instance Eq Color
+ Game.Goatee.Lib.Types: instance Eq CoordList
+ Game.Goatee.Lib.Types: instance Eq DoubleValue
+ Game.Goatee.Lib.Types: instance Eq GameResult
+ Game.Goatee.Lib.Types: instance Eq Mark
+ Game.Goatee.Lib.Types: instance Eq Ruleset
+ Game.Goatee.Lib.Types: instance Eq RulesetType
+ Game.Goatee.Lib.Types: instance Eq SimpleText
+ Game.Goatee.Lib.Types: instance Eq Text
+ Game.Goatee.Lib.Types: instance Eq UnknownPropertyValue
+ Game.Goatee.Lib.Types: instance Eq VariationMode
+ Game.Goatee.Lib.Types: instance Eq VariationModeSource
+ Game.Goatee.Lib.Types: instance Eq WinReason
+ Game.Goatee.Lib.Types: instance Show Color
+ Game.Goatee.Lib.Types: instance Show CoordList
+ Game.Goatee.Lib.Types: instance Show DoubleValue
+ Game.Goatee.Lib.Types: instance Show GameResult
+ Game.Goatee.Lib.Types: instance Show Mark
+ Game.Goatee.Lib.Types: instance Show Ruleset
+ Game.Goatee.Lib.Types: instance Show RulesetType
+ Game.Goatee.Lib.Types: instance Show SimpleText
+ Game.Goatee.Lib.Types: instance Show Text
+ Game.Goatee.Lib.Types: instance Show UnknownPropertyValue
+ Game.Goatee.Lib.Types: instance Show VariationMode
+ Game.Goatee.Lib.Types: instance Show VariationModeSource
+ Game.Goatee.Lib.Types: instance Show WinReason
+ Game.Goatee.Lib.Types: instance Stringlike GameResult
+ Game.Goatee.Lib.Types: instance Stringlike Ruleset
+ Game.Goatee.Lib.Types: instance Stringlike SimpleText
+ Game.Goatee.Lib.Types: instance Stringlike String
+ Game.Goatee.Lib.Types: instance Stringlike Text
+ Game.Goatee.Lib.Types: instance Stringlike UnknownPropertyValue
+ Game.Goatee.Lib.Types: isStarPoint :: Int -> Int -> Int -> Int -> Bool
+ Game.Goatee.Lib.Types: sgfToString :: Stringlike a => a -> String
+ Game.Goatee.Lib.Types: starLines :: Int -> Int -> Maybe [Int]
+ Game.Goatee.Lib.Types: stringToSgf :: Stringlike a => String -> a
+ Game.Goatee.Lib.Types: supportedFormatVersions :: [Int]
+ Game.Goatee.Lib.Types: supportedGameTypes :: [Int]
+ Game.Goatee.Lib.Types: toRuleset :: String -> Ruleset
+ Game.Goatee.Lib.Types: toSimpleText :: String -> SimpleText
+ Game.Goatee.Lib.Types: toText :: String -> Text
+ Game.Goatee.Lib.Types: toUnknownPropertyValue :: String -> UnknownPropertyValue
+ Game.Goatee.Lib.Types: toVariationMode :: Int -> Maybe VariationMode
+ Game.Goatee.Lib.Types: type ArrowList = [(Coord, Coord)]
+ Game.Goatee.Lib.Types: type Coord = (Int, Int)
+ Game.Goatee.Lib.Types: type LabelList = [(Coord, SimpleText)]
+ Game.Goatee.Lib.Types: type LineList = [(Coord, Coord)]
+ Game.Goatee.Lib.Types: type RealValue = Bigfloat
+ Game.Goatee.Lib.Types: variationModeBoardMarkup :: VariationMode -> Bool
+ Game.Goatee.Lib.Types: variationModeSource :: VariationMode -> VariationModeSource

Files

goatee.cabal view
@@ -1,5 +1,5 @@ name: goatee-version: 0.1.1+version: 0.2.0 synopsis: A monadic take on a 2,500-year-old board game - library. category: Game license: AGPL-3@@ -14,13 +14,11 @@ build-type: Simple data-files: LICENSE description:-    Goatee is a Go library and game editor, written in Haskell.  It provides a GUI-    for recording, studying, and editing game records.  Underneath this is a-    portable library for manipulating SGF files, to build UIs and tools.-    .-    Goatee, the library and GUI, aims to be full-featured, supporting all of the SGF-    spec and allowing for full customization of the game records you create.-    Currently it is in an alpha stage, supporting basic game viewing and editing.+    Goatee is a Go library and game editor, written in Haskell.  It provides a+    GUI for recording, studying, and editing game records.  Underneath this is a+    portable library for manipulating SGF files to build UIs and tools.  Goatee+    aims to be full-featured by supporting all of the SGF spec and allowing for+    full and easy customization of the game records you create.     .     This package is the shared library. @@ -34,19 +32,20 @@         containers >= 0.4 && < 0.6,         mtl >= 2.1 && < 2.3,         parsec >= 3.1 && < 3.2,-        template-haskell >= 2.7 && < 2.9+        template-haskell >= 2.7 && < 3.0     exposed-modules:         Game.Goatee.App         Game.Goatee.Common-        Game.Goatee.Sgf.Board-        Game.Goatee.Sgf.Monad-        Game.Goatee.Sgf.Parser-        Game.Goatee.Sgf.Property-        Game.Goatee.Sgf.Property.Parser-        Game.Goatee.Sgf.Renderer-        Game.Goatee.Sgf.Renderer.Tree-        Game.Goatee.Sgf.Tree-        Game.Goatee.Sgf.Types+        Game.Goatee.Common.Bigfloat+        Game.Goatee.Lib.Board+        Game.Goatee.Lib.Monad+        Game.Goatee.Lib.Parser+        Game.Goatee.Lib.Property+        Game.Goatee.Lib.Property.Parser+        Game.Goatee.Lib.Renderer+        Game.Goatee.Lib.Renderer.Tree+        Game.Goatee.Lib.Tree+        Game.Goatee.Lib.Types     extensions:         ExistentialQuantification         FlexibleContexts@@ -57,10 +56,10 @@     ghc-options: -W -fwarn-incomplete-patterns     hs-source-dirs: src     other-modules:-        Game.Goatee.Sgf.Property.Base-        Game.Goatee.Sgf.Property.Info-        Game.Goatee.Sgf.Property.Renderer-        Game.Goatee.Sgf.Property.Value+        Game.Goatee.Lib.Property.Base+        Game.Goatee.Lib.Property.Info+        Game.Goatee.Lib.Property.Renderer+        Game.Goatee.Lib.Property.Value         Paths_goatee  test-suite test-goatee@@ -70,25 +69,24 @@         goatee,         HUnit >= 1.2 && < 1.3,         mtl >= 2.1 && < 2.3,-        parsec >= 3.1 && < 3.2,-        test-framework >= 0.6 && < 0.9,-        test-framework-hunit >= 0.2 && < 0.4+        parsec >= 3.1 && < 3.2     ghc-options: -W -fwarn-incomplete-patterns     hs-source-dirs: tests     main-is: Test.hs     other-modules:+        Game.Goatee.Common.BigfloatTest         Game.Goatee.CommonTest-        Game.Goatee.Sgf.BoardTest-        Game.Goatee.Sgf.MonadTest-        Game.Goatee.Sgf.ParserTest-        Game.Goatee.Sgf.ParserTestUtils-        Game.Goatee.Sgf.Property.ParserTest-        Game.Goatee.Sgf.PropertyTest-        Game.Goatee.Sgf.RoundTripTest-        Game.Goatee.Sgf.TestInstances-        Game.Goatee.Sgf.TestUtils-        Game.Goatee.Sgf.TreeTest-        Game.Goatee.Sgf.TypesTest+        Game.Goatee.Lib.BoardTest+        Game.Goatee.Lib.MonadTest+        Game.Goatee.Lib.ParserTest+        Game.Goatee.Lib.ParserTestUtils+        Game.Goatee.Lib.Property.ParserTest+        Game.Goatee.Lib.PropertyTest+        Game.Goatee.Lib.RoundTripTest+        Game.Goatee.Lib.TestInstances+        Game.Goatee.Lib.TestUtils+        Game.Goatee.Lib.TreeTest+        Game.Goatee.Lib.TypesTest         Game.Goatee.Test.Common         Test     type: exitcode-stdio-1.0
src/Game/Goatee/Common.hs view
@@ -17,13 +17,10 @@  -- | Common utilities used throughout the project. module Game.Goatee.Common (-  listDeleteIndex,+  listDeleteAt,+  listInsertAt,   listReplace,   listUpdate,-  fromLeft,-  fromRight,-  onLeft,-  onRight,   andEithers,   for,   mapTuple,@@ -35,19 +32,25 @@   whileM,   whileM',   doWhileM,-  Seq(..),   ) where  import Control.Arrow ((***)) import Control.Monad (forM_, join, when) import Data.Either (partitionEithers)-import Data.Monoid (Monoid, mempty, mappend)  -- | Drops the element at an index from a list.  If the index is out of bounds -- then the list is returned unmodified.-listDeleteIndex :: Int -> [a] -> [a]-listDeleteIndex index list = take index list ++ drop (index + 1) list+listDeleteAt :: Int -> [a] -> [a]+listDeleteAt index list = take index list ++ drop (index + 1) list +-- | Inserts the element into the list before the given position.  If the+-- position is less than 0 or greater than the length of the list, then the+-- index is clamped to this range.+listInsertAt :: Int -> a -> [a] -> [a]+listInsertAt index x xs =+  let (before, after) = splitAt index xs+  in before ++ x : after+ -- | @listReplace old new list@ replaces all occurrences of @old@ with @new@ in -- @list@. listReplace :: Eq a => a -> a -> [a] -> [a]@@ -62,27 +65,6 @@         listSet' _ _ = error ("Cannot update index " ++ show ix ++                               " of list " ++ show xs ++ ".") --- | Extracts a left value from an 'Either'.-fromLeft :: Either a b -> a-fromLeft (Left a) = a-fromLeft _ = error "fromLeft given a Right."---- | Extracts a right value from an 'Either'.-fromRight :: Either a b -> b-fromRight (Right b) = b-fromRight _ = error "fromRight given a Left."---- | Transforms the left value of an 'Either', leaving a right value alone.-onLeft :: (a -> c) -> Either a b -> Either c b-f `onLeft` e = case e of-  Left x -> Left $ f x-  Right y -> Right y---- | Transforms the right value of an 'Either', leaving a left value alone.--- This is just 'fmap', but looks nicer when used beside 'onLeft'.-onRight :: (b -> c) -> Either a b -> Either a c-onRight = fmap- -- | If any item is a 'Left', then the list of 'Left's is returned, otherwise -- the list of 'Right's is returned. andEithers :: [Either a b] -> Either [a] [b]@@ -148,13 +130,3 @@   case value of     Right next -> doWhileM next body     Left end -> return end---- | This sequences @()@-valued monadic actions as a monoid.  If @m@ is some--- monad, then @Seq m@ is a monoid where 'mempty' does nothing and 'mappend'--- sequences actions via '>>'.-newtype Seq m = Seq (m ())--instance Monad m => Monoid (Seq m) where-  mempty = Seq $ return ()--  (Seq x) `mappend` (Seq y) = Seq (x >> y)
+ src/Game/Goatee/Common/Bigfloat.hs view
@@ -0,0 +1,183 @@+-- This file is part of Goatee.+--+-- Copyright 2014 Bryan Gardiner+--+-- Goatee is free software: you can redistribute it and/or modify+-- it under the terms of the GNU Affero General Public License as published by+-- the Free Software Foundation, either version 3 of the License, or+-- (at your option) any later version.+--+-- Goatee is distributed in the hope that it will be useful,+-- but WITHOUT ANY WARRANTY; without even the implied warranty of+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the+-- GNU Affero General Public License for more details.+--+-- You should have received a copy of the GNU Affero General Public License+-- along with Goatee.  If not, see <http://www.gnu.org/licenses/>.++-- | Base-10 arbitrary-precision floating-point numbers.+module Game.Goatee.Common.Bigfloat (+  Bigfloat, encode,+  significand, exponent,+  fromDouble, toDouble,+  ) where++import Data.Char (isDigit, isSpace)+import Data.Function (on)+import Prelude hiding (exponent, significand)++-- | A base-10, infinite-precision, floating-point number.  Implemented as an+-- infinite-precision significand together with an exponent, such that the+-- numeric value is equal to @'significand' f * (10 ^ 'exponent' f)@.  The+-- exponent is a limited-precision 'Int', because some operations may break if+-- the exponent is larger (specifically 'show' and 'toDouble').  This shouldn't+-- be an issue for Goatee.+--+-- These values form an integral domain.+--+-- The 'Show' instance always outputs in decimal notation, never scientific+-- notation.  Examples:+--+-- > 300   (never trailing .0 if there's no fractional part)+-- > 0.1   (never redundant trailing or leading zeros)+--+-- Similarly, the 'Read' instance accepts numbers matching the regex+-- @-?\\d+(\\.\\d+)?(e-?\\d+)?@.  Scientific exponent notation is supported for+-- reading, for ease of converting 'Double's to 'Bigfloat's.+data Bigfloat = Bigfloat+  { significand :: !Integer+  , exponent :: !Int+  }++zero, one, negOne :: Bigfloat+zero = Bigfloat 0 0+one = Bigfloat 1 0+negOne = Bigfloat (-1) 0++instance Eq Bigfloat where+  x == y = let (Bigfloat xv xe, Bigfloat yv ye) = normalize2 x y+           in xe == ye && xv == yv++instance Ord Bigfloat where+  compare = (uncurry (compare `on` significand) .) . normalize2++instance Num Bigfloat where+  (+) = lift2 (+)+  (-) = lift2 (-)+  Bigfloat xv xe * Bigfloat yv ye = reduce $ Bigfloat (xv * yv) (xe + ye)+  negate (Bigfloat v e) = Bigfloat (-v) e+  abs x@(Bigfloat v e) = if v >= 0 then x else Bigfloat (-v) e+  signum (Bigfloat v _)+    | v == 0 = zero+    | v > 0 = one+    | otherwise = negOne+  fromInteger v = reduce $ Bigfloat v 0++instance Show Bigfloat where+  show (Bigfloat v e) =+    let (addSign, vs) = if v >= 0+                        then (id, show v)+                        else (('-':), show (-v))+        vl = length vs+    in addSign $ case e of+      0 -> vs+      e | e > 0 -> vs ++ replicate e '0'+        | e <= -vl -> '0' : '.' : replicate ((-e) - vl) '0' ++ vs+      _ -> let (hd, tl) = splitAt (vl + e) vs+           in hd ++ '.' : tl++instance Read Bigfloat where+  readsPrec _ s =+    let (s', neg) = case s of+          '-':s' -> (s', True)+          _ -> (s, False)+        (whole, s'') = span isDigit s'+    in if null whole+       then []+       else case s'' of+         '.':s''' -> let (fractional, s'''') = span isDigit s'''+                     in if null fractional+                        then []+                        else succeedIfTerminatedProperly neg whole fractional s''''+         s''' -> succeedIfTerminatedProperly neg whole [] s'''+    where succeedIfTerminatedProperly neg whole fractional rest =+            let makeResult exp =+                  encode (fromInteger $+                          read $+                          (if neg then ('-':) else id) $+                          whole ++ fractional)+                         (-length fractional + exp)+            in if isValidEndOfNumber rest+               then [(makeResult 0, rest)]+               else case rest of+                 'e':exps -> let (addExpNeg, exps') = case exps of+                                   '-':exps' -> (('-':), exps')+                                   _ -> (id, exps)+                                 (hd, tl) = span isDigit exps'+                             in if null hd+                                then []+                                else let exp = read (addExpNeg exps') :: Int+                                     in [(makeResult exp, tl) | isValidEndOfNumber tl]+                 _ -> []+          isValidEndOfNumber rest = case rest of+            [] -> True+            c:_ | isSpace c -> True+            _ -> False++-- | @encode significand exponent@ creates a 'Bigfloat' value whose numeric+-- value is @significand * (10 ^ exponent)@.+encode :: Integer -> Int -> Bigfloat+encode = (reduce .) . Bigfloat++-- | Converts a 'Double' to a 'Bigfloat' (with as much precision as the 'Double'+-- 'Show' instance provides).+fromDouble :: Double -> Bigfloat+fromDouble = read . show++-- | Converts a 'Bigfloat' to a 'Double', lossily.+toDouble :: Bigfloat -> Double+toDouble = read . show++-- | @shift amount float@ adds @shift@ zeros onto the right side of @float@'s+-- numerator while keeping the numeric value the same.  @amount@ must be+-- non-negative.+shift :: Int -> Bigfloat -> Bigfloat+shift amount float@(Bigfloat v e) =+  if amount < 0+  then error $ "bigfloatShift: Can't shift by a negative amount.  amount = " +++       show amount ++ ", float = " ++ show float+  else Bigfloat (v * 10 ^ amount) (e - amount)++-- | Reduces a 'Bigfloat' to canonical form, keeping the numeric value the same+-- but removing trailing zeros from the numerator.+reduce :: Bigfloat -> Bigfloat+reduce x@(Bigfloat v e) =+  if v == 0+  then zero+  else let zeros = length $ takeWhile (== '0') $ reverse $ show v+       in if zeros == 0+          then x+          else Bigfloat (v `div` (10 ^ zeros)) (e + zeros)++-- | Converts two 'Bigfloat's so that they have the same number of decimal+-- places, so that 'Integer' arithmetic may be performed directly on their+-- 'significand's.+normalize2 :: Bigfloat -> Bigfloat -> (Bigfloat, Bigfloat)+normalize2 x y =+  let xe = exponent x+      ye = exponent y+  in if xe == ye+     then (x, y)+     else if xe < ye+          then (x, shift (ye - xe) y)+          else (shift (xe - ye) x, y)++-- | Lifts a function on two 'Integer's to a function on 'Bigfloat's.+--+-- This is not exported from this module because it's not a general lift+-- function: the given function only operates on the significands, so operations+-- that require the exponent (such as multiplication) can't use this function.+lift2 :: (Integer -> Integer -> Integer) -> Bigfloat -> Bigfloat -> Bigfloat+lift2 f x y =+  let (Bigfloat xv xe, Bigfloat yv _) = normalize2 x y+  in reduce $ Bigfloat (f xv yv) xe
+ src/Game/Goatee/Lib/Board.hs view
@@ -0,0 +1,768 @@+-- This file is part of Goatee.+--+-- Copyright 2014 Bryan Gardiner+--+-- Goatee is free software: you can redistribute it and/or modify+-- it under the terms of the GNU Affero General Public License as published by+-- the Free Software Foundation, either version 3 of the License, or+-- (at your option) any later version.+--+-- Goatee is distributed in the hope that it will be useful,+-- but WITHOUT ANY WARRANTY; without even the implied warranty of+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the+-- GNU Affero General Public License for more details.+--+-- You should have received a copy of the GNU Affero General Public License+-- along with Goatee.  If not, see <http://www.gnu.org/licenses/>.++-- | Data structures that wrap and provide a higher-level interface to the SGF+-- game tree, including a zipper that navigates the tree and provides the+-- current board state.+module Game.Goatee.Lib.Board (+  RootInfo(..), GameInfo(..), emptyGameInfo, internalIsGameInfoNode,+  gameInfoToProperties,+  BoardState(..), boardWidth, boardHeight,+  CoordState(..), rootBoardState, boardCoordState, mapBoardCoords,+  isValidMove, isCurrentValidMove,+  Cursor(..), rootCursor, cursorRoot, cursorChild, cursorChildren,+  cursorChildCount, cursorChildPlayingAt, cursorProperties,+  cursorModifyNode,+  cursorVariations,+  moveToProperty,+  ) where++import Control.Monad (unless, when)+import Control.Monad.Writer (execWriter, tell)+import Data.List (find, intercalate, nub)+import Data.Maybe (fromMaybe, isJust, isNothing)+import qualified Data.Set as Set+import Game.Goatee.Common+import Game.Goatee.Lib.Property+import Game.Goatee.Lib.Tree+import Game.Goatee.Lib.Types++-- TODO Stop using errors everywhere, they're not testable.++-- | Properties that are specified in the root nodes of game trees.+data RootInfo = RootInfo+  { rootInfoWidth :: Int+  , rootInfoHeight :: Int+  , rootInfoVariationMode :: VariationMode+  } deriving (Eq, Show)++-- | Properties that are specified in game info nodes.+data GameInfo = GameInfo+  { gameInfoRootInfo :: RootInfo++  , gameInfoBlackName :: Maybe SimpleText+  , gameInfoBlackTeamName :: Maybe SimpleText+  , gameInfoBlackRank :: Maybe SimpleText++  , gameInfoWhiteName :: Maybe SimpleText+  , gameInfoWhiteTeamName :: Maybe SimpleText+  , gameInfoWhiteRank :: Maybe SimpleText++  , gameInfoRuleset :: Maybe Ruleset+  , gameInfoBasicTimeSeconds :: Maybe RealValue+  , gameInfoOvertime :: Maybe SimpleText+  , gameInfoResult :: Maybe GameResult++  , gameInfoGameName :: Maybe SimpleText+  , gameInfoGameComment :: Maybe Text+  , gameInfoOpeningComment :: Maybe SimpleText++  , gameInfoEvent :: Maybe SimpleText+  , gameInfoRound :: Maybe SimpleText+  , gameInfoPlace :: Maybe SimpleText+  , gameInfoDatesPlayed :: Maybe SimpleText+  , gameInfoSource :: Maybe SimpleText+  , gameInfoCopyright :: Maybe SimpleText++  , gameInfoAnnotatorName :: Maybe SimpleText+  , gameInfoEntererName :: Maybe SimpleText+  } deriving (Show)++-- | Builds a 'GameInfo' with the given 'RootInfo' and no extra data.+emptyGameInfo :: RootInfo -> GameInfo+emptyGameInfo rootInfo = GameInfo+  { gameInfoRootInfo = rootInfo++  , gameInfoBlackName = Nothing+  , gameInfoBlackTeamName = Nothing+  , gameInfoBlackRank = Nothing++  , gameInfoWhiteName = Nothing+  , gameInfoWhiteTeamName = Nothing+  , gameInfoWhiteRank = Nothing++  , gameInfoRuleset = Nothing+  , gameInfoBasicTimeSeconds = Nothing+  , gameInfoOvertime = Nothing+  , gameInfoResult = Nothing++  , gameInfoGameName = Nothing+  , gameInfoGameComment = Nothing+  , gameInfoOpeningComment = Nothing++  , gameInfoEvent = Nothing+  , gameInfoRound = Nothing+  , gameInfoPlace = Nothing+  , gameInfoDatesPlayed = Nothing+  , gameInfoSource = Nothing+  , gameInfoCopyright = Nothing++  , gameInfoAnnotatorName = Nothing+  , gameInfoEntererName = Nothing+  }++-- | Returns whether a node contains any game info properties.+internalIsGameInfoNode :: Node -> Bool+internalIsGameInfoNode = any ((GameInfoProperty ==) . propertyType) . nodeProperties++-- | Converts a 'GameInfo' into a list of 'Property's that can be used to+-- reconstruct the 'GameInfo'.+gameInfoToProperties :: GameInfo -> [Property]+gameInfoToProperties info = execWriter $ do+  copy PB gameInfoBlackName+  copy BT gameInfoBlackTeamName+  copy BR gameInfoBlackRank++  copy PW gameInfoWhiteName+  copy WT gameInfoWhiteTeamName+  copy WR gameInfoWhiteRank++  copy RU gameInfoRuleset+  copy TM gameInfoBasicTimeSeconds+  copy OT gameInfoOvertime+  copy RE gameInfoResult++  copy GN gameInfoGameName+  copy GC gameInfoGameComment+  copy ON gameInfoOpeningComment++  copy EV gameInfoEvent+  copy RO gameInfoRound+  copy PC gameInfoPlace+  copy DT gameInfoDatesPlayed+  copy SO gameInfoSource+  copy CP gameInfoCopyright++  copy AN gameInfoAnnotatorName+  copy US gameInfoEntererName+  where copy ctor accessor = whenMaybe (accessor info) $ \x -> tell [ctor x]++-- | An object that corresponds to a node in some game tree, and represents the+-- state of the game at that node, including board position, player turn and+-- captures, and also board annotations.+data BoardState = BoardState+  { boardCoordStates :: [[CoordState]]+    -- ^ The state of individual points on the board.  Stored in row-major order.+    -- Point @(x, y)@ can be accessed via @!! y !! x@ (but prefer+    -- 'boardCoordState').+  , boardHasInvisible :: Bool+    -- ^ Whether any of the board's 'CoordState's are invisible.  This is an+    -- optimization to make it more efficient to set the board to "all visible."+  , boardHasDimmed :: Bool+    -- ^ Whether any of the board's 'CoordState's are dimmed.  This is an+    -- optimization to make it more efficient to clear all dimming from the+    -- board.+  , boardArrows :: ArrowList+  , boardLines :: LineList+  , boardLabels :: LabelList+  , boardMoveNumber :: Integer+  , boardPlayerTurn :: Color+  , boardBlackCaptures :: Int+  , boardWhiteCaptures :: Int+  , boardGameInfo :: GameInfo+  }++instance Show BoardState where+  show board = concat $ execWriter $ do+    tell ["Board: (Move ", show (boardMoveNumber board),+          ", ", show (boardPlayerTurn board), "'s turn, B:",+          show (boardBlackCaptures board), ", W:",+          show (boardWhiteCaptures board), ")\n"]+    tell [intercalate "\n" $ flip map (boardCoordStates board) $+          \row -> unwords $ map show row]++    let arrows = boardArrows board+    let lines = boardLines board+    let labels = boardLabels board+    unless (null arrows) $ tell ["\nArrows: ", show arrows]+    unless (null lines) $ tell ["\nLines: ", show lines]+    unless (null labels) $ tell ["\nLabels: ", show labels]++-- | Returns the width of the board, in stones.+boardWidth :: BoardState -> Int+boardWidth = rootInfoWidth . gameInfoRootInfo . boardGameInfo++-- | Returns the height of the board, in stones.+boardHeight :: BoardState -> Int+boardHeight = rootInfoHeight . gameInfoRootInfo . boardGameInfo++-- | Used by 'BoardState' to represent the state of a single point on the board.+-- Records whether a stone is present, as well as annotations and visibility+-- properties.+data CoordState = CoordState+  { coordStar :: Bool+    -- ^ Whether this point is a star point.+  , coordStone :: Maybe Color+  , coordMark :: Maybe Mark+  , coordVisible :: Bool+  , coordDimmed :: Bool+  }++instance Show CoordState where+  show c = if not $ coordVisible c+           then "--"+           else let stoneChar = case coordStone c of+                      Nothing -> if coordStar c then '*' else '\''+                      Just Black -> 'X'+                      Just White -> 'O'+                    markChar = case coordMark c of+                      Nothing -> ' '+                      Just MarkCircle -> 'o'+                      Just MarkSquare -> 's'+                      Just MarkTriangle -> 'v'+                      Just MarkX -> 'x'+                      Just MarkSelected -> '!'+                in [stoneChar, markChar]++-- | Creates a 'BoardState' for an empty board of the given width and height.+emptyBoardState :: Int -> Int -> BoardState+emptyBoardState width height = BoardState+  { boardCoordStates = coords+  , boardHasInvisible = False+  , boardHasDimmed = False+  , boardArrows = []+  , boardLines = []+  , boardLabels = []+  , boardMoveNumber = 0+  , boardPlayerTurn = Black+  , boardBlackCaptures = 0+  , boardWhiteCaptures = 0+  , boardGameInfo = emptyGameInfo rootInfo+  }+  where rootInfo = RootInfo { rootInfoWidth = width+                            , rootInfoHeight = height+                            , rootInfoVariationMode = defaultVariationMode+                            }+        emptyCoord = CoordState { coordStar = False+                                , coordStone = Nothing+                                , coordMark = Nothing+                                , coordVisible = True+                                , coordDimmed = False+                                }+        starCoord = emptyCoord { coordStar = True }+        isStarPoint' = isStarPoint width height+        coords = map (\y -> map (\x -> if isStarPoint' x y then starCoord else emptyCoord)+                                [0..width-1])+                     [0..height-1]++rootBoardState :: Node -> BoardState+rootBoardState rootNode =+  foldr applyProperty+        (emptyBoardState width height)+        (nodeProperties rootNode)+  where SZ width height = fromMaybe (SZ boardSizeDefault boardSizeDefault) $+                          findProperty propertySZ rootNode++-- | Returns the 'CoordState' for a coordinate on a board.+boardCoordState :: Coord -> BoardState -> CoordState+boardCoordState (x, y) board = boardCoordStates board !! y !! x++-- | Maps a function over each 'CoordState' in a 'BoardState', returning a+-- list-of-lists with the function's values.  The function is called like @fn y+-- x coordState@.+mapBoardCoords :: (Int -> Int -> CoordState -> a) -> BoardState -> [[a]]+mapBoardCoords fn board =+  zipWith applyRow [0..] $ boardCoordStates board+  where applyRow y = zipWith (fn y) [0..]++-- | Applies a function to update the 'RootInfo' within the 'GameInfo' of a+-- 'BoardState'.+updateRootInfo :: (RootInfo -> RootInfo) -> BoardState -> BoardState+updateRootInfo fn board = flip updateBoardInfo board $ \gameInfo ->+  gameInfo { gameInfoRootInfo = fn $ gameInfoRootInfo gameInfo }++-- | Applies a function to update the 'GameInfo' of a 'BoardState'.+updateBoardInfo :: (GameInfo -> GameInfo) -> BoardState -> BoardState+updateBoardInfo fn board = board { boardGameInfo = fn $ boardGameInfo board }++-- | Performs necessary updates to a 'BoardState' between nodes in the tree.+-- Clears marks.+boardChild :: BoardState -> BoardState+boardChild board =+  board { boardCoordStates = map (map clearMark) $ boardCoordStates board+        , boardArrows = []+        , boardLines = []+        , boardLabels = []+        }+  where clearMark coord = case coordMark coord of+          Nothing -> coord+          Just _ -> coord { coordMark = Nothing }++-- | Sets all points on a board to be visible (if given true) or invisible (if+-- given false).+setBoardVisible :: Bool -> BoardState -> BoardState+setBoardVisible visible board =+  if visible+  then if boardHasInvisible board+       then board { boardCoordStates = map (map $ setVisible True) $ boardCoordStates board+                  , boardHasInvisible = False+                  }+       else board+  else board { boardCoordStates = map (map $ setVisible False) $ boardCoordStates board+             , boardHasInvisible = True+             }+  where setVisible vis coord = coord { coordVisible = vis }++-- | Resets all points on a board not to be dimmed.+clearBoardDimmed :: BoardState -> BoardState+clearBoardDimmed board =+  if boardHasDimmed board+  then board { boardCoordStates = map (map clearDim) $ boardCoordStates board+             , boardHasDimmed = False+             }+  else board+  where clearDim coord = coord { coordDimmed = False }++-- | Applies a property to a 'BoardState'.  This function covers all properties+-- that modify 'BoardState's, including making moves, adding markup, and so on.+applyProperty :: Property -> BoardState -> BoardState++applyProperty (B maybeXy) board = updateBoardForMove Black $ case maybeXy of+  Nothing -> board  -- Pass.+  Just xy -> getApplyMoveResult board $+             applyMove playTheDarnMoveGoParams Black xy board+applyProperty KO board = board+applyProperty (MN moveNum) board = board { boardMoveNumber = moveNum }+applyProperty (W maybeXy) board = updateBoardForMove White $ case maybeXy of+  Nothing -> board  -- Pass.+  Just xy -> getApplyMoveResult board $+             applyMove playTheDarnMoveGoParams White xy board++applyProperty (AB coords) board =+  updateCoordStates' (\state -> state { coordStone = Just Black }) coords board+applyProperty (AW coords) board =+  updateCoordStates' (\state -> state { coordStone = Just White }) coords board+applyProperty (AE coords) board =+  updateCoordStates' (\state -> state { coordStone = Nothing }) coords board+applyProperty (PL color) board = board { boardPlayerTurn = color }++applyProperty (C {}) board = board+applyProperty (DM {}) board = board+applyProperty (GB {}) board = board+applyProperty (GW {}) board = board+applyProperty (HO {}) board = board+applyProperty (N {}) board = board+applyProperty (UC {}) board = board+applyProperty (V {}) board = board++applyProperty (BM {}) board = board+applyProperty (DO {}) board = board+applyProperty (IT {}) board = board+applyProperty (TE {}) board = board++applyProperty (AR arrows) board = board { boardArrows = arrows ++ boardArrows board }+applyProperty (CR coords) board =+  updateCoordStates' (\state -> state { coordMark = Just MarkCircle }) coords board+applyProperty (DD coords) board =+  let coords' = expandCoordList coords+      board' = clearBoardDimmed board+  in if null coords'+     then board'+     else updateCoordStates (\state -> state { coordDimmed = True }) coords'+          board { boardHasDimmed = True }+applyProperty (LB labels) board = board { boardLabels = labels ++ boardLabels board }+applyProperty (LN lines) board = board { boardLines = lines ++ boardLines board }+applyProperty (MA coords) board =+  updateCoordStates' (\state -> state { coordMark = Just MarkX }) coords board+applyProperty (SL coords) board =+  updateCoordStates' (\state -> state { coordMark = Just MarkSelected }) coords board+applyProperty (SQ coords) board =+  updateCoordStates' (\state -> state { coordMark = Just MarkSquare }) coords board+applyProperty (TR coords) board =+  updateCoordStates' (\state -> state { coordMark = Just MarkTriangle }) coords board++applyProperty (AP {}) board = board+applyProperty (CA {}) board = board+applyProperty (FF {}) board = board+applyProperty (GM {}) board = board+applyProperty (ST variationMode) board =+  updateRootInfo (\info -> info { rootInfoVariationMode = variationMode }) board+applyProperty (SZ {}) board = board++applyProperty (AN str) board =+  updateBoardInfo (\info -> info { gameInfoAnnotatorName = Just str }) board+applyProperty (BR str) board =+  updateBoardInfo (\info -> info { gameInfoBlackRank = Just str }) board+applyProperty (BT str) board =+  updateBoardInfo (\info -> info { gameInfoBlackTeamName = Just str }) board+applyProperty (CP str) board =+  updateBoardInfo (\info -> info { gameInfoCopyright = Just str }) board+applyProperty (DT str) board =+  updateBoardInfo (\info -> info { gameInfoDatesPlayed = Just str }) board+applyProperty (EV str) board =+  updateBoardInfo (\info -> info { gameInfoEvent = Just str }) board+applyProperty (GC str) board =+  updateBoardInfo (\info -> info { gameInfoGameComment = Just str }) board+applyProperty (GN str) board =+  updateBoardInfo (\info -> info { gameInfoGameName = Just str }) board+applyProperty (ON str) board =+  updateBoardInfo (\info -> info { gameInfoOpeningComment = Just str }) board+applyProperty (OT str) board =+  updateBoardInfo (\info -> info { gameInfoOvertime = Just str }) board+applyProperty (PB str) board =+  updateBoardInfo (\info -> info { gameInfoBlackName = Just str }) board+applyProperty (PC str) board =+  updateBoardInfo (\info -> info { gameInfoPlace = Just str }) board+applyProperty (PW str) board =+  updateBoardInfo (\info -> info { gameInfoWhiteName = Just str }) board+applyProperty (RE result) board =+  updateBoardInfo (\info -> info { gameInfoResult = Just result }) board+applyProperty (RO str) board =+  updateBoardInfo (\info -> info { gameInfoRound = Just str }) board+applyProperty (RU ruleset) board =+  updateBoardInfo (\info -> info { gameInfoRuleset = Just ruleset }) board+applyProperty (SO str) board =+  updateBoardInfo (\info -> info { gameInfoSource = Just str }) board+applyProperty (TM seconds) board =+  updateBoardInfo (\info -> info { gameInfoBasicTimeSeconds = Just seconds }) board+applyProperty (US str) board =+  updateBoardInfo (\info -> info { gameInfoEntererName = Just str }) board+applyProperty (WR str) board =+  updateBoardInfo (\info -> info { gameInfoWhiteRank = Just str }) board+applyProperty (WT str) board =+  updateBoardInfo (\info -> info { gameInfoWhiteTeamName = Just str }) board++applyProperty (BL {}) board = board+applyProperty (OB {}) board = board+applyProperty (OW {}) board = board+applyProperty (WL {}) board = board++applyProperty (VW coords) board =+  let coords' = expandCoordList coords+  in if null coords'+     then setBoardVisible True board+     else updateCoordStates (\state -> state { coordVisible = True }) coords' $+          setBoardVisible False board++applyProperty (HA {}) board = board+applyProperty (KM {}) board = board+applyProperty (TB {}) board = board+applyProperty (TW {}) board = board++applyProperty (UnknownProperty {}) board = board++applyProperties :: Node -> BoardState -> BoardState+applyProperties node board = foldr applyProperty board (nodeProperties node)++-- | Applies the transformation function to all of a board's coordinates+-- referred to by the 'CoordList'.+updateCoordStates :: (CoordState -> CoordState) -> [Coord] -> BoardState -> BoardState+updateCoordStates fn coords board =+  board { boardCoordStates = foldr applyFn (boardCoordStates board) coords }+  where applyFn (x, y) = listUpdate (updateRow x) y+        updateRow = listUpdate fn++updateCoordStates' :: (CoordState -> CoordState) -> CoordList -> BoardState -> BoardState+updateCoordStates' fn coords = updateCoordStates fn (expandCoordList coords)++-- | Updates properties of a 'BoardState' given that the player of the given+-- color has just made a move.  Increments the move number and updates the+-- player turn.+updateBoardForMove :: Color -> BoardState -> BoardState+updateBoardForMove movedPlayer board =+  board { boardMoveNumber = boardMoveNumber board + 1+        , boardPlayerTurn = cnot movedPlayer+        }++-- | A structure that configures how 'applyMove' should handle moves that are+-- normally illegal in Go.+data ApplyMoveParams = ApplyMoveParams+  { allowSuicide :: Bool+    -- ^ If false, suicide will cause 'applyMove' to return+    -- 'ApplyMoveSuicideError'.  If true, suicide will kill the+    -- friendly group and give points to the opponent.+  , allowOverwrite :: Bool+    -- ^ If false, playing on an occupied point will cause+    -- 'applyMove' to return 'ApplyMoveOverwriteError' with the+    -- color of the stone occupying the point.  If true,+    -- playing on an occupied point will overwrite the point+    -- (the previous stone vanishes), then capture rules are+    -- applied as normal.+  } deriving (Show)++-- | As an argument to 'applyMove', causes illegal moves to be treated as+-- errors.+standardGoMoveParams :: ApplyMoveParams+standardGoMoveParams = ApplyMoveParams+  { allowSuicide = False+  , allowOverwrite = False+  }++-- | As an argument to 'applyMove', causes illegal moves to be played+-- unconditionally.+playTheDarnMoveGoParams :: ApplyMoveParams+playTheDarnMoveGoParams = ApplyMoveParams+  { allowSuicide = True+  , allowOverwrite = True+  }++-- | The possible results from 'applyMove'.+data ApplyMoveResult =+  ApplyMoveOk BoardState+  -- ^ The move was accepted; playing it resulted in the given board without+  -- capture.+  | ApplyMoveCapture BoardState Color Int+    -- ^ The move was accepted; playing it resulted in the given board with a+    -- capture.  The specified side gained the number of points given.+  | ApplyMoveSuicideError+    -- ^ Playing the move would result in suicide, which is forbidden.+  | ApplyMoveOverwriteError Color+    -- ^ There is already a stone of the specified color on the target point,+    -- and overwriting is forbidden.++-- | If the 'ApplyMoveResult' represents a successful move, then the resulting+-- 'BoardState' is returned, otherwise, the default 'BoardState' given is+-- returned.+getApplyMoveResult :: BoardState -> ApplyMoveResult -> BoardState+getApplyMoveResult defaultBoard result = fromMaybe defaultBoard $ getApplyMoveResult' result++getApplyMoveResult' :: ApplyMoveResult -> Maybe BoardState+getApplyMoveResult' result = case result of+  ApplyMoveOk board -> Just board+  ApplyMoveCapture board color points -> Just $ case color of+    Black -> board { boardBlackCaptures = boardBlackCaptures board + points }+    White -> board { boardWhiteCaptures = boardWhiteCaptures board + points }+  ApplyMoveSuicideError -> Nothing+  ApplyMoveOverwriteError _ -> Nothing++-- | Internal data structure, only for move application code.  Represents a+-- group of stones.+data ApplyMoveGroup = ApplyMoveGroup+  { applyMoveGroupOrigin :: Coord+  , applyMoveGroupCoords :: [Coord]+  , applyMoveGroupLiberties :: Int+  } deriving (Show)++-- | Places a stone of a color at a point on a board, and runs move validation+-- and capturing logic according to the given parameters.  Returns whether the+-- move was successful, and the result if so.+applyMove :: ApplyMoveParams -> Color -> Coord -> BoardState -> ApplyMoveResult+applyMove params color xy board =+  let currentStone = coordStone $ boardCoordState xy board+  in case currentStone of+    Just color -> if allowOverwrite params+                  then moveResult+                  else ApplyMoveOverwriteError color+    Nothing -> moveResult+  where boardWithMove = updateCoordStates (\state -> state { coordStone = Just color })+                                          [xy]+                                          board+        (boardWithCaptures, points) = foldr (maybeCapture $ cnot color)+                                            (boardWithMove, 0)+                                            (adjacentPoints boardWithMove xy)+        playedGroup = computeGroup boardWithCaptures xy+        moveResult+          | applyMoveGroupLiberties playedGroup == 0 =+            if points /= 0+            then error "Cannot commit suicide and capture at the same time."+            else if allowSuicide params+                 then let (boardWithSuicide, suicidePoints) =+                            applyMoveCapture (boardWithCaptures, 0) playedGroup+                      in ApplyMoveCapture boardWithSuicide (cnot color) suicidePoints+                 else ApplyMoveSuicideError+          | points /= 0 = ApplyMoveCapture boardWithCaptures color points+          | otherwise = ApplyMoveOk boardWithCaptures++-- | Capture if there is a liberty-less group of a color at a point on+-- a board.  Removes captured stones from the board and accumulates+-- points for captured stones.+maybeCapture :: Color -> Coord -> (BoardState, Int) -> (BoardState, Int)+maybeCapture color xy result@(board, _) =+  if coordStone (boardCoordState xy board) /= Just color+  then result+  else let group = computeGroup board xy+       in if applyMoveGroupLiberties group /= 0+          then result+          else applyMoveCapture result group++computeGroup :: BoardState -> Coord -> ApplyMoveGroup+computeGroup board xy =+  if isNothing (coordStone $ boardCoordState xy board)+  then error "computeGroup called on an empty point."+  else let groupCoords = bucketFill board xy+       in ApplyMoveGroup { applyMoveGroupOrigin = xy+                         , applyMoveGroupCoords = groupCoords+                         , applyMoveGroupLiberties = getLibertiesOfGroup board groupCoords+                         }++applyMoveCapture :: (BoardState, Int) -> ApplyMoveGroup -> (BoardState, Int)+applyMoveCapture (board, points) group =+  (updateCoordStates (\state -> state { coordStone = Nothing })+                     (applyMoveGroupCoords group)+                     board,+   points + length (applyMoveGroupCoords group))++-- | Returns a list of the four coordinates that are adjacent to the+-- given coordinate on the board, excluding coordinates that are out+-- of bounds.+adjacentPoints :: BoardState -> Coord -> [Coord]+adjacentPoints board (x, y) = execWriter $ do+  when (x > 0) $ tell [(x - 1, y)]+  when (y > 0) $ tell [(x, y - 1)]+  when (x < boardWidth board - 1) $ tell [(x + 1, y)]+  when (y < boardHeight board - 1) $ tell [(x, y + 1)]++-- | Takes a list of coordinates that comprise a group (e.g. a list+-- returned from 'bucketFill') and returns the number of liberties the+-- group has.  Does no error checking to ensure that the list refers+-- to a single or maximal group.+getLibertiesOfGroup :: BoardState -> [Coord] -> Int+getLibertiesOfGroup board groupCoords =+  length $ nub $ concatMap findLiberties groupCoords+  where findLiberties xy = filter (\xy' -> isNothing $ coordStone $ boardCoordState xy' board)+                                  (adjacentPoints board xy)++-- | Expands a single coordinate on a board into a list of all the+-- coordinates connected to it by some continuous path of stones of+-- the same color (or empty spaces).+bucketFill :: BoardState -> Coord -> [Coord]+bucketFill board xy0 = bucketFill' Set.empty [xy0]+  where bucketFill' known [] = Set.toList known+        bucketFill' known (xy:xys) =+          if Set.member xy known+          then bucketFill' known xys+          else let new = filter ((stone0 ==) . coordStone . flip boardCoordState board)+                                (adjacentPoints board xy)+               in bucketFill' (Set.insert xy known) (new ++ xys)+        stone0 = coordStone $ boardCoordState xy0 board++-- | Returns whether it is legal to place a stone of the given color at a point+-- on a board.  Accepts out-of-bound coordinates and returns false.+isValidMove :: BoardState -> Color -> Coord -> Bool+-- TODO Should out-of-bound coordinates be accepted?+isValidMove board color coord@(x, y) =+  let w = boardWidth board+      h = boardHeight board+  in x >= 0 && y >= 0 && x < w && y < h &&+     isJust (getApplyMoveResult' $ applyMove standardGoMoveParams color coord board)++-- | Returns whether it is legal for the current player to place a stone at a+-- point on a board.  Accepts out-of-bound coordinates and returns false.+isCurrentValidMove :: BoardState -> Coord -> Bool+isCurrentValidMove board = isValidMove board (boardPlayerTurn board)++-- | A pointer to a node in a game tree that also holds information+-- about the current state of the game at that node.+data Cursor = Cursor { cursorParent :: Maybe Cursor+                       -- ^ The cursor for the node above this cursor's node in+                       -- the game tree.  The node of the parent cursor is the+                       -- parent of the cursor's node.+                       --+                       -- This is @Nothing@ iff the cursor's node has no parent.+                     , cursorChildIndex :: Int+                       -- ^ The index of this cursor's node in its parent's+                       -- child list.  When the cursor's node has no parent,+                       -- the value in this field is not specified.+                     , cursorNode :: Node+                       -- ^ The game tree node about which the cursor stores+                       -- information.+                     , cursorBoard :: BoardState+                       -- ^ The complete board state for the current node.+                     } deriving (Show) -- TODO Better Show Cursor instance.++-- | Returns a cursor for a root node.+rootCursor :: Node -> Cursor+rootCursor node =+  Cursor { cursorParent = Nothing+         , cursorChildIndex = -1+         , cursorNode = node+         , cursorBoard = rootBoardState node+         }++cursorRoot :: Cursor -> Cursor+cursorRoot cursor = case cursorParent cursor of+  Nothing -> cursor+  Just parent -> cursorRoot parent++cursorChild :: Cursor -> Int -> Cursor+cursorChild cursor index =+  Cursor { cursorParent = Just cursor+         , cursorChildIndex = index+         , cursorNode = child+         , cursorBoard = applyProperties child $ boardChild $ cursorBoard cursor+         }+  -- TODO Better handling or messaging for out-of-bounds:+  where child = (!! index) $ nodeChildren $ cursorNode cursor++cursorChildren :: Cursor -> [Cursor]+cursorChildren cursor =+  let board = boardChild $ cursorBoard cursor+  in map (\(index, child) -> Cursor { cursorParent = Just cursor+                                    , cursorChildIndex = index+                                    , cursorNode = child+                                    , cursorBoard = applyProperties child board+                                    })+     $ zip [0..]+     $ nodeChildren+     $ cursorNode cursor++cursorChildCount :: Cursor -> Int+cursorChildCount = length . nodeChildren . cursorNode++cursorChildPlayingAt :: Maybe Coord -> Cursor -> Maybe Cursor+cursorChildPlayingAt move cursor =+  let children = cursorChildren cursor+      color = boardPlayerTurn $ cursorBoard cursor+      hasMove = elem $ moveToProperty color move+  in find (hasMove . nodeProperties . cursorNode) children++-- | This is simply @'nodeProperties' . 'cursorNode'@.+cursorProperties :: Cursor -> [Property]+cursorProperties = nodeProperties . cursorNode++cursorModifyNode :: (Node -> Node) -> Cursor -> Cursor+cursorModifyNode fn cursor =+  let node' = fn $ cursorNode cursor+  in case cursorParent cursor of+    Nothing -> rootCursor node'+    Just parentCursor ->+      let index = cursorChildIndex cursor+          parentCursor' = cursorModifyNode+                          (\parentNode ->+                            parentNode { nodeChildren = listUpdate (const node')+                                                        index+                                                        (nodeChildren parentNode)+                                       })+                          parentCursor+      in cursorChild parentCursor' index++-- | Returns the variations to display for a cursor.  The returned list contains+-- the location and color of 'B' and 'W' properties in variation nodes.+-- Variation nodes are either children of the current node, or siblings of the+-- current node, depending on the variation mode source.+cursorVariations :: VariationModeSource -> Cursor -> [(Coord, Color)]+cursorVariations source cursor =+  case source of+    ShowChildVariations -> collectPlays $ nodeChildren $ cursorNode cursor+    ShowCurrentVariations ->+      case cursorParent cursor of+        Nothing -> []+        Just parent -> collectPlays $ listDeleteAt (cursorChildIndex cursor) $+                       nodeChildren $ cursorNode parent+  where collectPlays :: [Node] -> [(Coord, Color)]+        collectPlays = concatMap collectPlays'+        collectPlays' = concatMap collectPlays'' . nodeProperties+        collectPlays'' prop = case prop of+          B (Just xy) -> [(xy, Black)]+          W (Just xy) -> [(xy, White)]+          _ -> []++moveToProperty :: Color -> Maybe Coord -> Property+moveToProperty color =+  case color of+    Black -> B+    White -> W
+ src/Game/Goatee/Lib/Monad.hs view
@@ -0,0 +1,793 @@+-- This file is part of Goatee.+--+-- Copyright 2014 Bryan Gardiner+--+-- Goatee is free software: you can redistribute it and/or modify+-- it under the terms of the GNU Affero General Public License as published by+-- the Free Software Foundation, either version 3 of the License, or+-- (at your option) any later version.+--+-- Goatee is distributed in the hope that it will be useful,+-- but WITHOUT ANY WARRANTY; without even the implied warranty of+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the+-- GNU Affero General Public License for more details.+--+-- You should have received a copy of the GNU Affero General Public License+-- along with Goatee.  If not, see <http://www.gnu.org/licenses/>.++-- | A monad for working with game trees.+module Game.Goatee.Lib.Monad (+  -- * The Go monad+  MonadGo (..),+  GoT, GoM,+  runGoT, runGo,+  evalGoT, evalGo,+  execGoT, execGo,+  Step (..),+  NodeDeleteResult (..),+  -- * Event handling+  Event, AnyEvent (..), eventName, fire, eventHandlerFromAction,+  -- * Events+  childAddedEvent, ChildAddedHandler,+  childDeletedEvent, ChildDeletedHandler,+  gameInfoChangedEvent, GameInfoChangedHandler,+  navigationEvent, NavigationHandler,+  propertiesModifiedEvent, PropertiesModifiedHandler,+  variationModeChangedEvent, VariationModeChangedHandler,+  ) where++import Control.Applicative ((<$>), Applicative ((<*>), pure))+import Control.Monad (ap, liftM, when)+import Control.Monad.Identity (Identity, runIdentity)+import qualified Control.Monad.State as State+import Control.Monad.State (MonadState, StateT, get, put)+import Control.Monad.Trans (MonadIO, MonadTrans, lift, liftIO)+import Control.Monad.Writer.Class (MonadWriter, listen, pass, tell, writer)+import qualified Data.Function as F+import Data.List (delete, find, mapAccumL, nub, sortBy)+import Data.Maybe (fromMaybe, isJust, isNothing)+import Game.Goatee.Common+import Game.Goatee.Lib.Board+import Game.Goatee.Lib.Property+import qualified Game.Goatee.Lib.Tree as Tree+import Game.Goatee.Lib.Tree hiding (addChild, addChildAt)+import Game.Goatee.Lib.Types++-- | The internal state of a Go monad transformer.  @go@ is the type of+-- Go monad or transformer (instance of 'GoMonad').+data GoState go = GoState+  { stateCursor :: Cursor+    -- ^ The current position in the game tree.+  , statePathStack :: PathStack+    -- ^ The current path stack.++    -- Event handlers.+  , stateChildAddedHandlers :: [ChildAddedHandler go]+    -- ^ Handlers for 'childAddedEvent'.+  , stateChildDeletedHandlers :: [ChildDeletedHandler go]+    -- ^ Handlers for 'childDeletedEvent'.+  , stateGameInfoChangedHandlers :: [GameInfoChangedHandler go]+    -- ^ Handlers for 'gameInfoChangedEvent'.+  , stateNavigationHandlers :: [NavigationHandler go]+    -- ^ Handlers for 'navigationEvent'.+  , statePropertiesModifiedHandlers :: [PropertiesModifiedHandler go]+    -- ^ Handlers for 'propertiesModifiedEvent'.+  , stateVariationModeChangedHandlers :: [VariationModeChangedHandler go]+    -- ^ Handlers for 'variationModeChangedEvent'.+  }++-- | A path stack is a record of previous places visited in a game tree.  It is+-- encoded a list of paths (steps) to each previous memorized position.+--+-- The positions saved in calls to 'pushPosition' correspond to entries in the+-- outer list here, with the first sublist representing the last call.  The+-- sublist contains the steps in order that will trace the path back to the+-- saved position.+type PathStack = [[Step]]++-- | A simplified constructor function for 'GoState'.+initialState :: Cursor -> GoState m+initialState cursor = GoState { stateCursor = cursor+                              , statePathStack = []+                              , stateChildAddedHandlers = []+                              , stateChildDeletedHandlers = []+                              , stateGameInfoChangedHandlers = []+                              , stateNavigationHandlers = []+                              , statePropertiesModifiedHandlers = []+                              , stateVariationModeChangedHandlers = []+                              }++-- | A single step along a game tree.  Either up or down.+data Step =+  GoUp Int+  -- ^ Represents a step up from a child with the given index.+  | GoDown Int+    -- ^ Represents a step down to the child with the given index.+  deriving (Eq, Show)++-- | Reverses a step, such that taking a step then it's reverse will leave you+-- where you started.+reverseStep :: Step -> Step+reverseStep step = case step of+  GoUp index -> GoDown index+  GoDown index -> GoUp index++-- | Takes a 'Step' from a 'Cursor', returning a new 'Cursor'.+takeStep :: Step -> Cursor -> Cursor+takeStep (GoUp _) cursor = fromMaybe (error $ "takeStep: Can't go up from " ++ show cursor ++ ".") $+                           cursorParent cursor+takeStep (GoDown index) cursor = cursorChild cursor index++-- | Internal function.  Takes a 'Step' in the Go monad.  Updates the path stack+-- accordingly.+takeStepM :: Monad m => Step -> (PathStack -> PathStack) -> GoT m ()+takeStepM step = case step of+  GoUp _ -> goUp'+  GoDown index -> goDown' index++-- | A monad (transformer) for navigating and mutating 'Cursor's, and+-- remembering previous locations.  See 'GoT' and 'GoM'.+--+-- The monad supports handlers for events raised during actions it takes, such+-- as navigating through the tree and modifying nodes.+class (Functor go, Applicative go, Monad go) => MonadGo go where+  -- | Returns the current cursor.+  getCursor :: go Cursor++  -- | Returns the 'CoordState' at the given point.+  getCoordState :: Coord -> go CoordState+  getCoordState coord = liftM (boardCoordState coord . cursorBoard) getCursor++  -- | Navigates up the tree.  It must be valid to do so, otherwise 'fail' is+  -- called.  Fires a 'navigationEvent' after moving.+  goUp :: go ()++  -- | Navigates down the tree to the child with the given index.  The child+  -- must exist.  Fires a 'navigationEvent' after moving.+  goDown :: Int -> go ()++  -- | Navigates up to the root of the tree.  Fires 'navigationEvent's for each+  -- step.+  goToRoot :: go ()++  -- | Navigates up the tree to the node containing game info properties, if+  -- any.  Returns true if a game info node was found.+  goToGameInfoNode :: Bool+                      -- ^ When no node with game info is found, then if false,+                      -- return to the original node, otherwise finish at the+                      -- root node.+                   -> go Bool++  -- | Pushes the current location in the game tree onto an internal position+  -- stack, such that 'popPosition' is capable of navigating back to the same+  -- position, even if the game tree has been modified (though the old position+  -- must still exist in the tree to return to it).+  pushPosition :: go ()++  -- | Returns to the last position pushed onto the internal position stack via+  -- 'pushPosition'.  This action must be balanced by a 'pushPosition'.+  popPosition :: go ()++  -- | Drops the last position pushed onto the internal stack by 'pushPosition'+  -- off of the stack.  This action must be balanced by a 'pushPosition'.+  dropPosition :: go ()++  -- | Returns the set of properties on the current node.+  getProperties :: go [Property]+  getProperties = liftM cursorProperties getCursor++  -- | Modifies the set of properties on the current node.+  --+  -- The given function must end on the same node on which it started.+  modifyProperties :: ([Property] -> go [Property]) -> go ()++  -- | Searches for a property on the current node, returning it if found.+  getProperty :: Descriptor d => d -> go (Maybe Property)++  -- | Searches for a valued property on the current node, returning its value+  -- if found.+  getPropertyValue :: ValuedDescriptor d v => d -> go (Maybe v)+  getPropertyValue descriptor = liftM (liftM $ propertyValue descriptor) $ getProperty descriptor++  -- | Sets a property on the current node, replacing an existing property with+  -- the same name, if one exists.+  putProperty :: Property -> go ()+  putProperty property = modifyProperty (propertyInfo property) $ const $ Just property++  -- | Deletes a property from the current node, if it's set.+  --+  -- Note that although a 'Property' is a 'Descriptor', giving a valued+  -- @Property@ here will not cause deletion to match on the value of the+  -- property.  That is, the following code will result in 'Nothing', because+  -- the deletion only cares about the name of the property.+  --+  -- > do putProperty $ PL Black+  -- >    deleteProperty $ PL White+  -- >    getPropertyValue propertyPL+  deleteProperty :: Descriptor d => d -> go ()+  deleteProperty descriptor = modifyProperty descriptor $ const Nothing++  -- | Calls the given function to modify the state of the given property+  -- (descriptor) on the current node.  'Nothing' represents the property not+  -- existing on the node, and a 'Just' marks the property's presence.  This+  -- function does not do any validation to check that the resulting tree state+  -- is valid.+  modifyProperty :: Descriptor d => d -> (Maybe Property -> Maybe Property) -> go ()++  -- | Calls the given function to modify the state of the given valued property+  -- (descriptor) on the current node.  'Nothing' represents the property not+  -- existing on the node, and a 'Just' with the property's value marks the+  -- property's presence.  This function does not do any validation to check+  -- that the resulting tree state is valid.+  modifyPropertyValue :: ValuedDescriptor d v => d -> (Maybe v -> Maybe v) -> go ()+  modifyPropertyValue descriptor fn = modifyProperty descriptor $ \old ->+    propertyBuilder descriptor <$> fn (propertyValue descriptor <$> old)++  -- | Mutates the string-valued property attached to the current node according+  -- to the given function.  The input string will be empty if the current node+  -- either has the property with an empty value, or doesn't have the property.+  -- Returning an empty string removes the property from the node, if it was+  -- set.+  modifyPropertyString :: (Stringlike s, ValuedDescriptor d s) => d -> (String -> String) -> go ()+  modifyPropertyString descriptor fn =+    modifyPropertyValue descriptor $ \value -> case fn (maybe "" sgfToString value) of+      "" -> Nothing+      str -> let sgf = stringToSgf str+                 -- Because stringToSgf might do processing, we have to check+                 -- the conversion back to a string for emptiness.+             in if null $ sgfToString sgf then Nothing else Just sgf++  -- | Mutates the 'CoordList'-valued property attached to the current node+  -- according to the given function.  Conversion between @CoordList@ and+  -- @[Coord]@ is performed automatically.  The input list will be empty if the+  -- current node either has the property with an empty value, or doesn't have+  -- the property.  Returning an empty list removes the property from the node,+  -- if it was set.+  --+  -- Importantly, this might not be specific enough for properties such as 'DD'+  -- and 'VW' where a present, empty list has different semantics from the+  -- property not being present.  In that case, 'modifyPropertyValue' is better.+  modifyPropertyCoords :: ValuedDescriptor d CoordList => d -> ([Coord] -> [Coord]) -> go ()+  modifyPropertyCoords descriptor fn =+    modifyPropertyValue descriptor $ \value -> case fn $ maybe [] expandCoordList value of+      [] -> Nothing+      coords -> Just $ buildCoordList coords++  -- | Mutates the game info for the current path, returning the new info.  If+  -- the current node or one of its ancestors has game info properties, then+  -- that node is modified.  Otherwise, properties are inserted on the root+  -- node.+  modifyGameInfo :: (GameInfo -> GameInfo) -> go GameInfo++  -- | Sets the game's 'VariationMode' via the 'ST' property on the root node,+  -- then fires a 'variationModeChangedEvent' if the variation mode has changed.+  modifyVariationMode :: (VariationMode -> VariationMode) -> go ()++  -- | Returns the 'Mark' at a point on the current node.+  getMark :: Coord -> go (Maybe Mark)+  getMark = liftM coordMark . getCoordState++  -- | Calls the given function to modify the presence of a 'Mark' on the+  -- current node.+  modifyMark :: (Maybe Mark -> Maybe Mark) -> Coord -> go ()+  modifyMark fn coord = do+    maybeOldMark <- getMark coord+    case (maybeOldMark, fn maybeOldMark) of+      (Just oldMark, Nothing) -> remove oldMark+      (Nothing, Just newMark) -> add newMark+      (Just oldMark, Just newMark) | oldMark /= newMark -> remove oldMark >> add newMark+      (Just _, Just _) -> return ()+      (Nothing, Nothing) -> return ()+    where remove mark = modifyPropertyCoords (markProperty mark) (delete coord)+          add mark = modifyPropertyCoords (markProperty mark) (coord:)++  -- | Adds a child node to the current node at the end of the current node's+  -- child list.  Fires a 'childAddedEvent' after the child is added.+  addChild :: Node -> go ()+  addChild node = do+    childCount <- liftM (length . cursorChildren) getCursor+    addChildAt childCount node++  -- | Adds a child node to the current node at the given index, shifting all+  -- existing children at and after the index to the right.  The index must be+  -- in the range @[0, numberOfChildren]@.  Fires a 'childAddedEvent' after the+  -- child is added.+  addChildAt :: Int -> Node -> go ()++  -- | Tries to remove the child node at the given index below the current node.+  -- Returns a status code indicating whether the deletion succeeded, or why+  -- not.+  deleteChildAt :: Int -> go NodeDeleteResult++  -- | Registers a new event handler for a given event type.+  on :: Event go h -> h -> go ()++  -- | Registers a new event handler for a given event type.  Unlike 'on', whose+  -- handler may receive arguments, the handler given here doesn't receive any+  -- arguments.+  on0 :: Event go h -> go () -> go ()+  on0 event handler = on event $ eventHandlerFromAction event handler++-- | The result of deleting a node.+data NodeDeleteResult =+  NodeDeleteOk+  -- ^ The node was deleted successfully.+  | NodeDeleteBadIndex+    -- ^ The node couldn't be deleted, because an invalid index was given.+  | NodeDeleteOnPathStack+    -- ^ The node couldn't be deleted, because it is on the path stack.+  deriving (Bounded, Enum, Eq, Show)++-- | The standard monad transformer for 'MonadGo'.+newtype GoT m a = GoT { goState :: StateT (GoState (GoT m)) m a }++-- | The standard monad for 'MonadGo'.+type GoM = GoT Identity++instance Monad m => Functor (GoT m) where+  fmap = liftM++instance Monad m => Applicative (GoT m) where+  pure = return+  (<*>) = ap++instance Monad m => Monad (GoT m) where+  return x = GoT $ return x+  m >>= f = GoT $ goState . f =<< goState m+  fail = lift . fail++instance MonadTrans GoT where+  lift = GoT . lift++instance MonadIO m => MonadIO (GoT m) where+  liftIO = lift . liftIO++instance MonadState s m => MonadState s (GoT m) where+  get = lift get+  put = lift . put++instance MonadWriter w m => MonadWriter w (GoT m) where+  writer = lift . writer+  tell = lift . tell+  listen = GoT . listen . goState+  pass = GoT . pass . goState++-- | Executes a Go monad transformer on a cursor, returning in the underlying+-- monad a tuple that contains the resulting value and the final cursor.+runGoT :: Monad m => GoT m a -> Cursor -> m (a, Cursor)+runGoT go cursor = do+  (value, state) <- State.runStateT (goState go) (initialState cursor)+  return (value, stateCursor state)++-- | Executes a Go monad transformer on a cursor, returning in the underlying+-- monad the value in the transformer.+evalGoT :: Monad m => GoT m a -> Cursor -> m a+evalGoT go cursor = liftM fst $ runGoT go cursor++-- | Executes a Go monad transformer on a cursor, returning in the underlying+-- monad the final cursor.+execGoT :: Monad m => GoT m a -> Cursor -> m Cursor+execGoT go cursor = liftM snd $ runGoT go cursor++-- | Runs a Go monad on a cursor.  See 'runGoT'.+runGo :: GoM a -> Cursor -> (a, Cursor)+runGo go = runIdentity . runGoT go++-- | Runs a Go monad on a cursor and returns the value in the monad.+evalGo :: GoM a -> Cursor -> a+evalGo m cursor = fst $ runGo m cursor++-- | Runs a Go monad on a cursor and returns the final cursor.+execGo :: GoM a -> Cursor -> Cursor+execGo m cursor = snd $ runGo m cursor++getState :: Monad m => GoT m (GoState (GoT m))+getState = GoT State.get++putState :: Monad m => GoState (GoT m) -> GoT m ()+putState = GoT . State.put++modifyState :: Monad m => (GoState (GoT m) -> GoState (GoT m)) -> GoT m ()+modifyState = GoT . State.modify++instance Monad m => MonadGo (GoT m) where+  getCursor = liftM stateCursor getState++  -- TODO For goUp and goDown, optimize by seeing checking if (head $ head+  -- pathStack) is the step we're taking, and if so, dropping it from the list+  -- rather than pushing a fresh step.+  goUp = do+    index <- liftM cursorChildIndex getCursor+    goUp' $ \pathStack -> case pathStack of+      [] -> pathStack+      path:paths -> (GoDown index:path):paths++  goDown index = goDown' index $ \pathStack -> case pathStack of+    [] -> pathStack+    path:paths -> (GoUp index:path):paths++  goToRoot = whileM (isJust . cursorParent <$> getCursor) goUp++  goToGameInfoNode goToRootIfNotFound = pushPosition >> findGameInfoNode+    where findGameInfoNode = do+            cursor <- getCursor+            if hasGameInfo cursor+              then dropPosition >> return True+              else if isNothing $ cursorParent cursor+                   then do if goToRootIfNotFound then dropPosition else popPosition+                           return False+                   else goUp >> findGameInfoNode+          hasGameInfo cursor = internalIsGameInfoNode $ cursorNode cursor++  pushPosition = modifyState $ \state ->+    state { statePathStack = []:statePathStack state }++  popPosition = do+    getPathStack >>= \stack -> when (null stack) $+      fail "popPosition: No position to pop from the stack."++    -- Drop each step in the top list of the path stack one at a time, until the+    -- top list is empty.+    whileM' (do path:_ <- getPathStack+                return $ if null path then Nothing else Just $ head path) $+      flip takeStepM $ \((_:steps):paths) -> steps:paths++    -- Finally, drop the empty top of the path stack.+    modifyState $ \state -> case statePathStack state of+      []:rest -> state { statePathStack = rest }+      _ -> error "popPosition: Internal failure, top of path stack is not empty."++  dropPosition = do+    state <- getState+    -- If there are >=2 positions on the path stack, then we can't simply drop+    -- the moves that will return us to the top-of-stack position, because they+    -- may still be needed to return to the second-on-stack position by a+    -- following popPosition.+    case statePathStack state of+      x:y:xs -> putState $ state { statePathStack = (x ++ y):xs }+      _:[] -> putState $ state { statePathStack = [] }+      [] -> fail "dropPosition: No position to drop from the stack."++  modifyProperties fn = do+    oldCursor <- getCursor+    let oldProperties = cursorProperties oldCursor+    newProperties <- fn oldProperties+    modifyState $ \state ->+      state { stateCursor = cursorModifyNode+                            (\node -> node { nodeProperties = newProperties })+                            oldCursor+            }+    when (sortBy (compare `F.on` propertyName) newProperties /=+          sortBy (compare `F.on` propertyName) oldProperties) $+      fire propertiesModifiedEvent (\f -> f oldProperties newProperties)++    -- The current game info changes when modifying game info properties on the+    -- current node.  I think comparing game info properties should be faster+    -- than comparing 'GameInfo's.+    let filterToGameInfo = nub . filter ((GameInfoProperty ==) . propertyType)+        oldGameInfo = filterToGameInfo oldProperties+        newGameInfo = filterToGameInfo newProperties+    when (newGameInfo /= oldGameInfo) $ do+      newCursor <- getCursor+      fire gameInfoChangedEvent (\f -> f (boardGameInfo $ cursorBoard oldCursor)+                                         (boardGameInfo $ cursorBoard newCursor))++  getProperty descriptor = find (propertyPredicate descriptor) <$> getProperties++  modifyProperty descriptor fn = do+    cursor <- getCursor+    let node = cursorNode cursor+        old = findProperty descriptor node+        new = fn old+    when (maybe False (not . propertyPredicate descriptor) new) $+      fail $ "modifyProperty: May not change property type: " +++      show old ++ " -> " ++ show new ++ "."+    case (old, new) of+      (Just _, Nothing) -> modifyProperties $ return . remove descriptor+      (Nothing, Just value') -> modifyProperties $ return . add value'+      (Just value, Just value') | value /= value' ->+        modifyProperties $ return . add value' . remove descriptor+      _ -> return ()+    where remove descriptor = filter (not . propertyPredicate descriptor)+          add value = (value:)++  modifyGameInfo fn = do+    cursor <- getCursor+    let info = boardGameInfo $ cursorBoard cursor+        info' = fn info+    when (gameInfoRootInfo info /= gameInfoRootInfo info') $+      fail "Illegal modification of root info in modifyGameInfo."+    pushPosition+    goToGameInfoNode True+    modifyProperties $ \props ->+      return $ gameInfoToProperties info' ++ filter ((GameInfoProperty /=) . propertyType) props+    popPosition+    return info'++  modifyVariationMode fn = do+    pushPosition+    goToRoot+    modifyPropertyValue propertyST $ \maybeOld ->+      -- If the new variation mode is equal to the old effective variation mode+      -- (effective applying the default if the property isn't present), then+      -- leave the property unchanged.  Otherwise, apply the new variation mode,+      -- deleting the property if the default variation mode is selected.  We+      -- don't delete the property if @maybeOld == Just new == Just+      -- defaultVariationMode@, because we don't want to trigger dirtyness+      -- unnecessarily.+      let old = fromMaybe defaultVariationMode maybeOld+          new = fn old+      in if new == old+         then maybeOld+         else if new == defaultVariationMode+              then Nothing+              else Just new+    popPosition++  addChildAt index node = do+    cursor <- getCursor+    let childCount = cursorChildCount cursor+    when (index < 0 || index > childCount) $ fail $+      "Monad.addChildAt: Index " ++ show index ++ " is not in [0, " ++ show childCount ++ "]."+    let cursor' = cursorModifyNode (Tree.addChildAt index node) cursor+    modifyState $ \state ->+      state { stateCursor = cursor'+            , statePathStack = foldPathStack+                               (\step -> case step of+                                   GoUp n -> GoUp $ if n < index then n else n + 1+                                   down@(GoDown _) -> down)+                               (\step -> case step of+                                   up@(GoUp _) -> up+                                   GoDown n -> GoDown $ if n < index then n else n + 1)+                               id+                               cursor'+                               (statePathStack state)+            }+    fire childAddedEvent ($ index)++  deleteChildAt index = do+    childCount <- cursorChildCount <$> getCursor+    if index < 0 || index >= childCount+      then return NodeDeleteBadIndex+      else do goDown index+              childCursor <- getCursor+              deletingNodeOnPath <- doesPathStackEnterCurrentNode <$>+                                    pure childCursor <*> getPathStack+              goUp+              if deletingNodeOnPath+                then return NodeDeleteOnPathStack+                else do cursor <- getCursor+                        let cursor' = cursorModifyNode (Tree.deleteChildAt index) cursor+                        modifyState $ \state ->+                          state { stateCursor = cursor'+                                , statePathStack =+                                  foldPathStack+                                  (\step -> case step of+                                      GoUp n -> GoUp $ if n < index then n else n - 1+                                      down@(GoDown _) -> down)+                                  (\step -> case step of+                                      up@(GoUp _) -> up+                                      GoDown n -> GoDown $ if n < index then n else n - 1)+                                  id+                                  cursor'+                                  (statePathStack state)+                                }+                        fire childDeletedEvent ($ childCursor)+                        return NodeDeleteOk++  on event handler = modifyState $ addHandler event handler++-- | Takes a step up the game tree, updates the path stack according to the+-- given function, then fires navigation and game info changed events as+-- appropriate.+goUp' :: Monad m => (PathStack -> PathStack) -> GoT m ()+goUp' pathStackFn = do+  state@(GoState { stateCursor = cursor+                 , statePathStack = pathStack+                 }) <- getState+  case cursorParent cursor of+    Nothing -> error $ "goUp': Can't go up from a root cursor: " ++ show cursor+    Just parent -> do+      let index = cursorChildIndex cursor+      putState state { stateCursor = parent+                     , statePathStack = pathStackFn pathStack+                     }+      fire navigationEvent ($ GoUp index)++      -- The current game info changes when navigating up from a node that has+      -- game info properties.+      when (any ((GameInfoProperty ==) . propertyType) $ cursorProperties cursor) $+        fire gameInfoChangedEvent (\f -> f (boardGameInfo $ cursorBoard cursor)+                                           (boardGameInfo $ cursorBoard parent))++-- | Takes a step down the game tree, updates the path stack according to the+-- given function, then fires navigation and game info changed events as+-- appropriate.+goDown' :: Monad m => Int -> (PathStack -> PathStack) -> GoT m ()+goDown' index pathStackFn = do+  state@(GoState { stateCursor = cursor+                 , statePathStack = pathStack+                 }) <- getState+  case drop index $ cursorChildren cursor of+    [] -> error $ "goDown': Cursor does not have a child #" ++ show index ++ ": " ++ show cursor+    child:_ -> do+      putState state { stateCursor = child+                     , statePathStack = pathStackFn pathStack+                     }+      fire navigationEvent ($ GoDown index)++      -- The current game info changes when navigating down to a node that has+      -- game info properties.+      when (any ((GameInfoProperty ==) . propertyType) $ cursorProperties child) $+        fire gameInfoChangedEvent (\f -> f (boardGameInfo $ cursorBoard cursor)+                                           (boardGameInfo $ cursorBoard child))++-- | Returns the current path stack.+getPathStack :: Monad m => GoT m PathStack+getPathStack = liftM statePathStack getState++doesPathStackEnterCurrentNode :: Cursor -> PathStack -> Bool+doesPathStackEnterCurrentNode cursor pathStack =+  or $ or <$> foldPathStack (const True) (const False) (const False) cursor pathStack++-- | Maps over a path stack, updating with the given functions all steps that+-- enter and leave the cursor's current node.+foldPathStack :: (Step -> a)+              -> (Step -> a)+              -> (Step -> a)+              -> Cursor+              -> PathStack+              -> [[a]]+foldPathStack _ _ _ _ [] = []+foldPathStack onEnter onExit onOther cursor0 paths =+  snd $ mapAccumL updatePath (cursor0, []) paths+  where -- updatePath :: (Cursor, [Step]) -> [Step] -> ((Cursor, [Step]), [a])+        updatePath = mapAccumL updateStep+        -- updateStep :: (Cursor, [Step]) -> Step -> ((Cursor, [Step]), a)+        updateStep (cursor, []) step = ((takeStep step cursor, [reverseStep step]), onExit step)+        updateStep (cursor, pathToInitial@(stepToInitial:restToInitial)) step =+          let pathToInitial' = if stepToInitial == step+                               then restToInitial+                               else reverseStep step:pathToInitial+          in ((takeStep step cursor, pathToInitial'),+              if null pathToInitial' then onEnter step else onOther step)++-- | Fires all of the handlers for the given event, using the given function to+-- create a Go action from each of the handlers (normally themselves functions+-- that create Go actions, if they're not just Go actions directly, depending on+-- the event).+fire :: Monad m => Event (GoT m) h -> (h -> GoT m ()) -> GoT m ()+fire event handlerGenerator = do+  state <- getState+  mapM_ handlerGenerator $ eventStateGetter event state++-- | A type of event in a Go monad that can be handled by executing an action.+-- @go@ is the type of the Go monad.  @h@ is the handler type, a function that+-- takes some arguments relating to the event and returns an action in the Go+-- monad.  The arguments to the handler are usually things that would be+-- difficult to recover from the state of the monad alone, for example the+-- 'Step' associated with a 'navigationEvent'.+--+-- The 'Eq', 'Ord', and 'Show' instances use events' names, via 'eventName'.+data Event go h = Event+  { eventName :: String+  , eventStateGetter :: GoState go -> [h]+  , eventStateSetter :: [h] -> GoState go -> GoState go+  , eventHandlerFromAction :: go () -> h+  }++instance Eq (Event go h) where+  (==) = (==) `F.on` eventName++instance Ord (Event go h) where+  compare = compare `F.on` eventName++instance Show (Event go h) where+  show = eventName++-- | An existential type for any event in a particular Go monad.  Like 'Event',+-- the 'Eq', 'Ord', and 'Show' instances use events' names, via 'eventName'.+data AnyEvent go = forall h. AnyEvent (Event go h)++instance Eq (AnyEvent go) where+  (AnyEvent e) == (AnyEvent e') = eventName e == eventName e'++instance Ord (AnyEvent go) where+  compare (AnyEvent e) (AnyEvent e') = compare (eventName e) (eventName e')++instance Show (AnyEvent go) where+  show (AnyEvent e) = eventName e++addHandler :: Event go h -> h -> GoState go -> GoState go+addHandler event handler state =+  eventStateSetter event (eventStateGetter event state ++ [handler]) state++-- | An event corresponding to a child node being added to the current node.+childAddedEvent :: Event go (ChildAddedHandler go)+childAddedEvent = Event+  { eventName = "childAddedEvent"+  , eventStateGetter = stateChildAddedHandlers+  , eventStateSetter = \handlers state -> state { stateChildAddedHandlers = handlers }+  , eventHandlerFromAction = const+  }++-- | A handler for 'childAddedEvent's.  Called with the index of the child added+-- to the current node.+type ChildAddedHandler go = Int -> go ()++-- | An event corresponding to the deletion of one of the current node's+-- children.+childDeletedEvent :: Event go (ChildDeletedHandler go)+childDeletedEvent = Event+  { eventName = "childDeletedEvent"+  , eventStateGetter = stateChildDeletedHandlers+  , eventStateSetter = \handlers state -> state { stateChildDeletedHandlers = handlers }+  , eventHandlerFromAction = const+  }++-- | A handler for 'childDeletedEvent's.  It is called with a cursor at the+-- child that was deleted (this cursor is now out of date).+type ChildDeletedHandler go = Cursor -> go ()++-- | An event that is fired when the current game info changes, either by+-- navigating past a node with game info properties, or by modifying the current+-- game info properties.+gameInfoChangedEvent :: Event go (GameInfoChangedHandler go)+gameInfoChangedEvent = Event+  { eventName = "gameInfoChangedEvent"+  , eventStateGetter = stateGameInfoChangedHandlers+  , eventStateSetter = \handlers state -> state { stateGameInfoChangedHandlers = handlers }+  , eventHandlerFromAction = const . const+  }++-- | A handler for 'gameInfoChangedEvent's.  It is called with the old game info+-- then the new game info.+type GameInfoChangedHandler go = GameInfo -> GameInfo -> go ()++-- | An event that is fired when a single step up or down in a game tree is+-- made.+navigationEvent :: Event go (NavigationHandler go)+navigationEvent = Event+  { eventName = "navigationEvent"+  , eventStateGetter = stateNavigationHandlers+  , eventStateSetter = \handlers state -> state { stateNavigationHandlers = handlers }+  , eventHandlerFromAction = const+  }++-- | A handler for 'navigationEvent's.+--+-- A navigation handler may navigate further, but beware infinite recursion.  A+-- navigation handler must end on the same node on which it started.+type NavigationHandler go = Step -> go ()++-- | An event corresponding to a modification to the properties list of the+-- current node.+propertiesModifiedEvent :: Event go (PropertiesModifiedHandler go)+propertiesModifiedEvent = Event+  { eventName = "propertiesModifiedEvent"+  , eventStateGetter = statePropertiesModifiedHandlers+  , eventStateSetter = \handlers state -> state { statePropertiesModifiedHandlers = handlers }+  , eventHandlerFromAction = const . const+  }++-- | A handler for 'propertiesModifiedEvent's.  It is called with the old+-- property list then the new property list.+type PropertiesModifiedHandler go = [Property] -> [Property] -> go ()++-- | An event corresponding to a change in the active 'VariationMode'.  This can+-- happen when modifying the 'ST' property, and also when navigating between+-- collections (as they have different root nodes).+variationModeChangedEvent :: Event go (VariationModeChangedHandler go)+variationModeChangedEvent = Event+  { eventName = "variationModeChangedEvent"+  , eventStateGetter = stateVariationModeChangedHandlers+  , eventStateSetter = \handlers state -> state { stateVariationModeChangedHandlers = handlers }+  , eventHandlerFromAction = const . const+  }+-- TODO Test that this is fired when moving between root nodes in a collection.+-- For now, since we don't support multiple trees in a collection, we don't need+-- to worry about checking for active variation mode change on navigation.++-- | A handler for 'variationModeChangedEvent's.  It is called with the old+-- variation mode then the new variation mode.+type VariationModeChangedHandler go = VariationMode -> VariationMode -> go ()
+ src/Game/Goatee/Lib/Parser.hs view
@@ -0,0 +1,129 @@+-- This file is part of Goatee.+--+-- Copyright 2014 Bryan Gardiner+--+-- Goatee is free software: you can redistribute it and/or modify+-- it under the terms of the GNU Affero General Public License as published by+-- the Free Software Foundation, either version 3 of the License, or+-- (at your option) any later version.+--+-- Goatee is distributed in the hope that it will be useful,+-- but WITHOUT ANY WARRANTY; without even the implied warranty of+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the+-- GNU Affero General Public License for more details.+--+-- You should have received a copy of the GNU Affero General Public License+-- along with Goatee.  If not, see <http://www.gnu.org/licenses/>.++-- | A parser for reading SGF files.+module Game.Goatee.Lib.Parser (+  parseString,+  parseFile,+  parseSubtree,+  propertyParser,+  ) where++import Control.Arrow ((+++))+import Control.Applicative ((<*), (*>))+import Data.Maybe (fromMaybe)+import Game.Goatee.Common+import Game.Goatee.Lib.Board+import Game.Goatee.Lib.Property+import Game.Goatee.Lib.Tree+import Game.Goatee.Lib.Types+import Text.ParserCombinators.Parsec (+  (<?>), Parser, char, eof, many, many1, parse, spaces, upper,+  )++-- | Parses a string in SGF format.  Returns an error string if parsing fails.+parseString :: String -> Either String Collection+parseString str = case parse collectionParser "<collection>" str of+  Left err -> Left $ show err+  Right (Collection roots) -> (concatErrors +++ Collection) $+                              andEithers $+                              map processRoot roots+  where processRoot :: Node -> Either String Node+        processRoot = checkFormatVersion . \root ->+          let SZ width height = fromMaybe (SZ boardSizeDefault boardSizeDefault) $+                                findProperty propertySZ root+          in postProcessTree width height root++        concatErrors errs = "The following errors occurred while parsing:" +++                            concatMap ("\n-> " ++) errs++-- | Parses a file in SGF format.  Returns an error string if parsing fails.+parseFile :: String -> IO (Either String Collection)+parseFile = fmap parseString . readFile++-- | Parses a node as part of an existing game tree, from textual SGF+-- \"GameTree\" syntax.  The 'RootInfo' is needed to supply necessary+-- information from the existing game tree.+parseSubtree :: RootInfo -> String -> Either String Node+parseSubtree rootInfo str =+  case parse (spaces *> gameTreeParser <* spaces) "<gameTree>" str of+    Left err -> Left $ show err+    Right node ->+      let width = rootInfoWidth rootInfo+          height = rootInfoHeight rootInfo+      in Right $ postProcessTree width height node++-- Ensures that we are parsing an SGF version that we understand.+-- TODO Try to proceed, if it makes sense.+checkFormatVersion :: Node -> Either String Node+checkFormatVersion root =+  let version = case findProperty propertyFF root of+        Nothing -> defaultFormatVersion+        Just (FF x) -> x+        x -> error $ "Expected FF or nothing, received " ++ show x ++ "."+  in if version `elem` supportedFormatVersions+     then Right root+     else Left $+          "Unsupported SGF version " ++ show version ++ ".  Only versions " +++          show supportedFormatVersions ++ " are supported."++postProcessTree :: Int -> Int -> Node -> Node+postProcessTree width height node =+  -- SGF allows B[tt] and W[tt] to represent passes on boards <=19x19.+  -- Convert any passes from this format to B[] and W[] in a root node and+  -- its descendents.+  if width <= 19 && height <= 19 then convertNodeTtToPass node else node++convertNodeTtToPass :: Node -> Node+convertNodeTtToPass node =+  node { nodeProperties = map convertPropertyTtToPass $ nodeProperties node+       , nodeChildren = map convertNodeTtToPass $ nodeChildren node+       }++convertPropertyTtToPass :: Property -> Property+convertPropertyTtToPass prop = case prop of+  B (Just (19, 19)) -> B Nothing+  W (Just (19, 19)) -> W Nothing+  _ -> prop++collectionParser :: Parser Collection+collectionParser =+  fmap Collection (spaces *> many (gameTreeParser <* spaces) <* eof) <?>+  "collection"++gameTreeParser :: Parser Node+gameTreeParser = do+  char '('+  nodes <- spaces *> many1 (nodeParser <* spaces) <?> "sequence"+  subtrees <- many (gameTreeParser <* spaces) <?> "subtrees"+  char ')'+  let (sequence, [final]) = splitAt (length nodes - 1) nodes+  return $ foldr (\seqNode childNode -> seqNode { nodeChildren = [childNode] })+                 (final { nodeChildren = subtrees })+                 sequence++nodeParser :: Parser Node+nodeParser =+  fmap (\props -> emptyNode { nodeProperties = props })+  (char ';' *> spaces *> many (propertyParser <* spaces) <?>+   "node")++propertyParser :: Parser Property+propertyParser = do+  name <- many1 upper+  spaces+  propertyValueParser $ descriptorForName name
+ src/Game/Goatee/Lib/Property.hs view
@@ -0,0 +1,28 @@+-- This file is part of Goatee.+--+-- Copyright 2014 Bryan Gardiner+--+-- Goatee is free software: you can redistribute it and/or modify+-- it under the terms of the GNU Affero General Public License as published by+-- the Free Software Foundation, either version 3 of the License, or+-- (at your option) any later version.+--+-- Goatee is distributed in the hope that it will be useful,+-- but WITHOUT ANY WARRANTY; without even the implied warranty of+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the+-- GNU Affero General Public License for more details.+--+-- You should have received a copy of the GNU Affero General Public License+-- along with Goatee.  If not, see <http://www.gnu.org/licenses/>.++-- | Structures and functions for working with SGF node properties.+module Game.Goatee.Lib.Property (+  module Exported,+  PropertyValueType, pvtParser, pvtRenderer, pvtRendererPretty,+  ) where++import Game.Goatee.Lib.Property.Base as Exported+import Game.Goatee.Lib.Property.Info as Exported+import Game.Goatee.Lib.Property.Parser as Exported+import Game.Goatee.Lib.Property.Renderer as Exported+import Game.Goatee.Lib.Property.Value
+ src/Game/Goatee/Lib/Property/Base.hs view
@@ -0,0 +1,327 @@+-- This file is part of Goatee.+--+-- Copyright 2014 Bryan Gardiner+--+-- Goatee is free software: you can redistribute it and/or modify+-- it under the terms of the GNU Affero General Public License as published by+-- the Free Software Foundation, either version 3 of the License, or+-- (at your option) any later version.+--+-- Goatee is distributed in the hope that it will be useful,+-- but WITHOUT ANY WARRANTY; without even the implied warranty of+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the+-- GNU Affero General Public License for more details.+--+-- You should have received a copy of the GNU Affero General Public License+-- along with Goatee.  If not, see <http://www.gnu.org/licenses/>.++{-# LANGUAGE TemplateHaskell #-}+{-# OPTIONS_HADDOCK hide #-}++-- | Core property-related data types, and some Template Haskell declarations+-- for defining property metadata.+--+-- Import "Game.Goatee.Lib.Property" rather than importing this module.+module Game.Goatee.Lib.Property.Base (+  -- * Properties+  Property (..),+  -- * Property metadata+  PropertyType (..),+  Descriptor (..),+  SomeDescriptor (..),+  ValuedDescriptor (..),+  PropertyInfo,+  ValuedPropertyInfo (ValuedPropertyInfo),+  -- * Property declaration+  defProperty, defValuedProperty,+  ) where++import Control.Applicative ((<$))+import Game.Goatee.Lib.Property.Value (PropertyValueType(..), nonePvt)+import Game.Goatee.Lib.Renderer+import Game.Goatee.Lib.Types+import Language.Haskell.TH (+  Info (DataConI), DecsQ, Name, Type (AppT),+  appE, appT, caseE, conE, conP, conT, lam1E, match, mkName, newName,+  normalB, recP, reify, sigD, stringE, valD, varE, varP, wildP,+  )+import Text.ParserCombinators.Parsec (Parser)++-- | An SGF property that gives a node meaning.+data Property =+  -- Move properties.+    B (Maybe Coord)      -- ^ Black move (nothing iff pass).+  | KO                   -- ^ Execute move unconditionally (even if illegal).+  | MN Integer           -- ^ Assign move number.+  | W (Maybe Coord)      -- ^ White move (nothing iff pass).++  -- Setup properties.+  | AB CoordList         -- ^ Assign black stones.+  | AE CoordList         -- ^ Assign empty stones.+  | AW CoordList         -- ^ Assign white stones.+  | PL Color             -- ^ Player to play.++  -- Node annotation properties.+  | C Text               -- ^ Comment.+  | DM DoubleValue       -- ^ Even position.+  | GB DoubleValue       -- ^ Good for black.+  | GW DoubleValue       -- ^ Good for white.+  | HO DoubleValue       -- ^ Hotspot.+  | N SimpleText         -- ^ Node name.+  | UC DoubleValue       -- ^ Unclear position.+  | V RealValue          -- ^ Node value.++  -- Move annotation properties.+  | BM DoubleValue       -- ^ Bad move.+  | DO                   -- ^ Doubtful move.+  | IT                   -- ^ Interesting move.+  | TE DoubleValue       -- ^ Tesuji.++  -- Markup properties.+  | AR ArrowList         -- ^ Arrows.+  | CR CoordList         -- ^ Mark points with circles.+  | DD CoordList         -- ^ Dim points.+  | LB LabelList         -- ^ Label points with text.+  | LN LineList          -- ^ Lines.+  | MA CoordList         -- ^ Mark points with 'X's.+  | SL CoordList         -- ^ Mark points as selected.+  | SQ CoordList         -- ^ Mark points with squares.+  | TR CoordList         -- ^ Mark points with trianges.++  -- Root properties.+  | AP SimpleText SimpleText -- ^ Application info.+  | CA SimpleText        -- ^ Charset for SimpleText and Text.+  | FF Int               -- ^ File format version.+  | GM Int               -- ^ Game (must be 1 = Go).+  | ST VariationMode     -- ^ Variation display format.+  | SZ Int Int           -- ^ Board size, columns then rows.++  -- Game info properties.+  | AN SimpleText        -- ^ Name of annotator.+  | BR SimpleText        -- ^ Rank of black player.+  | BT SimpleText        -- ^ Name of black team.+  | CP SimpleText        -- ^ Copyright info.+  | DT SimpleText        -- ^ Dates played.+  | EV SimpleText        -- ^ Event name.+  | GC Text              -- ^ Game comment, or background, or summary.+  | GN SimpleText        -- ^ Game name.+  | ON SimpleText        -- ^ Information about the opening.+  | OT SimpleText        -- ^ The method used for overtime.+  | PB SimpleText        -- ^ Name of black player.+  | PC SimpleText        -- ^ Where the game was played.+  | PW SimpleText        -- ^ Name of white player.+  | RE GameResult        -- ^ Result of the game.+  | RO SimpleText        -- ^ Round info.+  | RU Ruleset           -- ^ Ruleset used.+  | SO SimpleText        -- ^ Source of the game.+  | TM RealValue         -- ^ Time limit, in seconds.+  | US SimpleText        -- ^ Name of user or program who entered the game.+  | WR SimpleText        -- ^ Rank of white player.+  | WT SimpleText        -- ^ Name of white team.++  -- Timing properties.+  | BL RealValue         -- ^ Black time left.+  | OB Int               -- ^ Black moves left in byo-yomi period.+  | OW Int               -- ^ White moves left in byo-yomi period.+  | WL RealValue         -- ^ White time left.++  -- Miscellaneous properties.+  -- TODO FG property.+  -- TODO PM property.+  | VW CoordList         -- ^ Set viewing region.++  -- Go-specific properties.+  | HA Int               -- ^ Handicap stones (>=2).+  | KM RealValue         -- ^ Komi.+  | TB CoordList         -- ^ Black territory.+  | TW CoordList         -- ^ White territory.++  | UnknownProperty String UnknownPropertyValue++  -- TODO Game info, timing, and miscellaneous properties.+  -- Also in functions below.+  deriving (Eq, Show)++-- | The property types that SGF uses to group properties.+data PropertyType = MoveProperty     -- ^ Cannot mix with setup nodes.+                  | SetupProperty    -- ^ Cannot mix with move nodes.+                  | RootProperty     -- ^ May only appear in root nodes.+                  | GameInfoProperty -- ^ At most one on any path.+                  | GeneralProperty  -- ^ May appear anywhere in the game tree.+                  deriving (Eq, Show)++-- | A class for types that contain metadata about a 'Property'.+class Descriptor a where+  -- | Returns the name of the property, as used in SGF files.+  propertyName :: a -> String++  -- | Returns the type of the property, as specified by the SGF spec.+  propertyType :: a -> PropertyType++  -- | Returns whether the value of the given property is inherited from the+  -- lowest ancestor specifying the property, when the property is not set on a+  -- node itself.+  propertyInherited :: a -> Bool++  -- | Returns whether the given property has the type of a descriptor.+  propertyPredicate :: a -> Property -> Bool++  -- | A parser of property values in SGF format (e.g. @"[ab]"@ for a property+  -- that takes a point).+  propertyValueParser :: a -> Parser Property++  -- | A renderer property values to SGF format (e.g. @B (Just (1,2))@ renders+  -- to @"[ab]"@).+  propertyValueRenderer :: a -> Property -> Render ()++  -- | A renderer for displaying property values in a UI.  Displays the value in+  -- a human-readable format.+  propertyValueRendererPretty :: a -> Property -> Render ()++data SomeDescriptor = forall a. Descriptor a => SomeDescriptor a++instance Descriptor SomeDescriptor where+  propertyName (SomeDescriptor d) = propertyName d+  propertyType (SomeDescriptor d) = propertyType d+  propertyInherited (SomeDescriptor d) = propertyInherited d+  propertyPredicate (SomeDescriptor d) = propertyPredicate d+  propertyValueParser (SomeDescriptor d) = propertyValueParser d+  propertyValueRenderer (SomeDescriptor d) = propertyValueRenderer d+  propertyValueRendererPretty (SomeDescriptor d) = propertyValueRendererPretty d++-- | A class for 'Descriptor's of 'Property's that also contain values.+class (Descriptor a, Eq v) => ValuedDescriptor a v | a -> v where+  -- | Extracts the value from a property of the given type.  Behaviour is+  -- undefined if the property is not of the given type.+  propertyValue :: a -> Property -> v++  -- | Builds a property from a given value.+  propertyBuilder :: a -> v -> Property++-- | Metadata for a property that does not contain a value.  Corresponds to a+-- single nullary data constructor of 'Property'.+data PropertyInfo = PropertyInfo+  { propertyInfoName :: String+    -- ^ The SGF textual name for the property.+  , propertyInfoInstance :: Property+    -- ^ The single instance of the property.+  , propertyInfoType :: PropertyType+    -- ^ The SGF property type.+  , propertyInfoInherited :: Bool+    -- ^ Whether the property is inherited.+  }++instance Descriptor PropertyInfo where+  propertyName = propertyInfoName+  propertyType = propertyInfoType+  propertyInherited = propertyInfoInherited+  propertyPredicate = (==) . propertyInfoInstance+  propertyValueParser descriptor = propertyInfoInstance descriptor <$ pvtParser nonePvt+  propertyValueRenderer _ _ = pvtRenderer nonePvt ()+  propertyValueRendererPretty _ _ = pvtRendererPretty nonePvt ()++-- | Metadata for a property that contains a value.  Corresponds to a single+-- unary data constructor of 'Property'.+data ValuedPropertyInfo v = ValuedPropertyInfo+  { valuedPropertyInfoName :: String+    -- ^ The SGF textual name for the property (also the name of the data+    -- constructor).+  , valuedPropertyInfoType :: PropertyType+    -- ^ The SGF property type.+  , valuedPropertyInfoInherited :: Bool+    -- ^ Whether the property is inherited.+  , valuedPropertyInfoPredicate :: Property -> Bool+    -- ^ A predicate that matches predicates to which this 'ValuedPropertyInfo'+    -- applies.+  , valuedPropertyInfoValueType :: PropertyValueType v+    -- ^ Metadata about the type of the property's value.+  , valuedPropertyInfoValue :: Property -> v+    -- ^ A function that extracts values from properties to which this+    -- 'ValuedPropertyInfo' applies.  It is invalid to call this function with a+    -- different type of property.+  , valuedPropertyInfoBuilder :: v -> Property+    -- ^ A function that builds a property containing a value.+  }++instance Descriptor (ValuedPropertyInfo v) where+  propertyName = valuedPropertyInfoName+  propertyType = valuedPropertyInfoType+  propertyInherited = valuedPropertyInfoInherited+  propertyPredicate = valuedPropertyInfoPredicate+  propertyValueParser descriptor =+    fmap (valuedPropertyInfoBuilder descriptor) $+    pvtParser $+    valuedPropertyInfoValueType descriptor+  propertyValueRenderer descriptor property =+    pvtRenderer (valuedPropertyInfoValueType descriptor) $+    valuedPropertyInfoValue descriptor property+  propertyValueRendererPretty descriptor property =+    pvtRendererPretty (valuedPropertyInfoValueType descriptor) $+    valuedPropertyInfoValue descriptor property++instance Eq v => ValuedDescriptor (ValuedPropertyInfo v) v where+  propertyValue = valuedPropertyInfoValue+  propertyBuilder = valuedPropertyInfoBuilder++-- | Template Haskell function to declare a property that does not contain a+-- value.+--+-- > $(defProperty "KO" 'MoveProperty False)+--+-- This example declares a @propertyKO :: 'PropertyInfo'@ that is a+-- 'MoveProperty' and is not inherited.+defProperty :: String+               -- ^ The SGF textual name of the property.+            -> Name+               -- ^ The name of the 'PropertyType'.+            -> Bool+               -- ^ Whether the property is inherited.+            -> DecsQ+defProperty name propType inherited = do+  let propName = mkName name+      varName = mkName $ "property" ++ name+  sequence [+    sigD varName $ conT $ mkName "PropertyInfo",+    valD (varP varName)+         (normalB [| PropertyInfo name $(conE propName) $(conE propType) inherited |])+         []+    ]++-- | Template Haskell function to declare a property that contains a value.+--+-- > $(defValuedProperty "B" 'MoveProperty False 'maybeCoordPrinter)+--+-- This example declares a @propertyB :: 'ValuedPropertyInfo' (Maybe 'Coord')@+-- that is a 'MoveProperty' and is not inherited.  The value type is+-- automatically inferred.+defValuedProperty :: String -> Name -> Bool -> Name -> DecsQ+defValuedProperty name propType inherited valueType = do+  let propName = mkName name+      varName = mkName $ "property" ++ name+  foo <- newName "foo"+  bar <- newName "bar"+  DataConI _ (AppT (AppT _ haskellValueType) _) _ _ <- reify propName+  sequence [+    sigD varName $ appT (conT ''ValuedPropertyInfo) $ return haskellValueType,+    valD (varP varName)+         (normalB [| ValuedPropertyInfo+                     name+                     $(conE propType)+                     inherited+                     $(lam1E (varP foo) $ caseE (varE foo)+                       [match (recP propName [])+                              (normalB $ conE $ mkName "True")+                              [],+                        match wildP (normalB $ conE $ mkName "False") []])+                     $(varE valueType)+                     $(lam1E (varP foo) $ caseE (varE foo)+                       [match (conP propName [varP bar]) (normalB $ varE bar) [],+                        match wildP+                              (normalB+                               [| error $ "Property value getter for " ++ $(stringE name) +++                                  " applied to " ++ show $(varE foo) ++ "." |])+                              []])+                     $(lam1E (varP foo) $ appE (conE propName) (varE foo))+                   |])+         []+    ]
+ src/Game/Goatee/Lib/Property/Info.hs view
@@ -0,0 +1,320 @@+-- This file is part of Goatee.+--+-- Copyright 2014 Bryan Gardiner+--+-- Goatee is free software: you can redistribute it and/or modify+-- it under the terms of the GNU Affero General Public License as published by+-- the Free Software Foundation, either version 3 of the License, or+-- (at your option) any later version.+--+-- Goatee is distributed in the hope that it will be useful,+-- but WITHOUT ANY WARRANTY; without even the implied warranty of+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the+-- GNU Affero General Public License for more details.+--+-- You should have received a copy of the GNU Affero General Public License+-- along with Goatee.  If not, see <http://www.gnu.org/licenses/>.++{-# LANGUAGE TemplateHaskell #-}+{-# OPTIONS_HADDOCK hide #-}++-- | Property metadata declarations.+--+-- Import "Game.Goatee.Lib.Property" rather than importing this module.+module Game.Goatee.Lib.Property.Info where++import Control.Arrow ((&&&))+import qualified Data.Map as Map+import Data.Map (Map)+import Data.Maybe (fromMaybe)+import Game.Goatee.Lib.Property.Base+import Game.Goatee.Lib.Property.Value+import Game.Goatee.Lib.Types++-- Move properties.+$(defValuedProperty "B" 'MoveProperty False 'movePvt)+$(defProperty "KO" 'MoveProperty False)+$(defValuedProperty "MN" 'MoveProperty False 'integralPvt)+$(defValuedProperty "W" 'MoveProperty False 'movePvt)++-- Setup properties.+$(defValuedProperty "AB" 'SetupProperty False 'coordListPvt)+$(defValuedProperty "AE" 'SetupProperty False 'coordListPvt)+$(defValuedProperty "AW" 'SetupProperty False 'coordListPvt)+$(defValuedProperty "PL" 'SetupProperty False 'colorPvt)++-- Node annotation properties.+$(defValuedProperty "C" 'GeneralProperty False 'textPvt)+$(defValuedProperty "DM" 'GeneralProperty False 'doublePvt)+$(defValuedProperty "GB" 'GeneralProperty False 'doublePvt)+$(defValuedProperty "GW" 'GeneralProperty False 'doublePvt)+$(defValuedProperty "HO" 'GeneralProperty False 'doublePvt)+$(defValuedProperty "N" 'GeneralProperty False 'simpleTextPvt)+$(defValuedProperty "UC" 'GeneralProperty False 'doublePvt)+$(defValuedProperty "V" 'GeneralProperty False 'realPvt)++-- Move annotation properties.+$(defValuedProperty "BM" 'MoveProperty False 'doublePvt)+$(defProperty "DO" 'MoveProperty False)+$(defProperty "IT" 'MoveProperty False)+$(defValuedProperty "TE" 'MoveProperty False 'doublePvt)++-- Markup properties.+$(defValuedProperty "AR" 'GeneralProperty False 'coordPairListPvt)+$(defValuedProperty "CR" 'GeneralProperty False 'coordListPvt)+$(defValuedProperty "DD" 'GeneralProperty True 'coordListPvt)+$(defValuedProperty "LB" 'GeneralProperty False 'labelListPvt)+$(defValuedProperty "LN" 'GeneralProperty False 'coordPairListPvt)+$(defValuedProperty "MA" 'GeneralProperty False 'coordListPvt)+$(defValuedProperty "SL" 'GeneralProperty False 'coordListPvt)+$(defValuedProperty "SQ" 'GeneralProperty False 'coordListPvt)+$(defValuedProperty "TR" 'GeneralProperty False 'coordListPvt)++-- Root properties.+propertyAP :: ValuedPropertyInfo (SimpleText, SimpleText)+propertyAP = ValuedPropertyInfo "AP" RootProperty False+             (\x -> case x of { AP {} -> True; _ -> False })+             simpleTextPairPvt+             (\(AP x y) -> (x, y))+             (uncurry AP)+$(defValuedProperty "CA" 'RootProperty False 'simpleTextPvt)+$(defValuedProperty "FF" 'RootProperty False 'integralPvt)  -- TODO Add parser validation.+$(defValuedProperty "GM" 'RootProperty False 'integralPvt)  -- TODO Add parser validation.+$(defValuedProperty "ST" 'RootProperty False 'variationModePvt)+propertySZ :: ValuedPropertyInfo (Int, Int)+propertySZ = ValuedPropertyInfo "SZ" RootProperty False+             (\x -> case x of { SZ {} -> True; _ -> False })+             sizePvt+             (\(SZ x y) -> (x, y))+             (uncurry SZ)++-- Game info properties.+$(defValuedProperty "AN" 'GameInfoProperty False 'simpleTextPvt)+$(defValuedProperty "BR" 'GameInfoProperty False 'simpleTextPvt)+$(defValuedProperty "BT" 'GameInfoProperty False 'simpleTextPvt)+$(defValuedProperty "CP" 'GameInfoProperty False 'simpleTextPvt)+$(defValuedProperty "DT" 'GameInfoProperty False 'simpleTextPvt)+$(defValuedProperty "EV" 'GameInfoProperty False 'simpleTextPvt)+$(defValuedProperty "GC" 'GameInfoProperty False 'textPvt)+$(defValuedProperty "GN" 'GameInfoProperty False 'simpleTextPvt)+$(defValuedProperty "ON" 'GameInfoProperty False 'simpleTextPvt)+$(defValuedProperty "OT" 'GameInfoProperty False 'simpleTextPvt)+$(defValuedProperty "PB" 'GameInfoProperty False 'simpleTextPvt)+$(defValuedProperty "PC" 'GameInfoProperty False 'simpleTextPvt)+$(defValuedProperty "PW" 'GameInfoProperty False 'simpleTextPvt)+$(defValuedProperty "RE" 'GameInfoProperty False 'gameResultPvt)+$(defValuedProperty "RO" 'GameInfoProperty False 'simpleTextPvt)+$(defValuedProperty "RU" 'GameInfoProperty False 'rulesetPvt)+$(defValuedProperty "SO" 'GameInfoProperty False 'simpleTextPvt)+$(defValuedProperty "TM" 'GameInfoProperty False 'realPvt)+$(defValuedProperty "US" 'GameInfoProperty False 'simpleTextPvt)+$(defValuedProperty "WR" 'GameInfoProperty False 'simpleTextPvt)+$(defValuedProperty "WT" 'GameInfoProperty False 'simpleTextPvt)++-- Timing properties.+$(defValuedProperty "BL" 'MoveProperty False 'realPvt)+$(defValuedProperty "OB" 'MoveProperty False 'integralPvt)+$(defValuedProperty "OW" 'MoveProperty False 'integralPvt)+$(defValuedProperty "WL" 'MoveProperty False 'realPvt)++-- Miscellaneous properties.+$(defValuedProperty "VW" 'GeneralProperty True 'coordElistPvt)++-- Go-specific properties.+$(defValuedProperty "HA" 'GameInfoProperty False 'integralPvt)+$(defValuedProperty "KM" 'GameInfoProperty False 'realPvt)+$(defValuedProperty "TB" 'GeneralProperty False 'coordElistPvt)+$(defValuedProperty "TW" 'GeneralProperty False 'coordElistPvt)++propertyUnknown :: String -> ValuedPropertyInfo UnknownPropertyValue+propertyUnknown name =+  ValuedPropertyInfo name GeneralProperty False+  (\x -> case x of+      UnknownProperty name' _ | name' == name -> True+      _ -> False)+  unknownPropertyPvt+  (\(UnknownProperty _ value) -> value)+  (UnknownProperty name)++allDescriptors :: [SomeDescriptor]+allDescriptors =+  [ SomeDescriptor propertyB+  , SomeDescriptor propertyKO+  , SomeDescriptor propertyMN+  , SomeDescriptor propertyW++  , SomeDescriptor propertyAB+  , SomeDescriptor propertyAE+  , SomeDescriptor propertyAW+  , SomeDescriptor propertyPL++  , SomeDescriptor propertyC+  , SomeDescriptor propertyDM+  , SomeDescriptor propertyGB+  , SomeDescriptor propertyGW+  , SomeDescriptor propertyHO+  , SomeDescriptor propertyN+  , SomeDescriptor propertyUC+  , SomeDescriptor propertyV++  , SomeDescriptor propertyBM+  , SomeDescriptor propertyDO+  , SomeDescriptor propertyIT+  , SomeDescriptor propertyTE++  , SomeDescriptor propertyAR+  , SomeDescriptor propertyCR+  , SomeDescriptor propertyDD+  , SomeDescriptor propertyLB+  , SomeDescriptor propertyLN+  , SomeDescriptor propertyMA+  , SomeDescriptor propertySL+  , SomeDescriptor propertySQ+  , SomeDescriptor propertyTR++  , SomeDescriptor propertyAP+  , SomeDescriptor propertyCA+  , SomeDescriptor propertyFF+  , SomeDescriptor propertyGM+  , SomeDescriptor propertyST+  , SomeDescriptor propertySZ++  , SomeDescriptor propertyAN+  , SomeDescriptor propertyBR+  , SomeDescriptor propertyBT+  , SomeDescriptor propertyCP+  , SomeDescriptor propertyDT+  , SomeDescriptor propertyEV+  , SomeDescriptor propertyGC+  , SomeDescriptor propertyGN+  , SomeDescriptor propertyON+  , SomeDescriptor propertyOT+  , SomeDescriptor propertyPB+  , SomeDescriptor propertyPC+  , SomeDescriptor propertyPW+  , SomeDescriptor propertyRE+  , SomeDescriptor propertyRO+  , SomeDescriptor propertyRU+  , SomeDescriptor propertySO+  , SomeDescriptor propertyTM+  , SomeDescriptor propertyUS+  , SomeDescriptor propertyWR+  , SomeDescriptor propertyWT++  , SomeDescriptor propertyBL+  , SomeDescriptor propertyOB+  , SomeDescriptor propertyOW+  , SomeDescriptor propertyWL++  , SomeDescriptor propertyVW++  , SomeDescriptor propertyHA+  , SomeDescriptor propertyKM+  , SomeDescriptor propertyTB+  , SomeDescriptor propertyTW+  ]++propertyInfo :: Property -> SomeDescriptor+propertyInfo property = case property of+  B {} -> SomeDescriptor propertyB+  KO {} -> SomeDescriptor propertyKO+  MN {} -> SomeDescriptor propertyMN+  W {} -> SomeDescriptor propertyW++  AB {} -> SomeDescriptor propertyAB+  AE {} -> SomeDescriptor propertyAE+  AW {} -> SomeDescriptor propertyAW+  PL {} -> SomeDescriptor propertyPL++  C {} -> SomeDescriptor propertyC+  DM {} -> SomeDescriptor propertyDM+  GB {} -> SomeDescriptor propertyGB+  GW {} -> SomeDescriptor propertyGW+  HO {} -> SomeDescriptor propertyHO+  N {} -> SomeDescriptor propertyN+  UC {} -> SomeDescriptor propertyUC+  V {} -> SomeDescriptor propertyV++  BM {} -> SomeDescriptor propertyBM+  DO {} -> SomeDescriptor propertyDO+  IT {} -> SomeDescriptor propertyIT+  TE {} -> SomeDescriptor propertyTE++  AR {} -> SomeDescriptor propertyAR+  CR {} -> SomeDescriptor propertyCR+  DD {} -> SomeDescriptor propertyDD+  LB {} -> SomeDescriptor propertyLB+  LN {} -> SomeDescriptor propertyLN+  MA {} -> SomeDescriptor propertyMA+  SL {} -> SomeDescriptor propertySL+  SQ {} -> SomeDescriptor propertySQ+  TR {} -> SomeDescriptor propertyTR++  AP {} -> SomeDescriptor propertyAP+  CA {} -> SomeDescriptor propertyCA+  FF {} -> SomeDescriptor propertyFF+  GM {} -> SomeDescriptor propertyGM+  ST {} -> SomeDescriptor propertyST+  SZ {} -> SomeDescriptor propertySZ++  AN {} -> SomeDescriptor propertyAN+  BR {} -> SomeDescriptor propertyBR+  BT {} -> SomeDescriptor propertyBT+  CP {} -> SomeDescriptor propertyCP+  DT {} -> SomeDescriptor propertyDT+  EV {} -> SomeDescriptor propertyEV+  GC {} -> SomeDescriptor propertyGC+  GN {} -> SomeDescriptor propertyGN+  ON {} -> SomeDescriptor propertyON+  OT {} -> SomeDescriptor propertyOT+  PB {} -> SomeDescriptor propertyPB+  PC {} -> SomeDescriptor propertyPC+  PW {} -> SomeDescriptor propertyPW+  RE {} -> SomeDescriptor propertyRE+  RO {} -> SomeDescriptor propertyRO+  RU {} -> SomeDescriptor propertyRU+  SO {} -> SomeDescriptor propertySO+  TM {} -> SomeDescriptor propertyTM+  US {} -> SomeDescriptor propertyUS+  WR {} -> SomeDescriptor propertyWR+  WT {} -> SomeDescriptor propertyWT++  BL {} -> SomeDescriptor propertyBL+  OB {} -> SomeDescriptor propertyOB+  OW {} -> SomeDescriptor propertyOW+  WL {} -> SomeDescriptor propertyWL++  VW {} -> SomeDescriptor propertyVW++  HA {} -> SomeDescriptor propertyHA+  KM {} -> SomeDescriptor propertyKM+  TB {} -> SomeDescriptor propertyTB+  TW {} -> SomeDescriptor propertyTW++  UnknownProperty name _ -> SomeDescriptor $ propertyUnknown name++instance Descriptor Property where+  propertyName = propertyName . propertyInfo+  propertyType = propertyType . propertyInfo+  propertyInherited = propertyInherited . propertyInfo+  propertyPredicate = propertyPredicate . propertyInfo+  propertyValueParser = propertyValueParser . propertyInfo+  propertyValueRenderer = propertyValueRenderer . propertyInfo+  propertyValueRendererPretty = propertyValueRendererPretty . propertyInfo++descriptorsByName :: Map String SomeDescriptor+descriptorsByName = Map.fromList $ map (propertyName &&& id) allDescriptors++descriptorForName :: String -> SomeDescriptor+descriptorForName name = fromMaybe (SomeDescriptor $ propertyUnknown name) $ descriptorForName' name++descriptorForName' :: String -> Maybe SomeDescriptor+descriptorForName' = flip Map.lookup descriptorsByName++-- | Returns the descriptor for a mark.+markProperty :: Mark -> ValuedPropertyInfo CoordList+markProperty MarkCircle = propertyCR+markProperty MarkSelected = propertySL+markProperty MarkSquare = propertySQ+markProperty MarkTriangle = propertyTR+markProperty MarkX = propertyMA
+ src/Game/Goatee/Lib/Property/Parser.hs view
@@ -0,0 +1,252 @@+-- This file is part of Goatee.+--+-- Copyright 2014 Bryan Gardiner+--+-- Goatee is free software: you can redistribute it and/or modify+-- it under the terms of the GNU Affero General Public License as published by+-- the Free Software Foundation, either version 3 of the License, or+-- (at your option) any later version.+--+-- Goatee is distributed in the hope that it will be useful,+-- but WITHOUT ANY WARRANTY; without even the implied warranty of+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the+-- GNU Affero General Public License for more details.+--+-- You should have received a copy of the GNU Affero General Public License+-- along with Goatee.  If not, see <http://www.gnu.org/licenses/>.++-- | Parsers of property values.+--+-- Import "Game.Goatee.Lib.Property" rather than importing this module.+module Game.Goatee.Lib.Property.Parser (+  colorParser,+  coordElistParser,+  coordListParser,+  coordPairListParser,+  doubleParser,+  gameResultParser,+  labelListParser,+  moveParser,+  noneParser,+  integralParser,+  realParser,+  rulesetParser,+  simpleTextPairParser,+  simpleTextParser,+  sizeParser,+  textParser,+  unknownPropertyParser,+  variationModeParser,+  -- * Exposed for testing+  compose,+  line,+  simpleText,+  text,+  ) where++import Control.Applicative ((<$), (<$>), (<*), (<*>), (*>))+import Control.Monad (when)+import Data.Char (isUpper, ord)+import Data.Maybe (catMaybes)+import Data.Monoid (Monoid, mappend, mconcat, mempty)+import qualified Game.Goatee.Common.Bigfloat as BF+import Game.Goatee.Lib.Types+import Text.ParserCombinators.Parsec (+  (<?>), (<|>), Parser,+  anyChar, char, choice, digit, many, many1,+  noneOf, oneOf, option, space, spaces, string,+  try, unexpected,+  )++{-# ANN module "HLint: ignore Reduce duplication" #-}++-- Internal parser builders not corresponding to any particular value type.++-- | A wrapper around 'CoordList' with a 'Monoid' instance used for parsing.+-- The monoid does simple concatenation of the single and rectangle lists, so it+-- is not appropriate for @CoordList@ proper, as it doesn't do duplicate removal+-- between two @CoordList@s.+newtype CoordListMonoid = CoordListMonoid { runCoordListMonoid :: CoordList }++instance Monoid CoordListMonoid where+  mempty = CoordListMonoid emptyCoordList++  mappend (CoordListMonoid x) (CoordListMonoid y) =+    CoordListMonoid $ coords' (coordListSingles x ++ coordListSingles y)+                              (coordListRects x ++ coordListRects y)++single :: Parser a -> Parser a+single valueParser = char '[' *> valueParser <* char ']'++compose :: Parser a -> Parser b -> Parser (a, b)+compose first second = do+  x <- first+  char ':'+  y <- second+  return (x, y)++line :: Parser Int+line = toLine <$> line' <?> "line"+  where line' = oneOf $ ['a'..'z'] ++ ['A'..'Z']+        toLine c = if isUpper c+                   then ord c - ord 'A' + 26+                   else ord c - ord 'a'++listOf :: Parser a -> Parser [a]+listOf valueParser = many1 (single valueParser <* spaces)++number :: Parser String+number = do+  addSign <- ('-':) <$ char '-' <|>+             id <$ char '+' <|>+             return id+  addSign <$> many1 digit++-- Public parsers.++colorParser :: Parser Color+colorParser = single color <?> "color"++color :: Parser Color+color = choice [Black <$ char 'B',+                White <$ char 'W']++coord :: Parser Coord+coord = (,) <$> line <*> line++coordElistParser :: Parser CoordList+coordElistParser =+  try (emptyCoordList <$ string "[]") <|>+  coordListParser <?>+  "list of points or empty"++coordListParser :: Parser CoordList+coordListParser =+  runCoordListMonoid . mconcat . map CoordListMonoid <$> listOf coordListEntry <?>+  "list of points"+  where coordListEntry = do x0 <- line+                            y0 <- line+                            choice [do char ':'+                                       x1 <- line+                                       y1 <- line+                                       return $ coordR ((x0, y0), (x1, y1)),+                                    return $ coord1 (x0, y0)]+        coordR rect = coords' [] [rect]++coordPairListParser :: Parser [(Coord, Coord)]+coordPairListParser = listOf coordPair <?> "list of point pairs"+  where coordPair = do+          x0 <- line+          y0 <- line+          char ':'+          x1 <- line+          y1 <- line+          return ((x0, y0), (x1, y1))++doubleParser :: Parser DoubleValue+doubleParser =+  single (Double1 <$ char '1' <|>+          Double2 <$ char '2') <?>+  "double"++gameResultParser :: Parser GameResult+gameResultParser = single (convertStringlike <$> simpleText False) <?> "game result"++labelListParser :: Parser [(Coord, SimpleText)]+labelListParser =+  listOf (compose coord $ simpleText True) <?> "list of points and labels"++moveParser :: Parser (Maybe Coord)+moveParser =+  char '[' *> (Nothing <$ char ']' <|> Just <$> coord <* char ']') <?>+  "move (point or pass)"++noneParser :: Parser ()+noneParser = () <$ string "[]" <?> "none"++-- This is what the SGF spec calls the Number type, i.e. a signed integer.+integralParser :: (Integral a, Read a) => Parser a+integralParser = single integral <?> "integer"++integral :: (Integral a, Read a) => Parser a+integral = read <$> number++realParser :: Parser RealValue+realParser = single real <?> "real"++real :: Parser RealValue+real = do+  whole <- number+  -- Try to read a fractional part of the number.+  -- If we fail, just return the whole part.+  option (fromInteger $ read whole) $ try $ do+    fractional <- char '.' *> many1 digit+    return $ BF.encode (read $ whole ++ fractional) (-length fractional)++rulesetParser :: Parser Ruleset+rulesetParser =+  single (toRuleset . fromSimpleText <$> simpleText False) <?> "ruleset"++simpleTextPairParser :: Parser (SimpleText, SimpleText)+simpleTextPairParser = single (compose composedText composedText) <?> "pair of simple texts"+  where composedText = simpleText True++-- | A parser for SGF SimpleText property values.+simpleTextParser :: Parser SimpleText+simpleTextParser = single (simpleText False) <?> "simple text"++simpleText :: Bool -> Parser SimpleText+simpleText isComposed = toSimpleText <$> text isComposed++sizeParser :: Parser (Int, Int)+sizeParser =+  (do char '['+      width <- integral+      height <- choice [width <$ char ']',+                        do char ':'+                           height <- integral+                           char ']'+                           -- TODO We should warn here rather than aborting.+                           when (width == height) $+                             fail $ show width ++ "x" ++ show height ++ " square board " +++                             " dimensions should be specified with a single number."+                           return height]+      when (width < 1 || width > boardSizeMax ||+            height < 1 || height > boardSizeMax) $+        fail $ show width ++ "x" ++ show height ++ " board dimensions are invalid.  " +++        "Each dimension must be between 1 and 52 inclusive."+      return (width, height)) <?>+  "board size (width or width:height)"++textParser :: Parser Text+textParser = single (toText <$> text False) <?> "text"++-- | A parser for SGF text property values.  Its argument should be true if the+-- text is inside of a composed property value, so @\':\'@ should terminate the+-- value in addition to @']'@.+text :: Bool -> Parser String+text isComposed = catMaybes <$> many textChar'+  where textChar' = textChar (if isComposed then ":]\\" else "]\\")++textChar :: String -> Parser (Maybe Char)+textChar specialChars =+  choice [Just <$> char '\n',+          Just ' ' <$ space,+          try (char '\\' *> (Nothing <$ char '\n' <|>+                             Just <$> anyChar)),+          Just <$> noneOf specialChars]++unknownPropertyParser :: Parser UnknownPropertyValue+unknownPropertyParser =+  single (toUnknownPropertyValue <$> text False) <?>+  "unknown property value"++variationModeParser :: Parser VariationMode+variationModeParser = single variationMode <?> "variation mode"++variationMode :: Parser VariationMode+variationMode = do+  value <- integral+  case toVariationMode value of+    Just mode -> return mode+    Nothing -> unexpected $ "variation mode " ++ show value
+ src/Game/Goatee/Lib/Property/Renderer.hs view
@@ -0,0 +1,332 @@+-- This file is part of Goatee.+--+-- Copyright 2014 Bryan Gardiner+--+-- Goatee is free software: you can redistribute it and/or modify+-- it under the terms of the GNU Affero General Public License as published by+-- the Free Software Foundation, either version 3 of the License, or+-- (at your option) any later version.+--+-- Goatee is distributed in the hope that it will be useful,+-- but WITHOUT ANY WARRANTY; without even the implied warranty of+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the+-- GNU Affero General Public License for more details.+--+-- You should have received a copy of the GNU Affero General Public License+-- along with Goatee.  If not, see <http://www.gnu.org/licenses/>.++{-# LANGUAGE CPP #-}++-- | Renderers of property values.+--+-- Import "Game.Goatee.Lib.Property" rather than importing this module.+module Game.Goatee.Lib.Property.Renderer (+  renderColorBracketed,+  renderColorPretty,+  renderCoordElistBracketed,+  renderCoordElistPretty,+  renderCoordListBracketed,+  renderCoordListPretty,+  renderCoordPairListBracketed,+  renderCoordPairListPretty,+  renderDoubleBracketed,+  renderDoublePretty,+  renderGameResultBracketed,+  renderGameResultPretty,+  renderGameResultPretty',+  renderIntegralBracketed,+  renderIntegralPretty,+  renderLabelListBracketed,+  renderLabelListPretty,+  renderMoveBracketed,+  renderMovePretty,+  renderNoneBracketed,+  renderNonePretty,+  renderRealBracketed,+  renderRealPretty,+  renderRulesetBracketed,+  renderRulesetPretty,+  renderSimpleTextBracketed,+  renderSimpleTextPairBracketed,+  renderSimpleTextPairPretty,+  renderSimpleTextPretty,+  renderSizeBracketed,+  renderSizePretty,+  renderTextBracketed,+  renderTextPretty,+  renderUnknownPropertyBracketed,+  renderUnknownPropertyPretty,+  renderVariationModeBracketed,+  renderVariationModePretty,+  ) where++import Control.Monad (forM_, void, when)+#if MIN_VERSION_mtl(2,2,1)+import Control.Monad.Except (throwError)+#else+import Control.Monad.Error (throwError)+#endif+import Control.Monad.Writer (tell)+import Data.Char (chr, ord)+import Data.List (intersperse)+import qualified Game.Goatee.Common.Bigfloat as BF+import Game.Goatee.Lib.Renderer+import Game.Goatee.Lib.Types++{-# ANN module "HLint: ignore Use <$>" #-}++-- Internal renderers not corresponding to any particular value type.++bracketed :: Render () -> Render ()+bracketed x = tell "[" >> x >> tell "]"++renderLine :: Int -> Render ()+renderLine = rendererOf "line" $ \x ->+  if x >= 0 && x < 52+  then tell [chr $ x + (if x < 26 then ord 'a' else ord 'A' - 26)]+  else throwError $ "renderLine: Index not in [0, 52): " ++ show x++renderCoord :: Coord -> Render ()+renderCoord = rendererOf "coord" $ \(x, y) -> renderLine x >> renderLine y++renderCoordBracketed :: Coord -> Render ()+renderCoordBracketed = fmap bracketed renderCoord++renderCoordPretty :: Coord -> Render ()+renderCoordPretty = rendererOf "coord pretty" $ \(x, y) ->+  mapM_ tell ["(", show x, ", ", show y, ")"]++renderCoordPairPretty :: (Coord, Coord) -> Render ()+renderCoordPairPretty = rendererOf "coord pair pretty" $ \(a, b) -> do+  renderCoordPretty a+  tell "-"+  renderCoordPretty b++renderCoordPair :: (Coord, Coord) -> Render ()+renderCoordPair = rendererOf "coord pair" $ \(a, b) ->+  renderCoord a >> tell ":" >> renderCoord b++renderCoordPairBracketed :: (Coord, Coord) -> Render ()+renderCoordPairBracketed = fmap bracketed renderCoordPair++renderShowable :: Show a => a -> Render ()+renderShowable = tell . show++renderStringlike :: (Show a, Stringlike a) => Bool -> a -> Render ()+renderStringlike isComposed =+  rendererOf (if isComposed then "composed stringlike" else "stringlike") $ \str ->+  tell $ escape $ sgfToString str+  where escape [] = []+        escape (first:rest) | first `elem` specialChars = '\\':first:escape rest+                            | otherwise = first:escape rest+        -- TODO Deduplicate these characters with the parser:+        specialChars = if isComposed then ":]\\" else "]\\"++-- Note that unlike the serialized SGF version, we don't care about escaping+-- characters in composed strings.+renderStringlikePretty :: (Show a, Stringlike a) => a -> Render ()+renderStringlikePretty = rendererOf "stringlike pretty" $ tell . sgfToString++-- Public renderers.++renderColorBracketed :: Color -> Render ()+renderColorBracketed color = bracketed $ tell $ case color of+  Black -> "B"+  White -> "W"++renderColorPretty :: Color -> Render ()+renderColorPretty Black = tell "Black"+renderColorPretty White = tell "White"++renderCoordElistBracketed :: CoordList -> Render ()+renderCoordElistBracketed = rendererOf "coord elist bracketed" $ \list ->+  if null $ expandCoordList list+  then tell "[]"+  else renderCoordListNonempty list++renderCoordElistPretty :: CoordList -> Render ()+renderCoordElistPretty = rendererOf "coord elist pretty" $ \list ->+  if null $ expandCoordList list+  then tell "empty"+  else renderCoordListNonemptyPretty list++renderCoordListBracketed :: CoordList -> Render ()+renderCoordListBracketed = rendererOf "coord list bracketed" $ \list ->+  if null $ expandCoordList list+  then throwError "renderCoordListBracketed: Unexpected empty CoordList."+  else renderCoordListNonempty list++renderCoordListPretty :: CoordList -> Render ()+renderCoordListPretty = rendererOf "coord list pretty" $ \list ->+  if null $ expandCoordList list+  then throwError "renderCoordListPretty: Unexpected empty CoordList."+  else renderCoordListNonemptyPretty list++renderCoordListNonempty :: CoordList -> Render ()+renderCoordListNonempty = rendererOf "coord list nonempty" $ \list -> do+  mapM_ renderCoordPairBracketed $ coordListRects list+  mapM_ renderCoordBracketed $ coordListSingles list++renderCoordListNonemptyPretty :: CoordList -> Render ()+renderCoordListNonemptyPretty = rendererOf "coord list nonempty pretty" $ \list ->+  sequence_ $ intersperse (tell ", ") $+  map renderCoordPairPretty (coordListRects list) +++  map renderCoordPretty (coordListSingles list)++renderCoordPairListBracketed :: [(Coord, Coord)] -> Render ()+renderCoordPairListBracketed = rendererOf "coord pair list bracketed" $ \list ->+  if null list+  then throwError "renderCoordPairListBracketed: Unexpected empty list."+  else mapM_ renderCoordPairBracketed list++renderCoordPairListPretty :: [(Coord, Coord)] -> Render ()+renderCoordPairListPretty list =+  if null list+  then throwError "renderCoordPairListPretty: Unexpected empty list."+  else sequence_ $ intersperse (tell ", ") $ map renderCoordPairPretty list++renderDoubleBracketed :: DoubleValue -> Render ()+renderDoubleBracketed = fmap bracketed $ rendererOf "double" $ \double -> tell $ case double of+  Double1 -> "1"+  Double2 -> "2"++renderDoublePretty :: DoubleValue -> Render ()+renderDoublePretty = rendererOf "double pretty" $ tell . \double -> case double of+  Double1 -> "1"+  Double2 -> "2"++renderGameResultBracketed :: GameResult -> Render ()+renderGameResultBracketed = fmap bracketed $ rendererOf "game result" $ \result -> case result of+  GameResultWin color reason ->+    tell $+    (case color of { Black -> 'B'; White -> 'W' }) : '+' :+    (case reason of+        WinByScore diff -> show diff+        WinByResignation -> "R"+        WinByTime -> "T"+        WinByForfeit -> "F")+  GameResultDraw -> tell "0"+  GameResultVoid -> tell "Void"+  GameResultUnknown -> tell "?"+  GameResultOther text -> renderStringlike False text++renderGameResultPretty :: GameResult -> Render ()+renderGameResultPretty =+  rendererOf "game result pretty" $ void . tell . renderGameResultPretty'++renderGameResultPretty' :: GameResult -> String+renderGameResultPretty' result = case result of+  GameResultWin color reason ->+    (case color of { Black -> 'B'; White -> 'W' }) : '+' :+    (case reason of+        WinByScore diff -> show diff+        WinByResignation -> "Resign"+        WinByTime -> "Time"+        WinByForfeit -> "Forfeit")+  GameResultDraw -> "Draw"+  GameResultVoid -> "Void"+  GameResultUnknown -> "Unknown"+  GameResultOther text -> sgfToString text++renderIntegralBracketed :: (Integral a, Show a) => a -> Render ()+renderIntegralBracketed = bracketed . rendererOf "integral" renderShowable++renderIntegralPretty :: (Integral a, Show a) => a -> Render ()+renderIntegralPretty = rendererOf "integral pretty" renderShowable++renderLabelListBracketed :: [(Coord, SimpleText)] -> Render ()+renderLabelListBracketed = rendererOf "label list" $ \list ->+  if null list+  then throwError "renderLabelListBracketed: Unexpected empty list."+  else forM_ list $ bracketed . \(coord, text) -> do+    renderCoord coord+    tell ":"+    renderStringlike True text++renderLabelListPretty :: [(Coord, SimpleText)] -> Render ()+renderLabelListPretty = rendererOf "label list pretty" $ \list ->+  if null list+  then throwError "renderLabelListPretty: Unexpected empty list."+  else sequence_ $ intersperse (tell ", ") $+       map (\(coord, text) -> do+               renderCoordPretty coord+               tell ":"+               renderStringlikePretty text)+       list++renderMoveBracketed :: Maybe Coord -> Render ()+renderMoveBracketed = rendererOf "move bracketed" $ maybe (tell "[]") renderCoordBracketed++renderMovePretty :: Maybe Coord -> Render ()+renderMovePretty = rendererOf "move pretty" $ maybe (tell "Pass") renderCoordPretty++renderNoneBracketed :: () -> Render ()+renderNoneBracketed = rendererOf "none bracketed" $ tell . const "[]"++renderNonePretty :: () -> Render ()+renderNonePretty = rendererOf "none pretty" $ tell . const ""++renderRealBracketed :: RealValue -> Render ()+renderRealBracketed =+  fmap bracketed $ rendererOf "real" (renderShowable :: BF.Bigfloat -> Render ())++renderRealPretty :: RealValue -> Render ()+renderRealPretty = rendererOf "real pretty" (renderShowable :: BF.Bigfloat -> Render ())++renderRulesetBracketed :: Ruleset -> Render ()+renderRulesetBracketed = fmap bracketed $ rendererOf "ruleset" $ tell . fromRuleset++renderRulesetPretty :: Ruleset -> Render ()+renderRulesetPretty = rendererOf "ruleset pretty" $ tell . fromRuleset++renderSimpleTextPairBracketed :: (SimpleText, SimpleText) -> Render ()+renderSimpleTextPairBracketed = fmap bracketed $ rendererOf "simple text pair" $ \(a, b) -> do+  renderStringlike True a+  tell ":"+  renderStringlike True b++renderSimpleTextPairPretty :: (SimpleText, SimpleText) -> Render ()+renderSimpleTextPairPretty = rendererOf "simple text pair pretty" $ \(a, b) -> do+  renderStringlike True a+  tell " "+  renderStringlike True b++renderSimpleTextBracketed :: SimpleText -> Render ()+renderSimpleTextBracketed = fmap bracketed $ rendererOf "simple text" $ renderStringlike False++renderSimpleTextPretty :: SimpleText -> Render ()+renderSimpleTextPretty = rendererOf "simple text pretty" $ tell . fromSimpleText++renderSizeBracketed :: (Int, Int) -> Render ()+renderSizeBracketed = fmap bracketed $ rendererOf "size" $ \(x, y) -> do+  tell $ show x+  when (x /= y) $ tell $ ':' : show y++renderSizePretty :: (Int, Int) -> Render ()+renderSizePretty = rendererOf "size pretty" renderCoordPretty++renderTextBracketed :: Text -> Render ()+renderTextBracketed = fmap bracketed $ rendererOf "text" $ renderStringlike False++renderTextPretty :: Text -> Render ()+renderTextPretty = rendererOf "text pretty" $ tell . fromText++renderUnknownPropertyBracketed :: UnknownPropertyValue -> Render ()+renderUnknownPropertyBracketed =+  fmap bracketed $ rendererOf "unknown property" $ renderStringlike False++renderUnknownPropertyPretty :: UnknownPropertyValue -> Render ()+renderUnknownPropertyPretty =+  rendererOf "unknown property pretty" $ tell . fromUnknownPropertyValue++renderVariationModeBracketed :: VariationMode -> Render ()+renderVariationModeBracketed =+  fmap bracketed $ rendererOf "variation mode" $ tell . show . fromVariationMode++renderVariationModePretty :: VariationMode -> Render ()+renderVariationModePretty = rendererOf "variation mode pretty" $ \(VariationMode source markup) ->+  tell $+  (case source of+      ShowChildVariations -> "Children"+      ShowCurrentVariations -> "Current") ++ " " +++  (if markup then "Shown" else "Hidden")
+ src/Game/Goatee/Lib/Property/Value.hs view
@@ -0,0 +1,179 @@+-- This file is part of Goatee.+--+-- Copyright 2014 Bryan Gardiner+--+-- Goatee is free software: you can redistribute it and/or modify+-- it under the terms of the GNU Affero General Public License as published by+-- the Free Software Foundation, either version 3 of the License, or+-- (at your option) any later version.+--+-- Goatee is distributed in the hope that it will be useful,+-- but WITHOUT ANY WARRANTY; without even the implied warranty of+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the+-- GNU Affero General Public License for more details.+--+-- You should have received a copy of the GNU Affero General Public License+-- along with Goatee.  If not, see <http://www.gnu.org/licenses/>.++-- | Metadata about the types of property values.+--+-- Import "Game.Goatee.Lib.Property" rather than importing this module.+module Game.Goatee.Lib.Property.Value (+  PropertyValueType, pvtParser, pvtRenderer, pvtRendererPretty,+  colorPvt,+  coordElistPvt,+  coordListPvt,+  coordPairListPvt,+  doublePvt,+  gameResultPvt,+  integralPvt,+  labelListPvt,+  movePvt,+  nonePvt,+  realPvt,+  rulesetPvt,+  simpleTextPairPvt,+  simpleTextPvt,+  sizePvt,+  textPvt,+  unknownPropertyPvt,+  variationModePvt,+  ) where++import qualified Game.Goatee.Lib.Property.Parser as P+import qualified Game.Goatee.Lib.Property.Renderer as R+import Game.Goatee.Lib.Renderer+import Game.Goatee.Lib.Types+import Text.ParserCombinators.Parsec (Parser)++data PropertyValueType a = PropertyValueType+  { pvtParser :: Parser a+  , pvtRenderer :: a -> Render ()+  , pvtRendererPretty :: a -> Render ()+  }++colorPvt :: PropertyValueType Color+colorPvt = PropertyValueType+  { pvtParser = P.colorParser+  , pvtRenderer = R.renderColorBracketed+  , pvtRendererPretty = R.renderColorPretty+  }++coordElistPvt :: PropertyValueType CoordList+coordElistPvt = PropertyValueType+  { pvtParser = P.coordElistParser+  , pvtRenderer = R.renderCoordElistBracketed+  , pvtRendererPretty = R.renderCoordElistPretty+  }++coordListPvt :: PropertyValueType CoordList+coordListPvt = PropertyValueType+  { pvtParser = P.coordListParser+  , pvtRenderer = R.renderCoordListBracketed+  , pvtRendererPretty = R.renderCoordListPretty+  }++coordPairListPvt :: PropertyValueType [(Coord, Coord)]+coordPairListPvt = PropertyValueType+  { pvtParser = P.coordPairListParser+  , pvtRenderer = R.renderCoordPairListBracketed+  , pvtRendererPretty = R.renderCoordPairListPretty+  }++doublePvt :: PropertyValueType DoubleValue+doublePvt = PropertyValueType+  { pvtParser = P.doubleParser+  , pvtRenderer = R.renderDoubleBracketed+  , pvtRendererPretty = R.renderDoublePretty+  }++gameResultPvt :: PropertyValueType GameResult+gameResultPvt = PropertyValueType+  { pvtParser = P.gameResultParser+  , pvtRenderer = R.renderGameResultBracketed+  , pvtRendererPretty = R.renderGameResultPretty+  }++integralPvt :: (Integral a, Read a, Show a) => PropertyValueType a+integralPvt = PropertyValueType+  { pvtParser = P.integralParser+  , pvtRenderer = R.renderIntegralBracketed+  , pvtRendererPretty = R.renderIntegralPretty+  }++labelListPvt :: PropertyValueType [(Coord, SimpleText)]+labelListPvt = PropertyValueType+  { pvtParser = P.labelListParser+  , pvtRenderer = R.renderLabelListBracketed+  , pvtRendererPretty = R.renderLabelListPretty+  }++movePvt :: PropertyValueType (Maybe Coord)+movePvt = PropertyValueType+  { pvtParser = P.moveParser+  , pvtRenderer = R.renderMoveBracketed+  , pvtRendererPretty = R.renderMovePretty+  }++nonePvt :: PropertyValueType ()+nonePvt = PropertyValueType+  { pvtParser = P.noneParser+  , pvtRenderer = R.renderNoneBracketed+  , pvtRendererPretty = R.renderNonePretty+  }++realPvt :: PropertyValueType RealValue+realPvt = PropertyValueType+  { pvtParser = P.realParser+  , pvtRenderer = R.renderRealBracketed+  , pvtRendererPretty = R.renderRealPretty+  }++rulesetPvt :: PropertyValueType Ruleset+rulesetPvt = PropertyValueType+  { pvtParser = P.rulesetParser+  , pvtRenderer = R.renderRulesetBracketed+  , pvtRendererPretty = R.renderRulesetPretty+  }++simpleTextPairPvt :: PropertyValueType (SimpleText, SimpleText)+simpleTextPairPvt = PropertyValueType+  { pvtParser = P.simpleTextPairParser+  , pvtRenderer = R.renderSimpleTextPairBracketed+  , pvtRendererPretty = R.renderSimpleTextPairPretty+  }++simpleTextPvt :: PropertyValueType SimpleText+simpleTextPvt = PropertyValueType+  { pvtParser = P.simpleTextParser+  , pvtRenderer = R.renderSimpleTextBracketed+  , pvtRendererPretty = R.renderSimpleTextPretty+  }++sizePvt :: PropertyValueType (Int, Int)+sizePvt = PropertyValueType+  { pvtParser = P.sizeParser+  , pvtRenderer = R.renderSizeBracketed+  , pvtRendererPretty = R.renderSizePretty+  }++textPvt :: PropertyValueType Text+textPvt = PropertyValueType+  { pvtParser = P.textParser+  , pvtRenderer = R.renderTextBracketed+  , pvtRendererPretty = R.renderTextPretty+  }++unknownPropertyPvt :: PropertyValueType UnknownPropertyValue+unknownPropertyPvt = PropertyValueType+  { pvtParser = P.unknownPropertyParser+  , pvtRenderer = R.renderUnknownPropertyBracketed+  , pvtRendererPretty = R.renderUnknownPropertyPretty+  }++variationModePvt :: PropertyValueType VariationMode+variationModePvt = PropertyValueType+  { pvtParser = P.variationModeParser+  , pvtRenderer = R.renderVariationModeBracketed+  , pvtRendererPretty = R.renderVariationModePretty+  }
+ src/Game/Goatee/Lib/Renderer.hs view
@@ -0,0 +1,56 @@+-- This file is part of Goatee.+--+-- Copyright 2014 Bryan Gardiner+--+-- Goatee is free software: you can redistribute it and/or modify+-- it under the terms of the GNU Affero General Public License as published by+-- the Free Software Foundation, either version 3 of the License, or+-- (at your option) any later version.+--+-- Goatee is distributed in the hope that it will be useful,+-- but WITHOUT ANY WARRANTY; without even the implied warranty of+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the+-- GNU Affero General Public License for more details.+--+-- You should have received a copy of the GNU Affero General Public License+-- along with Goatee.  If not, see <http://www.gnu.org/licenses/>.++{-# LANGUAGE CPP #-}++-- | Common definitions for a renderer that supports failure.+module Game.Goatee.Lib.Renderer (+  Render,+  runRender,+  rendererOf,+  ) where++#if MIN_VERSION_mtl(2,2,1)+import Control.Monad.Except (Except, catchError, runExcept, throwError)+#else+import Control.Monad.Error (catchError, throwError)+#endif+import Control.Monad.Writer (WriterT, execWriterT)++-- | A monad for accumulating string output with the possibility of failure.+#if MIN_VERSION_mtl(2,2,1)+type Render = WriterT String (Except String)+#else+type Render = WriterT String (Either String)+#endif++-- | Returns either the rendered result on the right, or a message describing a+-- failure on the left.+runRender :: Render a -> Either String String+#if MIN_VERSION_mtl(2,2,1)+runRender = runExcept . execWriterT+#else+runRender = execWriterT+#endif++-- | Wraps a renderer in an exception handler that, when the renderer or+-- something it calls fails, will add context about this renderer's invocation+-- to the failure message.+rendererOf :: Show a => String -> (a -> Render ()) -> a -> Render ()+rendererOf description renderer value = catchError (renderer value) $ \message ->+  throwError $+  message ++ "\n    while trying to render " ++ description ++ " from " ++ show value ++ "."
+ src/Game/Goatee/Lib/Renderer/Tree.hs view
@@ -0,0 +1,60 @@+-- This file is part of Goatee.+--+-- Copyright 2014 Bryan Gardiner+--+-- Goatee is free software: you can redistribute it and/or modify+-- it under the terms of the GNU Affero General Public License as published by+-- the Free Software Foundation, either version 3 of the License, or+-- (at your option) any later version.+--+-- Goatee is distributed in the hope that it will be useful,+-- but WITHOUT ANY WARRANTY; without even the implied warranty of+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the+-- GNU Affero General Public License for more details.+--+-- You should have received a copy of the GNU Affero General Public License+-- along with Goatee.  If not, see <http://www.gnu.org/licenses/>.++-- | Functions for serializing SGF trees.+module Game.Goatee.Lib.Renderer.Tree (+  renderCollection,+  renderGameTree,+  renderProperty,+  ) where++import Control.Monad.Writer (tell)+import Game.Goatee.Common+import Game.Goatee.Lib.Property+import Game.Goatee.Lib.Renderer+import Game.Goatee.Lib.Tree++-- | Renders an SGF 'Collection' to a string.+renderCollection :: Collection -> Render ()+renderCollection collection = do+  mapM_ renderGameTree $ collectionTrees collection+  tell "\n"++-- | Recursively renders an SGF GameTree (as defined in the spec) rooted at the+-- given node.+renderGameTree :: Node -> Render ()+renderGameTree node = do+  tell "("+  doWhileM node+    (\node' -> do renderNode node'+                  case nodeChildren node' of+                    [] -> return $ Left Nothing+                    child:[] -> return $ Right child+                    children -> return $ Left $ Just children)+    >>= maybe (return ()) (mapM_ renderGameTree)+  tell ")"++-- | Renders a node and its properties without recurring to its children.+renderNode :: Node -> Render ()+renderNode node = do+  tell "\n;"+  mapM_ renderProperty $ nodeProperties node++renderProperty :: Property -> Render ()+renderProperty property = do+  tell $ propertyName property+  propertyValueRenderer property property
+ src/Game/Goatee/Lib/Tree.hs view
@@ -0,0 +1,188 @@+-- This file is part of Goatee.+--+-- Copyright 2014 Bryan Gardiner+--+-- Goatee is free software: you can redistribute it and/or modify+-- it under the terms of the GNU Affero General Public License as published by+-- the Free Software Foundation, either version 3 of the License, or+-- (at your option) any later version.+--+-- Goatee is distributed in the hope that it will be useful,+-- but WITHOUT ANY WARRANTY; without even the implied warranty of+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the+-- GNU Affero General Public License for more details.+--+-- You should have received a copy of the GNU Affero General Public License+-- along with Goatee.  If not, see <http://www.gnu.org/licenses/>.++-- | SGF data structures modelling the hierarchical game tree.+module Game.Goatee.Lib.Tree (+  Collection(..), CollectionWithDeepEquality(..),+  Node(..), NodeWithDeepEquality(..),+  emptyNode, rootNode,+  findProperty, findProperty', findPropertyValue, findPropertyValue',+  addProperty, addChild, addChildAt, deleteChildAt,+  validateNode,+  ) where++import Control.Applicative ((<$>))+import Control.Monad (forM_, unless, when)+import Control.Monad.Writer (Writer, execWriter, tell)+import Data.Function (on)+import Data.List (find, groupBy, intercalate, nub, sortBy)+import Data.Version (showVersion)+import Game.Goatee.App (applicationName)+import Game.Goatee.Common+import Game.Goatee.Lib.Property+import Game.Goatee.Lib.Types+import Paths_goatee (version)++-- | An SGF collection of game trees.+data Collection = Collection+  { collectionTrees :: [Node]+  } deriving (Show)++-- | See 'NodeWithDeepEquality'.+newtype CollectionWithDeepEquality = CollectionWithDeepEquality+  { collectionWithDeepEquality :: Collection+  } deriving (Show)++instance Eq CollectionWithDeepEquality where+  (==) = (==) `on` map NodeWithDeepEquality . collectionTrees . collectionWithDeepEquality++-- | An SGF game tree node.  Unlike in the SGF spec, we represent a game tree+-- with nodes uniformly, rather than having the separation between sequences and+-- nodes.+data Node = Node+  { nodeProperties :: [Property]+  , nodeChildren :: [Node]+  } deriving (Show)++-- | A wrapper around 'Node' with an 'Eq' instance that considers two nodes+-- equal iff they contain the same properties (not necessarily in the same+-- order), and if they contain children (in the same order) whose nodes are+-- recursively equal.+--+-- This instance is not on 'Node' directly because it is not the only obvious+-- sense of equality (only comparing properties would be another one), and it's+-- also potentially expensive.+newtype NodeWithDeepEquality = NodeWithDeepEquality { nodeWithDeepEquality :: Node }++instance Eq NodeWithDeepEquality where+  node1 == node2 =+    let n1 = nodeWithDeepEquality node1+        n2 = nodeWithDeepEquality node2+    in propertiesSorted n1 == propertiesSorted n2 &&+       deepChildren n1 == deepChildren n2+    where propertiesSorted = sortBy (compare `on` show) . nodeProperties+          deepChildren = map NodeWithDeepEquality . nodeChildren++-- | A node with no properties and no children.+emptyNode :: Node+emptyNode = Node { nodeProperties = [], nodeChildren = [] }++-- | Returns a fresh root 'Node' with 'AP' set to Goatee and optionally with a+-- board size set via 'SZ'.+rootNode :: Maybe (Int, Int) -> Node+rootNode maybeSize =+  let props = FF 4 :+              GM 1 :+              AP (toSimpleText applicationName)+                 (toSimpleText $ showVersion version) :+              maybe [] ((:[]) . uncurry SZ) maybeSize+  in Node { nodeProperties = props+          , nodeChildren = []+          }++-- | Searches for a matching property in a node's property list.+findProperty :: Descriptor a => a -> Node -> Maybe Property+findProperty descriptor node = findProperty' descriptor $ nodeProperties node++-- | Searches for a matching property in a property list.+findProperty' :: Descriptor a => a -> [Property] -> Maybe Property+findProperty' = find . propertyPredicate++-- | Retrieves the value of a property in a node's property list.+findPropertyValue :: ValuedDescriptor a v => a -> Node -> Maybe v+findPropertyValue descriptor node = propertyValue descriptor <$> findProperty descriptor node++-- | Retrieves the value of a property in a property list.+findPropertyValue' :: ValuedDescriptor a v => a -> [Property] -> Maybe v+findPropertyValue' descriptor properties =+  propertyValue descriptor <$> findProperty' descriptor properties++-- | Appends a property to a node's property list.+addProperty :: Property -> Node -> Node+addProperty prop node = node { nodeProperties = nodeProperties node ++ [prop] }++-- | @addChild child parent@ appends a child node to a node's child list.+addChild :: Node -> Node -> Node+addChild child node = node { nodeChildren = nodeChildren node ++ [child] }++-- | @addChildAt index child parent@ inserts a child node into a node's child+-- list at the given index, shifting all nodes at or after the given index to+-- the right.  If the position is less than 0 or greater than the length of the+-- list, then the index is clamped to this range.+addChildAt :: Int -> Node -> Node -> Node+addChildAt index child node =+  let (before, after) = splitAt index $ nodeChildren node+  in node { nodeChildren = before ++ child:after }++-- | @deleteChildAt index node@ deletes the child at the given index from the+-- node.  If the index is invalid, @node@ is returned.+deleteChildAt :: Int -> Node -> Node+deleteChildAt index node =+  let children = nodeChildren node+  in if index < 0 || index > length children+     then node+     else node { nodeChildren = listDeleteAt index children }++-- | Returns a list of validation errors for the current node, an+-- empty list if no errors are detected.+validateNode :: Bool -> Bool -> Node -> [String]+validateNode isRoot _{-seenGameNode-} node = execWriter $ do+  let props = nodeProperties node+  let propTypes = nub $ map propertyType $ nodeProperties node++  -- Check for move and setup properties in a single node.+  when (MoveProperty `elem` propTypes && SetupProperty `elem` propTypes) $+    tell ["Node contains move and setup properties."]++  -- Check for root properties in non-root nodes.+  let rootProps = filter ((RootProperty ==) . propertyType) props+  when (not isRoot && not (null rootProps)) $+    tell $ map (\p -> "Root property found on non-root node: " +++                      show p ++ ".")+           rootProps++  -- TODO Check for game-info properties.++  -- Check for coordinates marked multiple times.+  validateNodeDuplicates props getMarkedCoords $ \group ->+    tell ["Coordinate " ++ show (fst $ head group) +++          " is specified multiple times in properties " +++          intercalate ", " (map snd group) ++ "."]++  -- TODO Validate recursively.++  where getMarkedCoords (CR cs) = tagMarkedCoords cs "CR"+        getMarkedCoords (MA cs) = tagMarkedCoords cs "MA"+        getMarkedCoords (SL cs) = tagMarkedCoords cs "SL"+        getMarkedCoords (SQ cs) = tagMarkedCoords cs "SQ"+        getMarkedCoords (TR cs) = tagMarkedCoords cs "TR"+        getMarkedCoords _ = []++        tagMarkedCoords cs tag = map (\x -> (x, tag)) $ expandCoordList cs++validateNodeDuplicates :: (Eq v, Ord v)+                          => [Property]+                          -> (Property -> [(v, t)])+                          -> ([(v, t)] -> Writer [String] ())+                          -> Writer [String] ()+validateNodeDuplicates props getTaggedElts errAction =+  let groups = groupBy ((==) `on` fst) $+               sortBy (compare `on` fst) $+               concatMap getTaggedElts props+  in forM_ groups $ \group ->+       unless (null $ tail group) $+         errAction group
+ src/Game/Goatee/Lib/Types.hs view
@@ -0,0 +1,505 @@+-- This file is part of Goatee.+--+-- Copyright 2014 Bryan Gardiner+--+-- Goatee is free software: you can redistribute it and/or modify+-- it under the terms of the GNU Affero General Public License as published by+-- the Free Software Foundation, either version 3 of the License, or+-- (at your option) any later version.+--+-- Goatee is distributed in the hope that it will be useful,+-- but WITHOUT ANY WARRANTY; without even the implied warranty of+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the+-- GNU Affero General Public License for more details.+--+-- You should have received a copy of the GNU Affero General Public License+-- along with Goatee.  If not, see <http://www.gnu.org/licenses/>.++-- | Constants and data types for property values used in SGF game trees.+module Game.Goatee.Lib.Types (+  -- * Constants+  supportedFormatVersions, defaultFormatVersion, supportedGameTypes,+  boardSizeDefault, boardSizeMin, boardSizeMax,+  -- * Board coordinates+  Coord, CoordList, coordListSingles, coordListRects, coord1, coords, coords',+  emptyCoordList, expandCoordList, buildCoordList,+  -- ** Star points and handicap stones+  starLines,+  isStarPoint,+  handicapStones,+  -- * Property values+  -- ** Text values+  Stringlike (..), convertStringlike,+  Text, fromText, toText,+  SimpleText, fromSimpleText, toSimpleText,+  -- ** Other values+  UnknownPropertyValue, fromUnknownPropertyValue, toUnknownPropertyValue,+  RealValue,+  DoubleValue (..),+  Color (..), cnot,+  VariationMode (..), VariationModeSource (..), defaultVariationMode,+  toVariationMode, fromVariationMode,+  ArrowList, LineList, LabelList, Mark (..),+  GameResult (..),+  WinReason (..),+  Ruleset (..), RulesetType (..), fromRuleset, toRuleset,+  ) where++import Control.Applicative ((<$>), (<*>))+import Data.Char (isSpace)+import Data.Function (on)+import Data.List (delete, groupBy, partition, sort)+import Data.Maybe (fromMaybe)+import Game.Goatee.Common+import qualified Game.Goatee.Common.Bigfloat as BF++-- | The FF versions supported by Goatee.  Currently only 4.+supportedFormatVersions :: [Int]+supportedFormatVersions = [4]+-- TODO Support FF versions 1-4.++-- | The default SGF version to use when @FF[]@ is not specified in a root node.+--+-- This value is actually INCORRECT: SGF defines it to be 1, but because we+-- don't support version 1 yet, for the sake of ignoring this issue (for now!)+-- in tests, we fix the default to be 4.+defaultFormatVersion :: Int+defaultFormatVersion = 4+-- TODO Fix the default version to be 1 as SGF mandates.++-- | SGF supports multiple game types.  This list contains the game types that+-- Goatee supports, which is only Go (1).+supportedGameTypes :: [Int]+supportedGameTypes = [1 {- Go -}]++-- | The default size of the board.  The FF[4] SGF spec says that the default Go+-- board is 19x19 square.+boardSizeDefault :: Int+boardSizeDefault = 19++-- | The minimum board size allowed by FF[4], 1.+boardSizeMin :: Int+boardSizeMin = 1++-- | The maximum board size allowed by FF[4], 52.+boardSizeMax :: Int+boardSizeMax = 52++-- | A coordinate on a Go board.  @(0, 0)@ refers to the upper-left corner of+-- the board.  The first component is the horizontal position; the second+-- component is the vertical position.+type Coord = (Int, Int)++-- | A structure for compact representation of a list of coordinates.  Contains+-- a list of individual points, as well as a list of rectangles of points+-- denoted by an ordered pair of the upper-left point and the lower-right point.+-- The union of the single points and points contained within rectangles make up+-- all of the points a @CoordList@ represents.  There is no rule saying that+-- adjacent points have to be grouped into rectangles; it's perfectly valid+-- (although possibly inefficient) to never use rectangles.+--+-- For any @CoordList@, all of the following hold:+--+-- 1. Any point may be specified at most once, either in the singles list or in+-- a single rectangle.+--+-- 2. For a rectangle @((x0,y0), (x1,y1))@, @x0 <= x1@ and @y0 <= y1@ and+-- @(x0,y0) /= (x1,y1)@ (otherwise the point belongs in the singles list).+data CoordList = CoordList+  { coordListSingles :: [Coord]+  -- ^ Returns the single points in a 'CoordList'.+  , coordListRects :: [(Coord, Coord)]+    -- ^ Returns the rectangles in a 'CoordList'.+  } deriving (Show)++-- | Equality is based on unordered, set equality of the underlying points.+instance Eq CoordList where+  (==) = (==) `on` sort . expandCoordList++-- | Constructs a 'CoordList' containing a single point.+coord1 :: Coord -> CoordList+coord1 xy = CoordList { coordListSingles = [xy]+                      , coordListRects = []+                      }++-- | Constructs a 'CoordList' containing the given single points.  For rectangle+-- detection, use 'buildCoordList'.+coords :: [Coord] -> CoordList+coords singles = CoordList { coordListSingles = singles+                           , coordListRects = []+                           }++-- | Constructs a 'CoordList' containing the given single points and rectangles.+coords' :: [Coord] -> [(Coord, Coord)] -> CoordList+coords' singles rects = CoordList { coordListSingles = singles+                                  , coordListRects = rects+                                  }++-- | A 'CoordList' that contains no points.+emptyCoordList :: CoordList+emptyCoordList = CoordList { coordListSingles = []+                           , coordListRects = []+                           }++-- | Converts a compact 'CoordList' to a list of coordinates.+expandCoordList :: CoordList -> [Coord]+expandCoordList cl = coordListSingles cl +++                     foldr (\r@((x0, y0), (x1, y1)) rest ->+                             if x0 > x1 || y0 > y1+                             then error ("Invalid coord. rectangle: " ++ show r)+                             else [(x, y) | x <- [x0..x1], y <- [y0..y1]] ++ rest)+                           []+                           (coordListRects cl)++-- | Constructs a 'CoordList' from a list of 'Coord's, doing some+-- not-completely-stupid rectangle detection.  The order of data in the result+-- is unspecified.+buildCoordList :: [Coord] -> CoordList+-- This algorithm doesn't generate the smallest result.  For the following+-- input:+--+-- F F T T+-- T T T T+-- T T F F+--+-- It will return [ca:da][ab:db][ac:bc] rather than the shorter [ca:db][ab:bc].+buildCoordList = toCoordList . generateRects 0 [] . buildTruePairs . toGrid+  where -- | Constructs a row-major grid of booleans where an coordinate is true+        -- iff it is in the given list.+        toGrid :: [Coord] -> [[Bool]]+        toGrid [] = []+        toGrid coords = let x1 = maximum $ map fst coords+                            y1 = maximum $ map snd coords+                        in [[(x,y) `elem` coords | x <- [0..x1]] | y <- [0..y1]]++        -- | For each row, converts a list of booleans into a list of pairs+        -- where each pair represents a consecutive run of true values.  The+        -- pair indicates the indices of the first and last boolean in each run.+        buildTruePairs :: [[Bool]] -> [[(Int, Int)]]+        buildTruePairs = map $+                         concatMap extractTrueGroups .+                         groupBy ((==) `on` snd) .+                         zip [0..]++        -- | Given a run of indexed booleans with the same boolean value within+        -- a row, returns @[(startIndex, endIndex)]@ if the value is true, and+        -- @[]@ if the value is false.+        extractTrueGroups :: [(Int, Bool)] -> [(Int, Int)]+        extractTrueGroups list@((start, True):_) = [(start, fst (last list))]+        extractTrueGroups _ = []++        -- | Converts the lists of true pairs for all rows into a list of+        -- @(Coord, Coord)@ rectangles.  We repeatedly grab the first span in+        -- the first row, and see how many leading rows contain that exact span.+        -- Then we build a (maybe multi-row) rectangle for the span, remove the+        -- span from all leading rows, and repeat.  When the first row becomes+        -- empty, we drop it and increment a counter that keeps track of our+        -- first row's y-position.+        generateRects :: Int -> [(Coord, Coord)] -> [[(Int, Int)]] -> [(Coord, Coord)]+        generateRects _ acc [] = acc+        generateRects topRowOffset acc ([]:rows) = generateRects (topRowOffset + 1) acc rows+        generateRects topRowOffset acc rows@((span:_):_) =+          let rowsWithSpan = matchRowsWithSpan span rows+              rowsWithSpanCount = length rowsWithSpan+          in generateRects topRowOffset+                           (((fst span, topRowOffset),+                             (snd span, topRowOffset + rowsWithSpanCount - 1)) : acc)+                           (rowsWithSpan ++ drop rowsWithSpanCount rows)++        -- | Determines how many leading rows contain the given span.  A list of+        -- all the matching rows is returned, with the span deleted from each.+        matchRowsWithSpan :: (Int, Int) -> [[(Int, Int)]] -> [[(Int, Int)]]+        matchRowsWithSpan span (row:rows)+          | span `elem` row = delete span row : matchRowsWithSpan span rows+          | otherwise = []+        matchRowsWithSpan _ [] = []++        -- | Builds a 'CoordList' from simple @(Coord, Coord)@ rectangles.+        toCoordList :: [(Coord, Coord)] -> CoordList+        toCoordList rects =+          let (singles, properRects) = partition (uncurry (==)) rects+          in coords' (map fst singles) properRects++-- | @starLines width height@ returns 'Just' a list of row/column indices that+-- have star points on a board of the given size, or 'Nothing' if the board size+-- does not have star points defined.+starLines :: Int -> Int -> Maybe [Int]+starLines 19 19 = Just [3, 9, 15]+starLines 13 13 = Just [3, 6, 9]+starLines 9 9 = Just [2, 4, 6]+starLines _ _ = Nothing++-- | @isStarPoint width height x y@ determines whether @(x, y)@ is a known star+-- point on a board of the given width and height.+isStarPoint :: Int -> Int -> Int -> Int -> Bool+isStarPoint width height x y =+  fromMaybe False $+  ((&&) <$> elem x <*> elem y) <$> starLines width height++-- | @handicapStoneIndices !! k@ the positions of the handicap stones for @k@+-- handicap stones, betweeen 0 and 9.  In the pairs, 0 indicates the first star+-- point along an axis, 1 indicates the second, and 2 indicates the third, in+-- the normal 'Coord' ordering.+handicapStoneIndices :: [[(Int, Int)]]+handicapStoneIndices =+  [ []+  , []  -- No single handicap stone for Black; black goes first instead.+  , [(2,0), (0,2)]+  , (2,2) : handicapStoneIndices !! 2+  , (0,0) : handicapStoneIndices !! 3+  , (1,1) : handicapStoneIndices !! 4+  , (0,1) : (2,1) : handicapStoneIndices !! 4+  , (1,1) : handicapStoneIndices !! 6+  , (1,0) : (1,2) : handicapStoneIndices !! 6+  , (1,1) : handicapStoneIndices !! 8+  ]++-- | @handicapStones width height handicap@ returns a list of points where+-- handicap stones should be placed for the given handicap, if handicap points+-- are defined for the given board size, otherwise 'Nothing'.+handicapStones :: Int -> Int -> Int -> Maybe [Coord]+handicapStones width height handicap =+  if handicap < 0 || handicap >= length handicapStoneIndices+  then Nothing+  else do positions <- starLines width height+          return $ map (mapTuple (positions !!)) (handicapStoneIndices !! handicap)++-- | A class for SGF data types that are coercable to and from strings.+--+-- The construction of an SGF value with 'stringToSgf' may process the input,+-- such that the resulting stringlike value does not represent the same string+-- as the input.  In other words, the following does *not* necessarily hold:+--+-- > sgfToString . stringToSgf = id   (does not necessarily hold!)+--+-- The following does hold, however, for a single stringlike type:+--+-- > stringToSgf . sgfToString = id+--+-- The 'String' instance is defined with @sgfToString = stringToSgf =+-- id@.  For other types, the string returned by 'sgfToString' is in a+-- raw, user-editable format: characters that need to be escaped in+-- serialized SGF aren't escaped, but the returned value is otherwise+-- similar to SGF format.+class Stringlike a where+  -- | Extracts the string value from an SGF value.+  sgfToString :: a -> String++  -- | Creates an SGF value from a string value.+  stringToSgf :: String -> a++instance Stringlike String where+  sgfToString = id+  stringToSgf = id++-- | Converts between 'Stringlike' types via a string.+--+-- > convertStringlike = stringToSgf . sgfToString+convertStringlike :: (Stringlike a, Stringlike b) => a -> b+convertStringlike = stringToSgf . sgfToString++-- | An SGF text value.+newtype Text = Text+  { fromText :: String+    -- ^ Converts an SGF 'Text' to a string.+  } deriving (Eq, Show)++instance Stringlike Text where+  sgfToString = fromText+  stringToSgf = toText++-- | Converts a string to an SGF 'Text'.+toText :: String -> Text+toText = Text++-- | An SGF SimpleText value.+newtype SimpleText = SimpleText+  { fromSimpleText :: String+    -- ^ Converts an SGF 'SimpleText' to a string.+  } deriving (Eq, Show)++instance Stringlike SimpleText where+  sgfToString = fromSimpleText+  stringToSgf = toSimpleText++sanitizeSimpleText :: String -> String+sanitizeSimpleText = map (\c -> if isSpace c then ' ' else c)++-- | Converts a string to an SGF 'SimpleText', replacing all whitespaces+-- (including newlines) with spaces.+toSimpleText :: String -> SimpleText+toSimpleText = SimpleText . sanitizeSimpleText++-- | The value type for an 'UnknownProperty'.  Currently represented as a+-- string.+data UnknownPropertyValue = UnknownPropertyValue+  { fromUnknownPropertyValue :: String+    -- ^ Returns the string contained within the 'UnknownProperty' this value is+    -- from.+  } deriving (Eq, Show)++instance Stringlike UnknownPropertyValue where+  sgfToString = fromUnknownPropertyValue+  stringToSgf = toUnknownPropertyValue++-- | Constructs a value for a 'UnknownProperty'.+toUnknownPropertyValue :: String -> UnknownPropertyValue+toUnknownPropertyValue = UnknownPropertyValue++-- | An SGF real value is a decimal number of unspecified precision.+type RealValue = BF.Bigfloat++-- | An SGF double value: either 1 or 2, nothing else.+data DoubleValue = Double1+                 | Double2+                 deriving (Eq, Show)++-- | Stone color: black or white.+data Color = Black+           | White+           deriving (Eq, Show)++-- | Returns the logical negation of a stone color, yang for yin and+-- yin for yang.+cnot :: Color -> Color+cnot Black = White+cnot White = Black++-- | SGF flags that control how move variations are to be presented while+-- displaying the game.+data VariationMode = VariationMode+  { variationModeSource :: VariationModeSource+    -- ^ Which moves to display as variations.+  , variationModeBoardMarkup :: Bool+    -- ^ Whether to overlay variations on the board.+  } deriving (Eq, Show)++-- | An enumeration that describes which variations are shown.+data VariationModeSource =+  ShowChildVariations+  -- ^ Show children of the current move.+  | ShowCurrentVariations+    -- ^ Show alternatives to the current move.+  deriving (Bounded, Enum, Eq, Show)++-- | The default variation mode as defined by the SGF spec is @VariationMode+-- ShowChildVariations True@.+defaultVariationMode :: VariationMode+defaultVariationMode = VariationMode ShowChildVariations True++-- | Parses a numeric variation mode, returning nothing if the number is+-- invalid.+toVariationMode :: Int -> Maybe VariationMode+toVariationMode n = case n of+  0 -> Just $ VariationMode ShowChildVariations True+  1 -> Just $ VariationMode ShowCurrentVariations True+  2 -> Just $ VariationMode ShowChildVariations False+  3 -> Just $ VariationMode ShowCurrentVariations False+  _ -> Nothing++-- | Returns the integer value for a variation mode.+fromVariationMode :: VariationMode -> Int+fromVariationMode mode = case mode of+  VariationMode ShowChildVariations True -> 0+  VariationMode ShowCurrentVariations True -> 1+  VariationMode ShowChildVariations False -> 2+  VariationMode ShowCurrentVariations False -> 3++-- | A list of arrows, each specified as @(startCoord, endCoord)@.+type ArrowList = [(Coord, Coord)]++-- | A list of lines, each specified as @(startCoord, endCoord)@.+type LineList = [(Coord, Coord)]++-- | A list of labels, each specified with a string and a coordinate about which+-- to center the string.+type LabelList = [(Coord, SimpleText)]++-- | The markings that SGF supports annotating coordinates with.+data Mark = MarkCircle | MarkSquare | MarkTriangle | MarkX | MarkSelected+          deriving (Bounded, Enum, Eq, Show)++data GameResult = GameResultWin Color WinReason+                | GameResultDraw+                | GameResultVoid+                | GameResultUnknown+                | GameResultOther SimpleText+                deriving (Eq, Show)++instance Stringlike GameResult where+  sgfToString result = case result of+    GameResultWin color reason ->+      (case color of { Black -> 'B'; White -> 'W' }) : '+' :+      (case reason of+          WinByScore diff -> show diff+          WinByResignation -> "R"+          WinByTime -> "T"+          WinByForfeit -> "F")+    GameResultDraw -> "0"+    GameResultVoid -> "Void"+    GameResultUnknown -> "?"+    GameResultOther text -> sgfToString text++  stringToSgf str = case str of+    "0" -> GameResultDraw+    "Draw" -> GameResultDraw+    "Void" -> GameResultVoid+    "?" -> GameResultUnknown+    _ ->+      let result = case str of+            'B':'+':winReasonStr -> parseWin (GameResultWin Black) winReasonStr+            'W':'+':winReasonStr -> parseWin (GameResultWin White) winReasonStr+            _ -> unknownResult+          parseWin builder winReasonStr = case winReasonStr of+            "R" -> builder WinByResignation+            "Resign" -> builder WinByResignation+            "T" -> builder WinByTime+            "Time" -> builder WinByTime+            "F" -> builder WinByForfeit+            "Forfeit" -> builder WinByForfeit+            _ -> case reads winReasonStr of+              (score, ""):_ -> builder $ WinByScore score+              _ -> unknownResult+          unknownResult = GameResultOther $ toSimpleText str+      in result++data WinReason = WinByScore RealValue+               | WinByResignation+               | WinByTime+               | WinByForfeit+               deriving (Eq, Show)++-- | A ruleset used for a Go game.  Can be one of the rulesets defined by the+-- SGF specification, or a custom string.+data Ruleset = KnownRuleset RulesetType+             | UnknownRuleset String+             deriving (Eq, Show)++instance Stringlike Ruleset where+  sgfToString = fromRuleset+  stringToSgf = toRuleset++-- | The rulesets defined by the SGF specification, for use with 'Ruleset'.+data RulesetType = RulesetAga+                 | RulesetIng+                 | RulesetJapanese+                 | RulesetNewZealand+                 deriving (Bounded, Enum, Eq, Show)++-- | Returns the string representation for a ruleset.+fromRuleset :: Ruleset -> String+fromRuleset ruleset = case ruleset of+  KnownRuleset RulesetAga -> "AGA"+  KnownRuleset RulesetIng -> "Goe"+  KnownRuleset RulesetJapanese -> "Japanese"+  KnownRuleset RulesetNewZealand -> "NZ"+  UnknownRuleset str -> str++-- | Parses a string representation of a ruleset.+toRuleset :: String -> Ruleset+toRuleset str = case str of+  "AGA" -> KnownRuleset RulesetAga+  "Goe" -> KnownRuleset RulesetIng+  "Japanese" -> KnownRuleset RulesetJapanese+  "NZ" -> KnownRuleset RulesetNewZealand+  _ -> UnknownRuleset str
− src/Game/Goatee/Sgf/Board.hs
@@ -1,792 +0,0 @@--- This file is part of Goatee.------ Copyright 2014 Bryan Gardiner------ Goatee is free software: you can redistribute it and/or modify--- it under the terms of the GNU Affero General Public License as published by--- the Free Software Foundation, either version 3 of the License, or--- (at your option) any later version.------ Goatee is distributed in the hope that it will be useful,--- but WITHOUT ANY WARRANTY; without even the implied warranty of--- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the--- GNU Affero General Public License for more details.------ You should have received a copy of the GNU Affero General Public License--- along with Goatee.  If not, see <http://www.gnu.org/licenses/>.---- | Data structures that wrap and provide a higher-level interface to the SGF--- game tree, including a zipper that navigates the tree and provides the--- current board state.-module Game.Goatee.Sgf.Board (-  RootInfo(..), GameInfo(..), emptyGameInfo, internalIsGameInfoNode,-  gameInfoToProperties,-  BoardState(..), boardWidth, boardHeight,-  CoordState(..), rootBoardState, boardCoordState, mapBoardCoords,-  isValidMove, isCurrentValidMove,-  Cursor(..), rootCursor, cursorRoot, cursorChild, cursorChildren,-  cursorChildCount, cursorChildPlayingAt, cursorProperties,-  cursorModifyNode,-  cursorVariations,-  colorToMove,-  ) where--import Control.Monad (unless, when)-import Control.Monad.Writer (execWriter, tell)-import Data.List (find, intercalate, nub)-import Data.Maybe (fromMaybe, isJust, isNothing)-import qualified Data.Set as Set-import Game.Goatee.Common-import Game.Goatee.Sgf.Property-import Game.Goatee.Sgf.Tree-import Game.Goatee.Sgf.Types---- TODO Stop using errors everywhere, they're not testable.---- | Properties that are specified in the root nodes of game trees.-data RootInfo = RootInfo { rootInfoWidth :: Int-                         , rootInfoHeight :: Int-                         , rootInfoVariationMode :: VariationMode-                         } deriving (Eq, Show)---- | Properties that are specified in game info nodes.-data GameInfo = GameInfo { gameInfoRootInfo :: RootInfo--                         , gameInfoBlackName :: Maybe String-                         , gameInfoBlackTeamName :: Maybe String-                         , gameInfoBlackRank :: Maybe String--                         , gameInfoWhiteName :: Maybe String-                         , gameInfoWhiteTeamName :: Maybe String-                         , gameInfoWhiteRank :: Maybe String--                         , gameInfoRuleset :: Maybe Ruleset-                         , gameInfoBasicTimeSeconds :: Maybe Rational-                         , gameInfoOvertime :: Maybe String-                         , gameInfoResult :: Maybe GameResult--                         , gameInfoGameName :: Maybe String-                         , gameInfoGameComment :: Maybe String-                         , gameInfoOpeningComment :: Maybe String--                         , gameInfoEvent :: Maybe String-                         , gameInfoRound :: Maybe String-                         , gameInfoPlace :: Maybe String-                         , gameInfoDatesPlayed :: Maybe String-                         , gameInfoSource :: Maybe String-                         , gameInfoCopyright :: Maybe String--                         , gameInfoAnnotatorName :: Maybe String-                         , gameInfoEntererName :: Maybe String-                         } deriving (Show)---- | Builds a 'GameInfo' with the given 'RootInfo' and no extra data.-emptyGameInfo :: RootInfo -> GameInfo-emptyGameInfo rootInfo =-  GameInfo { gameInfoRootInfo = rootInfo--           , gameInfoBlackName = Nothing-           , gameInfoBlackTeamName = Nothing-           , gameInfoBlackRank = Nothing--           , gameInfoWhiteName = Nothing-           , gameInfoWhiteTeamName = Nothing-           , gameInfoWhiteRank = Nothing--           , gameInfoRuleset = Nothing-           , gameInfoBasicTimeSeconds = Nothing-           , gameInfoOvertime = Nothing-           , gameInfoResult = Nothing--           , gameInfoGameName = Nothing-           , gameInfoGameComment = Nothing-           , gameInfoOpeningComment = Nothing--           , gameInfoEvent = Nothing-           , gameInfoRound = Nothing-           , gameInfoPlace = Nothing-           , gameInfoDatesPlayed = Nothing-           , gameInfoSource = Nothing-           , gameInfoCopyright = Nothing--           , gameInfoAnnotatorName = Nothing-           , gameInfoEntererName = Nothing-           }---- | Returns whether a node contains any game info properties.-internalIsGameInfoNode :: Node -> Bool-internalIsGameInfoNode = any ((GameInfoProperty ==) . propertyType) . nodeProperties---- | Converts a 'GameInfo' into a list of 'Property's that can be used to--- reconstruct the 'GameInfo'.-gameInfoToProperties :: GameInfo -> [Property]-gameInfoToProperties info = execWriter $ do-  copy (PB . toSimpleText) gameInfoBlackName-  copy (BT . toSimpleText) gameInfoBlackTeamName-  copy (BR . toSimpleText) gameInfoBlackRank--  copy (PW . toSimpleText) gameInfoWhiteName-  copy (WT . toSimpleText) gameInfoWhiteTeamName-  copy (WR . toSimpleText) gameInfoWhiteRank--  copy RU gameInfoRuleset-  copy TM gameInfoBasicTimeSeconds-  copy (OT . toSimpleText) gameInfoOvertime-  copy RE gameInfoResult--  copy (GN . toSimpleText) gameInfoGameName-  copy (GC . toSimpleText) gameInfoGameComment-  copy (ON . toSimpleText) gameInfoOpeningComment--  copy (EV . toSimpleText) gameInfoEvent-  copy (RO . toSimpleText) gameInfoRound-  copy (PC . toSimpleText) gameInfoPlace-  copy (DT . toSimpleText) gameInfoDatesPlayed-  copy (SO . toSimpleText) gameInfoSource-  copy (CP . toSimpleText) gameInfoCopyright--  copy (AN . toSimpleText) gameInfoAnnotatorName-  copy (US . toSimpleText) gameInfoEntererName-  where copy ctor accessor = whenMaybe (accessor info) $ \x -> tell [ctor x]---- | An object that corresponds to a node in some game tree, and represents the--- state of the game at that node, including board position, player turn and--- captures, and also board annotations.-data BoardState = BoardState {-  boardCoordStates :: [[CoordState]]-  -- ^ The state of individual points on the board.  Stored in row-major order.-  -- Point @(x, y)@ can be accessed via @!! y !! x@ (but prefer-  -- 'boardCoordState').-  , boardHasInvisible :: Bool-    -- ^ Whether any of the board's 'CoordState's are invisible.  This is an-    -- optimization to make it more efficient to set the board to "all visible."-  , boardHasDimmed :: Bool-    -- ^ Whether any of the board's 'CoordState's are dimmed.  This is an-    -- optimization to make it more efficient to clear all dimming from the-    -- board.-  , boardArrows :: ArrowList-  , boardLines :: LineList-  , boardLabels :: LabelList-  , boardMoveNumber :: Integer-  , boardPlayerTurn :: Color-  , boardBlackCaptures :: Int-  , boardWhiteCaptures :: Int-  , boardGameInfo :: GameInfo-  }--instance Show BoardState where-  show board = concat $ execWriter $ do-    tell ["Board: (Move ", show (boardMoveNumber board),-          ", ", show (boardPlayerTurn board), "'s turn, B:",-          show (boardBlackCaptures board), ", W:",-          show (boardWhiteCaptures board), ")\n"]-    tell [intercalate "\n" $ flip map (boardCoordStates board) $-          \row -> unwords $ map show row]--    let arrows = boardArrows board-    let lines = boardLines board-    let labels = boardLabels board-    unless (null arrows) $ tell ["\nArrows: ", show arrows]-    unless (null lines) $ tell ["\nLines: ", show lines]-    unless (null labels) $ tell ["\nLabels: ", show labels]---- | Returns the width of the board, in stones.-boardWidth :: BoardState -> Int-boardWidth = rootInfoWidth . gameInfoRootInfo . boardGameInfo---- | Returns the height of the board, in stones.-boardHeight :: BoardState -> Int-boardHeight = rootInfoHeight . gameInfoRootInfo . boardGameInfo---- | Used by 'BoardState' to represent the state of a single point on the board.--- Records whether a stone is present, as well as annotations and visibility--- properties.-data CoordState = CoordState { coordStar :: Bool-                               -- ^ Whether this point is a star point.-                             , coordStone :: Maybe Color-                             , coordMark :: Maybe Mark-                             , coordVisible :: Bool-                             , coordDimmed :: Bool-                             }--instance Show CoordState where-  show c = if not $ coordVisible c-           then "--"-           else let stoneChar = case coordStone c of-                      Nothing -> if coordStar c then '*' else '\''-                      Just Black -> 'X'-                      Just White -> 'O'-                    markChar = case coordMark c of-                      Nothing -> ' '-                      Just MarkCircle -> 'o'-                      Just MarkSquare -> 's'-                      Just MarkTriangle -> 'v'-                      Just MarkX -> 'x'-                      Just MarkSelected -> '!'-                in [stoneChar, markChar]---- | Creates a 'BoardState' for an empty board of the given width and height.-emptyBoardState :: Int -> Int -> BoardState-emptyBoardState width height =-  BoardState { boardCoordStates = coords-             , boardHasInvisible = False-             , boardHasDimmed = False-             , boardArrows = []-             , boardLines = []-             , boardLabels = []-             , boardMoveNumber = 0-             , boardPlayerTurn = Black-             , boardBlackCaptures = 0-             , boardWhiteCaptures = 0-             , boardGameInfo = emptyGameInfo rootInfo-             }-  where rootInfo = RootInfo { rootInfoWidth = width-                            , rootInfoHeight = height-                            , rootInfoVariationMode = defaultVariationMode-                            }-        emptyCoord = CoordState { coordStar = False-                                , coordStone = Nothing-                                , coordMark = Nothing-                                , coordVisible = True-                                , coordDimmed = False-                                }-        starCoord = emptyCoord { coordStar = True }-        isStarPoint' = isStarPoint width height-        coords = map (\y -> map (\x -> if isStarPoint' x y then starCoord else emptyCoord)-                                [0..width-1])-                     [0..height-1]--rootBoardState :: Node -> BoardState-rootBoardState rootNode =-  foldr applyProperty-        (emptyBoardState width height)-        (nodeProperties rootNode)-  where SZ width height = fromMaybe (SZ boardSizeDefault boardSizeDefault) $-                          findProperty propertySZ rootNode---- | Returns the 'CoordState' for a coordinate on a board.-boardCoordState :: Coord -> BoardState -> CoordState-boardCoordState (x, y) board = boardCoordStates board !! y !! x---- | Maps a function over each 'CoordState' in a 'BoardState', returning a--- list-of-lists with the function's values.  The function is called like @fn y--- x coordState@.-mapBoardCoords :: (Int -> Int -> CoordState -> a) -> BoardState -> [[a]]-mapBoardCoords fn board =-  zipWith applyRow [0..] $ boardCoordStates board-  where applyRow y = zipWith (fn y) [0..]---- | Applies a function to update the 'RootInfo' within the 'GameInfo' of a--- 'BoardState'.-updateRootInfo :: (RootInfo -> RootInfo) -> BoardState -> BoardState-updateRootInfo fn board = flip updateBoardInfo board $ \gameInfo ->-  gameInfo { gameInfoRootInfo = fn $ gameInfoRootInfo gameInfo }---- | Applies a function to update the 'GameInfo' of a 'BoardState'.-updateBoardInfo :: (GameInfo -> GameInfo) -> BoardState -> BoardState-updateBoardInfo fn board = board { boardGameInfo = fn $ boardGameInfo board }---- | Performs necessary updates to a 'BoardState' between nodes in the tree.--- Clears marks.-boardChild :: BoardState -> BoardState-boardChild board =-  board { boardCoordStates = map (map clearMark) $ boardCoordStates board-        , boardArrows = []-        , boardLines = []-        , boardLabels = []-        }-  where clearMark coord = case coordMark coord of-          Nothing -> coord-          Just _ -> coord { coordMark = Nothing }---- | Sets all points on a board to be visible (if given true) or invisible (if--- given false).-setBoardVisible :: Bool -> BoardState -> BoardState-setBoardVisible visible board =-  if visible-  then if boardHasInvisible board-       then board { boardCoordStates = map (map $ setVisible True) $ boardCoordStates board-                  , boardHasInvisible = False-                  }-       else board-  else board { boardCoordStates = map (map $ setVisible False) $ boardCoordStates board-             , boardHasInvisible = True-             }-  where setVisible vis coord = coord { coordVisible = vis }---- | Resets all points on a board not to be dimmed.-clearBoardDimmed :: BoardState -> BoardState-clearBoardDimmed board =-  if boardHasDimmed board-  then board { boardCoordStates = map (map clearDim) $ boardCoordStates board-             , boardHasDimmed = False-             }-  else board-  where clearDim coord = coord { coordDimmed = False }---- |> isStarPoint width height x y------ Returns whether @(x, y)@ is a known star point on a board of the given width--- and height.-isStarPoint :: Int -> Int -> Int -> Int -> Bool-isStarPoint width height-  | width == 9 && height == 9 = isStarPoint9-  | width == 13 && height == 13 = isStarPoint13-  | width == 19 && height == 19 = isStarPoint19-  | otherwise = const $ const False--isStarPoint' :: [Int] -> Int -> Int -> Bool-isStarPoint' ixs x y = x `elem` ixs && y `elem` ixs--isStarPoint9, isStarPoint13, isStarPoint19 :: Int -> Int -> Bool-isStarPoint9 = isStarPoint' [2, 4, 6]-isStarPoint13 = isStarPoint' [3, 6, 9]-isStarPoint19 = isStarPoint' [3, 9, 15]---- | Applies a property to a 'BoardState'.  This function covers all properties--- that modify 'BoardState's, including making moves, adding markup, and so on.-applyProperty :: Property -> BoardState -> BoardState--applyProperty (B maybeXy) board = updateBoardForMove Black $ case maybeXy of-  Nothing -> board  -- Pass.-  Just xy -> getApplyMoveResult board $-             applyMove playTheDarnMoveGoParams Black xy board-applyProperty KO board = board-applyProperty (MN moveNum) board = board { boardMoveNumber = moveNum }-applyProperty (W maybeXy) board = updateBoardForMove White $ case maybeXy of-  Nothing -> board  -- Pass.-  Just xy -> getApplyMoveResult board $-             applyMove playTheDarnMoveGoParams White xy board--applyProperty (AB coords) board =-  updateCoordStates' (\state -> state { coordStone = Just Black }) coords board-applyProperty (AW coords) board =-  updateCoordStates' (\state -> state { coordStone = Just White }) coords board-applyProperty (AE coords) board =-  updateCoordStates' (\state -> state { coordStone = Nothing }) coords board-applyProperty (PL color) board = board { boardPlayerTurn = color }--applyProperty (C {}) board = board-applyProperty (DM {}) board = board-applyProperty (GB {}) board = board-applyProperty (GW {}) board = board-applyProperty (HO {}) board = board-applyProperty (N {}) board = board-applyProperty (UC {}) board = board-applyProperty (V {}) board = board--applyProperty (BM {}) board = board-applyProperty (DO {}) board = board-applyProperty (IT {}) board = board-applyProperty (TE {}) board = board--applyProperty (AR arrows) board = board { boardArrows = arrows ++ boardArrows board }-applyProperty (CR coords) board =-  updateCoordStates' (\state -> state { coordMark = Just MarkCircle }) coords board-applyProperty (DD coords) board =-  let coords' = expandCoordList coords-      board' = clearBoardDimmed board-  in if null coords'-     then board'-     else updateCoordStates (\state -> state { coordDimmed = True }) coords'-          board { boardHasDimmed = True }-applyProperty (LB labels) board = board { boardLabels = labels ++ boardLabels board }-applyProperty (LN lines) board = board { boardLines = lines ++ boardLines board }-applyProperty (MA coords) board =-  updateCoordStates' (\state -> state { coordMark = Just MarkX }) coords board-applyProperty (SL coords) board =-  updateCoordStates' (\state -> state { coordMark = Just MarkSelected }) coords board-applyProperty (SQ coords) board =-  updateCoordStates' (\state -> state { coordMark = Just MarkSquare }) coords board-applyProperty (TR coords) board =-  updateCoordStates' (\state -> state { coordMark = Just MarkTriangle }) coords board--applyProperty (AP {}) board = board-applyProperty (CA {}) board = board-applyProperty (FF {}) board = board-applyProperty (GM {}) board = board-applyProperty (ST variationMode) board =-  updateRootInfo (\info -> info { rootInfoVariationMode = variationMode }) board-applyProperty (SZ {}) board = board--applyProperty (AN str) board =-  updateBoardInfo (\info -> info { gameInfoAnnotatorName = Just $ fromSimpleText str })-                  board-applyProperty (BR str) board =-  updateBoardInfo (\info -> info { gameInfoBlackRank = Just $ fromSimpleText str })-                  board-applyProperty (BT str) board =-  updateBoardInfo (\info -> info { gameInfoBlackTeamName = Just $ fromSimpleText str })-                  board-applyProperty (CP str) board =-  updateBoardInfo (\info -> info { gameInfoCopyright = Just $ fromSimpleText str })-                  board-applyProperty (DT str) board =-  updateBoardInfo (\info -> info { gameInfoDatesPlayed = Just $ fromSimpleText str })-                  board-applyProperty (EV str) board =-  updateBoardInfo (\info -> info { gameInfoEvent = Just $ fromSimpleText str })-                  board-applyProperty (GC str) board =-  updateBoardInfo (\info -> info { gameInfoGameComment = Just $ fromSimpleText str })-                  board-applyProperty (GN str) board =-  updateBoardInfo (\info -> info { gameInfoGameName = Just $ fromSimpleText str })-                  board-applyProperty (ON str) board =-  updateBoardInfo (\info -> info { gameInfoOpeningComment = Just $ fromSimpleText str })-                  board-applyProperty (OT str) board =-  updateBoardInfo (\info -> info { gameInfoOvertime = Just $ fromSimpleText str })-                  board-applyProperty (PB str) board =-  updateBoardInfo (\info -> info { gameInfoBlackName = Just $ fromSimpleText str })-                  board-applyProperty (PC str) board =-  updateBoardInfo (\info -> info { gameInfoPlace = Just $ fromSimpleText str })-                  board-applyProperty (PW str) board =-  updateBoardInfo (\info -> info { gameInfoWhiteName = Just $ fromSimpleText str })-                  board-applyProperty (RE result) board =-  updateBoardInfo (\info -> info { gameInfoResult = Just result })-                  board-applyProperty (RO str) board =-  updateBoardInfo (\info -> info { gameInfoRound = Just $ fromSimpleText str })-                  board-applyProperty (RU ruleset) board =-  updateBoardInfo (\info -> info { gameInfoRuleset = Just ruleset })-                  board-applyProperty (SO str) board =-  updateBoardInfo (\info -> info { gameInfoSource = Just $ fromSimpleText str })-                  board-applyProperty (TM seconds) board =-  updateBoardInfo (\info -> info { gameInfoBasicTimeSeconds = Just seconds })-                  board-applyProperty (US str) board =-  updateBoardInfo (\info -> info { gameInfoEntererName = Just $ fromSimpleText str })-                  board-applyProperty (WR str) board =-  updateBoardInfo (\info -> info { gameInfoWhiteRank = Just $ fromSimpleText str })-                  board-applyProperty (WT str) board =-  updateBoardInfo (\info -> info { gameInfoWhiteTeamName = Just $ fromSimpleText str })-                  board--applyProperty (VW coords) board =-  let coords' = expandCoordList coords-  in if null coords'-     then setBoardVisible True board-     else updateCoordStates (\state -> state { coordVisible = True }) coords' $-          setBoardVisible False board--applyProperty (UnknownProperty {}) board = board--applyProperties :: Node -> BoardState -> BoardState-applyProperties node board = foldr applyProperty board (nodeProperties node)---- | Applies the transformation function to all of a board's coordinates--- referred to by the 'CoordList'.-updateCoordStates :: (CoordState -> CoordState) -> [Coord] -> BoardState -> BoardState-updateCoordStates fn coords board =-  board { boardCoordStates = foldr applyFn (boardCoordStates board) coords }-  where applyFn (x, y) = listUpdate (updateRow x) y-        updateRow = listUpdate fn--updateCoordStates' :: (CoordState -> CoordState) -> CoordList -> BoardState -> BoardState-updateCoordStates' fn coords = updateCoordStates fn (expandCoordList coords)---- | Updates properties of a 'BoardState' given that the player of the given--- color has just made a move.  Increments the move number and updates the--- player turn.-updateBoardForMove :: Color -> BoardState -> BoardState-updateBoardForMove movedPlayer board =-  board { boardMoveNumber = boardMoveNumber board + 1-        , boardPlayerTurn = cnot movedPlayer-        }---- | A structure that configures how 'applyMove' should handle moves that are--- normally illegal in Go.-data ApplyMoveParams = ApplyMoveParams { allowSuicide :: Bool-                                         -- ^ If false, suicide will cause 'applyMove' to return-                                         -- 'ApplyMoveSuicideError'.  If true, suicide will kill the-                                         -- friendly group and give points to the opponent.-                                       , allowOverwrite :: Bool-                                         -- ^ If false, playing on an occupied point will cause-                                         -- 'applyMove' to return 'ApplyMoveOverwriteError' with the-                                         -- color of the stone occupying the point.  If true,-                                         -- playing on an occupied point will overwrite the point-                                         -- (the previous stone vanishes), then capture rules are-                                         -- applied as normal.-                                       } deriving (Show)---- | As an argument to 'applyMove', causes illegal moves to be treated as--- errors.-standardGoMoveParams :: ApplyMoveParams-standardGoMoveParams = ApplyMoveParams { allowSuicide = False-                                       , allowOverwrite = False-                                       }---- | As an argument to 'applyMove', causes illegal moves to be played--- unconditionally.-playTheDarnMoveGoParams :: ApplyMoveParams-playTheDarnMoveGoParams = ApplyMoveParams { allowSuicide = True-                                          , allowOverwrite = True-                                          }---- | The possible results from 'applyMove'.-data ApplyMoveResult = ApplyMoveOk BoardState-                       -- ^ The move was accepted; playing it resulted in the-                       -- given board without capture.-                     | ApplyMoveCapture BoardState Color Int-                       -- ^ The move was accepted; playing it resulted in the-                       -- given board with a capture.  The specified side gained-                       -- the number of points given.-                     | ApplyMoveSuicideError-                       -- ^ Playing the move would result in suicide, which is-                       -- forbidden.-                     | ApplyMoveOverwriteError Color-                       -- ^ There is already a stone of the specified color on-                       -- the target point, and overwriting is forbidden.---- | If the 'ApplyMoveResult' represents a successful move, then the resulting--- 'BoardState' is returned, otherwise, the default 'BoardState' given is--- returned.-getApplyMoveResult :: BoardState -> ApplyMoveResult -> BoardState-getApplyMoveResult defaultBoard result = fromMaybe defaultBoard $ getApplyMoveResult' result--getApplyMoveResult' :: ApplyMoveResult -> Maybe BoardState-getApplyMoveResult' result = case result of-  ApplyMoveOk board -> Just board-  ApplyMoveCapture board color points -> Just $ case color of-    Black -> board { boardBlackCaptures = boardBlackCaptures board + points }-    White -> board { boardWhiteCaptures = boardWhiteCaptures board + points }-  ApplyMoveSuicideError -> Nothing-  ApplyMoveOverwriteError _ -> Nothing---- | Internal data structure, only for move application code.  Represents a--- group of stones.-data ApplyMoveGroup = ApplyMoveGroup { applyMoveGroupOrigin :: Coord-                                     , applyMoveGroupCoords :: [Coord]-                                     , applyMoveGroupLiberties :: Int-                                     } deriving (Show)---- | Places a stone of a color at a point on a board, and runs move validation--- and capturing logic according to the given parameters.  Returns whether the--- move was successful, and the result if so.-applyMove :: ApplyMoveParams -> Color -> Coord -> BoardState -> ApplyMoveResult-applyMove params color xy board =-  let currentStone = coordStone $ boardCoordState xy board-  in case currentStone of-    Just color -> if allowOverwrite params-                  then moveResult-                  else ApplyMoveOverwriteError color-    Nothing -> moveResult-  where boardWithMove = updateCoordStates (\state -> state { coordStone = Just color })-                                          [xy]-                                          board-        (boardWithCaptures, points) = foldr (maybeCapture $ cnot color)-                                            (boardWithMove, 0)-                                            (adjacentPoints boardWithMove xy)-        playedGroup = computeGroup boardWithCaptures xy-        moveResult-          | applyMoveGroupLiberties playedGroup == 0 =-            if points /= 0-            then error "Cannot commit suicide and capture at the same time."-            else if allowSuicide params-                 then let (boardWithSuicide, suicidePoints) =-                            applyMoveCapture (boardWithCaptures, 0) playedGroup-                      in ApplyMoveCapture boardWithSuicide (cnot color) suicidePoints-                 else ApplyMoveSuicideError-          | points /= 0 = ApplyMoveCapture boardWithCaptures color points-          | otherwise = ApplyMoveOk boardWithCaptures---- | Capture if there is a liberty-less group of a color at a point on--- a board.  Removes captured stones from the board and accumulates--- points for captured stones.-maybeCapture :: Color -> Coord -> (BoardState, Int) -> (BoardState, Int)-maybeCapture color xy result@(board, _) =-  if coordStone (boardCoordState xy board) /= Just color-  then result-  else let group = computeGroup board xy-       in if applyMoveGroupLiberties group /= 0-          then result-          else applyMoveCapture result group--computeGroup :: BoardState -> Coord -> ApplyMoveGroup-computeGroup board xy =-  if isNothing (coordStone $ boardCoordState xy board)-  then error "computeGroup called on an empty point."-  else let groupCoords = bucketFill board xy-       in ApplyMoveGroup { applyMoveGroupOrigin = xy-                         , applyMoveGroupCoords = groupCoords-                         , applyMoveGroupLiberties = getLibertiesOfGroup board groupCoords-                         }--applyMoveCapture :: (BoardState, Int) -> ApplyMoveGroup -> (BoardState, Int)-applyMoveCapture (board, points) group =-  (updateCoordStates (\state -> state { coordStone = Nothing })-                     (applyMoveGroupCoords group)-                     board,-   points + length (applyMoveGroupCoords group))---- | Returns a list of the four coordinates that are adjacent to the--- given coordinate on the board, excluding coordinates that are out--- of bounds.-adjacentPoints :: BoardState -> Coord -> [Coord]-adjacentPoints board (x, y) = execWriter $ do-  when (x > 0) $ tell [(x - 1, y)]-  when (y > 0) $ tell [(x, y - 1)]-  when (x < boardWidth board - 1) $ tell [(x + 1, y)]-  when (y < boardHeight board - 1) $ tell [(x, y + 1)]---- | Takes a list of coordinates that comprise a group (e.g. a list--- returned from 'bucketFill') and returns the number of liberties the--- group has.  Does no error checking to ensure that the list refers--- to a single or maximal group.-getLibertiesOfGroup :: BoardState -> [Coord] -> Int-getLibertiesOfGroup board groupCoords =-  length $ nub $ concatMap findLiberties groupCoords-  where findLiberties xy = filter (\xy' -> isNothing $ coordStone $ boardCoordState xy' board)-                                  (adjacentPoints board xy)---- | Expands a single coordinate on a board into a list of all the--- coordinates connected to it by some continuous path of stones of--- the same color (or empty spaces).-bucketFill :: BoardState -> Coord -> [Coord]-bucketFill board xy0 = bucketFill' Set.empty [xy0]-  where bucketFill' known [] = Set.toList known-        bucketFill' known (xy:xys) =-          if Set.member xy known-          then bucketFill' known xys-          else let new = filter ((stone0 ==) . coordStone . flip boardCoordState board)-                                (adjacentPoints board xy)-               in bucketFill' (Set.insert xy known) (new ++ xys)-        stone0 = coordStone $ boardCoordState xy0 board---- | Returns whether it is legal to place a stone of the given color at a point--- on a board.  Accepts out-of-bound coordinates and returns false.-isValidMove :: BoardState -> Color -> Coord -> Bool--- TODO Should out-of-bound coordinates be accepted?-isValidMove board color coord@(x, y) =-  let w = boardWidth board-      h = boardHeight board-  in x >= 0 && y >= 0 && x < w && y < h &&-     isJust (getApplyMoveResult' $ applyMove standardGoMoveParams color coord board)---- | Returns whether it is legal for the current player to place a stone at a--- point on a board.  Accepts out-of-bound coordinates and returns false.-isCurrentValidMove :: BoardState -> Coord -> Bool-isCurrentValidMove board = isValidMove board (boardPlayerTurn board)---- | A pointer to a node in a game tree that also holds information--- about the current state of the game at that node.-data Cursor = Cursor { cursorParent :: Maybe Cursor-                       -- ^ The cursor for the node above this cursor's node in-                       -- the game tree.  The node of the parent cursor is the-                       -- parent of the cursor's node.-                       ---                       -- This is @Nothing@ iff the cursor's node has no parent.-                     , cursorChildIndex :: Int-                       -- ^ The index of this cursor's node in its parent's-                       -- child list.  When the cursor's node has no parent,-                       -- the value in this field is not specified.-                     , cursorNode :: Node-                       -- ^ The game tree node about which the cursor stores-                       -- information.-                     , cursorBoard :: BoardState-                       -- ^ The complete board state for the current node.-                     } deriving (Show) -- TODO Better Show Cursor instance.---- | Returns a cursor for a root node.-rootCursor :: Node -> Cursor-rootCursor node =-  Cursor { cursorParent = Nothing-         , cursorChildIndex = -1-         , cursorNode = node-         , cursorBoard = rootBoardState node-         }--cursorRoot :: Cursor -> Cursor-cursorRoot cursor = case cursorParent cursor of-  Nothing -> cursor-  Just parent -> cursorRoot parent--cursorChild :: Cursor -> Int -> Cursor-cursorChild cursor index =-  Cursor { cursorParent = Just cursor-         , cursorChildIndex = index-         , cursorNode = child-         , cursorBoard = applyProperties child $ boardChild $ cursorBoard cursor-         }-  -- TODO Better handling or messaging for out-of-bounds:-  where child = (!! index) $ nodeChildren $ cursorNode cursor--cursorChildren :: Cursor -> [Cursor]-cursorChildren cursor =-  let board = boardChild $ cursorBoard cursor-  in map (\(index, child) -> Cursor { cursorParent = Just cursor-                                    , cursorChildIndex = index-                                    , cursorNode = child-                                    , cursorBoard = applyProperties child board-                                    })-     $ zip [0..]-     $ nodeChildren-     $ cursorNode cursor--cursorChildCount :: Cursor -> Int-cursorChildCount = length . nodeChildren . cursorNode--cursorChildPlayingAt :: Coord -> Cursor -> Maybe Cursor-cursorChildPlayingAt coord cursor =-  let children = cursorChildren cursor-      color = boardPlayerTurn $ cursorBoard cursor-      hasMove = elem $ colorToMove color coord-  in find (hasMove . nodeProperties . cursorNode) children---- | This is simply @'nodeProperties' . 'cursorNode'@.-cursorProperties :: Cursor -> [Property]-cursorProperties = nodeProperties . cursorNode--cursorModifyNode :: (Node -> Node) -> Cursor -> Cursor-cursorModifyNode fn cursor =-  let node' = fn $ cursorNode cursor-  in case cursorParent cursor of-    Nothing -> rootCursor node'-    Just parentCursor ->-      let index = cursorChildIndex cursor-          parentCursor' = cursorModifyNode-                          (\parentNode ->-                            parentNode { nodeChildren = listUpdate (const node')-                                                        index-                                                        (nodeChildren parentNode)-                                       })-                          parentCursor-      in cursorChild parentCursor' index---- | Returns the variations to display for a cursor.  The returned list contains--- the location and color of 'B' and 'W' properties in variation nodes.--- Variation nodes are either children of the current node, or siblings of the--- current node, depending on the variation mode source.-cursorVariations :: VariationModeSource -> Cursor -> [(Coord, Color)]-cursorVariations source cursor =-  case source of-    ShowChildVariations -> collectPlays $ nodeChildren $ cursorNode cursor-    ShowCurrentVariations ->-      case cursorParent cursor of-        Nothing -> []-        Just parent -> collectPlays $ listDeleteIndex (cursorChildIndex cursor) $-                       nodeChildren $ cursorNode parent-  where collectPlays :: [Node] -> [(Coord, Color)]-        collectPlays = concatMap collectPlays'-        collectPlays' = concatMap collectPlays'' . nodeProperties-        collectPlays'' prop = case prop of-          B (Just xy) -> [(xy, Black)]-          W (Just xy) -> [(xy, White)]-          _ -> []--colorToMove :: Color -> Coord -> Property-colorToMove color coord =-  case color of-    Black -> B $ Just coord-    White -> W $ Just coord
− src/Game/Goatee/Sgf/Monad.hs
@@ -1,690 +0,0 @@--- This file is part of Goatee.------ Copyright 2014 Bryan Gardiner------ Goatee is free software: you can redistribute it and/or modify--- it under the terms of the GNU Affero General Public License as published by--- the Free Software Foundation, either version 3 of the License, or--- (at your option) any later version.------ Goatee is distributed in the hope that it will be useful,--- but WITHOUT ANY WARRANTY; without even the implied warranty of--- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the--- GNU Affero General Public License for more details.------ You should have received a copy of the GNU Affero General Public License--- along with Goatee.  If not, see <http://www.gnu.org/licenses/>.---- | A monad for working with game trees.-module Game.Goatee.Sgf.Monad (-  -- * The Go monad-  GoT, GoM,-  runGoT, runGo,-  evalGoT, evalGo,-  execGoT, execGo,-  getCursor, getCoordState,-  -- * Navigation-  Step(..), goUp, goDown, goToRoot, goToGameInfoNode,-  -- * Remembering positions-  pushPosition, popPosition, dropPosition,-  -- * Properties-  getProperties,-  modifyProperties,-  getProperty,-  getPropertyValue,-  putProperty,-  deleteProperty,-  modifyProperty,-  modifyPropertyValue,-  modifyPropertyString,-  modifyPropertyCoords,-  modifyGameInfo,-  modifyVariationMode,-  getMark,-  modifyMark,-  -- * Children-  addChild,-  -- * Event handling-  Event, on, fire,-  -- * Events-  childAddedEvent, ChildAddedHandler,-  gameInfoChangedEvent, GameInfoChangedHandler,-  navigationEvent, NavigationHandler,-  propertiesModifiedEvent, PropertiesModifiedHandler,-  variationModeChangedEvent, VariationModeChangedHandler,-  ) where--import Control.Applicative ((<$>), Applicative ((<*>), pure))-import Control.Monad (ap, liftM, when)-import Control.Monad.Identity (Identity, runIdentity)-import Control.Monad.Trans (MonadIO, MonadTrans, lift, liftIO)-import Control.Monad.Writer.Class (MonadWriter, listen, pass, tell, writer)-import qualified Control.Monad.State as State-import Control.Monad.State (StateT)-import Data.List (delete, find, mapAccumL, nub)-import Data.Maybe (fromMaybe, isJust, isNothing)-import Game.Goatee.Common-import Game.Goatee.Sgf.Board-import Game.Goatee.Sgf.Property-import Game.Goatee.Sgf.Tree hiding (addChild)-import Game.Goatee.Sgf.Types---- | The internal state of a Go monad transformer.  @go@ is the type of--- Go monad or transformer (instance of 'GoMonad').-data GoState go = GoState { stateCursor :: Cursor-                            -- ^ The current position in the game tree.-                          , statePathStack :: PathStack-                            -- ^ The current path stack.--                          -- Event handlers.-                          , stateChildAddedHandlers :: [ChildAddedHandler go]-                            -- ^ Handlers for 'childAddedEvent'.-                          , stateGameInfoChangedHandlers :: [GameInfoChangedHandler go]-                            -- ^ Handlers for 'gameInfoChangedEvent'.-                          , stateNavigationHandlers :: [NavigationHandler go]-                            -- ^ Handlers for 'navigationEvent'.-                          , statePropertiesModifiedHandlers :: [PropertiesModifiedHandler go]-                            -- ^ Handlers for 'propertiesModifiedEvent'.-                          , stateVariationModeChangedHandlers :: [VariationModeChangedHandler go]-                            -- ^ Handlers for 'variationModeChangedEvent'.-                          }---- | A path stack is a record of previous places visited in a game tree.  It is--- encoded a list of paths (steps) to each previous memorized position.------ The positions saved in calls to 'pushPosition' correspond to entries in the--- outer list here, with the first sublist representing the last call.  The--- sublist contains the steps in order that will trace the path back to the--- saved position.-type PathStack = [[Step]]---- | A simplified constructor function for 'GoState'.-initialState :: Cursor -> GoState m-initialState cursor = GoState { stateCursor = cursor-                              , statePathStack = []-                              , stateChildAddedHandlers = []-                              , stateGameInfoChangedHandlers = []-                              , stateNavigationHandlers = []-                              , statePropertiesModifiedHandlers = []-                              , stateVariationModeChangedHandlers = []-                              }---- | A single step along a game tree.  Either up or down.-data Step = GoUp Int-            -- ^ Represents a step up from a child with the given index.-          | GoDown Int-            -- ^ Represents a step down to the child with the given index.-          deriving (Eq, Show)---- | Reverses a step, such that taking a step then it's reverse will leave you--- where you started.-reverseStep :: Step -> Step-reverseStep step = case step of-  GoUp index -> GoDown index-  GoDown index -> GoUp index---- | Takes a 'Step' from a 'Cursor', returning a new 'Cursor'.-takeStep :: Step -> Cursor -> Cursor-takeStep (GoUp _) cursor = fromMaybe (error $ "takeStep: Can't go up from " ++ show cursor ++ ".") $-                           cursorParent cursor-takeStep (GoDown index) cursor = cursorChild cursor index---- | Internal function.  Takes a 'Step' in the Go monad.  Updates the path stack--- accordingly.-takeStepM :: Monad m => Step -> (PathStack -> PathStack) -> GoT m ()-takeStepM step = case step of-  GoUp _ -> goUp'-  GoDown index -> goDown' index---- | A monad (transformer) for navigating and mutating 'Cursor's, and--- remembering previous locations.  See 'GoT' and 'GoM'.------ The monad supports handlers for events raised during actions it takes, such--- as navigating through the tree and modifying nodes.-class Monad go => MonadGo go where-  -- | Returns the current cursor.-  getCursor :: go Cursor--  -- | Returns the 'CoordState' at the given point.-  getCoordState :: Coord -> go CoordState-  getCoordState coord = liftM (boardCoordState coord . cursorBoard) getCursor--  -- | Navigates up the tree.  It must be valid to do so, otherwise 'fail' is-  -- called.  Fires a 'navigationEvent' after moving.-  goUp :: go ()--  -- | Navigates down the tree to the child with the given index.  The child-  -- must exist.  Fires a 'navigationEvent' after moving.-  goDown :: Int -> go ()--  -- | Navigates up to the root of the tree.  Fires 'navigationEvent's for each-  -- step.-  goToRoot :: go ()--  -- | Navigates up the tree to the node containing game info properties, if-  -- any.  Returns true if a game info node was found.-  goToGameInfoNode :: Bool-                      -- ^ When no node with game info is found, then if false,-                      -- return to the original node, otherwise finish at the-                      -- root node.-                   -> go Bool--  -- | Pushes the current location in the game tree onto an internal position-  -- stack, such that 'popPosition' is capable of navigating back to the same-  -- position, even if the game tree has been modified (though the old position-  -- must still exist in the tree to return to it).-  pushPosition :: go ()--  -- | Returns to the last position pushed onto the internal position stack via-  -- 'pushPosition'.  This action must be balanced by a 'pushPosition'.-  popPosition :: go ()--  -- | Drops the last position pushed onto the internal stack by 'pushPosition'-  -- off of the stack.  This action must be balanced by a 'pushPosition'.-  dropPosition :: go ()--  -- | Returns the set of properties on the current node.-  getProperties :: go [Property]-  getProperties = liftM cursorProperties getCursor--  -- | Modifies the set of properties on the current node.-  ---  -- The given function must end on the same node on which it started.-  modifyProperties :: ([Property] -> go [Property]) -> go ()--  -- | Searches for a property on the current node, returning it if found.-  getProperty :: Descriptor d => d -> go (Maybe Property)--  -- | Searches for a valued property on the current node, returning its value-  -- if found.-  getPropertyValue :: ValuedDescriptor d v => d -> go (Maybe v)-  getPropertyValue descriptor = liftM (liftM $ propertyValue descriptor) $ getProperty descriptor--  -- | Sets a property on the current node, replacing an existing property with-  -- the same name, if one exists.-  putProperty :: Property -> go ()-  putProperty property = modifyProperty (propertyInfo property) $ const $ Just property--  -- | Deletes a property from the current node, if it's set.-  ---  -- Note that although a 'Property' is a 'Descriptor', giving a valued-  -- @Property@ here will not cause deletion to match on the value of the-  -- property.  That is, the following code will result in 'Nothing', because-  -- the deletion only cares about the name of the property.-  ---  -- > do putProperty $ PL Black-  -- >    deleteProperty $ PL White-  -- >    getPropertyValue propertyPL-  deleteProperty :: Descriptor d => d -> go ()-  deleteProperty descriptor = modifyProperty descriptor $ const Nothing--  -- | Calls the given function to modify the state of the given property-  -- (descriptor) on the current node.  'Nothing' represents the property not-  -- existing on the node, and a 'Just' marks the property's presence.  This-  -- function does not do any validation to check that the resulting tree state-  -- is valid.-  modifyProperty :: Descriptor d => d -> (Maybe Property -> Maybe Property) -> go ()--  -- | Calls the given function to modify the state of the given valued property-  -- (descriptor) on the current node.  'Nothing' represents the property not-  -- existing on the node, and a 'Just' with the property's value marks the-  -- property's presence.  This function does not do any validation to check-  -- that the resulting tree state is valid.-  modifyPropertyValue :: ValuedDescriptor d v => d -> (Maybe v -> Maybe v) -> go ()-  modifyPropertyValue descriptor fn = modifyProperty descriptor $ \old ->-    propertyBuilder descriptor <$> fn (propertyValue descriptor <$> old)--  -- | Mutates the string-valued property attached to the current node according-  -- to the given function.  The input string will be empty if the current node-  -- either has the property with an empty value, or doesn't have the property.-  -- Returning an empty string removes the property from the node, if it was-  -- set.-  modifyPropertyString :: (Stringlike s, ValuedDescriptor d s) => d -> (String -> String) -> go ()-  modifyPropertyString descriptor fn =-    modifyPropertyValue descriptor $ \value -> case fn (maybe "" sgfToString value) of-      "" -> Nothing-      str -> let sgf = stringToSgf str-                 -- Because stringToSgf might do processing, we have to check-                 -- the conversion back to a string for emptiness.-             in if null $ sgfToString sgf then Nothing else Just sgf--  -- | Mutates the 'CoordList'-valued property attached to the current node-  -- according to the given function.  Conversion between @CoordList@ and-  -- @[Coord]@ is performed automatically.  The input list will be empty if the-  -- current node either has the property with an empty value, or doesn't have-  -- the property.  Returning an empty list removes the property from the node,-  -- if it was set.-  ---  -- Importantly, this might not be specific enough for properties such as 'DD'-  -- and 'VW' where a present, empty list has different semantics from the-  -- property not being present.  In that case, 'modifyPropertyValue' is better.-  modifyPropertyCoords :: ValuedDescriptor d CoordList => d -> ([Coord] -> [Coord]) -> go ()-  modifyPropertyCoords descriptor fn =-    modifyPropertyValue descriptor $ \value -> case fn $ maybe [] expandCoordList value of-      [] -> Nothing-      coords -> Just $ buildCoordList coords--  -- | Mutates the game info for the current path, returning the new info.  If-  -- the current node or one of its ancestors has game info properties, then-  -- that node is modified.  Otherwise, properties are inserted on the root-  -- node.-  modifyGameInfo :: (GameInfo -> GameInfo) -> go GameInfo--  -- | Sets the game's 'VariationMode' via the 'ST' property on the root node,-  -- then fires a 'variationModeChangedEvent' if the variation mode has changed.-  modifyVariationMode :: (VariationMode -> VariationMode) -> go ()--  -- | Returns the 'Mark' at a point on the current node.-  getMark :: Coord -> go (Maybe Mark)-  getMark = liftM coordMark . getCoordState--  -- | Calls the given function to modify the presence of a 'Mark' on the-  -- current node.-  modifyMark :: (Maybe Mark -> Maybe Mark) -> Coord -> go ()-  modifyMark fn coord = do-    maybeOldMark <- getMark coord-    case (maybeOldMark, fn maybeOldMark) of-      (Just oldMark, Nothing) -> remove oldMark-      (Nothing, Just newMark) -> add newMark-      (Just oldMark, Just newMark) | oldMark /= newMark -> remove oldMark >> add newMark-      (Just _, Just _) -> return ()-      (Nothing, Nothing) -> return ()-    where remove mark = modifyPropertyCoords (markProperty mark) (delete coord)-          add mark = modifyPropertyCoords (markProperty mark) (coord:)--  -- | Adds a child node to the current node at the given index, shifting all-  -- existing children at and after the index to the right.  The index must in-  -- the range @[0, numberOfChildren]@.  Fires a 'childAddedEvent' after the-  -- child is added.-  addChild :: Int -> Node -> go ()--  -- | Registers a new event handler for a given event type.-  on :: Event go h -> h -> go ()---- | The regular monad transformer for 'MonadGo'.-newtype GoT m a = GoT { goState :: StateT (GoState (GoT m)) m a }---- | The regular monad for 'MonadGo'.-type GoM = GoT Identity--instance Monad m => Functor (GoT m) where-  fmap = liftM--instance Monad m => Applicative (GoT m) where-  pure = return-  (<*>) = ap--instance Monad m => Monad (GoT m) where-  return x = GoT $ return x-  m >>= f = GoT $ goState . f =<< goState m-  fail = lift . fail--instance MonadTrans GoT where-  lift = GoT . lift--instance MonadIO m => MonadIO (GoT m) where-  liftIO = lift . liftIO--instance MonadWriter w m => MonadWriter w (GoT m) where-  writer = lift . writer-  tell = lift . tell-  listen = GoT . listen . goState-  pass = GoT . pass . goState---- | Executes a Go monad transformer on a cursor, returning in the underlying--- monad a tuple that contains the resulting value and the final cursor.-runGoT :: Monad m => GoT m a -> Cursor -> m (a, Cursor)-runGoT go cursor = do-  (value, state) <- State.runStateT (goState go) (initialState cursor)-  return (value, stateCursor state)---- | Executes a Go monad transformer on a cursor, returning in the underlying--- monad the value in the transformer.-evalGoT :: Monad m => GoT m a -> Cursor -> m a-evalGoT go cursor = liftM fst $ runGoT go cursor---- | Executes a Go monad transformer on a cursor, returning in the underlying--- monad the final cursor.-execGoT :: Monad m => GoT m a -> Cursor -> m Cursor-execGoT go cursor = liftM snd $ runGoT go cursor---- | Runs a Go monad on a cursor.  See 'runGoT'.-runGo :: GoM a -> Cursor -> (a, Cursor)-runGo go = runIdentity . runGoT go---- | Runs a Go monad on a cursor and returns the value in the monad.-evalGo :: GoM a -> Cursor -> a-evalGo m cursor = fst $ runGo m cursor---- | Runs a Go monad on a cursor and returns the final cursor.-execGo :: GoM a -> Cursor -> Cursor-execGo m cursor = snd $ runGo m cursor--getState :: Monad m => GoT m (GoState (GoT m))-getState = GoT State.get--putState :: Monad m => GoState (GoT m) -> GoT m ()-putState = GoT . State.put--modifyState :: Monad m => (GoState (GoT m) -> GoState (GoT m)) -> GoT m ()-modifyState = GoT . State.modify--instance Monad m => MonadGo (GoT m) where-  getCursor = liftM stateCursor getState--  goUp = do-    index <- liftM cursorChildIndex getCursor-    goUp' $ \pathStack -> case pathStack of-      [] -> pathStack-      path:paths -> (GoDown index:path):paths--  goDown index = goDown' index $ \pathStack -> case pathStack of-    [] -> pathStack-    path:paths -> (GoUp index:path):paths--  goToRoot = whileM (isJust . cursorParent <$> getCursor) goUp--  goToGameInfoNode goToRootIfNotFound = pushPosition >> findGameInfoNode-    where findGameInfoNode = do-            cursor <- getCursor-            if hasGameInfo cursor-              then dropPosition >> return True-              else if isNothing $ cursorParent cursor-                   then do if goToRootIfNotFound then dropPosition else popPosition-                           return False-                   else goUp >> findGameInfoNode-          hasGameInfo cursor = internalIsGameInfoNode $ cursorNode cursor--  pushPosition = modifyState $ \state ->-    state { statePathStack = []:statePathStack state }--  popPosition = do-    getPathStack >>= \stack -> when (null stack) $-      fail "popPosition: No position to pop from the stack."--    -- Drop each step in the top list of the path stack one at a time, until the-    -- top list is empty.-    whileM' (do path:_ <- getPathStack-                return $ if null path then Nothing else Just $ head path) $-      flip takeStepM $ \((_:steps):paths) -> steps:paths--    -- Finally, drop the empty top of the path stack.-    modifyState $ \state -> case statePathStack state of-      []:rest -> state { statePathStack = rest }-      _ -> error "popPosition: Internal failure, top of path stack is not empty."--  dropPosition = do-    state <- getState-    -- If there are >=2 positions on the path stack, then we can't simply drop-    -- the moves that will return us to the top-of-stack position, because they-    -- may still be needed to return to the second-on-stack position by a-    -- following popPosition.-    case statePathStack state of-      x:y:xs -> putState $ state { statePathStack = (x ++ y):xs }-      _:[] -> putState $ state { statePathStack = [] }-      [] -> fail "dropPosition: No position to drop from the stack."--  modifyProperties fn = do-    oldCursor <- getCursor-    let oldProperties = cursorProperties oldCursor-    newProperties <- fn oldProperties-    modifyState $ \state ->-      state { stateCursor = cursorModifyNode-                            (\node -> node { nodeProperties = newProperties })-                            oldCursor-            }-    fire propertiesModifiedEvent (\f -> f oldProperties newProperties)--    -- The current game info changes when modifying game info properties on the-    -- current node.  I think comparing game info properties should be faster-    -- than comparing 'GameInfo's.-    let filterToGameInfo = nub . filter ((GameInfoProperty ==) . propertyType)-        oldGameInfo = filterToGameInfo oldProperties-        newGameInfo = filterToGameInfo newProperties-    when (newGameInfo /= oldGameInfo) $ do-      newCursor <- getCursor-      fire gameInfoChangedEvent (\f -> f (boardGameInfo $ cursorBoard oldCursor)-                                         (boardGameInfo $ cursorBoard newCursor))--  getProperty descriptor = find (propertyPredicate descriptor) <$> getProperties--  modifyProperty descriptor fn = do-    cursor <- getCursor-    let node = cursorNode cursor-        old = findProperty descriptor node-        new = fn old-    when (maybe False (not . propertyPredicate descriptor) new) $-      fail $ "modifyProperty: May not change property type: " ++-      show old ++ " -> " ++ show new ++ "."-    case (old, new) of-      (Just _, Nothing) -> modifyProperties $ return . remove descriptor-      (Nothing, Just value') -> modifyProperties $ return . add value'-      (Just value, Just value') | value /= value' ->-        modifyProperties $ return . add value' . remove descriptor-      _ -> return ()-    where remove descriptor = filter (not . propertyPredicate descriptor)-          add value = (value:)--  modifyGameInfo fn = do-    cursor <- getCursor-    let info = boardGameInfo $ cursorBoard cursor-        info' = fn info-    when (gameInfoRootInfo info /= gameInfoRootInfo info') $-      fail "Illegal modification of root info in modifyGameInfo."-    pushPosition-    goToGameInfoNode True-    modifyProperties $ \props ->-      return $ gameInfoToProperties info' ++ filter ((GameInfoProperty /=) . propertyType) props-    popPosition-    return info'--  modifyVariationMode fn = do-    pushPosition-    goToRoot-    modifyPropertyValue propertyST $ \maybeOld ->-      -- If the new variation mode is equal to the old effective variation mode-      -- (effective applying the default if the property isn't present), then-      -- leave the property unchanged.  Otherwise, apply the new variation mode,-      -- deleting the property if the default variation mode is selected.  We-      -- don't delete the property if @maybeOld == Just new == Just-      -- defaultVariationMode@, because we don't want to trigger dirtyness-      -- unnecessarily.-      let old = fromMaybe defaultVariationMode maybeOld-          new = fn old-      in if new == old-         then maybeOld-         else if new == defaultVariationMode-              then Nothing-              else Just new-    popPosition--  addChild index node = do-    cursor <- getCursor-    let childCount = cursorChildCount cursor-    when (index < 0 || index > childCount) $ fail $-      "Monad.addChild: Index " ++ show index ++ " is not in [0, " ++ show childCount ++ "]."-    let cursor' = cursorModifyNode (addChildAt index node) cursor-    modifyState $ \state ->-      state { stateCursor = cursor'-            , statePathStack = updatePathStackCurrentNode-                               (\step -> case step of-                                   GoUp n -> GoUp $ if n < index then n else n + 1-                                   down@(GoDown _) -> down)-                               (\step -> case step of-                                   up@(GoUp _) -> up-                                   GoDown n -> GoDown $ if n < index then n else n + 1)-                               cursor'-                               (statePathStack state)-            }-    fire childAddedEvent (\f -> f index (cursorChild cursor' index))--  on event handler = modifyState $ addHandler event handler---- | Takes a step up the game tree, updates the path stack according to the--- given function, then fires navigation and game info changed events as--- appropriate.-goUp' :: Monad m => (PathStack -> PathStack) -> GoT m ()-goUp' pathStackFn = do-  state@(GoState { stateCursor = cursor-                 , statePathStack = pathStack-                 }) <- getState-  case cursorParent cursor of-    Nothing -> fail $ "goUp': Can't go up from a root cursor: " ++ show cursor-    Just parent -> do-      let index = cursorChildIndex cursor-      putState state { stateCursor = parent-                     , statePathStack = pathStackFn pathStack-                     }-      fire navigationEvent ($ GoUp index)--      -- The current game info changes when navigating up from a node that has-      -- game info properties.-      when (any ((GameInfoProperty ==) . propertyType) $ cursorProperties cursor) $-        fire gameInfoChangedEvent (\f -> f (boardGameInfo $ cursorBoard cursor)-                                           (boardGameInfo $ cursorBoard parent))---- | Takes a step down the game tree, updates the path stack according to the--- given function, then fires navigation and game info changed events as--- appropriate.-goDown' :: Monad m => Int -> (PathStack -> PathStack) -> GoT m ()-goDown' index pathStackFn = do-  state@(GoState { stateCursor = cursor-                 , statePathStack = pathStack-                 }) <- getState-  case drop index $ cursorChildren cursor of-    [] -> fail $ "goDown': Cursor does not have a child #" ++ show index ++ ": " ++ show cursor-    child:_ -> do-      putState state { stateCursor = child-                     , statePathStack = pathStackFn pathStack-                     }-      fire navigationEvent ($ GoDown index)--      -- The current game info changes when navigating down to a node that has-      -- game info properties.-      when (any ((GameInfoProperty ==) . propertyType) $ cursorProperties child) $-        fire gameInfoChangedEvent (\f -> f (boardGameInfo $ cursorBoard cursor)-                                           (boardGameInfo $ cursorBoard child))---- | Returns the current path stack.-getPathStack :: Monad m => GoT m PathStack-getPathStack = liftM statePathStack getState---- | Maps over a path stack, updating with the given functions all steps that--- enter and leave the cursor's current node.-updatePathStackCurrentNode :: (Step -> Step)-                           -> (Step -> Step)-                           -> Cursor-                           -> PathStack-                           -> PathStack-updatePathStackCurrentNode _ _ _ [] = []-updatePathStackCurrentNode onEnter onExit cursor0 paths =-  snd $ mapAccumL updatePath (cursor0, []) paths-  where updatePath :: (Cursor, [Step]) -> [Step] -> ((Cursor, [Step]), [Step])-        updatePath = mapAccumL updateStep-        updateStep :: (Cursor, [Step]) -> Step -> ((Cursor, [Step]), Step)-        updateStep (cursor, []) step = ((takeStep step cursor, [reverseStep step]), onExit step)-        updateStep (cursor, pathToInitial@(stepToInitial:restToInitial)) step =-          let pathToInitial' = if stepToInitial == step-                               then restToInitial-                               else reverseStep step:pathToInitial-          in ((takeStep step cursor, pathToInitial'),-              if null pathToInitial' then onEnter step else step)---- | Fires all of the handlers for the given event, using the given function to--- create a Go action from each of the handlers (normally themselves functions--- that create Go actions, if they're not just Go actions directly, depending on--- the event).-fire :: Monad m => Event (GoT m) h -> (h -> GoT m ()) -> GoT m ()-fire event handlerGenerator = do-  state <- getState-  mapM_ handlerGenerator $ eventStateGetter event state---- | A type of event in the Go monad transformer that can be handled by--- executing an action.  @go@ is the type of the type of the Go--- monad/transformer.  @h@ is the type of monad or monadic function which will--- be used by Go actions that can trigger the event.  For example, a navigation--- event is characterized by a 'Step' that cannot easily be recovered from the--- regular monad state, and comparing before-and-after states would be a pain.--- So @h@ for navigation events is @'Step' -> go ()@; a handler takes a 'Step'--- and returns a Go action to run as a result.-data Event go h = Event { eventName :: String-                        , eventStateGetter :: GoState go -> [h]-                        , eventStateSetter :: [h] -> GoState go -> GoState go-                        }--instance Show (Event go h) where-  show = eventName--addHandler :: Event go h -> h -> GoState go -> GoState go-addHandler event handler state =-  eventStateSetter event (eventStateGetter event state ++ [handler]) state---- | An event corresponding to a child node being added to the current node.-childAddedEvent :: Event go (ChildAddedHandler go)-childAddedEvent = Event {-  eventName = "childAddedEvent"-  , eventStateGetter = stateChildAddedHandlers-  , eventStateSetter = \handlers state -> state { stateChildAddedHandlers = handlers }-  }---- | A handler for 'childAddedEvent's.-type ChildAddedHandler go = Int -> Cursor -> go ()---- | An event that is fired when the current game info changes, either by--- navigating past a node with game info properties, or by modifying the current--- game info properties.-gameInfoChangedEvent :: Event go (GameInfoChangedHandler go)-gameInfoChangedEvent = Event {-  eventName = "gameInfoChangedEvent"-  , eventStateGetter = stateGameInfoChangedHandlers-  , eventStateSetter = \handlers state -> state { stateGameInfoChangedHandlers = handlers }-  }---- | A handler for 'gameInfoChangedEvent's.  It is called with the old game info--- then the new game info.-type GameInfoChangedHandler go = GameInfo -> GameInfo -> go ()---- | An event that is fired when a single step up or down in a game tree is--- made.-navigationEvent :: Event go (NavigationHandler go)-navigationEvent = Event {-  eventName = "navigationEvent"-  , eventStateGetter = stateNavigationHandlers-  , eventStateSetter = \handlers state -> state { stateNavigationHandlers = handlers }-  }---- | A handler for 'navigationEvent's.------ A navigation handler may navigate further, but beware infinite recursion.  A--- navigation handler must end on the same node on which it started.-type NavigationHandler go = Step -> go ()---- | An event corresponding to a modification to the properties list of the--- current node.-propertiesModifiedEvent :: Event go (PropertiesModifiedHandler go)-propertiesModifiedEvent = Event {-  eventName = "propertiesModifiedEvent"-  , eventStateGetter = statePropertiesModifiedHandlers-  , eventStateSetter = \handlers state -> state { statePropertiesModifiedHandlers = handlers }-  }---- | A handler for 'propertiesModifiedEvent's.  It is called with the old--- property list then the new property list.-type PropertiesModifiedHandler go = [Property] -> [Property] -> go ()---- | An event corresponding to a change in the active 'VariationMode'.  This can--- happen when modifying the 'ST' property, and also when navigating between--- collections (as they have different root nodes).-variationModeChangedEvent :: Event go (VariationModeChangedHandler go)-variationModeChangedEvent = Event {-  eventName = "variationModeChangedEvent"-  , eventStateGetter = stateVariationModeChangedHandlers-  , eventStateSetter = \handlers state -> state { stateVariationModeChangedHandlers = handlers }-  }--- TODO Test that this is fired when moving between root nodes in a collection.--- For now, since we don't support multiple trees in a collection, we don't need--- to worry about checking for active variation mode change on navigation.---- | A handler for 'variationModeChangedEvent's.  It is called with the old--- variation mode then the new variation mode.-type VariationModeChangedHandler go = VariationMode -> VariationMode -> go ()
− src/Game/Goatee/Sgf/Parser.hs
@@ -1,113 +0,0 @@--- This file is part of Goatee.------ Copyright 2014 Bryan Gardiner------ Goatee is free software: you can redistribute it and/or modify--- it under the terms of the GNU Affero General Public License as published by--- the Free Software Foundation, either version 3 of the License, or--- (at your option) any later version.------ Goatee is distributed in the hope that it will be useful,--- but WITHOUT ANY WARRANTY; without even the implied warranty of--- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the--- GNU Affero General Public License for more details.------ You should have received a copy of the GNU Affero General Public License--- along with Goatee.  If not, see <http://www.gnu.org/licenses/>.---- | A parser for reading SGF files.-module Game.Goatee.Sgf.Parser (-  parseString,-  parseFile,-  ) where--import Control.Applicative ((<*), (*>))-import Data.Maybe (fromMaybe)-import Game.Goatee.Common-import Game.Goatee.Sgf.Property-import Game.Goatee.Sgf.Tree-import Game.Goatee.Sgf.Types-import Text.ParserCombinators.Parsec (-  (<?>), Parser, char, eof, many, many1, parse, spaces, upper,-  )---- | Parses a string in SGF format.  Returns an error string if parsing fails.-parseString :: String -> Either String Collection-parseString str = case parse collection "<collection>" str of-  Left err -> Left $ show err-  Right (Collection roots) -> onLeft concatErrors $-                              onRight Collection $-                              andEithers $-                              map processRoot roots-  where processRoot :: Node -> Either String Node-        processRoot = checkFormatVersion . ttToPass--        -- Ensures that we are parsing an SGF version that we understand.-        -- TODO Try to proceed, if it makes sense.-        checkFormatVersion :: Node -> Either String Node-        checkFormatVersion root =-          let version = case findProperty propertyFF root of-                Nothing -> defaultFormatVersion-                Just (FF x) -> x-                x -> error $ "Expected FF or nothing, received " ++ show x ++ "."-          in if version `elem` supportedFormatVersions-             then Right root-             else Left $-                  "Unsupported SGF version " ++ show version ++ ".  Only versions " ++-                  show supportedFormatVersions ++ " are supported."--        -- SGF allows B[tt] and W[tt] to represent passes on boards <=19x19.-        -- Convert any passes from this format to B[] and W[] in a root node and-        -- its descendents.-        ttToPass :: Node -> Node-        ttToPass root =-          let SZ width height = fromMaybe (SZ boardSizeDefault boardSizeDefault) $-                                findProperty propertySZ root-          in if width <= 19 && height <= 19-             then ttToPass' width height root-             else root--        -- Convert a node and its descendents.-        ttToPass' width height node =-          node { nodeProperties = map ttToPass'' $ nodeProperties node-               , nodeChildren = map (ttToPass' width height) $ nodeChildren node-               }--        -- Convert a property.-        ttToPass'' prop = case prop of-          B (Just (19, 19)) -> B Nothing-          W (Just (19, 19)) -> W Nothing-          _ -> prop--        concatErrors errs = "The following errors occurred while parsing:" ++-                            concatMap ("\n-> " ++) errs---- | Parses a file in SGF format.  Returns an error string if parsing fails.-parseFile :: String -> IO (Either String Collection)-parseFile = fmap parseString . readFile--collection :: Parser Collection-collection = fmap Collection (spaces *> many (gameTree <* spaces) <* eof)-             <?> "collection"--gameTree :: Parser Node-gameTree = do-  char '('-  nodes <- spaces *> many1 (node <* spaces) <?> "sequence"-  subtrees <- many (gameTree <* spaces) <?> "subtrees"-  char ')'-  let (sequence, [final]) = splitAt (length nodes - 1) nodes-  return $ foldr (\seqNode childNode -> seqNode { nodeChildren = [childNode] })-                 (final { nodeChildren = subtrees })-                 sequence--node :: Parser Node-node = fmap (\props -> emptyNode { nodeProperties = props })-       (char ';' *> spaces *> many (property <* spaces)-        <?> "node")--property :: Parser Property-property = do-  name <- many1 upper-  spaces-  propertyValueParser $ descriptorForName name
− src/Game/Goatee/Sgf/Property.hs
@@ -1,27 +0,0 @@--- This file is part of Goatee.------ Copyright 2014 Bryan Gardiner------ Goatee is free software: you can redistribute it and/or modify--- it under the terms of the GNU Affero General Public License as published by--- the Free Software Foundation, either version 3 of the License, or--- (at your option) any later version.------ Goatee is distributed in the hope that it will be useful,--- but WITHOUT ANY WARRANTY; without even the implied warranty of--- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the--- GNU Affero General Public License for more details.------ You should have received a copy of the GNU Affero General Public License--- along with Goatee.  If not, see <http://www.gnu.org/licenses/>.---- | Structures and functions for working with SGF node properties.-module Game.Goatee.Sgf.Property (-  module Game.Goatee.Sgf.Property.Base,-  module Game.Goatee.Sgf.Property.Info,-  PropertyValueType, pvtParser, pvtRenderer, pvtRendererPretty,-  ) where--import Game.Goatee.Sgf.Property.Base-import Game.Goatee.Sgf.Property.Info-import Game.Goatee.Sgf.Property.Value
− src/Game/Goatee/Sgf/Property/Base.hs
@@ -1,314 +0,0 @@--- This file is part of Goatee.------ Copyright 2014 Bryan Gardiner------ Goatee is free software: you can redistribute it and/or modify--- it under the terms of the GNU Affero General Public License as published by--- the Free Software Foundation, either version 3 of the License, or--- (at your option) any later version.------ Goatee is distributed in the hope that it will be useful,--- but WITHOUT ANY WARRANTY; without even the implied warranty of--- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the--- GNU Affero General Public License for more details.------ You should have received a copy of the GNU Affero General Public License--- along with Goatee.  If not, see <http://www.gnu.org/licenses/>.--{-# LANGUAGE TemplateHaskell #-}-{-# OPTIONS_HADDOCK hide #-}---- | Core property-related data types, and some Template Haskell declarations--- for defining property metadata.------ Import "Game.Goatee.Sgf.Property" rather than importing this module.-module Game.Goatee.Sgf.Property.Base (-  -- * Properties-  Property (..),-  -- * Property metadata-  PropertyType (..),-  Descriptor (..),-  SomeDescriptor (..),-  ValuedDescriptor (..),-  PropertyInfo,-  ValuedPropertyInfo (ValuedPropertyInfo),-  -- * Property declaration-  defProperty, defValuedProperty,-  ) where--import Control.Applicative ((<$))-import Game.Goatee.Sgf.Property.Value (PropertyValueType(..), nonePvt)-import Game.Goatee.Sgf.Renderer-import Game.Goatee.Sgf.Types-import Language.Haskell.TH (-  Info (DataConI), DecsQ, Name, Type (AppT),-  appE, appT, caseE, conE, conP, conT, lam1E, match, mkName, newName,-  normalB, recP, reify, sigD, stringE, valD, varE, varP, wildP,-  )-import Text.ParserCombinators.Parsec (Parser)---- | An SGF property that gives a node meaning.-data Property =-  -- Move properties.-    B (Maybe Coord)      -- ^ Black move (nothing iff pass).-  | KO                   -- ^ Execute move unconditionally (even if illegal).-  | MN Integer           -- ^ Assign move number.-  | W (Maybe Coord)      -- ^ White move (nothing iff pass).--  -- Setup properties.-  | AB CoordList         -- ^ Assign black stones.-  | AE CoordList         -- ^ Assign empty stones.-  | AW CoordList         -- ^ Assign white stones.-  | PL Color             -- ^ Player to play.--  -- Node annotation properties.-  | C Text               -- ^ Comment.-  | DM DoubleValue       -- ^ Even position.-  | GB DoubleValue       -- ^ Good for black.-  | GW DoubleValue       -- ^ Good for white.-  | HO DoubleValue       -- ^ Hotspot.-  | N SimpleText         -- ^ Node name.-  | UC DoubleValue       -- ^ Unclear position.-  | V RealValue          -- ^ Node value.--  -- Move annotation properties.-  | BM DoubleValue       -- ^ Bad move.-  | DO                   -- ^ Doubtful move.-  | IT                   -- ^ Interesting move.-  | TE DoubleValue       -- ^ Tesuji.--  -- Markup properties.-  | AR ArrowList         -- ^ Arrows.-  | CR CoordList         -- ^ Mark points with circles.-  | DD CoordList         -- ^ Dim points.-  | LB LabelList         -- ^ Label points with text.-  | LN LineList          -- ^ Lines.-  | MA CoordList         -- ^ Mark points with 'X's.-  | SL CoordList         -- ^ Mark points as selected.-  | SQ CoordList         -- ^ Mark points with squares.-  | TR CoordList         -- ^ Mark points with trianges.--  -- Root properties.-  | AP SimpleText SimpleText -- ^ Application info.-  | CA SimpleText        -- ^ Charset for SimpleText and Text.-  | FF Int               -- ^ File format version.-  | GM Int               -- ^ Game (must be 1 = Go).-  | ST VariationMode     -- ^ Variation display format.-  | SZ Int Int           -- ^ Board size, columns then rows.--  -- Game info properties.-  | AN SimpleText        -- ^ Name of annotator.-  | BR SimpleText        -- ^ Rank of black player.-  | BT SimpleText        -- ^ Name of black team.-  | CP SimpleText        -- ^ Copyright info.-  | DT SimpleText        -- ^ Dates played.-  | EV SimpleText        -- ^ Event name.-  | GC SimpleText        -- ^ Game comment, or background, or summary.-  | GN SimpleText        -- ^ Game name.-  | ON SimpleText        -- ^ Information about the opening.-  | OT SimpleText        -- ^ The method used for overtime.-  | PB SimpleText        -- ^ Name of black player.-  | PC SimpleText        -- ^ Where the game was played.-  | PW SimpleText        -- ^ Name of white player.-  | RE GameResult        -- ^ Result of the game.-  | RO SimpleText        -- ^ Round info.-  | RU Ruleset           -- ^ Ruleset used.-  | SO SimpleText        -- ^ Source of the game.-  | TM RealValue         -- ^ Time limit, in seconds.-  | US SimpleText        -- ^ Name of user or program who entered the game.-  | WR SimpleText        -- ^ Rank of white player.-  | WT SimpleText        -- ^ Name of white team.--  -- Miscellaneous properties.-  | VW CoordList         -- ^ Set viewing region.--  | UnknownProperty String UnknownPropertyValue--  -- TODO Game info, timing, and miscellaneous properties.-  -- Also in functions below.-  deriving (Eq, Show)---- | The property types that SGF uses to group properties.-data PropertyType = MoveProperty     -- ^ Cannot mix with setup nodes.-                  | SetupProperty    -- ^ Cannot mix with move nodes.-                  | RootProperty     -- ^ May only appear in root nodes.-                  | GameInfoProperty -- ^ At most one on any path.-                  | GeneralProperty  -- ^ May appear anywhere in the game tree.-                  deriving (Eq, Show)---- | A class for types that contain metadata about a 'Property'.-class Descriptor a where-  -- | Returns the name of the property, as used in SGF files.-  propertyName :: a -> String--  -- | Returns the type of the property, as specified by the SGF spec.-  propertyType :: a -> PropertyType--  -- | Returns whether the value of the given property is inherited from the-  -- lowest ancestor specifying the property, when the property is not set on a-  -- node itself.-  propertyInherited :: a -> Bool--  -- | Returns whether the given property has the type of a descriptor.-  propertyPredicate :: a -> Property -> Bool--  -- | A parser of property values in SGF format (e.g. @"[ab]"@ for a property-  -- that takes a point).-  propertyValueParser :: a -> Parser Property--  -- | A renderer property values to SGF format (e.g. @B (Just (1,2))@ renders-  -- to @"[ab]"@).-  propertyValueRenderer :: a -> Property -> Render ()--  -- | A renderer for displaying property values in a UI.  Displays the value in-  -- a human-readable format.-  propertyValueRendererPretty :: a -> Property -> Render ()--data SomeDescriptor = forall a. Descriptor a => SomeDescriptor a--instance Descriptor SomeDescriptor where-  propertyName (SomeDescriptor d) = propertyName d-  propertyType (SomeDescriptor d) = propertyType d-  propertyInherited (SomeDescriptor d) = propertyInherited d-  propertyPredicate (SomeDescriptor d) = propertyPredicate d-  propertyValueParser (SomeDescriptor d) = propertyValueParser d-  propertyValueRenderer (SomeDescriptor d) = propertyValueRenderer d-  propertyValueRendererPretty (SomeDescriptor d) = propertyValueRendererPretty d---- | A class for 'Descriptor's of 'Property's that also contain values.-class (Descriptor a, Eq v) => ValuedDescriptor a v | a -> v where-  -- | Extracts the value from a property of the given type.  Behaviour is-  -- undefined if the property is not of the given type.-  propertyValue :: a -> Property -> v--  -- | Builds a property from a given value.-  propertyBuilder :: a -> v -> Property---- | Metadata for a property that does not contain a value.  Corresponds to a--- single nullary data constructor of 'Property'.-data PropertyInfo = PropertyInfo {-  propertyInfoName :: String-  -- ^ The SGF textual name for the property.-  , propertyInfoInstance :: Property-    -- ^ The single instance of the property.-  , propertyInfoType :: PropertyType-    -- ^ The SGF property type.-  , propertyInfoInherited :: Bool-    -- ^ Whether the property is inherited.-  }--instance Descriptor PropertyInfo where-  propertyName = propertyInfoName-  propertyType = propertyInfoType-  propertyInherited = propertyInfoInherited-  propertyPredicate = (==) . propertyInfoInstance-  propertyValueParser descriptor = propertyInfoInstance descriptor <$ pvtParser nonePvt-  propertyValueRenderer _ _ = pvtRenderer nonePvt ()-  propertyValueRendererPretty _ _ = pvtRendererPretty nonePvt ()---- | Metadata for a property that contains a value.  Corresponds to a single--- unary data constructor of 'Property'.-data ValuedPropertyInfo v = ValuedPropertyInfo {-  valuedPropertyInfoName :: String-  -- ^ The SGF textual name for the property (also the name of the data-  -- constructor).-  , valuedPropertyInfoType :: PropertyType-    -- ^ The SGF property type.-  , valuedPropertyInfoInherited :: Bool-    -- ^ Whether the property is inherited.-  , valuedPropertyInfoPredicate :: Property -> Bool-    -- ^ A predicate that matches predicates to which this 'ValuedPropertyInfo'-    -- applies.-  , valuedPropertyInfoValueType :: PropertyValueType v-    -- ^ Metadata about the type of the property's value.-  , valuedPropertyInfoValue :: Property -> v-    -- ^ A function that extracts values from properties to which this-    -- 'ValuedPropertyInfo' applies.  It is invalid to call this function with a-    -- different type of property.-  , valuedPropertyInfoBuilder :: v -> Property-    -- ^ A function that builds a property containing a value.-  }--instance Descriptor (ValuedPropertyInfo v) where-  propertyName = valuedPropertyInfoName-  propertyType = valuedPropertyInfoType-  propertyInherited = valuedPropertyInfoInherited-  propertyPredicate = valuedPropertyInfoPredicate-  propertyValueParser descriptor =-    fmap (valuedPropertyInfoBuilder descriptor) $-    pvtParser $-    valuedPropertyInfoValueType descriptor-  propertyValueRenderer descriptor property =-    pvtRenderer (valuedPropertyInfoValueType descriptor) $-    valuedPropertyInfoValue descriptor property-  propertyValueRendererPretty descriptor property =-    pvtRendererPretty (valuedPropertyInfoValueType descriptor) $-    valuedPropertyInfoValue descriptor property--instance Eq v => ValuedDescriptor (ValuedPropertyInfo v) v where-  propertyValue = valuedPropertyInfoValue-  propertyBuilder = valuedPropertyInfoBuilder---- | Template Haskell function to declare a property that does not contain a--- value.------ > $(defProperty "KO" 'MoveProperty False)------ This example declares a @propertyKO :: 'PropertyInfo'@ that is a--- 'MoveProperty' and is not inherited.-defProperty :: String-               -- ^ The SGF textual name of the property.-            -> Name-               -- ^ The name of the 'PropertyType'.-            -> Bool-               -- ^ Whether the property is inherited.-            -> DecsQ-defProperty name propType inherited = do-  let propName = mkName name-      varName = mkName $ "property" ++ name-  foo <- newName "foo"-  sequence [-    sigD varName $ conT $ mkName "PropertyInfo",-    valD (varP varName)-         (normalB [| PropertyInfo name $(conE propName) $(conE propType) inherited |])-         []-    ]---- | Template Haskell function to declare a property that contains a value.------ > $(defValuedProperty "B" 'MoveProperty False 'maybeCoordPrinter)------ This example declares a @propertyB :: 'ValuedPropertyInfo' (Maybe 'Coord')@--- that is a 'MoveProperty' and is not inherited.  The value type is--- automatically inferred.-defValuedProperty :: String -> Name -> Bool -> Name -> DecsQ-defValuedProperty name propType inherited valueType = do-  let propName = mkName name-      varName = mkName $ "property" ++ name-  foo <- newName "foo"-  bar <- newName "bar"-  DataConI _ (AppT (AppT _ haskellValueType) _) _ _ <- reify propName-  sequence [-    sigD varName $ appT (conT ''ValuedPropertyInfo) $ return haskellValueType,-    valD (varP varName)-         (normalB [| ValuedPropertyInfo-                     name-                     $(conE propType)-                     inherited-                     $(lam1E (varP foo) $ caseE (varE foo)-                       [match (recP propName [])-                              (normalB $ conE $ mkName "True")-                              [],-                        match wildP (normalB $ conE $ mkName "False") []])-                     $(varE valueType)-                     $(lam1E (varP foo) $ caseE (varE foo)-                       [match (conP propName [varP bar]) (normalB $ varE bar) [],-                        match wildP-                              (normalB-                               [| error $ "Property value getter for " ++ $(stringE name) ++-                                  " applied to " ++ show $(varE foo) ++ "." |])-                              []])-                     $(lam1E (varP foo) $ appE (conE propName) (varE foo))-                   |])-         []-    ]
− src/Game/Goatee/Sgf/Property/Info.hs
@@ -1,288 +0,0 @@--- This file is part of Goatee.------ Copyright 2014 Bryan Gardiner------ Goatee is free software: you can redistribute it and/or modify--- it under the terms of the GNU Affero General Public License as published by--- the Free Software Foundation, either version 3 of the License, or--- (at your option) any later version.------ Goatee is distributed in the hope that it will be useful,--- but WITHOUT ANY WARRANTY; without even the implied warranty of--- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the--- GNU Affero General Public License for more details.------ You should have received a copy of the GNU Affero General Public License--- along with Goatee.  If not, see <http://www.gnu.org/licenses/>.--{-# LANGUAGE TemplateHaskell #-}-{-# OPTIONS_HADDOCK hide #-}---- | Property metadata declarations.------ Import "Game.Goatee.Sgf.Property" rather than importing this module.-module Game.Goatee.Sgf.Property.Info where--import Control.Arrow ((&&&))-import qualified Data.Map as Map-import Data.Map (Map)-import Data.Maybe (fromMaybe)-import Game.Goatee.Sgf.Property.Base-import Game.Goatee.Sgf.Property.Value-import Game.Goatee.Sgf.Types---- Move properties.-$(defValuedProperty "B" 'MoveProperty False 'movePvt)-$(defProperty "KO" 'MoveProperty False)-$(defValuedProperty "MN" 'MoveProperty False 'integralPvt)-$(defValuedProperty "W" 'MoveProperty False 'movePvt)---- Setup properties.-$(defValuedProperty "AB" 'SetupProperty False 'coordListPvt)-$(defValuedProperty "AE" 'SetupProperty False 'coordListPvt)-$(defValuedProperty "AW" 'SetupProperty False 'coordListPvt)-$(defValuedProperty "PL" 'SetupProperty False 'colorPvt)---- Node annotation properties.-$(defValuedProperty "C" 'GeneralProperty False 'textPvt)-$(defValuedProperty "DM" 'GeneralProperty False 'doublePvt)-$(defValuedProperty "GB" 'GeneralProperty False 'doublePvt)-$(defValuedProperty "GW" 'GeneralProperty False 'doublePvt)-$(defValuedProperty "HO" 'GeneralProperty False 'doublePvt)-$(defValuedProperty "N" 'GeneralProperty False 'simpleTextPvt)-$(defValuedProperty "UC" 'GeneralProperty False 'doublePvt)-$(defValuedProperty "V" 'GeneralProperty False 'realPvt)---- Move annotation properties.-$(defValuedProperty "BM" 'MoveProperty False 'doublePvt)-$(defProperty "DO" 'MoveProperty False)-$(defProperty "IT" 'MoveProperty False)-$(defValuedProperty "TE" 'MoveProperty False 'doublePvt)---- Markup properties.-$(defValuedProperty "AR" 'GeneralProperty False 'coordPairListPvt)-$(defValuedProperty "CR" 'GeneralProperty False 'coordListPvt)-$(defValuedProperty "DD" 'GeneralProperty True 'coordListPvt)-$(defValuedProperty "LB" 'GeneralProperty False 'labelListPvt)-$(defValuedProperty "LN" 'GeneralProperty False 'coordPairListPvt)-$(defValuedProperty "MA" 'GeneralProperty False 'coordListPvt)-$(defValuedProperty "SL" 'GeneralProperty False 'coordListPvt)-$(defValuedProperty "SQ" 'GeneralProperty False 'coordListPvt)-$(defValuedProperty "TR" 'GeneralProperty False 'coordListPvt)---- Root properties.-propertyAP :: ValuedPropertyInfo (SimpleText, SimpleText)-propertyAP = ValuedPropertyInfo "AP" RootProperty False-             (\x -> case x of { AP {} -> True; _ -> False })-             simpleTextPairPvt-             (\(AP x y) -> (x, y))-             (uncurry AP)-$(defValuedProperty "CA" 'RootProperty False 'simpleTextPvt)-$(defValuedProperty "FF" 'RootProperty False 'integralPvt)  -- TODO Add parser validation.-$(defValuedProperty "GM" 'RootProperty False 'integralPvt)  -- TODO Add parser validation.-$(defValuedProperty "ST" 'RootProperty False 'variationModePvt)-propertySZ :: ValuedPropertyInfo (Int, Int)-propertySZ = ValuedPropertyInfo "SZ" RootProperty False-             (\x -> case x of { SZ {} -> True; _ -> False })-             sizePvt-             (\(SZ x y) -> (x, y))-             (uncurry SZ)---- Game info properties.-$(defValuedProperty "AN" 'GameInfoProperty False 'simpleTextPvt)-$(defValuedProperty "BR" 'GameInfoProperty False 'simpleTextPvt)-$(defValuedProperty "BT" 'GameInfoProperty False 'simpleTextPvt)-$(defValuedProperty "CP" 'GameInfoProperty False 'simpleTextPvt)-$(defValuedProperty "DT" 'GameInfoProperty False 'simpleTextPvt)-$(defValuedProperty "EV" 'GameInfoProperty False 'simpleTextPvt)-$(defValuedProperty "GC" 'GameInfoProperty False 'simpleTextPvt)-$(defValuedProperty "GN" 'GameInfoProperty False 'simpleTextPvt)-$(defValuedProperty "ON" 'GameInfoProperty False 'simpleTextPvt)-$(defValuedProperty "OT" 'GameInfoProperty False 'simpleTextPvt)-$(defValuedProperty "PB" 'GameInfoProperty False 'simpleTextPvt)-$(defValuedProperty "PC" 'GameInfoProperty False 'simpleTextPvt)-$(defValuedProperty "PW" 'GameInfoProperty False 'simpleTextPvt)-$(defValuedProperty "RE" 'GameInfoProperty False 'gameResultPvt)-$(defValuedProperty "RO" 'GameInfoProperty False 'simpleTextPvt)-$(defValuedProperty "RU" 'GameInfoProperty False 'rulesetPvt)-$(defValuedProperty "SO" 'GameInfoProperty False 'simpleTextPvt)-$(defValuedProperty "TM" 'GameInfoProperty False 'realPvt)-$(defValuedProperty "US" 'GameInfoProperty False 'simpleTextPvt)-$(defValuedProperty "WR" 'GameInfoProperty False 'simpleTextPvt)-$(defValuedProperty "WT" 'GameInfoProperty False 'simpleTextPvt)---- Miscellaneous properties.-$(defValuedProperty "VW" 'GeneralProperty True 'coordElistPvt)--propertyUnknown :: String -> ValuedPropertyInfo UnknownPropertyValue-propertyUnknown name =-  ValuedPropertyInfo name GeneralProperty False-  (\x -> case x of-      UnknownProperty name' _ | name' == name -> True-      _ -> False)-  unknownPropertyPvt-  (\(UnknownProperty _ value) -> value)-  (UnknownProperty name)--allDescriptors :: [SomeDescriptor]-allDescriptors = [-  SomeDescriptor propertyB-  , SomeDescriptor propertyKO-  , SomeDescriptor propertyMN-  , SomeDescriptor propertyW--  , SomeDescriptor propertyAB-  , SomeDescriptor propertyAE-  , SomeDescriptor propertyAW-  , SomeDescriptor propertyPL--  , SomeDescriptor propertyC-  , SomeDescriptor propertyDM-  , SomeDescriptor propertyGB-  , SomeDescriptor propertyGW-  , SomeDescriptor propertyHO-  , SomeDescriptor propertyN-  , SomeDescriptor propertyUC-  , SomeDescriptor propertyV--  , SomeDescriptor propertyBM-  , SomeDescriptor propertyDO-  , SomeDescriptor propertyIT-  , SomeDescriptor propertyTE--  , SomeDescriptor propertyAR-  , SomeDescriptor propertyCR-  , SomeDescriptor propertyDD-  , SomeDescriptor propertyLB-  , SomeDescriptor propertyLN-  , SomeDescriptor propertyMA-  , SomeDescriptor propertySL-  , SomeDescriptor propertySQ-  , SomeDescriptor propertyTR--  , SomeDescriptor propertyAP-  , SomeDescriptor propertyCA-  , SomeDescriptor propertyFF-  , SomeDescriptor propertyGM-  , SomeDescriptor propertyST-  , SomeDescriptor propertySZ--  , SomeDescriptor propertyAN-  , SomeDescriptor propertyBR-  , SomeDescriptor propertyBT-  , SomeDescriptor propertyCP-  , SomeDescriptor propertyDT-  , SomeDescriptor propertyEV-  , SomeDescriptor propertyGC-  , SomeDescriptor propertyGN-  , SomeDescriptor propertyON-  , SomeDescriptor propertyOT-  , SomeDescriptor propertyPB-  , SomeDescriptor propertyPC-  , SomeDescriptor propertyPW-  , SomeDescriptor propertyRE-  , SomeDescriptor propertyRO-  , SomeDescriptor propertyRU-  , SomeDescriptor propertySO-  , SomeDescriptor propertyTM-  , SomeDescriptor propertyUS-  , SomeDescriptor propertyWR-  , SomeDescriptor propertyWT--  , SomeDescriptor propertyVW-  ]--propertyInfo :: Property -> SomeDescriptor-propertyInfo property = case property of-  B {} -> SomeDescriptor propertyB-  KO {} -> SomeDescriptor propertyKO-  MN {} -> SomeDescriptor propertyMN-  W {} -> SomeDescriptor propertyW--  AB {} -> SomeDescriptor propertyAB-  AE {} -> SomeDescriptor propertyAE-  AW {} -> SomeDescriptor propertyAW-  PL {} -> SomeDescriptor propertyPL--  C {} -> SomeDescriptor propertyC-  DM {} -> SomeDescriptor propertyDM-  GB {} -> SomeDescriptor propertyGB-  GW {} -> SomeDescriptor propertyGW-  HO {} -> SomeDescriptor propertyHO-  N {} -> SomeDescriptor propertyN-  UC {} -> SomeDescriptor propertyUC-  V {} -> SomeDescriptor propertyV--  BM {} -> SomeDescriptor propertyBM-  DO {} -> SomeDescriptor propertyDO-  IT {} -> SomeDescriptor propertyIT-  TE {} -> SomeDescriptor propertyTE--  AR {} -> SomeDescriptor propertyAR-  CR {} -> SomeDescriptor propertyCR-  DD {} -> SomeDescriptor propertyDD-  LB {} -> SomeDescriptor propertyLB-  LN {} -> SomeDescriptor propertyLN-  MA {} -> SomeDescriptor propertyMA-  SL {} -> SomeDescriptor propertySL-  SQ {} -> SomeDescriptor propertySQ-  TR {} -> SomeDescriptor propertyTR--  AP {} -> SomeDescriptor propertyAP-  CA {} -> SomeDescriptor propertyCA-  FF {} -> SomeDescriptor propertyFF-  GM {} -> SomeDescriptor propertyGM-  ST {} -> SomeDescriptor propertyST-  SZ {} -> SomeDescriptor propertySZ--  AN {} -> SomeDescriptor propertyAN-  BR {} -> SomeDescriptor propertyBR-  BT {} -> SomeDescriptor propertyBT-  CP {} -> SomeDescriptor propertyCP-  DT {} -> SomeDescriptor propertyDT-  EV {} -> SomeDescriptor propertyEV-  GC {} -> SomeDescriptor propertyGC-  GN {} -> SomeDescriptor propertyGN-  ON {} -> SomeDescriptor propertyON-  OT {} -> SomeDescriptor propertyOT-  PB {} -> SomeDescriptor propertyPB-  PC {} -> SomeDescriptor propertyPC-  PW {} -> SomeDescriptor propertyPW-  RE {} -> SomeDescriptor propertyRE-  RO {} -> SomeDescriptor propertyRO-  RU {} -> SomeDescriptor propertyRU-  SO {} -> SomeDescriptor propertySO-  TM {} -> SomeDescriptor propertyTM-  US {} -> SomeDescriptor propertyUS-  WR {} -> SomeDescriptor propertyWR-  WT {} -> SomeDescriptor propertyWT--  VW {} -> SomeDescriptor propertyVW--  UnknownProperty name _ -> SomeDescriptor $ propertyUnknown name--instance Descriptor Property where-  propertyName = propertyName . propertyInfo-  propertyType = propertyType . propertyInfo-  propertyInherited = propertyInherited . propertyInfo-  propertyPredicate = propertyPredicate . propertyInfo-  propertyValueParser = propertyValueParser . propertyInfo-  propertyValueRenderer = propertyValueRenderer . propertyInfo-  propertyValueRendererPretty = propertyValueRendererPretty . propertyInfo--descriptorsByName :: Map String SomeDescriptor-descriptorsByName = Map.fromList $ map (propertyName &&& id) allDescriptors--descriptorForName :: String -> SomeDescriptor-descriptorForName name = fromMaybe (SomeDescriptor $ propertyUnknown name) $ descriptorForName' name--descriptorForName' :: String -> Maybe SomeDescriptor-descriptorForName' = flip Map.lookup descriptorsByName---- | Returns the descriptor for a mark.-markProperty :: Mark -> ValuedPropertyInfo CoordList-markProperty MarkCircle = propertyCR-markProperty MarkSelected = propertySL-markProperty MarkSquare = propertySQ-markProperty MarkTriangle = propertyTR-markProperty MarkX = propertyMA
− src/Game/Goatee/Sgf/Property/Parser.hs
@@ -1,275 +0,0 @@--- This file is part of Goatee.------ Copyright 2014 Bryan Gardiner------ Goatee is free software: you can redistribute it and/or modify--- it under the terms of the GNU Affero General Public License as published by--- the Free Software Foundation, either version 3 of the License, or--- (at your option) any later version.------ Goatee is distributed in the hope that it will be useful,--- but WITHOUT ANY WARRANTY; without even the implied warranty of--- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the--- GNU Affero General Public License for more details.------ You should have received a copy of the GNU Affero General Public License--- along with Goatee.  If not, see <http://www.gnu.org/licenses/>.---- | Parsers of property values.------ Import "Game.Goatee.Sgf.Property" rather than importing this module.-module Game.Goatee.Sgf.Property.Parser (-  colorParser,-  coordElistParser,-  coordListParser,-  coordPairListParser,-  doubleParser,-  gameResultParser,-  labelListParser,-  moveParser,-  noneParser,-  integralParser,-  realParser,-  rulesetParser,-  simpleTextPairParser,-  simpleTextParser,-  sizeParser,-  textParser,-  unknownPropertyParser,-  variationModeParser,-  -- * Exposed for testing-  compose,-  line,-  simpleText,-  text,-  ) where--import Control.Applicative ((<$), (<$>), (<*), (<*>), (*>))-import Control.Monad (when)-import Data.Char (isUpper, ord)-import Data.Maybe (catMaybes)-import Data.Monoid (Monoid, mappend, mconcat, mempty)-import Game.Goatee.Sgf.Types-import Text.ParserCombinators.Parsec (-  (<?>), (<|>), Parser,-  anyChar, char, choice, digit, eof, many, many1,-  noneOf, oneOf, option, parse, space, spaces, string,-  try, unexpected,-  )--{-# ANN module "HLint: ignore Reduce duplication" #-}---- Internal parser builders not corresponding to any particular value type.---- | A wrapper around 'CoordList' with a 'Monoid' instance used for parsing.--- The monoid does simple concatenation of the single and rectangle lists, so it--- is not appropriate for @CoordList@ proper, as it doesn't do duplicate removal--- between two @CoordList@s.-newtype CoordListMonoid = CoordListMonoid { runCoordListMonoid :: CoordList }--instance Monoid CoordListMonoid where-  mempty = CoordListMonoid emptyCoordList--  mappend (CoordListMonoid x) (CoordListMonoid y) =-    CoordListMonoid $ coords' (coordListSingles x ++ coordListSingles y)-                              (coordListRects x ++ coordListRects y)--single :: Parser a -> Parser a-single valueParser = char '[' *> valueParser <* char ']'--compose :: Parser a -> Parser b -> Parser (a, b)-compose first second = do-  x <- first-  char ':'-  y <- second-  return (x, y)--line :: Parser Int-line = toLine <$> line' <?> "line"-  where line' = oneOf $ ['a'..'z'] ++ ['A'..'Z']-        toLine c = if isUpper c-                   then ord c - ord 'A' + 26-                   else ord c - ord 'a'--listOf :: Parser a -> Parser [a]-listOf valueParser = many1 (single valueParser <* spaces)--number :: Parser (String, Bool)-number = do-  sign <- "-" <$ char '-' <|>-          "" <$ char '+' <|>-          return ""-  digits <- many1 digit-  return (sign ++ digits, not $ null sign)---- Public parsers.--colorParser :: Parser Color-colorParser = single color <?> "color"--color :: Parser Color-color = choice [Black <$ char 'B',-                White <$ char 'W']--coord :: Parser Coord-coord = (,) <$> line <*> line--coordElistParser :: Parser CoordList-coordElistParser =-  try (emptyCoordList <$ string "[]") <|>-  coordListParser <?>-  "list of points or empty"--coordListParser :: Parser CoordList-coordListParser =-  runCoordListMonoid . mconcat . map CoordListMonoid <$> listOf coordListEntry <?>-  "list of points"-  where coordListEntry = do x0 <- line-                            y0 <- line-                            choice [do char ':'-                                       x1 <- line-                                       y1 <- line-                                       return $ coordR ((x0, y0), (x1, y1)),-                                    return $ coord1 (x0, y0)]-        coordR rect = coords' [] [rect]--coordPairListParser :: Parser [(Coord, Coord)]-coordPairListParser = listOf coordPair <?> "list of point pairs"-  where coordPair = do-          x0 <- line-          y0 <- line-          char ':'-          x1 <- line-          y1 <- line-          return ((x0, y0), (x1, y1))--doubleParser :: Parser DoubleValue-doubleParser =-  single (Double1 <$ char '1' <|>-          Double2 <$ char '2') <?>-  "double"--gameResultParser :: Parser GameResult-gameResultParser = single (parseGameResult <$> simpleText False) <?> "game result"-  where parseGameResult text = case fromSimpleText text of-          "0" -> GameResultDraw-          "Draw" -> GameResultDraw-          "Void" -> GameResultVoid-          "?" -> GameResultUnknown-          rawText -> case parse gameResultWin "<game result win>" rawText of-            Left _ -> GameResultOther text-            Right win -> win--gameResultWin :: Parser GameResult-gameResultWin = GameResultWin <$> color <* char '+' <*> winReason <* eof--winReason :: Parser WinReason-winReason =-  WinByScore <$> try real <|>-  WinByResignation <$ try (string "Resign") <|>-  WinByResignation <$ try (string "R") <|>-  WinByTime <$ try (string "Time") <|>-  WinByTime <$ try (string "T") <|>-  WinByForfeit <$ try (string "Forfeit") <|>-  WinByForfeit <$ try (string "F")--labelListParser :: Parser [(Coord, SimpleText)]-labelListParser =-  listOf (compose coord $ simpleText True) <?> "list of points and labels"--moveParser :: Parser (Maybe Coord)-moveParser =-  char '[' *> (Nothing <$ char ']' <|> Just <$> coord <* char ']') <?>-  "move (point or pass)"--noneParser :: Parser ()-noneParser = () <$ string "[]" <?> "none"---- This is what the SGF spec calls the Number type, i.e. a signed integer.-integralParser :: (Integral a, Read a) => Parser a-integralParser = single integral <?> "integer"--integral :: (Integral a, Read a) => Parser a-integral = read . fst <$> number--realParser :: Parser RealValue-realParser = single real <?> "real"--real :: Parser RealValue-real = do-  (whole, isNegative) <- number-  let wholePart = toRational (read whole :: Integer)-  -- Try to read a fractional part of the number.-  -- If we fail, just return the whole part.-  option wholePart $ try $ do-    fractionalStr <- char '.' *> many1 digit-    let fractionalPart = toRational (read fractionalStr) / 10 ^ length fractionalStr-    return $ (if isNegative then (-) else (+)) wholePart fractionalPart--rulesetParser :: Parser Ruleset-rulesetParser =-  single (toRuleset . fromSimpleText <$> simpleText False) <?> "ruleset"--simpleTextPairParser :: Parser (SimpleText, SimpleText)-simpleTextPairParser = single (compose composedText composedText) <?> "pair of simple texts"-  where composedText = simpleText True---- | A parser for SGF SimpleText property values.-simpleTextParser :: Parser SimpleText-simpleTextParser = single (simpleText False) <?> "simple text"--simpleText :: Bool -> Parser SimpleText-simpleText isComposed = toSimpleText <$> text isComposed--sizeParser :: Parser (Int, Int)-sizeParser =-  (do char '['-      width <- integral-      height <- choice [width <$ char ']',-                        do char ':'-                           height <- integral-                           char ']'-                           -- TODO We should warn here rather than aborting.-                           when (width == height) $-                             fail $ show width ++ "x" ++ show height ++ " square board " ++-                             " dimensions should be specified with a single number."-                           return height]-      when (width < 1 || width > boardSizeMax ||-            height < 1 || height > boardSizeMax) $-        fail $ show width ++ "x" ++ show height ++ " board dimensions are invalid.  " ++-        "Each dimension must be between 1 and 52 inclusive."-      return (width, height)) <?>-  "board size (width or width:height)"--textParser :: Parser Text-textParser = single (toText <$> text False) <?> "text"---- | A parser for SGF text property values.  Its argument should be true if the--- text is inside of a composed property value, so @\':\'@ should terminate the--- value in addition to @']'@.-text :: Bool -> Parser String-text isComposed = catMaybes <$> many textChar'-  where textChar' = textChar (if isComposed then ":]\\" else "]\\")--textChar :: String -> Parser (Maybe Char)-textChar specialChars =-  choice [Just <$> char '\n',-          Just ' ' <$ space,-          try (char '\\' *> (Nothing <$ char '\n' <|>-                             Just <$> anyChar)),-          Just <$> noneOf specialChars]--unknownPropertyParser :: Parser UnknownPropertyValue-unknownPropertyParser =-  single (toUnknownPropertyValue <$> text False) <?>-  "unknown property value"--variationModeParser :: Parser VariationMode-variationModeParser = single variationMode <?> "variation mode"--variationMode :: Parser VariationMode-variationMode = do-  value <- integral-  case toVariationMode value of-    Just mode -> return mode-    Nothing -> unexpected $ "variation mode " ++ show value
− src/Game/Goatee/Sgf/Property/Renderer.hs
@@ -1,326 +0,0 @@--- This file is part of Goatee.------ Copyright 2014 Bryan Gardiner------ Goatee is free software: you can redistribute it and/or modify--- it under the terms of the GNU Affero General Public License as published by--- the Free Software Foundation, either version 3 of the License, or--- (at your option) any later version.------ Goatee is distributed in the hope that it will be useful,--- but WITHOUT ANY WARRANTY; without even the implied warranty of--- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the--- GNU Affero General Public License for more details.------ You should have received a copy of the GNU Affero General Public License--- along with Goatee.  If not, see <http://www.gnu.org/licenses/>.--{-# LANGUAGE CPP #-}---- | Renderers of property values.------ Import "Game.Goatee.Sgf.Property" rather than importing this module.-module Game.Goatee.Sgf.Property.Renderer (-  renderColorBracketed,-  renderColorPretty,-  renderCoordElistBracketed,-  renderCoordElistPretty,-  renderCoordListBracketed,-  renderCoordListPretty,-  renderCoordPairListBracketed,-  renderCoordPairListPretty,-  renderDoubleBracketed,-  renderDoublePretty,-  renderGameResultBracketed,-  renderGameResultPretty,-  renderIntegralBracketed,-  renderIntegralPretty,-  renderLabelListBracketed,-  renderLabelListPretty,-  renderMoveBracketed,-  renderMovePretty,-  renderNoneBracketed,-  renderNonePretty,-  renderRealBracketed,-  renderRealPretty,-  renderRulesetBracketed,-  renderRulesetPretty,-  renderSimpleTextBracketed,-  renderSimpleTextPairBracketed,-  renderSimpleTextPairPretty,-  renderSimpleTextPretty,-  renderSizeBracketed,-  renderSizePretty,-  renderTextBracketed,-  renderTextPretty,-  renderUnknownPropertyBracketed,-  renderUnknownPropertyPretty,-  renderVariationModeBracketed,-  renderVariationModePretty,-  ) where--import Control.Monad (forM_, when)-#if MIN_VERSION_mtl(2,2,1)-import Control.Monad.Except (throwError)-#else-import Control.Monad.Error (throwError)-#endif-import Control.Monad.Writer (tell)-import Data.Char (chr, ord)-import Data.List (intersperse)-import Game.Goatee.Sgf.Renderer-import Game.Goatee.Sgf.Types--{-# ANN module "HLint: ignore Use <$>" #-}---- Internal renderers not corresponding to any particular value type.--bracketed :: Render () -> Render ()-bracketed x = tell "[" >> x >> tell "]"--renderLine :: Int -> Render ()-renderLine = rendererOf "line" $ \x ->-  if x >= 0 && x < 52-  then tell [chr $ x + (if x < 26 then ord 'a' else ord 'A' - 26)]-  else throwError $ "renderLine: Index not in [0, 52): " ++ show x--renderCoord :: Coord -> Render ()-renderCoord = rendererOf "coord" $ \(x, y) -> renderLine x >> renderLine y--renderCoordBracketed :: Coord -> Render ()-renderCoordBracketed = fmap bracketed renderCoord--renderCoordPretty :: Coord -> Render ()-renderCoordPretty = rendererOf "coord pretty" $ \(x, y) ->-  mapM_ tell ["(", show x, ", ", show y, ")"]--renderCoordPairPretty :: (Coord, Coord) -> Render ()-renderCoordPairPretty = rendererOf "coord pair pretty" $ \(a, b) -> do-  renderCoordPretty a-  tell "-"-  renderCoordPretty b--renderCoordPair :: (Coord, Coord) -> Render ()-renderCoordPair = rendererOf "coord pair" $ \(a, b) ->-  renderCoord a >> tell ":" >> renderCoord b--renderCoordPairBracketed :: (Coord, Coord) -> Render ()-renderCoordPairBracketed = fmap bracketed renderCoordPair--renderShowable :: Show a => a -> Render ()-renderShowable = tell . show--renderStringlike :: (Show a, Stringlike a) => Bool -> a -> Render ()-renderStringlike isComposed =-  rendererOf (if isComposed then "composed stringlike" else "stringlike") $ \str ->-  tell $ escape $ sgfToString str-  where escape [] = []-        escape (first:rest) | first `elem` specialChars = '\\':first:escape rest-                            | otherwise = first:escape rest-        -- TODO Deduplicate these characters with the parser:-        specialChars = if isComposed then ":]\\" else "]\\"---- Note that unlike the serialized SGF version, we don't care about escaping--- characters in composed strings.-renderStringlikePretty :: (Show a, Stringlike a) => a -> Render ()-renderStringlikePretty = rendererOf "stringlike pretty" $ tell . sgfToString---- Public renderers.--renderColorBracketed :: Color -> Render ()-renderColorBracketed color = bracketed $ tell $ case color of-  Black -> "[B]"-  White -> "[W]"--renderColorPretty :: Color -> Render ()-renderColorPretty Black = tell "Black"-renderColorPretty White = tell "White"--renderCoordElistBracketed :: CoordList -> Render ()-renderCoordElistBracketed = rendererOf "coord elist bracketed" $ \list ->-  if null $ expandCoordList list-  then tell "[]"-  else renderCoordListNonempty list--renderCoordElistPretty :: CoordList -> Render ()-renderCoordElistPretty = rendererOf "coord elist pretty" $ \list ->-  if null $ expandCoordList list-  then tell "empty"-  else renderCoordListNonemptyPretty list--renderCoordListBracketed :: CoordList -> Render ()-renderCoordListBracketed = rendererOf "coord list bracketed" $ \list ->-  if null $ expandCoordList list-  then throwError "renderCoordListBracketed: Unexpected empty CoordList."-  else renderCoordListNonempty list--renderCoordListPretty :: CoordList -> Render ()-renderCoordListPretty = rendererOf "coord list pretty" $ \list ->-  if null $ expandCoordList list-  then throwError "renderCoordListPretty: Unexpected empty CoordList."-  else renderCoordListNonemptyPretty list--renderCoordListNonempty :: CoordList -> Render ()-renderCoordListNonempty = rendererOf "coord list nonempty" $ \list -> do-  mapM_ renderCoordPairBracketed $ coordListRects list-  mapM_ renderCoordBracketed $ coordListSingles list--renderCoordListNonemptyPretty :: CoordList -> Render ()-renderCoordListNonemptyPretty = rendererOf "coord list nonempty pretty" $ \list ->-  sequence_ $ intersperse (tell ", ") $-  map renderCoordPairPretty (coordListRects list) ++-  map renderCoordPretty (coordListSingles list)--renderCoordPairListBracketed :: [(Coord, Coord)] -> Render ()-renderCoordPairListBracketed = rendererOf "coord pair list bracketed" $ \list ->-  if null list-  then throwError "renderCoordPairListBracketed: Unexpected empty list."-  else mapM_ renderCoordPairBracketed list--renderCoordPairListPretty :: [(Coord, Coord)] -> Render ()-renderCoordPairListPretty list =-  if null list-  then throwError "renderCoordPairListPretty: Unexpected empty list."-  else sequence_ $ intersperse (tell ", ") $ map renderCoordPairPretty list--renderDoubleBracketed :: DoubleValue -> Render ()-renderDoubleBracketed = fmap bracketed $ rendererOf "double" $ \double -> tell $ case double of-  Double1 -> "1"-  Double2 -> "2"--renderDoublePretty :: DoubleValue -> Render ()-renderDoublePretty = rendererOf "double pretty" $ tell . \double -> case double of-  Double1 -> "1"-  Double2 -> "2"--renderGameResultBracketed :: GameResult -> Render ()-renderGameResultBracketed = fmap bracketed $ rendererOf "game result" $ \result -> case result of-  GameResultWin color reason ->-    tell $-    (case color of { Black -> 'B'; White -> 'W' }) : '+' :-    (case reason of-        WinByScore diff -> show diff-        WinByResignation -> "R"-        WinByTime -> "T"-        WinByForfeit -> "F")-  GameResultDraw -> tell "0"-  GameResultVoid -> tell "Void"-  GameResultUnknown -> tell "?"-  GameResultOther text -> renderStringlike False text--renderGameResultPretty :: GameResult -> Render ()-renderGameResultPretty = rendererOf "game result pretty" $ \result -> case result of-  GameResultWin color reason ->-    tell $-    (case color of { Black -> 'B'; White -> 'W' }) : '+' :-    (case reason of-        WinByScore diff -> show diff-        WinByResignation -> "Resign"-        WinByTime -> "Time"-        WinByForfeit -> "Forfeit")-  GameResultDraw -> tell "Draw"-  GameResultVoid -> tell "Void"-  GameResultUnknown -> tell "Unknown"-  GameResultOther text -> renderStringlikePretty text--renderIntegralBracketed :: (Integral a, Show a) => a -> Render ()-renderIntegralBracketed = bracketed . rendererOf "integral" renderShowable--renderIntegralPretty :: (Integral a, Show a) => a -> Render ()-renderIntegralPretty = rendererOf "integral pretty" renderShowable--renderLabelListBracketed :: [(Coord, SimpleText)] -> Render ()-renderLabelListBracketed = rendererOf "label list" $ \list ->-  if null list-  then throwError "renderLabelListBracketed: Unexpected empty list."-  else forM_ list $ bracketed . \(coord, text) -> do-    renderCoord coord-    tell ":"-    renderStringlike True text--renderLabelListPretty :: [(Coord, SimpleText)] -> Render ()-renderLabelListPretty = rendererOf "label list pretty" $ \list ->-  if null list-  then throwError "renderLabelListPretty: Unexpected empty list."-  else sequence_ $ intersperse (tell ", ") $-       map (\(coord, text) -> do-               renderCoordPretty coord-               tell ":"-               renderStringlikePretty text)-       list--renderMoveBracketed :: Maybe Coord -> Render ()-renderMoveBracketed = rendererOf "move bracketed" $ maybe (tell "[]") renderCoordBracketed--renderMovePretty :: Maybe Coord -> Render ()-renderMovePretty = rendererOf "move pretty" $ maybe (tell "Pass") renderCoordPretty--renderNoneBracketed :: () -> Render ()-renderNoneBracketed = rendererOf "none bracketed" $ tell . const "[]"--renderNonePretty :: () -> Render ()-renderNonePretty = rendererOf "none pretty" $ tell . const ""--renderRealBracketed :: RealValue -> Render ()-renderRealBracketed = fmap bracketed $ rendererOf "real" $ renderShowable . fromRational--renderRealPretty :: RealValue -> Render ()-renderRealPretty = rendererOf "real pretty" renderShowable--renderRulesetBracketed :: Ruleset -> Render ()-renderRulesetBracketed = fmap bracketed $ rendererOf "ruleset" $ tell . fromRuleset--renderRulesetPretty :: Ruleset -> Render ()-renderRulesetPretty = rendererOf "ruleset pretty" $ tell . fromRuleset--renderSimpleTextPairBracketed :: (SimpleText, SimpleText) -> Render ()-renderSimpleTextPairBracketed = fmap bracketed $ rendererOf "simple text pair" $ \(a, b) -> do-  renderStringlike True a-  tell ":"-  renderStringlike True b--renderSimpleTextPairPretty :: (SimpleText, SimpleText) -> Render ()-renderSimpleTextPairPretty = fmap bracketed $ rendererOf "simple text pair pretty" $ \(a, b) -> do-  renderStringlike True a-  tell " "-  renderStringlike True b--renderSimpleTextBracketed :: SimpleText -> Render ()-renderSimpleTextBracketed = fmap bracketed $ rendererOf "simple text" $ renderStringlike False--renderSimpleTextPretty :: SimpleText -> Render ()-renderSimpleTextPretty = fmap bracketed $ rendererOf "simple text pretty" $ tell . fromSimpleText--renderSizeBracketed :: (Int, Int) -> Render ()-renderSizeBracketed = fmap bracketed $ rendererOf "size" $ \(x, y) -> do-  tell $ show x-  when (x /= y) $ tell $ ':' : show y--renderSizePretty :: (Int, Int) -> Render ()-renderSizePretty = rendererOf "size pretty" renderCoordPretty--renderTextBracketed :: Text -> Render ()-renderTextBracketed = fmap bracketed $ rendererOf "text" $ renderStringlike False--renderTextPretty :: Text -> Render ()-renderTextPretty = fmap bracketed $ rendererOf "text pretty" $ tell . fromText--renderUnknownPropertyBracketed :: UnknownPropertyValue -> Render ()-renderUnknownPropertyBracketed =-  fmap bracketed $ rendererOf "unknown property" $ renderStringlike False--renderUnknownPropertyPretty :: UnknownPropertyValue -> Render ()-renderUnknownPropertyPretty =-  rendererOf "unknown property pretty" $ tell . fromUnknownPropertyValue--renderVariationModeBracketed :: VariationMode -> Render ()-renderVariationModeBracketed =-  fmap bracketed $ rendererOf "variation mode" $ tell . show . fromVariationMode--renderVariationModePretty :: VariationMode -> Render ()-renderVariationModePretty = rendererOf "variation mode pretty" $ \(VariationMode source markup) ->-  tell $-  (case source of-      ShowChildVariations -> "Children"-      ShowCurrentVariations -> "Current") ++ " " ++-  (if markup then "Shown" else "Hidden")
− src/Game/Goatee/Sgf/Property/Value.hs
@@ -1,179 +0,0 @@--- This file is part of Goatee.------ Copyright 2014 Bryan Gardiner------ Goatee is free software: you can redistribute it and/or modify--- it under the terms of the GNU Affero General Public License as published by--- the Free Software Foundation, either version 3 of the License, or--- (at your option) any later version.------ Goatee is distributed in the hope that it will be useful,--- but WITHOUT ANY WARRANTY; without even the implied warranty of--- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the--- GNU Affero General Public License for more details.------ You should have received a copy of the GNU Affero General Public License--- along with Goatee.  If not, see <http://www.gnu.org/licenses/>.---- | Metadata about the types of property values.------ Import "Game.Goatee.Sgf.Property" rather than importing this module.-module Game.Goatee.Sgf.Property.Value (-  PropertyValueType, pvtParser, pvtRenderer, pvtRendererPretty,-  colorPvt,-  coordElistPvt,-  coordListPvt,-  coordPairListPvt,-  doublePvt,-  gameResultPvt,-  integralPvt,-  labelListPvt,-  movePvt,-  nonePvt,-  realPvt,-  rulesetPvt,-  simpleTextPairPvt,-  simpleTextPvt,-  sizePvt,-  textPvt,-  unknownPropertyPvt,-  variationModePvt,-  ) where--import qualified Game.Goatee.Sgf.Property.Parser as P-import qualified Game.Goatee.Sgf.Property.Renderer as R-import Game.Goatee.Sgf.Renderer-import Game.Goatee.Sgf.Types-import Text.ParserCombinators.Parsec (Parser)--data PropertyValueType a = PropertyValueType {-  pvtParser :: Parser a-  , pvtRenderer :: a -> Render ()-  , pvtRendererPretty :: a -> Render ()-  }--colorPvt :: PropertyValueType Color-colorPvt = PropertyValueType {-  pvtParser = P.colorParser-  , pvtRenderer = R.renderColorBracketed-  , pvtRendererPretty = R.renderColorPretty-  }--coordElistPvt :: PropertyValueType CoordList-coordElistPvt = PropertyValueType {-  pvtParser = P.coordElistParser-  , pvtRenderer = R.renderCoordElistBracketed-  , pvtRendererPretty = R.renderCoordElistPretty-  }--coordListPvt :: PropertyValueType CoordList-coordListPvt = PropertyValueType {-  pvtParser = P.coordListParser-  , pvtRenderer = R.renderCoordListBracketed-  , pvtRendererPretty = R.renderCoordListPretty-  }--coordPairListPvt :: PropertyValueType [(Coord, Coord)]-coordPairListPvt = PropertyValueType {-  pvtParser = P.coordPairListParser-  , pvtRenderer = R.renderCoordPairListBracketed-  , pvtRendererPretty = R.renderCoordPairListPretty-  }--doublePvt :: PropertyValueType DoubleValue-doublePvt = PropertyValueType {-  pvtParser = P.doubleParser-  , pvtRenderer = R.renderDoubleBracketed-  , pvtRendererPretty = R.renderDoublePretty-  }--gameResultPvt :: PropertyValueType GameResult-gameResultPvt = PropertyValueType {-  pvtParser = P.gameResultParser-  , pvtRenderer = R.renderGameResultBracketed-  , pvtRendererPretty = R.renderGameResultPretty-  }--integralPvt :: (Integral a, Read a, Show a) => PropertyValueType a-integralPvt = PropertyValueType {-  pvtParser = P.integralParser-  , pvtRenderer = R.renderIntegralBracketed-  , pvtRendererPretty = R.renderIntegralPretty-  }--labelListPvt :: PropertyValueType [(Coord, SimpleText)]-labelListPvt = PropertyValueType {-  pvtParser = P.labelListParser-  , pvtRenderer = R.renderLabelListBracketed-  , pvtRendererPretty = R.renderLabelListPretty-  }--movePvt :: PropertyValueType (Maybe Coord)-movePvt = PropertyValueType {-  pvtParser = P.moveParser-  , pvtRenderer = R.renderMoveBracketed-  , pvtRendererPretty = R.renderMovePretty-  }--nonePvt :: PropertyValueType ()-nonePvt = PropertyValueType {-  pvtParser = P.noneParser-  , pvtRenderer = R.renderNoneBracketed-  , pvtRendererPretty = R.renderNonePretty-  }--realPvt :: PropertyValueType RealValue-realPvt = PropertyValueType {-  pvtParser = P.realParser-  , pvtRenderer = R.renderRealBracketed-  , pvtRendererPretty = R.renderRealPretty-  }--rulesetPvt :: PropertyValueType Ruleset-rulesetPvt = PropertyValueType {-  pvtParser = P.rulesetParser-  , pvtRenderer = R.renderRulesetBracketed-  , pvtRendererPretty = R.renderRulesetPretty-  }--simpleTextPairPvt :: PropertyValueType (SimpleText, SimpleText)-simpleTextPairPvt = PropertyValueType {-  pvtParser = P.simpleTextPairParser-  , pvtRenderer = R.renderSimpleTextPairBracketed-  , pvtRendererPretty = R.renderSimpleTextPairPretty-  }--simpleTextPvt :: PropertyValueType SimpleText-simpleTextPvt = PropertyValueType {-  pvtParser = P.simpleTextParser-  , pvtRenderer = R.renderSimpleTextBracketed-  , pvtRendererPretty = R.renderSimpleTextPretty-  }--sizePvt :: PropertyValueType (Int, Int)-sizePvt = PropertyValueType {-  pvtParser = P.sizeParser-  , pvtRenderer = R.renderSizeBracketed-  , pvtRendererPretty = R.renderSizePretty-  }--textPvt :: PropertyValueType Text-textPvt = PropertyValueType {-  pvtParser = P.textParser-  , pvtRenderer = R.renderTextBracketed-  , pvtRendererPretty = R.renderTextPretty-  }--unknownPropertyPvt :: PropertyValueType UnknownPropertyValue-unknownPropertyPvt = PropertyValueType {-  pvtParser = P.unknownPropertyParser-  , pvtRenderer = R.renderUnknownPropertyBracketed-  , pvtRendererPretty = R.renderUnknownPropertyPretty-  }--variationModePvt :: PropertyValueType VariationMode-variationModePvt = PropertyValueType {-  pvtParser = P.variationModeParser-  , pvtRenderer = R.renderVariationModeBracketed-  , pvtRendererPretty = R.renderVariationModePretty-  }
− src/Game/Goatee/Sgf/Renderer.hs
@@ -1,56 +0,0 @@--- This file is part of Goatee.------ Copyright 2014 Bryan Gardiner------ Goatee is free software: you can redistribute it and/or modify--- it under the terms of the GNU Affero General Public License as published by--- the Free Software Foundation, either version 3 of the License, or--- (at your option) any later version.------ Goatee is distributed in the hope that it will be useful,--- but WITHOUT ANY WARRANTY; without even the implied warranty of--- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the--- GNU Affero General Public License for more details.------ You should have received a copy of the GNU Affero General Public License--- along with Goatee.  If not, see <http://www.gnu.org/licenses/>.--{-# LANGUAGE CPP #-}---- | Common definitions for a renderer that supports failure.-module Game.Goatee.Sgf.Renderer (-  Render,-  runRender,-  rendererOf,-  ) where--#if MIN_VERSION_mtl(2,2,1)-import Control.Monad.Except (Except, catchError, runExcept, throwError)-#else-import Control.Monad.Error (catchError, throwError)-#endif-import Control.Monad.Writer (WriterT, execWriterT)---- | A monad for accumulating string output with the possibility of failure.-#if MIN_VERSION_mtl(2,2,1)-type Render = WriterT String (Except String)-#else-type Render = WriterT String (Either String)-#endif---- | Returns either the rendered result on the right, or a message describing a--- failure on the left.-runRender :: Render a -> Either String String-#if MIN_VERSION_mtl(2,2,1)-runRender = runExcept . execWriterT-#else-runRender = execWriterT-#endif---- | Wraps a renderer in an exception handler that, when the renderer or--- something it calls fails, will add context about this renderer's invocation--- to the failure message.-rendererOf :: Show a => String -> (a -> Render ()) -> a -> Render ()-rendererOf description renderer value = catchError (renderer value) $ \message ->-  throwError $-  message ++ "\n    while trying to render " ++ description ++ " from " ++ show value ++ "."
− src/Game/Goatee/Sgf/Renderer/Tree.hs
@@ -1,56 +0,0 @@--- This file is part of Goatee.------ Copyright 2014 Bryan Gardiner------ Goatee is free software: you can redistribute it and/or modify--- it under the terms of the GNU Affero General Public License as published by--- the Free Software Foundation, either version 3 of the License, or--- (at your option) any later version.------ Goatee is distributed in the hope that it will be useful,--- but WITHOUT ANY WARRANTY; without even the implied warranty of--- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the--- GNU Affero General Public License for more details.------ You should have received a copy of the GNU Affero General Public License--- along with Goatee.  If not, see <http://www.gnu.org/licenses/>.---- | Functions for serializing SGF trees.-module Game.Goatee.Sgf.Renderer.Tree (-  renderCollection,-  ) where--import Control.Monad (forM_)-import Control.Monad.Writer (tell)-import Game.Goatee.Common-import Game.Goatee.Sgf.Property-import Game.Goatee.Sgf.Renderer-import Game.Goatee.Sgf.Tree---- | Renders an SGF 'Collection' to a string.-renderCollection :: Collection -> Render ()-renderCollection collection = do-  mapM_ renderGameTree $ collectionTrees collection-  tell "\n"---- | Recursively renders an SGF GameTree (as defined in the spec) rooted at the--- given node.-renderGameTree :: Node -> Render ()-renderGameTree node = do-  tell "("-  doWhileM node-    (\node' -> do renderNode node'-                  case nodeChildren node' of-                    [] -> return $ Left Nothing-                    child:[] -> return $ Right child-                    children -> return $ Left $ Just children)-    >>= maybe (return ()) (mapM_ renderGameTree)-  tell ")"---- | Renders a node and its properties without recurring to its children.-renderNode :: Node -> Render ()-renderNode node = do-  tell "\n;"-  forM_ (nodeProperties node) $ \property -> do-    tell $ propertyName property-    propertyValueRenderer property property
− src/Game/Goatee/Sgf/Tree.hs
@@ -1,175 +0,0 @@--- This file is part of Goatee.------ Copyright 2014 Bryan Gardiner------ Goatee is free software: you can redistribute it and/or modify--- it under the terms of the GNU Affero General Public License as published by--- the Free Software Foundation, either version 3 of the License, or--- (at your option) any later version.------ Goatee is distributed in the hope that it will be useful,--- but WITHOUT ANY WARRANTY; without even the implied warranty of--- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the--- GNU Affero General Public License for more details.------ You should have received a copy of the GNU Affero General Public License--- along with Goatee.  If not, see <http://www.gnu.org/licenses/>.---- | SGF data structures modelling the hierarchical game tree.-module Game.Goatee.Sgf.Tree (-  Collection(..), CollectionWithDeepEquality(..),-  Node(..), NodeWithDeepEquality(..),-  emptyNode, rootNode,-  findProperty, findProperty', findPropertyValue, findPropertyValue',-  addProperty, addChild, addChildAt,-  validateNode,-  ) where--import Control.Applicative ((<$>))-import Control.Monad (forM_, unless, when)-import Control.Monad.Writer (Writer, execWriter, tell)-import Data.Function (on)-import Data.List (find, groupBy, intercalate, nub, sortBy)-import Data.Version (showVersion)-import Game.Goatee.App (applicationName)-import Game.Goatee.Sgf.Property-import Game.Goatee.Sgf.Types-import Paths_goatee (version)---- | An SGF collection of game trees.-data Collection = Collection { collectionTrees :: [Node]-                             } deriving (Show)---- | See 'NodeWithDeepEquality'.-newtype CollectionWithDeepEquality = CollectionWithDeepEquality {-  collectionWithDeepEquality :: Collection-  } deriving (Show)--instance Eq CollectionWithDeepEquality where-  (==) = (==) `on` map NodeWithDeepEquality . collectionTrees . collectionWithDeepEquality---- | An SGF game tree node.  Unlike in the SGF spec, we represent a game tree--- with nodes uniformly, rather than having the separation between sequences and--- nodes.-data Node = Node { nodeProperties :: [Property]-                 , nodeChildren :: [Node]-                 } deriving (Show)---- | A wrapper around 'Node' with an 'Eq' instance that considers two nodes--- equal iff they contain the same properties (not necessarily in the same--- order), and if they contain children (in the same order) whose nodes are--- recursively equal.------ This instance is not on 'Node' directly because it is not the only obvious--- sense of equality (only comparing properties would be another one), and it's--- also potentially expensive.-newtype NodeWithDeepEquality = NodeWithDeepEquality { nodeWithDeepEquality :: Node }--instance Eq NodeWithDeepEquality where-  node1 == node2 =-    let n1 = nodeWithDeepEquality node1-        n2 = nodeWithDeepEquality node2-    in propertiesSorted n1 == propertiesSorted n2 &&-       deepChildren n1 == deepChildren n2-    where propertiesSorted = sortBy (compare `on` show) . nodeProperties-          deepChildren = map NodeWithDeepEquality . nodeChildren---- | A node with no properties and no children.-emptyNode :: Node-emptyNode = Node { nodeProperties = [], nodeChildren = [] }---- | Returns a fresh root 'Node' with 'AP' set to Goatee and optionally with a--- board size set via 'SZ'.-rootNode :: Maybe (Int, Int) -> Node-rootNode maybeSize =-  let props = FF 4 :-              GM 1 :-              AP (toSimpleText applicationName)-                 (toSimpleText $ showVersion version) :-              maybe [] ((:[]) . uncurry SZ) maybeSize-  in Node { nodeProperties = props-          , nodeChildren = []-          }---- | Searches for a matching property in a node's property list.-findProperty :: Descriptor a => a -> Node -> Maybe Property-findProperty descriptor node = findProperty' descriptor $ nodeProperties node---- | Searches for a matching property in a property list.-findProperty' :: Descriptor a => a -> [Property] -> Maybe Property-findProperty' = find . propertyPredicate---- | Retrieves the value of a property in a node's property list.-findPropertyValue :: ValuedDescriptor a v => a -> Node -> Maybe v-findPropertyValue descriptor node = propertyValue descriptor <$> findProperty descriptor node---- | Retrieves the value of a property in a property list.-findPropertyValue' :: ValuedDescriptor a v => a -> [Property] -> Maybe v-findPropertyValue' descriptor properties =-  propertyValue descriptor <$> findProperty' descriptor properties---- | Appends a property to a node's property list.-addProperty :: Property -> Node -> Node-addProperty prop node = node { nodeProperties = nodeProperties node ++ [prop] }---- | @addChild child parent@ appends a child node to a node's child list.-addChild :: Node -> Node -> Node-addChild child node = node { nodeChildren = nodeChildren node ++ [child] }---- | @addChildAt index child parent@ inserts a child node into a node's child--- list at the given index, shifting all nodes at or after the given index to--- the right.  The index must be in the range @[0, numberOfChildren]@.-addChildAt :: Int -> Node -> Node -> Node-addChildAt index child node =-  let (before, after) = splitAt index $ nodeChildren node-  in node { nodeChildren = before ++ child:after }---- | Returns a list of validation errors for the current node, an--- empty list if no errors are detected.-validateNode :: Bool -> Bool -> Node -> [String]-validateNode isRoot _{-seenGameNode-} node = execWriter $ do-  let props = nodeProperties node-  let propTypes = nub $ map propertyType $ nodeProperties node--  -- Check for move and setup properties in a single node.-  when (MoveProperty `elem` propTypes && SetupProperty `elem` propTypes) $-    tell ["Node contains move and setup properties."]--  -- Check for root properties in non-root nodes.-  let rootProps = filter ((RootProperty ==) . propertyType) props-  when (not isRoot && not (null rootProps)) $-    tell $ map (\p -> "Root property found on non-root node: " ++-                      show p ++ ".")-           rootProps--  -- TODO Check for game-info properties.--  -- Check for coordinates marked multiple times.-  validateNodeDuplicates props getMarkedCoords $ \group ->-    tell ["Coordinate " ++ show (fst $ head group) ++-          " is specified multiple times in properties " ++-          intercalate ", " (map snd group) ++ "."]--  -- TODO Validate recursively.--  where getMarkedCoords (CR cs) = tagMarkedCoords cs "CR"-        getMarkedCoords (MA cs) = tagMarkedCoords cs "MA"-        getMarkedCoords (SL cs) = tagMarkedCoords cs "SL"-        getMarkedCoords (SQ cs) = tagMarkedCoords cs "SQ"-        getMarkedCoords (TR cs) = tagMarkedCoords cs "TR"-        getMarkedCoords _ = []--        tagMarkedCoords cs tag = map (\x -> (x, tag)) $ expandCoordList cs--validateNodeDuplicates :: (Eq v, Ord v)-                          => [Property]-                          -> (Property -> [(v, t)])-                          -> ([(v, t)] -> Writer [String] ())-                          -> Writer [String] ()-validateNodeDuplicates props getTaggedElts errAction =-  let groups = groupBy ((==) `on` fst) $-               sortBy (compare `on` fst) $-               concatMap getTaggedElts props-  in forM_ groups $ \group ->-       unless (null $ tail group) $-         errAction group
− src/Game/Goatee/Sgf/Types.hs
@@ -1,389 +0,0 @@--- This file is part of Goatee.------ Copyright 2014 Bryan Gardiner------ Goatee is free software: you can redistribute it and/or modify--- it under the terms of the GNU Affero General Public License as published by--- the Free Software Foundation, either version 3 of the License, or--- (at your option) any later version.------ Goatee is distributed in the hope that it will be useful,--- but WITHOUT ANY WARRANTY; without even the implied warranty of--- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the--- GNU Affero General Public License for more details.------ You should have received a copy of the GNU Affero General Public License--- along with Goatee.  If not, see <http://www.gnu.org/licenses/>.---- | Constants and data types for property values used in SGF game trees.-module Game.Goatee.Sgf.Types (-  supportedFormatVersions, defaultFormatVersion, supportedGameTypes,-  boardSizeDefault, boardSizeMin, boardSizeMax,-  Coord, CoordList, coordListSingles, coordListRects, coord1, coords, coords',-  emptyCoordList, expandCoordList, buildCoordList,-  RealValue,-  Stringlike (..),-  Text, fromText, toText,-  SimpleText, fromSimpleText, toSimpleText,-  UnknownPropertyValue, fromUnknownPropertyValue, toUnknownPropertyValue,-  DoubleValue (..),-  Color (..), cnot,-  VariationMode (..), VariationModeSource (..), defaultVariationMode,-  toVariationMode, fromVariationMode,-  ArrowList, LineList, LabelList, Mark (..),-  GameResult (..),-  WinReason (..),-  Ruleset (..), RulesetType (..), fromRuleset, toRuleset,-  ) where--import Data.Char (isSpace)-import Data.Function (on)-import Data.List (delete, groupBy, partition, sort)---- TODO Support FF versions 1-4.-supportedFormatVersions :: [Int]-supportedFormatVersions = [4]---- | The default SGF version to use when @FF[]@ is not specified in a root node.------ This value is actually INCORRECT: SGF defines it to be 1, but because we--- don't support version 1 yet, for the sake of ignoring this issue (for now!)--- in tests, we fix the default to be 4.------ TODO Fix the default version to be 1 as SGF mandates.-defaultFormatVersion :: Int-defaultFormatVersion = 4--supportedGameTypes :: [Int]-supportedGameTypes = [1 {- Go -}]---- | The default size of the board.  The FF[4] SGF spec says that the default Go--- board is 19x19 square.-boardSizeDefault :: Int-boardSizeDefault = 19---- | The minimum board size allowed by FF[4], 1.-boardSizeMin :: Int-boardSizeMin = 1---- | The maximum board size allowed by FF[4], 52.-boardSizeMax :: Int-boardSizeMax = 52---- | A coordinate on a Go board.  @(0, 0)@ refers to the upper-left corner of--- the board.  The first component is the horizontal position; the second--- component is the vertical position.-type Coord = (Int, Int)---- | A structure for compact representation of a list of coordinates.  Contains--- a list of individual points, as well as a list of rectangles of points--- denoted by an ordered pair of the upper-left point and the lower-right point.--- The union of the single points and points contained within rectangles make up--- all of the points a @CoordList@ represents.  There is no rule saying that--- adjacent points have to be grouped into rectangles; it's perfectly valid--- (although possibly inefficient) to never use rectangles.------ For any @CoordList@, all of the following hold:------ 1. Any point may be specified at most once, either in the singles list or in--- a single rectangle.------ 2. For a rectangle @((x0,y0), (x1,y1))@, @x0 <= x1@ and @y0 <= y1@ and--- @(x0,y0) /= (x1,y1)@ (otherwise the point belongs in the singles list).-data CoordList = CoordList {-  coordListSingles :: [Coord]-  -- ^ Returns the single points in a 'CoordList'.-  , coordListRects :: [(Coord, Coord)]-    -- ^ Returns the rectangles in a 'CoordList'.-  } deriving (Show)---- | Equality is based on unordered, set equality of the underlying points.-instance Eq CoordList where-  (==) = (==) `on` sort . expandCoordList---- | Constructs a 'CoordList' containing a single point.-coord1 :: Coord -> CoordList-coord1 xy = CoordList { coordListSingles = [xy]-                      , coordListRects = []-                      }---- | Constructs a 'CoordList' containing the given single points.  For rectangle--- detection, use 'buildCoordList'.-coords :: [Coord] -> CoordList-coords singles = CoordList { coordListSingles = singles-                           , coordListRects = []-                           }---- | Constructs a 'CoordList' containing the given single points and rectangles.-coords' :: [Coord] -> [(Coord, Coord)] -> CoordList-coords' singles rects = CoordList { coordListSingles = singles-                                  , coordListRects = rects-                                  }---- | A 'CoordList' that contains no points.-emptyCoordList :: CoordList-emptyCoordList = CoordList { coordListSingles = []-                           , coordListRects = []-                           }---- | Converts a compact 'CoordList' to a list of coordinates.-expandCoordList :: CoordList -> [Coord]-expandCoordList cl = coordListSingles cl ++-                     foldr (\r@((x0, y0), (x1, y1)) rest ->-                             if x0 > x1 || y0 > y1-                             then error ("Invalid coord. rectangle: " ++ show r)-                             else [(x, y) | x <- [x0..x1], y <- [y0..y1]] ++ rest)-                           []-                           (coordListRects cl)---- | Constructs a 'CoordList' from a list of 'Coord's, doing some--- not-completely-stupid rectangle detection.  The order of data in the result--- is unspecified.-buildCoordList :: [Coord] -> CoordList--- This algorithm doesn't generate the smallest result.  For the following--- input:------ F F T T--- T T T T--- T T F F------ It will return [ca:da][ab:db][ac:bc] rather than the shorter [ca:db][ab:bc].-buildCoordList = toCoordList . generateRects 0 [] . buildTruePairs . toGrid-  where -- | Constructs a row-major grid of booleans where an coordinate is true-        -- iff it is in the given list.-        toGrid :: [Coord] -> [[Bool]]-        toGrid [] = []-        toGrid coords = let x1 = maximum $ map fst coords-                            y1 = maximum $ map snd coords-                        in [[(x,y) `elem` coords | x <- [0..x1]] | y <- [0..y1]]--        -- | For each row, converts a list of booleans into a list of pairs-        -- where each pair represents a consecutive run of true values.  The-        -- pair indicates the indices of the first and last boolean in each run.-        buildTruePairs :: [[Bool]] -> [[(Int, Int)]]-        buildTruePairs = map $-                         concatMap extractTrueGroups .-                         groupBy ((==) `on` snd) .-                         zip [0..]--        -- | Given a run of indexed booleans with the same boolean value within-        -- a row, returns @[(startIndex, endIndex)]@ if the value is true, and-        -- @[]@ if the value is false.-        extractTrueGroups :: [(Int, Bool)] -> [(Int, Int)]-        extractTrueGroups list@((start, True):_) = [(start, fst (last list))]-        extractTrueGroups _ = []--        -- | Converts the lists of true pairs for all rows into a list of-        -- @(Coord, Coord)@ rectangles.  We repeatedly grab the first span in-        -- the first row, and see how many leading rows contain that exact span.-        -- Then we build a (maybe multi-row) rectangle for the span, remove the-        -- span from all leading rows, and repeat.  When the first row becomes-        -- empty, we drop it and increment a counter that keeps track of our-        -- first row's y-position.-        generateRects :: Int -> [(Coord, Coord)] -> [[(Int, Int)]] -> [(Coord, Coord)]-        generateRects _ acc [] = acc-        generateRects topRowOffset acc ([]:rows) = generateRects (topRowOffset + 1) acc rows-        generateRects topRowOffset acc rows@((span:_):_) =-          let rowsWithSpan = matchRowsWithSpan span rows-              rowsWithSpanCount = length rowsWithSpan-          in generateRects topRowOffset-                           (((fst span, topRowOffset),-                             (snd span, topRowOffset + rowsWithSpanCount - 1)) : acc)-                           (rowsWithSpan ++ drop rowsWithSpanCount rows)--        -- | Determines how many leading rows contain the given span.  A list of-        -- all the matching rows is returned, with the span deleted from each.-        matchRowsWithSpan :: (Int, Int) -> [[(Int, Int)]] -> [[(Int, Int)]]-        matchRowsWithSpan span (row:rows)-          | span `elem` row = delete span row : matchRowsWithSpan span rows-          | otherwise = []-        matchRowsWithSpan _ [] = []--        -- | Builds a 'CoordList' from simple @(Coord, Coord)@ rectangles.-        toCoordList :: [(Coord, Coord)] -> CoordList-        toCoordList rects =-          let (singles, properRects) = partition (uncurry (==)) rects-          in coords' (map fst singles) properRects---- | An SGF real value.-type RealValue = Rational---- | A class for SGF data types that are coercable to and from strings.------ The construction of an SGF value with 'stringToSgf' may process the input,--- such that the resulting stringlike value does not represent the same string--- as the input.  In other words, the following does *not* necessarily hold:------ > sgfToString . stringToSgf = id   (does not necessarily hold!)------ The following does hold, however:------ > stringToSgf . sgfToString = id-class Stringlike a where-  -- | Extracts the string value from an SGF value.-  sgfToString :: a -> String--  -- | Creates an SGF value from a string value.-  stringToSgf :: String -> a---- | An SGF text value.-newtype Text = Text { fromText :: String-                      -- ^ Converts an SGF 'Text' to a string.-                    }-             deriving (Eq, Show)--instance Stringlike Text where-  sgfToString = fromText-  stringToSgf = toText---- | Converts a string to an SGF 'Text'.-toText :: String -> Text-toText = Text---- | An SGF SimpleText value.-newtype SimpleText = SimpleText { fromSimpleText :: String-                                  -- ^ Converts an SGF 'SimpleText' to a string.-                                }-                   deriving (Eq, Show)--instance Stringlike SimpleText where-  sgfToString = fromSimpleText-  stringToSgf = toSimpleText--sanitizeSimpleText :: String -> String-sanitizeSimpleText = map (\c -> if isSpace c then ' ' else c)---- | Converts a string to an SGF 'SimpleText', replacing all whitespaces--- (including newlines) with spaces.-toSimpleText :: String -> SimpleText-toSimpleText = SimpleText . sanitizeSimpleText---- | The value type for an 'UnknownProperty'.  Currently represented as a--- string.-data UnknownPropertyValue = UnknownPropertyValue {-  fromUnknownPropertyValue :: String-  -- ^ Returns the string contained within the 'UnknownProperty' this value is-  -- from.-  } deriving (Eq, Show)--instance Stringlike UnknownPropertyValue where-  sgfToString = fromUnknownPropertyValue-  stringToSgf = toUnknownPropertyValue---- | Constructs a value for a 'UnknownProperty'.-toUnknownPropertyValue :: String -> UnknownPropertyValue-toUnknownPropertyValue = UnknownPropertyValue---- | An SGF double value: either 1 or 2, nothing else.-data DoubleValue = Double1-                 | Double2-                 deriving (Eq, Show)---- | Stone color: black or white.-data Color = Black-           | White-           deriving (Eq, Show)---- | Returns the logical negation of a stone color, yang for yin and--- yin for yang.-cnot :: Color -> Color-cnot Black = White-cnot White = Black---- | SGF flags that control how move variations are to be presented while--- displaying the game.-data VariationMode = VariationMode {-  variationModeSource :: VariationModeSource-  -- ^ Which moves to display as variations.-  , variationModeBoardMarkup :: Bool-    -- ^ Whether to overlay variations on the board.-  } deriving (Eq, Show)---- | An enumeration that describes which variations are shown.-data VariationModeSource =-  ShowChildVariations-  -- ^ Show children of the current move.-  | ShowCurrentVariations-    -- ^ Show alternatives to the current move.-  deriving (Bounded, Enum, Eq, Show)---- | The default variation mode as defined by the SGF spec is @VariationMode--- ShowChildVariations True@.-defaultVariationMode :: VariationMode-defaultVariationMode = VariationMode ShowChildVariations True---- | Parses a numeric variation mode, returning nothing if the number is--- invalid.-toVariationMode :: Int -> Maybe VariationMode-toVariationMode n = case n of-  0 -> Just $ VariationMode ShowChildVariations True-  1 -> Just $ VariationMode ShowCurrentVariations True-  2 -> Just $ VariationMode ShowChildVariations False-  3 -> Just $ VariationMode ShowCurrentVariations False-  _ -> Nothing---- | Returns the integer value for a variation mode.-fromVariationMode :: VariationMode -> Int-fromVariationMode mode = case mode of-  VariationMode ShowChildVariations True -> 0-  VariationMode ShowCurrentVariations True -> 1-  VariationMode ShowChildVariations False -> 2-  VariationMode ShowCurrentVariations False -> 3---- | A list of arrows, each specified as @(startCoord, endCoord)@.-type ArrowList = [(Coord, Coord)]---- | A list of lines, each specified as @(startCoord, endCoord)@.-type LineList = [(Coord, Coord)]---- | A list of labels, each specified with a string and a coordinate about which--- to center the string.-type LabelList = [(Coord, SimpleText)]---- | The markings that SGF supports annotating coordinates with.-data Mark = MarkCircle | MarkSquare | MarkTriangle | MarkX | MarkSelected-          deriving (Bounded, Enum, Eq, Show)--data GameResult = GameResultWin Color WinReason-                | GameResultDraw-                | GameResultVoid-                | GameResultUnknown-                | GameResultOther SimpleText-                deriving (Eq, Show)--data WinReason = WinByScore RealValue-               | WinByResignation-               | WinByTime-               | WinByForfeit-               deriving (Eq, Show)---- | A ruleset used for a Go game.  Can be one of the rulesets defined by the--- SGF specification, or a custom string.-data Ruleset = KnownRuleset RulesetType-             | UnknownRuleset String-             deriving (Eq, Show)---- | The rulesets defined by the SGF specification, for use with 'Ruleset'.-data RulesetType = RulesetAga-                 | RulesetIng-                 | RulesetJapanese-                 | RulesetNewZealand-                 deriving (Bounded, Enum, Eq, Show)---- | Returns the string representation for a ruleset.-fromRuleset :: Ruleset -> String-fromRuleset ruleset = case ruleset of-  KnownRuleset RulesetAga -> "AGA"-  KnownRuleset RulesetIng -> "Goe"-  KnownRuleset RulesetJapanese -> "Japanese"-  KnownRuleset RulesetNewZealand -> "NZ"-  UnknownRuleset str -> str---- | Parses a string representation of a ruleset.-toRuleset :: String -> Ruleset-toRuleset str = case str of-  "AGA" -> KnownRuleset RulesetAga-  "Goe" -> KnownRuleset RulesetIng-  "Japanese" -> KnownRuleset RulesetJapanese-  "NZ" -> KnownRuleset RulesetNewZealand-  _ -> UnknownRuleset str
+ tests/Game/Goatee/Common/BigfloatTest.hs view
@@ -0,0 +1,269 @@+-- This file is part of Goatee.+--+-- Copyright 2014 Bryan Gardiner+--+-- Goatee is free software: you can redistribute it and/or modify+-- it under the terms of the GNU Affero General Public License as published by+-- the Free Software Foundation, either version 3 of the License, or+-- (at your option) any later version.+--+-- Goatee is distributed in the hope that it will be useful,+-- but WITHOUT ANY WARRANTY; without even the implied warranty of+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the+-- GNU Affero General Public License for more details.+--+-- You should have received a copy of the GNU Affero General Public License+-- along with Goatee.  If not, see <http://www.gnu.org/licenses/>.++module Game.Goatee.Common.BigfloatTest (tests) where++import Game.Goatee.Common.Bigfloat+import Game.Goatee.Test.Common+import Prelude hiding (exponent, significand)+import Test.HUnit ((~:), (@=?), Assertion, Test (TestList))++assertBigfloat :: Integer -> Int -> Bigfloat -> Assertion+assertBigfloat v e x =+  (v, e) @=? (significand x, exponent x)++testAddition :: String -> String -> Integer -> Int -> Test+testAddition x y v e =+  x ++ " + " ++ y ~: assertBigfloat v e $ read x + read y++testSubtraction :: String -> String -> Integer -> Int -> Test+testSubtraction x y v e =+  x ++ " - " ++ y ~: assertBigfloat v e $ read x - read y++testMultiplication :: String -> String -> Integer -> Int -> Test+testMultiplication x y v e =+  x ++ " * " ++ y ~: assertBigfloat v e $ read x * read y++tests = "Game.Goatee.Common.Bigfloat" ~: TestList+  [ encodeTests+  , readInstanceTests+  , showInstanceTests+  , fromDoubleTests+  , toDoubleTests+  , numInstanceTests+  , eqInstanceTests+  , ordInstanceTests+  ]++encodeTests = "encode" ~: TestList+  [ "zero" ~: assertBigfloat 0 0 $ encode 0 0++  , "whole numbers" ~: TestList+    [ "1" ~: assertBigfloat 1 0 $ encode 1 0+    , "5" ~: assertBigfloat 5 0 $ encode 5 0+    , "12" ~: assertBigfloat 12 0 $ encode 12 0+    , "1031" ~: assertBigfloat 1031 0 $ encode 1031 0++    , "-1" ~: assertBigfloat (-1) 0 $ encode (-1) 0+    , "-5" ~: assertBigfloat (-5) 0 $ encode (-5) 0+    , "-12" ~: assertBigfloat (-12) 0 $ encode (-12) 0+    , "-1031" ~: assertBigfloat (-1031) 0 $ encode (-1031) 0+    ]++  , "fractional numbers" ~: TestList+    [ "0.1" ~: assertBigfloat 1 (-1) $ encode 1 (-1)+    , "0.5" ~: assertBigfloat 5 (-1) $ encode 5 (-1)+    , "1.5" ~: assertBigfloat 15 (-1) $ encode 15 (-1)+    , "123.456" ~: assertBigfloat 123456 (-3) $ encode 123456 (-3)+    , "0.00125" ~: assertBigfloat 125 (-5) $ encode 125 (-5)++    , "-0.1" ~: assertBigfloat (-1) (-1) $ encode (-1) (-1)+    , "-0.5" ~: assertBigfloat (-5) (-1) $ encode (-5) (-1)+    , "-1.5" ~: assertBigfloat (-15) (-1) $ encode (-15) (-1)+    , "-123.456" ~: assertBigfloat (-123456) (-3) $ encode (-123456) (-3)+    , "-0.00125" ~: assertBigfloat (-125) (-5) $ encode (-125) (-5)+    ]++  , "extreme numbers" ~: TestList+    [ "12345678901234567890123456789012345678901234567890" ~:+      assertBigfloat 1234567890123456789012345678901234567890123456789 1 $+      encode 12345678901234567890123456789012345678901234567890 0++    , "0.00000000000000000000000000000000000000000000000001" ~:+      assertBigfloat 1 (-50) $ encode 1 (-50)++    , "123456789012345678901234567890.123456789012345678901234567890" ~:+      assertBigfloat 12345678901234567890123456789012345678901234567890123456789 (-29) $+      encode 123456789012345678901234567890123456789012345678901234567890 (-30)+    ]++  , "reduction" ~: TestList+    [ "50.0" ~: assertBigfloat 5 1 $ encode 500 (-1)+    , "123.456000" ~: assertBigfloat 123456 (-3) $ encode 123456000 (-6)++    , "-50.0" ~: assertBigfloat (-5) 1 $ encode (-500) (-1)+    , "-123.456000" ~: assertBigfloat (-123456) (-3) $ encode (-123456000) (-6)+    ]+  ]++readInstanceTests = "Read instance" ~: TestList+  [ "0" ~: assertBigfloat 0 0 $ read "0"+  , "3" ~: assertBigfloat 3 0 $ read "3"+  , "10" ~: assertBigfloat 1 1 $ read "10"+  , "51200" ~: assertBigfloat 512 2 $ read "51200"+  , "5120" ~: assertBigfloat 512 1 $ read "5120"+  , "512" ~: assertBigfloat 512 0 $ read "512"+  , "512.0" ~: assertBigfloat 512 0 $ read "512.0"+  , "51.2" ~: assertBigfloat 512 (-1) $ read "51.2"+  , "05.12" ~: assertBigfloat 512 (-2) $ read "05.12"+  , "0.512" ~: assertBigfloat 512 (-3) $ read "0.512"+  , "0.051200" ~: assertBigfloat 512 (-4) $ read "0.051200"++  , "-0" ~: assertBigfloat 0 0 $ read "-0"+  , "-3" ~: assertBigfloat (-3) 0 $ read "-3"+  , "-10" ~: assertBigfloat (-1) 1 $ read "-10"+  , "-51200" ~: assertBigfloat (-512) 2 $ read "-51200"+  , "-5120" ~: assertBigfloat (-512) 1 $ read "-5120"+  , "-512" ~: assertBigfloat (-512) 0 $ read "-512"+  , "-512.0" ~: assertBigfloat (-512) 0 $ read "-512.0"+  , "-51.2" ~: assertBigfloat (-512) (-1) $ read "-51.2"+  , "-05.12" ~: assertBigfloat (-512) (-2) $ read "-05.12"+  , "-0.512" ~: assertBigfloat (-512) (-3) $ read "-0.512"+  , "-0.051200" ~: assertBigfloat (-512) (-4) $ read "-0.051200"++  , "0e0" ~: assertBigfloat 0 0 $ read "0e0"+  , "0e-0" ~: assertBigfloat 0 0 $ read "0e-0"+  , "0e1" ~: assertBigfloat 0 0 $ read "0e1"+  , "0e-1" ~: assertBigfloat 0 0 $ read "0e-1"+  , "0.0e0" ~: assertBigfloat 0 0 $ read "0.0e0"+  , "1e0" ~: assertBigfloat 1 0 $ read "1e0"+  , "1e1" ~: assertBigfloat 1 1 $ read "1e1"+  , "1e2" ~: assertBigfloat 1 2 $ read "1e2"+  , "1e-1" ~: assertBigfloat 1 (-1) $ read "1e-1"+  , "1e-2" ~: assertBigfloat 1 (-2) $ read "1e-2"+  , "100e2" ~: assertBigfloat 1 4 $ read "100e2"+  , "100e-1" ~: assertBigfloat 1 1 $ read "100e-1"+  , "100e-3" ~: assertBigfloat 1 (-1) $ read "100e-3"+  , "0.0002e-5" ~: assertBigfloat 2 (-9) $ read "0.0002e-5"+  , "0.0002e2" ~: assertBigfloat 2 (-2) $ read "0.0002e2"+  , "0.0002e5" ~: assertBigfloat 2 1 $ read "0.0002e5"+  ]++showInstanceTests = "Show instance" ~: TestList+  [ "0" ~: "0" @=? show (encode 0 0)++  , "3" ~: "3" @=? show (encode 3 0)+  , "10" ~: "10" @=? show (encode 1 1)+  , "51200" ~: "51200" @=? show (encode 512 2)+  , "5120" ~: "5120" @=? show (encode 512 1)+  , "512" ~: "512" @=? show (encode 512 0)+  , "51.2" ~: "51.2" @=? show (encode 512 (-1))+  , "5.12" ~: "5.12" @=? show (encode 512 (-2))+  , "0.512" ~: "0.512" @=? show (encode 512 (-3))+  , "0.0512" ~: "0.0512" @=? show (encode 512 (-4))++  , "-3" ~: "-3" @=? show (encode (-3) 0)+  , "-10" ~: "-10" @=? show (encode (-10) 0)+  , "-51200" ~: "-51200" @=? show (encode (-512) 2)+  , "-5120" ~: "-5120" @=? show (encode (-512) 1)+  , "-512" ~: "-512" @=? show (encode (-512) 0)+  , "-51.2" ~: "-51.2" @=? show (encode (-512) (-1))+  , "-5.12" ~: "-5.12" @=? show (encode (-512) (-2))+  , "-0.512" ~: "-0.512" @=? show (encode (-512) (-3))+  , "-0.0512" ~: "-0.0512" @=? show (encode (-512) (-4))+  ]++fromDoubleTests = "fromDouble" ~: TestList+  [ "0" ~: read "0" @=? fromDouble 0+  , "120" ~: read "120" @=? fromDouble 120+  , "12" ~: read "12" @=? fromDouble 12+  , "1.2" ~: read "1.2" @=? fromDouble 1.2+  , "0.12" ~: read "0.12" @=? fromDouble 0.12+  , "0.012" ~: read "0.012" @=? fromDouble 0.012+  , "1234567890123456" ~: read "1234567890123456" @=? fromDouble 1234567890123456++  , "-0" ~: read "0" @=? fromDouble (-0)+  , "-120" ~: read "-120" @=? fromDouble (-120)+  , "-12" ~: read "-12" @=? fromDouble (-12)+  , "-1.2" ~: read "-1.2" @=? fromDouble (-1.2)+  , "-0.12" ~: read "-0.12" @=? fromDouble (-0.12)+  , "-0.012" ~: read "-0.012" @=? fromDouble (-0.012)+  , "-1234567890123456" ~: read "-1234567890123456" @=? fromDouble (-1234567890123456)+  ]++toDoubleTests = "toDouble" ~: TestList+  [ "0" ~: 0 @=? toDouble (read "0")+  , "30" ~: 30 @=? toDouble (read "30")+  , "0.125" ~: 0.125 @=? toDouble (read "0.125")+  , "-1234567890" ~: (-1234567890) @=? toDouble (read "-1234567890")+  ]++numInstanceTests = "Num instance" ~: TestList+  [ "addition" ~: TestList+    [ testAddition "2" "5" 7 0+    , testAddition "5" "2" 7 0+    , testAddition "123" "4560" 4683 0+    , testAddition "4560" "123" 4683 0+    , testAddition "1000" "0.01" 100001 (-2)+    , testAddition "-120" "120" 0 0+    , testAddition "-120" "119.9" (-1) (-1)+    ]++  , "subtraction" ~: TestList+    [ testSubtraction "2" "5" (-3) 0+    , testSubtraction "5" "2" 3 0+    , testSubtraction "123" "4560" (-4437) 0+    , testSubtraction "4560" "123" 4437 0+    , testSubtraction "1000" "0.01" 99999 (-2)+    , testSubtraction "119.9" "120" (-1) (-1)+    , testSubtraction "120" "120" 0 0+    ]++  , "multiplication" ~: TestList+    [ testMultiplication "0" "0" 0 0+    , testMultiplication "0" "1" 0 0+    , testMultiplication "10" "0" 0 0+    , testMultiplication "10000" "0.001" 1 1+    , testMultiplication "10000" "0.0001" 1 0+    , testMultiplication "10000" "0.00001" 1 (-1)+    , testMultiplication "123.456" "10000.4" 12346093824 (-4)+    ]++  , "negation" ~: TestList+    [ "0" ~: assertBigfloat 0 0 $ negate $ read "0"+    , "100" ~: assertBigfloat (-1) 2 $ negate $ read "100"+    , "-4.01" ~: assertBigfloat 401 (-2) $ negate $ read "-4.01"+    ]++  , "abs" ~: TestList+    [ "0" ~: assertBigfloat 0 0 $ abs $ read "0"+    , "100" ~: assertBigfloat 1 2 $ abs $ read "100"+    , "-4.01" ~: assertBigfloat 401 (-2) $ abs $ read "-4.01"+    ]++  , "signum" ~: TestList+    [ "0" ~: assertBigfloat 0 0 $ signum $ read "0"+    , "100" ~: assertBigfloat 1 0 $ signum $ read "100"+    , "-4.01" ~: assertBigfloat (-1) 0 $ signum $ read "-4.01"+    ]++  , "fromInteger" ~: TestList+    [ "0" ~: assertBigfloat 0 0 (0 :: Bigfloat)+    , "100" ~: assertBigfloat 1 2 (100 :: Bigfloat)+    , "-12" ~: assertBigfloat (-12) 0 (-12 :: Bigfloat)+    ]+  ]++eqInstanceTests = "Eq instance" ~: TestList+  [ "0 == 0" ~: encode 0 0 @=? encode 0 0+  , "1 == 1" ~: encode 1 0 @=? encode 1 0+  , "1 == 1.0" ~: encode 1 0 @=? encode 10 (-1)+  , "-1 == -1" ~: encode (-1) 0 @=? encode (-1) 0++  , "0 /= 1" ~: encode 0 0 @/=? encode 1 0+  , "1 /= -1" ~: encode 1 0 @/=? encode (-1) 0+  , "50 /= 5" ~: encode 50 0 @/=? encode 5 0+  , "314 /= 0.314" ~: encode 314 0 @/=? encode 314 (-3)+  ]++ordInstanceTests = "ordering" ~: TestList+  [ "0 EQ 0" ~: EQ @=? encode 0 0 `compare` encode 0 0+  , "0 LT 5" ~: LT @=? encode 0 0 `compare` encode 5 0+  , "-1 LT 5" ~: LT @=? encode (-1) 0 `compare` encode 5 0+  , "5 GT 0" ~: GT @=? encode 5 0 `compare` encode 0 0+  , "0.123 LT 123" ~: LT @=? encode 123 (-3) `compare` encode 123 0+  ]
tests/Game/Goatee/CommonTest.hs view
@@ -19,178 +19,177 @@  import qualified Control.Monad.State as State import Control.Monad.Identity (runIdentity)-import Control.Monad.State (StateT, get, put, runStateT)+import Control.Monad.State (get, put, runStateT) import Control.Monad.Writer (execWriter, tell) import Data.IORef (modifyIORef, newIORef, readIORef)-import Data.Monoid (mappend, mempty) import Game.Goatee.Common-import Test.Framework (testGroup)-import Test.Framework.Providers.HUnit (testCase)-import Test.HUnit ((@=?), assertFailure)+import Test.HUnit ((~:), (@=?), Test (TestList), assertFailure)  {-# ANN module "HLint: ignore Use camelCase" #-} -tests = testGroup "Game.Goatee.Common" [-  listDeleteIndexTests,-  listReplaceTests,-  onLeftTests,-  onRightTests,-  andEithersTests,-  forTests,-  mapTupleTests,-  whenMaybeTests,-  condTests,-  if'Tests,-  andMTests,-  forIndexM_Tests,-  whileMTests,-  whileM'Tests,-  doWhileMTests,-  seqTests+tests = "Game.Goatee.Common" ~: TestList+  [ listDeleteAtTests+  , listInsertAtTests+  , listReplaceTests+  , andEithersTests+  , forTests+  , mapTupleTests+  , whenMaybeTests+  , condTests+  , if'Tests+  , andMTests+  , forIndexM_Tests+  , whileMTests+  , whileM'Tests+  , doWhileMTests   ] -listDeleteIndexTests = testGroup "listDeleteIndex" [-  testCase "deletes nothing from an empty list" $ do-    [] @=? listDeleteIndex (-1) ([] :: [()])-    [] @=? listDeleteIndex 0 ([] :: [()])-    [] @=? listDeleteIndex 1 ([] :: [()]),+listDeleteAtTests = "listDeleteAt" ~: TestList+  [ "deletes nothing from an empty list" ~: do+    [] @=? listDeleteAt (-1) ([] :: [()])+    [] @=? listDeleteAt 0 ([] :: [()])+    [] @=? listDeleteAt 1 ([] :: [()]) -  testCase "deletes the only item from a singleton list" $-    [] @=? listDeleteIndex 0 [()],+  , "deletes the only item from a singleton list" ~:+    [] @=? listDeleteAt 0 [()] -  testCase "deletes the head of a list" $-    [False] @=? listDeleteIndex 0 [True, False],+  , "deletes the head of a list" ~:+    [False] @=? listDeleteAt 0 [True, False] -  testCase "deletes the tail of a list" $-    [Just 0, Nothing] @=? listDeleteIndex 2 [Just 0, Nothing, Just 1],+  , "deletes the tail of a list" ~:+    [Just 0, Nothing] @=? listDeleteAt 2 [Just 0, Nothing, Just 1] -  testCase "deletes an inner item from a list" $-    [Just 0, Just 1] @=? listDeleteIndex 1 [Just 0, Nothing, Just 1],+  , "deletes an inner item from a list" ~:+    [Just 0, Just 1] @=? listDeleteAt 1 [Just 0, Nothing, Just 1] -  testCase "ignores a negative index" $ do-    [1, 2, 3] @=? listDeleteIndex (-1) [1, 2, 3]-    [1, 2, 3] @=? listDeleteIndex (-2) [1, 2, 3],+  , "ignores a negative index" ~: do+    [1, 2, 3] @=? listDeleteAt (-1) [1, 2, 3]+    [1, 2, 3] @=? listDeleteAt (-2) [1, 2, 3] -  testCase "ignores an index too large" $ do-    [1, 2, 3] @=? listDeleteIndex 3 [1, 2, 3]-    [1, 2, 3] @=? listDeleteIndex 4 [1, 2, 3]+  , "ignores an index too large" ~: do+    [1, 2, 3] @=? listDeleteAt 3 [1, 2, 3]+    [1, 2, 3] @=? listDeleteAt 4 [1, 2, 3]   ] -listReplaceTests = testGroup "listReplace" [-  testCase "accepts an empty list" $-    [] @=? listReplace False True [],--  testCase "replaces the first element" $-    [100, 2, 3] @=? listReplace 1 100 [1, 2, 3],--  testCase "replaces the last element" $-    [1, 2, 300] @=? listReplace 3 300 [1, 2, 3],+listInsertAtTests = "listInsertAt" ~: TestList+  [ "adds at the beginning of a list" ~: do+    let expected = [10, 3, 1, 5, 2]+    expected @=? listInsertAt 0 elt base+    expected @=? listInsertAt (-1) elt base+    expected @=? listInsertAt (-2) elt base -  testCase "replaces an interior element" $-    [1, 200, 3] @=? listReplace 2 200 [1, 2, 3],+  , "adds in the middle of a list" ~: do+    [3, 10, 1, 5, 2] @=? listInsertAt 1 elt base+    [3, 1, 10, 5, 2] @=? listInsertAt 2 elt base+    [3, 1, 5, 10, 2] @=? listInsertAt 3 elt base -  testCase "replaces multiple elements" $ do-    [1, 0, 3, 4, 0, 0, 3] @=? listReplace 2 0 [1, 2, 3, 4, 2, 2, 3]-    replicate 10 True @=? listReplace False True (replicate 10 False)+  , "adds at the end of a list" ~: do+    let expected = [3, 1, 5, 2, 10]+    expected @=? listInsertAt (length base) elt base+    expected @=? listInsertAt (length base + 1) elt base+    expected @=? listInsertAt (length base + 2) elt base   ]+  where base = [3, 1, 5, 2]+        elt = 10 -onLeftTests = testGroup "onLeft" [-  testCase "changes a Left" $-    Left 10 @=? onLeft (* 2) (Left 5 :: Either Int ()),+listReplaceTests = "listReplace" ~: TestList+  [ "accepts an empty list" ~:+    [] @=? listReplace False True [] -  testCase "doesn't change a Right" $-    Right False @=? onLeft (* 2) (Right False)-  ]+  , "replaces the first element" ~:+    [100, 2, 3] @=? listReplace 1 100 [1, 2, 3] -onRightTests = testGroup "onRight" [-  testCase "doesn't change a Left" $-    Left False @=? onRight (* 2) (Left False),+  , "replaces the last element" ~:+    [1, 2, 300] @=? listReplace 3 300 [1, 2, 3] -  testCase "changes a Right" $-    Right 10 @=? onRight (* 2) (Right 5 :: Either () Int)+  , "replaces an interior element" ~:+    [1, 200, 3] @=? listReplace 2 200 [1, 2, 3]++  , "replaces multiple elements" ~: do+    [1, 0, 3, 4, 0, 0, 3] @=? listReplace 2 0 [1, 2, 3, 4, 2, 2, 3]+    replicate 10 True @=? listReplace False True (replicate 10 False)   ] -andEithersTests = testGroup "andEithers" [-  testCase "returns Right for an empty list" $-    Right [] @=? andEithers ([] :: [Either () ()]),+andEithersTests = "andEithers" ~: TestList+  [ "returns Right for an empty list" ~:+    Right [] @=? andEithers ([] :: [Either () ()]) -  testCase "returns Right when given all Rights" $ do+  , "returns Right when given all Rights" ~: do     Right [1] @=? andEithers [Right 1 :: Either () Int]     Right [1, 2] @=? andEithers [Right 1, Right 2 :: Either () Int]-    Right [1, 2, 3] @=? andEithers [Right 1, Right 2, Right 3 :: Either () Int],+    Right [1, 2, 3] @=? andEithers [Right 1, Right 2, Right 3 :: Either () Int] -  testCase "returns Left when there is a single Left" $ do+  , "returns Left when there is a single Left" ~: do     Left [1] @=? andEithers [Left 1 :: Either Int ()]     Left [1] @=? andEithers [Left 1, Right 2]     Left [2] @=? andEithers [Right 1, Left 2]     Left [2] @=? andEithers [Right 1, Left 2, Right 3]-    Left [3] @=? andEithers [Right 1, Right 2, Left 3],+    Left [3] @=? andEithers [Right 1, Right 2, Left 3] -  testCase "returns Left when there are many Lefts" $ do+  , "returns Left when there are many Lefts" ~: do     Left [1, 2] @=? andEithers [Left 1, Left 2, Right 3]     Left [1, 3] @=? andEithers [Left 1, Right 2, Left 3]     Left [2, 3] @=? andEithers [Right 1, Left 2, Left 3]     Left [1, 2, 3] @=? andEithers [Left 1, Left 2, Left 3 :: Either Int ()]   ] -forTests = testGroup "for" [-  testCase "passes an empty list through" $-    ([] @=?) =<< sequence (for [] $ const $ assertFailure "Nope."),+forTests = "for" ~: TestList+  [ "passes an empty list through" ~:+    ([] @=?) =<< sequence (for [] $ const $ assertFailure "Nope.") -  testCase "operates on each element of a list" $+  , "operates on each element of a list" ~:     [2, 2, 4, 6, 10] @=? for [1, 1, 2, 3, 5] (* 2)   ] -mapTupleTests = testGroup "mapTuple" [-  testCase "updates both values" $+mapTupleTests = "mapTuple" ~: TestList+  [ "updates both values" ~:     (9, 16) @=? mapTuple (^ 2) (3, 4)   ] -whenMaybeTests = testGroup "whenMaybeTests" [-  testCase "doesn't invoke its function when given Nothing" $-    0 @=? State.execState (whenMaybe Nothing $ \x -> State.put (x * 2)) 0,+whenMaybeTests = "whenMaybeTests" ~: TestList+  [ "doesn't invoke its function when given Nothing" ~:+    0 @=? State.execState (whenMaybe Nothing $ \x -> State.put (x * 2)) 0 -  testCase "invokes its function when given a Just" $+  , "invokes its function when given a Just" ~:     6 @=? State.execState (whenMaybe (Just 3) $ \x -> State.put (x * 2)) 0   ] -condTests = testGroup "cond" [-  testCase "accepts an empty list of cases" $-    True @=? cond True [],+condTests = "cond" ~: TestList+  [ "accepts an empty list of cases" ~:+    True @=? cond True [] -  testCase "returns the default when all cases are false" $ do+  , "returns the default when all cases are false" ~: do     0 @=? cond 0 [(False, 1)]     0 @=? cond 0 [(False, 1), (False, 2)]-    0 @=? cond 0 [(False, 1), (False, 2), (False, 3)],+    0 @=? cond 0 [(False, 1), (False, 2), (False, 3)] -  testCase "returns a single true case" $ do+  , "returns a single true case" ~: do     3 @=? cond 0 [(False, 1), (False, 2), (True, 3)]     2 @=? cond 0 [(False, 1), (True, 2), (False, 3)]-    1 @=? cond 0 [(True, 1), (False, 2), (False, 3)],+    1 @=? cond 0 [(True, 1), (False, 2), (False, 3)] -  testCase "returns the first true case" $ do+  , "returns the first true case" ~: do     3 @=? cond 0 [(False, 1), (False, 2), (True, 3)]     2 @=? cond 0 [(False, 1), (True, 2), (True, 3)]     1 @=? cond 0 [(True, 1), (True, 2), (True, 3)]   ] -if'Tests = testGroup "if'" [-  testCase "detects true" $ "yes" @=? if' "yes" "no" True,--  testCase "detects false" $ "no" @=? if' "yes" "no" False+if'Tests = "if'" ~: TestList+  [ "detects true" ~: "yes" @=? if' "yes" "no" True+  , "detects false" ~: "no" @=? if' "yes" "no" False   ] -andMTests = testGroup "andM" [-  testCase "empty list returns true" $ True @=? runIdentity (andM []),+andMTests = "andM" ~: TestList+  [ "empty list returns true" ~: True @=? runIdentity (andM []) -  testCase "aborts after an immediate true" $ do+  , "aborts after an immediate true" ~: do     ref <- newIORef 0     result <- andM [addToRef ref 1 >> return False,                     addToRef ref 2 >> return True]     False @=? result-    (1 @=?) =<< readIORef ref,+    (1 @=?) =<< readIORef ref -  testCase "aborts on a false after trues" $ do+  , "aborts on a false after trues" ~: do     ref <- newIORef 0     result <- andM [addToRef ref 1 >> return True,                     addToRef ref 2 >> return True,@@ -198,9 +197,9 @@                     addToRef ref 8 >> return False,                     addToRef ref 16 >> return False]     False @=? result-    (7 @=?) =<< readIORef ref,+    (7 @=?) =<< readIORef ref -  testCase "executes all trues" $ do+  , "executes all trues" ~: do     ref <- newIORef 0     result <- andM [addToRef ref 1 >> return True,                     addToRef ref 2 >> return True,@@ -211,20 +210,20 @@   ]   where addToRef ref num = modifyIORef ref (+ num) -forIndexM_Tests = testGroup "forIndexM_" [-  testCase "does nothing with an empty list" $-    "" @=? execWriter (forIndexM_ [] $ \_ _ -> tell "x"),+forIndexM_Tests = "forIndexM_" ~: TestList+  [ "does nothing with an empty list" ~:+    "" @=? execWriter (forIndexM_ [] $ \_ _ -> tell "x") -  testCase "iterates over all elements in order" $+  , "iterates over all elements in order" ~:     "(0,'a')(1,'b')(2,'c')" @=?     execWriter (forIndexM_ "abc" $ \index value -> tell $ show (index, value))   ] -whileMTests = testGroup "whileM" [-  testCase "never executes the body if the first test returns false" $-    "" @=? execWriter (whileM (return False) (tell "x")),+whileMTests = "whileM" ~: TestList+  [ "never executes the body if the first test returns false" ~:+    "" @=? execWriter (whileM (return False) (tell "x")) -  testCase "executes repeatedly as expected" $+  , "executes repeatedly as expected" ~:     let test = do n <- get                   tell $ show n                   if n > 0@@ -234,38 +233,28 @@     in "3x2x1x0" @=? execWriter (runStateT (whileM test body) 3)   ] -whileM'Tests = testGroup "whileM'" [-  testCase "never executes the body if the first test returns Nothing" $-    "" @=? execWriter (whileM' (return Nothing) (const $ tell "x")),+whileM'Tests = "whileM'" ~: TestList+  [ "never executes the body if the first test returns Nothing" ~:+    "" @=? execWriter (whileM' (return Nothing) (const $ tell "x")) -  testCase "executes repeatedly as expected" $+  , "executes repeatedly as expected" ~:     let test = get >>= \n -> return $ if n > 0 then Just n else Nothing         body n = do tell $ show n                     put (n - 1)     in "321" @=? execWriter (runStateT (whileM' test body) 3)   ] -doWhileMTests = testGroup "doWhileM" [-  testCase "executes a constantly-Left body once" $ do+doWhileMTests = "doWhileM" ~: TestList+  [ "executes a constantly-Left body once" ~: do     countVar <- newIORef 0     result <- doWhileM () (const $ do modifyIORef countVar (+ 1)                                       return $ Left "done")     count <- readIORef countVar-    (1, "done") @=? (count, result),+    (1, "done") @=? (count, result) -  testCase "executes until a Left is returned" $+  , "executes until a Left is returned" ~:     execWriter (doWhileM 9 $ \n -> do                    tell $ show n-                   return $ if n == 0 then Left () else Right $ n - 1)-    @=? "9876543210"-  ]--seqTests = testGroup "Seq" [-  testCase "mempty does nothing" $-    let Seq action = mempty-    in "" @=? execWriter action,--  testCase "mappend works" $-    let Seq action = Seq (tell "a") `mappend` Seq (tell "b")-    in "ab" @=? execWriter action+                   return $ if n == 0 then Left () else Right $ n - 1) @=?+    "9876543210"   ]
+ tests/Game/Goatee/Lib/BoardTest.hs view
@@ -0,0 +1,195 @@+-- This file is part of Goatee.+--+-- Copyright 2014 Bryan Gardiner+--+-- Goatee is free software: you can redistribute it and/or modify+-- it under the terms of the GNU Affero General Public License as published by+-- the Free Software Foundation, either version 3 of the License, or+-- (at your option) any later version.+--+-- Goatee is distributed in the hope that it will be useful,+-- but WITHOUT ANY WARRANTY; without even the implied warranty of+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the+-- GNU Affero General Public License for more details.+--+-- You should have received a copy of the GNU Affero General Public License+-- along with Goatee.  If not, see <http://www.gnu.org/licenses/>.++module Game.Goatee.Lib.BoardTest (tests) where++import Game.Goatee.Lib.Board+import Game.Goatee.Lib.Property+import Game.Goatee.Lib.TestInstances ()+import Game.Goatee.Lib.TestUtils+import Game.Goatee.Lib.Tree+import Game.Goatee.Lib.Types+import Test.HUnit ((~:), (@=?), (@?=), Test (TestList))++tests = "Game.Goatee.Lib.Board" ~: TestList+  [ boardCoordStateTests+  , moveNumberTests+  , markupPropertiesTests+  , visibilityPropertyTests+  , cursorModifyNodeTests+  , moveToPropertyTests+  ]++boardCoordStateTests = "boardCoordState" ~: TestList+  [ "works for a 1x1 board" ~: do+    let state = boardCoordState (0,0) $ rootBoardState $+                node [SZ 1 1, CR $ coord1 (0,0)]+    coordStone state @?= Nothing+    coordMark state @?= Just MarkCircle++  , "works for a 2x2 board" ~: do+    let board = rootBoardState $+                node [SZ 2 2, B $ Just (0,0), TR $ coord1 (0,0), MA $ coord1 (1,1)]+    coordStone (boardCoordState (0,0) board) @?= Just Black+    coordMark (boardCoordState (0,0) board) @?= Just MarkTriangle+    coordStone (boardCoordState (1,0) board) @?= Nothing+    coordMark (boardCoordState (1,0) board) @?= Nothing+    coordStone (boardCoordState (1,1) board) @?= Nothing+    coordMark (boardCoordState (1,1) board) @?= Just MarkX+  ]+++moveNumberTests = "move number" ~: TestList+  [ "starts at zero" ~:+    0 @=? boardMoveNumber (cursorBoard $ rootCursor $ node1 [] $ node [B Nothing])++  , "advances for nodes with moves" ~: do+    1 @=? boardMoveNumber (cursorBoard $ child 0 $ rootCursor $ node1 [] $ node [B Nothing])+    1 @=? boardMoveNumber (cursorBoard $ rootCursor $ node1 [B Nothing] $ node [])+    1 @=? boardMoveNumber (cursorBoard $ child 0 $ rootCursor $ node1 [B Nothing] $ node [])+    2 @=? boardMoveNumber (cursorBoard $ child 0 $ rootCursor $+                           node1 [B Nothing] $ node [W Nothing])+    2 @=? boardMoveNumber (cursorBoard $ child 0 $ child 0 $ rootCursor $+                           node1 [B Nothing] $ node1 [W Nothing] $ node [])+    3 @=? boardMoveNumber (cursorBoard $ child 0 $ child 0 $ rootCursor $+                           node1 [B Nothing] $ node1 [W Nothing] $ node [B Nothing])++  , "doesn't advance for non-move nodes" ~: do+    0 @=? boardMoveNumber (cursorBoard $ child 0 $ rootCursor $+                           node1 [] $ node [])+    0 @=? boardMoveNumber (cursorBoard $ child 0 $ rootCursor $+                           node1 [PL White] $ node [])+    0 @=? boardMoveNumber (cursorBoard $ child 0 $ rootCursor $+                           node1 [AB $ coords [(0,0)]] $ node [])+    0 @=? boardMoveNumber (cursorBoard $ child 0 $ rootCursor $+                           node1 [IT, TE Double2, GN $ toSimpleText "Title"] $ node [])+  ]++markupPropertiesTests = "markup properties" ~: TestList+  [ "adds marks to a BoardState" ~: TestList+    [ "CR" ~: Just MarkCircle @=? getMark 0 0 (rootCursor $ node [CR $ coords [(0,0)]])+    , "MA" ~: Just MarkX @=? getMark 0 0 (rootCursor $ node [MA $ coords [(0,0)]])+    , "SL" ~: Just MarkSelected @=? getMark 0 0 (rootCursor $ node [SL $ coords [(0,0)]])+    , "SQ" ~: Just MarkSquare @=? getMark 0 0 (rootCursor $ node [SQ $ coords [(0,0)]])+    , "TR" ~: Just MarkTriangle @=? getMark 0 0 (rootCursor $ node [TR $ coords [(0,0)]])+    , "multiple at once" ~: do+      let cursor = rootCursor $ node [SZ 2 2,+                                      CR $ coords [(0,0), (1,1)],+                                      TR $ coords [(1,0)]]+      [[Just MarkCircle, Just MarkTriangle],+       [Nothing, Just MarkCircle]]+        @=? map (map coordMark) (boardCoordStates $ cursorBoard cursor)+    ]++  , "adds more complex annotations to a BoardState" ~: TestList+    [ "AR" ~: [((0,0), (1,1))] @=? boardArrows (rootCoord $ node [AR [((0,0), (1,1))]])+    , "LB" ~: [((0,0), st "Hi")] @=? boardLabels (rootCoord $ node [LB [((0,0), st "Hi")]])+    , "LN" ~: [((0,0), (1,1))] @=? boardLines (rootCoord $ node [LN [((0,0), (1,1))]])+    ]++  , "clears annotations when moving to a child node" ~: do+    let root = node1 [SZ 3 2,+                      CR $ coords [(0,0)],+                      MA $ coords [(1,0)],+                      SL $ coords [(2,0)],+                      SQ $ coords [(0,1)],+                      TR $ coords [(1,1)],+                      AR [((0,0), (2,1))],+                      LB [((2,1), st "Empty")],+                      LN [((1,1), (2,0))]] $+                 node []+        board = cursorBoard $ child 0 $ rootCursor root+    mapM_ (mapM_ ((Nothing @=?) . coordMark)) $ boardCoordStates board+    [] @=? boardArrows board+    [] @=? boardLines board+    [] @=? boardLabels board+  ]+  where rootCoord = cursorBoard . rootCursor+        st = toSimpleText++        getMark :: Int -> Int -> Cursor -> Maybe Mark+        getMark x y cursor = coordMark $ boardCoordStates (cursorBoard cursor) !! y !! x++visibilityPropertyTests = "visibility properties" ~: TestList+  [ "boards start with all points undimmed" ~:+    replicate 9 (replicate 9 False) @=?+    map (map coordDimmed) (boardCoordStates $ cursorBoard $ rootCursor $ node [SZ 9 9])++  , "DD selectively dims points" ~:+    let root = node [SZ 5 2, DD $ coords' [(3,0)] [((0,0), (1,1))]]+    in [[True, True, False, True, False],+        [True, True, False, False, False]] @=?+       map (map coordDimmed) (boardCoordStates $ cursorBoard $ rootCursor root)++  , "dimming is inherited" ~:+    let root = node1 [SZ 5 2, DD $ coords' [(3,0)] [((0,0), (1,1))]] $ node []+    in [[True, True, False, True, False],+        [True, True, False, False, False]] @=?+       map (map coordDimmed) (boardCoordStates $ cursorBoard $ child 0 $ rootCursor root)++  , "DD[] clears dimming" ~:+    let root = node1 [SZ 2 1, DD $ coords [(0,0)]] $ node [DD $ coords []]+    in [[False, False]] @=?+       map (map coordDimmed) (boardCoordStates $ cursorBoard $ child 0 $ rootCursor root)++  , "boards start with all points visible" ~:+    replicate 9 (replicate 9 True) @=?+    map (map coordVisible) (boardCoordStates $ cursorBoard $ rootCursor $ node [SZ 9 9])++  , "VW selectively makes points visible" ~:+    let root = node [SZ 5 2, VW $ coords' [(1,0), (0,1)] [((2,0), (4,1))]]+    in [[False, True, True, True, True],+        [True, False, True, True, True]] @=?+       map (map coordVisible) (boardCoordStates $ cursorBoard $ rootCursor root)++  , "visibility is inherited" ~:+    let root = node1 [SZ 5 2, VW $ coords' [(1,0), (0,1)] [((2,0), (4,1))]] $ node []+    in [[False, True, True, True, True],+        [True, False, True, True, True]] @=?+       map (map coordVisible) (boardCoordStates $ cursorBoard $ child 0 $ rootCursor root)++  , "VW[] clears dimming" ~:+    let root = node1 [SZ 2 1, VW $ coords [(0,0)]] $ node [VW $ coords []]+    in [[True, True]] @=?+       map (map coordVisible) (boardCoordStates $ cursorBoard $ child 0 $ rootCursor root)+  ]++cursorModifyNodeTests = "cursorModifyNode" ~: TestList+  [ "updates the BoardState" ~:+    let cursor = child 0 $ rootCursor $+                 node1 [SZ 3 1, B $ Just (0,0)] $+                 node [W $ Just (1,0)]+        modifyProperty prop = case prop of+          W (Just (1,0)) -> W $ Just (2,0)+          _ -> prop+        modifyNode node = node { nodeProperties = map modifyProperty $ nodeProperties node }+        result = cursorModifyNode modifyNode cursor+    in map (map coordStone) (boardCoordStates $ cursorBoard result) @?=+       [[Just Black, Nothing, Just White]]+  ]++moveToPropertyTests = "moveToProperty" ~: TestList+  [ "creates Black moves" ~: do+    B Nothing @=? moveToProperty Black Nothing+    B (Just (3,2)) @=? moveToProperty Black (Just (3,2))+    B (Just (0,0)) @=? moveToProperty Black (Just (0,0))++  , "creates White moves" ~: do+    W Nothing @=? moveToProperty White Nothing+    W (Just (18,18)) @=? moveToProperty White (Just (18,18))+    W (Just (15,16)) @=? moveToProperty White (Just (15,16))+  ]
+ tests/Game/Goatee/Lib/MonadTest.hs view
@@ -0,0 +1,886 @@+-- This file is part of Goatee.+--+-- Copyright 2014 Bryan Gardiner+--+-- Goatee is free software: you can redistribute it and/or modify+-- it under the terms of the GNU Affero General Public License as published by+-- the Free Software Foundation, either version 3 of the License, or+-- (at your option) any later version.+--+-- Goatee is distributed in the hope that it will be useful,+-- but WITHOUT ANY WARRANTY; without even the implied warranty of+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the+-- GNU Affero General Public License for more details.+--+-- You should have received a copy of the GNU Affero General Public License+-- along with Goatee.  If not, see <http://www.gnu.org/licenses/>.++module Game.Goatee.Lib.MonadTest (tests) where++import Control.Applicative ((<$>))+import Control.Arrow ((&&&), second)+import Control.Monad (forM_, liftM, replicateM_, void)+import Control.Monad.Writer (Writer, execWriter, runWriter, tell)+import Data.List (unfoldr)+import Data.Maybe (fromJust, maybeToList)+import Game.Goatee.Common+import Game.Goatee.Lib.Board+import Game.Goatee.Lib.Monad+import Game.Goatee.Lib.Property+import Game.Goatee.Lib.TestInstances ()+import Game.Goatee.Lib.TestUtils+import Game.Goatee.Lib.Tree (emptyNode, nodeChildren)+import Game.Goatee.Lib.Types+import Game.Goatee.Test.Common+import Test.HUnit ((~:), (@=?), (@?=), Test (TestList))++{-# ANN module "HLint: ignore Reduce duplication" #-}++type LoggedGoM = GoT (Writer [String])++runLoggedGo :: LoggedGoM a -> Cursor -> (a, Cursor, [String])+runLoggedGo go cursor =+  let ((value, cursor'), log) = runWriter $ runGoT go cursor+  in (value, cursor', log)++tests = "Game.Goatee.Lib.Monad" ~: TestList+  [ monadTests+  , navigationTests+  , positionStackTests+  , propertiesTests+  , getPropertyTests+  , getPropertyValueTests+  , putPropertyTests+  , deletePropertyTests+  , modifyPropertyTests+  , modifyPropertyValueTests+  , modifyPropertyStringTests+  , modifyPropertyCoordsTests+  , modifyGameInfoTests+  , modifyVariationModeTests+  , getMarkTests+  , modifyMarkTests+  , addChildTests+  , addChildAtTests+  , deleteChildAtTests+  , gameInfoChangedTests+  ]++monadTests = "monad properties" ~: TestList+  [ "returns a value it's given" ~:+    let cursor = rootCursor $ node []+        (value, cursor') = runGo (return 3) cursor+    in (value, cursorNode cursor') @?= (3, cursorNode cursor)++  , "getCursor works" ~: do+    let cursor = rootCursor nodeA+        nodeA = node1 [SZ 3 3, B $ Just (0,0)] nodeB+        nodeB = node1 [W $ Just (1,1)] nodeC+        nodeC = node [B $ Just (2,2)]+    nodeA @=? evalGo (liftM cursorNode getCursor) cursor+    nodeB @=? evalGo (goDown 0 >> liftM cursorNode getCursor) cursor+    nodeC @=? evalGo (goDown 0 >> goDown 0 >> liftM cursorNode getCursor) cursor++  , "getCoordState works" ~: do+    let cursor = child 0 $ child 0 $ rootCursor $+                 node1 [SZ 2 2, B $ Just (0,0)] $+                 node1 [W $ Just (1,0), TR $ coord1 (0,0)] $+                 node [B $ Just (0,1), CR $ coord1 (1,0)]+        action = mapM getCoordState [(0,0), (1,0), (0,1), (1,1)]+        states = evalGo action cursor+    map coordStone states @?= [Just Black, Just White, Just Black, Nothing]+    map coordMark states @?= [Nothing, Just MarkCircle, Nothing, Nothing]+  ]++navigationTests = "navigation" ~: TestList+  [ "navigates down a tree" ~:+    let cursor = rootCursor $+                 node1 [B $ Just (0,0)] $+                 node' [W $ Just (1,1)] [node [B $ Just (2,2)],+                                         node [B Nothing]]+        action = goDown 0 >> goDown 1+        (_, cursor') = runGo action cursor+    in cursorProperties cursor' @?= [B Nothing]++  , "navigates up a tree" ~:+    let cursor = child 1 $ child 0 $ rootCursor $+                 node1 [B $ Just (0,0)] $+                 node' [W $ Just (1,1)] [node [B $ Just (2,2)],+                                         node [B Nothing]]+        action = goUp >> goUp+        (_, cursor') = runGo action cursor+    in cursorProperties cursor' @?= [B $ Just (0,0)]++  , "invokes handlers when navigating" ~:+    let cursor = rootCursor $ node1 [B Nothing] $ node [W Nothing]+        action = do on navigationEvent $ \step -> case step of+                      GoUp index -> tell ["Up " ++ show index]+                      _ -> return ()+                    on navigationEvent $ \step -> case step of+                      GoDown index -> tell ["Down " ++ show index]+                      _ -> return ()+                    goDown 0+                    goUp+        (_, _, log) = runLoggedGo action cursor+    in log @?= ["Down 0", "Up 0"]++  , "navigates to the root of a tree, invoking handlers" ~: do+    let cursor = child 0 $ child 0 $ rootCursor $+                 node1 [B $ Just (0,0)] $+                 node1 [W $ Just (1,1)] $+                 node [B $ Just (2,2)]+        action = do on navigationEvent $ \step -> tell [show step]+                    goToRoot+        (_, cursor', log) = runLoggedGo action cursor+    cursorProperties cursor' @?= [B $ Just (0,0)]+    log @?= ["GoUp 0", "GoUp 0"]++  , "goToGameInfoNode" ~: TestList+    [ "can navigate up to find game info" ~:+      let props = [PB (toSimpleText ""), B Nothing]+          cursor = child 0 $ rootCursor $ node1 props $ node [W Nothing]+          action = void $ goToGameInfoNode False+      in cursorProperties (execGo action cursor) @?= props++    , "can find game info at the current node" ~:+      let props = [PB (toSimpleText ""), W Nothing]+          cursor = child 0 $ rootCursor $ node1 [B Nothing] $ node props+          action = void $ goToGameInfoNode False+      in cursorProperties (execGo action cursor) @?= props++    , "doesn't find game info from a subnode" ~:+      let cursor = child 0 $ rootCursor $+                   node1 [B $ Just (0,0)] $+                   node1 [W $ Just (1,1)] $+                   node [PB (toSimpleText ""), B Nothing]+          action = void $ goToGameInfoNode False+      in cursorProperties (execGo action cursor) @?= [W $ Just (1,1)]++    , "can return to the initial node when no game info is found" ~:+      let cursor = child 0 $ child 0 $ rootCursor $+                   node1 [B $ Just (0,0)] $+                   node1 [W $ Just (1,1)] $+                   node [B $ Just (2,2)]+          action = void $ goToGameInfoNode False+      in cursorProperties (execGo action cursor) @?= [B $ Just (2,2)]++    , "can finish at the root node when no game info is found" ~:+      let cursor = child 0 $ child 0 $ rootCursor $+                   node1 [B $ Just (0,0)] $+                   node1 [W $ Just (1,1)] $+                   node [B $ Just (2,2)]+          action = void $ goToGameInfoNode True+      in cursorProperties (execGo action cursor) @?= [B $ Just (0,0)]+    ]+  ]++positionStackTests = "position stack" ~: TestList+  [ "should push, pop, and drop with no navigation" ~: do+    let cursor = rootCursor $ node []+        actions = [pushPosition >> popPosition,+                   pushPosition >> pushPosition >> popPosition >> popPosition,+                   pushPosition >> dropPosition,+                   pushPosition >> pushPosition >> dropPosition >> popPosition,+                   pushPosition >> pushPosition >> popPosition >> dropPosition]+    forM_ actions $ \action -> cursorProperties (execGo action cursor) @?= []++  , "should backtrack up and down the tree" ~: do+    let cursor = child 0 $ child 1 $ rootCursor $+                 node' [B $ Just (0,0)]+                       [node1 [W $ Just (1,1)] $ node [B $ Just (2,2)],+                        node1 [W Nothing] $ node [B Nothing]]+        action = pushPosition >> goUp >> goUp >> goDown 0 >> goDown 0+    cursorProperties (execGo (action >> popPosition) cursor) @?= [B Nothing]+    cursorProperties (execGo (action >> dropPosition) cursor) @?= [B $ Just (2,2)]++  , "should pop multiple stacks" ~: do+    let cursor = child 0 $ child 0 commonCursor+        action = do navigate+                    log >> popPosition+                    log >> popPosition+                    log+    execWriter (runGoT action cursor) @?=+      ["B (2,2)", "B (3,3)", "B (5,5)", "B (3,3)", "B (2,2)"]++  , "should drop then pop" ~: do+    let cursor = child 0 $ child 0 commonCursor+        action = do navigate+                    log >> dropPosition+                    log >> popPosition+                    log+    execWriter (runGoT action cursor) @?=+      ["B (2,2)", "B (3,3)", "B (5,5)", "B (5,5)", "B (2,2)"]++  , "should drop twice" ~: do+    let cursor = child 0 $ child 0 commonCursor+        action = do navigate+                    log >> dropPosition+                    log >> dropPosition+                    log+    execWriter (runGoT action cursor) @?=+      ["B (2,2)", "B (3,3)", "B (5,5)", "B (5,5)", "B (5,5)"]++  , "should fire navigation handlers while popping" ~: do+    let cursor = rootCursor $ node1 [B Nothing] $ node [W Nothing]+        action = do pushPosition+                    goDown 0+                    goUp+                    on navigationEvent $ \step -> tell [step]+                    popPosition+    execWriter (runGoT action cursor) @?= [GoDown 0, GoUp 0]+  ]+  where commonCursor = rootCursor $+                       node' [B $ Just (0,0)]+                             [node' [W $ Just (1,1)]+                                    [node [B $ Just (2,2)],+                                     node [B $ Just (3,3)]],+                              node1 [W $ Just (4,4)] $ node [B $ Just (5,5)]]+        log = getCursor >>= \cursor -> case cursorProperties cursor of+          [B (Just x)] -> tell ["B " ++ show x]+          [W (Just x)] -> tell ["W " ++ show x]+          xs -> error $ "Unexpected properties: " ++ show xs+        navigate = do log >> pushPosition+                      goUp >> goDown 1+                      log >> pushPosition+                      goUp >> goUp >> goDown 1 >> goDown 0++propertiesTests = "properties" ~: TestList+  [ "getProperties" ~: TestList+    [ "returns an empty list" ~:+      let cursor = rootCursor $ node []+      in evalGo getProperties cursor @?= []++    , "returns a non-empty list" ~:+      let properties = [PB $ toSimpleText "Foo", B Nothing]+          cursor = rootCursor $ node properties+      in evalGo getProperties cursor @?= properties+    ]++  , "modifyProperties" ~: TestList+    [ "adds properties" ~:+      let cursor = rootCursor $ node [FF 1]+          action = do modifyProperties $ \props -> return $ props ++ [B Nothing, W Nothing]+                      getProperties+      in evalGo action cursor @?= [FF 1, B Nothing, W Nothing]++    , "removes properties" ~:+      let cursor = rootCursor $ node [W Nothing, FF 1, B Nothing]+          action = do modifyProperties $ \props -> return $ filter (not . isMoveProperty) props+                      getProperties+      in evalGo action cursor @?= [FF 1]++    , "fires a properties modified event" ~:+      let cursor = rootCursor $ node [FF 1]+          action = do on propertiesModifiedEvent $ \old new -> tell [(old, new)]+                      modifyProperties $ const $ return [FF 2]+          log = execWriter (runGoT action cursor)+      in log @?= [([FF 1], [FF 2])]+    ]+  ]+  where isMoveProperty prop = case prop of+          B _ -> True+          W _ -> True+          _ -> False++getPropertyTests = "getProperty" ~: TestList+  [ "doesn't find an unset property" ~: do+    Nothing @=? evalGo (getProperty propertyW) (rootCursor $ node [])+    Nothing @=? evalGo (getProperty propertyW) (rootCursor $ node [B Nothing])++  , "finds a set property" ~: do+    Just (B Nothing) @=? evalGo (getProperty propertyB) (rootCursor $ node [B Nothing])+    Just (B Nothing) @=? evalGo (getProperty propertyB) (rootCursor $ node [B Nothing, DO])+    Just DO @=? evalGo (getProperty propertyDO) (rootCursor $ node [B Nothing, DO])+  ]++getPropertyValueTests = "getPropertyValue" ~: TestList+  [ "doesn't find an unset property" ~: do+    Nothing @=? evalGo (getPropertyValue propertyW) (rootCursor $ node [])+    Nothing @=? evalGo (getPropertyValue propertyW) (rootCursor $ node [B Nothing])++  , "finds a set property" ~: do+    Just Nothing @=? evalGo (getPropertyValue propertyB) (rootCursor $ node [B Nothing])+    Just (Just (0,0)) @=?+      evalGo (getPropertyValue propertyB) (rootCursor $ node [B $ Just (0,0), TE Double1])+    Just Double1 @=?+      evalGo (getPropertyValue propertyTE) (rootCursor $ node [B $ Just (0,0), TE Double1])+  ]++putPropertyTests = "putProperty" ~: TestList+  [ "adds an unset property" ~: do+    [IT] @=? cursorProperties (execGo (putProperty IT) $ rootCursor $ node [])+    [DO, IT] @=?+      sortProperties (cursorProperties (execGo (putProperty IT) $ rootCursor $ node [DO]))++  , "replaces an existing property" ~:+    [TE Double2] @=?+    cursorProperties (execGo (putProperty $ TE Double2) (rootCursor $ node [TE Double1]))+  ]++deletePropertyTests = "deleteProperty" ~: TestList+  [ "does nothing when the property isn't set" ~: do+    [] @=? cursorProperties (execGo (deleteProperty DO) $ rootCursor $ node [])+    [B Nothing] @=? cursorProperties (execGo (deleteProperty DO) $ rootCursor $ node [B Nothing])++  , "removes a property" ~: do+    [] @=? cursorProperties (execGo (deleteProperty DO) $ rootCursor $ node [DO])+    -- This is documented behaviour for deleteProperty, the fact that it doesn't+    -- matter what the property value is:+    [DO] @=? cursorProperties+      (execGo (deleteProperty $ B $ Just (0,0)) $ rootCursor $ node [DO, B Nothing])+  ]++modifyPropertyTests = "modifyProperty" ~: TestList+  [ "adds a property to an empty node" ~:+    let cursor = rootCursor $ node []+        action = modifyProperty propertyIT (\Nothing -> Just IT)+    in cursorProperties (execGo action cursor) @?= [IT]++  , "adds a property to a non-empty node" ~:+    let cursor = rootCursor $ node [KO]+        action = modifyProperty propertyIT (\Nothing -> Just IT)+    in sortProperties (cursorProperties $ execGo action cursor) @?= [IT, KO]++  , "removes a property" ~: do+    let cursor = rootCursor $ node [IT, KO]+        cursor' = execGo (modifyProperty propertyKO $ \(Just KO) -> Nothing) cursor+        cursor'' = execGo (modifyProperty propertyIT $ \(Just IT) -> Nothing) cursor'+    cursorProperties cursor' @?= [IT]+    cursorProperties cursor'' @?= []++  , "updates a property" ~: do+    let cursor = rootCursor $ node [B $ Just (0,0), BM Double2]+        action = modifyProperty propertyB $ \(Just (B (Just (0,0)))) -> Just $ B $ Just (1,1)+        cursor' = execGo action cursor+        action' = modifyProperty propertyBM $ \(Just (BM Double2)) -> Just $ BM Double1+        cursor'' = execGo action' cursor'+    sortProperties (cursorProperties cursor') @?= [B $ Just (1,1), BM Double2]+    sortProperties (cursorProperties cursor'') @?= [B $ Just (1,1), BM Double1]++  , "fires an event when modifying a property" ~: do+    let cursor = rootCursor $ node [B $ Just (0,0)]+        action = do on propertiesModifiedEvent $ \[B (Just (0,0))] [] -> tell [0]+                    modifyProperty propertyB $ \(Just (B (Just (0,0)))) -> Nothing+        (cursor', log) = runWriter (execGoT action cursor)+    [0] @=? log+    [] @=? cursorProperties cursor'++  , "modifying but not actually changing a property doesn't fire an event" ~: do+    let cursor = rootCursor $ node [B $ Just (0,0)]+        action = do on propertiesModifiedEvent $ \_ _ -> tell ["Event fired."]+                    modifyProperty propertyB $ \p@(Just (B {})) -> p+        (cursor', log) = runWriter (execGoT action cursor)+    [B $ Just (0,0)] @=? cursorProperties cursor'+    [] @=? log+  ]++modifyPropertyValueTests = "modifyPropertyValue" ~: TestList+  [ "adds a property to an empty node" ~:+    let cursor = rootCursor $ node []+        action = modifyPropertyValue propertyPL (\Nothing -> Just Black)+    in cursorProperties (execGo action cursor) @?= [PL Black]++  , "adds a property to a non-empty node" ~:+    let cursor = rootCursor $ node [KO]+        action = modifyPropertyValue propertyPL (\Nothing -> Just White)+    in sortProperties (cursorProperties $ execGo action cursor) @?= [KO, PL White]++  , "removes a property" ~: do+    let cursor = rootCursor $ node [B Nothing, TE Double2]+        cursor' = execGo (modifyPropertyValue propertyTE $ \(Just Double2) -> Nothing) cursor+        cursor'' = execGo (modifyPropertyValue propertyB $ \(Just Nothing) -> Nothing) cursor'+    cursorProperties cursor' @?= [B Nothing]+    cursorProperties cursor'' @?= []++  , "updates a property" ~: do+    let cursor = rootCursor $ node [B $ Just (0,0), BM Double2]+        action = modifyPropertyValue propertyB $ \(Just (Just (0,0))) -> Just $ Just (1,1)+        cursor' = execGo action cursor+        action' = modifyPropertyValue propertyBM $ \(Just Double2) -> Just Double1+        cursor'' = execGo action' cursor'+    sortProperties (cursorProperties cursor') @?= [B $ Just (1,1), BM Double2]+    sortProperties (cursorProperties cursor'') @?= [B $ Just (1,1), BM Double1]+  ]++modifyPropertyStringTests =+  "modifyPropertyString" ~: TestList $+  -- Test a Text property and a SimpleText property.+  assumptions ++ genTests "comment" propertyC ++ genTests "game name" propertyGN+  where assumptions = let _ = C $ toText ""+                          _ = GN $ toSimpleText ""+                      in []+        genTests name property =+          [ "adds a " ++ name ~:+            let cursor = rootCursor $ node []+                action = modifyPropertyString property (++ "Hello.")+            in cursorProperties (execGo action cursor) @?=+               [propertyBuilder property $ stringToSgf "Hello."]++          , "removes a " ++ name ~:+            let cursor = rootCursor $ node [propertyBuilder property $ stringToSgf "Hello."]+                action = modifyPropertyString property $ \value -> case value of+                  "Hello." -> ""+                  other -> error $ "Got: " ++ other+            in cursorProperties (execGo action cursor) @?= []++          , "updates a " ++ name ~:+            let cursor = child 0 $ rootCursor $+                         node1 [propertyBuilder property $ stringToSgf "one"] $+                         node [propertyBuilder property $ stringToSgf "two"]+                action = modifyPropertyString property $ \value -> case value of+                  "two" -> "three"+                  other -> error $ "Got: " ++ other+                cursor' = execGo action cursor+            in (cursorProperties cursor', cursorProperties $ fromJust $ cursorParent cursor') @?=+               ([propertyBuilder property $ stringToSgf "three"],+                [propertyBuilder property $ stringToSgf "one"])++          , "leaves a non-existant comment" ~:+            let result = execGo (modifyPropertyString property id) $ rootCursor $ node []+            in cursorProperties result @?= []+          ]++modifyPropertyCoordsTests = "modifyPropertyCoords" ~: TestList+  [ "adds a property where there was none" ~:+    let cursor = rootCursor $ node []+        action = modifyPropertyCoords propertySL $ \[] -> [(0,0), (1,1)]+    in [SL $ coords [(0,0), (1,1)]] @=? cursorProperties (execGo action cursor)++  , "removes a property where there was one" ~:+    let cursor = rootCursor $ node [TR $ coord1 (5,5)]+        action = modifyPropertyCoords propertyTR $ \[(5,5)] -> []+    in [] @=? cursorProperties (execGo action cursor)++  , "modifies an existing property" ~:+    let cursor = rootCursor $ node [CR $ coord1 (3,4)]+        action = modifyPropertyCoords propertyCR $ \[(3,4)] -> [(2,2)]+    in [CR $ coord1 (2,2)] @=? cursorProperties (execGo action cursor)++  , "doesn't affect other properties" ~:+    let cursor = rootCursor $ node [SQ $ coord1 (0,0), MA $ coord1 (1,1)]+        action = modifyPropertyCoords propertyMA $ \[(1,1)] -> [(2,2)]+    in [MA $ coord1 (2,2), SQ $ coord1 (0,0)] @=?+       sortProperties (cursorProperties $ execGo action cursor)+  ]++modifyGameInfoTests = "modifyGameInfo" ~: TestList+  [ "creates info on the root node, starting with none" ~: TestList+     [ "starting from the root node" ~:+       let cursor = rootCursor $ node []+           action = modifyGameInfo $ \info ->+             info { gameInfoGameName = Just $ toSimpleText "Orange" }+       in cursorProperties (execGo action cursor) @?= [GN $ toSimpleText "Orange"]++     , "starting from a non-root node" ~:+       let cursor = child 0 $ rootCursor $ node1 [] $ node [B Nothing]+           action = modifyGameInfo $ \info ->+             info { gameInfoGameName = Just $ toSimpleText "Orange" }+           cursor' = execGo action cursor+       in (cursorProperties cursor', cursorProperties $ fromJust $ cursorParent cursor') @?=+          ([B Nothing], [GN $ toSimpleText "Orange"])+     ]++  , "modifies existing info on the root" ~: TestList+    [ "starting from the root node" ~:+      let cursor = rootCursor $ node [GN $ toSimpleText "Orange"]+          action = modifyGameInfo (\info -> info { gameInfoGameName = Nothing+                                                 , gameInfoBlackName =+                                                   Just $ toSimpleText "Peanut butter"+                                                 })+      in cursorProperties (execGo action cursor) @?= [PB $ toSimpleText "Peanut butter"]++    , "starting from a non-root node" ~:+      let cursor = child 0 $ rootCursor $ node1 [GN $ toSimpleText "Orange"] $ node [B Nothing]+          action = modifyGameInfo (\info -> info { gameInfoGameName = Nothing+                                                 , gameInfoBlackName =+                                                   Just $ toSimpleText "Peanut butter"+                                                 })+          cursor' = execGo action cursor+      in (cursorProperties cursor', cursorProperties $ fromJust $ cursorParent cursor') @?=+         ([B Nothing], [PB $ toSimpleText "Peanut butter"])+    ]++  , "moves game info from a non-root node to the root" ~:+    let cursor = child 0 $ child 0 $ child 0 $ rootCursor $+                 node1 [SZ 19 19] $+                 node1 [B $ Just (0,0), GN $ toSimpleText "Orange"] $+                 node1 [W $ Just (1,1)] $+                 node [B $ Just (2,2)]+        action = modifyGameInfo (\info -> info { gameInfoGameName = Nothing })+        cursor' = execGo action cursor+        action' = modifyGameInfo (\info -> info { gameInfoBlackName =+                                                  Just $ toSimpleText "Peanut butter" })+        cursor'' = execGo action' cursor'+        -- unfoldr :: (Maybe Cursor -> Maybe ([Property], Maybe Cursor))+        --         -> Maybe Cursor+        --         -> [[Property]]+        properties = unfoldr (fmap $ cursorProperties &&& cursorParent)+                             (Just cursor'')+    in map sortProperties properties @?=+       [[B $ Just (2,2)],+        [W $ Just (1,1)],+        [B $ Just (0,0)],+        [PB $ toSimpleText "Peanut butter", SZ 19 19]]+  ]++modifyVariationModeTests = "modifyVariationMode" ~: TestList+  [ "testing modes are not default" ~: do+    defaultVariationMode @/=? mode+    defaultVariationMode @/=? mode2++  , "modifies when at a root node" ~:+    node1 [ST mode] (node []) @=?+    cursorNode (execGo setMode $ rootCursor $ node1 [] $ node [])++  , "modifies when at a non-root node" ~:+    node1 [ST mode] (node []) @=?+    cursorNode (cursorRoot $ execGo setMode $ child 0 $ rootCursor $ node1 [] $ node [])++  , "leaves an unset ST unset" ~:+    assertST Nothing Nothing $ const defaultVariationMode++  , "leaves a default ST alone" ~:+    assertST (Just defaultVariationMode) (Just defaultVariationMode) $ const defaultVariationMode++  , "leaves an existing ST alone" ~:+    assertST (Just mode) (Just mode) $ const mode++  , "adds an ST property" ~:+    assertST (Just mode) Nothing $ const mode++  , "removes an ST property when setting to default" ~:+    assertST Nothing (Just mode) $ const defaultVariationMode++  , "modifies an existing ST property" ~:+    assertST (Just mode2) (Just mode) $ const mode2+  ]+  where mode = VariationMode ShowCurrentVariations True+        mode2 = VariationMode ShowChildVariations False+        setMode = modifyVariationMode $ const mode+        assertST maybeExpectedST maybeInitialST fn =+          node (ST <$> maybeToList maybeExpectedST) @=?+          cursorNode (execGo (modifyVariationMode fn) $+                      rootCursor $ node $ ST <$> maybeToList maybeInitialST)++getMarkTests = "getMark" ~: TestList+  [ "returns Nothing for no mark" ~: do+    Nothing @=? evalGo (getMark (0,0)) (rootCursor $ node [])+    Nothing @=? evalGo (getMark (0,0)) (rootCursor $ node [W $ Just (0,0)])++  , "returns Just when a mark is present" ~: do+    Just MarkSquare @=? evalGo (getMark (1,2)) (rootCursor $ node [SQ $ coord1 (1,2)])+    Just MarkTriangle @=? evalGo (getMark (1,1)) (rootCursor $ node [B $ Just (1,1),+                                                                     TR $ coord1 (1,1),+                                                                     SQ $ coord1 (1,2)])++  , "matches all marks" ~: forM_ [minBound..maxBound] $ \mark ->+    Just mark @=?+    evalGo (getMark (0,0))+           (rootCursor $ node [propertyBuilder (markProperty mark) $ coord1 (0,0)])+  ]++modifyMarkTests = "modifyMark" ~: TestList+  [ "creates a mark where there was none" ~:+    let cursor = rootCursor $ node []+        action = do modifyMark (\Nothing -> Just MarkX) (0,0)+                    getMark (0,0)+    in Just MarkX @=? evalGo action cursor++  , "removes an existing mark" ~:+    let cursor = rootCursor $ node [CR $ coord1 (0,0)]+        action = do modifyMark (\(Just MarkCircle) -> Nothing) (0,0)+                    getMark (0,0)+    in Nothing @=? evalGo action cursor++  , "replaces an existing mark" ~:+    let cursor = rootCursor $ node [CR $ coord1 (0,0)]+        action = do modifyMark (\(Just MarkCircle) -> Just MarkX) (0,0)+                    getMark (0,0)+    in Just MarkX @=? evalGo action cursor++  , "adds on to an existing mark property" ~:+    let cursor = rootCursor $ node [MA $ coord1 (0,0)]+        action = do modifyMark (\Nothing -> Just MarkX) (1,0)+                    mapM getMark [(0,0), (1,0)]+    in [Just MarkX, Just MarkX] @=? evalGo action cursor++  , "removes from an existing mark property" ~:+    let cursor = rootCursor $ node [MA $ coords [(0,0), (1,0), (0,1)]]+        action = do modifyMark (\(Just MarkX) -> Nothing) (1,0)+                    modifyMark (\(Just MarkX) -> Nothing) (0,1)+                    mapM getMark [(0,0), (1,0), (0,1)]+    in [Just MarkX, Nothing, Nothing] @=? evalGo action cursor++  , "removes and adds at the same time" ~:+    let cursor = rootCursor $ node [MA $ coords [(0,0), (1,0), (0,1)]]+        action = do modifyMark (\(Just MarkX) -> Just MarkSquare) (0,0)+                    modifyMark (\(Just MarkX) -> Nothing) (0,1)+                    mapM getMark [(0,0), (1,0), (0,1)]+    in [Just MarkSquare, Just MarkX, Nothing] @=? evalGo action cursor+  ]++addChildTests = "addChild" ~: TestList+  [ "adds a first child" ~: do+    cursorNode (execGo (addChild emptyNode) $ rootCursor emptyNode) @?= node1 [] emptyNode+    cursorNode (execGo (addChild $ node1 [W $ Just (1,1)] $ node [B $ Just (2,2)]) $ rootCursor $+                node [B $ Just (0,0)]) @?=+      node1 [B $ Just (0,0)] (node1 [W $ Just (1,1)] $ node [B $ Just (2,2)])++  , "adds a child to the end of the parent's child list" ~:+    cursorNode (execGo (addChild $ node [MN 4]) $ rootCursor $+                node' [B Nothing] [node [MN 2], node [MN 3]]) @?=+    node' [B Nothing] [node [MN 2], node [MN 3], node [MN 4]]++  , "fires childAddedEvent after adding a child" ~:+    let cursor = rootCursor $ node' [B Nothing] [node [MN 2], node [MN 3]]+        action = do on childAddedEvent $ \index -> do+                      cursor <- getCursor+                      tell [(index, cursorNode cursor)]+                    addChild $ node [MN 4]+      in execWriter (runGoT action cursor) @?=+         [(2, node' [B Nothing] [node [MN 2], node [MN 3], node [MN 4]])]+  ]++addChildAtTests = "addChildAt" ~: TestList+  [ "adds an only child" ~:+    cursorNode (execGo (addChildAt 0 $ node [B Nothing]) (rootCursor $ node [])) @?=+    node' [] [node [B Nothing]]++  , "adds a first child" ~:+    cursorNode (execGo (addChildAt 0 $ node [B $ Just (0,0)])+                       (rootCursor $ node' [] [node [B $ Just (1,1)]])) @?=+     node' [] [node [B $ Just (0,0)],+               node [B $ Just (1,1)]]++  , "adds a middle child" ~:+    cursorNode (execGo (addChildAt 1 $ node [B $ Just (1,1)])+                       (rootCursor $ node' [] [node [B $ Just (0,0)],+                                               node [B $ Just (2,2)]])) @?=+    node' [] [node [B $ Just (0,0)],+              node [B $ Just (1,1)],+              node [B $ Just (2,2)]]++  , "adds a last child" ~:+    cursorNode (execGo (addChildAt 2 $ node [B $ Just (2,2)])+                       (rootCursor $ node' [] [node [B $ Just (0,0)],+                                               node [B $ Just (1,1)]])) @?=+    node' [] [node [B $ Just (0,0)],+              node [B $ Just (1,1)],+              node [B $ Just (2,2)]]++  , "fires childAddedEvent after adding a child" ~:+    let cursor = rootCursor $ node' [B Nothing] [node [MN 2], node [MN 4]]+        action = do on childAddedEvent $ \index -> do+                      cursor <- getCursor+                      tell [(index, cursorNode cursor)]+                    addChildAt 1 $ node [MN 3]+    in execWriter (runGoT action cursor) @?=+       [(1, node' [B Nothing] [node [MN 2], node [MN 3], node [MN 4]])]++  , "path stack correctness" ~: TestList+    [ "basic case just not needing updating" ~:+      let cursor = child 0 $ rootCursor $ node' [B Nothing] [node [W Nothing]]+          action = do pushPosition+                      goUp+                      addChildAt 1 $ node [W $ Just (0,0)]+                      popPosition+      in cursorNode (execGo action cursor) @?= node [W Nothing]++    , "basic case just needing updating" ~:+      let cursor = child 0 $ rootCursor $ node' [B Nothing] [node [W Nothing]]+          action = do pushPosition+                      goUp+                      addChildAt 0 $ node [W $ Just (0,0)]+                      popPosition+      in cursorNode (execGo action cursor) @?= node [W Nothing]++    , "basic case definitely needing updating" ~:+      let cursor = rootCursor $ node' [B Nothing] [node [W $ Just (0,0)],+                                                   node [W $ Just (1,1)]]+          action = do goDown 1+                      pushPosition+                      goUp+                      addChildAt 0 $ node [W Nothing]+                      popPosition+      in cursorNode (execGo action cursor) @?= node [W $ Just (1,1)]++    , "multiple paths to update" ~:+      let at y x = B $ Just (y,x)+          level0Node = node' [at 0 0] $ map level1Node [0..1]+          level1Node i = node' [at 1 i] $ map level2Node [0..2]+          level2Node i = node' [at 2 i] $ map level3Node [0..3]+          level3Node i = node [at 3 i]+          action = do goDown 1 >> goDown 2 >> goDown 3+                      pushPosition+                      replicateM_ 3 goUp+                      pushPosition+                      goDown 1 >> goDown 2 >> goDown 2 >> goUp >> goDown 1+                      addChildAt 0 $ node1 [] $ node []+                      goDown 0 >> goDown 0+                      goToRoot+                      popPosition+                      popPosition+      in cursorNode (execGo action $ rootCursor level0Node) @?= node [B $ Just (3,3)]++    , "updates paths with GoUp correctly" ~:+      let cursor = rootCursor $ node1 [B $ Just (0,0)] $ node [W $ Just (1,1)]+          action = do pushPosition+                      goDown 0+                      goUp+                      addChildAt 0 $ node [B $ Just (2,2)]+                      on navigationEvent $ \step -> tell [step]+                      popPosition+          log = execWriter (runGoT action cursor)+      in log @?= [GoDown 1, GoUp 1]+    ]+  ]++deleteChildAtTests = "deleteChildAt" ~: TestList+  [ "ignores invalid indices" ~: do+    second cursorNode (runGo (deleteChildAt 0) $ rootCursor $ node []) @?=+      (NodeDeleteBadIndex, node [])+    let base = node' [] [node [MN 0]]+    second cursorNode (runGo (deleteChildAt (-2)) $ rootCursor base) @?= (NodeDeleteBadIndex, base)+    second cursorNode (runGo (deleteChildAt (-1)) $ rootCursor base) @?= (NodeDeleteBadIndex, base)+    second cursorNode (runGo (deleteChildAt 1) $ rootCursor base) @?= (NodeDeleteBadIndex, base)+    second cursorNode (runGo (deleteChildAt 2) $ rootCursor base) @?= (NodeDeleteBadIndex, base)++  , "deletes an only child" ~:+    let cursor = rootCursor $ node1 [MN 0] $ node [MN 1]+        action = deleteChildAt 0+    in second cursorNode (runGo action cursor) @?= (NodeDeleteOk, node [MN 0])++  , "deletes a first child" ~:+    let cursor = rootCursor $ node' [MN 0] [node [MN 1], node [MN 2]]+        action = deleteChildAt 0+    in second cursorNode (runGo action cursor) @?= (NodeDeleteOk, node1 [MN 0] $ node [MN 2])++  , "deletes middle children" ~: do+    let base = node' [MN 0] [node [MN 1], node [MN 2], node [MN 3], node [MN 4]]+        cursor = rootCursor base+    second cursorNode (runGo (deleteChildAt 1) cursor) @?=+      (NodeDeleteOk, base { nodeChildren = listDeleteAt 1 $ nodeChildren base })+    second cursorNode (runGo (deleteChildAt 2) cursor) @?=+      (NodeDeleteOk, base { nodeChildren = listDeleteAt 2 $ nodeChildren base })++  , "deletes a final child" ~: do+    let base = node' [MN 0] [node [MN 1], node [MN 2], node [MN 3], node [MN 4]]+        cursor = rootCursor base+    second cursorNode (runGo (deleteChildAt 1) cursor) @?=+      (NodeDeleteOk, base { nodeChildren = listDeleteAt 1 $ nodeChildren base })+    second cursorNode (runGo (deleteChildAt 2) cursor) @?=+      (NodeDeleteOk, base { nodeChildren = listDeleteAt 2 $ nodeChildren base })++  , "fires childDeletedEvent after deleting a child" ~:+    let cursor = rootCursor $ node' [MN 0] [node [MN 1], node [MN 2]]+        action = do on childDeletedEvent $ tell . (:[]) . (cursorChildIndex &&& cursorNode)+                    deleteChildAt 1+    in execWriter (runGoT action cursor) @?= [(1, cursorNode $ child 1 cursor)]++  , "path stack correctness" ~: TestList+    [ "basic case just not needing updating" ~:+      let cursor = rootCursor $ node' [B Nothing] [node [W Nothing], node [W $ Just (0,0)]]+          action = do goDown 0+                      pushPosition+                      goUp+                      deleteChildAt 1+                      popPosition+      in cursorNode (execGo action cursor) @?= node [W Nothing]++    , "basic case just needing updating" ~:+      let cursor = rootCursor $ node' [B Nothing] [node [W Nothing], node [W $ Just (0,0)]]+          action = do goDown 1+                      pushPosition+                      goUp+                      deleteChildAt 0+                      popPosition+      in cursorNode (execGo action cursor) @?= node [W $ Just (0,0)]++    , "basic case definitely needing updating" ~:+      let cursor = rootCursor $ node' [B Nothing] [node [W $ Just (0,0)],+                                                   node [W $ Just (1,1)],+                                                   node [W $ Just (2,2)]]+          action = do goDown 2+                      pushPosition+                      goUp+                      deleteChildAt 0+                      popPosition+      in cursorNode (execGo action cursor) @?= node [W $ Just (2,2)]++    , "multiple paths to update" ~:+      let at y x = B $ Just (y,x)+          level0Node = node' [at 0 0] $ map level1Node [0..1]+          level1Node i = node' [at 1 i] $ map level2Node [0..2]+          level2Node i = node' [at 2 i] $ map level3Node [0..3]+          level3Node i = node [at 3 i]+          action = do goDown 1 >> goDown 2 >> goDown 3+                      pushPosition+                      goUp+                      deleteChildAt 1+                      pushPosition+                      goToRoot >> goDown 0 >> goDown 2+                      pushPosition+                      goUp+                      deleteChildAt 0+                      goToRoot >> goDown 1 >> goDown 2+                      deleteChildAt 1+                      replicateM_ 3 popPosition+      in cursorNode (execGo action $ rootCursor level0Node) @?= node [B $ Just (3,3)]++    , "returns an error if a node to delete is on the path stack" ~:+      let base = node' [B $ Just (0,0)] [node [W $ Just (1,1)],+                                         node [W $ Just (2,2)]]+          action = do goDown 1+                      pushPosition+                      goUp+                      pushPosition+                      goDown 0+                      pushPosition+                      goUp+                      deleteChildAt 1+      in second cursorNode (runGo action $ rootCursor base) @?=+         (NodeDeleteOnPathStack, base)+    ]+  ]++gameInfoChangedTests = "gameInfoChangedEvent" ~: TestList+  [ "fires when navigating down" ~:+    let cursor = rootCursor $+                 node1 [B $ Just (0,0)] $+                 node [W $ Just (0,0), GN $ toSimpleText "Foo"]+        action = do on gameInfoChangedEvent onInfo+                    goDown 0+    in execWriter (runGoT action cursor) @?= [(Nothing, Just $ toSimpleText "Foo")]++  , "fires when navigating up" ~:+    let cursor = child 0 $ rootCursor $+                 node1 [B $ Just (0,0)] $+                 node [W $ Just (0,0), GN $ toSimpleText "Foo"]+        action = do on gameInfoChangedEvent onInfo+                    goUp+    in execWriter (runGoT action cursor) @?= [(Just $ toSimpleText "Foo", Nothing)]++  , "fires from within popPosition" ~:+    let cursor = rootCursor $+                 node1 [B $ Just (0,0)] $+                 node [W $ Just (0,0), GN $ toSimpleText "Foo"]+        action = do pushPosition+                    goDown 0+                    goUp+                    on gameInfoChangedEvent onInfo+                    popPosition+    in execWriter (runGoT action cursor) @?=+       [(Nothing, Just $ toSimpleText "Foo"), (Just $ toSimpleText "Foo", Nothing)]++  , "fires when modifying properties" ~:+    let cursor = rootCursor $ node []+        action = do on gameInfoChangedEvent onInfo+                    modifyProperties $ const $ return [GN $ toSimpleText "Foo"]+                    modifyProperties $ const $ return [GN $ toSimpleText "Bar"]+                    modifyProperties $ const $ return []+    in execWriter (runGoT action cursor) @?=+       [(Nothing, Just $ toSimpleText "Foo"),+        (Just $ toSimpleText "Foo", Just $ toSimpleText "Bar"),+        (Just $ toSimpleText "Bar", Nothing)]+  ]+  where onInfo old new = tell [(gameInfoGameName old, gameInfoGameName new)]
+ tests/Game/Goatee/Lib/ParserTest.hs view
@@ -0,0 +1,240 @@+-- This file is part of Goatee.+--+-- Copyright 2014 Bryan Gardiner+--+-- Goatee is free software: you can redistribute it and/or modify+-- it under the terms of the GNU Affero General Public License as published by+-- the Free Software Foundation, either version 3 of the License, or+-- (at your option) any later version.+--+-- Goatee is distributed in the hope that it will be useful,+-- but WITHOUT ANY WARRANTY; without even the implied warranty of+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the+-- GNU Affero General Public License for more details.+--+-- You should have received a copy of the GNU Affero General Public License+-- along with Goatee.  If not, see <http://www.gnu.org/licenses/>.++module Game.Goatee.Lib.ParserTest (tests) where++import Control.Monad (forM_)+import Game.Goatee.Lib.ParserTestUtils+import Game.Goatee.Lib.Property+import Game.Goatee.Lib.TestInstances ()+import Game.Goatee.Lib.TestUtils+import Game.Goatee.Lib.Tree+import Game.Goatee.Lib.Types+import Test.HUnit ((~:), (@?=), Test (TestList))++tests = "Game.Goatee.Lib.Parser" ~: TestList+  [ baseCaseTests+  , whitespaceTests+  , passConversionTests+  , movePropertyTests+  , setupPropertyTests+  , nodeAnnotationPropertyTests+  , moveAnnotationPropertyTests+  , rootPropertyTests+  ]++baseCaseTests = "base cases" ~: TestList+  [ "works with the trivial collection" ~:+    parseOrFail "(;)" (@?= emptyNode)++  , "works with only a size property" ~: do+    parseOrFail "(;SZ[1])" (@?= root 1 1 [] [])+    parseOrFail "(;SZ[9])" (@?= root 9 9 [] [])+  ]++whitespaceTests = "whitespace handling" ~: TestList+  [ "parses with no extra whitespace" ~:+    parseOrFail "(;SZ[4];AB[aa][bb]AW[cc];W[dd])"+    (@?= root 4 4 [] [node1 [AB $ coords [(0,0), (1,1)], AW $ coords [(2,2)]] $+                      node [W $ Just (3,3)]])++  , "parses with spaces between nodes" ~:+    parseOrFail "(;SZ[1] ;B[])" (@?= root 1 1 [] [node [B Nothing]])++  , "parses with spaces between properties" ~:+    parseOrFail "(;SZ[1] AB[aa])" (@?= root 1 1 [AB $ coords [(0,0)]] [])++  , "parses with spaces between a property's name and value" ~:+    parseOrFail "(;SZ [1])" (@?= root 1 1 [] [])++  , "parses with spaces between property values" ~:+    parseOrFail "(;SZ[2]AB[aa] [bb])" (@?= root 2 2 [AB $ coords [(0,0), (1,1)]] [])++  , "parses with spaces between many elements" ~:+    parseOrFail " ( ; SZ [4] ; AB [aa:ad] [bb] AW [cc] ; W [dd] ; B [] ) "+    (@?= root 4 4 [] [node1 [AB $ coords' [(1,1)] [((0,0), (0,3))],+                             AW $ coords [(2,2)]] $+                      node1 [W $ Just (3,3)] $+                      node [B Nothing]])++  -- TODO Test handling of whitespace between an unknown property name and+  -- [value].+  ]++passConversionTests = "B[tt]/W[tt] pass conversion" ~: TestList+  [ "converts B[tt] for a board sizes <=19x19" ~: do+    parseOrFail "(;SZ[1];B[tt])" (@?= root 1 1 [] [node [B Nothing]])+    parseOrFail "(;SZ[9];B[tt])" (@?= root 9 9 [] [node [B Nothing]])+    parseOrFail "(;SZ[19];B[tt])" (@?= root 19 19 [] [node [B Nothing]])++  , "converts W[tt] for a board sizes <=19x19" ~: do+    parseOrFail "(;SZ[1];W[tt])" (@?= root 1 1 [] [node [W Nothing]])+    parseOrFail "(;SZ[9];W[tt])" (@?= root 9 9 [] [node [W Nothing]])+    parseOrFail "(;SZ[19];W[tt])" (@?= root 19 19 [] [node [W Nothing]])++  , "doesn't convert B[tt] for a board sizes >19x19" ~: do+    parseOrFail "(;SZ[20];B[tt])" (@?= root 20 20 [] [node [B $ Just (19, 19)]])+    parseOrFail "(;SZ[21];B[tt])" (@?= root 21 21 [] [node [B $ Just (19, 19)]])++  , "doesn't convert W[tt] for a board sizes >19x19" ~: do+    parseOrFail "(;SZ[20];W[tt])" (@?= root 20 20 [] [node [W $ Just (19, 19)]])+    parseOrFail "(;SZ[21];W[tt])" (@?= root 21 21 [] [node [W $ Just (19, 19)]])++  , "doesn't convert non-move properties" ~: do+    -- TODO These should error, rather than parsing fine.+    parseOrFail "(;SZ[9];AB[tt])" (@?= root 9 9 [] [node [AB $ coords [(19, 19)]]])+    parseOrFail "(;SZ[9];TR[tt])" (@?= root 9 9 [] [node [TR $ coords [(19, 19)]]])+  ]++movePropertyTests = "move properties" ~: TestList+  [ "B" ~: TestList+    [ "parses moves" ~: do+      parseOrFail "(;B[aa])" (@?= node [B $ Just (0, 0)])+      parseOrFail "(;B[cp])" (@?= node [B $ Just (2, 15)])+    , "parses passes" ~:+      parseOrFail "(;B[])" (@?= node [B Nothing])+    ]+  , "KO parses" ~:+    parseOrFail "(;KO[])" (@?= node [KO])+  -- TODO Test MN (assert positive).+  , "W" ~: TestList+    [ "parses moves" ~: do+      parseOrFail "(;W[aa])" (@?= node [W $ Just (0, 0)])+      parseOrFail "(;W[cp])" (@?= node [W $ Just (2, 15)])+    , "parses passes" ~:+      parseOrFail "(;W[])" (@?= node [W Nothing])+    ]+  ]++setupPropertyTests = "setup properties" ~: TestList+  [ "AB parses" ~: do+    parseOrFail "(;AB[ab])" (@?= node [AB $ coords [(0, 1)]])+    parseOrFail "(;AB[ab][cd:ef])" (@?= node [AB $ coords' [(0, 1)] [((2, 3), (4, 5))]])+  , "AE parses" ~: do+    parseOrFail "(;AE[ae])" (@?= node [AE $ coords [(0, 4)]])+    parseOrFail "(;AE[ae][ff:gg])" (@?= node [AE $ coords' [(0, 4)] [((5, 5), (6, 6))]])+  , "AW parses" ~: do+    parseOrFail "(;AW[aw])" (@?= node [AW $ coords [(0, 22)]])+    parseOrFail "(;AW[aw][xy:yz])" (@?= node [AW $ coords' [(0, 22)] [((23, 24), (24, 25))]])+  , "PL parses" ~: do+    parseOrFail "(;PL[B])" (@?= node [PL Black])+    parseOrFail "(;PL[W])" (@?= node [PL White])+  ]++nodeAnnotationPropertyTests = "node annotation properties" ~: TestList+  [ "C parses" ~:+    parseOrFail "(;C[Me [30k\\]: What is White doing??\\\n\n:(])"+      (@?= node [C $ toText "Me [30k]: What is White doing??\n:("])+  , "DM parses" ~: do+    parseOrFail "(;DM[1])" (@?= node [DM Double1])+    parseOrFail "(;DM[2])" (@?= node [DM Double2])+  , "GB parses" ~: do+    parseOrFail "(;GB[1])" (@?= node [GB Double1])+    parseOrFail "(;GB[2])" (@?= node [GB Double2])+  , "GW parses" ~: do+    parseOrFail "(;GW[1])" (@?= node [GW Double1])+    parseOrFail "(;GW[2])" (@?= node [GW Double2])+  , "HO parses" ~: do+    parseOrFail "(;HO[1])" (@?= node [HO Double1])+    parseOrFail "(;HO[2])" (@?= node [HO Double2])+  , "N parses" ~:+    parseOrFail "(;N[The best\\\n\nmove])" (@?= node [N $ toSimpleText "The best move"])+  , "UC parses" ~: do+    parseOrFail "(;UC[1])" (@?= node [UC Double1])+    parseOrFail "(;UC[2])" (@?= node [UC Double2])+  , "V parses" ~: do+    parseOrFail "(;V[-34.5])" (@?= node [V $ read "-34.5"])+    parseOrFail "(;V[50])" (@?= node [V $ read "50"])+  ]++moveAnnotationPropertyTests = "move annotation properties" ~: TestList+  [ "BM parses" ~: do+    parseOrFail "(;BM[1])" (@?= node [BM Double1])+    parseOrFail "(;BM[2])" (@?= node [BM Double2])+  , "DO parses" ~:+    parseOrFail "(;DO[])" (@?= node [DO])+  , "IT parses" ~:+    parseOrFail "(;IT[])" (@?= node [IT])+  , "TE parses" ~: do+    parseOrFail "(;TE[1])" (@?= node [TE Double1])+    parseOrFail "(;TE[2])" (@?= node [TE Double2])+  ]++-- TODO Test markup properties.++rootPropertyTests = "root properties" ~: TestList+  [ "AP parses" ~:+    parseOrFail "(;AP[GoGoGo:1.2.3])"+    (@?= node [AP (toSimpleText "GoGoGo") (toSimpleText "1.2.3")])+  , "CA parses" ~: do+    parseOrFail "(;CA[UTF-8])" (@?= node [CA $ toSimpleText "UTF-8"])+    parseOrFail "(;CA[ISO-8859-1])" (@?= node [CA $ toSimpleText "ISO-8859-1"])+  , "FF" ~: TestList+    [ "accepts version 4" ~:+      parseOrFail "(;FF[4])" (@?= node [FF 4])+    , "rejects versions 1-3" ~: do+      parseAndFail "(;FF[1])"+      parseAndFail "(;FF[2])"+      parseAndFail "(;FF[3])"+    ]+  , "GM" ~: TestList+    [ "parses 1 (Go)" ~:+      parseOrFail "(;GM[1])" (@?= node [GM 1])+    , "rejects unsupported games" ~:+      forM_ [2..16] $ \x -> parseAndFail $ "(;GM[" ++ show x ++ "]"+    ]+  , "ST" ~: TestList+    [ "parses valid variation modes" ~: do+      parseOrFail "(;ST[0])" (@?= node [ST $ VariationMode ShowChildVariations True])+      parseOrFail "(;ST[1])" (@?= node [ST $ VariationMode ShowCurrentVariations True])+      parseOrFail "(;ST[2])" (@?= node [ST $ VariationMode ShowChildVariations False])+      parseOrFail "(;ST[3])" (@?= node [ST $ VariationMode ShowCurrentVariations False])+    , "rejects invalid variation modes" ~:+      forM_ [4..10] $ \x -> parseAndFail $ "(;ST[" ++ show x ++ "]"+    ]+  , "SZ" ~: TestList+    [ "parses square boards" ~:+      forM_ [1..52] $ \x ->+        parseOrFail ("(;SZ[" ++ show x ++ "])") (@?= node [SZ x x])+    , "parses nonsquare boards" ~: do+      parseOrFail "(;SZ[1:2])" (@?= node [SZ 1 2])+      parseOrFail "(;SZ[1:9])" (@?= node [SZ 1 9])+      parseOrFail "(;SZ[1:19])" (@?= node [SZ 1 19])+      parseOrFail "(;SZ[2:1])" (@?= node [SZ 2 1])+      parseOrFail "(;SZ[9:1])" (@?= node [SZ 9 1])+      parseOrFail "(;SZ[19:1])" (@?= node [SZ 19 1])+      parseOrFail "(;SZ[19:52])" (@?= node [SZ 19 52])+      parseOrFail "(;SZ[10:16])" (@?= node [SZ 10 16])+    , "rejects invalid sizes" ~: do+      -- Boards must have length at least 1...+      parseAndFail "(;SZ[0])"+      parseAndFail "(;SZ[-1])"+      -- ...and at most 52.+      parseAndFail "(;SZ[53])"+      parseAndFail "(;SZ[54])"+      parseAndFail "(;SZ[0:19])"+      parseAndFail "(;SZ[19:0])"+      parseAndFail "(;SZ[9:53])"+      parseAndFail "(;SZ[53:9])"+    -- This is specified by the SGF spec:+    , "rejects square boards defined with two numbers" ~: do+      parseAndFail "(;SZ[1:1])"+      parseAndFail "(;SZ[19:19])"+    ]+  ]++-- TODO Test the test of the properties.
+ tests/Game/Goatee/Lib/ParserTestUtils.hs view
@@ -0,0 +1,63 @@+-- This file is part of Goatee.+--+-- Copyright 2014 Bryan Gardiner+--+-- Goatee is free software: you can redistribute it and/or modify+-- it under the terms of the GNU Affero General Public License as published by+-- the Free Software Foundation, either version 3 of the License, or+-- (at your option) any later version.+--+-- Goatee is distributed in the hope that it will be useful,+-- but WITHOUT ANY WARRANTY; without even the implied warranty of+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the+-- GNU Affero General Public License for more details.+--+-- You should have received a copy of the GNU Affero General Public License+-- along with Goatee.  If not, see <http://www.gnu.org/licenses/>.++module Game.Goatee.Lib.ParserTestUtils (+  parseOrFail,+  parseAndFail,+  assertParse,+  assertNoParse,+  ) where++import Control.Applicative ((<*))+import Game.Goatee.Lib.Parser+import Game.Goatee.Lib.Tree+import Test.HUnit (assertFailure)+import Text.ParserCombinators.Parsec (Parser, eof, parse)++-- Parses a string as a complete SGF document.  On success, executes the+-- continuation function with the result.  Otherwise, causes an assertion+-- failure.+parseOrFail :: String -> (Node -> IO ()) -> IO ()+parseOrFail input cont = case parseString input of+  Left error -> assertFailure $ "Failed to parse SGF: " ++ error+  Right (Collection roots) -> case roots of+    root:[] -> cont root+    _ -> assertFailure $ "Expected a single root node, got: " ++ show roots++-- Parses a string as a complete SGF document and expects failure.+parseAndFail :: String -> IO ()+parseAndFail input = case parseString input of+  Left _ -> return ()+  Right result -> assertFailure $ "Expected " ++ show input +++                  " not to parse.  Parsed to " ++ show result ++ "."++-- Parses a string using the given parser and expects the parser to consume the+-- entire input.  On success, executes the continuation function with the+-- result.  Otherwise, causes an assertion failure.+assertParse :: Parser a -> String -> (a -> IO ()) -> IO ()+assertParse parser input cont = case parse (parser <* eof) "<assertParse>" input of+  Left error -> assertFailure $ "Failed to parse: " ++ show error+  Right result -> cont result++-- Tries to parse a string using the given parser.  If the parse succeeds then+-- this function causes an assertion failure, otherwise this function succeeds.+assertNoParse :: Show a => Parser a -> String -> IO ()+assertNoParse parser input = case parse (parser <* eof) "<assertNoParse>" input of+  Left _ -> return ()+  Right result -> assertFailure $+                  "Expected " ++ show input ++ " not to parse.  " +++                  "Parsed to " ++ show result ++ "."
+ tests/Game/Goatee/Lib/Property/ParserTest.hs view
@@ -0,0 +1,388 @@+-- This file is part of Goatee.+--+-- Copyright 2014 Bryan Gardiner+--+-- Goatee is free software: you can redistribute it and/or modify+-- it under the terms of the GNU Affero General Public License as published by+-- the Free Software Foundation, either version 3 of the License, or+-- (at your option) any later version.+--+-- Goatee is distributed in the hope that it will be useful,+-- but WITHOUT ANY WARRANTY; without even the implied warranty of+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the+-- GNU Affero General Public License for more details.+--+-- You should have received a copy of the GNU Affero General Public License+-- along with Goatee.  If not, see <http://www.gnu.org/licenses/>.++module Game.Goatee.Lib.Property.ParserTest (tests) where++import Control.Applicative ((<$>))+import Control.Monad (forM_)+import Data.Maybe (catMaybes)+import Game.Goatee.Common+import Game.Goatee.Lib.ParserTestUtils+import Game.Goatee.Lib.Property+import Game.Goatee.Lib.TestInstances ()+import Game.Goatee.Lib.TestUtils+import Game.Goatee.Lib.Types+import Test.HUnit ((~:), (@?=), Test (TestList))+import Text.ParserCombinators.Parsec (Parser)++tests = "Game.Goatee.Lib.Property.ParserTest" ~: TestList+  [ -- Tests for public parsers.+    parserColorTests+  , parserDoubleTests+  , parserGameResultTests+  , parserIntegralTests+  , parserMoveTests+  , parserRealTests+  , parserRulesetTests+  , parserSizeTests+  , parserVariationModeTests+    -- Tests for private parsers.+  , lineTests+  , simpleTextTests+  , simpleTextComposedTests+  , textTests+  , textComposedTests+    -- Miscellaneous tests.+  , propertyValueArityTests+  ]++parserColorTests = "colorParser" ~: TestList+  [ "parses B" ~: assertParse colorParser "[B]" (@?= Black)+  , "parses W" ~: assertParse colorParser "[W]" (@?= White)+  ]++parserDoubleTests = "doubleParser" ~: TestList+  [ "parses 1" ~: assertParse doubleParser "[1]" (@?= Double1)+  , "parses 2" ~: assertParse doubleParser "[2]" (@?= Double2)+  ]++parserGameResultTests = "gameResultParser" ~: TestList+  [ "draw" ~: do+    assertParse gameResultParser "[0]" (@?= GameResultDraw)+    assertParse gameResultParser "[Draw]" (@?= GameResultDraw)++  , "void" ~:+    assertParse gameResultParser "[Void]" (@?= GameResultVoid)++  , "unknown" ~:+    assertParse gameResultParser "[?]" (@?= GameResultUnknown)++  , "Black wins by points" ~: do+    assertParse gameResultParser "[B+0.5]" (@?= GameResultWin Black (WinByScore $ read "0.5"))+    assertParse gameResultParser "[B+11]" (@?= GameResultWin Black (WinByScore $ read "11"))+    assertParse gameResultParser "[B+354.5]" (@?= GameResultWin Black (WinByScore $ read "354.5"))++  , "Black wins by resignation" ~: do+    assertParse gameResultParser "[B+R]" (@?= GameResultWin Black WinByResignation)+    assertParse gameResultParser "[B+Resign]" (@?= GameResultWin Black WinByResignation)++  , "Black wins on time" ~: do+    assertParse gameResultParser "[B+T]" (@?= GameResultWin Black WinByTime)+    assertParse gameResultParser "[B+Time]" (@?= GameResultWin Black WinByTime)++  , "Black wins by forfeit" ~: do+    assertParse gameResultParser "[B+F]" (@?= GameResultWin Black WinByForfeit)+    assertParse gameResultParser "[B+Forfeit]" (@?= GameResultWin Black WinByForfeit)++  , "White wins by points" ~: do+    assertParse gameResultParser "[W+0.5]" (@?= GameResultWin White (WinByScore $ read "0.5"))+    assertParse gameResultParser "[W+11]" (@?= GameResultWin White (WinByScore $ read "11"))+    assertParse gameResultParser "[W+354.5]" (@?= GameResultWin White (WinByScore $ read "354.5"))++  , "White wins by resignation" ~: do+    assertParse gameResultParser "[W+R]" (@?= GameResultWin White WinByResignation)+    assertParse gameResultParser "[W+Resign]" (@?= GameResultWin White WinByResignation)++  , "White wins on time" ~: do+    assertParse gameResultParser "[W+T]" (@?= GameResultWin White WinByTime)+    assertParse gameResultParser "[W+Time]" (@?= GameResultWin White WinByTime)++  , "White wins by forfeit" ~: do+    assertParse gameResultParser "[W+F]" (@?= GameResultWin White WinByForfeit)+    assertParse gameResultParser "[W+Forfeit]" (@?= GameResultWin White WinByForfeit)++  , "custom game results" ~: do+    assertParseToOther ""+    assertParseToOther "Everyone wins."+    assertParseToOther "W+Nuclear tesuji"+    assertParseToOther "B+Flamingo"+  ]+  where assertParseToOther input =+          assertParse gameResultParser ('[' : input ++ "]")+          (@?= GameResultOther (toSimpleText input))++parserIntegralTests = "integralParser" ~: TestList $ integerTestsFor integralParser++parserMoveTests = "moveParser" ~: TestList $ coordTestsFor moveParser Just +++  [ "parses an empty move as a pass" ~: assertParse moveParser "[]" (@?= Nothing)+  ]++parserRealTests = "realParser" ~: TestList $ integerTestsFor realParser +++  [ "parses a decimal zero" ~: assertParse realParser "[0.0]" (@?= 0)++  , "parses fractional positive numbers" ~: do+    assertParse realParser "[0.5]" (@?= read "0.5")+    assertParse realParser "[00.001250]" (@?= read "0.00125")+    assertParse realParser "[3.14]" (@?= read "3.14")+    assertParse realParser "[10.0]" (@?= read "10")++  , "parses fractional negative numbers" ~: do+    assertParse realParser "[-0.5]" (@?= read "-0.5")+    assertParse realParser "[-00.001250]" (@?= read "-0.00125")+    assertParse realParser "[-3.14]" (@?= read "-3.14")+    assertParse realParser "[-10.0]" (@?= read "-10")+  ]++parserRulesetTests = "rulesetParser" ~: TestList+  [ "parses known rules" ~: do+    assertParse rulesetParser "[AGA]" (@?= KnownRuleset RulesetAga)+    assertParse rulesetParser "[Goe]" (@?= KnownRuleset RulesetIng)+    assertParse rulesetParser "[Japanese]" (@?= KnownRuleset RulesetJapanese)+    assertParse rulesetParser "[NZ]" (@?= KnownRuleset RulesetNewZealand)++  , "parses unknown rules" ~: do+    assertParse rulesetParser "[Foo]" (@?= UnknownRuleset "Foo")+    assertParse rulesetParser "[First capture]" (@?= UnknownRuleset "First capture")+  ]++parserSizeTests = "size" ~: TestList+  [ "parses square boards" ~: do+    assertParse sizeParser "[1]" (@?= (1, 1))+    assertParse sizeParser "[4]" (@?= (4, 4))+    assertParse sizeParser "[9]" (@?= (9, 9))+    assertParse sizeParser "[19]" (@?= (19, 19))+    assertParse sizeParser "[52]" (@?= (52, 52))++  , "parses rectangular boards" ~: do+    assertParse sizeParser "[1:2]" (@?= (1, 2))+    assertParse sizeParser "[9:5]" (@?= (9, 5))+    assertParse sizeParser "[19:9]" (@?= (19, 9))++  , "rejects boards of non-positive size" ~: do+    assertNoParse sizeParser "[0]"+    assertNoParse sizeParser "[-1]"+    assertNoParse sizeParser "[0:5]"+    assertNoParse sizeParser "[5:0]"+    assertNoParse sizeParser "[0:-2]"++  , "rejects square boards given in rectangular format" ~: do+    assertNoParse sizeParser "[1:1]"+    assertNoParse sizeParser "[13:13]"+  ]++parserVariationModeTests = "variationModeParser" ~: TestList+  [ "mode 0" ~:+    assertParse variationModeParser "[0]" (@?= VariationMode ShowChildVariations True)++  , "mode 1" ~:+    assertParse variationModeParser "[1]" (@?= VariationMode ShowCurrentVariations True)++  , "mode 2" ~:+    assertParse variationModeParser "[2]" (@?= VariationMode ShowChildVariations False)++  , "mode 3" ~:+    assertParse variationModeParser "[3]" (@?= VariationMode ShowCurrentVariations False)+  ]++lineTests = "line" ~: TestList+  [ "parses all valid values" ~: do+     let cases = zip (['a'..'z'] ++ ['A'..'Z']) [0..]+     forM_ cases $ \(char, expected) ->+       assertParse line [char] (@?= expected)+  ]++simpleTextTests = "simpleText" ~: TestList $+  textTestsFor simpleText fromSimpleText False +++  textUnescapedNewlineConvertingTests (fromSimpleText <$> simpleText False)++simpleTextComposedTests = "simpleText composed" ~: TestList $+  textTestsFor simpleText fromSimpleText True +++  textUnescapedNewlineConvertingTests (fromSimpleText <$> simpleText True)++textTests = "text" ~: TestList $+  textTestsFor text id False +++  textUnescapedNewlinePreservingTests (text False)++textComposedTests = "text composed" ~: TestList $+  textTestsFor text id True +++  textUnescapedNewlinePreservingTests (text True)++propertyValueArityTests = "property value arities" ~: TestList+  [ "single values (single)" ~: TestList+    [ "accepts a property that requires a single number" ~:+      parseOrFail "(;SZ[1])" (@?= node [SZ 1 1])++    , "accepts a property that requires a single point" ~:+      parseOrFail "(;B[dd])" (@?= node [B $ Just (3, 3)])+    ]++  , "lists (listOf)" ~: TestList+    [ "doesn't accept an empty list" ~:+      parseAndFail "(;AR[])"++    , "accepts a single value" ~:+      parseOrFail "(;AR[aa:bb])" (@?= node [AR [((0, 0), (1, 1))]])++    , "accepts two values" ~:+      parseOrFail "(;AR[aa:bb][cc:de])" (@?= node [AR [((0, 0), (1, 1)),+                                                       ((2, 2), (3, 4))]])+    ]++  , "point lists (listOfPoint)" ~: TestList+    [ "doesn't accept an empty list" ~:+      parseAndFail "(;AB[])"++    , "accepts a single point" ~:+      parseOrFail "(;AB[aa])" (@?= node [AB $ coords [(0, 0)]])++    , "accepts two points" ~:+      parseOrFail "(;AB[aa][bb])" (@?= node [AB $ coords [(0, 0), (1, 1)]])++    , "accepts a rectangle" ~:+      parseOrFail "(;AB[aa:bb])" (@?= node [AB $ coords' [] [((0, 0), (1, 1))]])++    , "accepts two rectangles" ~:+      parseOrFail "(;AB[aa:bb][cd:de])" (@?= node [AB $ coords' [] [((0, 0), (1, 1)),+                                                                    ((2, 3), (3, 4))]])+    ]++  , "point elists (elistOfPoint)" ~: TestList+    [ "accepts an empty list" ~:+      parseOrFail "(;VW[])" (@?= node [VW $ coords []])++    , "accepts single points" ~:+      parseOrFail "(;VW[aa][bb])" (@?= node [VW $ coords [(0, 0), (1, 1)]])++    , "accepts a rectangle" ~:+      parseOrFail "(;VW[aa:bb])" (@?= node [VW $ coords' [] [((0, 0), (1, 1))]])++    , "accepts two rectangles" ~:+      parseOrFail "(;VW[aa:bb][cc:dd])" (@?= node [VW $ coords' [] [((0, 0), (1, 1)),+                                                                    ((2, 2), (3, 3))]])+    ]++  -- TODO Test that invalid rectangles such as cd:dc fail to parse (or rather,+  -- get corrected with a warning).+  ]++integerTestsFor :: (Eq a, Num a, Show a) => Parser a -> [Test]+integerTestsFor parser =+  [ "parses 0" ~: do+    assertParse parser "[0]" (@?= 0)+    assertParse parser "[+0]" (@?= 0)+    assertParse parser "[-0]" (@?= 0)++  , "parses positive integers" ~: do+    assertParse parser "[1]" (@?= 1)+    assertParse parser "[20]" (@?= 20)+    assertParse parser "[4294967296]" (@?= (2 ^ 32))+    assertParse parser "[18446744073709551616]" (@?= (2 ^ 64))++  , "parses positive integers with the plus sign" ~: do+    assertParse parser "[+1]" (@?= 1)+    assertParse parser "[+20]" (@?= 20)+    assertParse parser "[+4294967296]" (@?= (2 ^ 32))+    assertParse parser "[+18446744073709551616]" (@?= (2 ^ 64))++  , "parses negative integers" ~: do+    assertParse parser "[-1]" (@?= (-1))+    assertParse parser "[-20]" (@?= (-20))+    assertParse parser "[-4294967296]" (@?= (- (2 ^ 32)))+    assertParse parser "[-18446744073709551616]" (@?= (- (2 ^ 64)))+  ]++coordTestsFor :: (Eq a, Show a) => Parser a -> (Coord -> a) -> [Test]+coordTestsFor parser f =+  [ "parses boundary points" ~: do+    assertParse parser "[aa]" (@?= f (0, 0))+    assertParse parser "[zz]" (@?= f (25, 25))+    assertParse parser "[AA]" (@?= f (26, 26))+    assertParse parser "[ZZ]" (@?= f (51, 51))++  , "parses coordinate order correctly" ~: do+    assertParse parser "[ab]" (@?= f (0, 1))+    assertParse parser "[ba]" (@?= f (1, 0))++  , "doesn't parse a partial point" ~:+    assertNoParse parser "[a]"+  ]++textTestsFor :: (Bool -> Parser a) -> (a -> String) -> Bool -> [Test]+textTestsFor makeParser toString testComposed =+  let rawParser = makeParser testComposed+      parser = toString <$> rawParser+      composedParser = mapTuple toString <$> compose rawParser rawParser+  in catMaybes+    [ Just $ "parses an empty string" ~:+      assertParse parser "" (@?= "")++    , Just $ "parses a short string" ~:+      assertParse parser "Hello, world." (@?= "Hello, world.")++    , Just $ "preserves leading and trailing whitespace" ~:+      assertParse parser " \tHi. \t" (@?= "  Hi.  ")++    , Just $ "parses escaped backslashes" ~: do+      assertParse parser "\\\\" (@?= "\\")+      assertParse parser "\\\\\\\\" (@?= "\\\\")+      assertNoParse parser "\\"+      assertNoParse parser "\\\\\\"++    , Just $ "parses escaped ']'s" ~: do+      assertParse parser "\\]" (@?= "]")+      assertParse parser "\\]\\\\\\]" (@?= "]\\]")++    , Just $ if testComposed+           then "parses escaped ':'s" ~: do+             assertParse parser "\\:" (@?= ":")+             assertParse parser "\\:\\\\\\:" (@?= ":\\:")+             assertNoParse parser ":"+           else "parses unescaped ':'s" ~: do+             assertParse parser ":" (@?= ":")+             assertParse parser "::" (@?= "::")+             -- An escaped colon should parse just the same.+             assertParse parser "\\:" (@?= ":")++    , if not testComposed+      then Nothing+      else Just $ "supports composed values" ~: do+        assertParse composedParser ":" (@?= ("", ""))+        assertParse composedParser "a:" (@?= ("a", ""))+        assertParse composedParser ":z" (@?= ("", "z"))+        assertParse composedParser "a:z" (@?= ("a", "z"))+        assertParse composedParser "a\\\\:z" (@?= ("a\\", "z"))+        assertParse composedParser "a\\:b:y\\:z" (@?= ("a:b", "y:z"))+        assertNoParse composedParser ""++    , -- Tests non-newline whitespace replacement.  Newline handling is+      -- specific to individual parsers.+      Just $ "replaces whitespace with spaces" ~:+      assertParse parser "\t\r\f\v" (@?= "    ")++    , Just $ "removes escaped newlines" ~: do+      assertParse parser "\\\n" (@?= "")+      assertParse parser "foo\\\nbar" (@?= "foobar")+      assertParse parser "foo \\\n bar" (@?= "foo  bar")+    ]++textUnescapedNewlinePreservingTests :: Parser String -> [Test]+textUnescapedNewlinePreservingTests parser =+  [ "preserves unescaped newlines" ~: do+    assertParse parser "\n" (@?= "\n")+    assertParse parser "\n\n" (@?= "\n\n")+    assertParse parser "foo\nbar" (@?= "foo\nbar")+    assertParse parser "foo \n bar" (@?= "foo \n bar")+  ]++textUnescapedNewlineConvertingTests :: Parser String -> [Test]+textUnescapedNewlineConvertingTests parser =+  [ "converts unescaped newlines to spaces" ~: do+    assertParse parser "\n" (@?= " ")+    assertParse parser "\n\n" (@?= "  ")+    assertParse parser "foo\nbar" (@?= "foo bar")+    assertParse parser "foo \n bar" (@?= "foo   bar")+  ]
+ tests/Game/Goatee/Lib/PropertyTest.hs view
@@ -0,0 +1,72 @@+-- This file is part of Goatee.+--+-- Copyright 2014 Bryan Gardiner+--+-- Goatee is free software: you can redistribute it and/or modify+-- it under the terms of the GNU Affero General Public License as published by+-- the Free Software Foundation, either version 3 of the License, or+-- (at your option) any later version.+--+-- Goatee is distributed in the hope that it will be useful,+-- but WITHOUT ANY WARRANTY; without even the implied warranty of+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the+-- GNU Affero General Public License for more details.+--+-- You should have received a copy of the GNU Affero General Public License+-- along with Goatee.  If not, see <http://www.gnu.org/licenses/>.++module Game.Goatee.Lib.PropertyTest (tests) where++import qualified Data.Set as Set+import Game.Goatee.Lib.Property+import Game.Goatee.Lib.Types+import Test.HUnit ((~:), (@=?), Test (TestList))++tests = "Game.Goatee.Lib.Property" ~: TestList+  [ propertyMetadataTests+  , markPropertyTests+  ]++propertyMetadataTests = "property metadata" ~: TestList+  [ "game info properties" ~: gameInfoProperties @=? filterTo GameInfoProperty allProperties+  , "general properties" ~: generalProperties @=? filterTo GeneralProperty allProperties+  , "move properties" ~: moveProperties @=? filterTo MoveProperty allProperties+  , "root properties" ~: rootProperties @=? filterTo RootProperty allProperties+  , "setup properties" ~: setupProperties @=? filterTo SetupProperty allProperties++  , "inherited properties" ~: [DD cl] @=? filter propertyInherited allProperties+  ]+  where filterTo propType = filter ((propType ==) . propertyType)+        moveProperties = [-- Move properties.+                          B Nothing, KO, MN 1, W Nothing,+                          -- Move annotation properties.+                          BM db, DO, IT, TE db]+        setupProperties = [-- Setup properties.+                           AB cl, AE cl, AW cl, PL Black]+        generalProperties = [-- Node annotation properties.+                             C tx, DM db, GB db, GW db, HO db, N st, UC db, V rv,+                             -- Markup properties.+                             AR [], CR cl, DD cl, LB [], LN [], MA cl, SL cl, SQ cl, TR cl,+                             -- Guess this fits here.+                             UnknownProperty "" (toUnknownPropertyValue "")]+        rootProperties = [-- Root properties.+                          AP st st, CA st, FF 1, GM 1, ST vm, SZ 1 1]+        gameInfoProperties = [-- Game info properties.+                              AN st, BR st, BT st, CP st, DT st, EV st, GC tx, GN st, ON st, OT st,+                              PB st, PC st, PW st, RE GameResultVoid, RO st, RU ru,+                              SO st, TM rv, US st, WR st, WT st]+        allProperties = moveProperties ++ setupProperties ++ generalProperties +++                        rootProperties ++ gameInfoProperties+        cl = emptyCoordList+        db = Double1+        tx = toText ""+        st = toSimpleText ""+        ru = KnownRuleset RulesetJapanese+        rv = 1+        vm = defaultVariationMode++markPropertyTests = "markProperty" ~: TestList+  [ "doesn't repeat properties" ~:+    let marks = [minBound..maxBound]+    in length marks @=? Set.size (Set.fromList $ map (propertyName . markProperty) marks)+  ]
+ tests/Game/Goatee/Lib/RoundTripTest.hs view
@@ -0,0 +1,103 @@+-- This file is part of Goatee.+--+-- Copyright 2014 Bryan Gardiner+--+-- Goatee is free software: you can redistribute it and/or modify+-- it under the terms of the GNU Affero General Public License as published by+-- the Free Software Foundation, either version 3 of the License, or+-- (at your option) any later version.+--+-- Goatee is distributed in the hope that it will be useful,+-- but WITHOUT ANY WARRANTY; without even the implied warranty of+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the+-- GNU Affero General Public License for more details.+--+-- You should have received a copy of the GNU Affero General Public License+-- along with Goatee.  If not, see <http://www.gnu.org/licenses/>.++module Game.Goatee.Lib.RoundTripTest (tests) where++import Data.Function (on)+import Game.Goatee.Common+import Game.Goatee.Lib.Parser+import Game.Goatee.Lib.Property+import Game.Goatee.Lib.Renderer+import Game.Goatee.Lib.Renderer.Tree+import Game.Goatee.Lib.TestUtils+import Game.Goatee.Lib.Tree+import Game.Goatee.Lib.Types+import Test.HUnit ((~:), (@?=), Assertion, Test (TestList), assertFailure)++testCollection' :: Collection -> Assertion+testCollection' collection =+  case runRender $ renderCollection collection of+    Left message -> assertFailure $ "Failed to render: " ++ message+    Right serialized ->+      case parseString serialized of+        Left error -> assertFailure $ "Failed to parse: " ++ error +++                      "\n\nEntire SGF: " ++ serialized+        Right collection' -> do+          on (@?=) CollectionWithDeepEquality collection' collection+          case runRender $ renderCollection collection' of+            Left message -> assertFailure $ "Second render failed: " ++ message+            Right serializedAgain -> serializedAgain @?= serialized++-- | Returns an assertion that the given node round-trips okay.+testNode' :: Node -> Assertion+testNode' = testCollection' . Collection . (:[])++-- | Returns a test with the given string name that round-trips a single node.+testNode :: String -> Node -> Test+testNode label node = label ~: testNode' node++tests = "Game.Goatee.Lib.RoundTripTest" ~: TestList+  [ singleNodeGameTests+  , propertyValueTests+  ]++singleNodeGameTests = "games with single nodes" ~: TestList+  [ testNode "empty game" $ node []+  , testNode "some default properties" $ node [FF 4, GM 1, ST defaultVariationMode]+  ]++propertyValueTests = "property value types" ~: TestList+  [ "color values" ~: TestList+    [ testNode "black" $ node [PL Black]+    , testNode "white" $ node [PL White]+    ]++  , "double values" ~: TestList+    [ testNode "1" $ node [DM Double1]+    , testNode "2" $ node [DM Double2]+    ]++  , "label list values" ~: TestList+    [ testNode "one value" $ node [LB [((5,2), toSimpleText "Hi.")]]+    , testNode "multiple value" $ node [LB [((5, 2), toSimpleText "Hi."),+                                          ((0, 1), toSimpleText "Bye.")]]+    ]++  , testNode "none value" $ node [KO]++  , "point-valued values" ~: TestList $+    for [0..boardSizeMax-1]+    (\row -> testNode ("row " ++ show row) $ node [B $ Just (0, row)]) +++    for [0..boardSizeMax-1]+    (\col -> testNode ("row " ++ show col) $ node [B $ Just (col, 0)])++  , "real values" ~: TestList+    [ testNode "0" $ node [TM $ read "0"]+    , testNode "0.0" $ node [TM $ read "0.0"]+    , testNode "1500" $ node [TM $ read "1500"]+    , testNode "150" $ node [TM $ read "150"]+    , testNode "15" $ node [TM $ read "15"]+    , testNode "1.5" $ node [TM $ read "1.5"]+    , testNode "0.15" $ node [TM $ read "0.15"]+    , testNode "0.015" $ node [TM $ read "0.015"]+    , testNode "0.0015" $ node [TM $ read "0.0015"]+    , testNode "60.5" $ node [TM $ read "60.5"]+    , testNode "10.1" $ node [TM $ read "10.1"]+    ]+  ]++-- TODO Many more round-trip tests.
+ tests/Game/Goatee/Lib/TestInstances.hs view
@@ -0,0 +1,25 @@+-- This file is part of Goatee.+--+-- Copyright 2014 Bryan Gardiner+--+-- Goatee is free software: you can redistribute it and/or modify+-- it under the terms of the GNU Affero General Public License as published by+-- the Free Software Foundation, either version 3 of the License, or+-- (at your option) any later version.+--+-- Goatee is distributed in the hope that it will be useful,+-- but WITHOUT ANY WARRANTY; without even the implied warranty of+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the+-- GNU Affero General Public License for more details.+--+-- You should have received a copy of the GNU Affero General Public License+-- along with Goatee.  If not, see <http://www.gnu.org/licenses/>.++-- | Test utilities for working with the SGF modules.+module Game.Goatee.Lib.TestInstances () where++import Data.Function (on)+import Game.Goatee.Lib.Tree (Node, NodeWithDeepEquality (NodeWithDeepEquality))++instance Eq Node where+  (==) = (==) `on` NodeWithDeepEquality
+ tests/Game/Goatee/Lib/TestUtils.hs view
@@ -0,0 +1,62 @@+-- This file is part of Goatee.+--+-- Copyright 2014 Bryan Gardiner+--+-- Goatee is free software: you can redistribute it and/or modify+-- it under the terms of the GNU Affero General Public License as published by+-- the Free Software Foundation, either version 3 of the License, or+-- (at your option) any later version.+--+-- Goatee is distributed in the hope that it will be useful,+-- but WITHOUT ANY WARRANTY; without even the implied warranty of+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the+-- GNU Affero General Public License for more details.+--+-- You should have received a copy of the GNU Affero General Public License+-- along with Goatee.  If not, see <http://www.gnu.org/licenses/>.++-- | Test utilities for working with the SGF modules.+module Game.Goatee.Lib.TestUtils (+  node,+  node1,+  node',+  root,+  child,+  sortProperties,+  ) where++import Data.Function (on)+import Data.List (sortBy)+import Game.Goatee.Lib.Board+import Game.Goatee.Lib.Property+import Game.Goatee.Lib.Tree++node :: [Property] -> Node+node props = emptyNode { nodeProperties = props }++node1 :: [Property] -> Node -> Node+node1 props child = emptyNode { nodeProperties = props+                              , nodeChildren = [child]+                              }++node' :: [Property] -> [Node] -> Node+node' props children =+  emptyNode { nodeProperties = props+            , nodeChildren = children+            }++-- | Creates a root node that starts with only a 'SZ' property.  This is more+-- minimal than 'rootNode'.+root :: Int -> Int -> [Property] -> [Node] -> Node+root width height props =+  foldr addChild $ foldr addProperty emptyNode (SZ width height:props)++child :: Int -> Cursor -> Cursor+child = flip cursorChild++-- | Sorts properties by their 'Show' strings, for use in tests that need to+-- check an unordered list of properties.+--+-- TODO Probably better to have a compare-unordered operator.+sortProperties :: [Property] -> [Property]+sortProperties = sortBy (compare `on` show)
+ tests/Game/Goatee/Lib/TreeTest.hs view
@@ -0,0 +1,184 @@+-- This file is part of Goatee.+--+-- Copyright 2014 Bryan Gardiner+--+-- Goatee is free software: you can redistribute it and/or modify+-- it under the terms of the GNU Affero General Public License as published by+-- the Free Software Foundation, either version 3 of the License, or+-- (at your option) any later version.+--+-- Goatee is distributed in the hope that it will be useful,+-- but WITHOUT ANY WARRANTY; without even the implied warranty of+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the+-- GNU Affero General Public License for more details.+--+-- You should have received a copy of the GNU Affero General Public License+-- along with Goatee.  If not, see <http://www.gnu.org/licenses/>.++module Game.Goatee.Lib.TreeTest (tests) where++import Data.Version (showVersion)+import Game.Goatee.App (applicationName)+import Game.Goatee.Common+import Game.Goatee.Lib.Property+import Game.Goatee.Lib.TestInstances ()+import Game.Goatee.Lib.TestUtils+import Game.Goatee.Lib.Tree+import Game.Goatee.Lib.Types+import Game.Goatee.Test.Common+import Paths_goatee (version)+import Test.HUnit ((~:), (@=?), (@?=), Test (TestList))++tests = "Game.Goatee.Lib.Tree" ~: TestList+  [ emptyNodeTests+  , rootNodeTests+  , findPropertyTests+  , addPropertyTests+  , addChildTests+  , addChildAtTests+  , deleteChildAtTests+  ]++emptyNodeTests = "emptyNode" ~: TestList+  [ "has no properties" ~: [] @=? nodeProperties emptyNode+  , "has no children" ~: [] @=? nodeChildren emptyNode+  ]++rootNodeTests = "rootNode" ~: TestList+  [ "sets SZ correctly" ~: do+    assertElem (SZ 9 9) $ nodeProperties $ rootNode $ Just (9, 9)+    assertElem (SZ 19 19) $ nodeProperties $ rootNode $ Just (19, 19)+    assertElem (SZ 9 5) $ nodeProperties $ rootNode $ Just (9, 5)++  , "sets AP correctly" ~: do+    let ap = AP (toSimpleText applicationName)+                (toSimpleText $ showVersion version)+    assertElem ap $ nodeProperties $ rootNode Nothing+    assertElem ap $ nodeProperties $ rootNode $ Just (9, 9)+  ]++findPropertyTests = "findProperty" ~: TestList+  [ "returns Nothing for an empty node" ~:+    Nothing @=? findProperty propertyB (mk [])++  , "returns Nothing if no properties match" ~:+    Nothing @=? findProperty propertyB (mk [FF 4, GM 1, ST defaultVariationMode, SZ 9 9])++  , "finds present properties" ~: do+     Just (B (Just (2,3))) @=?+       findProperty propertyB (mk [B (Just (2,3))])+     Just (W Nothing) @=?+       findProperty propertyW (mk [B Nothing, W Nothing])+     Just IT @=?+       findProperty propertyIT (mk [IT, GW Double2])+     Just (GW Double2) @=?+       findProperty propertyGW (mk [IT, GW Double2])++  , "doesn't find absent properties" ~: do+    Nothing @=? findProperty propertyW (mk [B Nothing])+    Nothing @=? findProperty propertyW (mk [FF 4, GM 1, ST defaultVariationMode, SZ 9 9])+  ]+  where mk properties = emptyNode { nodeProperties = properties }++addPropertyTests = "addProperty" ~: TestList+  [ "adds properties in order" ~: do+    let prop1 = B (Just (6,6))+        prop2 = RE GameResultVoid+        prop3 = C (toText "Game over.")+        node1 = addProperty prop1 emptyNode+        node2 = addProperty prop2 node1+        node3 = addProperty prop3 node2+    node1 @?= emptyNode { nodeProperties = [prop1] }+    node2 @?= emptyNode { nodeProperties = [prop1, prop2] }+    node3 @?= emptyNode { nodeProperties = [prop1, prop2, prop3] }+  ]++addChildTests = "addChild" ~: TestList+  [ "adds children in order" ~: do+    let node1 = emptyNode { nodeProperties = [B (Just (0,0))] }+        node2 = emptyNode { nodeProperties = [W (Just (1,1))] }+        node3 = emptyNode { nodeProperties = [B (Just (2,2))] }+        node4 = emptyNode { nodeProperties = [W Nothing] }+        node2' = addChild node3 node2+        node1' = addChild node2' node1+        node1'' = addChild node4 node1'+    node1'' @?= mk [B (Just (0,0))]+                   [mk [W (Just (1,1))]+                       [mk [B (Just (2,2))]+                           []],+                    mk [W Nothing]+                       []]+  ]+  where mk properties children = emptyNode { nodeProperties = properties+                                           , nodeChildren = children+                                           }++addChildAtTests = "addChildAt" ~: TestList+  [ "adds to a childless node" ~:+    let parent = node [B Nothing]+        child = node [W Nothing]+    in node1 [B Nothing] (node [W Nothing]) @=? addChildAt 0 child parent++  , "adds as a first child" ~: do+    let expected = base { nodeChildren = newNode : baseChildren }+    expected @=? addChildAt 0 newNode base+    expected @=? addChildAt (-1) newNode base+    expected @=? addChildAt (-2) newNode base++  , "adds as a middle child" ~: do+    base { nodeChildren = listInsertAt 1 newNode baseChildren } @=?+      addChildAt 1 newNode base+    base { nodeChildren = listInsertAt 2 newNode baseChildren } @=?+      addChildAt 2 newNode base++  , "adds as a last child" ~: do+    let expected = base { nodeChildren = baseChildren ++ [newNode] }+    expected @=? addChildAt baseChildCount newNode base+    expected @=? addChildAt (baseChildCount + 1) newNode base+    expected @=? addChildAt (baseChildCount + 2) newNode base+  ]+  where base = node' [B $ Just (0,0)] baseChildren+        baseChildren = [node [W Nothing],+                        node [W $ Just (1,1)],+                        node [W $ Just (9,9)]]+        baseChildCount = length $ nodeChildren base+        newNode = node [B $ Just (1,1)]++deleteChildAtTests = "deleteChildAt" ~: TestList+  [ "deletes nothing from an empty list" ~: do+    emptyNode @=? deleteChildAt (-1) emptyNode+    emptyNode @=? deleteChildAt 0 emptyNode+    emptyNode @=? deleteChildAt 1 emptyNode++  , "deletes an only child" ~:+    node [B Nothing] @=? deleteChildAt 0 (node1 [B Nothing] $ node [W Nothing])++  , "deletes a first child" ~:+    base { nodeChildren = listDeleteAt 0 baseChildren } @=?+    deleteChildAt 0 base++  , "deletes middle children" ~: do+    base { nodeChildren = listDeleteAt 1 baseChildren } @=?+      deleteChildAt 1 base+    base { nodeChildren = listDeleteAt 2 baseChildren } @=?+      deleteChildAt 2 base++  , "delete a last child" ~:+    base { nodeChildren = init baseChildren } @=?+    deleteChildAt (baseChildCount - 1) base++  , "has no effect for invalid indices" ~: do+    base @=? deleteChildAt (-1) base+    base @=? deleteChildAt (-2) base+    base @=? deleteChildAt baseChildCount base+    base @=? deleteChildAt (baseChildCount + 1) base+    base @=? deleteChildAt (baseChildCount + 2) base+  ]+  where base = node' [B $ Just (0,0)] baseChildren+        baseChildren = [node [W Nothing],+                        node [W $ Just (1,1)],+                        node [W $ Just (2,2)],+                        node [W $ Just (9,9)]]+        baseChildCount = length $ nodeChildren base++-- TODO Test validateNode.
+ tests/Game/Goatee/Lib/TypesTest.hs view
@@ -0,0 +1,240 @@+-- This file is part of Goatee.+--+-- Copyright 2014 Bryan Gardiner+--+-- Goatee is free software: you can redistribute it and/or modify+-- it under the terms of the GNU Affero General Public License as published by+-- the Free Software Foundation, either version 3 of the License, or+-- (at your option) any later version.+--+-- Goatee is distributed in the hope that it will be useful,+-- but WITHOUT ANY WARRANTY; without even the implied warranty of+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the+-- GNU Affero General Public License for more details.+--+-- You should have received a copy of the GNU Affero General Public License+-- along with Goatee.  If not, see <http://www.gnu.org/licenses/>.++module Game.Goatee.Lib.TypesTest (tests) where++import Data.List (sort)+import Game.Goatee.Lib.Types+import Game.Goatee.Test.Common+import Test.HUnit ((~:), (@=?), (@?=), Assertion, Test (TestList))++tests = "Game.Goatee.Lib.Types" ~: TestList+  [ expandCoordListTests+  , buildCoordListTests+  , starLinesTests+  , isStarPointTests+  , handicapStonesTests+  , simpleTextTests+  , cnotTests+  ]++expandCoordListTests = "expandCoordList" ~: TestList+  [ "works with an empty CoordList" ~: do+    [] @=? expandCoordList (coords [])+    [] @=? expandCoordList (coords' [] [])++  , "works for single points" ~: do+    [(1,2)] @=? expandCoordList (coord1 (1,2))+    [(3,4), (1,2)] @=? expandCoordList (coords [(3,4), (1,2)])+    let ten = [(i,i) | i <- [1..10]]+    ten @=? expandCoordList (coords ten)++  -- TODO Test that a 1x1 rectangle is rejected.++  , "works for a nx1 rect" ~:+    [(5,2), (5,3), (5,4)] @=? expandCoordList (coords' [] [((5,2), (5,4))])++  , "works for a 1xn rect" ~:+    [(1,0), (1,1), (1,2), (1,3)] @=? expandCoordList (coords' [] [((1,0), (1,3))])++  , "works for an mxn rect" ~:+    [(m,n) | m <- [2..5], n <- [3..7]] @=?+    expandCoordList (coords' [] [((2,3), (5,7))])++  -- TODO Test that x0 > x1 || y0 > y1 is rejected.++  , "works with multiple rects" ~:+    [(0,0), (0,1), (0,2), (3,4), (4,4), (5,4)] @=?+    expandCoordList (coords' [] [((0,0), (0,2)), ((3,4), (5,4))])++  , "concatenates single points and rects" ~:+    [(1,1), (0,0), (2,2), (2,3), (2,4)] @=?+    expandCoordList (coords' [(1,1), (0,0)] [((2,2), (2,4))])+  ]++buildCoordListTests = "buildCoordList" ~: TestList+  [ "handles the empty case" ~: assertSinglesAndRects [] [] []++  , "handles one single" ~:+    assertSinglesAndRects [(0,1)] [] [(0,1)]++  , "handles multiple singles" ~:+    assertSinglesAndRects [(0,0), (1,1)] [] [(0,0), (1,1)]++  , "handles a small rect (1x2)" ~:+    assertSinglesAndRects [] [((0,1), (0,2))] [(0,1), (0,2)]++  , "handles a small square (2x2)" ~:+    assertSinglesAndRects [] [((1,1), (2,2))] [(x,y) | x <- [1..2], y <- [1..2]]++  , "handles a larger rect" ~:+    assertSinglesAndRects [] [((0,0), (2,4))] [(x,y) | x <- [0..2], y <- [0..4]]++  , "handles two rects" ~:+    assertSinglesAndRects [] [((0,1), (1,2)), ((3,0), (4,1))]+    [(0,1), (1,1), (0,2), (1,2), (3,0), (4,0), (3,1), (4,1)]++  , "handles rects and singles together" ~:+    assertSinglesAndRects [(0,0)] [((1,1), (2,2))]+    [(0,0), (1,1), (1,2), (2,1), (2,2)]++  , "handles five points together" ~: do+    assertCoords [(1,0), (0,1), (1,1), (0,2), (1,2)]+    assertCoords [(0,0), (1,0), (0,1), (1,1), (0,2)]+    assertCoords [(0,0), (1,0), (2,0), (0,1), (1,1)]+    assertCoords [(1,0), (2,0), (0,1), (1,1), (2,1)]++  , "handles a more complex case" ~:+    assertCoords [(3,0), (4,0),+                  (3,1), (4,1),+                  (0,2), (1,2), (2,2), (3,2), (4,2),+                  (0,3), (1,3),+                  (0,4), (1,4), (3,4), (4,4)]++  , "handles a diagonal pattern" ~:+    assertCoords [(0,0), (2,0), (4,0),+                  (1,1), (3,1), (5,1),+                  (2,2)]+  ]+  where -- | Asserts that the given grid of booleans parses to a 'CoordList'+        -- that has the expected single points (not necessarily in the same+        -- order), as well as the expected rectangles (not necessarily in the+        -- same order).+        assertSinglesAndRects :: [Coord] -> [(Coord, Coord)] -> [Coord] -> Assertion+        assertSinglesAndRects expectedSingles expectedRects input =+          coords' expectedSingles expectedRects @=? buildCoordList input++        -- | Asserts that the set of points returned from applying+        -- 'buildCoordList' to the given grid of booleans is equal to+        -- the given set of points.+        assertCoords :: [Coord] -> Assertion+        assertCoords input = sort input @=? sort (expandCoordList $ buildCoordList input)++starLinesTests = "starLines" ~: TestList+  [ "knows 9x9 boards" ~: Just [2, 4, 6] @=? starLines 9 9+  , "knows 13x13 boards" ~: Just [3, 6, 9] @=? starLines 13 13+  , "knows 19x19 boards" ~: Just [3, 9, 15] @=? starLines 19 19+  , "doesn't know 13x19" ~: Nothing @=? starLines 13 19+  , "doesn't know 10x10" ~: Nothing @=? starLines 10 10+  ]++isStarPointTests = "isStarPoint" ~: TestList+  [ "only knows correct star points on a 9x9 board" ~:+    let expected = [(x, y) | x <- [2, 4, 6], y <- [2, 4, 6]]+        actual = actualStarPoints 9 9+    in expected @=?* actual++  , "only knows correct star points on a 13x13 board" ~:+    let expected = [(x, y) | x <- [3, 6, 9], y <- [3, 6, 9]]+        actual = actualStarPoints 13 13+    in expected @=?* actual++  , "only knows correct star points on a 19x19 board" ~:+    let expected = [(x, y) | x <- [3, 9, 15], y <- [3, 9, 15]]+        actual = actualStarPoints 19 19+    in expected @=?* actual++  , "knows no star poinst on a 5x1 board" ~:+    [] @=? actualStarPoints 5 1++  , "knows no star points on a 15x15 board" ~:+    [] @=? actualStarPoints 15 15++  , "knows no star points on a 20x20 board" ~:+    [] @=? actualStarPoints 20 20+  ]+  where actualStarPoints width height =+          map fst $ filter snd+          [((x, y), isStarPoint width height x y) | x <- [0..width-1], y <- [0..height-1]]++handicapStonesTests = "handicapStones" ~: TestList+  [ "returns nothing for invalid handicaps" ~: do+     Nothing @=? handicapStones 9 9 (-1)+     Nothing @=? handicapStones 9 9 10+     Nothing @=? handicapStones 4 4 25++  , "9x9 0 handicap" ~: assertHandicap 9 9 0 $ Just []+  , "9x9 1 handicap" ~: assertHandicap 9 9 1 $ Just []+  , "9x9 2 handicap" ~: assertHandicap 9 9 2 $ Just [(2,6), (6,2)]+  , "9x9 3 handicap" ~: assertHandicap 9 9 3 $ Just [(2,6), (6,2), (6,6)]+  , "9x9 4 handicap" ~: assertHandicap 9 9 4 $ Just [(2,2), (2,6), (6,2), (6,6)]+  , "9x9 5 handicap" ~: assertHandicap 9 9 5 $ Just [(2,2), (2,6), (4,4), (6,2), (6,6)]+  , "9x9 6 handicap" ~: assertHandicap 9 9 6 $+    Just [(2,2), (2,4), (2,6), (6,2), (6,4), (6,6)]+  , "9x9 7 handicap" ~: assertHandicap 9 9 7 $+    Just [(2,2), (2,4), (2,6), (4,4), (6,2), (6,4), (6,6)]+  , "9x9 8 handicap" ~: assertHandicap 9 9 8 $+    Just [(2,2), (2,4), (2,6), (4,2), (4,6), (6,2), (6,4), (6,6)]+  , "9x9 9 handicap" ~: assertHandicap 9 9 9 $+    Just [(2,2), (2,4), (2,6), (4,2), (4,4), (4,6), (6,2), (6,4), (6,6)]++  , "13x13 0 handicap" ~: assertHandicap 13 13 0 $ Just []+  , "13x13 1 handicap" ~: assertHandicap 13 13 1 $ Just []+  , "13x13 2 handicap" ~: assertHandicap 13 13 2 $ Just [(3,9), (9,3)]+  , "13x13 3 handicap" ~: assertHandicap 13 13 3 $ Just [(3,9), (9,3), (9,9)]+  , "13x13 4 handicap" ~: assertHandicap 13 13 4 $ Just [(3,3), (3,9), (9,3), (9,9)]+  , "13x13 5 handicap" ~: assertHandicap 13 13 5 $ Just [(3,3), (3,9), (6,6), (9,3), (9,9)]+  , "13x13 6 handicap" ~: assertHandicap 13 13 6 $+    Just [(3,3), (3,6), (3,9), (9,3), (9,6), (9,9)]+  , "13x13 7 handicap" ~: assertHandicap 13 13 7 $+    Just [(3,3), (3,6), (3,9), (6,6), (9,3), (9,6), (9,9)]+  , "13x13 8 handicap" ~: assertHandicap 13 13 8 $+    Just [(3,3), (3,6), (3,9), (6,3), (6,9), (9,3), (9,6), (9,9)]+  , "13x13 9 handicap" ~: assertHandicap 13 13 9 $+    Just [(3,3), (3,6), (3,9), (6,3), (6,6), (6,9), (9,3), (9,6), (9,9)]++  , "19x19 0 handicap" ~: assertHandicap 19 19 0 $ Just []+  , "19x19 1 handicap" ~: assertHandicap 19 19 1 $ Just []+  , "19x19 2 handicap" ~: assertHandicap 19 19 2 $ Just [(3,15), (15,3)]+  , "19x19 3 handicap" ~: assertHandicap 19 19 3 $ Just [(3,15), (15,3), (15,15)]+  , "19x19 4 handicap" ~: assertHandicap 19 19 4 $ Just [(3,3), (3,15), (15,3), (15,15)]+  , "19x19 5 handicap" ~: assertHandicap 19 19 5 $ Just [(3,3), (3,15), (9,9), (15,3), (15,15)]+  , "19x19 6 handicap" ~: assertHandicap 19 19 6 $+    Just [(3,3), (3,9), (3,15), (15,3), (15,9), (15,15)]+  , "19x19 7 handicap" ~: assertHandicap 19 19 7 $+    Just [(3,3), (3,9), (3,15), (9,9), (15,3), (15,9), (15,15)]+  , "19x19 8 handicap" ~: assertHandicap 19 19 8 $+    Just [(3,3), (3,9), (3,15), (9,3), (9,15), (15,3), (15,9), (15,15)]+  , "19x19 9 handicap" ~: assertHandicap 19 19 9 $+    Just [(3,3), (3,9), (3,15), (9,3), (9,9), (9,15), (15,3), (15,9), (15,15)]+  ]+  where assertHandicap width height handicap maybeExpectedStones =+          case (handicapStones width height handicap, maybeExpectedStones) of+            (Just actualStones, Just expectedStones) -> actualStones @?=* expectedStones+            (actual, _) -> actual @?= maybeExpectedStones++simpleTextTests = "SimpleText" ~: TestList+  [ "accepts the empty string" ~:+    "" @=? fromSimpleText (toSimpleText "")++  , "passes through strings without newlines" ~: do+    "Hello." @=? fromSimpleText (toSimpleText "Hello.")+    "Bad for B." @=? fromSimpleText (toSimpleText "Bad for B.")+    "[4k?]" @=? fromSimpleText (toSimpleText "[4k?]")+    printableAsciiChars @=? fromSimpleText (toSimpleText printableAsciiChars)++  , "converts newlines to spaces" ~: do+    " " @=? fromSimpleText (toSimpleText "\n")+    "Hello, world." @=? fromSimpleText (toSimpleText "Hello,\nworld.")+    "Hello,   world." @=? fromSimpleText (toSimpleText "Hello, \n world.")+    " Hello, world. " @=? fromSimpleText (toSimpleText "\nHello, world.\n")+  ]++cnotTests = "cnot" ~: TestList+  [ "changes Black to White" ~: White @=? cnot Black+  , "changes White to Black" ~: Black @=? cnot White+  ]
− tests/Game/Goatee/Sgf/BoardTest.hs
@@ -1,195 +0,0 @@--- This file is part of Goatee.------ Copyright 2014 Bryan Gardiner------ Goatee is free software: you can redistribute it and/or modify--- it under the terms of the GNU Affero General Public License as published by--- the Free Software Foundation, either version 3 of the License, or--- (at your option) any later version.------ Goatee is distributed in the hope that it will be useful,--- but WITHOUT ANY WARRANTY; without even the implied warranty of--- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the--- GNU Affero General Public License for more details.------ You should have received a copy of the GNU Affero General Public License--- along with Goatee.  If not, see <http://www.gnu.org/licenses/>.--module Game.Goatee.Sgf.BoardTest (tests) where--import Game.Goatee.Sgf.Board-import Game.Goatee.Sgf.Property-import Game.Goatee.Sgf.TestInstances ()-import Game.Goatee.Sgf.TestUtils-import Game.Goatee.Sgf.Tree-import Game.Goatee.Sgf.Types-import Test.Framework (testGroup)-import Test.Framework.Providers.HUnit (testCase)-import Test.HUnit ((@=?), (@?=))--tests = testGroup "Game.Goatee.Sgf.Board" [-  boardCoordStateTests,-  moveNumberTests,-  markupPropertiesTests,-  visibilityPropertyTests,-  cursorModifyNodeTests,-  colorToMoveTests-  ]--boardCoordStateTests = testGroup "boardCoordState" [-  testCase "works for a 1x1 board" $ do-    let state = boardCoordState (0,0) $ rootBoardState $-                node [SZ 1 1, CR $ coord1 (0,0)]-    coordStone state @?= Nothing-    coordMark state @?= Just MarkCircle,--  testCase "works for a 2x2 board" $ do-    let board = rootBoardState $-                node [SZ 2 2, B $ Just (0,0), TR $ coord1 (0,0), MA $ coord1 (1,1)]-    coordStone (boardCoordState (0,0) board) @?= Just Black-    coordMark (boardCoordState (0,0) board) @?= Just MarkTriangle-    coordStone (boardCoordState (1,0) board) @?= Nothing-    coordMark (boardCoordState (1,0) board) @?= Nothing-    coordStone (boardCoordState (1,1) board) @?= Nothing-    coordMark (boardCoordState (1,1) board) @?= Just MarkX-  ]---moveNumberTests = testGroup "move number" [-  testCase "starts at zero" $-    0 @=? boardMoveNumber (cursorBoard $ rootCursor $ node1 [] $ node [B Nothing]),--  testCase "advances for nodes with moves" $ do-    1 @=? boardMoveNumber (cursorBoard $ child 0 $ rootCursor $ node1 [] $ node [B Nothing])-    1 @=? boardMoveNumber (cursorBoard $ rootCursor $ node1 [B Nothing] $ node [])-    1 @=? boardMoveNumber (cursorBoard $ child 0 $ rootCursor $ node1 [B Nothing] $ node [])-    2 @=? boardMoveNumber (cursorBoard $ child 0 $ rootCursor $-                           node1 [B Nothing] $ node [W Nothing])-    2 @=? boardMoveNumber (cursorBoard $ child 0 $ child 0 $ rootCursor $-                           node1 [B Nothing] $ node1 [W Nothing] $ node [])-    3 @=? boardMoveNumber (cursorBoard $ child 0 $ child 0 $ rootCursor $-                           node1 [B Nothing] $ node1 [W Nothing] $ node [B Nothing]),--  testCase "doesn't advance for non-move nodes" $ do-    0 @=? boardMoveNumber (cursorBoard $ child 0 $ rootCursor $-                           node1 [] $ node [])-    0 @=? boardMoveNumber (cursorBoard $ child 0 $ rootCursor $-                           node1 [PL White] $ node [])-    0 @=? boardMoveNumber (cursorBoard $ child 0 $ rootCursor $-                           node1 [AB $ coords [(0,0)]] $ node [])-    0 @=? boardMoveNumber (cursorBoard $ child 0 $ rootCursor $-                           node1 [IT, TE Double2, GN $ toSimpleText "Title"] $ node [])-  ]--markupPropertiesTests = testGroup "markup properties" [-  testGroup "adds marks to a BoardState" [-    testCase "CR" $ Just MarkCircle @=? getMark 0 0 (rootCursor $ node [CR $ coords [(0,0)]]),-    testCase "MA" $ Just MarkX @=? getMark 0 0 (rootCursor $ node [MA $ coords [(0,0)]]),-    testCase "SL" $ Just MarkSelected @=? getMark 0 0 (rootCursor $ node [SL $ coords [(0,0)]]),-    testCase "SQ" $ Just MarkSquare @=? getMark 0 0 (rootCursor $ node [SQ $ coords [(0,0)]]),-    testCase "TR" $ Just MarkTriangle @=? getMark 0 0 (rootCursor $ node [TR $ coords [(0,0)]]),-    testCase "multiple at once" $ do-      let cursor = rootCursor $ node [SZ 2 2,-                                      CR $ coords [(0,0), (1,1)],-                                      TR $ coords [(1,0)]]-      [[Just MarkCircle, Just MarkTriangle],-       [Nothing, Just MarkCircle]]-        @=? map (map coordMark) (boardCoordStates $ cursorBoard cursor)-    ],--  testGroup "adds more complex annotations to a BoardState" [-    testCase "AR" $ [((0,0), (1,1))] @=? boardArrows (rootCoord $ node [AR [((0,0), (1,1))]]),-    testCase "LB" $ [((0,0), st "Hi")] @=? boardLabels (rootCoord $ node [LB [((0,0), st "Hi")]]),-    testCase "LN" $ [((0,0), (1,1))] @=? boardLines (rootCoord $ node [LN [((0,0), (1,1))]])-    ],--  testCase "clears annotations when moving to a child node" $ do-    let root = node1 [SZ 3 2,-                      CR $ coords [(0,0)],-                      MA $ coords [(1,0)],-                      SL $ coords [(2,0)],-                      SQ $ coords [(0,1)],-                      TR $ coords [(1,1)],-                      AR [((0,0), (2,1))],-                      LB [((2,1), st "Empty")],-                      LN [((1,1), (2,0))]] $-                 node []-        board = cursorBoard $ child 0 $ rootCursor root-    mapM_ (mapM_ ((Nothing @=?) . coordMark)) $ boardCoordStates board-    [] @=? boardArrows board-    [] @=? boardLines board-    [] @=? boardLabels board-  ]-  where rootCoord = cursorBoard . rootCursor-        st = toSimpleText--        getMark :: Int -> Int -> Cursor -> Maybe Mark-        getMark x y cursor = coordMark $ boardCoordStates (cursorBoard cursor) !! y !! x--visibilityPropertyTests = testGroup "visibility properties" [-  testCase "boards start with all points undimmed" $-    replicate 9 (replicate 9 False) @=?-    map (map coordDimmed) (boardCoordStates $ cursorBoard $ rootCursor $ node [SZ 9 9]),--  testCase "DD selectively dims points" $-    let root = node [SZ 5 2, DD $ coords' [(3,0)] [((0,0), (1,1))]]-    in [[True, True, False, True, False],-        [True, True, False, False, False]] @=?-       map (map coordDimmed) (boardCoordStates $ cursorBoard $ rootCursor root),--  testCase "dimming is inherited" $-    let root = node1 [SZ 5 2, DD $ coords' [(3,0)] [((0,0), (1,1))]] $ node []-    in [[True, True, False, True, False],-        [True, True, False, False, False]] @=?-       map (map coordDimmed) (boardCoordStates $ cursorBoard $ child 0 $ rootCursor root),--  testCase "DD[] clears dimming" $-    let root = node1 [SZ 2 1, DD $ coords [(0,0)]] $ node [DD $ coords []]-    in [[False, False]] @=?-       map (map coordDimmed) (boardCoordStates $ cursorBoard $ child 0 $ rootCursor root),--  testCase "boards start with all points visible" $-    replicate 9 (replicate 9 True) @=?-    map (map coordVisible) (boardCoordStates $ cursorBoard $ rootCursor $ node [SZ 9 9]),--  testCase "VW selectively makes points visible" $-    let root = node [SZ 5 2, VW $ coords' [(1,0), (0,1)] [((2,0), (4,1))]]-    in [[False, True, True, True, True],-        [True, False, True, True, True]] @=?-       map (map coordVisible) (boardCoordStates $ cursorBoard $ rootCursor root),--  testCase "visibility is inherited" $-    let root = node1 [SZ 5 2, VW $ coords' [(1,0), (0,1)] [((2,0), (4,1))]] $ node []-    in [[False, True, True, True, True],-        [True, False, True, True, True]] @=?-       map (map coordVisible) (boardCoordStates $ cursorBoard $ child 0 $ rootCursor root),--  testCase "VW[] clears dimming" $-    let root = node1 [SZ 2 1, VW $ coords [(0,0)]] $ node [VW $ coords []]-    in [[True, True]] @=?-       map (map coordVisible) (boardCoordStates $ cursorBoard $ child 0 $ rootCursor root)-  ]--cursorModifyNodeTests = testGroup "cursorModifyNode" [-  testCase "updates the BoardState" $-    let cursor = child 0 $ rootCursor $-                 node1 [SZ 3 1, B $ Just (0,0)] $-                 node [W $ Just (1,0)]-        modifyProperty prop = case prop of-          W (Just (1,0)) -> W $ Just (2,0)-          _ -> prop-        modifyNode node = node { nodeProperties = map modifyProperty $ nodeProperties node }-        result = cursorModifyNode modifyNode cursor-    in map (map coordStone) (boardCoordStates $ cursorBoard result) @?=-       [[Just Black, Nothing, Just White]]-  ]--colorToMoveTests = testGroup "colorToMove" [-  testCase "creates Black moves" $ do-    B (Just (3,2)) @=? colorToMove Black (3,2)-    B (Just (0,0)) @=? colorToMove Black (0,0),--  testCase "creates White moves" $ do-    W (Just (18,18)) @=? colorToMove White (18,18)-    W (Just (15,16)) @=? colorToMove White (15,16)-  ]
− tests/Game/Goatee/Sgf/MonadTest.hs
@@ -1,736 +0,0 @@--- This file is part of Goatee.------ Copyright 2014 Bryan Gardiner------ Goatee is free software: you can redistribute it and/or modify--- it under the terms of the GNU Affero General Public License as published by--- the Free Software Foundation, either version 3 of the License, or--- (at your option) any later version.------ Goatee is distributed in the hope that it will be useful,--- but WITHOUT ANY WARRANTY; without even the implied warranty of--- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the--- GNU Affero General Public License for more details.------ You should have received a copy of the GNU Affero General Public License--- along with Goatee.  If not, see <http://www.gnu.org/licenses/>.--module Game.Goatee.Sgf.MonadTest (tests) where--import Control.Applicative ((<$>))-import Control.Arrow ((&&&))-import Control.Monad (forM_, liftM, replicateM_, void)-import Control.Monad.Writer (Writer, execWriter, runWriter, tell)-import Data.List (unfoldr)-import Data.Maybe (fromJust, maybeToList)-import Game.Goatee.Sgf.Board-import Game.Goatee.Sgf.Monad-import Game.Goatee.Sgf.Property-import Game.Goatee.Sgf.TestInstances ()-import Game.Goatee.Sgf.TestUtils-import Game.Goatee.Sgf.Types-import Game.Goatee.Test.Common-import Test.Framework (testGroup)-import Test.Framework.Providers.HUnit (testCase)-import Test.HUnit ((@=?), (@?=))--{-# ANN module "HLint: ignore Reduce duplication" #-}--type LoggedGoM = GoT (Writer [String])--runLoggedGo :: LoggedGoM a -> Cursor -> (a, Cursor, [String])-runLoggedGo go cursor =-  let ((value, cursor'), log) = runWriter $ runGoT go cursor-  in (value, cursor', log)--tests = testGroup "Game.Goatee.Sgf.Monad" [-  monadTests,-  navigationTests,-  positionStackTests,-  propertiesTests,-  getPropertyTests,-  getPropertyValueTests,-  putPropertyTests,-  deletePropertyTests,-  modifyPropertyTests,-  modifyPropertyValueTests,-  modifyPropertyStringTests,-  modifyPropertyCoordsTests,-  modifyGameInfoTests,-  modifyVariationModeTests,-  getMarkTests,-  modifyMarkTests,-  addChildTests,-  gameInfoChangedTests-  ]--monadTests = testGroup "monad properties" [-  testCase "returns a value it's given" $-    let cursor = rootCursor $ node []-        (value, cursor') = runGo (return 3) cursor-    in (value, cursorNode cursor') @?= (3, cursorNode cursor),--  testCase "getCursor works" $ do-    let cursor = rootCursor nodeA-        nodeA = node1 [SZ 3 3, B $ Just (0,0)] nodeB-        nodeB = node1 [W $ Just (1,1)] nodeC-        nodeC = node [B $ Just (2,2)]-    nodeA @=? evalGo (liftM cursorNode getCursor) cursor-    nodeB @=? evalGo (goDown 0 >> liftM cursorNode getCursor) cursor-    nodeC @=? evalGo (goDown 0 >> goDown 0 >> liftM cursorNode getCursor) cursor,--  testCase "getCoordState works" $ do-    let cursor = child 0 $ child 0 $ rootCursor $-                 node1 [SZ 2 2, B $ Just (0,0)] $-                 node1 [W $ Just (1,0), TR $ coord1 (0,0)] $-                 node [B $ Just (0,1), CR $ coord1 (1,0)]-        action = mapM getCoordState [(0,0), (1,0), (0,1), (1,1)]-        states = evalGo action cursor-    map coordStone states @?= [Just Black, Just White, Just Black, Nothing]-    map coordMark states @?= [Nothing, Just MarkCircle, Nothing, Nothing]-  ]--navigationTests = testGroup "navigation" [-  testCase "navigates down a tree" $-    let cursor = rootCursor $-                 node1 [B $ Just (0,0)] $-                 node' [W $ Just (1,1)] [node [B $ Just (2,2)],-                                         node [B Nothing]]-        action = goDown 0 >> goDown 1-        (_, cursor') = runGo action cursor-    in cursorProperties cursor' @?= [B Nothing],--  testCase "navigates up a tree" $-    let cursor = child 1 $ child 0 $ rootCursor $-                 node1 [B $ Just (0,0)] $-                 node' [W $ Just (1,1)] [node [B $ Just (2,2)],-                                         node [B Nothing]]-        action = goUp >> goUp-        (_, cursor') = runGo action cursor-    in cursorProperties cursor' @?= [B $ Just (0,0)],--  testCase "invokes handlers when navigating" $-    let cursor = rootCursor $ node1 [B Nothing] $ node [W Nothing]-        action = do on navigationEvent $ \step -> case step of-                      GoUp index -> tell ["Up " ++ show index]-                      _ -> return ()-                    on navigationEvent $ \step -> case step of-                      GoDown index -> tell ["Down " ++ show index]-                      _ -> return ()-                    goDown 0-                    goUp-        (_, _, log) = runLoggedGo action cursor-    in log @?= ["Down 0", "Up 0"],--  testCase "navigates to the root of a tree, invoking handlers" $ do-    let cursor = child 0 $ child 0 $ rootCursor $-                 node1 [B $ Just (0,0)] $-                 node1 [W $ Just (1,1)] $-                 node [B $ Just (2,2)]-        action = do on navigationEvent $ \step -> tell [show step]-                    goToRoot-        (_, cursor', log) = runLoggedGo action cursor-    cursorProperties cursor' @?= [B $ Just (0,0)]-    log @?= ["GoUp 0", "GoUp 0"],--  testGroup "goToGameInfoNode" [-    testCase "can navigate up to find game info" $-      let props = [PB (toSimpleText ""), B Nothing]-          cursor = child 0 $ rootCursor $ node1 props $ node [W Nothing]-          action = void $ goToGameInfoNode False-      in cursorProperties (execGo action cursor) @?= props,--    testCase "can find game info at the current node" $-      let props = [PB (toSimpleText ""), W Nothing]-          cursor = child 0 $ rootCursor $ node1 [B Nothing] $ node props-          action = void $ goToGameInfoNode False-      in cursorProperties (execGo action cursor) @?= props,--    testCase "doesn't find game info from a subnode" $-      let cursor = child 0 $ rootCursor $-                   node1 [B $ Just (0,0)] $-                   node1 [W $ Just (1,1)] $-                   node [PB (toSimpleText ""), B Nothing]-          action = void $ goToGameInfoNode False-      in cursorProperties (execGo action cursor) @?= [W $ Just (1,1)],--    testCase "can return to the initial node when no game info is found" $-      let cursor = child 0 $ child 0 $ rootCursor $-                   node1 [B $ Just (0,0)] $-                   node1 [W $ Just (1,1)] $-                   node [B $ Just (2,2)]-          action = void $ goToGameInfoNode False-      in cursorProperties (execGo action cursor) @?= [B $ Just (2,2)],--    testCase "can finish at the root node when no game info is found" $-      let cursor = child 0 $ child 0 $ rootCursor $-                   node1 [B $ Just (0,0)] $-                   node1 [W $ Just (1,1)] $-                   node [B $ Just (2,2)]-          action = void $ goToGameInfoNode True-      in cursorProperties (execGo action cursor) @?= [B $ Just (0,0)]-    ]-  ]--positionStackTests = testGroup "position stack" [-  testCase "should push, pop, and drop with no navigation" $ do-    let cursor = rootCursor $ node []-        actions = [pushPosition >> popPosition,-                   pushPosition >> pushPosition >> popPosition >> popPosition,-                   pushPosition >> dropPosition,-                   pushPosition >> pushPosition >> dropPosition >> popPosition,-                   pushPosition >> pushPosition >> popPosition >> dropPosition]-    forM_ actions $ \action -> cursorProperties (execGo action cursor) @?= [],--  testCase "should backtrack up and down the tree" $ do-    let cursor = child 0 $ child 1 $ rootCursor $-                 node' [B $ Just (0,0)]-                       [node1 [W $ Just (1,1)] $ node [B $ Just (2,2)],-                        node1 [W Nothing] $ node [B Nothing]]-        action = pushPosition >> goUp >> goUp >> goDown 0 >> goDown 0-    cursorProperties (execGo (action >> popPosition) cursor) @?= [B Nothing]-    cursorProperties (execGo (action >> dropPosition) cursor) @?= [B $ Just (2,2)],--  testCase "should pop multiple stacks" $ do-    let cursor = child 0 $ child 0 commonCursor-        action = do navigate-                    log >> popPosition-                    log >> popPosition-                    log-    execWriter (runGoT action cursor) @?=-      ["B (2,2)", "B (3,3)", "B (5,5)", "B (3,3)", "B (2,2)"],--  testCase "should drop then pop" $ do-    let cursor = child 0 $ child 0 commonCursor-        action = do navigate-                    log >> dropPosition-                    log >> popPosition-                    log-    execWriter (runGoT action cursor) @?=-      ["B (2,2)", "B (3,3)", "B (5,5)", "B (5,5)", "B (2,2)"],--  testCase "should drop twice" $ do-    let cursor = child 0 $ child 0 commonCursor-        action = do navigate-                    log >> dropPosition-                    log >> dropPosition-                    log-    execWriter (runGoT action cursor) @?=-      ["B (2,2)", "B (3,3)", "B (5,5)", "B (5,5)", "B (5,5)"],--  testCase "should fire navigation handlers while popping" $ do-    let cursor = rootCursor $ node1 [B Nothing] $ node [W Nothing]-        action = do pushPosition-                    goDown 0-                    goUp-                    on navigationEvent $ \step -> tell [step]-                    popPosition-    execWriter (runGoT action cursor) @?= [GoDown 0, GoUp 0]-  ]-  where commonCursor = rootCursor $-                       node' [B $ Just (0,0)]-                             [node' [W $ Just (1,1)]-                                    [node [B $ Just (2,2)],-                                     node [B $ Just (3,3)]],-                              node1 [W $ Just (4,4)] $ node [B $ Just (5,5)]]-        log = getCursor >>= \cursor -> case cursorProperties cursor of-          [B (Just x)] -> tell ["B " ++ show x]-          [W (Just x)] -> tell ["W " ++ show x]-          xs -> error $ "Unexpected properties: " ++ show xs-        navigate = do log >> pushPosition-                      goUp >> goDown 1-                      log >> pushPosition-                      goUp >> goUp >> goDown 1 >> goDown 0--propertiesTests = testGroup "properties" [-  testGroup "getProperties" [-    testCase "returns an empty list" $-      let cursor = rootCursor $ node []-      in evalGo getProperties cursor @?= [],--    testCase "returns a non-empty list" $-      let properties = [PB $ toSimpleText "Foo", B Nothing]-          cursor = rootCursor $ node properties-      in evalGo getProperties cursor @?= properties-    ],--  testGroup "modifyProperties" [-    testCase "adds properties" $-      let cursor = rootCursor $ node [FF 1]-          action = do modifyProperties $ \props -> return $ props ++ [B Nothing, W Nothing]-                      getProperties-      in evalGo action cursor @?= [FF 1, B Nothing, W Nothing],--    testCase "removes properties" $-      let cursor = rootCursor $ node [W Nothing, FF 1, B Nothing]-          action = do modifyProperties $ \props -> return $ filter (not . isMoveProperty) props-                      getProperties-      in evalGo action cursor @?= [FF 1],--    testCase "fires a properties modified event" $-      let cursor = rootCursor $ node [FF 1]-          action = do on propertiesModifiedEvent $ \old new -> tell [(old, new)]-                      modifyProperties $ const $ return [FF 2]-          log = execWriter (runGoT action cursor)-      in log @?= [([FF 1], [FF 2])]-    ]-  ]-  where isMoveProperty prop = case prop of-          B _ -> True-          W _ -> True-          _ -> False--getPropertyTests = testGroup "getProperty" [-  testCase "doesn't find an unset property" $ do-    Nothing @=? evalGo (getProperty propertyW) (rootCursor $ node [])-    Nothing @=? evalGo (getProperty propertyW) (rootCursor $ node [B Nothing]),--  testCase "finds a set property" $ do-    Just (B Nothing) @=? evalGo (getProperty propertyB) (rootCursor $ node [B Nothing])-    Just (B Nothing) @=? evalGo (getProperty propertyB) (rootCursor $ node [B Nothing, DO])-    Just DO @=? evalGo (getProperty propertyDO) (rootCursor $ node [B Nothing, DO])-  ]--getPropertyValueTests = testGroup "getPropertyValue" [-  testCase "doesn't find an unset property" $ do-    Nothing @=? evalGo (getPropertyValue propertyW) (rootCursor $ node [])-    Nothing @=? evalGo (getPropertyValue propertyW) (rootCursor $ node [B Nothing]),--  testCase "finds a set property" $ do-    Just Nothing @=? evalGo (getPropertyValue propertyB) (rootCursor $ node [B Nothing])-    Just (Just (0,0)) @=?-      evalGo (getPropertyValue propertyB) (rootCursor $ node [B $ Just (0,0), TE Double1])-    Just Double1 @=?-      evalGo (getPropertyValue propertyTE) (rootCursor $ node [B $ Just (0,0), TE Double1])-  ]--putPropertyTests = testGroup "putProperty" [-  testCase "adds an unset property" $ do-    [IT] @=? cursorProperties (execGo (putProperty IT) $ rootCursor $ node [])-    [DO, IT] @=?-      sortProperties (cursorProperties (execGo (putProperty IT) $ rootCursor $ node [DO])),--  testCase "replaces an existing property" $-    [TE Double2] @=?-    cursorProperties (execGo (putProperty $ TE Double2) (rootCursor $ node [TE Double1]))-  ]--deletePropertyTests = testGroup "deleteProperty" [-  testCase "does nothing when the property isn't set" $ do-    [] @=? cursorProperties (execGo (deleteProperty DO) $ rootCursor $ node [])-    [B Nothing] @=? cursorProperties (execGo (deleteProperty DO) $ rootCursor $ node [B Nothing]),--  testCase "removes a property" $ do-    [] @=? cursorProperties (execGo (deleteProperty DO) $ rootCursor $ node [DO])-    -- This is documented behaviour for deleteProperty, the fact that it doesn't-    -- matter what the property value is:-    [DO] @=? cursorProperties-      (execGo (deleteProperty $ B $ Just (0,0)) $ rootCursor $ node [DO, B Nothing])-  ]--modifyPropertyTests = testGroup "modifyProperty" [-  testCase "adds a property to an empty node" $-    let cursor = rootCursor $ node []-        action = modifyProperty propertyIT (\Nothing -> Just IT)-    in cursorProperties (execGo action cursor) @?= [IT],--  testCase "adds a property to a non-empty node" $-    let cursor = rootCursor $ node [KO]-        action = modifyProperty propertyIT (\Nothing -> Just IT)-    in sortProperties (cursorProperties $ execGo action cursor) @?= [IT, KO],--  testCase "removes a property" $ do-    let cursor = rootCursor $ node [IT, KO]-        cursor' = execGo (modifyProperty propertyKO $ \(Just KO) -> Nothing) cursor-        cursor'' = execGo (modifyProperty propertyIT $ \(Just IT) -> Nothing) cursor'-    cursorProperties cursor' @?= [IT]-    cursorProperties cursor'' @?= [],--  testCase "updates a property" $ do-    let cursor = rootCursor $ node [B $ Just (0,0), BM Double2]-        action = modifyProperty propertyB $ \(Just (B (Just (0,0)))) -> Just $ B $ Just (1,1)-        cursor' = execGo action cursor-        action' = modifyProperty propertyBM $ \(Just (BM Double2)) -> Just $ BM Double1-        cursor'' = execGo action' cursor'-    sortProperties (cursorProperties cursor') @?= [B $ Just (1,1), BM Double2]-    sortProperties (cursorProperties cursor'') @?= [B $ Just (1,1), BM Double1],--  testCase "fires an event when modifying a property" $ do-    let cursor = rootCursor $ node [B $ Just (0,0)]-        action = do on propertiesModifiedEvent $ \[B (Just (0,0))] [] -> tell [0]-                    modifyProperty propertyB $ \(Just (B (Just (0,0)))) -> Nothing-        (cursor', log) = runWriter (execGoT action cursor)-    [0] @=? log-    [] @=? cursorProperties cursor',--  testCase "modifying but not actually changing a property doesn't fire an event" $ do-    let cursor = rootCursor $ node [B $ Just (0,0)]-        action = do on propertiesModifiedEvent $ \_ _ -> tell ["Event fired."]-                    modifyProperty propertyB $ \p@(Just (B {})) -> p-        (cursor', log) = runWriter (execGoT action cursor)-    [B $ Just (0,0)] @=? cursorProperties cursor'-    [] @=? log-  ]--modifyPropertyValueTests = testGroup "modifyPropertyValue" [-  testCase "adds a property to an empty node" $-    let cursor = rootCursor $ node []-        action = modifyPropertyValue propertyPL (\Nothing -> Just Black)-    in cursorProperties (execGo action cursor) @?= [PL Black],--  testCase "adds a property to a non-empty node" $-    let cursor = rootCursor $ node [KO]-        action = modifyPropertyValue propertyPL (\Nothing -> Just White)-    in sortProperties (cursorProperties $ execGo action cursor) @?= [KO, PL White],--  testCase "removes a property" $ do-    let cursor = rootCursor $ node [B Nothing, TE Double2]-        cursor' = execGo (modifyPropertyValue propertyTE $ \(Just Double2) -> Nothing) cursor-        cursor'' = execGo (modifyPropertyValue propertyB $ \(Just Nothing) -> Nothing) cursor'-    cursorProperties cursor' @?= [B Nothing]-    cursorProperties cursor'' @?= [],--  testCase "updates a property" $ do-    let cursor = rootCursor $ node [B $ Just (0,0), BM Double2]-        action = modifyPropertyValue propertyB $ \(Just (Just (0,0))) -> Just $ Just (1,1)-        cursor' = execGo action cursor-        action' = modifyPropertyValue propertyBM $ \(Just Double2) -> Just Double1-        cursor'' = execGo action' cursor'-    sortProperties (cursorProperties cursor') @?= [B $ Just (1,1), BM Double2]-    sortProperties (cursorProperties cursor'') @?= [B $ Just (1,1), BM Double1]-  ]--modifyPropertyStringTests =-  testGroup "modifyPropertyString"-  -- Test a Text property and a SimpleText property.-  (assumptions ++ genTests "comment" propertyC ++ genTests "game name" propertyGN)-  where assumptions = let _ = C $ toText ""-                          _ = GN $ toSimpleText ""-                      in []-        genTests name property = [-          testCase ("adds a " ++ name) $-            let cursor = rootCursor $ node []-                action = modifyPropertyString property (++ "Hello.")-            in cursorProperties (execGo action cursor) @?=-               [propertyBuilder property $ stringToSgf "Hello."],--          testCase ("removes a " ++ name) $-            let cursor = rootCursor $ node [propertyBuilder property $ stringToSgf "Hello."]-                action = modifyPropertyString property $ \value -> case value of-                  "Hello." -> ""-                  other -> error $ "Got: " ++ other-            in cursorProperties (execGo action cursor) @?= [],--          testCase ("updates a " ++ name) $-            let cursor = child 0 $ rootCursor $-                         node1 [propertyBuilder property $ stringToSgf "one"] $-                         node [propertyBuilder property $ stringToSgf "two"]-                action = modifyPropertyString property $ \value -> case value of-                  "two" -> "three"-                  other -> error $ "Got: " ++ other-                cursor' = execGo action cursor-            in (cursorProperties cursor', cursorProperties $ fromJust $ cursorParent cursor') @?=-               ([propertyBuilder property $ stringToSgf "three"],-                [propertyBuilder property $ stringToSgf "one"]),--          testCase "leaves a non-existant comment" $-            let result = execGo (modifyPropertyString property id) $ rootCursor $ node []-            in cursorProperties result @?= []-          ]--modifyPropertyCoordsTests = testGroup "modifyPropertyCoords" [-  testCase "adds a property where there was none" $-    let cursor = rootCursor $ node []-        action = modifyPropertyCoords propertySL $ \[] -> [(0,0), (1,1)]-    in [SL $ coords [(0,0), (1,1)]] @=? cursorProperties (execGo action cursor),--  testCase "removes a property where there was one" $-    let cursor = rootCursor $ node [TR $ coord1 (5,5)]-        action = modifyPropertyCoords propertyTR $ \[(5,5)] -> []-    in [] @=? cursorProperties (execGo action cursor),--  testCase "modifies an existing property" $-    let cursor = rootCursor $ node [CR $ coord1 (3,4)]-        action = modifyPropertyCoords propertyCR $ \[(3,4)] -> [(2,2)]-    in [CR $ coord1 (2,2)] @=? cursorProperties (execGo action cursor),--  testCase "doesn't affect other properties" $-    let cursor = rootCursor $ node [SQ $ coord1 (0,0), MA $ coord1 (1,1)]-        action = modifyPropertyCoords propertyMA $ \[(1,1)] -> [(2,2)]-    in [MA $ coord1 (2,2), SQ $ coord1 (0,0)] @=?-       sortProperties (cursorProperties $ execGo action cursor)-  ]--modifyGameInfoTests = testGroup "modifyGameInfo" [-  testGroup "creates info on the root node, starting with none" [-     testCase "starting from the root node" $-       let cursor = rootCursor $ node []-           action = modifyGameInfo (\info -> info { gameInfoGameName = Just "Orange" })-       in cursorProperties (execGo action cursor) @?= [GN $ toSimpleText "Orange"],--     testCase "starting from a non-root node" $-       let cursor = child 0 $ rootCursor $ node1 [] $ node [B Nothing]-           action = modifyGameInfo (\info -> info { gameInfoGameName = Just "Orange" })-           cursor' = execGo action cursor-       in (cursorProperties cursor', cursorProperties $ fromJust $ cursorParent cursor') @?=-          ([B Nothing], [GN $ toSimpleText "Orange"])-     ],--  testGroup "modifies existing info on the root" [-    testCase "starting from the root node" $-      let cursor = rootCursor $ node [GN $ toSimpleText "Orange"]-          action = modifyGameInfo (\info -> info { gameInfoGameName = Nothing-                                                 , gameInfoBlackName = Just "Peanut butter"-                                                 })-      in cursorProperties (execGo action cursor) @?= [PB $ toSimpleText "Peanut butter"],--    testCase "starting from a non-root node" $-      let cursor = child 0 $ rootCursor $ node1 [GN $ toSimpleText "Orange"] $ node [B Nothing]-          action = modifyGameInfo (\info -> info { gameInfoGameName = Nothing-                                                 , gameInfoBlackName = Just "Peanut butter"-                                                 })-          cursor' = execGo action cursor-      in (cursorProperties cursor', cursorProperties $ fromJust $ cursorParent cursor') @?=-         ([B Nothing], [PB $ toSimpleText "Peanut butter"])-    ],--  testCase "moves game info from a non-root node to the root" $-    let cursor = child 0 $ child 0 $ child 0 $ rootCursor $-                 node1 [SZ 19 19] $-                 node1 [B $ Just (0,0), GN $ toSimpleText "Orange"] $-                 node1 [W $ Just (1,1)] $-                 node [B $ Just (2,2)]-        action = modifyGameInfo (\info -> info { gameInfoGameName = Nothing })-        cursor' = execGo action cursor-        action' = modifyGameInfo (\info -> info { gameInfoBlackName = Just "Peanut butter" })-        cursor'' = execGo action' cursor'-        -- unfoldr :: (Maybe Cursor -> Maybe ([Property], Maybe Cursor))-        --         -> Maybe Cursor-        --         -> [[Property]]-        properties = unfoldr (fmap $ cursorProperties &&& cursorParent)-                             (Just cursor'')-    in map sortProperties properties @?=-       [[B $ Just (2,2)],-        [W $ Just (1,1)],-        [B $ Just (0,0)],-        [PB $ toSimpleText "Peanut butter", SZ 19 19]]-  ]--modifyVariationModeTests = testGroup "modifyVariationMode" [-  testCase "testing modes are not default" $ do-    defaultVariationMode @/=? mode-    defaultVariationMode @/=? mode2,--  testCase "modifies when at a root node" $-    node1 [ST mode] (node []) @=?-    cursorNode (execGo setMode $ rootCursor $ node1 [] $ node []),--  testCase "modifies when at a non-root node" $-    node1 [ST mode] (node []) @=?-    cursorNode (cursorRoot $ execGo setMode $ child 0 $ rootCursor $ node1 [] $ node []),--  testCase "leaves an unset ST unset" $-    assertST Nothing Nothing $ const defaultVariationMode,--  testCase "leaves a default ST alone" $-    assertST (Just defaultVariationMode) (Just defaultVariationMode) $ const defaultVariationMode,--  testCase "leaves an existing ST alone" $-    assertST (Just mode) (Just mode) $ const mode,--  testCase "adds an ST property" $-    assertST (Just mode) Nothing $ const mode,--  testCase "removes an ST property when setting to default" $-    assertST Nothing (Just mode) $ const defaultVariationMode,--  testCase "modifies an existing ST property" $-    assertST (Just mode2) (Just mode) $ const mode2-  ]-  where mode = VariationMode ShowCurrentVariations True-        mode2 = VariationMode ShowChildVariations False-        setMode = modifyVariationMode $ const mode-        assertST maybeExpectedST maybeInitialST fn =-          node (ST <$> maybeToList maybeExpectedST) @=?-          cursorNode (execGo (modifyVariationMode fn) $-                      rootCursor $ node $ ST <$> maybeToList maybeInitialST)--getMarkTests = testGroup "getMark" [-  testCase "returns Nothing for no mark" $ do-    Nothing @=? evalGo (getMark (0,0)) (rootCursor $ node [])-    Nothing @=? evalGo (getMark (0,0)) (rootCursor $ node [W $ Just (0,0)]),--  testCase "returns Just when a mark is present" $ do-    Just MarkSquare @=? evalGo (getMark (1,2)) (rootCursor $ node [SQ $ coord1 (1,2)])-    Just MarkTriangle @=? evalGo (getMark (1,1)) (rootCursor $ node [B $ Just (1,1),-                                                                     TR $ coord1 (1,1),-                                                                     SQ $ coord1 (1,2)]),--  testCase "matches all marks" $ forM_ [minBound..maxBound] $ \mark ->-    Just mark @=?-    evalGo (getMark (0,0))-           (rootCursor $ node [propertyBuilder (markProperty mark) $ coord1 (0,0)])-  ]--modifyMarkTests = testGroup "modifyMark" [-  testCase "creates a mark where there was none" $-    let cursor = rootCursor $ node []-        action = do modifyMark (\Nothing -> Just MarkX) (0,0)-                    getMark (0,0)-    in Just MarkX @=? evalGo action cursor,--  testCase "removes an existing mark" $-    let cursor = rootCursor $ node [CR $ coord1 (0,0)]-        action = do modifyMark (\(Just MarkCircle) -> Nothing) (0,0)-                    getMark (0,0)-    in Nothing @=? evalGo action cursor,--  testCase "replaces an existing mark" $-    let cursor = rootCursor $ node [CR $ coord1 (0,0)]-        action = do modifyMark (\(Just MarkCircle) -> Just MarkX) (0,0)-                    getMark (0,0)-    in Just MarkX @=? evalGo action cursor,--  testCase "adds on to an existing mark property" $-    let cursor = rootCursor $ node [MA $ coord1 (0,0)]-        action = do modifyMark (\Nothing -> Just MarkX) (1,0)-                    mapM getMark [(0,0), (1,0)]-    in [Just MarkX, Just MarkX] @=? evalGo action cursor,--  testCase "removes from an existing mark property" $-    let cursor = rootCursor $ node [MA $ coords [(0,0), (1,0), (0,1)]]-        action = do modifyMark (\(Just MarkX) -> Nothing) (1,0)-                    modifyMark (\(Just MarkX) -> Nothing) (0,1)-                    mapM getMark [(0,0), (1,0), (0,1)]-    in [Just MarkX, Nothing, Nothing] @=? evalGo action cursor,--  testCase "removes and adds at the same time" $-    let cursor = rootCursor $ node [MA $ coords [(0,0), (1,0), (0,1)]]-        action = do modifyMark (\(Just MarkX) -> Just MarkSquare) (0,0)-                    modifyMark (\(Just MarkX) -> Nothing) (0,1)-                    mapM getMark [(0,0), (1,0), (0,1)]-    in [Just MarkSquare, Just MarkX, Nothing] @=? evalGo action cursor-  ]--addChildTests = testGroup "addChild" [-  testCase "adds an only child" $-    cursorNode (execGo (addChild 0 $ node [B Nothing]) (rootCursor $ node []))-    @?= node' [] [node [B Nothing]],--  testCase "adds a first child" $-    cursorNode (execGo (addChild 0 $ node [B $ Just (0,0)])-                       (rootCursor $ node' [] [node [B $ Just (1,1)]]))-    @?= node' [] [node [B $ Just (0,0)],-                  node [B $ Just (1,1)]],--  testCase "adds a middle child" $-    cursorNode (execGo (addChild 1 $ node [B $ Just (1,1)])-                       (rootCursor $ node' [] [node [B $ Just (0,0)],-                                               node [B $ Just (2,2)]]))-    @?= node' [] [node [B $ Just (0,0)],-                  node [B $ Just (1,1)],-                  node [B $ Just (2,2)]],--  testCase "adds a last child" $-    cursorNode (execGo (addChild 2 $ node [B $ Just (2,2)])-                       (rootCursor $ node' [] [node [B $ Just (0,0)],-                                               node [B $ Just (1,1)]]))-    @?= node' [] [node [B $ Just (0,0)],-                  node [B $ Just (1,1)],-                  node [B $ Just (2,2)]],--  testGroup "path stack correctness" [-    testCase "basic case just not needing updating" $-      let cursor = child 0 $ rootCursor $ node' [B Nothing] [node [W Nothing]]-          action = do pushPosition-                      goUp-                      addChild 1 $ node [W $ Just (0,0)]-                      popPosition-      in cursorNode (execGo action cursor) @?= node [W Nothing],--    testCase "basic case just needing updating" $-      let cursor = child 0 $ rootCursor $ node' [B Nothing] [node [W Nothing]]-          action = do pushPosition-                      goUp-                      addChild 0 $ node [W $ Just (0,0)]-                      popPosition-      in cursorNode (execGo action cursor) @?= node [W Nothing],--    testCase "basic case definitely needing updating" $-      let cursor = rootCursor $ node' [B Nothing] [node [W $ Just (0,0)],-                                                   node [W $ Just (1,1)]]-          action = do goDown 1-                      pushPosition-                      goUp-                      addChild 0 $ node [W Nothing]-                      popPosition-      in cursorNode (execGo action cursor) @?= node [W $ Just (1,1)],--    testCase "multiple paths to update" $-      let at y x = B $ Just (y,x)-          level0Node = node' [at 0 0] $ map level1Node [0..1]-          level1Node i = node' [at 1 i] $ map level2Node [0..2]-          level2Node i = node' [at 2 i] $ map level3Node [0..3]-          level3Node i = node [at 3 i]-          action = do goDown 1 >> goDown 2 >> goDown 3-                      pushPosition-                      replicateM_ 4 goUp-                      goDown 1 >> goDown 2 >> goDown 2 >> goUp >> goDown 1-                      addChild 0 $ node' [] [node []]-                      goDown 0 >> goDown 0-                      goToRoot-                      popPosition-      in cursorNode (execGo action $ rootCursor level0Node) @?= node [B $ Just (3,3)],--    testCase "updates paths with GoUp correctly" $-      let cursor = rootCursor $ node1 [B $ Just (0,0)] $ node [W $ Just (1,1)]-          action = do pushPosition-                      goDown 0-                      goUp-                      addChild 0 $ node [B $ Just (2,2)]-                      on navigationEvent $ \step -> tell [step]-                      popPosition-          (_, log) = runWriter (runGoT action cursor)-      in log @?= [GoDown 1, GoUp 1]-    ]-  ]--gameInfoChangedTests = testGroup "gameInfoChangedEvent" [-  testCase "fires when navigating down" $-    let cursor = rootCursor $-                 node1 [B $ Just (0,0)] $-                 node [W $ Just (0,0), GN $ toSimpleText "Foo"]-        action = do on gameInfoChangedEvent onInfo-                    goDown 0-    in execWriter (runGoT action cursor) @?= [(Nothing, Just "Foo")],--  testCase "fires when navigating up" $-    let cursor = child 0 $ rootCursor $-                 node1 [B $ Just (0,0)] $-                 node [W $ Just (0,0), GN $ toSimpleText "Foo"]-        action = do on gameInfoChangedEvent onInfo-                    goUp-    in execWriter (runGoT action cursor) @?= [(Just "Foo", Nothing)],--  testCase "fires from within popPosition" $-    let cursor = rootCursor $-                 node1 [B $ Just (0,0)] $-                 node [W $ Just (0,0), GN $ toSimpleText "Foo"]-        action = do pushPosition-                    goDown 0-                    goUp-                    on gameInfoChangedEvent onInfo-                    popPosition-    in execWriter (runGoT action cursor) @?=-       [(Nothing, Just "Foo"), (Just "Foo", Nothing)],--  testCase "fires when modifying properties" $-    let cursor = rootCursor $ node []-        action = do on gameInfoChangedEvent onInfo-                    modifyProperties $ const $ return [GN $ toSimpleText "Foo"]-                    modifyProperties $ const $ return [GN $ toSimpleText "Bar"]-                    modifyProperties $ const $ return []-    in execWriter (runGoT action cursor) @?=-       [(Nothing, Just "Foo"), (Just "Foo", Just "Bar"), (Just "Bar", Nothing)]-  ]-  where onInfo old new = tell [(gameInfoGameName old, gameInfoGameName new)]
− tests/Game/Goatee/Sgf/ParserTest.hs
@@ -1,242 +0,0 @@--- This file is part of Goatee.------ Copyright 2014 Bryan Gardiner------ Goatee is free software: you can redistribute it and/or modify--- it under the terms of the GNU Affero General Public License as published by--- the Free Software Foundation, either version 3 of the License, or--- (at your option) any later version.------ Goatee is distributed in the hope that it will be useful,--- but WITHOUT ANY WARRANTY; without even the implied warranty of--- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the--- GNU Affero General Public License for more details.------ You should have received a copy of the GNU Affero General Public License--- along with Goatee.  If not, see <http://www.gnu.org/licenses/>.--module Game.Goatee.Sgf.ParserTest (tests) where--import Control.Monad (forM_)-import Game.Goatee.Sgf.ParserTestUtils-import Game.Goatee.Sgf.Property-import Game.Goatee.Sgf.TestInstances ()-import Game.Goatee.Sgf.TestUtils-import Game.Goatee.Sgf.Tree-import Game.Goatee.Sgf.Types-import Test.Framework (testGroup)-import Test.Framework.Providers.HUnit (testCase)-import Test.HUnit ((@?=))--tests = testGroup "Game.Goatee.Sgf.Parser" [-  baseCaseTests,-  whitespaceTests,-  passConversionTests,-  movePropertyTests,-  setupPropertyTests,-  nodeAnnotationPropertyTests,-  moveAnnotationPropertyTests,-  rootPropertyTests-  ]--baseCaseTests = testGroup "base cases" [-  testCase "works with the trivial collection" $-    parseOrFail "(;)" (@?= emptyNode),--  testCase "works with only a size property" $ do-    parseOrFail "(;SZ[1])" (@?= root 1 1 [] [])-    parseOrFail "(;SZ[9])" (@?= root 9 9 [] [])-  ]--whitespaceTests = testGroup "whitespace handling" [-  testCase "parses with no extra whitespace" $-    parseOrFail "(;SZ[4];AB[aa][bb]AW[cc];W[dd])"-    (@?= root 4 4 [] [node1 [AB $ coords [(0,0), (1,1)], AW $ coords [(2,2)]] $-                      node [W $ Just (3,3)]]),--  testCase "parses with spaces between nodes" $-    parseOrFail "(;SZ[1] ;B[])" (@?= root 1 1 [] [node [B Nothing]]),--  testCase "parses with spaces between properties" $-    parseOrFail "(;SZ[1] AB[aa])" (@?= root 1 1 [AB $ coords [(0,0)]] []),--  testCase "parses with spaces between a property's name and value" $-    parseOrFail "(;SZ [1])" (@?= root 1 1 [] []),--  testCase "parses with spaces between property values" $-    parseOrFail "(;SZ[2]AB[aa] [bb])" (@?= root 2 2 [AB $ coords [(0,0), (1,1)]] []),--  testCase "parses with spaces between many elements" $-    parseOrFail " ( ; SZ [4] ; AB [aa:ad] [bb] AW [cc] ; W [dd] ; B [] ) "-    (@?= root 4 4 [] [node1 [AB $ coords' [(1,1)] [((0,0), (0,3))],-                             AW $ coords [(2,2)]] $-                      node1 [W $ Just (3,3)] $-                      node [B Nothing]])--  -- TODO Test handling of whitespace between an unknown property name and-  -- [value].-  ]--passConversionTests = testGroup "B[tt]/W[tt] pass conversion" [-  testCase "converts B[tt] for a board sizes <=19x19" $ do-    parseOrFail "(;SZ[1];B[tt])" (@?= root 1 1 [] [node [B Nothing]])-    parseOrFail "(;SZ[9];B[tt])" (@?= root 9 9 [] [node [B Nothing]])-    parseOrFail "(;SZ[19];B[tt])" (@?= root 19 19 [] [node [B Nothing]]),--  testCase "converts W[tt] for a board sizes <=19x19" $ do-    parseOrFail "(;SZ[1];W[tt])" (@?= root 1 1 [] [node [W Nothing]])-    parseOrFail "(;SZ[9];W[tt])" (@?= root 9 9 [] [node [W Nothing]])-    parseOrFail "(;SZ[19];W[tt])" (@?= root 19 19 [] [node [W Nothing]]),--  testCase "doesn't convert B[tt] for a board sizes >19x19" $ do-    parseOrFail "(;SZ[20];B[tt])" (@?= root 20 20 [] [node [B $ Just (19, 19)]])-    parseOrFail "(;SZ[21];B[tt])" (@?= root 21 21 [] [node [B $ Just (19, 19)]]),--  testCase "doesn't convert W[tt] for a board sizes >19x19" $ do-    parseOrFail "(;SZ[20];W[tt])" (@?= root 20 20 [] [node [W $ Just (19, 19)]])-    parseOrFail "(;SZ[21];W[tt])" (@?= root 21 21 [] [node [W $ Just (19, 19)]]),--  testCase "doesn't convert non-move properties" $ do-    -- TODO These should error, rather than parsing fine.-    parseOrFail "(;SZ[9];AB[tt])" (@?= root 9 9 [] [node [AB $ coords [(19, 19)]]])-    parseOrFail "(;SZ[9];TR[tt])" (@?= root 9 9 [] [node [TR $ coords [(19, 19)]]])-  ]--movePropertyTests = testGroup "move properties" [-  testGroup "B" [-    testCase "parses moves" $ do-      parseOrFail "(;B[aa])" (@?= node [B $ Just (0, 0)])-      parseOrFail "(;B[cp])" (@?= node [B $ Just (2, 15)]),-    testCase "parses passes" $-      parseOrFail "(;B[])" (@?= node [B Nothing])-    ],-  testCase "KO parses" $-    parseOrFail "(;KO[])" (@?= node [KO]),-  -- TODO Test MN (assert positive).-  testGroup "W" [-    testCase "parses moves" $ do-      parseOrFail "(;W[aa])" (@?= node [W $ Just (0, 0)])-      parseOrFail "(;W[cp])" (@?= node [W $ Just (2, 15)]),-    testCase "parses passes" $-      parseOrFail "(;W[])" (@?= node [W Nothing])-    ]-  ]--setupPropertyTests = testGroup "setup properties" [-  testCase "AB parses" $ do-    parseOrFail "(;AB[ab])" (@?= node [AB $ coords [(0, 1)]])-    parseOrFail "(;AB[ab][cd:ef])" (@?= node [AB $ coords' [(0, 1)] [((2, 3), (4, 5))]]),-  testCase "AE parses" $ do-    parseOrFail "(;AE[ae])" (@?= node [AE $ coords [(0, 4)]])-    parseOrFail "(;AE[ae][ff:gg])" (@?= node [AE $ coords' [(0, 4)] [((5, 5), (6, 6))]]),-  testCase "AW parses" $ do-    parseOrFail "(;AW[aw])" (@?= node [AW $ coords [(0, 22)]])-    parseOrFail "(;AW[aw][xy:yz])" (@?= node [AW $ coords' [(0, 22)] [((23, 24), (24, 25))]]),-  testCase "PL parses" $ do-    parseOrFail "(;PL[B])" (@?= node [PL Black])-    parseOrFail "(;PL[W])" (@?= node [PL White])-  ]--nodeAnnotationPropertyTests = testGroup "node annotation properties" [-  testCase "C parses" $-    parseOrFail "(;C[Me [30k\\]: What is White doing??\\\n\n:(])"-      (@?= node [C $ toText "Me [30k]: What is White doing??\n:("]),-  testCase "DM parses" $ do-    parseOrFail "(;DM[1])" (@?= node [DM Double1])-    parseOrFail "(;DM[2])" (@?= node [DM Double2]),-  testCase "GB parses" $ do-    parseOrFail "(;GB[1])" (@?= node [GB Double1])-    parseOrFail "(;GB[2])" (@?= node [GB Double2]),-  testCase "GW parses" $ do-    parseOrFail "(;GW[1])" (@?= node [GW Double1])-    parseOrFail "(;GW[2])" (@?= node [GW Double2]),-  testCase "HO parses" $ do-    parseOrFail "(;HO[1])" (@?= node [HO Double1])-    parseOrFail "(;HO[2])" (@?= node [HO Double2]),-  testCase "N parses" $-    parseOrFail "(;N[The best\\\n\nmove])" (@?= node [N $ toSimpleText "The best move"]),-  testCase "UC parses" $ do-    parseOrFail "(;UC[1])" (@?= node [UC Double1])-    parseOrFail "(;UC[2])" (@?= node [UC Double2]),-  testCase "V parses" $ do-    parseOrFail "(;V[-34.5])" (@?= node [V (-34.5)])-    parseOrFail "(;V[50])" (@?= node [V 50])-  ]--moveAnnotationPropertyTests = testGroup "move annotation properties" [-  testCase "BM parses" $ do-    parseOrFail "(;BM[1])" (@?= node [BM Double1])-    parseOrFail "(;BM[2])" (@?= node [BM Double2]),-  testCase "DO parses" $-    parseOrFail "(;DO[])" (@?= node [DO]),-  testCase "IT parses" $-    parseOrFail "(;IT[])" (@?= node [IT]),-  testCase "TE parses" $ do-    parseOrFail "(;TE[1])" (@?= node [TE Double1])-    parseOrFail "(;TE[2])" (@?= node [TE Double2])-  ]---- TODO Test markup properties.--rootPropertyTests = testGroup "root properties" [-  testCase "AP parses" $-    parseOrFail "(;AP[GoGoGo:1.2.3])"-    (@?= node [AP (toSimpleText "GoGoGo") (toSimpleText "1.2.3")]),-  testCase "CA parses" $ do-    parseOrFail "(;CA[UTF-8])" (@?= node [CA $ toSimpleText "UTF-8"])-    parseOrFail "(;CA[ISO-8859-1])" (@?= node [CA $ toSimpleText "ISO-8859-1"]),-  testGroup "FF" [-    testCase "accepts version 4" $-      parseOrFail "(;FF[4])" (@?= node [FF 4]),-    testCase "rejects versions 1-3" $ do-      parseAndFail "(;FF[1])"-      parseAndFail "(;FF[2])"-      parseAndFail "(;FF[3])"-    ],-  testGroup "GM" [-    testCase "parses 1 (Go)" $-      parseOrFail "(;GM[1])" (@?= node [GM 1]),-    testCase "rejects unsupported games" $-      forM_ [2..16] $ \x -> parseAndFail $ "(;GM[" ++ show x ++ "]"-    ],-  testGroup "ST" [-    testCase "parses valid variation modes" $ do-      parseOrFail "(;ST[0])" (@?= node [ST $ VariationMode ShowChildVariations True])-      parseOrFail "(;ST[1])" (@?= node [ST $ VariationMode ShowCurrentVariations True])-      parseOrFail "(;ST[2])" (@?= node [ST $ VariationMode ShowChildVariations False])-      parseOrFail "(;ST[3])" (@?= node [ST $ VariationMode ShowCurrentVariations False]),-    testCase "rejects invalid variation modes" $-      forM_ [4..10] $ \x -> parseAndFail $ "(;ST[" ++ show x ++ "]"-    ],-  testGroup "SZ" [-    testCase "parses square boards" $-      forM_ [1..52] $ \x ->-        parseOrFail ("(;SZ[" ++ show x ++ "])") (@?= node [SZ x x]),-    testCase "parses nonsquare boards" $ do-      parseOrFail "(;SZ[1:2])" (@?= node [SZ 1 2])-      parseOrFail "(;SZ[1:9])" (@?= node [SZ 1 9])-      parseOrFail "(;SZ[1:19])" (@?= node [SZ 1 19])-      parseOrFail "(;SZ[2:1])" (@?= node [SZ 2 1])-      parseOrFail "(;SZ[9:1])" (@?= node [SZ 9 1])-      parseOrFail "(;SZ[19:1])" (@?= node [SZ 19 1])-      parseOrFail "(;SZ[19:52])" (@?= node [SZ 19 52])-      parseOrFail "(;SZ[10:16])" (@?= node [SZ 10 16]),-    testCase "rejects invalid sizes" $ do-      -- Boards must have length at least 1...-      parseAndFail "(;SZ[0])"-      parseAndFail "(;SZ[-1])"-      -- ...and at most 52.-      parseAndFail "(;SZ[53])"-      parseAndFail "(;SZ[54])"-      parseAndFail "(;SZ[0:19])"-      parseAndFail "(;SZ[19:0])"-      parseAndFail "(;SZ[9:53])"-      parseAndFail "(;SZ[53:9])",-    -- This is specified by the SGF spec:-    testCase "rejects square boards defined with two numbers" $ do-      parseAndFail "(;SZ[1:1])"-      parseAndFail "(;SZ[19:19])"-    ]-  ]---- TODO Test the test of the properties.
− tests/Game/Goatee/Sgf/ParserTestUtils.hs
@@ -1,63 +0,0 @@--- This file is part of Goatee.------ Copyright 2014 Bryan Gardiner------ Goatee is free software: you can redistribute it and/or modify--- it under the terms of the GNU Affero General Public License as published by--- the Free Software Foundation, either version 3 of the License, or--- (at your option) any later version.------ Goatee is distributed in the hope that it will be useful,--- but WITHOUT ANY WARRANTY; without even the implied warranty of--- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the--- GNU Affero General Public License for more details.------ You should have received a copy of the GNU Affero General Public License--- along with Goatee.  If not, see <http://www.gnu.org/licenses/>.--module Game.Goatee.Sgf.ParserTestUtils (-  parseOrFail,-  parseAndFail,-  assertParse,-  assertNoParse,-  ) where--import Control.Applicative ((<*))-import Game.Goatee.Sgf.Parser-import Game.Goatee.Sgf.Tree-import Test.HUnit (assertFailure)-import Text.ParserCombinators.Parsec (Parser, eof, parse)---- Parses a string as a complete SGF document.  On success, executes the--- continuation function with the result.  Otherwise, causes an assertion--- failure.-parseOrFail :: String -> (Node -> IO ()) -> IO ()-parseOrFail input cont = case parseString input of-  Left error -> assertFailure $ "Failed to parse SGF: " ++ error-  Right (Collection roots) -> case roots of-    root:[] -> cont root-    _ -> assertFailure $ "Expected a single root node, got: " ++ show roots---- Parses a string as a complete SGF document and expects failure.-parseAndFail :: String -> IO ()-parseAndFail input = case parseString input of-  Left _ -> return ()-  Right result -> assertFailure $ "Expected " ++ show input ++-                  " not to parse.  Parsed to " ++ show result ++ "."---- Parses a string using the given parser and expects the parser to consume the--- entire input.  On success, executes the continuation function with the--- result.  Otherwise, causes an assertion failure.-assertParse :: Parser a -> String -> (a -> IO ()) -> IO ()-assertParse parser input cont = case parse (parser <* eof) "<assertParse>" input of-  Left error -> assertFailure $ "Failed to parse: " ++ show error-  Right result -> cont result---- Tries to parse a string using the given parser.  If the parse succeeds then--- this function causes an assertion failure, otherwise this function succeeds.-assertNoParse :: Show a => Parser a -> String -> IO ()-assertNoParse parser input = case parse (parser <* eof) "<assertNoParse>" input of-  Left _ -> return ()-  Right result -> assertFailure $-                  "Expected " ++ show input ++ " not to parse.  " ++-                  "Parsed to " ++ show result ++ "."
− tests/Game/Goatee/Sgf/Property/ParserTest.hs
@@ -1,391 +0,0 @@--- This file is part of Goatee.------ Copyright 2014 Bryan Gardiner------ Goatee is free software: you can redistribute it and/or modify--- it under the terms of the GNU Affero General Public License as published by--- the Free Software Foundation, either version 3 of the License, or--- (at your option) any later version.------ Goatee is distributed in the hope that it will be useful,--- but WITHOUT ANY WARRANTY; without even the implied warranty of--- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the--- GNU Affero General Public License for more details.------ You should have received a copy of the GNU Affero General Public License--- along with Goatee.  If not, see <http://www.gnu.org/licenses/>.--module Game.Goatee.Sgf.Property.ParserTest (tests) where--import Control.Applicative ((<$>))-import Control.Monad (forM_)-import Data.Maybe (catMaybes)-import Game.Goatee.Common-import Game.Goatee.Sgf.ParserTestUtils-import Game.Goatee.Sgf.Property-import Game.Goatee.Sgf.Property.Parser-import Game.Goatee.Sgf.TestInstances ()-import Game.Goatee.Sgf.TestUtils-import Game.Goatee.Sgf.Types-import Test.Framework (Test, testGroup)-import Test.Framework.Providers.HUnit (testCase)-import Test.HUnit ((@?=))-import Text.ParserCombinators.Parsec (Parser)--tests = testGroup "Game.Goatee.Sgf.Property.ParserTest" [-  -- Tests for public parsers.-  parserColorTests,-  parserDoubleTests,-  parserGameResultTests,-  parserIntegralTests,-  parserMoveTests,-  parserRealTests,-  parserRulesetTests,-  parserSizeTests,-  parserVariationModeTests,-  -- Tests for private parsers.-  lineTests,-  simpleTextTests,-  simpleTextComposedTests,-  textTests,-  textComposedTests,-  -- Miscellaneous tests.-  propertyValueArityTests-  ]--parserColorTests = testGroup "colorParser" [-  testCase "parses B" $ assertParse colorParser "[B]" (@?= Black),-  testCase "parses W" $ assertParse colorParser "[W]" (@?= White)-  ]--parserDoubleTests = testGroup "doubleParser" [-  testCase "parses 1" $ assertParse doubleParser "[1]" (@?= Double1),-  testCase "parses 2" $ assertParse doubleParser "[2]" (@?= Double2)-  ]--parserGameResultTests = testGroup "gameResultParser" [-  testCase "draw" $ do-    assertParse gameResultParser "[0]" (@?= GameResultDraw)-    assertParse gameResultParser "[Draw]" (@?= GameResultDraw),--  testCase "void" $-    assertParse gameResultParser "[Void]" (@?= GameResultVoid),--  testCase "unknown" $-    assertParse gameResultParser "[?]" (@?= GameResultUnknown),--  testCase "Black wins by points" $ do-    assertParse gameResultParser "[B+0.5]" (@?= GameResultWin Black (WinByScore 0.5))-    assertParse gameResultParser "[B+11]" (@?= GameResultWin Black (WinByScore 11))-    assertParse gameResultParser "[B+354.5]" (@?= GameResultWin Black (WinByScore 354.5)),--  testCase "Black wins by resignation" $ do-    assertParse gameResultParser "[B+R]" (@?= GameResultWin Black WinByResignation)-    assertParse gameResultParser "[B+Resign]" (@?= GameResultWin Black WinByResignation),--  testCase "Black wins on time" $ do-    assertParse gameResultParser "[B+T]" (@?= GameResultWin Black WinByTime)-    assertParse gameResultParser "[B+Time]" (@?= GameResultWin Black WinByTime),--  testCase "Black wins by forfeit" $ do-    assertParse gameResultParser "[B+F]" (@?= GameResultWin Black WinByForfeit)-    assertParse gameResultParser "[B+Forfeit]" (@?= GameResultWin Black WinByForfeit),--  testCase "White wins by points" $ do-    assertParse gameResultParser "[W+0.5]" (@?= GameResultWin White (WinByScore 0.5))-    assertParse gameResultParser "[W+11]" (@?= GameResultWin White (WinByScore 11))-    assertParse gameResultParser "[W+354.5]" (@?= GameResultWin White (WinByScore 354.5)),--  testCase "White wins by resignation" $ do-    assertParse gameResultParser "[W+R]" (@?= GameResultWin White WinByResignation)-    assertParse gameResultParser "[W+Resign]" (@?= GameResultWin White WinByResignation),--  testCase "White wins on time" $ do-    assertParse gameResultParser "[W+T]" (@?= GameResultWin White WinByTime)-    assertParse gameResultParser "[W+Time]" (@?= GameResultWin White WinByTime),--  testCase "White wins by forfeit" $ do-    assertParse gameResultParser "[W+F]" (@?= GameResultWin White WinByForfeit)-    assertParse gameResultParser "[W+Forfeit]" (@?= GameResultWin White WinByForfeit),--  testCase "custom game results" $ do-    assertParseToOther ""-    assertParseToOther "Everyone wins."-    assertParseToOther "W+Nuclear tesuji"-    assertParseToOther "B+Flamingo"-  ]-  where assertParseToOther input =-          assertParse gameResultParser ('[' : input ++ "]")-          (@?= GameResultOther (toSimpleText input))--parserIntegralTests = testGroup "integralParser" $ integerTestsFor integralParser--parserMoveTests = testGroup "moveParser" $ coordTestsFor moveParser Just ++ [-  testCase "parses an empty move as a pass" $ assertParse moveParser "[]" (@?= Nothing)-  ]--parserRealTests = testGroup "realParser" $ integerTestsFor realParser ++ [-  testCase "parses a decimal zero" $ assertParse realParser "[0.0]" (@?= 0),--  testCase "parses fractional positive numbers" $ do-    assertParse realParser "[0.5]" (@?= (1/2))-    assertParse realParser "[0.00125]" (@?= (1/800))-    assertParse realParser "[3.14]" (@?= (314/100))-    assertParse realParser "[10.0]" (@?= 10),--  testCase "parses fractional negative numbers" $ do-    assertParse realParser "[-0.5]" (@?= (-1/2))-    assertParse realParser "[-0.00125]" (@?= (-1/800))-    assertParse realParser "[-3.14]" (@?= (-314/100))-    assertParse realParser "[-10.0]" (@?= (-10))-  ]--parserRulesetTests = testGroup "rulesetParser" [-  testCase "parses known rules" $ do-    assertParse rulesetParser "[AGA]" (@?= KnownRuleset RulesetAga)-    assertParse rulesetParser "[Goe]" (@?= KnownRuleset RulesetIng)-    assertParse rulesetParser "[Japanese]" (@?= KnownRuleset RulesetJapanese)-    assertParse rulesetParser "[NZ]" (@?= KnownRuleset RulesetNewZealand),--  testCase "parses unknown rules" $ do-    assertParse rulesetParser "[Foo]" (@?= UnknownRuleset "Foo")-    assertParse rulesetParser "[First capture]" (@?= UnknownRuleset "First capture")-  ]--parserSizeTests = testGroup "size" [-  testCase "parses square boards" $ do-    assertParse sizeParser "[1]" (@?= (1, 1))-    assertParse sizeParser "[4]" (@?= (4, 4))-    assertParse sizeParser "[9]" (@?= (9, 9))-    assertParse sizeParser "[19]" (@?= (19, 19))-    assertParse sizeParser "[52]" (@?= (52, 52)),--  testCase "parses rectangular boards" $ do-    assertParse sizeParser "[1:2]" (@?= (1, 2))-    assertParse sizeParser "[9:5]" (@?= (9, 5))-    assertParse sizeParser "[19:9]" (@?= (19, 9)),--  testCase "rejects boards of non-positive size" $ do-    assertNoParse sizeParser "[0]"-    assertNoParse sizeParser "[-1]"-    assertNoParse sizeParser "[0:5]"-    assertNoParse sizeParser "[5:0]"-    assertNoParse sizeParser "[0:-2]",--  testCase "rejects square boards given in rectangular format" $ do-    assertNoParse sizeParser "[1:1]"-    assertNoParse sizeParser "[13:13]"-  ]--parserVariationModeTests = testGroup "variationModeParser" [-  testCase "mode 0" $-    assertParse variationModeParser "[0]" (@?= VariationMode ShowChildVariations True),--  testCase "mode 1" $-    assertParse variationModeParser "[1]" (@?= VariationMode ShowCurrentVariations True),--  testCase "mode 2" $-    assertParse variationModeParser "[2]" (@?= VariationMode ShowChildVariations False),--  testCase "mode 3" $-    assertParse variationModeParser "[3]" (@?= VariationMode ShowCurrentVariations False)-  ]--lineTests = testGroup "line" [-  testCase "parses all valid values" $ do-     let cases = zip (['a'..'z'] ++ ['A'..'Z']) [0..]-     forM_ cases $ \(char, expected) ->-       assertParse line [char] (@?= expected)-  ]--simpleTextTests = testGroup "simpleText" $-  textTestsFor simpleText fromSimpleText False ++-  textUnescapedNewlineConvertingTests (fromSimpleText <$> simpleText False)--simpleTextComposedTests = testGroup "simpleText composed" $-  textTestsFor simpleText fromSimpleText True ++-  textUnescapedNewlineConvertingTests (fromSimpleText <$> simpleText True)--textTests = testGroup "text" $-  textTestsFor text id False ++-  textUnescapedNewlinePreservingTests (text False)--textComposedTests = testGroup "text composed" $-  textTestsFor text id True ++-  textUnescapedNewlinePreservingTests (text True)--propertyValueArityTests = testGroup "property value arities" [-  testGroup "single values (single)" [-    testCase "accepts a property that requires a single number" $-      parseOrFail "(;SZ[1])" (@?= node [SZ 1 1]),--    testCase "accepts a property that requires a single point" $-      parseOrFail "(;B[dd])" (@?= node [B $ Just (3, 3)])-    ],--  testGroup "lists (listOf)" [-    testCase "doesn't accept an empty list" $-      parseAndFail "(;AR[])",--    testCase "accepts a single value" $-      parseOrFail "(;AR[aa:bb])" (@?= node [AR [((0, 0), (1, 1))]]),--    testCase "accepts two values" $-      parseOrFail "(;AR[aa:bb][cc:de])" (@?= node [AR [((0, 0), (1, 1)),-                                                       ((2, 2), (3, 4))]])-    ],--  testGroup "point lists (listOfPoint)" [-    testCase "doesn't accept an empty list" $-      parseAndFail "(;AB[])",--    testCase "accepts a single point" $-      parseOrFail "(;AB[aa])" (@?= node [AB $ coords [(0, 0)]]),--    testCase "accepts two points" $-      parseOrFail "(;AB[aa][bb])" (@?= node [AB $ coords [(0, 0), (1, 1)]]),--    testCase "accepts a rectangle" $-      parseOrFail "(;AB[aa:bb])" (@?= node [AB $ coords' [] [((0, 0), (1, 1))]]),--    testCase "accepts two rectangles" $-      parseOrFail "(;AB[aa:bb][cd:de])" (@?= node [AB $ coords' [] [((0, 0), (1, 1)),-                                                                    ((2, 3), (3, 4))]])-    ],--  testGroup "point elists (elistOfPoint)" [-    testCase "accepts an empty list" $-      parseOrFail "(;VW[])" (@?= node [VW $ coords []]),--    testCase "accepts single points" $-      parseOrFail "(;VW[aa][bb])" (@?= node [VW $ coords [(0, 0), (1, 1)]]),--    testCase "accepts a rectangle" $-      parseOrFail "(;VW[aa:bb])" (@?= node [VW $ coords' [] [((0, 0), (1, 1))]]),--    testCase "accepts two rectangles" $-      parseOrFail "(;VW[aa:bb][cc:dd])" (@?= node [VW $ coords' [] [((0, 0), (1, 1)),-                                                                    ((2, 2), (3, 3))]])-    ]--  -- TODO Test that invalid rectangles such as cd:dc fail to parse (or rather,-  -- get corrected with a warning).-  ]--integerTestsFor :: (Eq a, Num a, Show a) => Parser a -> [Test]-integerTestsFor parser = [-  testCase "parses 0" $ do-    assertParse parser "[0]" (@?= 0)-    assertParse parser "[+0]" (@?= 0)-    assertParse parser "[-0]" (@?= 0),--  testCase "parses positive integers" $ do-    assertParse parser "[1]" (@?= 1)-    assertParse parser "[20]" (@?= 20)-    assertParse parser "[4294967296]" (@?= (2 ^ 32))-    assertParse parser "[18446744073709551616]" (@?= (2 ^ 64)),--  testCase "parses positive integers with the plus sign" $ do-    assertParse parser "[+1]" (@?= 1)-    assertParse parser "[+20]" (@?= 20)-    assertParse parser "[+4294967296]" (@?= (2 ^ 32))-    assertParse parser "[+18446744073709551616]" (@?= (2 ^ 64)),--  testCase "parses negative integers" $ do-    assertParse parser "[-1]" (@?= (-1))-    assertParse parser "[-20]" (@?= (-20))-    assertParse parser "[-4294967296]" (@?= (- (2 ^ 32)))-    assertParse parser "[-18446744073709551616]" (@?= (- (2 ^ 64)))-  ]--coordTestsFor :: (Eq a, Show a) => Parser a -> (Coord -> a) -> [Test]-coordTestsFor parser f = [-  testCase "parses boundary points" $ do-    assertParse parser "[aa]" (@?= f (0, 0))-    assertParse parser "[zz]" (@?= f (25, 25))-    assertParse parser "[AA]" (@?= f (26, 26))-    assertParse parser "[ZZ]" (@?= f (51, 51)),--  testCase "parses coordinate order correctly" $ do-    assertParse parser "[ab]" (@?= f (0, 1))-    assertParse parser "[ba]" (@?= f (1, 0)),--  testCase "doesn't parse a partial point" $-    assertNoParse parser "[a]"-  ]--textTestsFor :: (Bool -> Parser a) -> (a -> String) -> Bool -> [Test]-textTestsFor makeParser toString testComposed =-  let rawParser = makeParser testComposed-      parser = toString <$> rawParser-      composedParser = mapTuple toString <$> compose rawParser rawParser-  in catMaybes [-    Just $ testCase "parses an empty string" $-      assertParse parser "" (@?= ""),--    Just $ testCase "parses a short string" $-      assertParse parser "Hello, world." (@?= "Hello, world."),--    Just $ testCase "preserves leading and trailing whitespace" $-      assertParse parser " \tHi. \t" (@?= "  Hi.  "),--    Just $ testCase "parses escaped backslashes" $ do-      assertParse parser "\\\\" (@?= "\\")-      assertParse parser "\\\\\\\\" (@?= "\\\\")-      assertNoParse parser "\\"-      assertNoParse parser "\\\\\\",--    Just $ testCase "parses escaped ']'s" $ do-      assertParse parser "\\]" (@?= "]")-      assertParse parser "\\]\\\\\\]" (@?= "]\\]"),--    Just $ if testComposed-           then testCase "parses escaped ':'s" $ do-             assertParse parser "\\:" (@?= ":")-             assertParse parser "\\:\\\\\\:" (@?= ":\\:")-             assertNoParse parser ":"-           else testCase "parses unescaped ':'s" $ do-             assertParse parser ":" (@?= ":")-             assertParse parser "::" (@?= "::")-             -- An escaped colon should parse just the same.-             assertParse parser "\\:" (@?= ":"),--    if not testComposed-      then Nothing-      else Just $ testCase "supports composed values" $ do-        assertParse composedParser ":" (@?= ("", ""))-        assertParse composedParser "a:" (@?= ("a", ""))-        assertParse composedParser ":z" (@?= ("", "z"))-        assertParse composedParser "a:z" (@?= ("a", "z"))-        assertParse composedParser "a\\\\:z" (@?= ("a\\", "z"))-        assertParse composedParser "a\\:b:y\\:z" (@?= ("a:b", "y:z"))-        assertNoParse composedParser "",--    -- Tests non-newline whitespace replacement.  Newline handling is-    -- specific to individual parsers.-    Just $ testCase "replaces whitespace with spaces" $-      assertParse parser "\t\r\f\v" (@?= "    "),--    Just $ testCase "removes escaped newlines" $ do-      assertParse parser "\\\n" (@?= "")-      assertParse parser "foo\\\nbar" (@?= "foobar")-      assertParse parser "foo \\\n bar" (@?= "foo  bar")-    ]--textUnescapedNewlinePreservingTests :: Parser String -> [Test]-textUnescapedNewlinePreservingTests parser = [-  testCase "preserves unescaped newlines" $ do-    assertParse parser "\n" (@?= "\n")-    assertParse parser "\n\n" (@?= "\n\n")-    assertParse parser "foo\nbar" (@?= "foo\nbar")-    assertParse parser "foo \n bar" (@?= "foo \n bar")-  ]--textUnescapedNewlineConvertingTests :: Parser String -> [Test]-textUnescapedNewlineConvertingTests parser = [-  testCase "converts unescaped newlines to spaces" $ do-    assertParse parser "\n" (@?= " ")-    assertParse parser "\n\n" (@?= "  ")-    assertParse parser "foo\nbar" (@?= "foo bar")-    assertParse parser "foo \n bar" (@?= "foo   bar")-  ]
− tests/Game/Goatee/Sgf/PropertyTest.hs
@@ -1,74 +0,0 @@--- This file is part of Goatee.------ Copyright 2014 Bryan Gardiner------ Goatee is free software: you can redistribute it and/or modify--- it under the terms of the GNU Affero General Public License as published by--- the Free Software Foundation, either version 3 of the License, or--- (at your option) any later version.------ Goatee is distributed in the hope that it will be useful,--- but WITHOUT ANY WARRANTY; without even the implied warranty of--- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the--- GNU Affero General Public License for more details.------ You should have received a copy of the GNU Affero General Public License--- along with Goatee.  If not, see <http://www.gnu.org/licenses/>.--module Game.Goatee.Sgf.PropertyTest (tests) where--import qualified Data.Set as Set-import Game.Goatee.Sgf.Property-import Game.Goatee.Sgf.Types-import Test.Framework (testGroup)-import Test.Framework.Providers.HUnit (testCase)-import Test.HUnit ((@=?))--tests = testGroup "Game.Goatee.Sgf.Property" [-  propertyMetadataTests,-  markPropertyTests-  ]--propertyMetadataTests = testGroup "property metadata" [-  testCase "game info properties" $ gameInfoProperties @=? filterTo GameInfoProperty allProperties,-  testCase "general properties" $ generalProperties @=? filterTo GeneralProperty allProperties,-  testCase "move properties" $ moveProperties @=? filterTo MoveProperty allProperties,-  testCase "root properties" $ rootProperties @=? filterTo RootProperty allProperties,-  testCase "setup properties" $ setupProperties @=? filterTo SetupProperty allProperties,--  testCase "inherited properties" $ [DD cl] @=? filter propertyInherited allProperties-  ]-  where filterTo propType = filter ((propType ==) . propertyType)-        moveProperties = [-- Move properties.-                          B Nothing, KO, MN 1, W Nothing,-                          -- Move annotation properties.-                          BM db, DO, IT, TE db]-        setupProperties = [-- Setup properties.-                           AB cl, AE cl, AW cl, PL Black]-        generalProperties = [-- Node annotation properties.-                             C tx, DM db, GB db, GW db, HO db, N st, UC db, V rv,-                             -- Markup properties.-                             AR [], CR cl, DD cl, LB [], LN [], MA cl, SL cl, SQ cl, TR cl,-                             -- Guess this fits here.-                             UnknownProperty "" (toUnknownPropertyValue "")]-        rootProperties = [-- Root properties.-                          AP st st, CA st, FF 1, GM 1, ST vm, SZ 1 1]-        gameInfoProperties = [-- Game info properties.-                              AN st, BR st, BT st, CP st, DT st, EV st, GC st, GN st, ON st, OT st,-                              PB st, PC st, PW st, RE GameResultVoid, RO st, RU ru,-                              SO st, TM rv, US st, WR st, WT st]-        allProperties = moveProperties ++ setupProperties ++ generalProperties ++-                        rootProperties ++ gameInfoProperties-        cl = emptyCoordList-        db = Double1-        tx = toText ""-        st = toSimpleText ""-        ru = KnownRuleset RulesetJapanese-        rv = 1-        vm = defaultVariationMode--markPropertyTests = testGroup "markProperty" [-  testCase "doesn't repeat properties" $-    let marks = [minBound..maxBound]-    in length marks @=? Set.size (Set.fromList $ map (propertyName . markProperty) marks)-  ]
− tests/Game/Goatee/Sgf/RoundTripTest.hs
@@ -1,85 +0,0 @@--- This file is part of Goatee.------ Copyright 2014 Bryan Gardiner------ Goatee is free software: you can redistribute it and/or modify--- it under the terms of the GNU Affero General Public License as published by--- the Free Software Foundation, either version 3 of the License, or--- (at your option) any later version.------ Goatee is distributed in the hope that it will be useful,--- but WITHOUT ANY WARRANTY; without even the implied warranty of--- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the--- GNU Affero General Public License for more details.------ You should have received a copy of the GNU Affero General Public License--- along with Goatee.  If not, see <http://www.gnu.org/licenses/>.--module Game.Goatee.Sgf.RoundTripTest (tests) where--import Data.Function (on)-import Game.Goatee.Common-import Game.Goatee.Sgf.Parser-import Game.Goatee.Sgf.Property-import Game.Goatee.Sgf.Renderer-import Game.Goatee.Sgf.Renderer.Tree-import Game.Goatee.Sgf.TestUtils-import Game.Goatee.Sgf.Tree-import Game.Goatee.Sgf.Types-import Test.Framework (Test, testGroup)-import Test.Framework.Providers.HUnit (testCase)-import Test.HUnit ((@?=), Assertion, assertFailure)--testCollection' :: Collection -> Assertion-testCollection' collection =-  case runRender $ renderCollection collection of-    Left message -> assertFailure $ "Failed to render: " ++ message-    Right serialized ->-      case parseString serialized of-        Left error -> assertFailure $ "Failed to parse: " ++ error ++-                      "\n\nEntire SGF: " ++ serialized-        Right collection' -> do-          on (@?=) CollectionWithDeepEquality collection' collection-          case runRender $ renderCollection collection' of-            Left message -> assertFailure $ "Second render failed: " ++ message-            Right serializedAgain -> serializedAgain @?= serialized---- | Returns an assertion that the given node round-trips okay.-testNode' :: Node -> Assertion-testNode' = testCollection' . Collection . (:[])---- | Returns a test with the given string name that round-trips a single node.-testNode :: String -> Node -> Test-testNode label node = testCase label $ testNode' node--tests = testGroup "Game.Goatee.Sgf.RoundTripTest" [-  singleNodeGameTests,-  propertyValueTests-  ]--singleNodeGameTests = testGroup "games with single nodes" [-  testNode "empty game" $ node [],-  testNode "some default properties" $ node [FF 4, GM 1, ST defaultVariationMode]-  ]--propertyValueTests = testGroup "property value types" [-  testGroup "label list values" [-    testNode "one value" $ node [LB [((5,2), toSimpleText "Hi.")]],-    testNode "multiple value" $ node [LB [((5, 2), toSimpleText "Hi."),-                                          ((0, 1), toSimpleText "Bye.")]]-    ],--  testGroup "point-valued values" $-    for [0..boardSizeMax-1]-    (\row -> testNode ("row " ++ show row) $ node [B $ Just (0, row)]) ++-    for [0..boardSizeMax-1]-    (\col -> testNode ("row " ++ show col) $ node [B $ Just (col, 0)]),--  testGroup "real values" [-    testNode "1500" $ node [TM 1500],-    testNode "60.5" $ node [TM 60.5],-    testNode "10.1" $ node [TM 10.1]-    ]-  ]---- TODO Many more round-trip tests.
− tests/Game/Goatee/Sgf/TestInstances.hs
@@ -1,25 +0,0 @@--- This file is part of Goatee.------ Copyright 2014 Bryan Gardiner------ Goatee is free software: you can redistribute it and/or modify--- it under the terms of the GNU Affero General Public License as published by--- the Free Software Foundation, either version 3 of the License, or--- (at your option) any later version.------ Goatee is distributed in the hope that it will be useful,--- but WITHOUT ANY WARRANTY; without even the implied warranty of--- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the--- GNU Affero General Public License for more details.------ You should have received a copy of the GNU Affero General Public License--- along with Goatee.  If not, see <http://www.gnu.org/licenses/>.---- | Test utilities for working with the SGF modules.-module Game.Goatee.Sgf.TestInstances () where--import Data.Function (on)-import Game.Goatee.Sgf.Tree (Node, NodeWithDeepEquality(NodeWithDeepEquality))--instance Eq Node where-  (==) = (==) `on` NodeWithDeepEquality
− tests/Game/Goatee/Sgf/TestUtils.hs
@@ -1,61 +0,0 @@--- This file is part of Goatee.------ Copyright 2014 Bryan Gardiner------ Goatee is free software: you can redistribute it and/or modify--- it under the terms of the GNU Affero General Public License as published by--- the Free Software Foundation, either version 3 of the License, or--- (at your option) any later version.------ Goatee is distributed in the hope that it will be useful,--- but WITHOUT ANY WARRANTY; without even the implied warranty of--- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the--- GNU Affero General Public License for more details.------ You should have received a copy of the GNU Affero General Public License--- along with Goatee.  If not, see <http://www.gnu.org/licenses/>.---- | Test utilities for working with the SGF modules.-module Game.Goatee.Sgf.TestUtils (-  node,-  node1,-  node',-  root,-  child,-  sortProperties,-  ) where--import Data.Function (on)-import Data.List (sortBy)-import Game.Goatee.Sgf.Board-import Game.Goatee.Sgf.Property-import Game.Goatee.Sgf.Tree--node :: [Property] -> Node-node props = emptyNode { nodeProperties = props }--node1 :: [Property] -> Node -> Node-node1 props child = emptyNode { nodeProperties = props-                              , nodeChildren = [child]-                              }--node' :: [Property] -> [Node] -> Node-node' props children = emptyNode { nodeProperties = props-                                , nodeChildren = children-                                }---- | Creates a root node that starts with only a 'SZ' property.  This is more--- minimal than 'rootNode'.-root :: Int -> Int -> [Property] -> [Node] -> Node-root width height props =-  foldr addChild $ foldr addProperty emptyNode (SZ width height:props)--child :: Int -> Cursor -> Cursor-child = flip cursorChild---- | Sorts properties by their 'Show' strings, for use in tests that need to--- check an unordered list of properties.------ TODO Probably better to have a compare-unordered operator.-sortProperties :: [Property] -> [Property]-sortProperties = sortBy (compare `on` show)
− tests/Game/Goatee/Sgf/TreeTest.hs
@@ -1,114 +0,0 @@--- This file is part of Goatee.------ Copyright 2014 Bryan Gardiner------ Goatee is free software: you can redistribute it and/or modify--- it under the terms of the GNU Affero General Public License as published by--- the Free Software Foundation, either version 3 of the License, or--- (at your option) any later version.------ Goatee is distributed in the hope that it will be useful,--- but WITHOUT ANY WARRANTY; without even the implied warranty of--- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the--- GNU Affero General Public License for more details.------ You should have received a copy of the GNU Affero General Public License--- along with Goatee.  If not, see <http://www.gnu.org/licenses/>.--module Game.Goatee.Sgf.TreeTest (tests) where--import Data.Version (showVersion)-import Game.Goatee.App (applicationName)-import Game.Goatee.Sgf.Property-import Game.Goatee.Sgf.TestInstances ()-import Game.Goatee.Sgf.Tree-import Game.Goatee.Sgf.Types-import Game.Goatee.Test.Common-import Paths_goatee (version)-import Test.Framework (testGroup)-import Test.Framework.Providers.HUnit (testCase)-import Test.HUnit ((@=?), (@?=))--tests = testGroup "Game.Goatee.Sgf.Tree" [-  emptyNodeTests,-  rootNodeTests,-  findPropertyTests,-  addPropertyTests,-  addChildTests-  ]--emptyNodeTests = testGroup "emptyNode" [-  testCase "has no properties" $ [] @=? nodeProperties emptyNode,-  testCase "has no children" $ [] @=? nodeChildren emptyNode-  ]--rootNodeTests = testGroup "rootNode" [-  testCase "sets SZ correctly" $ do-    assertElem (SZ 9 9) $ nodeProperties $ rootNode $ Just (9, 9)-    assertElem (SZ 19 19) $ nodeProperties $ rootNode $ Just (19, 19)-    assertElem (SZ 9 5) $ nodeProperties $ rootNode $ Just (9, 5),--  testCase "sets AP correctly" $ do-    let ap = AP (toSimpleText applicationName)-                (toSimpleText $ showVersion version)-    assertElem ap $ nodeProperties $ rootNode Nothing-    assertElem ap $ nodeProperties $ rootNode $ Just (9, 9)-  ]--findPropertyTests = testGroup "findProperty" [-  testCase "returns Nothing for an empty node" $-    Nothing @=? findProperty propertyB (mk []),--  testCase "returns Nothing if no properties match" $-    Nothing @=? findProperty propertyB (mk [FF 4, GM 1, ST defaultVariationMode, SZ 9 9]),--  testCase "finds present properties" $ do-     Just (B (Just (2,3))) @=?-       findProperty propertyB (mk [B (Just (2,3))])-     Just (W Nothing) @=?-       findProperty propertyW (mk [B Nothing, W Nothing])-     Just IT @=?-       findProperty propertyIT (mk [IT, GW Double2])-     Just (GW Double2) @=?-       findProperty propertyGW (mk [IT, GW Double2]),--  testCase "doesn't find absent properties" $ do-    Nothing @=? findProperty propertyW (mk [B Nothing])-    Nothing @=? findProperty propertyW (mk [FF 4, GM 1, ST defaultVariationMode, SZ 9 9])-  ]-  where mk properties = emptyNode { nodeProperties = properties }--addPropertyTests = testGroup "addProperty" [-  testCase "adds properties in order" $ do-    let prop1 = B (Just (6,6))-        prop2 = RE GameResultVoid-        prop3 = C (toText "Game over.")-        node1 = addProperty prop1 emptyNode-        node2 = addProperty prop2 node1-        node3 = addProperty prop3 node2-    node1 @?= emptyNode { nodeProperties = [prop1] }-    node2 @?= emptyNode { nodeProperties = [prop1, prop2] }-    node3 @?= emptyNode { nodeProperties = [prop1, prop2, prop3] }-  ]--addChildTests = testGroup "addChild" [-  testCase "adds children in order" $ do-    let node1 = emptyNode { nodeProperties = [B (Just (0,0))] }-        node2 = emptyNode { nodeProperties = [W (Just (1,1))] }-        node3 = emptyNode { nodeProperties = [B (Just (2,2))] }-        node4 = emptyNode { nodeProperties = [W Nothing] }-        node2' = addChild node3 node2-        node1' = addChild node2' node1-        node1'' = addChild node4 node1'-    node1'' @?= mk [B (Just (0,0))]-                   [mk [W (Just (1,1))]-                       [mk [B (Just (2,2))]-                           []],-                    mk [W Nothing]-                       []]-  ]-  where mk properties children = emptyNode { nodeProperties = properties-                                           , nodeChildren = children-                                           }---- TODO Test validateNode.
− tests/Game/Goatee/Sgf/TypesTest.hs
@@ -1,146 +0,0 @@--- This file is part of Goatee.------ Copyright 2014 Bryan Gardiner------ Goatee is free software: you can redistribute it and/or modify--- it under the terms of the GNU Affero General Public License as published by--- the Free Software Foundation, either version 3 of the License, or--- (at your option) any later version.------ Goatee is distributed in the hope that it will be useful,--- but WITHOUT ANY WARRANTY; without even the implied warranty of--- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the--- GNU Affero General Public License for more details.------ You should have received a copy of the GNU Affero General Public License--- along with Goatee.  If not, see <http://www.gnu.org/licenses/>.--module Game.Goatee.Sgf.TypesTest (tests) where--import Data.List (sort)-import Game.Goatee.Sgf.Types-import Game.Goatee.Test.Common-import Test.Framework (testGroup)-import Test.Framework.Providers.HUnit (testCase)-import Test.HUnit ((@=?), Assertion)--tests = testGroup "Game.Goatee.Sgf.Types" [-  expandCoordListTests,-  buildCoordListTests,-  simpleTextTests,-  cnotTests-  ]--expandCoordListTests = testGroup "expandCoordList" [-  testCase "works with an empty CoordList" $ do-    [] @=? expandCoordList (coords [])-    [] @=? expandCoordList (coords' [] []),--  testCase "works for single points" $ do-    [(1,2)] @=? expandCoordList (coord1 (1,2))-    [(3,4), (1,2)] @=? expandCoordList (coords [(3,4), (1,2)])-    let ten = [(i,i) | i <- [1..10]]-    ten @=? expandCoordList (coords ten),--  -- TODO Test that a 1x1 rectangle is rejected.--  testCase "works for a nx1 rect" $-    [(5,2), (5,3), (5,4)] @=? expandCoordList (coords' [] [((5,2), (5,4))]),--  testCase "works for a 1xn rect" $-    [(1,0), (1,1), (1,2), (1,3)] @=? expandCoordList (coords' [] [((1,0), (1,3))]),--  testCase "works for an mxn rect" $-    [(m,n) | m <- [2..5], n <- [3..7]] @=?-    expandCoordList (coords' [] [((2,3), (5,7))]),--  -- TODO Test that x0 > x1 || y0 > y1 is rejected.--  testCase "works with multiple rects" $-    [(0,0), (0,1), (0,2), (3,4), (4,4), (5,4)] @=?-    expandCoordList (coords' [] [((0,0), (0,2)), ((3,4), (5,4))]),--  testCase "concatenates single points and rects" $-    [(1,1), (0,0), (2,2), (2,3), (2,4)] @=?-    expandCoordList (coords' [(1,1), (0,0)] [((2,2), (2,4))])-  ]--buildCoordListTests = testGroup "buildCoordList" [-  testCase "handles the empty case" $ assertSinglesAndRects [] [] [],--  testCase "handles one single" $-    assertSinglesAndRects [(0,1)] [] [(0,1)],--  testCase "handles multiple singles" $-    assertSinglesAndRects [(0,0), (1,1)] [] [(0,0), (1,1)],--  testCase "handles a small rect (1x2)" $-    assertSinglesAndRects [] [((0,1), (0,2))] [(0,1), (0,2)],--  testCase "handles a small square (2x2)" $-    assertSinglesAndRects [] [((1,1), (2,2))] [(x,y) | x <- [1..2], y <- [1..2]],--  testCase "handles a larger rect" $-    assertSinglesAndRects [] [((0,0), (2,4))] [(x,y) | x <- [0..2], y <- [0..4]],--  testCase "handles two rects" $-    assertSinglesAndRects [] [((0,1), (1,2)), ((3,0), (4,1))]-    [(0,1), (1,1), (0,2), (1,2), (3,0), (4,0), (3,1), (4,1)],--  testCase "handles rects and singles together" $-    assertSinglesAndRects [(0,0)] [((1,1), (2,2))]-    [(0,0), (1,1), (1,2), (2,1), (2,2)],--  testCase "handles five points together" $ do-    assertCoords [(1,0), (0,1), (1,1), (0,2), (1,2)]-    assertCoords [(0,0), (1,0), (0,1), (1,1), (0,2)]-    assertCoords [(0,0), (1,0), (2,0), (0,1), (1,1)]-    assertCoords [(1,0), (2,0), (0,1), (1,1), (2,1)],--  testCase "handles a more complex case" $-    assertCoords [(3,0), (4,0),-                  (3,1), (4,1),-                  (0,2), (1,2), (2,2), (3,2), (4,2),-                  (0,3), (1,3),-                  (0,4), (1,4), (3,4), (4,4)],--  testCase "handles a diagonal pattern" $-    assertCoords [(0,0), (2,0), (4,0),-                  (1,1), (3,1), (5,1),-                  (2,2)]-  ]-  where -- | Asserts that the given grid of booleans parses to a 'CoordList'-        -- that has the expected single points (not necessarily in the same-        -- order), as well as the expected rectangles (not necessarily in the-        -- same order).-        assertSinglesAndRects :: [Coord] -> [(Coord, Coord)] -> [Coord] -> Assertion-        assertSinglesAndRects expectedSingles expectedRects input =-          coords' expectedSingles expectedRects @=? buildCoordList input--        -- | Asserts that the set of points returned from applying-        -- 'buildCoordList' to the given grid of booleans is equal to-        -- the given set of points.-        assertCoords :: [Coord] -> Assertion-        assertCoords input = sort input @=? sort (expandCoordList $ buildCoordList input)--simpleTextTests = testGroup "SimpleText" [-  testCase "accepts the empty string" $-    "" @=? fromSimpleText (toSimpleText ""),--  testCase "passes through strings without newlines" $ do-    "Hello." @=? fromSimpleText (toSimpleText "Hello.")-    "Bad for B." @=? fromSimpleText (toSimpleText "Bad for B.")-    "[4k?]" @=? fromSimpleText (toSimpleText "[4k?]")-    printableAsciiChars @=? fromSimpleText (toSimpleText printableAsciiChars),--  testCase "converts newlines to spaces" $ do-    " " @=? fromSimpleText (toSimpleText "\n")-    "Hello, world." @=? fromSimpleText (toSimpleText "Hello,\nworld.")-    "Hello,   world." @=? fromSimpleText (toSimpleText "Hello, \n world.")-    " Hello, world. " @=? fromSimpleText (toSimpleText "\nHello, world.\n")-  ]--cnotTests = testGroup "cnot" [-  testCase "changes Black to White" $ White @=? cnot Black,-  testCase "changes White to Black" $ Black @=? cnot White-  ]
tests/Game/Goatee/Test/Common.hs view
@@ -18,13 +18,18 @@ -- | Common utilities for testing code. module Game.Goatee.Test.Common (   (@/=?),+  (@=?*),+  (@?=*),   assertElem,   printableAsciiChars,   ) where  import Control.Monad (unless, when)-import Test.HUnit (Assertion, assertFailure)+import Data.List (sort)+import Test.HUnit ((@=?), (@?=), Assertion, assertFailure) +infix 1 @/=?, @=?*, @?=*+ -- | Asserts that two values are not equal.  Typically, the left argument is a -- constant value that the right, computed argument should not equal, to mirror -- '@=?'.@@ -37,6 +42,14 @@      then "did not expect: " ++ actual'      else "      this value: " ++ actual' ++           "\nshould not equal: " ++ notExpected'++-- | Order-insensitive version of '@=?'.+(@=?*) :: (Eq a, Ord a, Show a) => [a] -> [a] -> Assertion+xs @=?* ys = sort xs @=? sort ys++-- | Order-insensitive version of '@?='.+(@?=*) :: (Eq a, Ord a, Show a) => [a] -> [a] -> Assertion+xs @?=* ys = sort xs @?= sort ys  assertElem :: (Eq a, Show a) => a -> [a] -> Assertion assertElem item list =
tests/Test.hs view
@@ -17,27 +17,36 @@  module Main (main) where +import qualified Game.Goatee.Common.BigfloatTest import qualified Game.Goatee.CommonTest-import qualified Game.Goatee.Sgf.BoardTest-import qualified Game.Goatee.Sgf.MonadTest-import qualified Game.Goatee.Sgf.ParserTest-import qualified Game.Goatee.Sgf.Property.ParserTest-import qualified Game.Goatee.Sgf.PropertyTest-import qualified Game.Goatee.Sgf.RoundTripTest-import qualified Game.Goatee.Sgf.TreeTest-import qualified Game.Goatee.Sgf.TypesTest-import Test.Framework (defaultMain)+import qualified Game.Goatee.Lib.BoardTest+import qualified Game.Goatee.Lib.MonadTest+import qualified Game.Goatee.Lib.ParserTest+import qualified Game.Goatee.Lib.Property.ParserTest+import qualified Game.Goatee.Lib.PropertyTest+import qualified Game.Goatee.Lib.RoundTripTest+import qualified Game.Goatee.Lib.TreeTest+import qualified Game.Goatee.Lib.TypesTest+import System.Exit (exitFailure, exitSuccess)+import Test.HUnit (Counts (errors, failures), Test (TestList), runTestTT) -tests = [ Game.Goatee.CommonTest.tests-        , Game.Goatee.Sgf.BoardTest.tests-        , Game.Goatee.Sgf.MonadTest.tests-        , Game.Goatee.Sgf.ParserTest.tests-        , Game.Goatee.Sgf.Property.ParserTest.tests-        , Game.Goatee.Sgf.PropertyTest.tests-        , Game.Goatee.Sgf.RoundTripTest.tests-        , Game.Goatee.Sgf.TreeTest.tests-        , Game.Goatee.Sgf.TypesTest.tests-        ]+tests :: Test+tests = TestList+  [ Game.Goatee.Common.BigfloatTest.tests+  , Game.Goatee.CommonTest.tests+  , Game.Goatee.Lib.BoardTest.tests+  , Game.Goatee.Lib.MonadTest.tests+  , Game.Goatee.Lib.ParserTest.tests+  , Game.Goatee.Lib.Property.ParserTest.tests+  , Game.Goatee.Lib.PropertyTest.tests+  , Game.Goatee.Lib.RoundTripTest.tests+  , Game.Goatee.Lib.TreeTest.tests+  , Game.Goatee.Lib.TypesTest.tests+  ]  main :: IO ()-main = defaultMain tests+main = do+  counts <- runTestTT tests+  if errors counts > 0 || failures counts > 0+    then exitFailure+    else exitSuccess