goatee-0.3.1.1: src/Game/Goatee/Lib/Types.hs
-- This file is part of Goatee.
--
-- Copyright 2014-2015 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 #-}
-- | 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, Line (..), lineToPair, LabelList, Mark (..),
GameResult (..),
WinReason (..),
Ruleset (..), RulesetType (..), fromRuleset, toRuleset,
) where
#if !MIN_VERSION_base(4,8,0)
import Control.Applicative ((<$>), (<*>))
#endif
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 (Bounded, Enum, Eq, Ord, Show)
-- | Stone color: black or white.
data Color = Black
| White
deriving (Bounded, Enum, Eq, Ord, 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, Ord, 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 = [Line]
-- | An undirected line between two coordinates.
data Line = Line Coord Coord
deriving (Show)
instance Eq Line where
(Line a b) == (Line c d) = a == c && b == d || a == d && b == c
-- | Converts a 'Line' to a pair of 'Coord's representing the line's endpoints.
lineToPair :: Line -> (Coord, Coord)
lineToPair (Line a b) = (a, b)
-- | 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, Ord, 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, Ord, 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