Наверное на неделю игры 2048 на хабре уже не успеваю, но статья не столько о игре сколько о websocket сервере на Erlang. Небольшая предыстория. Когда начал играть в 2048, то просто не мог прекратить. В ущерб работе и семье. Поэтому принял решение, что играть за меня должен бот. Но загвоздка в том, что игра клиентская, из-за чего не ведется глобальный рейтинг и не так удобно играть без браузера. Поэтому я и решил сделать серверную часть, где был бы рейтинг. И где мог бы играть мой бот без браузера.
Отмечу, что это мой первый проект на Erlang. Много программистов боится Erlang, предполагая, что это сложно. Но на самом деле это не так. Плюс, я постараюсь высветлить моменты, которые не совсем очевидны новичку в Erlang.
Для упрощения много чего захардкожено. Но я всегда рад конструктивной критике и комментариям.
Ссылка на github — erl2048.
Ссылка на рабочий проект — erl2048. Но, думаю, под хабраэффектом проживет он недолго.
JavaScript
Как ни странно — начну с JS. Я не изменял оригинальные файлы, чтобы их можно было обновить с первичного репозитория, если понадобится. Я использовал:
- main.css;
- animframe_polyfill.js для requestAnimationFrame;
- html_actuator.js для всех анимаций
- keyboard_input_manager.js для событий клавиатуры, и, как показала практика, зря;
Я создал файл «main.js». Логика простая — браузер шлет на сервер события, и потом обновляет поле. Благо, animframe_polyfill создан таким образом, что принимает сформированный grid.
Что я добавил. Инициализация соединения:
var websocket = new Websocket(SERVER);
websocket
.connect()
.done(function(){
var myGame = new MyGame(websocket);
});
На скорую руку написал обертку над «Websocket». Она очень проста, чтобы приводить здесь исходный код.
Начало новой игры:
self.restart = function(evt){
websocket.send(JSON.stringify({
action:'start'
}));
};
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
}));
};
И самый большой.
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;
}
}
};
Как видно, игра полностью зависит от сервера, потому что все расчеты происходят там. Не так как, например, в моей игре Крестики нолики, где логика дублируется.
На самом деле, не понял, зачем в оригинале используется x и y в Tile, поэтому сервер обходится без них. А на клиенте уже дописываю, чтобы actuator сьел.
Также с сервера приходит список топ10 лучших игроков. Это нововведение моей версии. И еще игрок может изменять свой ник. Никаких регистраций и защит. Ввел имя и играй. Нужно навести на квадратик с best score чтобы увидеть общий рейтинг. Выглядит это так.
Использовать родной keyboard_input_manager не очень хорошо. Потому что теперь в поле ввода никнейма можно вводить не все символы. Но вы можете вставить свой ник с буфера обмена.
Плюс, я реализовал не весь функционал. Часть, что отвечает за «проигрыш» пока закрыта заглушкой, но это не очень влияет на игровой процесс. И продолжить игру после выигрыша пока нет возможности. Но выиграть еще не получилось.
Erlang
Эта часть будет более детально расписана. Для начала нужно установить rebar. Сделать это можно отсюда. Rebar может сгенерировать начальные файлы, но я их создавал вручную.
«rebar.config» — используется для автоматического скачивания и сборки зависимостей.
% 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
Чтобы скачать и собрать зависимости. Возможно понадобится установить «libsqlite3-dev» для sqlite драйвера.
Для запуска сервера я использую:
# rebar compile skip_deps=true; erl -pa ebin deps/*/ebin -eval 'starter:start().' -noshell -detached
После этого игра будет доступна на 8080 порту. На самом деле, научится запускать проект было самым сложным. Дальше — легче. Я создал специльный модуль «starter», который запускает все зависимости и приложение.
-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).
Теперь рассмотрю содержимое директории «src». Первое — файл «erl2048.app.src». Не знаю, на самом деле, для чего он нужен, но добавил и свой проект на всякий случай.
{application, erl2048, [
{description, "2048 game server."},
{vsn, "1"},
{modules, []},
{registered, [erl2048_sup]},
{applications, [
kernel,
stdlib,
cowboy
]},
{mod, {erl2048_app, []}},
{env, []}
]}.
%% 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}}.
Я так понимаю, что эта штука следит, чтобы приложение не падало и перезапускает в случае надобности. Взял из примера — решил оставить.
Теперь главный файл приложения — «erl2048_app.erl».
%% 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.
Здесь я уже могу кое-что объяснить. Во-первых, компилируются роуты для cowboy. Потом запускается cowboy и подключение к базе данных.
В роли субд выступает sqlite. Я рассматривал еще Postgresql, mongoDB и Redis. Но остановился на sqlite, так как он самый простой. Плюс хранит данные после перезапуска. Но, думаю, создаст большую нагрузку на приложение из-за чего оно скорее ляжет. Как бы там ни было — код модуля:
-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,
erlang:display({Score,DBScore}),
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}]).
Перейдем к модулю, который обрабатывает websocket соединения.
-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.
Поначалу я не понимал как оно все устроено. Оказывается, все очень просто. Есть состояние, которое задается при установке соединения. И которое передается в каждый обработчик запроса для каждого клиента свое. Основной метод здесь это «websocket_handle». Он принимает сообщение и состояние а возвращает ответ и состояние.
Для общение используется формат JSON. В Erlang он представляется структурой типа:
{struct, [
{key1, Value1},
{key2, Value2},
....
]}
Теперь непосредственно файлы игры. Самый простой «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).
Только и умеет, что создавать новый тайл и сохранять предыдущую позицию.
«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.
Обратите внимание на availableCells. В Erlang нужно по максимуму использовать рекурсию. Но здесь я сам себя перемудрил. Сначала сгенерировал лист, который в содержал листы с одной координатой и вторую координату. А потом вносил вторую к первой. Я решил больше так не делать. Остальные функции, думаю, очевидны.
И, основной файл игры. Так и называется «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
),
if
PreparedJsonData =/= NewJsonData -> %If changed - add new tile
Grid = proplists:get_value(grid, NewJsonData),
{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(Grid) of
true -> false;
fale -> true % Game over!
end,
Removed = proplists:delete(grid, proplists:delete(over, NewJsonData)),
{struct,[{ grid, addRandomTile(Grid) }, { over, Over } | Removed ]};
true -> %return state otherwise
{struct,PreparedJsonData}
end
end
.
movesAvailable(_) ->
true.
updateBestScore(JsonData) ->
[{ scores, db:select() } | proplists:delete(scores, JsonData) ].
Функция init — создает нового пользователя, если тот не был создан. Или берет из предыдущей игры.
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}
]
}.
Основная функция — move. Отвечает за пересчет игрового поля. Здесь были труднощи, в основном из-зи недостатка опыта функционального программирования.
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
),
if
PreparedJsonData =/= NewJsonData -> %If changed - add new tile
Grid = proplists:get_value(grid, NewJsonData),
{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(Grid) of
true -> false;
fale -> true % Game over!
end,
Removed = proplists:delete(grid, proplists:delete(over, NewJsonData)),
{struct,[{ grid, addRandomTile(Grid) }, { over, Over } | Removed ]};
true -> %return state otherwise
{struct,PreparedJsonData}
end
end.
Например, чтобы узнать, совершился ли ход, я сравниваю старое состояние и новое. Не используется внешняя переменная как в JS варианте. Не знаю, уменьшит ли это производительность. И потом проверяю изменился ли счет, чтобы не делать лишних запросов к БД.
Вообще, при функциональном подходе, редко когда требуется передавать много параметров в функцию. Здесь наибольше меня смущает то, что я передаю TraversalsY, TraversalsX, Vector в process_travesals_y, хотя TraversalsY и TraversalsX и так зависят от Vector. Но решил пока оставить так.
Чтобы не повторять опыт «availableCells» функцию «process_travesals_y» я расписал больше, но теперь она отдельно идет по X и отдельно по Y. И в итоге для каждого ненулевого элемента игрового поля вызывает «moveTile». Которая, в принципе, практически полностью соответствует JS-оригиналу.
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.
На этом, думаю, рассказ об обработке websocket запросов посредством Erlang закончен. С удовольствием отвечу на все вопросы.
Автор: peinguin