Task

Implement a 2D sliding block puzzle game where blocks with numbers are combined to add their values.



Rules of the game

   The rules are that on each turn the player must choose a direction   (up, down, left or right).
   All tiles move as far as possible in that direction, some move more than others. 
   Two adjacent tiles (in that direction only) with matching numbers combine into one bearing the sum of those numbers. 
   A move is valid when at least one tile can be moved,   if only by combination. 
   A new tile with the value of   2   is spawned at the end of each turn at a randomly chosen empty square   (if there is one). 
   Adding a new tile on a blank space.   Most of the time,   a new   2   is to be added,   and occasionally   (10% of the time),   a   4.
   To win,   the player must create a tile with the number   2048. 
   The player loses if no valid moves are possible.


The name comes from the popular open-source implementation of this game mechanic, 2048.



Requirements

   "Non-greedy" movement.   
  The tiles that were created by combining other tiles should not be combined again during the same turn (move).   
  That is to say,   that moving the tile row of:
               [2][2][2][2] 

 to the right should result in: 
               ......[4][4] 

 and not:
               .........[8] 

   "Move direction priority".   
  If more than one variant of combining is possible,   move direction shall indicate which combination will take effect. 
  For example, moving the tile row of:
               ...[2][2][2] 

 to the right should result in:
               ......[2][4] 

 and not:
               ......[4][2] 



   Check for valid moves.   The player shouldn't be able to skip their turn by trying a move that doesn't change the board.
   Check for a  win condition.
   Check for a lose condition.


-- demo\rosetta\2048.exw
with javascript_semantics
include pGUI.e

Ihandle canvas, dialog
cdCanvas cddbuffer, cdcanvas

constant tile_colours = {#CCC0B4,   -- blank
                         #EEE4DA,   -- 2
                         #EDE0C8,   -- 4
                         #F2B179,   -- 8
                         #F59563,   -- 16
                         #F67C5F,   -- 32
                         #F65E3B,   -- 64
                         #EDCF72,   -- 128
                         #EDCC61,   -- 256
                         #EDC850,   -- 512
                         #EDC53F,   -- 1024
                         #EDC22E}   -- 2048

-- the 4x4 board.
--  note that values are [1..12] for [blank,2,4,8,..2048].
--  (merging two eights is not 8+8->16 but 4+1->5, internally)
sequence board

integer newgame = 1,
        valid = 0, 
        prev, nxt

procedure add_rand(integer count)
-- (nb infinite loop if board is full)
    while count do
        integer x = rand(4),
                y = rand(4)
        if board[y][x]=1 then   -- blank
            board[y][x] = 2+(rand(10)=10)
            count -= 1
        end if
    end while   
end procedure

procedure move_x(integer x, y, d)
    integer bxy = board[x][y]
    if bxy!=1 then -- (not blank)
        if bxy=prev then
            board[x][y] = 1
            bxy += 1
            board[x][nxt] = bxy
            nxt += d
            prev = 13
            valid = 1 
        else
            if prev=1 -- (blank)
            or y!=nxt then
                if prev!=1
                and prev!=13 then
                    nxt += d
                end if
                if y!=nxt then
                    board[x][y] = 1
                    board[x][nxt] = bxy
                    valid = 1
                end if
            end if
            prev = bxy
        end if
    end if
end procedure

procedure move_y(integer x, y, d)
    integer bxy = board[x][y]
    if bxy!=1 then -- (not blank)
        if bxy=prev then
            board[x][y] = 1
            bxy += 1
            board[nxt][y] = bxy
            nxt += d
            prev = 13
            valid = 1 
        else
            if prev=1 -- (blank)
            or x!=nxt then
                if prev!=1
                and prev!=13 then
                    nxt += d
                end if
                if x!=nxt then
                    board[x][y] = 1
                    board[nxt][y] = bxy
                    valid = 1
                end if
            end if
            prev = bxy
        end if
    end if
end procedure

function move(integer key)
-- a non-zero result means it changed something.
    valid = 0
    if key=K_LEFT then
        for x=1 to 4 do
            prev = 13
            nxt = 1
            for y=1 to 4 do
                move_x(x,y,+1)
            end for
        end for
    elsif key=K_UP then
        for y=1 to 4 do
            prev = 13
            nxt = 4
            for x=4 to 1 by -1 do
                move_y(x,y,-1)
            end for
        end for
    elsif key=K_RIGHT then
        for x=1 to 4 do
            prev = 13
            nxt = 4
            for y=4 to 1 by -1 do
                move_x(x,y,-1)
            end for
        end for
    elsif key=K_DOWN then
        for y=1 to 4 do
            prev = 13
            nxt = 1
            for x=1 to 4 do
                move_y(x,y,+1)
            end for
        end for
    end if
    return valid
end function

function game_won()
    for i=1 to length(board) do
        if find(12,board[i]) then return 1 end if
    end for
    return 0
end function

constant valid_keys = {K_LEFT,K_DOWN,K_RIGHT,K_UP}

function no_valid_moves()
    sequence saved_board = deep_copy(board)
    for i=1 to length(valid_keys) do
        if move(valid_keys[i]) then
            board = saved_board
            return 0    -- OK
        end if
    end for
    return 1 -- game over...
end function

function redraw_cb(Ihandle /*ih*/)
    integer ox,oy,              -- top right coords
            os,ts,              -- overall and tile size
            ts2,                -- half tile, for number positioning
            {dw,dh} = IupGetIntInt(canvas, "DRAWSIZE")
    if dw>=dh then
        ox = floor((dw-dh)/2)
        oy = 0
        os = dh
    else
        ox = 0
        oy = floor((dh-dw)/2)
        os = dw
    end if
    ts = floor((os-10)/4-7)
    ts2 = floor(ts/2+5)-10

    if newgame then
        board = repeat(repeat(1,4),4)
        add_rand(2)
        newgame = 0
    end if

    cdCanvasActivate(cddbuffer)
    cdCanvasSetBackground(cddbuffer, #FAF8EF)
    cdCanvasClear(cddbuffer)
    cdCanvasSetForeground(cddbuffer, #BBADA0)
    cdCanvasRoundedBox(cddbuffer, ox+5, ox+os-5, oy+5, oy+os-5, 10, 10)

    integer tx = ox+15
    for y=1 to 4 do
        integer ty = oy+15
        for x=1 to 4 do
            integer bxy = board[x][y]
            cdCanvasSetForeground(cddbuffer, tile_colours[bxy])
            cdCanvasRoundedBox(cddbuffer, tx, tx+ts-10, ty, ty+ts-10, 5, 5)
            if bxy>1 then
                cdCanvasSetForeground(cddbuffer, iff(bxy<=3?#776E65:#F9F6F2))
                cdCanvasFont(cddbuffer, "Calibri", CD_BOLD, iff(bxy>10?32:40))
                cdCanvasText(cddbuffer, tx+ts2, ty+ts2-25-iff(bxy<11?7:0), sprint(power(2,bxy-1))) 
            end if
            ty += ts+5
        end for
        tx += ts+5
    end for
    cdCanvasFlush(cddbuffer)
    return IUP_DEFAULT
end function

function map_cb(Ihandle ih)
    cdcanvas = cdCreateCanvas(CD_IUP, ih)
    cddbuffer = cdCreateCanvas(CD_DBUFFER, cdcanvas)
    cdCanvasSetTextAlignment(cddbuffer, CD_SOUTH) 
    return IUP_DEFAULT
end function

function key_cb(Ihandle /*ih*/, atom c)
    if c=K_ESC then return IUP_CLOSE end if
    if find(c,valid_keys) then
        if move(c) then
            IupUpdate(canvas)
            string mbmsg = ""
            if game_won() then
                mbmsg = "!!!YOU WON!!!\n\nAnother Go?"
            else
                add_rand(1)
                IupUpdate(canvas)
                if no_valid_moves() then
                    mbmsg = "You Lost.\n\nAnother Go?"
                end if
            end if
            if length(mbmsg) then
                if platform()=JS then
                    IupMessage("Game Over",mbmsg);
                    newgame=1
                else
                    if IupAlarm("Game Over",mbmsg,"Yes","No")=1 then
                        newgame=1
                    else
                        return IUP_CLOSE
                    end if
                end if
            end if
        end if
        IupUpdate(canvas)
    end if
    return IUP_CONTINUE
end function

procedure main()

    IupOpen()

    canvas = IupCanvas("RASTERSIZE=520x540")
    IupSetCallback(canvas, "MAP_CB", Icallback("map_cb"))
    IupSetCallback(canvas, "ACTION", Icallback("redraw_cb"))

    dialog = IupDialog(canvas,"MINSIZE=440x450")
    IupSetAttribute(dialog,"TITLE","2048");
    IupSetCallback(dialog, "K_ANY", Icallback("key_cb"));

    IupShow(dialog)
    IupSetAttribute(canvas, "RASTERSIZE", NULL)

    if platform()!=JS then
        IupMainLoop()
        IupClose()
    end if
end procedure
main()
