h2048-0.1.0.1: src/System/Game/H2048/Core.hs
{-|
Module : System.Game.H2048.Core
Copyright : (c) 2014 Javran Cheng
License : MIT
Maintainer : Javran.C@gmail.com
Stability : experimental
Portability : POSIX
The core game logic implementation for Game 2048.
The routine for using this library would be:
1. use `initGameBoard` to get a valid board to begin with.
(two new cells are inserted for you, if you want to use an empty board,
`initBoard` is a shorthand)
2. interact with user / algorithm / etc., use `updateBoard` to update a board.
3. use `insertNewCell` to insert a new cell randomly
4. examine if the player wins / loses / is still alive using `gameState`.
-}
module System.Game.H2048.Core
( Board
, Line
, Dir (..)
, BoardUpdated (..)
, GameState (..)
, gameState
, compactLine
, initBoard
, initGameBoard
, updateBoard
, insertNewCell
, generateNewCell
)
where
import Control.Arrow
import Control.Monad
import Control.Monad.Writer
import Control.Monad.Random
import Data.List
import Data.Maybe
import System.Game.H2048.Utils
-- | represent a 4x4 board for Game 2048
-- each element should be either zero or 2^i
-- where i >= 1.
type Board = [[Int]]
-- | a list of 4 elements, stands for
-- one column / row in the board
type Line = [Int]
-- | result after a successful 'updateBoard'
data BoardUpdated = BoardUpdated
{ brBoard :: Board -- ^ new board
, brScore :: Int -- ^ score collected in this update
} deriving (Eq, Show)
-- | current game state, see also 'gameState'
data GameState = Win
| Lose
| Alive
deriving (Enum, Eq, Show)
-- | move direction
data Dir = DUp
| DDown
| DLeft
| DRight
deriving (Enum, Bounded, Eq, Ord, Show)
-- | the initial board before a game started
initBoard :: Board
initBoard = (replicate 4 . replicate 4) 0
-- | move each non-zero element to their leftmost possible
-- position while preserving the order
compactLine :: Line -> Writer (Sum Int) Line
compactLine = runKleisli
-- remove zeros
( filter (/=0)
-- do merge and collect score
^>> Kleisli merge
-- restore zeros, on the "fst" part
>>^ take 4 . (++ repeat 0))
where
merge :: [Int] -> Writer (Sum Int) [Int]
merge (x:y:xs) =
if x == y
-- only place where score are collected.
then do
-- try to merge first two elements,
-- and process rest of it.
xs' <- merge xs
tell . Sum $ x + y
return $ (x+y) : xs'
else do
-- just skip the first one,
-- and process rest of it.
xs' <- merge (y:xs)
return $ x : xs'
merge r = return r
-- | update the board taking a direction,
-- a 'BoardUpdated' is returned on success,
-- if this update does nothing, that means a failure (Nothing)
updateBoard :: Dir -> Board -> Maybe BoardUpdated
updateBoard d board = if board /= board'
then Just $ BoardUpdated board' (getSum score)
else Nothing
where
board' :: Board
-- transform boards so that
-- we only focus on "gravitize to the left".
-- and convert back after the gravitization is done.
(board',score) = runWriter $
runKleisli
-- transform to a "gravitize to the left" problem
( rTransL
-- gravitize to the left
^>> Kleisli (mapM compactLine)
-- transform back
>>^ rTransR) board
-- rTrans for "a list of reversible transformations, that will be performed in order"
rTrans :: [Board -> Board]
rTrans =
case d of
-- the problem itself is "gravitize to the left"
DLeft -> []
-- we use a mirror
DRight -> [map reverse]
-- diagonal mirror
DUp -> [transpose]
-- same as DUp case + DRight case
DDown -> [transpose, map reverse]
-- how we convert it "into" and "back"
rTransL = foldl (flip (.)) id rTrans
rTransR = foldr (.) id rTrans
-- | find blank cells in a board,
-- return coordinates for each blank cell
blankCells :: Board -> [(Int, Int)]
blankCells b = map (\(row, (col, _)) -> (row,col)) blankCells'
where
blankCells' = filter ((== 0) . snd . snd) linearBoard
-- flatten to make it ready for filter
linearBoard = concat $ zipWith tagRow [0..] colTagged
-- tag cells with row num
tagRow row = map ( (,) row )
-- tag cells with column num
colTagged = map (zip [0..]) b
-- | return current game state.
-- 'Win' if any cell is equal to or greater than 2048
-- or 'Lose' if we can move no further
-- otherwise, 'Alive'.
gameState :: Board -> GameState
gameState b
| any (>= 2048) . concat $ b
= Win
| all (isNothing . ( `updateBoard` b)) universe
= Lose
| otherwise
= Alive
-- | initialize the board by puting two cells randomly
-- into the board.
-- See 'generateNewCell' for the cell generating rule.
initGameBoard :: (MonadRandom r) => r (Board, Int)
initGameBoard =
-- insert two cells and return the resulting board
-- here we can safely assume that the board has at least two empty cells
-- so that we can never have Nothing on the LHS
liftM ( (\x -> (x,0)) . fromJust) (insertNewCell initBoard >>= (insertNewCell . fromJust))
-- | try to insert a new cell randomly
insertNewCell :: (MonadRandom r) => Board -> r (Maybe Board)
insertNewCell b = do
-- get a list of coordinates of blank cells
let availableCells = blankCells b
if null availableCells
-- cannot find any empty cell, then fail
then return Nothing
else do
-- randomly pick up an available cell by choosing index
choice <- getRandomR (0, length availableCells - 1)
let (row,col) = availableCells !! choice
value <- generateNewCell
return $ Just $ (inPos row . inPos col) (const value) b
-- | generate a new cell according to the game rule
-- we have 90% probability of getting a cell of value 2,
-- and 10% probability of getting a cell of value 4.
generateNewCell :: (MonadRandom r) => r Int
generateNewCell = do
r <- getRandom
return $ if r < (0.9 :: Float) then 2 else 4