2048 at Erlang

    imageProbably I don’t have time for the week of playing 2048 on the hub, but the article is not so much about the game as about the websocket server on Erlang. A little background. When he started playing in 2048, he simply could not stop. To the detriment of work and family. Therefore, he decided that the bot should play for me. But the catch is that the game is client-based, which is why the global rating is not maintained and it is not so convenient to play without a browser. Therefore, I decided to make the server part, where there would be a rating. And where could my bot play without a browser.


    I note that this is my first Erlang project. Many programmers are afraid of Erlang, suggesting that it is difficult. But actually it is not. Plus, I will try to highlight moments that are not entirely obvious to a newbie in Erlang.

    To simplify, a lot of things are hardcoded. But I am always happy with constructive criticism and comments.
    The link to github is erl2048 .
    Link to the working draft - erl2048 . But, I think, he will not live under the habraeffect for long.

    Javascript


    Oddly enough, I'll start with JS. I did not modify the original files so that they could be updated from the primary repository, if necessary. I used:
    • main.css;
    • animframe_polyfill.js for requestAnimationFrame;
    • html_actuator.js for all animations
    • keyboard_input_manager.js for keyboard events, and, as practice has shown, in vain;

    I created the “main.js” file. The logic is simple - the browser sends events to the server, and then updates the field. Fortunately, animframe_polyfill is created in such a way that it accepts the generated grid.

    What I added. Connection initialization:

    var websocket = new Websocket(SERVER);
      websocket
      .connect()
      .done(function(){
        var myGame = new MyGame(websocket);    
      });
    

    He whipped up a wrapper over Websocket. It is very simple to list the source code here.
    Start a new game:

    self.restart = function(evt){
      websocket.send(JSON.stringify({
        action:'start'
      }));
    };
    

    Make a move:
    self.move = function(direction){
      // 0: up, 1: right, 2:down, 3: left
      if(!toMove){
        return false;
      }
      if(direction === 0){
        direction = 'up';
      }else if(direction === 1){
        direction = 'right';
      }else if(direction === 2){
        direction = 'down';
      }else if(direction === 3){
        direction = 'left';
      }
      websocket.send(JSON.stringify({
        action:'move',
        value: direction
      }));
    };
    


    And the biggest one.
    Server response processing:
    self.wsHandler = function(evt){
      var game = JSON.parse(evt.data);
      if(game.grid){
        var grid = {cells: []};
        game.grid.forEach(function (column, y) {
          var row = [];
          column.forEach(function (cell, x) {
            if(cell){
              if(cell.mergedFrom){
                cell.mergedFrom.forEach(function(tile){
                  tile['x'] = x;
                  tile['y'] = y;
                });
              }
              row.push({
                value:            cell.value,
                x:                x,
                y:                y,
                previousPosition: cell.previousPosition,
                mergedFrom:       cell.mergedFrom
              });
            }
          });
          grid.cells.push(row);
        });
        var scores = game.scores,
          bestScore = 0;
        if(scores && scores.length>0){
          bestScore = scores[0].score;
          while (scoresEl.firstChild) {
            scoresEl.removeChild(scoresEl.firstChild);
          }
          scores.forEach(function(score){
            var div = document.createElement('Div');
            var name = document.createElement('Div');
            var scoreEl = document.createElement('Div');
            div.setAttribute("class", 'score');
            name.setAttribute("class", 'name');
            scoreEl.setAttribute("class", 'score');
            name.appendChild(document.createTextNode(score.name));
            scoreEl.appendChild(document.createTextNode(score.score));
            div.appendChild(name);
            div.appendChild(scoreEl);
            scoresEl.appendChild(div);
          });
        }
        actuator.actuate(grid, {
          score:     game.score,
          bestScore: bestScore,
          score: game.score,
          won: game.won,
          over: game.over,
          keepPlaying: game.keepPlaying
        });
      }
      //playername actuator
      if(game.user){
        if(playername.value !== playername){
          playername.value = game.user.name;
        }
      }
    };
    


    As you can see, the game is completely dependent on the server, because all the calculations take place there. Not like, for example, in my game Tic Tac Toe , where logic is duplicated.
    In fact, I did not understand why the original uses x and y in Tile, so the server does without them. And on the client I’m already adding to eat the actuator.
    Also from the server comes a list of the top 10 best players. This is an innovation of my version. And the player can change his nickname. No registrations or protections. Entered a name and play. You need to point to the box with the best score to see the overall rating. It looks like this.



    Using the native keyboard_input_manager is not very good. Because now not all characters can be entered in the nickname input field. But you can paste your nickname from the clipboard.
    Plus, I did not implement all the functionality. The part that is responsible for the “loss” is still closed by a stub, but this does not really affect the gameplay. And there is no way to continue the game after winning. But to win yet failed.

    Erlang


    This part will be painted in more detail. First you need to install rebar. You can do it from here . Rebar can generate initial files, but I created them manually.
    "Rebar.config" - used to automatically download and build dependencies.
    Hidden text
    % The next option is required so we can use lager.  
    {erl_opts, [{parse_transform, lager_transform}]}.  
    {lib_dirs,["deps"]}.  
    % Our dependencies.  
    {deps, [    
        {'lager', ".*", {  
            git, "git://github.com/basho/lager.git", "master"}  
        },
        {'cowboy', ".*", {  
            git, "git://github.com/extend/cowboy.git", "master"}  
        },
        {'mochiweb', ".*", {
        	git, "git://github.com/mochi/mochiweb.git", "master"}
        },
        {'sqlite3', ".*", {
        	git, "git://github.com/alexeyr/erlang-sqlite3.git", "master"}
        }
    ]}.  
    

    # rebar g-d
    # rebar co
    

    To download and collect dependencies. You may need to install “libsqlite3-dev” for the sqlite driver.

    To start the server, I use:
    # rebar compile skip_deps=true; erl -pa ebin deps/*/ebin -eval 'starter:start().' -noshell -detached
    

    After that, the game will be available on port 8080. In fact, learning how to launch a project was the most difficult. Further it is easier. I created a special module "starter", which runs all the dependencies and the application.

    -module(starter).
    -export([start/0]).
    start() ->
    	application:start(ranch),
    	application:start(crypto),
    	application:start(cowlib),
    	application:start(cowboy),
    	application:start(inets),
    	application:start(mochiweb),
    	application:start(erl2048).
    

    Now consider the contents of the src directory. The first is the “erl2048.app.src” file. I don’t really know what it is for, but I added my own project just in case.

    Hidden text
    {application, erl2048, [
    {description, "2048 game server."},
    {vsn, "1"},
    {modules, []},
    {registered, [erl2048_sup]},
    {applications, [
    kernel,
    stdlib,
    cowboy
    ]},
    {mod, {erl2048_app, []}},
    {env, []}
    ]}.
    


    erl2048_sup.erl
    %% Feel free to use, reuse and abuse the code in this file.
    %% @private
    -module(erl2048_sup).
    -behaviour(supervisor).
    %% API.
    -export([start_link/0]).
    %% supervisor.
    -export([init/1]).
    %% API.
    -spec start_link() -> {ok, pid()}.
    start_link() ->
        supervisor:start_link({local, ?MODULE}, ?MODULE, []).
    %% supervisor.
    init([]) ->
        Procs = [],
        {ok, {{one_for_one, 10, 10}, Procs}}.
    

    I understand that this thing ensures that the application does not crash and restarts if necessary. Took from an example - decided to leave.

    Now the main application file is “erl2048_app.erl”.

    Hidden text
    %% Feel free to use, reuse and abuse the code in this file.
    %% @private
    -module(erl2048_app).
    -behaviour(application).
    %% API.
    -export([start/2]).
    -export([stop/1]).
    %% API.
    start(_Type, _Args) ->
        Dispatch = cowboy_router:compile([
            {'_', [
                {"/", cowboy_static, {file, "../client/index.html"}},
                {"/websocket", ws_handler, []},
                {"/static/[...]", cowboy_static, {dir, "../client/static"}}
            ]}
        ]),
        {ok, _} = cowboy:start_http(http, 100, [{port, 8080}],
            [{env, [{dispatch, Dispatch}]}]),
        {ok, _} = db:start_link(),
        erl2048_sup:start_link().
    stop(_State) ->
        {ok, _} = db:stop(),
        ok.
    

    Here I can already explain something. Firstly, routes for cowboy are compiled. Then cowboy starts and connects to the database.
    The subl role is sqlite. I considered Postgresql, mongoDB and Redis as well. But settled on sqlite, since it is the simplest. Plus stores data after a restart. But, I think, it will create a big load on the application because of what it will most likely lie. Anyway - the module code:

    Hidden text
    -module(db).
    -export([start_link/0,stop/0]).
    -export([insert/2, select/0, createUser/1, changeName/2]).
    start_link() ->
        {ok, PID} = sqlite3:open(db, [{file, "db.sqlite3"}]),
        Tables = sqlite3:list_tables(db),
        case lists:member("scores", Tables) of false ->
            sqlite3:create_table(db, scores, [{id, integer, [{primary_key, [asc, autoincrement]}]}, {userid, integer}, {score, integer}])
        end,
        case lists:member("users", Tables) of false ->
            sqlite3:create_table(db, users, [{id, integer, [{primary_key, [asc, autoincrement]}]}, {name, text}])
        end,
        {ok, PID}.
    stop() ->
        sqlite3:close(db).
    select() ->
        Ret = sqlite3:sql_exec(db, "select users.name, scores.score from scores LEFT JOIN users ON (users.id = scores.userid) ORDER BY score desc;"),
        [{columns,_},{rows,Rows}] = Ret,
        formatScores(Rows).
    insert(Score, Player) ->
        [{columns,_},{rows,Rows}] = sqlite3:sql_exec(db, "SELECT score FROM scores WHERE userid = ?", [{1,Player}]),
        DBScore = if
            length(Rows) > 0  -> element(1,hd(Rows));
            true -> 0
        end,
        if Score > DBScore ->
            sqlite3:delete(db, scores, {userid, Player}),
            sqlite3:write(db, scores, [{userid, Player}, {score, Score}]),
            sqlite3:sql_exec(db, "DELETE FROM scores WHERE id IN (SELECT id FROM scores ORDER BY score desc LIMIT 1 OFFSET 10)");
            true -> undefined
        end.
    formatScores([]) ->
        [];
    formatScores([{Name, Score} | Rows]) ->
        [{struct, [{name, Name},{score, Score}]} | formatScores(Rows)].
    createUser(UserName) ->
        sqlite3:write(db, users, [{name, UserName}]).
    changeName(Id, NewName) ->
        sqlite3:update(db, users, {id, Id}, [{name, NewName}]).
    


    Let's move on to the module that processes websocket connections.

    ws_handler.erl
    -module(ws_handler).
    -behaviour(cowboy_websocket_handler).
    -export([init/3]).
    -export([websocket_init/3]).
    -export([websocket_handle/3]).
    -export([websocket_info/3]).
    -export([websocket_terminate/3]).
    init({tcp, http}, _Req, _Opts) ->
        {upgrade, protocol, cowboy_websocket}.
    websocket_init(_TransportName, Req, _Opts) ->
        State = {struct, [ 
            { user, { struct, [{id, null},{name, <<"Player">>}] } } 
        ]},
        {ok, Req, State}.
    websocket_handle({text, Msg}, Req, State) ->
        Message = mochijson2:decode(Msg, [{format, proplist}]),
        Action =  binary_to_list(proplists:get_value(<<"action">>, Message)),
        {NewState, Response} = case Action of
            "start" ->
                TmpState = game:init(State),
                {TmpState, TmpState};
            "move"  ->
                TmpState = game:move(list_to_atom(binary_to_list(proplists:get_value(<<"value">>, Message))), State),
                {TmpState, TmpState};
            "newName" ->
                NewName = proplists:get_value(<<"value">>, Message),
                JsonData = element(2, State),
                User = proplists:get_value(user, JsonData),
                {struct,UserJsonData} = User,
                Id = proplists:get_value(id, UserJsonData),
                db:changeName(Id, NewName),
                TmpState = {struct, [
                        { user, { struct, [ { name, NewName },{ id, Id } ] } }
                        | proplists:delete(user, JsonData)
                    ]},
                {
                    TmpState,
                    {struct, [{ user, { struct, [ { name, NewName },{ id, Id } ] } }]}
                };
            _Else -> State
        end,
        {reply, {text, mochijson2:encode(Response)}, Req, NewState};
    websocket_handle(_Data, Req, State) ->
        {ok, Req, State}.
    websocket_info({send, Msg}, Req, State) ->
        {reply, {text, Msg}, Req, State};
    websocket_info(_Info, Req, State) ->
        {ok, Req, State}.
    websocket_terminate(_Reason, _Req, _State) ->
        ok.
    

    At first, I did not understand how it was all arranged. It turns out that everything is very simple. There is a state that is set when the connection is established. And which is passed to each request handler for each client. The main method here is “websocket_handle”. It accepts the message and status and returns a response and status.
    For communication, the JSON format is used. In Erlang, it appears as a structure like:

    {struct, [
      {key1, Value1},
      {key2, Value2},
      ....
    ]}
    


    Now directly the game files. The simplest is “tile.erl”.

    tile.erl
    -module(tile).
    -export([init/1, init/0, prepare/2]).
    prepare(null, _) ->
        null;
    prepare(Tile, { X, Y }) ->
        {
            struct,
            [
                {value, proplists:get_value(value, element(2, Tile))},
                {mergedFrom, null},
                {previousPosition, {struct, [{ x, X - 1},{ y, Y - 1 }]}}
            ]
        }.
    init(Value) ->
        {
            struct,
            [
                {value, Value},
                {mergedFrom, null},
                {previousPosition, null}
            ]
        }.
    init() ->
        init(2).
    

    He only knows how to create a new tile and maintain the previous position.
    "Grid.erl" is already more complicated.

    grid.erl
    -module(grid).
    -export([
        build/0,
        cellsAvailable/1,
        randomAvailableCell/1,
        insertTile/3,
        availableCells/1,
        cellContent/2,
        removeTile/2,
        moveTile/3,
        size/0,
        withinBounds/1,
        cellAvailable/2
    ]).
    -define(SIZE, 4).
    size() ->
        ?SIZE.
    build() ->
        [[null || _ <- lists:seq(1, ?SIZE)] || _ <- lists:seq(1, ?SIZE)].
    availableCells(Grid) ->
        lists:append(
            setY(
                availableCells(Grid, 1)
            )
        ).
    availableCells([Grid | Tail ], N) when is_list(Grid) ->
        [{availableCells(Grid, 1), N} | availableCells(Tail, N +1)];
    availableCells([Grid | Tail ], N) ->
        case Grid =:= null of
            true -> [ N | availableCells(Tail, N +1)];
            false ->  availableCells(Tail, N +1)
        end;
    availableCells([], _) ->
        [].
    setY([{Cell, Y}|Tail]) -> 
        [ setY(Cell, Y) | setY(Tail)];
    setY([]) -> 
        [].
    setY([Head | Tail], Y) ->
        [ {Head, Y} | setY(Tail, Y)];
    setY([], _) ->
        [].
    cellsAvailable(Grid) ->
        length(availableCells(Grid)) > 0.
    randomAvailableCell(Grid) ->
        Cells = availableCells(Grid),
        lists:nth(random:uniform(length(Cells)) ,Cells).
    insertTile({X, Y}, Tile, Grid) ->
        Row = lists:nth(Y,Grid),
        lists:sublist(Grid,Y - 1) ++ [ lists:sublist(Row,X - 1) ++ [Tile] ++ lists:nthtail(X,Row)] ++ lists:nthtail(Y,Grid).
    cellContent({ X, Y }, Grid) ->
        case withinBounds({ X, Y }) of
            true -> lists:nth(X,lists:nth(Y,Grid));
            false -> null
        end.
    removeTile({ X, Y }, Grid) ->
        insertTile({X, Y}, null, Grid).
    moveTile(Cell, Cell, Grid) ->
        Grid;
    moveTile(Cell, Next, Grid) ->
        insertTile(Next, grid:cellContent(Cell, Grid), removeTile(Cell, Grid)).
    withinBounds({X, Y}) when
        (X > 0), (X =< ?SIZE), 
        (Y > 0), (Y =< ?SIZE) ->
        true;
    withinBounds(_) ->
        false.
    cellAvailable(Cell, Grid) ->
        case grid:withinBounds(Cell) of
            true -> cellContent(Cell, Grid) =:= null;
            false -> false
        end.
    

    Pay attention to availableCells. Erlang needs to make the most of recursion. But here I am too clever. First, a sheet was generated that contained sheets with one coordinate and a second coordinate. And then he introduced the second to the first. I decided not to do that again. Other functions, I think, are obvious.
    And, the main game file. This is called "game.erl".

    game.erl
    -module(game).
    -export([init/1, move/2]).
    init(State) ->
        StateUser = proplists:get_value(user, element(2, State)),
        StateUserJsonData = element(2, StateUser),
        User = case proplists:get_value(id, StateUserJsonData) of
            null ->
                Name = proplists:get_value(name, StateUserJsonData),
                {rowid, Id} = db:createUser(Name),
                { struct, [{name, Name},{id, Id}]};
            _Else ->
                StateUser
        end,
        {
            struct,
            [
                {grid ,addStartTiles(grid:build())},
                {user , User},
                {score,0},
                {scores, db:select()},
                {won, false},
                {over, false},
                {keepPlaying, false}
            ]
        }.
    addStartTiles(Grid, 0) -> 
        Grid;
    addStartTiles(Grid, N) -> 
        NewGrid = addRandomTile(Grid),
        addStartTiles(NewGrid, N - 1).
    addStartTiles(Grid) ->
        addStartTiles(Grid, 2).
    addRandomTile(Grid) ->
        random:seed(now()),
        case grid:cellsAvailable(Grid) of
            true -> 
                case random:uniform(10) < 9 of
                    true -> Tile = tile:init();
                    false -> Tile = tile:init(grid:size())
                end,
                grid:insertTile(grid:randomAvailableCell(Grid), Tile, Grid);
            false -> Grid
        end.
    getVector(left) ->
        { -1, 0 };
    getVector(up) ->
        { 0,  -1 };
    getVector(right) ->
        { 1,  0 };
    getVector(down) ->
        { 0,  1 }.
    buildTraversals() ->
        Traver = lists:seq(1, grid:size()),
        { Traver, Traver }.
    buildTraversals({ 1 , _ }) ->
        { T1, T2} = buildTraversals(),
        { lists:reverse(T1), T2 };
    buildTraversals({ _ , 1 }) ->
        { T1, T2} = buildTraversals(),
        { T1, lists:reverse(T2) };
    buildTraversals({ _ , _ }) ->
        buildTraversals().
    prepareTiles( [{_Key, _Value} | _Tail ] ) ->
        JsonData = [{_Key, _Value} | _Tail ],
        [{ grid, prepareTiles(proplists:get_value(grid, JsonData)) } | proplists:delete(grid, JsonData) ];
    prepareTiles( Grid ) ->
        prepareTiles( Grid, 1).
    prepareTiles([], _) ->
        [];
    prepareTiles([Row | Tail], Y) ->
        [ prepareTileY(Row, 1, Y) | prepareTiles(Tail, Y + 1)].
    prepareTileY([], _, _) ->
        [];
    prepareTileY([Cell | Tail], X, Y) ->
        [prepareTileX(Cell, X, Y) | prepareTileY(Tail, X + 1, Y) ].
    prepareTileX(Tile, X, Y) ->
        tile:prepare(Tile, {X, Y}).
    process_travesals_y([], _, _, JsonData) ->
        JsonData;
    process_travesals_y(_, [], _, JsonData) ->
        JsonData;
    process_travesals_y([ Y | Tail ], TraversalsX, Vector, JsonData) ->
        process_travesals_y(
            Tail,
            TraversalsX,
            Vector,
            process_travesals_y( Y, TraversalsX, Vector, JsonData)
        );
    process_travesals_y(Y, [ X | Tail ], Vector, JsonData) ->
        process_travesals_y(Y, Tail, Vector, process_travesals_y( Y, X, Vector, JsonData ));
    process_travesals_y( Y, X, Vector, JsonData ) ->
        moveTile({ X, Y }, Vector, JsonData).
    findFarthestPosition({X, Y}, {VecX, VecY}, Grid) ->
        Next = { X + VecX, Y + VecY },
        case grid:cellAvailable(Next, Grid) of
            true -> 
                findFarthestPosition(Next, {VecX, VecY}, Grid);
            false -> 
                {
                    {X, Y},
                    Next % Used to check if a merge is required
                }
        end.
    moveTile(Cell, Vector, JsonData) ->
        Grid = proplists:get_value(grid, JsonData),
        Tile = grid:cellContent(Cell, Grid),
        case Tile =:= null of
            true -> JsonData;
            false ->
                { Farthest, Next } = findFarthestPosition(Cell, Vector, Grid),
                {struct, CurrJsonData} = Tile,
                CurrValue = proplists:get_value(value, CurrJsonData),
                NextTile = if
                    Next =:= null -> null;
                    true ->
                        grid:cellContent(Next, Grid)
                end,
                {NextValue, NextMerged} = if
                    NextTile =:= null -> {null, null};
                    true ->
                        NextJsonData = element(2, NextTile),
                        {proplists:get_value(value, NextJsonData), proplists:get_value(mergedFrom, NextJsonData)}
                end,
                if  CurrValue =:= NextValue,
                    NextMerged =:= null
                    ->
                        MergedValue = CurrValue * 2,
                        Merged = {
                            struct,
                            [
                                {value, MergedValue},
                                {mergedFrom, [Tile,NextTile]},
                                {previousPosition, null}
                            ]
                        },
                        NewGrid = grid:insertTile(Next, Merged, grid:removeTile(Cell, Grid)),
                        % Update the score
                        Score = proplists:get_value(score, JsonData) + MergedValue,
                        % The mighty 2048 tile
                        Won = if
                            MergedValue =:= 2048 -> true;
                            true -> false
                        end,
                        Removed = proplists:delete(score, proplists:delete(won, proplists:delete(grid, JsonData))),
                        [
                            {grid,NewGrid},
                            {won,Won},
                            {score,Score} |
                            Removed
                        ];
                    true ->
                        [
                            {
                                grid,
                                grid:moveTile(Cell, Farthest, proplists:get_value(grid, JsonData))
                            }
                            | proplists:delete(grid, JsonData)
                        ]
                end
        end.
    move(left, State) ->
        move(getVector(left), State);
    move(right, State) -> 
        move(getVector(right), State);
    move(up, State) -> 
        move(getVector(up), State);
    move(down, State) -> 
        move(getVector(down), State);
    move(Vector, State) ->
        {struct, JsonData} = State,
        case 
            proplists:get_value(over, JsonData) or (
                proplists:get_value(won, JsonData) and (not proplists:get_value(keepPlaying, JsonData))
            )
        of
            true -> State;
            _Else ->
                PreparedJsonData = updateBestScore(prepareTiles(JsonData)),
                { TraversalsX, TraversalsY } = buildTraversals(Vector),
                NewJsonData = process_travesals_y(
                    TraversalsY,
                    TraversalsX,
                    Vector,
                    PreparedJsonData
                ),
                NewGrid = proplists:get_value(grid, NewJsonData),
                Grid = proplists:get_value(grid, PreparedJsonData),
                if
                    NewGrid =/= Grid -> %If changed - add new tile
                        {struct, UserJsonData} = proplists:get_value(user, NewJsonData),
                        NewScore = proplists:get_value(score, NewJsonData),
                        Score = proplists:get_value(score, PreparedJsonData),
                        case NewScore > Score of true ->
                            db:insert(
                                proplists:get_value(score, NewJsonData),
                                proplists:get_value(id, UserJsonData)
                            );
                            _Else -> undefined
                        end,
                        Over = case movesAvailable(NewGrid) of
                            true -> false;
                            fale -> true % Game over!
                        end,
                        Removed = proplists:delete(grid, proplists:delete(over, NewJsonData)),
                        {struct,[{ grid, addRandomTile(NewGrid) }, { over, Over } | Removed ]};
                    true -> %return state otherwise
                        {struct,PreparedJsonData}
                end
        end.
    movesAvailable(_) ->
        true.
    updateBestScore(JsonData) ->
        [{ scores, db:select() } | proplists:delete(scores, JsonData) ].
    

    Init function - creates a new user if one has not been created. Or takes from a previous game.

    init(State) ->
        StateUser = proplists:get_value(user, element(2, State)),
        StateUserJsonData = element(2, StateUser),
        User = case proplists:get_value(id, StateUserJsonData) of
            null ->
                Name = proplists:get_value(name, StateUserJsonData),
                {rowid, Id} = db:createUser(Name),
                { struct, [{name, Name},{id, Id}]};
            _Else ->
                StateUser
        end,
        {
            struct,
            [
                {grid ,addStartTiles(grid:build())},
                {user , User},
                {score,0},
                {scores, db:select()},
                {won, false},
                {over, false},
                {keepPlaying, false}
            ]
        }.
    

    The main function is move. Responsible for recounting the playing field. There were difficulties here, mainly due to a lack of experience in functional programming.

    move(left, State) ->
        move(getVector(left), State);
    move(right, State) -> 
        move(getVector(right), State);
    move(up, State) -> 
        move(getVector(up), State);
    move(down, State) -> 
        move(getVector(down), State);
    move(Vector, State) ->
        {struct, JsonData} = State,
        case 
            proplists:get_value(over, JsonData) or (
                proplists:get_value(won, JsonData) and (not proplists:get_value(keepPlaying, JsonData))
            )
        of
            true -> State;
            _Else ->
                PreparedJsonData = updateBestScore(prepareTiles(JsonData)),
                { TraversalsX, TraversalsY } = buildTraversals(Vector),
                NewJsonData = process_travesals_y(
                    TraversalsY,
                    TraversalsX,
                    Vector,
                    PreparedJsonData
                ),
                NewGrid = proplists:get_value(grid, NewJsonData),
                Grid = proplists:get_value(grid, PreparedJsonData),
                if
                    NewGrid =/= Grid -> %If changed - add new tile
                        {struct, UserJsonData} = proplists:get_value(user, NewJsonData),
                        NewScore = proplists:get_value(score, NewJsonData),
                        Score = proplists:get_value(score, PreparedJsonData),
                        case NewScore > Score of true ->
                            db:insert(
                                proplists:get_value(score, NewJsonData),
                                proplists:get_value(id, UserJsonData)
                            );
                            _Else -> undefined
                        end,
                        Over = case movesAvailable(NewGrid) of
                            true -> false;
                            fale -> true % Game over!
                        end,
                        Removed = proplists:delete(grid, proplists:delete(over, NewJsonData)),
                        {struct,[{ grid, addRandomTile(NewGrid) }, { over, Over } | Removed ]};
                    true -> %return state otherwise
                        {struct,PreparedJsonData}
                end
        end.
    

    For example, to find out if a move is complete, I compare the old state and the new one. An external variable is not used as in the JS version. I don’t know if this will decrease performance. And then I check if the account has changed so as not to make unnecessary queries to the database.
    In general, with a functional approach, it is rarely necessary to pass many parameters to a function. What confuses me the most is that I pass TraversalsY, TraversalsX, Vector to process_travesals_y, although TraversalsY and TraversalsX are already dependent on Vector. But I decided to leave it this way.
    In order not to repeat the experience of “availableCells”, I wrote the function “process_travesals_y” more, but now it goes separately on X and separately on Y. And in the end, it calls “moveTile” for each non-zero element of the playing field. Which, in principle, is almost completely consistent with the JS-original.

    moveTile(Cell, Vector, JsonData) ->
        Grid = proplists:get_value(grid, JsonData),
        Tile = grid:cellContent(Cell, Grid),
        case Tile =:= null of
            true -> JsonData;
            false ->
                { Farthest, Next } = findFarthestPosition(Cell, Vector, Grid),
                {struct, CurrJsonData} = Tile,
                CurrValue = proplists:get_value(value, CurrJsonData),
                NextTile = if
                    Next =:= null -> null;
                    true ->
                        grid:cellContent(Next, Grid)
                end,
                {NextValue, NextMerged} = if
                    NextTile =:= null -> {null, null};
                    true ->
                        NextJsonData = element(2, NextTile),
                        {proplists:get_value(value, NextJsonData), proplists:get_value(mergedFrom, NextJsonData)}
                end,
                if  CurrValue =:= NextValue,
                    NextMerged =:= null
                    ->
                        MergedValue = CurrValue * 2,
                        Merged = {
                            struct,
                            [
                                {value, MergedValue},
                                {mergedFrom, [Tile,NextTile]},
                                {previousPosition, null}
                            ]
                        },
                        NewGrid = grid:insertTile(Next, Merged, grid:removeTile(Cell, Grid)),
                        % Update the score
                        Score = proplists:get_value(score, JsonData) + MergedValue,
                        % The mighty 2048 tile
                        Won = if
                            MergedValue =:= 2048 -> true;
                            true -> false
                        end,
                        Removed = proplists:delete(score, proplists:delete(won, proplists:delete(grid, JsonData))),
                        [
                            {grid,NewGrid},
                            {won,Won},
                            {score,Score} |
                            Removed
                        ];
                    true ->
                        [
                            {
                                grid,
                                grid:moveTile(Cell, Farthest, proplists:get_value(grid, JsonData))
                            }
                            | proplists:delete(grid, JsonData)
                        ]
                end
        end.
    


    On this, I think, the story about processing websocket requests through Erlang is finished. I will be happy to answer all questions.

    Also popular now: