📜 ⬆️ ⬇️

2048 at Erlang

image Probably for a week of the 2048 game on Habré I do not have time, but the article is not so much about the game as about the websocket server on Erlang. A little background. When I started playing in 2048, I just could not stop it. To the detriment of work and family. Therefore, I decided that the bot should play for me. But the snag is that the game is client-side, which is why there is no global rating and it’s not so convenient to play without a browser. That's why I decided to make the server part where the rating would be. And where could my bot play without a browser.


I note that this is my first project on Erlang. Many programmers are afraid of Erlang, assuming that it is difficult. But actually it is not. Plus, I will try to highlight moments that are not entirely obvious to the newcomer to Erlang.

To simplify a lot of things are hard. But I am always glad to constructive criticism and comments.
The github link is erl2048 .
Reference to the working draft - erl2048 . But, I think, he will live not for long under a habraeffekt.
')

Javascript


Oddly enough - I'll start with JS. I have not changed the original files so that they can be updated from the primary repository, if necessary. I used:

I created the file "main.js". The logic is simple - the browser sends events to the server, and then updates the field. Fortunately, animframe_polyfill is designed 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); }); 

Quickly wrote a wrapper over "Websocket". It is very simple to provide 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.
Processing server response:
 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 server dependent, because all the calculations take place there. Not like, for example, in my game Tic tac toe , where the logic is duplicated.
In fact, I did not understand why x and y are used in the original in Tile, so the server does without them. And on the client already I finish, that actuator has eaten.
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 and protections. Entered the name and play. It is necessary to point the box with the best score to see the overall rating. It looks like this.



Using native keyboard_input_manager is not very good. Because now not all characters can be entered into the nickname input field. But you can paste your nickname from the clipboard.
Plus, I have implemented not all the functionality. The part that is responsible for the "loss" is still closed with a plug, but this does not really affect the gameplay. And to continue the game after winning is not possible. But the win has not happened yet.

Erlang


This part will be described 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” is 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 gd # rebar co 

To download and compile 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 - easier. I created a special starter module that runs all 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 directory "src". The first is the “erl2048.app.src” file. I do not know, in fact, what it is for, but I added my 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 fall and restarts if necessary. Took from the 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. First, the cowboy routes are compiled. Then the cowboy starts and the database connection.
In the role of subd performs sqlite. I also considered Postgresql, mongoDB and Redis. But I stopped at sqlite, as it is the easiest. Plus stores data after a restart. But, I think, it will create a greater load on the application because of what it will rather fall. 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 handles 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 all works. It turns out that everything is very simple. There is a state that is set when the connection is established. And that is passed to each request handler for each client. The main method here is “websocket_handle”. It accepts the message and the status and returns the response and status.
JSON format is used for communication. In Erlang, it is represented by a structure like:

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


Now directly the game files. The easiest "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). 

Only knows how to create a new tile and save the previous position.
"Grid.erl" is 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 the availableCells. In Erlang, you need to use recursion to the maximum. But here I was too clever by myself. First generated sheet, which contained sheets with one coordinate and the second coordinate. And then he made the second to the first. I decided not to do more. The remaining functions, I think, are obvious.
And, the main file of the game. 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) ]. 

The init function creates a new user if one has not been created. Or takes from the 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 the recalculation of the playing field. There were difficulties, mainly due to the lack of functional programming experience.

 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 was completed, I compare the old state with the new one. An external variable is not used as in the JS version. I do not know whether this will reduce performance. And then I check if the account has changed in order not to make unnecessary queries to the database.
In general, with the functional approach, it is rarely necessary to transfer many parameters to a function. What confuses me most is that I pass in TraversalsY, TraversalsX, Vector in process_travesals_y, although TraversalsY and TraversalsX already depend on Vector. But I decided to leave it for now.
In order not to repeat the experience of “availableCells”, I have described the process_travesals_y function more, but now it goes separately in X and separately in Y. And as a result, for each non-zero element of the playing field, calls “moveTile”. Which, in principle, almost completely corresponds to 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 it, I think, the story about processing websocket of requests by means of Erlang is finished. I am pleased to answer all questions.

Source: https://habr.com/ru/post/216517/


All Articles