module PatienceBoard ( Game (..) , Board (..) , amountOfGameStacks , amountOfEndingStacks , gameStacksCoord , endingStacksCoord , pileCoord , handleInputs , initGame , isInGame , isInEnding , isInPile ) where import CardDeck import Selector import InputHandler ----------------------------- Constants ------------------------------ -- Prepresentation of a Patience game data Game = Game { -- The playboard board :: Board, -- The selector selector :: Selector } deriving (Show) -- Representation of a Patience board data Board = Board { -- 7 colums of cards (the 'playing field') gameStacks :: [Stack], -- 4 ending stacks (top right corner, usually) endingStacks :: [Stack], -- Stack of cards that are not yet on the board pile :: Stack } deriving (Show) amountOfGameStacks :: Int amountOfGameStacks = 7 amountOfEndingStacks :: Int amountOfEndingStacks = 4 -- Coordinate of the GameStacks gameStacksCoord :: Coordinate gameStacksCoord = (0, 0) -- Coordinate of the EndingStacks endingStacksCoord :: Coordinate endingStacksCoord = (x, 1) where x = amountOfGameStacks - amountOfEndingStacks -- Coordinate of the Pile pileCoord :: Coordinate pileCoord = (0, 1) -- Step size to rotate the pile of the game rotateStep :: Int rotateStep = 3 ------------------------------- Init --------------------------------- -- Split a full deck into 7 gameStacks and one pile of unused cards. splitDeck :: Stack -> [Stack] splitDeck = reverse . splitDeck' amountOfGameStacks where splitDeck' :: Int -> Stack -> [Stack] splitDeck' 0 cs = [cs] splitDeck' n cs = let (stack,rest) = splitAt n cs in showFirst stack : splitDeck' (n - 1) rest -- The initial board consisting of a stack of yet-to-be-turned cards -- and n stacks of increasingly large amount of cards (1, ..., n) initBoard :: Board initBoard = Board { gameStacks = stacks, endingStacks = replicate amountOfEndingStacks [], pile = map showCard pile } where pile:stacks = splitDeck generateShuffledDeck -- The initial state of the playboard, with a board and a cursor. initGame :: Game initGame = Game { board = initBoard, selector = initSelector } ------------------- Coordinate to Card conversion -------------------- -- Check if a coordinate is in the pile. isInPile :: Coordinate -> Bool isInPile = (pileCoord == ) -- Check if a coordinate is in an endingStack. isInEnding :: Coordinate -> Bool isInEnding (x, y) = leftBound && rightBound && yCheck where leftBound = fst endingStacksCoord <= x rightBound = x < amountOfGameStacks yCheck = y == snd endingStacksCoord -- Check if a coordinate is in a GameStack. isInGame :: Coordinate -> Game -> Bool isInGame (x, y) g = horizontalCheck && verticalCheck where horizontalCheck = leftBound && rightBound verticalCheck = upBound && downBound leftBound = fst gameStacksCoord <= x rightBound = x < amountOfGameStacks upBound = y <= snd gameStacksCoord downBound = negate y < length (gameStacks (board g) !! x) -- Based on a coordinate, return a stack. getStackFromCoord :: Game -> Coordinate -> Stack getStackFromCoord game (x, y) | isInPile (x, y) = pile $ board game | isInEnding (x, y) = endingStacks (board game) !! (x - (amountOfGameStacks - amountOfEndingStacks)) | isInGame (x, y) game = gameStacks (board game) !! x | otherwise = pile $ board game -- Based on a coordinate, return a card. getCardFromCoord :: Game -> Coordinate -> Card getCardFromCoord game (x, y) | isInPile (x, y) = head $ pile (board game) | isInEnding (x, y) = head $ endingStacks (board game) !! (x - (amountOfGameStacks - amountOfEndingStacks)) | isInGame (x, y) game = (gameStacks (board game) !! x ) !! negate y | otherwise = (NoneType, NoneValue, Hidden) --------------------------- Change cards ----------------------------- -- Show the first of a stack of cards. showFirst :: Stack -> Stack showFirst [] = [] showFirst (c:cs) = showCard c : cs -- Rotate the pile n times. rotatePile :: Game -> Game rotatePile g@Game{ board = b } = g{ board = rotatedBoard } where rotatedBoard = b{ pile = tail ++ head } (head, tail) = splitAt rotateStep $ pile b -- Check if a card can be placed ontop of a gameStack. canPlayOn :: Card -> Stack -> Bool canPlayOn (_,King,_) [] = True canPlayOn (t1,v1,_) ((t2,v2,_):cs) = differentColor && predValue where differentColor = t1 /= t2 && fromEnum t1 + fromEnum t2 `elem` [1,2,4,5] predValue = succ v1 == v2 canPlayOn _ _ = False -- Check if a card can be played ontop of an EndingStack. canFinishOn :: Card -> Stack -> Bool canFinishOn (_,Ace,_) [] = True canFinishOn (t1,v1,_) ((t2,v2,_):cs) = sameType && succValue where sameType = t1 == t2 succValue = v1 == succ v2 canFinishOn _ _ = False -- Move a card to a GameStack. Move all the cards below the given card -- on the 'from' stack as well. moveToGS :: Stack -> Int -> Stack -> (Stack,Stack) moveToGS from index to | canPlayOn (from !! index) to = (showFirst removed, added) | otherwise = (from,to) where (diff,removed) = splitAt (index + 1) from added = diff ++ to -- Move a card to an EndingStack. This can only be a single card at once. moveToES :: Stack -> Stack -> (Stack,Stack) moveToES from to | canFinishOn (head from) to = (showFirst removed, added) | otherwise = (from,to) where (diff,removed) = splitAt 1 from added = diff ++ to -- Switch a stack for another stack in a list of stacks. switchStack :: [Stack] -> Int -> Stack -> [Stack] switchStack ss index new = front ++ new:back where (front, back') = splitAt index ss back = tail back' -- Swap a stack in the Game for another stack. updateStack :: Game -> Coordinate -> Stack -> Game updateStack game (x, y) new | isInEnding (x, y) = let originalBoard = board game index = x - (amountOfGameStacks - amountOfEndingStacks) newStack = switchStack (endingStacks originalBoard) index new updatedBoard = originalBoard{ endingStacks = newStack } in game{ board = updatedBoard } | isInPile (x, y) = let originalBoard = board game updatedBoard = originalBoard{ pile = new } in game{ board = updatedBoard } | otherwise = let originalBoard = board game stackNr = x index = negate y newGameStacks = switchStack (gameStacks originalBoard) stackNr new updatedBoard = originalBoard{ gameStacks = newGameStacks } in game{ board = updatedBoard } -- Move a card from Coordinate to Coordinate. moveCard :: Game -> Coordinate -> Coordinate -> Game moveCard game (x, y) (a, b) | isInEnding (a, b) = let fromStack = getStackFromCoord game (x, y) toStack = getStackFromCoord game (a, b) (removed, added) = moveToES fromStack toStack -- Swapping to old stack. applyFirst = updateStack game (x, y) removed -- Swapping the new stack. result = updateStack applyFirst (a, b) added in result | otherwise = game ------------------------------ Input --------------------------------- -- Check if moving in a direction is legal. isLegalMove :: Direction -> Game -> Bool isLegalMove dir g = isInPile coord || isInEnding coord || isInGame coord g where coord = position $ move dir $ selector g -- Move the selector of the game. (Wrapper) moveSelector :: Direction -> Game -> Game moveSelector dir g@Game{ selector = s } | isLegalMove dir g = g{ selector = move dir s } | otherwise = g -- Toggle selector. If a card was already selected, try to move it. toggleSelector :: Game -> Game toggleSelector g@Game{ selector = s@Selector{ selected = Nothing } } = toggled where toggled = g{ selector = toggleSelection s } toggleSelector g@Game{ selector = s@Selector{ selected = Just coord } } = moved where moved = moveCard g{ selector = toggleSelection s } (getSelected s) (position s) -- Handle all the inputs necessary for patience. handleInputs :: Event -> Game -> Game handleInputs = composeInputHandler [ handleUp (moveSelector U), handleDown (moveSelector D), handleLeft (moveSelector L), handleRight (moveSelector R), handleSpace toggleSelector, handleEnter rotatePile ]