Last active
August 29, 2015 13:58
-
-
Save JoeyEremondi/10343020 to your computer and use it in GitHub Desktop.
2048 in Elm
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--2048 in Elm | |
--Written by Joey Eremondi | |
[email protected] | |
--Based on 2048 by Gabriele Cirulli | |
--which was based on 1024 by Veewo Studio | |
--and similar to Threes by Asher Vollme | |
{- Copyright (c) 2014, Joey Eremondi | |
All rights reserved. | |
Redistribution and use in source and binary forms, with or without | |
modification, are permitted provided that the following conditions are met: | |
* Redistributions of source code must retain the above copyright | |
notice, this list of conditions and the following disclaimer. | |
* Redistributions in binary form must reproduce the above | |
copyright notice, this list of conditions and the following | |
disclaimer in the documentation and/or other materials provided | |
with the distribution. | |
* Neither the name of Joey Eremondi nor the names of other | |
contributors may be used to endorse or promote products derived | |
from this software without specific prior written permission. | |
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
-} | |
import Graphics.Collage as Collage | |
import Keyboard | |
import Random | |
import Transform2D as TF | |
import Window | |
--Represent each square of the game | |
type GridSquare = {contents: Int, x:Int, y:Int} | |
--The whole game is just the list of squares | |
type Grid = [GridSquare] | |
--The move data given from the keyboard | |
type KeyMove = { x:Int, y:Int } | |
--Represent different states the came can be in | |
data GameState = Playing Grid | GameWon Grid | GameLost Grid | |
--Datatype wrapping all of our input signals together | |
--Has moves from the user, and a random ordering of squares | |
data Input = Move KeyMove [(Int, Int)] | |
--Get the color for a particular number's square | |
colorFor n = case n of | |
2 -> rgb 238 238 218 | |
4 -> rgb 237 224 200 | |
8 -> rgb 242 177 121 | |
16 -> rgb 245 149 99 | |
32 -> rgb 246 130 96 | |
64 -> rgb 246 94 59 | |
128 -> rgb 237 207 114 | |
256 -> rgb 237 204 97 | |
512 -> rgb 237 201 82 | |
1024 -> rgb 237 197 63 | |
2048 -> rgb 237 194 46 | |
_ -> green | |
--Get the scale factor for a number | |
--More digits means bigger number | |
--We assume none have 5 digits | |
scaleForNumber n = if | |
| n >= 1000 -> 1/60.0 | |
| n >= 100 -> 1/30.0 | |
| n >= 10 -> 1/20.0 | |
| otherwise -> 1/15.0 | |
--Apply a function 4 times | |
--We use this for shifting: we shift squares as much as we can | |
--By shifting them 1 square 4 times | |
apply4 f = f . f . f . f | |
--Get the square at a given position in the grid | |
squareAt : Grid -> (Int, Int) -> Maybe GridSquare | |
squareAt grid (x,y) = case filter (\sq -> sq.x == x && sq.y == y) grid of | |
[] -> Nothing | |
[sq] -> Just sq | |
--Returns true if the grid has a 2048 | |
has2048 : Grid -> Bool | |
has2048 grid = case filter (\sq -> sq.contents >= 2048) grid of | |
[] -> False | |
_ -> True | |
--Delete a square from a given position, if it exists | |
deleteSquare : (Int, Int) -> Grid -> Grid | |
deleteSquare (x,y) = filter (\sq -> not <| sq.x == x && sq.y == y) | |
--Double the value of a given square | |
--Fails if the square does not exist | |
doubleSquare : (Int, Int) -> Grid -> Grid | |
doubleSquare coords grid = let | |
sq = case squareAt grid coords of | |
Just s -> s | |
removedGrid = deleteSquare coords grid | |
in ({sq | contents <- sq.contents*2} :: removedGrid) | |
--Convert the list of squares to a Form to be drawn | |
drawGrid : Grid -> Form | |
drawGrid grid = let | |
gridForms = map drawSquare grid | |
background = Collage.move (2.5, 2.5) <| Collage.filled black <| Collage.square 4 | |
in Collage.group <| [background]++gridForms | |
--Functions to sort elements from top to bottom, bottom to top, etc | |
--Useful for shifting elements in the right order | |
sortUp : Grid -> Grid | |
sortUp = sortBy (\sq -> sq.y) | |
sortDown = sortBy (\sq -> -1*sq.y) | |
sortLeft = sortBy (\sq -> -1*sq.x) | |
sortRight = sortBy (\sq -> sq.x) | |
--If there's an empty spot above (below, etc.) | |
--Shift the given square into it, otherwise put it in its original place | |
--Takes in a "partial" grid of squares above (below, etc.) already placed | |
shiftSquareUp : GridSquare -> Grid -> Grid | |
shiftSquareUp sq grid = | |
if sq.y == 4 | |
then (sq :: grid) | |
else case squareAt grid (sq.x, sq.y+1) of | |
Nothing -> ({sq | y <- sq.y + 1} :: grid) | |
_ -> (sq :: grid) | |
shiftSquareDown : GridSquare -> Grid -> Grid | |
shiftSquareDown sq grid = | |
if sq.y == 1 | |
then (sq :: grid) | |
else case squareAt grid (sq.x, sq.y-1) of | |
Nothing -> ({sq | y <- sq.y - 1} :: grid) | |
_ -> (sq :: grid) | |
shiftSquareLeft : GridSquare -> Grid -> Grid | |
shiftSquareLeft sq grid = | |
if sq.x == 1 | |
then (sq :: grid) | |
else case squareAt grid (sq.x-1, sq.y) of | |
Nothing -> ({sq | x <- sq.x - 1} :: grid) | |
_ -> (sq :: grid) | |
shiftSquareRight : GridSquare -> Grid -> Grid | |
shiftSquareRight sq grid = | |
if sq.x == 4 | |
then (sq :: grid) | |
else case squareAt grid (sq.x+1, sq.y) of | |
Nothing -> ({sq | x <- sq.x + 1} :: grid) | |
_ -> (sq :: grid) | |
--Functions to shift the squares for each time the player moves | |
--To move down, a square moves to the position in the grid where | |
--Except when squares get combined | |
--Similar math is performed for left, right, etc. | |
shift : (GridSquare -> Grid -> Grid) -> (Grid -> Grid) -> Grid -> Grid | |
shift shiftFun sortFun grid = let | |
shiftFold = (foldr shiftFun []) . sortFun | |
in (apply4 shiftFold) grid --apply 4 times, move as far as can | |
shiftUp = shift shiftSquareUp sortUp | |
shiftDown = shift shiftSquareDown sortDown | |
shiftLeft = shift shiftSquareLeft sortLeft | |
shiftRight = shift shiftSquareRight sortRight | |
--Functions to look at a given square, and see if it can be merged with | |
--the square above (below, left of, right of) it | |
--Note that we sort in the opposite order of shifting | |
--Since if we're moving up, the bottom square gets absorbed | |
mergeSquareUp : GridSquare -> Grid -> Grid | |
mergeSquareUp sq grid = case squareAt grid (sq.x, sq.y+1) of | |
Nothing -> (sq::grid) | |
Just adj -> | |
if adj.contents == sq.contents | |
then doubleSquare (sq.x, sq.y+1) grid | |
else (sq::grid) | |
mergeSquareDown : GridSquare -> Grid -> Grid | |
mergeSquareDown sq grid = case squareAt grid (sq.x, sq.y-1) of | |
Nothing -> (sq::grid) | |
Just adj -> | |
if adj.contents == sq.contents | |
then doubleSquare (sq.x, sq.y-1) grid | |
else (sq::grid) | |
mergeSquareLeft : GridSquare -> Grid -> Grid | |
mergeSquareLeft sq grid = case squareAt grid (sq.x-1, sq.y) of | |
Nothing -> (sq::grid) | |
Just adj -> | |
if adj.contents == sq.contents | |
then doubleSquare (sq.x-1, sq.y) grid | |
else (sq::grid) | |
mergeSquareRight : GridSquare -> Grid -> Grid | |
mergeSquareRight sq grid = case squareAt grid (sq.x+1, sq.y) of | |
Nothing -> (sq::grid) | |
Just adj -> | |
if adj.contents == sq.contents | |
then doubleSquare (sq.x+1, sq.y) grid | |
else (sq::grid) | |
--Apply the merges to tiles in the correct order | |
applyInOrder mergeFun sortFun = (foldl mergeFun []) . sortFun | |
--Given a grid and a square, see if that square can be merged | |
--by moving up (down, left, right) and if so, do the merge | |
--And double the tile that absorbs it | |
mergeUp = applyInOrder mergeSquareUp sortDown | |
mergeDown = applyInOrder mergeSquareDown sortUp | |
mergeLeft = applyInOrder mergeSquareLeft sortRight | |
mergeRight = applyInOrder mergeSquareRight sortLeft | |
--Given a list of tiles, find the first free tile, if any | |
--Used for placing random elements | |
firstFree : Grid -> [(Int, Int)] -> Maybe (Int, Int) | |
firstFree grid lst = case lst of | |
[] -> Nothing | |
(h::t) -> case squareAt grid h of | |
Nothing -> Just h | |
_ -> firstFree grid t | |
--Draw an individual square, and translate it into the right position | |
--We assume each square is 1 "unit" wide, and positioned somewhere in [1,4]*[1,4] | |
drawSquare : GridSquare -> Form | |
drawSquare square = let | |
rawSquare = Collage.filled (colorFor square.contents) <| Collage.square 1 | |
numElem = Collage.scale (scaleForNumber square.contents)<| Collage.toForm <| plainText <| show square.contents | |
completeSquare = Collage.group [rawSquare, numElem] | |
in Collage.move (toFloat square.x, toFloat square.y) completeSquare | |
--Given the current state of the game, and a change in input from the user | |
--Generate the new state of the game | |
updateGameState : Input -> GameState -> GameState | |
updateGameState input gs = case (input, gs) of | |
--The user moved, so shift, do any merges, then shift again to clean up | |
(Move move lst, Playing grid) -> let | |
updatedGrid = | |
if move.x == 1 | |
then shiftRight <| mergeRight <| shiftRight grid | |
else if move.x == -1 | |
then shiftLeft <| shiftLeft <| mergeLeft <| shiftLeft grid | |
else if move.y == -1 | |
then shiftDown <| mergeDown <| shiftDown grid | |
else if move.y == 1 | |
then shiftUp <| mergeUp <| shiftUp grid | |
else grid | |
--We only move on key down, not when the move returns to 0,0 | |
in case (firstFree updatedGrid lst, move.x == 0 && move.y == 0) of | |
(_, True) -> gs | |
(Just (x,y), False) -> Playing ({contents=2, x=x,y=y}:: updatedGrid) | |
(Nothing, False) -> if (has2048 updatedGrid) | |
then GameWon updatedGrid | |
else GameLost updatedGrid | |
_ -> gs | |
--The different coordinates a tile can have | |
--We randomly permute this to add new tiles to the board | |
allTiles = [(1,1), (1,2), (1,3), (1,4), (2,1), (2,2), (2,3), (2,4), | |
(3,1), (3,2), (3,3), (3,4), (4,1), (4,2), (4,3), (4,4)] | |
--For now, we always start with the same two tiles | |
--Will be made more sophisticated in future versions | |
startState = Playing [{contents=2, x=1, y=4},{contents=2, x=1, y=3}] | |
--Given a game state, convert it to a form to be drawn | |
drawGame gs = case gs of | |
Playing grid -> drawGrid grid | |
GameLost grid -> let | |
messageForm = Collage.move (2.5, 2.5) <| Collage.scale (1/40.0) <| Collage.toForm <| plainText "Game Over" | |
in Collage.group [drawGrid grid, messageForm ] | |
GameWon grid -> let | |
messageForm = Collage.move (2.5, 2.5) <| Collage.scale (1/40.0) <| Collage.toForm <| plainText "Congratulations" | |
in Collage.group [drawGrid grid, messageForm ] | |
--Extracts the nth element of a list, starting at 1 | |
--Fails on empty lists | |
nth1 : Int -> [a] -> (a,[a]) | |
nth1 n (h::t) = case n of | |
1 -> (h,t) | |
_ -> let | |
(nth, tailLeftOver) = nth1 (n-1) t | |
in (nth, h::tailLeftOver) | |
--Shuffle the elements of the given list, assuming we have n random numbers | |
--Not exceeding n, n-1, etc. | |
shuffle lst randNums = let | |
shuffleStep indexToAdd (elemsToAdd, listSoFar) = let | |
(nextElem, leftOver) = nth1 indexToAdd elemsToAdd | |
in (leftOver, nextElem::listSoFar) | |
in snd <| foldr shuffleStep (lst, []) randNums | |
--Convert WASD and Arrow input from the user into our input data type | |
--Bundling it with a random permutations of the tiles each time | |
keyInput = let | |
randNums = combine <| map (\upper -> Random.range 1 upper Keyboard.wasd) [1..16] | |
randomList = lift (shuffle allTiles) randNums | |
inputSignal = merge Keyboard.wasd Keyboard.arrows | |
in lift2 Move inputSignal randomList | |
--Wrap everything together: take the game state | |
--Get the form to draw it, transform it into screen coordinates | |
--Then convert it to an Element and draw it to the screen | |
main = let | |
gameState = foldp updateGameState startState keyInput | |
rawFormList = lift (\x -> [drawGame x]) gameState | |
scaleFor x y = (toFloat (min x y))/4.0 | |
makeTform (x,y) = TF.multiply (TF.translation (toFloat x/(-2.0)) (toFloat (y+100)/(-2.0) )) (TF.scale <| scaleFor x y) | |
tform = lift makeTform Window.dimensions | |
gameForm = lift2 Collage.groupTransform tform rawFormList | |
formList = lift (\x -> [x]) gameForm | |
collageFunc = lift (\(x,y) -> collage x y) Window.dimensions | |
in lift2 {-(asText . show) gameState-} (\f l -> f l) collageFunc formList |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment