├── .gitignore ├── docs ├── runmain.js ├── index.html ├── manifest.webapp ├── out.stats └── css │ └── main.css ├── packages.nix ├── .gitmodules ├── README.md ├── twentyfortyeight.cabal └── src ├── Lib.hs └── Main.hs /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | TAGS 3 | -------------------------------------------------------------------------------- /docs/runmain.js: -------------------------------------------------------------------------------- 1 | h$main(h$mainZCZCMainzimain); 2 | 3 | -------------------------------------------------------------------------------- /packages.nix: -------------------------------------------------------------------------------- 1 | { reflex-platform, ... }: reflex-platform.ghcjs.override { 2 | overrides = self: super: { 3 | reflex-dom-contrib = self.callPackage deps/reflex-dom-contrib {}; 4 | }; 5 | } 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "deps/reflex-dom-contrib"] 2 | path = deps/reflex-dom-contrib 3 | url = https://github.com/reflex-frp/reflex-dom-contrib.git 4 | [submodule "deps/reflex-platform"] 5 | path = deps/reflex-platform 6 | url = https://github.com/reflex-frp/reflex-platform.git 7 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /docs/manifest.webapp: -------------------------------------------------------------------------------- 1 | { 2 | "name": "GHCJS Web Application", 3 | "description": "GHCJS Web Application", 4 | "launch_path": "/index.html", 5 | "icons": { 6 | "16": "/icons/icon16x16.png", 7 | "48": "/icons/icon48x48.png", 8 | "60": "/icons/icon60x60.png", 9 | "128": "/icons/icon128x128.png" 10 | }, 11 | "type": "privileged", 12 | "permissions": {} 13 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | To build this project: 2 | 3 | First clone the repo, then retrieve all the submodules with: 4 | 5 | git submodule update --init --recursive 6 | 7 | Now run: 8 | 9 | deps/reflex-platform/work-on ./packages.nix ./. 10 | 11 | When that is done running, it puts you in a nix-shell with all the 12 | dependencies in place. Then you can build with: 13 | 14 | cabal configure --ghcjs 15 | cabal build 16 | -------------------------------------------------------------------------------- /twentyfortyeight.cabal: -------------------------------------------------------------------------------- 1 | name: twentyfortyeight 2 | version: 0.1 3 | homepage: https://github.com/mightybyte/reflex-2048 4 | license: BSD3 5 | license-file: LICENSE 6 | author: Doug Beardsley 7 | maintainer: mightybyte@gmail.com 8 | category: Web 9 | build-type: Simple 10 | cabal-version: >=1.10 11 | 12 | executable reflex-2048 13 | hs-source-dirs: src 14 | main-is: Main.hs 15 | default-language: Haskell2010 16 | build-depends: base >= 4.7 && < 5 17 | , boxes 18 | , data-default 19 | , ghcjs-dom 20 | , lens 21 | , linear 22 | , mtl 23 | , reflex 24 | , reflex-dom 25 | , reflex-dom-contrib >= 0.4.2 26 | , rng-utils >= 0.2 && < 0.3 27 | , text 28 | , transformers 29 | ghc-options: -Wall 30 | -------------------------------------------------------------------------------- /src/Lib.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE ConstraintKinds #-} 2 | {-# LANGUAGE FlexibleContexts #-} 3 | {-# LANGUAGE RankNTypes #-} 4 | {-# LANGUAGE ScopedTypeVariables #-} 5 | {-# LANGUAGE TypeFamilies #-} 6 | 7 | module Lib 8 | ( App 9 | , AppState(..) 10 | , appMain 11 | ) where 12 | 13 | ------------------------------------------------------------------------------ 14 | import Control.Monad 15 | import Control.Monad.Trans 16 | import Control.Monad.Trans.Reader 17 | import Data.Char 18 | import GHCJS.DOM 19 | import GHCJS.DOM.Document (getElementById, getBody) 20 | import GHCJS.DOM.Element 21 | import GHCJS.DOM.EventM (preventDefault, eventTarget, on) 22 | import GHCJS.DOM.HTMLDocument 23 | import GHCJS.DOM.HTMLElement 24 | import Reflex.Dom hiding (preventDefault, getKeyEvent) 25 | import Reflex.Dom.Contrib.KeyEvent 26 | import Reflex.Dom.Contrib.Utils 27 | ------------------------------------------------------------------------------ 28 | 29 | 30 | type App t m a = ReaderT (AppState t) m a 31 | 32 | 33 | ------------------------------------------------------------------------------ 34 | data AppState t = AppState 35 | { bsKeyDown :: Event t KeyEvent 36 | , bsKeyPress :: Event t KeyEvent 37 | } 38 | 39 | 40 | ------------------------------------------------------------------------------ 41 | getRoot :: HTMLDocument -> String -> IO HTMLElement 42 | getRoot doc appRootId = waitUntilJust $ liftM (fmap castToHTMLElement) $ 43 | getElementById doc appRootId 44 | 45 | 46 | ------------------------------------------------------------------------------ 47 | -- appMain :: String -> (forall t m. MonadWidget t m => App t m ()) -> IO () 48 | appMain appRootId app = runWebGUI $ \webView -> do 49 | doc <- waitUntilJust $ liftM (fmap castToHTMLDocument) $ 50 | webViewGetDomDocument webView 51 | root <- getRoot doc appRootId 52 | body <- waitUntilJust $ getBody doc 53 | attachWidget root (WebViewSingleton webView) $ do 54 | let eventTargetAbsorbsKeys = do 55 | Just t <- liftM (fmap castToHTMLElement) eventTarget 56 | n <- liftIO $ getTagName t 57 | return $ n `elem` [Just "INPUT", Just "SELECT", Just "TEXTAREA"] 58 | _ <- liftIO $ (`on` keyDown) body $ do 59 | ke <- getKeyEvent 60 | absorbs <- eventTargetAbsorbsKeys 61 | when (ke == (key $ chr 8) && not absorbs) preventDefault 62 | let wrapKeypress connectFunc = wrapDomEventMaybe body connectFunc $ do 63 | ke <- getKeyEvent 64 | absorbs <- eventTargetAbsorbsKeys 65 | return $ if absorbs && ke /= (key $ chr 27) -- Let the escape through 66 | then Nothing 67 | else Just ke 68 | kd <- wrapKeypress (`on` keyDown) 69 | kp <- wrapKeypress (`on` keyPress) 70 | runReaderT app $ AppState kd kp 71 | -------------------------------------------------------------------------------- /docs/out.stats: -------------------------------------------------------------------------------- 1 | code size summary per package: 2 | 3 | base: 480309 4 | containers-0.5.7.1-9AY76Rzb1QLJmP8p4wSZRz:101688 5 | dependent-map-0.2.2.0-Aye7JC6J9Q4ASbo6OrJWec:96769 6 | dependent-sum-0.3.2.2-7nS29TvovqDNS76jfcaer:3509 7 | exception-transformers-0.4.0.4-J9YAqZKciXL3MfrHBDw5Iu:10054 8 | ghc-prim: 17619 9 | ghcjs-base-0.2.0.0-LVG5gWl9dFjHHRfyhRxvZP:7977 10 | ghcjs-dom-0.2.3.1-6qX2PLzkmzh2kwPJafhipW:144406 11 | ghcjs-prim-0.1.0.0-IFhsWclGfLr1HfhUvIVylA:12000 12 | integer-gmp: 41609 13 | lens-4.14-Aqr7ePvamMy13LutSdlEKG:2565 14 | linear-1.20.5-AulRBgfjiKbmEZaogosJk:1153 15 | main: 201469 16 | profunctors-5.2-6kuGzRpxkaN1El3K6gyCU2:222 17 | ref-tf-0.4.0.1-3hM7hnw7bOB1FwMAOCGwpN:1300 18 | reflex-0.5.0-Ltuaubz1bCa8R8AxMsLzlx:802414 19 | reflex-dom-0.4-BnDS1gYBEmRCSifNxmgtdQ:569993 20 | text-1.2.2.1-1XjsQ0Vb7BTChTwQbqjN2E:40124 21 | these-0.7.1-6x0ReCW2wzq1xZQOYaHO8P:915 22 | transformers-0.5.2.0-Lvs3wlJMkpyLRFNHJ8pmeB:15493 23 | 24 | 25 | code size per module: 26 | 27 | base 28 | Control.Concurrent.Chan: 4187 29 | Control.Exception.Base: 8663 30 | Control.Monad: 779 31 | Control.Monad.Fix: 584 32 | Control.Monad.IO.Class: 612 33 | Data.Either: 397 34 | Data.Foldable: 1514 35 | Data.Functor.Compose: 103 36 | Data.List.NonEmpty: 20896 37 | Data.Maybe: 2460 38 | Data.Monoid: 1 39 | Data.OldList: 1538 40 | Data.Semigroup: 1922 41 | Data.Traversable: 1642 42 | Data.Tuple: 334 43 | Data.Type.Equality: 287 44 | Data.Typeable: 885 45 | Data.Typeable.Internal: 2378 46 | Debug.Trace: 5072 47 | Foreign.C.Error: 9608 48 | Foreign.C.String: 1568 49 | Foreign.Marshal.Alloc: 375 50 | Foreign.Marshal.Array: 2045 51 | Foreign.Storable: 9757 52 | GHC.Base: 8831 53 | GHC.Char: 768 54 | GHC.Conc.Sync: 8495 55 | GHC.Enum: 3346 56 | GHC.Err: 259 57 | GHC.Exception: 16231 58 | GHC.Fingerprint: 1524 59 | GHC.Fingerprint.Type: 745 60 | GHC.Foreign: 12274 61 | GHC.ForeignPtr: 1314 62 | GHC.IO: 3542 63 | GHC.IO.Buffer: 1276 64 | GHC.IO.BufferedIO: 864 65 | GHC.IO.Device: 1297 66 | GHC.IO.Encoding: 1070 67 | GHC.IO.Encoding.Failure: 726 68 | GHC.IO.Encoding.Types: 1032 69 | GHC.IO.Encoding.UTF8: 60978 70 | GHC.IO.Exception: 24055 71 | GHC.IO.FD: 35801 72 | GHC.IO.Handle: 416 73 | GHC.IO.Handle.FD: 7630 74 | GHC.IO.Handle.Internals: 27738 75 | GHC.IO.Handle.Types: 3571 76 | GHC.IO.Unsafe: 375 77 | GHC.IORef: 166 78 | GHC.Int: 790 79 | GHC.List: 8489 80 | GHC.MVar: 188 81 | GHC.Num: 530 82 | GHC.Ptr: 631 83 | GHC.Read: 12726 84 | GHC.Real: 467 85 | GHC.STRef: 648 86 | GHC.Show: 17529 87 | GHC.Stack.CCS: 4098 88 | GHC.Stack.Types: 1547 89 | GHC.Storable: 1084 90 | GHC.TopHandler: 13352 91 | GHC.Unicode: 1870 92 | GHC.Weak: 188 93 | GHC.Word: 2113 94 | System.Posix.Internals: 15974 95 | Text.ParserCombinators.ReadP: 23943 96 | Text.ParserCombinators.ReadPrec: 1 97 | Text.Read.Lex: 72210 98 | 99 | containers-0.5.7.1-9AY76Rzb1QLJmP8p4wSZRz 100 | Data.IntMap.Base: 26114 101 | Data.IntMap.Strict: 10456 102 | Data.Map.Base: 56696 103 | Data.Tree: 8422 104 | 105 | dependent-map-0.2.2.0-Aye7JC6J9Q4ASbo6OrJWec 106 | Data.Dependent.Map: 40828 107 | Data.Dependent.Map.Internal: 55941 108 | 109 | dependent-sum-0.3.2.2-7nS29TvovqDNS76jfcaer 110 | Data.Dependent.Sum: 751 111 | Data.GADT.Compare: 2116 112 | Data.Some: 642 113 | 114 | exception-transformers-0.4.0.4-J9YAqZKciXL3MfrHBDw5Iu 115 | Control.Monad.Exception: 10054 116 | 117 | ghc-prim 118 | GHC.CString: 4515 119 | GHC.Classes: 9844 120 | GHC.IntWord64: 312 121 | GHC.Tuple: 802 122 | GHC.Types: 2146 123 | 124 | ghcjs-base-0.2.0.0-LVG5gWl9dFjHHRfyhRxvZP 125 | Data.JSString.Text: 1185 126 | GHCJS.Marshal: 4906 127 | GHCJS.Marshal.Internal: 1886 128 | 129 | ghcjs-dom-0.2.3.1-6qX2PLzkmzh2kwPJafhipW 130 | GHCJS.DOM: 5430 131 | GHCJS.DOM.EventM: 2614 132 | GHCJS.DOM.JSFFI.Generated.Document: 3642 133 | GHCJS.DOM.JSFFI.Generated.Element: 35924 134 | GHCJS.DOM.JSFFI.Generated.Event: 859 135 | GHCJS.DOM.JSFFI.Generated.HTMLInputElement:1006 136 | GHCJS.DOM.JSFFI.Generated.HTMLSelectElement:1007 137 | GHCJS.DOM.JSFFI.Generated.HTMLTextAreaElement:1009 138 | GHCJS.DOM.JSFFI.Generated.Node: 5715 139 | GHCJS.DOM.JSFFI.Generated.Window: 1470 140 | GHCJS.DOM.Types: 85730 141 | 142 | ghcjs-prim-0.1.0.0-IFhsWclGfLr1HfhUvIVylA 143 | GHCJS.Prim: 7138 144 | GHCJS.Prim.Internal: 4862 145 | 146 | integer-gmp 147 | GHC.Integer.Type: 41609 148 | 149 | lens-4.14-Aqr7ePvamMy13LutSdlEKG 150 | Control.Lens.Indexed: 2565 151 | 152 | linear-1.20.5-AulRBgfjiKbmEZaogosJk 153 | Linear.V4: 1153 154 | 155 | main 156 | Main: 201469 157 | 158 | profunctors-5.2-6kuGzRpxkaN1El3K6gyCU2 159 | Data.Profunctor.Unsafe: 222 160 | 161 | ref-tf-0.4.0.1-3hM7hnw7bOB1FwMAOCGwpN 162 | Control.Monad.Ref: 1300 163 | 164 | reflex-0.5.0-Ltuaubz1bCa8R8AxMsLzlx 165 | Data.Functor.Misc: 1358 166 | Data.WeakBag: 1618 167 | Reflex.Class: 38191 168 | Reflex.Deletable.Class: 1055 169 | Reflex.Dynamic: 1859 170 | Reflex.Host.Class: 3014 171 | Reflex.PerformEvent.Base: 40252 172 | Reflex.PerformEvent.Class: 2989 173 | Reflex.PostBuild.Class: 34889 174 | Reflex.Spider.Internal: 677189 175 | 176 | reflex-dom-0.4-BnDS1gYBEmRCSifNxmgtdQ 177 | Foreign.JavaScript.TH: 5899 178 | Reflex.Dom.Builder.Class: 27503 179 | Reflex.Dom.Builder.Class.Events: 139203 180 | Reflex.Dom.Builder.Immediate: 286473 181 | Reflex.Dom.Internal: 24952 182 | Reflex.Dom.Widget.Basic: 85963 183 | 184 | text-1.2.2.1-1XjsQ0Vb7BTChTwQbqjN2E 185 | Data.Text: 18803 186 | Data.Text.Array: 1575 187 | Data.Text.Internal: 1712 188 | Data.Text.Internal.Fusion.Common: 1420 189 | Data.Text.Show: 16381 190 | Data.Text.Unsafe: 233 191 | 192 | these-0.7.1-6x0ReCW2wzq1xZQOYaHO8P 193 | Data.These: 915 194 | 195 | transformers-0.5.2.0-Lvs3wlJMkpyLRFNHJ8pmeB 196 | Control.Monad.Trans.Reader: 11396 197 | Control.Monad.Trans.State.Strict: 4097 198 | 199 | 200 | 201 | packed metadata: 513647 202 | 203 | -------------------------------------------------------------------------------- /src/Main.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleContexts #-} 2 | {-# LANGUAGE FlexibleInstances #-} 3 | {-# LANGUAGE LambdaCase #-} 4 | {-# LANGUAGE OverloadedStrings #-} 5 | {-# LANGUAGE RecursiveDo #-} 6 | {-# LANGUAGE ScopedTypeVariables #-} 7 | {-# LANGUAGE TemplateHaskell #-} 8 | {-# LANGUAGE TypeFamilies #-} 9 | {-# LANGUAGE TypeSynonymInstances #-} 10 | {-# LANGUAGE ViewPatterns #-} 11 | 12 | module Main where 13 | 14 | ------------------------------------------------------------------------------ 15 | import Control.Lens 16 | import Control.Monad 17 | import Control.Monad.Reader 18 | import Data.Default 19 | import Data.Foldable 20 | import Data.Maybe 21 | import Data.Monoid 22 | import Data.RNG 23 | import qualified Data.Text as T 24 | import Linear (_w, _x, _y, _z) 25 | import qualified Linear as L 26 | import Reflex 27 | import Reflex.Dom 28 | import Reflex.Dom.Contrib.KeyEvent 29 | import Reflex.Dom.Contrib.Utils 30 | ------------------------------------------------------------------------------ 31 | import Lib 32 | ------------------------------------------------------------------------------ 33 | 34 | 35 | data SquareStatus = SquareMerged | SquareNew 36 | deriving (Eq,Ord,Show,Read) 37 | 38 | data Square = Square 39 | { _squareNumber :: Maybe (Sum Int) 40 | , _squareStatus :: Maybe SquareStatus 41 | } deriving (Eq,Ord,Show,Read) 42 | 43 | makeLenses ''Square 44 | 45 | instance Default Square where 46 | def = Square Nothing Nothing 47 | 48 | mergeSquares :: Square -> Square -> Square 49 | mergeSquares a b = Square (_squareNumber a <> _squareNumber b) (Just SquareMerged) 50 | 51 | data Board = Board 52 | { _boardSquares :: L.M44 Square 53 | , _boardScore :: Int 54 | } deriving (Eq,Ord,Show,Read) 55 | 56 | makeLenses ''Board 57 | 58 | instance Default Board where 59 | def = Board (L.V4 naught naught naught naught) 0 60 | where naught = L.V4 def def def def 61 | 62 | data Move = MoveUp | MoveDown | MoveLeft | MoveRight 63 | deriving (Eq,Ord,Show,Read) 64 | 65 | initBoard :: Board 66 | initBoard = def & boardSquares . _y . _y .~ square2 67 | & boardSquares . _w . _z .~ square2 68 | 69 | square2 :: Square 70 | square2 = Square (Just (Sum 2)) Nothing 71 | 72 | clearStatus :: Board -> Board 73 | clearStatus = boardSquares . each . each . squareStatus .~ Nothing 74 | 75 | updateScore :: Board -> Board 76 | updateScore b = b & boardScore %~ (+newPoints) 77 | where 78 | isMerged s = _squareStatus s == Just SquareMerged 79 | newPoints = 80 | sum $ map (maybe 0 getSum . _squareNumber) 81 | $ filter isMerged $ concat $ toList 82 | $ fmap (toList) $ _boardSquares b 83 | 84 | addNumber :: (Int,Int) -> Board -> Board 85 | addNumber (i,j) b = b & boardSquares . (indToLens i) . (indToLens j) .~ s2 86 | where 87 | s2 = Square (Just (Sum 2)) (Just SquareNew) 88 | indToLens 0 = _x 89 | indToLens 1 = _y 90 | indToLens 2 = _z 91 | indToLens 3 = _w 92 | indToLens _ = error "bad ind" 93 | 94 | 95 | procKeyPress :: (KeyEvent, Double) -> Board -> Board 96 | procKeyPress (ke,r) b = updateScore b3 97 | where 98 | newSquare = emptySquares !! (floor $ r * fromIntegral (length emptySquares)) 99 | b1 = clearStatus b 100 | b2 = next b1 101 | b3 = if b2 == b1 then b1 else addNumber newSquare b2 102 | emptySquares = getEmptyInds b2 103 | next = case keKeyCode ke of 104 | 37 -> mkMove MoveLeft 105 | 38 -> mkMove MoveUp 106 | 39 -> mkMove MoveRight 107 | 40 -> mkMove MoveDown 108 | _ -> id 109 | 110 | addRandomVal :: MonadWidget t m => RNG -> Event t a -> m (Event t (a,Double)) 111 | addRandomVal rng e = performEvent (add <$> e) 112 | where 113 | add a = do 114 | r <- liftIO $ withRNG rng uniform 115 | return (a,r - 2**(-53)) 116 | 117 | gameWidget :: MonadWidget t m => App t m () 118 | gameWidget = do 119 | rng <- liftIO mkRNG 120 | kp <- asks bsKeyDown 121 | kpWithRand <- addRandomVal rng kp 122 | rec b <- foldDyn ($) initBoard $ leftmost 123 | [ procKeyPress <$> kpWithRand 124 | , const initBoard <$ newGame 125 | ] 126 | elAttr "link" ("href" =: "css/main.css" <> 127 | "rel" =: "stylesheet" <> 128 | "type" =: "text/css" 129 | ) $ return () 130 | newGame <- divClass "container" $ do 131 | divClass "heading" $ do 132 | elClass "h1" "title" $ elAttr "a" ("href" =: "/") $ text "2048" 133 | divClass "scores-container" $ do 134 | divClass "score-container" $ do 135 | display $ _boardScore <$> b 136 | divClass "score-addition" $ text "+4" 137 | divClass "best-container" $ text "0" 138 | restart <- divClass "heading" $ do 139 | (buttonEl,_) <- elClass' "a" "restart-button" $ text "New Game" 140 | elClass "h2" "subtitle" $ do 141 | text "Play " 142 | el "strong" $ text "2048 Game" 143 | text " Online" 144 | divClass "above-game" $ el "p" $ do 145 | text "Join the numbers and get to the " 146 | el "strong" $ text "2048 tile!" 147 | return $ domEvent Click buttonEl 148 | divClass "game-container" $ do 149 | divClass "game-message" $ return () 150 | divClass "grid-container" $ do 151 | replicateM_ 4 row 152 | _ <- widgetHoldHelper boardWidget initBoard (updated b) 153 | return () 154 | return restart 155 | return () 156 | return () 157 | where 158 | row = divClass "grid-row" $ replicateM_ 4 cell 159 | cell = divClass "grid-cell" $ return () 160 | 161 | boardWidget :: MonadWidget t m => Board -> m () 162 | boardWidget b = do 163 | divClass "tile-container" $ do 164 | void $ itraverse lineWidget $ toList $ _boardSquares b 165 | 166 | lineWidget :: MonadWidget t m => Int -> L.V4 Square -> m () 167 | lineWidget i a = void $ itraverse (cellWidget i) $ toList a 168 | 169 | cellWidget :: MonadWidget t m => Int -> Int -> Square -> m () 170 | cellWidget i j s = 171 | case _squareNumber s of 172 | Nothing -> return () 173 | Just v -> do 174 | let cls = addNewOrMerged $ T.unwords 175 | [ "tile" 176 | , "tile-" <> tshow (getSum v) 177 | , T.intercalate "-" ["tile-position", tshow (j+1), tshow (i+1)] 178 | ] 179 | divClass cls $ divClass "tile-inner" $ text $ tshow (getSum v) 180 | where 181 | addNewOrMerged = case _squareStatus s of 182 | Nothing -> id 183 | Just SquareMerged -> (<> " tile-merged") 184 | Just SquareNew -> (<> " tile-new") 185 | 186 | mkMove :: Move -> Board -> Board 187 | mkMove MoveUp = over boardSquares (cols %~ mergeLine) 188 | mkMove MoveDown = over boardSquares (locs %~ mergeLine) 189 | mkMove MoveLeft = over boardSquares (rows %~ mergeLine) 190 | mkMove MoveRight = over boardSquares (wors %~ mergeLine) 191 | 192 | instance Reversing (L.V4 a) where 193 | reversing v = L.V4 (v ^. _w) (v ^. _z) (v ^. _y) (v ^. _x) 194 | 195 | mergeLine :: [Square] -> [Square] 196 | mergeLine (x:x':xs) | _squareNumber x == _squareNumber x' = 197 | (mergeSquares x x') : mergeLine xs 198 | mergeLine (x:xs) = x : mergeLine xs 199 | mergeLine [] = [] 200 | 201 | rows, wors, cols, locs :: Traversal' (L.M44 Square) [Square] 202 | rows = traverse . vecList 203 | wors = traverse . reversed . vecList 204 | cols = transposed . rows 205 | locs = transposed . wors 206 | 207 | transposed :: Iso' (L.M44 a) (L.M44 a) 208 | transposed = iso L.transpose L.transpose 209 | 210 | isSquareEmpty :: Square -> Bool 211 | isSquareEmpty (Square Nothing _) = True 212 | isSquareEmpty _ = False 213 | 214 | vecList :: Iso' (L.V4 Square) [Square] 215 | vecList = iso vecToList vecFromList 216 | 217 | vecToList :: L.V4 Square -> [Square] 218 | vecToList v = reverse $ filter (not . isSquareEmpty) $ foldl (flip (:)) [] v 219 | 220 | vecFromList :: [Square] -> L.V4 Square 221 | vecFromList xs = L.V4 (mk $ xs^?ix 0) (mk $ xs^?ix 1) 222 | (mk $ xs^?ix 2) (mk $ xs^?ix 3) 223 | where 224 | mk :: Maybe Square -> Square 225 | mk = fromMaybe def 226 | 227 | getEmptyInds :: Board -> [(Int, Int)] 228 | getEmptyInds = 229 | map dropLast . filter isEmpty . concat . imap f . toList . fmap (toList) 230 | . _boardSquares 231 | where 232 | f i = imap (\j a -> (i,j,a)) 233 | isEmpty (_,_,s) = isSquareEmpty s 234 | dropLast (a,b,_) = (a,b) 235 | 236 | main :: IO () 237 | main = appMain "approot" gameWidget 238 | -------------------------------------------------------------------------------- /docs/css/main.css: -------------------------------------------------------------------------------- 1 | @import url(fonts/clear-sans.css); 2 | html, body { 3 | margin: 0; 4 | padding: 0; 5 | background: #faf8ef; 6 | color: #776e65; 7 | font-family: "Clear Sans", "Helvetica Neue", Arial, sans-serif; 8 | font-size: 18px; } 9 | 10 | body { 11 | margin: 80px 0; } 12 | 13 | .heading:after { 14 | content: ""; 15 | display: block; 16 | clear: both; } 17 | 18 | h1.title { 19 | font-size: 80px; 20 | font-weight: bold; 21 | margin: 0; 22 | display: block; 23 | float: left; } 24 | h1.title a { 25 | text-decoration: none; } 26 | 27 | @-webkit-keyframes move-up { 28 | 0% { 29 | top: 25px; 30 | opacity: 1; } 31 | 32 | 100% { 33 | top: -50px; 34 | opacity: 0; } } 35 | 36 | @-moz-keyframes move-up { 37 | 0% { 38 | top: 25px; 39 | opacity: 1; } 40 | 41 | 100% { 42 | top: -50px; 43 | opacity: 0; } } 44 | 45 | @keyframes move-up { 46 | 0% { 47 | top: 25px; 48 | opacity: 1; } 49 | 50 | 100% { 51 | top: -50px; 52 | opacity: 0; } } 53 | 54 | .scores-container { 55 | float: right; 56 | text-align: right; } 57 | 58 | .score-container, .best-container { 59 | position: relative; 60 | display: inline-block; 61 | background: #bbada0; 62 | padding: 15px 25px; 63 | font-size: 25px; 64 | height: 25px; 65 | line-height: 47px; 66 | font-weight: bold; 67 | border-radius: 3px; 68 | color: white; 69 | margin-top: 8px; 70 | text-align: center; } 71 | .score-container:after, .best-container:after { 72 | position: absolute; 73 | width: 100%; 74 | top: 10px; 75 | left: 0; 76 | text-transform: uppercase; 77 | font-size: 13px; 78 | line-height: 13px; 79 | text-align: center; 80 | color: #eee4da; } 81 | .score-container .score-addition, .best-container .score-addition { 82 | position: absolute; 83 | right: 30px; 84 | color: red; 85 | font-size: 25px; 86 | line-height: 25px; 87 | font-weight: bold; 88 | color: rgba(119, 110, 101, 0.9); 89 | z-index: 100; 90 | -webkit-animation: move-up 600ms ease-in; 91 | -moz-animation: move-up 600ms ease-in; 92 | animation: move-up 600ms ease-in; 93 | -webkit-animation-fill-mode: both; 94 | -moz-animation-fill-mode: both; 95 | animation-fill-mode: both; } 96 | 97 | .score-container:after { 98 | content: "Score"; } 99 | 100 | .best-container:after { 101 | content: "Best"; } 102 | 103 | p { 104 | margin-top: 0; 105 | margin-bottom: 10px; 106 | line-height: 1.65; } 107 | 108 | a { 109 | color: #776e65; 110 | font-weight: bold; 111 | text-decoration: underline; 112 | cursor: pointer; } 113 | 114 | strong.important { 115 | text-transform: uppercase; } 116 | 117 | hr { 118 | border: none; 119 | border-bottom: 1px solid #d8d4d0; 120 | margin-top: 20px; 121 | margin-bottom: 30px; } 122 | 123 | .container { 124 | width: 500px; 125 | margin: 0 auto; } 126 | 127 | @-webkit-keyframes fade-in { 128 | 0% { 129 | opacity: 0; } 130 | 131 | 100% { 132 | opacity: 1; } } 133 | 134 | @-moz-keyframes fade-in { 135 | 0% { 136 | opacity: 0; } 137 | 138 | 100% { 139 | opacity: 1; } } 140 | 141 | @keyframes fade-in { 142 | 0% { 143 | opacity: 0; } 144 | 145 | 100% { 146 | opacity: 1; } } 147 | 148 | .game-container { 149 | margin-top: 40px; 150 | position: relative; 151 | padding: 15px; 152 | cursor: default; 153 | -webkit-touch-callout: none; 154 | -ms-touch-callout: none; 155 | -webkit-user-select: none; 156 | -moz-user-select: none; 157 | -ms-user-select: none; 158 | -ms-touch-action: none; 159 | touch-action: none; 160 | background: #bbada0; 161 | border-radius: 6px; 162 | width: 500px; 163 | height: 500px; 164 | -webkit-box-sizing: border-box; 165 | -moz-box-sizing: border-box; 166 | box-sizing: border-box; } 167 | .game-container .game-message { 168 | display: none; 169 | position: absolute; 170 | top: 0; 171 | right: 0; 172 | bottom: 0; 173 | left: 0; 174 | background: rgba(238, 228, 218, 0.5); 175 | z-index: 100; 176 | text-align: center; 177 | -webkit-animation: fade-in 800ms ease 1200ms; 178 | -moz-animation: fade-in 800ms ease 1200ms; 179 | animation: fade-in 800ms ease 1200ms; 180 | -webkit-animation-fill-mode: both; 181 | -moz-animation-fill-mode: both; 182 | animation-fill-mode: both; } 183 | .game-container .game-message p { 184 | font-size: 60px; 185 | font-weight: bold; 186 | height: 60px; 187 | line-height: 60px; 188 | margin-top: 222px; } 189 | .game-container .game-message .lower { 190 | display: block; 191 | margin-top: 59px; } 192 | .game-container .game-message a { 193 | display: inline-block; 194 | background: #8f7a66; 195 | border-radius: 3px; 196 | padding: 0 20px; 197 | text-decoration: none; 198 | color: #f9f6f2; 199 | height: 40px; 200 | line-height: 42px; 201 | margin-left: 9px; } 202 | .game-container .game-message a.keep-playing-button { 203 | display: none; } 204 | .game-container .game-message.game-won { 205 | background: rgba(237, 194, 46, 0.5); 206 | color: #f9f6f2; } 207 | .game-container .game-message.game-won a.keep-playing-button { 208 | display: inline-block; } 209 | .game-container .game-message.game-won, .game-container .game-message.game-over { 210 | display: block; } 211 | 212 | .grid-container { 213 | position: absolute; 214 | z-index: 1; } 215 | 216 | .grid-row { 217 | margin-bottom: 15px; } 218 | .grid-row:last-child { 219 | margin-bottom: 0; } 220 | .grid-row:after { 221 | content: ""; 222 | display: block; 223 | clear: both; } 224 | 225 | .grid-cell { 226 | width: 106.25px; 227 | height: 106.25px; 228 | margin-right: 15px; 229 | float: left; 230 | border-radius: 3px; 231 | background: rgba(238, 228, 218, 0.35); } 232 | .grid-cell:last-child { 233 | margin-right: 0; } 234 | 235 | .tile-container { 236 | position: absolute; 237 | z-index: 2; } 238 | 239 | .tile, .tile .tile-inner { 240 | width: 107px; 241 | height: 107px; 242 | line-height: 116.25px; } 243 | .tile.tile-position-1-1 { 244 | -webkit-transform: translate(0px, 0px); 245 | -moz-transform: translate(0px, 0px); 246 | transform: translate(0px, 0px); } 247 | .tile.tile-position-1-2 { 248 | -webkit-transform: translate(0px, 121px); 249 | -moz-transform: translate(0px, 121px); 250 | transform: translate(0px, 121px); } 251 | .tile.tile-position-1-3 { 252 | -webkit-transform: translate(0px, 242px); 253 | -moz-transform: translate(0px, 242px); 254 | transform: translate(0px, 242px); } 255 | .tile.tile-position-1-4 { 256 | -webkit-transform: translate(0px, 363px); 257 | -moz-transform: translate(0px, 363px); 258 | transform: translate(0px, 363px); } 259 | .tile.tile-position-2-1 { 260 | -webkit-transform: translate(121px, 0px); 261 | -moz-transform: translate(121px, 0px); 262 | transform: translate(121px, 0px); } 263 | .tile.tile-position-2-2 { 264 | -webkit-transform: translate(121px, 121px); 265 | -moz-transform: translate(121px, 121px); 266 | transform: translate(121px, 121px); } 267 | .tile.tile-position-2-3 { 268 | -webkit-transform: translate(121px, 242px); 269 | -moz-transform: translate(121px, 242px); 270 | transform: translate(121px, 242px); } 271 | .tile.tile-position-2-4 { 272 | -webkit-transform: translate(121px, 363px); 273 | -moz-transform: translate(121px, 363px); 274 | transform: translate(121px, 363px); } 275 | .tile.tile-position-3-1 { 276 | -webkit-transform: translate(242px, 0px); 277 | -moz-transform: translate(242px, 0px); 278 | transform: translate(242px, 0px); } 279 | .tile.tile-position-3-2 { 280 | -webkit-transform: translate(242px, 121px); 281 | -moz-transform: translate(242px, 121px); 282 | transform: translate(242px, 121px); } 283 | .tile.tile-position-3-3 { 284 | -webkit-transform: translate(242px, 242px); 285 | -moz-transform: translate(242px, 242px); 286 | transform: translate(242px, 242px); } 287 | .tile.tile-position-3-4 { 288 | -webkit-transform: translate(242px, 363px); 289 | -moz-transform: translate(242px, 363px); 290 | transform: translate(242px, 363px); } 291 | .tile.tile-position-4-1 { 292 | -webkit-transform: translate(363px, 0px); 293 | -moz-transform: translate(363px, 0px); 294 | transform: translate(363px, 0px); } 295 | .tile.tile-position-4-2 { 296 | -webkit-transform: translate(363px, 121px); 297 | -moz-transform: translate(363px, 121px); 298 | transform: translate(363px, 121px); } 299 | .tile.tile-position-4-3 { 300 | -webkit-transform: translate(363px, 242px); 301 | -moz-transform: translate(363px, 242px); 302 | transform: translate(363px, 242px); } 303 | .tile.tile-position-4-4 { 304 | -webkit-transform: translate(363px, 363px); 305 | -moz-transform: translate(363px, 363px); 306 | transform: translate(363px, 363px); } 307 | 308 | .tile { 309 | position: absolute; 310 | -webkit-transition: 100ms ease-in-out; 311 | -moz-transition: 100ms ease-in-out; 312 | transition: 100ms ease-in-out; 313 | -webkit-transition-property: -webkit-transform; 314 | -moz-transition-property: -moz-transform; 315 | transition-property: transform; } 316 | .tile .tile-inner { 317 | border-radius: 3px; 318 | background: #eee4da; 319 | text-align: center; 320 | font-weight: bold; 321 | z-index: 10; 322 | font-size: 55px; } 323 | .tile.tile-2 .tile-inner { 324 | background: #eee4da; 325 | box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0), inset 0 0 0 1px rgba(255, 255, 255, 0); } 326 | .tile.tile-4 .tile-inner { 327 | background: #ede0c8; 328 | box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0), inset 0 0 0 1px rgba(255, 255, 255, 0); } 329 | .tile.tile-8 .tile-inner { 330 | color: #f9f6f2; 331 | background: #f2b179; } 332 | .tile.tile-16 .tile-inner { 333 | color: #f9f6f2; 334 | background: #f59563; } 335 | .tile.tile-32 .tile-inner { 336 | color: #f9f6f2; 337 | background: #f67c5f; } 338 | .tile.tile-64 .tile-inner { 339 | color: #f9f6f2; 340 | background: #f65e3b; } 341 | .tile.tile-128 .tile-inner { 342 | color: #f9f6f2; 343 | background: #edcf72; 344 | box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0.2381), inset 0 0 0 1px rgba(255, 255, 255, 0.14286); 345 | font-size: 45px; } 346 | @media screen and (max-width: 520px) { 347 | .tile.tile-128 .tile-inner { 348 | font-size: 25px; } } 349 | .tile.tile-256 .tile-inner { 350 | color: #f9f6f2; 351 | background: #edcc61; 352 | box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0.31746), inset 0 0 0 1px rgba(255, 255, 255, 0.19048); 353 | font-size: 45px; } 354 | @media screen and (max-width: 520px) { 355 | .tile.tile-256 .tile-inner { 356 | font-size: 25px; } } 357 | .tile.tile-512 .tile-inner { 358 | color: #f9f6f2; 359 | background: #edc850; 360 | box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0.39683), inset 0 0 0 1px rgba(255, 255, 255, 0.2381); 361 | font-size: 45px; } 362 | @media screen and (max-width: 520px) { 363 | .tile.tile-512 .tile-inner { 364 | font-size: 25px; } } 365 | .tile.tile-1024 .tile-inner { 366 | color: #f9f6f2; 367 | background: #edc53f; 368 | box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0.47619), inset 0 0 0 1px rgba(255, 255, 255, 0.28571); 369 | font-size: 35px; } 370 | @media screen and (max-width: 520px) { 371 | .tile.tile-1024 .tile-inner { 372 | font-size: 15px; } } 373 | .tile.tile-2048 .tile-inner { 374 | color: #f9f6f2; 375 | background: #edc22e; 376 | box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0.55556), inset 0 0 0 1px rgba(255, 255, 255, 0.33333); 377 | font-size: 35px; } 378 | @media screen and (max-width: 520px) { 379 | .tile.tile-2048 .tile-inner { 380 | font-size: 15px; } } 381 | .tile.tile-super .tile-inner { 382 | color: #f9f6f2; 383 | background: #3c3a32; 384 | font-size: 30px; } 385 | @media screen and (max-width: 520px) { 386 | .tile.tile-super .tile-inner { 387 | font-size: 10px; } } 388 | 389 | @-webkit-keyframes appear { 390 | 0% { 391 | opacity: 0; 392 | -webkit-transform: scale(0); 393 | -moz-transform: scale(0); 394 | transform: scale(0); } 395 | 396 | 100% { 397 | opacity: 1; 398 | -webkit-transform: scale(1); 399 | -moz-transform: scale(1); 400 | transform: scale(1); } } 401 | 402 | @-moz-keyframes appear { 403 | 0% { 404 | opacity: 0; 405 | -webkit-transform: scale(0); 406 | -moz-transform: scale(0); 407 | transform: scale(0); } 408 | 409 | 100% { 410 | opacity: 1; 411 | -webkit-transform: scale(1); 412 | -moz-transform: scale(1); 413 | transform: scale(1); } } 414 | 415 | @keyframes appear { 416 | 0% { 417 | opacity: 0; 418 | -webkit-transform: scale(0); 419 | -moz-transform: scale(0); 420 | transform: scale(0); } 421 | 422 | 100% { 423 | opacity: 1; 424 | -webkit-transform: scale(1); 425 | -moz-transform: scale(1); 426 | transform: scale(1); } } 427 | 428 | .tile-new .tile-inner { 429 | -webkit-animation: appear 200ms ease 100ms; 430 | -moz-animation: appear 200ms ease 100ms; 431 | animation: appear 200ms ease 100ms; 432 | -webkit-animation-fill-mode: backwards; 433 | -moz-animation-fill-mode: backwards; 434 | animation-fill-mode: backwards; } 435 | 436 | @-webkit-keyframes pop { 437 | 0% { 438 | -webkit-transform: scale(0); 439 | -moz-transform: scale(0); 440 | transform: scale(0); } 441 | 442 | 50% { 443 | -webkit-transform: scale(1.2); 444 | -moz-transform: scale(1.2); 445 | transform: scale(1.2); } 446 | 447 | 100% { 448 | -webkit-transform: scale(1); 449 | -moz-transform: scale(1); 450 | transform: scale(1); } } 451 | 452 | @-moz-keyframes pop { 453 | 0% { 454 | -webkit-transform: scale(0); 455 | -moz-transform: scale(0); 456 | transform: scale(0); } 457 | 458 | 50% { 459 | -webkit-transform: scale(1.2); 460 | -moz-transform: scale(1.2); 461 | transform: scale(1.2); } 462 | 463 | 100% { 464 | -webkit-transform: scale(1); 465 | -moz-transform: scale(1); 466 | transform: scale(1); } } 467 | 468 | @keyframes pop { 469 | 0% { 470 | -webkit-transform: scale(0); 471 | -moz-transform: scale(0); 472 | transform: scale(0); } 473 | 474 | 50% { 475 | -webkit-transform: scale(1.2); 476 | -moz-transform: scale(1.2); 477 | transform: scale(1.2); } 478 | 479 | 100% { 480 | -webkit-transform: scale(1); 481 | -moz-transform: scale(1); 482 | transform: scale(1); } } 483 | 484 | .tile-merged .tile-inner { 485 | z-index: 20; 486 | -webkit-animation: pop 200ms ease 100ms; 487 | -moz-animation: pop 200ms ease 100ms; 488 | animation: pop 200ms ease 100ms; 489 | -webkit-animation-fill-mode: backwards; 490 | -moz-animation-fill-mode: backwards; 491 | animation-fill-mode: backwards; } 492 | 493 | .above-game:after { 494 | content: ""; 495 | display: block; 496 | clear: both; } 497 | 498 | .game-intro { 499 | float: left; 500 | line-height: 42px; 501 | margin-bottom: 0; } 502 | 503 | .restart-button { 504 | display: inline-block; 505 | background: #8f7a66; 506 | border-radius: 3px; 507 | padding: 0 20px; 508 | text-decoration: none; 509 | color: #f9f6f2; 510 | height: 40px; 511 | line-height: 42px; 512 | display: block; 513 | text-align: center; 514 | float: right; } 515 | 516 | .game-explanation { 517 | margin-top: 50px; } 518 | 519 | @media screen and (max-width: 520px) { 520 | html, body { 521 | font-size: 15px; } 522 | 523 | body { 524 | margin: 20px 0; 525 | padding: 0 20px; } 526 | 527 | h1.title { 528 | font-size: 27px; 529 | margin-top: 15px; } 530 | 531 | .container { 532 | width: 280px; 533 | margin: 0 auto; } 534 | 535 | .score-container, .best-container { 536 | margin-top: 0; 537 | padding: 15px 10px; 538 | min-width: 40px; } 539 | 540 | .heading { 541 | margin-bottom: 10px; } 542 | 543 | .game-intro { 544 | width: 55%; 545 | display: block; 546 | box-sizing: border-box; 547 | line-height: 1.65; } 548 | 549 | .restart-button { 550 | width: 42%; 551 | padding: 0; 552 | display: block; 553 | box-sizing: border-box; 554 | margin-top: 2px; } 555 | 556 | .game-container { 557 | margin-top: 17px; 558 | position: relative; 559 | padding: 10px; 560 | cursor: default; 561 | -webkit-touch-callout: none; 562 | -ms-touch-callout: none; 563 | -webkit-user-select: none; 564 | -moz-user-select: none; 565 | -ms-user-select: none; 566 | -ms-touch-action: none; 567 | touch-action: none; 568 | background: #bbada0; 569 | border-radius: 6px; 570 | width: 280px; 571 | height: 280px; 572 | -webkit-box-sizing: border-box; 573 | -moz-box-sizing: border-box; 574 | box-sizing: border-box; } 575 | .game-container .game-message { 576 | display: none; 577 | position: absolute; 578 | top: 0; 579 | right: 0; 580 | bottom: 0; 581 | left: 0; 582 | background: rgba(238, 228, 218, 0.5); 583 | z-index: 100; 584 | text-align: center; 585 | -webkit-animation: fade-in 800ms ease 1200ms; 586 | -moz-animation: fade-in 800ms ease 1200ms; 587 | animation: fade-in 800ms ease 1200ms; 588 | -webkit-animation-fill-mode: both; 589 | -moz-animation-fill-mode: both; 590 | animation-fill-mode: both; } 591 | .game-container .game-message p { 592 | font-size: 60px; 593 | font-weight: bold; 594 | height: 60px; 595 | line-height: 60px; 596 | margin-top: 222px; } 597 | .game-container .game-message .lower { 598 | display: block; 599 | margin-top: 59px; } 600 | .game-container .game-message a { 601 | display: inline-block; 602 | background: #8f7a66; 603 | border-radius: 3px; 604 | padding: 0 20px; 605 | text-decoration: none; 606 | color: #f9f6f2; 607 | height: 40px; 608 | line-height: 42px; 609 | margin-left: 9px; } 610 | .game-container .game-message a.keep-playing-button { 611 | display: none; } 612 | .game-container .game-message.game-won { 613 | background: rgba(237, 194, 46, 0.5); 614 | color: #f9f6f2; } 615 | .game-container .game-message.game-won a.keep-playing-button { 616 | display: inline-block; } 617 | .game-container .game-message.game-won, .game-container .game-message.game-over { 618 | display: block; } 619 | 620 | .grid-container { 621 | position: absolute; 622 | z-index: 1; } 623 | 624 | .grid-row { 625 | margin-bottom: 10px; } 626 | .grid-row:last-child { 627 | margin-bottom: 0; } 628 | .grid-row:after { 629 | content: ""; 630 | display: block; 631 | clear: both; } 632 | 633 | .grid-cell { 634 | width: 57.5px; 635 | height: 57.5px; 636 | margin-right: 10px; 637 | float: left; 638 | border-radius: 3px; 639 | background: rgba(238, 228, 218, 0.35); } 640 | .grid-cell:last-child { 641 | margin-right: 0; } 642 | 643 | .tile-container { 644 | position: absolute; 645 | z-index: 2; } 646 | 647 | .tile, .tile .tile-inner { 648 | width: 58px; 649 | height: 58px; 650 | line-height: 67.5px; } 651 | .tile.tile-position-1-1 { 652 | -webkit-transform: translate(0px, 0px); 653 | -moz-transform: translate(0px, 0px); 654 | transform: translate(0px, 0px); } 655 | .tile.tile-position-1-2 { 656 | -webkit-transform: translate(0px, 67px); 657 | -moz-transform: translate(0px, 67px); 658 | transform: translate(0px, 67px); } 659 | .tile.tile-position-1-3 { 660 | -webkit-transform: translate(0px, 135px); 661 | -moz-transform: translate(0px, 135px); 662 | transform: translate(0px, 135px); } 663 | .tile.tile-position-1-4 { 664 | -webkit-transform: translate(0px, 202px); 665 | -moz-transform: translate(0px, 202px); 666 | transform: translate(0px, 202px); } 667 | .tile.tile-position-2-1 { 668 | -webkit-transform: translate(67px, 0px); 669 | -moz-transform: translate(67px, 0px); 670 | transform: translate(67px, 0px); } 671 | .tile.tile-position-2-2 { 672 | -webkit-transform: translate(67px, 67px); 673 | -moz-transform: translate(67px, 67px); 674 | transform: translate(67px, 67px); } 675 | .tile.tile-position-2-3 { 676 | -webkit-transform: translate(67px, 135px); 677 | -moz-transform: translate(67px, 135px); 678 | transform: translate(67px, 135px); } 679 | .tile.tile-position-2-4 { 680 | -webkit-transform: translate(67px, 202px); 681 | -moz-transform: translate(67px, 202px); 682 | transform: translate(67px, 202px); } 683 | .tile.tile-position-3-1 { 684 | -webkit-transform: translate(135px, 0px); 685 | -moz-transform: translate(135px, 0px); 686 | transform: translate(135px, 0px); } 687 | .tile.tile-position-3-2 { 688 | -webkit-transform: translate(135px, 67px); 689 | -moz-transform: translate(135px, 67px); 690 | transform: translate(135px, 67px); } 691 | .tile.tile-position-3-3 { 692 | -webkit-transform: translate(135px, 135px); 693 | -moz-transform: translate(135px, 135px); 694 | transform: translate(135px, 135px); } 695 | .tile.tile-position-3-4 { 696 | -webkit-transform: translate(135px, 202px); 697 | -moz-transform: translate(135px, 202px); 698 | transform: translate(135px, 202px); } 699 | .tile.tile-position-4-1 { 700 | -webkit-transform: translate(202px, 0px); 701 | -moz-transform: translate(202px, 0px); 702 | transform: translate(202px, 0px); } 703 | .tile.tile-position-4-2 { 704 | -webkit-transform: translate(202px, 67px); 705 | -moz-transform: translate(202px, 67px); 706 | transform: translate(202px, 67px); } 707 | .tile.tile-position-4-3 { 708 | -webkit-transform: translate(202px, 135px); 709 | -moz-transform: translate(202px, 135px); 710 | transform: translate(202px, 135px); } 711 | .tile.tile-position-4-4 { 712 | -webkit-transform: translate(202px, 202px); 713 | -moz-transform: translate(202px, 202px); 714 | transform: translate(202px, 202px); } 715 | 716 | .tile .tile-inner { 717 | font-size: 35px; } 718 | 719 | .game-message p { 720 | font-size: 30px !important; 721 | height: 30px !important; 722 | line-height: 30px !important; 723 | margin-top: 90px !important; } 724 | .game-message .lower { 725 | margin-top: 30px !important; } } 726 | .right { 727 | float: right; } 728 | 729 | .left { 730 | float: left; } 731 | 732 | .clearfix { 733 | clear: both; } 734 | 735 | .play-now { 736 | margin-top: 22px; } 737 | .play-now a { 738 | border-radius: 6px; 739 | padding: 12px; 740 | text-decoration: none; 741 | background-color: #bbada0; 742 | color: white; } 743 | .play-now a:hover { 744 | text-decoration: underline; } 745 | 746 | body { 747 | margin: 30px 0; 748 | margin-top: 0px; 749 | margin-bottom: 0px; 750 | padding-top: 15px; 751 | position: relative; } 752 | 753 | footer { 754 | padding-bottom: 30px; } 755 | 756 | p { 757 | line-height: 1.45; } 758 | 759 | .heading { 760 | margin-bottom: 20px; } 761 | 762 | .side-column { 763 | background-image: url(/img/pattern/amam.png); 764 | background-position: 0px 0px; 765 | background-repeat: repeat-y; 766 | width: 180px; 767 | height: 100%; 768 | position: absolute; 769 | top: 0; 770 | bottom: 0; } 771 | 772 | .side-column-left { 773 | left: 0; 774 | border-right: 1px solid white; } 775 | 776 | .side-column-right { 777 | right: 0; 778 | border-left: 1px solid white; 779 | background-position: -19px 0px; } 780 | 781 | .side-ad { 782 | margin-top: 360px; 783 | margin-left: 10px; } 784 | @media (min-height: 769px) { 785 | .side-ad { 786 | margin-top: 400px; } } 787 | .side-ad .sponsored { 788 | text-transform: uppercase; 789 | font-size: 10px; } 790 | 791 | .game-container { 792 | margin-top: 25px; } 793 | 794 | .grammarly-box { 795 | padding: 20px; 796 | background: #fff; 797 | margin-bottom: 30px; 798 | border: solid 1px #eee; 799 | border-top: solid 2px #F08150; } 800 | .grammarly-box .caption { 801 | font-size: 18px; 802 | line-height: 25px; 803 | margin-bottom: 10px; 804 | display: inline-block; } 805 | .grammarly-box p { 806 | margin-bottom: 0; 807 | line-height: 1.3; 808 | font-size: 14px; } 809 | 810 | .bold { 811 | font-weight: bold; } 812 | 813 | .box-shadow { 814 | background: #fff; 815 | position: relative; } 816 | 817 | .box-shadow:after, 818 | .box-shadow:before { 819 | top: 80%; 820 | left: 5px; 821 | width: 50%; 822 | z-index: -1; 823 | content: ""; 824 | bottom: 15px; 825 | max-width: 300px; 826 | background: #999; 827 | position: absolute; } 828 | 829 | .shadow-effect { 830 | position: relative; } 831 | 832 | .shadow-effect:after, .shadow-effect:before { 833 | transform: rotate(-3deg); 834 | -o-transform: rotate(-3deg); 835 | -ms-transform: rotate(-3deg); 836 | -moz-transform: rotate(-3deg); 837 | -webkit-transform: rotate(-3deg); 838 | box-shadow: 0 15px 10px #999; 839 | -moz-box-shadow: 0 15px 10px #999; 840 | -webkit-box-shadow: 0 15px 10px #999; } 841 | 842 | .shadow-effect:after { 843 | left: auto; 844 | right: 5px; 845 | transform: rotate(3deg); 846 | -o-transform: rotate(3deg); 847 | -ms-transform: rotate(3deg); 848 | -moz-transform: rotate(3deg); 849 | -webkit-transform: rotate(3deg); } 850 | 851 | .top-link-ads-wrapper { 852 | border-bottom: 1px solid #D7C7B9; 853 | padding: 4px; 854 | margin-bottom: 15px; } 855 | 856 | .top-link-ads { 857 | height: 15px; } 858 | 859 | .text-center { 860 | text-align: center; } 861 | 862 | h2.subtitle { 863 | font-size: 1em; 864 | margin: 0; } 865 | 866 | .game-intro { 867 | line-height: inherit; 868 | float: none; 869 | width: 100% !important; } 870 | 871 | .game-explanation { 872 | margin-top: 0px; } 873 | 874 | .focus-line { 875 | background-color: #ffc; 876 | text-align: center; 877 | padding: 5px 0; } 878 | 879 | .ads-button { 880 | margin-top: 15px; 881 | margin-bottom: 10px; } 882 | 883 | .fb-line { 884 | margin-bottom: 20px; 885 | float: right; 886 | width: 160px; } 887 | .fb-line .game-explanation { 888 | font-size: 14px; 889 | margin-bottom: 20px; 890 | text-align: justify; } 891 | .fb-line p { 892 | margin-bottom: 0px; 893 | float: none; } 894 | .fb-line .fb-like-wrapper { 895 | height: 30px; } 896 | 897 | .ads-bellow-game { 898 | margin-bottom: 20px; 899 | height: 250px; 900 | overflow: hidden; 901 | float: left; } 902 | 903 | .share-img-container { 904 | padding-right: 25px; } 905 | 906 | .share-img { 907 | display: inline-block; } 908 | 909 | .share-img-small { 910 | display: none; } 911 | 912 | .facebook-share-button { 913 | color: #394F9F; 914 | font-size: 14px; 915 | display: block; 916 | float: left; } 917 | 918 | .twitter-button-wrapper { 919 | float: left; 920 | font-size: 14px; 921 | margin-right: 10px; 922 | width: 61px; } 923 | 924 | .unstyled { 925 | margin: 0px; 926 | padding-left: 0px; } 927 | .unstyled li { 928 | list-style: none; } 929 | 930 | h1.title-internal { 931 | font-size: 40px; } 932 | 933 | .shop-learn-more { 934 | padding: 10px 0px; 935 | border: 1px solid #d8d4d0; 936 | text-align: center; 937 | display: block; } 938 | .shop-learn-more:hover { 939 | background-color: #d8d4d0; } 940 | 941 | .tips-and-tricks { 942 | clear: both; } 943 | .tips-and-tricks li { 944 | margin-bottom: 10px; 945 | margin-right: 10px; 946 | float: left; 947 | border: 1px solid #d8d4d0; } 948 | .tips-and-tricks li a { 949 | display: inline-block; 950 | padding: 10px 0px; 951 | width: 242px; 952 | text-align: center; } 953 | .tips-and-tricks li a:hover { 954 | background-color: #d8d4d0; } 955 | .tips-and-tricks li.even { 956 | margin-right: 0px; } 957 | 958 | .shop-list .item-link img { 959 | width: 195px; 960 | height: 195px; } 961 | .shop-list .item-link .title { 962 | margin: 5px 0; } 963 | 964 | .quotes .quote { 965 | margin-bottom: 20px; } 966 | 967 | .iframe-main { 968 | position: absolute; 969 | background: transparent; 970 | width: 100%; 971 | height: 100%; 972 | top: 50px; 973 | padding: 0; } 974 | 975 | .score-sharing { 976 | display: inline-block; 977 | margin-left: 10px; } 978 | 979 | footer { 980 | font-size: 17px; } 981 | 982 | #flappy-thirds-wrapper { 983 | text-align: center; } 984 | #flappy-thirds-wrapper canvas { 985 | border: 1px solid #d8d4d0; } 986 | 987 | .choose-language, .solitaire-ad { 988 | text-decoration: none; 989 | font-size: 14px; 990 | margin-left: 10px; } 991 | 992 | .solitaire-ad { 993 | padding-left: 5px; } 994 | .solitaire-ad img { 995 | margin-left: 15px; } 996 | 997 | /* patch flag icons */ 998 | .choose-language span.flag-icon { 999 | border: 1px solid #776e65; } 1000 | 1001 | .flag-icon-en { 1002 | background-image: url(../flags/4x3/us.svg); } 1003 | 1004 | .mobile-top { 1005 | position: relative; 1006 | display: none; 1007 | margin-bottom: 15px; 1008 | color: #f9f6f2; } 1009 | .mobile-top .open { 1010 | background-color: #776e65; 1011 | padding: 10px 0; } 1012 | .mobile-top .closed { 1013 | display: none; 1014 | height: 20px; } 1015 | .mobile-top .ad { 1016 | width: 320px; 1017 | height: 100px; 1018 | margin: 0 auto; 1019 | background-color: white; } 1020 | .mobile-top .close-btn { 1021 | position: absolute; 1022 | top: 5px; 1023 | right: 10px; 1024 | color: white; 1025 | text-decoration: none; } 1026 | 1027 | .mobile-top-alt { 1028 | display: none; } 1029 | 1030 | @media screen and (max-width: 860px) { 1031 | .side-column { 1032 | width: 130px; } 1033 | 1034 | .side-ad { 1035 | display: none; } 1036 | 1037 | .solitaire-ad img { 1038 | width: 100px; } 1039 | 1040 | .side-column-left { 1041 | background-position: -60px 0px; } } 1042 | @media screen and (max-width: 760px) { 1043 | .side-column { 1044 | display: none; } 1045 | 1046 | .mobile-top { 1047 | display: block; } 1048 | 1049 | body { 1050 | padding: 0px; } } 1051 | @media screen and (min-width: 481px) { 1052 | .tile .tile-inner { 1053 | line-height: 107px; } 1054 | 1055 | h1.title { 1056 | font-size: 74px; 1057 | line-height: 66px; } } 1058 | @media screen and (max-height: 768px) and (min-width: 481px) { 1059 | .heading { 1060 | margin-bottom: 15px; } 1061 | 1062 | h1.title-internal { 1063 | font-size: 40px; 1064 | line-height: 64px; } 1065 | 1066 | .container { 1067 | width: 450px; } 1068 | 1069 | .game-container { 1070 | margin-top: 40px; 1071 | position: relative; 1072 | padding: 12px; 1073 | cursor: default; 1074 | -webkit-touch-callout: none; 1075 | -ms-touch-callout: none; 1076 | -webkit-user-select: none; 1077 | -moz-user-select: none; 1078 | -ms-user-select: none; 1079 | -ms-touch-action: none; 1080 | touch-action: none; 1081 | background: #bbada0; 1082 | border-radius: 6px; 1083 | width: 450px; 1084 | height: 450px; 1085 | -webkit-box-sizing: border-box; 1086 | -moz-box-sizing: border-box; 1087 | box-sizing: border-box; } 1088 | .game-container .game-message { 1089 | display: none; 1090 | position: absolute; 1091 | top: 0; 1092 | right: 0; 1093 | bottom: 0; 1094 | left: 0; 1095 | background: rgba(238, 228, 218, 0.5); 1096 | z-index: 100; 1097 | text-align: center; 1098 | -webkit-animation: fade-in 800ms ease 1200ms; 1099 | -moz-animation: fade-in 800ms ease 1200ms; 1100 | animation: fade-in 800ms ease 1200ms; 1101 | -webkit-animation-fill-mode: both; 1102 | -moz-animation-fill-mode: both; 1103 | animation-fill-mode: both; } 1104 | .game-container .game-message p { 1105 | font-size: 60px; 1106 | font-weight: bold; 1107 | height: 60px; 1108 | line-height: 60px; 1109 | margin-top: 222px; } 1110 | .game-container .game-message .lower { 1111 | display: block; 1112 | margin-top: 59px; } 1113 | .game-container .game-message a { 1114 | display: inline-block; 1115 | background: #8f7a66; 1116 | border-radius: 3px; 1117 | padding: 0 20px; 1118 | text-decoration: none; 1119 | color: #f9f6f2; 1120 | height: 40px; 1121 | line-height: 42px; 1122 | margin-left: 9px; } 1123 | .game-container .game-message a.keep-playing-button { 1124 | display: none; } 1125 | .game-container .game-message.game-won { 1126 | background: rgba(237, 194, 46, 0.5); 1127 | color: #f9f6f2; } 1128 | .game-container .game-message.game-won a.keep-playing-button { 1129 | display: inline-block; } 1130 | .game-container .game-message.game-won, .game-container .game-message.game-over { 1131 | display: block; } 1132 | 1133 | .grid-container { 1134 | position: absolute; 1135 | z-index: 1; } 1136 | 1137 | .grid-row { 1138 | margin-bottom: 12px; } 1139 | .grid-row:last-child { 1140 | margin-bottom: 0; } 1141 | .grid-row:after { 1142 | content: ""; 1143 | display: block; 1144 | clear: both; } 1145 | 1146 | .grid-cell { 1147 | width: 97.5px; 1148 | height: 97.5px; 1149 | margin-right: 12px; 1150 | float: left; 1151 | border-radius: 3px; 1152 | background: rgba(238, 228, 218, 0.35); } 1153 | .grid-cell:last-child { 1154 | margin-right: 0; } 1155 | 1156 | .tile-container { 1157 | position: absolute; 1158 | z-index: 2; } 1159 | 1160 | .tile, .tile .tile-inner { 1161 | width: 98px; 1162 | height: 98px; 1163 | line-height: 107.5px; } 1164 | .tile.tile-position-1-1 { 1165 | -webkit-transform: translate(0px, 0px); 1166 | -moz-transform: translate(0px, 0px); 1167 | transform: translate(0px, 0px); } 1168 | .tile.tile-position-1-2 { 1169 | -webkit-transform: translate(0px, 109px); 1170 | -moz-transform: translate(0px, 109px); 1171 | transform: translate(0px, 109px); } 1172 | .tile.tile-position-1-3 { 1173 | -webkit-transform: translate(0px, 219px); 1174 | -moz-transform: translate(0px, 219px); 1175 | transform: translate(0px, 219px); } 1176 | .tile.tile-position-1-4 { 1177 | -webkit-transform: translate(0px, 328px); 1178 | -moz-transform: translate(0px, 328px); 1179 | transform: translate(0px, 328px); } 1180 | .tile.tile-position-2-1 { 1181 | -webkit-transform: translate(109px, 0px); 1182 | -moz-transform: translate(109px, 0px); 1183 | transform: translate(109px, 0px); } 1184 | .tile.tile-position-2-2 { 1185 | -webkit-transform: translate(109px, 109px); 1186 | -moz-transform: translate(109px, 109px); 1187 | transform: translate(109px, 109px); } 1188 | .tile.tile-position-2-3 { 1189 | -webkit-transform: translate(109px, 219px); 1190 | -moz-transform: translate(109px, 219px); 1191 | transform: translate(109px, 219px); } 1192 | .tile.tile-position-2-4 { 1193 | -webkit-transform: translate(109px, 328px); 1194 | -moz-transform: translate(109px, 328px); 1195 | transform: translate(109px, 328px); } 1196 | .tile.tile-position-3-1 { 1197 | -webkit-transform: translate(219px, 0px); 1198 | -moz-transform: translate(219px, 0px); 1199 | transform: translate(219px, 0px); } 1200 | .tile.tile-position-3-2 { 1201 | -webkit-transform: translate(219px, 109px); 1202 | -moz-transform: translate(219px, 109px); 1203 | transform: translate(219px, 109px); } 1204 | .tile.tile-position-3-3 { 1205 | -webkit-transform: translate(219px, 219px); 1206 | -moz-transform: translate(219px, 219px); 1207 | transform: translate(219px, 219px); } 1208 | .tile.tile-position-3-4 { 1209 | -webkit-transform: translate(219px, 328px); 1210 | -moz-transform: translate(219px, 328px); 1211 | transform: translate(219px, 328px); } 1212 | .tile.tile-position-4-1 { 1213 | -webkit-transform: translate(328px, 0px); 1214 | -moz-transform: translate(328px, 0px); 1215 | transform: translate(328px, 0px); } 1216 | .tile.tile-position-4-2 { 1217 | -webkit-transform: translate(328px, 109px); 1218 | -moz-transform: translate(328px, 109px); 1219 | transform: translate(328px, 109px); } 1220 | .tile.tile-position-4-3 { 1221 | -webkit-transform: translate(328px, 219px); 1222 | -moz-transform: translate(328px, 219px); 1223 | transform: translate(328px, 219px); } 1224 | .tile.tile-position-4-4 { 1225 | -webkit-transform: translate(328px, 328px); 1226 | -moz-transform: translate(328px, 328px); 1227 | transform: translate(328px, 328px); } 1228 | 1229 | .tile .tile-inner { 1230 | font-size: 53px; 1231 | line-height: 98px; } 1232 | 1233 | .tile-128 .tile-inner, .tile-256 .tile-inner, .tile-512 .tile-inner { 1234 | font-size: 43px !important; } 1235 | 1236 | .tile-1024 .tile-inner, .tile-2048 .tile-inner { 1237 | font-size: 33px !important; } 1238 | 1239 | .game-intro { 1240 | font-size: 17px; } 1241 | 1242 | .restart-button { 1243 | padding: 0 14px; } 1244 | 1245 | .game-container { 1246 | margin-top: 10px; } 1247 | 1248 | .tips-and-tricks li a { 1249 | width: 218px; } 1250 | 1251 | footer { 1252 | font-size: 16px; } } 1253 | @media screen and (max-width: 480px) { 1254 | html, body { 1255 | font-size: 15px; } 1256 | 1257 | body { 1258 | margin: 0 0 20px 0; } 1259 | 1260 | #flappy-thirds-wrapper canvas { 1261 | width: 100%; } 1262 | 1263 | .restart-button { 1264 | width: 35%; } 1265 | 1266 | .game-container { 1267 | margin-top: 10px; } 1268 | 1269 | .score-container:after, .best-container:after { 1270 | font-size: 11px; } 1271 | 1272 | .approved img { 1273 | width: 280px; } 1274 | 1275 | .play-now a { 1276 | padding: 8px; } 1277 | 1278 | h1.title-internal { 1279 | font-size: 24px; } 1280 | 1281 | .share-img { 1282 | display: none; } 1283 | 1284 | .share-img-small { 1285 | display: none; } 1286 | 1287 | .tips-and-tricks li a { 1288 | width: 280px; } 1289 | .tips-and-tricks li a.even { 1290 | margin-right: 0px; } 1291 | 1292 | .top-link-ads-wrapper { 1293 | display: none; } 1294 | 1295 | .fb-line { 1296 | float: left; 1297 | width: 100%; 1298 | font-size: 15px; } 1299 | 1300 | .ads-bellow-game { 1301 | margin-top: 30px; 1302 | text-align: center; 1303 | float: none; 1304 | width: 100%; } } 1305 | --------------------------------------------------------------------------------