Browse Source

add game server

Roman Dayneko 11 years ago
parent
commit
57493e5611
77 changed files with 17600 additions and 38 deletions
  1. 104 0
      apps/db/include/accounts.hrl
  2. 3 0
      apps/db/include/config.hrl
  3. 34 0
      apps/db/include/scoring.hrl
  4. 48 0
      apps/db/include/table.hrl
  5. 42 0
      apps/db/include/tournaments.hrl
  6. 3 0
      apps/db/rebar.config
  7. 9 0
      apps/db/src/db.app.src
  8. 9 0
      apps/db/src/db_app.erl
  9. 23 0
      apps/db/src/db_sup.erl
  10. 9 0
      apps/face/ebin/face.app
  11. BIN
      apps/face/ebin/hello_view.beam
  12. BIN
      apps/face/ebin/index.beam
  13. BIN
      apps/face/ebin/index_view.beam
  14. BIN
      apps/face/ebin/login.beam
  15. BIN
      apps/face/ebin/login_view.beam
  16. BIN
      apps/face/ebin/n2o_game.beam
  17. BIN
      apps/face/ebin/routes.beam
  18. BIN
      apps/face/ebin/users.beam
  19. BIN
      apps/face/ebin/web_app.beam
  20. BIN
      apps/face/ebin/web_sup.beam
  21. 1 0
      apps/face/priv/static/nitrogen
  22. 41 37
      apps/face/src/n2o_game.erl
  23. 116 0
      apps/face/src/n2o_game.erl~
  24. 1 1
      apps/rebar.config
  25. 5 0
      apps/server/include/authtoken.hrl
  26. 23 0
      apps/server/include/basic_types.hrl
  27. 36 0
      apps/server/include/classes.hrl
  28. 1 0
      apps/server/include/conf.hrl
  29. 3 0
      apps/server/include/config.hrl
  30. 244 0
      apps/server/include/game_okey.hrl
  31. 282 0
      apps/server/include/game_tavla.hrl
  32. 17 0
      apps/server/include/games.hrl
  33. 3 0
      apps/server/include/kamf.hrl
  34. 33 0
      apps/server/include/log.hrl
  35. 16 0
      apps/server/include/logging.hrl
  36. 163 0
      apps/server/include/requests.hrl
  37. 9 0
      apps/server/include/settings.hrl
  38. 6 0
      apps/server/include/social_actions.hrl
  39. 10 0
      apps/server/include/types.hrl
  40. 3 0
      apps/server/rebar.config
  41. 136 0
      apps/server/src/auth_server.erl
  42. 153 0
      apps/server/src/deck.erl
  43. 490 0
      apps/server/src/game_manager.erl
  44. 485 0
      apps/server/src/game_session.erl
  45. 203 0
      apps/server/src/game_stats.erl
  46. 27 0
      apps/server/src/game_sup.erl
  47. 20 0
      apps/server/src/id_generator.erl
  48. 86 0
      apps/server/src/lucky_sup.erl
  49. 283 0
      apps/server/src/midict.erl
  50. 33 0
      apps/server/src/nsg_crowd_lib.erl
  51. 10 0
      apps/server/src/nsg_games_app.erl
  52. 136 0
      apps/server/src/nsg_matrix_elimination.erl
  53. 1062 0
      apps/server/src/nsg_trn_elimination.erl
  54. 745 0
      apps/server/src/nsg_trn_lucky.erl
  55. 1156 0
      apps/server/src/nsg_trn_standalone.erl
  56. 20 0
      apps/server/src/okey/game_okey.erl
  57. 403 0
      apps/server/src/okey/game_okey_bot.erl
  58. 530 0
      apps/server/src/okey/game_okey_ng_desk.erl
  59. 584 0
      apps/server/src/okey/game_okey_ng_scoring.erl
  60. 1541 0
      apps/server/src/okey/game_okey_ng_table_trn.erl
  61. 1145 0
      apps/server/src/okey/game_okey_scoring.erl
  62. 110 0
      apps/server/src/okey/test_hands.erl
  63. 970 0
      apps/server/src/okey/test_okey.erl
  64. 89 0
      apps/server/src/okey_sup.erl
  65. 423 0
      apps/server/src/relay_ng.erl
  66. 13 0
      apps/server/src/server.app.src
  67. 18 0
      apps/server/src/tavla/game_tavla.erl
  68. 663 0
      apps/server/src/tavla/game_tavla_bot.erl
  69. 124 0
      apps/server/src/tavla/game_tavla_lib.erl
  70. 643 0
      apps/server/src/tavla/game_tavla_ng_desk.erl
  71. 210 0
      apps/server/src/tavla/game_tavla_ng_scoring.erl
  72. 1883 0
      apps/server/src/tavla/game_tavla_ng_table.erl
  73. 1305 0
      apps/server/src/tavla/game_tavla_ng_trn_paired.erl
  74. 300 0
      apps/server/src/tavla/test_tavla.erl
  75. 89 0
      apps/server/src/tavla_sup.erl
  76. 214 0
      apps/server/src/tests.erl
  77. 1 0
      apps/server/src/timestamp

+ 104 - 0
apps/db/include/accounts.hrl

@@ -0,0 +1,104 @@
+%%----------------------------------------------------------------------
+%% @author Vladimir Baranov <vladimir.b.n.b@gmail.com>
+%% @copyright Paynet Internet ve Bilisim Hizmetleri A.S. All Rights Reserved.
+%% @doc
+%% Definitions file for accounts and transactions related records and macroses
+%% @end
+%%--------------------------------------------------------------------
+
+-ifndef(NSM_ACCOUNTS).
+-define(NSM_ACCOUNTS, true).
+
+-type currency()         :: kakaush | quota | game_point | money.
+%% game events when points affected
+-type game_event_type()  :: round_start | round_end | game_start | game_end | tour_start.
+-type tournament_type()  :: standalone | elimination | pointing | lucky. %% FIXME: Copypasted from game_okey.hrl
+-type game_name()        :: okey | tavla. %% TODO: add other game names
+-type game_mode()        :: standard | evenodd | color | countdown | feellucky |
+                            esli | kakara. %%  TODO: add other game types
+-type account_id()       :: {string(), currency()}. %% {username, currency}.
+-type transaction_id()   :: string().
+
+-define(SYSTEM_ACCOUNT_ID, system).
+
+%% common fields for all accounts
+
+-record(account, {id          :: account_id(),
+				  debet       :: integer(),
+				  credit      :: integer(),
+				  last_change :: integer() %% can be negative. Store last change
+				                           %% to resolve conflicts just by applying
+                                           %% all changes to one of the conflicting
+
+				  }).
+
+-define(ACC_ID(Account, Currency), {Account, Currency}). %% account id helper
+
+-record(pointing_rule,
+		{
+		 id,            %% {Game, GameType, Rounds} | {Game, GameType}
+		 game,
+		 game_type,
+		 rounds,        %% rounds | points | undefined
+		 kakush_winner, %% kakush points will be assigned for the winner of the game.
+		 kakush_other,  %% kakush points will be assigned for the rest of the
+ 		                %% players other then winner, per player.
+		 quota,         %% quota, no of quota will be charged to players.
+		 game_points    %% game points will be assigned at the end of the game
+		                %% to winner player
+		}).
+
+%% Transaction info definitions. This records should be used as
+%% values in #transaction.info. 'ti' prefix = 'transaction info'.
+
+-record(ti_game_event,
+        {
+         id                           :: integer(),         %% GameId
+         type                         :: game_event_type(),
+         game_name                    :: game_name(),
+         game_mode                    :: game_mode(),
+         double_points                :: integer(),
+         tournament_type = standalone :: tournament_type()
+        }).
+
+-record(ti_payment,
+		{
+		 id                         :: integer()
+		}).
+
+-record(ti_admin_change,
+		{
+		 reason                     :: binary()
+		 }).
+
+-record(ti_default_assignment,
+		{
+
+		 }).
+
+-type transaction_info() :: #ti_game_event{} | #ti_payment{} | #ti_admin_change{}|#ti_default_assignment{}.
+
+-record(user_transaction, {user,top}).
+
+-record(transaction,
+		{
+		 id                         :: transaction_id(),
+		 commit_time                :: erlang:now(),
+		 amount                     :: integer(),    %% amount to move between accounts
+		 remitter                   :: account_id(), %% accout that gives money/points
+		 acceptor                   :: account_id(), %% account receive money/points
+		 currency                   :: currency(),   %% some of the points or money
+		 info                       :: transaction_info(),
+                 next,
+                 prev
+		 }).
+
+%% Currencies
+
+-define(CURRENCY_KAKUSH,               kakush).
+-define(CURRENCY_KAKUSH_CURRENCY,      kakush_currency). % used for gifts section, charged only when user buy package
+-define(CURRENCY_MONEY,       money).
+-define(CURRENCY_GAME_POINTS, game_point).
+-define(CURRENCY_QUOTA,       quota).
+
+-endif.

+ 3 - 0
apps/db/include/config.hrl

@@ -0,0 +1,3 @@
+-record(config, {key, value}).
+
+-define(DBA, kvs:config(dba)).

+ 34 - 0
apps/db/include/scoring.hrl

@@ -0,0 +1,34 @@
+-record(player_scoring, {
+    id,
+    temp, 
+    permanent, % top of Permanent Scoring record linked list
+    agregated_score %-- aggregated score for all game types is is a list
+                    %  [{game_okey, 15},{game_tavla, 12},...]
+
+    }).
+
+-record(scoring_record, {
+    id,          % id of that record
+    next,        % next record for traversal
+    prev,
+    game_id,     % game id for rematching and lost connections
+    who,         % player
+    all_players, % with other players
+    game_type,   % okey, tavla, batak 
+    game_kind,   % chanak, standard, even-odd
+    condition,   % reveal with even tashes, color okey reveal, show gosterge, batak 3 aces, tavla mars.
+    score_points,       % result score points for player
+    score_kakaush,      % result score kakuş for player
+    custom,      % erlang record for a specific game
+    timestamp    % now() of the record
+    }).
+
+-record(personal_score, { % total count of everything
+        uid,
+        games = 0,  
+        wins = 0,
+        loses = 0,
+        disconnects = 0,
+        points = 0,
+        average_time = 0
+    }).

+ 48 - 0
apps/db/include/table.hrl

@@ -0,0 +1,48 @@
+-include("types.hrl").
+
+-record(game_table, {id :: id_type() | '_', %% Dialyzer and record MatchSpec warnings http://j.mp/vZ8670
+                     name,
+                     gameid,
+                     trn_id,
+                     game_type,
+                     rounds :: integer() | 'undefined' | '_',
+                     sets :: integer() | 'undefined' | '_',
+                     owner :: username_type() | '_',
+                     timestamp,
+                     users = [] :: [username_type()] | '_',
+                     users_options = [] :: [username_type()] | '_',
+                     game_mode,
+                     game_options,
+                     game_speed,
+                     friends_only,
+                     invited_users = [] :: [username_type()] | '_',
+                     private :: boolean() | '_',
+                     feel_lucky = false :: boolean(),
+                     creator,
+                     age_limit,
+                     groups_only = [] :: [id_type()] | '_',
+                     gender_limit,
+                     location_limit = "",
+                     paid_only,
+                     deny_robots = false :: boolean() | '_',
+                     slang,
+                     deny_observers,
+                     gosterge_finish = false :: boolean() | '_',
+                     double_points = 1 :: integer(),
+                     game_state,
+                     game_process :: pid() | '_',
+                     game_module :: atom(),
+                     pointing_rules :: any() | '_', %% #pointing_rule{}
+                     pointing_rules_ex :: [] | '_', %% [#pointing_rule{}] - list of additional pointing rules,
+                                                    %% for example IFeelLucky for okey game
+                     game_process_monitor :: reference() | '_',
+                
+                     tournament_type = simple :: simple | paired | paired_lobby | tournament,
+                     robots_replacement_allowed = true :: boolean()
+    }).
+
+-record(save_game_table, {uid :: username_type() | '_', %% Dialyzer and record MatchSpec warnings http://j.mp/vZ8670
+                          id :: id_type() | '_',
+                          name,
+                          create_time,
+                          settings}).

+ 42 - 0
apps/db/include/tournaments.hrl

@@ -0,0 +1,42 @@
+-record(team,
+        {name, % { team name for now will bu just first player username }
+         id,
+         play_record, % { linked list history of played users under that ticket }
+         type }).
+
+-record(tournament,
+        {name, % { tournament name }
+         id,
+         game_type,
+         description,
+         creator,
+         created,
+         start_date,
+         start_time,
+         end_date,
+         status, % { activated, ongoing, finished, canceled }
+         quota,
+         tours,
+         awards, 
+         winners :: list(), % [{UserId, Position, GiftId}]
+         waiting_queue, % { play_record, added here when user wants to join tournament }
+         avatar,
+         owner,
+         players_count,
+         speed,
+         type,
+         game_mode }). % { eliminatin, pointing, etc }
+
+-record(play_record, % { tournament_player, game_record, tournament_info, choose your name :) }
+        {who, % { user }
+         tournament, % { tournament in which user played }
+         team, % { team under which user player tournament }
+         game_id, % { game id that user played under that team }
+
+         realname,
+         game_points,
+         kakush,
+         kakush_currency,
+         quota,
+         other }).
+

+ 3 - 0
apps/db/rebar.config

@@ -0,0 +1,3 @@
+{deps, [
+    {kvs,     ".*", {git, "https://github.com/synrc/kvs_core", "HEAD"}}
+]}.

+ 9 - 0
apps/db/src/db.app.src

@@ -0,0 +1,9 @@
+{application, db,
+ [
+  {description, "Distributed Persistance"},
+  {vsn, "1"},
+  {registered, []},
+  {applications, [kernel,stdlib,kvs]},
+  {mod, { db_app, []}},
+  {env, []}
+ ]}.

+ 9 - 0
apps/db/src/db_app.erl

@@ -0,0 +1,9 @@
+-module(db_app).
+-behaviour(application).
+-export([start/2, stop/1]).
+
+start(_StartType, _StartArgs) ->
+    db_sup:start_link().
+
+stop(_State) ->
+    ok.

+ 23 - 0
apps/db/src/db_sup.erl

@@ -0,0 +1,23 @@
+-module(db_sup).
+-behaviour(supervisor).
+-export([start_link/0]).
+-export([init/1]).
+-define(CHILD(I, Type), {I, {I, start_link, []}, permanent, 5000, Type, [I]}).
+
+start_link() ->
+    supervisor:start_link({local, ?MODULE}, ?MODULE, []).
+
+init([]) ->
+
+  RestartStrategy = one_for_one,
+    MaxRestarts = 1000,
+    MaxSecondsBetweenRestarts = 3600,
+
+    SupFlags = {RestartStrategy, MaxRestarts, MaxSecondsBetweenRestarts},
+
+    Restart = permanent,
+    Shutdown = 2000,
+    Type = worker,
+
+    {ok, { {one_for_one, 5, 10}, []} }.
+

+ 9 - 0
apps/face/ebin/face.app

@@ -0,0 +1,9 @@
+{application,face,
+             [{description,"Kakaranet Lite"},
+              {vsn,"1"},
+              {registered,[]},
+              {applications,[kernel,stdlib]},
+              {mod,{web_app,[]}},
+              {env,[]},
+              {modules,[hello_view,index,index_view,login,login_view,n2o_game,
+                        routes,users,web_app,web_sup]}]}.

BIN
apps/face/ebin/hello_view.beam


BIN
apps/face/ebin/index.beam


BIN
apps/face/ebin/index_view.beam


BIN
apps/face/ebin/login.beam


BIN
apps/face/ebin/login_view.beam


BIN
apps/face/ebin/n2o_game.beam


BIN
apps/face/ebin/routes.beam


BIN
apps/face/ebin/users.beam


BIN
apps/face/ebin/web_app.beam


BIN
apps/face/ebin/web_sup.beam


+ 1 - 0
apps/face/priv/static/nitrogen

@@ -0,0 +1 @@
+../../../../deps/n2o/priv/static/n2o

+ 41 - 37
apps/face/src/n2o_game.erl

@@ -19,21 +19,23 @@ init(_Transport, Req, _Opts, _Active) ->
     Req1 = wf:header(<<"Access-Control-Allow-Origin">>, <<"*">>, NewCtx#context.req),
     {ok, Req1, NewCtx}.
 
+%% {'KamfMessage',23,game_event,[{game,undefined},{event,okey_tile_taken},{args,[{player,<<"dusler">>},{pile,0},{revealed,null},{pile_height,43}]}]}
+
 stream(<<"ping">>, Req, State) ->
     wf:info("ping received~n"),
     {reply, <<"pong">>, Req, State};
 stream({text,Data}, Req, State) ->
-    %wf:info("Text Received ~p",[Data]),
+    wf:info("Text Received ~p",[Data]),
     self() ! Data,
     {ok, Req,State};
 stream({binary,Info}, Req, State) ->
-    % wf:info("Binary Received: ~p",[Info]),
+    wf:info("Binary Received: ~p",[Info]),
     Pro = binary_to_term(Info,[safe]),
     wf:info("N2O Unknown Event: ~p",[Pro]),
     Pickled = proplists:get_value(pickle,Pro),
     Linked = proplists:get_value(linked,Pro),
     Depickled = wf:depickle(Pickled),
-    % wf:info("Depickled: ~p",[Depickled]),
+    wf:info("Depickled: ~p",[Depickled]),
     case Depickled of
         #ev{module=Module,name=Function,payload=Parameter,trigger=Trigger} ->
             case Function of 
@@ -67,41 +69,43 @@ render_actions(InitActions) ->
     [RenderInit,RenderInitGenActions].
 
 info(Pro, Req, State) ->
-    Render = case Pro of
-        {flush,Actions} ->
-            % wf:info("Comet Actions: ~p",[Actions]),
-            wf:render(Actions);
-        <<"N2O,",Rest/binary>> ->
-            Module = State#context.module, Module:event(init),
-            InitActions = get(actions),
-            wf_context:clear_actions(),
-            Pid = wf:depickle(Rest),
-            %wf:info("Transition Pid: ~p",[Pid]),
-            case Pid of
-                undefined -> 
-                    %wf:info("Path: ~p",[wf:path(Req)]),
-                    %wf:info("Module: ~p",[Module]),
-                    Elements = try Module:main() catch C:E -> wf:error_page(C,E) end,
-                    wf_core:render(Elements),
-                    render_actions(InitActions);
+    Render = 
+        case Pro of
+            {flush,Actions} ->
+                                                % wf:info("Comet Actions: ~p",[Actions]),
+                wf:render(Actions);
+            <<"N2O,",Rest/binary>> ->
+                Module = State#context.module, Module:event(init),
+                InitActions = get(actions),
+                wf_context:clear_actions(),
+                Pid = wf:depickle(Rest),
+                                                %wf:info("Transition Pid: ~p",[Pid]),
+                case Pid of
+                    undefined -> 
+                                                %wf:info("Path: ~p",[wf:path(Req)]),
+                                                %wf:info("Module: ~p",[Module]),
+                        Elements = try Module:main() catch C:E -> wf:error_page(C,E) end,
+                        wf_core:render(Elements),
+                        render_actions(InitActions);
 
-                Transition ->
-                    X = Pid ! {'N2O',self()},
-                    R = receive Actions -> [ render_actions(InitActions) | wf:render(Actions) ]
-                    after 100 ->
-                          QS = element(14, Req),
-                          wf:redirect(case QS of <<>> -> ""; _ -> "" ++ "?" ++ wf:to_list(QS) end),
-                          []
-                    end,
-                    R
-                    end;
-        <<"PING">> -> [];
-        Unknown ->
-            M = State#context.module,
-            catch M:event(Unknown),
-            Actions = get(actions),
-            wf_context:clear_actions(),
-            wf:render(Actions) end,
+                    Transition ->
+                        X = Pid ! {'N2O',self()},
+                        R = receive Actions -> [ render_actions(InitActions) | wf:render(Actions) ]
+                            after 100 ->
+                                    QS = element(14, Req),
+                                    wf:redirect(case QS of <<>> -> ""; _ -> "" ++ "?" ++ wf:to_list(QS) end),
+                                    []
+                            end,
+                        R
+                end;
+            <<"PING">> -> [];
+            Unknown ->
+                wf:info("unk ~p", [Unknown]),
+                M = State#context.module,
+                catch M:event(Unknown),
+                Actions = get(actions),
+                wf_context:clear_actions(),
+                wf:render(Actions) end,
     GenActions = get(actions),
     wf_context:clear_actions(),
     RenderGenActions = wf:render(GenActions),

+ 116 - 0
apps/face/src/n2o_game.erl~

@@ -0,0 +1,116 @@
+-module(n2o_game).
+-author('Maxim Sokhatsky').
+-include_lib("n2o/include/wf.hrl").
+-export([init/4]).
+-export([stream/3]).
+-export([info/3]).
+-export([terminate/2]).
+
+-define(PERIOD, 1000).
+
+init(_Transport, Req, _Opts, _Active) ->
+    put(actions,[]),
+    Ctx = wf_context:init_context(Req),
+    NewCtx = wf_core:fold(init,Ctx#context.handlers,Ctx),
+    wf_context:context(NewCtx),
+    Res = ets:update_counter(globals,onlineusers,{2,1}),
+    wf:reg(broadcast,wf:peer(Req)),
+    wf:send(broadcast,{counter,Res}),
+    Req1 = wf:header(<<"Access-Control-Allow-Origin">>, <<"*">>, NewCtx#context.req),
+    {ok, Req1, NewCtx}.
+
+stream(<<"ping">>, Req, State) ->
+    wf:info("ping received~n"),
+    {reply, <<"pong">>, Req, State};
+stream({text,Data}, Req, State) ->
+    %wf:info("Text Received ~p",[Data]),
+    self() ! Data,
+    {ok, Req,State};
+stream({binary,Info}, Req, State) ->
+    % wf:info("Binary Received: ~p",[Info]),
+    Pro = binary_to_term(Info,[safe]),
+    wf:info("N2O Unknown Event: ~p",[Pro]),
+    Pickled = proplists:get_value(pickle,Pro),
+    Linked = proplists:get_value(linked,Pro),
+    Depickled = wf:depickle(Pickled),
+    % wf:info("Depickled: ~p",[Depickled]),
+    case Depickled of
+        #ev{module=Module,name=Function,payload=Parameter,trigger=Trigger} ->
+            case Function of 
+                control_event   -> lists:map(fun({K,V})-> put(K,V) end,Linked),
+                                   Module:Function(Trigger, Parameter);
+                api_event       -> Module:Function(Parameter,Linked,State);
+                event           -> lists:map(fun({K,V})-> put(K,V) end,Linked),
+                                   Module:Function(Parameter);
+                UserCustomEvent -> Module:Function(Parameter,Trigger,State) end;
+          _Ev -> wf:error("N2O allows only #ev{} events") end,
+
+    Actions = get(actions),
+    wf_context:clear_actions(),
+    Render = wf:render(Actions),
+
+    GenActions = get(actions),
+    RenderGenActions = wf:render(GenActions),
+    wf_context:clear_actions(),
+
+    {reply, [Render,RenderGenActions], Req, State};
+stream(Data, Req, State) ->
+    wf:info("Data Received ~p",[Data]),
+    self() ! Data,
+    {ok, Req,State}.
+
+render_actions(InitActions) ->
+    RenderInit = wf:render(InitActions),
+    InitGenActions = get(actions),
+    RenderInitGenActions = wf:render(InitGenActions),
+    wf_context:clear_actions(),
+    [RenderInit,RenderInitGenActions].
+
+info(Pro, Req, State) ->
+    Render = case Pro of
+        {flush,Actions} ->
+            % wf:info("Comet Actions: ~p",[Actions]),
+            wf:render(Actions);
+        <<"N2O,",Rest/binary>> ->
+            Module = State#context.module, Module:event(init),
+            InitActions = get(actions),
+            wf_context:clear_actions(),
+            Pid = wf:depickle(Rest),
+            %wf:info("Transition Pid: ~p",[Pid]),
+            case Pid of
+                undefined -> 
+                    %wf:info("Path: ~p",[wf:path(Req)]),
+                    %wf:info("Module: ~p",[Module]),
+                    Elements = try Module:main() catch C:E -> wf:error_page(C,E) end,
+                    wf_core:render(Elements),
+                    render_actions(InitActions);
+
+                Transition ->
+                    X = Pid ! {'N2O',self()},
+                    R = receive Actions -> [ render_actions(InitActions) | wf:render(Actions) ]
+                    after 100 ->
+                          QS = element(14, Req),
+                          wf:redirect(case QS of <<>> -> ""; _ -> "" ++ "?" ++ wf:to_list(QS) end),
+                          []
+                    end,
+                    R
+                    end;
+        <<"PING">> -> [];
+        Unknown ->
+            M = State#context.module,
+            catch M:event(Unknown),
+            Actions = get(actions),
+            wf_context:clear_actions(),
+            wf:render(Actions) end,
+    GenActions = get(actions),
+    wf_context:clear_actions(),
+    RenderGenActions = wf:render(GenActions),
+    wf_context:clear_actions(),
+    {reply, [Render,RenderGenActions], Req, State}.
+
+terminate(_Req, _State=#context{module=Module}) ->
+    % wf:info("Bullet Terminated~n"),
+    Res = ets:update_counter(globals,onlineusers,{2,-1}),
+    wf:send(broadcast,{counter,Res}),
+    catch Module:event(terminate),
+    ok.

+ 1 - 1
apps/rebar.config

@@ -1,3 +1,3 @@
-{sub_dirs, ["face"]}.
+{sub_dirs, ["db","server","face"]}.
 {lib_dirs, ["../apps"]}.
 {deps_dir, ["../deps"]}.

+ 5 - 0
apps/server/include/authtoken.hrl

@@ -0,0 +1,5 @@
+-record(authtoken, {
+          token :: string(),
+          id    :: 'PlayerId'()
+          }).
+

+ 23 - 0
apps/server/include/basic_types.hrl

@@ -0,0 +1,23 @@
+-ifndef(BASIC_TYPES).
+-define(BASIC_TYPES, true).
+-type 'tag'()        :: any().
+-type 'PlayerId'()   :: any().
+-type 'GameId'()     :: any().
+-type 'ChatId'()     :: any().
+-type 'SocialActionEnum'() :: integer().
+%-type proplist()     :: list(tuple(atom(), any())).
+-type 'MonitorRef'() :: reference().
+
+-record('PlayerInfo', {
+          id         :: 'PlayerId'(),
+          login      :: string(),
+          name       :: string(),
+          surname    :: string(),
+          age        :: integer(),
+          skill :: integer(),
+          score :: integer(),
+          avatar_url :: string(),
+          robot = false :: boolean()
+         }).
+
+-endif.

+ 36 - 0
apps/server/include/classes.hrl

@@ -0,0 +1,36 @@
+%%%
+%%% #object can be passed to amf:encode
+%%%
+-type amf0() :: any().
+
+-record(object, {
+          name          :: string(),
+          members = []  :: list(tuple(atom(), amf0()))
+         }).
+%%%
+%%% Next four are packed into #object before amf:encode
+%%%
+-record('KamfRequest', {
+          id        :: any(),
+          method    :: string(),
+          args = [] :: list(tuple(atom(), any()))
+         }).
+
+-record('KamfResponse', {
+          id      :: any(),
+          success :: boolean(),
+          result  :: any()
+         }).
+
+-record('KamfMessage', {
+          id         :: any(),
+          event_type :: string(),
+          args = []  :: list(tuple(atom(), any()))
+         }).
+
+-record('KamfFatalError', {
+          type   :: atom(), %% request or message
+          id     :: any(),
+          reason :: string()
+         }).
+

+ 1 - 0
apps/server/include/conf.hrl

@@ -0,0 +1 @@
+-define(LISTEN_PORT, nsx_opt:get_env(nsg_srv, game_srv_port, 9000)).

+ 3 - 0
apps/server/include/config.hrl

@@ -0,0 +1,3 @@
+-define(PRODUCTION, false).
+
+

+ 244 - 0
apps/server/include/game_okey.hrl

@@ -0,0 +1,244 @@
+%% data structures send over the wire
+-record('OkeyPiece', {
+          color = -1 :: integer(),           %% 1..4
+          value = -1 :: integer()            %% 1..13
+          %% color set to 1 and value set to zero mean that this is false okey
+         }).
+
+-define(FALSE_OKEY, #'OkeyPiece'{value = 0, color = 1}).
+
+-record('OkeyScoringDetail', {
+          reason = [] :: list(atom()),
+          score  = 0  :: integer()
+         }).
+-record('OkeyGameR', {
+          player_id,           %%
+          disconnected = false :: boolean(),
+          winner = <<"none">>  :: binary(), %% similar to skill_delta
+          good_shot            :: boolean(),
+          skill,               %% skill level the start of the game
+          skill_delta,         %% 0 for defeat, 0.25 for draw and 1 for win
+          score = 0,           %% total accumulated score for this set
+          score_delta = 0,     %% delta of okey game points
+          breakdown = []       :: list(#'OkeyScoringDetail'{}) %% breakdown of okey game points
+         }).
+-record('OkeySeriesResult', {
+          player_id :: 'PlayerId'(),
+          place :: integer(),
+          winner = <<"none">> :: binary(),
+          score :: integer()
+         }).
+-record('OkeyGameResults', {
+          game_id :: integer(),
+          start_datetime = 0 :: integer(),
+          end_datetime = 0   :: integer(),
+          results = []       :: list(#'OkeyGameR'{}),
+          series_results = [] :: list(#'OkeySeriesResult'{})
+         }).
+-record('PlayerOkeyStats', {
+          playerId, %% : int
+          playerName, %% : String;
+          level, %% : int; Number
+          levelName, %% : String;
+          badges, %% : Array; Array of int between [1; 5],
+          %%from 1 to 5 items in array
+
+          %%Okey Game Stats
+          skill,     %% : int;
+          score,     %% : int;
+          totalWins, %% : int;
+          totalLose, %% : int;
+          totalDisconnects, %% : int;
+          overalSuccessRatio, %% : Number;
+          averagePlayDuration, %% : Number;
+
+          %%Okey Game Detailed Stats
+          number8Tashes, %% : int;
+          numberColor, %% : int;
+          numberColorOkey, %% : int;
+          numberColorOdd, %% : int;
+          numberColorOddOkey, %% : int;
+          numberOkey, %% : int;
+          numberOdd %% : int;
+         }).
+
+
+%% incoming messages, wrapped in #kaka_game_action
+-record(okey_ready, {}).
+-record(okey_has_gosterge, {}).
+-record(okey_discard, {
+          tile :: #'OkeyPiece'{}
+         }).
+-record(okey_reveal, {
+          discarded :: #'OkeyPiece'{},
+          hand :: list(#'OkeyPiece'{} | null)
+         }).
+-record(okey_surrender, {
+         }).
+-record(okey_take, {
+          pile :: integer() %% 0 or 1
+         }).
+-record(okey_challenge, {
+          challenge = false :: boolean()
+         }).
+-record(okey_i_saw_okey, {}).
+-record(okey_i_have_8_tashes, {}).
+
+-record('OkeyTimeouts', {
+          speed             :: atom(),      %% [slow, normal, fast, blitz]
+          turn_timeout      :: integer(),   %% timeout value for player turn
+          challenge_timeout :: integer(),   %% timeout value for challenge
+          ready_timeout     :: integer(),   %% timeout value for ready msg
+          rematch_timeout   :: integer()    %% timeout value for general api #rematch{} msg
+         }).
+
+-type tournament() :: standalone | elimination | pointing | lucky.
+
+%% outgoing messages; wrapped into #'KakaMessage'
+-record(okey_game_info, {
+          players :: list(#'PlayerInfo'{}),
+          timeouts :: #'OkeyTimeouts'{},   %% timeout value for player turn
+          game_type :: atom(),
+          finish_with_gosterge :: boolean(),
+          pairs = null :: null | list(list('PlayerId'())),
+          table_name :: binary(),
+          sets   :: integer(), %% number of sets defined for this table
+          set_no :: integer(), %% number of current set
+          rounds :: integer(), %% number of rounds in this set
+          mul_factor    :: pos_integer(),
+          slang_flag    :: boolean(),
+          observer_flag :: boolean(),
+          pause_enabled = true :: boolean(),
+          social_actions_enabled = true :: boolean(),
+          tournament_type = standalone :: tournament(),
+          series_confirmation_mode = yes_exit :: yes_exit | no_exit | no
+         }).
+-record(okey_player_ready, {
+          player :: 'PlayerId'()
+         }).
+-record(okey_player_has_gosterge, {
+          player :: 'PlayerId'()
+         }).
+-record(okey_game_started, {
+          tiles         :: list(#'OkeyPiece'{}),
+          gosterge      :: #'OkeyPiece'{},
+          pile_height   :: integer(),
+          current_round :: integer,
+          current_set   :: integer,
+          game_type     = null :: atom(), %% FIXME Deprecated
+          game_speed    = null :: atom(), %% FIXME Deprecated
+          game_submode  = null :: atom(), %% FIXME Deprecated
+          chanak_points :: integer(),
+          round_timeout = null :: null | integer(),
+          set_timeout = null :: null | integer()
+         }).
+
+-record(okey_game_player_state, {
+          whos_move     :: 'PlayerId'(),
+          game_state    :: atom(),
+          piles         :: list(#'OkeyPiece'{} | null),
+          %% piles are in counterclock-wise order,
+          %% with addressie pile being first one
+          %% (the pile he is taking tiles from)
+          tiles         :: list(#'OkeyPiece'{}),
+          gosterge      :: #'OkeyPiece'{},
+          pile_height   :: integer(),
+          current_round :: integer(),
+          game_sub_type = null :: atom(), %% FIXME Deprecated
+          next_turn_in  :: integer() | atom(),
+          %% number of milliseconds until next turn or 'infinity'
+          paused = false :: boolean(),
+          chanak_points  = 0 :: integer(),
+          round_timeout = null :: null | integer(),
+          set_timeout = null :: null | integer()
+         }).
+
+-record(okey_next_turn, {
+          player                :: 'PlayerId'(),
+          can_challenge = false :: boolean()
+          }).
+
+-record(okey_disable_okey, {
+          player :: 'PlayerId'(),
+          who_disabled :: list('PlayerId'())
+         }).
+
+-record(okey_tile_taken, {
+          player :: 'PlayerId'(),
+          pile   ::  integer(),
+          revealed    :: #'OkeyPiece'{},
+          pile_height :: integer()
+          }).
+
+-record(okey_turn_timeout, {
+          tile_taken = null :: #'OkeyPiece'{} | null,
+          tile_discarded    :: #'OkeyPiece'{}
+          }).
+
+-record(okey_tile_discarded, {
+          player            :: 'PlayerId'(),
+          tile              :: #'OkeyPiece'{},
+          timeouted = false :: boolean
+         }).
+
+-record(okey_revealed, {
+          player    :: 'PlayerId'(),
+          discarded :: #'OkeyPiece'{},
+          hand      :: list(#'OkeyPiece'{} | null)
+         }).
+
+-record(okey_round_ended, {
+          good_shot         :: boolean(),
+          reason            :: atom(),
+          results           :: #'OkeyGameResults'{},
+          next_action       :: atom() | binary()
+         }).
+
+-record(okey_series_ended, {
+          standings :: list(#'OkeySeriesResult'{}),
+          dialog_type = yes_no :: yes_no | ok
+         }).
+
+-record(okey_turn_record, {
+          player_id       :: 'PlayerId'(),
+          place           :: integer(),
+          score           :: integer(),
+          status          :: atom() | binary() %% active | eliminated 
+         }).
+
+-record(okey_turn_result, {
+          turn_num         :: integer(),
+          records          :: list(#okey_turn_record{})
+         }).
+
+-record(okey_player_has_8_tashes, {
+           player          :: 'PlayerId'(),
+           value           :: integer()   %% 1 - 13
+         }).
+
+-record(okey_playing_tables, {
+          num              :: integer()
+         }).
+
+%%%%%
+%%%%%  Debug
+%%%%%
+-record(okey_debug, {}).
+
+
+-record(okey_player, {
+          pid                      :: pid(),
+          player_id                :: 'PlayerId'(),
+          player_info              :: #'PlayerInfo'{},
+          hand                     :: list(#'OkeyPiece'{}),
+          pile = []                :: list(#'OkeyPiece'{}),  %% a pile player draws from; aka pile number 1
+          skill                    :: integer(),
+          can_show_gosterge = true :: boolean()
+         }).
+
+-record('OkeySetState', {
+          round_cur,
+          round_max,
+          set_cur,
+          set_max
+         }).

+ 282 - 0
apps/server/include/game_tavla.hrl

@@ -0,0 +1,282 @@
+
+-type 'Color'()   :: integer(). %% 'black' or 'red'
+-type 'Position'()   :: integer(). %% 'black' or 'red'
+
+%%%%%%%%%%%%%%%%%%%%%%%
+%% Events data types
+%%%%%%%%%%%%%%%%%%%%%%%
+
+%%-record('TavlaPlace', { count :: integer } ).
+
+-record('TavlaAtomicMoveServer',
+         {from :: 'Position'(),
+          to   :: 'Position'(),
+          hits :: boolean(),
+          pips :: integer()
+         }).
+
+-record(tavla_checkers,
+        {color :: integer(),
+         number :: integer()
+        }).
+
+-record(tavla_tour_record, {
+          player_id       :: 'PlayerId'(),
+          place           :: integer(),
+          score           :: integer(),
+          status          :: atom() | binary() %% active | eliminated 
+         }).
+
+
+-record('TavlaPlayerScore', {
+           player_id      :: 'PlayerId'(),
+%%           reason         :: atom(),
+           winner = <<"none">> :: binary(),
+           score_delta    :: integer(),
+           score          :: integer()
+                         }).
+
+-record('TavlaSeriesResult', {
+          player_id :: 'PlayerId'(),
+          place :: integer(),
+          winner = <<"none">> :: binary(),
+          score :: integer()
+                             }).
+
+-record('PlayerTavlaStats', {
+          playerId, %% : int
+          playerName, %% : String;
+          level, %% : int; Number
+          levelName, %% : String;
+          badges, %% : Array; Array of int between [1; 5],
+          skill,     %% : int;
+          score,     %% : int;
+          totalWins, %% : int;
+          totalLose, %% : int;
+          totalDisconnects, %% : int;
+          overalSuccessRatio, %% : Number;
+          averagePlayDuration %% : Number;
+          }).
+
+-record(tavla_color_info, {
+          name :: any(),
+          color :: integer()
+                     }).
+
+%% -record(tavla_board, {
+%%           id :: integer(),
+%%           name :: any(),
+%%           players :: list(#'PlayerInfo'{}),
+%%           main = false :: boolean()
+%%                      }).
+
+-record('TavlaGameResults', {
+%%           game_id :: integer(),
+           players :: list(#'TavlaPlayerScore'{})
+                     }).
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%     EVENTS      %%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+-record(tavla_game_info, {
+          table_id          :: integer(),
+%%          game_type         :: atom(), %% TODO: Remove
+          table_name        :: binary(),
+          game_mode = undefined :: atom(),
+          sets              :: null | integer(), %% total number of sets
+          set_no            :: null | integer(), %% number of current set
+          tables_num        :: integer(),
+%%          current_round     :: integer(), %% TODO: Remove
+          rounds            :: integer(),
+          players           :: list(#'PlayerInfo'{}),
+          speed             :: atom(),      %% [slow, normal, fast, blitz]
+          turn_timeout      :: integer(),   %% timeout value for player turn
+%%          challenge_timeout :: integer(),   %% TODO: Remove, timeout value for challenge 
+          ready_timeout     :: integer(),   %% timeout value for ready msg
+%%          timeout           :: integer(),   %% TODO: Remove
+          mul_factor        :: pos_integer(),
+          slang_flag        :: boolean(),
+          observer_flag     :: boolean(),
+          pause_enabled = true :: boolean(),
+          social_actions_enabled = true :: boolean(),
+          tournament_type = standalone :: atom(), %% standalone | elimination | pointing | lucky
+          series_confirmation_mode = yes_exit :: yes_exit | no_exit | no
+                         }).
+
+-record(tavla_player_ready, {
+          table_id  :: integer(),
+          player :: 'PlayerId'()
+                     }).
+
+-record(tavla_game_started, {
+          table_id       :: integer(),
+          board          :: list(#tavla_checkers{} | null),
+%%          another_boards :: list(#tavla_board{}),
+          players        :: list(#tavla_color_info{}), %% TODO: Rename to players_colors
+          current_round  :: integer(),
+          round_timeout  :: null | integer(),
+          set_timeout    :: null | integer(),
+          do_first_move_competition_roll :: boolean()
+          }).
+
+-record(tavla_game_player_state, {
+          table_id             :: integer(),
+          board                :: null | list(#tavla_checkers{} | null),
+          dice                 :: list(null | integer()),
+          players_colors       :: list(#tavla_color_info{}),
+          whos_move            :: list(integer()), %% Color
+          game_state           :: initializing | first_move_competition | waiting_for_roll | waiting_for_move | finished,
+          current_round        :: integer(),
+          next_turn_in         :: integer() | infinity, %% milliseconds
+          paused = false       :: boolean(),
+          round_timeout = null :: null | integer(),
+          set_timeout = null   :: null | integer()
+         }).
+
+-record(tavla_won_first_move, {
+          table_id  :: integer(),
+          color     :: integer(),
+          player    :: 'PlayerId'(),
+          dice      :: list(integer()),
+          reroll    :: boolean()
+                     }).
+
+-record(tavla_next_turn, {
+          table_id  :: integer(),
+          color     :: integer(),
+          player    :: 'PlayerId'()
+                     }).
+
+-record(tavla_rolls, {
+          table_id  :: integer(),
+          color     :: integer(),
+          player    :: 'PlayerId'(),
+          dices     :: list(integer())
+                     }).
+
+-record(tavla_moves, {
+          table_id     :: integer(),
+          color        :: integer(),
+          player       :: 'PlayerId'(),
+          from         :: 'Position'(),
+          to           :: 'Position'(),
+          hits = false :: boolean(),
+          pips         :: integer()
+                          }).
+
+-record(tavla_turn_timeout, {
+          table_id     :: integer(),
+          color        :: integer(),
+          player       :: 'PlayerId'(),
+          dice         :: null | list(integer()),
+          moves        :: null | list(#'TavlaAtomicMoveServer'{})
+                            }).
+
+
+%-record(tavla_vidoes, {
+%          table_id  :: integer(),
+%          player   :: 'PlayerId'()
+%                      }).
+
+%-record(tavla_accepts, {
+%          table_id  :: integer(),
+%          player   :: 'PlayerId'(),
+%          accept   :: boolean()
+%                      }).
+
+%-record(tavla_timeouts, {
+%          table_id  :: integer(),
+%          player  :: 'PlayerId'()
+%                      }).
+
+-record(tavla_series_ended, {
+          table_id  :: integer(),
+          standings :: list(#'TavlaSeriesResult'{})
+                            }).
+
+-record(tavla_game_ended, {
+          table_id  :: integer(),
+          reason  :: binary(), %% "win", "round_timeout", "set_timeout"
+          winner  :: null | 'PlayerId'(),
+          results :: #'TavlaGameResults'{}
+                     }).
+
+-record(tavla_tour_result, {
+          table_id         :: integer(),
+          tour_num         :: integer(),
+          records          :: list(#tavla_tour_record{})
+         }).
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%     ACTION      %%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+-record(tavla_ready, {table_id  :: integer()}).
+
+-record(tavla_roll, {table_id :: any()}).
+
+-record('TavlaAtomicMove', {from :: 'Position'(), to :: 'Position'() } ).
+-record(tavla_move, { table_id  :: integer(), moves :: list(#'TavlaAtomicMove'{}), player :: 'PlayerId'() }).
+
+-record(tavla_skip, {table_id  :: integer()}).
+
+
+-record(tavla_request, {table_id  :: integer()}).
+
+-record(tavla_vido, {table_id :: integer()}).
+
+-record(tavla_vido_request, { table_id :: integer(),
+          from :: 'PlayerId'(),
+          to   :: 'PlayerId'()
+                    }).
+
+-record(tavla_vido_answer, { table_id  :: integer(),
+          from :: 'PlayerId'(),
+          to   :: 'PlayerId'(),
+          answer :: boolean()
+                    }).
+
+-record(tavla_ack, { table_id  :: integer(),
+       type :: atom(),
+       from :: 'PlayerId'(),
+       to   :: 'PlayerId'(),
+       answer :: boolean()}).
+
+-record(tavla_accepts_vido, { table_id  :: integer(),
+          accept :: boolean()
+                    }).
+
+-record(tavla_surrender, { table_id  :: integer()}).
+
+-record(tavla_surrender_request, { table_id  :: integer(),
+          from :: 'PlayerId'(),
+          to   :: 'PlayerId'()
+                    }).
+
+-record(tavla_surrender_answer, { table_id  :: integer(),
+          from :: 'PlayerId'(),
+          to   :: 'PlayerId'(),
+          answer :: boolean()
+                    }).
+
+-record(tavla_accept_timeout, { table_id  :: integer(),
+          accept = true:: boolean()
+         }).
+
+%% -record('TavlaSetState', {
+%%           round_cur,
+%%           round_max,
+%%           set_cur,
+%%           set_max
+%%          }).
+
+%% -record('TavlaTimeouts', {
+%%           speed             :: atom(),      %% [slow, normal, fast, blitz]
+%%           turn_timeout      :: integer(),   %% timeout value for player turn
+%%           challenge_timeout :: integer(),   %% timeout value for challenge
+%%           ready_timeout     :: integer(),   %% timeout value for ready msg
+%%           rematch_timeout   :: integer()    %% timeout value for general api #rematch{} msg
+%%          }).

+ 17 - 0
apps/server/include/games.hrl

@@ -0,0 +1,17 @@
+%%%
+%%% Message is sent when matchmaker finds match.
+%%%
+-record('PlayerResults', {
+          player_id,           %%
+          disconnected = false :: boolean(),
+          winner = <<"none">>  :: binary(), %% similar to skill_delta
+          skill,               %% skill level the start of the game
+          skill_delta,         %% 0 for defeat, 0.25 for draw and 1 for win
+          score = 0,           %% total accumulated score for this set
+          score_delta = 0     %% delta of okey game points
+         }).
+
+-record('GameResults', {
+          game_id :: integer(),
+          results = []       :: list(#'PlayerResults'{})
+         }).

+ 3 - 0
apps/server/include/kamf.hrl

@@ -0,0 +1,3 @@
+-define(KAMF_VERSION, 1).
+-define(KAMF_MAGIC, 16#ccaaccaa0001).
+-define(AMF, kamf).

+ 33 - 0
apps/server/include/log.hrl

@@ -0,0 +1,33 @@
+-define(LOG(Format, Args, Level, Tags), error_logger:info_msg("~p:~p " ++ Format,[?MODULE,?LINE] ++ Args)).
+
+-define(DBG(Format, Args, Tag), ?LOG(Format, Args, ?debug, Tag)).
+-define(DBG(Format, Args),      ?DBG(Format, Args, [])).
+-define(DBG(Format),            ?DBG(Format, [])).
+
+-define(INFO(Format, Args, Tag), ?LOG(Format, Args, ?info, Tag)).
+-define(INFO(Format, Args),      ?INFO(Format, Args, [])).
+-define(INFO(Format),            ?INFO(Format, [])).
+
+-define(NOTICE(Format, Args, Tag), ?LOG(Format, Args, ?notice, Tag)).
+-define(NOTICE(Format, Args),      ?NOTICE(Format, Args, [])).
+-define(NOTICE(Format),            ?NOTICE(Format, [])).
+
+-define(WARNING(Format, Args, Tag), ?LOG(Format, Args, ?warning, Tag)).
+-define(WARNING(Format, Args),      ?WARNING(Format, Args, [])).
+-define(WARNING(Format),            ?WARNING(Format, [])).
+
+-define(ERROR(Format, Args, Tag), ?LOG(Format, Args, ?error, Tag)).
+-define(ERROR(Format, Args),      ?ERROR(Format, Args, [])).
+-define(ERROR(Format),            ?ERROR(Format, [])).
+
+-define(CRITICAL(Format, Args, Tag), ?LOG(Format, Args, ?critical, Tag)).
+-define(CRITICAL(Format, Args),      ?CRITICAL(Format, Args, [])).
+-define(CRITICAL(Format),            ?CRITICAL(Format, [])).
+
+-define(ALERT(Format, Args, Tag), ?LOG(Format, Args, ?alert, Tag)).
+-define(ALERT(Format, Args),      ?ALERT(Format, Args, [])).
+-define(ALERT(Format),            ?ALERT(Format, [])).
+
+-define(EMERGENCY(Format, Args, Tag), ?LOG(Format, Args, ?emergency, Tag)).
+-define(EMERGENCY(Format, Args),      ?EMERGENCY(Format, Args, [])).
+-define(EMERGENCY(Format),            ?EMERGENCY(Format, [])).

+ 16 - 0
apps/server/include/logging.hrl

@@ -0,0 +1,16 @@
+-define(LOG(Format, Args),
+	io:format("~p:~p:~p:~p::: " ++ Format ++ "~n", [self(), ?MODULE, ?LINE, erlang:localtime() | Args])).
+
+-define(PP(Format, Args),
+	io:format("~p:~p:~p::: " ++ Format ++ "~n", [self(), ?MODULE, ?LINE | Args])).
+
+% -define(DEBUG(Format, Args),
+%         io:format("~p:~p:~p::: " ++ Format ++ "~n", [self(), ?MODULE, ?LINE | Args])).
+
+-define(DP(Format, Args),
+	io:format(user, "~p:~p:~p::: " ++ Format ++ "~n", [self(), ?MODULE, ?LINE | Args])).
+
+%% -define(PP(F, A), ok).
+%% -define(DP(F, A), ok).
+-define(DEBUG(F,A), ok).
+%% -define(LOG(F, A), ok).

+ 163 - 0
apps/server/include/requests.hrl

@@ -0,0 +1,163 @@
+-include("basic_types.hrl").
+-include("types.hrl").
+%%% Contains list of API requests, that can be made, using KamfRequest call
+%%% Name of record corresponds to method, memebers to params
+%%% All this requests are processed by session
+
+-record(session_attach, {
+          token :: string() %% shared secret, stored in auth_server
+         }).
+
+-record(session_attach_debug, {
+          token :: string(), %% shared secret, stored in auth_server
+          id :: string()
+         }).
+
+-record(login, {
+          username :: string(),
+          password :: string()
+         }).
+
+-record(logout, {}).
+
+-record(match_me, {
+          game_type :: binary()
+         }).
+
+-record(match_found, {
+          ref       :: any(),          %% request ref
+          game_id   :: 'GameId'(),     %% id of game
+          is_replacing = false :: boolean(), %% id of game
+          pid       :: pid()           %% relay, session should connect to
+         }).
+
+-record(join_game, {
+          game :: 'GameId'()
+         }).
+
+-record(rematch, {
+          game :: 'GameId'()
+         }).
+
+-record(get_game_info, {
+          game :: 'GameId'()
+         }).
+
+-record(get_player_info, {
+          player_id :: 'PlayerId'() | 0 %% 0 stands for currently logged in user
+         }).
+
+-record(get_player_stats, {
+          player_id :: 'PlayerId'() | 0, %% 0 stands for currently logged in user
+          game_type :: binary()
+         }).
+
+-record(subscribe_player_rels, {
+          players :: list()
+         }).
+
+-record(unsubscribe_player_rels, {
+          players :: list()
+         }).
+
+-record(chat, {
+          chat_id :: 'GameId'(),
+          message :: string()
+         }).
+
+-record(game_action, {
+          game      :: 'GameId'(),
+          action    :: string(),
+          args = [] :: proplist()
+         }).
+%%%
+%%% Events, passed via #KakaMessage
+%%%
+-record(game_event, {
+          game      :: 'GameId'(),
+          event     :: string(),
+          args = [] :: proplist()
+         }).
+
+-record(game_matched, {
+          ref  :: any(),
+          is_replacing = false :: boolean,
+          game :: 'GameId'()
+         }).
+
+-record(game_rematched, {
+          game :: 'GameId'()
+         }).
+
+-record(game_crashed, {
+          game :: 'GameId'()
+         }).
+
+-record(dummy_player_change, {
+          player :: 'PlayerId'()
+         }).
+
+-record(chat_msg, {
+          chat        :: 'GameId'(),
+          content     :: string(),
+          author_id   :: 'PlayerId'(),
+          author_nick :: string()
+         }).
+
+-record(social_action, {
+          game :: 'GameId'(),
+          type :: 'SocialActionEnum'(),
+          recipient :: 'PlayerId'() | null
+         }).
+
+-record(social_action_msg, {
+          type :: 'SocialActionEnum'(),
+          game :: 'GameId'(),
+          initiator :: 'PlayerId'(),
+          recipient :: 'PlayerId'() | null
+         }).
+
+-record(pause_game, {
+          table_id :: integer(),
+          game    :: 'GameId'(),
+          action  :: string()
+         }).
+
+-record(game_paused, {
+          table_id :: integer(),
+          game    :: 'GameId'(),
+          action  :: string(),
+          who     :: 'PlayerId'(),
+          retries :: integer()
+         }).
+
+-record(disconnect, {
+          reason_id = null :: null | string(),
+          reason = null :: null | string()
+         }).
+
+%% packet as game_event:
+-record(player_left, {
+          player :: 'PlayerId'(),
+          bot_replaced = false :: boolean(),    %% will be replaced by bot?
+          human_replaced = false :: boolean(),  %% will be replaced by human?
+          replacement = null :: 'PlayerId'() | null %% id of replacement player/bot
+         }).
+
+-record('TableInfo', {
+          chat    = []      :: list(any()),
+          viewers = []      :: list('PlayerId'()),
+          players = []      :: list('PlayerId'()),
+          game              :: atom(),
+          game_status       :: atom()
+         }).
+
+%%% tests
+-record(getobjecttypefromserver, {}).
+-record(getstringtypefromserver, {}).
+-record(getintegertypefromserver, {}).
+-record(getmixedtypesfromserver, {}).
+-record('some_named_object', {name1, name2, name3}).
+
+-record(fastping, {}).
+-record(slowping, {}).

+ 9 - 0
apps/server/include/settings.hrl

@@ -0,0 +1,9 @@
+-define(LOBBY_TIMEOUT, begin {ok, X}   = nsm_db:get(config,"games/lobby_timeout", 60000), X end).
+-define(REMATCH_TIMEOUT, begin {ok, X} = nsm_db:get(config,"games/rematch_timeout", 45000), X end).
+-define(ROBOT_DELAY, begin {ok, X}     = nsm_db:get(config,"games/okey/robot_delay", 500), X end).
+-define(SKILL_SEED, begin {ok, X}      = nsm_db:get(config, "games/okey/skill_seed", 500), X end). %% skill value for new player
+-define(IS_TEST, begin {ok, X}         = nsm_db:get(config,"debug/is_test", false), X end). %% determines if code is executed in test
+-define(ALL_SESSIONS, <<"all_sessions_topic">>).
+-define(TEST_TOKEN, "EBAs6dg2Xw6XuCdg8qiPmlBLgYJ6N4Ti0P+oGpWgYz4NW4nBBUzTe/wAuLYtPnjFpsjCExxSpV78fipmsPxcf+NGy+QKIM6rmVJhpnIlKf0bpFNuGaAPjZAWthhGO8nZ0V8UnA==").
+-define(TEST_TOKEN2, "EBAs6dg2Xw6XuCdg8qiPmlBLgYJ6N4Ti0P+oGpWgYz4NW4nBBUzTe/wAuLYtPnjFpsjCExxSpV78fipmsPxcf+NGy+QKIM6rmVJhpnIlKf0bpFNuGaAPjZAWthhGO8nZ0V8Un2==").
+-define(PAIRED_TAVLA_ASYNC, false).

+ 6 - 0
apps/server/include/social_actions.hrl

@@ -0,0 +1,6 @@
+-define(SOCIAL_ACTION_SUBSCRIBE, 1).
+-define(SOCIAL_ACTION_UNSUBSCRIBE, 2).
+-define(SOCIAL_ACTION_BLOCK, 3).
+-define(SOCIAL_ACTION_UNBLOCK, 4).
+-define(SOCIAL_ACTION_LOVE, 5).
+-define(SOCIAL_ACTION_HAMMER, 6).

+ 10 - 0
apps/server/include/types.hrl

@@ -0,0 +1,10 @@
+-ifndef(TYPES_HRL).
+-define(TYPES_HRL, true).
+
+-type username_type() :: string().
+-type id_type() :: integer().
+-type user_state() :: 'ok' | 'not_verified' | 'banned'.
+-type proplist() :: [proplists:property()].
+
+
+-endif.

+ 3 - 0
apps/server/rebar.config

@@ -0,0 +1,3 @@
+{lib_dirs, ["../../apps"]}.
+{deps_dir, ["../../deps"]}.
+{deps,[db]}.

+ 136 - 0
apps/server/src/auth_server.erl

@@ -0,0 +1,136 @@
+-module(auth_server).
+
+-include_lib("server/include/conf.hrl").
+-include_lib("server/include/settings.hrl").
+-include_lib("server/include/log.hrl").
+-include_lib("server/include/authtoken.hrl").
+-include_lib("server/include/requests.hrl").
+
+-behaviour(gen_server).
+
+-export([store_token/3,start_link/0,
+         robot_credentials/0,
+         fake_credentials/0,
+         get_user_info/1, get_user_info/2,
+         get_user_info_by_user_id/1
+        ]).
+
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
+
+-define(SERVER, ?MODULE).
+-define(SPARE_LOGINS, [#'PlayerInfo'{name = <<"Abe">>, surname="Kobo", login = <<"dunes">>, avatar_url = <<"/files/users/user_dunes/avatar/1-small.jpg">>},
+                       #'PlayerInfo'{name = <<"Herman">>, surname="Hesse", login = <<"wolves">>, avatar_url = <<"/files/users/user_wolves/avatar/1-small.jpg">>},
+                       #'PlayerInfo'{name = <<"Ernest">>, surname = <<"Hemingway">>, login = <<"oldman">>, avatar_url = <<"/files/users/user_oldman/avatar/1-small.jpg">>},
+                       #'PlayerInfo'{name = <<"Erich Maria">>, surname = <<"Remarque">>, login = <<"imwesten">>, avatar_url = <<"/files/users/user_imwesten/avatar/1-small.jpg">>}]).
+
+-record(state, {
+          spare = ?SPARE_LOGINS,
+          tokens
+         }).
+
+%% definition of user from zealot/include/user.hrl
+-record(user_info,
+        {username,
+         name,
+         surname,
+         age,
+	 avatar_url,
+         sex,
+         skill :: integer(),
+         score :: integer()}).
+
+start_link() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
+store_token(GameId, Token, UserId) when is_list(Token) -> store_token(GameId, list_to_binary(Token), UserId);
+store_token(GameId, Token, UserId) when is_binary(Token) -> gen_server:call(?SERVER, {store_token, GameId, Token, UserId}).
+get_user_info(Token) when is_list(Token)  -> get_user_info(list_to_binary(Token));
+get_user_info(Token) when is_binary(Token) -> gen_server:call(?SERVER, {get_user_info, Token}).
+get_user_info(Token, Id) when is_list(Token) -> get_user_info(list_to_binary(Token), Id);
+get_user_info(Token, Id) when is_list(Id) -> get_user_info(Token, list_to_binary(Id));
+get_user_info(Token, Id) when is_binary(Token), is_binary(Id) -> gen_server:call(?SERVER, {get_user_info, Token, Id}).
+get_user_info_by_user_id(UserId) when is_list(UserId) -> get_user_info_by_user_id(list_to_binary(UserId));
+get_user_info_by_user_id(UserId) -> user_info(UserId).
+fake_credentials() -> gen_server:call(?SERVER, {fake_credentials}).
+robot_credentials() -> gen_server:call(?SERVER, {robot_credentials}).
+
+init([]) ->
+    Tokens = ets:new(tokens, [private, ordered_set, {keypos, #authtoken.token}]),
+    store_token(0,Tokens, <<?TEST_TOKEN>>, "maxim"),
+    store_token(0,Tokens, <<?TEST_TOKEN2>>, "alice"),
+    {ok, #state{tokens = Tokens}}.
+
+handle_call({store_token, GameId, Token, UserId}, _From, #state{tokens = E} = State) ->
+    store_token(GameId, E, Token, UserId),
+    {reply, Token, State};
+
+handle_call({get_user_info, Token}, _From, #state{tokens = E} = State) ->
+    ?INFO("checking token: ~p", [Token]),
+    case ets:lookup(E, Token) of
+        [] ->
+            ?INFO("token not found", []),
+            {reply, false, State};
+        List ->
+            {authtoken, _, UserId} = hd(List),
+            ?INFO("token was registred, getting user info for ~p",[UserId]),
+	            proc_lib:spawn_link(fun() ->
+                                        Reply =
+                                            case user_info(UserId) of
+                                                {ok, UserInfo} ->
+                                                    ?INFO("..user info retrieved", []),
+                                                    UserInfo;
+                                                {error, user_not_found} ->
+                                                    ?INFO("..no such user info, providing fake credentials", []),
+                                                    fake_credentials0(State#state.spare); %% for eunit tests. FIX
+                                                {badrpc, _} ->
+                                                    ?INFO("..bad rpc, providing fake credentials", []),
+                                                    fake_credentials0(State#state.spare)  %% for eunit tests. FIX
+                                            end,
+                                        gen_server:reply(_From, Reply)
+                                end),
+            {noreply, State}
+    end;
+
+handle_call({get_user_info, Token, Id}, _From, #state{tokens = E} = State) ->
+    ?INFO("checking token: ~p", [Token]),
+    case ets:lookup(E, Token) of
+        [] ->
+            ?INFO("token not found", []),
+            {reply, false, State};
+        _List ->
+            Reply0 = fake_credentials0(State#state.spare),
+            Reply = Reply0#'PlayerInfo'{id = Id},
+            {reply, Reply, State}
+    end;
+
+
+handle_call({fake_credentials}, _From, #state{spare = Spare} = State) -> H = fake_credentials0(Spare), {reply, H, State};
+handle_call({robot_credentials}, _From, #state{spare = Spare} = State) -> H = fake_credentials0(Spare), {reply, H#'PlayerInfo'{robot = true}, State};
+handle_call(_Request, _From, State) -> Reply = ok, {reply, Reply, State}.
+handle_cast(_Msg, State) -> {noreply, State}.
+handle_info(_Info, State) -> {noreply, State}.
+terminate(_Reason, _State) -> ok.
+code_change(_OldVsn, State, _Extra) -> {ok, State}.
+
+fake_credentials0(Spare) ->
+    Pos = crypto:rand_uniform(1, length(Spare)),
+    H0 = lists:nth(Pos, Spare),
+    Id = list_to_binary(binary_to_list(H0#'PlayerInfo'.login) ++ integer_to_list(id_generator:get_id2())),
+    H0#'PlayerInfo'{id = Id}.
+
+store_token(GameId, E, Token, UserId) ->
+    ?INFO("storing token: ~p", [Token]),
+    Data = #authtoken{token = Token, id = UserId},
+    ets:insert(E, Data).
+
+user_info(UserId) ->
+    case nsm_auth:get_user_info(UserId) of
+        {ok, UserData} ->
+            {ok, #'PlayerInfo'{id = list_to_binary(UserData#user_info.username),
+                               login = list_to_binary(UserData#user_info.username),
+                               name = utils:convert_if(UserData#user_info.name, binary),
+                               avatar_url = utils:convert_if(UserData#user_info.avatar_url, binary),
+                               skill = UserData#user_info.skill,
+                               score = UserData#user_info.score,
+                               surname = utils:convert_if(UserData#user_info.surname, binary)}};
+        Error ->
+            Error
+    end.

+ 153 - 0
apps/server/src/deck.erl

@@ -0,0 +1,153 @@
+%% Author: Serge Polkovnikov <serge.polkovnikov@gmail.com>
+%% Created: Oct 8, 2012
+%% Description: The library for manipulations with decks.
+-module(deck).
+
+
+%% Predefined types of deck:
+%%    empty - No one element in the deck.
+%%
+%%    okey - The deck consist of such types of element: {Color, Value} | false_okey.
+%%         There are two instances of each element in the deck.
+%%              Color = 1..4
+%%              Value = 1..13
+
+%%
+%% Include files
+%%
+
+%%
+%% Exported Functions
+%%
+-export([
+         init_deck/1,
+         from_list/1,
+         to_list/1,
+         shuffle/1,
+         pop/2,
+         push/2,
+         get/2,
+         put/3,
+         replace/3,
+         size/1,
+         del_first/2,
+         member/2
+        ]).
+
+%%
+%% API Functions
+%%
+
+
+%% @spec init_deck(Type) -> Deck
+%% @doc Creates a deck.
+%% @end
+
+init_deck(empty) ->
+    [];
+init_deck(okey) ->
+    [false_okey, false_okey | [{C, V} || C <- [1,2,3,4], V <- lists:seq(1,13), _ <- [1,2]]].
+
+
+%% @spec from_list(List) -> Deck
+%% @doc Creates a deck from the list of elements.
+%% @end
+
+from_list(List) ->
+    List.
+
+
+%% @spec to_list(Deck) -> List
+%% @doc Convers the deck to a list of elements.
+%% @end
+
+to_list(Deck) ->
+    Deck.
+
+%% @spec shuffle(Deck1) -> Deck2
+%% @doc Shuffles the deck.
+%% @end
+
+shuffle(Deck) when is_list(Deck) ->
+    shuffle(Deck, deck:size(Deck), []).
+
+shuffle([], 0, Acc) -> Acc;
+shuffle(Deck, Size, Acc) ->
+    Pos = crypto:rand_uniform(1, Size+1),
+    {E, Deck1} = get(Pos, Deck),
+    shuffle(Deck1, Size-1, [E |Acc]).
+
+
+%% @spec pop(Num, Deck1) -> {Deck2, Deck3}
+%% @doc Takes specified number of elements from top of the deck.
+%% @end
+
+pop(Num, Deck) when Num > 0, is_list(Deck) ->
+    lists:split(Num, Deck).
+
+
+%% @spec push(Deck1, Deck2) -> Deck3.
+%% @doc Puts the first deck on the top of the second deck.
+%% @end
+
+push(Deck1, Deck2) when is_list(Deck1), is_list(Deck2) ->
+    Deck1 ++ Deck2.
+
+
+%% @spec get(Pos, Deck1) -> {E, Deck2}
+%% @doc Draws an element at the position Pos of the deck.
+%% Position is counted from top of the deck.
+%% @end
+
+get(Pos, Deck) when Pos > 0, is_list(Deck) ->
+    {Head, [E | Tail]} = lists:split(Pos - 1, Deck),
+    {E, Head ++ Tail}.
+
+%% @spec put(E, Pos, Deck1) -> Deck2
+%% @doc Inserts the element to the position Pos of the deck.
+%% Position is counted from top of the deck.
+%% @end
+
+put(E, Pos, Deck) when Pos > 0, is_list(Deck) ->
+    {Head, Tail} = lists:split(Pos - 1, Deck),
+    Head ++ [E | Tail].
+
+
+%% @spec replace(E1, E2, Deck1) -> Deck2
+%% @doc Replaces all instances of the element E1 in the deck by the element E2.
+%% @end
+
+replace(E1, E2, Deck) ->
+    [if E == E1 -> E2; true -> E end || E <- Deck].
+
+
+%% @spec size(Deck) -> Size
+%% @doc Returns a number of elements in the deck.
+%% @end
+
+size(Deck) when is_list(Deck) ->
+    length(Deck).
+
+
+%% @spec del_first(E, Deck1) -> {ok, Deck2} | error
+%% @doc Deletes the element from the deck.
+%% @end
+
+del_first(E, Deck) ->
+    case lists:member(E, Deck) of
+        true ->
+            {ok, lists:delete(E, Deck)};
+        false ->
+            error
+    end.
+
+%% @spec member(E, Deck) -> boolean()
+%% @doc Checks is the element a memeber of the deck.
+%% @end
+
+member(E, Deck) ->
+    lists:member(E, Deck).
+%%
+%% Local Functions
+%%
+

+ 490 - 0
apps/server/src/game_manager.erl

@@ -0,0 +1,490 @@
+-module(game_manager).
+-behaviour(gen_server).
+-author('Maxim Sokhatsky <maxim@synrc.com>').
+-compile(export_all).
+-export([init/1, start/0, start_link/0, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
+-include_lib("server/include/requests.hrl").
+-include_lib("server/include/conf.hrl").
+-include_lib("db/include/table.hrl").
+-include_lib("db/include/tournaments.hrl").
+-include_lib("server/include/log.hrl").
+-include_lib("stdlib/include/qlc.hrl").
+-include_lib("db/include/accounts.hrl").
+-record(state, { game_tavla = 0, game_okey = 0 }).
+
+destroy_game(Pid,Sup) -> game_sup:stop_game(Sup,Pid).
+
+gen_game_id() ->
+    PoolNum = nsx_opt:get_env(nsx_idgen,game_pool,5000000) div 1000000,
+    PoolNumStr = integer_to_list(PoolNum),
+    nsm_db:next_id("game_id_pool_"++PoolNumStr, PoolNum*1000000 + 200, 1). %% 200 is reserved for lucky games and for already created games
+
+create_game(GameId, GameFSM, Params) ->
+    {ok, Pid} = create_game_monitor(GameId, GameFSM, Params),
+    {ok, GameId, Pid}.
+
+%% rank_table(GameId) -> {ok, {LastTourNum, TourResult}} | {error, Reason}
+%% TourResult = {UserId, Pos, Points, Status}
+rank_table(GameId) ->
+    case get_relay_mod_pid(GameId) of
+        {Module, Pid} -> Module:system_request(Pid, last_tour_result);
+        undefined -> {error, not_found}
+    end.
+
+get_lucky_pid(Sup) ->
+    [X]=game_manager:get_lucky_table(Sup),
+    X#game_table.game_process.
+get_relay_pid(GameId) -> case get_tables(GameId) of [] -> undefined;
+    [#game_table{game_process = P} | _] -> ?INFO("GameRelay: ~p",[P]), P end.
+get_relay_mod_pid(GameId) -> case get_tables(GameId) of [] -> undefined;
+    [#game_table{game_process = P, game_module = M} | _] ->  ?INFO("GameRelay: ~p",[{M,P}]), {M,P} end.
+get_relay(GameId) -> gen_server:call(?MODULE, {get_relay, GameId}).
+game_requirements(GameAtom) -> GameAtom:get_requirements().
+game_requirements(game_tavla,paired) -> paired_tavla:get_requirements();
+game_requirements(GameAtom,_) -> GameAtom:get_requirements().
+counter(Game) -> PL = supervisor:count_children(case Game of game_okey -> okey_sup; 
+                                                            game_tavla -> tavla_sup; _ -> game_sup end),
+                 Res = proplists:get_value(active, PL, 0),
+                 case Game of
+                      game_okey -> Res;
+                      game_tavla -> Res;
+                      _ -> 0 end.
+
+start() -> gen_server:start({local, ?MODULE}, ?MODULE, [], []).
+start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
+stop(Ref) -> gen_server:cast(Ref, stop).
+
+init([]) -> {ok,#state{}}.
+
+handle_call({get_relay, Topic}, _From, State) -> Res = get_relay_pid(Topic), {reply, Res, State};
+handle_call({game_counter, FSM}, _From, State) ->
+    {reply, case FSM of game_tavla -> State#state.game_tavla; game_okey -> State#state.game_okey; _ -> 0 end, State};
+handle_call(Event, From, State) -> {stop, {unknown_call, Event, From}, State}.
+handle_cast(stop, State) -> {stop, normal, State};
+handle_cast(Event, State) -> {stop, {unknown_cast, Event}, State}.
+handle_info({'DOWN', _, process, Pid, Reason}, State) -> {noreply, State};
+handle_info(Info, State) -> {stop, {unknown_info, Info}, State}.
+terminate(_Reason, _State) -> ok.
+code_change(_OldVsn, State, _Extra) -> {ok, State}.
+
+game_sup_domain(Module, Params) ->
+    case Module of
+        game_tavla_ng_trn_paired -> tavla_sup;
+        nsg_trn_standalone ->
+            case proplists:get_value(game, Params) of
+                game_okey -> okey_sup;
+                game_tavla -> tavla_sup;
+                _ -> game_sup
+            end;
+        nsg_trn_elimination ->
+            case proplists:get_value(game_type, Params) of
+                game_okey -> okey_sup;
+                game_tavla -> tavla_sup;
+                _ -> game_sup
+            end;
+        _ -> game_sup
+    end.
+
+create_game_monitor(Topic, GameFSM, Params) ->
+    Sup = game_sup_domain(GameFSM, Params),
+    ?INFO("Create Root Game Process (Game Monitor2): ~p Params: ~p Sup: ~p",[GameFSM, Params,Sup]),
+    RelayInit = Sup:start_game(GameFSM,[Topic,Params],Topic),
+    ?INFO("RelayInit ~p",[RelayInit]),
+    RelayInit.
+
+get_lucky_table(Game) ->
+    Lucky = true,
+    Check = fun(undefined, _Value) -> true;
+               (Param, Value) ->  Param == Value
+            end,
+    Cursor = fun() ->
+                     qlc:cursor(qlc:q([V || {{_,_,_K},_,V=#game_table{game_type=G,
+                                                                      feel_lucky = L}}
+                                                <- gproc:table(props),
+                                            Check(Game, G),
+                                            Check(Lucky, L)]))
+             end,
+    Tables = qlc:next_answers(Cursor(), 1),
+    Tables.
+
+get_tournament(TrnId) ->
+    Check = fun(undefined, _Value) -> true;
+               (Param, Value) ->  Param == Value
+            end,
+    Cursor = fun() ->
+                     qlc:cursor(qlc:q([V || {{_,_,_K},_, V = #game_table{trn_id=TId}} <- gproc:table(props),
+                                            Check(TrnId, TId)]))
+             end,
+    Table = case qlc:next_answers(Cursor(), 1) of
+                   [T] -> X = T#game_table.id, X;
+                     _ -> []
+            end,
+%    ?INFO("~w:get_tournament Table = ~p", [?MODULE, Table]),
+    Table.
+
+
+%% stress_test(NumberOfRooms) ->
+%%     OkeyPlayers = [begin
+%%           {ok,GameId,A} = game_manager:create_table(game_okey,[{table_name,"okey maxim and alice + 2 robots"},
+%%                           {speed,normal},
+%%                           {rounds,80},
+%%                           {sets,1},
+%%                           {game_mode,standard},
+%%                           {owner,"kate"}],[<<"maxim">>,<<"alice">>,robot,robot]),
+%% 
+%%             Clients = [ proc_lib:spawn_link(fun() -> 
+%%                                  test_okey:init_with_join_game(self(), '127.0.0.1', ?LISTEN_PORT, GameId, Id, 1, normal)
+%%                         end) || Id <- [<<"maxim">>,<<"alice">>] ],
+%% 
+%%                     {ok,GameId,A}
+%%                   
+%% 
+%%                    end ||X<-lists:seq(1,NumberOfRooms)],
+%%     [{ok,OP1,_}|_] = OkeyPlayers,
+%%     [{ok,OP2,_}|_] = lists:reverse(OkeyPlayers),
+%%     ?INFO("Okey bot rooms runned (STRESS): ~p~n",[{OP1,OP2}]).
+
+
+create_standalone_game(Game, Params, Users) ->
+    ?INFO("create_standalone_game/3 Params:~p", [Params]),
+    case Game of
+        game_okey ->
+            #pointing_rule{quota = Quota,
+                           kakush_winner = KakushForWinners,
+                           kakush_other = KakushForLoser,
+                           game_points = WinGamePoints
+                          } = proplists:get_value(pointing_rules, Params),
+            GameId = proplists:get_value(game_id, Params),
+            TableName = proplists:get_value(table_name, Params),
+            MulFactor = proplists:get_value(double_points, Params, 1),
+            SlangAllowed = proplists:get_value(slang, Params, false),
+            ObserversAllowed = proplists:get_value(observers, Params, false),
+            Speed = proplists:get_value(speed, Params, normal),
+            GameMode = proplists:get_value(game_mode, Params),
+            Rounds = case GameMode of
+                         countdown -> undefined;
+                         _ -> proplists:get_value(rounds, Params, undefined)
+                     end,
+            GostergeFinishAllowed = proplists:get_value(gosterge_finish, Params, false),
+            BotsReplacementMode = case proplists:get_value(robots_replacement_allowed, Params, true) of
+                                      true -> enabled;
+                                      false -> disabled
+                                  end,
+            TableParams = [
+                           {table_name, TableName},
+                           {mult_factor, MulFactor},
+                           {slang_allowed, SlangAllowed},
+                           {observers_allowed, ObserversAllowed},
+                           {tournament_type, standalone},
+                           {round_timeout, infinity},
+%%                           {round_timeout, 30 * 1000},
+                           {set_timeout, infinity},
+%%                           {set_timeout, 10 * 60 *1000},
+                           {speed, Speed},
+                           {game_type, GameMode},
+                           {rounds, Rounds},
+                           {reveal_confirmation, true},
+                           {next_series_confirmation, no_exit},
+                           {pause_mode, normal},
+                           {social_actions_enabled, true},
+                           {gosterge_finish_allowed, GostergeFinishAllowed}
+                         ],
+
+            create_game(GameId, nsg_trn_standalone,
+                         [{game, Game},
+                          {game_mode, GameMode},
+                          {game_name, TableName},
+                          {seats, 4},
+                          {registrants, Users},
+                          {initial_points, 0},
+                          {quota_per_round, Quota},
+                          {kakush_for_winners, KakushForWinners},
+                          {kakush_for_loser, KakushForLoser},
+                          {win_game_points, WinGamePoints},
+                          {mul_factor, MulFactor},
+                          {table_module, game_okey_ng_table_trn},
+                          {bot_module, game_okey_bot},
+                          {bots_replacement_mode, BotsReplacementMode},
+                          {table_params, TableParams},
+                          {common_params, Params}
+                         ]);
+        game_tavla ->
+            #pointing_rule{quota = Quota,
+                           kakush_winner = KakushForWinners,
+                           kakush_other = KakushForLoser,
+                           game_points = WinGamePoints
+                          } = proplists:get_value(pointing_rules, Params),
+            GameId = proplists:get_value(game_id, Params),
+            TableName = proplists:get_value(table_name, Params),
+            MulFactor = proplists:get_value(double_points, Params, 1),
+            SlangAllowed = proplists:get_value(slang, Params, false),
+            ObserversAllowed = proplists:get_value(observers, Params, false),
+            Speed = proplists:get_value(speed, Params, normal),
+            GameMode = proplists:get_value(game_mode, Params),
+            Rounds = case GameMode of
+                         _ -> proplists:get_value(rounds, Params, undefined)
+                     end,
+            BotsReplacementMode = case proplists:get_value(robots_replacement_allowed, Params, true) of
+                                      true -> enabled;
+                                      false -> disabled
+                                  end,
+            TableParams = [
+                           {table_name, TableName},
+                           {mult_factor, MulFactor},
+                           {slang_allowed, SlangAllowed},
+                           {observers_allowed, ObserversAllowed},
+                           {tournament_type, standalone},
+                           {round_timeout, infinity},
+%%                           {round_timeout, 30 * 1000},
+                           {set_timeout, infinity},
+%%                           {set_timeout, 10 * 60 *1000},
+                           {speed, Speed},
+                           {game_mode, GameMode},
+                           {rounds, Rounds},
+                           {next_series_confirmation, no_exit},
+                           {pause_mode, normal},
+                           {social_actions_enabled, true},
+                           {tables_num, 1}
+                         ],
+
+            create_game(GameId, nsg_trn_standalone,
+                         [{game, Game},
+                          {game_mode, GameMode},
+                          {game_name, TableName},
+                          {seats, 2},
+                          {registrants, Users},
+                          {initial_points, 0},
+                          {quota_per_round, Quota},
+                          {kakush_for_winners, KakushForWinners},
+                          {kakush_for_loser, KakushForLoser},
+                          {win_game_points, WinGamePoints},
+                          {mul_factor, MulFactor},
+                          {table_module, game_tavla_ng_table},
+                          {bot_module, game_tavla_bot},
+                          {bots_replacement_mode, BotsReplacementMode},
+                          {table_params, TableParams},
+                          {common_params, Params}
+                         ])
+    end.
+
+
+create_paired_game(Game, Params, Users) ->
+    ?INFO("create_paired_game/3 Params:~p", [Params]),
+    case Game of
+        game_tavla ->
+            #pointing_rule{quota = Quota,
+                           kakush_winner = KakushForWinners,
+                           kakush_other = KakushForLoser,
+                           game_points = WinGamePoints
+                          } = proplists:get_value(pointing_rules, Params),
+            GameId = proplists:get_value(game_id, Params),
+            TableName = proplists:get_value(table_name, Params),
+            MulFactor = proplists:get_value(double_points, Params, 1),
+            SlangAllowed = proplists:get_value(slang, Params, false),
+            ObserversAllowed = proplists:get_value(observers, Params, false),
+            Speed = proplists:get_value(speed, Params, normal),
+            GameMode = proplists:get_value(game_mode, Params),
+            Rounds = case GameMode of
+                         _ -> proplists:get_value(rounds, Params, undefined)
+                     end,
+            BotsReplacementMode = case proplists:get_value(robots_replacement_allowed, Params, true) of
+                                      true -> enabled;
+                                      false -> disabled
+                                  end,
+            TablesNum = length(Users) div 2 + length(Users) rem 2,
+            TableParams = [
+                           {table_name, TableName},
+                           {mult_factor, MulFactor},
+                           {slang_allowed, SlangAllowed},
+                           {observers_allowed, ObserversAllowed},
+                           {tournament_type, paired},
+                           {round_timeout, infinity},
+                           {set_timeout, infinity},
+                           {speed, Speed},
+                           {game_mode, GameMode},
+                           {rounds, Rounds},
+                           {next_series_confirmation, no_exit},
+                           {pause_mode, disabled},
+                           {social_actions_enabled, true},
+                           {tables_num, TablesNum}
+                         ],
+
+            create_game(GameId, game_tavla_ng_trn_paired,
+                         [{game, Game},
+                          {game_mode, GameMode},
+                          {game_name, TableName},
+                          {tables_num, TablesNum},
+                          {registrants, Users},
+                          {quota_per_round, Quota},
+                          {kakush_for_winners, KakushForWinners},
+                          {kakush_for_loser, KakushForLoser},
+                          {win_game_points, WinGamePoints},
+                          {mul_factor, MulFactor},
+                          {table_module, game_tavla_ng_table},
+                          {bot_module, game_tavla_bot},
+                          {bots_replacement_mode, BotsReplacementMode},
+                          {table_params, TableParams},
+                          {common_params, Params}
+                         ])
+    end.
+
+
+create_elimination_trn(GameType, Params, Registrants) ->
+    ?INFO("create_elimination_trn/3 Params:~p", [Params]),
+    TrnId         = proplists:get_value(trn_id, Params),
+    QuotaPerRound = proplists:get_value(quota_per_round, Params),
+    PlayersNumber = proplists:get_value(players_number, Params),
+    Tours         = proplists:get_value(tours, Params),
+    GameMode      = proplists:get_value(game_mode, Params),
+    Speed         = proplists:get_value(speed, Params),
+    Awards        = proplists:get_value(awards, Params),
+    RegistrantsNum = length(Registrants),
+    if RegistrantsNum =/= PlayersNumber ->
+           ?ERROR("create_elimination_trn/3 Error: Wrong number of the registrants: ~p (required: ~p). ",
+                  [RegistrantsNum, PlayersNumber]),
+           exit(wrong_registrants_number);
+       true -> do_nothing
+    end,
+    {ok, Plan} = nsg_matrix_elimination:get_plan(GameType, QuotaPerRound, PlayersNumber, Tours),
+    case GameType of
+        game_okey ->
+            Rounds = 10,
+            {ok, SetTimeout} = nsm_db:get(config,"games/okey/trn/elim/tour_time_limit/"++integer_to_list(Tours), 35*60*1000),
+            TableParams = [
+                           {table_name, ""},
+                           {tournament_type, elimination},
+                           {round_timeout, infinity},
+                           {set_timeout, SetTimeout},
+                           {speed, Speed},
+                           {game_type, GameMode},
+                           {rounds, Rounds},
+                           {gosterge_finish_allowed, undefined},
+                           {reveal_confirmation, true},
+                           {next_series_confirmation, no},
+                           {pause_mode, disabled},
+                           {observers_allowed, false},
+                           {slang_allowed, false},
+                           {social_actions_enabled, false},
+                           {mult_factor, 1}
+                         ],
+            create_game(TrnId, nsg_trn_elimination,
+                        [{game_type, GameType},
+                         {game_mode, GameMode},
+                         {registrants, Registrants},
+                         {plan, Plan},
+                         {quota_per_round, QuotaPerRound},
+                         {rounds_per_tour, Rounds},
+                         {tours, Tours},
+                         {players_per_table, 4},
+                         {speed, Speed},
+                         {awards, Awards},
+                         {trn_id, TrnId},
+                         {table_module, game_okey_ng_table_trn},
+                         {demo_mode, false},
+                         {table_params, TableParams}
+                        ]);
+        game_tavla ->
+            Rounds = 3,
+            {ok, SetTimeout} = nsm_db:get(config,"games/tavla/trn/elim/tour_time_limit/"++integer_to_list(Tours), 35*60*1000),
+            TableParams = [
+                           {table_name, ""},
+                           {tournament_type, elimination},
+                           {round_timeout, infinity},
+                           {set_timeout, SetTimeout},
+                           {speed, Speed},
+                           {game_mode, GameMode},
+                           {rounds, Rounds},
+                           {next_series_confirmation, no},
+                           {pause_mode, disabled},
+                           {slang_allowed, false},
+                           {observers_allowed, false},
+                           {social_actions_enabled, false},
+                           {mult_factor, 1},
+                           {tables_num, 1}
+                         ],
+
+            create_game(TrnId, nsg_trn_elimination,
+                        [{game_type, GameType},
+                         {game_mode, GameMode},
+                         {registrants, Registrants},
+                         {plan, Plan},
+                         {quota_per_round, QuotaPerRound},
+                         {rounds_per_tour, Rounds},
+                         {tours, Tours},
+                         {players_per_table, 2},
+                         {speed, Speed},
+                         {awards, Awards},
+                         {trn_id,TrnId},
+                         {table_module, game_tavla_ng_table},
+                         {demo_mode, false},
+                         {table_params, TableParams}
+                        ])
+    end.
+
+
+start_tournament(TrnId,NumberOfTournaments,NumberOfPlayers,_Quota,_Tours,_Speed,GiftIds) ->
+    ?INFO("START TOURNAMENT: ~p",[{TrnId,NumberOfTournaments,NumberOfPlayers,_Quota,_Tours,_Speed,GiftIds}]),
+    {ok,Tournament} = nsm_db:get(tournament,TrnId),
+    RealPlayersUnsorted = nsm_tournaments:joined_users(TrnId),
+
+ if NumberOfPlayers - length(RealPlayersUnsorted) > 300 ->
+           nsm_db:put(Tournament#tournament{status=canceled}),
+           nsx_msg:notify([tournament, TrnId, cancel], {TrnId}),
+           error;
+       true ->
+
+    #tournament{quota = QuotaPerRound,
+                tours = Tours,
+                game_type = GameType,
+                game_mode = GameMode,
+                speed = Speed} = Tournament,
+%%    ImagioUsers = nsm_auth:imagionary_users2(),
+    RealPlayersPR = lists:keysort(#play_record.other, RealPlayersUnsorted),
+    ?INFO("Head: ~p",[hd(RealPlayersPR)]),
+    RealPlayers = [list_to_binary(Who)||#play_record{who=Who}<-RealPlayersPR, Who /= undefined],
+
+%%     Registrants = case NumberOfPlayers > length(RealPlayers) of
+%%                        true -> nsm_db:put(Tournament#tournament{status=canceled}), RealPlayers;
+%%                        false -> [lists:nth(N,RealPlayers)||N<-lists:seq(1,NumberOfPlayers)] end,
+    RealPlayersNumber = length(RealPlayers),
+    Registrants = if NumberOfPlayers == RealPlayersNumber -> RealPlayers;
+                     NumberOfPlayers > RealPlayersNumber ->
+                         RealPlayers ++ [list_to_binary(nsm_auth:ima_gio2(N)) ||
+                                            N <- lists:seq(1, NumberOfPlayers-RealPlayersNumber)];
+                     true -> lists:sublist(RealPlayers, NumberOfPlayers)
+                  end,
+
+    ?INFO("Registrants: ~p",[Registrants]),
+    OkeyTournaments =
+        [begin
+             Params = [{trn_id, TrnId},
+                       {quota_per_round, QuotaPerRound},
+                       {players_number, NumberOfPlayers},
+                       {tours, Tours},
+                       {game_mode, GameMode},
+                       {speed, Speed},
+                       {awards, GiftIds}],
+             {ok,GameId,A} = create_elimination_trn(GameType, Params, Registrants),
+             nsm_db:put(Tournament#tournament{status=activated,start_time=time()}),
+             nsx_msg:notify([tournament, TrnId, activate], {TrnId}),
+             {ok,GameId,A}
+         end || _ <-lists:seq(1,NumberOfTournaments)],
+    [{ok,OP1,_}|_] = OkeyTournaments,
+    [{ok,OP2,_}|_] = lists:reverse(OkeyTournaments),
+    ?INFO("Okey tournaments runned: ~p~n",[{OP1,OP2}]),
+    OP1
+
+   end.
+
+get_tables(Id) ->
+   qlc:e(qlc:q([Val || {{_,_,_Key},_,Val=#game_table{id = _Id}} <- gproc:table(props), Id == _Id ])).
+
+qlc_id(Id) ->
+    qlc:e(qlc:q([Val || {{_,_,_Key},_,Val=#game_table{gameid = _GameId, id = _Id, 
+                            owner = _Owner, creator = _Creator}} <- 
+             gproc:table(props), Id == _Id])).
+
+qlc_id_creator(Id,Creator,Owner) ->
+    qlc:e(qlc:q([Val || {{_,_,_Key},_,Val=#game_table{gameid = _GameId, id = _Id, 
+                            owner = _Owner, creator = _Creator}} <- 
+             gproc:table(props), Id == _Id, Creator == _Creator, Owner ==_Owner])).

+ 485 - 0
apps/server/src/game_session.erl

@@ -0,0 +1,485 @@
+-module(game_session).
+-behaviour(gen_server).
+
+-include_lib("server/include/social_actions.hrl").
+-include_lib("server/include/logging.hrl").
+-include_lib("server/include/games.hrl").
+-include_lib("server/include/classes.hrl").
+-include_lib("server/include/requests.hrl").
+-include_lib("server/include/settings.hrl").
+-include_lib("server/include/log.hrl").
+
+-export([start_link/1]).
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
+-export([process_request/3]).
+-export([bot_session_attach/2]).
+
+-define(SERVER, ?MODULE).
+
+-record(state, {
+          user = undefined,
+          rpc,     %% rpc process
+          rpc_mon, %% rpc monitor referense
+          games = [],
+          rels_notif_channel = undefined,
+          rels_players =[]
+         }).
+
+-record(participation, {
+          game_id       :: 'GameId'(), %% generated by id_generator
+          reg_num       :: integer(),
+          rel_module    :: atom(),
+          rel_pid       :: pid(),      %% relay, which handles communication gameman maps game_id onto pid
+          tab_module    :: atom(),
+          tab_pid       :: pid(),
+          ref           :: any(),      %% monitor reference to relay
+          role = viewer :: atom()      %% [viewer, player, ghost]
+         }).
+
+start_link(RPC) when is_pid(RPC) ->
+    gen_server:start_link(?MODULE, [RPC], []).
+
+bot_session_attach(Pid, UserInfo) ->
+    gen_server:cast(Pid, {bot_session_attach, UserInfo}).
+
+process_request(Pid, Source, Msg) ->
+    ?INFO("API from ~p payload ~p pid ~p",[Source,Msg,Pid]),
+    gen_server:call(Pid, {client_request, Msg}).
+
+%% gen_server callbacks
+
+init([RPC]) ->
+    MonRef = erlang:monitor(process, RPC),
+    {ok, #state{rpc = RPC, rpc_mon = MonRef}}.
+
+handle_call({client_request, Request}, From, State) ->
+    handle_client_request(Request, From, State);
+
+handle_call(Request, From, State) ->
+    ?INFO("unrecognized call: ~p", [Request]),
+    {stop, {unknown_call, From, Request}, State}.
+
+
+handle_cast({bot_session_attach, UserInfo}, State = #state{user = undefined}) ->
+%    ?INFO("bot session attach", []),
+    {noreply, State#state{user = UserInfo}};
+
+handle_cast(Msg, State) ->
+    ?INFO("session: unrecognized cast: ~p", [Msg]),
+    {stop, {error, {unknown_cast, Msg}}, State}.
+
+
+handle_info({relay_event, SubscrId, RelayMsg}, State) ->
+    handle_relay_message(RelayMsg, SubscrId, State);
+
+handle_info({relay_kick, SubscrId, Reason}, State) ->
+    ?INFO("Recived a kick notification from the table: ~p", [Reason]),
+    handle_relay_kick(Reason, SubscrId, State);
+
+handle_info({delivery, ["user_action", Action, Who, Whom], _} = Notification,
+            #state{rels_players = RelsPlayers,
+                   user = User,
+                   rpc = RPC
+                  } = State) ->
+    ?INFO("~w:handle_info/2 Delivery: ~p", [?MODULE, Notification]),
+    UserId = User#'PlayerInfo'.id,
+    case list_to_binary(Who) of
+        UserId ->
+            PlayerId = list_to_binary(Whom),
+            case lists:member(PlayerId, RelsPlayers) of
+                true ->
+                    Type = case Action of
+                               "subscribe" -> ?SOCIAL_ACTION_SUBSCRIBE;
+                               "unsubscribe" -> ?SOCIAL_ACTION_UNSUBSCRIBE;
+                               "block" -> ?SOCIAL_ACTION_BLOCK;
+                               "unblock" -> ?SOCIAL_ACTION_UNBLOCK
+                           end,
+                    Msg = #social_action_msg{initiator = UserId,
+                                             recipient = PlayerId,
+                                             type = Type
+                                            },
+                    % TODO: put real db change notification from users:343 module here
+                    %       nsx_msg:notify_db_subscription_change
+                    %       should be additionaly subscribed in bg feed worker binded to USER_EXCHANGE
+
+                    ok = send_message_to_player(RPC, Msg);
+                false ->
+                    do_nothing
+            end;
+        _ ->
+            do_nothing
+    end,
+    {noreply, State};
+
+
+handle_info({'DOWN', MonitorRef, _Type, _Object, _Info} = Msg, State = #state{rpc_mon = MonitorRef}) ->
+    ?INFO("connection closed, shutting down session:~p", [Msg]),
+    {stop, normal, State};
+
+handle_info({'DOWN', OtherRef, process, _Object, Info} = _Msg,
+            #state{games = Games, rpc = RPC} = State) ->
+    case lists:keyfind(OtherRef, #participation.ref, Games) of
+        #participation{} ->
+            ?INFO("The table is down: ~p", [Info]),
+            ?INFO("Closing the client and sutting down the session.", []),
+            send_message_to_player(RPC, #disconnect{reason_id = <<"tableDown">>,
+                                                    reason = <<"The table you are playing on is unexpectedly down.">>}),
+            {stop, table_down, State};
+        _ ->
+            {noreply, State}
+    end;
+
+handle_info(Info, State) ->
+    ?INFO("session: unrecognized info: ~p", [Info]),
+    {noreply, State}.
+
+terminate(Reason, #state{rels_notif_channel = RelsChannel}) ->
+    ?INFO("terminating session. reason: ~p", [Reason]),
+    if RelsChannel =/= undefined -> nsm_mq_channel:close(RelsChannel);
+       true -> do_nothing end,
+    ok.
+
+code_change(_OldVsn, State, _Extra) ->
+    {ok, State}.
+
+%%===================================================================
+
+handle_client_request(#session_attach{token = Token}, _From,
+                      #state{user = undefined} = State) ->
+    ?INFO("checking session token: ~p", [Token]),
+    case auth_server:get_user_info(Token) of
+        false ->
+            ?INFO("failed session attach: ~p", [Token]),
+            {stop, normal, {error, invalid_token}, State};
+        UserInfo ->
+            ?INFO("successfull session attach. Your user info: ~p", [UserInfo]),
+            {reply, UserInfo, State#state{user = UserInfo}}
+    end;
+
+handle_client_request(#session_attach_debug{token = Token, id = Id}, _From,
+                      #state{user = undefined} = State) ->
+    ?INFO("checking debug session token: ~p", [{Token,Id}]),
+    case {?IS_TEST, auth_server:get_user_info(Token, Id)} of
+        {_Test, false} ->
+            ?INFO("... ~p", [{_Test,false}]),
+            {stop, normal, {error, invalid_token}, State};
+        {false, true} ->
+            ?INFO("... ~p", [{false,true}]),
+            {stop, normal, {error, invalid_token}, State};
+        {true, UserInfo} ->
+            ?INFO("... ~p", [{true,UserInfo}]),
+            {reply, UserInfo, State#state{user = UserInfo}};
+        {false, UserInfo} ->
+            ?INFO("... ~p", [{true,UserInfo}]),
+            {reply, UserInfo, State#state{user = UserInfo}}
+    end;
+
+handle_client_request(_, _From, #state{user = undefined} = State) ->
+    ?INFO("unknown session call", []),
+    {reply, {error, do_session_attach_first}, State};
+
+handle_client_request(#get_game_info{}, _From, State) ->
+    ?INFO("session get game info", []),
+    {reply, {error, not_implemented}, State};
+
+handle_client_request(#logout{}, _From, State) ->
+    ?INFO("client requests #logout{}", []),
+    {stop, normal, ok, State};
+
+handle_client_request(#get_player_stats{player_id = PlayerId, game_type = Game}, _From,
+                      State) when is_binary(Game) ->
+    ?INFO("get player stats", []),
+    GameModule = api_utils:gametype_to_gamemodule(api_utils:gametype_to_atom(Game)),
+    Res = GameModule:get_player_stats(PlayerId),
+    {reply, Res, State};
+
+handle_client_request(#chat{chat_id = GameId, message = Msg0}, _From,
+                      #state{user = User, games = Games} = State) ->
+    ?INFO("chat", []),
+    Msg = #chat_msg{chat = GameId, content = Msg0,
+                    author_id = User#'PlayerInfo'.id,
+                    author_nick = User#'PlayerInfo'.login
+                   },
+    Participation = get_relay(GameId, Games),
+    Res = case Participation of
+              false ->
+                  {error, chat_not_registered};
+              #participation{rel_pid = Srv, rel_module = RMod} ->
+                  RMod:publish(Srv, Msg)
+          end,
+    {reply, Res, State};
+
+handle_client_request(#social_action_msg{type=Type, initiator=P1, recipient=P2}, _From,
+                      #state{user = User} = State) when User =/= undefined ->
+    UserIdBin = User#'PlayerInfo'.id,
+    ?INFO("social action msg from ~p to ~p (casted by ~p)", [P1, P2, UserIdBin]),
+    UserId = binary_to_list(UserIdBin),
+    case Type of
+        ?SOCIAL_ACTION_SUBSCRIBE ->
+            Subject = binary_to_list(P2),
+            nsm_users:subscribe_user(UserId, Subject),
+            {reply, ok, State};
+        ?SOCIAL_ACTION_UNSUBSCRIBE ->
+            Subject = binary_to_list(P2),
+            nsm_users:remove_subscribe(UserId, Subject),
+            {reply, ok, State};
+        ?SOCIAL_ACTION_BLOCK ->
+            Subject = binary_to_list(P2),
+            nsx_msg:notify(["subscription", "user", UserId, "block_user"], {Subject}),
+            {reply, ok, State};
+        ?SOCIAL_ACTION_UNBLOCK ->
+            Subject = binary_to_list(P2),
+            nsx_msg:notify(["subscription", "user", UserId, "unblock_user"], {Subject}),
+            {reply, ok, State};
+        ?SOCIAL_ACTION_LOVE ->
+            {reply, ok, State};
+        ?SOCIAL_ACTION_HAMMER ->
+            {reply, ok, State};
+        UnknownAction ->
+            ?ERROR("Unknown social action msg from ~p to ~p: ~w", [P1,P2, UnknownAction]),
+            {reply, {error, unknown_action}, State}
+    end;
+
+handle_client_request(#social_action{} = Msg, _From,
+                      #state{user = User, games = Games} = State) ->
+    ?INFO("social action", []),
+    GameId = Msg#social_action.game,
+    Res = #social_action_msg{type = Msg#social_action.type,
+                             game = GameId,
+                             recipient = Msg#social_action.recipient,
+                             initiator = User#'PlayerInfo'.id},
+    Participation = get_relay(GameId, Games),
+    Ans = case Participation of
+              false ->
+                  {error, chat_not_registered};
+              #participation{rel_pid = Srv, rel_module=RMod} ->
+                  RMod:publish(Srv, Res)
+          end,
+    {reply, Ans, State};
+
+
+handle_client_request(#subscribe_player_rels{players = Players}, _From,
+            #state{user = User, rels_notif_channel = RelsChannel,
+                   rels_players = RelsPlayers, rpc = RPC} = State) ->
+    ?INFO("subscribe player relations notifications: ~p", [Players]),
+    UserId = User#'PlayerInfo'.id,
+    UserIdStr = binary_to_list(UserId),
+    %% Create subscription if we need
+    NewRelsChannel =
+        if RelsChannel == undefined ->
+               {ok, Channel} = nsx_msg:subscribe_for_user_actions(UserIdStr, self()),
+               Channel;
+           true ->
+               RelsChannel
+        end,
+    %% Add players relations to which we need to common list
+    F = fun(PlayerId, Acc) ->
+                case lists:member(PlayerId, Acc) of
+                    true -> Acc;
+                    false -> [PlayerId | Acc]
+                end
+        end,
+    NewRelsPlayers = lists:foldl(F, RelsPlayers, Players),
+
+    %% Notify the client about current state of subscription relations.
+    %% (Blocking relations state should be "false" at the start)
+    F2 =
+        fun(PlayerId) ->
+                PlayerIdStr = binary_to_list(PlayerId),
+                Type = case nsm_users:is_user_subscr(UserIdStr, PlayerIdStr) of
+                           true -> ?SOCIAL_ACTION_SUBSCRIBE;
+                           false -> ?SOCIAL_ACTION_UNSUBSCRIBE
+                       end,
+                Msg = #social_action_msg{initiator = UserId,
+                                         recipient = PlayerId,
+                                         type = Type},
+                ok = send_message_to_player(RPC, Msg)
+        end,
+    lists:foreach(F2, Players),
+    NewState = State#state{rels_players = NewRelsPlayers,
+                           rels_notif_channel = NewRelsChannel},
+    {reply, ok, NewState};
+
+
+handle_client_request(#unsubscribe_player_rels{players = Players}, _From,
+                      #state{rels_notif_channel = RelsChannel,
+                             rels_players = RelsPlayers
+                            } = State) ->
+    ?INFO("unsubscribe player relations notifications", []),
+    %% Remove players from common list
+    NewRelsPlayers = RelsPlayers -- Players,
+
+    %% Remove subscription if we don't need it now
+    NewRelsChannel =
+        if NewRelsPlayers == [] -> nsm_mq_channel:close(RelsChannel),
+               undefined;
+           true ->
+               RelsChannel
+        end,
+    NewState = State#state{rels_players = NewRelsPlayers,
+                           rels_notif_channel = NewRelsChannel},
+    {reply, ok, NewState};
+
+
+handle_client_request(#join_game{game = GameId}, _From,
+                      #state{user = User, rpc = RPC, games = Games} = State) ->
+    UserId = User#'PlayerInfo'.id,
+    ?INFO("join game ~p user ~p from ~p", [GameId, UserId,_From]),
+    case get_relay(GameId, Games) of
+        #participation{} ->
+            {reply, {error, already_joined}, State};
+        false ->
+            ?INFO("Requesting main relay info...",[]),
+            case game_manager:get_relay_mod_pid(GameId) of
+                {FLMod, FLPid} ->
+                    ?INFO("Found the game: ~p. Trying to register...",[{FLMod, FLPid}]),
+                    case FLMod:reg(FLPid, User) of
+                        {ok, {RegNum, {RMod, RPid}, {TMod, TPid}}} ->
+                            ?INFO("join to game relay: ~p",[{RMod, RPid}]),
+                            {ok, _SubscrId} = RMod:subscribe(RPid, self(), UserId, RegNum),
+                            Ref = erlang:monitor(process, RPid),
+                            Part = #participation{ref = Ref, game_id = GameId, reg_num = RegNum,
+                                                  rel_module = RMod, rel_pid = RPid,
+                                                  tab_module = TMod, tab_pid = TPid, role = player},
+                            Res = #'TableInfo'{}, % FIXME: The client should accept 'ok' responce
+                            {reply, Res, State#state{games = [Part | State#state.games]}};
+                        {error, finished} ->
+                            ?INFO("The game is finished: ~p.",[GameId]),
+                            ok = send_message_to_player(RPC, #disconnect{reason_id = <<"gameFinished">>,
+                                                                         reason = null}),
+                            {reply, {error, finished}, State};
+                        {error, out} ->
+                            ?INFO("Out of the game: ~p.",[GameId]),
+                            ok = send_message_to_player(RPC, #disconnect{reason_id = <<"disconnected">>,
+                                                                         reason = null}),
+                            {reply, {error, out}, State};
+                        {error, not_allowed} ->
+                            ?ERROR("Not allowed to connect: ~p.",[GameId]),
+                            ok = send_message_to_player(RPC, #disconnect{reason_id = <<"notAllowed">>,
+                                                                         reason = null}),
+                            {reply, {error, not_allowed}, State}
+                    end;
+                undefined ->
+                    ?ERROR("Game not found: ~p.",[GameId]),
+                    ok = send_message_to_player(RPC, #disconnect{reason_id = <<"notExists">>,
+                                                                 reason = null}),
+                    {reply, {error, not_exists}, State}
+            end
+    end;
+
+
+handle_client_request(#game_action{game = GameId} = Msg, _From, State) ->
+%    ?INFO("game action ~p", [{GameId,Msg,_From}]),
+    Participation = get_relay(GameId, State#state.games),
+    case Participation of
+        false ->
+            {reply, {error, game_not_found}, State};
+        #participation{reg_num = RegNum, tab_pid = TPid, tab_module = TMod} ->
+            UId = (State#state.user)#'PlayerInfo'.id,
+            ?INFO("PLAYER ~p MOVES ~p in GAME ~p",[UId,Msg,GameId]),
+            {reply, TMod:submit(TPid, RegNum, Msg), State}
+    end;
+
+
+handle_client_request(#pause_game{game = GameId, action = Action}, _From, State) ->
+%    ?INFO("pause game", []),
+%    UId = (State#state.user)#'PlayerInfo'.id,
+    Participation = get_relay(GameId, State#state.games),
+%    ?INFO("ID: ~p, pause game: ~p, my games: ~p", [UId, GameId, State#state.games]),
+    case Participation of
+        false ->
+            ?INFO("A", []),
+            {reply, {error, game_not_found}, State};
+        #participation{reg_num = RegNum, tab_pid = TPid, tab_module = TMod} ->
+            Signal = case Action of
+                         <<"pause">> -> pause_game;
+                         <<"resume">> -> resume_game
+                     end,
+            Res = TMod:signal(TPid, RegNum, {Signal, self()}),
+            ?INFO("B. Res: ~p", [Res]),
+            {reply, Res, State}
+    end;
+
+handle_client_request(Request, _From, State) ->
+    ?INFO("unrecognized client request: ~p", [Request]),
+    {stop, {unknown_client_request, Request}, State}.
+
+%%===================================================================
+
+handle_relay_message(Msg, _SubscrId, #state{rpc = RPC} = State) ->
+    try send_message_to_player(RPC, Msg) of
+        ok ->
+            {noreply, State};
+        tcp_closed ->
+            {stop, normal, State}
+    catch
+        exit:{normal, {gen_server,call, [RPC, {send_message, _}]}} ->
+            {stop, normal, State};
+        exit:{noproc, {gen_server,call, [RPC, {send_message, _}]}} ->
+            {stop, normal, State}
+    end.
+
+%%===================================================================
+
+%% The notification from the current table to rejoin to the game
+%% because the user for example was moved to another table.
+handle_relay_kick({rejoin, GameId}, _SubscrId,
+                  #state{user = User, games = Games, rpc = RPC} = State) ->
+    ?INFO("Requesting main relay info...",[]),
+    UserId = User#'PlayerInfo'.id,
+    case game_manager:get_relay_mod_pid(GameId) of
+        {FLMod, FLPid} ->
+            ?INFO("Found the game: ~p. Trying to register...",[{FLMod, FLPid}]),
+            case FLMod:reg(FLPid, User) of
+                {ok, {RegNum, {RMod, RPid}, {TMod, TPid}}} ->
+                    ?INFO("join to game relay: ~p",[{RMod, RPid}]),
+                    {ok, _NewSubscrId} = RMod:subscribe(RPid, self(), UserId, RegNum),
+                    Ref = erlang:monitor(process, RPid),
+                    Part = #participation{ref = Ref, game_id = GameId, reg_num = RegNum,
+                                          rel_module = RMod, rel_pid = RPid,
+                                          tab_module = TMod, tab_pid = TPid, role = player},
+                    NewGames = lists:keyreplace(GameId, #participation.game_id, Games, Part),
+                    {noreply, State#state{games = NewGames}};
+                {error, finished} ->
+                    ?INFO("The game is finished: ~p.",[GameId]),
+                    send_message_to_player(RPC, #disconnect{reason_id = <<"gameFinished">>,
+                                                            reason = null}),
+                    {stop, normal, State};
+                {error, out} ->
+                    ?INFO("Out of the game: ~p.",[GameId]),
+                    send_message_to_player(RPC, #disconnect{reason_id = <<"kicked">>,
+                                                            reason = null}),
+                    {stop, normal, State};
+                {error, not_allowed} ->
+                    ?ERROR("Not allowed to connect: ~p.",[GameId]),
+                    send_message_to_player(RPC, #disconnect{reason_id = <<"notAllowed">>,
+                                                            reason = null}),
+                    {stop, {error, not_allowed_to_join}, State}
+            end;
+        undefined ->
+            ?ERROR("Game not found: ~p.",[GameId]),
+            send_message_to_player(RPC, #disconnect{reason_id = <<"notExists">>,
+                                                    reason = null}),
+            {stop, {error, game_not_found}, State}
+    end;
+
+handle_relay_kick(Reason, _SubscrId, #state{rpc = RPC} = State) ->
+    {ReasonId, ReasonText} =
+        case Reason of
+            table_closed -> {<<"tableClosed">>, null};
+            table_down -> {null, <<"The table was closed unexpectedly.">>};
+            game_over -> {null, <<"The game is over.">>};
+            _ -> {<<"kicked">>, null}
+        end,
+    send_message_to_player(RPC, #disconnect{reason_id = ReasonId, reason = ReasonText}),
+    {stop, normal, State}.
+
+%%===================================================================
+
+get_relay(GameId, GameList) ->
+    lists:keyfind(GameId, #participation.game_id, GameList).
+
+
+send_message_to_player(Pid, Message) ->
+    ?INFO("MESSAGE to ~p ~p",[Pid,Message]),
+    gen_server:call(Pid,{send_message,Message}).

+ 203 - 0
apps/server/src/game_stats.erl

@@ -0,0 +1,203 @@
+-module(game_stats).
+-behaviour(gen_server).
+
+-export([start_link/0, add_game/1, get_skill/1, get_game_points/2, get_player_stats/1,
+         init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3,
+         assign_points/2, is_feel_lucky/1, game_info_to_ti/1, charge_quota/1]).
+
+-include_lib("server/include/basic_types.hrl").
+-include_lib("server/include/game_okey.hrl").
+-include_lib("server/include/game_tavla.hrl").
+-include_lib("server/include/log.hrl").
+-include_lib("server/include/games.hrl").
+-include_lib("db/include/accounts.hrl").
+-include_lib("db/include/scoring.hrl").
+
+
+-record(raw_result,
+        {player_id :: binary(),
+         winner    :: boolean(),
+         score     :: integer(),
+         pos       :: integer()
+        }).
+
+-record(result,
+        {player_id :: string(),
+         robot     :: boolean(),
+         winner    :: boolean(),
+         score     :: integer(),
+         pos       :: integer(),
+         kakush_points :: integer(),
+         game_points :: integer()
+        }).
+
+start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
+add_game(Game) -> gen_server:cast(?MODULE, {add_game, Game}).
+
+get_skill(UserId) when is_binary(UserId) -> get_skill(binary_to_list(UserId));
+get_skill(UserId) -> {ok, crypto:rand_uniform(1,1000)}.
+
+get_game_points(GameType, UserId) when is_binary(UserId) -> get_game_points(GameType, binary_to_list(UserId));
+get_game_points(GameType, UserId) -> {ok, [{game_points,crypto:rand_uniform(1,1000)},
+                                           {finished_with_okey,crypto:rand_uniform(1,1000)},
+                                           {finished_with_8_tashes,crypto:rand_uniform(1,1000)}]}.
+
+get_player_stats(UserId) when is_binary(UserId) -> get_player_stats(binary_to_list(UserId));
+get_player_stats(UserId) -> {ok, [{total_games,crypto:rand_uniform(1,10)},
+                                  {total_wins,crypto:rand_uniform(1,10)},
+                                  {total_loses,crypto:rand_uniform(1,10)},
+                                  {overal_success_ratio,crypto:rand_uniform(1,100)},
+                                  {average_play_time,crypto:rand_uniform(1000,5000)}]}.
+
+init([]) -> {ok, no_state}.
+
+handle_call(Request, From, State) ->
+    error_logger:error_msg("unknown call ~p ~p ~n", [Request, From]),
+    {noreply, State}.
+
+handle_cast({add_game, Game}, State) -> {noreply, State};
+handle_cast(Msg, State) -> error_logger:error_msg("unknown cast ~p ~n", [Msg]), {noreply, State}.
+handle_info(Info, State) -> error_logger:error_msg("unknown info ~p~n", [Info]), {noreply, State}.
+terminate(_Reason, _State) -> ok.
+code_change(_OldVsn, State, _Extra) -> {ok, State}.
+
+is_feel_lucky(GameInfo) ->
+    proplists:get_value(lucky, GameInfo,false).
+
+game_info_to_ti(GameInfo) ->
+    #ti_game_event{game_name = okey,
+                   game_mode = proplists:get_value(mode, GameInfo),
+                   id = proplists:get_value(id, GameInfo),
+                   double_points = proplists:get_value(double_points, GameInfo)
+                  }.
+
+
+charge_quota(GameInfo) ->
+    PR0       = proplists:get_value(pointing_rules, GameInfo),
+    PRLucky   = proplists:get_value(pointing_rules_lucky, GameInfo),
+    Players   = proplists:get_value(initial_players, GameInfo),
+    Double    = proplists:get_value(double_points, GameInfo),
+    TI = game_info_to_ti(GameInfo),
+    PR = pointing_rules:double_points(PR0, Double),
+    [begin
+         UId = user_id_to_string(U#'PlayerInfo'.id),
+         Amount = case is_feel_lucky(GameInfo) of
+                      true -> PRLucky#pointing_rule.quota;
+                      _ -> PR#pointing_rule.quota
+                  end,
+        ok = nsm_accounts:transaction(UId, ?CURRENCY_QUOTA, -Amount, TI#ti_game_event{type = game_start})
+     end || U  <- Players].
+
+assign_points(#'TavlaGameResults'{players = Results}, GameInfo) ->
+    ConvertedResults = [ #raw_result{winner = Winner == <<"true">>, player_id = PlayerId, score = Score,
+                                     pos = if Winner == <<"true">> -> 1; true -> 2 end}
+                        || #'TavlaPlayerScore'{winner = Winner, player_id = PlayerId, score = Score} <- Results],
+    assign_points(ConvertedResults, GameInfo);
+
+assign_points(#'OkeyGameResults'{series_results = Results}, GameInfo) ->
+    ConvertedResults = [ #raw_result{winner = Winner == <<"true">>, player_id = PlayerId, score = Score, pos = Pos}
+                        || #'OkeySeriesResult'{winner = Winner, player_id = PlayerId, score = Score, place = Pos} <- Results],
+    assign_points(ConvertedResults, GameInfo);
+
+assign_points(RawResults, GameInfo) ->
+    GameId   = proplists:get_value(id, GameInfo),
+    PR0      = proplists:get_value(pointing_rules, GameInfo),
+    PRLucky  = proplists:get_value(pointing_rules_lucky, GameInfo),
+    Players  = proplists:get_value(initial_players, GameInfo),
+    Double   = proplists:get_value(double_points, GameInfo),
+    TI = game_info_to_ti(GameInfo),
+
+    PR1 = pointing_rules:double_points(PR0, Double),
+
+    PlayersIds = [if Robot -> user_id_to_string(UserId);
+                     true -> "(robot)"
+                  end || #'PlayerInfo'{id = UserId, robot = Robot} <- Players],
+
+    #pointing_rule{
+                   kakush_winner = KakushWinner,
+                   kakush_other = KakushOther,
+                   game_points = WinGamePoints
+                  } = case is_feel_lucky(GameInfo) of true -> PRLucky; false -> PR1 end,
+
+    Bots    = [UserId || #raw_result{player_id = UserId} <- RawResults, is_bot(UserId, Players)],
+    Paids   = [UserId || #raw_result{player_id = UserId} <- RawResults, is_paid(UserId)],
+    Winners = [UserId || #raw_result{player_id = UserId, winner = true} <- RawResults],
+    TotalNum = length(RawResults),
+    PaidsNum = length(Paids),
+    WinnersNum = length(Winners),
+    KakushPerWinner = round((KakushWinner * PaidsNum div TotalNum) / WinnersNum),
+    KakushPerLoser = KakushOther * PaidsNum div TotalNum,
+    ?INFO("GAME_STATS <~p> KakushWinner: ~p KakushOther: ~p", [GameId, KakushWinner, KakushOther]),
+    ?INFO("GAME_STATS <~p> KakushPerWinner: ~p KakushPerLoser: ~p", [GameId, KakushPerWinner, KakushPerLoser]),
+    Results = [begin
+                   Robot = lists:member(UserId, Bots),
+                   Paid = lists:member(UserId, Paids),
+                   {KakushPoints, GamePoints} = calc_points(KakushPerWinner, KakushPerLoser,
+                                                            WinGamePoints, Paid, Robot, Winner),
+                   #result{player_id = user_id_to_string(UserId), robot = Robot, winner = Winner,
+                           pos = Pos, kakush_points = KakushPoints, game_points = GamePoints}
+               end || #raw_result{player_id = UserId, winner = Winner, pos = Pos} <- RawResults],
+    ?INFO("GAME_STATS <~p> Results: ~p", [GameId, Results]),
+    [begin
+         if not Robot ->
+                SR = #scoring_record{
+                                     game_id = proplists:get_value(id, GameInfo),
+                                     who = UserId,
+                                     all_players = PlayersIds,
+                                     game_type = PR0#pointing_rule.game_type,
+                                     game_kind = PR0#pointing_rule.game,
+%                                     condition, % where'd I get that?
+                                     score_points = GamePoints,
+                                     score_kakaush = KakushPoints,
+%                                     custom,    % no idea what to put here
+                                     timestamp = erlang:now()
+                                    },
+                Route = [feed, user, UserId, scores, 0, add],  % maybe it would require separate worker for this
+                nsx_msg:notify(Route, [SR]),
+                {Wins, Loses} = if Winner-> {1, 0};
+                                   true -> {0, 1}
+                                end,
+                % haven't found a way to properly get average time
+                nsx_msg:notify([personal_score, user, UserId, add],
+                               {_Games = 1, Wins, Loses, _Disconnects = 0, GamePoints, 0});
+            true -> do_nothing  % no statistics for robots
+         end,
+         if not Robot ->
+                if KakushPoints /= 0 ->
+                       ok = nsm_accounts:transaction(UserId, ?CURRENCY_KAKUSH, KakushPoints, TI#ti_game_event{type = game_end});
+                   true -> ok
+                end,
+                if GamePoints /= 0 ->
+                        ok = nsm_accounts:transaction(UserId, ?CURRENCY_GAME_POINTS, GamePoints, TI#ti_game_event{type = game_end});
+                   true -> ok
+                end;
+            true -> do_nothing %% no points for robots
+         end
+     end || #result{player_id = UserId, robot = Robot, winner = Winner,
+                    kakush_points = KakushPoints, game_points = GamePoints}  <- Results],
+
+    GameName = proplists:get_value(name, GameInfo, ""),
+    GameType = proplists:get_value(game_type, GameInfo),
+    GameEndRes = [{if Robot -> "robot"; true -> UserId end, Robot, Pos, KPoints, GPoints}
+                  || #result{player_id = UserId, robot = Robot, pos = Pos,
+                             kakush_points = KPoints, game_points = GPoints} <- Results],
+    ?INFO("GAME_STATS <~p> Notificaton: ~p", [GameId, {{GameName, GameType}, GameEndRes}]),
+    nsx_msg:notify(["system", "game_ends_note"], {{GameName, GameType}, GameEndRes}).
+
+is_bot(UserId, Players) ->
+    case lists:keyfind(UserId, #'PlayerInfo'.id, Players) of
+        #'PlayerInfo'{robot = Robot} -> Robot;
+        _ -> true % If UserId is not found then the player is a replaced bot. 
+    end.
+
+is_paid(UserId) -> nsm_accounts:user_paid(UserId).
+
+user_id_to_string(UserId) -> binary_to_list(UserId).
+
+calc_points(KakushPerWinner, KakushPerLoser, WinGamePoints, Paid, Robot, Winner) ->
+    if Robot -> {0, 0};
+       not Paid andalso Winner -> {0, WinGamePoints};
+       not Paid -> {0, 0};
+       Paid andalso Winner -> {KakushPerWinner, WinGamePoints};
+       Paid -> {KakushPerLoser, 0}
+    end.

+ 27 - 0
apps/server/src/game_sup.erl

@@ -0,0 +1,27 @@
+-module(game_sup).
+-behaviour(supervisor).
+-export([start_link/0, stop/0]).
+-include_lib("server/include/conf.hrl").
+-export([init/1, start/0, start_game/3, stop_game/2]).
+-define(SERVER, ?MODULE).
+
+start() -> supervisor:start({local, ?SERVER}, ?MODULE, []).
+start_link() -> supervisor:start_link({local, ?SERVER}, ?MODULE, []).
+stop() -> exit(?SERVER, shutdown).
+start_game(Sup,Mod,Par) -> supervisor:start_child(Sup,[Mod,Par]).
+stop_game(Sup,Pid) -> supervisor:terminate_child(Sup,Pid).
+
+init([]) ->
+    RestartStrategy = one_for_one,
+    MaxRestarts = 100,
+    MaxSecondsBetweenRestarts = 3600,
+    SupFlags = {RestartStrategy, MaxRestarts, MaxSecondsBetweenRestarts},
+    Restart = permanent,
+    Shutdown = 2000,
+    IdGen = {id_generator, {id_generator, start_link, []},  Restart, Shutdown, worker, [id_generator]},
+    GameManager = {game_manager, {game_manager, start_link, []}, Restart, Shutdown, worker, [game_manager]},
+    TavlaSup = {tavla_sup, {tavla_sup, start_link, []}, Restart, Shutdown, supervisor, [tavla_sup]},
+    OkeySup = {okey_sup, {okey_sup, start_link, []}, Restart, Shutdown, supervisor, [okey_sup]},
+    LuckySup = {lucky_sup, {lucky_sup, start_link, []}, Restart, Shutdown, supervisor, [lucky_sup]},
+    {ok, {SupFlags, [GameManager,LuckySup,TavlaSup,OkeySup,IdGen]}}.
+

+ 20 - 0
apps/server/src/id_generator.erl

@@ -0,0 +1,20 @@
+-module(id_generator).
+-behaviour(gen_server).
+-export([start_link/0]).
+-export([get_id/0,get_id2/0]).
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
+-define(SERVER, ?MODULE).
+-record(state, {lastid = 1000000, robotid = 1500000}).
+
+get_id() -> gen_server:call(?MODULE, get_id).
+get_id2() -> gen_server:call(?MODULE, get_id2).
+start_link() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
+
+init([]) -> {ok, #state{lastid=nsx_opt:get_env(nsx_idgen,game_pool,1000000)}}.
+handle_call(get_id, _From, #state{lastid = LID} = State) -> Reply = LID + 1, {reply, Reply, State#state{lastid = Reply}};
+handle_call(get_id2, _From, #state{robotid = RID} = State) -> Reply = RID + 1, {reply, Reply, State#state{robotid = Reply}};
+handle_call(_Request, _From, State) -> Reply = ok, {reply, Reply, State}.
+handle_cast(_Msg, State) -> {noreply, State}.
+handle_info(_Info, State) -> {noreply, State}.
+terminate(_Reason, _State) -> ok.
+code_change(_OldVsn, State, _Extra) -> {ok, State}.

+ 86 - 0
apps/server/src/lucky_sup.erl

@@ -0,0 +1,86 @@
+-module(lucky_sup).
+-behaviour(supervisor).
+-include_lib("db/include/config.hrl").
+-export([start_link/0]).
+-export([init/1]).
+-define(CHILD(I, Type), {I, {I, start_link, []}, permanent, 5000, Type, [I]}).
+
+start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []).
+
+init([]) ->
+    RestartStrategy = one_for_one,
+    MaxRestarts = 1000,
+    MaxSecondsBetweenRestarts = 1,
+    SupFlags = {RestartStrategy, MaxRestarts, MaxSecondsBetweenRestarts},
+    Restart = permanent,
+    Shutdown = 2000,
+
+    OkeyTableParams = [{mult_factor, 1},
+                       {slang_allowed, false},
+                       {observers_allowed, false},
+                       {tournament_type, lucky},
+                       {round_timeout, infinity},
+%%%                        {round_timeout, 30 * 1000},
+                       {set_timeout, infinity},
+%%%                        {set_timeout, 10 * 60 *1000},
+                       {speed, normal},
+                       {game_type, standard},
+                       {rounds, undefined},
+                       {reveal_confirmation, true},
+                       {next_series_confirmation, no},
+                       {pause_mode, normal},
+                       {social_actions_enabled, true},
+                       {gosterge_finish_allowed, undefined}
+                      ],
+    OkeyGameId = id_generator:get_id(),
+    GameName = "I'm filling lucky - " ++ erlang:integer_to_list(OkeyGameId),
+    OkeyParams = [{game, game_okey},
+                  {game_mode, standard},
+                  {game_name, GameName},
+                  {mode, normal}, % Common table for several real players
+                  {seats, 4},
+%%%                  {quota_per_round, Quota},
+                  {table_module, game_okey_ng_table_trn},
+                  {bot_module, game_okey_bot},
+                  {table_params, OkeyTableParams}
+                 ],
+    OkeySpec = {okey_lucky, {nsg_trn_lucky, start_link, [OkeyGameId, OkeyParams]},
+                  Restart, Shutdown, worker, [nsg_trn_lucky]},
+
+
+    TavlaTableParams = [{mult_factor, 1},
+                        {slang_allowed, false},
+                        {observers_allowed, false},
+                        {tournament_type, lucky},
+                        {round_timeout, infinity},
+%%%                        {round_timeout, 30 * 1000},
+                        {set_timeout, infinity},
+%%%                        {set_timeout, 10 * 60 *1000},
+                        {speed, normal},
+                        {game_mode, standard},
+                        {rounds, undefined},
+                        {next_series_confirmation, no},
+                        {pause_mode, normal},
+                        {social_actions_enabled, true}
+                       ],
+
+    TavlaGameId = id_generator:get_id(),
+    TavlaGameName = "I'm filling lucky - " ++ erlang:integer_to_list(TavlaGameId),
+    TavlaParams = [{game, game_tavla},
+                   {game_mode, standard},
+                   {game_name, TavlaGameName},
+                   {mode, normal}, % Common table for several real players
+                   {seats, 2},
+%%%                  {quota_per_round, Quota},
+                   {table_module, game_tavla_ng_table},
+                   {bot_module, game_tavla_bot},
+                   {table_params, TavlaTableParams}
+                  ],
+    TavlaSpec = {tavla_lucky, {nsg_trn_lucky, start_link, [TavlaGameId, TavlaParams]},
+                  Restart, Shutdown, worker, [nsg_trn_lucky]},
+
+%%     TavlaSpec = {tavla_lucky, {fl_lucky, start_link, [TavlaGameId,[{game_type, game_tavla}, {mode, normal}]]}, 
+%%                   Restart, Shutdown, worker, [fl_lucky]},
+
+    {ok, { SupFlags, [OkeySpec, TavlaSpec]} }.
+

+ 283 - 0
apps/server/src/midict.erl

@@ -0,0 +1,283 @@
+%% Author: Sergei Polkovnikov <serge.polkovnikov@gmail.com>
+%% Created: Jan 8, 2010
+%% Description: Multi indices dictionary
+-module(midict).
+-author("Sergei Polkovnikov <serge.polkovnikov@gmail.com>").
+
+%%
+%% Include files
+%%
+
+-include_lib("eunit/include/eunit.hrl").
+%%
+%% Exported Functions
+%%
+-export([
+         new/0,
+         store/4,
+         find/2,
+         fetch/2,
+         geti/3,
+         erase/2,
+         size/1,
+         to_list/1,
+         from_list/1,
+         fetch_keys/1,
+         all_values/1
+        ]).
+
+-record(midict,
+        {
+         keyd,
+         indds
+        }).
+
+-type midict() :: #midict{}.
+
+%%
+%% API Functions
+%%
+
+-spec new() -> midict().
+%% @spec new() -> midict()
+%% @doc Creates empty midict.
+%% @end
+
+new() ->
+    #midict{
+            keyd = dict:new(),
+            indds = dict:new()
+           }.
+
+-spec store(term(), term(), list(), midict()) -> midict().
+%% @spec store(Key, Value, Indices, MIDict) -> MIDict2
+%% @doc Puts the entity to the midict.
+%% Types:
+%%     Key = Value = term()
+%%     Indices = [{Index, IndValue}]
+%%       Index = IndValue = term()
+%%     MIDict = MIDict2 = midict()
+%% @end
+
+store(Key, Value, Indices, #midict{keyd = KeyD, indds = IndD}) ->
+    Obj = {Key, Value, Indices},
+    IndD1 = case dict:find(Key, KeyD) of
+                 {ok, OldObj} ->
+                     del_indices(OldObj, IndD);
+                 error ->
+                     IndD
+             end,
+    NewKeyD = dict:store(Key, Obj, KeyD),
+    NewIndD = add_indices(Obj, IndD1),
+    #midict{keyd = NewKeyD, indds = NewIndD}.
+
+-spec find(term(), midict()) -> {ok, term()} | error.
+%% @spec find(Key, MIDict) -> {ok, Value} | error
+%% @doc Get an entity from the dict by the key.
+%% Types:
+%%     Key = Value = term()
+%%     MIDict = midict()
+%% @end
+
+find(Key, #midict{keyd = KeyD}) ->
+    case dict:find(Key, KeyD) of
+        {ok, {_, Value, _}} ->
+            {ok, Value};
+        error ->
+            error
+    end.
+
+-spec fetch(term(), midict()) -> term().
+%% @spec fetch(Key, MIDict) -> Value}
+%% @doc Fetch an entity from the dict by the key. An exception
+%% will arise if there is no entity with the key.
+%% Types:
+%%     Key = Value = term()
+%%     MIDict = midict()
+%% @end
+
+fetch(Key, #midict{keyd = KeyD}) ->
+    {_, Value, _} = dict:fetch(Key, KeyD),
+    Value.
+
+-spec geti(term(), term(), midict()) -> [term()].
+%% @spec geti(IndexVal, Index, MIDict) -> Values
+%% @doc Gets entities from the midict by the index.
+%% Types:
+%%     IndexVal = Index  = term()
+%%     MIDict = midict()
+%%     Values = [Value]
+%%       Value = term()
+%% @end
+
+geti(IVal, Index, #midict{indds = IndD}) ->
+    case dict:find({Index, IVal}, IndD) of
+        {ok, Set} ->
+            [Value || {_, Value, _} <- sets:to_list(Set)];
+        error ->
+            []
+    end.
+
+-spec erase(term(), midict()) -> midict().
+%% @spec erase(Key, MIDict) -> MIDict2
+%% @doc Delete an entity from the midict by the key.
+%% Types:
+%%     Key = term()
+%%     MIDict = MIDict2 = midict()
+%% @end
+
+erase(Key, #midict{keyd = KeyD, indds = IndD}) ->
+    NewIndD = case dict:find(Key, KeyD) of
+                  {ok, Obj} ->
+                      del_indices(Obj, IndD);
+                  error ->
+                      IndD
+              end,
+    NewKeyD = dict:erase(Key, KeyD),
+    #midict{keyd = NewKeyD, indds = NewIndD}.
+
+-spec size(midict()) -> non_neg_integer().
+%% @spec size(midict()) -> integer()
+%% @doc Returns number of entities in the dict.
+%% @end
+
+size(#midict{keyd = KeyD}) ->
+    dict:size(KeyD).
+
+-spec to_list(midict()) -> list({term(), term(), list()}).
+%% @spec to_list(midict()) -> List
+%% @doc Convert the midict to a list.
+%% Types:
+%%     List = {Key, Value, Indices}
+%%       Key = Value = term()
+%%       Indices = [{Index, IndValue}]
+%%         Index = IndValue = term()
+%% @end
+
+to_list(#midict{keyd = KeyD}) ->
+    [Obj || {_, Obj} <- dict:to_list(KeyD)].
+
+-spec from_list(list({term(), term(), list()})) -> midict().
+%% @spec from_list(List) -> midict()
+%% @doc Convert the list to a midict.
+%% Types:
+%%     List = {Key, Value, Indices}
+%%       Key = Value = term()
+%%       Indices = [{Index, IndValue}]
+%%         Index = IndValue = term()
+%% @end
+
+from_list([]) ->
+    midict:new();
+from_list([{Key, Value, Indices} | Rest]) ->
+    midict:store(Key, Value, Indices, from_list(Rest)).
+
+-spec fetch_keys(midict()) -> list(term()).
+%% @spec fetch_keys(midict()) -> Keys
+%% @doc Fetches all keys of the midict.
+%% Types:
+%%     Keys = [Key]
+%%       Key = term()
+%% @end
+
+fetch_keys(#midict{keyd = KeyD}) ->
+    dict:fetch_keys(KeyD).
+
+-spec all_values(midict()) -> list(term()).
+%% @spec all_values(midict()) -> Values
+%% @doc Fetches all enities values of the midict.
+%% Types:
+%%     Values = [Value]
+%%       Value = term()
+%% @end
+
+all_values(#midict{keyd = KeyD}) ->
+    [Val || {_, {_, Val, _}} <- dict:to_list(KeyD)].
+
+%%
+%% Local Functions
+%%
+add_indices({_, _, Indices} = Obj, IndD) ->
+    add_indices(Obj, IndD, Indices).
+
+add_indices(_Obj, IndD, []) ->
+    IndD;
+add_indices(Obj, IndD, [I | Indices]) when tuple_size(I) == 2->
+    CurSet = get_set(I, IndD),
+    NewSet = sets:add_element(Obj, CurSet),
+    NewIndD = put_set(NewSet, I, IndD),
+    add_indices(Obj, NewIndD, Indices).
+
+del_indices({_, _, Indices} = Obj, IndD) ->
+    del_indices(Obj, IndD, Indices).
+
+del_indices(_Obj, IndD, []) ->
+    IndD;
+del_indices(Obj, IndD, [I | Indices]) ->
+    CurSet = get_set(I, IndD),
+    NewSet = sets:del_element(Obj, CurSet),
+    NewIndD = put_set(NewSet, I, IndD),
+    del_indices(Obj, NewIndD, Indices).
+
+get_set(Index, IndDs) ->
+    case dict:find(Index, IndDs) of
+        {ok, Set} -> Set;
+        error -> sets:new()
+    end.
+
+put_set(Set, Index, IndDs) ->
+    case sets:size(Set) of
+        0 -> dict:erase(Index, IndDs);
+        _ -> dict:store(Index, Set, IndDs)
+    end.
+%%
+
+test_test_() ->
+    [
+     {"new",
+      [
+       ?_assert(begin midict:new(), true end)
+      ]},
+     {"store",
+      [
+       ?_assert(begin midict:store("xxx", value, [{ccc, 5}, {ttt,fff}], x()), true end),
+       ?_assert(begin midict:store("xxx", value, [{ccc, 5}, {ttt,fff}], x2()), true end)
+      ]},
+     {"find",
+      [
+       ?_assertEqual({ok, {record1}}, midict:find("Key1", x5())),
+       ?_assertEqual({ok, record3} , midict:find(<<"Key3">>, x5())),
+       ?_assertEqual({ok, {record2, hello}} , midict:find("Key2", x5())),
+       ?_assertEqual(error , midict:find(<<"UnknownKey">>, x5()))
+      ]},
+     {"geti",
+      [
+       ?_assertEqual(lists:sort([{record1}, {record2, hello}]), lists:sort(midict:geti(5, table, x5()))),
+       ?_assertEqual(lists:sort([record3, "record4"]), lists:sort(midict:geti({3,1}, table_seat, x5()))),
+       ?_assertEqual([{record2, hello}], midict:geti({5, 3}, table_seat, x5())),
+       ?_assertEqual(["record4"], midict:geti(true, haha, x5())),
+       ?_assertEqual([], midict:geti(4, table_seat, x5())),
+       ?_assertEqual([], midict:geti(100500, xxx, x5()))
+      ]},
+     {"erase",
+      [
+       ?_assertEqual(error, midict:find("Key2", midict:erase("Key2", x5())))
+      ]},
+     {"size",
+      [
+       ?_assertEqual(0, midict:size(x())),
+       ?_assertEqual(4, midict:size(x5())),
+       ?_assertEqual(3, midict:size(x6()))
+      ]},
+     {"fetch_keys",
+      [
+       ?_assertEqual(lists:sort(["Key1", "Key2", <<"Key3">>, key4]), lists:sort(midict:fetch_keys(x5())))
+      ]}
+    ].
+
+x() -> midict:new().
+x2() -> midict:store("Key1", {record1}, [{table, 5}, {table_seat, {5, 2}}], x()).
+x3() -> midict:store("Key2", {record2, hello}, [{table, 5}, {table_seat, {5, 3}}], x2()).
+x4() -> midict:store(<<"Key3">>, record3, [{table, 3}, {table_seat, {3, 1}}], x3()).
+x5() -> midict:store(key4, "record4", [{table, 2}, {table_seat, {3, 1}}, {haha, true}], x4()).
+x6() -> midict:erase("Key2", x5()).

+ 33 - 0
apps/server/src/nsg_crowd_lib.erl

@@ -0,0 +1,33 @@
+%%% -------------------------------------------------------------------
+%%% Author  : Sergei Polkovnikov <serge.polkovnikov@gmail.com>
+%%% Description : The library with functions for generation random lists
+%%%              of the virtual users
+%%% Created : Mar 25, 2013
+%%% -------------------------------------------------------------------
+
+-module(nsg_crowd_lib).
+
+-export([virtual_users/0,
+         random_users/2]).
+
+virtual_users() ->
+    {_, AllUsers} = lists:unzip(nsm_auth:imagionary_users2()),
+    F = fun(UserId, Acc) ->
+                case auth_server:get_user_info_by_user_id(UserId) of
+                    {ok, _} -> [UserId | Acc];
+                    {error, _} -> Acc
+                end
+        end,
+    lists:usort(lists:foldl(F, [], AllUsers)).
+
+random_users(Num, AllUsers) ->
+    AllUsersNum = length(AllUsers),
+    random_users(Num, [], AllUsers, AllUsersNum).
+
+random_users(0, Acc, _AllUsers, _AllUsersNum) -> Acc;
+random_users(N, Acc, AllUsers, AllUsersNum) ->
+    User = list_to_binary(lists:nth(crypto:rand_uniform(1, AllUsersNum + 1), AllUsers)),
+    case lists:member(User, Acc) of
+        false -> random_users(N - 1, [User | Acc], AllUsers, AllUsersNum);
+        true -> random_users(N, Acc, AllUsers, AllUsersNum)
+    end.

+ 10 - 0
apps/server/src/nsg_games_app.erl

@@ -0,0 +1,10 @@
+-module(nsg_games_app).
+-behaviour(application).
+-export([start/2, start/0, stop/0, stop/1]).
+-include_lib("eunit/include/eunit.hrl").
+-include_lib("server/include/conf.hrl").
+
+start() -> start(init,[]).
+start(_StartType, _StartArgs) -> game_sup:start_link().
+stop() -> stop([]).
+stop(_State) -> game_sup:stop().

+ 136 - 0
apps/server/src/nsg_matrix_elimination.erl

@@ -0,0 +1,136 @@
+%%% -------------------------------------------------------------------
+%%% Author  : Sergii Polkovnikov <serge.polkovnikov@gmail.com>
+%%% Description : The interface to the elimination tournaments plans matrix.
+%%%
+%%% Created : Feb 15, 2013
+%%% -------------------------------------------------------------------
+
+-module(nsg_matrix_elimination).
+
+-export([get_plan/4,
+         get_prize_fund/4,
+         get_plan_desc/4,
+         get_tours/3]).
+
+get_plan(GameType, KakushPerRound, RegistrantsNum, Tours) ->
+    case lists:keyfind({KakushPerRound, RegistrantsNum, Tours}, 1, tournament_matrix(GameType)) of
+        false -> {error, no_such_plan};
+        {_NQ, _K, Plan, _String} -> {ok, Plan}
+    end.
+
+get_prize_fund(GameType, KakushPerRound, RegistrantsNum, Tours) ->
+    case lists:keyfind({KakushPerRound, RegistrantsNum, Tours}, 1, tournament_matrix(GameType)) of
+        false -> {error, no_such_plan};
+        {_NQ, K, _Plan, _String} -> {ok, K}
+    end.
+
+get_plan_desc(GameType, KakushPerRound, RegistrantsNum, Tours) ->
+    case lists:keyfind({KakushPerRound, RegistrantsNum, Tours}, 1, tournament_matrix(GameType)) of
+        false -> {error, no_such_plan};
+        {_NQ, _K, _Plan, String} -> {ok, String}
+    end.
+
+get_tours(GameType, KakushPerRound, RegistrantsNum) ->
+    [T || {{Q, N, T}, _, _, _String} <- tournament_matrix(GameType), Q==KakushPerRound, N==RegistrantsNum].
+
+tournament_matrix(game_okey) ->
+    [%% Quota Pl.No  Fund   1          2         3         4         5         6         7         8
+     { {  8,   16,3}, 54,   [ne      , {ce,  4}, {te,  1}                                                  ], ["yok","1.ler","Final"]},
+     { { 10,   16,3}, 72,   [ne      , {ce,  4}, {te,  1}                                                  ], ["yok","1.ler","Final"]},
+     { {  2,   64,4}, 80,   [ne      , {ce, 16}, {te,  1}, {te,  1}                                        ], ["yok","yok","1.ler","Final"]},
+     { {  4,   64,4}, 98,   [ne      , {ce, 16}, {te,  1}, {te,  1}                                        ], ["yok","yok","1.ler","Final"]},
+     { {  6,   64,4}, 158,  [ne      , {ce, 16}, {te,  1}, {te,  1}                                        ], ["yok","yok","1.ler","Final"]},
+     { {  8,   64,4}, 223,  [ne      , {ce, 16}, {te,  1}, {te,  1}                                        ], ["yok","yok","1.ler","Final"]},
+     { { 10,   64,4}, 295,  [ne      , {ce, 16}, {te,  1}, {te,  1}                                        ], ["yok","yok","1.ler","Final"]},
+     { {  2,  128,5}, 81,   [{te,  2}, {te,  2}, {te,  2}, {te,  1}, {te,  1}                              ], ["yok","yok","1.ler","Final"]},
+     { {  4,  128,5}, 162,  [{te,  2}, {te,  2}, {te,  2}, {te,  1}, {te,  1}                              ], []},
+     { {  6,  128,5}, 260,  [{te,  2}, {te,  2}, {te,  2}, {te,  1}, {te,  1}                              ], []},
+     { {  8,  128,5}, 368,  [{te,  2}, {te,  2}, {te,  2}, {te,  1}, {te,  1}                              ], []},
+     { { 10,  128,5}, 487,  [{te,  2}, {te,  2}, {te,  2}, {te,  1}, {te,  1}                              ], []},
+     { {  2,  256,5}, 198,  [ne      , {ce, 64}, {te,  1}, {te,  1}, {te,  1}                              ], []},
+     { {  4,  256,5}, 397,  [ne      , {ce, 64}, {te,  1}, {te,  1}, {te,  1}                              ], []},
+     { {  6,  256,5}, 635,  [ne      , {ce, 64}, {te,  1}, {te,  1}, {te,  1}                              ], []},
+     { {  8,  256,5}, 899,  [ne      , {ce, 64}, {te,  1}, {te,  1}, {te,  1}                              ], []},
+     { { 10,  256,5}, 1190, [ne      , {ce, 64}, {te,  1}, {te,  1}, {te,  1}                              ], []},
+     { {  2,  256,7}, 283,  [ne      , {ce,128}, ne      , {ce, 64}, {te,  1}, {te,  1}, {te,   1}         ], []},
+     { {  4,  256,7}, 566,  [ne      , {ce,128}, ne      , {ce, 64}, {te,  1}, {te,  1}, {te,   1}         ], []},
+     { {  6,  256,7}, 907,  [ne      , {ce,128}, ne      , {ce, 64}, {te,  1}, {te,  1}, {te,   1}         ], []},
+     { {  8,  256,7}, 1285, [ne      , {ce,128}, ne      , {ce, 64}, {te,  1}, {te,  1}, {te,   1}         ], []},
+     { { 10,  256,7}, 1701, [ne      , {ce,128}, ne      , {ce, 64}, {te,  1}, {te,  1}, {te,   1}         ], []},
+     { {  2,  512,6}, 326,  [{te,  2}, {te,  2}, {te,  2}, {te,  1}, {te,  1}, {te,  1}                    ], ["ilk 2","ilk 2","ilk 2","ilk 2","1.ler","Final"]},
+     { {  4,  512,6}, 652,  [{te,  2}, {te,  2}, {te,  2}, {te,  1}, {te,  1}, {te,  1}                    ], ["ilk 2","ilk 2","ilk 2","ilk 2","1.ler","Final"]},
+     { {  6,  512,6}, 1043, [{te,  2}, {te,  2}, {te,  2}, {te,  1}, {te,  1}, {te,  1}                    ], ["ilk 2","ilk 2","ilk 2","ilk 2","1.ler","Final"]},
+     { {  8,  512,6}, 1478, [{te,  2}, {te,  2}, {te,  2}, {te,  1}, {te,  1}, {te,  1}                    ], ["ilk 2","ilk 2","ilk 2","ilk 2","1.ler","Final"]},
+     { { 10,  512,6}, 1957, [{te,  2}, {te,  2}, {te,  2}, {te,  1}, {te,  1}, {te,  1}                    ], ["ilk 2","ilk 2","ilk 2","ilk 2","1.ler","Final"]},
+     { {  2,  512,8}, 582,  [ne      , {ce,256}, ne      , {ce,128}, {te,  2}, {te,  1}, {te,  1}, {te,  1}], []},
+     { {  4,  512,8}, 1163, [ne      , {ce,256}, ne      , {ce,128}, {te,  2}, {te,  1}, {te,  1}, {te,  1}], []},
+     { {  6,  512,8}, 1861, [ne      , {ce,256}, ne      , {ce,128}, {te,  2}, {te,  1}, {te,  1}, {te,  1}], []},
+     { {  8,  512,8}, 2637, [ne      , {ce,256}, ne      , {ce,128}, {te,  2}, {te,  1}, {te,  1}, {te,  1}], []},
+     { { 10,  512,8}, 3490, [ne      , {ce,256}, ne      , {ce,128}, {te,  2}, {te,  1}, {te,  1}, {te,  1}], []},
+     { {  2, 1024,6}, 795,  [ne      , {ce,256}, {te,  1}, {te,  1}, {te,  1}, {te,  1}                    ], []},
+     { {  4, 1024,6}, 1589, [ne      , {ce,256}, {te,  1}, {te,  1}, {te,  1}, {te,  1}                    ], []},
+     { {  6, 1024,6}, 2543, [ne      , {ce,256}, {te,  1}, {te,  1}, {te,  1}, {te,  1}                    ], []},
+     { {  8, 1024,6}, 3602, [ne      , {ce,256}, {te,  1}, {te,  1}, {te,  1}, {te,  1}                    ], []},
+     { { 10, 1024,6}, 4767, [ne      , {ce,256}, {te,  1}, {te,  1}, {te,  1}, {te,  1}                    ], []},
+     { {  2, 1024,8}, 1135, [ne      , {ce,512}, ne      , {ce,256}, {te,  1}, {te,  1}, {te,  1}, {te,  1}], []},
+     { {  4, 1024,8}, 2271, [ne      , {ce,512}, ne      , {ce,256}, {te,  1}, {te,  1}, {te,  1}, {te,  1}], []},
+     { {  6, 1024,8}, 3633, [ne      , {ce,512}, ne      , {ce,256}, {te,  1}, {te,  1}, {te,  1}, {te,  1}], []},
+     { {  8, 1024,8}, 5147, [ne      , {ce,512}, ne      , {ce,256}, {te,  1}, {te,  1}, {te,  1}, {te,  1}], []},
+     { { 10, 1024,8}, 6812, [ne      , {ce,512}, ne      , {ce,256}, {te,  1}, {te,  1}, {te,  1}, {te,  1}], []},
+     { {  2, 2048,6}, 1135, [{te,  2}, {te,  1}, {te,  1}, {te,  1}, {te,  1}, {te,  1}                    ], ["ilk 2","1.ler","1.ler","1.ler","1.ler","Final"]},
+     { {  4, 2048,6}, 2271, [{te,  2}, {te,  1}, {te,  1}, {te,  1}, {te,  1}, {te,  1}                    ], ["ilk 2","1.ler","1.ler","1.ler","1.ler","Final"]},
+     { {  6, 2048,6}, 3633, [{te,  2}, {te,  1}, {te,  1}, {te,  1}, {te,  1}, {te,  1}                    ], ["ilk 2","1.ler","1.ler","1.ler","1.ler","Final"]},
+     { {  8, 2048,6}, 5147, [{te,  2}, {te,  1}, {te,  1}, {te,  1}, {te,  1}, {te,  1}                    ], ["ilk 2","1.ler","1.ler","1.ler","1.ler","Final"]},
+     { { 10, 2048,6}, 6812, [{te,  2}, {te,  1}, {te,  1}, {te,  1}, {te,  1}, {te,  1}                    ], ["ilk 2","1.ler","1.ler","1.ler","1.ler","Final"]},
+     { {  2, 2048,8}, 1987, [ne      , {ce,1024},{te,  2}, {te,  2}, {te,  1}, {te,  1}, {te,  1}, {te,  1}], []},
+     { {  4, 2048,8}, 3974, [ne      , {ce,1024},{te,  2}, {te,  2}, {te,  1}, {te,  1}, {te,  1}, {te,  1}], []},
+     { {  2, 1020,1}, 9359, [{te,1}                                                                        ], []},
+     { {  4, 1020,1}, 8359, [{te,1}                                                                        ], []},
+     { {  6, 1020,1}, 7359, [{te,1}                                                                        ], []},
+     { {  8, 1020,1}, 6359, [{te,1}                                                                        ], []},
+     { { 10, 1020,1}, 5359, [{te,1}                                                                        ], []},
+     { {  2, 1020,2}, 5959, [ne      , {te,1}                                                              ], []},
+     { {  4, 1020,2}, 5859, [ne      , {te,1}                                                              ], []},
+     { {  6, 1020,2}, 5759, [ne      , {te,1}                                                              ], []},
+     { {  8, 1020,2}, 5659, [ne      , {te,1}                                                              ], []},
+     { { 10, 1020,2}, 5559, [ne      , {te,1}                                                              ], []}, 
+     { {  2, 1020,3}, 4359, [ne      , ne       , {te,1}                                                   ], []},
+     { {  4, 1020,3}, 4359, [ne      , ne       , {te,1}                                                   ], []},
+     { {  6, 1020,3}, 4359, [ne      , ne       , {te,1}                                                   ], []},
+     { {  8, 1020,3}, 4359, [ne      , ne       , {te,1}                                                   ], []},
+     { { 10, 1020,3}, 4359, [ne      , ne       , {te,1}                                                   ], []},
+     { {  2, 1020,4}, 3359, [ne      , ne       , ne     , {te,1}                                          ], []},
+     { {  4, 1020,4}, 3359, [ne      , ne       , ne     , {te,1}                                          ], []},
+     { {  6, 1020,4}, 3359, [ne      , ne       , ne     , {te,1}                                          ], []},
+     { {  8, 1020,4}, 3359, [ne      , ne       , ne     , {te,1}                                          ], []},
+     { {  2, 1020,6}, 2359, [ne      , ne       , ne     , ne      , ne      , {te,1}                      ], []},
+     { {  4, 1020,6}, 2359, [ne      , ne       , ne     , ne      , ne      , {te,1}                      ], []},
+     { {  6, 1020,6}, 2359, [ne      , ne       , ne     , ne      , ne      , {te,1}                      ], []},
+     { {  2,  500,1}, 1359, [{te,1}                                                                        ], []},
+     { {  4,  500,2}, 5959, [ne      , {te,1}                                                              ], []},
+     { {  6,  500,3}, 4859, [ne      , ne       , {te,1}                                                   ], []},
+     { {  8,  500,4}, 3759, [ne      , ne       , ne     , {te,1}                                          ], []},
+     { { 10,  500,6}, 2359, [ne      , ne       , ne     , ne      , ne      , {te,1}                      ], []},
+     { {  2,  400,1}, 4359, [{te,1}                                                                        ], []},
+     { {  4,  400,2}, 3359, [ne      , {te,1}                                                              ], []},
+     { {  6,  400,3}, 2359, [ne      , ne       , {te,1}                                                   ], []},
+     { {  8,  400,4}, 1359, [ne      , ne       , ne     , {te,1}                                          ], []},
+     { { 10,  400,6}, 3359, [ne      , ne       , ne     , ne      , ne      , {te,1}                      ], []},
+     { {  2,  300,1}, 2359, [{te,1}                                                                        ], []},
+     { {  4,  300,2}, 1359, [ne      , {te,1}                                                              ], []},
+     { {  6,  300,3}, 0959, [ne      , ne       , {te,1}                                                   ], []},
+     { {  8,  300,4}, 0359, [ne      , ne       , ne     , {te,1}                                          ], []},
+     { { 10,  300,6}, 1959, [ne      , ne       , ne     , ne      , ne      , {te,1}                      ], []},
+     { {  2,  200,1}, 0859, [{te,1}                                                                        ], []},
+     { {  4,  200,2}, 0759, [ne      , {te,1}                                                              ], []},
+     { {  6,  200,3}, 0659, [ne      , ne       , {te,1}                                                   ], []},
+     { {  8,  200,4}, 0559, [ne      , ne       , ne     , {te,1}                                          ], []},
+     { { 10,  200,6}, 0459, [ne      , ne       , ne     , ne      , ne      , {te,1}                      ], []},
+     { { 10,   40,2}, 0459, [ne      , {te,1}                                                              ], []}
+  ];
+
+tournament_matrix(game_tavla) ->
+    [%% Quota Pl.No  Fund    1          2         3         4         5         6         7         8
+     { { 10,   16,4}, 54,   [{te,  1} , {te,  1}, {te,  1}, {te,  1}                                       ], ["1.ler","1.ler","1.ler","Final"]}
+    ].
+

+ 1062 - 0
apps/server/src/nsg_trn_elimination.erl

@@ -0,0 +1,1062 @@
+%%% -------------------------------------------------------------------
+%%% Author  : Sergii Polkovnikov <serge.polkovnikov@gmail.com>
+%%% Description : The "Eliminate tournament" logic
+%%%
+%%% Created : Nov 02, 2012
+%%% -------------------------------------------------------------------
+
+%%% Terms explanation:
+%%% GameId   - uniq identifier of the tournament. Type: integer().
+%%% PlayerId - registration number of a player in the tournament. Type: integer()
+%%% UserId   - cross system identifier of a physical user. Type: binary() (or string()?).
+%%% TableId  - uniq identifier of a table in the tournament. Used by the
+%%%          tournament logic. Type: integer().
+%%% TableGlobalId - uniq identifier of a table in the system. Can be used
+%%%          to refer to a table directly - without pointing to a tournament.
+%%%          Type: integer()
+
+-module(nsg_trn_elimination).
+
+-behaviour(gen_fsm).
+%% --------------------------------------------------------------------
+%% Include files
+%% --------------------------------------------------------------------
+-include_lib("server/include/log.hrl").
+-include_lib("server/include/basic_types.hrl").
+-include_lib("db/include/table.hrl").
+-include_lib("db/include/accounts.hrl").
+
+%% --------------------------------------------------------------------
+%% External exports
+-export([start/1, start/2, start_link/2, reg/2]).
+
+%% gen_fsm callbacks
+-export([init/1, handle_event/3, handle_sync_event/4, handle_info/3, terminate/3, code_change/4]).
+
+-export([table_message/3, client_message/2, client_request/2, client_request/3,
+         system_request/2, system_request/3]).
+
+-record(state,
+        {%% Static values
+         game_id           :: pos_integer(),
+         trn_id            :: term(),
+         table_params      :: proplists:proplist(),
+         tours_plan        :: list(integer()), %% Defines how many players will be passed to a next tour
+         tours             :: integer(),
+         players_per_table :: integer(),
+         quota_per_round   :: integer(), %% Using for the calculation of qouta amount which be deducted from players every tour
+         rounds_per_tour   :: integer(), %% Using for the calculation of qouta amount which be deducted from players every tour
+         speed             :: fast | normal,
+         awards            :: list(), %% [FirtsPrize, SecondPrize, ThirdPrize], Prize = undefined | GiftId
+         demo_mode         :: boolean(), %% If true then results of tours will be generated randomly
+         table_module      :: atom(),
+         game_type         :: atom(), %% Using only for quota transactions
+         game_mode         :: atom(), %% Using only for quota transactions
+         %% Dinamic values
+         players,          %% The register of tournament players
+         tables,           %% The register of tournament tables
+         seats,            %% Stores relation between players and tables seats
+         tournament_table  :: list(), %% [{TurnNum, TurnRes}], TurnRes = [{PlayerId, CommonPos, Points, Status}]
+         table_id_counter  :: pos_integer(),
+         tour              :: pos_integer(),
+         cr_tab_requests   :: dict(),  %% {TableId, PlayersIds}
+         reg_requests      :: dict(),  %% {PlayerId, From}
+         tab_requests      :: dict(),  %% {RequestId, RequestContext}
+         timer             :: undefined | reference(),
+         timer_magic       :: undefined | reference(),
+         tables_wl         :: list(), %% Tables waiting list
+         tables_results    :: list()  %% [{TableId, TableResult}]
+        }).
+
+-record(player,
+        {
+         id              :: pos_integer(),
+         user_id,
+         user_info       :: #'PlayerInfo'{},
+         is_bot          :: boolean(),
+         status          :: active | eliminated
+        }).
+
+-record(table,
+        {
+         id              :: pos_integer(),
+         global_id       :: pos_integer(),
+         pid             :: pid(),
+         relay           :: {atom(), pid()}, %% {RelayMod, RelayPid}
+         mon_ref         :: reference(),
+         state           :: initializing | ready | in_process | finished,
+         context         :: term(), %% Context term of a table. For failover proposes.
+         timer           :: reference()
+        }).
+
+-record(seat,
+        {
+         table           :: pos_integer(),
+         seat_num        :: integer(),
+         player_id       :: undefined | pos_integer(),
+         registered_by_table :: undefined | boolean(),
+         connected       :: undefined | boolean()
+        }).
+
+
+-define(STATE_INIT, state_init).
+-define(STATE_WAITING_FOR_TABLES, state_waiting_for_tables).
+-define(STATE_WAITING_FOR_PLAYERS, state_waiting_for_players).
+-define(STATE_TURN_PROCESSING, state_turn_processing).
+-define(STATE_SHOW_SERIES_RESULT, state_show_series_result).
+-define(STATE_FINISHED, state_finished).
+
+-define(TABLE_STATE_INITIALIZING, initializing).
+-define(TABLE_STATE_READY, ready).
+-define(TABLE_STATE_IN_PROGRESS, in_progress).
+-define(TABLE_STATE_WAITING_NEW_ROUND, waiting_new_round).
+-define(TABLE_STATE_FINISHED, finished).
+
+-define(WAITING_PLAYERS_TIMEOUT, 3000). %% Time between all table was created and starting a turn
+-define(REST_TIMEOUT, 5000).            %% Time between a round finish and start of a new one
+-define(SHOW_SERIES_RESULT_TIMEOUT, 15000). %% Time between a tour finish and start of a new one
+-define(SHOW_TOURNAMENT_RESULT_TIMEOUT, 15000). %% Time between last tour result showing and the tournament finish
+
+-define(TOURNAMENT_TYPE, elimination).
+-define(ROUNDS_PER_TOUR, 10).
+%% ====================================================================
+%% External functions
+%% ====================================================================
+
+start([GameId, Params]) -> start(GameId, Params).
+
+start(GameId, Params) ->
+    gen_fsm:start(?MODULE, [GameId, Params, self()], []).
+
+start_link(GameId, Params) ->
+    gen_fsm:start_link(?MODULE, [GameId, Params, self()], []).
+
+reg(Pid, User) ->
+    client_request(Pid, {join, User}, 10000).
+
+table_message(Pid, TableId, Message) ->
+    gen_fsm:send_all_state_event(Pid, {table_message, TableId, Message}).
+
+client_message(Pid, Message) ->
+    gen_fsm:send_all_state_event(Pid, {client_message, Message}).
+
+client_request(Pid, Message) ->
+    client_request(Pid, Message, 5000).
+
+client_request(Pid, Message, Timeout) ->
+    gen_fsm:sync_send_all_state_event(Pid, {client_request, Message}, Timeout).
+
+system_request(Pid, Message) ->
+    system_request(Pid, Message, 5000).
+
+system_request(Pid, Message, Timeout) ->
+    gen_fsm:sync_send_all_state_event(Pid, {system_request, Message}, Timeout).
+
+%% ====================================================================
+%% Server functions
+%% ====================================================================
+
+init([GameId, Params, _Manager]) ->
+    ?INFO("TRN_ELIMINATION <~p> Init started",[GameId]),
+    Registrants = get_param(registrants, Params),
+    QuotaPerRound = get_param(quota_per_round, Params),
+    RoundsPerTour = get_param(rounds_per_tour, Params),
+    Tours = get_param(tours, Params),
+    ToursPlan = get_param(plan, Params),
+    PlayersPerTable = get_param(players_per_table, Params),
+    Speed = get_param(speed, Params),
+    GameType = get_param(game_type, Params),
+    GameMode = get_param(game_mode, Params),
+    Awards = get_param(awards, Params),
+    TableParams = get_param(table_params, Params),
+    TableModule = get_param(table_module, Params),
+
+    DemoMode = get_option(demo_mode, Params, false),
+    TrnId = get_option(trn_id, Params, undefined),
+
+    [?INFO("TRN_ELIMINATION_DBG <~p> Parameter <~p> : ~p", [GameId, P, V]) ||
+     {P, V} <- Params],
+
+    Players = setup_players(Registrants),
+    PlayersIds = get_players_ids(Players),
+    TTable = ttable_init(PlayersIds),
+
+    ?INFO("TRN_ELIMINATION_DBG <~p> TTable: ~p", [GameId, TTable]),
+    ?INFO("TRN_ELIMINATION <~p> started.  Pid:~p", [GameId, self()]),
+
+    gen_fsm:send_all_state_event(self(), go),
+    {ok, ?STATE_INIT, #state{game_id = GameId,
+                             trn_id = TrnId,
+                             table_params = TableParams,
+                             quota_per_round = QuotaPerRound,
+                             rounds_per_tour = RoundsPerTour,
+                             tours_plan = ToursPlan,
+                             tours = Tours,
+                             players_per_table = PlayersPerTable,
+                             speed = Speed,
+                             awards  = Awards,
+                             demo_mode = DemoMode,
+                             table_module = TableModule,
+                             game_type = GameType,
+                             game_mode = GameMode,
+                             players = Players,
+                             tournament_table = TTable,
+                             table_id_counter = 1
+                            }}.
+
+%%===================================================================
+handle_event(go, ?STATE_INIT, #state{game_id = GameId, trn_id = TrnId,
+                                     game_type = GameType} = StateData) ->
+    ?INFO("TRN_ELIMINATION <~p> Received a directive to starting the tournament.", [GameId]),
+    GProcVal = #game_table{game_type = GameType,
+                           game_process = self(),
+                           game_module = ?MODULE,
+                           id = GameId,
+                           trn_id = TrnId,
+                           age_limit = 100,
+                           game_mode = undefined,
+                           game_speed = undefined,
+                           feel_lucky = false,
+                           owner = undefined,
+                           creator = undefined,
+                           rounds = undefined,
+                           pointing_rules   = [],
+                           pointing_rules_ex = [],
+                           users = [],
+                           name = "Elimination Tournament - " ++ erlang:integer_to_list(GameId) ++ " "
+                          },
+    gproc:reg({p,l,self()}, GProcVal),
+    init_tour(1, StateData);
+
+handle_event({client_message, Message}, StateName, #state{game_id = GameId} = StateData) ->
+    ?INFO("TRN_ELIMINATION <~p> Received the message from a client: ~p.", [GameId, Message]),
+    handle_client_message(Message, StateName, StateData);
+
+handle_event({table_message, TableId, Message}, StateName, #state{game_id = GameId} = StateData) ->
+    ?INFO("TRN_ELIMINATION <~p> Received the message from table <~p>: ~p.", [GameId, TableId, Message]),
+    handle_table_message(TableId, Message, StateName, StateData);
+
+handle_event(Message, StateName, #state{game_id = GameId} = StateData) ->
+    ?INFO("TRN_ELIMINATION <~p> Unhandled message(event) received in state <~p>: ~p.",
+          [GameId, StateName, Message]),
+    {next_state, StateName, StateData}.
+
+handle_sync_event({client_request, Request}, From, StateName, #state{game_id = GameId} = StateData) ->
+    ?INFO("TRN_ELIMINATION <~p> Received the request from a client: ~p.", [GameId, Request]),
+    handle_client_request(Request, From, StateName, StateData);
+
+handle_sync_event({system_request, Request}, From, StateName, #state{game_id = GameId} = StateData) ->
+    ?INFO("TRN_ELIMINATION <~p> Received the request from the system: ~p.", [GameId, Request]),
+    handle_system_request(Request, From, StateName, StateData);
+
+handle_sync_event(Request, From, StateName, #state{game_id = GameId} = StateData) ->
+    ?INFO("TRN_ELIMINATION <~p> Unhandled request(event) received in state <~p> from ~p: ~p.",
+          [GameId, StateName, From, Request]),
+    {reply, {error, unknown_request}, StateName, StateData}.
+
+%%===================================================================
+
+handle_info({'DOWN', MonRef, process, _Pid, _}, StateName,
+            #state{game_id = GameId, tables = Tables} = StateData) ->
+    case get_table_by_mon_ref(MonRef, Tables) of
+        #table{id = TableId} ->
+            ?INFO("TRN_ELIMINATION <~p> Table <~p> is down. Stopping", [GameId, TableId]),
+            %% TODO: More smart handling (failover) needed
+            {stop, {one_of_tables_down, TableId}, StateData};
+        not_found ->
+            {next_state, StateName, StateData}
+    end;
+
+
+handle_info({rest_timeout, TableId}, StateName,
+            #state{game_id = GameId, tables = Tables, table_module = TableModule} = StateData) ->
+    ?INFO("TRN_ELIMINATION <~p> Time to start new round for table <~p>.", [GameId, TableId]),
+    #table{pid = TablePid, state = TableState} = Table = fetch_table(TableId, Tables),
+    if TableState == ?TABLE_STATE_WAITING_NEW_ROUND ->
+           NewTable = Table#table{state = ?TABLE_STATE_IN_PROGRESS},
+           NewTables = store_table(NewTable, Tables),
+           send_to_table(TableModule, TablePid, start_round),
+           {next_state, StateName, StateData#state{tables = NewTables}};
+       true ->
+           ?INFO("TRN_ELIMINATION <~p> Don't start new round at table <~p> because it is not waiting for start.",
+                 [GameId, TableId]),
+           {next_state, StateName, StateData}
+    end;
+
+
+handle_info({timeout, Magic}, ?STATE_WAITING_FOR_PLAYERS,
+            #state{timer_magic = Magic, game_id = GameId} = StateData) ->
+    ?INFO("TRN_ELIMINATION <~p> Time to start new turn.", [GameId]),
+    start_turn(StateData);
+
+
+handle_info({timeout, Magic}, ?STATE_SHOW_SERIES_RESULT,
+            #state{timer_magic = Magic, tour = Tour, tours = Tours,
+                   game_id = GameId} = StateData) ->
+    if Tour == Tours ->
+           ?INFO("TRN_ELIMINATION <~p> Time to finalize the tournament.", [GameId]),
+           finalize_tournament(StateData);
+       true ->
+           NewTour = Tour + 1,
+           ?INFO("TRN_ELIMINATION <~p> Time to initialize tour <~p>.", [GameId, NewTour]),
+           init_tour(NewTour, StateData)
+    end;
+
+
+handle_info({timeout, Magic}, ?STATE_FINISHED,
+            #state{timer_magic = Magic, tables = Tables, game_id = GameId,
+                   table_module = TableModule} = StateData) ->
+    ?INFO("TRN_ELIMINATION <~p> Time to stopping the tournament.", [GameId]),
+    finalize_tables_with_disconnect(TableModule, Tables),
+    {stop, normal, StateData#state{tables = [], seats = []}};
+
+
+handle_info(Message, StateName, #state{game_id = GameId} = StateData) ->
+    ?INFO("TRN_ELIMINATION <~p> Unhandled message(info) received in state <~p>: ~p.",
+          [GameId, StateName, Message]),
+    {next_state, StateName, StateData}.
+
+%%===================================================================
+
+terminate(_Reason, _StateName, #state{game_id=GameId}=_StatData) ->
+    ?INFO("TRN_ELIMINATION <~p> Shutting down at state: <~p>. Reason: ~p",
+          [GameId, _StateName, _Reason]),
+    ok.
+
+%%===================================================================
+
+code_change(_OldVsn, StateName, StateData, _Extra) ->
+    {ok, StateName, StateData}.
+
+%% --------------------------------------------------------------------
+%%% Internal functions
+%% --------------------------------------------------------------------
+
+
+handle_client_message(Message, StateName, #state{game_id = GameId} = StateData) ->
+    ?INFO("TRN_ELIMINATION <~p> Unhandled client message received in "
+          "state <~p>: ~p.", [GameId, StateName, Message]),
+    {next_state, StateName, StateData}.
+
+%%===================================================================
+
+handle_table_message(TableId, {player_connected, PlayerId},
+                     StateName,
+                     #state{seats = Seats} = StateData) ->
+    case find_seats_by_player_id(PlayerId, Seats) of
+        [#seat{seat_num = SeatNum}] ->
+            NewSeats = update_seat_connect_status(TableId, SeatNum, true, Seats),
+            {next_state, StateName, StateData#state{seats = NewSeats}};
+        [] -> %% Ignoring the message
+            {next_state, StateName, StateData}
+    end;
+
+
+handle_table_message(TableId, {player_disconnected, PlayerId},
+                     StateName, #state{seats = Seats} = StateData) ->
+    case find_seats_by_player_id(PlayerId, Seats) of
+        [#seat{seat_num = SeatNum}] ->
+            NewSeats = update_seat_connect_status(TableId, SeatNum, false, Seats),
+            {next_state, StateName, StateData#state{seats = NewSeats}};
+        [] -> %% Ignoring the message
+            {next_state, StateName, StateData}
+    end;
+
+
+handle_table_message(TableId, {table_created, Relay},
+                     ?STATE_WAITING_FOR_TABLES,
+                     #state{tables = Tables, seats = Seats, cr_tab_requests = TCrRequests,
+                            table_module = TableModule, reg_requests = RegRequests} = StateData) ->
+    TabInitPlayers = dict:fetch(TableId, TCrRequests),
+    NewTCrRequests = dict:erase(TableId, TCrRequests),
+    %% Update status of players
+    TabSeats = find_seats_by_table_id(TableId, Seats),
+    F = fun(#seat{player_id = PlayerId} = S, Acc) ->
+                case lists:member(PlayerId, TabInitPlayers) of
+                    true -> store_seat(S#seat{registered_by_table = true}, Acc);
+                    false -> Acc
+                end
+        end,
+    NewSeats = lists:foldl(F, Seats, TabSeats),
+
+    %% Process delayed registration requests
+    TablePid = get_table_pid(TableId, Tables),
+    F2 = fun(PlayerId, Acc) ->
+                 case dict:find(PlayerId, Acc) of
+                     {ok, From} ->
+                         gen_fsm:reply(From, {ok, {PlayerId, Relay, {TableModule, TablePid}}}),
+                         dict:erase(PlayerId, Acc);
+                     error -> Acc
+                 end
+         end,
+    NewRegRequests = lists:foldl(F2, RegRequests, TabInitPlayers),
+    NewTables = update_created_table(TableId, Relay, Tables),
+    case dict:size(NewTCrRequests) of
+        0 -> 
+            {TRef, Magic} = start_timer(?WAITING_PLAYERS_TIMEOUT),
+            {next_state, ?STATE_WAITING_FOR_PLAYERS,
+              StateData#state{tables = NewTables, seats = NewSeats, cr_tab_requests = NewTCrRequests,
+                              reg_requests = NewRegRequests, timer = TRef, timer_magic = Magic}};
+        _ -> {next_state, ?STATE_WAITING_FOR_TABLES,
+              StateData#state{tables = NewTables, seats = NewSeats,
+                              cr_tab_requests = NewTCrRequests, reg_requests = NewRegRequests}}
+    end;
+
+
+handle_table_message(TableId, {round_finished, NewScoringState, _RoundScore, _TotalScore},
+                     ?STATE_TURN_PROCESSING,
+                     #state{tables = Tables, table_module = TableModule} = StateData) ->
+    #table{pid = TablePid} = Table = fetch_table(TableId, Tables),
+    TRef = erlang:send_after(?REST_TIMEOUT, self(), {rest_timeout, TableId}),
+    NewTable = Table#table{context = NewScoringState, state = ?TABLE_STATE_WAITING_NEW_ROUND, timer = TRef},
+    NewTables = store_table(NewTable, Tables),
+    send_to_table(TableModule, TablePid, show_round_result),
+    {next_state, ?STATE_TURN_PROCESSING, StateData#state{tables = NewTables}};
+
+
+handle_table_message(TableId, {game_finished, TableContext, _RoundScore, TotalScore},
+                     ?STATE_TURN_PROCESSING = StateName,
+                     #state{tables = Tables, tables_wl = WL, demo_mode = DemoMode,
+                            tables_results = TablesResults, table_module = TableModule
+                           } = StateData) ->
+    TableScore = if DemoMode -> [{PlayerId, crypto:rand_uniform(1, 30)} || {PlayerId, _} <- TotalScore];
+                    true -> TotalScore
+                 end,
+    NewTablesResults = [{TableId, TableScore} | TablesResults],
+    #table{pid = TablePid, state = TableState, timer = TRef} = Table = fetch_table(TableId, Tables),
+    if TableState == ?TABLE_STATE_WAITING_NEW_ROUND -> erlang:cancel_timer(TRef);
+       true -> do_nothing
+    end,
+    NewTable = Table#table{context = TableContext, state = ?TABLE_STATE_FINISHED, timer = undefined},
+    NewTables = store_table(NewTable, Tables),
+    send_to_table(TableModule, TablePid, show_round_result),
+    NewWL = lists:delete(TableId, WL),
+    [send_to_table(TableModule, TPid, {playing_tables_num, length(NewWL)})
+       || #table{pid = TPid, state = ?TABLE_STATE_FINISHED} <- tables_to_list(Tables)],
+    if NewWL == [] ->
+           process_tour_result(StateData#state{tables = NewTables,
+                                               tables_results = NewTablesResults,
+                                               tables_wl = []});
+       true ->
+           {next_state, StateName, StateData#state{tables = NewTables,
+                                                   tables_results = NewTablesResults,
+                                                   tables_wl = NewWL}}
+    end;
+
+
+handle_table_message(TableId, {response, RequestId, Response},
+                     StateName,
+                     #state{game_id = GameId, tab_requests = TabRequests} = StateData) ->
+    NewTabRequests = dict:erase(RequestId, TabRequests),
+    case dict:find(RequestId, TabRequests) of
+        {ok, ReqContext} ->
+            ?INFO("TRN_ELIMINATION <~p> The a response received from table <~p>. "
+                  "RequestId: ~p. Request context: ~p. Response: ~p",
+                  [GameId, TableId, RequestId, ReqContext, Response]),
+            handle_table_response(TableId, ReqContext, Response, StateName,
+                                  StateData#state{tab_requests = NewTabRequests});
+        error ->
+            ?ERROR("TRN_ELIMINATION <~p> Table <~p> sent a response for unknown request. "
+                   "RequestId: ~p. Response", []),
+            {next_state, StateName, StateData#state{tab_requests = NewTabRequests}}
+    end;
+
+
+handle_table_message(TableId, Message, StateName, #state{game_id = GameId} = StateData) ->
+    ?INFO("TRN_ELIMINATION <~p> Unhandled table message received from table <~p> in "
+          "state <~p>: ~p.", [GameId, TableId, StateName, Message]),
+    {next_state, StateName, StateData}.
+
+%%===================================================================
+%% handle_table_response(_TableId, {register_player, PlayerId, TableId, SeatNum}, ok = _Response,
+%%                       StateName,
+%%                       #state{reg_requests = RegRequests, seats = Seats,
+%%                              tables = Tables} = StateData) ->
+%%     Seat = fetch_seat(TableId, SeatNum, Seats),
+%%     NewSeats = store_seat(Seat#seat{registered_by_table = true}, Seats),
+%%     %% Send response to a client for a delayed request
+%%     NewRegRequests =
+%%         case dict:find(PlayerId, RegRequests) of
+%%             {ok, From} ->
+%%                 #table{relay = Relay, pid = TablePid} = fetch_table(TableId, Tables),
+%%                 gen_fsm:reply(From, {ok, {PlayerId, Relay, {?TAB_MOD, TablePid}}}),
+%%                 dict:erase(PlayerId, RegRequests);
+%%             error -> RegRequests
+%%         end,
+%%     {next_state, StateName, StateData#state{seats = NewSeats,
+%%                                             reg_requests = NewRegRequests}};
+
+%% handle_table_response(_TableId, {replace_player, PlayerId, TableId, SeatNum}, ok = _Response,
+%%                       StateName,
+%%                       #state{reg_requests = RegRequests, seats = Seats,
+%%                              tables = Tables} = StateData) ->
+%%     Seat = fetch_seat(TableId, SeatNum, Seats),
+%%     NewSeats = store_seat(Seat#seat{registered_by_table = true}, Seats),
+%%     %% Send response to a client for a delayed request
+%%     NewRegRequests =
+%%         case dict:find(PlayerId, RegRequests) of
+%%             {ok, From} ->
+%%                 #table{relay = Relay, pid = TablePid} = fetch_table(TableId, Tables),
+%%                 gen_fsm:reply(From, {ok, {PlayerId, Relay, {?TAB_MOD, TablePid}}}),
+%%                 dict:erase(PlayerId, RegRequests);
+%%             error -> RegRequests
+%%         end,
+%%     {next_state, StateName, StateData#state{seats = NewSeats,
+%%                                             reg_requests = NewRegRequests}}.
+
+handle_table_response(TableId, RequestContext, Response, StateName,
+                      #state{game_id = GameId} = StateData) ->
+    ?INFO("TRN_ELIMINATION <~p> Unhandled 'table response' received from table <~p> "
+          "in state <~p>. Request context: ~p. Response: ~p.",
+          [GameId, TableId, StateName, RequestContext, Response]),
+    {next_state, StateName, StateData}.
+
+%%===================================================================
+
+handle_client_request({join, User}, From, StateName,
+                      #state{game_id = GameId, reg_requests = RegRequests,
+                             seats = Seats, players=Players, tables = Tables,
+                             table_module = TableModule} = StateData) ->
+    #'PlayerInfo'{id = UserId, robot = _IsBot} = User,
+    ?INFO("TRN_ELIMINATION <~p> The 'Join' request received from user: ~p.", [GameId, UserId]),
+    if StateName == ?STATE_FINISHED ->
+           ?INFO("TRN_ELIMINATION <~p> The tournament is finished. "
+                 "Reject to join user ~p.", [GameId, UserId]),
+           {reply, {error, finished}, StateName, StateData};
+       true ->
+           case get_player_by_user_id(UserId, Players) of
+               {ok, #player{status = active, id = PlayerId}} -> %% The user is an active member of the tournament.
+                   ?INFO("TRN_ELIMINATION <~p> User ~p is an active member of the tournament. "
+                         "Allow to join.", [GameId, UserId]),
+                   [#seat{table = TableId, registered_by_table = RegByTable}] = find_seats_by_player_id(PlayerId, Seats),
+                   case RegByTable of
+                       false -> %% Store this request to the waiting pool
+                           ?INFO("TRN_ELIMINATION <~p> User ~p not yet regirested by the table. "
+                                 "Add the request to the waiting pool.", [GameId, UserId]),
+                           NewRegRequests = dict:store(PlayerId, From, RegRequests),
+                           {next_state, StateName, StateData#state{reg_requests = NewRegRequests}};
+                       _ ->
+                           ?INFO("TRN_ELIMINATION <~p> Return join response for player ~p immediately.",
+                                 [GameId, UserId]),
+                           #table{relay = Relay, pid = TPid} = fetch_table(TableId, Tables),
+                           {reply, {ok, {PlayerId, Relay, {TableModule, TPid}}}, StateName, StateData}
+                   end;
+               {ok, #player{status = eliminated}} ->
+                   ?INFO("TRN_ELIMINATION <~p> User ~p is member of the tournament but he was eliminated. "
+                         "Reject to join.", [GameId, UserId]),
+                   {reply, {error, out}, StateName, StateData};
+               error -> %% Not a member
+                   ?INFO("TRN_ELIMINATION <~p> User ~p is not a member of the tournament. "
+                         "Reject to join.", [GameId, UserId]),
+                   {reply, {error, not_allowed}, StateName, StateData}
+           end
+    end;
+
+handle_client_request(Request, From, StateName, #state{game_id = GameId} = StateData) ->
+    ?INFO("TRN_ELIMINATION <~p> Unhandled client request received from ~p in "
+          "state <~p>: ~p.", [GameId, From, StateName, Request]),
+   {reply, {error, unexpected_request}, StateName, StateData}.
+
+handle_system_request(last_tour_result, _From, StateName,
+                      #state{game_id = GameId, tournament_table = TTable,
+                             players = Players} = StateData) ->
+    ?INFO("TRN_ELIMINATION <~p> Received request for the last tour results.", [GameId]),
+    {LastTourNum, TourResultsRaw} = hd(lists:reverse(lists:keysort(1, TTable))),
+    TourResults = [{get_user_id(PlayerId, Players), CommonPos, Points, Status}
+                   || {PlayerId, CommonPos, Points, Status} <- TourResultsRaw],
+    {reply, {ok, {LastTourNum, TourResults}}, StateName, StateData};
+
+handle_system_request(Request, From, StateName, #state{game_id = GameId} = StateData) ->
+    ?INFO("TRN_ELIMINATION <~p> Unhandled system request received from ~p in "
+          "state <~p>: ~p.", [GameId, From, StateName, Request]),
+    {reply, {error, unexpected_request}, StateName, StateData}.
+
+%%===================================================================
+init_tour(Tour, #state{game_id = GameId, tours_plan = Plan, tournament_table = TTable,
+                       table_params = TableParams, players = Players, quota_per_round = QuotaPerRound,
+                       table_id_counter = TableIdCounter, tables = OldTables, tours = Tours,
+                       players_per_table = PlayersPerTable, game_type = GameType,
+                       game_mode = GameMode, table_module = TableModule,
+                       rounds_per_tour = RoundsPerTour} = StateData) ->
+    ?INFO("TRN_ELIMINATION <~p> Initializing tour <~p>...", [GameId, Tour]),
+    PlayersList = prepare_players_for_new_tour(Tour, TTable, Plan, Players),
+    PrepTTable = prepare_ttable_for_tables(TTable, Players),
+    UsersIds = [UserId || {_, #'PlayerInfo'{id = UserId}, _} <- PlayersList],
+    deduct_quota(GameId, GameType, GameMode, QuotaPerRound * RoundsPerTour, UsersIds),
+    {NewTables, Seats, NewTableIdCounter, CrRequests} =
+        setup_tables(TableModule, PlayersList, PlayersPerTable, PrepTTable, Tour,
+                     Tours, TableIdCounter, GameId, TableParams),
+    if Tour > 1 -> finalize_tables_with_rejoin(TableModule, OldTables);
+       true -> do_nothing
+    end,
+    ?INFO("TRN_ELIMINATION <~p> Initializing of tour <~p> is finished. "
+          "Waiting creating confirmations from the tours' tables...",
+          [GameId, Tour]),
+    {next_state, ?STATE_WAITING_FOR_TABLES, StateData#state{tables = NewTables,
+                                                            seats = Seats,
+                                                            table_id_counter = NewTableIdCounter,
+                                                            tour = Tour,
+                                                            cr_tab_requests = CrRequests,
+                                                            reg_requests = dict:new(),
+                                                            tab_requests = dict:new(),
+                                                            tables_results = []
+                                                           }}.
+
+start_turn(#state{game_id = GameId, tour = Tour, tables = Tables,
+                  table_module = TableModule} = StateData) ->
+    ?INFO("TRN_ELIMINATION <~p> Starting tour <~p>...", [GameId, Tour]),
+    TablesList = tables_to_list(Tables),
+    [send_to_table(TableModule, Pid, start_round) || #table{pid = Pid} <- TablesList],
+    F = fun(Table, Acc) ->
+                store_table(Table#table{state = ?TABLE_STATE_IN_PROGRESS}, Acc)
+        end,
+    NewTables = lists:foldl(F, Tables, TablesList),
+    WL = [T#table.id || T <- TablesList],
+    ?INFO("TRN_ELIMINATION <~p> Tour <~p> is started. Processing...",
+          [GameId, Tour]),
+    {next_state, ?STATE_TURN_PROCESSING, StateData#state{tables = NewTables,
+                                                         tables_wl = WL}}.
+
+
+process_tour_result(#state{game_id = GameId, tournament_table = TTable, tours = Tours,
+                           tours_plan = Plan, tour = Tour, tables_results = TablesResults,
+                           players = Players, tables = Tables, trn_id = TrnId,
+                           table_module = TableModule} = StateData) ->
+    ?INFO("TRN_ELIMINATION <~p> Tour <~p> is completed. Starting results processing...", [GameId, Tour]),
+    TourType = lists:nth(Tour, Plan),
+    TourResult1 = case TourType of
+                     ne -> tour_result_all(TablesResults);
+                     {te, Limit} -> tour_result_per_table(Limit, TablesResults);
+                     {ce, Limit} -> tour_result_overall(Limit, TablesResults)
+                 end,
+    TourResult = set_tour_results_position(TourResult1), %% [{PlayerId, CommonPos, Points, Status}]
+    NewTTable = ttable_store_tour_result(Tour, TourResult, TTable),
+    F = fun({PlayerId, _, _, eliminated}, Acc) -> set_player_status(PlayerId, eliminated, Acc);
+           (_, Acc) -> Acc
+        end,
+    NewPlayers = lists:foldl(F, Players, TourResult),
+    TourResultWithUserId = [{get_user_id(PlayerId, Players), Position, Points, Status}
+                            || {PlayerId, Position, Points, Status} <- TourResult],
+    TablesResultsWithPos = set_tables_results_position(TablesResults, TourResult),
+    [send_to_table(TableModule, TablePid, {tour_result, Tour, TourResultWithUserId})
+       || #table{pid = TablePid} <- tables_to_list(Tables)],
+    [send_to_table(TableModule, get_table_pid(TableId, Tables),
+                   {show_series_result, subs_status(TableResultWithPos, Tour, Plan)})
+       || {TableId, TableResultWithPos} <- TablesResultsWithPos],
+    TourResultWithStrUserId = [{user_id_to_string(UserId), Position, Points, Status}
+                               || {UserId, Position, Points, Status} <- TourResultWithUserId],
+    nsx_msg:notify(["system", "tournament_tour_note"], {TrnId, Tour, Tours, TourType, TourResultWithStrUserId}),
+    {TRef, Magic} = start_timer(?SHOW_SERIES_RESULT_TIMEOUT),
+    ?INFO("TRN_ELIMINATION <~p> Results processing of tour <~p> is finished. "
+          "Waiting some time (~p secs) before continue...",
+          [GameId, Tour, ?SHOW_SERIES_RESULT_TIMEOUT div 1000]),
+    {next_state, ?STATE_SHOW_SERIES_RESULT, StateData#state{timer = TRef, timer_magic = Magic,
+                                                            tournament_table = NewTTable,
+                                                            players = NewPlayers}}.
+
+finalize_tournament(#state{game_id = GameId, awards = Awards, tournament_table = TTable,
+                           players = Players, trn_id = TrnId} = StateData) ->
+    ?INFO("TRN_ELIMINATION <~p> Finalizing the tournament...", [GameId]),
+    AwardsDistrib = awards_distribution(TTable, Awards),
+    AwardsDistribUserId = [{user_id_to_string(get_user_id(PlayerId, Players)), Pos, GiftId}
+                           || {PlayerId, Pos, GiftId} <- AwardsDistrib],
+    [nsx_msg:notify(["gifts", "user", UserId, "give_gift"], {GiftId})
+       || {UserId, _Pos, GiftId} <- AwardsDistribUserId],
+    %% TODO: Do we need advertise the prizes to game clients?
+    ?INFO("TRN_ELIMINATION <~p> Awards distribution: ~p", [GameId, AwardsDistribUserId]),
+    nsx_msg:notify(["system", "tournament_ends_note"], {TrnId, AwardsDistribUserId}),
+    {TRef, Magic} = start_timer(?SHOW_TOURNAMENT_RESULT_TIMEOUT),
+    ?INFO("TRN_ELIMINATION <~p> The tournament is finalized. "
+          "Waiting some time (~p secs) before continue...",
+          [GameId, ?SHOW_TOURNAMENT_RESULT_TIMEOUT div 1000]),
+    {next_state, ?STATE_FINISHED, StateData#state{timer = TRef, timer_magic = Magic}}.
+
+%% awards_distribution(TTable, Awards) -> [{PlayerId, Pos, GiftId}]
+awards_distribution(TTable, Awards) ->
+    MTTable = merge_tournament_table(TTable),
+    F = fun({PlayerId, Pos}, Acc) ->
+                try lists:nth(Pos, Awards) of
+                    undefined -> Acc;
+                    GiftId -> [{PlayerId, Pos, GiftId} | Acc]
+                catch
+                    _:_ -> Acc
+                end
+        end,
+    lists:foldl(F, [], MTTable).
+
+%% merge_tournament_table(TTable) -> [{PlayerId, Pos}]
+merge_tournament_table(TTable) ->
+    [{_, Tour0} | Tours] = lists:keysort(1, TTable),
+    PlayersPos0 = tour_res_to_players_pos(Tour0),
+    F = fun({_, Tour}, Acc) ->
+                lists:foldl(fun({PlayerId, Pos}, Acc2) ->
+                                    lists:keyreplace(PlayerId, 1, Acc2, {PlayerId, Pos})
+                            end, Acc, tour_res_to_players_pos(Tour))
+        end,
+    lists:foldl(F, PlayersPos0, Tours).
+
+%% tour_res_to_players_pos(TourRes) -> [{PlayerId, Pos}]
+tour_res_to_players_pos(TourRes) ->
+    [{PlayerId, Pos} || {PlayerId, Pos, _Points, _Status} <- TourRes].
+
+
+deduct_quota(GameId, GameType, GameMode, Amount, UsersIds) ->
+    TI = #ti_game_event{game_name = GameType, game_mode = GameMode,
+                        id = GameId, double_points = 1,
+                        type = start_tour, tournament_type = ?TOURNAMENT_TYPE},
+    [nsm_accounts:transaction(binary_to_list(UserId), ?CURRENCY_QUOTA, -Amount, TI)
+       || UserId <- UsersIds],
+    ok.
+
+
+tour_result_all(TablesResults) ->
+    F = fun({_, TableRes}, Acc) ->
+            [{Pl, Points, active} || {Pl, Points} <- TableRes] ++ Acc
+        end,
+    lists:foldl(F, [], TablesResults).
+
+
+tour_result_per_table(NextTourLimit, TablesResults) ->
+    F = fun({_, TableResult}, Acc) ->
+                SortedRes = sort_results(TableResult),
+                {Winners, _} = lists:unzip(lists:sublist(SortedRes, NextTourLimit)),
+                [case lists:member(Pl, Winners) of
+                     true -> {Pl, Points, active};
+                     false -> {Pl, Points, eliminated}
+                 end || {Pl, Points} <- TableResult] ++ Acc
+        end,
+    lists:foldl(F, [], TablesResults).
+
+
+tour_result_overall(TourLimit, TablesResults) ->
+    F = fun({_, TableRes}, Acc) -> TableRes ++ Acc end,
+    OverallResults = lists:foldl(F, [], TablesResults),
+    SortedResults = sort_results(OverallResults),
+    {Winners, _} = lists:unzip(lists:sublist(SortedResults, TourLimit)),
+    [case lists:member(Pl, Winners) of
+         true -> {Pl, Points, active};
+         false -> {Pl, Points, eliminated}
+     end || {Pl, Points} <- OverallResults].
+
+%% set_tour_results_position([{PlayerId, Points, Status}]) -> [{PlayerId, Pos, Points, Status}]
+set_tour_results_position(TourResult) ->
+    F = fun({PlayerId, Points, Status}, Pos) ->
+                {{PlayerId, Pos, Points, Status}, Pos + 1}
+        end,
+    {TourResultsWithPos, _} = lists:mapfoldl(F, 1, sort_results2(TourResult)),
+    TourResultsWithPos.
+
+%% set_tables_results_position/2 -> [{TableId, [{PlayerId, Position, Points, Status}]}]
+set_tables_results_position(TablesResults, TurnResult) ->
+    [begin
+         TabResWithStatus = [lists:keyfind(PlayerId, 1, TurnResult) || {PlayerId, _} <- TableResult],
+         {TableId, set_table_results_position(TabResWithStatus)}
+     end || {TableId, TableResult} <- TablesResults].
+
+%% set_table_results_position([{PlayerId, CommonPos, Points, Status}]) -> [{PlayerId, TabPos, Points, Status}]
+set_table_results_position(TableResult) ->
+    F = fun({PlayerId, _, Points, Status}, Pos) ->
+                {{PlayerId, Pos, Points, Status}, Pos + 1}
+        end,
+    {TurnResultsWithPos, _} = lists:mapfoldl(F, 1, lists:keysort(2, TableResult)),
+    TurnResultsWithPos.
+
+subs_status(TableResultWithPos, Turn, Plan) ->
+    LastTurn = Turn == length(Plan),
+    {ActSubst, ElimSubst} = if LastTurn -> {winner, eliminated};
+                               true -> {none, eliminated}
+                            end,
+    [case Status of
+         active -> {PlayerId, Pos, Points, ActSubst};
+         eliminated -> {PlayerId, Pos, Points, ElimSubst}
+     end || {PlayerId, Pos, Points, Status} <- TableResultWithPos].
+
+
+%% sort_results(Results) -> SortedResults
+%% Types: Results = SortedResults = [{PlayerId, Points}]
+%% Description: Sort the list from a best result to a lower one.
+sort_results(Results) ->
+    SF = fun({PId1, Points}, {PId2, Points}) -> PId1 =< PId2;
+            ({_, Points1}, {_, Points2}) -> Points2 =< Points1
+         end,
+    lists:sort(SF, Results).
+
+%% sort_results2(Results) -> SortedResults
+%% Types: Results = SortedResults = [{PlayerId, Points, Status}] Status = active | eliminated
+sort_results2(Results) ->
+    SF = fun({PId1, Points, Status}, {PId2, Points, Status}) -> PId1 =< PId2;
+            ({_PId1, Points1, Status}, {_PId2, Points2, Status}) -> Points2 =< Points1;
+            ({_PId1, _Points1, _Status1}, {_PId2, _Points2, Status2}) -> Status2 == eliminated
+         end,
+    lists:sort(SF, Results).
+
+%% replace_player_by_bot(PlayerId, TableId, SeatNum,
+%%                       #state{players = Players, seats = Seats,
+%%                              game_id = GameId, bots_params = BotsParams,
+%%                              player_id_counter = NewPlayerId, tables = Tables,
+%%                              tab_requests = Requests} = StateData) ->
+%%     NewPlayers = del_player(PlayerId, Players),
+%%     [#'PlayerInfo'{id = UserId} = UserInfo] = spawn_bots(GameId, BotsParams, 1),
+%%     NewPlayers2 = reg_player(#player{id = NewPlayerId, user_id = UserId, is_bot = true}, NewPlayers),
+%%     NewSeats = assign_seat(TableId, SeatNum, NewPlayerId, true, false, false, Seats),
+%%     TablePid = get_table_pid(TableId, Tables),
+%%     NewRequests = table_req_replace_player(TablePid, NewPlayerId, UserInfo, TableId, SeatNum, Requests),
+%%     {next_state, ?STATE_PROCESSING, StateData#state{players = NewPlayers2,
+%%                                                     seats = NewSeats,
+%%                                                     player_id_counter = NewPlayerId + 1,
+%%                                                     tab_requests = NewRequests}}.
+%% 
+
+%% table_req_replace_player(TablePid, PlayerId, UserInfo, TableId, SeatNum, TabRequests) ->
+%%     RequestId = make_ref(),
+%%     NewRequests = dict:store(RequestId, {replace_player, PlayerId, TableId, SeatNum}, TabRequests),
+%%     send_to_table(TablePid, {replace_player, RequestId, UserInfo, PlayerId, SeatNum}),
+%%     NewRequests.
+
+
+
+%% prepare_players_for_new_tour(Tour, TTable, ToursPlan, Players) -> [{PlayerId, UserInfo, Points}]
+prepare_players_for_new_tour(Tour, TTable, ToursPlan, Players) ->
+    PrevTour = Tour - 1,
+    TResult = ttable_get_tour_result(PrevTour, TTable),
+    if Tour == 1 ->
+           [{PlayerId, get_user_info(PlayerId, Players), _Points = 0}
+            || {PlayerId, _, _, active} <- TResult];
+       true ->
+           case lists:nth(PrevTour, ToursPlan) of
+               ne -> %% No one was eliminated => using the prev turn points
+                   [{PlayerId, get_user_info(PlayerId, Players), Points}
+                    || {PlayerId, _, Points, active} <- TResult];
+               _ ->
+                   [{PlayerId, get_user_info(PlayerId, Players), _Points = 0}
+                    || {PlayerId, _, _, active} <- TResult]
+           end
+    end.
+
+
+prepare_ttable_for_tables(TTable, Players) ->
+    [{Tour, [{get_user_id(PlayerId, Players), Place, Score, Status}
+             || {PlayerId, Place, Score, Status} <- Results]}
+     || {Tour, Results} <- TTable].
+
+%% setup_tables(TableModule, Players, PlayersPerTable, TTable, Tour, Tours, TableIdCounter, GameId, TableParams) ->
+%%                              {Tables, Seats, NewTableIdCounter, CrRequests}
+%% Types: Players = {PlayerId, UserInfo, Points}
+%%        TTable = [{Tour, [{UserId, CommonPos, Score, Status}]}]
+setup_tables(TableModule, Players, PlayersPerTable, TTable, Tour,
+             Tours, TableIdCounter, GameId, TableParams) ->
+    SPlayers = shuffle(Players),
+    Groups = split_by_num(PlayersPerTable, SPlayers),
+    F = fun(Group, {TAcc, SAcc, TableId, TCrRequestsAcc}) ->
+                {TPlayers, _} = lists:mapfoldl(fun({PlayerId, UserInfo, Points}, SeatNum) ->
+                                                       {{PlayerId, UserInfo, SeatNum, Points}, SeatNum+1}
+                                               end, 1, Group),
+                TableParams2 = [{players, TPlayers}, {ttable, TTable}, {tour, Tour},
+                                {tours, Tours}, {parent, {?MODULE, self()}} | TableParams],
+                {ok, TabPid} = spawn_table(TableModule, GameId, TableId, TableParams2),
+                MonRef = erlang:monitor(process, TabPid),
+                NewTAcc = reg_table(TableId, TabPid, MonRef, _GlTableId = 0, _Context = undefined, TAcc),
+                F2 = fun({PlId, _, SNum, _}, Acc) ->
+                             assign_seat(TableId, SNum, PlId, _Reg = false, _Conn = false, Acc)
+                     end,
+                NewSAcc = lists:foldl(F2, SAcc, TPlayers),
+                PlayersIds = [PlayerId || {PlayerId, _, _} <- Group],
+                NewTCrRequestsAcc = dict:store(TableId, PlayersIds, TCrRequestsAcc),
+                {NewTAcc, NewSAcc, TableId + 1, NewTCrRequestsAcc}
+        end,
+    lists:foldl(F, {tables_init(), seats_init(), TableIdCounter, dict:new()}, Groups).
+
+
+%% setup_players(Registrants) -> Players
+setup_players(Registrants) ->
+    F = fun(UserId, {Acc, PlayerId}) ->
+                {ok, UserInfo} = auth_server:get_user_info_by_user_id(UserId),
+                NewAcc = store_player(#player{id = PlayerId, user_id = UserId,
+                                              user_info = UserInfo, status = active}, Acc),
+                {NewAcc, PlayerId + 1}
+        end,
+    {Players, _} = lists:foldl(F, {players_init(), 1}, Registrants),
+    Players.
+
+
+%% finalize_tables_with_rejoin(TableModule, Tables) -> ok
+finalize_tables_with_rejoin(TableModule, Tables) ->
+    F = fun(#table{mon_ref = MonRef, pid = TablePid}) ->
+                erlang:demonitor(MonRef, [flush]),
+                send_to_table(TableModule, TablePid, rejoin_players),
+                send_to_table(TableModule, TablePid, stop)
+        end,
+    lists:foreach(F, tables_to_list(Tables)).
+
+%% finalize_tables_with_rejoin(TableModule, Tables) -> ok
+finalize_tables_with_disconnect(TableModule, Tables) ->
+    F = fun(#table{mon_ref = MonRef, pid = TablePid}) ->
+                erlang:demonitor(MonRef, [flush]),
+                send_to_table(TableModule, TablePid, disconnect_players),
+                send_to_table(TableModule, TablePid, stop)
+        end,
+    lists:foreach(F, tables_to_list(Tables)).
+
+
+%% ttable_init(PlayersIds) -> TTable
+%% Types: TTable = [{Tour, TourResult}]
+ttable_init(PlayersIds) -> [{0, [{Id, Id, 0, active} || Id <- PlayersIds]}].
+
+%% ttable_get_tour_result(Tour, TTable) -> undefined | TourResult
+%% Types: TourResult = [{PlayerId, CommonPos, Points, PlayerState}]
+%%          PlayerState = undefined | active | eliminated
+ttable_get_tour_result(Tour, TTable) ->
+    proplists:get_value(Tour, TTable).
+
+%% ttable_store_tour_result(Tour, TourResult, TTable) -> NewTTable
+ttable_store_tour_result(Tour, TourResult, TTable) ->
+    lists:keystore(Tour, 1, TTable, {Tour, TourResult}).
+
+
+%% players_init() -> players()
+players_init() -> midict:new().
+
+%% store_player(#player{}, Players) -> NewPlayers
+store_player(#player{id =Id, user_id = UserId} = Player, Players) ->
+    midict:store(Id, Player, [{user_id, UserId}], Players).
+
+get_players_ids(Players) ->
+    [P#player.id || P <- players_to_list(Players)].
+
+get_player_by_user_id(UserId, Players) ->
+    case midict:geti(UserId, user_id, Players) of
+        [Player] -> {ok, Player};
+        [] -> error
+    end.
+
+%% players_to_list(Players) -> List
+players_to_list(Players) -> midict:all_values(Players).
+
+get_user_info(PlayerId, Players) ->
+    #player{user_info = UserInfo} = midict:fetch(PlayerId, Players),
+    UserInfo.
+
+get_user_id(PlayerId, Players) ->
+    #player{user_id = UserId} = midict:fetch(PlayerId, Players),
+    UserId.
+
+set_player_status(PlayerId, Status, Players) ->
+    Player = midict:fetch(PlayerId, Players),
+    store_player(Player#player{status = Status}, Players).
+
+tables_init() -> midict:new().
+
+reg_table(TableId, Pid, MonRef, GlobalId, TableContext, Tables) ->
+    Table = #table{id = TableId, pid = Pid, mon_ref = MonRef, global_id = GlobalId,
+                   state = initializing, context = TableContext},
+    store_table(Table, Tables).
+
+update_created_table(TableId, Relay, Tables) ->
+    Table = midict:fetch(TableId, Tables),
+    NewTable = Table#table{relay = Relay, state = ?TABLE_STATE_READY},
+    store_table(NewTable, Tables).
+
+store_table(#table{id = TableId, pid = Pid, mon_ref = MonRef, global_id = GlobalId} = Table, Tables) ->
+    midict:store(TableId, Table, [{pid, Pid}, {global_id, GlobalId}, {mon_ref, MonRef}], Tables).
+
+fetch_table(TableId, Tables) -> midict:fetch(TableId, Tables).
+
+get_table_pid(TabId, Tables) ->
+    #table{pid = TabPid} = midict:fetch(TabId, Tables),
+    TabPid.
+
+get_table_by_mon_ref(MonRef, Tables) ->
+    case midict:geti(MonRef, mon_ref, Tables) of
+        [Table] -> Table;
+        [] -> not_found
+    end.
+
+tables_to_list(Tables) -> midict:all_values(Tables).
+
+seats_init() -> midict:new().
+
+find_seats_by_player_id(PlayerId, Seats) ->
+    midict:geti(PlayerId, player_id, Seats).
+
+find_seats_by_table_id(TabId, Seats) ->
+    midict:geti(TabId, table_id, Seats).
+
+%% assign_seat(TabId, SeatNum, PlayerId, RegByTable, Connected, Seats) -> NewSeats
+%% PlayerId = integer()
+%% RegByTable = Connected = undefined | boolean()
+assign_seat(TabId, SeatNum, PlayerId, RegByTable, Connected, Seats) ->
+    Seat = #seat{table = TabId, seat_num = SeatNum, player_id = PlayerId,
+                 registered_by_table = RegByTable, connected = Connected},
+    store_seat(Seat, Seats).
+
+update_seat_connect_status(TableId, SeatNum, ConnStatus, Seats) ->
+    Seat = midict:fetch({TableId, SeatNum}, Seats),
+    NewSeat = Seat#seat{connected = ConnStatus},
+    store_seat(NewSeat, Seats).
+
+store_seat(#seat{table = TabId, seat_num = SeatNum, player_id = PlayerId,
+                 registered_by_table = _RegByTable,
+                 connected = Connected} = Seat, Seats) ->
+    Indices = if PlayerId == undefined ->
+                     [{table_id, TabId}, {free, true}, {free_at_tab, TabId}];
+                 true ->
+                     [{table_id, TabId}, {free, false}, {non_free_at_tab, TabId},
+                      {player_id, PlayerId}, {{connected, TabId}, Connected}]
+              end,
+    midict:store({TabId, SeatNum}, Seat, Indices, Seats).
+
+user_id_to_string(UserId) -> binary_to_list(UserId).
+
+shuffle(List) -> deck:to_list(deck:shuffle(deck:from_list(List))).
+
+split_by_num(Num, List) -> split_by_num(Num, List, []).
+
+split_by_num(_, [], Acc) -> lists:reverse(Acc);
+split_by_num(Num, List, Acc) ->
+    {Group, Rest} = lists:split(Num, List),
+    split_by_num(Num, Rest, [Group | Acc]).
+
+%% start_timer(Timeout) -> {TRef, Magic}
+start_timer(Timeout) ->
+    Magic = make_ref(),
+    TRef = erlang:send_after(Timeout, self(), {timeout, Magic}),
+    {TRef, Magic}.
+
+spawn_table(TableModule, GameId, TableId, Params) -> TableModule:start(GameId, TableId, Params).
+
+send_to_table(TableModule, TabPid, Message) -> TableModule:parent_message(TabPid, Message).
+
+%% table_parameters(ParentMod, ParentPid, Speed) -> Proplist
+%% table_parameters(ParentMod, ParentPid, Speed, GameType, Tours) ->
+%%     [
+%%      {parent, {ParentMod, ParentPid}},
+%%      {seats_num, 4},
+%% %%     {players, []},
+%%      {table_name, ""},
+%%      {mult_factor, 1},
+%%      {slang_allowed, false},
+%%      {observers_allowed, false},
+%%      {tournament_type, elimination},
+%%      {turn_timeout, get_timeout(turn, Speed)},
+%%      {reveal_confirmation_timeout, get_timeout(reveal_confirmation, Speed)},
+%%      {ready_timeout, get_timeout(ready, Speed)},
+%%      {round_timeout, get_timeout(round, Speed)},
+%%      {set_timeout, get_timeout(tour, Tours)},
+%%      {speed, Speed},
+%%      {game_type, GameType},
+%%      {rounds, ?ROUNDS_PER_TOUR},
+%%      {reveal_confirmation, true},
+%%      {next_series_confirmation, no},
+%%      {pause_mode, disabled},
+%%      {social_actions_enabled, false}
+%%     ].
+
+get_param(ParamId, Params) ->
+    {_, Value} = lists:keyfind(ParamId, 1, Params),
+    Value.
+
+get_option(OptionId, Params, DefValue) ->
+    proplists:get_value(OptionId, Params, DefValue).
+
+
+

+ 745 - 0
apps/server/src/nsg_trn_lucky.erl

@@ -0,0 +1,745 @@
+%%% -------------------------------------------------------------------
+%%% Author  : Sergii Polkovnikov <serge.polkovnikov@gmail.com>
+%%% Description : The "Quick play" logic
+%%%
+%%% Created : Oct 16, 2012
+%%% -------------------------------------------------------------------
+
+%%% Terms explanation:
+%%% GameId   - uniq identifier of the tournament. Type: integer().
+%%% PlayerId - registration number of a player in the tournament. Type: integer()
+%%% UserId   - cross system identifier of a physical user. Type: binary() (or string()?).
+%%% TableId  - uniq identifier of a table in the tournament. Used by the
+%%%          tournament logic. Type: integer().
+%%% TableGlobalId - uniq identifier of a table in the system. Can be used
+%%%          to refer to a table directly - without pointing to a tournament.
+%%%          Type: integer()
+
+-module(nsg_trn_lucky).
+
+-behaviour(gen_fsm).
+%% --------------------------------------------------------------------
+%% Include files
+%% --------------------------------------------------------------------
+-include_lib("server/include/log.hrl").
+-include_lib("server/include/basic_types.hrl").
+-include_lib("db/include/table.hrl").
+
+%% --------------------------------------------------------------------
+%% External exports
+-export([start/1, start/2, start_link/2, reg/2]).
+
+%% gen_fsm callbacks
+-export([init/1, handle_event/3, handle_sync_event/4, handle_info/3, terminate/3, code_change/4]).
+
+-export([table_message/3, client_message/2, client_request/2, client_request/3]).
+
+-record(state,
+        {%% Static values
+         game_id           :: pos_integer(),
+         game              :: atom(),
+         game_mode         :: atom(),
+         table_params      :: proplists:proplist(),
+         table_module      :: atom(),
+         bot_module        :: atom(),
+         seats_per_table   :: integer(),
+         game_name         :: string(),
+         mode              :: normal | exclusive,
+         %% Dynamic values
+         players,          %% The register of tournament players
+         tables,           %% The register of tournament tables
+         seats,            %% Stores relation between players and tables seats
+         player_id_counter :: pos_integer(),
+         table_id_counter  :: pos_integer(),
+         cr_tab_requests   :: dict(), %% {TableId, PlayersIds}
+         reg_requests      :: dict(),  %% {PlayerId, From}
+         tab_requests      :: dict()   %% {RequestId, RequestContext}
+        }).
+
+-record(player,
+        {
+         id              :: pos_integer(),
+         user_id,
+         is_bot          :: boolean()
+        }).
+
+-record(table,
+        {
+         id              :: pos_integer(),
+         global_id       :: pos_integer(),
+         pid,
+         relay           :: {atom(), pid()}, %%{RelayMod, RelayPid}
+         mon_ref,
+         state           :: initializing | ready | in_progress | finished,
+         scoring_state,
+         timer           :: reference()
+        }).
+
+-record(seat,
+        {
+         table           :: pos_integer(),
+         seat_num        :: integer(),
+         player_id       :: undefined | pos_integer(),
+         is_bot          :: undefined | boolean(),
+         registered_by_table :: undefined | boolean(),
+         connected       :: undefined | boolean()
+        }).
+
+
+-define(STATE_INIT, state_init).
+-define(STATE_PROCESSING, state_processing).
+
+-define(TABLE_STATE_INITIALIZING, initializing).
+-define(TABLE_STATE_READY, ready).
+-define(TABLE_STATE_IN_PROGRESS, in_progress).
+-define(TABLE_STATE_FINISHED, finished).
+
+-define(REST_TIMEOUT, 5000). %% Time between game finsh and start of new round
+
+%% ====================================================================
+%% External functions
+%% ====================================================================
+
+start([GameId, Params]) -> %% XXX WTF?
+    ?INFO(" +++ START LUCKY"),
+    start(GameId,Params).
+
+start(GameId, Params) ->
+    gen_fsm:start(?MODULE, [GameId, Params, self()], []).
+
+start_link(GameId, Params) ->
+    gen_fsm:start_link(?MODULE, [GameId, Params, self()], []).
+
+reg(Pid, User) ->
+    client_request(Pid, {reg, User}, 10000).
+
+table_message(Pid, TableId, Message) ->
+    gen_fsm:send_all_state_event(Pid, {table_message, TableId, Message}).
+
+client_message(Pid, Message) ->
+    gen_fsm:send_all_state_event(Pid, {client_message, Message}).
+
+client_request(Pid, Message) ->
+    client_request(Pid, Message, 5000).
+
+client_request(Pid, Message, Timeout) ->
+    gen_fsm:sync_send_all_state_event(Pid, {client_request, Message}, Timeout).
+
+
+%% ====================================================================
+%% Server functions
+%% ====================================================================
+
+init([GameId, Params, _Manager]) ->
+    SeatsPerTable = get_param(seats, Params),
+    Game =          get_param(game, Params),
+    GameMode =      get_param(game_mode, Params),
+    GameName =      get_param(game_name, Params),
+%%% XXX    QuotaPerRound = get_param(quota_per_round, Params),
+    TableParams =   get_param(table_params, Params),
+    TableModule =   get_param(table_module, Params),
+    BotModule =     get_param(bot_module, Params),
+
+    ?INFO("TRN_LUCKY <~p> All parameteres are read. Send the directive to start the game.", [GameId]),
+    gen_fsm:send_all_state_event(self(), go),
+    {ok, ?STATE_INIT,
+     #state{game_id = GameId,
+            game = Game,
+            game_mode = GameMode,
+            game_name = GameName,
+            seats_per_table = SeatsPerTable,
+            table_params = [{parent, {?MODULE, self()}} | TableParams],
+            table_module = TableModule,
+            bot_module = BotModule
+           }}.
+
+%%===================================================================
+handle_event(go, ?STATE_INIT, #state{game_id = GameId, game = Game, game_mode = GameMode,
+                                     game_name = GameName} = StateData) ->
+    ?INFO("TRN_LUCKY <~p> Received the directive to start the game.", [GameId]),
+    DeclRec = #game_table{id = GameId,
+                          game_type = Game,
+                          game_mode = GameMode,
+                          game_process = self(),
+                          game_module = ?MODULE,
+                          name = GameName,
+                          age_limit = 100,
+                          game_speed = undefined,
+                          feel_lucky = true,
+                          owner = undefined,
+                          creator = undefined,
+                          rounds = undefined,
+                          pointing_rules   = [],
+                          pointing_rules_ex = [],
+                          users = []
+                         },
+    gproc:reg({p,l,self()}, DeclRec),
+    {next_state, ?STATE_PROCESSING,
+     StateData#state{players = players_init(),
+                     tables = tables_init(),
+                     seats = seats_init(),
+                     player_id_counter = 1,
+                     table_id_counter = 1,
+                     cr_tab_requests = dict:new(),
+                     reg_requests = dict:new(),
+                     tab_requests = dict:new()
+                    }};
+
+handle_event({client_message, Message}, StateName, StateData) ->
+    handle_client_message(Message, StateName, StateData);
+
+handle_event({table_message, TableId, Message}, StateName, StateData) ->
+    handle_table_message(TableId, Message, StateName, StateData);
+
+handle_event(_Event, StateName, StateData) ->
+    {next_state, StateName, StateData}.
+
+handle_sync_event({client_request, Request}, From, StateName, StateData) ->
+    handle_client_request(Request, From, StateName, StateData);
+
+handle_sync_event(_Event, _From, StateName, StateData) ->
+    Reply = ok,
+    {reply, Reply, StateName, StateData}.
+
+%%===================================================================
+
+handle_info({'DOWN', MonRef, process, _Pid, _}, StateName,
+            #state{game_id = GameId, tables = Tables,
+                   seats = Seats, players = Players} = StateData) ->
+    case get_table_by_mon_ref(MonRef, Tables) of
+        #table{id = TabId, timer = TRef} ->
+            ?INFO("TRN_LUCKY <~p> Table <~p> is down. Cleaning up registeres.", [GameId, TabId]),
+            case TRef == undefined of false -> erlang:cancel_timer(TRef); true -> skip end,
+            PlayersIds =
+                [PlayerId || #seat{player_id = PlayerId} <- find_seats_with_players_for_table_id(TabId, Seats)],
+            NewTables = del_table(TabId, Tables),
+            NewSeats = del_seats_by_table_id(TabId, Seats),
+            NewPlayers = del_players(PlayersIds, Players),
+            {next_state, StateName, StateData#state{tables = NewTables,
+                                                    seats = NewSeats,
+                                                    players = NewPlayers}};
+        not_found ->
+            {next_state, StateName, StateData}
+    end;
+
+
+handle_info({rest_timeout, TableId}, StateName,
+            #state{game_id = GameId, tables = Tables, table_module = TableModule} = StateData) ->
+    ?INFO("TRN_LUCKY <~p> Time to start new round for table <~p>.", [GameId, TableId]),
+    case get_table(TableId, Tables) of
+        {ok, #table{pid = TablePid}} ->
+            ?INFO("TRN_LUCKY <~p> Initiating new round at table <~p>.", [GameId, TableId]),
+            NewTables = set_table_state(TableId, ?TABLE_STATE_IN_PROGRESS, Tables),
+            send_to_table(TableModule, TablePid, start_round),
+            {next_state, StateName, StateData#state{tables = NewTables}};
+        error -> %% If no such table ignore the timeout
+            ?INFO("TRN_LUCKY <~p> There is no table <~p>. Can't start new round for it.", [GameId, TableId]),
+            {next_state, StateName, StateData}
+    end;
+
+
+handle_info(Message, StateName, #state{game_id = GameId} = StateData) ->
+    ?INFO("TRN_STANDALONE <~p> Unhandled message(info) received in state <~p>: ~p.",
+          [GameId, StateName, Message]),
+    {next_state, StateName, StateData}.
+
+%%===================================================================
+
+terminate(_Reason, _StateName, #state{game_id=GameId}=_StatData) ->
+    ?INFO("TRN_LUCKY <~p> Shutting down at state: <~p>. Reason: ~p",
+          [GameId, _StateName, _Reason]),
+    ok.
+
+%%===================================================================
+
+code_change(_OldVsn, StateName, StateData, _Extra) ->
+    {ok, StateName, StateData}.
+
+%% --------------------------------------------------------------------
+%%% Internal functions
+%% --------------------------------------------------------------------
+
+
+handle_client_message(_Msg, StateName, StateData) ->
+    {next_state, StateName, StateData}.
+
+%%===================================================================
+
+%% handle_table_message(TableId, Message, StateName, StateData)
+
+handle_table_message(TableId, {player_connected, PlayerId},
+                     ?STATE_PROCESSING,
+                     #state{game_id = GameId, seats = Seats, seats_per_table = SeatsNum,
+                            tables = Tables, table_module = TableModule} = StateData)
+  when is_integer(TableId), is_integer(PlayerId) ->
+    ?INFO("TRN_LUCKY <~p> The player_connected notification received from "
+          "table <~p>. PlayerId: <~p>", [GameId, TableId, PlayerId]),
+    case find_seats_by_player_id(PlayerId, Seats) of
+        [#seat{seat_num = SeatNum}] ->
+            NewSeats = update_seat_connect_status(TableId, SeatNum, true, Seats),
+            case fetch_table(TableId, Tables) of
+                #table{state = ?TABLE_STATE_READY, pid = TabPid} ->
+                    case is_all_players_connected(TableId, SeatsNum, NewSeats) of
+                        true ->
+                            ?INFO("TRN_LUCKY <~p> All clients connected. Starting a game.",
+                                  [GameId]),
+                            NewTables = set_table_state(TableId, ?TABLE_STATE_IN_PROGRESS, Tables),
+                            send_to_table(TableModule, TabPid, start_round),
+                            {next_state, ?STATE_PROCESSING, StateData#state{seats = NewSeats,
+                                                                            tables = NewTables}};
+                        false ->
+                            {next_state, ?STATE_PROCESSING, StateData#state{seats = NewSeats}}
+                    end;
+                _ ->
+                    {next_state, ?STATE_PROCESSING, StateData#state{seats = NewSeats}}
+            end;
+        [] -> %% Ignoring the message
+            {next_state, ?STATE_PROCESSING, StateData}
+    end;
+
+
+handle_table_message(TableId, {player_disconnected, PlayerId},
+                     ?STATE_PROCESSING, #state{game_id = GameId, seats = Seats} = StateData)
+  when is_integer(TableId), is_integer(PlayerId) ->
+    ?INFO("TRN_LUCKY <~p> The player_disconnected notification received from "
+          "table <~p>. PlayerId: <~p>", [GameId, TableId, PlayerId]),
+    case find_seats_by_player_id(PlayerId, Seats) of
+        [#seat{seat_num = SeatNum, is_bot = IsBot}] ->
+            case real_players_at_table(TableId, Seats) of
+                1 when not IsBot -> %% Last real player gone
+                    ?INFO("TRN_LUCKY <~p> Last real player gone from "
+                          "table <~p>. Closing the table.", [GameId, TableId]),
+                    unreg_player_and_eliminate_table(PlayerId, TableId, StateData);
+                _ ->
+                    ?INFO("TRN_LUCKY <~p> Al least one real player is at table <~p>. "
+                          "Starting a bot to replace free seat.", [GameId, TableId]),
+                    replace_player_by_bot(PlayerId, TableId, SeatNum, StateData)
+            end;
+        [] -> %% Ignoring the message
+            {next_state, ?STATE_PROCESSING, StateData}
+    end;
+
+handle_table_message(TableId, {table_created, Relay}, ?STATE_PROCESSING,
+                    #state{game_id = GameId, tables = Tables, seats = Seats,
+                           cr_tab_requests = TCrRequests, table_module = TableModule,
+                           reg_requests = RegRequests} = StateData)
+  when is_integer(TableId) ->
+    ?INFO("TRN_LUCKY <~p> The <table_created> notification received from table: ~p.",
+          [GameId, TableId]),
+
+    TabInitPlayers = dict:fetch(TableId, TCrRequests),
+    %% Update status of players
+    TabSeats = find_seats_by_table_id(TableId, Seats),
+    F = fun(#seat{player_id = PlayerId} = S, Acc) ->
+                case lists:member(PlayerId, TabInitPlayers) of
+                    true -> store_seat(S#seat{registered_by_table = true}, Acc);
+                    false -> Acc
+                end
+        end,
+    NewSeats = lists:foldl(F, Seats, TabSeats),
+
+    %% Process delayed registration requests
+    TablePid = get_table_pid(TableId, Tables),
+    F2 = fun(PlayerId, Acc) ->
+                 case dict:find(PlayerId, Acc) of
+                     {ok, From} ->
+                         gen_fsm:reply(From, {ok, {PlayerId, Relay, {TableModule, TablePid}}}),
+                         dict:erase(PlayerId, Acc);
+                     error -> Acc
+                 end
+         end,
+    NewRegRequests = lists:foldl(F2, RegRequests, TabInitPlayers),
+    NewTCrRequests = dict:erase(TableId, TCrRequests),
+    NewTables = update_created_table(TableId, Relay, Tables),
+    {next_state, ?STATE_PROCESSING, StateData#state{tables = NewTables,
+                                                    seats = NewSeats,
+                                                    cr_tab_requests = NewTCrRequests,
+                                                    reg_requests = NewRegRequests}};
+
+
+handle_table_message(TableId, {round_finished, NewScoringState, _RoundScore, _TotalScore},
+                     ?STATE_PROCESSING,
+                     #state{game_id = GameId, tables = Tables, table_module = TableModule
+                           } = StateData)
+  when is_integer(TableId) ->
+    ?INFO("TRN_LUCKY <~p> The <round_finished> notification received from table: ~p.",
+          [GameId, TableId]),
+    #table{pid = TablePid} = Table = fetch_table(TableId, Tables),
+    TRef = erlang:send_after(?REST_TIMEOUT, self(), {rest_timeout, TableId}),
+    NewTable = Table#table{scoring_state = NewScoringState, state = ?TABLE_STATE_FINISHED, timer = TRef},
+    NewTables = store_table(NewTable, Tables),
+    send_to_table(TableModule, TablePid, show_round_result),
+    {next_state, ?STATE_PROCESSING, StateData#state{tables = NewTables}};
+
+
+handle_table_message(TableId, {response, RequestId, Response},
+                     ?STATE_PROCESSING,
+                     #state{game_id = GameId, tab_requests = TabRequests} = StateData)
+  when is_integer(TableId) ->
+    NewTabRequests = dict:erase(RequestId, TabRequests),
+    case dict:find(RequestId, TabRequests) of
+        {ok, ReqContext} ->
+            ?INFO("TRN_LUCKY <~p> A response received from table <~p>. "
+                  "RequestId: ~p. Request context: ~p. Response: ~p",
+                  [GameId, TableId, RequestId, ReqContext, Response]),
+            handle_table_response(ReqContext, Response, ?STATE_PROCESSING,
+                                  StateData#state{tab_requests = NewTabRequests});
+        error ->
+            ?ERROR("TRN_LUCKY <~p> Table <~p> sent a response for unknown request. "
+                   "RequestId: ~p. Response", []),
+            {next_state, ?STATE_PROCESSING, StateData#state{tab_requests = NewTabRequests}}
+    end;
+
+handle_table_message(_TableId, _Event, StateName, StateData) ->
+    {next_state, StateName, StateData}.
+
+%%===================================================================
+
+%% handle_table_response(RequestContext, Response, StateName, StateData)
+
+handle_table_response({replace_player, PlayerId, TableId, SeatNum}, ok = _Response,
+                      ?STATE_PROCESSING,
+                      #state{reg_requests = RegRequests, seats = Seats,
+                             tables = Tables, table_module = TableModule} = StateData) ->
+    Seat = fetch_seat(TableId, SeatNum, Seats),
+    NewSeats = store_seat(Seat#seat{registered_by_table = true}, Seats),
+    %% Send response to a client for a delayed request
+    NewRegRequests =
+        case dict:find(PlayerId, RegRequests) of
+            {ok, From} ->
+                #table{relay = Relay, pid = TablePid} = fetch_table(TableId, Tables),
+                gen_fsm:reply(From, {ok, {PlayerId, Relay, {TableModule, TablePid}}}),
+                dict:erase(PlayerId, RegRequests);
+            error -> RegRequests
+        end,
+    {next_state, ?STATE_PROCESSING, StateData#state{seats = NewSeats,
+                                                    reg_requests = NewRegRequests}}.
+
+%%===================================================================
+
+handle_client_request({reg, User}, From, ?STATE_PROCESSING,
+                      #state{game_id = GameId, reg_requests = RegRequests,
+                             seats = Seats, players=Players, tables = Tables,
+                             table_module = TableModule} = StateData) ->
+    #'PlayerInfo'{id = UserId, robot = IsBot} = User,
+    ?INFO("TRN_LUCKY <~p> The Register request received from user: ~p.", [GameId, UserId]),
+    case IsBot of
+        true -> %% Bots can't initiate a registration
+            case get_player_id_by_user_id(UserId, Players) of
+                {ok, PlayerId} -> %% Already registered. Send table requsites.
+                    [#seat{table = TableId, registered_by_table = RegByTable}] = find_seats_by_player_id(PlayerId, Seats),
+                    case RegByTable of
+                        false -> %% Store delayed request
+                            NewRegRequests = dict:store(PlayerId, From, RegRequests),
+                            {next_state, ?STATE_PROCESSING, StateData#state{reg_requests = NewRegRequests}};
+                        _ ->
+                            #table{relay = Relay, pid = TPid} = fetch_table(TableId, Tables),
+                            {reply, {ok, {PlayerId, Relay, {TableModule, TPid}}}, ?STATE_PROCESSING, StateData}
+                    end;
+                error -> %% Not registered
+                    ?INFO("TRN_LUCKY <~p> User ~p is a bot. The user not registered. "
+                              "Rejecting registration.", [GameId, UserId]),
+                    {reply, {error, indy_bots_not_allowed}, ?STATE_PROCESSING, StateData}
+            end;
+        false -> %% Normal user
+            IgnoredPlayers = [Id || #player{id = Id} <- midict:geti(UserId, user_id, Players)],
+            ?INFO("TRN_LUCKY <~p> There are no table with free seats.", [GameId]),
+            case find_bot_seat_without_players(Seats, IgnoredPlayers) of
+                #seat{table = TabId, seat_num = SeatNum, player_id = OldPlayerId} ->
+                    ?INFO("TRN_LUCKY <~p> Found a seat with a bot. Replacing by the user. "
+                              "UserId:~p TabId: ~p SeatNum: ~p.", [GameId, UserId, TabId, SeatNum]),
+                    reg_player_with_replace(User, TabId, SeatNum, OldPlayerId, From, StateData);
+                not_found ->
+                    ?INFO("TRN_LUCKY <~p> There are no seats with bots. "
+                              "Creating new table for user: ~p.", [GameId, UserId]),
+                    reg_player_at_new_table(User, From, StateData)
+            end
+    end;
+
+handle_client_request(Request, From, StateName, #state{game_id = GameId} = StateData) ->
+    ?INFO("TRN_LUCKY <~p> Unhandled client request received from ~p in "
+          "state <~p>: ~p.", [GameId, From, StateName, Request]),
+    {reply, {error, unexpected_request}, StateName, StateData}.
+
+%%===================================================================
+
+
+reg_player_with_replace(UserInfo, TableId, SeatNum, OldPlayerId, From,
+                        #state{game_id = GameId, players = Players, tables = Tables,
+                               seats = Seats, player_id_counter = PlayerId,
+                               tab_requests = TabRequests, reg_requests = RegRequests,
+                               table_module = TableModule} = StateData) ->
+    #'PlayerInfo'{id = UserId, robot = IsBot} = UserInfo,
+    NewPlayers = del_player(OldPlayerId, Players),
+    NewPlayers2 = reg_player(#player{id = PlayerId, user_id = UserId, is_bot = IsBot}, NewPlayers),
+    ?INFO("TRN_LUCKY <~p> User ~p registered as player <~p>.", [GameId, UserId, PlayerId]),
+    NewSeats = assign_seat(TableId, SeatNum, PlayerId, IsBot, false, false, Seats),
+    ?INFO("TRN_LUCKY <~p> User ~p assigned to seat <~p> of table <~p>.", [GameId, UserId, SeatNum, TableId]),
+    NewRegRequests = dict:store(PlayerId, From, RegRequests),
+    TablePid = get_table_pid(TableId, Tables),
+    NewTabRequests = table_req_replace_player(TableModule, TablePid, PlayerId, UserInfo, TableId, SeatNum, TabRequests),
+    {next_state, ?STATE_PROCESSING, StateData#state{players = NewPlayers2,
+                                                    seats = NewSeats,
+                                                    player_id_counter = PlayerId + 1,
+                                                    tab_requests = NewTabRequests,
+                                                    reg_requests = NewRegRequests}}.
+
+
+reg_player_at_new_table(User, From,
+                        #state{game_id = GameId, players = Players,
+                               tables = Tables, seats = Seats, seats_per_table = SeatsNum,
+                               player_id_counter = PlayerIdCounter,
+                               table_id_counter = TableId, table_module = TableModule,
+                               bot_module = BotModule, table_params = TableParams,
+                               reg_requests = RegRequests, cr_tab_requests = TCrRequests
+                              } = StateData) ->
+    #'PlayerInfo'{id = UserId, robot = IsBot} = User,
+    RobotsInfo = spawn_bots(GameId, BotModule, SeatsNum - 1),
+    ?INFO("TRN_LUCKY <~p> Bots for table <~p> are spawned.", [GameId, TableId]),
+    F = fun(BotInfo, {PlId,SNum}) -> {{PlId, BotInfo, SNum, _Points = 0}, {PlId + 1, SNum + 1}} end,
+    {RobotsRegData, {PlayerId, SeatNum}} = lists:mapfoldl(F, {PlayerIdCounter, 1}, RobotsInfo),
+
+    TPlayers = [{PlayerId, User, SeatNum, 0} | RobotsRegData],
+    TableParams2 = [{players, TPlayers}, {table_name, "I'm filling lucky"} | TableParams],
+    {ok, TabPid} = spawn_table(TableModule, GameId, TableId, TableParams2),
+
+    MonRef = erlang:monitor(process, TabPid),
+    %% FIXME: Table global id should use a persistent counter
+    NewTables = reg_table(TableId, TabPid, MonRef, 0, undefined, Tables),
+    ?INFO("TRN_LUCKY <~p> New table created: ~p.", [GameId, TableId]),
+
+    NewPlayers = reg_player(#player{id = PlayerId, user_id = UserId, is_bot = IsBot}, Players),
+    F2 = fun({PlId, #'PlayerInfo'{id = UId}, _SNum, _Points}, Acc) ->
+                 reg_player(#player{id = PlId, user_id = UId, is_bot = true}, Acc)
+         end,
+    NewPlayers2 = lists:foldl(F2, NewPlayers, RobotsRegData),
+    ?INFO("TRN_LUCKY <~p> User ~p registered as player <~p>.", [GameId, UserId, PlayerId]),
+
+    NewSeats = assign_seat(TableId, SeatNum, PlayerId, IsBot, false, false, Seats),
+    F3 = fun({PlId, _UserInfo, SNum, _Points}, Acc) ->
+                 assign_seat(TableId, SNum, PlId, true, false, false, Acc)
+         end,
+    NewSeats2 = lists:foldl(F3, NewSeats, RobotsRegData),
+    ?INFO("TRN_LUCKY <~p> User ~p assigned to seat <~p> of table <~p>.", [GameId, UserId, SeatNum, TableId]),
+
+    NewRegRequests = dict:store(PlayerId, From, RegRequests),
+    PlayersIds = [PlayerId | [PlId || {PlId, _, _, _} <- RobotsRegData]],
+    NewTCrRequests = dict:store(TableId, PlayersIds, TCrRequests),
+    {next_state, ?STATE_PROCESSING, StateData#state{players = NewPlayers2,
+                                                    seats = NewSeats2,
+                                                    tables = NewTables,
+                                                    player_id_counter = PlayerId + 1,
+                                                    table_id_counter = TableId + 1,
+                                                    reg_requests = NewRegRequests,
+                                                    cr_tab_requests = NewTCrRequests}}.
+
+
+unreg_player_and_eliminate_table(PlayerId, TableId,
+                                 #state{players = Players, tables = Tables,
+                                        table_module = TableModule, seats = Seats} = StateData) ->
+    NewPlayers = del_player(PlayerId, Players),
+    TablePid = get_table_pid(TableId, Tables),
+    NewSeats = del_seats_by_table_id(TableId, Seats),
+    NewTables = del_table(TableId, Tables),
+    send_to_table(TableModule, TablePid, stop),
+    {next_state, ?STATE_PROCESSING, StateData#state{players = NewPlayers,
+                                                    seats = NewSeats,
+                                                    tables = NewTables}}.
+
+
+replace_player_by_bot(PlayerId, TableId, SeatNum,
+                      #state{players = Players, seats = Seats, game_id = GameId,
+                             bot_module = BotModule, table_module = TableModule,
+                             player_id_counter = NewPlayerId, tables = Tables,
+                             tab_requests = Requests} = StateData) ->
+    NewPlayers = del_player(PlayerId, Players),
+    [#'PlayerInfo'{id = UserId} = UserInfo] = spawn_bots(GameId, BotModule, 1),
+    NewPlayers2 = reg_player(#player{id = NewPlayerId, user_id = UserId, is_bot = true}, NewPlayers),
+    NewSeats = assign_seat(TableId, SeatNum, NewPlayerId, true, false, false, Seats),
+    TablePid = get_table_pid(TableId, Tables),
+    NewRequests = table_req_replace_player(TableModule, TablePid, NewPlayerId, UserInfo, TableId, SeatNum, Requests),
+    {next_state, ?STATE_PROCESSING, StateData#state{players = NewPlayers2,
+                                                    seats = NewSeats,
+                                                    player_id_counter = NewPlayerId + 1,
+                                                    tab_requests = NewRequests}}.
+
+
+%% table_req_replace_player(TableModule, TablePid, PlayerId, UserInfo, TableId, SeatNum, TabRequests) -> NewRequests
+table_req_replace_player(TableModule, TablePid, PlayerId, UserInfo, TableId, SeatNum, TabRequests) ->
+    RequestId = make_ref(),
+    NewRequests = dict:store(RequestId, {replace_player, PlayerId, TableId, SeatNum}, TabRequests),
+    send_to_table(TableModule, TablePid, {replace_player, RequestId, UserInfo, PlayerId, SeatNum}),
+    NewRequests.
+
+
+%% players_init() -> players()
+players_init() ->
+    midict:new().
+
+%% reg_player(#player{}, Players) -> NewPlayers
+reg_player(#player{id =Id, user_id = UserId} = Player, Players) ->
+    midict:store(Id, Player, [{user_id, UserId}], Players).
+
+get_player_id_by_user_id(UserId, Players) ->
+    case midict:geti(UserId, user_id, Players) of
+        [#player{id = PlayerId}] -> {ok, PlayerId};
+        [] -> error
+    end.
+
+%% del_player(PlayerId, Players) -> NewPlayers
+del_player(PlayerId, Players) ->
+    midict:erase(PlayerId, Players).
+
+%% del_player(PlayersIds, Players) -> NewPlayers
+del_players([], Players) -> Players;
+del_players([PlayerId | Rest], Players) ->
+    del_players(Rest, del_player(PlayerId, Players)).
+
+
+tables_init() ->
+    midict:new().
+
+reg_table(TableId, Pid, MonRef, GlobalId, Scoring, Tables) ->
+    Table = #table{id = TableId, pid = Pid, mon_ref = MonRef, global_id = GlobalId,
+                   state = initializing, scoring_state = Scoring},
+    store_table(Table, Tables).
+
+update_created_table(TableId, Relay, Tables) ->
+    Table = midict:fetch(TableId, Tables),
+    NewTable = Table#table{relay = Relay, state = ?TABLE_STATE_READY},
+    store_table(NewTable, Tables).
+
+store_table(#table{id = TableId, pid = Pid, mon_ref = MonRef, global_id = GlobalId} = Table, Tables) ->
+    midict:store(TableId, Table, [{pid, Pid}, {global_id, GlobalId}, {mon_ref, MonRef}], Tables).
+
+fetch_table(TableId, Tables) ->
+    midict:fetch(TableId, Tables).
+
+get_table(TableId, Tables) ->
+    midict:find(TableId, Tables).
+
+get_table_pid(TabId, Tables) ->
+    {ok, #table{pid = TabPid}} = midict:find(TabId, Tables),
+    TabPid.
+
+del_table(TabId, Tables) ->
+    midict:erase(TabId, Tables).
+
+get_table_by_mon_ref(MonRef, Tables) ->
+    case midict:geti(MonRef, mon_ref, Tables) of
+        [Table] -> Table;
+        [] -> not_found
+    end.
+
+set_table_state(TableId, State, Tables) ->
+    Table = midict:fetch(TableId, Tables),
+    store_table(Table#table{state = State}, Tables).
+
+seats_init() ->
+    midict:new().
+
+
+find_bot_seat_without_players(Seats, PlayersList) ->
+    case midict:geti(true, is_bot, Seats) of
+        [] -> not_found;
+        List ->
+            TabList = lists:usort([TabId || #seat{table = TabId} <- List]),
+            lookup_bot_seat_without_players(TabList, PlayersList, Seats)
+    end.
+
+lookup_bot_seat_without_players([], _, _) -> not_found;
+lookup_bot_seat_without_players([TabId | Rest], PlayersList, Seats) ->
+    TabPlayers = [Id || #seat{player_id=Id} <-
+                                 midict:geti(TabId, non_free_at_tab, Seats), lists:member(Id, PlayersList)],
+    if TabPlayers == [] ->
+           ?INFO("TRN_LUCKY Seats:~p", [midict:geti(TabId, table_id, Seats)]),
+           hd(midict:geti(TabId, bot_at_tab, Seats));
+       true -> lookup_bot_seat_without_players(Rest, PlayersList, Seats)
+    end.
+
+find_seats_with_players_for_table_id(TabId, Seats) ->
+    midict:geti(TabId, non_free_at_tab, Seats).
+
+find_seats_by_player_id(PlayerId, Seats) ->
+    midict:geti(PlayerId, player_id, Seats).
+
+find_seats_by_table_id(TabId, Seats) ->
+    midict:geti(TabId, table_id, Seats).
+
+%% real_players_at_table(TabId, Seats) -> Num
+real_players_at_table(TabId, Seats) ->
+    length(find_real_players_seats_at_tab(TabId, Seats)).
+
+is_all_players_connected(TableId, TableSeatsNum, Seats) ->
+    TableSeatsNum == length(midict:geti(true, {connected, TableId}, Seats)).
+
+find_real_players_seats_at_tab(TabId, Seats) ->
+    midict:geti(TabId, real_player_at_tab, Seats).
+
+fetch_seat(TableId, SeatNum, Seats) ->
+    midict:fetch({TableId, SeatNum}, Seats).
+
+%% assign_seat(TabId, SeatNum, PlayerId, IsBot, RegByTable, Connected, Seats) -> NewSeats
+%% PlayerId = integer()
+%% IsBot = RegByTable = Connected = undefined | boolean()
+assign_seat(TabId, SeatNum, PlayerId, IsBot, RegByTable, Connected, Seats) ->
+    Seat = #seat{table = TabId, seat_num = SeatNum, player_id = PlayerId,
+                 is_bot = IsBot, registered_by_table = RegByTable, connected = Connected},
+    store_seat(Seat, Seats).
+
+update_seat_connect_status(TableId, SeatNum, ConnStatus, Seats) ->
+    Seat = midict:fetch({TableId, SeatNum}, Seats),
+    NewSeat = Seat#seat{connected = ConnStatus},
+    store_seat(NewSeat, Seats).
+
+store_seat(#seat{table = TabId, seat_num = SeatNum, player_id = PlayerId,
+                 is_bot = IsBot, registered_by_table = _RegByTable,
+                 connected = Connected} = Seat, Seats) ->
+    Indices = if PlayerId == undefined ->
+                     [{table_id, TabId}, {free, true}, {free_at_tab, TabId}];
+                 true ->
+                     I = [{table_id, TabId}, {free, false}, {non_free_at_tab, TabId},
+                          {player_id, PlayerId}, {is_bot, IsBot},
+                          {{connected, TabId}, Connected}],
+                     if IsBot -> [{bot_at_tab, TabId} | I];
+                        true -> [{real_player_at_tab, TabId} | I]
+                     end
+              end,
+    midict:store({TabId, SeatNum}, Seat, Indices, Seats).
+
+create_seats(_TabId, 0, Seats) -> Seats;
+create_seats(TabId, SeatNum, Seats) ->
+    NewSeats = assign_seat(TabId, SeatNum, undefined, undefined, undefined, undefined, Seats),
+    create_seats(TabId, SeatNum - 1, NewSeats).
+
+
+del_seats_by_table_id(TabId, Seats) ->
+    F = fun(#seat{seat_num = SeatNum}, Acc) ->
+                midict:erase({TabId, SeatNum}, Acc)
+        end,
+    lists:foldl(F, Seats, find_seats_by_table_id(TabId, Seats)).
+
+spawn_bots(GameId, BotModule, BotsNum) ->
+    [spawn_bot(BotModule, GameId) || _ <- lists:seq(1, BotsNum)].
+
+spawn_bot(BotModule, GameId) ->
+    {NPid, UserInfo} = create_robot(BotModule, GameId),
+    BotModule:join_game(NPid),
+    UserInfo.
+
+create_robot(BotModule, GameId) ->
+    UserInfo = auth_server:robot_credentials(),
+    {ok, NPid} = BotModule:start(self(), UserInfo, GameId),
+    BotModule:get_session(NPid), %% Hack for the game_tavla_bot. Creates a game session process.
+    {NPid, UserInfo}.
+
+spawn_table(TabMod, GameId, TableId, Params) ->
+    Pid = TabMod:start(GameId, TableId, Params),
+    Pid.
+
+send_to_table(TabMod, TabPid, Message) ->
+    TabMod:parent_message(TabPid, Message).
+
+get_param(ParamId, Params) ->
+    {_, Value} = lists:keyfind(ParamId, 1, Params),
+    Value.

+ 1156 - 0
apps/server/src/nsg_trn_standalone.erl

@@ -0,0 +1,1156 @@
+%%% -------------------------------------------------------------------
+%%% Author  : Sergei Polkovnikov <serge.polkovnikov@gmail.com>
+%%% Description : The "Stand alone table" logic
+%%%
+%%% Created : Nov 19, 2012
+%%% -------------------------------------------------------------------
+
+%%% Terms explanation:
+%%% GameId   - uniq identifier of the tournament/game. Type: integer().
+%%% PlayerId - registration number of a player in the tournament/game. Type: integer()
+%%% UserId   - cross system identifier of a physical user. Type: binary() (or string()?).
+%%% TableId  - uniq identifier of a table in the tournament/game. Used by the
+%%%          tournament/game logic. Type: integer().
+%%% TableGlobalId - uniq identifier of a table in the system. Can be used
+%%%          to refer to a table directly - without pointing to a tournament/game.
+%%%          Type: integer()
+
+-module(nsg_trn_standalone).
+
+-behaviour(gen_fsm).
+%% --------------------------------------------------------------------
+%% Include files
+%% --------------------------------------------------------------------
+-include_lib("server/include/log.hrl").
+-include_lib("server/include/basic_types.hrl").
+-include_lib("db/include/table.hrl").
+-include_lib("db/include/accounts.hrl").
+-include_lib("eunit/include/eunit.hrl").
+
+%% --------------------------------------------------------------------
+%% External exports
+-export([start/2, start_link/2, reg/2]).
+
+%% gen_fsm callbacks
+-export([init/1, handle_event/3, handle_sync_event/4, handle_info/3, terminate/3, code_change/4]).
+
+-export([table_message/3, client_message/2, client_request/2, client_request/3]).
+
+-record(state,
+        {%% Static values
+         game_id           :: pos_integer(),
+         trn_id            :: term(),
+         game              :: atom(),
+         game_mode         :: atom(),
+         game_name         :: string(),
+         seats_per_table   :: integer(),
+         params            :: proplists:proplist(),
+         table_module      :: atom(),
+         bot_module        :: atom(),
+         quota_per_round   :: integer(),
+         kakush_for_winners :: integer(),
+         kakush_for_loser  :: integer(),
+         win_game_points   :: integer(),
+         mul_factor        :: integer(),
+         registrants       :: [robot | binary()],
+         initial_points    :: integer(),
+         bots_replacement_mode :: enabled | disabled,
+         common_params     :: proplists:proplist(),
+         %% Dinamic values
+         players,          %% The register of tournament players
+         tables,           %% The register of tournament tables
+         seats,            %% Stores relation between players and tables seats
+         table_id_counter  :: pos_integer(),
+         player_id_counter :: pos_integer(),
+         cur_table         :: pos_integer(),
+         tour              :: pos_integer(),
+         cr_tab_requests   :: dict(),  %% {TableId, PlayersIds}
+         reg_requests      :: dict(),  %% {PlayerId, From}
+         tab_requests      :: dict(),  %% {RequestId, RequestContext}
+         timer             :: undefined | reference(),
+         timer_magic       :: undefined | reference(),
+         tables_wl         :: list(), %% Tables waiting list
+         tables_results    :: list()  %% [{TableId, TableResult}]
+        }).
+
+-record(player,
+        {
+         id              :: pos_integer(),
+         user_id,
+         user_info       :: #'PlayerInfo'{},
+         is_bot          :: boolean()
+        }).
+
+-record(table,
+        {
+         id              :: pos_integer(),
+         global_id       :: pos_integer(),
+         pid             :: pid(),
+         relay           :: {atom(), pid()}, %% {RelayMod, RelayPid}
+         mon_ref         :: reference(),
+         state           :: initializing | ready | in_process | finished,
+         context         :: term(), %% Context term of a table. For failover proposes.
+         timer           :: reference()
+        }).
+
+-record(seat,
+        {
+         table           :: pos_integer(),
+         seat_num        :: integer(),
+         player_id       :: undefined | pos_integer(),
+         is_bot          :: undefined | boolean(),
+         registered_by_table :: undefined | boolean(),
+         connected       :: undefined | boolean(),
+         free            :: boolean()
+        }).
+
+
+-define(STATE_INIT, state_init).
+-define(STATE_WAITING_FOR_TABLES, state_waiting_for_tables).
+-define(STATE_EMPTY_SEATS_FILLING, state_empty_seats_filling).
+-define(STATE_WAITING_FOR_PLAYERS, state_waiting_for_players).
+-define(STATE_SET_PROCESSING, state_set_processing).
+-define(STATE_SET_FINISHED, state_set_finished).
+-define(STATE_SHOW_SET_RESULT, state_show_set_result).
+-define(STATE_FINISHED, state_finished).
+
+-define(TOURNAMENT_TYPE, standalone).
+
+-define(TABLE_STATE_INITIALIZING, initializing).
+-define(TABLE_STATE_READY, ready).
+-define(TABLE_STATE_IN_PROGRESS, in_progress).
+-define(TABLE_STATE_FINISHED, finished).
+
+-define(WAITING_PLAYERS_TIMEOUT, 1000) . %% Time between a table was created and start of first round
+-define(REST_TIMEOUT, 5000).             %% Time between a round finish and start of a new one
+-define(SHOW_SET_RESULT_TIMEOUT, 15000). %% Time between a set finish and start of a new one
+-define(SHOW_TOURNAMENT_RESULT_TIMEOUT, 15000). %% Time between last tour result showing and the tournament finish
+
+%% ====================================================================
+%% External functions
+%% ====================================================================
+
+start(GameId, Params) ->
+    gen_fsm:start(?MODULE, [GameId, Params, self()], []).
+
+start_link(GameId, Params) ->
+    gen_fsm:start_link(?MODULE, [GameId, Params, self()], []).
+
+reg(Pid, User) ->
+    client_request(Pid, {join, User}, 10000).
+
+table_message(Pid, TableId, Message) ->
+    gen_fsm:send_all_state_event(Pid, {table_message, TableId, Message}).
+
+client_message(Pid, Message) ->
+    gen_fsm:send_all_state_event(Pid, {client_message, Message}).
+
+client_request(Pid, Message) ->
+    client_request(Pid, Message, 5000).
+
+client_request(Pid, Message, Timeout) ->
+    gen_fsm:sync_send_all_state_event(Pid, {client_request, Message}, Timeout).
+
+
+%% ====================================================================
+%% Server functions
+%% ====================================================================
+
+init([GameId, Params, _Manager]) ->
+    ?INFO("TRN_STANDALONE <~p> Init started.", [GameId]),
+    Registrants =   get_param(registrants, Params),
+    SeatsPerTable = get_param(seats, Params),
+    Game =          get_param(game, Params),
+    GameMode =      get_param(game_mode, Params),
+    GameName =      get_param(game_name, Params),
+    QuotaPerRound = get_param(quota_per_round, Params),
+    KakushForWinners = get_param(kakush_for_winners, Params),
+    KakushForLoser = get_param(kakush_for_loser, Params),
+    WinGamePoints = get_param(win_game_points, Params),
+    MulFactor =     get_param(mul_factor, Params),
+    TableParams =   get_param(table_params, Params),
+    TableModule =   get_param(table_module, Params),
+    BotModule =     get_param(bot_module, Params),
+    InitialPoints = get_param(initial_points, Params),
+    BotsReplacementMode = get_param(bots_replacement_mode, Params),
+    CommonParams  = get_param(common_params, Params),
+
+    ?INFO("TRN_STANDALONE <~p> All parameteres are read. Send the directive to start the game.", [GameId]),
+    gen_fsm:send_all_state_event(self(), go),
+    {ok, ?STATE_INIT, #state{game_id = GameId,
+                             game = Game,
+                             game_mode = GameMode,
+                             game_name = GameName,
+                             seats_per_table = SeatsPerTable,
+                             params = TableParams,
+                             table_module = TableModule,
+                             bot_module = BotModule,
+                             quota_per_round = QuotaPerRound,
+                             kakush_for_winners = KakushForWinners,
+                             kakush_for_loser = KakushForLoser,
+                             win_game_points = WinGamePoints,
+                             mul_factor = MulFactor,
+                             bots_replacement_mode = BotsReplacementMode,
+                             registrants = Registrants,
+                             initial_points = InitialPoints,
+                             table_id_counter = 1,
+                             common_params = CommonParams
+                            }}.
+
+%%===================================================================
+handle_event(go, ?STATE_INIT, #state{game_id = GameId, game = GameType,
+                                     registrants = Registrants, bot_module = BotModule,
+                                     common_params = CommonParams} = StateData) ->
+    ?INFO("TRN_STANDALONE <~p> Received the directive to start the game.", [GameId]),
+    DeclRec = create_decl_rec(GameType, CommonParams, GameId, Registrants),
+    gproc:reg({p,l,self()}, DeclRec),
+    {Players, PlayerIdCounter} = setup_players(Registrants, GameId, BotModule),
+    NewStateData = StateData#state{players = Players,
+                                   player_id_counter = PlayerIdCounter},
+    init_tour(1, NewStateData);
+
+handle_event({client_message, Message}, StateName, #state{game_id = GameId} = StateData) ->
+    ?INFO("TRN_STANDALONE <~p> Received the message from a client: ~p.", [GameId, Message]),
+    handle_client_message(Message, StateName, StateData);
+
+handle_event({table_message, TableId, Message}, StateName, #state{game_id = GameId} = StateData) ->
+    ?INFO("TRN_STANDALONE <~p> Received the message from table <~p>: ~p.", [GameId, TableId, Message]),
+    handle_table_message(TableId, Message, StateName, StateData);
+
+handle_event(Message, StateName, #state{game_id = GameId} = StateData) ->
+    ?INFO("TRN_STANDALONE <~p> Unhandled message(event) received in state <~p>: ~p.",
+          [GameId, StateName, Message]),
+    {next_state, StateName, StateData}.
+
+handle_sync_event({client_request, Request}, From, StateName, #state{game_id = GameId} = StateData) ->
+    ?INFO("TRN_STANDALONE <~p> Received the request from a client: ~p.", [GameId, Request]),
+    handle_client_request(Request, From, StateName, StateData);
+
+handle_sync_event(Request, From, StateName, #state{game_id = GameId} = StateData) ->
+    ?INFO("TRN_STANDALONE <~p> Unhandled request(event) received in state <~p> from ~p: ~p.",
+          [GameId, StateName, From, Request]),
+    {reply, {error, unknown_request}, StateName, StateData}.
+
+%%===================================================================
+
+handle_info({'DOWN', MonRef, process, _Pid, _}, StateName,
+            #state{game_id = GameId, tables = Tables} = StateData) ->
+    case get_table_by_mon_ref(MonRef, Tables) of
+        #table{id = TableId} ->
+            ?INFO("TRN_STANDALONE <~p> Table <~p> is down. Stopping", [GameId, TableId]),
+            %% TODO: More smart handling (failover) needed
+            {stop, {one_of_tables_down, TableId}, StateData};
+        not_found ->
+            {next_state, StateName, StateData}
+    end;
+
+
+handle_info({rest_timeout, TableId}, ?STATE_SET_PROCESSING = StateName,
+            #state{game_id = GameId, game = GameType, game_mode = GameMode,
+                   quota_per_round = Amount, mul_factor = MulFactor, tables = Tables,
+                   players = Players, seats = Seats, cur_table = TableId, bot_module = BotModule,
+                   player_id_counter = PlayerIdCounter, tab_requests = Requests,
+                   table_module = TableMod, common_params = CommonParams} = StateData) ->
+    ?INFO("TRN_STANDALONE <~p> Time to start new round for table <~p>.", [GameId, TableId]),
+    Disconnected = find_disconnected_players(TableId, Seats),
+    ConnectedRealPlayers = [PlayerId || #player{id = PlayerId, is_bot = false} <- players_to_list(Players),
+                                        not lists:member(PlayerId, Disconnected)],
+    case ConnectedRealPlayers of
+        [] -> %% Finish game
+            ?INFO("TRN_STANDALONE <~p> No real players left in table <~p>. "
+                  "Stopping the game.", [GameId, TableId]),
+            finalize_tables_with_disconnect(TableMod, Tables),
+            {stop, normal, StateData#state{tables = [], seats = []}};
+        _ -> %% Replace disconnected players by bots
+            ?INFO("TRN_STANDALONE <~p> Initiating new round at table <~p>.", [GameId, TableId]),
+            {Replacements, NewPlayers, NewSeats, NewPlayerIdCounter} =
+                replace_by_bots(Disconnected, GameId, BotModule, TableId, Players, Seats, PlayerIdCounter),
+            #table{pid = TablePid} = Table = fetch_table(TableId, Tables),
+            NewTables = store_table(Table#table{state = ?TABLE_STATE_IN_PROGRESS}, Tables),
+            RealUsersIds = [UserId || #player{user_id = UserId, is_bot = false} <- players_to_list(NewPlayers)],
+            deduct_quota(GameId, GameType, GameMode, Amount, MulFactor, RealUsersIds),
+            NewRequests = table_req_replace_players(TableMod, TablePid, TableId, Replacements, Requests),
+            send_to_table(TableMod, TablePid, start_round),
+            Users = [if Bot -> robot; true -> UserId end || #player{user_id = UserId, is_bot = Bot} <- players_to_list(NewPlayers)],
+            DeclRec = create_decl_rec(GameType, CommonParams, GameId, Users),
+            gproc:set_value({p,l,self()}, DeclRec),
+            {next_state, StateName, StateData#state{tables = NewTables, players = NewPlayers, seats = NewSeats,
+                                                    tab_requests = NewRequests, player_id_counter = NewPlayerIdCounter}}
+    end;
+
+
+handle_info({rest_timeout, TableId}, ?STATE_SET_FINISHED,
+            #state{game_id = GameId, game = GameType, game_mode = GameMode,
+                   game_name = GameName, tables_results = TablesResults, tables = Tables,
+                   players = Players, cur_table = TableId, table_module = TableMod,
+                   kakush_for_winners = KakushForWinners, kakush_for_loser = KakushForLoser,
+                   win_game_points = WinGamePoints, mul_factor = MulFactor} = StateData) ->
+    ?INFO("TRN_STANDALONE <~p> Time to determinate set results (table: <~p>).", [GameId, TableId]),
+    #table{pid = TablePid} = fetch_table(TableId, Tables),
+    {_, TableScore} = lists:keyfind(TableId, 1, TablesResults),
+    SeriesResult = series_result(TableScore),
+    ?INFO("TRN_STANDALONE <~p> Set result: ~p", [GameId, SeriesResult]),
+    send_to_table(TableMod, TablePid, {show_series_result, SeriesResult}),
+    Points = calc_players_prize_points(SeriesResult, KakushForWinners, KakushForLoser, WinGamePoints, MulFactor, Players),
+    UsersPrizePoints = prepare_users_prize_points(Points, Players),
+    ?INFO("TRN_STANDALONE <~p> Prizes: ~p", [GameId, UsersPrizePoints]),
+    add_points_to_accounts(UsersPrizePoints, GameId, GameType, GameMode, MulFactor),
+    EndsNotePoints = prepare_ends_note_points(SeriesResult, Points, Players),
+    send_ends_note(GameName, GameType, EndsNotePoints),
+    {TRef, Magic} = start_timer(?SHOW_SET_RESULT_TIMEOUT),
+    {next_state, ?STATE_SHOW_SET_RESULT, StateData#state{timer = TRef,
+                                                         timer_magic = Magic}};
+
+
+handle_info({timeout, Magic}, ?STATE_WAITING_FOR_PLAYERS,
+            #state{timer_magic = Magic, game_id = GameId} = StateData) ->
+    ?INFO("TRN_STANDALONE <~p> Time to start new set.", [GameId]),
+    start_set(StateData);
+
+
+handle_info({timeout, Magic}, ?STATE_SHOW_SET_RESULT,
+            #state{timer_magic = Magic, game_id = GameId} = StateData) ->
+    ?INFO("TRN_STANDALONE <~p> Time to finalize the game.", [GameId]),
+    finalize_tournament(StateData);
+
+
+handle_info({timeout, Magic}, ?STATE_FINISHED,
+            #state{timer_magic = Magic, tables = Tables, game_id = GameId,
+                   table_module = TableMod} = StateData) ->
+    ?INFO("TRN_STANDALONE <~p> Time to stopping the game.", [GameId]),
+    finalize_tables_with_disconnect(TableMod, Tables),
+    {stop, normal, StateData#state{tables = [], seats = []}};
+
+
+handle_info(Message, StateName, #state{game_id = GameId} = StateData) ->
+    ?INFO("TRN_STANDALONE <~p> Unhandled message(info) received in state <~p>: ~p.",
+          [GameId, StateName, Message]),
+    {next_state, StateName, StateData}.
+
+%%===================================================================
+
+terminate(_Reason, _StateName, #state{game_id=GameId}=_StatData) ->
+    ?INFO("TRN_STANDALONE <~p> Shutting down at state: <~p>. Reason: ~p",
+          [GameId, _StateName, _Reason]),
+    ok.
+
+%%===================================================================
+
+code_change(_OldVsn, StateName, StateData, _Extra) ->
+    {ok, StateName, StateData}.
+
+%% --------------------------------------------------------------------
+%%% Internal functions
+%% --------------------------------------------------------------------
+
+
+handle_client_message(Message, StateName, #state{game_id = GameId} = StateData) ->
+    ?ERROR("TRN_STANDALONE <~p> Unhandled client message received in "
+           "state <~p>: ~p.", [GameId, StateName, Message]),
+    {next_state, StateName, StateData}.
+
+%%===================================================================
+
+handle_table_message(TableId, {player_connected, PlayerId},
+                     StateName,
+                     #state{seats = Seats} = StateData) ->
+    case find_seats_by_player_id(PlayerId, Seats) of
+        [#seat{seat_num = SeatNum}] ->
+            NewSeats = update_seat_connect_status(TableId, SeatNum, true, Seats),
+            {next_state, StateName, StateData#state{seats = NewSeats}};
+        [] -> %% Ignoring the message
+            {next_state, StateName, StateData}
+    end;
+
+
+handle_table_message(TableId, {player_disconnected, PlayerId},
+                     StateName, #state{seats = Seats} = StateData) ->
+    case find_seats_by_player_id(PlayerId, Seats) of
+        [#seat{seat_num = SeatNum}] ->
+            NewSeats = update_seat_connect_status(TableId, SeatNum, false, Seats),
+            {next_state, StateName, StateData#state{seats = NewSeats}};
+        [] -> %% Ignoring the message
+            {next_state, StateName, StateData}
+    end;
+
+
+handle_table_message(TableId, {table_created, Relay},
+                     ?STATE_WAITING_FOR_TABLES,
+                     #state{tables = Tables, seats = Seats, table_module = TableMod,
+                            cr_tab_requests = TCrRequests, reg_requests = RegRequests,
+                            seats_per_table = SeatsPerTable} = StateData) ->
+    TabInitPlayers = dict:fetch(TableId, TCrRequests),
+    NewTCrRequests = dict:erase(TableId, TCrRequests),
+    %% Update status of players
+    TabSeats = find_seats_by_table_id(TableId, Seats),
+    F = fun(#seat{player_id = PlayerId} = S, Acc) ->
+                case lists:member(PlayerId, TabInitPlayers) of
+                    true -> store_seat(S#seat{registered_by_table = true}, Acc);
+                    false -> Acc
+                end
+        end,
+    NewSeats = lists:foldl(F, Seats, TabSeats),
+
+    %% Process delayed join/registration requests
+    TablePid = get_table_pid(TableId, Tables),
+    F2 = fun(PlayerId, Acc) ->
+                 case dict:find(PlayerId, Acc) of
+                     {ok, From} ->
+                         gen_fsm:reply(From, {ok, {PlayerId, Relay, {TableMod, TablePid}}}),
+                         dict:erase(PlayerId, Acc);
+                     error -> Acc
+                 end
+         end,
+    NewRegRequests = lists:foldl(F2, RegRequests, TabInitPlayers),
+    NewTables = update_created_table(TableId, Relay, Tables),
+
+    case is_table_full_enought(TableId, NewSeats, SeatsPerTable) of
+        true ->
+            {TRef, Magic} = start_timer(?WAITING_PLAYERS_TIMEOUT),
+            {next_state, ?STATE_WAITING_FOR_PLAYERS,
+             StateData#state{tables = NewTables, seats = NewSeats, cr_tab_requests = NewTCrRequests,
+                             reg_requests = NewRegRequests, timer = TRef, timer_magic = Magic}};
+        false ->
+            {next_state, ?STATE_EMPTY_SEATS_FILLING,
+             StateData#state{tables = NewTables, seats = NewSeats, cr_tab_requests = NewTCrRequests,
+                             reg_requests = NewRegRequests}}
+    end;
+
+
+handle_table_message(TableId, {round_finished, TableContext, _RoundScore, _TotalScore},
+                     ?STATE_SET_PROCESSING,
+                     #state{game_id = GameId, tables = Tables, table_module = TableMod} = StateData) ->
+    ?INFO("TRN_STANDALONE <~p> Round is finished (table: <~p>).", [GameId, TableId]),
+    #table{pid = TablePid} = Table = fetch_table(TableId, Tables),
+    TRef = erlang:send_after(?REST_TIMEOUT, self(), {rest_timeout, TableId}),
+    NewTable = Table#table{context = TableContext, state = ?TABLE_STATE_FINISHED, timer = TRef},
+    NewTables = store_table(NewTable, Tables),
+    send_to_table(TableMod, TablePid, show_round_result),
+    ?INFO("TRN_STANDALONE <~p> Waiting some time (~p secs) before start of next round.",
+          [GameId, ?REST_TIMEOUT div 1000]),
+    {next_state, ?STATE_SET_PROCESSING, StateData#state{tables = NewTables}};
+
+
+handle_table_message(TableId, {game_finished, TableContext, _RoundScore, TableScore},
+                     ?STATE_SET_PROCESSING,
+                     #state{game_id = GameId, tables = Tables, table_module = TableMod} = StateData) ->
+    ?INFO("TRN_STANDALONE <~p> Last round of the set is finished (table: <~p>).", [GameId, TableId]),
+    TablesResults = [{TableId, TableScore}],
+    #table{pid = TablePid} = Table = fetch_table(TableId, Tables),
+    TRef = erlang:send_after(?REST_TIMEOUT, self(), {rest_timeout, TableId}),
+    NewTable = Table#table{context = TableContext, state = ?TABLE_STATE_FINISHED, timer = TRef},
+    NewTables = store_table(NewTable, Tables),
+    send_to_table(TableMod, TablePid, show_round_result),
+    ?INFO("TRN_STANDALONE <~p> Waiting some time (~p secs) before the set results calculation.",
+          [GameId, ?REST_TIMEOUT div 1000]),
+    {next_state, ?STATE_SET_FINISHED, StateData#state{tables = NewTables,
+                                                      tables_results = TablesResults}};
+
+
+handle_table_message(TableId, {response, RequestId, Response},
+                     StateName,
+                     #state{game_id = GameId, tab_requests = TabRequests} = StateData) ->
+    NewTabRequests = dict:erase(RequestId, TabRequests),
+    case dict:find(RequestId, TabRequests) of
+        {ok, ReqContext} ->
+            ?INFO("TRN_STANDALONE <~p> The a response received from table <~p>. "
+                  "RequestId: ~p. Request context: ~p. Response: ~p",
+                  [GameId, TableId, RequestId, ReqContext, Response]),
+            handle_table_response(TableId, ReqContext, Response, StateName,
+                                  StateData#state{tab_requests = NewTabRequests});
+        error ->
+            ?ERROR("TRN_STANDALONE <~p> Table <~p> sent a response for unknown request. "
+                   "RequestId: ~p. Response", []),
+            {next_state, StateName, StateData#state{tab_requests = NewTabRequests}}
+    end;
+
+
+handle_table_message(TableId, Message, StateName, #state{game_id = GameId} = StateData) ->
+    ?ERROR("TRN_STANDALONE <~p> Unhandled table message received from table <~p> in "
+           "state <~p>: ~p.", [GameId, TableId, StateName, Message]),
+    {next_state, StateName, StateData}.
+
+%%===================================================================
+%% handle_table_response(_TableId, {register_player, PlayerId, TableId, SeatNum}, ok = _Response,
+%%                       StateName,
+%%                       #state{reg_requests = RegRequests, seats = Seats,
+%%                              tables = Tables} = StateData) ->
+%%     Seat = fetch_seat(TableId, SeatNum, Seats),
+%%     NewSeats = store_seat(Seat#seat{registered_by_table = true}, Seats),
+%%     %% Send response to a client for a delayed request
+%%     NewRegRequests =
+%%         case dict:find(PlayerId, RegRequests) of
+%%             {ok, From} ->
+%%                 #table{relay = Relay, pid = TablePid} = fetch_table(TableId, Tables),
+%%                 gen_fsm:reply(From, {ok, {PlayerId, Relay, {?TAB_MOD, TablePid}}}),
+%%                 dict:erase(PlayerId, RegRequests);
+%%             error -> RegRequests
+%%         end,
+%%     {next_state, StateName, StateData#state{seats = NewSeats,
+%%                                             reg_requests = NewRegRequests}};
+
+handle_table_response(_TableId, {replace_player, PlayerId, TableId, SeatNum}, ok = _Response,
+                      StateName,
+                      #state{reg_requests = RegRequests, seats = Seats,
+                             tables = Tables, table_module = TableMod} = StateData) ->
+    Seat = fetch_seat(TableId, SeatNum, Seats),
+    NewSeats = store_seat(Seat#seat{registered_by_table = true}, Seats),
+    %% Send response to a client for a delayed request
+    NewRegRequests =
+        case dict:find(PlayerId, RegRequests) of
+            {ok, From} ->
+                #table{relay = Relay, pid = TablePid} = fetch_table(TableId, Tables),
+                gen_fsm:reply(From, {ok, {PlayerId, Relay, {TableMod, TablePid}}}),
+                dict:erase(PlayerId, RegRequests);
+            error -> RegRequests
+        end,
+    {next_state, StateName, StateData#state{seats = NewSeats,
+                                            reg_requests = NewRegRequests}};
+
+handle_table_response(TableId, RequestContext, Response, StateName,
+                      #state{game_id = GameId} = StateData) ->
+    ?ERROR("TRN_STANDALONE <~p> Unhandled 'table response' received from table <~p> "
+           "in state <~p>. Request context: ~p. Response: ~p.",
+           [GameId, TableId, StateName, RequestContext, Response]),
+    {next_state, StateName, StateData}.
+
+%%===================================================================
+
+handle_client_request({join, UserInfo}, From, StateName,
+                      #state{game_id = GameId, reg_requests = RegRequests,
+                             seats = Seats, players=Players, tables = Tables,
+                             table_module = TableMod, cur_table = TableId,
+                             bots_replacement_mode = BotsReplacementMode} = StateData) ->
+    #'PlayerInfo'{id = UserId, robot = _IsBot} = UserInfo,
+    ?INFO("TRN_STANDALONE <~p> The 'Join' request received from user: ~p.", [GameId, UserId]),
+    if StateName == ?STATE_FINISHED ->
+           ?INFO("TRN_STANDALONE <~p> The game is finished. "
+                 "Reject to join user ~p.", [GameId, UserId]),
+           {reply, {error, finished}, StateName, StateData};
+       true -> %% Game in progress. Find a seat for the user
+           case get_player_by_user_id(UserId, Players) of
+               {ok, #player{id = PlayerId}} -> %% The user is a registered member of the game (player)
+                   ?INFO("TRN_STANDALONE <~p> User ~p is a registered member of the game. "
+                         "Allow to join.", [GameId, UserId]),
+                   [#seat{table = TableId, registered_by_table = RegByTable}] = find_seats_by_player_id(PlayerId, Seats),
+                   case RegByTable of
+                       false -> %% The player is not registered by the table yet
+                           ?INFO("TRN_STANDALONE <~p> User ~p not yet regirested by the table. "
+                                 "Add the request to the waiting pool.", [GameId, UserId]),
+                           NewRegRequests = dict:store(PlayerId, From, RegRequests),
+                           {next_state, StateName, StateData#state{reg_requests = NewRegRequests}};
+                       _ -> %% The player is registered by the table. Return the table requisites
+                           ?INFO("TRN_STANDALONE <~p> Return the join response for player ~p immediately.",
+                                 [GameId, UserId]),
+                           #table{relay = Relay, pid = TPid} = fetch_table(TableId, Tables),
+                           {reply, {ok, {PlayerId, Relay, {TableMod, TPid}}}, StateName, StateData}
+                   end;
+               error -> %% Not a member yet
+                   ?INFO("TRN_STANDALONE <~p> User ~p is not a member of the game.", [GameId, UserId]),
+                   case find_free_seats(TableId, Seats) of
+                       [] when BotsReplacementMode == disabled ->
+                           ?INFO("TRN_STANDALONE <~p> No free seats for user ~p. Robots replacement is disabled. "
+                                 "Reject to join.", [GameId, UserId]),
+                           {reply, {error, not_allowed}, StateName, StateData};
+                       [] when BotsReplacementMode == enabled ->
+                           ?INFO("TRN_STANDALONE <~p> No free seats for user ~p. Robots replacement is enabled. "
+                                 "Tring to find a robot for replace.", [GameId, UserId]),
+                           case find_registered_robot_seats(TableId, Seats) of
+                               [] ->
+                                   ?INFO("TRN_STANDALONE <~p> No robots for replacement by user ~p. "
+                                         "Reject to join.", [GameId, UserId]),
+                                   {reply, {error, not_allowed}, StateName, StateData};
+                               [#seat{seat_num = SeatNum, player_id = OldPlayerId} | _] ->
+                                   ?INFO("TRN_STANDALONE <~p> There is a robot for replacement by user ~p. "
+                                         "Registering.", [GameId, UserId]),
+                                   reg_player_with_replace(UserInfo, TableId, SeatNum, OldPlayerId, From, StateName, StateData)
+                           end;
+                       [#seat{seat_num = SeatNum} | _] ->
+                           ?INFO("TRN_STANDALONE <~p> There is a free seat for user ~p. "
+                                 "Registering.", [GameId, UserId]),
+                           reg_new_player(UserInfo, TableId, SeatNum, From, StateName, StateData)
+                   end
+           end
+    end;
+
+handle_client_request(Request, From, StateName, #state{game_id = GameId} = StateData) ->
+    ?ERROR("TRN_STANDALONE <~p> Unhandled client request received from ~p in "
+           "state <~p>: ~p.", [GameId, From, StateName, Request]),
+    {reply, {error, unexpected_request}, StateName, StateData}.
+
+%%===================================================================
+init_tour(Tour, #state{game_id = GameId, seats_per_table = SeatsPerTable,
+                       params = TableParams, players = Players, table_module = TableMod,
+                       table_id_counter = TableIdCounter, tables = OldTables,
+                       initial_points = InitialPoints} = StateData) ->
+    ?INFO("TRN_STANDALONE <~p> Initializing tour <~p>...", [GameId, Tour]),
+    PlayersList = prepare_players_for_new_tour(InitialPoints, Players),
+    {NewTables, Seats, NewTableIdCounter, CrRequests} =
+        setup_tables(TableMod, PlayersList, SeatsPerTable, undefined, Tour,
+                     undefined, TableIdCounter, GameId, TableParams),
+    if Tour > 1 -> finalize_tables_with_rejoin(TableMod, OldTables);
+       true -> do_nothing
+    end,
+    ?INFO("TRN_STANDALONE <~p> Initializing of tour <~p> is finished. "
+          "Waiting creating confirmations from the tours' tables...",
+          [GameId, Tour]),
+    {next_state, ?STATE_WAITING_FOR_TABLES, StateData#state{tables = NewTables,
+                                                            seats = Seats,
+                                                            table_id_counter = NewTableIdCounter,
+                                                            cur_table = TableIdCounter,
+                                                            tour = Tour,
+                                                            cr_tab_requests = CrRequests,
+                                                            reg_requests = dict:new(),
+                                                            tab_requests = dict:new(),
+                                                            tables_results = []
+                                                           }}.
+
+
+start_set(#state{game_id = GameId, game = Game, game_mode = GameMode, mul_factor = MulFactor,
+                 quota_per_round = Amount, tour = Tour, tables = Tables, players = Players,
+                 table_module = TableMod} = StateData) ->
+    ?INFO("TRN_STANDALONE <~p> Starting tour <~p>...", [GameId, Tour]),
+    UsersIds = [UserId || #player{user_id = UserId, is_bot = false} <- players_to_list(Players)],
+    deduct_quota(GameId, Game, GameMode, Amount, MulFactor, UsersIds),
+    TablesList = tables_to_list(Tables),
+    [send_to_table(TableMod, Pid, start_round) || #table{pid = Pid} <- TablesList],
+    F = fun(Table, Acc) ->
+                store_table(Table#table{state = ?TABLE_STATE_IN_PROGRESS}, Acc)
+        end,
+    NewTables = lists:foldl(F, Tables, TablesList),
+    WL = [T#table.id || T <- TablesList],
+    ?INFO("TRN_STANDALONE <~p> Tour <~p> is started. Processing...",
+          [GameId, Tour]),
+    {next_state, ?STATE_SET_PROCESSING, StateData#state{tables = NewTables,
+                                                        tables_wl = WL}}.
+
+
+finalize_tournament(#state{game_id = GameId} = StateData) ->
+    ?INFO("TRN_STANDALONE <~p> Finalizing the game...", [GameId]),
+    %% TODO: Real finalization needed
+    {TRef, Magic} = start_timer(?SHOW_TOURNAMENT_RESULT_TIMEOUT),
+    ?INFO("TRN_STANDALONE <~p> The game is finalized. "
+          "Waiting some time (~p secs) before continue...",
+          [GameId, ?SHOW_TOURNAMENT_RESULT_TIMEOUT div 1000]),
+    {next_state, ?STATE_FINISHED, StateData#state{timer = TRef, timer_magic = Magic}}.
+
+
+reg_player_with_replace(UserInfo, TableId, SeatNum, OldPlayerId, From, StateName,
+                        #state{game_id = GameId, players = Players, tables = Tables,
+                               game = GameType, seats = Seats, player_id_counter = PlayerId,
+                               tab_requests = TabRequests, reg_requests = RegRequests,
+                               table_module = TableModule, common_params = CommonParams} = StateData) ->
+    #'PlayerInfo'{id = UserId, robot = IsBot} = UserInfo,
+    NewPlayers = del_player(OldPlayerId, Players),
+    NewPlayers2 = store_player(#player{id = PlayerId, user_id = UserId,
+                                       user_info = UserInfo, is_bot = IsBot}, NewPlayers),
+    ?INFO("TRN_STANDALONE <~p> User ~p registered as player <~p>.", [GameId, UserId, PlayerId]),
+    NewSeats = set_seat(TableId, SeatNum, PlayerId, _Bot = false, _RegByTable = false,
+                        _Connected = false, _Free = false, Seats),
+    ?INFO("TRN_STANDALONE <~p> User ~p assigned to seat <~p> of table <~p>.", [GameId, UserId, SeatNum, TableId]),
+    NewRegRequests = dict:store(PlayerId, From, RegRequests),
+    TablePid = get_table_pid(TableId, Tables),
+    NewTabRequests = table_req_replace_player(TableModule, TablePid, PlayerId, UserInfo, TableId, SeatNum, TabRequests),
+    Users = [if Bot -> robot; true -> UId end || #player{user_id = UId, is_bot = Bot}
+                                                            <- players_to_list(NewPlayers2)],
+    DeclRec = create_decl_rec(GameType,CommonParams, GameId, Users),
+    gproc:set_value({p,l,self()}, DeclRec),
+    {next_state, StateName, StateData#state{players = NewPlayers2,
+                                            seats = NewSeats,
+                                            player_id_counter = PlayerId + 1,
+                                            tab_requests = NewTabRequests,
+                                            reg_requests = NewRegRequests}}.
+
+
+reg_new_player(UserInfo, TableId, SeatNum, From, StateName,
+               #state{game_id = GameId, players = Players, tables = Tables,
+                      game = GameType, seats = Seats, player_id_counter = PlayerId,
+                      tab_requests = TabRequests, reg_requests = RegRequests,
+                      table_module = TableModule, common_params = CommonParams,
+                      seats_per_table = SeatsPerTable} = StateData) ->
+    {SeatNum, NewPlayers, NewSeats} =
+        register_new_player(UserInfo, TableId, Players, Seats, PlayerId),
+    TablePid = get_table_pid(TableId, Tables),
+    NewTabRequests = table_req_replace_player(TableModule, TablePid, PlayerId, UserInfo,
+                                              TableId, SeatNum, TabRequests),
+    NewRegRequests = dict:store(PlayerId, From, RegRequests),
+
+    Users = [if Bot -> robot; true -> UId end || #player{user_id = UId, is_bot = Bot}
+                                                            <- players_to_list(NewPlayers)],
+    DeclRec = create_decl_rec(GameType, CommonParams, GameId, Users),
+    gproc:set_value({p,l,self()}, DeclRec),
+
+    TableIsFull = is_table_full_enought(TableId, NewSeats, SeatsPerTable),
+    NewStateData = StateData#state{reg_requests = NewRegRequests, tab_requests = NewTabRequests,
+                                   players = NewPlayers, seats = NewSeats,
+                                   player_id_counter = PlayerId + 1},
+    if StateName == ?STATE_EMPTY_SEATS_FILLING andalso TableIsFull ->
+           ?INFO("TRN_STANDALONE <~p> It's enough players registered to start the game. "
+                 "Initiating the procedure.", [GameId]),
+           start_set(NewStateData);
+       true ->
+           ?INFO("TRN_STANDALONE <~p> Not enough players registered to start the game. "
+                 "Waiting for more registrations.", [GameId]),
+           {next_state, StateName, NewStateData}
+    end.
+
+
+%% series_result(TableResult) -> WithPlaceAndStatus
+%% Types: TableResult = [{PlayerId, Points}] 
+%%        WithPlaceAndStatus = [{PlayerId, Place, Points, Status}]
+%%          Status = winner | looser
+series_result(TableResult) ->
+    {_, PointsList} = lists:unzip(TableResult),
+    Max = lists:max(PointsList),
+    F = fun({Pl, Points}, {CurPlace, CurPos, LastPoints}) ->
+                if Points == LastPoints ->
+                       {{Pl, CurPlace, Points, if Points == Max -> winner; true -> looser end},
+                        {CurPlace, CurPos + 1, Points}};
+                   true ->
+                       {{Pl, CurPos, Points, looser},
+                        {CurPos, CurPos + 1, Points}}
+                end
+        end,
+    {WithPlaceAndStatus, _} = lists:mapfoldl(F, {1, 1, Max}, lists:reverse(lists:keysort(2, TableResult))),
+    WithPlaceAndStatus.
+
+
+is_table_full_enought(TableId, Seats, EnoughtNumber) ->
+    NonEmptySeats = find_non_free_seats(TableId, Seats),
+    length(NonEmptySeats) >= EnoughtNumber.
+
+
+%% prepare_players_for_new_tour(InitialPoints, Players) -> [{PlayerId, UserInfo, Points}]
+prepare_players_for_new_tour(InitialPoints, Players) ->
+    [{PlayerId, UserInfo, InitialPoints}
+     || #player{id = PlayerId, user_info = UserInfo} <- players_to_list(Players)].
+
+
+%% setup_tables(TableMod, Players, SeatsPerTable, TTable, TableIdCounter, GameId, TableParams) ->
+%%                              {Tables, Seats, NewTableIdCounter, CrRequests}
+%% Types: Players = {PlayerId, UserInfo, Points}
+%%        TTable = [{Tour, [{UserId, CommonPos, Score, Status}]}]
+setup_tables(TableMod, Players, SeatsPerTable, TTable, Tour, Tours, TableId, GameId, TableParams) ->
+    EmptySeatsNum = SeatsPerTable - length(Players),
+    Players2 = lists:duplicate(EmptySeatsNum, empty) ++ Players,
+    SPlayers = shuffle(Players2),
+    {TPlayers, _} = lists:mapfoldl(fun({PlayerId, UserInfo, Points}, SeatNum) ->
+                                           {{PlayerId, UserInfo, SeatNum, Points}, SeatNum+1};
+                                      (empty, SeatNum) ->
+                                           {{_PlayerId = {empty, SeatNum+1}, empty_seat_userinfo(SeatNum+1), SeatNum, _Points=0}, SeatNum+1}
+                                   end, 1, SPlayers),
+    ?INFO("EmptySeatsNum:~p SPlayers: ~p TPlayers:~p", [EmptySeatsNum, SPlayers, TPlayers]),
+    TableParams2 = [{players, TPlayers}, {ttable, TTable}, {tour, Tour},
+                    {tours, Tours}, {parent, {?MODULE, self()}} | TableParams],
+    {ok, TabPid} = spawn_table(TableMod, GameId, TableId, TableParams2),
+    MonRef = erlang:monitor(process, TabPid),
+    Tables = reg_table(TableId, TabPid, MonRef, _GlTableId = 0, _Context = undefined, tables_init()),
+    F2 = fun({PlId, #'PlayerInfo'{robot = Bot}, SNum, _Points}, Acc) ->
+                 case PlId of
+                     {empty,_} -> set_seat(TableId, SNum, _PlId = undefined, _Bot = undefined, _Reg = false, _Conn = false, _Free = true, Acc);
+                     _ -> set_seat(TableId, SNum, PlId, Bot, _Reg = false, _Conn = false, _Free = false, Acc)
+                 end
+         end,
+    Seats = lists:foldl(F2, seats_init(), TPlayers),
+    PlayersIds = [PlayerId || {PlayerId, _, _} <- SPlayers],
+    TCrRequests = dict:store(TableId, PlayersIds, dict:new()),
+    {Tables, Seats, TableId + 1, TCrRequests}.
+
+
+empty_seat_userinfo(Num) ->
+    #'PlayerInfo'{id         = list_to_binary(["empty_", integer_to_list(Num)]),
+                  login      = <<"">>,
+                  name       = <<"empty">>,
+                  surname    = <<" ">>,
+                  age        = 0,
+                  skill      = 0,
+                  score      = 0,
+                  avatar_url = null,
+                  robot      = true }.
+
+
+%% setup_players(Registrants, GameId, BotModule) -> {Players, PlayerIdCounter}
+setup_players(Registrants, GameId, BotModule) ->
+    F = fun(robot, {Acc, PlayerId}) ->
+                #'PlayerInfo'{id = UserId} = UserInfo = spawn_bot(GameId, BotModule),
+                NewAcc = store_player(#player{id = PlayerId, user_id = UserId,
+                                              user_info = UserInfo, is_bot = true}, Acc),
+                {NewAcc, PlayerId + 1};
+           (UserId, {Acc, PlayerId}) ->
+                {ok, UserInfo} = auth_server:get_user_info_by_user_id(UserId),
+                NewAcc = store_player(#player{id = PlayerId, user_id = UserId,
+                                              user_info = UserInfo, is_bot = false}, Acc),
+                {NewAcc, PlayerId + 1}
+        end,
+    lists:foldl(F, {players_init(), 1}, Registrants).
+
+
+%% register_new_player(UserInfo, TableId, Players, Seats, PlayerId) -> {SeatNum, NewPlayers, NewSeats}
+register_new_player(UserInfo, TableId, Players, Seats, PlayerId) ->
+    #'PlayerInfo'{id = UserId, robot = Bot} = UserInfo,
+    [#seat{seat_num = SeatNum} |_] = find_free_seats(TableId, Seats),
+    NewSeats = set_seat(TableId, SeatNum, PlayerId, Bot, _RegByTable = false,
+                        _Connected = false, _Free = false, Seats),
+    NewPlayers = store_player(#player{id = PlayerId, user_id = UserId,
+                                      user_info = UserInfo, is_bot = Bot}, Players),
+    {SeatNum, NewPlayers, NewSeats}.
+
+
+%% replace_by_bots(Disconnected, GameId, BotModule, TableId, Players, Seats, PlayerIdCounter) ->
+%%                                       {Replacements, NewPlayers, NewSeats, NewPlayerIdCounter}
+%% Types: Disconnected = [PlayerId]
+%%        Replacements = [{PlayerId, UserInfo, SeatNum}]
+replace_by_bots(Disconnected, GameId, BotModule, TableId, Players, Seats, PlayerIdCounter) ->
+    F = fun(PlayerId, {RAcc, PAcc, SAcc, Counter}) ->
+                NewPAcc1 = del_player(PlayerId, PAcc),
+                [#seat{seat_num = SeatNum}] = find_seats_by_player_id(PlayerId, TableId, SAcc),
+                NewSAcc = set_seat(TableId, SeatNum, _PlayerId = Counter, _Bot = true,
+                                   _RegByTable = false, _Connected = false, _Free = false, SAcc),
+                #'PlayerInfo'{id = UserId} = UserInfo = spawn_bot(GameId, BotModule),
+                NewPAcc = store_player(#player{id = Counter, user_id = UserId,
+                                               user_info = UserInfo, is_bot = true}, NewPAcc1),
+                NewRAcc = [{Counter, UserInfo, SeatNum} | RAcc],
+                {NewRAcc, NewPAcc, NewSAcc, Counter + 1}
+        end,
+    lists:foldl(F, {[], Players, Seats, PlayerIdCounter}, Disconnected).
+
+
+%% finalize_tables_with_rejoin(TableMod, Tables) -> ok
+finalize_tables_with_rejoin(TableMod, Tables) ->
+    F = fun(#table{mon_ref = MonRef, pid = TablePid}) ->
+                erlang:demonitor(MonRef, [flush]),
+                send_to_table(TableMod, TablePid, rejoin_players),
+                send_to_table(TableMod, TablePid, stop)
+        end,
+    lists:foreach(F, tables_to_list(Tables)).
+
+%% finalize_tables_with_rejoin(TableMod, Tables) -> ok
+finalize_tables_with_disconnect(TableMod, Tables) ->
+    F = fun(#table{mon_ref = MonRef, pid = TablePid}) ->
+                erlang:demonitor(MonRef, [flush]),
+                send_to_table(TableMod, TablePid, disconnect_players),
+                send_to_table(TableMod, TablePid, stop)
+        end,
+    lists:foreach(F, tables_to_list(Tables)).
+
+
+%% table_req_replace_players(TableMod, TablePid, TableId, Replacements, TabRequests) -> NewRequests
+table_req_replace_players(TableMod, TablePid, TableId, Replacements, TabRequests) ->
+    F = fun({NewPlayerId, UserInfo, SeatNum}, Acc) ->
+                table_req_replace_player(TableMod, TablePid, NewPlayerId, UserInfo, TableId, SeatNum, Acc)
+        end,
+    lists:foldl(F, TabRequests, Replacements).
+
+
+%% table_req_replace_player(TableMod, TablePid, PlayerId, UserInfo, TableId, SeatNum, TabRequests) -> NewRequests
+table_req_replace_player(TableMod, TablePid, PlayerId, UserInfo, TableId, SeatNum, TabRequests) ->
+    RequestId = make_ref(),
+    NewRequests = dict:store(RequestId, {replace_player, PlayerId, TableId, SeatNum}, TabRequests),
+    send_to_table(TableMod, TablePid, {replace_player, RequestId, UserInfo, PlayerId, SeatNum}),
+    NewRequests.
+
+
+%% find_disconnected_players(TableId, Seats) -> PlayersIds
+find_disconnected_players(TableId, Seats) ->
+    [PlayerId || #seat{player_id = PlayerId} <- find_disconnected_seats(TableId, Seats)].
+
+%% players_init() -> players()
+players_init() -> midict:new().
+
+%% store_player(#player{}, Players) -> NewPlayers
+store_player(#player{id =Id, user_id = UserId} = Player, Players) ->
+    midict:store(Id, Player, [{user_id, UserId}], Players).
+
+get_player_by_user_id(UserId, Players) ->
+    case midict:geti(UserId, user_id, Players) of
+        [Player] -> {ok, Player};
+        [] -> error
+    end.
+
+%% players_to_list(Players) -> List
+players_to_list(Players) -> midict:all_values(Players).
+
+get_user_info(PlayerId, Players) ->
+    #player{user_info = UserInfo} = midict:fetch(PlayerId, Players),
+    UserInfo.
+
+fetch_player(PlayerId, Players) ->
+    midict:fetch(PlayerId, Players).
+
+get_user_id(PlayerId, Players) ->
+    #player{user_id = UserId} = midict:fetch(PlayerId, Players),
+    UserId.
+
+%% del_player(PlayerId, Players) -> NewPlayers
+del_player(PlayerId, Players) ->
+    midict:erase(PlayerId, Players).
+
+tables_init() -> midict:new().
+
+reg_table(TableId, Pid, MonRef, GlobalId, TableContext, Tables) ->
+    Table = #table{id = TableId, pid = Pid, mon_ref = MonRef, global_id = GlobalId,
+                   state = initializing, context = TableContext},
+    store_table(Table, Tables).
+
+update_created_table(TableId, Relay, Tables) ->
+    Table = midict:fetch(TableId, Tables),
+    NewTable = Table#table{relay = Relay, state = ?TABLE_STATE_READY},
+    store_table(NewTable, Tables).
+
+store_table(#table{id = TableId, pid = Pid, mon_ref = MonRef, global_id = GlobalId} = Table, Tables) ->
+    midict:store(TableId, Table, [{pid, Pid}, {global_id, GlobalId}, {mon_ref, MonRef}], Tables).
+
+fetch_table(TableId, Tables) -> midict:fetch(TableId, Tables).
+
+get_table_pid(TabId, Tables) ->
+    #table{pid = TabPid} = midict:fetch(TabId, Tables),
+    TabPid.
+
+del_table(TabId, Tables) -> midict:erase(TabId, Tables).
+
+get_table_by_mon_ref(MonRef, Tables) ->
+    case midict:geti(MonRef, mon_ref, Tables) of
+        [Table] -> Table;
+        [] -> not_found
+    end.
+
+tables_to_list(Tables) -> midict:all_values(Tables).
+
+seats_init() -> midict:new().
+
+find_seats_by_player_id(PlayerId, Seats) ->
+    midict:geti(PlayerId, player_id, Seats).
+
+find_seats_by_player_id(PlayerId, TableId, Seats) ->
+    midict:geti({PlayerId, TableId}, player_at_table, Seats).
+
+find_seats_by_table_id(TabId, Seats) ->
+    midict:geti(TabId, table_id, Seats).
+
+find_disconnected_seats(TableId, Seats) ->
+    midict:geti(false, {connected, TableId}, Seats).
+
+find_non_free_seats(TableId, Seats) ->
+    midict:geti(false, {free_at_tab, TableId}, Seats).
+
+find_free_seats(TableId, Seats) ->
+    midict:geti(true, {free_at_tab, TableId}, Seats).
+
+find_registered_robot_seats(TableId, Seats) ->
+    [S || S = #seat{registered_by_table = true, is_bot = true} <- midict:geti(TableId, table_id, Seats)].
+
+fetch_seat(TableId, SeatNum, Seats) -> midict:fetch({TableId, SeatNum}, Seats).
+
+%% set_seat(TabId, SeatNum, PlayerId, IsBot, RegByTable, Connected, Free, Seats) -> NewSeats
+%% PlayerId = integer()
+%% IsBot = RegByTable = Connected = undefined | boolean()
+set_seat(TabId, SeatNum, PlayerId, IsBot, RegByTable, Connected, Free, Seats) ->
+    Seat = #seat{table = TabId, seat_num = SeatNum, player_id = PlayerId, is_bot = IsBot,
+                 registered_by_table = RegByTable, connected = Connected, free = Free},
+    store_seat(Seat, Seats).
+
+update_seat_connect_status(TableId, SeatNum, ConnStatus, Seats) ->
+    Seat = midict:fetch({TableId, SeatNum}, Seats),
+    NewSeat = Seat#seat{connected = ConnStatus},
+    store_seat(NewSeat, Seats).
+
+store_seat(#seat{table = TabId, seat_num = SeatNum, player_id = PlayerId,
+                 is_bot = _IsBot, registered_by_table = _RegByTable,
+                 connected = Connected, free = Free} = Seat, Seats) ->
+    Indices = if Free == true ->
+                     [{table_id, TabId}, {free, true}, {{free_at_tab, TabId}, true}];
+                 true ->
+                     [{table_id, TabId}, {free, false}, {{free_at_tab, TabId}, false},
+                      {player_at_table, {PlayerId, TabId}}, {player_id, PlayerId},
+                      {{connected, TabId}, Connected}]
+              end,
+    midict:store({TabId, SeatNum}, Seat, Indices, Seats).
+
+
+%% calc_players_prize_points(SeriesResult, KakushForWinner, KakushForLoser,
+%%                           WinGamePoints, MulFactor, Players) -> Points
+%% Types:
+%%     SeriesResult = [{PlayerId, _Pos, _Points, Status}]
+%%          Status = winner | looser
+%%     Points = [{PlayerId, KakushPoints, GamePoints}]
+calc_players_prize_points(SeriesResult, KakushForWinners, KakushForLoser, WinGamePoints, MulFactor, Players) ->
+    SeriesResult1 = [begin
+                         #'PlayerInfo'{id = UserId, robot = Robot} = get_user_info(PlayerId, Players),
+                         Paid = is_paid(user_id_to_string(UserId)),
+                         Winner = Status == winner,
+                         {PlayerId, Winner, Robot, Paid}
+                     end || {PlayerId, _Pos, _Points, Status} <- SeriesResult],
+    Paids   = [PlayerId || {PlayerId, _Winner, _Robot, _Paid = true} <- SeriesResult1],
+    Winners = [PlayerId || {PlayerId, _Winner = true, _Robot, _Paid} <- SeriesResult1],
+    TotalNum = length(SeriesResult),
+    PaidsNum = length(Paids),
+    WinnersNum = length(Winners),
+    KakushPerWinner = round(((KakushForWinners * MulFactor) * PaidsNum div TotalNum) / WinnersNum),
+    KakushPerLoser = (KakushForLoser * MulFactor) * PaidsNum div TotalNum,
+    WinGamePoints1 = WinGamePoints * MulFactor,
+    [begin
+         {KakushPoints, GamePoints} = calc_points(KakushPerWinner, KakushPerLoser, WinGamePoints1, Paid, Robot, Winner),
+         {PlayerId, KakushPoints, GamePoints}
+     end || {PlayerId, Winner, Robot, Paid} <- SeriesResult1].
+
+
+calc_points(KakushPerWinner, KakushPerLoser, WinGamePoints, Paid, Robot, Winner) ->
+    if Robot -> {0, 0};
+       not Paid andalso Winner -> {0, WinGamePoints};
+       not Paid -> {0, 0};
+       Paid andalso Winner -> {KakushPerWinner, WinGamePoints};
+       Paid -> {KakushPerLoser, 0}
+    end.
+
+%% prepare_ends_note_points(SeriesResult, Points, Players) -> EndsNotePoints
+%% Types: EndsNotePoints = [{UserIdStr, Robot, Pos, KakushPoints, GamePoints}]
+prepare_ends_note_points(SeriesResult, Points, Players) ->
+    [begin
+         #player{user_id = UserId, is_bot = Robot} = fetch_player(PlayerId, Players),
+         {_, KPoints, GPoints} = lists:keyfind(PlayerId, 1, Points),
+         {user_id_to_string(UserId), Robot, Pos, KPoints, GPoints}
+     end || {PlayerId, Pos, _Points, _Status} <- SeriesResult].
+
+%% prepare_users_prize_points(Points, Players) -> UsersPrizePoints
+%% Types:
+%%     Points = [{PlayerId, KakushPoints, GamePoints}]
+%%     UserPrizePoints = [{UserIdStr, KakushPoints, GamePoints}]
+prepare_users_prize_points(Points, Players) ->
+    [{user_id_to_string(get_user_id(PlayerId, Players)), K, G} || {PlayerId, K, G} <- Points].
+
+is_paid(UserId) -> nsm_accounts:user_paid(UserId).
+
+deduct_quota(GameId, GameType, GameMode, Amount, MulFactor, UsersIds) ->
+    RealAmount = Amount * MulFactor,
+    [begin
+         TI = #ti_game_event{game_name = GameType, game_mode = GameMode,
+                             id = GameId, double_points = MulFactor,
+                             type = start_round, tournament_type = ?TOURNAMENT_TYPE},
+         nsm_accounts:transaction(binary_to_list(UserId), ?CURRENCY_QUOTA, -RealAmount, TI)
+     end || UserId <- UsersIds],
+    ok.
+
+
+%% add_points_to_accounts(Points, GameId, GameType, GameMode, MulFactor) -> ok
+%% Types: Points = [{UserId, KakushPoints, GamePoints}]
+add_points_to_accounts(Points, GameId, GameType, GameMode, MulFactor) ->
+    TI = #ti_game_event{game_name = GameType, game_mode = GameMode,
+                        id = GameId, double_points = MulFactor,
+                        type = game_end, tournament_type = ?TOURNAMENT_TYPE},
+    [begin
+         if KakushPoints =/= 0 ->
+                ok = nsm_accounts:transaction(UserId, ?CURRENCY_KAKUSH, KakushPoints, TI);
+            true -> do_nothing
+         end,
+         if GamePoints =/= 0 ->
+                ok = nsm_accounts:transaction(UserId, ?CURRENCY_GAME_POINTS, GamePoints, TI);
+            true -> do_nothing
+         end
+     end || {UserId, KakushPoints, GamePoints} <- Points],
+    ok.
+
+send_ends_note(GameName, GameType, EndsNotePoints) ->
+    nsx_msg:notify(["system", "game_ends_note"], {{GameName, GameType}, EndsNotePoints}).
+
+shuffle(List) -> deck:to_list(deck:shuffle(deck:from_list(List))).
+
+
+create_decl_rec(GameType, CParams, GameId, Users) ->
+    #game_table{id              = GameId,
+                name            = proplists:get_value(table_name, CParams),
+%                gameid,
+%                trn_id,
+                game_type       = GameType,
+                rounds          = proplists:get_value(rounds, CParams),
+                sets            = proplists:get_value(sets, CParams),
+                owner           = proplists:get_value(owner, CParams),
+                timestamp       = now(),
+                users           = Users,
+                users_options   = proplists:get_value(users_options, CParams),
+                game_mode       = proplists:get_value(game_mode, CParams),
+%                game_options,
+                game_speed      = proplists:get_value(speed, CParams),
+                friends_only    = proplists:get_value(friends_only, CParams),
+%                invited_users = [],
+                private         = proplists:get_value(private, CParams),
+                feel_lucky = false,
+%                creator,
+                age_limit       = proplists:get_value(age, CParams),
+%                groups_only = [],
+                gender_limit    = proplists:get_value(gender_limit, CParams),
+%                location_limit = "",
+                paid_only       = proplists:get_value(paid_only, CParams),
+                deny_robots     = proplists:get_value(deny_robots, CParams),
+                slang           = proplists:get_value(slang, CParams),
+                deny_observers  = proplists:get_value(deny_observers, CParams),
+                gosterge_finish = proplists:get_value(gosterge_finish, CParams),
+                double_points   = proplists:get_value(double_points, CParams),
+                game_state      = started,
+                game_process    = self(),
+                game_module     = ?MODULE,
+                pointing_rules  = proplists:get_value(pointing_rules, CParams),
+                pointing_rules_ex = proplists:get_value(pointing_rules, CParams),
+%                game_process_monitor =
+%                tournament_type = 
+                robots_replacement_allowed = proplists:get_value(robots_replacement_allowed, CParams)
+               }.
+
+user_id_to_string(UserId) -> binary_to_list(UserId).
+
+%% start_timer(Timeout) -> {TRef, Magic}
+start_timer(Timeout) ->
+    Magic = make_ref(),
+    TRef = erlang:send_after(Timeout, self(), {timeout, Magic}),
+    {TRef, Magic}.
+
+%% spawn_bot(GameId, BotModule) -> UserInfo
+spawn_bot(GameId, BotModule) ->
+    {NPid, UserInfo} = create_robot(BotModule, GameId),
+    BotModule:join_game(NPid),
+    UserInfo.
+
+create_robot(BM, GameId) ->
+    UserInfo = auth_server:robot_credentials(),
+    {ok, NPid} = BM:start(self(), UserInfo, GameId),
+    {NPid, UserInfo}.
+
+spawn_table(TabMod, GameId, TableId, Params) -> TabMod:start(GameId, TableId, Params).
+
+send_to_table(TabMod, TabPid, Message) -> TabMod:parent_message(TabPid, Message).
+
+get_param(ParamId, Params) ->
+    {_, Value} = lists:keyfind(ParamId, 1, Params),
+    Value.
+
+get_option(OptionId, Params, DefValue) ->
+    proplists:get_value(OptionId, Params, DefValue).
+
+
+
+series_result_test_() ->
+    [
+     ?_assertEqual(lists:sort([{2, 1, 100, winner},
+                               {4, 1, 100, winner},
+                               {1, 3,  50, looser},
+                               {3, 4,  20, looser}]),
+                   lists:sort(series_result([{1,  50},
+                                             {2, 100},
+                                             {3,  20},
+                                             {4, 100}]))),
+     ?_assertEqual(lists:sort([{2, 1, 100, winner},
+                               {4, 2,  50, looser},
+                               {1, 2,  50, looser},
+                               {3, 2,  50, looser}]),
+                   lists:sort(series_result([{1,  50},
+                                             {2, 100},
+                                             {3,  50},
+                                             {4,  50}]))),
+     ?_assertEqual(lists:sort([{2, 1, 100, winner},
+                               {4, 2,  70, looser},
+                               {1, 3,  50, looser},
+                               {3, 3,  50, looser}]),
+                   lists:sort(series_result([{1,  50},
+                                             {2, 100},
+                                             {3,  50},
+                                             {4,  70}])))
+    ].

+ 20 - 0
apps/server/src/okey/game_okey.erl

@@ -0,0 +1,20 @@
+-module(game_okey).
+-export([get_player_stats/1]).
+-include_lib("server/include/basic_types.hrl").
+-include_lib("server/include/game_okey.hrl").
+
+get_player_stats(PlayerId) ->
+    {ok, GameStats} = game_stats:get_game_points(okey, PlayerId),
+    {ok, Skill} = game_stats:get_skill(PlayerId),
+    {ok, PlayerStats} = game_stats:get_player_stats(PlayerId),
+    #'PlayerOkeyStats'{playerId = PlayerId,
+                       level = Skill,
+                       score = proplists:get_value(game_points, GameStats),
+                       numberOkey = proplists:get_value(finished_with_okey, GameStats),
+                       number8Tashes = proplists:get_value(finished_with_8_tashes, GameStats),
+                       totalWins = proplists:get_value(total_wins, PlayerStats),
+                       totalLose = proplists:get_value(total_loses, PlayerStats),
+                       totalDisconnects = proplists:get_value(total_disconnects, PlayerStats),
+                       overalSuccessRatio = proplists:get_value(overall_success_ratio, PlayerStats),
+                       averagePlayDuration = proplists:get_value(average_play_time, PlayerStats)
+                      }.

+ 403 - 0
apps/server/src/okey/game_okey_bot.erl

@@ -0,0 +1,403 @@
+-module(game_okey_bot).
+-behaviour(gen_server).
+
+-export([start/3, start_link/3, robot_init/1]).
+-export([init_state/2, join_game/1, get_session/1]).
+-export([send_message/2]).
+-export([call_rpc/2]).
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
+
+-include_lib("server/include/conf.hrl").
+-include_lib("server/include/requests.hrl").
+-include_lib("server/include/game_okey.hrl").
+-include_lib("server/include/log.hrl").
+
+-record(state, {
+        is_robot = true :: boolean(),
+        conn :: pid(),
+        mode :: atom(),
+        hand = [] :: list(#'OkeyPiece'{}),
+        gosterge :: #'OkeyPiece'{},
+        set_state :: #'OkeySetState'{},
+        delay :: integer(),
+        user :: #'PlayerInfo'{},
+        uid :: 'PlayerId'(),
+        owner :: pid(),
+        owner_mon :: 'MonitorRef'(),
+        session :: pid(),
+        gid :: 'GameId'(),
+        bot :: pid(),
+        okey_disable = false :: boolean(),
+        running_requests = dict:new() :: any(),
+        request_id = 0
+    }).
+
+send_message(Pid, Message) -> gen_server:call(Pid, {send_message, Message}).
+call_rpc(Pid, Message) -> gen_server:call(Pid, {call_rpc, Message}).
+get_session(Pid) -> gen_server:call(Pid, get_session).
+init_state(Pid, Situation) -> gen_server:cast(Pid, {init_state, Situation}).
+join_game(Pid) -> gen_server:cast(Pid, join_game).
+start(Owner, PlayerInfo, GameId) -> gen_server:start(?MODULE, [Owner, PlayerInfo, GameId], []).
+start_link(Owner, PlayerInfo, GameId) -> gen_server:start_link(?MODULE, [Owner, PlayerInfo, GameId], []).
+
+init([Owner, PlayerInfo, GameId]) ->
+    {ok, SPid} = game_session:start_link(self()),
+    game_session:bot_session_attach(SPid, PlayerInfo),
+    UId = PlayerInfo#'PlayerInfo'.id,
+    ?INFO("BOTMODULE ~p started with game_session pid ~p", [UId,SPid]),
+    {ok, #state{user = PlayerInfo, uid = UId, owner = Owner, gid = GameId, session = SPid}}.
+
+handle_call({send_message, Msg0}, _From, #state{uid = UId, bot = BPid} = State) ->
+    Msg = flashify(Msg0),
+    ?INFO("OKEY BOT ~p: Resend message to bot process (~p): ~p",[UId, BPid, Msg0]),
+    BPid ! Msg,
+    {reply, ok, State};
+
+handle_call({call_rpc, Msg}, From, State) ->
+    RR = State#state.running_requests,
+    Id = State#state.request_id + 1,
+    Self = self(),
+    RR1 = dict:store(Id, From, RR),
+    proc_lib:spawn_link(fun() ->
+                                Res = try
+                                          Answer = game_session:process_request(State#state.session, "OKEY BOT", Msg),
+%                            		  ?INFO("Process Request from OKEY BOT:",[]),
+%                            		  ?INFO("                      REQUEST: ~p",[Msg]),
+%                            		  ?INFO("                        REPLY: ~p",[Answer]),
+                                          {reply, Id, Answer}
+                                      catch
+                                          _Err:Reason ->
+                                              {reply, Id, {error, Reason}}
+                                      end,
+                                gen_server:call(Self, Res)
+                        end),
+    {noreply, State#state{running_requests = RR1, request_id = Id, okey_disable = false}};
+
+handle_call({reply, Id, Answer}, _From, State) ->
+    RR = State#state.running_requests,
+    From = dict:fetch(Id, RR),
+    gen_server:reply(From, Answer),
+    {reply, ok, State};
+
+handle_call(get_session, _From, State) ->
+    {reply, State#state.session, State};
+
+handle_call(Request, _From, State) ->
+    Reply = ok,
+    ?INFO("unknown call: ~p", [Request]),
+    {reply, Reply, State}.
+
+handle_cast(join_game, State) ->
+    Mon = erlang:monitor(process, State#state.owner),
+    UId = State#state.uid,
+    GId = State#state.gid,
+    BPid = proc_lib:spawn_link(game_okey_bot, robot_init, [#state{gid = GId, uid = UId, conn = self()}]),
+    BPid ! join_game,
+    {noreply, State#state{bot = BPid, owner_mon = Mon}};
+
+handle_cast(Msg, State) ->
+    ?INFO("unknown cast: ~p", [Msg]),
+    {noreply, State}.
+
+handle_info({'DOWN', Ref, process, _, Reason},
+            State = #state{owner_mon = OMon}) when OMon == Ref->
+    ?INFO
+    ("relay goes down with reason ~p so does bot", [Reason]),
+    {stop, Reason, State};
+handle_info(Info, State) ->
+    {stop, {unrecognized_info, Info}, State}.
+
+terminate(_Reason, _State) ->
+    ok.
+
+code_change(_OldVsn, State, _Extra) ->
+    {ok, State}.
+
+flashify(R) when is_tuple(R) ->
+    [RecName | Rest] = tuple_to_list(R),
+    Rest1 = lists:map(fun
+                          (X) -> flashify(X)
+                      end, Rest),
+    list_to_tuple([RecName | Rest1]);
+flashify([{Key, _Value} | _] = P) when is_atom(Key) ->
+    lists:map(fun
+                  ({K, V}) when is_atom(K) -> {K, flashify(V)}
+              end, P);
+flashify(A) when A == true -> A;
+flashify(A) when A == false -> A;
+flashify(A) when A == null -> A;
+flashify(A) when A == undefined -> A;
+flashify(A) when is_atom(A) ->
+    list_to_binary(atom_to_list(A));
+flashify(Other) ->
+    Other.
+
+%%------------------------ BOT LOGIC
+
+robot_init(State) ->
+    robot_init_loop(State).
+
+robot_init_loop(State) ->
+    S = State#state.conn,
+    Id = State#state.uid,
+    GameId = State#state.gid,
+    receive
+        join_game ->
+            ?INFO("OKEY BOT JOINED"),
+            case call_rpc(S, #join_game{game = GameId}) of
+                {error, _Err} ->
+                    ?INFO("ID: ~p failed take with msg ~p", [Id, _Err]),
+                    erlang:error(robot_cant_join_game);
+                _ ->
+                    okey_client_loop(State)
+            end;
+        _X ->
+           ?INFO("OKEY BOT X: ~p",[_X])
+    end.
+
+okey_client_loop(State) ->
+    ?INFO("OKEY BOT CLIENT LOOP"),
+    Hand0 = State#state.hand,
+    Id = State#state.uid,
+    receive
+        #game_event{event = <<"okey_next_turn">>, args = Args} = Msg ->
+            ?INFO("OKEY_BOT <~p> : Received message: ~p", [Id, Msg]),
+            Hand1 = case {proplists:get_value(player, Args), proplists:get_value(can_challenge, Args)} of
+                        {Id, false} ->
+                            ?INFO("OKEY NEXT TURN"),
+                            do_turn(State, Hand0);
+                        {_OtherId, _Val} ->
+                            Hand0
+                    end,
+            okey_client_loop(State#state{hand = Hand1});
+        #game_event{event = <<"okey_revealed">>} = Msg ->
+            ?INFO("OKEY_BOT <~p> : Received message: ~p", [Id, Msg]),
+            do_challenge(State),
+            okey_client_loop(State);
+        #game_event{event = <<"okey_series_ended">>} = Msg ->
+            ?INFO("OKEY_BOT <~p> : Received message: ~p", [Id, Msg]),
+%%            S = State#state.conn,
+%%            call_rpc(S, #logout{});
+            okey_client_loop(State);
+        #game_event{event = <<"okey_round_ended">>, args = Args} = Msg ->
+            ?INFO("OKEY_BOT <~p> : Received message: ~p", [Id, Msg]),
+            NextAction = proplists:get_value(next_action, Args),
+            ?INFO("ID: ~p round ended", [Id]),
+            okey_client_round(NextAction, State);
+        #game_event{event = <<"okey_game_info">>, args = Args} = Msg ->
+            ?INFO("OKEY_BOT <~p> : Received message: ~p", [Id, Msg]),
+            Mode = proplists:get_value(game_type, Args),
+            SM = proplists:get_value(sets, Args),
+            SC = proplists:get_value(set_no, Args),
+            RM = proplists:get_value(rounds, Args),
+            TO = proplists:get_value(timeouts, Args),
+%%            Speed = TO#'OkeyTimeouts'.speed,
+%%            SpeedAtom = list_to_atom(binary_to_list(Speed)),
+            Delay = get_delay(fast),
+            ST = #'OkeySetState'{round_cur = 1, round_max = RM, set_cur = SC, set_max = SM},
+            okey_client_loop(State#state{set_state = ST, delay = Delay, mode = Mode});
+        #game_event{event = <<"okey_disable_okey">>, args = Args} = Msg ->
+            ?INFO("OKEY_BOT <~p> : Received message: ~p", [Id, Msg]),
+            okey_client_loop(State#state{okey_disable = true});
+        #game_event{event = <<"okey_game_started">>, args = Args} = Msg ->
+            ?INFO("OKEY_BOT <~p> : Received message: ~p", [Id, Msg]),
+            MH = proplists:get_value(tiles, Args),
+            G = proplists:get_value(gosterge, Args),
+            RC = proplists:get_value(current_round, Args),
+            ST = State#state.set_state,
+            ST1 = ST#'OkeySetState'{round_cur = RC},
+%            State#state{hand = MH, gosterge = G, set_state = ST1},
+            ?INFO("OKEY BOT GAME STARTED : ~p",[length(MH)]),
+            okey_client_loop(State#state{hand = MH, gosterge = G, set_state = ST1});
+        #game_event{event = <<"okey_game_player_state">>, args = Args} = Msg ->
+            ?INFO("OKEY_BOT <~p> : Received message: ~p", [Id, Msg]),
+            SS = #'OkeySetState'{round_cur = 1, round_max = 3,
+                            set_cur = 1,set_max = 1},
+
+            Pause = proplists:get_value(paused, Args, false),
+            Turn = proplists:get_value(whos_move, Args),
+            GameState = proplists:get_value(game_state, Args),
+            Gosterge = State#state.gosterge,
+            ?INFO("Id ~p Turn: ~p GameState: ~p",[Id, Turn,GameState]),
+
+            case Pause of
+                 true -> wait_for_resume();
+                 false -> ok
+            end,
+
+
+            case {Turn, GameState} of
+                {Id, <<"do_okey_take">>} ->
+                    ?INFO("init bot: take", []),
+                    Hand1 = do_turn(State, Hand0),
+                    okey_client_loop(State#state{hand = Hand1, gosterge = Gosterge});
+                {Id, <<"do_okey_discard">>} ->
+                    ?INFO("init bot: discard", []),
+                    {TryDiscard, _} = draw_random(Hand0),
+                    Hand1 = do_discard(State, Hand0, TryDiscard),
+                    okey_client_loop(State#state{hand = Hand1, gosterge = Gosterge});
+                {_, <<"game_finished">>} ->
+                    ?INFO("init bot: finished", []),
+                     okey_client_rematch(State),
+                    okey_client_loop(State);
+                {_, <<"do_okey_ready">>} ->
+                    ?INFO("init bot: ready", []),
+                    say_ready(State),
+                    okey_client_round(<<"done">>, State);
+                {_, <<"do_okey_challenge">>} ->
+                    ?INFO("init bot: challenge", []),
+                    do_challenge(State),
+                    okey_client_loop(State#state{hand = Hand0, gosterge = Gosterge});
+                {_, _B} ->
+                    ?INFO("init bot: UNKNOWN ~p", [_B]),
+                    okey_client_loop(State#state{hand = Hand0, gosterge = Gosterge})
+            end;
+
+        _Other = Msg ->
+            ?INFO("OKEY_BOT <~p> : Received unhandled message: ~p", [Id, Msg]),
+            okey_client_loop(State)
+    end.
+
+do_challenge(State) ->
+    GameId = State#state.gid,
+    S = State#state.conn,
+    ZZZ = call_rpc(S, #game_action{
+                         game = GameId,
+                         action = okey_challenge,
+                         args = [ {challenge, random_bool(0.2)} ]}),
+    ?INFO("ID: ~p challenge result: ~p", [State#state.uid, ZZZ]),
+    ok.
+
+okey_client_round(<<"done">>, State = #state{}) ->
+    %% series of sets ended, do rematch, if needed.
+    okey_client_loop(State);
+%%    S = State#state.conn,
+%%    call_rpc(S, #logout{});
+    %okey_client_rematch(State),
+    %init_set(State);
+okey_client_round(<<"next_set">>, State = #state{}) ->
+    %% set ended, wait for new set info
+    okey_client_loop(State);
+okey_client_round(<<"next_round">>, State) ->
+    %% round ended, wait for new round info
+%    State2 = get_hand(State),
+    okey_client_loop(State).
+
+okey_client_rematch(State) ->
+    S = State#state.conn,
+    GameId = State#state.gid,
+    ?INFO("sending rematch", []),
+    A = call_rpc(S, #rematch{game = GameId}),
+    ?INFO("rematch result: ~p", [A]),
+    ok = A,
+    okey_client_rematch2(State).
+
+okey_client_rematch2(State) ->
+    S = State#state.conn,
+    GameId = State#state.gid,
+    ?INFO("rematch loop receive", []),
+    receive
+        #game_rematched{game = GI} when GameId == GI ->
+            ?INFO("#game_rematched{game = GameId}", []);
+        #game_event{event = <<"player_left">>, args = Args} ->
+            ?INFO("#game_event{event = <<\"player_left\">>, args = Args}", []),
+            Replaced = proplists:get_value(bot_replaced, Args, false) orelse
+                proplists:get_value(human_replaced, Args, false),
+            case Replaced of
+                false ->
+                    call_rpc(S, #logout{});
+                true ->
+                    okey_client_rematch2(State)
+            end
+    end.
+
+say_ready(State) ->
+    S = State#state.conn,
+    GameId = State#state.gid,
+    ok = call_rpc(S, #game_action{game = GameId, action = okey_ready, args = []}).
+
+do_turn(#state{delay = Delay} = State, Hand) ->
+    Hand1 = if length(Hand) == 15 ->
+                   Hand;
+               true ->
+                   ?INFO("OKEY BOT TAKE ? ~p ",[length(Hand)]),
+                   simulate_delay(take, Delay),
+                   {_, H1} = do_take(State, Hand),
+                   H1
+            end,
+    true = is_list(Hand1),
+    {TryDiscard, _} = draw_random(Hand1),
+    ?INFO("DO TURN"),
+    simulate_delay(discard, Delay),
+    do_discard(State, Hand1, TryDiscard).
+
+time_to_sleep(take, Delay) ->
+    erlang:trunc((Delay / 3) * 1);
+time_to_sleep(discard, Delay) ->
+    erlang:trunc((Delay / 3) * 2).
+
+simulate_delay(Action, Delay) ->
+    TheDelay = time_to_sleep(Action, Delay),
+    receive
+        #game_paused{action = <<"pause">>} ->
+            wait_for_resume()
+    after TheDelay ->
+            ok
+    end.
+
+wait_for_resume() ->
+    receive
+        #game_paused{action = <<"resume">>} ->
+            ok
+    end.
+
+do_take(State, Hand) ->
+    S = State#state.conn,
+    GameId = State#state.gid,
+    Id = State#state.uid,
+    Pile = case State#state.okey_disable of
+                true -> 0;
+                false -> crypto:rand_uniform(0, 2)
+           end,
+    case call_rpc(S, #game_action{
+                        game = GameId,
+                        action = okey_take,
+                        args = [ {pile, Pile} ]}) of
+        #'OkeyPiece'{} = Tosh ->
+            MyHand = [Tosh | Hand],
+            {false, MyHand};
+        {error, cant_take_do_discard} ->
+            {false, Hand};
+        {error, game_has_already_ended} when State#state.mode == <<"countdown">> ->
+            {false, Hand};
+        _Err ->
+            ?INFO("ID: ~p failed take with msg ~p", [Id, _Err]),
+            erlang:error(failed_take),
+            {false, Hand}
+    end.
+
+do_discard(State, Hand, Item) ->
+    S = State#state.conn,
+    GameId = State#state.gid,
+    Hand1 = lists:delete(Item, Hand),
+    _Res = call_rpc(S, #game_action{game = GameId, action = okey_discard,
+                                        args = [ {tile, Item} ]}),
+    Hand1.
+
+draw_random([One]) ->
+    {One, []};
+draw_random(List) ->
+    {is_list, true} = {is_list, is_list(List)},
+    Pos = crypto:rand_uniform(1, length(List)),
+    Item = lists:nth(Pos, List),
+    ResList = lists:delete(Item, List),
+    {Item, ResList}.
+
+random_bool(Prob) ->
+    Point = crypto:rand_uniform(0, 1000),
+    Prob*1000 > Point.
+
+get_delay(fast) -> {ok, Val}   = nsm_db:get(config,"games/okey/robot_delay_fast", 6000), Val;
+get_delay(normal) -> {ok, Val} = nsm_db:get(config,"games/okey/robot_delay_normal", 9000), Val;
+get_delay(slow) -> {ok, Val}   = nsm_db:get(config,"games/okey/robot_delay_slow", 15000), Val.
+

+ 530 - 0
apps/server/src/okey/game_okey_ng_desk.erl

@@ -0,0 +1,530 @@
+%%% -------------------------------------------------------------------
+%%% Author  : Sergei Polkovnikov <serge.polkovnikov@gmail.com>
+%%% Description : The desk-level logic for okey game.
+%%%
+%%% Created : Oct 8, 2012
+%%% -------------------------------------------------------------------
+
+%% Parameters:
+%%  hands - contains tashes of players. A position of an element in the list
+%%        is a seat number of a player. Each element contain a list of tashes.
+%%        One of the element must contain 15 tashes and all other elements must
+%%        contain 14 tashes per each.
+%%        Type: [Hand1, Hand2, Hand3, Hand4],
+%%              Hand1 = Hand2 = Hand3 = Hand4 = [tash()]
+%%  deck - contains tashes which will be placed on the table.
+%%        Type: [tash()]
+%%  gosterge - a tash that indicate a jocker. It must not be "fasle okey" tash.
+%%        Type: tash()
+%%  cur_player - a seat number of a player who will make first move. The hand
+%%        of the player must contain 15 tashes.
+%%        Type: 1 | 2 | 3 | 4
+%%  gosterge_finish_list - the list of players who allowed to do gosterge finish.
+%%        Type: [SeatNum]
+%%              SeatNum = 1 | 2 | 3 | 4
+%%  have_8_tashes_enabled - if true then users are allowed to show 8 tashes combination
+%%        Type: boolean()
+
+%% tash() = {Color, Value} | false_okey
+%%    Color = 1 - 4
+%%    Value = 1 - 13
+
+%%        Players actions:        ||      Errors:
+%%  i_have_gosterge               || action_disabled, no_gosterge
+%%  i_have_8_tashes               || action_disabled, no_8_tashes
+%%  see_okey                      || no_okey_discarded
+%%  take_from_discarded           || not_your_order, blocked, no_tash
+%%  take_from_table               || not_your_order, no_tash
+%%  {discard, Tash}               || not_your_order, no_such_tash
+%%  {reveal, Tash, TashPlaces}    || not_your_order, no_such_tash, hand_not_match
+
+%% Outgoing events:
+%%  {has_gosterge, SeatNum}
+%%  {has_8_tashes, SeatNum, Value}
+%%  {saw_okey, SeatNum}
+%%  {taked_from_discarded, SeatNum, Tash}
+%%  {taked_from_table, SeatNum, Tash}
+%%  {tash_discarded, SeatNum, Tash}
+%%  {next_player, SeatNum}
+%%  no_winner_finish
+%%  {reveal, SeatNum, TashPlaces, DicardedTash}
+%%  {gosterge_finish, SeatNum}
+
+-module(game_okey_ng_desk).
+
+-behaviour(gen_fsm).
+%% --------------------------------------------------------------------
+%% Include files
+%% --------------------------------------------------------------------
+-include_lib("server/include/log.hrl").
+
+%% --------------------------------------------------------------------
+%% External exports
+-export([
+         start/1,
+         stop/1,
+         player_action/3
+        ]).
+
+-export([
+         get_state_name/1,
+         get_seats_nums/1,
+         get_cur_seat/1,
+         get_gosterge/1,
+         get_deck/1,
+         get_hand/2,
+         get_discarded/2,
+         get_have_8_tashes/1,
+         get_has_gosterge/1
+        ]).
+
+%% gen_fsm callbacks
+-export([init/1, handle_event/3,
+	 handle_sync_event/4, handle_info/3, terminate/3, code_change/4]).
+
+-type tash() :: false_okey | {integer(), integer()}.
+
+-record(player,
+        {
+         id                :: integer(),
+         hand              :: deck:deck(),
+         can_show_gosterge :: boolean(),
+         can_show_8_tashes :: boolean(),
+         finished_by_okey  :: boolean(),
+         discarded         :: deck:deck()
+        }).
+
+-record(state,
+        {
+         gosterge_finish_list :: list(integer()), %% Seats nums of players which allowed to do gosterge finish
+         have_8_tashes_enabled :: boolean(),
+         players           :: list(#player{}),
+         cur_player        :: integer(),
+         deck              :: deck:deck(),
+         gosterge          :: tash(),
+         okey              :: tash(),
+         okey_blocked      :: boolean(),
+         has_gosterge      :: undefined | integer(), %% Seat num of a player who has gosterge
+         have_8_tashes_list :: list(integer())    %% Seats nums of players who collected 8 tashes combination
+        }).
+
+
+-define(STATE_TAKE, state_take).
+-define(STATE_DISCARD, state_discard).
+-define(STATE_FINISHED, state_finished).
+
+%% ====================================================================
+%% External functions
+%% ====================================================================
+start(Params) -> gen_fsm:start(?MODULE, Params, []).
+
+player_action(Desk, SeatNum, Action) ->
+    gen_fsm:sync_send_all_state_event(Desk, {player_action, SeatNum, Action}).
+
+stop(Desk) -> gen_fsm:send_all_state_event(Desk, stop).
+
+
+get_state_name(Desk) ->  gen_fsm:sync_send_all_state_event(Desk, get_state_name).
+
+get_cur_seat(Desk) -> gen_fsm:sync_send_all_state_event(Desk, get_cur_seat).
+
+get_seats_nums(Desk) -> gen_fsm:sync_send_all_state_event(Desk, get_seats_nums).
+
+get_gosterge(Desk) -> gen_fsm:sync_send_all_state_event(Desk, get_gosterge).
+
+get_deck(Desk) -> gen_fsm:sync_send_all_state_event(Desk, get_deck).
+
+get_hand(Desk, SeatNum) -> gen_fsm:sync_send_all_state_event(Desk, {get_hand, SeatNum}).
+
+get_discarded(Desk, SeatNum) -> gen_fsm:sync_send_all_state_event(Desk, {get_discarded, SeatNum}).
+
+get_have_8_tashes(Desk) -> gen_fsm:sync_send_all_state_event(Desk, get_have_8_tashes).
+
+get_has_gosterge(Desk) -> gen_fsm:sync_send_all_state_event(Desk, get_has_gosterge).
+
+%% ====================================================================
+%% Server functions
+%% ====================================================================
+%% --------------------------------------------------------------------
+init(Params) ->
+    Hands = get_param(hands, Params),
+    Deck = get_param(deck, Params),
+    Gosterge = get_param(gosterge, Params),
+    CurPlayer = get_param(cur_player, Params),
+    GostFinishList = get_param(gosterge_finish_list, Params),
+    Have8TashesEnabled = get_param(have_8_tashes_enabled, Params),
+    validate_params(Hands, Deck, Gosterge, CurPlayer, GostFinishList, Have8TashesEnabled),
+    Players = init_players(Hands),
+    Okey = gosterge_to_okey(Gosterge),
+    {ok, ?STATE_DISCARD, #state{players = Players,
+                                gosterge_finish_list = GostFinishList,
+                                have_8_tashes_enabled = Have8TashesEnabled,
+                                deck = deck:from_list(Deck),
+                                gosterge = Gosterge,
+                                okey = Okey,
+                                cur_player = CurPlayer,
+                                okey_blocked = false,
+                                has_gosterge = undefined,
+                                have_8_tashes_list = []}}.
+
+%% --------------------------------------------------------------------
+handle_event(stop, _StateName, StateData) ->
+    {stop, normal, StateData};
+
+handle_event(_Event, StateName, StateData) ->
+    {next_state, StateName, StateData}.
+
+%% --------------------------------------------------------------------
+handle_sync_event({player_action, SeatNum, Action}, _From, StateName, StateData) ->
+    case handle_player_action(SeatNum, Action, StateName, StateData) of
+        {ok, Events, NewStateName, NewStateData} ->
+            {reply, {ok, lists:reverse(Events)}, NewStateName, NewStateData};
+        {error, Reason} ->
+            {reply, {error, Reason}, StateName, StateData}
+    end;
+
+handle_sync_event(get_state_name, _From, StateName,
+                  StateData) ->
+    {reply, StateName, StateName, StateData};
+
+handle_sync_event(get_cur_seat, _From, StateName,
+                  #state{cur_player = CurSeat} = StateData) ->
+    {reply, CurSeat, StateName, StateData};
+
+handle_sync_event(get_seats_nums, _From, StateName,
+                  #state{players = Players} = StateData) ->
+    SeatsNums = [P#player.id || P <- Players],
+    {reply, SeatsNums, StateName, StateData};
+
+handle_sync_event(get_gosterge, _From, StateName,
+                  #state{gosterge = Gosterge} = StateData) ->
+    {reply, Gosterge, StateName, StateData};
+
+handle_sync_event(get_deck, _From, StateName,
+                  #state{deck = Deck} = StateData) ->
+    {reply, deck:to_list(Deck), StateName, StateData};
+
+handle_sync_event({get_hand, SeatNum}, _From, StateName,
+                  #state{players = Players} = StateData) ->
+    #player{hand = Hand} = get_player(SeatNum, Players),
+    {reply, deck:to_list(Hand), StateName, StateData};
+
+handle_sync_event({get_discarded, SeatNum}, _From, StateName,
+                  #state{players = Players} = StateData) ->
+    #player{discarded = Discarded} = get_player(SeatNum, Players),
+    {reply, deck:to_list(Discarded), StateName, StateData};
+
+handle_sync_event(get_have_8_tashes, _From, StateName,
+                  #state{have_8_tashes_list = Have8TashesList} = StateData) ->
+    {reply, Have8TashesList, StateName, StateData};
+
+handle_sync_event(get_has_gosterge, _From, StateName,
+                  #state{has_gosterge = HasGosterge} = StateData) ->
+    {reply, HasGosterge, StateName, StateData};
+
+
+handle_sync_event(_Event, _From, StateName, StateData) ->
+    {reply, {error, unknown_request}, StateName, StateData}.
+%% --------------------------------------------------------------------
+handle_info(_Info, StateName, StateData) ->
+    {next_state, StateName, StateData}.
+%% --------------------------------------------------------------------
+terminate(_Reason, _StateName, _StatData) ->
+    ok.
+%% --------------------------------------------------------------------
+code_change(_OldVsn, StateName, StateData, _Extra) ->
+    {ok, StateName, StateData}.
+
+%% --------------------------------------------------------------------
+%%% Internal functions
+%% --------------------------------------------------------------------
+
+%% @spec handle_player_action(SeatNum, Action, StateName, StateData) ->
+%%          {ok, Events, NextStateName, NextStateData}          |
+%%          {error, Reason}
+%% @end
+
+handle_player_action(PlayerId, i_have_gosterge, StateName,
+                     #state{players = Players, gosterge = Gosterge,
+                            gosterge_finish_list = GostFinishList} = StateData) when
+  StateName == ?STATE_TAKE;
+  StateName == ?STATE_DISCARD ->
+    case get_player(PlayerId, Players) of
+        #player{can_show_gosterge = true,
+                hand = Hand} = Player ->
+            case deck:member(Gosterge, Hand) of
+                true ->
+                    case lists:member(PlayerId, GostFinishList) of
+                        false ->
+                            NewPlayer = Player#player{can_show_gosterge = false},
+                            NewPlayers = update_player(NewPlayer, Players),
+                            Events = [{has_gosterge, PlayerId}],
+                            {ok, Events, StateName, StateData#state{players = NewPlayers,
+                                                                    has_gosterge = PlayerId}};
+                        true ->
+                           Events = [{gosterge_finish, PlayerId}],
+                           {ok, Events, ?STATE_FINISHED,
+                            StateData#state{has_gosterge = PlayerId}}
+                    end;
+                false ->
+                    {error, no_gosterge}
+            end;
+        #player{can_show_gosterge = false} ->
+            {error, action_disabled}
+    end;
+
+handle_player_action(PlayerId, i_have_8_tashes, StateName,
+                     #state{players = Players, have_8_tashes_enabled = Enabled,
+                            have_8_tashes_list = Have8TashesList
+                           } = StateData) when
+  StateName == ?STATE_TAKE;
+  StateName == ?STATE_DISCARD ->
+    case get_player(PlayerId, Players) of
+        #player{can_show_8_tashes = true,
+                hand = Hand} = Player when Enabled ->
+            case find_8_tashes(Hand) of
+                {ok, Value} ->
+                    NewPlayer = Player#player{can_show_8_tashes = false},
+                    NewPlayers = update_player(NewPlayer, Players),
+                    Events = [{has_8_tashes, PlayerId, Value}],
+                    {ok, Events, StateName,
+                     StateData#state{players = NewPlayers,
+                                     have_8_tashes_list = [PlayerId | Have8TashesList]}};
+                not_found ->
+                    {error, no_8_tashes}
+            end;
+        #player{} ->
+            {error, action_disabled}
+    end;
+
+
+handle_player_action(PlayerId, see_okey, ?STATE_TAKE,
+                     #state{cur_player = CurPlayerId,
+                            players = Players,
+                            okey = Okey} = StateData) ->
+    #player{discarded = Discarded} = get_player(prev_id(CurPlayerId), Players),
+    case deck:get(1, Discarded) of
+        {Okey, _} ->
+            Events = [{saw_okey, PlayerId}],
+            {ok, Events, ?STATE_TAKE, StateData#state{okey_blocked = true}};
+        _ ->
+            {error, no_okey_discarded}
+    end;
+
+
+handle_player_action(PlayerId, take_from_discarded, ?STATE_TAKE,
+                     #state{cur_player = CurPlayerId,
+                            players = Players,
+                            okey_blocked = Blocked} = StateData) ->
+    if PlayerId == CurPlayerId ->
+           if not Blocked ->
+                  case take_tash_from_discarded(PlayerId, Players) of
+                      {Tash, NewPlayers} ->
+                          Events = [{taked_from_discarded, PlayerId, Tash}],
+                          {ok, Events, ?STATE_DISCARD,
+                           StateData#state{players = NewPlayers, okey_blocked = false}};
+                      error ->
+                          {error, no_tash}
+                  end;
+              true ->
+                  {error, blocked}
+           end;
+       true ->
+           {error, not_your_order}
+    end;
+
+
+handle_player_action(PlayerId, take_from_table, ?STATE_TAKE,
+                     #state{cur_player = CurPlayerId,
+                            players = Players,
+                            deck = Deck} = StateData) ->
+    if PlayerId == CurPlayerId ->
+           case take_tash_from_table(PlayerId, Players, Deck) of
+               {Tash, NewPlayers, NewDeck} ->
+                   Events = [{taked_from_table, PlayerId, Tash}],
+                   {ok, Events, ?STATE_DISCARD,
+                    StateData#state{players = NewPlayers, deck = NewDeck, okey_blocked = false}};
+               error ->
+                   {error, no_tash}
+           end;
+       true ->
+           {error, not_your_order}
+    end;
+
+
+handle_player_action(PlayerId, {discard, Tash}, ?STATE_DISCARD,
+                     #state{cur_player = CurPlayerId,
+                            players = Players,
+                            deck = Deck} = StateData) ->
+    if PlayerId == CurPlayerId ->
+           case discard_tash(Tash, PlayerId, Players) of
+               error ->
+                   ?INFO("OKEY_NG_DESK Discard error. SeatNum: ~p. Tash: ~p", [PlayerId, Tash]),
+                   {error, no_such_tash};
+               NewPlayers ->
+                   Events1 = [{tash_discarded, PlayerId, Tash}],
+                   case deck:size(Deck) of
+                       0 ->
+                           Events = [no_winner_finish | Events1],
+                           {ok, Events, ?STATE_FINISHED,
+                            StateData#state{players = NewPlayers}};
+                       _ ->
+                           NextPlayerId = next_id(CurPlayerId),
+                           Events = [{next_player, NextPlayerId} | Events1],
+                           {ok, Events, ?STATE_TAKE,
+                            StateData#state{players = NewPlayers, cur_player = NextPlayerId}}
+                   end
+            end;
+       true ->
+           {error, not_your_order}
+    end;
+
+handle_player_action(PlayerId, {reveal, Tash, TashPlaces}, ?STATE_DISCARD,
+                     #state{cur_player = CurPlayerId,
+                            players = Players
+                           } = StateData) ->
+    if PlayerId == CurPlayerId ->
+           case discard_tash(Tash, PlayerId, Players) of
+               error ->
+                   {error, no_such_tash};
+               NewPlayers ->
+                   RevealHand = tash_places_to_hand(TashPlaces),
+                   #player{hand = PlayerHand} = get_player(PlayerId, NewPlayers),
+                   case is_same_hands(RevealHand, PlayerHand) of
+                       true ->
+                           Events = [{reveal, PlayerId, TashPlaces, Tash}],
+                           {ok, Events, ?STATE_FINISHED,
+                            StateData#state{players = NewPlayers}};
+                       false ->
+                           {error, hand_not_match}
+                   end
+            end;
+       true ->
+           {error, not_your_order}
+    end;
+
+handle_player_action(_PlayerId, _Action, _StateName, _StateData) ->
+    ?ERROR("OKEY_NG_DESK Invalid action passed. Player: ~p. Action: ~p. StateName: ~p.",
+           [_PlayerId, _Action, _StateName]),
+    {error, invalid_action}.
+
+%%===================================================================
+
+%% get_param(Id, Params) -> Value
+get_param(Id, Params) ->
+    {_, Value} =lists:keyfind(Id, 1, Params),
+    Value.
+
+%% TODO: Implement the validator
+
+validate_params(_Hands, _Deck, _Gosterge, _CurPlayer, _GostFinishList, _Have8TashedEnabled) ->
+    ok.
+
+init_players(Hands) ->
+    F = fun(Hand, Id) ->
+                {#player{id = Id,      %% Seat num
+                         hand = Hand,
+                         discarded = deck:init_deck(empty),
+                         finished_by_okey = false,
+                         can_show_gosterge = true,
+                         can_show_8_tashes = true},
+                 Id+1}
+        end,
+    {Players, _} = lists:mapfoldl(F, 1, Hands),
+    Players.
+
+
+%% gosterge_to_okey(GostergyTash) -> OkeyTash
+gosterge_to_okey({Color, Value}) ->
+    if Value == 13 -> {Color, 1};
+       true -> {Color, Value + 1}
+    end.
+
+
+%% next_id(Id) -> NextId
+next_id(4) -> 1;
+next_id(Id) -> Id + 1.
+
+%% prev_id(Id) -> PrevId
+prev_id(1) -> 4;
+prev_id(Id) -> Id - 1.
+
+%% take_tash_from_discarded(PlayerId, Players) -> {Tash, NewPlayers} | error
+take_tash_from_discarded(PlayerId, Players) ->
+    #player{discarded = Discarded} = PrevPlayer = get_player(prev_id(PlayerId), Players),
+    case deck:size(Discarded) of
+        0 ->
+            error;
+        _ ->
+            {Taken, RestDiscarded} = deck:pop(1, Discarded),
+            NewPrevPlayer = PrevPlayer#player{discarded = RestDiscarded},
+            #player{hand = Hand} = Player = get_player(PlayerId, Players),
+            NewPlayer = Player#player{hand = deck:push(Taken, Hand),
+                                      can_show_gosterge = false},
+            NewPlayers1 = update_player(NewPrevPlayer, Players),
+            NewPlayers2 = update_player(NewPlayer, NewPlayers1),
+            [Tash] = deck:to_list(Taken),
+            {Tash, NewPlayers2}
+    end.
+
+%% take_tash_from_table(PlayerId, Players, Deck) -> {Tash, NewPlayers, NewDeck} | error
+take_tash_from_table(PlayerId, Players, Deck) ->
+    case deck:size(Deck) of
+        0 ->
+            error;
+        _ ->
+            {Taken, NewDeck} = deck:pop(1, Deck),
+            #player{hand = Hand} = Player = get_player(PlayerId, Players),
+            NewPlayer = Player#player{hand = deck:push(Taken, Hand),
+                                      can_show_gosterge = false},
+            NewPlayers = update_player(NewPlayer, Players),
+            [Tash] = deck:to_list(Taken),
+            {Tash, NewPlayers, NewDeck}
+    end.
+
+%% discard_tash(Tash, PlayerId, Players) -> NewPlayers
+discard_tash(Tash, PlayerId, Players) ->
+    #player{hand = Hand, discarded = Discarded} = Player = get_player(PlayerId, Players),
+    case deck:del_first(Tash, Hand) of
+        {ok, NewHand} ->
+            NewDiscarded = deck:put(Tash, 1, Discarded),
+            NewPlayer = Player#player{hand = NewHand, discarded = NewDiscarded},
+            update_player(NewPlayer, Players);
+        error ->
+            error
+    end.
+
+
+%% tash_places_to_hand(TashPlaces) -> Hand
+tash_places_to_hand(TashPlaces) ->
+    Elements = [P || P <- lists:flatten(TashPlaces), P =/= null],
+    deck:from_list(Elements).
+
+%% get_player(PlayerId, Players) -> Player
+get_player(PlayerId, Players) ->
+    #player{} = lists:keyfind(PlayerId, #player.id, Players).
+
+
+%% update_player(Player, Players) -> NewPlayers
+update_player(#player{id = Id} = Player, Players) ->
+    lists:keyreplace(Id, #player.id, Players, Player).
+
+
+%% is_same_hands(Hand1, Hand2) -> boolean()
+is_same_hands(Hand1, Hand2) ->
+    L1 = lists:sort(deck:to_list(Hand1)),
+    L2 = lists:sort(deck:to_list(Hand2)),
+    L1 == L2.
+
+%% find_8_tashes(Hand) -> {ok, Value} | not_found
+find_8_tashes(Hand) -> find_8_tashes2(deck:to_list(Hand)).
+
+find_8_tashes2(Hand) when length(Hand) < 8 -> not_found;
+find_8_tashes2([{_, Value} | Rest]) ->
+    case count_tashes_with_value(Value, Rest) of
+        7 -> {ok, Value};
+        _ -> find_8_tashes2(Rest)
+    end.
+
+count_tashes_with_value(Value, Tashes) ->
+    length([1 || {_, TVal} <- Tashes, TVal == Value]).

+ 584 - 0
apps/server/src/okey/game_okey_ng_scoring.erl

@@ -0,0 +1,584 @@
+%% Author: serge
+%% Created: Oct 23, 2012
+%% Description:
+-module(game_okey_ng_scoring).
+
+
+%%
+%% Include files
+%%
+-include_lib("eunit/include/eunit.hrl").
+-include_lib("server/include/log.hrl").
+
+%%
+%% Exported Functions
+%%
+-export([
+         init/3,
+         last_round_result/1,
+         chanak/1,
+         round_finished/6
+        ]).
+
+-type normal_tash() :: {integer(), integer()}.
+-type tash() :: false_okey | normal_tash().
+
+-define(MODE_STANDARD, standard).
+-define(MODE_EVENODD, evenodd).
+-define(MODE_COLOR, color).
+-define(MODE_COUNTDOWN, countdown).
+
+-define(COUNTDOWN_MAX_POINTS, 10).
+
+-define(ACH_CHANAK_WINNER, 0). %% Not defined in the points matrix
+
+-define(ACH_GOSTERGE_SHOWN, 1).
+-define(ACH_WIN_REVEAL, 2).
+-define(ACH_WIN_REVEAL_WITH_OKEY, 3).
+-define(ACH_WIN_REVEAL_WITH_PAIRS, 4).
+-define(ACH_WIN_REVEAL_WITH_OKEY_PAIRS, 5).
+-define(ACH_8_TASHES, 6).
+-define(ACH_WIN_REVEAL_WITH_COLOR, 7).
+-define(ACH_WIN_REVEAL_WITH_COLOR_OKEY, 8).
+-define(ACH_WIN_REVEAL_WITH_COLOR_PAIRS, 9).
+-define(ACH_WIN_REVEAL_WITH_COLOR_OKEY_PAIRS, 10).
+-define(ACH_FAIL_REVEAL, 11).
+-define(ACH_CAUGHT_BLUFF, 12).
+-define(ACH_EMPTY_BOX, 13).
+-define(ACH_REJECT_GOOD_HAND, 14).
+-define(ACH_GOSTERGE_WINNER, 15).
+
+
+-record(state,
+        {mode             :: standard | evenodd | color | countdown,
+         seats_num        :: integer(),
+         rounds_num       :: undefined | pos_integer(),
+         last_round_num   :: integer(),
+         round_score      :: list(),    %% [{SeatNum, DeltaPoints}]
+         round_achs       :: list(),    %% [{SeatNum, [{AchId, Points}]}]
+         total_score      :: list(),    %% [{SeatNum, Points}]
+         finish_info      :: term(),    %% FinishInfo
+         chanak           :: integer()  %% Defined only for evenodd and color mode
+        }).
+
+%%
+%% API Functions
+%%
+
+%% @spec init(Mode, SeatsInfo, RoundsNum) -> ScoringState
+%% @doc Initialises scoring state.
+%% @end
+%% Types:
+%%     Mode = standard | evenodd | color | countdown
+%%     SeatsInfo = [{SeatNum, Points}]
+%%       SeatNum = integer()
+%%       Points = integer()
+%%     RoundsNum = undefined | pos_integer()
+
+init(Mode, SeatsInfo, RoundsNum) ->
+    ?INFO("OKEY_NG_SCORING init Mode: ~p SeatsInfo = ~p RoundsNum = ~p", [Mode, SeatsInfo, RoundsNum]),
+    true = lists:member(Mode, [?MODE_STANDARD, ?MODE_EVENODD, ?MODE_COLOR, ?MODE_COUNTDOWN]),
+    true = if Mode == ?MODE_COUNTDOWN -> RoundsNum == undefined;
+              true -> is_integer(RoundsNum) orelse RoundsNum == undefined end,
+    SeatsNum = length(SeatsInfo),
+    true = lists:seq(1, SeatsNum) == lists:sort([SeatNum || {SeatNum, _} <- SeatsInfo]),
+    #state{mode = Mode,
+           seats_num = SeatsNum,
+           rounds_num = RoundsNum,
+           last_round_num = 0,
+           chanak = 0,
+           round_score = undefined,
+           finish_info = undefined,
+           round_achs = undefined,
+           total_score = SeatsInfo
+          }.
+
+
+%% @spec last_round_result(ScoringState) -> {FinishInfo, RoundScore, AchsPoints, TotalScore} |
+%%                                          no_rounds_played
+%% @end
+%% Types:
+%%     FinishInfo =  tashes_out | timeout | set_timeout |
+%%                   {win_reveal, Revealer, WrongRejects, RevealWithColor, RevealWithOkey, RevealWithPairs} |
+%%                   {fail_reveal, Revealer} |
+%%                   {gosterge_finish, Winner}
+%%     RoundScore = [{SeatNum, DeltaPoints}]
+%%     AchsPoints = [{SeatNum, [{AchId, Points}]}]
+%%     TotalScore = [{SeatNum, Points}]
+
+last_round_result(#state{last_round_num = 0}) -> no_rounds_played;
+
+last_round_result(#state{round_score = RoundScore,
+                         round_achs = RoundAchs,
+                         total_score = TotalScore,
+                         finish_info = FinishInfo
+                        }) ->
+    {FinishInfo, RoundScore, RoundAchs, TotalScore}.
+
+%% @spec chanak(ScoringState) -> Chanak
+%% @end
+%% Types: Chanak = integer()
+
+chanak(#state{chanak = Chanak}) ->
+    Chanak.
+
+%% @spec round_finished(ScoringState, FinishReason, Hands, Gosterge, WhoHasGosterge, Has8Tashes) ->
+%%                                    {NewScoringState, GameIsOver}
+%% @end
+%% Types:
+%%     FinishReason = tashes_out |
+%%                    {reveal, SeatNum, Tashes, Discarded, ConfirmationList} |
+%%                    {gosterge_finish, SeatNum}
+%%       Tashes = [Row1, Row2]
+%%         Row1 = Row2 = [null | tash()]
+%%       Discaded = tash()
+%%       ConfirmationList = [SeatNum, boolean()]
+%%     Hands = [{SeatNum, Hand}]
+%%       Hand = [tash()]
+%%     Gosterge = normal_tash()
+%%     WhoHasGosterge = undefined | SeatNum
+%%     Has8Tahses = [SeatNum]
+%%     GameisOver = boolean() 
+
+round_finished(#state{mode = GameMode, seats_num = SeatsNum,
+                      last_round_num = LastRoundNum,
+                      total_score = TotalScore, chanak = Chanak} = State,
+               FinishReason, Hands, Gosterge, WhoHasGosterge, Has8Tashes) ->
+    ScoringMode = get_scoring_mode(GameMode, Gosterge),
+    PointingRules = get_pointing_rules(ScoringMode),
+    ChanakRefillingPoints = get_chanak_refilling_points(ScoringMode),
+    Seats = lists:seq(1, SeatsNum),
+    FinishInfo = finish_info(GameMode, FinishReason, Gosterge),
+    PlayersAchs = players_achivements(GameMode, Seats, Hands, WhoHasGosterge, Has8Tashes, FinishInfo),
+    {ChanakWinPoints, NewChanak} = calc_chanak(GameMode, WhoHasGosterge, PlayersAchs,
+                                               ChanakRefillingPoints, PointingRules, Chanak),
+    PlayersAchsRecs = [{SeatNum, get_achivements_points(PointingRules, Achivements)}
+                       || {SeatNum, Achivements} <- PlayersAchs],
+    RoundAchs = merge_points(PlayersAchsRecs, ChanakWinPoints),
+    RoundScore = [{SeatNum, sum_achivements_points(AchPoints)}
+                   || {SeatNum, AchPoints} <- RoundAchs],
+    RoundNum = LastRoundNum + 1,
+    NewTotalScore = add_delta(TotalScore, RoundScore),
+    NewState = State#state{last_round_num = RoundNum,
+                           round_score = RoundScore,
+                           total_score = NewTotalScore,
+                           round_achs = RoundAchs,
+                           finish_info = FinishInfo,
+                           chanak = NewChanak},
+    {NewState, detect_game_finish(NewState)}.
+
+%%
+%% Local Functions
+%%
+
+merge_points(PlayersAchsRecs, ChanakWinPoints) ->
+    F = fun({SeatNum, ChanakPoints}, Acc) ->
+                ChanakRec = {?ACH_CHANAK_WINNER, ChanakPoints},
+                case lists:keyfind(SeatNum, 1, Acc) of
+                    {_, AchsRecs} -> lists:keyreplace(SeatNum, 1, Acc,
+                                                      {SeatNum, [ChanakRec | AchsRecs]});
+                    false ->
+                        [{SeatNum, [ChanakRec]}]
+                end
+        end,
+    lists:foldl(F, PlayersAchsRecs, ChanakWinPoints).
+
+%% calc_chanak(GameMode, WhoHasGosterge, PlayersAchs, Chanak) -> {ChanakWinPoints, NewChanak}
+calc_chanak(GameMode, WhoHasGosterge, PlayersAchs, RefillingPoints, PointingRules, Chanak) ->
+    if GameMode == ?MODE_EVENODD;
+       GameMode == ?MODE_COLOR ->
+           GostergePoints = if WhoHasGosterge == undefined ->
+                                   lists:nth(?ACH_GOSTERGE_SHOWN, PointingRules);
+                               true -> 0
+                            end,
+           case find_chanak_winners(PlayersAchs) of
+               [] ->
+                   {[], Chanak + GostergePoints};
+               ChanakWinners ->
+                   WinnersNum = length(ChanakWinners),
+                   PerWinnerPoints = Chanak div WinnersNum,
+                   RestPoints = Chanak rem WinnersNum,
+                   ChanakWinPoints = [{SeatNum, PerWinnerPoints} || SeatNum <- ChanakWinners],
+                   {ChanakWinPoints, RestPoints + GostergePoints + RefillingPoints}
+           end;
+       true -> {[], 0}
+    end.
+
+
+find_chanak_winners(PlayersAchs) ->
+    [SeatNum || {SeatNum, Achs} <- PlayersAchs, is_chanak_winner(Achs)].
+
+
+is_chanak_winner(Achs) ->
+    F = fun(Ach) -> lists:member(Ach, chanak_win_achievements())
+        end,
+    lists:any(F, Achs).
+
+
+detect_game_finish(#state{mode = GameMode, last_round_num = RoundNum, finish_info = FinishInfo,
+                          rounds_num = MaxRoundNum, total_score = TotalScore}) ->
+    if GameMode == ?MODE_COUNTDOWN ->
+           lists:any(fun({_, Points}) -> Points >= ?COUNTDOWN_MAX_POINTS end, TotalScore);
+       FinishInfo == set_timeout ->
+           true;
+       true ->
+           RoundNum == MaxRoundNum
+    end.
+
+
+players_achivements(Mode, Seats, Hands, WhoHasGosterge, Has8Tashes, FinishInfo) ->
+    case FinishInfo of
+        tashes_out ->
+            [begin
+                 Achivements = player_achivements_no_winner(Mode, SeatNum, WhoHasGosterge, Has8Tashes),
+                 {SeatNum, Achivements}
+             end || SeatNum <- Seats];
+        timeout ->
+            [begin
+                 Achivements = player_achivements_no_winner(Mode, SeatNum, WhoHasGosterge, Has8Tashes),
+                 {SeatNum, Achivements}
+             end || SeatNum <- Seats];
+        set_timeout ->
+            [begin
+                 Achivements = player_achivements_no_winner(Mode, SeatNum, WhoHasGosterge, Has8Tashes),
+                 {SeatNum, Achivements}
+             end || SeatNum <- Seats];
+        {win_reveal, Revealer, WrongRejects, RevealWithColor, RevealWithOkey, RevealWithPairs} ->
+            [begin
+                 {_, _Hand} = lists:keyfind(SeatNum, 1, Hands),
+                 Achivements = player_achivements_win_reveal(Mode, SeatNum, WhoHasGosterge, Has8Tashes,
+                                                             Revealer, WrongRejects, RevealWithOkey,
+                                                             RevealWithPairs, RevealWithColor),
+                 {SeatNum, Achivements}
+             end || SeatNum <- Seats];
+        {fail_reveal, Revealer} ->
+            [begin
+                 {_, _Hand} = lists:keyfind(SeatNum, 1, Hands),
+                 Achivements = player_achivements_fail_reveal(Mode, SeatNum, WhoHasGosterge,
+                                                              Has8Tashes, Revealer),
+                 {SeatNum, Achivements}
+             end || SeatNum <- Seats];
+        {gosterge_finish, _Winner} ->
+            [begin
+                 {_, _Hand} = lists:keyfind(SeatNum, 1, Hands),
+                 Achivements = player_achivements_gosterge_finish(Mode, SeatNum, WhoHasGosterge, Has8Tashes),
+                 {SeatNum, Achivements}
+             end || SeatNum <- Seats]
+    end.
+
+
+%% finish_info(GameMode, FinishReason, Gosterge) ->
+%%      tashes_out |
+%%      timeout |
+%%      set_timeout |
+%%      {win_reveal, Revealer, WrongRejects, RevealWithColor, RevealWithOkey, RevealWithPairs} |
+%%      {fail_reveal, Revealer} |
+%%      {gosterge_finish, Winner}
+finish_info(GameMode, FinishReason, Gosterge) ->
+    case FinishReason of
+        tashes_out -> tashes_out;
+        timeout -> timeout;
+        set_timeout -> set_timeout;
+        {reveal, Revealer, Tashes, Discarded, ConfirmationList} ->
+            {RightReveal, RevealWithPairs, WithColor} = check_reveal(Tashes, Gosterge),
+            WinReveal = RightReveal orelse lists:all(fun({_, Answer}) -> Answer == true end, ConfirmationList),
+            if WinReveal ->
+                   RevealWithColor = case GameMode of
+                                         ?MODE_STANDARD -> false;
+                                         ?MODE_EVENODD -> WithColor;
+                                         ?MODE_COLOR -> WithColor;
+                                         ?MODE_COUNTDOWN -> false
+                                     end,
+                   Okey = gosterge_to_okey(Gosterge),
+                   RevealWithOkey = Discarded == Okey,
+                   WrongRejects = if RightReveal ->
+                                         [S || {S, Answer} <- ConfirmationList, Answer==false];
+                                     true -> []
+                                  end,
+                   {win_reveal, Revealer, WrongRejects, RevealWithColor, RevealWithOkey, RevealWithPairs};
+               true ->
+                   {fail_reveal, Revealer}
+            end;
+        {gosterge_finish, Winner} when GameMode == ?MODE_COUNTDOWN ->
+            {gosterge_finish, Winner}
+    end.
+
+
+%% @spec get_achivements_points(PointingRules, Achivements) -> AchsPoints
+%% @end
+get_achivements_points(PointingRules, Achivements) ->
+    [{Ach, lists:nth(Ach, PointingRules)} || Ach <- Achivements].
+
+%% @spec sum_achivements_points(AchPoints) -> integer()
+%% @end
+sum_achivements_points(AchPoints) ->
+    lists:foldl(fun({_, P}, Acc)-> Acc + P end, 0, AchPoints).
+
+%% @spec add_delta(TotalScore, RoundScores) -> NewTotalScore
+%% @end
+add_delta(TotalScore, RoundScores) ->
+    [{SeatNum, proplists:get_value(SeatNum, TotalScore) + Delta}
+     || {SeatNum, Delta} <- RoundScores].
+
+
+%% @spec gosterge_to_okey(GostergyTash) -> OkeyTash
+%% @end
+gosterge_to_okey({Color, Value}) ->
+    if Value == 13 -> {Color, 1};
+       true -> {Color, Value + 1}
+    end.
+
+%% @spec check_reveal(TashPlaces, Gosterge) -> {RightReveal, WithPairs, SameColor}
+%% @end
+check_reveal([TopRow, BottomRow], Gosterge) ->
+    FlatList = TopRow ++ [null | BottomRow],
+    Okey = gosterge_to_okey(Gosterge),
+    Normalized = [case E of
+                      Okey -> okey;
+                      false_okey -> Okey;
+                       _ -> E
+                  end || E <- FlatList],
+    Sets = split_by_delimiter(null, Normalized),
+    ProperHand = lists:all(fun(S) -> is_set(S) orelse is_run(S) end, Sets),
+    Pairs = lists:all(fun(S) -> is_pair(S) end, Sets),
+    [Color | Rest] = [C || {C, _} <- Normalized],
+    SameColor = lists:all(fun(C) -> C==Color end, Rest),
+    {ProperHand orelse Pairs, Pairs, ProperHand andalso SameColor}.
+
+%% @spec split_by_delimiter(Delimiter, List) -> ListOfList
+%% @end
+split_by_delimiter(Delimiter, Hand) -> split_by_delimiter(Delimiter, Hand, []).
+split_by_delimiter(_, [], Acc) -> lists:reverse(Acc);
+split_by_delimiter(Delimiter, [Delimiter | Hand], Acc) -> split_by_delimiter(Delimiter, Hand, Acc);
+split_by_delimiter(Delimiter, Hand, Acc) ->
+    {L, Rest} = lists:splitwith(fun(X) -> X =/= Delimiter end, Hand),
+    split_by_delimiter(Delimiter, Rest, [L | Acc]).
+
+%% @spec is_set(Set) -> boolean()
+%% @end
+is_set(Set) when
+  length(Set) < 3;
+  length(Set) > 4 -> false;
+is_set(Set) ->
+    Normals = [ X || X <- Set, X =/= okey ],
+    {_, Value} = hd(Normals),
+    SameValue = lists:all(fun({_, V}) -> V == Value end, Normals),
+    UniqColors = length(Normals) == length(lists:usort([C || {C, _} <- Normals])),
+    SameValue andalso UniqColors.
+
+
+%% @spec is_run(Set) -> boolean()
+%% @end
+is_run(Set) when length(Set) < 3 -> false;
+is_run(Set) ->
+    {Okeys, Normals} = lists:partition(fun(X)-> X == okey end, Set),
+    {Color, _} = hd(Normals),
+    {Colors, Values} = lists:unzip(Normals),
+    SameColor = lists:all(fun(C) -> C == Color end, Colors),
+    SortedValues = lists:sort(Values),
+    NormalizedValues = if hd(SortedValues)==1 -> tl(SortedValues) ++ [14]; true -> false end,
+    OkeysNum = length(Okeys),
+    Check1 = check_run(SortedValues, OkeysNum),
+    Check2 = check_run(NormalizedValues, OkeysNum),
+    SameColor andalso (Check1 orelse Check2).
+
+
+check_run(false, _) -> false;
+check_run([First | Rest], OkeysNum) ->
+    check_run(First, Rest, OkeysNum).
+
+
+check_run(Cur, [Cur | _], _OkeysNum) -> false;
+check_run(Cur, [Next | Rest], OkeysNum) when Next == Cur + 1 ->
+    check_run(Cur+1, Rest, OkeysNum);
+check_run(_Cur, [_Next | _Rest], 0) -> false;
+check_run(Cur, [Next | Rest], OkeysNum) ->
+    check_run(Cur+1, [Next | Rest], OkeysNum - 1);
+check_run(_Cur, [], _OkeysNum) -> true.
+
+%% @spec is_pair(Set) -> boolean()
+%% @end
+is_pair([_A, okey]) -> true;
+is_pair([okey, _B]) -> true;
+is_pair([A, A]) -> true;
+is_pair(_) -> false.
+
+
+player_achivements_no_winner(Mode, SeatNum, WhoHasGosterge, Has8Tashes) ->
+    player_achivements(Mode, SeatNum, WhoHasGosterge, Has8Tashes, no_winner, undefined,
+                       undefined, undefined, undefined, undefined, undefined).
+
+player_achivements_win_reveal(Mode, SeatNum, WhoHasGosterge, Has8Tashes,
+                              Revealer, WrongRejects, RevealWithOkey,
+                              RevealWithPairs, RevealWithColor) ->
+    player_achivements(Mode, SeatNum, WhoHasGosterge, Has8Tashes, reveal,
+                       Revealer, WrongRejects, true, RevealWithOkey,
+                       RevealWithPairs, RevealWithColor).
+
+player_achivements_fail_reveal(Mode, SeatNum, WhoHasGosterge, Has8Tashes, Revealer) ->
+    player_achivements(Mode, SeatNum, WhoHasGosterge, Has8Tashes, reveal,
+                       Revealer, [], false, false, false, false).
+
+player_achivements_gosterge_finish(Mode, SeatNum, WhoHasGosterge, Has8Tashes) ->
+    player_achivements(Mode, SeatNum, WhoHasGosterge, Has8Tashes, gosterge_finish,
+                       undefined, [], false, false, false, false).
+
+%% player_achivements(Mode, SeatNum, WhoHasGosterge, Has8Tashes, FinishType, Revealer, WrongRejects,
+%%                    WinReveal, RevealWithOkey, RevealWithPairs, WithColor) -> [{AchId}]
+player_achivements(Mode, SeatNum, WhoHasGosterge, Has8Tashes, FinishType, Revealer, WrongRejects,
+                   WinReveal, RevealWithOkey, RevealWithPairs, WithColor) ->
+    L=[
+       %% 1
+       {?ACH_GOSTERGE_SHOWN, SeatNum == WhoHasGosterge},
+       %% 2
+       {?ACH_WIN_REVEAL, FinishType == reveal andalso SeatNum == Revealer andalso WinReveal andalso (not RevealWithOkey) andalso (not RevealWithPairs) andalso (not WithColor)},
+       %% 3
+       {?ACH_WIN_REVEAL_WITH_OKEY, FinishType == reveal andalso SeatNum == Revealer andalso WinReveal andalso RevealWithOkey andalso (not RevealWithPairs) andalso (not WithColor)},
+       %% 4
+       {?ACH_WIN_REVEAL_WITH_PAIRS, FinishType == reveal andalso SeatNum == Revealer andalso WinReveal andalso (not RevealWithOkey) andalso RevealWithPairs andalso (not WithColor)},
+       %% 5
+       {?ACH_WIN_REVEAL_WITH_OKEY_PAIRS, FinishType == reveal andalso SeatNum == Revealer andalso WinReveal andalso RevealWithOkey andalso RevealWithPairs andalso (not WithColor)},
+       %% 6
+       {?ACH_8_TASHES, (Mode == ?MODE_EVENODD orelse Mode == ?MODE_COLOR) andalso FinishType == reveal andalso lists:member(SeatNum, Has8Tashes)},
+       %% 7
+       {?ACH_WIN_REVEAL_WITH_COLOR, (Mode == ?MODE_EVENODD orelse Mode == ?MODE_COLOR) andalso FinishType == reveal andalso SeatNum == Revealer andalso WinReveal andalso (not RevealWithOkey) andalso (not RevealWithPairs) andalso WithColor},
+       %% 8
+       {?ACH_WIN_REVEAL_WITH_COLOR_OKEY, (Mode == ?MODE_EVENODD orelse Mode == ?MODE_COLOR) andalso FinishType == reveal andalso SeatNum == Revealer andalso WinReveal andalso RevealWithOkey andalso (not RevealWithPairs) andalso WithColor},
+       %% 9
+       {?ACH_WIN_REVEAL_WITH_COLOR_PAIRS, (Mode == ?MODE_EVENODD orelse Mode == ?MODE_COLOR) andalso FinishType == reveal andalso SeatNum == Revealer andalso WinReveal andalso (not RevealWithOkey) andalso RevealWithPairs andalso WithColor},
+       %% 10
+       {?ACH_WIN_REVEAL_WITH_COLOR_OKEY_PAIRS, (Mode == ?MODE_EVENODD orelse Mode == ?MODE_COLOR) andalso FinishType == reveal andalso SeatNum == Revealer andalso WinReveal andalso RevealWithOkey andalso RevealWithPairs andalso WithColor},
+       %% 11
+       {?ACH_FAIL_REVEAL, FinishType == reveal andalso SeatNum == Revealer andalso (not WinReveal)},
+       %% 12 AKA others_on_wrong_reveal
+       {?ACH_CAUGHT_BLUFF, FinishType == reveal andalso SeatNum =/= Revealer andalso (not WinReveal)},
+       %% 13
+       {?ACH_EMPTY_BOX, false}, %% XXX: By the last information it is no deduction from players to the chanak
+       %% 14
+       {?ACH_REJECT_GOOD_HAND, FinishType == reveal andalso lists:member(SeatNum, WrongRejects)},
+       %% 15
+       {?ACH_GOSTERGE_WINNER, Mode == ?MODE_COUNTDOWN andalso FinishType == gosterge_finish andalso SeatNum == WhoHasGosterge}
+      ],
+    [Ach || {Ach, true} <- L].
+
+chanak_win_achievements() ->
+    [?ACH_WIN_REVEAL_WITH_OKEY,
+     ?ACH_WIN_REVEAL_WITH_PAIRS,
+     ?ACH_WIN_REVEAL_WITH_OKEY_PAIRS,
+     ?ACH_8_TASHES,
+     ?ACH_WIN_REVEAL_WITH_COLOR,
+     ?ACH_WIN_REVEAL_WITH_COLOR_OKEY,
+     ?ACH_WIN_REVEAL_WITH_COLOR_PAIRS,
+     ?ACH_WIN_REVEAL_WITH_COLOR_OKEY_PAIRS].
+
+get_pointing_rules(ScoringMode) ->
+    {_, Rules} = lists:keyfind(ScoringMode, 1, points_matrix()),
+    Rules.
+
+%% TODO: Check the table
+points_matrix() ->
+    [%%          1   2   3   4   5   6   7   8   9  10  11  12  13  14  15 <--- achievement number
+     {standard, [1,  3,  6,  6, 12,  0,  0,  0,  0,  0, -9,  3,  0, -1,  0]},
+     {odd,      [1,  3,  6,  6, 12, 12, 24, 48, 48, 96, -9,  3,  0, -1,  0]},
+     {even,     [2,  6, 12, 12, 24, 24, 48, 96, 96,192,-18,  6,  0, -1,  0]},
+     {ybodd,    [1,  3,  6,  6, 12, 12, 24, 48, 48, 96, -9,  3,  0, -1,  0]},
+     {ybeven,   [2,  6, 12, 12, 24, 24, 48, 96, 96,192,-18,  6,  0, -1,  0]},
+     {rbodd,    [2,  6, 12, 12, 24, 24, 48, 96, 96,192,-18,  6,  0, -1,  0]},
+     {rbeven,   [4, 12, 24, 24, 48, 48, 96,192,192,384,-36, 12,  0, -1,  0]},
+     {countdown,[1,  2,  4,  4,  8,  0,  0,  0,  0,  0, -2,  0,  0, -1,  1]}
+    ].
+
+get_chanak_refilling_points(ScoringMode) ->
+    {_, RefillingPoints} = lists:keyfind(ScoringMode, 1, chanak_refilling_table()),
+    RefillingPoints.
+
+chanak_refilling_table() ->
+    [
+     {standard, 0},
+     {odd,      8},
+     {even,     8},
+     {ybodd,   16},
+     {ybeven,  16},
+     {rbodd,   16},
+     {rbeven,  16},
+     {countdown,0}
+    ].
+%%===================================================================
+
+%% @spec get_scoring_mode(GameMode, Gosterge) ->  ScoringMode
+%% @end
+get_scoring_mode(?MODE_STANDARD, _) ->  standard;
+get_scoring_mode(?MODE_COUNTDOWN, _) -> countdown;
+get_scoring_mode(?MODE_COLOR, {Color, Val}) when (Val rem 2) == 0 -> get_scoring_mode_c_even(b2c(Color));
+get_scoring_mode(?MODE_COLOR, {Color, Val}) when (Val rem 2) == 1 -> get_scoring_mode_c_odd(b2c(Color));
+get_scoring_mode(?MODE_EVENODD, {_Color, Val}) when (Val rem 2) == 0 -> even;
+get_scoring_mode(?MODE_EVENODD, {_Color, Val}) when (Val rem 2) == 1 -> odd.
+
+get_scoring_mode_c_odd(C) when C == yellow; C == blue -> ybodd;
+get_scoring_mode_c_odd(C) when C == black; C == red -> rbodd.
+get_scoring_mode_c_even(C) when C == yellow; C == blue -> ybeven;
+get_scoring_mode_c_even(C) when C == black; C == red -> rbeven.
+
+b2c(1) -> red;
+b2c(2) -> blue;
+b2c(3) -> yellow;
+b2c(4) -> black.
+
+
+%% Tests
+test_test_() ->
+    [{"is_pair",
+      [?_assertEqual(true,  is_pair([{1,3}, {1,3}])),
+       ?_assertEqual(true,  is_pair([{4,13}, {4,13}])),
+       ?_assertEqual(false, is_pair([{4,12}, {4,13}])),
+       ?_assertEqual(false, is_pair([{1,1}, {4,8}])),
+       ?_assertEqual(true,  is_pair([okey, {3,8}])),
+       ?_assertEqual(true,  is_pair([{2,3}, okey])),
+       ?_assertEqual(true,  is_pair([okey, okey])),
+       ?_assertEqual(false, is_pair([{2,4}, {4,2}, {3,3}])),
+       ?_assertEqual(false, is_pair([{2,4}])),
+       ?_assertEqual(false, is_pair([okey])),
+       ?_assertEqual(false, is_pair([okey, okey, {2,6}]))
+      ]},
+     {"is_set",
+      [?_assertEqual(true,  is_set([{1,3}, {3,3}, {2,3}])),
+       ?_assertEqual(true,  is_set([{4,8}, okey, {2,8}])),
+       ?_assertEqual(true,  is_set([{4,3}, okey, okey])),
+       ?_assertEqual(true,  is_set([{4,13}, {1,13}, {3,13}, {2,13}])),
+       ?_assertEqual(true,  is_set([okey, {1,13}, {3,13}, {2,13}])),
+       ?_assertEqual(true,  is_set([okey, okey, {3,13}, {2,13}])),
+       ?_assertEqual(false, is_set([{2,6}])),
+       ?_assertEqual(false, is_set([okey])),
+       ?_assertEqual(false, is_set([{3,4}, {2,6}])),
+       ?_assertEqual(false, is_set([{2,3}, {4,3}])),
+       ?_assertEqual(false, is_set([okey, okey])),
+       ?_assertEqual(false, is_set([{3,4}, {1,4}, {3,4}])),
+       ?_assertEqual(false, is_set([{3,4}, {1,4}, {2,5}])),
+       ?_assertEqual(false, is_set([{3,4}, okey, {2,5}])),
+       ?_assertEqual(false, is_set([{2,5}, {3,5}, {4,5}, {1,6}])),
+       ?_assertEqual(false, is_set([{2,5}, {3,5}, {4,5}, {2,5}])),
+       ?_assertEqual(false, is_set([{2,3}, {3,3}, {4,3}, {1,3}, {3,1}])),
+       ?_assertEqual(false, is_set([{2,3}, okey, {4,3}, {1,3}, {3,3}]))
+      ]},
+     {"is_run",
+      [?_assertEqual(false, is_run([{1,3}])),
+       ?_assertEqual(false, is_run([okey])),
+       ?_assertEqual(false, is_run([{2,2}, {2,3}])),
+       ?_assertEqual(false, is_run([okey, {2,3}])),
+       ?_assertEqual(false, is_run([{4,1}, {2,3}])),
+       ?_assertEqual(true,  is_run([{4,4}, {4,6}, {4,5}])),
+       ?_assertEqual(true,  is_run([okey, {4,6}, {4,5}])),
+       ?_assertEqual(true,  is_run([okey, {4,6}, okey])),
+       ?_assertEqual(true,  is_run([{1,12}, {1,1}, {1,13}])),
+       ?_assertEqual(true,  is_run([{1,12}, {1,1}, {1,11}, {1,13}])),
+       ?_assertEqual(true,  is_run([{2,4}, {2,6}, {2,7}, {2,5}])),
+       ?_assertEqual(false, is_run([{2,4}, {1,6}, {2,7}, {2,5}])),
+       ?_assertEqual(true,  is_run([{1,12}, {1,1}, {1,11}, {1,10}, {1,13}])),
+       ?_assertEqual(false, is_run([{1,12}, {1,1}, {1,11}, {1,11}, {1,13}])),
+       ?_assertEqual(false, is_run([{1,12}, {1,1}, {1,2}, {1,11}, {1,13}])),
+       ?_assertEqual(true,  is_run([{3,6}, {3,8}, okey, {3,5}, okey])),
+       ?_assertEqual(true,  is_run([{3,6}, {3,8}, okey, {3,5}, {3,9}])),
+       ?_assertEqual(false, is_run([{3,6}, {3,8}, okey, {3,5}, {3,9}, {3,2}])),
+       ?_assertEqual(false, is_run([{3,6}, {3,8}, okey, {3,5}, {3,9}, {1,2}]))
+      ]}
+    ].

+ 1541 - 0
apps/server/src/okey/game_okey_ng_table_trn.erl

@@ -0,0 +1,1541 @@
+%%% -------------------------------------------------------------------
+%%% Author  : Sergii Polkovnikov <serge.polkovnikov@gmail.com>
+%%% Description :
+%%%
+%%% Created : Oct 15, 2012
+%%% -------------------------------------------------------------------
+-module(game_okey_ng_table_trn).
+
+-behaviour(gen_fsm).
+%% --------------------------------------------------------------------
+%% Include files
+%% --------------------------------------------------------------------
+-include_lib("server/include/log.hrl").
+-include_lib("server/include/basic_types.hrl").
+-include_lib("server/include/settings.hrl").
+-include_lib("server/include/game_okey.hrl").
+-include_lib("server/include/requests.hrl").
+
+%% --------------------------------------------------------------------
+%% External exports
+-export([start/3,
+         player_action/3,
+         parent_message/2,
+         relay_message/2
+        ]).
+
+-export([submit/3, signal/3]).
+
+%% gen_fsm callbacks
+-export([init/1, handle_event/3, handle_sync_event/4,
+         handle_info/3, terminate/3, code_change/4]).
+
+
+-type tash() :: false_okey | {integer(), integer()}.
+
+-record(desk_state,
+        {
+         state              :: state_take | state_discard | state_finished,
+         hands              :: list({integer(), list(tash())}), %% {SeatNum, Tashes}
+         discarded          :: list({integer(), list(tash())}), %% {SeatNum, Tashes}
+         deck               :: list(tash()),
+         cur_seat           :: integer(),
+         gosterge           :: tash(),
+         has_gosterge       :: undefined | integer(), %% Seat num of a player who has gosterge
+         have_8_tashes      :: list(integer()), %% Seats of players who show 8 tashes combination
+         finish_reason      :: tashes_out | reveal | gosterge_finish, %% Defined only when state = state_finished
+         finish_info        :: term()
+        }).
+
+-record(state,
+        {%% Fixed parameters
+         game_id              :: pos_integer(),
+         table_id             :: pos_integer(),
+         table_name           :: string(),
+         parent               :: {atom(), pid()},
+         relay                :: pid(),
+         mult_factor          :: integer(),
+         slang_flag           :: boolean(),
+         observer_flag        :: boolean(),
+         tournament_type      :: atom(), %%  standalone | elimination | pointing | lucky
+         speed                :: slow | normal | fast,
+         turn_timeout         :: integer(),
+         reveal_confirmation_timeout    :: integer(),
+         ready_timeout        :: integer(),
+         round_timeout        :: infinity | integer(),
+         set_timeout          :: infinity | integer(),
+         game_mode            :: standard | color | evenodd | countdown,
+         rounds               :: undefined | integer(), %% Not defined for countdown game type
+         reveal_confirmation  :: boolean(),
+         next_series_confirmation :: yes_exit | no_exit | no,
+         pause_mode           :: disabled | normal,
+         gosterge_finish_allowed :: undefined | boolean(), %% Only defined for countdown game type
+         social_actions_enabled :: boolean(),
+         tour                 :: undefined | integer(),
+         tours                :: undefined | integer(),
+         %% Dynamic parameters
+         desk_rule_pid        :: undefined | pid(),
+         players,             %% The register of table players
+         tournament_table     :: list(), %% [{TurnNum, TurnRes}], TurnRes = [{PlayerId, Points, Status}]
+         start_seat           :: integer(), %% The player who moves first
+         cur_round            :: integer(),
+         desk_state           :: #desk_state{}, %% For tracking current state of a game on the table
+         scoring_state        :: term(), %% generated by a scoring module
+         reveal_confirmation_list :: list(), %% {SeatNum, Answer}
+         wait_list            :: list(),
+         timeout_timer        :: undefined | reference(),
+         timeout_magic        :: term(),
+         round_timer          :: undefined | reference(),
+         set_timer            :: undefined | reference(),
+         paused_statename     :: atom(), %% For storing a statename before pause
+         paused_timeout_value :: integer() %% For storing remain timeout value
+        }).
+
+-record(player,
+        {
+         id              :: pos_integer(), %% Player Id
+         seat_num        :: integer(),
+         user_id         :: binary(),
+         is_bot          :: boolean(),
+         info            :: #'PlayerInfo'{},
+         connected       :: boolean()
+        }).
+
+-define(STATE_WAITING_FOR_START, state_waiting_for_start).
+-define(STATE_PLAYING, state_playing).
+-define(STATE_REVEAL_CONFIRMATION, state_reveal_confirmation).
+-define(STATE_FINISHED, state_finished).
+-define(STATE_PAUSE, state_pause).
+-define(STATE_SET_FINISHED, state_set_finished).
+
+-define(HAND_SIZE, 14).
+-define(SEATS_NUM, 4).
+-define(RELAY, relay_ng).
+-define(DESK, game_okey_ng_desk).
+-define(SCORING, game_okey_ng_scoring).
+
+%% ====================================================================
+%% External functions
+%% ====================================================================
+
+start(GameId, TableId, TableParams) ->
+    gen_fsm:start(?MODULE, [GameId, TableId, TableParams], []).
+
+player_action(Srv, PlayerId, Action) ->
+    gen_fsm:sync_send_all_state_event(Srv, {player_action, PlayerId, Action}).
+
+parent_message(Srv, Message) ->
+    gen_fsm:send_all_state_event(Srv, {parent_message, Message}).
+
+relay_message(Srv, Message) ->
+    gen_fsm:send_all_state_event(Srv, {relay_message, Message}).
+
+
+submit(Table, PlayerId, Action) ->
+    player_action(Table, PlayerId, {submit, Action}).
+
+signal(Table, PlayerId, Signal) ->
+    player_action(Table, PlayerId, {signal, Signal}).
+
+%% ====================================================================
+%% Server functions
+%% ====================================================================
+init([GameId, TableId, Params]) ->
+    Parent = proplists:get_value(parent, Params),
+    PlayersInfo = proplists:get_value(players, Params),
+    TableName = proplists:get_value(table_name, Params),
+    MultFactor = proplists:get_value(mult_factor, Params),
+    SlangFlag = proplists:get_value(slang_allowed, Params),
+    ObserversFlag = proplists:get_value(observers_allowed, Params),
+    TournamentType = proplists:get_value(tournament_type, Params),
+    Speed = proplists:get_value(speed, Params),
+    TurnTimeout = proplists:get_value(turn_timeout, Params, get_timeout(turn, Speed)), %% TODO Set this param explictly
+    RevConfirmTimeout = proplists:get_value(reveal_confirmation_timeout, Params, get_timeout(challenge, Speed)), %% TODO Set this param explictly
+    ReadyTimeout = proplists:get_value(ready_timeout, Params, get_timeout(ready, Speed)), %% TODO Set this param explictly
+    RoundTimeout = proplists:get_value(round_timeout, Params),
+    SetTimeout = proplists:get_value(set_timeout, Params),
+    GameMode = proplists:get_value(game_type, Params),
+    Rounds = proplists:get_value(rounds, Params),
+    RevealConfirmation = proplists:get_value(reveal_confirmation, Params),
+    NextSeriesConfirmation = proplists:get_value(next_series_confirmation, Params),
+    PauseMode = proplists:get_value(pause_mode, Params),
+    GostergeFinishAllowed = proplists:get_value(gosterge_finish_allowed, Params),
+    SocialActionsEnabled = proplists:get_value(social_actions_enabled, Params),
+    TTable = proplists:get_value(ttable, Params),
+    Tour = proplists:get_value(tour, Params),
+    Tours = proplists:get_value(tours, Params),
+    %% Next two options will be passed on table respawn (after fail or service maintaince)
+    ScoringState = proplists:get_value(scoring_state, Params, init_scoring(GameMode, PlayersInfo, Rounds)),
+    CurRound = proplists:get_value(cur_round, Params, 0),
+
+    Players = init_players(PlayersInfo),
+    RelayParams = [{players, [{PlayerId, UserInfo#'PlayerInfo'.id} || {PlayerId, UserInfo, _, _} <- PlayersInfo]},
+                   {observers_allowed, false},
+                   {table, {?MODULE, self()}}],
+    {ok, Relay} = ?RELAY:start(RelayParams),
+    ?INFO("OKEY_NG_TABLE_TRN_DBG <~p,~p> Set timeout: ~p, round timeout: ~p.", [GameId, TableId, SetTimeout, RoundTimeout]),
+    ?INFO("OKEY_NG_TABLE_TRN_DBG <~p,~p> PlayersInfo: ~p.", [GameId, TableId, PlayersInfo]),
+    ?INFO("OKEY_NG_TABLE_TRN <~p,~p> Started.", [GameId, TableId]),
+    parent_notify_table_created(Parent, TableId, Relay),
+    {ok, ?STATE_WAITING_FOR_START, #state{game_id = GameId,
+                                          table_id = TableId,
+                                          table_name = TableName,
+                                          parent = Parent,
+                                          relay = Relay,
+                                          mult_factor = MultFactor,
+                                          slang_flag = SlangFlag,
+                                          observer_flag = ObserversFlag,
+                                          tournament_type = TournamentType,
+                                          tour = Tour,
+                                          tours = Tours,
+                                          speed = Speed,
+                                          turn_timeout = TurnTimeout,
+                                          reveal_confirmation_timeout = RevConfirmTimeout,
+                                          ready_timeout = ReadyTimeout,
+                                          round_timeout = RoundTimeout,
+                                          set_timeout = SetTimeout,
+                                          game_mode = GameMode,
+                                          rounds = Rounds,
+                                          reveal_confirmation = RevealConfirmation,
+                                          next_series_confirmation = NextSeriesConfirmation,
+                                          pause_mode = PauseMode,
+                                          gosterge_finish_allowed = GostergeFinishAllowed,
+                                          social_actions_enabled = SocialActionsEnabled,
+                                          players = Players,
+                                          start_seat = crypto:rand_uniform(1, ?SEATS_NUM + 1),
+                                          cur_round = CurRound,
+                                          scoring_state = ScoringState,
+                                          tournament_table = TTable
+                                         }}.
+
+handle_event({parent_message, Message}, StateName,
+             #state{game_id = GameId, table_id = TableId} = StateData) ->
+    ?INFO("OKEY_NG_TABLE_TRN <~p,~p> Received message from the parent: ~p.",
+          [GameId, TableId, Message]),
+    handle_parent_message(Message, StateName, StateData);
+
+handle_event({relay_message, Message}, StateName,
+             #state{game_id = GameId, table_id = TableId} =  StateData) ->
+    ?INFO("OKEY_NG_TABLE_TRN <~p,~p> Received message from the relay: ~p.",
+          [GameId, TableId, Message]),
+    handle_relay_message(Message, StateName, StateData);
+
+handle_event(_Event, StateName, StateData) ->
+    {next_state, StateName, StateData}.
+
+handle_sync_event({player_action, PlayerId, Action}, From, StateName,
+                  #state{players = Players} = StateData) ->
+    case get_player(PlayerId, Players) of
+        {ok, Player} ->
+            handle_player_action(Player, Action, From, StateName, StateData);
+        error ->
+            {reply, {error, you_are_not_a_player}, StateName, StateData}
+    end;
+
+handle_sync_event(_Event, _From, StateName, StateData) ->
+    Reply = ok,
+    {reply, Reply, StateName, StateData}.
+
+handle_info({timeout, Magic}, ?STATE_PLAYING,
+            #state{timeout_magic = Magic, game_id = GameId, table_id = TableId} = StateData) ->
+    ?INFO("OKEY_NG_TABLE_TRN <~p,~p> Move timeout. Do an automatic move(s).", [GameId, TableId]),
+    do_timeout_moves(StateData);
+
+handle_info({round_timeout, Round}, ?STATE_PLAYING,
+            #state{cur_round = Round, desk_state = DeskState, game_id = GameId,
+                   table_id = TableId, timeout_timer = TRef} = StateData) ->
+    ?INFO("OKEY_NG_TABLE_TRN <~p,~p> Time to finish round ~p because the round timeout.", [GameId, TableId, Round]),
+    if TRef =/= undefined -> erlang:cancel_timer(TRef);
+       true -> do_nothing
+    end,
+    finalize_round(StateData#state{desk_state = DeskState#desk_state{finish_reason = timeout}});
+
+handle_info(set_timeout, StateName,
+            #state{cur_round = Round, desk_state = DeskState, game_id = GameId,
+                   table_id = TableId, timeout_timer = TRef} = StateData) when
+  StateName =/= ?STATE_SET_FINISHED ->
+    ?INFO("OKEY_NG_TABLE_TRN <~p,~p> Time to finish round ~p and the set because the set timeout.", [GameId, TableId, Round]),
+    if TRef =/= undefined -> erlang:cancel_timer(TRef);
+       true -> do_nothing
+    end,
+    finalize_round(StateData#state{desk_state = DeskState#desk_state{finish_reason = set_timeout}});
+
+handle_info({timeout, Magic}, ?STATE_REVEAL_CONFIRMATION,
+            #state{timeout_magic = Magic, wait_list = WL, game_id = GameId, table_id = TableId,
+                   reveal_confirmation_list = CList} = StateData) ->
+    ?INFO("OKEY_NG_TABLE_TRN <~p,~p> Time to check reveal confirmation responses.", [GameId, TableId]),
+    NewCList = lists:foldl(fun(SeatNum, Acc) -> [{SeatNum, false} | Acc] end, CList, WL),
+    finalize_round(StateData#state{reveal_confirmation_list = NewCList});
+
+handle_info(Info, StateName, #state{game_id = GameId, table_id = TableId} = StateData) ->
+    ?INFO("OKEY_NG_TABLE_TRN <~p,~p> Unexpected message(info) received at state <~p>: ~p.",
+          [GameId, TableId, StateName, Info]),
+    {next_state, StateName, StateData}.
+
+terminate(Reason, StateName, #state{game_id = GameId, table_id = TableId, relay = Relay}) ->
+    ?INFO("OKEY_NG_TABLE_TRN <~p,~p> Shutting down at state: <~p>. Reason: ~p",
+          [GameId, TableId, StateName, Reason]),
+    relay_stop(Relay),
+    ok.
+
+code_change(_OldVsn, StateName, StateData, _Extra) ->
+    {ok, StateName, StateData}.
+
+%% --------------------------------------------------------------------
+%%% Internal functions
+%% --------------------------------------------------------------------
+
+%% handle_parent_message(Msg, StateName, StateData)
+
+handle_parent_message({register_player, RequestId, UserInfo, PlayerId, SeatNum}, StateName,
+                      #state{table_id = TableId, players = Players,
+                             parent = Parent, relay = Relay} = StateData) ->
+    #'PlayerInfo'{id = UserId, robot = IsBot} = UserInfo,
+    NewPlayers = reg_player(PlayerId, SeatNum, UserId, IsBot, UserInfo, _Connected = false, Players),
+    relay_register_player(Relay, UserId, PlayerId),
+    %% TODO: Send notificitations to gamesessions (we have no such notification)
+    parent_confirm_registration(Parent, TableId, RequestId),
+    {next_state, StateName, StateData#state{players = NewPlayers}};
+
+handle_parent_message({replace_player, RequestId, UserInfo, PlayerId, SeatNum}, StateName,
+                      #state{table_id = TableId, players = Players,
+                             parent = Parent, relay = Relay} = StateData) ->
+    #'PlayerInfo'{id = UserId, robot = IsBot} = UserInfo,
+    #player{id = OldPlayerId} = get_player_by_seat_num(SeatNum, Players),
+    NewPlayers = del_player(OldPlayerId, Players),
+    NewPlayers2 = reg_player(PlayerId, SeatNum, UserId, IsBot, UserInfo, _Connected = false, NewPlayers),
+    relay_kick_player(Relay, OldPlayerId),
+    relay_register_player(Relay, UserId, PlayerId),
+    ReplaceMsg = create_player_left(SeatNum, UserInfo, Players),
+    relay_publish_ge(Relay, ReplaceMsg),
+    parent_confirm_replacement(Parent, TableId, RequestId),
+    {next_state, StateName, StateData#state{players = NewPlayers2}};
+
+handle_parent_message(start_round, StateName,
+                      #state{game_mode = GameMode, cur_round = CurRound,
+                             gosterge_finish_allowed = GostergeFinishAllowed,
+                             start_seat = LastStartSeat, players = Players,
+                             relay = Relay, turn_timeout = TurnTimeout,
+                             round_timeout = RoundTimeout, set_timeout = SetTimeout,
+                             set_timer = SetTRef, scoring_state = ScoringState} = StateData)
+  when StateName == ?STATE_WAITING_FOR_START;
+       StateName == ?STATE_FINISHED ->
+    NewCurRound = CurRound + 1,
+    StartSeat = next_seat_num(LastStartSeat),
+    Deck = deck:shuffle(deck:init_deck(okey)),
+    {Gosterge, Deck1} = choose_gosterge(Deck),
+    F = fun(SeatNum, AccDeck) ->
+                Num = if SeatNum==StartSeat -> ?HAND_SIZE + 1; true -> ?HAND_SIZE end,
+                lists:split(Num, AccDeck)
+        end,
+    {Hands, TablePile} = lists:mapfoldl(F, deck:to_list(Deck1), lists:seq(1, ?SEATS_NUM)),
+    GostFinishList = if GameMode == countdown andalso GostergeFinishAllowed andalso NewCurRound > 1 ->
+                            {_,_,_,Scores} = ?SCORING:last_round_result(ScoringState),
+                            [SeatNum || {SeatNum, 1} <- Scores];
+                        true -> []
+                     end,
+    Have8TashesEnabled = GameMode == evenodd orelse GameMode == color,
+    Params = [{hands, Hands},
+              {deck, TablePile},
+              {gosterge, Gosterge},
+              {cur_player, StartSeat},
+              {gosterge_finish_list, GostFinishList},
+              {have_8_tashes_enabled, Have8TashesEnabled}],
+    {ok, Desk} = ?DESK:start(Params),
+    DeskState = init_desk_state(Desk),
+    %% Init timers
+    {Magic, TRef} = start_timer(TurnTimeout),
+    RoundTRef = if is_integer(RoundTimeout) ->
+                       erlang:send_after(RoundTimeout, self(), {round_timeout, NewCurRound});
+                   true -> undefined
+                end,
+    NewSetTRef = if NewCurRound == 1 ->
+                        if is_integer(SetTimeout) -> erlang:send_after(SetTimeout, self(), set_timeout);
+                           true -> undefined
+                        end;
+                    true -> SetTRef
+                 end,
+    NewStateData = StateData#state{cur_round = NewCurRound,
+                                   start_seat = StartSeat,
+                                   desk_rule_pid = Desk,
+                                   desk_state = DeskState,
+                                   timeout_timer = TRef,
+                                   timeout_magic = Magic,
+                                   round_timer = RoundTRef,
+                                   set_timer = NewSetTRef},
+    [begin
+         GameStartedMsg = create_okey_game_started(SeatNum, DeskState, NewCurRound, NewStateData),
+         send_to_client_ge(Relay, PlayerId, GameStartedMsg)
+     end || #player{id = PlayerId, seat_num = SeatNum} <- find_connected_players(Players)],
+    CurSeatNum = DeskState#desk_state.cur_seat,
+    relay_publish_ge(Relay, create_okey_next_turn(CurSeatNum, Players)),
+    {next_state, ?STATE_PLAYING, NewStateData};
+
+handle_parent_message(show_round_result, StateName,
+                      #state{relay = Relay, scoring_state = ScoringState,
+                             game_id = GameId, table_id = TableId} = StateData) ->
+    {FinishInfo, RoundScore, AchsPoints, TotalScore} = ?SCORING:last_round_result(ScoringState),
+    ?INFO("OKEY_NG_TABLE_TRN <~p,~p> RoundScore: ~p Total score: ~p.", [GameId, TableId, RoundScore, TotalScore]),
+    Msg = case FinishInfo of
+              {win_reveal, Revealer, WrongRejects, _RevealWithColor, _RevealWithOkey, _RevealWithPairs} ->
+                  create_okey_round_ended_reveal(Revealer, true, WrongRejects, RoundScore,
+                                                 TotalScore, AchsPoints, StateData);
+              {fail_reveal, Revealer} ->
+                  create_okey_round_ended_reveal(Revealer, false, [], RoundScore,
+                                                 TotalScore, AchsPoints, StateData);
+              tashes_out ->
+                  create_okey_round_ended_tashes_out(RoundScore, TotalScore, AchsPoints,
+                                                     StateData);
+              timeout ->
+                  create_okey_round_ended_tashes_out(RoundScore, TotalScore, AchsPoints,
+                                                     StateData);
+              set_timeout ->
+                  create_okey_round_ended_tashes_out(RoundScore, TotalScore, AchsPoints,
+                                                     StateData);
+              {gosterge_finish, Winner} ->
+                  create_okey_round_ended_gosterge_finish(Winner, RoundScore, TotalScore,
+                                                          AchsPoints, StateData)
+          end,
+    relay_publish_ge(Relay, Msg),
+    {next_state, StateName, StateData#state{}};
+
+%% Results = [{PlayerId, Position, Score, Status}] Status = winner | loser | eliminated | none
+handle_parent_message({show_series_result, Results}, StateName,
+                      #state{relay = Relay, players = Players,
+                             next_series_confirmation = Confirm} = StateData) ->
+    Msg = create_okey_series_ended(Results, Players, Confirm),
+    relay_publish_ge(Relay, Msg),
+    {next_state, StateName, StateData#state{}};
+
+%% Results = [{UserId, Position, Score, Status}] Status = active | eliminated
+handle_parent_message({tour_result, TourNum, Results}, StateName,
+                      #state{relay = Relay, tournament_table = TTable} = StateData) ->
+    NewTTable = [{TourNum, Results} | TTable],
+    Msg = create_okey_tour_result(TourNum, Results),
+    relay_publish_ge(Relay, Msg),
+    {next_state, StateName, StateData#state{tournament_table = NewTTable}};
+
+handle_parent_message({playing_tables_num, Num}, StateName,
+                      #state{relay = Relay} = StateData) ->
+%%XXX    Msg = create_okey_playing_tables(Num),
+%%    relay_publish_ge(Relay, Msg),
+    {next_state, StateName, StateData};
+
+handle_parent_message(rejoin_players, StateName,
+                      #state{game_id = GameId, relay = Relay,
+                             players = Players} = StateData) ->
+    [relay_unregister_player(Relay, P#player.id, {rejoin, GameId}) || P <- players_to_list(Players)],
+    {next_state, StateName, StateData#state{players = players_init()}};
+
+
+handle_parent_message(disconnect_players, StateName,
+                      #state{relay = Relay, players = Players} = StateData) ->
+    [relay_unregister_player(Relay, P#player.id, game_over) || P <- players_to_list(Players)],
+    {next_state, StateName, StateData#state{players = players_init()}};
+
+
+handle_parent_message(stop, _StateName, StateData) ->
+    {stop, normal, StateData};
+
+handle_parent_message(Message, StateName,
+                      #state{game_id = GameId, table_id = TableId} = StateData) ->
+    ?ERROR("OKEY_NG_TABLE_TRN <~p,~p> Unexpected parent message received in state <~p>: ~p. State: ~p. Stopping.",
+           [GameId, TableId, StateName, Message, StateName]),
+    {stop, unexpected_parent_message, StateData}.
+
+
+%%===================================================================
+
+%% handle_relay_message(Msg, StateName, StateData)
+
+handle_relay_message({player_connected, PlayerId} = Msg, StateName,
+                     #state{parent = Parent, game_id = GameId,
+                            table_id = TableId, players = Players} = StateData) ->
+    ?INFO("OKEY_NG_TABLE_TRN <~p,~p> Received nofitication from the relay: ~p", [GameId, TableId, Msg]),
+    case get_player(PlayerId, Players) of
+        {ok, Player} ->
+            NewPlayers = store_player_rec(Player#player{connected = true}, Players),
+            parent_send_player_connected(Parent, TableId, PlayerId),
+            {next_state, StateName, StateData#state{players = NewPlayers}};
+        error ->
+            {next_state, StateName, StateData}
+    end;
+
+
+handle_relay_message({player_disconnected, PlayerId}, StateName,
+                     #state{parent = Parent, table_id = TableId, players = Players} = StateData) ->
+    case get_player(PlayerId, Players) of
+        {ok, Player} ->
+            NewPlayers = store_player_rec(Player#player{connected = false}, Players),
+            parent_send_player_disconnected(Parent, TableId, PlayerId),
+            {next_state, StateName, StateData#state{players = NewPlayers}};
+        error ->
+            {next_state, StateName, StateData}
+    end;
+
+handle_relay_message({subscriber_added, PlayerId, SubscrId} = Msg, StateName,
+                     #state{relay = Relay, game_id = GameId,
+                            table_id = TableId, tournament_table = TTable,
+                            players = Players} = StateData) ->
+    ?INFO("OKEY_NG_TABLE_TRN <~p,~p> Received nofitication from the relay: ~p", [GameId, TableId, Msg]),
+    PlayerIdIsValid = case PlayerId of
+                          observer -> true;
+                          administrator -> true;
+                          _ ->
+                              case get_player(PlayerId, Players) of
+                                  {ok, _} -> true;
+                                  error -> false
+                              end
+                      end,
+    if PlayerIdIsValid ->
+           GI = create_okey_game_info(StateData),
+           PlState = create_okey_game_player_state(PlayerId, StateName, StateData),
+           send_to_subscriber_ge(Relay, SubscrId, GI),
+           send_to_subscriber_ge(Relay, SubscrId, PlState),
+           relay_allow_broadcast_for_player(Relay, PlayerId),
+           if TTable =/= undefined ->
+                  [send_to_subscriber_ge(Relay, SubscrId, create_okey_tour_result(TurnNum, Results))
+                     || {TurnNum, Results} <- lists:sort(TTable)];
+              true -> do_nothing
+           end;
+       true -> do_nothing
+    end,
+    {next_state, StateName, StateData};
+
+handle_relay_message(Message, StateName, #state{game_id = GameId, table_id = TableId} = StateData) ->
+    ?ERROR("OKEY_NG_TABLE_TRN <~p,~p> Unknown relay message received in state <~p>: ~p. State: ~p. Stopping.",
+           [GameId, TableId, StateName, Message]),
+    {next_state, StateName, StateData}.
+
+%%===================================================================
+
+%% handle_player_action(Player, Msg, StateName, StateData)
+
+handle_player_action(#player{id = PlayerId, seat_num = SeatNum, user_id = UserId},
+                     {submit, #game_action{action = Action, args = Args} = GA}, From,
+                     StateName,
+                     #state{game_id = GameId, table_id = TableId} = StateData) ->
+    try api_utils:to_known_record(Action, Args) of
+        ExtAction ->
+            ?INFO("OKEY_NG_TABLE_TRN <~p,~p> Player <~p> (~p) submit the game action: ~p.",
+                  [GameId, TableId, PlayerId, UserId, ExtAction]),
+            do_action(SeatNum, ExtAction, From, StateName, StateData)
+    catch
+        _Class:_Exception ->
+            ?ERROR("OKEY_NG_TABLE_TRN <~p,~p> Can't convert action ~p. Exception: ~p:~p.",
+                   [GameId, TableId, GA, _Class, _Exception]),
+            {reply, {error, invalid_action}, StateName, StateData}
+    end;
+
+
+handle_player_action(#player{id = PlayerId, user_id = UserId},
+                     {signal, {pause_game, _}=Signal}, _From,
+                     StateName,
+                     #state{table_id = TableId, game_id = GameId, timeout_timer = TRef,
+                            pause_mode = PauseMode, relay = Relay} = StateData) ->
+    ?INFO("OKEY_NG_TABLE_TRN <~p,~p> Received signal from player <~p> : ~p. PauseMode: ~p",
+          [GameId, TableId, PlayerId, Signal, PauseMode]),
+    case PauseMode of
+        disabled ->
+            {reply, {error, pause_disabled}, StateName, StateData};
+        normal ->
+            if StateName == ?STATE_PLAYING;
+               StateName == ?STATE_REVEAL_CONFIRMATION ->
+                   Timeout = case erlang:cancel_timer(TRef) of
+                                 false -> 0;
+                                 T -> T
+                             end,
+                   relay_publish(Relay, create_game_paused_pause(UserId, GameId)),
+                   {reply, 0, ?STATE_PAUSE, StateData#state{paused_statename = StateName,
+                                                            paused_timeout_value = Timeout,
+                                                            timeout_magic = undefined}};
+               true ->
+                   {reply, {error, pause_not_possible}, StateName, StateData}
+            end
+    end;
+
+
+handle_player_action(#player{id = PlayerId, user_id = UserId},
+                     {signal, {resume_game, _}=Signal}, _From,
+                     StateName,
+                     #state{table_id = TableId, game_id = GameId, pause_mode = PauseMode,
+                            relay = Relay, paused_statename = ResumedStateName,
+                            paused_timeout_value = Timeout} = StateData) ->
+    ?INFO("OKEY_NG_TABLE_TRN <~p,~p> Received signal from player <~p> : ~p. PauseMode: ~p",
+          [GameId, TableId, PlayerId, Signal, PauseMode]),
+    case PauseMode of
+        disabled ->
+            {reply, {error, pause_disabled}, StateName, StateData};
+        normal ->
+            if StateName == ?STATE_PAUSE ->
+                   relay_publish(Relay, create_game_paused_resume(UserId, GameId)),
+                   {Magic, TRef} = start_timer(Timeout),
+                   {reply, 0, ResumedStateName, StateData#state{timeout_timer = TRef,
+                                                                timeout_magic = Magic}};
+               true ->
+                   {reply, {error, game_is_not_paused}, StateName, StateData}
+            end
+    end;
+
+
+handle_player_action(#player{id = PlayerId},
+                     {signal, Signal}, _From, StateName,
+                     #state{table_id = TableId, game_id = GameId} = StateData) ->
+    ?INFO("OKEY_NG_TABLE_TRN <~p,~p> Received signal from player <~p> : ~p. Ignoring.",
+          [GameId, TableId, PlayerId, Signal]),
+    {reply, ok, StateName, StateData};
+
+
+handle_player_action(_Player, _Message, _From, StateName, StateData) ->
+    {next_state, StateName, StateData}.
+
+
+
+%%===================================================================
+
+do_action(SeatNum, #okey_has_gosterge{}, From, ?STATE_PLAYING = StateName, StateData) ->
+    do_game_action(SeatNum, i_have_gosterge, From, StateName, StateData);
+
+do_action(_SeatNum, #okey_has_gosterge{}, _From, StateName, StateData) ->
+    {reply, {error, message_not_valid_for_a_current_state}, StateName, StateData};
+
+
+do_action(SeatNum, #okey_i_have_8_tashes{}, From, ?STATE_PLAYING = StateName, StateData) ->
+    do_game_action(SeatNum, i_have_8_tashes, From, StateName, StateData);
+
+do_action(_SeatNum, #okey_i_have_8_tashes{}, _From, StateName, StateData) ->
+    {reply, {error, message_not_valid_for_a_current_state}, StateName, StateData};
+
+
+do_action(SeatNum, #okey_i_saw_okey{}, From, ?STATE_PLAYING = StateName, StateData) ->
+    do_game_action(SeatNum, see_okey, From, StateName, StateData);
+
+do_action(_SeatNum, #okey_i_saw_okey{}, _From, StateName, StateData) ->
+    {reply, {error, message_not_valid_for_a_current_state}, StateName, StateData};
+
+
+do_action(SeatNum, #okey_take{pile = 0}, From, ?STATE_PLAYING = StateName, StateData) ->
+    do_game_action(SeatNum, take_from_table, From, StateName, StateData);
+
+do_action(_SeatNum, #okey_take{pile = 0}, _From, StateName, StateData) ->
+    {reply, {error, message_not_valid_for_a_current_state}, StateName, StateData};
+
+
+do_action(SeatNum, #okey_take{pile = 1}, From, ?STATE_PLAYING = StateName, StateData) ->
+    do_game_action(SeatNum, take_from_discarded, From, StateName, StateData);
+
+do_action(_SeatNum, #okey_take{pile = 1}, _From, StateName, StateData) ->
+    {reply, {error, message_not_valid_for_a_current_state}, StateName, StateData};
+
+
+do_action(SeatNum, #okey_discard{tile = ExtTash}, From, ?STATE_PLAYING = StateName, StateData) ->
+    Tash = ext_to_tash(ExtTash),
+    do_game_action(SeatNum, {discard, Tash}, From, StateName, StateData);
+
+do_action(_SeatNum, #okey_discard{}, _From, StateName, StateData) ->
+    {reply, {error, message_not_valid_for_a_current_state}, StateName, StateData};
+
+
+do_action(SeatNum, #okey_reveal{discarded = ExtDiscarded, hand = ExtHand}, From,
+          ?STATE_PLAYING = StateName, StateData) ->
+    Discarded = ext_to_tash(ExtDiscarded),
+    Hand = [[if ExtTash == null -> null;
+                true -> ext_to_tash(ExtTash)
+             end || ExtTash <- Row] || Row <- ExtHand],
+    do_game_action(SeatNum, {reveal, Discarded, Hand}, From, StateName, StateData);
+
+do_action(_SeatNum, #okey_reveal{}, _From, StateName, StateData) ->
+    {reply, {error, message_not_valid_for_a_current_state}, StateName, StateData};
+
+
+do_action(SeatNum, #okey_challenge{challenge = Challenge}, From,
+          ?STATE_REVEAL_CONFIRMATION = StateName, #state{reveal_confirmation_list = CList,
+                                                         wait_list = WL,
+                                                         timeout_timer = TRef} = StateData) ->
+    case lists:member(SeatNum, WL) of
+        true ->
+            Confirmed = not Challenge,
+            NewCList = [{SeatNum, Confirmed} | CList],
+            NewWL = lists:delete(SeatNum, WL),
+            if NewWL == [] ->
+                   gen_fsm:reply(From, ok),
+                   erlang:cancel_timer(TRef),
+                   finalize_round(StateData#state{timeout_timer = undefined,
+                                                  reveal_confirmation_list = NewCList});
+               true ->
+                   {reply, ok, StateName,
+                    StateData#state{reveal_confirmation_list = NewCList,
+                                    wait_list = NewWL}}
+            end;
+        false ->
+            {reply, {error, not_your_turn}, StateName, StateData}
+    end;
+
+do_action(_SeatNum, #okey_challenge{}, _From, StateName, StateData) ->
+    {reply, {error, message_not_valid_for_a_current_state}, StateName, StateData};
+
+
+do_action(_SeatNum, #okey_ready{}, _From, StateName, StateData) ->
+    {reply, ok, StateName, StateData};
+
+
+do_action(_SeatNum, _UnsupportedAction, _From, StateName, StateData) ->
+    {reply, {error, unsupported}, StateName, StateData}.
+
+
+%%===================================================================
+do_timeout_moves(#state{desk_rule_pid = Desk, desk_state = DeskState} = StateData) ->
+    #desk_state{cur_seat = CurSeatNum,
+                hands = Hands,
+                state = DeskStateName} = DeskState,
+    case DeskStateName of
+        state_take ->
+            {ok, Events1} = desk_player_action(Desk, CurSeatNum, take_from_table),
+            [Tash] = [Tash || {taked_from_table, S, Tash} <- Events1, S==CurSeatNum],
+            {ok, Events2} = desk_player_action(Desk, CurSeatNum, {discard, Tash}),
+            Events2_1 = [case E of
+                             {tash_discarded, SeatNum, Tash} ->
+                                 {tash_discarded_timeout, SeatNum, Tash};
+                             _ -> E
+                         end || E <- Events2],
+            Events = Events1 ++ [{auto_take_discard, CurSeatNum, Tash}] ++ Events2_1,
+            process_game_events(Events, StateData);
+        state_discard ->
+            {_, [Tash | _]} = lists:keyfind(CurSeatNum, 1, Hands),
+            {ok, Events1} = desk_player_action(Desk, CurSeatNum, {discard, Tash}),
+            Events1_1 = [case E of
+                             {tash_discarded, SeatNum, Tash} ->
+                                 {tash_discarded_timeout, SeatNum, Tash};
+                             _ -> E
+                         end || E <- Events1],
+            Events = [{auto_discard, CurSeatNum, Tash} | Events1_1],
+            process_game_events(Events, StateData)
+    end.
+
+%%===================================================================
+
+do_game_action(SeatNum, GameAction, From, StateName,
+               #state{desk_rule_pid = Desk} = StateData) ->
+    ?INFO("OKEY_NG_TABLE_TRN do_game_action SeatNum: ~p  GameAction: ~p", [SeatNum, GameAction]),
+    case desk_player_action(Desk, SeatNum, GameAction) of
+        {ok, Events} ->
+            Response = case GameAction of
+                           i_have_gosterge ->
+                               true;
+                           i_have_8_tashes ->
+                               true;
+                           take_from_table ->
+                               [Tash] = [Tash || {taked_from_table, S, Tash} <- Events, S==SeatNum],
+                               tash_to_ext(Tash);
+                           take_from_discarded ->
+                               [Tash] = [Tash || {taked_from_discarded, S, Tash} <- Events, S==SeatNum],
+                               tash_to_ext(Tash);
+                           _ -> ok
+                       end,
+            gen_fsm:reply(From, Response),
+            process_game_events(Events, StateData);
+        {error, Reason} ->
+            ExtError = desk_error_to_ext(Reason),
+            {reply, ExtError, StateName, StateData}
+    end.
+
+
+process_game_events(Events, #state{desk_state = DeskState, players = Players,
+                                   relay = Relay, timeout_timer = OldTRef,
+                                   round_timeout = RoundTimeout, round_timer = RoundTRef,
+                                   turn_timeout = TurnTimeout} = StateData) ->
+    NewDeskState = handle_desk_events(Events, DeskState, Players, Relay), %% Track the desk and send game events to clients
+    #desk_state{state = DeskStateName} = NewDeskState,
+    case DeskStateName of
+        state_finished ->
+            if is_integer(RoundTimeout) -> erlang:cancel_timer(RoundTRef); true -> do_nothing end,
+            erlang:cancel_timer(OldTRef),
+            on_game_finish(StateData#state{desk_state = NewDeskState});
+        state_take ->
+            case [E || {next_player, _} = E <- Events] of %% Find a next player event
+                [] ->
+                    {next_state, ?STATE_PLAYING, StateData#state{desk_state = NewDeskState}};
+                [_|_] ->
+                    erlang:cancel_timer(OldTRef),
+                    {Magic, TRef} = start_timer(TurnTimeout),
+                    {next_state, ?STATE_PLAYING, StateData#state{desk_state = NewDeskState,
+                                                                 timeout_timer = TRef,
+                                                                 timeout_magic = Magic}}
+            end;
+        state_discard ->
+            {next_state, ?STATE_PLAYING, StateData#state{desk_state = NewDeskState}}
+    end.
+
+
+on_game_finish(#state{desk_state = DeskState,
+                      reveal_confirmation = RevealConfirmation,
+                      reveal_confirmation_timeout = Timeout} = StateData) ->
+    #desk_state{finish_reason = FinishReason,
+                finish_info = FinishInfo,
+                hands = Hands} = DeskState,
+    if FinishReason == reveal andalso RevealConfirmation ->
+           {Revealer, _Tashes, _Discarded} = FinishInfo,
+           WL = [SeatNum || {SeatNum, _} <- Hands, SeatNum =/= Revealer],
+           {Magic, TRef} = start_timer(Timeout),
+           {next_state, ?STATE_REVEAL_CONFIRMATION,
+            StateData#state{reveal_confirmation_list = [],
+                            wait_list = WL,
+                            timeout_timer = TRef,
+                            timeout_magic = Magic}};
+       true ->
+            finalize_round(StateData)
+    end.
+
+%%===================================================================
+
+finalize_round(#state{desk_state = #desk_state{finish_reason = FinishReason,
+                                               finish_info = FinishInfo,
+                                               hands = Hands,
+                                               gosterge = Gosterge,
+                                               has_gosterge = WhoHasGosterge,
+                                               have_8_tashes = Have8Tashes},
+                      scoring_state = ScoringState,
+                      reveal_confirmation = RevealConfirmation,
+                      reveal_confirmation_list = CList,
+                      parent = Parent, players = Players,
+                      game_id = GameId, table_id = TableId} = StateData) ->
+    ?INFO("OKEY_NG_TABLE_TRN <~p,~p> Finalizing the round. Finish reason: ~p. Finish info: ~p.",
+          [GameId, TableId, FinishReason, FinishInfo]),
+    FR = case FinishReason of
+             tashes_out -> tashes_out;
+             timeout -> timeout;
+             set_timeout -> set_timeout;
+             reveal ->
+                 {Revealer, Tashes, Discarded} = FinishInfo,
+                 ConfirmationList = if RevealConfirmation -> CList; true -> [] end,
+                 CListUId = [{SeatNum, get_user_id_by_seat_num(SeatNum, Players), Response}
+                             || {SeatNum, Response} <- ConfirmationList],
+                 ?INFO("OKEY_NG_TABLE_TRN <~p,~p> Confirmation list: ~p.", [GameId, TableId, CListUId]),
+                 {reveal, Revealer, Tashes, Discarded, ConfirmationList};
+             gosterge_finish ->
+                 Winner = FinishInfo,
+                 {gosterge_finish, Winner}
+         end,
+    {NewScoringState, GameOver} = ?SCORING:round_finished(ScoringState, FR, Hands, Gosterge,
+                                                          WhoHasGosterge, Have8Tashes),
+    {_, RoundScore, _, TotalScore} = ?SCORING:last_round_result(NewScoringState),
+    RoundScorePl = [{get_player_id_by_seat_num(SeatNum, Players), Points} || {SeatNum, Points} <- RoundScore],
+    TotalScorePl = [{get_player_id_by_seat_num(SeatNum, Players), Points} || {SeatNum, Points} <- TotalScore],
+    if GameOver ->
+           ?INFO("OKEY_NG_TABLE_TRN <~p,~p> Set is over.", [GameId, TableId]),
+           parent_send_game_res(Parent, TableId, NewScoringState, RoundScorePl, TotalScorePl),
+           {next_state, ?STATE_SET_FINISHED, StateData#state{scoring_state = NewScoringState}};
+       true ->
+           ?INFO("OKEY_NG_TABLE_TRN <~p,~p> Round is over.", [GameId, TableId]),
+           parent_send_round_res(Parent, TableId, NewScoringState, RoundScorePl, TotalScorePl),
+           {next_state, ?STATE_FINISHED, StateData#state{scoring_state = NewScoringState}}
+    end.
+
+
+
+%% handle_desk_events(Events, DeskState, Players) -> NextStateData
+%% Tracks the desk state and sends events to clients
+handle_desk_events([], DeskState, _Players, _Relay) ->
+    DeskState;
+
+handle_desk_events([Event | Events], DeskState, Players, Relay) ->
+    #desk_state{cur_seat = CurSeatNum,
+                hands = Hands,
+                discarded = Discarded,
+                deck = Deck,
+                have_8_tashes = Have8Tashes} = DeskState,
+    NewDeskState =
+        case Event of
+            {has_gosterge, SeatNum} ->
+                Msg = create_okey_player_has_gosterge(SeatNum, Players),
+                relay_publish_ge(Relay, Msg),
+                DeskState#desk_state{has_gosterge = SeatNum};
+            {has_8_tashes, SeatNum, Value} ->
+                Msg = create_okey_player_has_8_tashes(SeatNum, Value, Players),
+                relay_publish_ge(Relay, Msg),
+                DeskState#desk_state{have_8_tashes = [SeatNum | Have8Tashes]};
+            {saw_okey, SeatNum} ->
+                Msg = create_okey_disable_okey(SeatNum, CurSeatNum, Players),
+                relay_publish_ge(Relay, Msg),
+                DeskState;
+            {taked_from_discarded, SeatNum, Tash} ->
+                PrevSeatNum = prev_seat_num(SeatNum),
+                {_, [Tash | NewPile]} = lists:keyfind(PrevSeatNum, 1, Discarded),
+                Msg = create_okey_tile_taken_discarded(SeatNum, Tash, length(NewPile), Players),
+                relay_publish_ge(Relay, Msg),
+                NewDiskarded = lists:keyreplace(PrevSeatNum, 1, Discarded, {PrevSeatNum, NewPile}),
+                {_, Hand} = lists:keyfind(SeatNum, 1, Hands),
+                NewHands = lists:keyreplace(SeatNum, 1, Hands, {SeatNum, [Tash | Hand]}),
+                DeskState#desk_state{hands = NewHands, discarded = NewDiskarded, state = state_discard};
+            {taked_from_table, SeatNum, Tash} ->
+                [Tash | NewDeck] = Deck,
+                Msg = create_okey_tile_taken_table(SeatNum, length(NewDeck), Players),
+                relay_publish_ge(Relay, Msg),
+                {_, Hand} = lists:keyfind(SeatNum, 1, Hands),
+                NewHands = lists:keyreplace(SeatNum, 1, Hands, {SeatNum, [Tash | Hand]}),
+                DeskState#desk_state{hands = NewHands, deck = NewDeck, state = state_discard};
+            {tash_discarded, SeatNum, Tash} ->
+                Msg = create_okey_tile_discarded(SeatNum, Tash, false, Players),
+                relay_publish_ge(Relay, Msg),
+                {_, Hand} = lists:keyfind(SeatNum, 1, Hands),
+                NewHands = lists:keyreplace(SeatNum, 1, Hands, {SeatNum, lists:delete(Tash, Hand)}),
+                {_, Pile} = lists:keyfind(SeatNum, 1, Discarded),
+                NewDiscarded = lists:keyreplace(SeatNum, 1, Discarded, {SeatNum, [Tash | Pile]}),
+                DeskState#desk_state{hands = NewHands, discarded = NewDiscarded, state = state_take};
+            {tash_discarded_timeout, SeatNum, Tash} -> %% Injected event
+                Msg = create_okey_tile_discarded(SeatNum, Tash, true, Players),
+                relay_publish_ge(Relay, Msg),
+                {_, Hand} = lists:keyfind(SeatNum, 1, Hands),
+                NewHands = lists:keyreplace(SeatNum, 1, Hands, {SeatNum, lists:delete(Tash, Hand)}),
+                {_, Pile} = lists:keyfind(SeatNum, 1, Discarded),
+                NewDiscarded = lists:keyreplace(SeatNum, 1, Discarded, {SeatNum, [Tash | Pile]}),
+                DeskState#desk_state{hands = NewHands, discarded = NewDiscarded, state = state_take};
+            {auto_take_discard, SeatNum, Tash} ->    %% Injected event
+                #player{id = PlayerId} = get_player_by_seat_num(SeatNum, Players),
+                Msg = create_okey_turn_timeout(Tash, Tash),
+                send_to_client_ge(Relay, PlayerId, Msg),
+                DeskState;
+            {auto_discard, SeatNum, Tash} ->         %% Injected event
+                #player{id = PlayerId} = get_player_by_seat_num(SeatNum, Players),
+                Msg = create_okey_turn_timeout(null, Tash),
+                send_to_client_ge(Relay, PlayerId, Msg),
+                DeskState;
+            {next_player, SeatNum} ->
+                Msg = create_okey_next_turn(SeatNum, Players),
+                relay_publish_ge(Relay, Msg),
+                DeskState#desk_state{cur_seat = SeatNum, state = state_take};
+            no_winner_finish ->
+                DeskState#desk_state{state = state_finished,
+                                     finish_reason = tashes_out};
+            {reveal, SeatNum, RevealedTashes, DiscardedTash} ->
+                Msg = create_okey_revealed(SeatNum, DiscardedTash, RevealedTashes, Players),
+                relay_publish_ge(Relay, Msg),
+                DeskState#desk_state{state = state_finished,
+                                     finish_reason = reveal,
+                                     finish_info = {SeatNum, RevealedTashes, DiscardedTash}};
+            {gosterge_finish, SeatNum} ->
+                DeskState#desk_state{state = state_finished,
+                                     finish_reason = gosterge_finish,
+                                     finish_info = SeatNum}
+        end,
+    handle_desk_events(Events, NewDeskState, Players, Relay).
+
+%%===================================================================
+init_scoring(GameType, PlayersInfo, Rounds) ->
+    SeatsInfo = [{SeatNum, Points} || {_PlayerId, _UserInfo, SeatNum, Points} <- PlayersInfo],
+    ?SCORING:init(GameType, SeatsInfo, Rounds).
+
+
+%% start_timer(Timeout) -> {Magic, TRef}
+start_timer(Timeout) ->
+    Magic = make_ref(),
+    TRef = erlang:send_after(Timeout, self(), {timeout, Magic}),
+    {Magic, TRef}.
+
+%% players_init() -> players()
+players_init() ->
+    midict:new().
+
+%% reg_player(PlayerId, SeatNum, UserId, IsBot, Players) -> NewPlayers
+reg_player(PlayerId, SeatNum, UserId, IsBot, UserInfo, Connected, Players) ->
+    store_player_rec(#player{id =PlayerId, seat_num = SeatNum, user_id = UserId,
+                             is_bot = IsBot, info = UserInfo, connected = Connected}, Players).
+
+%% reg_player(#player{}, Players) -> NewPlayers
+store_player_rec(#player{id =Id, seat_num = SeatNum, user_id = UserId,
+                         is_bot = IsBot, connected = Connected} = Player, Players) ->
+    Indices = [{seat_num, SeatNum}, {user_id, UserId}, {is_bot, IsBot}, {connected, Connected}],
+    midict:store(Id, Player, Indices, Players).
+
+%% get_player_id_by_seat_num(SeatNum, Players) -> PlayerId
+get_player_id_by_seat_num(SeatNum, Players) ->
+    [#player{id = PlayerId}] = midict:geti(SeatNum, seat_num, Players),
+    PlayerId.
+
+%% get_user_id_by_seat_num(SeatNum, Players) -> PlayerId
+get_user_id_by_seat_num(SeatNum, Players) ->
+    [#player{user_id = UserId}] = midict:geti(SeatNum, seat_num, Players),
+    UserId.
+
+%% fetch_player(PlayerId, Players) -> Player
+fetch_player(PlayerId, Players) ->
+    midict:fetch(PlayerId, Players).
+
+%% get_player(PlayerId, Players) -> {ok, Player} | error
+get_player(PlayerId, Players) ->
+    midict:find(PlayerId, Players).
+
+%% get_player_by_seat_num(SeatNum, Players) -> Player
+get_player_by_seat_num(SeatNum, Players) ->
+    [Player] = midict:geti(SeatNum, seat_num, Players),
+    Player.
+
+%% find_players_by_seat_num(SeatNum, Players) -> [Player]
+find_players_by_seat_num(SeatNum, Players) ->
+    midict:geti(SeatNum, seat_num, Players).
+
+%% find_connected_players(Players) -> [Player]
+find_connected_players(Players) ->
+    midict:geti(true, connected, Players).
+
+%% del_player(PlayerId, Players) -> NewPlayers
+del_player(PlayerId, Players) ->
+    midict:erase(PlayerId, Players).
+
+%% players_to_list(Players) -> List
+players_to_list(Players) ->
+    midict:all_values(Players).
+
+%% @spec init_players(PlayersInfo) -> Players
+%% @end
+%% PlayersInfo = [{PlayerId, UserInfo, SeatNum, StartPoints}]
+
+init_players(PlayersInfo) ->
+    init_players(PlayersInfo, players_init()).
+
+init_players([], Players) ->
+    Players;
+
+init_players([{PlayerId, UserInfo, SeatNum, _StartPoints} | PlayersInfo], Players) ->
+    #'PlayerInfo'{id = UserId, robot = IsBot} = UserInfo,
+    NewPlayers = reg_player(PlayerId, SeatNum, UserId, IsBot, UserInfo, _Connected = false, Players),
+    init_players(PlayersInfo, NewPlayers).
+
+%%===================================================================
+send_to_subscriber_ge(Relay, SubscrId, Msg) ->
+    Event = #game_event{event = api_utils:name(Msg), args = api_utils:members(Msg)},
+    ?RELAY:table_message(Relay, {to_subscriber, SubscrId, Event}).
+
+send_to_client_ge(Relay, PlayerId, Msg) ->
+    Event = #game_event{event = api_utils:name(Msg), args = api_utils:members(Msg)},
+    ?RELAY:table_message(Relay, {to_client, PlayerId, Event}).
+
+relay_publish_ge(Relay, Msg) ->
+    Event = #game_event{event = api_utils:name(Msg), args = api_utils:members(Msg)},
+    relay_publish(Relay, Event).
+
+relay_publish(Relay, Msg) ->
+    ?RELAY:table_message(Relay, {publish, Msg}).
+
+relay_allow_broadcast_for_player(Relay, PlayerId) ->
+    ?RELAY:table_message(Relay, {allow_broadcast_for_player, PlayerId}).
+
+relay_register_player(Relay, UserId, PlayerId) ->
+    ?RELAY:table_request(Relay, {register_player, UserId, PlayerId}).
+
+relay_unregister_player(Relay, PlayerId, Reason) ->
+    ?RELAY:table_request(Relay, {unregister_player, PlayerId, Reason}).
+
+relay_kick_player(Relay, PlayerId) ->
+    ?RELAY:table_request(Relay, {kick_player, PlayerId}).
+
+relay_stop(Relay) ->
+    ?RELAY:table_message(Relay, stop).
+
+parent_confirm_registration({ParentMod, ParentPid}, TableId, RequestId) ->
+    ParentMod:table_message(ParentPid, TableId, {response, RequestId, ok}).
+
+parent_confirm_replacement({ParentMod, ParentPid}, TableId, RequestId) ->
+    ParentMod:table_message(ParentPid, TableId, {response, RequestId, ok}).
+
+parent_notify_table_created({ParentMod, ParentPid}, TableId, RelayPid) ->
+    ParentMod:table_message(ParentPid, TableId, {table_created, {?RELAY, RelayPid}}).
+
+parent_send_round_res({ParentMod, ParentPid}, TableId, ScoringState, RoundScores, TotalScores) ->
+    ParentMod:table_message(ParentPid, TableId, {round_finished, ScoringState, RoundScores, TotalScores}).
+
+parent_send_game_res({ParentMod, ParentPid}, TableId, ScoringState, RoundScores, TotalScores) ->
+    ParentMod:table_message(ParentPid, TableId, {game_finished, ScoringState, RoundScores, TotalScores}).
+
+parent_send_player_connected({ParentMod, ParentPid}, TableId, PlayerId) ->
+    ParentMod:table_message(ParentPid, TableId, {player_connected, PlayerId}).
+
+parent_send_player_disconnected({ParentMod, ParentPid}, TableId, PlayerId) ->
+    ParentMod:table_message(ParentPid, TableId, {player_disconnected, PlayerId}).
+
+desk_player_action(Desk, SeatNum, Action) ->
+    ?DESK:player_action(Desk, SeatNum, Action).
+
+%%===================================================================
+
+create_okey_game_info(#state{table_name = TName, mult_factor = MulFactor,
+                             slang_flag = SlangFlag, observer_flag = ObserverFlag,
+                             speed = Speed, turn_timeout = TurnTimeout,
+                             reveal_confirmation_timeout = RevealConfirmationTimeout,
+                             ready_timeout = ReadyTimeout, game_mode = GameMode,
+                             rounds = Rounds1, players = Players, tour = Tour,
+                             tours = Tours, gosterge_finish_allowed = GostergeFinish,
+                             tournament_type = TournamentType, pause_mode = PauseMode,
+                             social_actions_enabled = SocialActionsEnabled,
+                             next_series_confirmation = ConfirmMode}) ->
+    PInfos = [case find_players_by_seat_num(SeatNum, Players) of
+                  [#player{info = UserInfo}] -> UserInfo;
+                  [] -> null
+              end || SeatNum <- lists:seq(1, ?SEATS_NUM)],
+    Timeouts = #'OkeyTimeouts'{speed = Speed,
+                               turn_timeout = TurnTimeout,
+                               challenge_timeout = RevealConfirmationTimeout,
+                               ready_timeout = ReadyTimeout,
+                               rematch_timeout = ?REMATCH_TIMEOUT},
+    Sets = if Tours == undefined -> null; true -> Tours end,
+    SetNo = if Tour == undefined -> null; true -> Tour end,
+    Rounds = if Rounds1 == infinity -> -1; true -> Rounds1 end,
+    #okey_game_info{table_name = list_to_binary(TName),
+                    players = PInfos,
+                    timeouts = Timeouts,
+                    game_type = GameMode,
+                    finish_with_gosterge = GostergeFinish,
+                    rounds = Rounds,
+                    sets = Sets,
+                    set_no = SetNo,
+                    mul_factor = MulFactor,
+                    slang_flag = SlangFlag,
+                    observer_flag = ObserverFlag,
+                    pause_enabled = PauseMode == normal,
+                    social_actions_enabled = SocialActionsEnabled,
+                    tournament_type = TournamentType,
+                    series_confirmation_mode = list_to_binary(atom_to_list(ConfirmMode))
+                   }.
+
+
+create_okey_game_player_state(_PlayerId, ?STATE_WAITING_FOR_START,
+                              #state{cur_round = CurRound, scoring_state = ScoringState,
+                                     set_timeout = SetTimeout1, set_timer = SetTRef}) ->
+    Chanak = ?SCORING:chanak(ScoringState),
+    SetTimeout = if SetTimeout1 == infinity -> null;
+                    true -> calc_timeout_comp(SetTRef, 2000)
+                 end,
+    #okey_game_player_state{whos_move = null,
+                            game_state = game_initializing,
+                            piles = null,
+                            tiles = null,
+                            gosterge = null,
+                            pile_height = null,
+                            current_round = CurRound,
+                            next_turn_in = 0,
+                            paused = false,
+                            chanak_points = Chanak,
+                            round_timeout = null,
+                            set_timeout = SetTimeout};
+
+create_okey_game_player_state(PlayerId, ?STATE_PLAYING,
+                              #state{timeout_timer = TRef, cur_round = CurRound,
+                                     players = Players, desk_state = DeskState,
+                                     scoring_state = ScoringState, round_timer = RoundTRef,
+                                     round_timeout = RoundTimeout1, set_timer = SetTRef,
+                                     set_timeout = SetTimeout1}) ->
+    #player{seat_num = SeatNum} = fetch_player(PlayerId, Players),
+    #desk_state{state = DeskStateName,
+                hands = Hands,
+                discarded = Discarded,
+                gosterge = Gosterge,
+                deck = DeskDeck,
+                cur_seat = CurSeatNum} = DeskState,
+    {_, PlayerHand} = lists:keyfind(SeatNum, 1, Hands),
+    Hand = [tash_to_ext(Tash) || Tash <- PlayerHand],
+    #player{user_id = CurUserId} = get_player_by_seat_num(CurSeatNum, Players),
+    Timeout = calc_timeout(TRef),
+    F = fun(_, N) ->
+                Pile = case lists:keyfind(N, 1, Discarded) of
+                           {_, []} -> null;
+                           {_, [Tash|_]} -> tash_to_ext(Tash)
+                       end,
+                {Pile, next_seat_num(N)}
+        end,
+    {Piles, _} = lists:mapfoldl(F, prev_seat_num(SeatNum), lists:seq(1, ?SEATS_NUM)),
+    GameState = statename_to_api_string(DeskStateName),
+    Chanak = ?SCORING:chanak(ScoringState),
+    RoundTimeout = if RoundTimeout1 == infinity -> null;
+                      true -> calc_timeout_comp(RoundTRef, 2000)
+                   end,
+    SetTimeout = if SetTimeout1 == infinity -> null;
+                    true -> calc_timeout_comp(SetTRef, 2000)
+                 end,
+    #okey_game_player_state{whos_move = CurUserId,
+                            game_state = GameState,
+                            piles = Piles,
+                            tiles = Hand,
+                            gosterge = tash_to_ext(Gosterge),
+                            pile_height = length(DeskDeck),
+                            current_round = CurRound,
+                            next_turn_in = Timeout,
+                            paused = false,
+                            chanak_points = Chanak,
+                            round_timeout = RoundTimeout,
+                            set_timeout = SetTimeout};
+
+create_okey_game_player_state(PlayerId, ?STATE_REVEAL_CONFIRMATION,
+                              #state{timeout_timer = TRef, cur_round = CurRound,
+                                     players = Players, desk_state = DeskState,
+                                     scoring_state = ScoringState,
+                                     set_timeout = SetTimeout1, set_timer = SetTRef}) ->
+    #player{seat_num = SeatNum} = fetch_player(PlayerId, Players),
+    #desk_state{hands = Hands,
+                discarded = Discarded,
+                gosterge = Gosterge,
+                deck = DeskDeck,
+                cur_seat = CurSeatNum} = DeskState,
+    {_, PlayerHand} = lists:keyfind(SeatNum, 1, Hands),
+    Hand = [tash_to_ext(Tash) || Tash <- PlayerHand],
+    #player{user_id = CurUserId} = get_player_by_seat_num(CurSeatNum, Players),
+    Timeout = calc_timeout(TRef),
+    F = fun(_, N) ->
+                Pile = case lists:keyfind(N, 1, Discarded) of
+                           {_, []} -> null;
+                           {_, [Tash|_]} -> tash_to_ext(Tash)
+                       end,
+                {Pile, next_seat_num(N)}
+        end,
+    {Piles, _} = lists:mapfoldl(F, prev_seat_num(SeatNum), lists:seq(1, ?SEATS_NUM)),
+    Chanak = ?SCORING:chanak(ScoringState),
+    SetTimeout = if SetTimeout1 == infinity -> null;
+                    true -> calc_timeout_comp(SetTRef, 2000)
+                 end,
+    #okey_game_player_state{whos_move = CurUserId,
+                            game_state = do_okey_challenge,
+                            piles = Piles,
+                            tiles = Hand,
+                            gosterge = tash_to_ext(Gosterge),
+                            pile_height = length(DeskDeck),
+                            current_round = CurRound,
+                            next_turn_in = Timeout,
+                            paused = false,
+                            chanak_points = Chanak,
+                            round_timeout = null,
+                            set_timeout = SetTimeout};
+
+create_okey_game_player_state(_PlayerId, ?STATE_FINISHED,
+                              #state{cur_round = CurRound, scoring_state = ScoringState,
+                                     set_timeout = SetTimeout1, set_timer = SetTRef}) ->
+    Chanak = ?SCORING:chanak(ScoringState),
+    SetTimeout = if SetTimeout1 == infinity -> null;
+                    true -> calc_timeout_comp(SetTRef, 2000)
+                 end,
+    #okey_game_player_state{whos_move = null,
+                            game_state = game_initializing,
+                            piles = null,
+                            tiles = null,
+                            gosterge = null,
+                            pile_height = null,
+                            current_round = CurRound,
+                            next_turn_in = 0,
+                            paused = false,
+                            chanak_points = Chanak,
+                            round_timeout = null,
+                            set_timeout = SetTimeout};
+
+create_okey_game_player_state(PlayerId, ?STATE_PAUSE,
+                              #state{paused_statename = PausedStateName,
+                                     paused_timeout_value = Timeout
+                                     } = StateData) ->
+    Msg = create_okey_game_player_state(PlayerId, PausedStateName, StateData),
+    Msg#okey_game_player_state{next_turn_in = Timeout,
+                               paused = true}.
+
+
+create_okey_game_started(SeatNum, DeskState, CurRound,
+                         #state{scoring_state = ScoringState, round_timeout = RoundTimeout1,
+                                set_timeout = SetTimeout1, set_timer = SetTRef}) ->
+    Chanak = ?SCORING:chanak(ScoringState),
+    #desk_state{hands = Hands,
+                gosterge = Gosterge,
+                deck = DeskDeck} = DeskState,
+    {_, PlayerHand} = lists:keyfind(SeatNum, 1, Hands),
+    Hand = [tash_to_ext(Tash) || Tash <- PlayerHand],
+    RoundTimeout = if RoundTimeout1 == infinity -> null;
+                      true -> RoundTimeout1 - 2000
+                   end,
+    SetTimeout = if SetTimeout1 == infinity -> null;
+                    true -> calc_timeout_comp(SetTRef, 2000)
+                 end,
+    #okey_game_started{tiles = Hand,
+                       gosterge = tash_to_ext(Gosterge),
+                       pile_height = length(DeskDeck),
+                       current_round = CurRound,
+                       current_set = 1,        %% XXX Concept of sets is deprecated
+                       chanak_points = Chanak,
+                       round_timeout = RoundTimeout,
+                       set_timeout = SetTimeout}.
+
+
+create_okey_next_turn(CurSeat, Players) ->
+    #player{user_id = UserId} = get_player_by_seat_num(CurSeat, Players),
+    #okey_next_turn{player = UserId}.
+
+
+create_player_left(SeatNum, UserInfo, Players) ->
+    #player{user_id = OldUserId} = get_player_by_seat_num(SeatNum, Players),
+    IsBot = UserInfo#'PlayerInfo'.robot,
+    #player_left{player = OldUserId,
+                 human_replaced = not IsBot, %% XXX WTF?
+                 bot_replaced = IsBot,       %% XXX WTF?
+                 replacement = UserInfo}.
+
+
+create_okey_player_has_gosterge(SeatNum, Players) ->
+    #player{user_id = UserId} = get_player_by_seat_num(SeatNum, Players),
+    #okey_player_has_gosterge{player = UserId}.
+
+create_okey_player_has_8_tashes(SeatNum, Value, Players) ->
+    #player{user_id = UserId} = get_player_by_seat_num(SeatNum, Players),
+    #okey_player_has_8_tashes{player = UserId,
+                              value = Value}.
+
+create_okey_disable_okey(SeatNum, CurSeatNum, Players) ->
+    #player{user_id = Who} = get_player_by_seat_num(SeatNum, Players),
+    #player{user_id = Whom} = get_player_by_seat_num(CurSeatNum, Players),
+    #okey_disable_okey{player = Whom,
+                       who_disabled = Who}.
+
+
+create_okey_tile_taken_discarded(SeatNum, Tash, PileHeight, Players) ->
+    #player{user_id = UserId} = get_player_by_seat_num(SeatNum, Players),
+    #okey_tile_taken{player = UserId,
+                     pile = 1, %% From discarded tashes of the previous player
+                     revealed = tash_to_ext(Tash),
+                     pile_height = PileHeight}.
+
+
+create_okey_tile_taken_table(SeatNum, PileHeight, Players) ->
+    #player{user_id = UserId} = get_player_by_seat_num(SeatNum, Players),
+    #okey_tile_taken{player = UserId,
+                     pile = 0, %% From the deck on the table
+                     revealed = null,
+                     pile_height = PileHeight}.
+
+
+create_okey_tile_discarded(SeatNum, Tash, Timeouted, Players) ->
+    #player{user_id = UserId} = get_player_by_seat_num(SeatNum, Players),
+    #okey_tile_discarded{player = UserId,
+                         tile = tash_to_ext(Tash),
+                         timeouted = Timeouted}.
+
+create_okey_round_ended_reveal(Revealer, RevealerWin, WrongRejects, RoundScore, TotalScore, PlayersAchsPoints,
+                        #state{players = Players}) ->
+    PlResults = [begin
+                     #player{user_id = UserId} = get_player_by_seat_num(SeatNum, Players),
+                     WinnerStatus = if SeatNum == Revealer -> RevealerWin;
+                                       true -> not RevealerWin
+                                    end,
+                     GoodShot = if SeatNum == Revealer -> RevealerWin;
+                                   true -> not lists:member(SeatNum, WrongRejects)
+                                end,
+                     {_, Score} = lists:keyfind(SeatNum, 1, TotalScore),
+                     {_, ScoreDelta} = lists:keyfind(SeatNum, 1, RoundScore),
+                     Breakdown = [], %% XXX
+                     #'OkeyGameR'{player_id = UserId,
+                                  disconnected = false,
+                                  winner = WinnerStatus,
+                                  good_shot = GoodShot,
+                                  skill = 0,
+                                  skill_delta = 0,
+                                  score = Score,
+                                  score_delta = ScoreDelta,
+                                  breakdown = Breakdown}
+                 end || SeatNum <- lists:seq(1, ?SEATS_NUM)],
+    Results = #'OkeyGameResults'{game_id = 0,
+                                 start_datetime = 0,
+                                 end_datetime = 0,
+                                 results = PlResults,
+                                 series_results = []},
+    #okey_round_ended{good_shot = RevealerWin,
+                      reason = <<"reveal">>,
+                      results = Results,
+                      next_action = next_round}. %%XXX
+
+create_okey_round_ended_tashes_out(RoundScore, TotalScore, PlayersAchsPoints,
+                                   #state{players = Players}) ->
+    PlResults = [begin
+                     #player{user_id = UserId} = get_player_by_seat_num(SeatNum, Players),
+                     {_, Score} = lists:keyfind(SeatNum, 1, TotalScore),
+                     {_, ScoreDelta} = lists:keyfind(SeatNum, 1, RoundScore),
+                     Breakdown = [], %% XXX
+                     #'OkeyGameR'{player_id = UserId,
+                                  disconnected = false,
+                                  winner = none,
+                                  good_shot = false,
+                                  skill = 0,
+                                  skill_delta = 0,
+                                  score = Score,
+                                  score_delta = ScoreDelta,
+                                  breakdown = Breakdown}
+                 end || SeatNum <- lists:seq(1, ?SEATS_NUM)],
+    Results = #'OkeyGameResults'{game_id = 0,
+                                 start_datetime = 0,
+                                 end_datetime = 0,
+                                 results = PlResults,
+                                 series_results = []},
+    #okey_round_ended{good_shot = false,
+                      reason = <<"draw">>,
+                      results = Results,
+                      next_action = next_round}. %%XXX
+
+create_okey_round_ended_gosterge_finish(Winner, RoundScore, TotalScore, PlayersAchsPoints,
+                                        #state{players = Players}) ->
+    PlResults = [begin
+                     #player{user_id = UserId} = get_player_by_seat_num(SeatNum, Players),
+                     WinnerStatus = SeatNum == Winner,
+                     {_, Score} = lists:keyfind(SeatNum, 1, TotalScore),
+                     {_, ScoreDelta} = lists:keyfind(SeatNum, 1, RoundScore),
+                     Breakdown = [], %% XXX
+                     #'OkeyGameR'{player_id = UserId,
+                                  disconnected = false,
+                                  winner = WinnerStatus,
+                                  good_shot = WinnerStatus,
+                                  skill = 0,
+                                  skill_delta = 0,
+                                  score = Score,
+                                  score_delta = ScoreDelta,
+                                  breakdown = Breakdown}
+                 end || SeatNum <- lists:seq(1, ?SEATS_NUM)],
+    Results = #'OkeyGameResults'{game_id = 0,
+                                 start_datetime = 0,
+                                 end_datetime = 0,
+                                 results = PlResults,
+                                 series_results = []},
+    #okey_round_ended{good_shot = false,
+                      reason = <<"gosterge_instant">>,
+                      results = Results,
+                      next_action = next_round}. %%XXX
+
+create_okey_series_ended(Results, Players, Confirm) ->
+    Standings = [begin
+                     #player{user_id = UserId} = fetch_player(PlayerId, Players),
+                     Winner = case Status of    %% TODO: Implement in the client support of all statuses
+                                  winner -> <<"true">>;
+                                  _ -> <<"none">>
+                              end,
+                     #'OkeySeriesResult'{player_id = UserId, place = Position, score = Score,
+                                         winner = Winner}
+                 end || {PlayerId, Position, Score, Status} <- Results],
+    DialogType = if Confirm -> yes_no;
+                    true -> ok
+                 end,
+    #okey_series_ended{standings = Standings,
+                       dialog_type = DialogType}.
+
+create_okey_tour_result(TurnNum, Results) ->
+    Records = [begin
+                   #okey_turn_record{player_id = UserId, place = Position, score = Score,
+                                     status = Status}
+               end || {UserId, Position, Score, Status} <- Results],
+    #okey_turn_result{turn_num = TurnNum,
+                      records = Records}.
+
+create_okey_revealed(SeatNum, DiscardedTash, TashPlaces, Players) ->
+    #player{user_id = UserId} = get_player_by_seat_num(SeatNum, Players),
+    TashPlacesExt = [[case T of
+                         null -> null;
+                         _ -> tash_to_ext(T)
+                      end || T <- Row ] || Row <- TashPlaces],
+    #okey_revealed{player = UserId,              %% FIXME: We need reveal message without confirmation
+                   discarded = tash_to_ext(DiscardedTash),
+                   hand = TashPlacesExt}.
+
+
+create_okey_turn_timeout(null, TashDiscarded) ->
+    #okey_turn_timeout{tile_taken = null,
+                       tile_discarded = tash_to_ext(TashDiscarded)};
+create_okey_turn_timeout(TashTaken, TashDiscarded) ->
+    #okey_turn_timeout{tile_taken = tash_to_ext(TashTaken),
+                       tile_discarded = tash_to_ext(TashDiscarded)}.
+
+create_game_paused_pause(UserId, GameId) ->
+    #game_paused{game = GameId,
+                 who = UserId,
+                 action = <<"pause">>,
+                 retries = 0}.
+
+create_game_paused_resume(UserId, GameId) ->
+    #game_paused{game = GameId,
+                 who = UserId,
+                 action = <<"resume">>,
+                 retries = 0}.
+
+create_okey_playing_tables(Num) ->
+    #okey_playing_tables{num = Num}.
+
+tash_to_ext(false_okey) -> #'OkeyPiece'{color = 1, value = 0};
+tash_to_ext({Color, Value}) ->  #'OkeyPiece'{color = Color, value = Value}.
+
+ext_to_tash(#'OkeyPiece'{color = 1, value = 0}) -> false_okey;
+ext_to_tash(#'OkeyPiece'{color = Color, value = Value}) -> {Color, Value}.
+
+%statename_to_api_string(state_wait) -> do_okey_ready;
+statename_to_api_string(state_take) ->      do_okey_take;
+statename_to_api_string(state_discard) ->   do_okey_discard;
+statename_to_api_string(state_finished) ->  game_finished.
+
+desk_error_to_ext(action_disabled) -> false;
+desk_error_to_ext(no_gosterge) -> false;
+desk_error_to_ext(no_8_tashes) -> false;
+desk_error_to_ext(no_okey_discarded) -> {error, there_is_no_okey_there};
+desk_error_to_ext(not_your_order) -> {error, not_your_turn};
+desk_error_to_ext(blocked) -> {error, okey_is_blocked};
+desk_error_to_ext(no_tash) -> {error, no_tash};
+desk_error_to_ext(no_such_tash) -> {error, no_such_tash};
+desk_error_to_ext(hand_not_match) -> {error, discarded_hand_does_not_match_server_state};
+desk_error_to_ext(E) -> {error, E}.
+
+%%===================================================================
+
+get_timeout(turn, fast) -> {ok, Val}   = nsm_db:get(config,"games/okey/turn_timeout_fast", 15000), Val;
+get_timeout(turn, normal) -> {ok, Val} = nsm_db:get(config,"games/okey/turn_timeout_normal", 30000), Val;
+get_timeout(turn, slow) -> {ok, Val}   = nsm_db:get(config,"games/okey/turn_timeout_slow", 60000), Val;
+
+get_timeout(challenge, fast) ->  {ok, Val}   = nsm_db:get(config,"games/okey/challenge_timeout_fast", 5000), Val;
+get_timeout(challenge, normal) ->  {ok, Val} = nsm_db:get(config,"games/okey/challenge_timeout_normal", 10000), Val;
+get_timeout(challenge, slow) -> {ok, Val}    = nsm_db:get(config,"games/okey/challenge_timeout_slow", 20000), Val;
+
+get_timeout(ready, fast) -> {ok, Val}   = nsm_db:get(config,"games/okey/ready_timeout_fast", 15000), Val;
+get_timeout(ready, normal) -> {ok, Val} = nsm_db:get(config,"games/okey/ready_timeout_normal", 25000), Val;
+get_timeout(ready, slow) -> {ok, Val}   = nsm_db:get(config,"games/okey/ready_timeout_slow", 45000), Val.
+
+%%===================================================================
+
+calc_timeout(undefined) -> 0;
+calc_timeout(TRef) ->
+    case erlang:read_timer(TRef) of
+        false -> 0;
+        Timeout -> Timeout
+    end.
+
+calc_timeout_comp(TRef, Compensation) ->
+    if TRef == undefined -> null;
+       true -> case erlang:read_timer(TRef) of
+                   false -> 0;
+                   T when T < Compensation -> 0; %% Latency time compensation
+                   T -> T - Compensation
+               end
+    end.
+
+%%===================================================================
+
+next_seat_num(?SEATS_NUM) -> 1;
+next_seat_num(N) -> N + 1.
+
+prev_seat_num(1) -> ?SEATS_NUM;
+prev_seat_num(N) -> N - 1.
+
+%%===================================================================
+init_desk_state(Desk) ->
+    SeatsNums = ?DESK:get_seats_nums(Desk),
+    Hands = [{SeatNum, ?DESK:get_hand(Desk, SeatNum)} || SeatNum <- SeatsNums],
+    Discarded = [{SeatNum, ?DESK:get_discarded(Desk, SeatNum)} || SeatNum <- SeatsNums],
+    #desk_state{state = ?DESK:get_state_name(Desk),
+                hands = Hands,
+                discarded = Discarded,
+                deck = ?DESK:get_deck(Desk),
+                cur_seat = ?DESK:get_cur_seat(Desk),
+                gosterge = ?DESK:get_gosterge(Desk),
+                have_8_tashes = ?DESK:get_have_8_tashes(Desk),
+                has_gosterge = ?DESK:get_has_gosterge(Desk)}.
+
+%%===================================================================
+
+choose_gosterge(Deck) ->
+    Pos = crypto:rand_uniform(1, deck:size(Deck)),
+    case deck:get(Pos, Deck) of
+        {false_okey, _} -> choose_gosterge(Deck);
+        {Gosterge, Deck1} -> {Gosterge, Deck1}
+    end.

+ 1145 - 0
apps/server/src/okey/game_okey_scoring.erl

@@ -0,0 +1,1145 @@
+%%----------------------------------------------------------------------
+%% @author Paul Peregud <paulperegud@gmail.com>
+%% @copyright Paynet Internet ve Bilisim Hizmetleri A.S. All Rights Reserved.
+%% @doc
+%% Scoring for game_okey. Implemented in different thread to isolate complexity.
+%% @end
+%%-------------------------------------------------------------------
+-module(game_okey_scoring).
+-behaviour(gen_server).
+
+-export([start_link/1]).
+-export([standings/1]).
+-export([add_event/3]).
+-export([get_chanak_points/1, show_gosterge/4, stats/2, finish_round/4, finish_round/7]).
+-export([replace_uid/3, reset_scoring/1, new_chanak/1, set_chanak_points/2]).
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+         terminate/2, code_change/3]).
+
+-include_lib("server/include/log.hrl").
+-include_lib("server/include/requests.hrl").
+-include_lib("server/include/game_okey.hrl").
+-include_lib("server/include/basic_types.hrl").
+-include_lib("db/include/config.hrl").
+-include_lib("db/include/accounts.hrl").
+-include_lib("eunit/include/eunit.hrl").
+-include_lib("server/include/game_tavla.hrl").
+
+
+
+-record(okey_stats_entry, {
+          uid   :: integer(),
+          % move_no   :: integer(),
+          reason    :: atom()
+         }).
+-record(state, {
+          %% table data
+          game_pid    :: pid(),
+          relay_pid   :: pid(),
+          mode        :: atom(), %% standard | oddeven | color | countdown
+          %% set statistics
+          game_no = 0               :: integer(),
+          chanak = 0                :: integer(),
+          set = []                  :: list(tuple('PlayerId()', integer())),
+          round_max                 :: integer(),
+          round_cur                 :: integer(),
+          set_max                   :: integer(),
+          set_cur                   :: integer(),
+          history = []              :: list(#'OkeyGameResults'{}),
+
+          %% round statistics
+          stats = []                :: list(list(#okey_stats_entry{})),
+          gosterge_shown = false    :: boolean(),
+          gosterge_finish = false   :: boolean(),
+
+          game_info = []            :: [{atom(), any()}]
+         }).
+
+%%%===================================================================
+%%% API
+%%%===================================================================
+
+%%--------------------------------------------------------------------
+%% @doc
+%% Starts the server
+%%
+%% @spec start_link() -> {ok, Pid} | ignore | {error, Error}
+%% @end
+%%--------------------------------------------------------------------
+start_link(Params) ->
+    gen_server:start_link(?MODULE, [Params], []).
+
+add_event(Srv, UId, Reason) ->
+    gen_server:cast(Srv, {add_event, UId, Reason}).
+
+reset_scoring(Srv) ->
+    gen_server:cast(Srv, reset_scoring).
+
+replace_uid(Srv, UId1, UId2) ->
+    gen_server:cast(Srv, {replace_uid, UId1, UId2}).
+
+show_gosterge(Srv, Uid, Gosterge, Hand) ->
+    gen_server:call(Srv, {show_gosterge, Uid, Gosterge, Hand}).
+-spec finish_round(pid(), #'OkeyGameResults'{}, [{'PlayerId'(),[#'OkeyPiece'{}|null]}], #'OkeyPiece'{}) -> #'OkeyGameResults'{}.
+finish_round(Srv, Results, Hands, Gosterge) ->
+    finish_round(Srv, Results, Hands, Gosterge, undefined, undefined, undefined).
+-spec finish_round(pid(), #'OkeyGameResults'{}, [{'PlayerId'(),[#'OkeyPiece'{}|null]}], #'OkeyPiece'{},
+                  'PlayerId'()|undefined, #'OkeyPiece'{}|undefined, [#'OkeyPiece'{}|null]|undefined) -> #'OkeyGameResults'{}.
+finish_round(Srv, Results, Hands, Gosterge, Winner, LastRevealTash, RevealHand) ->
+    gen_server:call(Srv, {finish_round, Results, Hands, Gosterge, Winner, LastRevealTash, RevealHand}).
+
+get_chanak_points(Srv) ->
+    gen_server:call(Srv, get_chanak_points).
+
+set_chanak_points(Srv, Points) ->
+    gen_server:call(Srv, {set_chanak_points, Points}).
+
+standings(#'OkeyGameResults'{} = R) ->
+    standings0(R).
+
+stats(Srv, Uid) ->
+    gen_server:call(Srv, {stats, Uid}).
+
+%%%===================================================================
+%%% gen_server callbacks
+%%%===================================================================
+
+%%--------------------------------------------------------------------
+%% @private
+%% @doc
+%% Initiates the server
+%%
+%% @spec init(Args) -> {ok, State} |
+%%                     {ok, State, Timeout} |
+%%                     ignore |
+%%                     {stop, Reason}
+%% @end
+%%--------------------------------------------------------------------
+init([Params]) ->
+    ?INFO("Scoring Params: ~p", [Params]),
+    Mode = proplists:get_value(game_mode, Params),
+    {is_mode, true} = {is_mode, is_mode(Mode)},
+    Sets = proplists:get_value(sets, Params),
+    Rounds = proplists:get_value(rounds, Params),
+    GF = proplists:get_value(gosterge_finish, Params),
+    %% game info filled in game_okey:init/1
+    GameInfo = proplists:get_value(game_info, Params),
+    game_stats:charge_quota(GameInfo),
+    {ok, #state{mode = Mode, gosterge_finish = GF, set_max = Sets,
+                set_cur = 1, round_max = Rounds, round_cur = 1,
+                game_info = GameInfo}}.
+
+%%--------------------------------------------------------------------
+%% @private
+%% @doc
+%% Handling call messages
+%%
+%% @spec handle_call(Request, From, State) ->
+%%                                   {reply, Reply, State} |
+%%                                   {reply, Reply, State, Timeout} |
+%%                                   {noreply, State} |
+%%                                   {noreply, State, Timeout} |
+%%                                   {stop, Reason, Reply, State} |
+%%                                   {stop, Reason, State}
+%% @end
+%%--------------------------------------------------------------------
+handle_call({show_gosterge, UId, Gosterge, Hand}, _From, #state{gosterge_shown = false} = State) ->
+    #state{stats = Stats, mode = Mode, gosterge_finish = GF} = State,
+    case {lists:member(Gosterge, Hand), Mode, GF} of
+        {true, countdown, true} ->
+            Stats2 = [#okey_stats_entry{uid = UId,
+                                        reason = gosterge} | Stats],
+            {[gosterge], GP} = score_single([gosterge], achievements(countdown, with_scores)),
+            Best = get_last_score(UId, State) + GP,
+            case Best of
+                X when X >= 10 ->
+                    Res0 = hd(State#state.history),
+                    {RoundRes, GameRes} = countdown_points_reset0(UId, Res0),
+                    {reply, {true, RoundRes, GameRes},
+                     State#state{gosterge_shown = true, stats = Stats2}};
+                _ ->
+                    {reply, true, State#state{gosterge_shown = true, stats = Stats2}}
+            end;
+        {true, _, _} ->
+            Stats2 = [#okey_stats_entry{uid = UId,
+                                        reason = gosterge} | Stats],
+            {reply, true, State#state{gosterge_shown = true, stats = Stats2}};
+        {false, _, _} ->
+            {reply, false, State}
+    end;
+handle_call({show_gosterge, _Uid, _Gosterge, _Hand}, _From, #state{gosterge_shown = true} = State) ->
+    {reply, false, State};
+
+handle_call({finish_round, _, _, _, _, _, _} = Msg, From, State = #state{set = []}) ->
+    Value = State#state.round_cur,
+    Stats = State#state.stats,
+    Counters = [ {UId, Value} || #okey_stats_entry{uid = UId, reason = Reason} <- Stats, Reason == started ],
+    true = (4 =:= length(Counters)),
+    State1 = State#state{set = Counters},
+    handle_call(Msg, From, State1);
+handle_call({finish_round, #'OkeyGameResults'{} = Res0, Hands, Gosterge, undefined, undefined, undefined}, _From, State0) ->
+    #state{mode = Mode0, stats = Stats0} = State0,
+    Mode = game_okey:get_scoring_mode(Mode0, Gosterge),
+    ?INFO("scoring mode: ~p", [Mode]),
+    {State1, Stats1} = refill_chanak(Mode, State0, Stats0),
+    ?INFO("chanak value: ~p", [State1#state.chanak]),
+    Stats = check_hands_for_8_tashes(Hands, Stats1),
+    common_round_finish(Stats, Res0, Mode, State1);
+handle_call({finish_round, #'OkeyGameResults'{} = Res0, Hands, Gosterge, Winners, LastRevealTash, RevealHand}, _From, State) ->
+    #state{mode = Mode0, stats = Stats0} = State,
+    Mode = game_okey:get_scoring_mode(Mode0, Gosterge),
+    ?INFO("scoring mode: ~p", [Mode]),
+    {State1, Stats1} = refill_chanak(Mode, State, Stats0),
+    ?INFO("chanak value: ~p", [State1#state.chanak]),
+    Stats = check_hands_for_8_tashes(Hands, Stats1),
+    [ ?INFO("Player ~p ended with ~p", [binary_to_list(Player),Ending]) || {_,Player,Ending}<-Stats],
+    Okey = game_okey:get_okey(Gosterge),
+    Stats2 = case Winners of
+                 [Winner] ->
+                     A = if_list(with_okey(Okey, LastRevealTash),
+                                 #okey_stats_entry{uid = Winner#okey_player.player_id, reason = okey}),
+                     B = if_list(with_even_tashes(Gosterge, RevealHand),
+                                 #okey_stats_entry{uid = Winner#okey_player.player_id, reason = even}),
+                     C = if_list(with_color(Gosterge, RevealHand),
+                                 #okey_stats_entry{uid = Winner#okey_player.player_id, reason = color}),
+                     ?INFO("A: ~p, B: ~p, C: ~p", [A, B, C]),
+                     lists:flatten([A, B, C| Stats]);
+                 _ ->
+                     Stats
+             end,
+    common_round_finish(Stats2, Res0, Mode, State1);
+handle_call(get_chanak_points, _From, State) ->
+    {reply, State#state.chanak, State};
+handle_call({set_chanak_points, Points}, _From, State) ->
+    {reply, ok, State#state{chanak = Points}};
+handle_call(_Request, _From, State) ->
+    {reply, {error, {unknown_call, _Request}}, State}.
+
+
+
+%%--------------------------------------------------------------------
+%% @private
+%% @doc
+%% Handling cast messages
+%%
+%% @spec handle_cast(Msg, State) -> {noreply, State} |
+%%                                  {noreply, State, Timeout} |
+%%                                  {stop, Reason, State}
+%% @end
+%%--------------------------------------------------------------------
+handle_cast({add_event, UId, Reason}, State) ->
+    #state{stats = Stats} = State,
+    Stats2 = [#okey_stats_entry{uid = UId, reason = Reason} | Stats],
+    {noreply, State#state{stats = Stats2}};
+
+handle_cast({replace_uid, UId1, UId2}, State) ->
+    #state{stats = Stats, history = History} = State,
+    Stats2 = lists:map(fun
+                           (#okey_stats_entry{uid = UId} = X) when UId == UId1 ->
+                               X#okey_stats_entry{uid = UId2};
+                           (X) ->
+                               X
+                       end, Stats),
+    Set2 = lists:map(fun
+                          ({UId, Val}) when UId == UId1 ->
+                              {UId2, Val};
+                          (X) ->
+                              X
+                      end, Stats),
+    F2 = fun
+             (#'OkeyGameR'{player_id = X} = R) when X == UId1 ->
+                 R#'OkeyGameR'{player_id = UId2};
+             (R) ->
+                 R
+         end,
+    F1 = fun(#'OkeyGameResults'{results = RRs} = R) ->
+                 RRs2 = lists:map(F2, RRs),
+                 R#'OkeyGameResults'{results = RRs2}
+         end,
+    History2 = lists:map(F1, History),
+    {noreply, State#state{stats = Stats2, set = Set2, history = History2}};
+
+handle_cast(update_to_next_round_or_set, State) ->
+    {noreply, update_to_next_round_or_set(State)};
+
+handle_cast(reset_scoring, State) ->
+    {noreply, reset_scoring0(State)};
+
+handle_cast(_Msg, State) ->
+    {noreply, State}.
+
+%%--------------------------------------------------------------------
+%% @private
+%% @doc
+%% Handling all non call/cast messages
+%%
+%% @spec handle_info(Info, State) -> {noreply, State} |
+%%                                   {noreply, State, Timeout} |
+%%                                   {stop, Reason, State}
+%% @end
+%%--------------------------------------------------------------------
+% handle_info(die, State) ->
+%     {stop, normal, State};
+handle_info(Info, State) ->
+    ?INFO("unrecognized info: ~p", [Info]),
+    {stop, {error, unrecognized_info}, State}.
+
+%%--------------------------------------------------------------------
+%% @private
+%% @doc
+%% This function is called by a gen_server when it is about to
+%% terminate. It should be the opposite of Module:init/1 and do any
+%% necessary cleaning up. When it returns, the gen_server terminates
+%% with Reason. The return value is ignored.
+%%
+%% @spec terminate(Reason, State) -> void()
+%% @end
+%%--------------------------------------------------------------------
+terminate(_Reason, _State) ->
+    ok.
+
+%%--------------------------------------------------------------------
+%% @private
+%% @doc
+%% Convert process state when code is changed
+%%
+%% @spec code_change(OldVsn, State, Extra) -> {ok, NewState}
+%% @end
+%%--------------------------------------------------------------------
+code_change(_OldVsn, State, _Extra) ->
+    {ok, State}.
+
+%%%===================================================================
+%%% Internal functions
+%%%===================================================================
+
+get_last_score(UId, State) ->
+    case State#state.history of
+        [] ->
+            0;
+        [H | _] ->
+            RRs = H#'OkeyGameResults'.results,
+            R = lists:keyfind(UId, #'OkeyGameR'.player_id, RRs),
+            R#'OkeyGameR'.score
+    end.
+
+gosterge_finish_round_result(UId, RoundScore, #'OkeyGameResults'{results = RRs0} = Res0) ->
+    RRs1 = lists:map(fun(X) ->
+                             X#'OkeyGameR'{
+                               score_delta = 0,
+                               breakdown = [],
+                               skill_delta = 0
+                              }
+                     end, RRs0),
+    {value, R, RRs2} = lists:keytake(UId, #'OkeyGameR'.player_id, RRs1),
+    R1 = R#'OkeyGameR'{
+           winner = true,
+           good_shot = true,
+           skill_delta = 1,
+           score_delta = RoundScore,
+           breakdown = [#'OkeyScoringDetail'{reason = [gosterge],
+                                             score = RoundScore}]
+          },
+    Res0#'OkeyGameResults'{results = [R1 | RRs2]}.
+
+gosterge_finish_game_result(UId, GameScore, Res0) ->
+    #'OkeyGameResults'{results = RRs1} = strip_gamepoints(Res0),
+    {value, R, RRs2} = lists:keytake(UId, #'OkeyGameR'.player_id, RRs1),
+    R1 = R#'OkeyGameR'{
+           winner = true,
+           good_shot = true,
+           skill_delta = 1,
+           score_delta = GameScore,
+           score = GameScore,
+           breakdown = [#'OkeyScoringDetail'{reason = [gosterge_winner],
+                                             score = GameScore}]
+          },
+    Res0#'OkeyGameResults'{results = [R1 | RRs2]}.
+
+positions_arrange(Rs0) ->
+    Rs = lists:reverse(lists:keysort(#'OkeyGameR'.score, Rs0)),
+    [#'OkeyGameR'{score = MaxScore} | _] = Rs,
+    positions_arrange(1, MaxScore, Rs).
+
+positions_arrange(_, _, []) ->
+    [];
+positions_arrange(P, Score, [R | Rest]) ->
+    case R of
+        #'OkeyGameR'{score = Score} ->
+            [{P, R} | positions_arrange(P, Score, Rest) ];
+        #'OkeyGameR'{score = LowerScore} ->
+            [{P+1, R} | positions_arrange(P+1, LowerScore, Rest) ]
+    end.
+
+standings0(#'OkeyGameResults'{results = Rs0}) ->
+    Rs = positions_arrange(Rs0),
+    [ begin
+          #'OkeyGameR'{player_id = UId, score = Score} = R,
+          #'OkeySeriesResult'{player_id = UId, place = Position, score = Score, 
+                     winner = case Position =:= 1 of true -> <<"true">>; false -> <<"none">> end }
+      end
+      || {Position, R} <- Rs ].
+
+strip_gamepoints(#'OkeyGameResults'{results = R} = Res) ->
+    R2 = [ strip_gamepoints(R0) || R0 <- R],
+    Res#'OkeyGameResults'{results = R2};
+strip_gamepoints(#'OkeyGameR'{} = R) ->
+    R#'OkeyGameR'{
+      winner = false,
+      good_shot = false,
+      score_delta = 0,
+      skill_delta = 0,
+      score = 0,
+      breakdown = []
+     }.
+
+countdown_points_reset(false, Res) ->
+    {Res, Res};
+countdown_points_reset({true, PlayerId}, Res) ->
+    countdown_points_reset0(PlayerId, Res).
+
+countdown_points_reset0(PlayerId, Res) ->
+    {[gosterge], RoundScore} = score_single([gosterge], achievements(countdown, with_scores)),
+    {[gosterge_winner], GameScore} = score_single([gosterge_winner], achievements(countdown, with_scores)),
+    RoundRes = gosterge_finish_round_result(PlayerId, RoundScore, Res),
+    GameRes = gosterge_finish_game_result(PlayerId, GameScore, Res),
+    {RoundRes, GameRes}.
+
+update_history(Res, State) ->
+    Spin = fun(#'OkeyGameResults'{results = List}) ->
+                   lists:map(fun(X) -> {X#'OkeyGameR'.score_delta, X#'OkeyGameR'.score} end, List)
+           end,
+    case State#state.history of
+        [] ->
+            Res1 = trim_to_10(Res, State#state.mode),
+            ?INFO("old entry: none", []),
+            ?INFO("new entry: ~p", [Spin(Res1)]),
+            ?INFO("cmb entry: ~p", [Spin(Res1)]),
+            {State#state{history = [Res1]}, Res1};
+        [H|Rest] ->
+            C = combine(Res, H, State#state.mode),
+            ?INFO("old entry: ~p", [Spin(H)]),
+            ?INFO("new entry: ~p", [Spin(Res)]),
+            ?INFO("cmb entry: ~p", [Spin(C)]),
+            {State#state{history = [C, H | Rest]}, C}
+    end.
+
+combine(Base = #'OkeyGameResults'{}, History = #'OkeyGameResults'{}, Mode) ->
+    BaseR = Base#'OkeyGameResults'.results,
+    HistoryR = History#'OkeyGameResults'.results,
+    ComulativeRes = [ combine(B, H, Mode) || B = #'OkeyGameR'{player_id = UIdB} <- BaseR,
+                                             H = #'OkeyGameR'{player_id = UIdH} <- HistoryR,
+                                             UIdB == UIdH ],
+    Base#'OkeyGameResults'{results = ComulativeRes};
+combine(B = #'OkeyGameR'{}, H = #'OkeyGameR'{}, Mode) when Mode == countdown->
+    trim_to_10(combine(B, H), Mode);
+combine(B = #'OkeyGameR'{}, H = #'OkeyGameR'{}, _Mode) ->
+    combine(B, H).
+combine(B = #'OkeyGameR'{}, H = #'OkeyGameR'{}) ->
+    BPD = B#'OkeyGameR'.score_delta,
+    HPD = H#'OkeyGameR'.score,
+    B#'OkeyGameR'{score = BPD + HPD}.
+
+trim_to_10(Results = #'OkeyGameResults'{}, Mode) when Mode == countdown ->
+    RRs = Results#'OkeyGameResults'.results,
+    RRs1 = [ trim_to_10(R, Mode) || R <- RRs ],
+    Results#'OkeyGameResults'{results = RRs1};
+trim_to_10(Results = #'OkeyGameResults'{}, _Mode)  ->
+    Results;
+trim_to_10(S = #'OkeyGameR'{score = Score}, Mode) when Score < 0, Mode == countdown ->
+    S#'OkeyGameR'{score = 0};
+trim_to_10(S, _Mode) ->
+    S.
+
+get_set_state(State) ->
+    #'OkeySetState'{round_cur = State#state.round_cur,
+                    round_max = State#state.round_max,
+                    set_cur = State#state.set_cur,
+                    set_max = State#state.set_max}.
+
+
+get_max_countdown_counter(#state{history = [L|_]} = _State) ->
+    #'OkeyGameResults'{results = Rs} = L,
+    MaxP = lists:last(lists:keysort(#'OkeyGameR'.score, Rs)),
+    {MaxP#'OkeyGameR'.player_id, MaxP#'OkeyGameR'.score}.
+
+%% begin CODE for countdown
+check_set_ending(#state{mode = countdown, round_cur = RCur,
+                        set_cur = SetCur, set_max = SetMax} = State) ->
+    {A, B} = get_max_countdown_counter(State),
+    ?INFO("SetCur: ~p, SetMax: ~p", [SetCur, SetMax]),
+    ?INFO("coundown best score: ~p", [{A, B}]),
+    case {A, B} of
+        {PlayerId, Points} when Points >= 10 ->
+            Acc =
+                case SetCur of
+                    SetMax -> done; %% it was last set
+                    _ ->      next_set %% there are few more sets left
+                end,
+            %% PlayerResults2 = avard_countdown_bonus(PlayerId, PlayerResults),
+            {State#state{round_cur = 1,
+                         set_cur = SetCur + 1,
+                         stats = [],
+                         gosterge_shown = false},
+             Acc,
+             {true, PlayerId}};
+        {_PlayerId, Points} when Points < 10 ->
+            {State#state{round_cur = RCur + 1, stats = [], gosterge_shown = false},
+             next_round,
+             false}
+    end;%% end CODE for countdown
+
+
+check_set_ending(State = #state{round_cur = RCur0, round_max = RMax0,
+                                set_cur = SetCur0, set_max = SetMax0})
+  when (RCur0 /= RMax0) ->
+    ?INFO("check_set_ending A, next_round. debug: ~p", [{RCur0, RMax0, SetCur0, SetMax0}]),
+    State1 = update_to_next_round_or_set(State),
+    {State1, next_round, false};
+check_set_ending(State = #state{set_cur = SetCur0, set_max = SetMax0,
+                                round_cur = RCur0, round_max = RMax0})
+  when (RCur0 == RMax0) andalso (SetCur0 < SetMax0)->
+    ?INFO("check_set_ending A, next_set. debug: ~p", [{RCur0, RMax0, SetCur0, SetMax0}]),
+    State1 = update_to_next_round_or_set(State),
+    {State1, next_set, false};
+check_set_ending(State = #state{set_cur = SetCur, set_max = SetMax,
+                                round_cur = RCur, round_max = RMax}) ->
+    ?INFO("check_set_ending B, done. debug: ~p", [{RCur, RMax, SetCur, SetMax}]),
+    State1 = update_to_next_round_or_set(State),
+    {State1, done, false}.
+
+update_to_next_round_or_set(State = #state{mode = Mode}) when Mode == standard; Mode == evenodd; Mode == color ->
+    NextRound = State#state.round_cur + 1,
+    CurrentSet = State#state.set_cur,
+    ?INFO("update_to_next_round_or_set. {set_cur, round_cur}: ~p", [{State#state.set_cur, State#state.round_cur}]),
+    case NextRound > State#state.round_max of
+        true ->
+            %% new set
+            ?INFO("next set", []),
+            State#state{round_cur = 1, set_cur = CurrentSet + 1, stats = [], gosterge_shown = false};
+        false ->
+            ?INFO("next round", []),
+            %% new round
+            State#state{round_cur = NextRound, stats = [], gosterge_shown = false}
+    end;
+update_to_next_round_or_set(State) -> %% case when Mode = countdown
+    State#state{stats = [], gosterge_shown = false}.
+
+
+check_hands_for_8_tashes(Hands, Stats) ->
+    lists:foldl(fun({UId, H}, Acc) ->
+                        case with_8_tashes(H) of
+                            true ->
+                                A = #okey_stats_entry{uid = UId, reason = with_8_tashes},
+                                [A | Acc];
+                            false ->
+                                Acc
+                        end
+                end, Stats, Hands).
+
+reset_scoring0(State) ->
+    ?INFO("External reset scoring. set 1, round 1", []),
+    State#state{round_cur = 1, set_cur = 1, stats = [], gosterge_shown = false, history = []}.
+
+common_round_finish(Stats, Res0, Mode, State1) ->
+    Res2 = analyze_game(Stats, Res0, Mode),
+    {State2, Res4} = update_set(Mode, State1, Res2),
+    {State3, Res5} = update_history(Res4, State2),
+    {State4, IsLast, CountdownReset} = check_set_ending(State3),
+    RoundResults = translate_reasons(Res5),
+    {_, GameResults0} = countdown_points_reset(CountdownReset, Res5),
+    case IsLast of
+        done ->
+            GameResults = translate_reasons(GameResults0),
+            %game_stats:assign_points(GameResults, State1#state.game_info),
+            {reply, {ok, {done, GameResults},                 RoundResults}, State4#state{history = []}};
+        next_set ->
+            {reply, {ok, {next_set,   get_set_state(State4)}, RoundResults}, State4#state{history = []}};
+        next_round ->
+            {reply, {ok, {next_round, get_set_state(State4)}, RoundResults}, State4}
+    end.
+
+%% REWRITE THIS PART!!!
+update_set(Mode, State, R) when Mode =/= standard ->
+    #'OkeyGameResults'{results = Res} = R,
+    %% find reveal, if available
+    FilterFun = fun(#'OkeyScoringDetail'{reason = X}) ->
+                        length(X) > 1 andalso lists:member(reveal, X) %% reason is [reveal, ..]
+                end,
+    CW = find_first_ogr(FilterFun, Res),
+    case CW of
+        null ->
+            update_set2(State, R);
+        _ ->
+            update_set_state(State, R, CW)
+    end;
+update_set(_, State, R) ->
+    {State, R}.
+
+update_set2(State = #state{}, R) ->
+    #'OkeyGameResults'{results = Res} = R,
+    %% find with_8_tashes, if available
+    FilterFun = fun(#'OkeyScoringDetail'{reason = X}) ->
+                        lists:member(with_8_tashes, X) %% reason is [with_8_tashes, ..]
+                end,
+    CW = find_first_ogr(FilterFun, Res),
+    case CW of
+        null ->
+            {State, R};
+        _ ->
+            update_set_state(State, R, CW)
+    end.
+
+find_first_ogr(FilterFun, Res) ->
+    lists:foldl(fun
+                    (#'OkeyGameR'{breakdown = BR} = OGR, null) ->
+                        P = lists:filter(FilterFun, BR),
+                        case P of
+                            [] -> null;
+                            _ -> OGR
+                        end;
+                    (_, Acc) ->
+                             Acc
+                end, null, Res).
+
+update_set_state(State, R = #'OkeyGameResults'{}, CW) ->
+    Entries0 = R#'OkeyGameResults'.results,
+    Entries1 = lists:delete(CW, Entries0),
+    
+    Reward = State#state.chanak,
+    #'OkeyGameR'{breakdown = BR, score = OrgScore, score_delta = OrgDelta} = CW,
+    ?INFO("chanak points: ~p", [Reward]),
+    BR1 = [#'OkeyScoringDetail'{reason = <<"chanak points">>, score = Reward} | BR],
+
+    Entries2 = [CW#'OkeyGameR'{breakdown = BR1,
+                               score = OrgScore + Reward,
+                               score_delta = OrgDelta + Reward} | Entries1],
+    {State#state{chanak = 0}, R#'OkeyGameResults'{results = Entries2}}.
+
+refill_chanak(Mode, State = #state{gosterge_shown = X}, Stats0) when X == false, Mode =/= standard, Mode =/= countdown ->
+    Chanak = State#state.chanak,
+    L = [ #okey_stats_entry{uid = UId, reason = box_empty} || {UId, _} <- State#state.set ],
+    Achievements = achievements(Mode, with_scores),
+    {_, Deduction} = lists:keyfind([box_empty], 1, Achievements),
+    Stats = %L ++ 
+            Stats0,
+    NewChanak = Chanak + Deduction,
+    {State#state{chanak = NewChanak, stats = Stats}, Stats};
+refill_chanak(_, State, Stats) ->
+    {State, Stats}.
+
+new_chanak(Mode) ->
+    Achievements = achievements(Mode, with_scores),
+    {_, Deduction} = lists:keyfind([gosterge_winner], 1, Achievements),
+    Deduction.
+
+analyze_game(Stats, #'OkeyGameResults'{results = PlayerResults0} = Res, Mode) ->
+    PlayerEvents = dict:from_list([ {Id, []} || #'OkeyGameR'{player_id = Id} <- PlayerResults0 ]),
+%    ?INFO("PlayerEvents: ~p", [PlayerEvents]),
+    PlayerEvents1 = analyze_game0(Stats, PlayerEvents, fun describe_achievement/2),
+%    ?INFO("PlayerEvents1: ~p", [PlayerEvents1]),
+    Check = [ check_stats(X) ||  {_Id, X} <- dict:to_list(PlayerEvents1) ],
+    case lists:any(fun(Bool) -> Bool end, Check) of
+        false ->
+%            ?INFO("PlayerEvents: ~p", [PlayerEvents1]),
+            erlang:error(incomplete_stats_detected);
+        true -> ok
+    end,
+    PEL = dict:to_list(PlayerEvents1),
+    PlayerEvents2 = [ {Id, ordsets:from_list(X) } ||  {Id, X} <- PEL ],
+    AMode = achievements(Mode),
+    Achievements = ordsets:from_list(AMode),
+    Reduced = [ {Id, reduce_ach(match_ach(PE, Achievements))} || {Id, PE} <- PlayerEvents2 ],
+    Scored = [ score_ach(R, RA, Mode)
+               || {{_Id, RA}, R} <- lists:zip(lists:keysort(1, Reduced),
+                                              lists:keysort(#'OkeyGameR'.player_id, PlayerResults0)) ],
+    Res#'OkeyGameResults'{results = Scored}.
+
+match_ach(MyList, OffList) ->
+    lists:filter(fun(X) -> ordsets:is_subset(X, MyList) end, OffList).
+
+
+reduce_ach(L) ->
+    reduce_ach(L, L, []).
+reduce_ach([], _L, Acc) ->
+    Acc;
+reduce_ach([H | T], L, Acc) ->
+    case lists:any(fun
+                       (X) when X =:= H -> false;
+                       (X) -> ordsets:is_subset(H, X)
+                   end, L) of
+        true ->
+            reduce_ach(T, L, Acc);
+        false ->
+            reduce_ach(T, L, [H | Acc])
+    end.
+
+score_ach(R, [], _Mode) ->
+    R;
+score_ach(#'OkeyGameR'{score_delta = PD} = R, [H0|T] = _RA, Mode) ->
+    Achievements = achievements(Mode, with_scores),
+    case score_single(H0, Achievements) of
+        false ->
+            score_ach(R, T, Mode);
+        {H, Score} ->
+            BD = R#'OkeyGameR'.breakdown,
+            A = #'OkeyScoringDetail'{reason = H, score = Score},
+            R1 = R#'OkeyGameR'{score_delta = PD + Score,
+                               score = PD + Score,
+                               breakdown = [A | BD]},
+            score_ach(R1, T, Mode)
+    end.
+
+score_single(Entry, Achievements) ->
+    H = lists:usort(ordsets:to_list(Entry)),
+    lists:keyfind(H, 1, Achievements).
+
+translate_reasons(#'OkeyGameResults'{results = R} = Res) ->
+    R2 = [ translate_reasons(X) || X  <- R ],
+    Res#'OkeyGameResults'{results = R2};
+translate_reasons(#'OkeyGameR'{breakdown = R} = Res) ->
+    R2 = [ translate_reasons(X) || X  <- R ],
+    Res#'OkeyGameR'{breakdown = R2};
+translate_reasons(#'OkeyScoringDetail'{reason = R} = Res) when is_binary(R) ->
+    Res;
+translate_reasons(#'OkeyScoringDetail'{reason = R} = Res) ->
+    Descs = lists:zip(achievements(), achievements_desc()),
+    {R, Reason} = lists:keyfind(R, 1, Descs),
+    Res#'OkeyScoringDetail'{reason = Reason}.
+
+analyze_game0([], Players, _F) ->
+    Players;
+analyze_game0([#okey_stats_entry{uid = UId, reason = Reason} | Rest], Players, F) ->
+    Player = dict:fetch(UId, Players),
+    Player2 = F(Reason, Player),
+    analyze_game0(Rest, dict:store(UId, Player2, Players), F).
+
+describe_achievement(Event, List) ->
+    [Event | List].
+
+with_okey(Okey, Okey) ->
+    true;
+with_okey(_Okey, _LastTash) ->
+    false.
+
+with_even_tashes(Gosterge, Hand0) ->
+    Okey = game_okey:get_okey(Gosterge),
+    Hand = game_okey:normalize_hand(null, Hand0),
+    Rev2 = lists:map(fun(J) when J == Okey -> okey;
+                        (Other) -> Other
+                     end, Hand),
+    Rev3 = lists:map(fun(?FALSE_OKEY) -> Okey;
+                        (Other) -> Other
+                     end, Rev2),
+    Sets = game_okey:split_by_delimiter(null, Rev3),
+    Res = lists:all(fun(S) ->
+                      game_okey:is_pair(S)
+              end, Sets),
+    Res andalso ?INFO("Detected even tashes, gosterge: ~p~nhand ~p", [Gosterge, Hand0]),
+    Res.
+
+with_8_tashes(Hand0) ->
+    Hand = game_okey:normalize_hand(null, Hand0),
+    Sets = game_okey:split_by_delimiter(null, Hand),
+    Pairs0 = lists:filter(fun
+                              ([A, A]) -> true;
+                              (_) -> false
+                          end, Sets),
+    Singles = lists:map(fun(L) -> hd(L) end, Pairs0),
+    case length(Singles) of
+        X when X > 3 ->
+            check_combinations(Singles);
+        _ ->
+            false
+    end.
+
+with_color(Gosterge, Hand) ->
+    Okey = game_okey:get_okey(Gosterge),
+    Tashes0 = lists:flatten(game_okey:split_by_delimiter(null, game_okey:normalize_hand(null, Hand))),
+    Tashes1 = lists:filter(fun(J) when J == Okey -> false;
+                              (_) -> true
+                           end, Tashes0),
+    Tashes = lists:map(fun(J) when J == ?FALSE_OKEY -> Okey;
+                          (J) -> J
+                       end, Tashes1),
+    TheC = (hd(Tashes))#'OkeyPiece'.color,
+    lists:all(fun
+                  (#'OkeyPiece'{color = C}) when C == TheC -> true;
+                  (#'OkeyPiece'{color = _}) -> false
+              end, Tashes).
+
+check_combinations(Singles) ->
+    Combs = kakamath:combination(4, Singles),
+    lists:any(fun(List) ->
+                      same_values(List)
+              end, Combs).
+
+same_values(List) ->
+    Val = (hd(List))#'OkeyPiece'.value,
+    lists:all(fun
+                  (#'OkeyPiece'{value = V}) when V == Val -> true;
+                  (_) -> false
+              end, List).
+
+
+if_list(true, X) ->
+    [X];
+if_list(false, _) ->
+    [].
+
+is_mode(Mode) ->
+    lists:member(Mode, [standard, evenodd, color, countdown]).
+
+achievements(Mode, with_scores) ->
+    GP = game_points(),
+    {Mode, Points} = lists:keyfind(Mode, 1, GP),
+    Res0 = lists:zip(achievements(), Points),
+    Res1 = [ P || {_, Score} = P <- Res0, Score =/= 0 ],
+    Res1.
+achievements(Mode) ->
+    {Ach, _Scores} = lists:unzip(achievements(Mode, with_scores)),
+    Ach.
+
+achievements() ->
+   A = [
+        [gosterge],                  %% 1
+        [reveal],                    %% 2
+        [reveal, okey],              %% 3
+        [reveal, even],              %% 4
+        [reveal, even, okey],        %% 5
+        [no_reveal, with8tashes],    %% 6
+        [reveal, color],             %% 7
+        [reveal, color, okey],       %% 8
+        [reveal, color, even],       %% 8
+        [reveal, color, even, okey], %% 10
+        [wrong_reveal],              %% 11
+        [caught_bluff],              %% 12 %% AKA others_on_wrong_reveal
+        [box_empty],                 %% 13
+        [rejected_good_hand],        %% 14
+        [gosterge_winner]            %% 15
+       ],%% note: those points are added
+    [ lists:usort(X) || X <- A ].
+achievements_desc() ->
+    [
+     <<"gosterge shown">>,                            %% 1
+     <<"proper reveal">>,                             %% 2
+     <<"reveal with okey">>,                          %% 3
+     <<"reveal with even tashes">>,                   %% 4
+     <<"reveal with even tashes and okey">>,          %% 5
+     <<"8 tashes of value in hand">>,                 %% 6
+     <<"reveal with color">>,                         %% 7
+     <<"reveal with color and okey">>,                %% 8
+     <<"reveal with color and even tashes">>,         %% 9
+     <<"reveal with color, even tashes and okey">>,   %% 10
+     <<"reveal with wrong hand">>,                    %% 11
+     <<"caught bluff">>,                              %% 12 %% AKA others_on_wrong_reveal
+     <<"deduction to empty box">>,                    %% 13
+     <<"rejected good hand">>,                        %% 14
+     <<"gosterge winner">>                            %% 15
+    ].%% note: those points are added
+
+
+%% note - all penalties/deductions should be expressed here as negative numbers
+game_points() ->
+    [%%          1   2   3   4   5   6   7   8   9  10  11  12  13  14, 15 <--- achievement number
+     {standard, [1,  3,  6,  6, 12,  0,  0,  0,  0,  0, -9,  3,  1,  0,  1]},
+     {odd,      [1,  3,  6,  6, 12, 12, 24, 48, 48, 96, -9,  3,  1,  0,  8]},
+     {even,     [2,  6, 12, 12, 24, 24, 48, 96, 96,192,-18,  6,  2,  0,  8]},
+     {ybodd,    [1,  3,  6,  6, 12, 12, 24, 48, 48, 96, -9,  3,  1,  0, 16]},
+     {ybeven,   [2,  6, 12, 12, 24, 24, 48, 96, 96,192,-18,  6,  2,  0, 16]},
+     {rbodd,    [2,  6, 12, 12, 24, 24, 48, 96, 96,192,-18,  6,  2,  0, 16]},
+     {rbeven,   [4, 12, 24, 24, 48, 48, 96,192,192,384,-36, 12,  4,  0, 16]},
+     {countdown,[1,  2,  4,  4,  8,  0,  0,  0,  0,  0, -2,  0,  1,  0,  1]}
+    ].%% note: those points are added
+
+
+%% debug
+check_stats(List) ->
+    lists:member(reveal, List)
+        orelse lists:member(wrong_reveal, List)
+        orelse lists:member(disconnected, List)
+        orelse lists:member(out_of_tashes, List).
+
+%%%===================================================================
+%%% Tests
+%%%===================================================================
+
+detecting_8_tashes_test() ->
+    H = [ {true, [#'OkeyPiece'{color = 1, value = 2},
+                  #'OkeyPiece'{color = 1, value = 2},
+                  null,
+                  #'OkeyPiece'{color = 2, value = 2},
+                  #'OkeyPiece'{color = 2, value = 2},
+                  null,
+                  #'OkeyPiece'{color = 3, value = 2},
+                  #'OkeyPiece'{color = 3, value = 2},
+                  null,
+                  #'OkeyPiece'{color = 4, value = 1},
+                  #'OkeyPiece'{color = 4, value = 3},
+                  null,
+                  #'OkeyPiece'{color = 4, value = 2},
+                  #'OkeyPiece'{color = 4, value = 2},
+                  null,
+                  #'OkeyPiece'{color = 2, value = 12},
+                  #'OkeyPiece'{color = 3, value = 7},
+                  #'OkeyPiece'{color = 2, value = 7},
+                  #'OkeyPiece'{color = 2, value = 12}
+                 ], #'OkeyPiece'{color = 2, value = 12}},
+          {false, [#'OkeyPiece'{color = 1, value = 2},
+                   #'OkeyPiece'{color = 1, value = 2},
+                   null,
+                   #'OkeyPiece'{color = 2, value = 2},
+                   #'OkeyPiece'{color = 2, value = 2},
+                   null,
+                   #'OkeyPiece'{color = 3, value = 2},
+                   #'OkeyPiece'{color = 3, value = 2},
+                   null,
+                   #'OkeyPiece'{color = 4, value = 1},
+                   #'OkeyPiece'{color = 4, value = 3},
+                   null,
+                   #'OkeyPiece'{color = 4, value = 2},
+                   #'OkeyPiece'{color = 2, value = 12},
+                   null,
+                   #'OkeyPiece'{color = 2, value = 12},
+                   #'OkeyPiece'{color = 3, value = 7},
+                   #'OkeyPiece'{color = 2, value = 7},
+                   #'OkeyPiece'{color = 2, value = 12}
+                  ], #'OkeyPiece'{color = 2, value = 12}},
+          {false, [#'OkeyPiece'{color = 1, value = 2},
+                   #'OkeyPiece'{color = 1, value = 2},
+                   null,
+                   #'OkeyPiece'{color = 2, value = 2},
+                   ?FALSE_OKEY,
+                   null,
+                   #'OkeyPiece'{color = 3, value = 2},
+                   #'OkeyPiece'{color = 3, value = 2},
+                   null,
+                   #'OkeyPiece'{color = 4, value = 1},
+                   #'OkeyPiece'{color = 4, value = 3},
+                   null,
+                   #'OkeyPiece'{color = 4, value = 2},
+                   #'OkeyPiece'{color = 2, value = 12},
+                   null,
+                   #'OkeyPiece'{color = 2, value = 12},
+                   #'OkeyPiece'{color = 3, value = 7},
+                   #'OkeyPiece'{color = 2, value = 7},
+                   #'OkeyPiece'{color = 2, value = 12}
+                  ], #'OkeyPiece'{color = 2, value = 2}},
+          {false, [#'OkeyPiece'{color = 1, value = 2},
+                  #'OkeyPiece'{color = 1, value = 2},
+                  #'OkeyPiece'{color = 2, value = 5},
+                  #'OkeyPiece'{color = 2, value = 5},
+                  null,
+                  null,
+                  #'OkeyPiece'{color = 3, value = 7},
+                  #'OkeyPiece'{color = 3, value = 7},
+                  null,
+                  #'OkeyPiece'{color = 4, value = 7},
+                  #'OkeyPiece'{color = 4, value = 7},
+                  null,
+                  #'OkeyPiece'{color = 2, value = 1},
+                  #'OkeyPiece'{color = 2, value = 1},
+                  null,
+                  #'OkeyPiece'{color = 2, value = 12},
+                  #'OkeyPiece'{color = 3, value = 7},
+                  #'OkeyPiece'{color = 2, value = 7},
+                  #'OkeyPiece'{color = 2, value = 12}
+                 ], #'OkeyPiece'{color = 2, value = 12}},
+          {false, [#'OkeyPiece'{color = 1, value = 2},
+                   #'OkeyPiece'{color = 1, value = 3},
+                   #'OkeyPiece'{color = 1, value = 4},
+                   #'OkeyPiece'{color = 1, value = 6},
+                   null,
+                   #'OkeyPiece'{color = 1, value = 5},
+                   #'OkeyPiece'{color = 2, value = 5},
+                   #'OkeyPiece'{color = 3, value = 5},
+                   #'OkeyPiece'{color = 4, value = 5},
+                   null,
+                   #'OkeyPiece'{color = 2, value = 1},
+                   #'OkeyPiece'{color = 2, value = 13},
+                   #'OkeyPiece'{color = 2, value = 12},
+                   null,
+                   #'OkeyPiece'{color = 3, value = 7},
+                   #'OkeyPiece'{color = 2, value = 7},
+                   #'OkeyPiece'{color = 2, value = 12}
+                  ], #'OkeyPiece'{color = 2, value = 12}}
+         ],
+    HS = lists:zip(lists:seq(1, length(H)), H),
+    lists:map(fun({Num, {ProperResult, Hand, _}}) ->
+                      Z = {Num, with_8_tashes(Hand), Hand},
+                      {Num, ProperResult, Hand} = Z
+              end, HS).
+
+detecting_color_test() ->
+    H = [ {true, [#'OkeyPiece'{color = 1, value = 1},
+                  #'OkeyPiece'{color = 1, value = 2},
+                  #'OkeyPiece'{color = 1, value = 3},
+                  #'OkeyPiece'{color = 1, value = 4},
+                  null,
+                  #'OkeyPiece'{color = 1, value = 5},
+                  #'OkeyPiece'{color = 1, value = 6},
+                  #'OkeyPiece'{color = 1, value = 7},
+                  #'OkeyPiece'{color = 1, value = 8},
+                  null,
+                  #'OkeyPiece'{color = 1, value = 8},
+                  #'OkeyPiece'{color = 1, value = 9},
+                  #'OkeyPiece'{color = 1, value = 11},
+                  #'OkeyPiece'{color = 1, value = 12},
+                  #'OkeyPiece'{color = 1, value = 13},
+                  #'OkeyPiece'{color = 2, value = 12}
+                 ], #'OkeyPiece'{color = 2, value = 11}},
+          {false, [#'OkeyPiece'{color = 1, value = 1},
+                   #'OkeyPiece'{color = 1, value = 2},
+                   #'OkeyPiece'{color = 1, value = 3},
+                   #'OkeyPiece'{color = 1, value = 4},
+                   null,
+                   #'OkeyPiece'{color = 2, value = 5},
+                   #'OkeyPiece'{color = 2, value = 6},
+                   #'OkeyPiece'{color = 2, value = 7},
+                   #'OkeyPiece'{color = 2, value = 8},
+                   null,
+                   #'OkeyPiece'{color = 1, value = 8},
+                   #'OkeyPiece'{color = 1, value = 9},
+                   #'OkeyPiece'{color = 1, value = 11},
+                   #'OkeyPiece'{color = 1, value = 12},
+                   #'OkeyPiece'{color = 1, value = 13},
+                   #'OkeyPiece'{color = 2, value = 12}
+                  ], #'OkeyPiece'{color = 2, value = 11}},
+          {false, [[null,
+                    {'OkeyPiece',1,2},
+                    {'OkeyPiece',3,2},
+                    {'OkeyPiece',1,3},
+                    {'OkeyPiece',3,5},
+                    {'OkeyPiece',4,5},
+                    {'OkeyPiece',1,7},
+                    {'OkeyPiece',3,7},
+                    {'OkeyPiece',4,7},
+                    {'OkeyPiece',2,8},
+                    {'OkeyPiece',4,8},
+                    {'OkeyPiece',3,9},
+                    {'OkeyPiece',3,10},
+                    {'OkeyPiece',1,11}],
+                   [{'OkeyPiece',1,12},
+                    null,null,null,null,null,null,null,null,null,
+                    null,null,null,null]], {'OkeyPiece',1,0}}
+
+        ],
+    HS = lists:zip(lists:seq(1, length(H)), H),
+    lists:map(fun({Num, {ProperResult, Hand, Gosterge}}) ->
+                      Z = {Num, with_color(Gosterge, Hand), Hand, Gosterge},
+                      {Num, ProperResult, Hand, Gosterge} = Z
+              end, HS).
+
+reduce_ach_test() ->
+    H0 = [
+          { [[a]],
+            [[a],[]] },
+
+          { [[a,b]],
+            [[a],[b],[a,b]] },
+
+          { [[a,b],[a,c]],
+            [[a],[b],[a,b],[a,c]] },
+
+          { [[a,b],[c]],
+            [[a],[],[c],[b],[a,b]] }
+         ],
+    H = [ { [ ordsets:from_list(AX) || AX <- A ], [ ordsets:from_list(BX) || BX <- B ]} || {A, B} <- H0 ],
+    HS = lists:zip(lists:seq(1, length(H)), H),
+    lists:map(fun({Num, {Ideal0, Fresh}}) ->
+                      Reduced = reduce_ach(Fresh),
+                      Z = {Num, ordsets:from_list(Reduced), Fresh},
+                      Ideal = ordsets:from_list(Ideal0),
+                      {Num, Ideal, Fresh} = Z
+              end, HS).
+
+insert_at_random_test() ->
+    A = put_some_nulls(20, lists:seq(1, 10)),
+    ?INFO("insert_at_random_test: ~p", [A]).
+
+
+positions_arrange_test() ->
+    [{1, #'OkeyGameR'{player_id = 1}},
+     {2, #'OkeyGameR'{player_id = 2}},
+     {3, #'OkeyGameR'{player_id = 3}},
+     {4, #'OkeyGameR'{player_id = 4}}]
+        = positions_arrange([#'OkeyGameR'{score = 40, player_id = 1},
+                          #'OkeyGameR'{score = 30, player_id = 2},
+                          #'OkeyGameR'{score = 20, player_id = 3},
+                          #'OkeyGameR'{score = 10, player_id = 4}]),
+    [{1, #'OkeyGameR'{player_id = 2}},
+     {1, #'OkeyGameR'{player_id = 1}},
+     {2, #'OkeyGameR'{player_id = 3}},
+     {3, #'OkeyGameR'{player_id = 4}}]
+        = positions_arrange([#'OkeyGameR'{score = 40, player_id = 1},
+                          #'OkeyGameR'{score = 40, player_id = 2},
+                          #'OkeyGameR'{score = 20, player_id = 3},
+                          #'OkeyGameR'{score = 10, player_id = 4}]),
+    [{1, #'OkeyGameR'{player_id = 4}},
+     {1, #'OkeyGameR'{player_id = 3}},
+     {1, #'OkeyGameR'{player_id = 2}},
+     {1, #'OkeyGameR'{player_id = 1}}]
+        = positions_arrange([#'OkeyGameR'{score = 10, player_id = 1},
+                          #'OkeyGameR'{score = 10, player_id = 2},
+                          #'OkeyGameR'{score = 10, player_id = 3},
+                          #'OkeyGameR'{score = 10, player_id = 4}]),
+
+    [{1, #'OkeyGameR'{player_id = 1}},
+     {2, #'OkeyGameR'{player_id = 2}},
+     {3, #'OkeyGameR'{player_id = 3}},
+     {4, #'OkeyGameR'{player_id = 4}}]
+        = positions_arrange([#'OkeyGameR'{score = 30, player_id = 2},
+                          #'OkeyGameR'{score = 20, player_id = 3},
+                          #'OkeyGameR'{score = 40, player_id = 1},
+                          #'OkeyGameR'{score = 10, player_id = 4}]).
+
+score_ach_test() ->
+    %% ok = app_util:start(kakaconfig),
+    %% {ok, Srv} = ?MODULE:start_link([{game_mode, standard}, {sets, 2}, {rounds, 2}]),
+    %% Players = [<<"gleber">>, <<"paul">>, <<"kunthar">>, <<"radistao">>],
+    %% Events = [gosterge, reveal, okey, even, odd, no_reveal, with8tashes, color, wrong_reveal, caught_bluff, box_empty, out_of_tashes],
+    %% lists:map(fun(UId) ->
+    %%                   Event = started,
+    %%                   ?MODULE:add_event(Srv, UId, Event)
+    %%           end, Players),
+    %% lists:map(fun(_) ->
+    %%                   {UId, _} = kakamath:draw_random(Players),
+    %%                   {Event, _} = kakamath:draw_random(Events),
+    %%                   ?MODULE:add_event(Srv, UId, Event)
+    %%           end, lists:seq(1, crypto:rand_uniform(10, 30))),
+    %% {Gosterge, Hands0, _Pile0} = game_okey:hand_out_pieces(),
+    %% Hands = [ {UId, put_some_nulls(21, H)} || {UId, H} <- lists:zip(Players, Hands0) ],
+    %% ?MODULE:finish_round(Srv, [<<"paul">>], Hands, Gosterge).
+    ok.
+
+put_some_nulls(Target, List) ->
+    lists:foldl(fun(_, Acc) ->
+                        insert_at_random(null, Acc)
+                end, List, lists:seq(length(List), Target)).
+
+insert_at_random(Element, List) ->
+    L1 = length(List)+1,
+    R = crypto:rand_uniform(0, L1),
+    case R of
+        0 -> [Element | List];
+        L1 -> List ++ [Element];
+        _ -> lists:sublist(List, R) ++ [Element] ++ lists:nthtail(R, List)
+    end.
+
+
+
+        % [gosterge],                  %% 1
+        % [reveal],                    %% 2
+        % [reveal, okey],              %% 3
+        % [reveal, even],              %% 4
+        % [reveal, even, okey],        %% 5
+        % [no_reveal, with8tashes],    %% 6
+        % [reveal, color],             %% 7
+        % [reveal, color, even],       %% 8
+        % [reveal, color, okey],       %% 9
+        % [reveal, color, even, okey], %% 10
+        % [wrong_reveal],              %% 11
+        % [caught_bluff],              %% 12 %% AKA others_on_wrong_reveal
+        % [box_empty]                  %% 13
+
+

+ 110 - 0
apps/server/src/okey/test_hands.erl

@@ -0,0 +1,110 @@
+%%----------------------------------------------------------------------
+%% @author Yura Zhloba <yzh44yzh@gmail.com>
+%% @copyright Paynet Internet ve Bilisim Hizmetleri A.S. All Rights Reserved.
+%% @doc
+%% This module allows to set up pieces for next game
+%% @end
+%%----------------------------------------------------------------------
+
+-module(test_hands).
+
+-include_lib("server/include/logging.hrl").
+-include_lib("server/include/requests.hrl").
+-include_lib("server/include/game_okey.hrl").
+
+-include_lib("eunit/include/eunit.hrl").
+
+-export([set_pieces/1, clear/0]).
+-export([pieces_set_1/0, pieces_set_2/0]).
+
+-spec set_pieces(list() | undefined) -> ok.
+set_pieces(P) ->
+    ?PP("pieces ~p", [P]),
+    A = kakaconfig:set([games, okey, debug_next_round_pieces], P),
+    ?PP("set result: ~p", [A]),
+    ok.
+
+-spec clear() -> ok.
+clear() ->
+    set_pieces(undefined).
+
+set_1() ->
+    A = to_pieces([
+                   {1,1}, {2,1}, {3,1}, {4,1},
+                   {1,2}, {2,2}, {3,2}, {4,2},
+                   {1,3}, {2,3}, {3,3}, {4,3},
+                   {1,4}, {2,4}, {3,4}, {4,4},
+                   {1,5}, {2,5}, {3,5}, {4,5},
+                   {1,6}, {2,6}, {3,6}, {4,6},
+                   {1,7}, {2,7}, {3,7}, {4,7},
+                   {1,8}, {2,8}, {3,8}, {4,8},
+                   {1,9}, {2,9}, {3,9}, {4,9},
+                   {1,10}, {2,10}, {3,10}, {4,10},
+                   {1,11}, {2,11}, {3,11}, {4,11},
+                   {1,12}, {2,12}, {3,12}, {4,12},
+                   {1,13}, {2,13}, {3,13}, {4,13},
+                   {1,1}, {2,1}, {3,1}, {4,1},
+                   {1,2}, {2,2}, {3,2}, {4,2},
+                   {1,3}, {2,3}, {3,3}, {4,3},
+                   {1,4}, {2,4}, {3,4}, {4,4},
+                   {1,5}, {2,5}, {3,5}, {4,5},
+                   {1,6}, {2,6}, {3,6}, {4,6},
+                   {1,7}, {2,7}, {3,7}, {4,7},
+                   {1,8}, {2,8}, {3,8}, {4,8},
+                   {1,9}, {2,9}, {3,9}, {4,9},
+                   {1,10}, {2,10}, {3,10}, {4,10},
+                   {1,11}, {2,11}, {3,11}, {4,11},
+                   {1,12}, {2,12}, {3,12}, {4,12},
+                   {1,13}, {2,13}, {3,13}, {4,13}
+                  ]),
+    A ++ [?FALSE_OKEY, ?FALSE_OKEY].
+
+set_2() ->
+    A = to_pieces([
+                   {2,1}, {4,1}, {3,1}, {1,1},
+                   {2,2}, {4,2}, {3,2}, {1,2},
+                   {2,3}, {4,3}, {3,3}, {1,3},
+                   {2,4}, {4,4}, {3,4}, {1,4},
+                   {2,5}, {4,5}, {3,5}, {1,5},
+                   {2,6}, {4,6}, {3,6}, {1,6},
+                   {2,7}, {4,7}, {3,7}, {1,7},
+                   {2,8}, {4,8}, {3,8}, {1,8},
+                   {2,9}, {4,9}, {3,9}, {1,9},
+                   {2,10}, {4,10}, {3,10}, {1,10},
+                   {2,11}, {4,11}, {3,11}, {1,11},
+                   {2,12}, {4,12}, {3,12}, {1,12},
+                   {2,13}, {4,13}, {3,13}, {1,13},
+                   {2,1}, {4,1}, {3,1}, {1,1},
+                   {2,2}, {4,2}, {3,2}, {1,2},
+                   {2,3}, {4,3}, {3,3}, {1,3},
+                   {2,4}, {4,4}, {3,4}, {1,4},
+                   {2,5}, {4,5}, {3,5}, {1,5},
+                   {2,6}, {4,6}, {3,6}, {1,6},
+                   {2,7}, {4,7}, {3,7}, {1,7},
+                   {2,8}, {4,8}, {3,8}, {1,8},
+                   {2,9}, {4,9}, {3,9}, {1,9},
+                   {2,10}, {4,10}, {3,10}, {1,10},
+                   {2,11}, {4,11}, {3,11}, {1,11},
+                   {2,12}, {4,12}, {3,12}, {1,12},
+                   {2,13}, {4,13}, {3,13}, {1,13}
+                  ]),
+    A ++ [?FALSE_OKEY, ?FALSE_OKEY].
+
+pieces_set_1() ->
+    set_pieces(set_1()).
+
+pieces_set_2() ->
+    set_pieces(set_2()).
+
+to_pieces(Hands) ->
+    [#'OkeyPiece'{color = Color, value = Value} || {Color, Value} <- Hands].
+
+are_hands_proper_test() ->
+    {A, LL1, L2} = game_okey:generate_hand(),
+    Ideal = lists:sort([A] ++ lists:flatten(LL1) ++ L2),
+    Set1 = lists:sort(set_1()),
+    Set2 = lists:sort(set_2()),
+    ?DP("~nIdeal -- Set1 = ~p~nSet1 -- Ideal = ~p~n", [Ideal -- Set1, Set1 -- Ideal]),
+    ?DP("~nIdeal -- Set2 = ~p~nSet2 -- Ideal = ~p~n", [Ideal -- Set2, Set2 -- Ideal]),
+    true = Ideal == Set1,
+    true = Ideal == Set2.

+ 970 - 0
apps/server/src/okey/test_okey.erl

@@ -0,0 +1,970 @@
+-module(test_okey).
+
+-include_lib("eunit/include/eunit.hrl").
+-include_lib("server/include/log.hrl").
+-include_lib("server/include/settings.hrl").
+-include_lib("server/include/conf.hrl").
+-include_lib("server/include/kamf.hrl").
+-include_lib("server/include/requests.hrl").
+-include_lib("server/include/classes.hrl").
+-include_lib("server/include/game_okey.hrl").
+
+%% debug
+-export([validate_hand/3,conductor/3]).
+-export([start/0, alt_play_3_players_1_bot/0, init_with_join_game/7]).
+
+-define(BT, 30000).
+-define(FLUSH_DELAY, 10).
+-define(SIM_DELAY, 10).
+-define(TCM, tc).
+
+-record(state, {
+          conn,
+          gid,
+          uid,
+          mode,
+          hand,
+          gosterge,
+          pileh,
+          acker_fun = fun(_Event) -> continue end,
+          set_state :: #'OkeySetState'{}
+         }).
+
+%% bot order
+-record(bo, {
+          pid,
+          event,
+          event_no,
+          order
+         }).
+
+%% ===================================================================
+%% ===================================================================
+%% Tests
+%% ===================================================================
+%% ===================================================================
+
+game_creation_test_() ->
+    {foreach,
+     fun() -> tests:setup() end,
+     fun(State) -> tests:cleanup(State) end,
+     [
+      % {timeout, 100, fun test_join_game_random_reveal/0},
+      % {timeout, 100, fun test_match_me_random_reveal/0},
+      % {timeout, 100, fun create_game_with_robots/0},
+      % {timeout, 100, fun test_join_game_observer_settings/0},
+      {timeout, 100, fun test_social_actions/0},
+      fun() -> ok end
+     ]
+    }.
+
+game_ending_test_() ->
+    {foreach,
+     fun() -> tests:setup() end,
+     fun(State) -> tests:cleanup(State) end,
+     [
+      % {timeout, 100, fun test_join_game_empty_pile/0},
+      % {timeout, 100, fun test_join_game_countdown_random_reveal/0},
+      fun() -> ok end
+     ]
+    }.
+
+replacement_test_() ->
+    {foreach,
+     fun() -> tests:setup() end,
+     fun(State) -> tests:cleanup(State) end,
+     [
+      % {timeout, 100, fun alt_play_3_players_1_bot/0},
+      % {timeout, 100, fun player_replacement/0},
+      fun() -> ok end
+     ]
+    }.
+
+matchmaker_test_() ->
+    {foreach,
+     fun() -> tests:setup() end,
+     fun(State) -> tests:cleanup(State) end,
+     [
+      % {timeout, 100, fun matchmaker_disconnect/0},
+      fun() -> ok end
+     ]
+    }.
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%                    tests setup                         %%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%player_replacement() ->
+%    MultiOwner = self(),
+%    process_flag(trap_exit, true),
+%    Clients = [ proc_lib:spawn_link(fun() ->
+%                          start_test_game_t(MultiOwner, join_game_player_replacement, empty_pile)
+%                 end) || _ <- lists:seq(1, 1) ],
+%
+%    [ wait_for(C) || C <- Clients ].
+
+start() ->
+    MultiOwner = self(),
+    process_flag(trap_exit, true),
+    Clients = [ proc_lib:spawn_link(fun() -> 
+                         start_test_game_t(MultiOwner, join_game_robots, normal)
+                 end) || _ <- lists:seq(1, 1) ],
+
+    [ wait_for(C) || C <- Clients ].
+
+alt_play_3_players_1_bot() ->
+    MultiOwner = self(),
+    process_flag(trap_exit, true),
+    Clients = [ proc_lib:spawn_link(fun() ->
+                         start_test_game_t(MultiOwner, join_game_new, normal)
+                 end) || _ <- lists:seq(1, 1) ],
+
+    [ wait_for(C) || C <- Clients ].
+
+%matchmaker_disconnect() ->
+%    MultiOwner = self(),
+%    process_flag(trap_exit, true),
+%    Clients = [ proc_lib:spawn_link(fun() -> 
+%                         start_test_game_t(MultiOwner, match_me_disconnect, normal) 
+%                 end) || _ <- lists:seq(1, 1) ],%
+%
+%    [ wait_for(C) || C <- Clients ].
+
+%test_join_game_random_reveal() ->
+%    MultiOwner = self(),
+%    process_flag(trap_exit, true),
+%    Clients = [ proc_lib:spawn_link(fun() -> 
+%                         start_test_game_t(MultiOwner, join_game, normal) end) || _ <- lists:seq(1, 1) ],
+%    [ wait_for(C) || C <- Clients ].
+
+%test_join_game_countdown_random_reveal() ->
+%    MultiOwner = self(),
+%    process_flag(trap_exit, true),%
+%
+%    Clients = [ proc_lib:spawn_link(fun() -> %
+%			 start_test_game_t(MultiOwner, join_game_countdown, normal)
+%                end) || _ <- lists:seq(1, 1) ],
+%
+ %   [ wait_for(C) || C <- Clients ].
+
+%test_join_game_observer_settings() ->
+%    MultiOwner = self(),
+%    process_flag(trap_exit, true),
+%    Clients = [ proc_lib:spawn_link(fun() ->
+%                         start_test_game_t(MultiOwner, join_game_observer, normal) 
+%                end) || _ <- lists:seq(1, 1) ],
+%
+%    [ wait_for(C) || C <- Clients ].
+
+%test_join_game_empty_pile() ->
+%    MultiOwner = self(),
+%    process_flag(trap_exit, true),
+%    Clients = [ proc_lib:spawn_link(fun() -> 
+%			 start_test_game_t(MultiOwner, join_game, empty_pile) 
+%		end) || _ <- lists:seq(1, 1) ],
+%
+%   [ wait_for(C) || C <- Clients ].
+
+%test_match_me_random_reveal() ->
+%    MultiOwner = self(),
+%    process_flag(trap_exit, true),
+%    Clients = [ proc_lib:spawn_link(fun() -> 
+%			 start_test_game_t(MultiOwner, match_me, normal)
+%		end) || _ <- lists:seq(1, 1) ],
+%
+ %   [ wait_for(C) || C <- Clients ].
+
+test_social_actions() ->
+    MultiOwner = self(),
+    process_flag(trap_exit, true),
+    Clients = [ proc_lib:spawn_link(fun() -> 
+			 start_test_game_t(MultiOwner, test_social_actions, normal) 
+                end) || _ <- lists:seq(1, 1) ],
+
+    [ wait_for(C) || C <- Clients ].
+
+start_test_game_t(MultiOwner, CreateMode, RevealMode) ->
+    process_flag(trap_exit, true),
+    Ids = [<<"radistao">>,<<"paul">>,<<"kunthar">>,<<"gleber">>],
+    Host = localhost,
+    Port = ?LISTEN_PORT,
+    Owner = self(),
+    Rematch = 0,
+    case CreateMode of
+
+        join_game_player_replacement ->
+
+            ReplacementId = <<"kate">>,
+
+            {ok, GameId, _} = game_manager:create_table(game_okey, 
+					 [{allow_replacement, true}, {deny_robots, true},
+					  {sets, 1}, {rounds, 2}], Ids),
+
+            Clients0 = [ proc_lib:spawn_link(fun() -> 
+				  init_with_join_game(Owner, Host, Port, GameId, Id, 0, RevealMode) 
+		       end) || Id <- Ids ],
+
+            Replacement = proc_lib:spawn_link(fun() ->
+				   init_with_match_me_replaceable(Owner, Host, Port, ReplacementId, 0, RevealMode) 
+			  end),
+
+            ?INFO("replacement players pid: ~p", [Replacement]),
+
+            Others = [],
+            Clients = Clients0 ++ [Replacement] ++ Others,
+            Orders = [#bo{pid = 2, event = game_ended, event_no = 1, order = stop}];
+
+        join_game_robots ->
+
+            Robots = [robot,robot,robot],
+            Humans = [<<"paul">>],%<<"radistao">>, <<"paul">>],
+            {ok, GameId, _A} = game_manager:create_table(game_okey, [{sets,2}, {rounds,20},{game_mode,color}], Robots ++ Humans),
+
+	    ?INFO("created table for Okey Game: gameid ~p",[{GameId,_A}]),
+
+            Clients = [ proc_lib:spawn_link(fun() -> 
+				 timer:sleep(crypto:rand_uniform(0, 10)),
+                                 init_with_join_game(Owner, Host, Port, GameId, Id, Rematch, RevealMode)
+                        end) || Id <- Humans ],
+
+            ?INFO("Human Pids: ~p",[Clients]),
+            
+            Orders = [];
+
+        join_game ->
+
+            {ok, GameId, _} = game_manager:create_table(game_okey, [{game_mode, color}, {sets, 2}, {rounds, 2}], Ids),
+
+            Orders = [],
+
+            Clients = [ proc_lib:spawn_link(fun() -> 
+				 timer:sleep(crypto:rand_uniform(0, 300)),
+                                 init_with_join_game(Owner, Host, Port, GameId, Id, Rematch, RevealMode)
+                        end) || Id <- Ids ];
+                        
+
+        join_game_new ->
+
+            {ok, GameId, _} = game_manager:create_table(game_okey, [{game_mode, color}, {sets, 2}, {rounds, 2}], Ids),
+
+            Clients = [ proc_lib:spawn_link(fun() -> 
+			         timer:sleep(crypto:rand_uniform(0, 300)),
+                                 init_with_join_game(Owner, Host, Port, GameId, Id, Rematch, RevealMode)
+                        end) || Id <- Ids ],
+
+            Orders = [#bo{pid = 2, event = game_ended, event_no = 2, order = stop}];
+
+        join_game_countdown ->
+            {ok, GameId, _} = game_manager:create_table(game_okey, [{game_mode, countdown}, {gosterge_finish, true}], Ids),
+            Orders = [#bo{pid = 2, event = game_ended, event_no = 10, order = stop}],
+            Clients = [ proc_lib:spawn_link(fun() -> 
+			      timer:sleep(crypto:rand_uniform(0, 300)),
+                              init_with_join_game(Owner, Host, Port, GameId, Id, Rematch, RevealMode) end) || Id <- Ids ];
+
+        join_game_observer ->
+
+             L = [{true, #'TableInfo'{}}, {false, {error, <<"this_game_is_private">>}}],
+
+            lists:map(fun({Setting, ExpectedAnswer}) ->
+
+                    {ok, GameId, _} = game_manager:create_table(game_okey, [{observers, Setting}, 
+								{game_mode, standard}, {sets, 1}, {rounds, 1}], Ids),
+
+                    Bots = [ proc_lib:spawn_link(fun() -> 
+				  timer:sleep(crypto:rand_uniform(0, 300)),
+                                   init_with_join_game(Owner, Host, Port, GameId, Id, Rematch, RevealMode)
+                             end) || Id <- Ids ],
+
+                    Observers = [],  % Observers = [<<"sustel">>, <<"kate">>],
+
+                    Bots2 = [ proc_lib:spawn_link(fun() -> 
+			         timer:sleep(0), 
+                                 init_with_join_game_observe(Owner, Host, Port, GameId, Id, RevealMode, ExpectedAnswer) 
+                                end) || Id <- Observers ],
+
+                   conductor([], Bots ++ Bots2)
+
+            end, L),
+
+            Orders = [],
+            Clients = [];
+
+        test_social_actions ->
+            SenderFun = fun send_and_receive_social_action/2,
+            ReceiverFun = fun receive_social_action/3,
+            MyIds = [<<"radistao">>,<<"paul">>,<<"kunthar">>,<<"gleber">>],
+            A = #bo{pid = 1, event = got_hand, event_no = 1, order = {do_and_continue, SenderFun, [<<"paul">>]}},
+            B = #bo{pid = 2, event = got_hand, event_no = 1, order = {do_and_continue, ReceiverFun, [<<"radistao">>, <<"paul">>]}},
+            C = #bo{pid = 3, event = got_hand, event_no = 1, order = {do_and_continue, ReceiverFun, [<<"radistao">>, <<"paul">>]}},
+            D = #bo{pid = 4, event = got_hand, event_no = 1, order = {do_and_continue, ReceiverFun, [<<"radistao">>, <<"paul">>]}},
+            Orders = [A, B, C, D],
+            Clients = [ proc_lib:spawn_link(fun() -> init_with_match_me(Owner, Host, Port, Id, 0, RevealMode)
+                                            end) || Id <- MyIds ];
+
+        match_me ->
+            Orders = [],
+            Clients = [ proc_lib:spawn_link(fun() -> timer:sleep(crypto:rand_uniform(0, 300)),
+                                                     init_with_match_me(Owner, Host, Port, Id, Rematch, RevealMode)
+                                            end) || Id <- Ids ];
+
+        match_me_disconnect ->
+            DisconnectOrNot = kakamath:variate(lists:duplicate(3, true) ++
+                                                   lists:duplicate(4, false)),
+            IdsDisconnect = [<<"radistao">>,<<"paul">>,<<"kunthar">>,<<"gleber">>,<<"sustel">>,<<"peter">>,<<"christian">>],
+            Tasks = kakamath:variate(lists:zip(DisconnectOrNot, IdsDisconnect)),
+            Orders = [],
+            Clients = [ proc_lib:spawn_link(fun() ->
+                                  timer:sleep(crypto:rand_uniform(0, 300)),
+                                  init_with_match_me_disconnect(Owner, Host, Port, Id, Rematch, RevealMode, Play)
+                        end) || {Play, Id} <- Tasks ]
+
+    end,
+    conductor(Orders, Clients),
+    MultiOwner ! {self(), game_ended}.
+
+init_with_join_game(Owner, Host, Port, GameId, OwnId, Rematch, Mode) ->
+    put(mode, Mode),
+    log(started),
+    S1 = ?TCM:connect(Host, Port),
+    TT = ?TEST_TOKEN,
+    #'PlayerInfo'{id = Id} = ?TCM:call_rpc(S1, #session_attach_debug{token = TT, id = OwnId}) ,
+    log(connected),
+    Stats = ?TCM:call_rpc(S1, #get_player_stats{game_type = <<"okey">>, player_id = Id}) ,
+    #'PlayerOkeyStats'{} = Stats,
+    #'TableInfo'{game = Atom} = ?TCM:call_rpc(S1, #join_game{game = GameId}) ,
+    <<"okey">> = Atom,
+    State = #state{conn = S1, gid = GameId, uid = Id, acker_fun = standard_acker(Owner)},
+    play_set(State, Rematch),
+    log(finished),
+    ok = ?TCM:flush_events(?FLUSH_DELAY),
+    ok = ?TCM:close(S1).
+
+init_with_join_game_observe(_Owner, Host, Port, GameId, OwnId, Mode, EJoinResult) ->
+    put(mode, Mode),
+    log(started),
+    S1 = ?TCM:connect(Host, Port),
+    TT = ?TEST_TOKEN,
+    #'PlayerInfo'{id = Id} = ?TCM:call_rpc(S1, #session_attach_debug{token = TT, id = OwnId}) ,
+    log(connected),
+    Stats = ?TCM:call_rpc(S1, #get_player_stats{game_type = <<"okey">>, player_id = Id}) ,
+    #'PlayerOkeyStats'{} = Stats,
+    JR = ?TCM:call_rpc(S1, #join_game{game = GameId}) ,
+    ?INFO("JR: ~p, expected result: ~p", [JR, EJoinResult]),
+    true = cmpr(EJoinResult, JR),
+    log(finished),
+    ok = ?TCM:flush_events(?FLUSH_DELAY),
+    ok = ?TCM:close(S1).
+
+init_with_match_me(Owner, Host, Port, OwnId, Rematch, Mode) ->
+    put(mode, Mode),
+    log(started),
+    S1 = ?TCM:connect(Host, Port),
+    TT = ?TEST_TOKEN,
+    #'PlayerInfo'{id = Id} = ?TCM:call_rpc(S1, #session_attach_debug{token = TT, id = OwnId}) ,
+    log(connected),
+    Stats = tc:call_rpc(S1, #get_player_stats{game_type = <<"okey">>, player_id = Id}),
+    #'PlayerOkeyStats'{level = _Level} = Stats,
+    ZZZ = tc:call_rpc(S1, #match_me{game_type = <<"okey">>}) ,
+    GameId =
+        receive
+            #'game_matched'{} = Rec->
+                log(matched),
+                Rec#game_matched.game
+        after ?BT -> erlang:error({server_timeout, self(), ZZZ, "game_matched"})
+        end,
+    State = #state{conn = S1, gid = GameId, uid = Id, acker_fun = standard_acker(Owner)},
+    play_set(State, Rematch).
+
+init_with_match_me_replaceable(_Owner, Host, Port, OwnId, _Rematch, RevealMode) ->
+    put(mode, RevealMode),
+    log(started),
+    S1 = ?TCM:connect(Host, Port),
+    #'PlayerInfo'{id = Id} = ?TCM:call_rpc(S1, #session_attach_debug{token = ?TEST_TOKEN, id = OwnId}),
+    log(connected),
+    Stats = ?TCM:call_rpc(S1, #get_player_stats{game_type = <<"okey">>, player_id = Id}),
+    #'PlayerOkeyStats'{level = _Level} = Stats,
+    ?INFO("match me replaceable 2", []),
+    ZZZ = ?TCM:call_rpc(S1, #match_me{game_type = <<"okey">>}),
+    ?INFO("match me replaceable status: ~p", [ZZZ]),
+    GameId =
+        receive
+            #'game_matched'{} = Rec ->
+                ?INFO("got game_matched: ~p", [Rec]),
+                true = Rec#game_matched.is_replacing,
+                log(matched),
+                Rec#game_matched.game
+        after ?BT -> erlang:error({server_timeout, self(), ZZZ, "game_matched"})
+        end,
+    State = #state{conn = S1, gid = GameId, uid = Id},
+    ?INFO("picking up game", []),
+    pickup_game(State).
+
+init_with_match_me_disconnect(Owner, Host, Port, OwnId, Rematch, Mode, false) ->
+    ?INFO("player discon normal", []),
+    init_with_match_me(Owner, Host, Port, OwnId, Rematch, Mode);
+init_with_match_me_disconnect(_Owner, Host, Port, OwnId, _Rematch, _Mode, true) ->
+    ?INFO("player discon bad", []),
+    S1 = ?TCM:connect(Host, Port),
+    TT = ?TEST_TOKEN,
+    #'PlayerInfo'{id = Id} = ?TCM:call_rpc(S1, #session_attach_debug{token = TT, id = OwnId}) ,
+    log(connected),
+    Stats = ?TCM:call_rpc(S1, #get_player_stats{game_type = <<"okey">>, player_id = Id}) ,
+    #'PlayerOkeyStats'{level = _Level} = Stats,
+    _ZZZ = ?TCM:call_rpc(S1, #match_me{game_type = <<"okey">>}) ,
+    log(disconnected),
+    ok = ?TCM:flush_events(?FLUSH_DELAY),
+    ok = ?TCM:close(S1).
+
+pickup_game(S0) ->
+    Id = S0#state.uid,
+    ?INFO("ID: ~p, waiting for #okey_game_info", [Id]),
+    log(game_picked_up),
+    GI = receive
+             #'game_event'{event = <<"okey_game_info">>, args = Args0} ->
+                 A0 = api_utils:to_known_record(okey_game_info, Args0),
+                 ?INFO("A0: ~p", [A0]),
+                 A0
+         after ?BT -> erlang:error({server_timeout, "game_rematched"})
+         end,
+    ?INFO("ID: ~p, waiting for #okey_game_player_state", [Id]),
+    GS = receive
+             #'game_event'{event = <<"okey_game_player_state">>, args = Args} ->
+                 A = api_utils:to_known_record(okey_game_player_state, Args),
+                 ?INFO("A: ~p", [A]),
+                 A
+         after ?BT -> erlang:error({server_timeout, "game_rematched"})
+         end,
+    ?INFO("picking up the game", []),
+    NTI = GS#okey_game_player_state.next_turn_in,
+    true = is_integer(NTI) orelse NTI =:= <<"infinity">>,
+    SS = #'OkeySetState'{
+      round_cur = GS#okey_game_player_state.current_round,
+      round_max = GI#okey_game_info.rounds,
+      set_cur = GI#okey_game_info.set_no,
+      set_max = GI#okey_game_info.sets
+     },
+    Hand = GS#okey_game_player_state.tiles,
+    State = S0#state{
+              mode = GI#okey_game_info.game_type,
+              set_state = SS,
+              hand = Hand,
+              gosterge = GS#okey_game_player_state.gosterge
+             },
+    Turn = GS#okey_game_player_state.whos_move,
+    GameState = GS#okey_game_player_state.game_state,
+    ?INFO("ID: ~p, picking up the game. Turn: ~p, GameState: ~p", [Id, Turn, GameState]),
+    case {Turn, GameState} of
+        {_, <<"game_finished">>} ->
+            ?INFO("init bot finished", []),
+            okey_client_rematch(State),
+            play_set(State, 0);
+        {_, <<"do_okey_ready">>} ->
+            ?INFO("init bot wait", []),
+            loop_and_restart(State);
+        {Id, <<"do_okey_take">>} ->
+            ?INFO("init bot: move both", []),
+            State1 = do_turn(State, 1),
+            okey_client_loop(State1);
+        {Id, <<"do_okey_discard">>} ->
+            ?INFO("init bot: move discard only", []),
+            {TryDiscard, _} = draw_random(Hand),
+            Hand1 = do_discard(State, Hand, TryDiscard, 1),
+            okey_client_loop(State#state{hand = Hand1});
+        {_, <<"do_okey_challenge">>} ->
+            ?INFO("init bot: challenge", []),
+            do_challenge(State),
+            okey_client_loop(State#state{hand = Hand});
+        {_, _} ->
+            ?INFO("init bot: not bot's move", []),
+            okey_client_loop(State#state{hand = Hand})
+    end.
+
+loop_and_restart(#state{set_state = #'OkeySetState'{round_max = MR, round_cur = RC}})
+  when RC > MR, MR /= -1 ->
+    ok;
+loop_and_restart(State) ->
+    #state{set_state = #'OkeySetState'{round_cur = RC}} = State,
+    {Hand0, Gosterge0} = get_hand(State),
+    ?INFO("Human {Hand,Gosterge}: ~p",[{Hand0,Gosterge0}]),
+    LoopRes = okey_client_loop(State#state{hand = Hand0, gosterge = Gosterge0}),
+    ?INFO("Human LoopRes: ~p",[LoopRes]),
+    case LoopRes of
+        <<"done">> ->
+            ?INFO("ID: ~p, next action done", [State#state.uid]),
+            ok;
+        <<"next_set">> ->
+            say_ready(State),
+            ?INFO("ID: ~p, next action next_set", [State#state.uid]),
+            ok;
+        <<"next_round">> ->
+            say_ready(State),
+            ?INFO("ID: ~p, next action next_round", [State#state.uid]),
+            State1 = State#state{set_state = #'OkeySetState'{round_cur = RC + 1}},
+            check_ack(State1, game_ended, fun loop_and_restart/1, [State1])
+    end.
+
+play_set(State0, Rematch) ->
+%    ?INFO("ID: ~p, sets: start", [State0#state.uid]),
+    Id = State0#state.uid,
+    State = init_okey_tour(State0),
+    Rounds = (State#state.set_state)#'OkeySetState'.round_max,
+    SetNo = (State#state.set_state)#'OkeySetState'.set_cur,
+    Sets = (State#state.set_state)#'OkeySetState'.set_max,
+    ?INFO("ID: ~p Start playing ~p/~p set of ~p rounds",[State0#state.uid,SetNo,Sets,Rounds]),
+    loop_and_restart(State),
+    ?INFO("ID: ~p Finish playing ~p/~p set of ~p rounds",[State0#state.uid,SetNo,Sets,Rounds]),
+    case {SetNo, Rematch} of
+        {X, 0} when X == Sets ->
+            ?INFO("last set, no rematch", []),
+            get_series_ended(Id),
+            ok;
+        {X, _} when X == Sets ->
+            ?INFO("ID: ~p, last set, do rematch", [State0#state.uid]),
+            okey_client_rematch(State),
+            play_set(State, Rematch-1);
+        _ ->
+            ?INFO("ID: ~p,  play next set", [State0#state.uid]),
+            play_set(State, Rematch)
+    end.
+
+init_okey_tour(State) ->
+    GameId = State#state.gid,
+    receive
+        #'game_event'{event = <<"okey_game_info">>, args = Args, game = GI} ->
+%           ?INFO("CLIENT TOUR STARTED okey_game_info:~n ~p", [Args]),
+            GT = proplists:get_value(game_type, Args),
+            Rounds = proplists:get_value(rounds, Args),
+            Sets = proplists:get_value(sets, Args),
+            SetNo = proplists:get_value(set_no, Args),
+            true = (GI =/= <<"undefined">>),
+            true = (Sets =/= <<"undefined">>),
+            true = (SetNo =/= <<"undefined">>),
+            true = (GT =/= <<"undefined">>),
+            true = (Rounds =/= <<"undefined">>),
+            true = (GI =:= GameId),
+            SS = #'OkeySetState'{
+              round_cur = 1,
+              round_max = Rounds,
+              set_cur = SetNo,
+              set_max = Sets
+             },
+            log(game_info),
+            State#state{set_state = SS, mode = GT}
+    after ?BT ->
+            ?INFO("ERROR: ~p", [{server_timeout, "game_event:okey_game_info"}]),
+            erlang:error({server_timeout, "game_event:okey_game_info"})
+    end.
+
+get_series_ended(Id)->
+    ?INFO("ID: ~p; waiting for okey_series_ended", [Id]),
+    receive
+        #game_event{event = <<"okey_series_ended">>} ->
+            ?INFO("ID: ~p CLIENT SERIES ENDED", [Id]),
+            log(tour_ended)
+    after ?BT -> erlang:error({server_timeout, "okey_series_ended"})
+    end.
+
+okey_client_rematch(State) ->
+    S1 = State#state.conn, GameId = State#state.gid, Id = State#state.uid,
+    get_series_ended(Id),
+    RematchR = ?TCM:call_rpc(S1, #rematch{game = GameId}) ,
+    ?INFO("ID: ~p; rematch result: ~p", [Id, RematchR]),
+    <<"ok">> = RematchR,
+    receive
+        #game_rematched{game = GameId} ->
+            ?INFO("#game_rematched{game = GameId}", []),
+            log(game_rematched)
+    after ?BT -> erlang:error({server_timeout, "game_rematched"})
+    end.
+
+say_ready(State) ->
+    S1 = State#state.conn,
+    GameId = State#state.gid,
+    <<"ok">> = ?TCM:call_rpc(S1, #game_action{game = GameId, action = okey_ready, args = []}) .
+
+get_hand(State) ->
+    S1 = State#state.conn,
+    GameId = State#state.gid,
+    receive
+        #'game_event'{event = <<"okey_game_started">>, args = Args} = _Msg ->
+            log(game_started),
+            MH = proplists:get_value(tiles, Args),
+            G = proplists:get_value(gosterge, Args),
+            CR = proplists:get_value(current_round, Args),
+            CS = proplists:get_value(current_set, Args),
+
+            ((MH == undefined) orelse (G == undefined)) andalso erlang:error(cant_get_params_of_hand),
+
+            ?INFO("Human Round/Set: ~p/~p", [CR,CS]),
+            HasGosterge = lists:member(G, MH),
+            HasGostergeServer = ?TCM:call_rpc(S1, #game_action{game = GameId, action = okey_has_gosterge, args = []}),
+            ?INFO("HasGostergeServer: ~p",[HasGostergeServer]),
+            HasGosterge = HasGostergeServer,
+            check_ack(State, got_hand, fun() -> ok end, []),
+            {MH, G}
+    after ?BT ->
+            erlang:error({server_timeout, "game_event:okey_game_started"})
+    end.
+
+check_ack(State = #state{acker_fun = F}, Event, ContinueFun, Args) ->
+    A = F(Event),
+    case A of
+        continue ->
+            apply(ContinueFun, Args);
+        {sleep, T} ->
+            timer:sleep(T),
+            apply(ContinueFun, Args);
+        {do_and_continue, What, With} ->
+            apply(What, [State] ++ With),
+            apply(ContinueFun, Args);
+        stop ->
+            ?INFO("ID: ~p, Pid: ~p, check_ack. STOPPING THE BOT!!!", [State#state.uid, self()]),
+            erlang:error({terminate, acker_stop})
+    end.
+
+validate_hand(S, GameId, Hand) ->
+    case ?TCM:call_rpc(S, #game_action{
+                         game = GameId,
+                         action = okey_debug,
+                         args = []})  of
+        {error, Reason} ->
+            ?INFO("dying in fire. Reason: ~p", [Reason]),
+            erlang:error(die_in_fire);
+        ServerHand ->
+            Res = game_okey:is_same_hand(ServerHand, Hand),
+            case Res of
+                false ->
+                    ?INFO("validation failed: ~p =/= ~p", [Hand, ServerHand]);
+                _ ->
+                    ok
+            end,
+            Res
+    end.
+
+okey_client_loop(State) ->
+    Id = State#state.uid,
+    receive
+        #'game_event'{event = <<"okey_next_turn">>, args = Args} ->
+            Timeout = crypto:rand_uniform(0, ?SIM_DELAY),
+            State1 = case {proplists:get_value(player, Args), proplists:get_value(can_challenge, Args)} of
+                         {Id, false} ->
+                             do_turn(State, Timeout);
+                         {_OtherId, _} ->
+                             State
+                     end,
+            okey_client_loop(State1);
+        #'game_event'{event = <<"okey_player_ready">>} ->
+            okey_client_loop(State);
+        #'game_event'{event = <<"okey_player_has_gosterge">>, args = _Args} ->
+            okey_client_loop(State);
+        #'game_event'{event = <<"okey_tile_taken">>, args = Args} ->
+            case proplists:get_value(revealed, Args) of
+                null ->
+                    NewH = proplists:get_value(pile_height, Args),
+                    okey_client_loop(State#state{pileh = NewH});
+                _ ->
+                    okey_client_loop(State)
+            end;
+        #'game_event'{event = <<"okey_tile_discarded">>} ->
+            okey_client_loop(State);
+        #'game_event'{event = <<"okey_revealed">>} ->
+            do_challenge(State),
+            okey_client_loop(State);
+        #'game_event'{event = <<"okey_series_ended">>, args = Args} ->
+            S = State#state.conn,
+            ?TCM:rpc(S, #logout{}),
+            NextAction = proplists:get_value(next_action, Args),
+            NextAction;
+        #'game_event'{event = <<"okey_round_ended">>, args = Args} ->
+            okey_round_ended_checks(Args, State),
+            GS = proplists:get_value(good_shot, Args),
+            Reason = proplists:get_value(reason, Args),
+            NextAction = proplists:get_value(next_action, Args),
+            ?INFO("ID: ~p game ended, good_shot: ~p, reason: ~p", [Id, GS, Reason]),
+            NextAction;
+        #player_left{} ->
+            okey_client_loop(State);
+        #'game_event'{event = <<"game_rematched">>, args = _Args} ->
+            okey_client_loop(State);
+        #'game_event'{event = <<"okey_game_started">>, args = _Args} ->
+            okey_client_loop(State);
+%            erlang:error({protocol_breach, okey_series_ended});
+        #'game_event'{event = <<"okey_game_info">>, args = _Args} ->
+            okey_client_loop(State);
+%            erlang:error({protocol_breach, okey_game_info});
+        #'game_event'{event = <<"player_left">>} ->
+            okey_client_loop(State);
+        #'game_event'{args = Args} ->
+            NextAction = proplists:get_value(next_action, Args),
+            NextAction;
+        _Msg ->
+            ?INFO("the msg: ~p", [_Msg]),
+            erlang:error({bot_received_unrecognized_message, _Msg})
+    after ?BT ->
+            log(server_timeouted),
+            erlang:error({server_timeout, "okey_client_loop_timeout"})
+    end.
+
+do_turn(State, Timeout) ->
+    {Timeouted, Hand1} = do_take(State, Timeout),
+    true = is_list(Hand1),
+    {TryDiscard, WinningHand} = draw_random(Hand1),
+    Pile0Height = State#state.pileh,
+    FHand = case {Timeouted, is_revealing(Pile0Height)} of
+                {false, true} ->
+                    do_reveal(State, WinningHand, TryDiscard);
+                {false, false} ->
+                            do_discard(State, Hand1, TryDiscard, Timeout);
+                {true, _} ->
+                    Hand1
+            end,
+    State#state{hand = FHand}.
+
+do_challenge(State) ->
+    GameId = State#state.gid,
+    S = State#state.conn,
+    ZZZ = ?TCM:call_rpc(S, #game_action{
+                         game = GameId,
+                         action = okey_challenge,
+                         args = [ {challenge, random_bool(0.2)} ]}),
+    ?INFO("ID: ~p challenge result: ~p", [State#state.uid, ZZZ]),
+    ok.
+
+do_take(State, Timeout) ->
+    S = State#state.conn,
+    GameId = State#state.gid,
+    Hand = State#state.hand,
+    {is_list, true} = {is_list, is_list(Hand)},
+    receive
+        #'game_event'{event = <<"okey_turn_timeout">>, args = Args} ->
+                                                % ?INFO("ID: ~p I timeouted on take", [Id]),
+            TileT = proplists:get_value(<<"tile_taken">>, Args),
+            TileD = proplists:get_value(<<"tile_discarded">>, Args),
+            Hand1 = lists:delete(TileD, Hand),
+            {true, [TileT | Hand1]}
+    after Timeout ->
+            Pile = crypto:rand_uniform(0, 2),
+            case ?TCM:call_rpc(S, #game_action{
+                               game = GameId,
+                               action = okey_take,
+                               args = [ {pile, Pile} ]}) of
+                #'OkeyPiece'{} = Tosh ->
+                    MyHand = [Tosh | Hand],
+                                                % ?INFO("ID: ~p Take tosh in ~p pile! Get: ~p", [Id, Pile, Tosh]),
+                    {false, MyHand};
+                {error, <<"cant_take_do_discard">>} ->
+                                                % ?INFO("ID: ~p Has 15 items in hand. 15=~p", [Id, length(Hand)]),
+                    {false, Hand};
+                {error, <<"game_has_already_ended">>} = Err ->
+                    case State#state.mode of
+                        <<"countdown">> ->
+                            {false, Hand};
+                        _ ->
+                            ?INFO("ID: ~p; mode:~p; failed take with msg ~p",
+                                [State#state.uid, State#state.mode, Err]),
+                            erlang:error(failed_take)
+                    end;
+                Err ->
+                    ?INFO("ID: ~p failed take with msg ~p", [State#state.uid, Err]),
+                    erlang:error(failed_take)
+            end
+    end.
+
+do_discard(State, Hand, Item, Timeout) ->
+    S = State#state.conn,
+    GameId = State#state.gid,
+    Id = State#state.uid,
+    receive
+        #'game_event'{event = <<"okey_tile_discarded">>, args = Args} ->
+            case proplists:get_value(<<"player">>, Args) of
+                Id ->
+                    Tile = proplists:get_value(<<"player">>, Args),
+                    lists:delete(Tile, Hand)
+            end
+    after Timeout ->
+            Hand1 = lists:delete(Item, Hand),
+            _Res = ?TCM:call_rpc(S, #game_action{game = GameId, action = <<"okey_discard">>,
+                                               args = [ {tile, Item} ]}),
+            Hand1
+    end.
+
+do_reveal(State, Hand, Item) ->
+    S = State#state.conn,
+    GameId = State#state.gid,
+    ?TCM:call_rpc(S, #game_action{
+                    game = GameId,
+                    action = okey_reveal,
+                    args = [ {discarded, Item},
+                             {hand, Hand}
+                            ]}),
+    Hand.
+
+draw_random([One]) ->
+    {One, []};
+draw_random(List) ->
+    {is_list, true} = {is_list, is_list(List)},
+    Pos = crypto:rand_uniform(1, length(List)),
+    Joker = lists:nth(Pos, List),
+    ResList = lists:delete(Joker, List),
+    {Joker, ResList}.
+
+is_revealing(undefined) ->
+    false;
+is_revealing(PileHeight) ->
+    Mode = case get(mode) of
+               undefined -> normal;
+               A -> A
+           end,
+    MaxProb = case get(reveal_probability) of
+                  undefined -> 700.0;
+                  Value -> Value
+              end,
+    is_revealing(PileHeight, MaxProb, Mode).
+
+is_revealing(PileHeight, MaxProb, normal) ->
+    HS = game_okey:hand_size(),
+    MaxHeight = ((HS - 1) * 4 * 2 + 2.0) - (HS * 4),
+    X = erlang:abs((MaxHeight - PileHeight) / MaxHeight),
+    Prob = (X * X * X * X * X * X) * MaxProb,
+    Point = crypto:rand_uniform(1, 1000),
+    Prob > Point;
+is_revealing(_PileHeight, _, empty_pile) ->
+    false.
+
+wait_for(_C) ->
+    receive
+        {_, game_ended} ->
+            ?INFO("client: game ended");
+        {'EXIT', _, normal} = M ->
+            ?INFO("client: normal ending: ~p", [M]);
+        {'EXIT', _C, Reason} = M ->
+            ?INFO("client: FAILURE message: ~p", [{M,_C,Reason}]),
+            erlang:error(Reason);
+        M ->
+            ?INFO("client: unrecognized result: ~p", [M]),
+            erlang:error(M)
+    end.
+
+%% checks
+okey_round_ended_checks(Args, State) ->
+    Score = proplists:get_value(score, Args),
+    Mode = State#state.mode,
+    {newer_go_below_10, true} =
+        {newer_go_below_10, Mode =/= <<"countdown">> orelse Score > -1}.
+
+%% Tests
+reveal_probability_loop(0) ->
+    erlang:error(reveal_probability_loop_failed);
+reveal_probability_loop(N) ->
+    case is_revealing(N) of
+        true ->
+            ok;
+        false ->
+            reveal_probability_loop(N-1)
+    end.
+reveal_probability_loop_test() ->
+    put(reveal_probability, 1000.0),
+    reveal_probability_loop(50).
+log(Msg) ->
+    ?TCM:log(Msg).
+
+random_bool(Prob) ->
+    Point = crypto:rand_uniform(0, 1000),
+    Prob*1000 > Point.
+
+standard_acker(Owner) ->
+    Self = self(),
+    Ref = make_ref(),
+    fun(Event) ->
+            E = {event, Ref, Self, Event},
+            ?INFO("standard acker. ~p ! ~p", [Owner, E]),
+            Owner ! E,
+            receive
+                {ARef, Res} when Ref == ARef ->
+                    ?INFO("standard acker got '~p' answer on question ~p", [Res,{Owner, E}]),
+                    Res
+            end
+    end.
+
+conductor(Orders, Clients) ->
+    ?INFO("conductor init", []),
+    Pairs = lists:zip(lists:seq(1, length(Clients)), Clients),
+    Orders2 = lists:map(fun(O) ->
+                                {_, P} = lists:keyfind(O#bo.pid, 1, Pairs),
+                                O#bo{pid = P}
+                        end, Orders),
+    conductor(Orders2, Clients, []).
+conductor(_Orders, [], _Events) ->
+    ?INFO("conductor stop", []),
+    ok;
+conductor(Orders, Clients, Events) ->
+    receive
+        {event, Ref, Pid, Event} = _E ->
+            E2 = update_events(Events, Pid, Event),
+            Z = match_event(Orders, E2, Pid, Event),
+            case Z of
+                #bo{order = Order} ->
+                    Pid ! {Ref, Order};
+                _ ->
+                    Pid ! {Ref, continue}
+            end,
+            conductor(Orders, Clients, E2);
+        {C, game_ended} ->
+            ?INFO("conductor: game ended", []),
+            conductor(Orders, lists:delete(C, Clients), Events);
+        {'EXIT', C, normal} = M ->
+            ?INFO("conductor: normal ending: ~p", [M]),
+            conductor(Orders, lists:delete(C, Clients), Events);
+        {'EXIT', C, {terminate, acker_stop}} = M ->
+            ?INFO("conductor: removing client ~p because of ~p", [C, M]),
+            conductor(Orders, lists:delete(C, Clients), Events);
+        {'EXIT', _C, Reason} = M ->
+            ?INFO("conductor: FAILURE message: ~p", [{M,_C,Reason}]),
+            erlang:error(Reason);
+        M ->
+            ?INFO("conductor: unrecognized msg from client: ~p", [M]),
+            erlang:error(M)
+    end.
+
+update_events(Events, Pid, Event) ->
+    F = [ X || {APid, AEvent, _} = X <- Events, Pid == APid, Event == AEvent ],
+    case F of
+        [] ->
+            [{Pid, Event, 1} | Events];
+        [{_,_,C} = X] ->
+            L1 = lists:delete(X, Events),
+            [{Pid, Event, C+1} | L1]
+    end.
+
+match_event(Orders, Events, Pid, Event) ->
+    Z = [ X || {APid, AEvent, _} = X <- Events, Pid == APid, Event == AEvent ],
+    [{_, _, C}] = Z,
+    Matched = [ Y || Y = #bo{pid = P, event = E} <- Orders, P == Pid, E == Event ],
+    case Matched of
+        [BO = #bo{event_no = EN} | _] when EN == C ->
+            BO;
+        _X ->
+            false
+    end.
+
+send_and_receive_social_action(State, Recipient) ->
+    GID = State#state.gid,
+    ?TCM:call_rpc(State#state.conn, #social_action{game = GID, type = 0, recipient = Recipient}),
+    receive_social_action(State, State#state.uid, Recipient).
+
+receive_social_action(_State, Sender, Recipient) ->
+    receive
+        #social_action_msg{type = Type, initiator = I, recipient = R} ->
+            true = Type == 0,
+            true = Sender == I,
+            true = Recipient == R
+    end.
+
+cmpr(X, Y) when element(1, X) == element(1, Y) ->
+    true;
+cmpr(B, B) ->
+    true;
+cmpr(_, _) ->
+    false.
+
+

+ 89 - 0
apps/server/src/okey_sup.erl

@@ -0,0 +1,89 @@
+-module(okey_sup).
+-behaviour(supervisor).
+-export([start_link/0, stop/0]).
+-include_lib("server/include/conf.hrl").
+-include_lib("server/include/log.hrl").
+-export([init/1, start/0, start_game/3]).
+-define(SERVER, ?MODULE).
+
+-define(CROWD_STANDALONE_3PL_NUM, 15).
+
+start() -> supervisor:start({local, ?SERVER}, ?MODULE, []).
+start_link() -> supervisor:start_link({local, ?SERVER}, ?MODULE, []).
+stop() -> exit(?SERVER, shutdown).
+start_game(Mod,Par,GameId) -> 
+    ?INFO("OKEY SUP START CHILD"),
+    Restart = transient,
+    Shutdown = 200,
+    ChildSpec = {GameId, {Mod, start_link, Par}, Restart, Shutdown, worker, [Mod]},
+    supervisor:start_child(?MODULE,ChildSpec).
+
+init([]) ->
+    ?INFO("OKEY SUP STARTED"),
+    RestartStrategy = one_for_one,
+    MaxRestarts = 1,
+    MaxSecondsBetweenRestarts = 600,
+    SupFlags = {RestartStrategy, MaxRestarts, MaxSecondsBetweenRestarts},
+    Specs = okey_standalone_specs(?CROWD_STANDALONE_3PL_NUM, 3),
+    {ok, {SupFlags, Specs}}.
+
+
+okey_standalone_specs(GamesNum, VirtUsersPerTable) ->
+    VirtualUsers = nsg_crowd_lib:virtual_users(),
+    if length(VirtualUsers) < VirtUsersPerTable ->
+           [];
+       true ->
+           F = fun(_) ->
+                       GameId = game_manager:gen_game_id(),
+                       GameName = "Okey/Crowd game - " ++ erlang:integer_to_list(GameId),
+                       Users = nsg_crowd_lib:random_users(VirtUsersPerTable, VirtualUsers),
+%%%                      Users = [robot, robot, robot],
+                       TableParams = [
+                                      {table_name, ""},
+                                      {mult_factor, 1},
+                                      {slang_allowed, false},
+                                      {observers_allowed, false},
+                                      {tournament_type, standalone},
+                                      {round_timeout, infinity},
+                                      {set_timeout, infinity},
+                                      {speed, fast},
+                                      {game_type, standard},
+                                      {rounds, 10},
+                                      {reveal_confirmation, true},
+                                      {next_series_confirmation, no_exit},
+                                      {pause_mode, normal},
+                                      {social_actions_enabled, true},
+                                      {gosterge_finish_allowed, undefined}
+                                     ],
+                       CommonParams = [{speed, fast},
+                                       {rounds,10},
+                                       {double_points, 1},
+                                       {game_mode,standard},
+                                       {slang, false},
+                                       {observers, false},
+                                       {owner,"maxim"}
+                                      ],
+                       Params = [{game, game_okey},
+                                 {game_mode, standard},
+                                 {game_name, GameName},
+                                 {seats, 4},
+                                 {registrants, Users},
+                                 {initial_points, 0},
+                                 {quota_per_round, 1},
+                                 {kakush_for_winners, 1},
+                                 {kakush_for_loser, 1},
+                                 {win_game_points, 1},
+                                 {mul_factor, 1},
+                                 {table_module, game_okey_ng_table_trn},
+                                 {bot_module, game_okey_bot},
+                                 {bots_replacement_mode, enabled},
+                                 {table_params, TableParams},
+                                 {common_params, CommonParams} %% This data will used for the gproc register
+                                ],
+                       {GameId,
+                        {nsg_trn_standalone, start_link, [GameId, Params]},
+                        _Restart = permanent, _Shutdown = 2000, worker, [nsg_trn_standalone]}
+               end,
+           lists:map(F, lists:seq(1, GamesNum))
+    end.
+

+ 423 - 0
apps/server/src/relay_ng.erl

@@ -0,0 +1,423 @@
+%%% -------------------------------------------------------------------
+%%% Author  : Sergei Polkovnikov <serge.polkovnikov@gmail.com>
+%%% Description : The server for retranslating events of the table.
+%%%
+%%% Created : Feb 25, 2013
+%%% -------------------------------------------------------------------
+
+%% Table -> Relay requests:
+%% {register_player, UserId, PlayerId} -> ok
+%% {unregister_player, PlayerId, Reason} -> ok
+
+%% Table -> Relay messages:
+%% {publish, Event}
+%% {to_client, PlayerId, Event}
+%% {to_subscriber, SubscrId, Event}
+%% {allow_broadcast_for_player, PlayerId}
+%% stop
+
+%% Relay -> Table notifications:
+%% {player_disconnected, PlayerId}
+%% {player_connected, PlayerId}
+%% {subscriber_added, PlayerId, SubscriptionId} - it's a hack to retrive current game state
+
+%% Subscriber -> Relay requests:
+%% {subscribe, Pid, UserId, RegNum} -> {ok, SubscriptionId} | {error, Reason}
+%% {unsubscribe, SubscriptionId} -> ok | {error, Reason}
+
+%% Relay -> Subscribers notifications:
+%% {relay_kick, SubscrId, Reason}
+%% {relay_event, SubscrId, Event}
+
+-module(relay_ng).
+
+-behaviour(gen_server).
+%% --------------------------------------------------------------------
+%% Include files
+%% --------------------------------------------------------------------
+-include_lib("server/include/basic_types.hrl").
+-include_lib("server/include/log.hrl").
+
+%% --------------------------------------------------------------------
+%% External exports
+-export([start/1, start_link/1, table_message/2, table_request/2]).
+-export([subscribe/4, unsubscribe/2, publish/2]).
+
+%% gen_server callbacks
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
+
+-record(state, {subscribers,
+                players,
+                observers_allowed  :: boolean(),
+                table              :: {atom(), pid()},
+                table_mon_ref      :: reference()
+               }).
+
+-record(subscriber,
+        {id           :: reference(),
+         pid          :: pid(),
+         user_id      :: binary(),
+         player_id    :: integer(),
+         mon_ref      :: reference(),
+         broadcast_allowed :: boolean()
+        }).
+
+-record(player,
+        {id           :: integer(),
+         user_id      :: binary(),
+         status       :: online | offline
+        }).
+
+%% ====================================================================
+%% External functions
+%% ====================================================================
+
+
+start(Params) ->
+    gen_server:start(?MODULE, [Params], []).
+
+start_link(Params) ->
+    gen_server:start_link(?MODULE, [Params], []).
+
+subscribe(Relay, Pid, UserId, RegNum) ->
+    client_request(Relay, {subscribe, Pid, UserId, RegNum}).
+
+unsubscribe(Relay, SubscriptionId) ->
+    client_request(Relay, {unsubscribe, SubscriptionId}).
+
+publish(Relay, Message) ->
+    client_message(Relay, {publish, Message}).
+
+table_message(Relay, Message) ->
+    gen_server:cast(Relay, {table_message, Message}).
+
+table_request(Relay, Request) ->
+    table_request(Relay, Request, 5000).
+
+table_request(Relay, Request, Timeout) ->
+    gen_server:call(Relay, {table_request, Request}, Timeout).
+
+client_message(Relay, Message) ->
+    gen_server:cast(Relay, {client_message, Message}).
+
+client_request(Relay, Request) ->
+    client_request(Relay, Request, 5000).
+
+client_request(Relay, Request, Timeout) ->
+    gen_server:call(Relay, {client_request, Request}, Timeout).
+
+%% ====================================================================
+%% Server functions
+%% ====================================================================
+
+%% --------------------------------------------------------------------
+init([Params]) ->
+    PlayersInfo = proplists:get_value(players, Params),
+    ObserversAllowed = proplists:get_value(observers_allowed, Params),
+    Table = {_, TablePid} = proplists:get_value(table, Params),
+    Players = init_players(PlayersInfo),
+    MonRef = erlang:monitor(process, TablePid),
+    {ok, #state{subscribers = subscribers_init(),
+                players = Players,
+                observers_allowed = ObserversAllowed,
+                table = Table,
+                table_mon_ref = MonRef}}.
+
+%% --------------------------------------------------------------------
+handle_call({client_request, Request}, From, State) ->
+    handle_client_request(Request, From, State);
+
+handle_call({table_request, Request}, From, State) ->
+    handle_table_request(Request, From, State);
+
+handle_call(_Request, _From, State) ->
+    Reply = ok,
+    {reply, Reply, State}.
+
+%% --------------------------------------------------------------------
+handle_cast({client_message, Msg}, State) ->
+    ?INFO("RELAY_NG Received client message: ~p", [Msg]),
+    handle_client_message(Msg, State);
+
+handle_cast({table_message, Msg}, State) ->
+    ?INFO("RELAY_NG Received table message: ~p", [Msg]),
+    handle_table_message(Msg, State);
+
+handle_cast(_Msg, State) ->
+    {noreply, State}.
+
+%% --------------------------------------------------------------------
+handle_info({'DOWN', TableMonRef, process, _Pid, _Info},
+            #state{subscribers = Subscribers,
+                   table_mon_ref = TableMonRef} = State) ->
+    ?INFO("RELAY_NG All The parent table is down. "
+          "Disconnecting all subscribers and sutting down.", []),
+    [begin
+         erlang:demonitor(MonRef, [flush]),
+         Pid ! {relay_kick, SubscrId, table_down}
+     end || #subscriber{id = SubscrId, pid = Pid, mon_ref = MonRef}
+                           <- subscribers_to_list(Subscribers)],
+    {stop, normal, State};
+
+handle_info({'DOWN', MonRef, process, _Pid, _Info},
+            #state{subscribers = Subscribers, players = Players,
+                   table = {TableMod, TablePid}} = State) ->
+    case find_subscribers_by_mon_ref(MonRef, Subscribers) of
+        [#subscriber{player_id = undefined, id = SubscrId}] ->
+            NewSubscribers = del_subscriber(SubscrId, Subscribers),
+            {noreply, State#state{subscribers = NewSubscribers}};
+        [#subscriber{player_id = PlayerId, user_id = UserId, id = SubscrId}] ->
+            NewSubscribers = del_subscriber(SubscrId, Subscribers),
+            case find_subscribers_by_player_id(PlayerId, NewSubscribers) of
+                [] ->
+                    ?INFO("RELAY_NG All sessions of player <~p> (~p) are closed. "
+                          "Sending the notification to the table.", [PlayerId, UserId]),
+                    NewPlayers = update_player_status(PlayerId, offline, Players),
+                    TableMod:relay_message(TablePid, {player_disconnected, PlayerId}),
+                    {noreply, State#state{subscribers = NewSubscribers, players = NewPlayers}};
+                _ ->
+                    {noreply, State#state{subscribers = NewSubscribers}}
+            end;
+        [] ->
+            {noreply, State}
+    end;
+
+handle_info(_Info, State) ->
+    {noreply, State}.
+
+%% --------------------------------------------------------------------
+terminate(_Reason, _State) ->
+    ok.
+
+%% --------------------------------------------------------------------
+code_change(_OldVsn, State, _Extra) ->
+    {ok, State}.
+
+%% --------------------------------------------------------------------
+%%% Internal functions
+%% --------------------------------------------------------------------
+
+handle_client_request({subscribe, Pid, UserId, observer}, _From,
+                      #state{observers_allowed = ObserversAllowed,
+                             subscribers = Subscribers} = State) ->
+    if ObserversAllowed ->
+           MonRef = erlang:monitor(process, Pid),
+           SubscrId = erlang:make_ref(),
+           NewSubscribers = store_subscriber(SubscrId, Pid, UserId, undefined, MonRef, true, Subscribers),
+           {reply, ok, State#state{subscribers = NewSubscribers}};
+       true ->
+           {reply, {error, observers_not_allowed}, State}
+    end;
+
+
+handle_client_request({subscribe, Pid, UserId, PlayerId}, _From,
+                      #state{players = Players, subscribers = Subscribers,
+                             table = {TableMod, TablePid}} = State) ->
+    ?INFO("RELAY_NG Subscription request from user ~p, PlayerId: <~p>", [UserId, PlayerId]),
+    case find_player(PlayerId, Players) of
+        {ok, #player{user_id = UserId, status = Status} = P} ->  %% The user id is matched
+            ?INFO("RELAY_NG User ~p is registered as player <~p>", [UserId, PlayerId]),
+            ?INFO("RELAY_NG User ~p player info: ~p", [UserId, P]),
+            MonRef = erlang:monitor(process, Pid),
+            SubscrId = erlang:make_ref(),
+            NewSubscribers = store_subscriber(SubscrId, Pid, UserId, PlayerId, MonRef,
+                                              _BroadcastAllowed = false, Subscribers),
+            NewPlayers = if Status == offline ->
+                                ?INFO("RELAY_NG Notifying the table about user ~p (<~p>).", [PlayerId, UserId]),
+                                TableMod:relay_message(TablePid, {player_connected, PlayerId}),
+                                update_player_status(PlayerId, online, Players);
+                            true ->
+                                ?INFO("RELAY_NG User ~p (<~p>) is already subscribed.", [PlayerId, UserId]),
+                                Players
+                         end,
+            TableMod:relay_message(TablePid, {subscriber_added, PlayerId, SubscrId}),
+            {reply, {ok, SubscrId}, State#state{players = NewPlayers, subscribers = NewSubscribers}};
+        {ok, #player{}=P} ->
+            ?INFO("RELAY_NG Subscription for user ~p rejected. There is another owner of the "
+                  "PlayerId <~p>: ~p", [UserId, PlayerId, P]),
+            {reply, {error, not_player_id_owner}, State};
+        error ->
+            {reply, {error, unknown_player_id}, State}
+    end;
+
+
+handle_client_request({unsubscribe, SubscrId}, _From,
+                      #state{subscribers = Subscribers, players = Players,
+                             table = {TableMod, TablePid}} = State) ->
+    case get_subscriber(SubscrId, Subscribers) of
+        error ->
+            {reply, {error, not_subscribed}, State};
+        {ok, #subscriber{id = SubscrId, mon_ref = MonRef, player_id = undefined}} ->
+            erlang:demonitor(MonRef, [flush]),
+            NewSubscribers = del_subscriber(SubscrId, Subscribers),
+            {reply, ok, State#state{subscribers = NewSubscribers}};
+        {ok, #subscriber{id = SubscrId, mon_ref = MonRef, player_id = PlayerId}} ->
+            erlang:demonitor(MonRef, [flush]),
+            NewSubscribers = del_subscriber(SubscrId, Subscribers),
+            NewPlayers = case find_subscribers_by_player_id(PlayerId, Subscribers) of
+                             [] ->
+                                TableMod:relay_message(TablePid, {player_disconnected, PlayerId}),
+                                update_player_status(PlayerId, offline, Players);
+                             _ -> Players
+                         end,
+            {reply, ok, State#state{subscribers = NewSubscribers,
+                                    players = NewPlayers}}
+    end;
+
+
+handle_client_request(_Request, _From, State) ->
+    {reply, {error, unknown_request}, State}.
+
+%%===================================================================
+
+handle_table_request({register_player, UserId, PlayerId}, _From,
+                     #state{players = Players} = State) ->
+    NewPlayers = store_player(PlayerId, UserId, offline, Players),
+    {reply, ok, State#state{players = NewPlayers}};
+
+
+handle_table_request({unregister_player, PlayerId, Reason}, _From,
+                     #state{players = Players,
+                            subscribers = Subscribers} = State) ->
+    NewPlayers = del_player(PlayerId, Players),
+    UpdSubscribers = find_subscribers_by_player_id(PlayerId, Subscribers),
+    F = fun(#subscriber{id = SubscrId, pid = Pid, mon_ref = MonRef}, Acc) ->
+                erlang:demonitor(MonRef, [flush]),
+                Pid ! {relay_kick, SubscrId, Reason},
+                del_subscriber(SubscrId, Acc)
+        end,
+    NewSubscribers = lists:foldl(F, Subscribers, UpdSubscribers),
+    {reply, ok, State#state{players = NewPlayers,
+                            subscribers = NewSubscribers}};
+
+handle_table_request(_Request, _From, State) ->
+    Reply = ok,
+    {reply, Reply, State}.
+
+%%===================================================================
+%% XXX: Unsecure because of spoofing posibility (a client session can send events by the
+%% name of the table). Legacy.
+handle_client_message({publish, Msg}, #state{subscribers = Subscribers} = State) ->
+    Receipients = subscribers_to_list(Subscribers),
+    [Pid ! {relay_event, SubscrId, Msg} ||
+     #subscriber{id = SubscrId, pid = Pid, broadcast_allowed = true} <- Receipients],
+    {noreply, State};
+
+handle_client_message(_Msg, State) ->
+    {noreply, State}.
+
+%%===================================================================
+
+handle_table_message({publish, Msg}, #state{subscribers = Subscribers} = State) ->
+    ?INFO("RELAY_NG The table publish message: ~p", [Msg]),
+    Receipients = subscribers_to_list(Subscribers),
+    [Pid ! {relay_event, SubscrId, Msg} ||
+     #subscriber{id = SubscrId, pid = Pid, broadcast_allowed = true} <- Receipients],
+    {noreply, State};
+
+handle_table_message({to_client, PlayerId, Msg}, #state{subscribers = Subscribers} = State) ->
+    Recepients = find_subscribers_by_player_id(PlayerId, Subscribers),
+    ?INFO("RELAY_NG Send table message to player's (~p) sessions: ~p. Message: ~p",
+          [PlayerId, Recepients, Msg]),
+    [Pid ! {relay_event, SubscrId, Msg} || #subscriber{id = SubscrId, pid = Pid} <- Recepients],
+    {noreply, State};
+
+handle_table_message({to_subscriber, SubscrId, Msg}, #state{subscribers = Subscribers} = State) ->
+    ?INFO("RELAY_NG Send table message to subscriber: ~p. Message: ~p", [SubscrId, Msg]),
+    case get_subscriber(SubscrId, Subscribers) of
+        {ok, #subscriber{pid = Pid}} -> Pid ! {relay_event, SubscrId, Msg};
+        _ -> do_nothing
+    end,
+    {noreply, State};
+
+handle_table_message({allow_broadcast_for_player, PlayerId},
+                     #state{subscribers = Subscribers} = State) ->
+    ?INFO("RELAY_NG Received directive to allow receiving published messages for player <~p>",
+          [PlayerId]),
+    PlSubscribers = find_subscribers_by_player_id(PlayerId, Subscribers),
+    F = fun(Subscriber, Acc) ->
+                store_subscriber_rec(Subscriber#subscriber{broadcast_allowed = true}, Acc)
+        end,
+    NewSubscribers = lists:foldl(F, Subscribers, PlSubscribers),
+    {noreply, State#state{subscribers = NewSubscribers}};
+
+
+handle_table_message(stop, #state{subscribers = Subscribers} = State) ->
+    [begin
+         erlang:demonitor(MonRef, [flush]),
+         Pid ! {relay_kick, SubscrId, table_closed}
+     end || #subscriber{id = SubscrId, pid = Pid, mon_ref = MonRef}
+                           <- subscribers_to_list(Subscribers)],
+    {stop, normal, State#state{subscribers = subscribers_init()}};
+
+
+handle_table_message(_Msg, State) ->
+    {noreply, State}.
+
+%%===================================================================
+
+init_players(PlayersInfo) ->
+    init_players(PlayersInfo, players_init()).
+
+init_players([], Players) ->
+    Players;
+init_players([{PlayerId, UserId} | PlayersInfo], Players) ->
+    NewPlayers = store_player(PlayerId, UserId, offline, Players),
+    init_players(PlayersInfo, NewPlayers).
+
+%%===================================================================
+
+players_init() -> midict:new().
+
+store_player(PlayerId, UserId, Status, Players) ->
+    store_player_rec(#player{id = PlayerId, user_id = UserId, status = Status}, Players).
+
+store_player_rec(#player{id = PlayerId, user_id = _UserId, status = _Status} = Rec, Players) ->
+    midict:store(PlayerId, Rec, [], Players).
+
+del_player(PlayerId, Players) ->
+    midict:erase(PlayerId, Players).
+
+fetch_player(PlayerId, Players) ->
+    midict:fetch(PlayerId, Players).
+
+find_player(PlayerId, Players) ->
+    midict:find(PlayerId, Players).
+
+update_player_status(PlayerId, Status, Players) ->
+    Rec = fetch_player(PlayerId, Players),
+    store_player_rec(Rec#player{status = Status}, Players).
+
+%%===================================================================
+
+subscribers_init() -> midict:new().
+
+store_subscriber(SubscrId, Pid, UserId, PlayerId, MonRef, BroadcastAllowed, Subscribers) ->
+    store_subscriber_rec(#subscriber{id = SubscrId, pid = Pid, user_id = UserId,
+                                     player_id = PlayerId, mon_ref = MonRef,
+                                     broadcast_allowed = BroadcastAllowed}, Subscribers).
+
+store_subscriber_rec(#subscriber{id = SubscrId, pid = Pid, user_id = _UserId,
+                                 player_id = PlayerId, mon_ref = MonRef} = Rec, Subscribers) ->
+    midict:store(SubscrId, Rec, [{pid, Pid}, {player_id, PlayerId}, {mon_ref, MonRef}],
+                 Subscribers).
+
+%% del_subscriber(SubscrId, Subscribers) -> NewSubscribers
+del_subscriber(SubscrId, Subscribers) ->
+    midict:erase(SubscrId, Subscribers).
+
+%% get_subscriber(Id, Subscribers) -> {ok, #subscriber{}} | error
+get_subscriber(Id, Subscribers) ->
+    midict:find(Id, Subscribers).
+
+%% find_subscribers_by_player_id(PlayerId, Subscribers) -> list(#subscriber{})
+find_subscribers_by_player_id(PlayerId, Subscribers) ->
+    midict:geti(PlayerId, player_id, Subscribers).
+
+%% find_subscribers_by_mon_ref(MonRef, Subscribers) -> list(#subscriber{})
+find_subscribers_by_mon_ref(MonRef, Subscribers) ->
+    midict:geti(MonRef, mon_ref, Subscribers).
+
+%% subscribers_to_list(Subscribers) -> list(#subscriber{})
+subscribers_to_list(Subscribers) ->
+    midict:all_values(Subscribers).

+ 13 - 0
apps/server/src/server.app.src

@@ -0,0 +1,13 @@
+{application, server,
+ [
+  {description, "Kakaranet Game Server"},
+  {vsn, "1"},
+  {registered, []},
+  {applications, [
+                  kernel,
+                  gproc,
+                  stdlib,db
+                 ]},
+  {mod, { nsg_games_app, []}},
+  {env, []}
+ ]}.

+ 18 - 0
apps/server/src/tavla/game_tavla.erl

@@ -0,0 +1,18 @@
+-module(game_tavla).
+-export([get_player_stats/1]).
+-include_lib("server/include/basic_types.hrl").
+-include_lib("server/include/game_tavla.hrl").
+
+get_player_stats(PlayerId) ->
+    {ok, GameStats} = game_stats:get_game_points(okey, PlayerId),
+    {ok, Skill} = game_stats:get_skill(PlayerId),
+    {ok, PlayerStats} = game_stats:get_player_stats(PlayerId),
+    #'PlayerTavlaStats'{playerId = PlayerId,
+                       level = Skill,
+                       score = proplists:get_value(game_points, GameStats),
+                       totalWins = proplists:get_value(total_wins, PlayerStats),
+                       totalLose = proplists:get_value(total_loses, PlayerStats),
+                       totalDisconnects = proplists:get_value(total_disconnects, PlayerStats),
+                       overalSuccessRatio = proplists:get_value(overall_success_ratio, PlayerStats),
+                       averagePlayDuration = proplists:get_value(average_play_time, PlayerStats)
+                      }.

+ 663 - 0
apps/server/src/tavla/game_tavla_bot.erl

@@ -0,0 +1,663 @@
+-module(game_tavla_bot).
+-author('Maxim Sokhatsky <maxim@synrc.com>').
+-behaviour(gen_server).
+
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
+-export([start/3, start_link/3, robot_init/1, init_state/2, join_game/1, get_session/1,
+         send_message/2, call_rpc/2, do_skip/2, do_rematch/1, first_move_table/0, follow_board/3 ]).
+
+-include_lib("server/include/log.hrl").
+-include_lib("server/include/conf.hrl").
+-include_lib("server/include/basic_types.hrl").
+-include_lib("server/include/requests.hrl").
+-include_lib("server/include/game_tavla.hrl").
+-include_lib("server/include/game_okey.hrl").
+-include_lib("server/include/settings.hrl").
+
+-record(state, {
+        moves = 0 :: integer(),
+%%        started = false :: boolean(),
+%%        next_initiated = false :: boolean(),
+        table_id :: integer(),
+        is_robot = true :: boolean(),
+        board :: list(tuple('Color'(), integer) | null),
+        user :: #'PlayerInfo'{},
+        player_color :: integer(),
+        players,
+        uid :: 'PlayerId'(),
+        owner :: pid(),
+        owner_mon :: 'MonitorRef'(),
+        session :: pid(),
+        gid :: 'GameId'(),
+        bot :: pid(),
+        conn :: pid(),
+        hand :: list(),
+        running_requests = dict:new() :: any(),
+        delay :: integer(),
+        mode :: atom(),
+        request_id = 0,
+        confirmation  :: yes_exit | no_exit | no}).
+
+% gen_server
+
+send_message(Pid, Message) -> gen_server:call(Pid, {send_message, Message}).
+call_rpc(Pid, Message) -> gen_server:call(Pid, {call_rpc, Message}).
+get_session(Pid) -> gen_server:call(Pid, get_session).
+init_state(Pid, Situation) -> gen_server:cast(Pid, {init_state, Situation}).
+join_game(Pid) -> gen_server:cast(Pid, join_game).
+start(Owner, PlayerInfo, GameId) -> gen_server:start(?MODULE, [Owner, PlayerInfo, GameId], []).
+start_link(Owner, PlayerInfo, GameId) -> gen_server:start_link(?MODULE, [Owner, PlayerInfo, GameId], []).
+
+init([Owner, PlayerInfo, GameId]) ->
+    {ok, SPid} = game_session:start_link(self()),
+    game_session:bot_session_attach(SPid, PlayerInfo),
+    UId = PlayerInfo#'PlayerInfo'.id,
+    ?INFO("BOTMODULE ~p started with game_session pid ~p", [UId,SPid]),
+    {ok, #state{user = PlayerInfo, uid = UId, owner = Owner, gid = GameId, session = SPid}}.
+
+
+handle_call({send_message, Msg0}, _From, State) ->
+    BPid = State#state.bot,
+    Msg = flashify(Msg0),
+    BPid ! Msg,
+    {reply, ok, State};
+
+handle_call({call_rpc, Msg}, From, State) ->
+    RR = State#state.running_requests,
+    Id = State#state.request_id + 1,
+    Self = self(),
+    RR1 = dict:store(Id, From, RR),
+    proc_lib:spawn_link(fun() ->
+        Res = try
+                  Answer = game_session:process_request(State#state.session, "TAVLA BOT", Msg),
+                  {reply, Id, Answer}
+              catch
+                  _Err:Reason -> {reply, Id, {error, Reason}}
+              end,
+        gen_server:call(Self, Res)
+    end),
+    {noreply, State#state{running_requests = RR1, request_id = Id}};
+
+handle_call({reply, Id, Answer}, _From, State) ->
+    RR = State#state.running_requests,
+    From = dict:fetch(Id, RR),
+    gen_server:reply(From, Answer),
+    {reply, ok, State};
+
+handle_call(get_session, _From, State) ->
+    {reply, State#state.session, State};
+%    {ok, SPid} = game_session:start_link(self()),
+%    game_session:bot_session_attach(SPid, State#state.user),
+%    {reply, State#state.session, State#state{session = SPid}};
+
+handle_call(Request, _From, State) ->
+    Reply = ok,
+    ?INFO("unknown call: ~p", [Request]),
+    {reply, Reply, State}.
+
+handle_cast(join_game, State) ->
+    Mon = erlang:monitor(process, State#state.owner),
+    UId = State#state.uid,
+    GId = State#state.gid,
+    ?INFO("Init State User ~p",[State#state.user]),
+    BPid = proc_lib:spawn_link(game_tavla_bot, robot_init, [#state{gid = GId, uid = UId, conn = self(), table_id = State#state.table_id, user = State#state.user}]),
+    BPid ! join_game,
+    {noreply, State#state{bot = BPid, owner_mon = Mon}};
+
+handle_cast(Msg, State) ->
+    ?INFO("unknown cast: ~p", [Msg]),
+    {noreply, State}.
+
+handle_info({'DOWN', Ref, process, _, Reason}, State = #state{owner_mon = OMon}) when OMon == Ref ->
+    ?INFO("relay goes down with reason ~p so does bot", [Reason]),
+    {stop, Reason, State};
+handle_info(Info, State) ->
+    {stop, {unrecognized_info, Info}, State}.
+
+terminate(_Reason, _State) -> ok.
+code_change(_OldVsn, State, _Extra) -> {ok, State}.
+
+% loops
+
+robot_init(State) ->
+    ?INFO("Robot Init User Info ~p",[State#state.user]),
+    robot_init_loop(State).
+
+robot_init_loop(State) -> % receiving messages from relay
+    S = State#state.conn,
+    Id = State#state.uid,
+    GameId = State#state.gid,
+    receive
+        join_game ->
+            case call_rpc(S, #join_game{game = GameId}) of
+                #'TableInfo'{game = _Atom} -> tavla_client_loop(State);
+                _Err -> ?INFO("ID: ~p failed take with msg ~p", [Id, _Err]),
+                        erlang:error(robot_cant_join_game)
+            end
+    end.
+
+tavla_client_loop(State) -> % incapsulate tavla protocol
+    timer:sleep(300),
+    S = State#state.conn,
+    GameId = State#state.gid,
+    Id = State#state.uid,
+    MyColor = State#state.player_color,
+    GameMode = State#state.mode,
+    receive
+        #game_event{event = <<"tavla_next_turn">>, args = Params} = Msg ->
+            TableId = proplists:get_value(table_id, Params, 0),
+            case TableId == State#state.table_id of true ->
+            ?INFO("TAVLABOT ~p : Received message: ~p", [Id, Msg]),
+            case check_can_roll(GameMode) of
+                true ->
+                    case fix_color(proplists:get_value(color, Params)) of
+                        MyColor ->
+                            ?INFO("TAVLABOT ~p Doing roll", [Id]),
+                            roll_action(State,TableId),
+                            tavla_client_loop(State);
+                        _  -> tavla_client_loop(State)
+                    end;
+                false ->
+                    ?INFO("TAVLABOT ~p Ignoring tavla_next_turn (paired mode). Server rolls automaticly.", [Id]),
+                    tavla_client_loop(State)
+            end;
+            _ -> tavla_client_loop(State) end;
+        #game_event{event = <<"tavla_moves">>, args = Params} = Msg ->
+            TableId = proplists:get_value(table_id, Params, 0),
+            case TableId == State#state.table_id of true ->
+            ?INFO("TAVLABOT ~p : Received message: ~p", [Id, Msg]),
+            PlayerColor = fix_color(proplists:get_value(color, Params)),
+            From = proplists:get_value(from, Params),
+            To = proplists:get_value(to, Params),
+%%            ?INFO("board before moves: ~p", [State#state.board]),
+            Board = reverse_board(State#state.board, PlayerColor),
+            FromR=rel(From,PlayerColor), ToR=rel(To,PlayerColor),
+            NewBoard = reverse_board(follow_board(Board,FromR,ToR,PlayerColor), PlayerColor),
+%%            ?INFO("board after moves: ~p", [NewBoard]),
+            tavla_client_loop(State#state{board = NewBoard,moves = State#state.moves + 1});
+
+            _ -> tavla_client_loop(State) end;
+        #game_event{event = <<"tavla_turn_timeout">>, args = Params} = Msg ->
+            TableId = proplists:get_value(table_id, Params, 0),
+            case TableId == State#state.table_id of true ->
+            ?INFO("TAVLABOT ~p : Received message: ~p", [Id, Msg]),
+            PlayerColor = fix_color(proplists:get_value(color, Params)),
+            Moves = ext_to_moves(proplists:get_value(moves, Params)),
+%%            ?INFO("board before moves: ~p", [State#state.board]),
+            Board = reverse_board(State#state.board, PlayerColor),
+            F = fun({From, To}, B) ->
+                        FromR = rel(From, PlayerColor), ToR = rel(To, PlayerColor),
+                        follow_board(B, FromR, ToR, PlayerColor)
+                end,
+            NewBoard = reverse_board(lists:foldl(F, Board, Moves), PlayerColor),
+%%            ?INFO("board after moves: ~p", [NewBoard]),
+            tavla_client_loop(State#state{board = NewBoard, moves = State#state.moves + length(Moves)});
+            _ -> tavla_client_loop(State) end;
+%%         #game_event{event = <<"tavla_vido_request">>, args = Params} ->
+%%             TableId = proplists:get_value(table_id, Params, 0),
+%%             case TableId == State#state.table_id of true ->
+%% 
+%% %            ?INFO("tavla moves: ~p",[Params]),
+%%             To = proplists:get_value(from, Params),
+%%             vido(State,To,TableId),
+%%             tavla_client_loop(State);
+%% 
+%%             _ -> tavla_client_loop(State) end;
+        #game_event{event = <<"tavla_surrender_request">>, args = Params} = Msg->
+            TableId = proplists:get_value(table_id, Params, 0),
+            case TableId == State#state.table_id of true ->
+            ?INFO("TAVLABOT ~p : Received message: ~p", [Id, Msg]),
+
+%            ?INFO("tavla moves: ~p",[Params]),
+            To = proplists:get_value(from, Params),
+            surrender(State,To,TableId),
+            tavla_client_loop(State);
+
+            _ -> tavla_client_loop(State) end;
+%%         #game_event{event = <<"tavla_ack">>, args = Params} ->
+%%             TableId = proplists:get_value(table_id, Params, 0),
+%%             case TableId == State#state.table_id of true ->
+%%             _To = proplists:get_value(from, Params),
+%%             _Type = proplists:get_value(type, Params),
+%%             tavla_client_loop(State);
+%% 
+%%             _ -> tavla_client_loop(State) end;
+        #game_event{event = <<"tavla_game_player_state">>, args = Params} = Msg->
+            TableId = proplists:get_value(table_id, Params, 0),
+            case TableId == State#state.table_id of true ->
+            ?INFO("TAVLABOT ~p : Received message: ~p", [Id, Msg]),
+            PlayersColors = proplists:get_value(players_colors, Params),
+            WhosMove = proplists:get_value(whos_move, Params),
+            BoardRaw = proplists:get_value(board, Params),
+            Dice = proplists:get_value(dice, Params),
+            GameState = proplists:get_value(game_state, Params),
+            Paused = proplists:get_value(paused, Params),
+
+            case Paused of
+                 true -> wait_for_resume();
+                 false -> ok
+            end,
+
+            FoundMyself = lists:keyfind(Id, #tavla_color_info.name, PlayersColors),
+            PlayerColor = fix_color(FoundMyself#tavla_color_info.color),
+            ?INFO("TAVLABOT ~p BoardRaw: ~p", [Id, BoardRaw]),
+            Board = if BoardRaw == null -> null;
+                       true -> ext_to_board(BoardRaw)
+                    end,
+            State1 = State#state{board = Board, moves = 0, player_color=PlayerColor},
+
+            ?INFO("TAVLABOT ~p player color: ~p",[Id, PlayerColor]),
+            MyMove = lists:member(fix_color(PlayerColor), WhosMove),
+            case {MyMove, GameState} of
+                {true, <<"first_move_competition">>} ->
+                    roll_action(State1, TableId),
+                    tavla_client_loop(State1);
+                {true, <<"waiting_for_roll">>} ->
+                    roll_action(State1, TableId),
+                    tavla_client_loop(State1);
+                {true, <<"waiting_for_move">>} ->
+                    do_move(State1, Dice, TableId, PlayerColor),
+                    tavla_client_loop(State1#state{moves = 1});
+                {_, <<"initializing">>} ->
+                    tavla_client_loop(State1);
+                {_, <<"finished">>} ->
+                    tavla_client_loop(State1);
+                {false, _} ->
+                    tavla_client_loop(State1)
+            end;
+            _ -> tavla_client_loop(State) end;
+        #game_event{event = <<"tavla_game_started">>, args = Params} = Msg->
+            TableId = proplists:get_value(table_id, Params, 0),
+            case TableId == State#state.table_id of true ->
+            ?INFO("TAVLABOT ~p : Received message: ~p", [Id, Msg]),
+            Players = proplists:get_value(players, Params),
+            BoardRaw = proplists:get_value(board, Params),
+            Competition = proplists:get_value(do_first_move_competition_roll, Params),
+            FoundMyself = lists:keyfind(Id, #tavla_color_info.name, Players),
+            PlayerColor = fix_color(FoundMyself#tavla_color_info.color),
+            ?INFO("TAVLABOT ~p game_started, color: ~p",[Id, PlayerColor]),
+            ?INFO("TAVLABOT ~p game_started, BoardRaw: ~p",[Id, BoardRaw]),
+            Board = if BoardRaw == null -> null;
+                       true -> ext_to_board(BoardRaw)
+                    end,
+            if Competition -> roll_action(State,TableId); true -> do_nothing end,
+            tavla_client_loop(State#state{board = Board, moves=0, player_color=PlayerColor});
+            _ -> tavla_client_loop(State) end;
+        #game_event{event = <<"tavla_won_first_move">>, args = Params} = Msg ->
+            TableId = proplists:get_value(table_id, Params, 0),
+            case TableId == State#state.table_id of true ->
+            ?INFO("TAVLABOT ~p : Received message: ~p", [Id, Msg]),
+            State2 = case fix_color(proplists:get_value(color, Params)) of
+                MyColor ->
+                    ?INFO("TAVLABOT ~p : I won the first move order.", [Id]),
+                    case proplists:get_value(reroll, Params) of
+                        false ->
+                            Dice = proplists:get_value(dice, Params),
+                            ?INFO("TAVLABOT ~p Reroll is not needed. Doing the first move with dice:~p",[Id, Dice]),
+                            do_move(State,Dice,TableId,MyColor), State#state{moves = State#state.moves + 1};
+                        true ->
+                            ?INFO("TAVLABOT ~p Reroll needed. So doing it.",[Id]),
+                            roll_action(State, TableId)
+                    end;
+                _  -> ?INFO("TAVLABOT ~p tavla_won_first_move: Ignore rolls (another color)",[Id]), State
+            end,
+            tavla_client_loop(State2);
+            _ -> tavla_client_loop(State) end;
+        #game_event{event = <<"tavla_rolls">>, args = Params} = Msg->
+            TableId = proplists:get_value(table_id, Params, 0),
+            case TableId == State#state.table_id of true ->
+            ?INFO("TAVLABOT ~p : Received message: ~p", [Id, Msg]),
+            State2 = case fix_color(proplists:get_value(color, Params)) of
+                MyColor ->
+                      Dice = proplists:get_value(dices, Params),
+                      case Dice of
+                        [_A,_B] ->
+                            ?INFO("TAVLABOT ~p Doing moves with dice: ~p",[Id, Dice]),
+                            do_move(State,Dice,TableId,MyColor), State#state{moves = State#state.moves + 1};
+                        [_C] ->
+                            ?INFO("TAVLABOT ~p Ignore the roll (first move competition)",[Id]),
+                            State
+                      end;
+                _  -> ?INFO("TAVLABOT ~p Ignore the roll (another color)",[Id]), State
+            end,
+            tavla_client_loop(State2);
+            _ -> tavla_client_loop(State) end;
+        #game_event{event = <<"player_left">>, args = Params} = Msg ->
+            TableId = proplists:get_value(table_id, Params, 0),
+            case TableId == State#state.table_id of true ->
+            ?INFO("TAVLABOT ~p : Received message: ~p", [Id, Msg]),
+            Replaced = proplists:get_value(bot_replaced, Params, false) orelse
+                       proplists:get_value(human_replaced, Params, false),
+            case Replaced of
+                false ->
+                    call_rpc(S, #logout{});
+                true ->
+                    tavla_client_loop(State)
+            end;
+            _ -> tavla_client_loop(State) end;
+        #game_event{event = <<"tavla_game_info">>, args = Args} = Msg->
+            ?INFO("TAVLABOT ~p : Received message: ~p", [Id, Msg]),
+             User = State#state.user,
+             Mode = proplists:get_value(game_mode, Args),
+             TableId = proplists:get_value(table_id, Args),
+             ?INFO("TAVLABOT game_info ~p ~p",[self(),User]),
+             Players = proplists:get_value(players, Args),
+             SeriesConfirmMode = proplists:get_value(series_confirmation_mode, Args),
+             ?INFO("TAVLABOT players: ~p",[Players]),
+             Delay = get_delay(fast),
+             CatchTID = case User == undefined of
+                            false -> FoundMyself = lists:keyfind(User#'PlayerInfo'.id,#'PlayerInfo'.id,Players),
+                                    case FoundMyself of false -> State#state.table_id; _ -> TableId end;
+                            true -> ?INFO("ERROR USER in ~p is not set!",[self()]), undefined
+                        end,
+             tavla_client_loop( State#state{table_id = CatchTID, delay = Delay, mode = Mode,
+                                            players = Players, confirmation = SeriesConfirmMode});
+        #game_event{event = <<"tavla_series_ended">>, args = Params} = Msg->
+            TableId = proplists:get_value(table_id, Params, 0),
+            case TableId == State#state.table_id of true ->
+            ?INFO("TAVLABOT ~p : Received message: ~p", [Id, Msg]),
+            tavla_client_loop(State);
+            _ -> tavla_client_loop(State) end;
+        #game_event{event = <<"tavla_game_ended">>, args = Params} = Msg->
+            TableId = proplists:get_value(table_id, Params, 0),
+            case TableId == State#state.table_id of true ->
+            ?INFO("TAVLABOT ~p : Received message: ~p", [Id, Msg]),
+            say_ready(State,TableId),
+            tavla_client_loop(State);
+            _ -> tavla_client_loop(State) end;
+        Msg ->
+            ?INFO("TAVLABOT ~p Received UNKNOWN message: ~p",[Id, Msg]),
+            tavla_client_loop(State)
+    end.
+
+fix_color(Color) ->  case Color of 1 -> 2; 2 -> 1 end.
+
+
+
+check_can_roll(Mode) ->
+    case Mode of
+        <<"paired">> -> false;
+        _ -> true
+    end.
+
+
+
+% logic
+
+follow_board(Board,Moves,PlayerColor) ->
+    lists:foldl(fun ({From,To}, Acc) -> follow_board(Acc,From,To,PlayerColor) end, Board, Moves).
+
+follow_board(Board,From,To,PlayerColor) -> % track the moves to keep board consistent
+    FromCell = lists:nth(From + 1,Board),
+    {FromColor,_FromCount} = case FromCell of 
+                                 null -> {0,0};
+                                 _Else -> _Else
+                            end,
+    BoardWithKicks = [ case No of
+          From -> case Cell of
+                       {_Color,1} -> {{0,0},null};
+                       {Color,Count} -> {{0,0},{Color,Count-1}};
+                       _ -> ?INFO("Board: ~p From ~p To ~p",[Board,From,To]),
+                            ?INFO("follow_board: cant move from empty slot"), exit(self(),kill), {{0,0},null}
+                  end;
+            To -> case Cell of
+                       null -> case FromColor of 0 -> {{0,0},null}; _ -> {{0,0},{FromColor,1}} end;
+                       {0,0} -> case FromColor of 0 -> {{0,0},null}; _ -> {{0,0},{FromColor,1}} end;
+                       {Color,Count} -> 
+                            case Color =:= FromColor of
+                                 true -> {{0,0},{Color,Count+1}};
+                                 false -> case Count of
+                                               1 when FromColor =/= 0 -> {{Color,1},{FromColor,1}};
+                                               _ -> ?INFO("Board: ~p From ~p To ~p",[Board,From,To]),
+                                                    ?INFO("follow_board: cant kick tower"), exit(self(),kill), {{0,0},{Color,Count}}
+                                          end
+                            end
+                  end;
+           _ -> {{0,0},Cell}
+    end || {Cell,No} <- lists:zip(Board,lists:seq(0,27)) ],
+    {KickColor,KickAmount} = lists:foldl(
+            fun({{KC,KA},_},{Col,Sum}) ->
+                  case KC of
+                       0 -> {Col,Sum};
+                       _ -> {KC,Sum+KA} 
+                  end
+            end,{0,0}, BoardWithKicks),
+%%    ?INFO("Kick: ~p",[{KickColor,KickAmount}]),
+    NewBoard = [ case {No,KickColor} of
+        {25,_} when KickColor =/= 0 -> case Cell of
+                       null -> {KickColor,KickAmount};
+                       {_Color,Sum} -> {KickColor,KickAmount+Sum}
+                  end;
+        _ -> Cell
+    end || {{{_KC,_KA},Cell},No} <- lists:zip(BoardWithKicks,lists:seq(0,27))],
+    NewBoard.
+
+all_in_home(Board,Color) ->
+    Lates = lists:foldr(fun (A,Acc) -> 
+                              case A of
+                                   {null,_No} -> Acc;
+                                   {{C,_Count},No} -> NotInHome = (((No > 0)and(No < 19)) or (No == 26)),
+                                       case {C, NotInHome} of
+                                            {Color,true} -> Acc + 1;
+                                            _ -> Acc
+                                       end
+                              end
+                          end,0,lists:zip(Board,lists:seq(0,27))),
+    Lates =:= 0.
+
+make_decision(Board,Dices2,Color,TableId) -> % produces tavla moves
+    [X,Y] = Dices2,
+    Dices = case X =:= Y of true -> [X,X,X,X]; false -> Dices2 end,
+    Decision = first_available_move(Board,Dices,Color,TableId),
+    ?INFO("Decision: ~p",[Decision]),
+    Decision.
+
+%% norm([A,B]) -> case A > B of true -> {A,B}; false -> {B,A} end.
+first_move_table() -> [{{6,6},[{13,7},{13,7},{24,18},{24,18}]}, % based on 
+                       {{6,5},[{24,13}]},
+                       {{6,4},[{24,18},{13,9}]},
+                       {{6,3},[{24,18},{13,10}]},
+                       {{6,2},[{24,18},{13,11}]},
+                       {{6,1},[{13,7},{8,7}]},
+                       {{5,5},[{13,3},{13,3}]},
+                       {{5,4},[{24,20},{13,8}]},
+                       {{5,3},[{8,3},{5,3}]},
+                       {{5,2},[{24,22},{13,8}]},
+                       {{5,1},[{24,23},{13,8}]},
+                       {{4,4},[{24,20},{24,20},{13,9},{13,9}]},
+                       {{4,3},[{24,21},{13,9}]},
+                       {{4,2},[{8,4},{6,4}]},
+                       {{4,1},[{24,23},{13,9}]},
+                       {{3,3},[{24,21},{24,21},{13,10},{13,10}]},
+                       {{3,2},[{24,21},{13,11}]},
+                       {{3,1},[{8,5},{6,5}]},
+                       {{2,2},[{13,11},{13,11},{6,4},{6,4}]},
+                       {{2,1},[{13,11},{6,5}]},
+                       {{1,1},[{8,7},{8,7},{6,5},{6,5}]}
+                      ].
+
+%make_first_move(Dices,TableId,PlayerColor) -> 
+%    {_,Moves} = lists:keyfind(norm(Dices),1,first_move_table()),
+%    case PlayerColor of
+%         1 -> [ #'TavlaAtomicMove'{table_id = TableId,from=25-From,to=25-To} || {From,To} <- Moves];
+%         2 -> [ #'TavlaAtomicMove'{table_id = TableId,from=From,to=To} || {From,To} <- Moves]
+%    end.
+
+tactical_criteria(Board,Color) -> 
+   case all_in_home(Board,Color) of
+        true  -> finish;
+        false -> case lists:nth(27,Board) of
+                      {Color,Count} -> kicks;
+                      _ -> race
+                 end
+   end.
+
+rel(X,C) -> case C of 2 -> case X of 0->27;27->0;26->25;25->26;_->25-X end; 
+                      1 -> X end.
+
+reverse_board(Board,PlayerColor) ->
+    case PlayerColor of
+         1 -> Board;
+         2 -> [BE|Rest] = Board, [WE,WK,BK|Rest1] = lists:reverse(Rest), [WE] ++ Rest1 ++ [WK,BK,BE]
+    end.
+
+%% first_available_move(RealBoard,Dice,Color,TableId) -> Moves
+first_available_move(RealBoard,Dice,Color,TableId) ->
+    RelativeBoard = reverse_board(RealBoard,Color),
+    F = fun(Die, {MovesAcc, BoardAcc, FailedDiceAcc}) ->
+%%                ?INFO("board: ~p", [BoardAcc]),
+                Tactic = tactical_criteria(BoardAcc, Color),
+%%                ?INFO("tactical criteria: ~p", [Tactic]),
+                case find_move(Color, Die, Tactic, BoardAcc) of
+                    {Move, NewBoard} -> {[Move | MovesAcc], NewBoard, FailedDiceAcc};
+                    false -> {MovesAcc, BoardAcc, [Die | FailedDiceAcc]}
+                end
+        end,
+    {List, Board, FailedDice} = lists:foldl(F, {[], RelativeBoard, []}, Dice),
+%%    ?INFO("moves found: ~p",[{List, FailedDice}]),
+    [#'TavlaAtomicMove'{from=rel(From,Color), to=rel(To,Color)} || {From,To} <- lists:reverse(List)]  ++
+        if length(FailedDice) == 0; length(FailedDice) == length(Dice) -> [];
+           true -> first_available_move(reverse_board(Board,Color),FailedDice,Color,TableId) end.
+
+%% find_move(Die, Tactic, Board) -> {Move, NewBoard} | false
+find_move(Color, Die, Tactic, Board) ->
+    OppColor = case Color of 1 -> 2; 2 -> 1 end,
+    find_move(Color, OppColor, Die, Tactic, Board, lists:sublist(lists:zip(Board,lists:seq(0,27)),2,24)).
+
+find_move(_Color, _OppColor, _Die, _Tactic, _Board, []) -> false;
+find_move(Color, OppColor, Die, Tactic, Board, [{Cell, No} | Rest]) ->
+%%    ?INFO("checking pos: ~p", [{Cell, No, Die, Tactic}]),
+    case {Cell, Tactic} of
+        {null,kicks} when No == Die -> ?INFO("found kick to empty: ~p",[{26,No}]), {{26,No}, follow_board(Board,26,No,Color)};
+        {{OppColor,1},kicks} when No == Die -> ?INFO("found kick kick: ~p",[{26,No}]), {{26,No}, follow_board(Board,26,No,Color)};
+        {{Color,_},kicks} when No == Die -> ?INFO("found kick over own: ~p",[{26,No}]), {{26,No}, follow_board(Board,26,No,Color)};
+        {{Color,_},finish} when No + Die >= 25 -> ?INFO("found finish move: ~p",[{No,27}]), {{No,27}, follow_board(Board,No,27,Color)};
+        {{Color,_},Tactic} when (Tactic==race orelse Tactic==finish) andalso (No + Die < 25) ->
+            case lists:nth(No+Die+1, Board) of
+                null -> ?INFO("found race to empty: ~p",[{No,No+Die}]), {{No,No+Die}, follow_board(Board,No,No+Die,Color)};
+                {Color,_} -> ?INFO("found race over own: ~p",[{No,No+Die}]), {{No,No+Die}, follow_board(Board,No,No+Die,Color)};
+                {OppColor,1} -> ?INFO("found race kick: ~p",[{No,No+Die}]), {{No,No+Die}, follow_board(Board,No,No+Die,Color)};
+                _ -> find_move(Color, OppColor, Die, Tactic, Board, Rest)
+            end;
+        _ -> find_move(Color, OppColor, Die, Tactic, Board, Rest)
+    end.
+
+%% make_end_series(Color,TableId,Board) ->
+%%     lists:foldl(fun (A,Acc) -> ?INFO("~p / ~p~n",[A,Acc]), {Cell,No} = A, case Cell of null -> Acc; 
+%%                                   {Color,Count} -> Acc ++
+%%     [ #'TavlaAtomicMove'{from=No, to=27} || X <- lists:seq(1,Count)]; _ -> Acc end
+%%                                  end, [], lists:sublist(lists:zip(Board,lists:seq(0,27)),1,27)  ).
+
+% actions
+
+do_rematch(State) ->
+    S = State#state.conn,
+    GameId = State#state.gid,
+    ok = call_rpc(S, #rematch{game = GameId}).
+
+say_ready(State,TableId) ->
+    S = State#state.conn,
+    GameId = State#state.gid,
+    ok = call_rpc(S, #game_action{game = GameId, action = tavla_ready, args = [{table_id,TableId}]}).
+
+vido(State,To,TableId) ->
+    S = State#state.conn,
+    GameId = State#state.gid,
+    ok = call_rpc(S, #game_action{game = GameId, 
+                                  action = tavla_vido_answer,
+                                  args = [{table_id,TableId},{from,State#state.uid},{to,To},{answer,false}]}).
+
+surrender(State,To,TableId) ->
+    S = State#state.conn,
+    GameId = State#state.gid,
+    ok = call_rpc(S, #game_action{game = GameId, 
+                                  action = tavla_surrender_answer,
+                                  args = [{table_id,TableId},{from,State#state.uid},{to,To},{answer,true}]}).
+
+roll_action(State,TableId) ->
+    S = State#state.conn,
+    GameId = State#state.gid,
+    ok = call_rpc(S, #game_action{game = GameId, action = tavla_roll, args = [{table_id,TableId}]}).
+
+do_skip(State,TableId) ->
+    S = State#state.conn,
+    GameId = State#state.gid,
+    call_rpc(S, #game_action{
+                        game = GameId,
+                        action = tavla_skip,
+                        args = [{table_id,TableId}]}).
+
+do_move(State, Dices,TableId,PlayerColor) ->
+    Delay = State#state.delay,
+    simulate_delay(take, Delay),
+    S = State#state.conn,
+    GameId = State#state.gid,
+    Id = State#state.uid,
+    
+%    Decision = case State#state.moves > 5 of 
+%                    false -> make_decision(State#state.board, Dices, PlayerColor,TableId);
+%                    true -> make_end_series(PlayerColor,TableId,State#state.board)
+%               end,
+    Decision = make_decision(State#state.board, Dices, PlayerColor,TableId),
+    case Decision of
+        [] ->
+            ?INFO("TAVLABOT ~p : No moves available. Do nothing", [Id]),
+            no_moves;
+        _ ->
+            ?INFO("TAVLABOT ~p : Moves: ~p", [Id, Decision]),
+            Resp = call_rpc(S, #game_action{game = GameId,
+                                            action = tavla_move,
+                                            args = [{table_id, TableId},{player, Id},{moves, Decision }]}),
+            ?INFO("TAVLABOT_DBG ~p : Server response: ~p", [Id, Resp]),
+            {ok, Decision, Resp}
+    end.
+
+flashify(R) when is_tuple(R) ->
+    [RecName | Rest] = tuple_to_list(R),
+    Rest1 = lists:map(fun
+                          (X) -> flashify(X)
+                      end, Rest),
+    list_to_tuple([RecName | Rest1]);
+flashify([{Key, _Value} | _] = P) when is_atom(Key) ->
+    lists:map(fun
+                  ({K, V}) when is_atom(K) -> {K, flashify(V)}
+              end, P);
+flashify(A) when A == true -> A;
+flashify(A) when A == false -> A;
+flashify(A) when A == null -> A;
+flashify(A) when A == undefined -> A;
+flashify(A) when is_atom(A) ->
+    list_to_binary(atom_to_list(A));
+flashify(Other) ->
+    Other.
+
+time_to_sleep(_, Delay) ->
+    erlang:trunc((Delay / 3) * 2).
+
+simulate_delay(Action, Delay) ->
+    TheDelay = time_to_sleep(Action, Delay),
+    receive
+        #game_paused{action = <<"pause">>} ->
+            wait_for_resume()
+    after TheDelay ->
+            ok
+    end.
+
+wait_for_resume() ->
+    receive
+        #game_paused{action = <<"resume">>} ->
+            ok
+    end.
+
+ext_to_board(ExtBoard) ->
+    [case Cell of
+         null -> null;
+         #tavla_checkers{color = C, number = N} -> {fix_color(C), N}
+     end || Cell <- ExtBoard].
+
+ext_to_moves(ExtMoves) ->
+    [{From, To} || #'TavlaAtomicMoveServer'{from = From, to = To} <- ExtMoves].
+
+get_delay(fast) -> {ok, Val}   = nsm_db:get(config,"games/tavla/robot_delay_fast", 6000), Val;
+get_delay(normal) -> {ok, Val} = nsm_db:get(config,"games/tavla/robot_delay_normal", 9000), Val;
+get_delay(slow) -> {ok, Val}   = nsm_db:get(config,"games/tavla/robot_delay_slow", 15000), Val.

+ 124 - 0
apps/server/src/tavla/game_tavla_lib.erl

@@ -0,0 +1,124 @@
+%% Author: serge
+%% Created: Feb 14, 2013
+%% Description: TODO: Add description to game_tavla_lib
+-module(game_tavla_lib).
+
+%%
+%% Include files
+%%
+
+%%
+%% Exported Functions
+%%
+-export([board_to_text1/1,
+         board_to_text2/1,
+         board_to_text3/1,
+         board_to_text4/1]).
+
+-define(BLACK, black).
+-define(WHITE, white).
+-define(WHITE_OUT, wo).
+-define(WHITE_BAR, wb).
+-define(BLACK_OUT, bo).
+-define(BLACK_BAR, bb).
+
+%%
+%% API Functions
+%%
+board_to_text1(Board) ->
+    Str1 = "13 14 15 16 17 18 BB 19 20 21 22 23 24 BO",
+    Str4 = "                  ||                     ",
+    Str7 = "12 11 10 09 08 07 WB 06 05 04 03 02 01 WO",
+    List1 = [13, 14, 15, 16, 17, 18, ?BLACK_BAR, 19, 20 ,21, 22, 23, 24, ?BLACK_OUT],
+    List2 = [12, 11, 10, 09, 08, 07, ?WHITE_BAR, 06, 05 ,04, 03, 02, 01, ?WHITE_OUT],
+    Str2 = list_to_colors(List1, Board),
+    Str3 = list_to_checkers(List1, Board),
+    Str5 = list_to_checkers(List2, Board),
+    Str6 = list_to_colors(List2, Board),
+    [Str1, Str2, Str3, Str4, Str5, Str6, Str7].
+
+board_to_text2(Board) ->
+    Str1 = "|xoxoxo-xoxoxo| ",
+    Str4 = "|------+------| ",
+    Str7 = "|xoxoxo-xoxoxo| ",
+    List1 = [13, 14, 15, 16, 17, 18, ?BLACK_BAR, 19, 20 ,21, 22, 23, 24],
+    List2 = [12, 11, 10, 09, 08, 07, ?WHITE_BAR, 06, 05 ,04, 03, 02, 01],
+    Str2 = "|" ++ list_to_colors2(List1, Board) ++ "|" ++ list_to_colors2([?BLACK_OUT], Board),
+    Str3 = "|" ++ list_to_checkers2(List1, Board) ++ "|" ++ list_to_checkers2([?BLACK_OUT], Board),
+    Str5 = "|" ++ list_to_checkers2(List2, Board) ++ "|" ++ list_to_checkers2([?WHITE_OUT], Board),
+    Str6 = "|" ++ list_to_colors2(List2, Board) ++ "|" ++ list_to_colors2([?WHITE_OUT], Board),
+    [Str1, Str2, Str3, Str4, Str5, Str6, Str7].
+
+board_to_text3(Board) ->
+    ["(" ++ checker(?WHITE_OUT, Board)++ ")" ++ checker(?BLACK_BAR, Board) ++
+    "|" ++ quater(1, 6, Board) ++ "|" ++ quater(7, 12, Board) ++ "|" ++
+    quater(13, 18, Board) ++ "|" ++ quater(19, 24, Board) ++ "|" ++
+    checker(?WHITE_BAR, Board) ++ "(" ++ checker(?BLACK_OUT, Board) ++ ")"].
+
+board_to_text4(Board) ->
+    Str1 = "|xoxoxo+-+xoxoxo| ",
+    Str4 = "|------|+|------| ",
+    Str7 = "|xoxoxo+-+xoxoxo| ",
+    List1 = [13, 14, 15, 16, 17, 18],
+    List2 = [19, 20 ,21, 22, 23, 24],
+    List3 = [12, 11, 10, 09, 08, 07],
+    List4 = [06, 05 ,04, 03, 02, 01],
+    Str2 = "|" ++ list_to_colors2(List1, Board) ++ "|" ++ list_to_colors2([?BLACK_BAR], Board) ++ "|" ++ list_to_colors2(List2, Board) ++ "|" ++ list_to_colors2([?BLACK_OUT], Board),
+    Str3 = "|" ++ list_to_checkers2(List1, Board) ++ "|" ++ list_to_checkers2([?BLACK_BAR], Board) ++ "|" ++ list_to_checkers2(List2, Board) ++ "|" ++ list_to_checkers2([?BLACK_OUT], Board),
+    Str5 = "|" ++ list_to_checkers2(List3, Board) ++ "|" ++ list_to_checkers2([?WHITE_BAR], Board) ++ "|" ++ list_to_checkers2(List4, Board)++ "|" ++ list_to_checkers2([?WHITE_OUT], Board),
+    Str6 = "|" ++ list_to_colors2(List3, Board) ++ "|" ++ list_to_colors2([?WHITE_BAR], Board) ++ "|" ++ list_to_colors2(List4, Board)++ "|" ++ list_to_colors2([?WHITE_OUT], Board),
+    [Str1, Str2, Str3, Str4, Str5, Str6, Str7].
+
+get_checkers(Pos, Board) ->
+    {_, Value} = lists:keyfind(Pos, 1, Board),
+    Value.
+
+%%
+%% Local Functions
+%%
+
+list_to_checkers(List, Board) ->
+    F = fun(Pos) ->
+                case get_checkers(Pos, Board) of
+                    empty -> "   ";
+                    {_, Num} -> io_lib:format("~2b ", [Num])
+                end
+        end,
+    lists:flatmap(F, List).
+
+list_to_colors(List, Board) ->
+    F = fun(Pos) ->
+                case get_checkers(Pos, Board) of
+                    empty -> "   ";
+                    {?WHITE, _} -> " W ";
+                    {?BLACK, _} -> " B "
+                end
+        end,
+    lists:flatmap(F, List).
+
+list_to_checkers2(List, Board) ->
+    F = fun(Pos) ->
+                case get_checkers(Pos, Board) of
+                    empty -> " ";
+                    {_, Num} -> io_lib:format("~.16b", [Num])
+                end
+        end,
+    lists:flatmap(F, List).
+
+list_to_colors2(List, Board) ->
+    [case get_checkers(Pos, Board) of
+         empty -> " ";
+         {?WHITE, _} -> "W";
+         {?BLACK, _} -> "B"
+     end || Pos <- List].
+
+checker(Pos, Board) ->
+    case get_checkers(Pos, Board) of
+        empty -> "  ";
+        {?WHITE, Num} -> "W" ++ io_lib:format("~.16b", [Num]);
+        {?BLACK, Num} -> "B" ++ io_lib:format("~.16b", [Num])
+    end.
+
+quater(First, Last, Board)->
+    F = fun(Pos) -> "-" ++ checker(Pos, Board) end,
+    lists:flatmap(F, lists:seq(First, Last)) ++ "-".

+ 643 - 0
apps/server/src/tavla/game_tavla_ng_desk.erl

@@ -0,0 +1,643 @@
+%%% -------------------------------------------------------------------
+%%% Author  : Sergei Polkovnikov <serge.polkovnikov@gmail.com>
+%%% Description : The tavla game rules implementation
+%%%
+%%% Created : Jan 14, 2013
+%%% -------------------------------------------------------------------
+-module(game_tavla_ng_desk).
+-behaviour(gen_fsm).
+
+%% Board model schema:
+%%  13 14 15 16 17 18    19 20 21 22 23 24   BO
+%%                    BB
+%%
+%%                    WB
+%%  12 11 10 09 08 07    06 05 04 03 02 01   WO
+
+%% Parameters:
+%%  home_hit_and_run - specifies is the "hit and run" allowed in the home.
+%%           Type : enabled | disabled
+%%  bearoff_waste_moves - specifies are "waste" moves allowed in the bear-off phase.
+%%           Waste move means a normal move when a bear-off can be done.
+%%           Type: enabled | disabled
+%%  first_move - a color of player who should make first move.
+%%           Type: black | white
+%% Options:
+%%  board - an initial board definition. Describes how the checkers are placed on the
+%%           board. All table position must be specified in the definition. The total
+%%           number of checkers must be exectly 15 per each color. The first position
+%%           in the white home is 1 and the first position in the black home is 24. White
+%%           checkers goes counter-clockwise, and the black ones clockwise.
+%%          If the option is not defined then the regular board definition will be applyed.
+%%           Type: [{Position, State}]
+%%                 Position = 1-24, wb, bb, wo, bo
+%%                 State = empty | {Color, CheckersNumber}
+%%                   Color = black | white
+%%                   CheckersNumber = 1-15
+%%  dice - initial dice value. Define this option if dice was defined by the first move competition
+%%           procedure. So the player should do moves at start instead roll.
+%%           Type: {Die1, Die2}
+%%                    Die1 = Die2 = 1-6
+
+%%             Players actions       ||            Errors
+%%  {roll, Die1, Die2}               || not_your_order, invalid_action
+%%      Die1 = Die2 = 1-6            ||
+%%                                   ||
+%%  {move, Moves}                    || not_your_order, invalid_action, too_many_moves,
+%%      Moves = [{From, To}]         || {position_occupied, Move, RestMoves},
+%%        From = wb, bb, 1-24        || {waste_move_disabled, Move, RestMoves},
+%%        To = wo, bo, 1-24          || {hit_and_run_disabled, Move, RestMoves},
+%%                                   || {not_bear_off_mode, Move, RestMoves},
+%%                                   || {no_checker, Move, RestMoves},
+%%                                   || {invalid_move, Move, RestMoves},
+%%                                   || {move_from_bar_first, Move, RestMoves}
+
+%% Outgoing events:
+%%  {next_player, Color}
+%%      Color = black | white
+%%  {rolls, Color, Die1, Die2}
+%%  {moves, Color, Moves}
+%%      Moves = [{Type, From, To, Pips}]
+%%        Type = move | hit
+%%        Pips = 1-6
+%%  {win, Color, Condition}
+%%      Condition = normal | mars
+
+%% External exports
+-export([
+         start/1,
+         stop/1,
+         player_action/3
+        ]).
+
+%% gen_fsm callbacks
+-export([init/1, handle_event/3, handle_sync_event/4, handle_info/3, terminate/3, code_change/4]).
+
+-define(CHECKERS_NUM, 15).
+
+-define(WHITE_OUT, wo).
+-define(BLACK_OUT, bo).
+-define(WHITE_BAR, wb).
+-define(BLACK_BAR, bb).
+-define(WHITE, white).
+-define(BLACK, black).
+
+-define(STATE_WAIT_ROLL, state_wait_roll).
+-define(STATE_WAIT_MOVE, state_wait_move).
+-define(STATE_FINISHED, state_finished).
+
+-record(state,
+        {home_hit_and_run_enabled    :: boolean(),
+         bearoff_waste_moves_enabled :: boolean(),
+         first_move                  :: black | white,
+         board                       :: dict(),
+         pips_list                   :: undefined | list(integer()),
+         hitted_home_positions       :: list(),
+         current                     :: black | white,
+         finish_conditions           :: undefined | {black | white, normal | mars}
+        }).
+
+%% ====================================================================
+%% External functions
+%% ====================================================================
+start(Params) -> gen_fsm:start(?MODULE, Params, []).
+
+player_action(Desk, Color, Action) ->
+    gen_fsm:sync_send_all_state_event(Desk, {player_action, Color, Action}).
+
+stop(Desk) -> gen_fsm:send_all_state_event(Desk, stop).
+
+
+%% ====================================================================
+%% Server functions
+%% ====================================================================
+% --------------------------------------------------------------------
+init(Params) ->
+    HomeHitAndRun = get_param(home_hit_and_run, Params),
+    BearoffWasteMoves = get_param(bearoff_waste_moves, Params),
+    FirstMove = get_param(first_move, Params),
+    BoardSpec = get_option(board, Params, undefined),
+    Dice = get_option(dice, Params, undefined),
+    validate_params(HomeHitAndRun, BearoffWasteMoves, FirstMove, BoardSpec, Dice),
+    Board = if BoardSpec == undefined -> init_board(initial_board());
+               true -> init_board(BoardSpec)
+            end,
+    State = #state{home_hit_and_run_enabled = HomeHitAndRun == enabled,
+                   bearoff_waste_moves_enabled = BearoffWasteMoves == enabled,
+                   first_move = FirstMove,
+                   current = FirstMove,
+                   board = Board,
+                   pips_list = undefined,
+                   hitted_home_positions = []
+                  },
+    case Dice of
+        undefined ->
+           {ok, ?STATE_WAIT_ROLL, State};
+        {Die1, Die2} ->
+           PipsList = pips_list(Die1, Die2),
+           {ok, ?STATE_WAIT_MOVE, State#state{pips_list = PipsList}}
+    end.
+
+%% --------------------------------------------------------------------
+handle_event(_Event, StateName, StateData) ->
+    {next_state, StateName, StateData}.
+
+%% --------------------------------------------------------------------
+handle_sync_event({player_action, SeatNum, Action}, _From, StateName, StateData) ->
+    case handle_player_action(SeatNum, Action, StateName, StateData) of
+        {ok, Events, NewStateName, NewStateData} ->
+            {reply, {ok, lists:reverse(Events)}, NewStateName, NewStateData};
+        {error, Reason} ->
+            {reply, {error, Reason}, StateName, StateData}
+    end;
+
+handle_sync_event(_Event, _From, StateName, StateData) ->
+    Reply = ok,
+    {reply, Reply, StateName, StateData}.
+
+%% --------------------------------------------------------------------
+handle_info(_Info, StateName, StateData) ->
+    {next_state, StateName, StateData}.
+
+%% --------------------------------------------------------------------
+terminate(_Reason, _StateName, _StatData) ->
+    ok.
+
+%% --------------------------------------------------------------------
+code_change(_OldVsn, StateName, StateData, _Extra) ->
+    {ok, StateName, StateData}.
+
+
+%% --------------------------------------------------------------------
+
+%% @spec handle_player_action(Color, Action, StateName, StateData) ->
+%%          {ok, Events, NextStateName, NextStateData}          |
+%%          {error, Reason}
+%% @end
+
+handle_player_action(PlayerId, {roll, Die1, Die2}, ?STATE_WAIT_ROLL = StateName,
+                     #state{current = Current} = StateData) ->
+    if PlayerId == Current ->
+           process_roll(PlayerId, Die1, Die2, StateName, StateData);
+       true ->
+           {error, not_your_order}
+    end;
+
+handle_player_action(PlayerId, {move, Moves}, ?STATE_WAIT_MOVE = StateName,
+                     #state{current = Current} = StateData) ->
+    if PlayerId == Current ->
+           process_moves(PlayerId, Moves, StateName, StateData);
+       true ->
+           {error, not_your_order}
+    end;
+
+handle_player_action(_PlayerId, _Action, _StateName, _StateData) ->
+    {error, invalid_action}.
+
+%% --------------------------------------------------------------------
+%%% Internal functions
+%% --------------------------------------------------------------------
+
+process_roll(PlayerId, Die1, Die2, _StateName,
+             #state{board = Board, bearoff_waste_moves_enabled = BearoffWasteMovesEnabled,
+                    home_hit_and_run_enabled = HomeHitAndRunEnabled} = StateData) ->
+    PipsList = if Die1 == Die2 -> [Die1, Die1, Die1, Die1];
+                  true -> [Die1, Die2]
+               end,
+    case is_any_move_available(PlayerId, PipsList, Board, BearoffWasteMovesEnabled,
+                               HomeHitAndRunEnabled, []) of
+        true ->
+            Events = [{rolls, PlayerId, Die1, Die2}],
+            {ok, Events, ?STATE_WAIT_MOVE, StateData#state{pips_list = PipsList}};
+        false ->
+            Opponent = opponent(PlayerId),
+            Events = [{next_player, Opponent} , {rolls, PlayerId, Die1, Die2}],
+            {ok, Events, ?STATE_WAIT_ROLL, StateData#state{current = Opponent}}
+    end.
+
+process_moves(PlayerId, Moves, _StateName,
+             #state{board = Board, pips_list = PipsList,
+                    bearoff_waste_moves_enabled = BearoffWasteMovesEnabled,
+                    home_hit_and_run_enabled = HomeHitAndRunEnabled,
+                    hitted_home_positions = HittedHomePositions} = StateData) ->
+    case apply_moves(PlayerId, PipsList, Moves, Board, BearoffWasteMovesEnabled,
+                     HomeHitAndRunEnabled, HittedHomePositions) of
+        {ok, NewBoard, NewPipsList, NewHittedHomePositions, RealMoves} ->
+            MovesEvents = [{moves, PlayerId, lists:reverse(RealMoves)}],
+            case is_game_finished(PlayerId, NewBoard) of
+                {yes, Condition} ->
+                    Events = [{win, PlayerId, Condition} | MovesEvents],
+                    {ok, Events, ?STATE_FINISHED, StateData#state{board = NewBoard,
+                                                                  finish_conditions = {PlayerId, Condition}}};
+                no ->
+                    AnyMoveAvailable = NewPipsList =/= [] andalso
+                                       is_any_move_available(PlayerId, NewPipsList, NewBoard, BearoffWasteMovesEnabled,
+                                                             HomeHitAndRunEnabled, NewHittedHomePositions),
+                    if AnyMoveAvailable ->
+                           {ok, MovesEvents, ?STATE_WAIT_MOVE,
+                            StateData#state{board = NewBoard,
+                                            pips_list = NewPipsList,
+                                            hitted_home_positions = NewHittedHomePositions}};
+                       true ->
+                           Opponent = opponent(PlayerId),
+                           Events = [{next_player, Opponent} | MovesEvents],
+                           {ok, Events, ?STATE_WAIT_ROLL,
+                            StateData#state{board = NewBoard,
+                                            pips_list = [],
+                                            hitted_home_positions = [],
+                                            current = Opponent}}
+                    end
+            end;
+        {error, Reason} ->
+            {error, Reason}
+    end.
+
+
+%% initial_board() -> BoardList
+initial_board() ->
+    [{01, {?BLACK, 2}},
+     {02, empty},
+     {03, empty},
+     {04, empty},
+     {05, empty},
+     {06, {?WHITE, 5}},
+     {07, empty},
+     {08, {?WHITE, 3}},
+     {09, empty},
+     {10, empty},
+     {11, empty},
+     {12, {?BLACK, 5}},
+     {13, {?WHITE, 5}},
+     {14, empty},
+     {15, empty},
+     {16, empty},
+     {17, {?BLACK, 3}},
+     {18, empty},
+     {19, {?BLACK, 5}},
+     {20, empty},
+     {21, empty},
+     {22, empty},
+     {23, empty},
+     {24, {?WHITE, 2}},
+     {?WHITE_OUT, empty},
+     {?BLACK_OUT, empty},
+     {?WHITE_BAR, empty},
+     {?BLACK_BAR, empty}
+    ].
+
+init_board(Spec) ->
+    EmptyBoard = empty_board(),
+    F = fun({Pos, Value}, Acc) ->
+                dict:store(Pos, Value, Acc)
+        end,
+    lists:foldl(F, EmptyBoard, Spec).
+
+empty_board() ->
+    dict:from_list([{Pos, empty} || Pos <- [wb, bb, wo,bo | lists:seq(1, 24)]]).
+
+%% get_param(Id, Params) -> Value
+get_param(Id, Params) ->
+    {_, Value} = lists:keyfind(Id, 1, Params),
+    Value.
+
+
+%% get_option(Id, Params, DefaultValue) -> Value
+get_option(Id, Params, DefaultValue) ->
+    case lists:keyfind(Id, 1, Params) of
+        {_, Value} -> Value;
+        false -> DefaultValue
+    end.
+
+
+%% TODO: Implement the validator
+validate_params(_HomeHitAndRun, _WastePipsDuringBearoff, _FirstMove, _Desk, _Dice) ->
+    ok.
+
+pips_list(Die, Die) -> [Die, Die, Die, Die];
+pips_list(Die1, Die2) -> [Die1, Die2].
+
+
+%% opponent(Color1) -> Color2
+opponent(?WHITE) -> ?BLACK;
+opponent(?BLACK) -> ?WHITE.
+
+
+%% is_game_finished(Color, Board) -> {yes, normal | mars} | no
+is_game_finished(Color, Board) ->
+    case get_checkers(out_position(Color), Board) of
+        {Color, ?CHECKERS_NUM} ->
+            case get_checkers(out_position(opponent(Color)), Board) of
+                empty -> {yes, mars};
+                _ -> {yes, normal}
+            end;
+        _ -> no
+    end.
+
+
+%% get_checkers(Pos, Board) -> {Color, Num} | empty
+get_checkers(Pos, Board) -> dict:fetch(Pos, Board).
+
+%% move_checker(From, To, Board) -> NewBoard
+move_checker(From, To, Board) ->
+    {Color, FromNum} = dict:fetch(From, Board),
+    NewBoard = if FromNum == 1 -> dict:store(From, empty, Board);
+                  true -> dict:store(From, {Color, FromNum - 1}, Board)
+               end,
+    case dict:fetch(To, NewBoard) of
+        empty -> dict:store(To, {Color, 1}, NewBoard);
+        {Color, ToNum} -> dict:store(To, {Color, ToNum + 1}, NewBoard)
+    end.
+
+
+apply_moves(_Color, PipsList, Moves, _Board, _BearoffWasteMoveEnabled,
+            _HomeHitAndRunEnabled, _HittedHomePositions)
+  when length(Moves) > length(PipsList) ->
+    {error, too_many_moves};
+
+apply_moves(Color, PipsList, Moves, Board,
+            BearoffWasteMoveEnabled, HomeHitAndRunEnabled, HittedHomePositions) ->
+    apply_moves2(Color, PipsList, Moves, Board, _MoveEvents = [], HittedHomePositions,
+                 BearoffWasteMoveEnabled, HomeHitAndRunEnabled).
+
+
+apply_moves2(_Color, PipsList, _Moves = [], Board, MoveEvents, HittedHomePositions,
+             _BearoffWasteMoveEnabled, _HomeHitAndRunEnabled) ->
+    {ok, Board, PipsList, HittedHomePositions, MoveEvents};
+
+apply_moves2(Color, PipsList, [{From, To} = Move | RestMoves], Board, MoveEvents,
+             HittedHomePositions, BearoffWasteMoveEnabled, HomeHitAndRunEnabled) ->
+    case take_pips(Color, Move, PipsList, Board) of
+        {ok, Pips, NewPipsList} ->
+            case check_move_posibility(Color, From, To, PipsList, Board, BearoffWasteMoveEnabled,
+                                       HomeHitAndRunEnabled, HittedHomePositions) of
+                ok ->
+                    NewBoard = move_checker(From, To, Board),
+                    NewMoveEvents = [{move, From, To, Pips} | MoveEvents],
+                    apply_moves2(Color, NewPipsList, RestMoves, NewBoard, NewMoveEvents,
+                                 HittedHomePositions, BearoffWasteMoveEnabled, HomeHitAndRunEnabled);
+                hit ->
+                    NewBoard1 = move_checker(To, bar_position(opponent(Color)), Board),
+                    NewBoard2 = move_checker(From, To, NewBoard1),
+                    {HomeMin, HomeMax} = home_range(Color),
+                    NewHittedHomePositions = if To >= HomeMin andalso To =< HomeMax ->
+                                                    [To | HittedHomePositions];
+                                                true -> HittedHomePositions end,
+                    NewMoveEvents = [{hit, From, To, Pips} | MoveEvents],
+                    apply_moves2(Color, NewPipsList, RestMoves, NewBoard2, NewMoveEvents,
+                                 NewHittedHomePositions, BearoffWasteMoveEnabled,
+                                 HomeHitAndRunEnabled);
+                {error, occupied} -> {error, {position_occupied, Move, RestMoves}};
+                {error, waste_move} -> {error, {waste_move_disabled, Move, RestMoves}};
+                {error, hit_and_run} -> {error, {hit_and_run_disabled, Move, RestMoves}};
+                {error, not_bear_off_mode} -> {error, {not_bear_off_mode, Move, RestMoves}};
+                {error, no_checker} -> {error, {no_checker, Move, RestMoves}};
+                {error, move_from_bar_first} -> {error, {move_from_bar_first, Move, RestMoves}}
+            end;
+        error -> {error, {invalid_move, Move, RestMoves}}
+    end.
+
+
+is_any_move_available(Color, Pips, Board, BearoffWasteMoveEnabled,
+                      HomeHitAndRunEnabled, HitedHomePositions) ->
+    Bar = bar_position(Color),
+    case get_checkers(Bar, Board) of
+        {Color, _} -> is_any_move_available_bar(Color, Pips, Board);
+        empty -> is_any_move_available_desk(Color, Pips, Board, BearoffWasteMoveEnabled,
+                                            HomeHitAndRunEnabled, HitedHomePositions)
+    end.
+
+
+is_any_move_available_bar(Color, Pips, Board) ->
+    F = fun(P) ->
+                case check_destination(Color, new_pos(Color, bar_position(Color), P), Board) of
+                    ok -> true;
+                    hit -> true;
+                    occupied -> false
+                end
+        end,
+    lists:any(F, Pips).
+
+
+is_any_move_available_desk(Color, Pips, Board, BearoffWasteMoveEnabled,
+                           HomeHitAndRunEnabled, HitedHomePositions) ->
+    F = fun(Pos) ->
+                F2 = fun(P) ->
+                             case check_move_posibility(Color, Pos, new_pos(Color, Pos, P), Pips,
+                                                        Board, BearoffWasteMoveEnabled,
+                                                        HomeHitAndRunEnabled, HitedHomePositions) of
+                                 ok -> true;
+                                 hit -> true;
+                                 {error, _} -> false
+                             end
+                     end,
+                lists:any(F2, Pips)
+        end,
+    lists:any(F, route(Color)).
+
+
+%% check_move_posibility/8 -> ok | hit | {error, Reason}
+check_move_posibility(Color, From, To, Pips, Board, BearoffWasteMoveEnabled,
+                      HomeHitAndRunEnabled, HitedHomePositions) ->
+    BarPos = bar_position(Color),
+    CheckersOnBar = case get_checkers(BarPos, Board) of
+                        {Color, _} -> true;
+                        empty -> false
+                    end,
+    if From =/= BarPos andalso CheckersOnBar ->
+           {error, move_from_bar_first};
+       true ->
+           check_checker(Color, From, To, Pips, Board, BearoffWasteMoveEnabled,
+                         HomeHitAndRunEnabled, HitedHomePositions)
+    end.
+
+
+check_checker(Color, From, To, Pips, Board, BearoffWasteMoveEnabled,
+              HomeHitAndRunEnabled, HitedHomePositions) ->
+    case get_checkers(From, Board) of
+        {Color, _} -> check_home_hit_and_run(Color, From, To, Pips, Board, BearoffWasteMoveEnabled,
+                                             HomeHitAndRunEnabled, HitedHomePositions);
+        _ -> {error, no_checker}
+    end.
+
+
+check_home_hit_and_run(Color, From, To, Pips, Board, BearoffWasteMoveEnabled,
+                       HomeHitAndRunEnabled, HitedHomePositions) ->
+    TestPassed = if HomeHitAndRunEnabled -> true;
+                    true ->
+                        {_, Num} = get_checkers(From, Board),
+                        if Num > 1 -> true;
+                           true -> not lists:member(From, HitedHomePositions)
+                        end
+                 end,
+    if TestPassed -> check_waste_move(Color, From, To, Pips, Board, BearoffWasteMoveEnabled,
+                                      HomeHitAndRunEnabled, HitedHomePositions);
+       true -> {error, hit_and_run}
+    end.
+
+
+check_waste_move(Color, From, To, Pips, Board, BearoffWasteMoveEnabled,
+                 HomeHitAndRunEnabled, HitedHomePositions) ->
+    BearOffMode = detect_bearoff_mode(Color, Board),
+    OutPos = out_position(Color),
+    TestPassed = if BearoffWasteMoveEnabled -> true;
+                    true ->
+                        case BearOffMode of
+                            false -> true;
+                            true ->
+                                case To == OutPos of
+                                    true -> true;
+                                    false -> not out_possible(Color, Pips, Board)
+                                end
+                        end
+                 end,
+    if TestPassed -> check_destination_pos(Color, To, Board, BearOffMode, OutPos);
+       true -> {error, waste_move}
+    end.
+
+
+check_destination_pos(Color, To, Board, BearOffMode, OutPos) ->
+    if To == OutPos andalso not BearOffMode ->
+           {error, not_bear_off_mode};
+       true ->
+           case check_destination(Color, To, Board) of
+               ok -> ok;
+               hit -> hit;
+               occupied -> {error, occupied}
+           end
+    end.
+
+out_possible(Color, Pips, Board) ->
+    out_pip_exists(Color, Pips, Board) orelse
+    far_pip_exists(Color, Pips, Board).
+
+far_pip_exists(Color, Pips, Board) ->
+    MaxPip = lists:max(Pips),
+    not more_far_checkers_exist(Color, MaxPip, Board).
+
+out_pip_exists(Color, Pips, Board) ->
+    F = fun(Pip) -> case get_checkers(prev_pos(Color, ?WHITE_OUT, Pip), Board) of
+                        {Color, _} -> true;
+                        _ -> false
+                    end
+        end,
+    lists:any(F, Pips).
+
+
+detect_bearoff_mode(Color, Board) ->
+    Out = out_position(Color),
+    Bar = bar_position(Color),
+    {HomeMin, HomeMax} = home_range(Color),
+    F = fun({Pos, {C, _}}) when C == Color, Pos == Out -> true;
+           ({Pos, {C, _}}) when C == Color, Pos == Bar -> false;
+           ({Pos, {C, _}}) when C == Color -> Pos >= HomeMin andalso Pos =< HomeMax;
+           (_) -> true
+        end,
+    lists:all(F, dict:to_list(Board)).
+
+
+out_position(?WHITE) -> ?WHITE_OUT;
+out_position(?BLACK) -> ?BLACK_OUT.
+
+bar_position(?WHITE) -> ?WHITE_BAR;
+bar_position(?BLACK) -> ?BLACK_BAR.
+
+home_range(?WHITE) -> {1, 6};
+home_range(?BLACK) -> {19, 24}.
+
+new_pos(?WHITE, From, Pips) ->
+    if From == ?WHITE_BAR -> 25 - Pips;
+       (From - Pips) > 0 -> From - Pips;
+       (From - Pips) =< 0 -> ?WHITE_OUT
+    end;
+new_pos(?BLACK, From, Pips) ->
+    if From == ?BLACK_BAR -> Pips;
+       (From + Pips) < 25 -> From + Pips;
+       (From + Pips) >= 25 -> ?BLACK_OUT
+    end.
+
+prev_pos(?WHITE, At, Pips) ->
+    if At == ?WHITE_OUT -> Pips;
+       is_integer(At) -> At + Pips
+    end;
+prev_pos(?BLACK, At, Pips) ->
+    if At == ?BLACK_OUT -> 25 - Pips;
+       is_integer(At) -> At - Pips
+    end.
+
+
+route(?WHITE) -> lists:seq(24, 1, -1);
+route(?BLACK) -> lists:seq(1, 24).
+
+check_destination(Color, To, Board) ->
+    OpColor = opponent(Color),
+    case get_checkers(To, Board) of
+        empty -> ok;
+        {Color, _} -> ok;
+        {OpColor, 1} -> hit;
+        {OpColor, _} -> occupied
+    end.
+
+
+%% take_pips(Color, {From, To}, Pips, BearoffMode) -> {ok, NewPips} | error
+take_pips(Color, {From, To}, _Pips, _BearoffMode)
+  when From == ?WHITE_OUT;
+       From == ?BLACK_OUT;
+       From == To;
+       From == ?WHITE_BAR andalso To == ?WHITE_OUT;
+       From == ?BLACK_BAR andalso To == ?BLACK_OUT;
+       Color == ?WHITE andalso (From == ?BLACK_BAR orelse To == ?BLACK_OUT);
+       Color == ?BLACK andalso (From == ?WHITE_BAR orelse To == ?WHITE_OUT)
+  ->
+    error;
+
+take_pips(Color, {From, To}, PipsList, Board) ->
+    Dist = if is_integer(From), is_integer(To) -> abs(To - From);
+              From == ?WHITE_BAR -> 25 - To;
+              From == ?BLACK_BAR -> To;
+              To == ?WHITE_OUT -> From;
+              To == ?BLACK_OUT -> 25 - From
+           end,
+    case find_pips(Color, Dist, To, PipsList, Board) of
+        {ok, Pips} -> {ok, Pips, lists:delete(Pips, PipsList)};
+        error -> error
+    end.
+
+
+%% find_pips(Color, Dist, To, PipsList, Board) -> {ok, Pips} | error
+find_pips(Color, Dist, To, PipsList, Board) ->
+    case lists:member(Dist, PipsList) of
+        true -> {ok, Dist};
+        false ->
+            BearoffMode = detect_bearoff_mode(Color, Board),
+            if BearoffMode ->
+                   Out = out_position(Color),
+                   if To == Out ->
+                          case more_far_checkers_exist(Color, Dist, Board) of
+                              true -> error;
+                              false ->
+                                  BiggestPips = lists:max(PipsList),
+                                  if BiggestPips >= Dist -> {ok, BiggestPips};
+                                     true -> error
+                                  end
+                          end;
+                      true -> error
+                   end;
+               true -> error
+            end
+    end.
+
+
+%% more_far_checkers_exist(Color, Dist, Board) -> boolean()
+more_far_checkers_exist(Color, Dist, Board) ->
+    {HomeMin, HomeMax} = home_range(Color),
+    {RangeMin, RangeMax} = if Color == ?WHITE -> {HomeMin + Dist, HomeMax};
+                              Color == ?BLACK -> {HomeMin, HomeMax - Dist}
+                           end,
+    F = fun(Pos) ->
+                case get_checkers(Pos, Board) of
+                    {Color, _} -> true;
+                    _ -> false
+                end
+        end,
+    lists:any(F, lists:seq(RangeMin, RangeMax)).
+

+ 210 - 0
apps/server/src/tavla/game_tavla_ng_scoring.erl

@@ -0,0 +1,210 @@
+%% Author: serge
+%% Created: Jan 24, 2013
+%% Description:
+-module(game_tavla_ng_scoring).
+
+-include_lib("eunit/include/eunit.hrl").
+-include_lib("server/include/log.hrl").
+
+%%
+%% Exported Functions
+%%
+-export([
+         init/3,
+         last_round_result/1,
+         round_finished/2
+        ]).
+
+
+-define(MODE_STANDARD, standard).
+-define(MODE_PAIRED, paired).
+
+-define(ACH_WIN_NORMAL, 1).
+-define(ACH_WIN_MARS, 2).
+-define(ACH_OPP_SURRENDER_NORMAL, 3).
+-define(ACH_OPP_SURRENDER_MARS, 4).
+
+
+-record(state,
+        {mode             :: standard,
+         seats_num        :: integer(),
+         rounds_num       :: undefined | pos_integer(),
+         last_round_num   :: integer(),
+         round_score      :: list(),    %% [{SeatNum, DeltaPoints}]
+         round_achs       :: list(),    %% [{SeatNum, [{AchId, Points}]}]
+         total_score      :: list(),    %% [{SeatNum, Points}]
+         finish_info      :: term()     %% FinishInfo
+        }).
+
+%%
+%% API Functions
+%%
+
+%% @spec init(Mode, SeatsInfo, RoundsNum) -> ScoringState
+%% @doc Initialises scoring state.
+%% @end
+%% Types:
+%%     Mode = standard | paired
+%%     SeatsInfo = [{SeatNum, Points}]
+%%       SeatNum = integer()
+%%       Points = integer()
+%%     RoundsNum = undefined | pos_integer()
+
+init(Mode, SeatsInfo, RoundsNum) ->
+    ?INFO("TAVLA_NG_SCORING init Mode: ~p SeatsInfo = ~p RoundsNum = ~p", [Mode, SeatsInfo, RoundsNum]),
+    true = lists:member(Mode, [?MODE_STANDARD, ?MODE_PAIRED]),
+    true = is_integer(RoundsNum) orelse RoundsNum == undefined,
+    SeatsNum = length(SeatsInfo),
+    true = lists:seq(1, SeatsNum) == lists:sort([SeatNum || {SeatNum, _} <- SeatsInfo]),
+    #state{mode = Mode,
+           seats_num = SeatsNum,
+           rounds_num = RoundsNum,
+           last_round_num = 0,
+           round_score = undefined,
+           finish_info = undefined,
+           round_achs = undefined,
+           total_score = SeatsInfo
+          }.
+
+
+%% @spec last_round_result(ScoringState) -> {FinishInfo, RoundScore, AchsPoints, TotalScore} |
+%%                                          no_rounds_played
+%% @end
+%% Types:
+%%     FinishInfo =  timeout | set_timeout |
+%%                   {win, WinnerSeatNum, Condition}
+%%                      Condition = normal | mars
+%%     RoundScore = [{SeatNum, DeltaPoints}]
+%%     AchsPoints = [{SeatNum, [{AchId, Points}]}]
+%%     TotalScore = [{SeatNum, Points}]
+
+last_round_result(#state{last_round_num = 0}) -> no_rounds_played;
+
+last_round_result(#state{round_score = RoundScore,
+                         round_achs = RoundAchs,
+                         total_score = TotalScore,
+                         finish_info = FinishInfo
+                        }) ->
+    {FinishInfo, RoundScore, RoundAchs, TotalScore}.
+
+
+%% @spec round_finished(ScoringState, FinishReason) ->
+%%                                    {NewScoringState, GameIsOver}
+%% @end
+%% Types:
+%%     FinishReason = timeout | set_timeout |
+%%                    {win, SeatNum, Condition} |
+%%                    {surrender, SeatNum, Condition}
+%%       Condition = normal | mars
+%%     GameisOver = boolean() 
+
+round_finished(#state{mode = GameMode, seats_num = SeatsNum,
+                      last_round_num = LastRoundNum,
+                      total_score = TotalScore} = State,
+               FinishReason) ->
+    ScoringMode = get_scoring_mode(GameMode),
+    PointingRules = get_pointing_rules(ScoringMode),
+    Seats = lists:seq(1, SeatsNum),
+    FinishInfo = finish_info(GameMode, FinishReason),
+    PlayersAchs = players_achivements(GameMode, Seats, FinishInfo),
+    RoundAchs = [{SeatNum, get_achivements_points(PointingRules, Achivements)}
+                 || {SeatNum, Achivements} <- PlayersAchs],
+    RoundScore = [{SeatNum, sum_achivements_points(AchPoints)}
+                   || {SeatNum, AchPoints} <- RoundAchs],
+    RoundNum = LastRoundNum + 1,
+    NewTotalScore = add_delta(TotalScore, RoundScore),
+    NewState = State#state{last_round_num = RoundNum,
+                           round_score = RoundScore,
+                           total_score = NewTotalScore,
+                           round_achs = RoundAchs,
+                           finish_info = FinishInfo},
+    {NewState, detect_game_finish(NewState)}.
+
+%%
+%% Local Functions
+%%
+
+detect_game_finish(#state{last_round_num = RoundNum, finish_info = FinishInfo,
+                          rounds_num = MaxRoundNum}) ->
+    if FinishInfo == set_timeout -> true;
+       true -> RoundNum == MaxRoundNum
+    end.
+
+
+players_achivements(Mode, Seats, FinishInfo) ->
+    case FinishInfo of
+        timeout ->
+            [{SeatNum, player_achivements_no_winner(Mode, SeatNum)} || SeatNum <- Seats];
+        set_timeout ->
+            [{SeatNum, player_achivements_no_winner(Mode, SeatNum)} || SeatNum <- Seats];
+        {win, Winner, Condition} ->
+            [{SeatNum, player_achivements_win(Mode, SeatNum, Winner, Condition)} || SeatNum <- Seats];
+        {surrender, Surrender, Condition} ->
+            [{SeatNum, player_achivements_surrender(Mode, SeatNum, Surrender, Condition)} || SeatNum <- Seats]
+    end.
+
+
+%% finish_info(GameMode, FinishReason) ->
+%%      timeout |
+%%      set_timeout |
+%%      {win, Winner, Condition} |
+%%      {surrender, Surrender, Condition}
+finish_info(_GameMode, FinishReason) -> FinishReason.
+
+
+%% @spec get_achivements_points(PointingRules, Achivements) -> AchsPoints
+%% @end
+get_achivements_points(PointingRules, Achivements) ->
+    [{Ach, lists:nth(Ach, PointingRules)} || Ach <- Achivements].
+
+%% @spec sum_achivements_points(AchPoints) -> integer()
+%% @end
+sum_achivements_points(AchPoints) ->
+    lists:foldl(fun({_, P}, Acc)-> Acc + P end, 0, AchPoints).
+
+%% @spec add_delta(TotalScore, RoundScores) -> NewTotalScore
+%% @end
+add_delta(TotalScore, RoundScores) ->
+    [{SeatNum, proplists:get_value(SeatNum, TotalScore) + Delta}
+     || {SeatNum, Delta} <- RoundScores].
+
+
+player_achivements_no_winner(Mode, SeatNum) ->
+    player_achivements(Mode, SeatNum, no_winner, undefined, undefined).
+
+player_achivements_win(Mode, SeatNum, Winner, Condition) ->
+    player_achivements(Mode, SeatNum, win, Winner, Condition).
+
+player_achivements_surrender(Mode, SeatNum, Surrender, Condition) ->
+    player_achivements(Mode, SeatNum, surrender, Surrender, Condition).
+
+
+%% player_achivements(Mode, SeatNum, FinishType, Subject, Condition) -> [{AchId}]
+player_achivements(_Mode, SeatNum, FinishType, Subject, Condition) ->
+    L=[%% 1
+       {?ACH_WIN_NORMAL, FinishType == win andalso SeatNum == Subject andalso Condition == normal},
+       %% 2
+       {?ACH_WIN_MARS, FinishType == win andalso SeatNum == Subject andalso Condition == mars},
+       %% 3
+       {?ACH_OPP_SURRENDER_NORMAL, FinishType == surrender andalso SeatNum =/= Subject andalso Condition == normal},
+       %% 4
+       {?ACH_OPP_SURRENDER_MARS, FinishType == surrender andalso SeatNum =/= Subject andalso Condition == mars}
+      ],
+    [Ach || {Ach, true} <- L].
+
+
+get_pointing_rules(ScoringMode) ->
+    {_, Rules} = lists:keyfind(ScoringMode, 1, points_matrix()),
+    Rules.
+
+points_matrix() ->
+    [%ScoringMode  1   2   3   4 <--- achievement number
+     {standard,   [1,  2,  1,  2]}
+    ].
+
+%%===================================================================
+
+%% @spec get_scoring_mode(GameMode) ->  ScoringMode
+%% @end
+get_scoring_mode(?MODE_STANDARD) ->  standard;
+get_scoring_mode(?MODE_PAIRED) ->  standard.

+ 1883 - 0
apps/server/src/tavla/game_tavla_ng_table.erl

@@ -0,0 +1,1883 @@
+%%% -------------------------------------------------------------------
+%%% Author  : Sergei Polkovnikov <serge.polkovnikov@gmail.com>
+%%% Description :
+%%%
+%%% Created : Jan 21, 2013
+%%% -------------------------------------------------------------------
+-module(game_tavla_ng_table).
+
+-behaviour(gen_fsm).
+%% --------------------------------------------------------------------
+%% Include files
+%% --------------------------------------------------------------------
+-include_lib("server/include/log.hrl").
+-include_lib("server/include/basic_types.hrl").
+-include_lib("server/include/settings.hrl").
+-include_lib("server/include/game_tavla.hrl").
+-include_lib("server/include/requests.hrl").
+
+%% --------------------------------------------------------------------
+%% External exports
+-export([start/3,
+         player_action/3,
+         parent_message/2,
+         relay_message/2
+        ]).
+
+-export([submit/3, signal/3]).
+
+%% gen_fsm callbacks
+-export([init/1, handle_event/3, handle_sync_event/4,
+         handle_info/3, terminate/3, code_change/4]).
+
+-type color() :: black | white.
+
+-record(desk_state,
+        {
+         state              :: state_playing | state_finished,
+         board              :: term(),
+         cur_color          :: integer(),
+         dice               :: {undefined | integer(), undefined | integer()}, %% The dice are used for first move order
+                               %% determination and for playing. In first case the first element is for white die, and the
+                               %% second one for black die.
+         pips_list          :: list(integer()),
+         finish_reason      :: undefined | win | round_timeout | set_timeout | surrender,
+         finish_info        :: undefined | {black | white, normal | mars}
+        }).
+
+-record(state,
+        {%% Fixed parameters
+         game_id              :: pos_integer(),
+         table_id             :: pos_integer(),
+         table_name           :: string(),
+         parent               :: {atom(), pid()},
+         relay                :: pid(),
+         mult_factor          :: integer(),
+         slang_flag           :: boolean(),
+         observer_flag        :: boolean(),
+         tournament_type      :: atom(), %%  standalone | elimination | pointing | lucky
+         game_mode            :: normal | kakara,
+         speed                :: slow | normal | fast,
+         turn_timeout         :: integer(),
+         ready_timeout        :: integer(),
+         round_timeout        :: infinity | integer(),
+         set_timeout          :: infinity | integer(),
+         rounds               :: undefined | integer(), %% Not defined for countdown game type
+         next_series_confirmation :: yes_exit | no_exit | no,
+         pause_mode           :: disabled | normal,
+         social_actions_enabled :: boolean(),
+         tour                 :: undefined | integer(),
+         tours                :: undefined | integer(),
+         parent_mon_ref       :: reference(),
+         tables_num           :: integer(), %% For paired mode >= 1, otherwise  = 1
+         %% Dynamic parameters
+         desk_rule_pid        :: undefined | pid(),
+         players,             %% The register of table players
+         tournament_table     :: list(), %% [{TurnNum, TurnRes}], TurnRes = [{PlayerId, Points, Status}]
+         start_color          :: color(), %% The player who moves first
+         cur_round            :: integer(),
+         desk_state           :: #desk_state{}, %% For tracking current state of a game on the table
+         scoring_state        :: term(), %% generated by a scoring module
+         wait_list            :: list(),
+         timeout_timer        :: undefined | reference(),
+         timeout_magic        :: term(),
+         round_timer          :: undefined | reference(),
+         set_timer            :: undefined | reference(),
+         paused_statename     :: atom(), %% For storing a statename before pause
+         paused_timeout_value :: integer() %% For storing remain timeout value
+        }).
+
+-record(player,
+        {
+         id              :: pos_integer(), %% Player Id
+         color           :: undefined | color(),
+         seat_num        :: integer(),
+         user_id         :: binary(),
+         is_bot          :: boolean(),
+         info            :: #'PlayerInfo'{},
+         connected       :: boolean()
+        }).
+
+-define(STATE_WAITING_FOR_START, state_waiting_for_start).
+-define(STATE_FIRST_MOVE_COMPETITION, state_first_move_competition).
+-define(STATE_PLAYING, state_playing).
+-define(STATE_FINISHED, state_finished).
+-define(STATE_PAUSE, state_pause).
+-define(STATE_SET_FINISHED, state_set_finished).
+
+-define(RELAY, relay_ng).
+-define(DESK, game_tavla_ng_desk).
+-define(SCORING, game_tavla_ng_scoring).
+-define(LIB, game_tavla_lib).
+
+-define(BLACK, black).
+-define(WHITE, white).
+-define(WHITE_OUT, wo).
+-define(WHITE_BAR, wb).
+-define(BLACK_OUT, bo).
+-define(BLACK_BAR, bb).
+
+%% ====================================================================
+%% External functions
+%% ====================================================================
+
+start(GameId, TableId, TableParams) ->
+    gen_fsm:start(?MODULE, [GameId, TableId, TableParams], []).
+
+player_action(Srv, PlayerId, Action) ->
+    gen_fsm:sync_send_all_state_event(Srv, {player_action, PlayerId, Action}).
+
+parent_message(Srv, Message) ->
+    gen_fsm:send_all_state_event(Srv, {parent_message, Message}).
+
+relay_message(Srv, Message) ->
+    gen_fsm:send_all_state_event(Srv, {relay_message, Message}).
+
+
+submit(Table, PlayerId, Action) ->
+    player_action(Table, PlayerId, {submit, Action}).
+
+signal(Table, PlayerId, Signal) ->
+    player_action(Table, PlayerId, {signal, Signal}).
+
+%% ====================================================================
+%% Server functions
+%% ====================================================================
+init([GameId, TableId, Params]) ->
+    Parent = proplists:get_value(parent, Params),
+    PlayersInfo = proplists:get_value(players, Params),
+    TableName = proplists:get_value(table_name, Params),
+    MultFactor = proplists:get_value(mult_factor, Params),
+    SlangFlag = proplists:get_value(slang_allowed, Params),
+    ObserversFlag = proplists:get_value(observers_allowed, Params),
+    TournamentType = proplists:get_value(tournament_type, Params),
+    Speed = proplists:get_value(speed, Params),
+    TurnTimeout = proplists:get_value(turn_timeout, Params, get_timeout(turn, Speed)), %% TODO Set this param explictly
+    ReadyTimeout = proplists:get_value(ready_timeout, Params, get_timeout(ready, Speed)), %% TODO Set this param explictly
+    RoundTimeout = proplists:get_value(round_timeout, Params),
+    SetTimeout = proplists:get_value(set_timeout, Params),
+    GameMode = proplists:get_value(game_mode, Params),
+    Rounds = proplists:get_value(rounds, Params),
+    NextSeriesConfirmation = proplists:get_value(next_series_confirmation, Params),
+    PauseMode = proplists:get_value(pause_mode, Params),
+    SocialActionsEnabled = proplists:get_value(social_actions_enabled, Params),
+    TTable = proplists:get_value(ttable, Params),
+    Tour = proplists:get_value(tour, Params),
+    Tours = proplists:get_value(tours, Params),
+    TablesNum = proplists:get_value(tables_num, Params),
+    %% Next two options will be passed on table respawn (after fail or service maintaince)
+    ScoringState = proplists:get_value(scoring_state, Params, init_scoring(GameMode, PlayersInfo, Rounds)),
+    CurRound = proplists:get_value(cur_round, Params, 0),
+
+    Players = init_players(PlayersInfo),
+    RelayParams = [{players, [{PlayerId, UserInfo#'PlayerInfo'.id} || {PlayerId, UserInfo, _, _} <- PlayersInfo]},
+                   {observers_allowed, false},
+                   {table, {?MODULE, self()}}],
+    {ok, Relay} = ?RELAY:start(RelayParams),
+    [?INFO("TAVLA_NG_TABLE_DBG <~p,~p> Parameter <~p> : ~p", [GameId, TableId, P, V]) ||
+           {P, V} <- Params],
+    ?INFO("TAVLA_NG_TABLE <~p,~p> Started.", [GameId, TableId]),
+    parent_notify_table_created(Parent, TableId, Relay),
+    {_, ParentPid} = Parent,
+    ParentMonRef = erlang:monitor(process, ParentPid),
+    {ok, ?STATE_WAITING_FOR_START, #state{game_id = GameId,
+                                          table_id = TableId,
+                                          table_name = TableName,
+                                          parent = Parent,
+                                          parent_mon_ref = ParentMonRef,
+                                          relay = Relay,
+                                          mult_factor = MultFactor,
+                                          slang_flag = SlangFlag,
+                                          observer_flag = ObserversFlag,
+                                          tournament_type = TournamentType,
+                                          tour = Tour,
+                                          tours = Tours,
+                                          speed = Speed,
+                                          turn_timeout = TurnTimeout,
+                                          ready_timeout = ReadyTimeout,
+                                          round_timeout = RoundTimeout,
+                                          set_timeout = SetTimeout,
+                                          game_mode = GameMode,
+                                          rounds = Rounds,
+                                          next_series_confirmation = NextSeriesConfirmation,
+                                          pause_mode = PauseMode,
+                                          social_actions_enabled = SocialActionsEnabled,
+                                          players = Players,
+                                          start_color = undefined,
+                                          cur_round = CurRound,
+                                          scoring_state = ScoringState,
+                                          tournament_table = TTable,
+                                          tables_num = TablesNum
+                                         }}.
+
+handle_event({parent_message, Message}, StateName,
+             #state{game_id = GameId, table_id = TableId} = StateData) ->
+    ?INFO("TAVLA_NG_TABLE_DBG <~p,~p> Received message from the parent: ~p.",
+          [GameId, TableId, Message]),
+    handle_parent_message(Message, StateName, StateData);
+
+handle_event({relay_message, Message}, StateName,
+             #state{game_id = GameId, table_id = TableId} =  StateData) ->
+    ?INFO("TAVLA_NG_TABLE_DBG <~p,~p> Received message from the relay: ~p.",
+          [GameId, TableId, Message]),
+    handle_relay_message(Message, StateName, StateData);
+
+handle_event(_Event, StateName, StateData) ->
+    {next_state, StateName, StateData}.
+
+handle_sync_event({player_action, PlayerId, Action}, From, StateName,
+                  #state{players = Players} = StateData) ->
+    case get_player(PlayerId, Players) of
+        {ok, Player} ->
+            handle_player_action(Player, Action, From, StateName, StateData);
+        error ->
+            {reply, {error, you_are_not_a_player}, StateName, StateData}
+    end;
+
+handle_sync_event(_Event, _From, StateName, StateData) ->
+    Reply = ok,
+    {reply, Reply, StateName, StateData}.
+
+handle_info({timeout, Magic}, ?STATE_FIRST_MOVE_COMPETITION,
+            #state{timeout_magic = Magic, game_id = GameId, table_id = TableId} = StateData) ->
+    ?INFO("TAVLA_NG_TABLE <~p,~p> First move competition timeout. Do an automatic rolls.", [GameId, TableId]),
+    do_first_move_competition_timeout_rolls(StateData);
+
+handle_info({timeout, Magic}, ?STATE_PLAYING,
+            #state{timeout_magic = Magic, game_id = GameId, table_id = TableId} = StateData) ->
+    ?INFO("TAVLA_NG_TABLE <~p,~p> Move timeout. Do an automatic action(s).", [GameId, TableId]),
+    do_timeout_moves(StateData);
+
+handle_info({round_timeout, Round}, ?STATE_PLAYING,
+            #state{cur_round = Round, desk_state = DeskState, game_id = GameId,
+                   table_id = TableId} = StateData) ->
+    ?INFO("TAVLA_NG_TABLE <~p,~p> Time to finish round ~p because of the "
+          "round timeout.", [GameId, TableId, Round]),
+    finalize_round(StateData#state{desk_state = DeskState#desk_state{finish_reason = timeout}});
+
+handle_info(set_timeout, ?STATE_PLAYING,
+            #state{cur_round = Round, desk_state = DeskState, game_id = GameId,
+                   table_id = TableId} = StateData) ->
+    ?INFO("TAVLA_NG_TABLE <~p,~p> Time to finish round ~p and the set because of "
+          "the set timeout.", [GameId, TableId, Round]),
+    finalize_round(StateData#state{desk_state = DeskState#desk_state{finish_reason = set_timeout}});
+
+handle_info(set_timeout, ?STATE_SET_FINISHED = StateName,
+            #state{game_id = GameId, table_id = TableId} = StateData) ->
+    ?INFO("TAVLA_NG_TABLE <~p,~p> Time to finish the set because of the set timeout. "
+          "But the set is finished already. Ignoring", [GameId, TableId]),
+    {next_state, StateName, StateData};
+
+handle_info(set_timeout, StateName,
+            #state{game_id = GameId, table_id = TableId, scoring_state = ScoringState,
+                   parent = Parent, players = Players, timeout_timer = TRef} = StateData) ->
+    ?INFO("TAVLA_NG_TABLE <~p,~p> Time to finish the set because of the set "
+          "timeout at state <~p>. The set is over", [GameId, TableId, StateName]),
+    if TRef =/= undefined -> erlang:cancel_timer(TRef);
+       true -> do_nothing end,
+    NewStateData = StateData#state{timeout_timer = undefined,
+                                   timeout_magic = undefined},
+    {_, RoundScore, _, TotalScore} = ?SCORING:last_round_result(ScoringState),
+    RoundScorePl = [{get_player_id_by_seat_num(SeatNum, Players), Points} || {SeatNum, Points} <- RoundScore],
+    TotalScorePl = [{get_player_id_by_seat_num(SeatNum, Players), Points} || {SeatNum, Points} <- TotalScore],
+    parent_send_set_res(Parent, TableId, ScoringState, RoundScorePl, TotalScorePl),
+    {next_state, ?STATE_SET_FINISHED, NewStateData};
+
+handle_info({'DOWN', MonitorRef, _Type, _Object, Info}, _StateName,
+             #state{game_id = GameId, table_id = TableId, parent_mon_ref = MonitorRef
+                   } = StateData) ->
+    ?INFO("TAVLA_NG_TABLE_DBG <~p,~p> The parent is died with reason: ~p. Stopping",
+          [GameId, TableId, Info]),
+    {stop, parent_died, StateData};
+
+handle_info(Info, StateName, #state{game_id = GameId, table_id = TableId} = StateData) ->
+    ?INFO("TAVLA_NG_TABLE_DBG <~p,~p> Unexpected message(info) received at state <~p>: ~p.",
+          [GameId, TableId, StateName, Info]),
+    {next_state, StateName, StateData}.
+
+terminate(Reason, StateName, #state{game_id = GameId, table_id = TableId, relay = Relay}) ->
+    ?INFO("TAVLA_NG_TABLE_DBG <~p,~p> Shutting down at state: <~p>. Reason: ~p",
+          [GameId, TableId, StateName, Reason]),
+    relay_stop(Relay),
+    ok.
+
+code_change(_OldVsn, StateName, StateData, _Extra) ->
+    {ok, StateName, StateData}.
+
+%% --------------------------------------------------------------------
+%%% Internal functions
+%% --------------------------------------------------------------------
+
+%% handle_parent_message(Msg, StateName, StateData)
+
+handle_parent_message({register_player, RequestId, UserInfo, PlayerId, SeatNum}, StateName,
+                      #state{table_id = TableId, players = Players,
+                             parent = Parent, relay = Relay} = StateData) ->
+    #'PlayerInfo'{id = UserId, robot = IsBot} = UserInfo,
+    Color = if SeatNum == 1 -> ?WHITE;
+               SeatNum == 2 -> ?BLACK end,
+    NewPlayers = reg_player(PlayerId, SeatNum, Color, UserId, IsBot, UserInfo, _Connected = false, Players),
+    relay_register_player(Relay, UserId, PlayerId),
+    %% TODO: Send notificitations to gamesessions (we have no such notification)
+    parent_confirm_registration(Parent, TableId, RequestId),
+    {next_state, StateName, StateData#state{players = NewPlayers}};
+
+handle_parent_message({replace_player, RequestId, UserInfo, PlayerId, SeatNum}, StateName,
+                      #state{table_id = TableId, players = Players,
+                             parent = Parent, relay = Relay} = StateData) ->
+    #'PlayerInfo'{id = UserId, robot = IsBot} = UserInfo,
+    #player{id = OldPlayerId} = get_player_by_seat_num(SeatNum, Players),
+    Color = if SeatNum == 1 -> ?WHITE;
+               SeatNum == 2 -> ?BLACK end,
+    NewPlayers = del_player(OldPlayerId, Players),
+    NewPlayers2 = reg_player(PlayerId, SeatNum, Color, UserId, IsBot, UserInfo, _Connected = false, NewPlayers),
+    relay_kick_player(Relay, OldPlayerId),
+    relay_register_player(Relay, UserId, PlayerId),
+    ReplaceMsg = create_player_left(SeatNum, UserInfo, Players),
+    publish_ge(ReplaceMsg, StateData),
+    parent_confirm_replacement(Parent, TableId, RequestId),
+    {next_state, StateName, StateData#state{players = NewPlayers2}};
+
+handle_parent_message(start_round, ?STATE_WAITING_FOR_START,
+                      #state{game_id = GameId, table_id = TableId, turn_timeout = TurnTimeout, game_mode = GameMode,
+                             round_timeout = RoundTimeout, set_timeout = SetTimeout} = StateData) ->
+    CurRound = 1,
+    ?INFO("TAVLA_NG_TABLE <~p,~p> Recieved the directive to start new round (~p)", [GameId, TableId, CurRound]),
+    ?INFO("TAVLA_NG_TABLE <~p,~p> The start color will be determined by the competition rolls", [GameId, TableId]),
+    %% This fake desk state is needed because the order of the first move is not defined yet, so
+    %% we can't use the desk module to create it.
+    DeskState = #desk_state{state = undefined,
+                            board = init_board(),
+                            dice = {undefined, undefined},
+                            cur_color = undefined,
+                            finish_reason = undefined,
+                            finish_info = undefined},
+    %% Init timers
+    {TRef, Magic} = start_timer(TurnTimeout),
+    RoundTRef = if is_integer(RoundTimeout) ->
+                       erlang:send_after(RoundTimeout, self(), {round_timeout, CurRound});
+                   true -> undefined
+                end,
+    SetTRef = if is_integer(SetTimeout) -> erlang:send_after(SetTimeout, self(), set_timeout);
+                 true -> undefined
+              end,
+    NewStateData = StateData#state{desk_state = DeskState,
+                                   cur_round = CurRound,
+                                   timeout_timer = TRef,
+                                   timeout_magic = Magic,
+                                   round_timer = RoundTRef,
+                                   set_timer = SetTRef},
+    DoCompetitionRoll = if GameMode == paired -> false; true -> true end,
+    GameStartedMsg = create_tavla_game_started(DeskState, DoCompetitionRoll, NewStateData),
+    publish_ge(GameStartedMsg, NewStateData),
+    {next_state, ?STATE_FIRST_MOVE_COMPETITION, NewStateData};
+
+handle_parent_message(start_round, ?STATE_FINISHED,
+                      #state{game_id = GameId, table_id = TableId, cur_round = CurRound,
+                             start_color = LastStartColor, turn_timeout = TurnTimeout,
+                             round_timeout = RoundTimeout, set_timeout = SetTimeout,
+                             set_timer = SetTRef, game_mode = GameMode} = StateData) ->
+    NewCurRound = CurRound + 1,
+    StartColor = opponent_color(LastStartColor),
+    ?INFO("TAVLA_NG_TABLE <~p,~p> Recieved the directive to start new round (~p)", [GameId, TableId, NewCurRound]),
+    ?INFO("TAVLA_NG_TABLE <~p,~p> Start color is ~p", [GameId, TableId, StartColor]),
+    Params = [{home_hit_and_run, enabled},
+              {bearoff_waste_moves, enabled},
+              {first_move, StartColor}],
+    {ok, Desk} = ?DESK:start(Params),
+    DeskState = #desk_state{state = state_wait_roll,
+                            board = init_board(),
+                            dice = {undefined, undefined},
+                            cur_color = StartColor,
+                            finish_reason = undefined,
+                            finish_info = undefined},
+%%    DeskState = init_desk_state(Desk),
+    %% Init timers
+    {TRef, Magic} = start_timer(TurnTimeout),
+    RoundTRef = if is_integer(RoundTimeout) ->
+                       erlang:send_after(RoundTimeout, self(), {round_timeout, NewCurRound});
+                   true -> undefined
+                end,
+    NewSetTRef = if NewCurRound == 1 ->
+                        if is_integer(SetTimeout) -> erlang:send_after(SetTimeout, self(), set_timeout);
+                           true -> undefined
+                        end;
+                    true -> SetTRef
+                 end,
+    NewStateData = StateData#state{cur_round = NewCurRound,
+                                   start_color = StartColor,
+                                   desk_rule_pid = Desk,
+                                   desk_state = DeskState,
+                                   timeout_timer = TRef,
+                                   timeout_magic = Magic,
+                                   round_timer = RoundTRef,
+                                   set_timer = NewSetTRef},
+    %% Send notifications to clients
+    GameStartedMsg = create_tavla_game_started(DeskState, _DoRollMove = false, NewStateData),
+    publish_ge(GameStartedMsg, NewStateData),
+    if GameMode =/= paired ->
+           CurColor = DeskState#desk_state.cur_color,
+           publish_ge(create_tavla_next_turn(CurColor, NewStateData), NewStateData);
+       true -> do_nothing
+    end,
+    {next_state, ?STATE_PLAYING, NewStateData};
+
+handle_parent_message(show_round_result, StateName,
+                      #state{scoring_state = ScoringState,
+                             game_id = GameId, table_id = TableId} = StateData) ->
+    {FinishInfo, RoundScore, AchsPoints, TotalScore} = ?SCORING:last_round_result(ScoringState),
+    ?INFO("TAVLA_NG_TABLE <~p,~p> RoundScore: ~p Total score: ~p.", [GameId, TableId, RoundScore, TotalScore]),
+    Msg = case FinishInfo of
+              {win, Winner, _Condition} ->
+                  create_tavla_round_ended_win(Winner, RoundScore, TotalScore, AchsPoints,
+                                               StateData);
+              {surrender, Surrender, _Condition} ->
+                  create_tavla_round_ended_surrender(Surrender, RoundScore, TotalScore, AchsPoints,
+                                                     StateData);
+              timeout ->
+                  create_tavla_round_ended_draw(round_timeout, RoundScore, TotalScore, AchsPoints,
+                                                StateData);
+              set_timeout ->
+                  create_tavla_round_ended_draw(set_timeout, RoundScore, TotalScore, AchsPoints,
+                                                StateData)
+          end,
+    publish_ge(Msg, StateData),
+    {next_state, StateName, StateData#state{}};
+
+%% Results = [{PlayerId, Position, Score, Status}] Status = winner | loser | eliminated | none
+handle_parent_message({show_series_result, Results}, StateName,
+                      StateData) ->
+    Msg = create_tavla_series_ended(Results, StateData),
+    publish_ge(Msg, StateData),
+    {next_state, StateName, StateData#state{}};
+
+%% Results = [{UserId, Position, Score, Status}] Status = active | eliminated
+handle_parent_message({tour_result, TourNum, Results}, StateName,
+                      #state{tournament_table = TTable} = StateData) ->
+    NewTTable = [{TourNum, Results} | TTable],
+    Msg = create_tavla_tour_result(TourNum, Results, StateData),
+    publish_ge(Msg, StateData),
+    {next_state, StateName, StateData#state{tournament_table = NewTTable}};
+
+handle_parent_message({playing_tables_num, Num}, StateName,
+                      StateData) ->
+%%XXX The request for the feature was canncelled
+%%    Msg = create_tavla_playing_tables(Num),
+%%    publish_ge(Msg, StateData),
+    {next_state, StateName, StateData};
+
+handle_parent_message(rejoin_players, StateName,
+                      #state{game_id = GameId, relay = Relay,
+                             players = Players} = StateData) ->
+    [relay_unregister_player(Relay, P#player.id, {rejoin, GameId}) || P <- players_to_list(Players)],
+    {next_state, StateName, StateData#state{players = players_init()}};
+
+
+handle_parent_message(disconnect_players, StateName,
+                      #state{relay = Relay, players = Players} = StateData) ->
+    [relay_unregister_player(Relay, P#player.id, game_over) || P <- players_to_list(Players)],
+    {next_state, StateName, StateData#state{players = players_init()}};
+
+handle_parent_message({send_table_state, DestTableId, PlayerId, Ref}, StateName,
+                      #state{game_id = GameId, table_id = TableId, parent = Parent} = StateData) ->
+    ?INFO("TAVLA_NG_TABLE <~p,~p>  Received request to send the table state events for player "
+          "<~p> (~p) at table ~p. Processing.",
+          [GameId, TableId, PlayerId, Ref, DestTableId]),
+    GI = create_tavla_game_info(StateData),
+    PlState = create_tavla_game_player_state(PlayerId, StateName, StateData),
+    parent_table_state_to_player(Parent, TableId, DestTableId, PlayerId, Ref, GI),
+    parent_table_state_to_player(Parent, TableId, DestTableId, PlayerId, Ref, PlState),
+    {next_state, StateName, StateData};
+
+handle_parent_message(stop, _StateName, StateData) ->
+    {stop, normal, StateData};
+
+
+handle_parent_message({action, {competition_rolls, Die1, Die2} = Action},
+                      ?STATE_FIRST_MOVE_COMPETITION = StateName,
+                      #state{game_id = GameId, table_id = TableId,
+                             desk_state = DeskState} = StateData) ->
+    ?INFO("TAVLA_NG_TABLE <~p,~p>  Parent action received in state <~p>: ~p. Processing.",
+          [GameId, TableId, StateName, Action]),
+%%    publish_ge(create_tavla_rolls_die(?WHITE, Die1, StateData), StateData),
+%%    publish_ge(create_tavla_rolls_die(?BLACK, Die2, StateData), StateData),
+    NewDeskState = DeskState#desk_state{dice = {Die1, Die2}},
+    do_start_game(StateData#state{desk_state = NewDeskState});
+
+handle_parent_message({action, {rolls, Color, Die1, Die2} = Action},
+                      ?STATE_PLAYING = StateName,
+                      #state{game_id = GameId, table_id = TableId,
+                             relay = Relay, turn_timeout = TurnTimeout} = StateData) ->
+    ?INFO("TAVLA_NG_TABLE <~p,~p>  Parent action received in state <~p>: ~p. Processing.",
+          [GameId, TableId, StateName, Action]),
+    relay_publish_ge(Relay, create_tavla_next_turn(Color, StateData)),
+    {TRef, Magic} = start_timer(TurnTimeout),
+    NewStateData1 = StateData#state{timeout_timer = TRef, timeout_magic = Magic},
+    case do_parent_game_action(Color, {roll, Die1, Die2}, NewStateData1) of
+        {ok, NewStateName, NewStateData2} ->
+            {next_state, NewStateName, NewStateData2};
+        {error, Reason} ->
+            {stop, {parent_action_failed, Reason}, NewStateData1}
+    end;
+
+handle_parent_message({action, Action}, StateName,
+                      #state{game_id = GameId, table_id = TableId} = StateData) ->
+    ?INFO("TAVLA_NG_TABLE <~p,~p>  Parent action received in state <~p>: ~p. Ignoring.",
+          [GameId, TableId, StateName, Action]),
+    {next_state, StateName, StateData};
+
+handle_parent_message({game_event, GameEvent}, StateName,
+                      #state{game_id = GameId, table_id = TableId, relay = Relay} = StateData) ->
+    ?INFO("TAVLA_NG_TABLE <~p,~p>  A game event received from the parent in state <~p>: ~p. Publish it.",
+          [GameId, TableId, StateName, GameEvent]),
+    relay_publish_ge(Relay, GameEvent),
+    {next_state, StateName, StateData};
+
+handle_parent_message({table_state_event, _PlayerId, SubscrId, StateEvent}, StateName,
+                      #state{game_id = GameId, table_id = TableId, relay = Relay} = StateData) ->
+    ?INFO("TAVLA_NG_TABLE <~p,~p>  A table state event received from the parent in state <~p>: ~p.",
+          [GameId, TableId, StateName, StateEvent]),
+    send_to_subscriber_ge(Relay, SubscrId, StateEvent),
+    {next_state, StateName, StateData};
+
+handle_parent_message(Message, StateName,
+                      #state{game_id = GameId, table_id = TableId} = StateData) ->
+    ?ERROR("TAVLA_NG_TABLE <~p,~p> Unexpected parent message received in state <~p>: ~p. Stopping.",
+           [GameId, TableId, StateName, Message]),
+    {stop, unexpected_parent_message, StateData}.
+
+
+%%===================================================================
+
+%% handle_relay_message(Msg, StateName, StateData)
+
+handle_relay_message({player_connected, PlayerId} = Msg, StateName,
+                     #state{parent = Parent, game_id = GameId,
+                            table_id = TableId, players = Players} = StateData) ->
+    ?INFO("TAVLA_NG_TABLE <~p,~p> Received nofitication from the relay: ~p", [GameId, TableId, Msg]),
+    case get_player(PlayerId, Players) of
+        {ok, Player} ->
+            NewPlayers = store_player_rec(Player#player{connected = true}, Players),
+            parent_send_player_connected(Parent, TableId, PlayerId),
+            {next_state, StateName, StateData#state{players = NewPlayers}};
+        error ->
+            {next_state, StateName, StateData}
+    end;
+
+
+handle_relay_message({player_disconnected, PlayerId}, StateName,
+                     #state{parent = Parent, table_id = TableId, players = Players} = StateData) ->
+    case get_player(PlayerId, Players) of
+        {ok, Player} ->
+            NewPlayers = store_player_rec(Player#player{connected = false}, Players),
+            parent_send_player_disconnected(Parent, TableId, PlayerId),
+            {next_state, StateName, StateData#state{players = NewPlayers}};
+        error ->
+            {next_state, StateName, StateData}
+    end;
+
+handle_relay_message({subscriber_added, PlayerId, SubscrId} = Msg, StateName,
+                     #state{relay = Relay, game_id = GameId, game_mode = GameMode,
+                            table_id = TableId, tournament_table = TTable,
+                            players = Players, parent = Parent} = StateData) ->
+    ?INFO("TAVLA_NG_TABLE <~p,~p> Received nofitication from the relay: ~p", [GameId, TableId, Msg]),
+    IsValidPlayerId = case PlayerId of
+                          observer -> true;
+                          administrator -> true;
+                          _ ->
+                              case get_player(PlayerId, Players) of
+                                  {ok, _} -> true;
+                                  error -> false
+                              end
+                      end,
+    if IsValidPlayerId ->
+           GI = create_tavla_game_info(StateData),
+           PlState = create_tavla_game_player_state(PlayerId, StateName, StateData),
+           send_to_subscriber_ge(Relay, SubscrId, GI),
+           send_to_subscriber_ge(Relay, SubscrId, PlState),
+           relay_allow_broadcast_for_player(Relay, PlayerId),
+           if TTable =/= undefined ->
+                  [send_to_subscriber_ge(Relay, SubscrId, create_tavla_tour_result(TurnNum, Results, StateData))
+                     || {TurnNum, Results} <- lists:sort(TTable)];
+              true -> do_nothing
+           end,
+           if GameMode == paired ->
+                  parent_send_get_tables_states(Parent, TableId, PlayerId, SubscrId);
+              true -> do_nothing
+           end;
+       true -> do_nothing
+    end,
+    {next_state, StateName, StateData};
+
+handle_relay_message(Message, StateName, #state{game_id = GameId, table_id = TableId} = StateData) ->
+    ?ERROR("TAVLA_NG_TABLE <~p,~p> Unknown relay message received in state <~p>: ~p. State: ~p. Stopping.",
+           [GameId, TableId, StateName, Message]),
+    {next_state, StateName, StateData}.
+
+%%===================================================================
+
+%% handle_player_action(Player, Msg, StateName, StateData)
+
+handle_player_action(#player{id = PlayerId, seat_num = SeatNum, user_id = UserId},
+                     {submit, #game_action{action = Action, args = Args} = GA}, From,
+                     StateName,
+                     #state{game_id = GameId, table_id = TableId} = StateData) ->
+    try api_utils:to_known_record(Action, Args) of
+        ExtAction ->
+            ?INFO("TAVLA_NG_TABLE <~p,~p> Player <~p> (~p) submit the game action: ~p.",
+                  [GameId, TableId, PlayerId, UserId, ExtAction]),
+            do_action(SeatNum, ExtAction, From, StateName, StateData)
+    catch
+        _Class:_Exception ->
+            ?ERROR("TAVLA_NG_TABLE <~p,~p> Can't convert action ~p. Exception: ~p:~p.",
+                   [GameId, TableId, GA, _Class, _Exception]),
+            {reply, {error, invalid_action}, StateName, StateData}
+    end;
+
+
+handle_player_action(#player{id = PlayerId, user_id = UserId},
+                     {signal, {pause_game, _}=Signal}, _From,
+                     StateName,
+                     #state{table_id = TableId, game_id = GameId, timeout_timer = TRef,
+                            pause_mode = PauseMode, relay = Relay} = StateData) ->
+    ?INFO("TAVLA_NG_TABLE <~p,~p> Received signal from player <~p> : ~p. PauseMode: ~p",
+          [GameId, TableId, PlayerId, Signal, PauseMode]),
+    case PauseMode of
+        disabled ->
+            {reply, {error, pause_disabled}, StateName, StateData};
+        normal ->
+            if StateName == ?STATE_PLAYING ->
+                   Timeout = case erlang:cancel_timer(TRef) of
+                                 false -> 0;
+                                 T -> T
+                             end,
+                   relay_publish(Relay, create_game_paused_pause(UserId, GameId)),
+                   {reply, 0, ?STATE_PAUSE, StateData#state{paused_statename = StateName,
+                                                            paused_timeout_value = Timeout,
+                                                            timeout_magic = undefined}};
+               true ->
+                   {reply, {error, pause_not_possible}, StateName, StateData}
+            end
+    end;
+
+
+handle_player_action(#player{id = PlayerId, user_id = UserId},
+                     {signal, {resume_game, _}=Signal}, _From,
+                     StateName,
+                     #state{table_id = TableId, game_id = GameId, pause_mode = PauseMode,
+                            relay = Relay, paused_statename = ResumedStateName,
+                            paused_timeout_value = Timeout} = StateData) ->
+    ?INFO("TAVLA_NG_TABLE <~p,~p> Received signal from player <~p> : ~p. PauseMode: ~p",
+          [GameId, TableId, PlayerId, Signal, PauseMode]),
+    case PauseMode of
+        disabled ->
+            {reply, {error, pause_disabled}, StateName, StateData};
+        normal ->
+            if StateName == ?STATE_PAUSE ->
+                   relay_publish(Relay, create_game_paused_resume(UserId, GameId)),
+                   {TRef, Magic} = start_timer(Timeout),
+                   {reply, 0, ResumedStateName, StateData#state{timeout_timer = TRef,
+                                                                timeout_magic = Magic}};
+               true ->
+                   {reply, {error, game_is_not_paused}, StateName, StateData}
+            end
+    end;
+
+
+handle_player_action(#player{id = PlayerId},
+                     {signal, Signal}, _From, StateName,
+                     #state{table_id = TableId, game_id = GameId} = StateData) ->
+    ?INFO("TAVLA_NG_TABLE <~p,~p> Received signal from player <~p> : ~p. Ignoring.",
+          [GameId, TableId, PlayerId, Signal]),
+    {reply, ok, StateName, StateData};
+
+
+handle_player_action(_Player, _Message, _From, StateName, StateData) ->
+    {next_state, StateName, StateData}.
+
+
+
+%%===================================================================
+
+do_action(SeatNum, #tavla_roll{}, From, ?STATE_FIRST_MOVE_COMPETITION = StateName,
+          #state{game_id = GameId, table_id = TableId,
+                 desk_state = DeskState, players = Players} = StateData) ->
+    ?INFO("TAVLA_NG_TABLE_DBG <~p,~p> Action tavla_roll{} Deskstate: ~p.",
+          [GameId, TableId, DeskState]),
+    #desk_state{dice = Dice} = DeskState,
+    #player{color = Color} = get_player_by_seat_num(SeatNum, Players),
+    Pos = if Color == ?WHITE -> 1;
+             Color == ?BLACK -> 2 end,
+    if element(Pos, Dice) == undefined ->
+           Die = random_die(),
+           publish_ge(create_tavla_rolls_die(Color, Die, StateData), StateData),
+           NewDice = {WhiteDie, BlackDie} = setelement(Pos, Dice, Die),
+           NewDeskState = DeskState#desk_state{dice = NewDice},
+           if WhiteDie =/= undefined andalso BlackDie =/= undefined ->
+                  gen_fsm:reply(From, ok),
+                  do_start_game(StateData#state{desk_state = NewDeskState});
+              true ->
+                  {reply, ok, StateName, StateData#state{desk_state = NewDeskState}}
+           end;
+       true ->
+           {reply, {error, already_rolled}, StateName, StateData}
+    end;
+
+do_action(SeatNum, #tavla_roll{}, From, ?STATE_PLAYING = StateName,
+          #state{game_mode = standard,
+                 desk_state = DeskState, players = Players} = StateData) ->
+    #desk_state{state = DeskStateName,
+                cur_color = CurColor} = DeskState,
+    #player{color = Color} = get_player_by_seat_num(SeatNum, Players),
+    if CurColor == Color ->
+           if DeskStateName == state_wait_roll ->
+                  {Die1, Die2} = random_dice(),
+                  do_game_action(Color, {roll, Die1, Die2}, From, StateName, StateData);
+              true ->
+                  {reply, {error, already_rolled}, StateName, StateData}
+           end;
+       true ->
+           {reply, {error, not_your_turn}, StateName, StateData}
+    end;
+
+do_action(_SeatNum, #tavla_roll{}, _From, StateName, StateData) ->
+    {reply, {error, action_not_valid_for_a_current_state}, StateName, StateData};
+
+
+do_action(SeatNum, #tavla_move{moves = ExtMoves}, From, ?STATE_PLAYING = StateName,
+          #state{desk_state = DeskState, players = Players} = StateData) ->
+    #desk_state{state = DeskStateName,
+                cur_color = CurColor} = DeskState,
+    #player{color = Color} = get_player_by_seat_num(SeatNum, Players),
+    if Color == CurColor ->
+           if DeskStateName == state_wait_move ->
+                  try ext_to_moves(ExtMoves) of
+                      Moves ->
+                          do_game_action(Color, {move, Moves}, From, StateName, StateData)
+                  catch
+                      _:_ ->
+                          {reply, {error, invalid_action}, StateName, StateData}
+                  end;
+              true ->
+                  {reply, {error, roll_first}, StateName, StateData}
+           end;
+       true ->
+           {reply, {error, not_your_turn}, StateName, StateData}
+    end;
+
+do_action(_SeatNum, #tavla_move{}, _From, StateName, StateData) ->
+    {reply, {error, action_not_valid_for_a_current_state}, StateName, StateData};
+
+do_action(_SeatNum, #tavla_ready{}, _From, StateName, StateData) ->
+    {reply, ok, StateName, StateData};
+
+do_action(SeatNum, #tavla_surrender{}, From, ?STATE_PLAYING,
+          #state{desk_state = DeskState, players = Players} = StateData) ->
+    #player{color = Color} = get_player_by_seat_num(SeatNum, Players),
+    #desk_state{board = Board} = DeskState,
+    gen_fsm:reply(From, ok),
+    Condition = surrender_condition(Color, Board),
+    NewDeskState = DeskState#desk_state{finish_reason = surrender,
+                                        finish_info = {Color, Condition}},
+    finalize_round(StateData#state{desk_state = NewDeskState});
+
+do_action(_SeatNum, _UnsupportedAction, _From, StateName, StateData) ->
+    {reply, {error, invalid_action}, StateName, StateData}.
+
+
+%%===================================================================
+do_first_move_competition_timeout_rolls(#state{desk_state = DeskState} = StateData) ->
+    {WhiteDie, BlackDie} = DeskState#desk_state.dice,
+    FinWhiteDie = if WhiteDie == undefined ->
+                         NewWhiteDie = random_die(),
+                         publish_ge(create_tavla_rolls_die(?WHITE, NewWhiteDie, StateData), StateData),
+                         NewWhiteDie;
+                     true -> WhiteDie
+                  end,
+    FinBlackDie = if BlackDie == undefined ->
+                         NewBlackDie = random_die(),
+                         publish_ge(create_tavla_rolls_die(?BLACK, NewBlackDie, StateData), StateData),
+                         NewBlackDie;
+                     true -> BlackDie
+                  end,
+    NewDeskState = DeskState#desk_state{dice = {FinWhiteDie, FinBlackDie}},
+    do_start_game(StateData#state{desk_state = NewDeskState}).
+
+do_start_game(#state{desk_state = DeskState, turn_timeout = TurnTimeout} = StateData) ->
+    {WhiteDie, BlackDie} = Dice = DeskState#desk_state.dice,
+    StartColor = if WhiteDie >= BlackDie -> ?WHITE;
+                    true -> ?BLACK
+                 end,
+    Params = [{home_hit_and_run, enabled},
+              {bearoff_waste_moves, enabled},
+              {first_move, StartColor},
+              {dice, Dice}],
+    {ok, Desk} = ?DESK:start(Params),
+    NewDeskState = #desk_state{state = state_wait_move,
+                               board = init_board(),
+                               dice = {undefined, undefined},
+                               pips_list = pips_list(WhiteDie, BlackDie),
+                               cur_color = StartColor,
+                               finish_reason = undefined,
+                               finish_info = undefined},
+
+%%    NewDeskState = init_desk_state(Desk),
+    NewDeskState2 = NewDeskState#desk_state{dice = Dice},
+    Msg = create_won_first_move(StartColor, Dice, _Reroll = false, StateData),
+    publish_ge(Msg, StateData),
+    {TRef, Magic} = start_timer(TurnTimeout),
+    {next_state, ?STATE_PLAYING, StateData#state{start_color = StartColor,
+                                                 desk_rule_pid = Desk,
+                                                 desk_state = NewDeskState2,
+                                                 timeout_timer = TRef,
+                                                 timeout_magic = Magic
+                                                }}.
+
+do_timeout_moves(#state{desk_rule_pid = Desk, desk_state = DeskState,
+                        game_id = GameId, table_id = TableId,
+                        players = Players, game_mode = GameMode} = StateData) ->
+    #desk_state{state = DeskStateName,
+                pips_list = PipsList,
+                cur_color = CurColor,
+                board = Board} = DeskState,
+    [#player{user_id = UserId}] = find_players_by_color(CurColor, Players),
+    case DeskStateName of
+        state_wait_roll when GameMode == standard ->
+            ?INFO("TAVLA_NG_TABLE <~p,~p> Do automatic roll for player <~p> (~p)",
+                  [GameId, TableId, CurColor, UserId]),
+            {Die1, Die2} = random_dice(),
+            {ok, Events1} = desk_player_action(Desk, CurColor, {roll, Die1, Die2}),
+            case [E || {next_player, _} = E <- Events1] of
+                [] -> %% Player can move => rolls_moves_timeout
+                    ?INFO("TAVLA_NG_TABLE <~p,~p> Do automatic move with dice ~p for player <~p> (~p)",
+                          [GameId, TableId, {Die1, Die2}, CurColor, UserId]),
+                    NewPipsList = pips_list(Die1, Die2),
+                    Moves = find_moves(CurColor, NewPipsList, Board),
+                    {ok, Events2} = desk_player_action(Desk, CurColor, {move, Moves}),
+                    Events = [case E of
+                                  {moves, CurColor, M} ->
+                                      {rolls_moves_timeout, CurColor, Die1, Die2, M};
+                                  _ -> E
+                              end || E <- Events2],
+                    NewDeskState = DeskState#desk_state{pips_list = NewPipsList},
+                    process_game_events(Events, StateData#state{desk_state = NewDeskState});
+                _ -> %% Only rolls_timeout
+                    ?INFO("TAVLA_NG_TABLE <~p,~p> No moves can be done for the dice ~p for player <~p> (~p)",
+                          [GameId, TableId, {Die1, Die2}, CurColor, UserId]),
+                    Events = [case E of
+                                  {rolls, CurColor, D1, D2} ->
+                                      {rolls_timeout, CurColor, D1, D2};
+                                  _ -> E
+                              end || E <- Events1],
+                    process_game_events(Events, StateData)
+                end;
+        state_wait_move ->
+            ?INFO("TAVLA_NG_TABLE <~p,~p> Do rest automatic move with pips ~p for player <~p> (~p)",
+                  [GameId, TableId, PipsList, CurColor, UserId]),
+            Moves = find_moves(CurColor, PipsList, Board),
+            {ok, Events1} = desk_player_action(Desk, CurColor, {move, Moves}),
+            Events = [case E of
+                          {moves, CurColor, M} ->
+                              {moves_timeout, CurColor, M};
+                          _ -> E
+                      end || E <- Events1],
+            process_game_events(Events, StateData)
+    end.
+
+%%===================================================================
+
+do_parent_game_action(Color, GameAction,
+               #state{desk_rule_pid = Desk} = StateData) ->
+    ?INFO("TAVLA_NG_TABLE do_parent_game_action Color: ~p  GameAction: ~p", [Color, GameAction]),
+    case desk_player_action(Desk, Color, GameAction) of
+        {ok, Events} ->
+            {next_state, NewStateName, NewStateData} = process_game_events(Events, StateData),
+            {ok, NewStateName, NewStateData};
+        {error, Reason} ->
+            {error, Reason}
+    end.
+
+
+do_game_action(Color, GameAction, From, StateName,
+               #state{game_id = GameId, table_id = TableId, desk_rule_pid = Desk} = StateData) ->
+    ?INFO("TAVLA_NG_TABLE <~p,~p> Appling the action: Color <~p>  GameAction: ~p",
+          [GameId, TableId, Color, GameAction]),
+    case desk_player_action(Desk, Color, GameAction) of
+        {ok, Events} ->
+            ?INFO("TAVLA_NG_TABLE <~p,~p> The game action applied successfully.",
+                  [GameId, TableId]),
+            gen_fsm:reply(From, ok),
+            process_game_events(Events, StateData);
+        {error, Reason} ->
+            ?INFO("TAVLA_NG_TABLE <~p,~p> The action was rejected by the reason: ~p",
+                  [GameId, TableId, Reason]),
+            ExtError = desk_error_to_ext(Reason),
+            {reply, ExtError, StateName, StateData}
+    end.
+
+%% process_game_events(Events, StateData) -> {next_state, StateName, NewStateData}
+process_game_events(Events, #state{desk_state = DeskState, timeout_timer = OldTRef,
+                                   round_timeout = RoundTimeout, round_timer = RoundTRef,
+                                   turn_timeout = TurnTimeout, game_mode = GameMode} = StateData) ->
+    NewDeskState = handle_desk_events(Events, DeskState, StateData), %% Track the desk and send game events to clients
+    #desk_state{state = DeskStateName} = NewDeskState,
+    case DeskStateName of
+        state_finished ->
+            if is_integer(RoundTimeout) -> erlang:cancel_timer(RoundTRef); true -> do_nothing end,
+            erlang:cancel_timer(OldTRef),
+            on_game_finish(StateData#state{desk_state = NewDeskState});
+        _ ->
+            case [E || {next_player, _} = E <- Events] of %% Find a next player event
+                [] ->
+                    {next_state, ?STATE_PLAYING, StateData#state{desk_state = NewDeskState}};
+                [_|_] when GameMode == paired ->
+                    erlang:cancel_timer(OldTRef),
+                    {next_state, ?STATE_PLAYING, StateData#state{desk_state = NewDeskState,
+                                                                 timeout_timer = undefined,
+                                                                 timeout_magic = undefined}};
+                [_|_] ->
+                    erlang:cancel_timer(OldTRef),
+                    {TRef, Magic} = start_timer(TurnTimeout),
+                    {next_state, ?STATE_PLAYING, StateData#state{desk_state = NewDeskState,
+                                                                 timeout_timer = TRef,
+                                                                 timeout_magic = Magic}}
+            end
+    end.
+
+
+on_game_finish(StateData) ->
+    finalize_round(StateData).
+
+%%===================================================================
+
+finalize_round(#state{desk_state = #desk_state{finish_reason = FinishReason,
+                                               finish_info = FinishInfo},
+                      scoring_state = ScoringState, timeout_timer = TimeoutTRef,
+                      round_timer = RoundTRef, parent = Parent, players = Players,
+                      game_id = GameId, table_id = TableId, cur_round = CurRound} = StateData) ->
+    ?INFO("TAVLA_NG_TABLE <~p,~p> Finalizing the round. Finish reason: ~p. Finish info: ~p.",
+          [GameId, TableId, FinishReason, FinishInfo]),
+    if TimeoutTRef =/= undefined -> erlang:cancel_timer(TimeoutTRef);
+       true -> do_nothing
+    end,
+    if RoundTRef =/= undefined -> erlang:cancel_timer(RoundTRef);
+       true -> do_nothing
+    end,
+    FR = case FinishReason of
+             timeout -> timeout;
+             set_timeout -> set_timeout;
+             win ->
+                 {WinnerColor, Condition} = FinishInfo,
+                 [#player{seat_num = SeatNum}] = find_players_by_color(WinnerColor, Players),
+                 {win, SeatNum, Condition};
+             surrender ->
+                 {SurrenderColor, Condition} = FinishInfo,
+                 [#player{seat_num = SeatNum}] = find_players_by_color(SurrenderColor, Players),
+                 {surrender, SeatNum, Condition}
+         end,
+    {NewScoringState, GameOver} = ?SCORING:round_finished(ScoringState, FR),
+    NewStateData = StateData#state{scoring_state = NewScoringState,
+                                   timeout_timer = undefined,
+                                   timeout_magic = undefined,
+                                   round_timer = undefined},
+
+    {_, RoundScore, _, TotalScore} = ?SCORING:last_round_result(NewScoringState),
+    RoundScorePl = [{get_player_id_by_seat_num(SeatNum, Players), Points} || {SeatNum, Points} <- RoundScore],
+    TotalScorePl = [{get_player_id_by_seat_num(SeatNum, Players), Points} || {SeatNum, Points} <- TotalScore],
+
+    if GameOver ->
+           ?INFO("TAVLA_NG_TABLE <~p,~p> The set is over.", [GameId, TableId]),
+           parent_send_set_res(Parent, TableId, NewScoringState, RoundScorePl, TotalScorePl),
+           {next_state, ?STATE_SET_FINISHED, NewStateData};
+       true ->
+           ?INFO("TAVLA_NG_TABLE <~p,~p> Round <~p> is over.", [GameId, TableId, CurRound]),
+           parent_send_round_res(Parent, TableId, NewScoringState, RoundScorePl, TotalScorePl),
+           {next_state, ?STATE_FINISHED, NewStateData}
+    end.
+
+
+
+%% handle_desk_events(Events, DeskState, StateData) -> NewDeskState
+%% Tracks the desk state and sends events to clients
+handle_desk_events([], DeskState, _StateData) ->
+    DeskState;
+
+handle_desk_events([Event | Events], DeskState,
+                    #state{game_id = GameId, table_id = TableId} = StateData) ->
+    ?INFO("TAVLA_NG_TABLE_DBG <~p,~p> handle_desk_events/3 Event: ~p", [GameId, TableId, Event]),
+    #desk_state{board = Board,
+                pips_list = OldPipsList} = DeskState,
+    NewDeskState =
+        case Event of
+            {rolls, Color, Die1, Die2} ->
+                ?INFO("TAVLA_NG_TABLE_DBG <~p,~p> Board: Color <~p> rolls(himself): ~p", [GameId, TableId, Color, {Die1, Die2}]),
+                publish_ge(create_tavla_rolls_dice(Color, {Die1, Die2}, StateData), StateData),
+                PipsList = pips_list(Die1, Die2),
+                DeskState#desk_state{state = state_wait_move,
+                                     dice = {Die1, Die2}, pips_list = PipsList};
+            {moves, Color, Moves} ->
+                [publish_ge(create_tavla_moves(Color, From, To, Type, Pips, StateData), StateData) ||
+                   {Type, From, To, Pips} <- Moves],
+                UsedPipsList = [Pips || {_Type, _From, _To, Pips} <- Moves],
+                NewBoard = apply_moves(Color, Moves, Board),
+                ?INFO("TAVLA_NG_TABLE_DBG <~p,~p> Board: Color <~p> moves(himself): ~p", [GameId, TableId, Color, Moves]),
+                show_boards(GameId, TableId, Board, NewBoard),
+                DeskState#desk_state{board = NewBoard, pips_list = OldPipsList -- UsedPipsList};
+            {rolls_timeout, Color, Die1, Die2} -> %% Injected event
+                ?INFO("TAVLA_NG_TABLE_DBG <~p,~p> Board: Color <~p> rolls(auto): ~p", [GameId, TableId, Color, {Die1, Die2}]),
+                Msg = create_tavla_turn_timeout(Color, {Die1, Die2}, _Moves = [], StateData),
+                publish_ge(Msg, StateData),
+                DeskState#desk_state{dice = {Die1, Die2}};
+            {rolls_moves_timeout, Color, Die1, Die2, Moves} -> %% Injected event
+                ?INFO("TAVLA_NG_TABLE_DBG <~p,~p> Board: Color <~p> rolls(auto): ~p", [GameId, TableId, Color, {Die1, Die2}]),
+                Msg = create_tavla_turn_timeout(Color, {Die1, Die2}, Moves, StateData),
+                publish_ge(Msg, StateData),
+                NewBoard = apply_moves(Color, Moves, Board),
+                ?INFO("TAVLA_NG_TABLE_DBG <~p,~p> Board: Color <~p> moves(auto): ~p", [GameId, TableId, Color, Moves]),
+                show_boards(GameId, TableId, Board, NewBoard),
+                DeskState#desk_state{dice = {Die1, Die2}, board = NewBoard};
+            {moves_timeout, Color, Moves} ->    %% Injected event
+                Msg = create_tavla_turn_timeout(Color, _Dice = undefined, Moves, StateData),
+                publish_ge(Msg, StateData),
+                NewBoard = apply_moves(Color, Moves, Board),
+                ?INFO("TAVLA_NG_TABLE_DBG <~p,~p> Board: Color <~p> moves(auto): ~p", [GameId, TableId, Color, Moves]),
+                show_boards(GameId, TableId, Board, NewBoard),
+                DeskState#desk_state{board = NewBoard};
+            {next_player, Color} ->
+                ?INFO("TAVLA_NG_TABLE_DBG <~p,~p> Board: Waiting for Color <~p> roll...", [GameId, TableId, Color]),
+                Msg = create_tavla_next_turn(Color, StateData),
+                publish_ge(Msg, StateData),
+                DeskState#desk_state{state = state_wait_roll, cur_color = Color,
+                                     dice = {undefined, undefined},
+                                     pips_list = []};
+            {win, Color, Condition} ->
+                DeskState#desk_state{state = state_finished,
+                                     finish_reason = win,
+                                     finish_info = {Color, Condition}}
+        end,
+    handle_desk_events(Events, NewDeskState, StateData).
+
+
+show_boards(GameId, TableId, Board1, Board2) ->
+    B1 = ?LIB:board_to_text4(Board1),
+    B2 = ?LIB:board_to_text4(Board2),
+    M = ["    ", "    ", "    ", " => " ,"    ", "    ", "    "],
+    T = lists:zip3(B1, M, B2),
+    [?INFO("TAVLA_NG_TABLE_DBG <~p,~p> Board: ~s~s~s", [GameId, TableId, S1, S2, S3])
+     || {S1, S2, S3} <- T].
+
+%%===================================================================
+init_scoring(GameType, PlayersInfo, Rounds) ->
+    SeatsInfo = [{SeatNum, Points} || {_PlayerId, _UserInfo, SeatNum, Points} <- PlayersInfo],
+    ?SCORING:init(GameType, SeatsInfo, Rounds).
+
+random_die() ->
+    crypto:rand_uniform(1, 7).
+
+%% random_dice() -> {Die1, Die2}
+random_dice() ->
+    {random_die(), random_die()}.
+
+%% start_timer(Timeout) -> {TRef, Magic}
+start_timer(Timeout) ->
+    Magic = make_ref(),
+    TRef = erlang:send_after(Timeout, self(), {timeout, Magic}),
+    {TRef, Magic}.
+
+%% players_init() -> players()
+players_init() ->
+    midict:new().
+
+%% reg_player(PlayerId, SeatNum, Color, UserId, IsBot, Players) -> NewPlayers
+reg_player(PlayerId, SeatNum, Color, UserId, IsBot, UserInfo, Connected, Players) ->
+    store_player_rec(#player{id =PlayerId, seat_num = SeatNum, color = Color, user_id = UserId,
+                             is_bot = IsBot, info = UserInfo, connected = Connected}, Players).
+
+%% reg_player(#player{}, Players) -> NewPlayers
+store_player_rec(#player{id =Id, seat_num = SeatNum, color = Color, user_id = UserId,
+                         is_bot = IsBot, connected = Connected} = Player, Players) ->
+    Indices = [{seat_num, SeatNum}, {color, Color}, {user_id, UserId},
+               {is_bot, IsBot}, {connected, Connected}],
+    midict:store(Id, Player, Indices, Players).
+
+%% get_player_id_by_seat_num(SeatNum, Players) -> PlayerId
+get_player_id_by_seat_num(SeatNum, Players) ->
+    [#player{id = PlayerId}] = midict:geti(SeatNum, seat_num, Players),
+    PlayerId.
+
+%% fetch_player(PlayerId, Players) -> Player
+fetch_player(PlayerId, Players) ->
+    midict:fetch(PlayerId, Players).
+
+%% get_player(PlayerId, Players) -> {ok, Player} | error
+get_player(PlayerId, Players) ->
+    midict:find(PlayerId, Players).
+
+%% get_player_by_seat_num(SeatNum, Players) -> Player
+get_player_by_seat_num(SeatNum, Players) ->
+    [Player] = midict:geti(SeatNum, seat_num, Players),
+    Player.
+
+%% find_players_by_seat_num(SeatNum, Players) -> [Player]
+find_players_by_seat_num(SeatNum, Players) ->
+    midict:geti(SeatNum, seat_num, Players).
+
+find_players_by_color(Color, Players) ->
+    midict:geti(Color, color, Players).
+
+
+%% del_player(PlayerId, Players) -> NewPlayers
+del_player(PlayerId, Players) ->
+    midict:erase(PlayerId, Players).
+
+%% players_to_list(Players) -> List
+players_to_list(Players) ->
+    midict:all_values(Players).
+
+%% @spec init_players(PlayersInfo) -> Players
+%% @end
+%% PlayersInfo = [{PlayerId, UserInfo, SeatNum, StartPoints}]
+
+init_players(PlayersInfo) ->
+    init_players(PlayersInfo, players_init()).
+
+init_players([], Players) ->
+    Players;
+
+init_players([{PlayerId, UserInfo, SeatNum, _StartPoints} | PlayersInfo], Players) ->
+    #'PlayerInfo'{id = UserId, robot = IsBot} = UserInfo,
+    Color = if SeatNum == 1 -> ?WHITE;
+               SeatNum == 2 -> ?BLACK end,
+    NewPlayers = reg_player(PlayerId, SeatNum, Color, UserId, IsBot, UserInfo, _Connected = false, Players),
+    init_players(PlayersInfo, NewPlayers).
+
+%%===================================================================
+
+send_to_subscriber_ge(Relay, SubscrId, Msg) ->
+    Event = #game_event{event = api_utils:name(Msg), args = api_utils:members(Msg)},
+    ?RELAY:table_message(Relay, {to_subscriber, SubscrId, Event}).
+
+relay_publish_ge(Relay, Msg) ->
+    Event = #game_event{event = api_utils:name(Msg), args = api_utils:members(Msg)},
+    relay_publish(Relay, Event).
+
+relay_publish(Relay, Msg) ->
+    ?RELAY:table_message(Relay, {publish, Msg}).
+
+relay_allow_broadcast_for_player(Relay, PlayerId) ->
+    ?RELAY:table_message(Relay, {allow_broadcast_for_player, PlayerId}).
+
+relay_register_player(Relay, UserId, PlayerId) ->
+    ?RELAY:table_request(Relay, {register_player, UserId, PlayerId}).
+
+relay_unregister_player(Relay, PlayerId, Reason) ->
+    ?RELAY:table_request(Relay, {unregister_player, PlayerId, Reason}).
+
+relay_kick_player(Relay, PlayerId) ->
+    ?RELAY:table_request(Relay, {kick_player, PlayerId}).
+
+relay_stop(Relay) ->
+    ?RELAY:table_message(Relay, stop).
+
+parent_confirm_registration({ParentMod, ParentPid}, TableId, RequestId) ->
+    ParentMod:table_message(ParentPid, TableId, {response, RequestId, ok}).
+
+parent_confirm_replacement({ParentMod, ParentPid}, TableId, RequestId) ->
+    ParentMod:table_message(ParentPid, TableId, {response, RequestId, ok}).
+
+parent_notify_table_created({ParentMod, ParentPid}, TableId, RelayPid) ->
+    ParentMod:table_message(ParentPid, TableId, {table_created, {?RELAY, RelayPid}}).
+
+parent_send_round_res({ParentMod, ParentPid}, TableId, ScoringState, RoundScores, TotalScores) ->
+    ParentMod:table_message(ParentPid, TableId, {round_finished, ScoringState, RoundScores, TotalScores}).
+
+parent_send_set_res({ParentMod, ParentPid}, TableId, ScoringState, RoundScores, TotalScores) ->
+    ParentMod:table_message(ParentPid, TableId, {game_finished, ScoringState, RoundScores, TotalScores}).
+
+parent_send_player_connected({ParentMod, ParentPid}, TableId, PlayerId) ->
+    ParentMod:table_message(ParentPid, TableId, {player_connected, PlayerId}).
+
+parent_send_player_disconnected({ParentMod, ParentPid}, TableId, PlayerId) ->
+    ParentMod:table_message(ParentPid, TableId, {player_disconnected, PlayerId}).
+
+parent_send_get_tables_states({ParentMod, ParentPid}, TableId, PlayerId, SubscrId) ->
+    ParentMod:table_message(ParentPid, TableId, {get_tables_states, PlayerId, SubscrId}).
+
+parent_table_state_to_player({ParentMod, ParentPid}, TableId, DestTableId, PlayerId, Ref, StateEvent) ->
+    ParentMod:table_message(ParentPid, TableId, {table_state_event, DestTableId, PlayerId, Ref, StateEvent}).
+
+parent_publish_ge({ParentMod, ParentPid}, TableId, GameEvent) ->
+    ParentMod:table_message(ParentPid, TableId, {game_event, GameEvent}).
+
+desk_player_action(Desk, Color, Action) ->
+    ?DESK:player_action(Desk, Color, Action).
+
+publish_ge(GameEvent, #state{relay = Relay, parent = Parent, table_id = TableId,
+                             game_mode = GameMode}) ->
+    case GameMode of
+        paired ->
+            case GameEvent of
+                #tavla_next_turn{} -> do_nothing;
+                _ ->  relay_publish_ge(Relay, GameEvent)
+            end,
+            parent_publish_ge(Parent, TableId, GameEvent);
+        _ ->
+            relay_publish_ge(Relay, GameEvent)
+    end.
+
+%%===================================================================
+
+create_tavla_game_info(#state{table_name = TName, mult_factor = MulFactor,
+                              slang_flag = SlangFlag, observer_flag = ObserverFlag,
+                              speed = Speed, turn_timeout = TurnTimeout,
+                              ready_timeout = ReadyTimeout, game_mode = GameMode,
+                              rounds = Rounds1, players = Players, tour = Tour,
+                              tours = Tours, pause_mode = PauseMode, tables_num = TablesNum,
+                              tournament_type = TournamentType, table_id = TableId,
+                              social_actions_enabled = SocialActionsEnabled,
+                              next_series_confirmation = ConfirmMode}) ->
+    PInfos = [case find_players_by_seat_num(SeatNum, Players) of
+                  [#player{info = UserInfo}] -> UserInfo;
+                  [] -> null
+              end || SeatNum <- [1, 2]],
+    Sets = if Tours == undefined -> null; true -> Tours end,
+    SetNo = if Tour == undefined -> null; true -> Tour end,
+    Rounds = if Rounds1 == infinity -> -1; true -> Rounds1 end,
+    #tavla_game_info{%%game_type :: atom(),
+                     table_name = list_to_binary(TName),
+                     game_mode = GameMode,
+                     sets = Sets,
+                     set_no = SetNo,
+                     table_id  = TableId,
+                     tables_num = TablesNum,
+                     %%current_round :: integer(),
+                     rounds = Rounds,
+                     players = PInfos,
+                     speed = Speed,
+                     turn_timeout = TurnTimeout,
+                     %%challenge_timeout :: integer(),   %% timeout value for challenge
+                     ready_timeout = ReadyTimeout,
+                     mul_factor = MulFactor,
+                     slang_flag = SlangFlag,
+                     observer_flag = ObserverFlag,
+                     pause_enabled = PauseMode == normal,
+                     social_actions_enabled = SocialActionsEnabled,
+                     tournament_type = TournamentType,
+                     series_confirmation_mode = list_to_binary(atom_to_list(ConfirmMode))
+                    }.
+
+create_tavla_game_player_state(_PlayerId, ?STATE_WAITING_FOR_START,
+                               #state{table_id = TableId, cur_round = CurRound, players = Players,
+                                      set_timeout = SetTimeout1, set_timer = SetTRef}) ->
+    Colors = players_ext_color_info(Players),
+    SetTimeout = if SetTimeout1 == infinity -> null;
+                    true -> calc_timeout_comp(SetTRef, 2000)
+                 end,
+    #tavla_game_player_state{table_id = TableId,
+                             board = null,
+                             dice = [null, null],
+                             players_colors = Colors,
+                             whos_move = [],
+                             game_state = initializing,
+                             current_round = CurRound,
+                             next_turn_in = null,
+                             paused = false,
+                             round_timeout = null,
+                             set_timeout = SetTimeout};
+
+create_tavla_game_player_state(_PlayerId, ?STATE_FIRST_MOVE_COMPETITION,
+                               #state{timeout_timer = TRef, cur_round = CurRound,
+                                      players = Players, desk_state = DeskState,
+                                      round_timer = RoundTRef,
+                                      round_timeout = RoundTimeout1, set_timer = SetTRef,
+                                      set_timeout = SetTimeout1, table_id = TableId}) ->
+    #desk_state{dice = {WhiteDie, BlackDie} = Dice,
+                board = Board} = DeskState,
+    Colors = players_ext_color_info(Players),
+    WhosMove = case {WhiteDie, BlackDie} of
+                   {undefined, undefined} -> [color_to_ext(?WHITE), color_to_ext(?BLACK)];
+                   {_, undefined} -> [color_to_ext(?BLACK)];
+                   {undefined, _} -> [color_to_ext(?WHITE)]
+               end,
+    Timeout = calc_timeout(TRef),
+    RoundTimeout = if RoundTimeout1 == infinity -> null;
+                      true -> calc_timeout_comp(RoundTRef, 2000)
+                   end,
+    SetTimeout = if SetTimeout1 == infinity -> null;
+                    true -> calc_timeout_comp(SetTRef, 2000)
+                 end,
+    #tavla_game_player_state{table_id = TableId,
+                             board = board_to_ext(Board),
+                             dice = dice_to_ext(Dice),
+                             players_colors = Colors,
+                             whos_move = WhosMove,
+                             game_state = first_move_competition,
+                             current_round = CurRound,
+                             next_turn_in = Timeout,
+                             paused = false,
+                             round_timeout = RoundTimeout,
+                             set_timeout = SetTimeout};
+
+
+create_tavla_game_player_state(_PlayerId, ?STATE_PLAYING,
+                               #state{timeout_timer = TRef, cur_round = CurRound,
+                                      players = Players, desk_state = DeskState,
+                                      round_timer = RoundTRef,
+                                      round_timeout = RoundTimeout1, set_timer = SetTRef,
+                                      set_timeout = SetTimeout1, table_id = TableId}) ->
+    #desk_state{board = Board,
+                cur_color = CurColor,
+                dice = Dice} = DeskState,
+    Colors = players_ext_color_info(Players),
+    GameState = if Dice == {undefined, undefined} -> waiting_for_roll;
+                   true -> waiting_for_move
+                end,
+    Timeout = calc_timeout(TRef),
+    RoundTimeout = if RoundTimeout1 == infinity -> null;
+                      true -> calc_timeout_comp(RoundTRef, 2000)
+                   end,
+    SetTimeout = if SetTimeout1 == infinity -> null;
+                    true -> calc_timeout_comp(SetTRef, 2000)
+                 end,
+    #tavla_game_player_state{table_id = TableId,
+                             board = board_to_ext(Board),
+                             dice = dice_to_ext(Dice),
+                             players_colors = Colors,
+                             whos_move = [color_to_ext(CurColor)],
+                             game_state = GameState,
+                             current_round = CurRound,
+                             next_turn_in = Timeout,
+                             paused = false,
+                             round_timeout = RoundTimeout,
+                             set_timeout = SetTimeout};
+
+create_tavla_game_player_state(_PlayerId, ?STATE_SET_FINISHED,
+                               #state{table_id = TableId, cur_round = CurRound,
+                                      players = Players}) ->
+    Colors = players_ext_color_info(Players),
+    #tavla_game_player_state{table_id = TableId,
+                             board = null,
+                             dice = [null, null],
+                             players_colors = Colors,
+                             whos_move = [],
+                             game_state = finished,
+                             current_round = CurRound,
+                             next_turn_in = null,
+                             paused = false,
+                             round_timeout = null,
+                             set_timeout = null};
+
+create_tavla_game_player_state(_PlayerId, ?STATE_FINISHED,
+                               #state{table_id = TableId, cur_round = CurRound, players = Players,
+                                      set_timeout = SetTimeout1, set_timer = SetTRef}) ->
+    Colors = players_ext_color_info(Players),
+    SetTimeout = if SetTimeout1 == infinity -> null;
+                    true -> calc_timeout_comp(SetTRef, 2000)
+                 end,
+    #tavla_game_player_state{table_id = TableId,
+                             board = null,
+                             dice = [null, null],
+                             players_colors = Colors,
+                             whos_move = [],
+                             game_state = initializing,
+                             current_round = CurRound,
+                             next_turn_in = null,
+                             paused = false,
+                             round_timeout = null,
+                             set_timeout = SetTimeout};
+
+create_tavla_game_player_state(PlayerId, ?STATE_PAUSE,
+                               #state{paused_statename = PausedStateName,
+                                      paused_timeout_value = Timeout
+                                     } = StateData) ->
+    Msg = create_tavla_game_player_state(PlayerId, PausedStateName, StateData),
+    Msg#tavla_game_player_state{next_turn_in = Timeout,
+                                paused = true}.
+
+
+create_tavla_game_started(DeskState, DoFirstMoveCompetitionRoll,
+                         #state{table_id = TableId, cur_round = CurRound,
+                                round_timeout = RoundTimeout1, players = Players,
+                                set_timeout = SetTimeout1, set_timer = SetTRef}) ->
+    #desk_state{board = Board} = DeskState,
+    Colors = players_ext_color_info(Players),
+    RoundTimeout = if RoundTimeout1 == infinity -> null;
+                      true -> RoundTimeout1 - 2000
+                   end,
+    SetTimeout = if SetTimeout1 == infinity -> null;
+                    true -> calc_timeout_comp(SetTRef, 2000)
+                 end,
+    #tavla_game_started{table_id = TableId,
+                        board = board_to_ext(Board),
+                        players = Colors,
+                        current_round = CurRound,
+                        round_timeout = RoundTimeout,
+                        set_timeout = SetTimeout,
+                        do_first_move_competition_roll = DoFirstMoveCompetitionRoll}.
+
+create_won_first_move(Color, Dice,Reroll, #state{table_id = TableId, players = Players}) ->
+    [#player{user_id = UserId}] = find_players_by_color(Color, Players),
+    #tavla_won_first_move{table_id = TableId,
+                          color = color_to_ext(Color),
+                          player = UserId,
+                          dice = dice_to_ext(Dice),
+                          reroll = Reroll}.
+
+create_tavla_next_turn(Color, #state{table_id = TableId, players = Players}) ->
+    [#player{user_id = UserId}] = find_players_by_color(Color, Players),
+    #tavla_next_turn{table_id = TableId, color = color_to_ext(Color), player = UserId}.
+
+create_tavla_rolls_die(Color, Die, #state{table_id = TableId, players = Players}) ->
+    [#player{user_id = UserId}] = find_players_by_color(Color, Players),
+    #tavla_rolls{table_id = TableId,
+                 player = UserId,
+                 color = color_to_ext(Color),
+                 dices = [die_to_ext(Die)]
+                }.
+
+create_tavla_rolls_dice(Color, Dice, #state{table_id = TableId, players = Players}) ->
+    [#player{user_id = UserId}] = find_players_by_color(Color, Players),
+    #tavla_rolls{table_id = TableId,
+                 player = UserId,
+                 color = color_to_ext(Color),
+                 dices = dice_to_ext(Dice)
+                }.
+
+create_tavla_moves(Color, From, To, Type, Pips, #state{table_id = TableId, players = Players}) ->
+    [#player{user_id = UserId}] = find_players_by_color(Color, Players),
+    #tavla_moves{table_id = TableId,
+                 color = color_to_ext(Color),
+                 player = UserId,
+                 from = pos_to_ext(From),
+                 to = pos_to_ext(To),
+                 hits = Type == hit,
+                 pips = Pips}.
+
+create_player_left(SeatNum, UserInfo, Players) ->
+    #player{user_id = OldUserId} = get_player_by_seat_num(SeatNum, Players),
+    IsBot = UserInfo#'PlayerInfo'.robot,
+    #player_left{player = OldUserId,
+                 human_replaced = not IsBot, %% XXX WTF?
+                 bot_replaced = IsBot,       %% XXX WTF?
+                 replacement = UserInfo}.
+
+
+create_tavla_round_ended_win(Winner, RoundScore, TotalScore, _PlayersAchsPoints,
+                             #state{table_id = TableId, players = Players}) ->
+    PlResults = [begin
+                     #player{user_id = UserId} = get_player_by_seat_num(SeatNum, Players),
+                     WinnerStatus = if SeatNum == Winner -> <<"true">>;
+                                       true -> <<"none">> end,
+                     {_, Score} = lists:keyfind(SeatNum, 1, TotalScore),
+                     {_, ScoreDelta} = lists:keyfind(SeatNum, 1, RoundScore),
+                     #'TavlaPlayerScore'{player_id = UserId,
+                                         %%reason :: atom(),
+                                         winner = WinnerStatus,
+                                         score_delta = ScoreDelta,
+                                         score = Score
+                                        }
+                 end || SeatNum <- [1, 2]],
+    Results = #'TavlaGameResults'{players = PlResults},
+    #'TavlaPlayerScore'{player_id = WinnerUserId} =
+                            lists:keyfind(<<"true">>, #'TavlaPlayerScore'.winner, PlResults),
+    #tavla_game_ended{table_id = TableId,
+                      reason = <<"win">>,
+                      winner = WinnerUserId,
+                      results =  Results}.
+
+create_tavla_round_ended_surrender(Surrender, RoundScore, TotalScore, _PlayersAchsPoints,
+                                   #state{table_id = TableId, players = Players}) ->
+    PlResults = [begin
+                     #player{user_id = UserId} = get_player_by_seat_num(SeatNum, Players),
+                     WinnerStatus = if SeatNum =/= Surrender -> <<"true">>;
+                                       true -> <<"none">> end,
+                     {_, Score} = lists:keyfind(SeatNum, 1, TotalScore),
+                     {_, ScoreDelta} = lists:keyfind(SeatNum, 1, RoundScore),
+                     #'TavlaPlayerScore'{player_id = UserId,
+                                         %%reason :: atom(),
+                                         winner = WinnerStatus,
+                                         score_delta = ScoreDelta,
+                                         score = Score
+                                        }
+                 end || SeatNum <- [1, 2]],
+    Results = #'TavlaGameResults'{players = PlResults},
+    #'TavlaPlayerScore'{player_id = WinnerUserId} =
+                            lists:keyfind(<<"true">>, #'TavlaPlayerScore'.winner, PlResults),
+    #tavla_game_ended{table_id = TableId,
+                      reason = <<"surrender">>,
+                      winner = WinnerUserId,
+                      results =  Results}.
+
+create_tavla_round_ended_draw(Reason, RoundScore, TotalScore, _PlayersAchsPoints,
+                             #state{table_id = TableId, players = Players}) ->
+    PlResults = [begin
+                     #player{user_id = UserId} = get_player_by_seat_num(SeatNum, Players),
+                     {_, Score} = lists:keyfind(SeatNum, 1, TotalScore),
+                     {_, ScoreDelta} = lists:keyfind(SeatNum, 1, RoundScore),
+                     #'TavlaPlayerScore'{player_id = UserId,
+                                         %%reason :: atom(),
+                                         winner = <<"none">>,
+                                         score_delta = ScoreDelta,
+                                         score = Score
+                                        }
+                 end || SeatNum <- [1, 2]],
+    Results = #'TavlaGameResults'{players = PlResults},
+    ReasonStr = case Reason of
+                    round_timeout -> <<"round_timeout">>;
+                    set_timeout -> <<"set_timeout">>
+                end,
+    #tavla_game_ended{table_id = TableId,
+                      reason = ReasonStr,
+                      winner = null,
+                      results =  Results}.
+
+create_tavla_series_ended(Results, #state{table_id = TableId, players = Players}) ->
+    Standings = [begin
+                     #player{user_id = UserId} = fetch_player(PlayerId, Players),
+                     Winner = case Status of    %% TODO: Implement in the client support of all statuses
+                                  winner -> <<"true">>;
+                                  _ -> <<"none">>
+                              end,
+                     #'TavlaSeriesResult'{player_id = UserId, place = Position, score = Score,
+                                          winner = Winner}
+                 end || {PlayerId, Position, Score, Status} <- Results],
+    #tavla_series_ended{table_id = TableId,
+                        standings = Standings}.
+
+create_tavla_tour_result(TourNum, Results, #state{table_id = TableId}) ->
+    Records = [#tavla_tour_record{player_id = UserId, place = Position,
+                                  score = Score, status = Status}
+               || {UserId, Position, Score, Status} <- Results],
+    #tavla_tour_result{table_id = TableId,
+                       tour_num = TourNum,
+                       records = Records}.
+
+create_tavla_turn_timeout(Color, Dice, Moves, #state{table_id = TableId, players = Players}) ->
+    DiceExt = if Dice == undefined -> null;
+                 true -> dice_to_ext(Dice) end,
+    [#player{user_id = UserId}] = find_players_by_color(Color, Players),
+    #tavla_turn_timeout{table_id = TableId,
+                        player = UserId,
+                        color = color_to_ext(Color),
+                        dice = DiceExt,
+                        moves = moves_to_ext(Moves)}.
+
+create_game_paused_pause(UserId, GameId) ->
+    #game_paused{game = GameId,
+                 who = UserId,
+                 action = <<"pause">>,
+                 retries = 0}.
+
+create_game_paused_resume(UserId, GameId) ->
+    #game_paused{game = GameId,
+                 who = UserId,
+                 action = <<"resume">>,
+                 retries = 0}.
+
+
+desk_error_to_ext({position_occupied, _, _}) -> {error, position_occupied};
+desk_error_to_ext({waste_move_disabled, _, _}) -> {error, waste_move_disabled};
+desk_error_to_ext({hit_and_run_disabled, _, _}) -> {error, hit_and_run_disabled};
+desk_error_to_ext({not_bear_off_mode, _, _}) -> {error, not_bear_off_mode};
+desk_error_to_ext({no_checker, _, _}) -> {error, no_checker};
+desk_error_to_ext({move_from_bar_first, _, _}) -> {error, move_from_bar_first};
+desk_error_to_ext({invalid_move, _, _}) -> {error, invalid_move};
+desk_error_to_ext(too_many_moves) -> {error, too_many_moves};
+desk_error_to_ext(invalid_action) -> {error, invalid_action};
+desk_error_to_ext(not_your_order) -> {error, not_your_turn};
+desk_error_to_ext(_E) -> {error, unknown_error}.
+
+players_ext_color_info(Players) ->
+    [begin
+         [#player{user_id = UId}] = find_players_by_color(C, Players),
+         #tavla_color_info{name = UId, color = color_to_ext(C)}
+     end || C <- [?WHITE, ?BLACK]].
+
+board_to_ext(Board) ->
+%%    ?INFO("board_to_ext Board: ~p", [Board]),
+    Order = [?WHITE_OUT] ++ lists:seq(1, 24) ++ [?WHITE_BAR, ?BLACK_BAR, ?BLACK_OUT],
+    [case lists:keyfind(Pos, 1, Board) of
+         {_, empty} -> null;
+         {_, {C, Num}} -> #tavla_checkers{color = color_to_ext(C), number = Num}
+     end || Pos <- Order].
+
+moves_to_ext(Moves) ->
+    [#'TavlaAtomicMoveServer'{from = pos_to_ext(From),
+                              to =  pos_to_ext(To),
+                              hits = Type == hit,
+                              pips = Pips} || {Type, From, To, Pips} <- Moves].
+
+ext_to_moves(ExtMoves) ->
+    F = fun(#'TavlaAtomicMove'{from = From, to = To}) ->
+                {ext_to_pos(From), ext_to_pos(To)}
+        end,
+    lists:map(F, ExtMoves).
+
+pos_to_ext(Pos) ->
+    case Pos of
+        ?WHITE_OUT -> 0;
+        ?WHITE_BAR -> 25;
+        ?BLACK_BAR -> 26;
+        ?BLACK_OUT -> 27;
+        X -> X
+    end.
+
+ext_to_pos(ExtPos) ->
+     case ExtPos of
+         0 -> ?WHITE_OUT;
+         25 -> ?WHITE_BAR;
+         26 -> ?BLACK_BAR;
+         27 -> ?BLACK_OUT;
+         X when is_integer(X), X >=1, X =< 24 -> X
+     end.
+
+%% XXX Different colors id for different external terms is strange... 
+color_to_ext(?WHITE) -> 1;
+color_to_ext(?BLACK) -> 2.
+
+dice_to_ext({Die1, Die2}) -> [die_to_ext(Die1), die_to_ext(Die2)].
+
+die_to_ext(undefined) -> null;
+die_to_ext(Die) -> Die.
+
+
+%%===================================================================
+
+get_timeout(turn, fast) -> {ok, Val}   = nsm_db:get(config,"games/okey/turn_timeout_fast", 15000), Val;
+get_timeout(turn, normal) -> {ok, Val} = nsm_db:get(config,"games/okey/turn_timeout_normal", 30000), Val;
+get_timeout(turn, slow) -> {ok, Val}   = nsm_db:get(config,"games/okey/turn_timeout_slow", 60000), Val;
+
+get_timeout(ready, fast) -> {ok, Val}   = nsm_db:get(config,"games/okey/ready_timeout_fast", 15000), Val;
+get_timeout(ready, normal) -> {ok, Val} = nsm_db:get(config,"games/okey/ready_timeout_normal", 25000), Val;
+get_timeout(ready, slow) -> {ok, Val}   = nsm_db:get(config,"games/okey/ready_timeout_slow", 45000), Val.
+
+%%===================================================================
+
+calc_timeout(undefined) -> 0;
+calc_timeout(TRef) ->
+    case erlang:read_timer(TRef) of
+        false -> 0;
+        Timeout -> Timeout
+    end.
+
+calc_timeout_comp(TRef, Compensation) ->
+    if TRef == undefined -> null;
+       true -> case erlang:read_timer(TRef) of
+                   false -> 0;
+                   T when T < Compensation -> 0; %% Latency time compensation
+                   T -> T - Compensation
+               end
+    end.
+
+%%===================================================================
+init_desk_state(Desk) ->
+    #desk_state{state = ?DESK:get_state_name(Desk),
+                board = ?DESK:get_board(),
+                cur_color = ?DESK:get_cur_color(Desk),
+                dice = {undefined, undefined},
+                finish_reason = undefined,
+                finish_info = undefined}.
+
+init_board() ->
+    [{01, {?BLACK, 2}}, {02, empty}, {03, empty}, {04, empty}, {05, empty}, {06, {?WHITE, 5}},
+     {07, empty}, {08, {?WHITE, 3}}, {09, empty}, {10, empty}, {11, empty}, {12, {?BLACK, 5}},
+     {13, {?WHITE, 5}}, {14, empty}, {15, empty}, {16, empty}, {17, {?BLACK, 3}}, {18, empty},
+     {19, {?BLACK, 5}}, {20, empty}, {21, empty}, {22, empty}, {23, empty}, {24, {?WHITE, 2}},
+     {?WHITE_OUT, empty}, {?BLACK_OUT, empty}, {?WHITE_BAR, empty}, {?BLACK_BAR, empty}
+    ].
+
+
+opponent_color(?WHITE) -> ?BLACK;
+opponent_color(?BLACK) -> ?WHITE.
+
+
+pips_list(Die1, Die2) ->
+    if Die1 == Die2 -> [Die1, Die1, Die1, Die1];
+       true -> [Die1, Die2]
+    end.
+
+apply_moves(Color, Moves, Board) ->
+    F = fun({Type, From, To, _Pips}, BoardAcc) ->
+                apply_move(Color, From, To, Type, BoardAcc)
+        end,
+    lists:foldl(F, Board, Moves).
+
+apply_move(Color, From, To, Type, Board) ->
+    OppColor = opponent_color(Color),
+    Board1 = case get_checkers(To, Board) of
+                 empty -> set_checkers(To, {Color, 1}, Board);
+                 {Color, Num} -> set_checkers(To, {Color, Num + 1}, Board);
+                 {OppColor, 1} -> set_checkers(To, {Color, 1}, Board)
+             end,
+    Board2 = case get_checkers(From, Board1) of
+                 {Color, 1} -> set_checkers(From, empty, Board1);
+                 {Color, Num2} -> set_checkers(From, {Color, Num2 -1}, Board1)
+             end,
+    if Type == hit -> %% Increase number of the opponents battons on the bar
+           BarPos = bar_position(OppColor),
+           case get_checkers(BarPos, Board2) of
+               empty -> set_checkers(BarPos, {OppColor, 1}, Board2);
+               {OppColor, Num3} -> set_checkers(BarPos, {OppColor, Num3 + 1}, Board2)
+           end;
+       Type == move ->
+           Board2
+    end.
+
+%% surrender_condition(Color, Board) -> normal | mars
+surrender_condition(Color, Board) ->
+    case get_checkers(out_position(Color), Board) of
+        {Color, _} -> normal;
+        empty -> mars
+    end.
+
+get_checkers(Pos, Board) ->
+    {_, Value} = lists:keyfind(Pos, 1, Board),
+    Value.
+
+set_checkers(Pos, Value, Board) ->
+    lists:keyreplace(Pos, 1, Board, {Pos, Value}).
+
+out_position(?WHITE) -> ?WHITE_OUT;
+out_position(?BLACK) -> ?BLACK_OUT.
+
+bar_position(?WHITE) -> ?WHITE_BAR;
+bar_position(?BLACK) -> ?BLACK_BAR.
+
+%%===================================================================
+%%
+
+find_moves(Color, PipsList, Board) ->
+    AdoptedBoard = if Color == ?BLACK -> reverse_board(Board);
+                      true -> Board end,
+    Moves = lists:reverse(find_moves2(AdoptedBoard, PipsList)),
+    if Color == ?BLACK -> reverse_moves(Moves);
+       true -> Moves
+    end.
+
+find_moves2(Board, PipsList) ->
+    case find_bar_out_moves(Board, PipsList) of
+        {ok, NewBoard, NewPipsList, Moves} ->
+            find_normal_moves(NewBoard, NewPipsList, Moves);
+        {stop, Moves} ->
+            Moves
+    end.
+
+find_bar_out_moves(Board, PipsList) ->
+    case get_checkers(?WHITE_BAR, Board) of
+        empty -> {ok, Board, PipsList, []};
+        {?WHITE, Num} -> find_bar_out_moves(Board, PipsList, PipsList, Num, [])
+    end.
+
+find_bar_out_moves(Board, OrigPipsList, _PipsList, _Num = 0, Moves) -> {ok, Board, OrigPipsList, Moves};
+find_bar_out_moves(_Board, _OrigPipsList, _PipsList = [], _Num, Moves) -> {stop, Moves};
+find_bar_out_moves(Board, OrigPipsList, [Pips | RestPipsList], Num, Moves) ->
+    case check_move(?WHITE_BAR, Pips, Board, false) of
+        {Type, To} ->
+            NewBoard = apply_move(?WHITE, ?WHITE_BAR, To, Type, Board),
+            find_bar_out_moves(NewBoard, OrigPipsList -- [Pips], RestPipsList, Num - 1, [{?WHITE_BAR, To} | Moves]);
+        error ->
+            find_bar_out_moves(Board, OrigPipsList, RestPipsList, Num, Moves)
+    end.
+
+
+find_normal_moves(Board, PipsList, Moves) ->
+    BearOffMode = bearoff_mode(Board),
+    if BearOffMode -> find_normal_moves(Board, PipsList, [], true, Moves, 6);
+       true -> find_normal_moves(Board, PipsList, [], false, Moves, 24)
+    end.
+
+find_normal_moves(_Board, _PipsList = [], _FailedPipsList = [], _PreBearOffMode, Moves, _Pos) ->
+    Moves;
+find_normal_moves(_Board, _PipsList, _FailedPipsList, _PreBearOffMode, Moves, _Pos = 0) ->
+    Moves;
+find_normal_moves(Board, _PipsList = [], FailedPipsList, PreBearOffMode, Moves, Pos) ->
+    find_normal_moves(Board, FailedPipsList, [], PreBearOffMode, Moves, Pos - 1);
+find_normal_moves(Board, [Pips | Rest] = PipsList, FailedPipsList, PreBearOffMode, Moves, Pos) ->
+    case get_checkers(Pos, Board) of
+        {?WHITE, _} ->
+            BearOffMode = PreBearOffMode orelse bearoff_mode(Board), %% Optimization
+            case check_move(Pos, Pips, Board, BearOffMode) of
+                {Type, To} ->
+                    NewBoard = apply_move(?WHITE, Pos, To, Type, Board),
+                    find_normal_moves(NewBoard, Rest, FailedPipsList, BearOffMode, [{Pos, To} | Moves], Pos);
+                error ->
+                    find_normal_moves(Board, Rest, [Pips | FailedPipsList], BearOffMode, Moves, Pos)
+            end;
+        _ ->
+            find_normal_moves(Board, PipsList, FailedPipsList, PreBearOffMode, Moves, Pos - 1)
+    end.
+
+bearoff_mode(Board) ->
+    F = fun(Pos) -> not is_white(Pos, Board) end,
+    lists:all(F, [?WHITE_BAR | lists:seq(7, 24)]).
+
+is_white(Pos, Board) ->
+    case get_checkers(Pos, Board) of
+        {?WHITE, _} -> true;
+        _ -> false
+    end.
+
+%% check_move(From, Pips, Board, BearOffMode) -> {move, To} | {hit, To} | error
+check_move(From, Pips, Board, BearOffMode) ->
+    To = new_pos(From, Pips),
+    if To == ?WHITE_OUT andalso BearOffMode ->
+           case no_white_checkers_behind(From, Board) of
+               true -> {move, To};
+               false -> error
+           end;
+       To == ?WHITE_OUT -> error;
+       true ->
+           case can_move_to(To, Board) of
+               {yes, normal} -> {move, To};
+               {yes, hit} -> {hit, To};
+               no -> error
+           end
+    end.
+
+no_white_checkers_behind(From, Board) ->
+    if From == 6 -> true;
+       true ->
+           F = fun(Pos) -> not is_white(Pos, Board) end,
+           lists:all(F, lists:seq(From + 1, 6))
+    end.
+
+
+can_move_to(Pos, Board) ->
+    case get_checkers(Pos, Board) of
+        empty -> {yes, normal};
+        {?WHITE, _} -> {yes, normal};
+        {?BLACK, 1} -> {yes, hit};
+        {?BLACK, _} -> no
+    end.
+
+new_pos(Pos, Pips) ->
+    case Pos of
+        ?WHITE_BAR -> 25 - Pips;
+        _ when is_integer(Pos) ->
+            Diff = Pos - Pips,
+            if Diff =< 0 -> ?WHITE_OUT;
+               Diff > 0 -> Diff
+            end
+    end.
+
+reverse_board(Board) ->
+    [{reverse_pos(Pos), reverse_value(Value)} || {Pos, Value} <- Board].
+
+reverse_moves(Moves) ->
+    [{reverse_pos(From), reverse_pos(To)} || {From, To} <- Moves].
+
+reverse_pos(Pos) ->
+    case Pos of
+        ?WHITE_OUT -> ?BLACK_OUT;
+        ?BLACK_OUT -> ?WHITE_OUT;
+        ?WHITE_BAR -> ?BLACK_BAR;
+        ?BLACK_BAR -> ?WHITE_BAR;
+        _ -> 25 - Pos
+    end.
+
+reverse_value(Value) ->
+    case Value of
+        empty -> empty;
+        {Color, Num} -> {opponent_color(Color), Num}
+    end.
+

+ 1305 - 0
apps/server/src/tavla/game_tavla_ng_trn_paired.erl

@@ -0,0 +1,1305 @@
+%%% -------------------------------------------------------------------
+%%% Author  : Sergii Polkovnikov <serge.polkovnikov@gmail.com>
+%%% Description : The paired tavla logic
+%%%
+%%% Created : Feb 01, 2013
+%%% -------------------------------------------------------------------
+
+%%% Terms explanation:
+%%% GameId   - uniq identifier of the tournament. Type: integer().
+%%% PlayerId - registration number of a player in the tournament. Type: integer()
+%%% UserId   - cross system identifier of a physical user. Type: binary() (or string()?).
+%%% TableId  - uniq identifier of a table in the tournament. Used by the
+%%%          tournament logic. Type: integer().
+%%% TableGlobalId - uniq identifier of a table in the system. Can be used
+%%%          to refer to a table directly - without pointing to a tournament.
+%%%          Type: integer()
+
+-module(game_tavla_ng_trn_paired).
+
+-behaviour(gen_fsm).
+%% --------------------------------------------------------------------
+%% Include files
+%% --------------------------------------------------------------------
+-include_lib("server/include/log.hrl").
+-include_lib("server/include/basic_types.hrl").
+-include_lib("db/include/table.hrl").
+-include_lib("db/include/accounts.hrl").
+-include_lib("server/include/game_tavla.hrl").
+
+%% --------------------------------------------------------------------
+%% External exports
+-export([start/1, start/2, start_link/2, reg/2]).
+
+%% gen_fsm callbacks
+-export([init/1, handle_event/3, handle_sync_event/4, handle_info/3, terminate/3, code_change/4]).
+
+-export([table_message/3, client_message/2, client_request/2, client_request/3]).
+
+
+-record(state,
+        {%% Static values
+         game_id           :: pos_integer(),
+         game_type         :: atom(),
+         game_mode         :: atom(),
+         params            :: proplists:proplist(),
+         tables_num        :: integer(), %% 1 - 5
+         seats_per_table   :: integer(),
+         table_module      :: atom(),
+         bot_module        :: atom(),
+         quota_per_round   :: integer(),
+         kakush_for_winners :: integer(),
+         kakush_for_loser  :: integer(),
+         win_game_points   :: integer(),
+         mul_factor        :: integer(),
+         registrants       :: list(), %% [robot | binary()]
+         bots_replacement_mode :: enabled | disabled,
+         common_params     :: proplists:proplist(),
+         %% Dynamic values
+         players,          %% The register of tournament players
+         tables,           %% The register of tournament tables
+         seats,            %% Stores relation between players and tables seats
+         tour              :: pos_integer(),
+         tournament_table  :: list(), %% [{TurnNum, TurnRes}], TurnRes = [{PlayerId, CommonPos, Points, Status}]
+         table_id_counter  :: pos_integer(),
+         player_id_counter :: pos_integer(),
+         cr_tab_requests   :: dict(),  %% {TableId, PlayersIds}
+         reg_requests      :: dict(),  %% {PlayerId, From}
+         tab_requests      :: dict(),  %% {RequestId, RequestContext}
+         timer             :: undefined | reference(),
+         timer_magic       :: undefined | reference(),
+         tables_wl         :: list(),  %% Tables waiting list
+         tables_results    :: list(),  %% [{TableId, TableResult}]
+         series_results    :: list(),  %% [{TableId, SeriesResult}]
+         start_color       :: white | black,
+         cur_color         :: white | black,
+         next_turn_wl      :: list() %% [TableId]
+        }).
+
+-record(player,
+        {
+         id              :: pos_integer(),
+         user_id,
+         user_info       :: #'PlayerInfo'{},
+         is_bot          :: boolean()
+        }).
+
+-record(table,
+        {
+         id              :: pos_integer(),
+         global_id       :: pos_integer(),
+         pid             :: pid(),
+         relay           :: {atom(), pid()}, %% {RelayMod, RelayPid}
+         mon_ref         :: reference(),
+         state           :: initializing | ready | in_process | finished,
+         context         :: term(), %% Context term of a table. For failover proposes.
+         timer           :: reference()
+        }).
+
+-record(seat,
+        {
+         table           :: pos_integer(),
+         seat_num        :: integer(),
+         player_id       :: undefined | pos_integer(),
+         is_bot          :: undefined | boolean(),
+         registered_by_table :: undefined | boolean(),
+         connected       :: undefined | boolean(),
+         free            :: boolean()
+        }).
+
+
+
+-define(STATE_INIT, state_init).
+-define(STATE_WAITING_FOR_TABLES, state_waiting_for_tables).
+-define(STATE_EMPTY_SEATS_FILLING, state_empty_seats_filling).
+-define(STATE_WAITING_FOR_PLAYERS, state_waiting_for_players).
+-define(STATE_TOUR_PROCESSING, state_tour_processing).
+-define(STATE_TOUR_FINISHED, state_tour_finished).
+-define(STATE_SHOW_TOUR_RESULT, state_show_tour_result).
+-define(STATE_FINISHED, state_finished).
+
+-define(TAB_MOD, game_okey_ng_table_trn).
+
+-define(TABLE_STATE_INITIALIZING, initializing).
+-define(TABLE_STATE_READY, ready).
+-define(TABLE_STATE_IN_PROGRESS, in_progress).
+-define(TABLE_STATE_WAITING_NEW_ROUND, waiting_new_round).
+-define(TABLE_STATE_FINISHED, finished).
+
+-define(WAITING_PLAYERS_TIMEOUT, 10000) . %% Time between all table was created and starting a turn
+-define(REST_TIMEOUT, 5000).             %% Time between a round finish and start of a new one
+-define(SHOW_SERIES_RESULT_TIMEOUT, 30000).%% Time between a tour finish and start of a new one
+%%-define(SHOW_TOURNAMENT_RESULT_TIMEOUT, 15000). %% Time between last tour result showing and the tournament finish
+
+-define(TOURNAMENT_TYPE, paired).
+-define(GAME_TYPE, game_tavla).
+-define(SEATS_NUM, 2). %% TODO: Define this by a parameter. Number of seats per table
+
+-define(WHITE, white).
+-define(BLACK, black).
+
+%% ====================================================================
+%% External functions
+%% ====================================================================
+
+start([GameId, Params]) -> start(GameId, Params).
+
+start(GameId, Params) ->
+    gen_fsm:start(?MODULE, [GameId, Params, self()], []).
+
+start_link(GameId, Params) ->
+    gen_fsm:start_link(?MODULE, [GameId, Params, self()], []).
+
+reg(Pid, User) ->
+    client_request(Pid, {join, User}, 10000).
+
+table_message(Pid, TableId, Message) ->
+    gen_fsm:send_all_state_event(Pid, {table_message, TableId, Message}).
+
+client_message(Pid, Message) ->
+    gen_fsm:send_all_state_event(Pid, {client_message, Message}).
+
+client_request(Pid, Message) ->
+    client_request(Pid, Message, 5000).
+
+client_request(Pid, Message, Timeout) ->
+    gen_fsm:sync_send_all_state_event(Pid, {client_request, Message}, Timeout).
+
+
+%% ====================================================================
+%% Server functions
+%% ====================================================================
+
+init([GameId, Params, _Manager]) ->
+    ?INFO("TRN_PAIRED <~p> Init started",[GameId]),
+    Registrants =   get_param(registrants, Params),
+    GameMode =      get_param(game_mode, Params),
+    GameName =      get_param(game_name, Params),
+    TablesNum =     get_param(tables_num, Params),
+    QuotaPerRound = get_param(quota_per_round, Params),
+    KakushForWinners = get_param(kakush_for_winners, Params),
+    KakushForLoser = get_param(kakush_for_loser, Params),
+    WinGamePoints = get_param(win_game_points, Params),
+    MulFactor =     get_param(mul_factor, Params),
+    TableParams =   get_param(table_params, Params),
+    TableModule =   get_param(table_module, Params),
+    BotModule =     get_param(bot_module, Params),
+    BotsReplacementMode = get_param(bots_replacement_mode, Params),
+    CommonParams  = get_param(common_params, Params),
+
+    [?INFO("TRN_PAIRED_DBG <~p> Parameter <~p> : ~p", [GameId, P, V]) || {P, V} <- Params],
+
+    ?INFO("TRN_PAIRED <~p> started.  Pid:~p", [GameId, self()]),
+
+    gen_fsm:send_all_state_event(self(), go),
+    {ok, ?STATE_INIT, #state{game_id = GameId,
+                             game_type = ?GAME_TYPE,
+                             game_mode = GameMode,
+                             params = TableParams,
+                             tables_num = TablesNum,
+                             seats_per_table = ?SEATS_NUM,
+                             table_module = TableModule,
+                             bot_module = BotModule,
+                             quota_per_round = QuotaPerRound,
+                             kakush_for_winners = KakushForWinners,
+                             kakush_for_loser = KakushForLoser,
+                             win_game_points = WinGamePoints,
+                             mul_factor = MulFactor,
+                             registrants = Registrants,
+                             bots_replacement_mode = BotsReplacementMode,
+                             common_params = CommonParams,
+                             table_id_counter = 1
+                            }}.
+
+%%===================================================================
+handle_event(go, ?STATE_INIT, #state{game_id = GameId, registrants = Registrants,
+                                     game_type = GameType, bot_module = BotModule,
+                                     common_params = CommonParams} = StateData) ->
+    ?INFO("TRN_PAIRED <~p> Received a directive to starting the game.", [GameId]),
+    DeclRec = create_decl_rec(GameType, CommonParams, GameId, Registrants),
+    gproc:reg({p,l,self()}, DeclRec),
+    {Players, PlayerIdCounter} = setup_players(Registrants, GameId, BotModule),
+    NewStateData = StateData#state{players = Players,
+                                   player_id_counter = PlayerIdCounter},
+   init_tour(1, NewStateData);
+
+handle_event({client_message, Message}, StateName, #state{game_id = GameId} = StateData) ->
+    ?INFO("TRN_PAIRED_DBG <~p> Received the message from a client: ~p.", [GameId, Message]),
+    handle_client_message(Message, StateName, StateData);
+
+handle_event({table_message, TableId, Message}, StateName, #state{game_id = GameId} = StateData) ->
+    ?INFO("TRN_PAIRED_DBG <~p> Received the message from table <~p>: ~p.", [GameId, TableId, Message]),
+    handle_table_message(TableId, Message, StateName, StateData);
+
+handle_event(Message, StateName, #state{game_id = GameId} = StateData) ->
+    ?INFO("TRN_PAIRED_DBG <~p> Unhandled message(event) received in state <~p>: ~p.",
+          [GameId, StateName, Message]),
+    {next_state, StateName, StateData}.
+
+handle_sync_event({client_request, Request}, From, StateName, #state{game_id = GameId} = StateData) ->
+    ?INFO("TRN_PAIRED_DBG <~p> Received the request from a client: ~p.", [GameId, Request]),
+    handle_client_request(Request, From, StateName, StateData);
+
+handle_sync_event(Request, From, StateName, #state{game_id = GameId} = StateData) ->
+    ?INFO("TRN_PAIRED_DBG <~p> Unhandled request(event) received in state <~p> from ~p: ~p.",
+          [GameId, StateName, From, Request]),
+    {reply, {error, unknown_request}, StateName, StateData}.
+
+%%===================================================================
+
+handle_info({'DOWN', MonRef, process, _Pid, _}, StateName,
+            #state{game_id = GameId, tables = Tables} = StateData) ->
+    case get_table_by_mon_ref(MonRef, Tables) of
+        #table{id = TableId} ->
+            ?INFO("TRN_PAIRED <~p> Table <~p> is down. Stopping", [GameId, TableId]),
+            %% TODO: More smart handling (failover) needed
+            {stop, {one_of_tables_down, TableId}, StateData};
+        not_found ->
+            {next_state, StateName, StateData}
+    end;
+
+
+handle_info({timeout, Magic}, ?STATE_WAITING_FOR_PLAYERS,
+            #state{timer_magic = Magic, game_id = GameId} = StateData) ->
+    ?INFO("TRN_PAIRED <~p> Time to start the tour.", [GameId]),
+    start_tour(StateData);
+
+handle_info({timeout, Magic}, ?STATE_TOUR_PROCESSING,
+            #state{timer_magic = Magic, game_id = GameId, tables = Tables,
+                   seats = Seats, players = Players, table_module = TableModule,
+                   bot_module = BotModule, player_id_counter = PlayerIdCounter,
+                   game_type = GameType, common_params = CommonParams,
+                   tab_requests = Requests} = StateData) ->
+    ?INFO("TRN_PAIRED <~p> Time to start new round. Checking start conditions...", [GameId]),
+    DisconnectedSeats = find_disconnected_seats(Seats),
+    DisconnectedPlayers = [PlayerId || #seat{player_id = PlayerId} <- DisconnectedSeats],
+    ConnectedRealPlayers = [PlayerId || #player{id = PlayerId, is_bot = false} <- players_to_list(Players),
+                                        not lists:member(PlayerId, DisconnectedPlayers)],
+    case ConnectedRealPlayers of
+        [] -> %% Finish game
+            ?INFO("TRN_PAIRED <~p> No real players left in tournament. "
+                  "Stopping the game.", [GameId]),
+            finalize_tables_with_disconnect(TableModule, Tables),
+            {stop, normal, StateData#state{tables = [], seats = []}};
+        _ -> %% Replace disconnected players by bots and start the round
+            ?INFO("TRN_PAIRED <~p> Enough real players in the game to continue. "
+                  "Replacing disconnected players by bots.", [GameId]),
+            {Replacements, NewPlayers, NewSeats, NewPlayerIdCounter} =
+                replace_by_bots(DisconnectedSeats, GameId, BotModule, Players, Seats, PlayerIdCounter),
+            NewRequests = req_replace_players(TableModule, Tables, Replacements, Requests),
+            update_gproc(GameId, GameType, CommonParams, NewPlayers),
+            ?INFO("TRN_PAIRED <~p> The replacement is completed.", [GameId]),
+            start_round(StateData#state{tab_requests = NewRequests,
+                                        players = NewPlayers,
+                                        seats = NewSeats,
+                                        player_id_counter = NewPlayerIdCounter})
+    end;
+
+handle_info({timeout, Magic}, ?STATE_TOUR_FINISHED,
+            #state{timer_magic = Magic, game_id = GameId} = StateData) ->
+    ?INFO("TRN_PAIRED <~p> Time to finalize the tour.", [GameId]),
+    finalize_tour(StateData);
+
+handle_info({timeout, Magic}, ?STATE_SHOW_TOUR_RESULT,
+            #state{timer_magic = Magic, game_id = GameId} = StateData) ->
+    ?INFO("TRN_PAIRED <~p> Time to finalize the game.", [GameId]),
+    finalize_tournament(StateData);
+
+handle_info({publish_series_result, TableId}, StateName,
+            #state{game_id = GameId, tables = Tables, table_module = TableModule,
+                   series_results = SeriesResults} = StateData) ->
+    ?INFO("TRN_PAIRED <~p> Time to publish the series result for table <~p>.", [GameId, TableId]),
+    case fetch_table(TableId, Tables) of
+        #table{state = ?TABLE_STATE_FINISHED, pid = TablePid} ->
+            {_, SeriesResult} = lists:keyfind(TableId, 1, SeriesResults),
+            send_to_table(TableModule, TablePid, {show_series_result, SeriesResult});
+        _ ->
+            ?INFO("TRN_PAIRED <~p> Don't publish the series result because the state of table <~p> "
+                  "is not 'finished'.", [GameId, TableId])
+    end,
+    {next_state, StateName, StateData};
+
+%% handle_info({timeout, Magic}, ?STATE_FINISHED,
+%%             #state{timer_magic = Magic, tables = Tables, game_id = GameId,
+%%                    table_module = TableModule} = StateData) ->
+%%     ?INFO("TRN_PAIRED <~p> Time to stopping the tournament.", [GameId]),
+%%     finalize_tables_with_disconnect(TableModule, Tables),
+%%     {stop, normal, StateData#state{tables = [], seats = []}};
+
+
+handle_info(Message, StateName, #state{game_id = GameId} = StateData) ->
+    ?INFO("TRN_PAIRED <~p> Unhandled message(info) received in state <~p>: ~p.",
+          [GameId, StateName, Message]),
+    {next_state, StateName, StateData}.
+
+%%===================================================================
+
+terminate(_Reason, _StateName, #state{game_id=GameId}=_StatData) ->
+    ?INFO("TRN_PAIRED <~p> Shutting down at state: <~p>. Reason: ~p",
+          [GameId, _StateName, _Reason]),
+    ok.
+
+%%===================================================================
+
+code_change(_OldVsn, StateName, StateData, _Extra) ->
+    {ok, StateName, StateData}.
+
+%% --------------------------------------------------------------------
+%%% Internal functions
+%% --------------------------------------------------------------------
+
+
+handle_client_message(Message, StateName, #state{game_id = GameId} = StateData) ->
+    ?INFO("TRN_PAIRED <~p> Unhandled client message received in "
+          "state <~p>: ~p.", [GameId, StateName, Message]),
+    {next_state, StateName, StateData}.
+
+%%===================================================================
+
+handle_table_message(TableId, {player_connected, PlayerId},
+                     StateName,
+                     #state{seats = Seats} = StateData) ->
+    case find_seats_by_player_id(PlayerId, Seats) of
+        [#seat{seat_num = SeatNum}] ->
+            NewSeats = update_seat_connect_status(TableId, SeatNum, true, Seats),
+            {next_state, StateName, StateData#state{seats = NewSeats}};
+        [] -> %% Ignoring the message
+            {next_state, StateName, StateData}
+    end;
+
+
+handle_table_message(TableId, {player_disconnected, PlayerId},
+                     StateName, #state{seats = Seats} = StateData) ->
+    case find_seats_by_player_id(PlayerId, Seats) of
+        [#seat{seat_num = SeatNum}] ->
+            NewSeats = update_seat_connect_status(TableId, SeatNum, false, Seats),
+            {next_state, StateName, StateData#state{seats = NewSeats}};
+        [] -> %% Ignoring the message
+            {next_state, StateName, StateData}
+    end;
+
+handle_table_message(TableId, {get_tables_states, PlayerId, Ref},
+                     StateName,
+                     #state{tables = Tables, table_module = TableModule} = StateData) ->
+    [send_to_table(TableModule, TPid, {send_table_state, TableId, PlayerId, Ref}) ||
+       #table{id = TId, pid = TPid} <- tables_to_list(Tables), TId =/= TableId],
+    {next_state, StateName, StateData};
+
+handle_table_message(TableId, {table_created, Relay},
+                     ?STATE_WAITING_FOR_TABLES,
+                     #state{tables = Tables, seats = Seats, seats_per_table = SeatsPerTable,
+                            cr_tab_requests = TCrRequests, tables_num = TablesNum,
+                            reg_requests = RegRequests} = StateData) ->
+    TabInitPlayers = dict:fetch(TableId, TCrRequests),
+    NewTCrRequests = dict:erase(TableId, TCrRequests),
+    %% Update status of players
+    TabSeats = find_seats_by_table_id(TableId, Seats),
+    F = fun(#seat{player_id = PlayerId} = S, Acc) ->
+                case lists:member(PlayerId, TabInitPlayers) of
+                    true -> store_seat(S#seat{registered_by_table = true}, Acc);
+                    false -> Acc
+                end
+        end,
+    NewSeats = lists:foldl(F, Seats, TabSeats),
+
+    %% Process delayed registration requests
+    TablePid = get_table_pid(TableId, Tables),
+    F2 = fun(PlayerId, Acc) ->
+                 case dict:find(PlayerId, Acc) of
+                     {ok, From} ->
+                         gen_fsm:reply(From, {ok, {PlayerId, Relay, {?TAB_MOD, TablePid}}}),
+                         dict:erase(PlayerId, Acc);
+                     error -> Acc
+                 end
+         end,
+    NewRegRequests = lists:foldl(F2, RegRequests, TabInitPlayers),
+    NewTables = update_created_table(TableId, Relay, Tables),
+    case dict:size(NewTCrRequests) of
+        0 ->
+            case enough_players(NewSeats, TablesNum*SeatsPerTable) of
+                true ->
+                    {TRef, Magic} = start_timer(?WAITING_PLAYERS_TIMEOUT),
+                    {next_state, ?STATE_WAITING_FOR_PLAYERS,
+                     StateData#state{tables = NewTables, seats = NewSeats, cr_tab_requests = NewTCrRequests,
+                                     reg_requests = NewRegRequests, timer = TRef, timer_magic = Magic}};
+                false ->
+                    {next_state, ?STATE_EMPTY_SEATS_FILLING,
+                     StateData#state{tables = NewTables, seats = NewSeats, cr_tab_requests = NewTCrRequests,
+                                     reg_requests = NewRegRequests}}
+            end;
+        _ -> {next_state, ?STATE_WAITING_FOR_TABLES,
+              StateData#state{tables = NewTables, seats = NewSeats,
+                              cr_tab_requests = NewTCrRequests, reg_requests = NewRegRequests}}
+    end;
+
+
+handle_table_message(TableId, {round_finished, NewScoringState, _RoundScore, _TotalScore},
+                     ?STATE_TOUR_PROCESSING = StateName,
+                     #state{game_id = GameId, tables = Tables, table_module = TableModule,
+                            tables_wl = WL} = StateData) ->
+    ?INFO("TRN_PAIRED <~p> Round is finished (table <~p>).", [GameId, TableId]),
+    #table{pid = TablePid} = Table = fetch_table(TableId, Tables),
+    NewTable = Table#table{context = NewScoringState, state = ?TABLE_STATE_WAITING_NEW_ROUND},
+    NewTables = store_table(NewTable, Tables),
+    send_to_table(TableModule, TablePid, show_round_result),
+    NewWL = lists:delete(TableId, WL),
+    [send_to_table(TableModule, TPid, {playing_tables_num, length(NewWL)})
+       || #table{pid = TPid, state = ?TABLE_STATE_WAITING_NEW_ROUND} <- tables_to_list(Tables)],
+    NewStateData = StateData#state{tables = NewTables,
+                                   tables_wl = NewWL},
+    if NewWL == [] ->
+           {TRef, Magic} = start_timer(?REST_TIMEOUT),
+           {next_state, StateName, NewStateData#state{timer = TRef,
+                                                      timer_magic = Magic}};
+       true ->
+           remove_table_from_next_turn_wl(TableId, StateName, NewStateData)
+    end;
+
+
+handle_table_message(TableId, {game_finished, TableContext, _RoundScore, TableScore},
+                     ?STATE_TOUR_PROCESSING = StateName,
+                     #state{game_id = GameId, tables = Tables, tables_wl = WL,
+                            table_module = TableModule, tables_results = TablesResults,
+                            game_type = GameType, game_mode = GameMode, mul_factor = MulFactor,
+                            kakush_for_winners = KakushForWinners, kakush_for_loser = KakushForLoser,
+                            win_game_points = WinGamePoints, players = Players,
+                            series_results = SeriesResults} = StateData) ->
+    ?INFO("TRN_PAIRED <~p> Last round of the set is finished (table <~p>).", [GameId, TableId]),
+    NewTablesResults = [{TableId, TableScore} | TablesResults],
+    #table{pid = TablePid} = Table = fetch_table(TableId, Tables),
+    NewTable = Table#table{context = TableContext, state = ?TABLE_STATE_FINISHED},
+    NewTables = store_table(NewTable, Tables),
+    send_to_table(TableModule, TablePid, show_round_result),
+    NewWL = lists:delete(TableId, WL),
+    [send_to_table(TableModule, TPid, {playing_tables_num, length(NewWL)})
+       || #table{pid = TPid, state = ?TABLE_STATE_FINISHED} <- tables_to_list(Tables)],
+    SeriesResult = series_result(TableScore),
+    NewSeriesResults = [{TableId, SeriesResult} | SeriesResults],
+    ?INFO("TRN_PAIRED <~p> Set result: ~p", [GameId, SeriesResult]),
+    Points = calc_players_prize_points(SeriesResult, KakushForWinners, KakushForLoser, WinGamePoints, MulFactor, Players),
+    UsersPrizePoints = prepare_users_prize_points(Points, Players),
+    ?INFO("TRN_PAIRED <~p> Prizes: ~p", [GameId, UsersPrizePoints]),
+    add_points_to_accounts(UsersPrizePoints, GameId, GameType, GameMode, MulFactor),
+    NewStateData = StateData#state{tables = NewTables,
+                                   tables_results = NewTablesResults,
+                                   series_results = NewSeriesResults,
+                                   tables_wl = NewWL},
+    erlang:send_after(?REST_TIMEOUT, self(), {publish_series_result, TableId}),
+    if NewWL == [] ->
+           {TRef, Magic} = start_timer(?REST_TIMEOUT),
+           {next_state, ?STATE_TOUR_FINISHED, NewStateData#state{timer = TRef,
+                                                                 timer_magic = Magic}};
+       true ->
+           remove_table_from_next_turn_wl(TableId, StateName, NewStateData)
+    end;
+
+
+handle_table_message(TableId, {response, RequestId, Response},
+                     StateName,
+                     #state{game_id = GameId, tab_requests = TabRequests} = StateData) ->
+    NewTabRequests = dict:erase(RequestId, TabRequests),
+    case dict:find(RequestId, TabRequests) of
+        {ok, ReqContext} ->
+            ?INFO("TRN_PAIRED <~p> The a response received from table <~p>. "
+                  "RequestId: ~p. Request context: ~p. Response: ~p",
+                  [GameId, TableId, RequestId, ReqContext, Response]),
+            handle_table_response(TableId, ReqContext, Response, StateName,
+                                  StateData#state{tab_requests = NewTabRequests});
+        error ->
+            ?ERROR("TRN_PAIRED <~p> Table <~p> sent a response for unknown request. "
+                   "RequestId: ~p. Response", []),
+            {next_state, StateName, StateData#state{tab_requests = NewTabRequests}}
+    end;
+
+
+handle_table_message(TableId, {game_event, #tavla_next_turn{table_id = TableId,
+                                                            color = ExtColor}},
+                     ?STATE_TOUR_PROCESSING = StateName,
+                     #state{cur_color = CurColor, next_turn_wl = NextTurnWL,
+                            game_id = GameId} = StateData) ->
+    Color = ext_to_color(ExtColor),
+    ?INFO("TRN_PAIRED <~p> The 'tavla_next_turn event' received from table <~p>. "
+          "Color: ~p. CurColor: ~p, WaitList: ~p",
+          [GameId, TableId, Color, CurColor, NextTurnWL]),
+    true = opponent(CurColor) == Color, %% Assert 
+    true = lists:member(TableId, NextTurnWL), %% Assert
+    remove_table_from_next_turn_wl(TableId, StateName, StateData);
+
+handle_table_message(TableId, {game_event, GameEvent},
+                     ?STATE_TOUR_PROCESSING = StateName,
+                     #state{tables = Tables, table_module = TableModule} = StateData) ->
+    [send_to_table(TableModule, TablePid, {game_event, GameEvent}) ||
+       #table{pid = TablePid, id = TId} <- tables_to_list(Tables), TId =/= TableId],
+    {next_state, StateName, StateData};
+
+handle_table_message(_TableId, {table_state_event, DestTableId, PlayerId, Ref, StateEvent},
+                     StateName,
+                     #state{tables = Tables, table_module = TableModule} = StateData) ->
+    case get_table(DestTableId, Tables) of
+        {ok, #table{pid = TPid}} ->
+            send_to_table(TableModule, TPid, {table_state_event, PlayerId, Ref, StateEvent});
+        error -> do_nothing
+    end,
+    {next_state, StateName, StateData};
+
+handle_table_message(TableId, Message, StateName, #state{game_id = GameId} = StateData) ->
+    ?INFO("TRN_PAIRED <~p> Unhandled table message received from table <~p> in "
+          "state <~p>: ~p.", [GameId, TableId, StateName, Message]),
+    {next_state, StateName, StateData}.
+
+%%===================================================================
+%% handle_table_response(_TableId, {register_player, PlayerId, TableId, SeatNum}, ok = _Response,
+%%                       StateName,
+%%                       #state{reg_requests = RegRequests, seats = Seats,
+%%                              tables = Tables} = StateData) ->
+%%     Seat = fetch_seat(TableId, SeatNum, Seats),
+%%     NewSeats = store_seat(Seat#seat{registered_by_table = true}, Seats),
+%%     %% Send response to a client for a delayed request
+%%     NewRegRequests =
+%%         case dict:find(PlayerId, RegRequests) of
+%%             {ok, From} ->
+%%                 #table{relay = Relay, pid = TablePid} = fetch_table(TableId, Tables),
+%%                 gen_fsm:reply(From, {ok, {PlayerId, Relay, {?TAB_MOD, TablePid}}}),
+%%                 dict:erase(PlayerId, RegRequests);
+%%             error -> RegRequests
+%%         end,
+%%     {next_state, StateName, StateData#state{seats = NewSeats,
+%%                                             reg_requests = NewRegRequests}};
+
+handle_table_response(_TableId, {replace_player, PlayerId, TableId, SeatNum}, ok = _Response,
+                      StateName,
+                      #state{reg_requests = RegRequests, seats = Seats,
+                             tables = Tables, table_module = TableMod} = StateData) ->
+    Seat = fetch_seat(TableId, SeatNum, Seats),
+    NewSeats = store_seat(Seat#seat{registered_by_table = true}, Seats),
+    %% Send response to a client for a delayed request
+    NewRegRequests =
+        case dict:find(PlayerId, RegRequests) of
+            {ok, From} ->
+                #table{relay = Relay, pid = TablePid} = fetch_table(TableId, Tables),
+                gen_fsm:reply(From, {ok, {PlayerId, Relay, {TableMod, TablePid}}}),
+                dict:erase(PlayerId, RegRequests);
+            error -> RegRequests
+        end,
+    {next_state, StateName, StateData#state{seats = NewSeats,
+                                            reg_requests = NewRegRequests}};
+
+handle_table_response(TableId, RequestContext, Response, StateName,
+                      #state{game_id = GameId} = StateData) ->
+    ?INFO("TRN_PAIRED <~p> Unhandled 'table response' received from table <~p> "
+          "in state <~p>. Request context: ~p. Response: ~p.",
+          [GameId, TableId, StateName, RequestContext, Response]),
+    {next_state, StateName, StateData}.
+
+%%===================================================================
+
+handle_client_request({join, UserInfo}, From, StateName,
+                      #state{game_id = GameId, reg_requests = RegRequests,
+                             seats = Seats, players=Players, tables = Tables,
+                             bots_replacement_mode = BotsReplacementMode,
+                             table_module = TableMod} = StateData) ->
+    #'PlayerInfo'{id = UserId, robot = _IsBot} = UserInfo,
+    ?INFO("TRN_PAIRED <~p> The 'Join' request received from user: ~p.", [GameId, UserId]),
+    if StateName == ?STATE_FINISHED ->
+           ?INFO("TRN_PAIRED <~p> The game is finished. "
+                 "Reject to join user ~p.", [GameId, UserId]),
+           {reply, {error, finished}, StateName, StateData};
+       true -> %% Game in progress. Find a seat for the user
+           case get_player_by_user_id(UserId, Players) of
+               {ok, #player{id = PlayerId}} -> %% The user is a registered member of the game (player)
+                   ?INFO("TRN_PAIRED <~p> User ~p is a registered member of the game. "
+                         "Allow to join.", [GameId, UserId]),
+                   [#seat{table = TableId, registered_by_table = RegByTable}] = find_seats_by_player_id(PlayerId, Seats),
+                   case RegByTable of
+                       false -> %% The player is not registered by the table yet
+                           ?INFO("TRN_PAIRED <~p> User ~p not yet regirested by the table. "
+                                 "Add the request to the waiting pool.", [GameId, UserId]),
+                           NewRegRequests = dict:store(PlayerId, From, RegRequests),
+                           {next_state, StateName, StateData#state{reg_requests = NewRegRequests}};
+                       _ -> %% The player is registered by the table. Return the table requisites
+                           ?INFO("TRN_PAIRED <~p> Return the join response for player ~p immediately.",
+                                 [GameId, UserId]),
+                           #table{relay = Relay, pid = TPid} = fetch_table(TableId, Tables),
+                           {reply, {ok, {PlayerId, Relay, {TableMod, TPid}}}, StateName, StateData}
+                   end;
+               error -> %% Not a member yet
+                   ?INFO("TRN_PAIRED <~p> User ~p is not a member of the game.", [GameId, UserId]),
+                   case find_free_seats(Seats) of
+                       [] when BotsReplacementMode == disabled ->
+                           ?INFO("TRN_PAIRED <~p> No free seats for user ~p. Robots replacement is disabled. "
+                                 "Reject to join.", [GameId, UserId]),
+                           {reply, {error, not_allowed}, StateName, StateData};
+                       [] when BotsReplacementMode == enabled ->
+                           ?INFO("TRN_PAIRED <~p> No free seats for user ~p. Robots replacement is enabled. "
+                                 "Tring to find a robot for replace.", [GameId, UserId]),
+                           case find_registered_robot_seats(Seats) of
+                               [] ->
+                                   ?INFO("TRN_PAIRED <~p> No robots for replacement by user ~p. "
+                                         "Reject to join.", [GameId, UserId]),
+                                   {reply, {error, not_allowed}, StateName, StateData};
+                               [#seat{table = TableId, seat_num = SeatNum, player_id = OldPlayerId} | _] ->
+                                   ?INFO("TRN_PAIRED <~p> There is a robot for replacement by user ~p. "
+                                         "Registering.", [GameId, UserId]),
+                                   reg_player_with_replace(UserInfo, TableId, SeatNum, OldPlayerId, From, StateName, StateData)
+                           end;
+                       [#seat{table = TableId, seat_num = SeatNum} | _] ->
+                           ?INFO("TRN_PAIRED <~p> There is a free seat for user ~p. "
+                                 "Registering.", [GameId, UserId]),
+                           reg_new_player(UserInfo, TableId, SeatNum, From, StateName, StateData)
+                   end
+           end
+    end;
+
+handle_client_request(Request, From, StateName, #state{game_id = GameId} = StateData) ->
+    ?INFO("TRN_PAIRED <~p> Unhandled client request received from ~p in "
+          "state <~p>: ~p.", [GameId, From, StateName, Request]),
+   {reply, {error, unexpected_request}, StateName, StateData}.
+
+%%===================================================================
+init_tour(Tour, #state{game_id = GameId, table_module = TableModule,
+                      params = TableParams, players = Players, tables_num = TablesNum,
+                      table_id_counter = TableIdCounter, tables = OldTables,
+                      seats_per_table = SeatsPerTable} = StateData) ->
+    ?INFO("TRN_PAIRED <~p> Initializing tour <~p>...", [GameId, Tour]),
+    PlayersList = prepare_players_for_new_tour(0, Players),
+    {NewTables, Seats, NewTableIdCounter, CrRequests} =
+        setup_tables(TableModule, PlayersList, SeatsPerTable, TablesNum, _TTable = undefined,
+                     Tour, _Tours = undefined, TableIdCounter, GameId, TableParams),
+    if Tour > 1 -> finalize_tables_with_rejoin(TableModule, OldTables);
+       true -> do_nothing
+    end,
+    ?INFO("TRN_PAIRED <~p> Initializing of tour <~p> is finished. "
+          "Waiting creating confirmations from the tours' tables...",
+          [GameId, Tour]),
+    {next_state, ?STATE_WAITING_FOR_TABLES, StateData#state{tables = NewTables,
+                                                            seats = Seats,
+                                                            table_id_counter = NewTableIdCounter,
+                                                            cr_tab_requests = CrRequests,
+                                                            tour = Tour,
+                                                            reg_requests = dict:new(),
+                                                            tab_requests = dict:new(),
+                                                            tables_results = [],
+                                                            series_results = []
+                                                           }}.
+
+start_tour(#state{game_id = GameId, tour = Tour} = StateData) ->
+    ?INFO("TRN_PAIRED <~p> Starting tour <~p>...", [GameId, Tour]),
+    start_round(StateData#state{start_color = undefined}).
+
+start_round(#state{game_id = GameId, game_type = GameType, game_mode = GameMode,
+                   mul_factor = MulFactor, quota_per_round = Amount,
+                   tables = Tables, players = Players, table_module = TableModule,
+                   start_color = StartColor} = StateData) ->
+    ?INFO("TRN_PAIRED <~p> Starting new round...", [GameId]),
+    UsersIds = [UserId || #player{user_id = UserId, is_bot = false} <- players_to_list(Players)],
+    deduct_quota(GameId, GameType, GameMode, Amount, MulFactor, UsersIds),
+    TablesList = tables_to_list(Tables),
+    [send_to_table(TableModule, Pid, start_round) || #table{pid = Pid} <- TablesList],
+    F = fun(Table, Acc) ->
+                store_table(Table#table{state = ?TABLE_STATE_IN_PROGRESS}, Acc)
+        end,
+    NewTables = lists:foldl(F, Tables, TablesList),
+    WL = [T#table.id || T <- TablesList],
+    ?INFO("TRN_PAIRED <~p> The round is started. Processing...", [GameId]),
+    NewStartColor =
+        if StartColor == undefined ->
+               {Die1, Die2} = competition_roll(),
+               [send_to_table(TableModule, Pid, {action, {competition_rolls, Die1, Die2}})
+                  || #table{pid = Pid} <- TablesList],
+               if Die1 > Die2 -> ?WHITE;
+                  true -> ?BLACK end;
+           true ->
+               OppColor = opponent(StartColor),
+               {Die1, Die2} = roll(),
+               [send_to_table(TableModule, Pid, {action, {rolls, OppColor, Die1, Die2}})
+                  || #table{pid = Pid} <- TablesList],
+               OppColor
+        end,
+    ?INFO("TRN_PAIRED <~p> Start color is ~p", [GameId, NewStartColor]),
+    {next_state, ?STATE_TOUR_PROCESSING, StateData#state{tables = NewTables,
+                                                         tables_wl = WL,
+                                                         start_color = NewStartColor,
+                                                         cur_color = NewStartColor,
+                                                         next_turn_wl = WL}}.
+
+finalize_tour(#state{game_id = GameId} = StateData) ->
+    ?INFO("TRN_PAIRED <~p> Finalizing the tour...", [GameId]),
+    {TRef, Magic} = start_timer(?SHOW_SERIES_RESULT_TIMEOUT),
+    ?INFO("TRN_PAIRED <~p> The tour is finalized. "
+          "Waiting some time (~p secs) before continue...",
+          [GameId, ?SHOW_SERIES_RESULT_TIMEOUT div 1000]),
+    {next_state, ?STATE_SHOW_TOUR_RESULT, StateData#state{timer = TRef, timer_magic = Magic}}.
+
+
+finalize_tournament(#state{game_id = GameId, table_module = TableModule,
+                           tables = Tables} = StateData) ->
+    ?INFO("TRN_PAIRED <~p> Finalizing the tournament...", [GameId]),
+    finalize_tables_with_disconnect(TableModule, Tables),
+    ?INFO("TRN_PAIRED <~p> Finalization completed. Stopping...", [GameId]),
+    {stop, normal, StateData#state{tables = [], seats = []}}.
+
+
+%% series_result(TableResult) -> WithPlaceAndStatus
+%% Types: TableResult = [{PlayerId, Points}]
+%%        WithPlaceAndStatus = [{PlayerId, Place, Points, Status}]
+%%          Status = winner | looser
+series_result(TableResult) ->
+    {_, PointsList} = lists:unzip(TableResult),
+    Max = lists:max(PointsList),
+    F = fun({Pl, Points}, {CurPlace, CurPos, LastPoints}) ->
+                if Points == LastPoints ->
+                       {{Pl, CurPlace, Points, if Points == Max -> winner; true -> looser end},
+                        {CurPlace, CurPos + 1, Points}};
+                   true ->
+                       {{Pl, CurPos, Points, looser},
+                        {CurPos, CurPos + 1, Points}}
+                end
+        end,
+    {WithPlaceAndStatus, _} = lists:mapfoldl(F, {1, 1, Max}, lists:reverse(lists:keysort(2, TableResult))),
+    WithPlaceAndStatus.
+
+
+deduct_quota(GameId, GameType, GameMode, Amount, MulFactor, UsersIds) ->
+    RealAmount = Amount * MulFactor,
+    [begin
+         TI = #ti_game_event{game_name = GameType, game_mode = GameMode,
+                             id = GameId, double_points = MulFactor,
+                             type = start_round, tournament_type = ?TOURNAMENT_TYPE},
+         nsm_accounts:transaction(binary_to_list(UserId), ?CURRENCY_QUOTA, -RealAmount, TI)
+     end || UserId <- UsersIds],
+    ok.
+
+%% calc_players_prize_points(SeriesResult, KakushForWinner, KakushForLoser,
+%%                           WinGamePoints, MulFactor, Players) -> Points
+%% Types:
+%%     SeriesResult = [{PlayerId, _Pos, _Points, Status}]
+%%          Status = winner | looser
+%%     Points = [{PlayerId, KakushPoints, GamePoints}]
+calc_players_prize_points(SeriesResult, KakushForWinners, KakushForLoser, WinGamePoints, MulFactor, Players) ->
+    SeriesResult1 = [begin
+                         #'PlayerInfo'{id = UserId, robot = Robot} = get_user_info(PlayerId, Players),
+                         Paid = is_paid(user_id_to_string(UserId)),
+                         Winner = Status == winner,
+                         {PlayerId, Winner, Robot, Paid}
+                     end || {PlayerId, _Pos, _Points, Status} <- SeriesResult],
+    Paids   = [PlayerId || {PlayerId, _Winner, _Robot, _Paid = true} <- SeriesResult1],
+    Winners = [PlayerId || {PlayerId, _Winner = true, _Robot, _Paid} <- SeriesResult1],
+    TotalNum = length(SeriesResult),
+    PaidsNum = length(Paids),
+    WinnersNum = length(Winners),
+    KakushPerWinner = round(((KakushForWinners * MulFactor) * PaidsNum div TotalNum) / WinnersNum),
+    KakushPerLoser = (KakushForLoser * MulFactor) * PaidsNum div TotalNum,
+    WinGamePoints1 = WinGamePoints * MulFactor,
+    [begin
+         {KakushPoints, GamePoints} = calc_points(KakushPerWinner, KakushPerLoser, WinGamePoints1, Paid, Robot, Winner),
+         {PlayerId, KakushPoints, GamePoints}
+     end || {PlayerId, Winner, Robot, Paid} <- SeriesResult1].
+
+
+calc_points(KakushPerWinner, KakushPerLoser, WinGamePoints, Paid, Robot, Winner) ->
+    if Robot -> {0, 0};
+       not Paid andalso Winner -> {0, WinGamePoints};
+       not Paid -> {0, 0};
+       Paid andalso Winner -> {KakushPerWinner, WinGamePoints};
+       Paid -> {KakushPerLoser, 0}
+    end.
+
+
+%% prepare_users_prize_points(Points, Players) -> UsersPrizePoints
+%% Types:
+%%     Points = [{PlayerId, KakushPoints, GamePoints}]
+%%     UserPrizePoints = [{UserIdStr, KakushPoints, GamePoints}]
+prepare_users_prize_points(Points, Players) ->
+    [{user_id_to_string(get_user_id(PlayerId, Players)), K, G} || {PlayerId, K, G} <- Points].
+
+
+is_paid(UserId) -> nsm_accounts:user_paid(UserId).
+
+
+%% add_points_to_accounts(Points, GameId, GameType, GameMode, MulFactor) -> ok
+%% Types: Points = [{UserId, KakushPoints, GamePoints}]
+add_points_to_accounts(Points, GameId, GameType, GameMode, MulFactor) ->
+    TI = #ti_game_event{game_name = GameType, game_mode = GameMode,
+                        id = GameId, double_points = MulFactor,
+                        type = game_end, tournament_type = ?TOURNAMENT_TYPE},
+    [begin
+         if KakushPoints =/= 0 ->
+                ok = nsm_accounts:transaction(UserId, ?CURRENCY_KAKUSH, KakushPoints, TI);
+            true -> do_nothing
+         end,
+         if GamePoints =/= 0 ->
+                ok = nsm_accounts:transaction(UserId, ?CURRENCY_GAME_POINTS, GamePoints, TI);
+            true -> do_nothing
+         end
+     end || {UserId, KakushPoints, GamePoints} <- Points],
+    ok.
+
+%% TODO: Deduct quota if replaces in the middle of a round
+reg_player_with_replace(UserInfo, TableId, SeatNum, OldPlayerId, From, StateName,
+                        #state{game_id = GameId, players = Players, tables = Tables,
+                               game_type = GameType, seats = Seats, player_id_counter = PlayerId,
+                               tab_requests = TabRequests, reg_requests = RegRequests,
+                               table_module = TableModule, common_params = CommonParams,
+                               game_mode = GameMode, mul_factor = MulFactor,
+                               quota_per_round = Amount} = StateData) ->
+    #'PlayerInfo'{id = UserId, robot = IsBot} = UserInfo,
+    NewPlayers = del_player(OldPlayerId, Players),
+    NewPlayers2 = store_player(#player{id = PlayerId, user_id = UserId,
+                                       user_info = UserInfo, is_bot = IsBot}, NewPlayers),
+    ?INFO("TRN_PAIRED <~p> User ~p registered as player <~p>.", [GameId, UserId, PlayerId]),
+    NewSeats = set_seat(TableId, SeatNum, PlayerId, _Bot = false, _RegByTable = false,
+                        _Connected = false, _Free = false, Seats),
+    ?INFO("TRN_PAIRED <~p> User ~p assigned to seat <~p> of table <~p>.", [GameId, UserId, SeatNum, TableId]),
+    NewRegRequests = dict:store(PlayerId, From, RegRequests),
+    #table{pid = TablePid, state = TableStateName} = fetch_table(TableId, Tables),
+    NewTabRequests = table_req_replace_player(TableModule, TablePid, PlayerId, UserInfo, TableId, SeatNum, TabRequests),
+    case TableStateName of
+        ?TABLE_STATE_IN_PROGRESS when not IsBot->
+            ?INFO("TRN_PAIRED <~p> User ~p is a real player <~p> and he was registered in the middle of the round."
+                  "So deduct some quota.", [GameId, UserId, PlayerId]),
+            deduct_quota(GameId, GameType, GameMode, Amount, MulFactor, [UserId]);
+        _ -> do_nothing
+    end,
+
+    update_gproc(GameId, GameType, CommonParams, NewPlayers2),
+    {next_state, StateName, StateData#state{players = NewPlayers2,
+                                            seats = NewSeats,
+                                            player_id_counter = PlayerId + 1,
+                                            tab_requests = NewTabRequests,
+                                            reg_requests = NewRegRequests}}.
+
+
+reg_new_player(UserInfo, TableId, SeatNum, From, StateName,
+               #state{game_id = GameId, players = Players, tables = Tables,
+                      game_type = GameType, seats = Seats, player_id_counter = PlayerId,
+                      tab_requests = TabRequests, reg_requests = RegRequests,
+                      table_module = TableModule, common_params = CommonParams,
+                      tables_num = TablesNum, seats_per_table = SeatsPerTable
+                     } = StateData) ->
+    {SeatNum, NewPlayers, NewSeats} =
+        register_new_player(UserInfo, TableId, Players, Seats, PlayerId),
+    TablePid = get_table_pid(TableId, Tables),
+    NewTabRequests = table_req_replace_player(TableModule, TablePid, PlayerId, UserInfo,
+                                              TableId, SeatNum, TabRequests),
+    NewRegRequests = dict:store(PlayerId, From, RegRequests),
+    update_gproc(GameId, GameType, CommonParams, NewPlayers),
+    NewStateData = StateData#state{reg_requests = NewRegRequests, tab_requests = NewTabRequests,
+                                   players = NewPlayers, seats = NewSeats,
+                                   player_id_counter = PlayerId + 1},
+    EnoughPlayers = enough_players(NewSeats, TablesNum*SeatsPerTable),
+    if StateName == ?STATE_EMPTY_SEATS_FILLING andalso EnoughPlayers ->
+           ?INFO("TRN_PAIRED <~p> It's enough players registered to start the game. "
+                 "Initiating the procedure.", [GameId]),
+           start_tour(NewStateData);
+       true ->
+           ?INFO("TRN_PAIRED <~p> Not enough players registered to start the game. "
+                 "Waiting for more registrations.", [GameId]),
+           {next_state, StateName, NewStateData}
+    end.
+
+%% register_new_player(UserInfo, TableId, Players, Seats, PlayerId) -> {SeatNum, NewPlayers, NewSeats}
+register_new_player(UserInfo, TableId, Players, Seats, PlayerId) ->
+    #'PlayerInfo'{id = UserId, robot = Bot} = UserInfo,
+    [#seat{seat_num = SeatNum} |_] = find_free_seats(TableId, Seats),
+    NewSeats = set_seat(TableId, SeatNum, PlayerId, Bot, _RegByTable = false,
+                        _Connected = false, _Free = false, Seats),
+    NewPlayers = store_player(#player{id = PlayerId, user_id = UserId,
+                                      user_info = UserInfo, is_bot = Bot}, Players),
+    {SeatNum, NewPlayers, NewSeats}.
+
+%% replace_by_bots(DisconnectedSeats, GameId, BotModule, Players, Seats, PlayerIdCounter) ->
+%%                                       {Replacements, NewPlayers, NewSeats, NewPlayerIdCounter}
+%% Types: Disconnected = [#seat{}]
+%%        Replacements = [{PlayerId, UserInfo, TableId, SeatNum}]
+replace_by_bots(DisconnectedSeats, GameId, BotModule, Players, Seats, PlayerIdCounter) ->
+    F = fun(#seat{player_id = PlayerId,
+                  table = TableId,
+                  seat_num = SeatNum}, {RAcc, PAcc, SAcc, Counter}) ->
+                NewPAcc1 = del_player(PlayerId, PAcc),
+                NewSAcc = set_seat(TableId, SeatNum, _PlayerId = Counter, _Bot = true,
+                                   _RegByTable = false, _Connected = false, _Free = false, SAcc),
+                #'PlayerInfo'{id = UserId} = UserInfo = spawn_bot(GameId, BotModule),
+                NewPAcc = store_player(#player{id = Counter, user_id = UserId,
+                                               user_info = UserInfo, is_bot = true}, NewPAcc1),
+                NewRAcc = [{Counter, UserInfo, TableId, SeatNum} | RAcc],
+                {NewRAcc, NewPAcc, NewSAcc, Counter + 1}
+        end,
+    lists:foldl(F, {[], Players, Seats, PlayerIdCounter}, DisconnectedSeats).
+
+
+enough_players(Seats, Threshold) ->
+    NonEmptySeats = find_non_free_seats(Seats),
+    length(NonEmptySeats) >= Threshold.
+
+
+update_gproc(GameId, GameType, CommonParams, Players) ->
+    Users = [if Bot -> robot; true -> UId end || #player{user_id = UId, is_bot = Bot}
+                                                            <- players_to_list(Players)],
+    DeclRec = create_decl_rec(GameType, CommonParams, GameId, Users),
+    gproc:set_value({p,l,self()}, DeclRec).
+
+%% prepare_players_for_new_tour(InitialPoints, Players) -> [{PlayerId, UserInfo, Points}]
+prepare_players_for_new_tour(InitialPoints, Players) ->
+    [{PlayerId, UserInfo, InitialPoints}
+     || #player{id = PlayerId, user_info = UserInfo} <- players_to_list(Players)].
+
+
+%% setup_tables(Players, TTable, TableIdCounter, GameId, TableParams) ->
+%%                              {Tables, Seats, NewTableIdCounter, CrRequests}
+%% Types: Players = [{PlayerId, UserInfo, Points} | empty]
+%%        TTable = [{Tour, [{UserId, CommonPos, Score, Status}]}]
+
+setup_tables(TableMod, Players, SeatsPerTable, TablesNum, TTable, Tour, Tours, TableIdCounter, GameId, TableParams) ->
+    EmptySeatsNum = SeatsPerTable*TablesNum - length(Players),
+    Players2 = lists:duplicate(EmptySeatsNum, empty) ++ Players,
+    SPlayers = shuffle(Players2),
+    Groups = split_by_num(SeatsPerTable, SPlayers),
+    F = fun(Group, {TAcc, SAcc, TableId, TCrRequestsAcc}) ->
+                TPlayers = prepare_table_players(Group),
+                TableParams2 = [{players, TPlayers}, {ttable, TTable}, {tour, Tour},
+                                {tours, Tours}, {parent, {?MODULE, self()}} | TableParams],
+                {ok, TabPid} = spawn_table(TableMod, GameId, TableId, TableParams2),
+                MonRef = erlang:monitor(process, TabPid),
+                NewTAcc = reg_table(TableId, TabPid, MonRef, TAcc),
+                NewSAcc = reg_seats(TableId, TPlayers, SAcc),
+                PlayersIds = [PlId || {PlId, _, _} <- Group, PlId =/= empty],
+                NewTCrRequestsAcc = dict:store(TableId, PlayersIds, TCrRequestsAcc),
+                {NewTAcc, NewSAcc, TableId + 1, NewTCrRequestsAcc}
+        end,
+    lists:foldl(F, {tables_init(), seats_init(), TableIdCounter, dict:new()}, Groups).
+
+reg_seats(TableId, TPlayers, Seats) ->
+    F = fun({{empty, _}, _PlayerInfo, SNum, _Points}, Acc) ->
+                set_seat(TableId, SNum, _PlId = undefined, _Bot = undefined, _Reg = false, _Conn = false, _Free = true, Acc);
+           ({PlId, #'PlayerInfo'{robot = Bot}, SNum, _Points}, Acc) ->
+                set_seat(TableId, SNum, PlId, Bot, _Reg = false, _Conn = false, _Free = false, Acc)
+        end,
+    lists:foldl(F, Seats, TPlayers).
+
+
+%% prepare_table_players(PlayersList) -> TPlayersList
+%% PlayersList = {PlayerId, UserInfo, Points} | empty
+%% TPlayersList = {APlayerId, UserInfo, SeatNum, Points}
+%%   APlayerId = PlayerId | {empty, integer()}
+prepare_table_players(PlayersList) ->
+    F = fun({PlayerId, UserInfo, Points}, SeatNum) ->
+              {{PlayerId, UserInfo, SeatNum, Points}, SeatNum+1};
+           (empty, SeatNum) ->
+              {{_PlayerId = {empty, SeatNum}, empty_seat_userinfo(SeatNum), SeatNum, _Points=0}, SeatNum+1}
+        end,
+    {TPlayers, _} = lists:mapfoldl(F, 1, PlayersList),
+    TPlayers.
+
+
+empty_seat_userinfo(Num) ->
+    #'PlayerInfo'{id         = list_to_binary(["empty_", integer_to_list(Num)]),
+                  login      = <<"">>,
+                  name       = <<"empty">>,
+                  surname    = <<" ">>,
+                  age        = 0,
+                  skill      = 0,
+                  score      = 0,
+                  avatar_url = null,
+                  robot      = true }.
+
+%% setup_players(Registrants, GameId, BotModule) -> {Players, PlayerIdCounter}
+setup_players(Registrants, GameId, BotModule) ->
+    F = fun(robot, {Acc, PlayerId}) ->
+                #'PlayerInfo'{id = UserId} = UserInfo = spawn_bot(GameId, BotModule),
+                NewAcc = store_player(#player{id = PlayerId, user_id = UserId,
+                                              user_info = UserInfo, is_bot = true}, Acc),
+                {NewAcc, PlayerId + 1};
+           (UserId, {Acc, PlayerId}) ->
+                {ok, UserInfo} = auth_server:get_user_info_by_user_id(UserId),
+                NewAcc = store_player(#player{id = PlayerId, user_id = UserId,
+                                              user_info = UserInfo, is_bot = false}, Acc),
+                {NewAcc, PlayerId + 1}
+        end,
+    lists:foldl(F, {players_init(), 1}, Registrants).
+
+
+%% finalize_tables_with_rejoin(TableModule, Tables) -> ok
+finalize_tables_with_rejoin(TableModule, Tables) ->
+    F = fun(#table{mon_ref = MonRef, pid = TablePid}) ->
+                erlang:demonitor(MonRef, [flush]),
+                send_to_table(TableModule, TablePid, rejoin_players),
+                send_to_table(TableModule, TablePid, stop)
+        end,
+    lists:foreach(F, tables_to_list(Tables)).
+
+%% finalize_tables_with_rejoin(TableModule, Tables) -> ok
+finalize_tables_with_disconnect(TableModule, Tables) ->
+    F = fun(#table{mon_ref = MonRef, pid = TablePid}) ->
+                erlang:demonitor(MonRef, [flush]),
+                send_to_table(TableModule, TablePid, disconnect_players),
+                send_to_table(TableModule, TablePid, stop)
+        end,
+    lists:foreach(F, tables_to_list(Tables)).
+
+
+%% req_replace_players(TableMod, Tables, Replacements, TabRequests) -> NewRequests
+req_replace_players(TableMod, Tables, Replacements, TabRequests) ->
+    F = fun({NewPlayerId, UserInfo, TableId, SeatNum}, Acc) ->
+                #table{pid = TablePid} = fetch_table(TableId, Tables),
+                table_req_replace_player(TableMod, TablePid, NewPlayerId, UserInfo, TableId, SeatNum, Acc)
+        end,
+    lists:foldl(F, TabRequests, Replacements).
+
+
+
+%% table_req_replace_player(TableMod, TablePid, PlayerId, UserInfo, TableId, SeatNum, TabRequests) -> NewRequests
+table_req_replace_player(TableMod, TablePid, PlayerId, UserInfo, TableId, SeatNum, TabRequests) ->
+    RequestId = make_ref(),
+    NewRequests = dict:store(RequestId, {replace_player, PlayerId, TableId, SeatNum}, TabRequests),
+    send_to_table(TableMod, TablePid, {replace_player, RequestId, UserInfo, PlayerId, SeatNum}),
+    NewRequests.
+
+
+remove_table_from_next_turn_wl(TableId, StateName,
+                               #state{game_id = GameId, cur_color = CurColor,
+                                      next_turn_wl = NextTurnWL, table_module = TableModule,
+                                      tables = Tables, tables_wl = TablesWL} = StateData) ->
+    ?INFO("TRN_PAIRED <~p> Removing table <~p> from the next turn waiting list: ~p.",
+          [GameId, TableId, NextTurnWL]),
+    NewNextTurnWL = lists:delete(TableId, NextTurnWL),
+    if NewNextTurnWL == [] ->
+           OppColor = opponent(CurColor),
+           {Die1, Die2} = roll(),
+           ?INFO("TRN_PAIRED <~p> The next turn waiting list is empty. Rolling dice for color ~p: ~p",
+                 [GameId, OppColor, [Die1, Die2]]),
+           [send_to_table(TableModule, TablePid, {action, {rolls, OppColor, Die1, Die2}}) ||
+              #table{pid = TablePid} <- tables_to_list(Tables)],
+           ?INFO("TRN_PAIRED <~p> New next turn waiting list is ~p",
+                 [GameId, TablesWL]),
+           {next_state, StateName, StateData#state{next_turn_wl = TablesWL,
+                                                   cur_color = OppColor}};
+       true ->
+           ?INFO("TRN_PAIRED <~p> The next turn waiting list is not empty:~p. Waiting for the rest players.",
+                 [GameId, NewNextTurnWL]),
+           {next_state, StateName, StateData#state{next_turn_wl = NewNextTurnWL}}
+    end.
+
+
+
+
+
+%% players_init() -> players()
+players_init() -> midict:new().
+
+%% store_player(#player{}, Players) -> NewPlayers
+store_player(#player{id =Id, user_id = UserId} = Player, Players) ->
+    midict:store(Id, Player, [{user_id, UserId}], Players).
+
+get_players_ids(Players) ->
+    [P#player.id || P <- players_to_list(Players)].
+
+get_player_by_user_id(UserId, Players) ->
+    case midict:geti(UserId, user_id, Players) of
+        [Player] -> {ok, Player};
+        [] -> error
+    end.
+
+%% players_to_list(Players) -> List
+players_to_list(Players) -> midict:all_values(Players).
+
+get_user_info(PlayerId, Players) ->
+    #player{user_info = UserInfo} = midict:fetch(PlayerId, Players),
+    UserInfo.
+
+get_user_id(PlayerId, Players) ->
+    #player{user_id = UserId} = midict:fetch(PlayerId, Players),
+    UserId.
+
+%% del_player(PlayerId, Players) -> NewPlayers
+del_player(PlayerId, Players) ->
+    midict:erase(PlayerId, Players).
+
+
+tables_init() -> midict:new().
+
+reg_table(TableId, Pid, MonRef, Tables) ->
+        reg_table(TableId, Pid, MonRef, _GlobalId = 0, _TableContext = undefined, Tables).
+
+reg_table(TableId, Pid, MonRef, GlobalId, TableContext, Tables) ->
+    Table = #table{id = TableId, pid = Pid, mon_ref = MonRef, global_id = GlobalId,
+                   state = initializing, context = TableContext},
+    store_table(Table, Tables).
+
+update_created_table(TableId, Relay, Tables) ->
+    Table = midict:fetch(TableId, Tables),
+    NewTable = Table#table{relay = Relay, state = ?TABLE_STATE_READY},
+    store_table(NewTable, Tables).
+
+store_table(#table{id = TableId, pid = Pid, mon_ref = MonRef, global_id = GlobalId} = Table, Tables) ->
+    midict:store(TableId, Table, [{pid, Pid}, {global_id, GlobalId}, {mon_ref, MonRef}], Tables).
+
+fetch_table(TableId, Tables) -> midict:fetch(TableId, Tables).
+
+
+%% get_table(TableId, Tables) -> {ok, Table} | error
+get_table(TableId, Tables) -> midict:find(TableId, Tables).
+
+get_table_pid(TabId, Tables) ->
+    #table{pid = TabPid} = midict:fetch(TabId, Tables),
+    TabPid.
+
+get_table_by_mon_ref(MonRef, Tables) ->
+    case midict:geti(MonRef, mon_ref, Tables) of
+        [Table] -> Table;
+        [] -> not_found
+    end.
+
+tables_to_list(Tables) -> midict:all_values(Tables).
+
+seats_init() -> midict:new().
+
+find_seats_by_player_id(PlayerId, Seats) ->
+    midict:geti(PlayerId, player_id, Seats).
+
+find_seats_by_table_id(TabId, Seats) ->
+    midict:geti(TabId, table_id, Seats).
+
+find_disconnected_seats(Seats) ->
+    midict:geti(false, connected, Seats).
+
+find_free_seats(TableId, Seats) ->
+    midict:geti(true, {free_at_tab, TableId}, Seats).
+
+find_free_seats(Seats) ->
+    midict:geti(true, free, Seats).
+
+find_non_free_seats(Seats) ->
+    midict:geti(false, free, Seats).
+
+find_registered_robot_seats(Seats) ->
+    [S || S = #seat{registered_by_table = true, is_bot = true} <- find_non_free_seats(Seats)].
+
+fetch_seat(TableId, SeatNum, Seats) -> midict:fetch({TableId, SeatNum}, Seats).
+
+%% set_seat(TabId, SeatNum, PlayerId, IsBot, RegByTable, Connected, Free, Seats) -> NewSeats
+%% PlayerId = integer()
+%% IsBot = RegByTable = Connected = undefined | boolean()
+set_seat(TabId, SeatNum, PlayerId, IsBot, RegByTable, Connected, Free, Seats) ->
+    Seat = #seat{table = TabId, seat_num = SeatNum, player_id = PlayerId, is_bot = IsBot,
+                 registered_by_table = RegByTable, connected = Connected, free = Free},
+    store_seat(Seat, Seats).
+
+
+update_seat_connect_status(TableId, SeatNum, ConnStatus, Seats) ->
+    Seat = midict:fetch({TableId, SeatNum}, Seats),
+    NewSeat = Seat#seat{connected = ConnStatus},
+    store_seat(NewSeat, Seats).
+
+
+store_seat(#seat{table = TabId, seat_num = SeatNum, player_id = PlayerId,
+                 is_bot = _IsBot, registered_by_table = _RegByTable,
+                 connected = Connected, free = Free} = Seat, Seats) ->
+    Indices = if Free == true ->
+                     [{table_id, TabId}, {free, true}, {{free_at_tab, TabId}, true}];
+                 true ->
+                     [{table_id, TabId}, {free, false}, {{free_at_tab, TabId}, false},
+                      {player_at_table, {PlayerId, TabId}}, {player_id, PlayerId},
+                      {{connected, TabId}, Connected}, {connected, Connected}]
+              end,
+    midict:store({TabId, SeatNum}, Seat, Indices, Seats).
+
+user_id_to_string(UserId) -> binary_to_list(UserId).
+
+shuffle(List) -> deck:to_list(deck:shuffle(deck:from_list(List))).
+
+split_by_num(Num, List) -> split_by_num(Num, List, []).
+
+split_by_num(_, [], Acc) -> lists:reverse(Acc);
+split_by_num(Num, List, Acc) ->
+    {Group, Rest} = lists:split(Num, List),
+    split_by_num(Num, Rest, [Group | Acc]).
+
+%% start_timer(Timeout) -> {TRef, Magic}
+start_timer(Timeout) ->
+    Magic = make_ref(),
+    TRef = erlang:send_after(Timeout, self(), {timeout, Magic}),
+    {TRef, Magic}.
+
+spawn_table(TableModule, GameId, TableId, Params) ->
+    TableModule:start(GameId, TableId, Params).
+
+send_to_table(TableModule, TablePid, Message) ->
+    TableModule:parent_message(TablePid, Message).
+
+
+get_param(ParamId, Params) ->
+    ?INFO("get_param/2 ParamId:~p", [ParamId]),
+    {_, Value} = lists:keyfind(ParamId, 1, Params),
+    Value.
+
+get_option(OptionId, Params, DefValue) ->
+    proplists:get_value(OptionId, Params, DefValue).
+
+
+create_decl_rec(GameType, CParams, GameId, Users) ->
+    #game_table{id              = GameId,
+                name            = proplists:get_value(table_name, CParams),
+%                gameid,
+%                trn_id,
+                game_type       = GameType,
+                rounds          = proplists:get_value(rounds, CParams),
+                sets            = proplists:get_value(sets, CParams),
+                owner           = proplists:get_value(owner, CParams),
+                timestamp       = now(),
+                users           = Users,
+                users_options   = proplists:get_value(users_options, CParams),
+                game_mode       = proplists:get_value(game_mode, CParams),
+%                game_options,
+                game_speed      = proplists:get_value(speed, CParams),
+                friends_only    = proplists:get_value(friends_only, CParams),
+%                invited_users = [],
+                private         = proplists:get_value(private, CParams),
+                feel_lucky = false,
+%                creator,
+                age_limit       = proplists:get_value(age, CParams),
+%                groups_only = [],
+                gender_limit    = proplists:get_value(gender_limit, CParams),
+%                location_limit = "",
+                paid_only       = proplists:get_value(paid_only, CParams),
+                deny_robots     = proplists:get_value(deny_robots, CParams),
+                slang           = proplists:get_value(slang, CParams),
+                deny_observers  = proplists:get_value(deny_observers, CParams),
+                gosterge_finish = proplists:get_value(gosterge_finish, CParams),
+                double_points   = proplists:get_value(double_points, CParams),
+                game_state      = started,
+                game_process    = self(),
+                game_module     = ?MODULE,
+                pointing_rules  = proplists:get_value(pointing_rules, CParams),
+                pointing_rules_ex = proplists:get_value(pointing_rules, CParams),
+%                game_process_monitor =
+%                tournament_type =
+                robots_replacement_allowed = proplists:get_value(robots_replacement_allowed, CParams)
+               }.
+
+%% spawn_bot(GameId, BotModule) -> UserInfo
+spawn_bot(GameId, BotModule) ->
+    {NPid, UserInfo} = create_robot(BotModule, GameId),
+    BotModule:join_game(NPid),
+    UserInfo.
+
+create_robot(BM, GameId) ->
+    UserInfo = auth_server:robot_credentials(),
+    {ok, NPid} = BM:start(self(), UserInfo, GameId),
+    {NPid, UserInfo}.
+
+competition_roll() ->
+    {Die1, Die2} = roll(),
+    if Die1 == Die2 -> competition_roll();
+       true -> {Die1, Die2}
+    end.
+
+roll() ->
+    Die1 = crypto:rand_uniform(1, 7),
+    Die2 = crypto:rand_uniform(1, 7),
+    {Die1, Die2}.
+
+opponent(?WHITE) -> ?BLACK;
+opponent(?BLACK) -> ?WHITE.
+
+ext_to_color(1) -> ?WHITE;
+ext_to_color(2) -> ?BLACK.

+ 300 - 0
apps/server/src/tavla/test_tavla.erl

@@ -0,0 +1,300 @@
+-module(test_tavla).
+-author('Maxim Sokhatsky <maxim@synrc.com').
+
+-include_lib("server/include/conf.hrl").
+-include_lib("server/include/log.hrl").
+-include_lib("server/include/kamf.hrl").
+-include_lib("server/include/requests.hrl").
+-include_lib("server/include/classes.hrl").
+-include_lib("server/include/game_tavla.hrl").
+-include_lib("server/include/settings.hrl").
+
+-export([start/0]).
+
+-define(BT, 30000).
+-define(FLUSH_DELAY, 10).
+-define(SIM_DELAY, 10).
+-define(TCM, tc).
+
+-record(state, {
+        conn,
+        gid,
+        uid,
+        mode,
+        hand,
+        pileh,
+        rounds,
+        current_round,
+        acker_fun = fun(_Event) -> continue end }).
+
+-record(bo, {pid, event, event_no, order }).
+
+start() ->
+    MultiOwner = self(),
+    process_flag(trap_exit, true),
+    Clients = [ proc_lib:spawn_link(fun() -> 
+                         fire_starter(MultiOwner, join_game_humans, normal)
+                 end) || _ <- lists:seq(1, 1) ],
+
+    [ wait_for(C) || C <- Clients ].
+
+fire_starter(MultiOwner, CreateMode, RevealMode) ->
+    process_flag(trap_exit, true),
+    Host = localhost,
+    Port = ?LISTEN_PORT,
+    Owner = self(),
+    Rematch = 1,
+    case CreateMode of
+
+         join_game_humans ->
+
+            Humans = [<<"maxim">>,<<"alice">>], % see authored users in auth_server.erl
+            {ok, GameId, _A} = game_manager:create_table(game_tavla, [{deny_robots,true},{rounds, 1}], Humans),
+            ?INFO("created table for Tavla Game: gameid ~p",[{GameId,_A}]),
+            Clients = [ proc_lib:spawn_link(fun() -> 
+                                 timer:sleep(crypto:rand_uniform(0, 10)),
+                                 attach_and_join(Owner, Host, Port, GameId, Id, Rematch, RevealMode)
+                        end) || Id <- Humans ],
+
+            ?INFO("Human Pids: ~p",[Clients]),
+            Orders = []
+
+    end,
+    conductor(Orders, Clients),
+    MultiOwner ! {self(), game_ended}.
+
+attach_and_join(Owner, Host, Port, GameId, OwnId, Rematch, Mode) ->
+    put(mode, Mode),
+    log(started),
+    S1 = ?TCM:connect(Host, Port),
+    TT = case OwnId of
+            <<"maxim">> -> ?TEST_TOKEN;
+            <<"alice">> -> ?TEST_TOKEN2
+    end,
+    #'PlayerInfo'{id = Id} = ?TCM:call_rpc(S1, #session_attach_debug{token = TT, id = OwnId}) ,
+    log(connected),
+    #'TableInfo'{game = _Atom} = ?TCM:call_rpc(S1, #join_game{game = GameId}) ,
+    State = #state{conn = S1, gid = GameId, uid = Id, acker_fun = standard_acker(Owner)},
+    play_round(State, Rematch),
+    log(finished),
+    ok = ?TCM:flush_events(?FLUSH_DELAY),
+    ok = ?TCM:close(S1).
+
+% in tavla game we have only rounds (from 1 to 7) withing one game
+
+play_round(State0, Rematch) ->
+    _Id = State0#state.uid,
+    State = init_tavla_roundroll(State0),
+    loop_and_restart(State),
+    play_round(State, Rematch).
+
+loop_and_restart(State) ->
+    LoopRes = tavla_client_loop(State#state{}),
+    ?INFO("Human LoopRes: ~p",[LoopRes]),
+    say_ready(State),
+    case LoopRes of
+        <<"done">> ->
+            ?INFO("ID: ~p, next action done", [State#state.uid]),
+            ok;
+        <<"next_set">> ->
+            ?INFO("ID: ~p, next action next_set", [State#state.uid]),
+            ok;
+        <<"next_round">> ->
+            ?INFO("ID: ~p, next action next_round", [State#state.uid]),
+            State1 = State#state{current_round = -1},
+            check_ack(State1, game_ended, fun loop_and_restart/1, [State1])
+    end.
+
+init_tavla_roundroll(State) ->
+    _GameId = State#state.gid,
+    receive
+        #'game_event'{event = <<"tavla_game_info">>, args = Args, game = _GI} ->
+            GT = proplists:get_value(game_type, Args),
+            Rounds = proplists:get_value(rounds, Args),
+            CurrentRound = proplists:get_value(current_round, Args),
+            log(game_info),
+            State#state{mode = GT, current_round = CurrentRound, rounds = Rounds}
+    after ?BT ->
+            ?INFO("ERROR: ~p", [{server_timeout, "game_event:tavla_game_info"}]),
+            erlang:error({server_timeout, "game_event:tavla_game_info"})
+    end.
+
+say_ready(State) ->
+    S1 = State#state.conn,
+    GameId = State#state.gid,
+    <<"ok">> = ?TCM:call_rpc(S1, #game_action{game = GameId, action = tavla_ready, args = []}) .
+
+tavla_client_loop(State) ->
+    Id = State#state.uid,
+    receive
+        #'game_event'{event = <<"tavla_next_turn">>, args = Args} ->
+            Timeout = crypto:rand_uniform(0, ?SIM_DELAY),
+            State1 = case {proplists:get_value(player, Args), proplists:get_value(can_challenge, Args)} of
+                         {Id, false} ->
+                             do_turn(State, Timeout);
+                         {_OtherId, _} ->
+                             State
+                     end,
+            tavla_client_loop(State1);
+        #'game_event'{event = <<"tavla_rolls">>} ->
+            tavla_client_loop(State);
+        #'game_event'{event = <<"tavla_moves">>} ->
+            tavla_client_loop(State);
+        #'game_event'{event = <<"tavla_ack">>} ->
+            tavla_client_loop(State);
+        #'game_event'{event = <<"tavla_game_ended">>, args = Args} ->
+            %okey_round_ended_checks(Args, State),
+            Reason = proplists:get_value(reason, Args),
+            NextAction = proplists:get_value(next_action, Args),
+            ?INFO("ID: ~p game ended, reason: ~p", [Id, Reason]),
+            NextAction;
+        #player_left{} ->
+            tavla_client_loop(State);
+        #'game_event'{event = <<"tavla_game_info">>, args = _Args} ->
+            erlang:error({protocol_breach, okey_game_info});
+        #'game_event'{event = <<"player_left">>} ->
+            tavla_client_loop(State);
+        _Msg ->
+            ?INFO("the msg: ~p", [_Msg]),
+            erlang:error({bot_received_unrecognized_message, _Msg})
+    after ?BT ->
+            log(server_timeouted),
+            erlang:error({server_timeout, "tavla_client_loop_timeout"})
+    end.
+
+do_turn(State, _Timeout) ->
+    RollAnswer = do_roll(State),
+    log(RollAnswer),
+    MoveAnswer = do_move(State),
+    log(MoveAnswer),
+    State.
+
+do_roll(State) ->
+    GameId = State#state.gid,
+    S = State#state.conn,
+    ZZZ = ?TCM:call_rpc(S, #game_action{
+                         game = GameId,
+                         action = tavla_roll,
+                         args = []}),
+    ?INFO("ID: ~p roll result: ~p", [State#state.uid, ZZZ]),
+    ok.
+
+do_move(State) ->
+    GameId = State#state.gid,
+    S = State#state.conn,
+    Moves = [{from,2},{to, 25}],
+    Player = State#state.uid,
+    ZZZ = ?TCM:call_rpc(S, #game_action{
+                         game = GameId,
+                         action = tavla_move,
+                         args = [ {moves, Moves}, {player, Player} ]}),
+    ?INFO("ID: ~p move result: ~p", [State#state.uid, ZZZ]),
+    ok.
+
+log(Msg) ->
+    ?TCM:log(Msg).
+
+wait_for(_C) ->
+    receive
+        {_, game_ended} ->
+            ?INFO("client: game ended");
+        {'EXIT', _, normal} = M ->
+            ?INFO("client: normal ending: ~p", [M]);
+        {'EXIT', _C, Reason} = M ->
+            ?INFO("client: FAILURE message: ~p", [{M,_C,Reason}]),
+            erlang:error(Reason);
+        M ->
+            ?INFO("client: unrecognized result: ~p", [M]),
+            erlang:error(M)
+    end.
+
+check_ack(State = #state{acker_fun = F}, Event, ContinueFun, Args) ->
+    A = F(Event),
+    case A of
+        continue ->
+            apply(ContinueFun, Args);
+        {sleep, T} ->
+            timer:sleep(T),
+            apply(ContinueFun, Args);
+        {do_and_continue, What, With} ->
+            apply(What, [State] ++ With),
+            apply(ContinueFun, Args);
+        stop ->
+            ?INFO("ID: ~p, Pid: ~p, check_ack. STOPPING THE BOT!!!", [State#state.uid, self()]),
+            erlang:error({terminate, acker_stop})
+    end.
+
+standard_acker(Owner) ->
+    Self = self(),
+    Ref = make_ref(),
+    fun(Event) ->
+            E = {event, Ref, Self, Event},
+            ?INFO("standard acker. ~p ! ~p", [Owner, E]),
+            Owner ! E,
+            receive
+                {ARef, Res} when Ref == ARef ->
+                    ?INFO("standard acker got '~p' answer on question ~p", [Res,{Owner, E}]),
+                    Res
+            end
+    end.
+
+update_events(Events, Pid, Event) ->
+    F = [ X || {APid, AEvent, _} = X <- Events, Pid == APid, Event == AEvent ],
+    case F of
+        [] ->
+            [{Pid, Event, 1} | Events];
+        [{_,_,C} = X] ->
+            L1 = lists:delete(X, Events),
+            [{Pid, Event, C+1} | L1]
+    end.
+
+match_event(Orders, Events, Pid, Event) ->
+    Z = [ X || {APid, AEvent, _} = X <- Events, Pid == APid, Event == AEvent ],
+    [{_, _, C}] = Z,
+    Matched = [ Y || Y = #bo{pid = P, event = E} <- Orders, P == Pid, E == Event ],
+    case Matched of
+        [BO = #bo{event_no = EN} | _] when EN == C ->
+            BO;
+        _X ->
+            false
+    end.
+
+conductor(Orders, Clients) ->
+    ?INFO("conductor init", []),
+    Pairs = lists:zip(lists:seq(1, length(Clients)), Clients),
+    Orders2 = lists:map(fun(O) ->
+                                {_, P} = lists:keyfind(O#bo.pid, 1, Pairs),
+                                O#bo{pid = P}
+                        end, Orders),
+    conductor(Orders2, Clients, []).
+conductor(_Orders, [], _Events) ->
+    ?INFO("conductor stop", []),
+    ok;
+conductor(Orders, Clients, Events) ->
+    receive
+        {event, Ref, Pid, Event} = _E ->
+            E2 = update_events(Events, Pid, Event),
+            Z = match_event(Orders, E2, Pid, Event),
+            case Z of
+                #bo{order = Order} ->
+                    Pid ! {Ref, Order};
+                _ ->
+                    Pid ! {Ref, continue}
+            end,
+            conductor(Orders, Clients, E2);
+        {C, game_ended} ->
+            ?INFO("conductor: game ended", []),
+            conductor(Orders, lists:delete(C, Clients), Events);
+        {'EXIT', C, normal} = M ->
+            ?INFO("conductor: normal ending: ~p", [M]),
+            conductor(Orders, lists:delete(C, Clients), Events);
+        {'EXIT', C, {terminate, acker_stop}} = M ->
+            ?INFO("conductor: removing client ~p because of ~p", [C, M]),
+            conductor(Orders, lists:delete(C, Clients), Events);
+        {'EXIT', _C, Reason} = M ->
+            ?INFO("conductor: FAILURE message: ~p", [{M,_C,Reason}]),
+            erlang:error(Reason);
+        M ->
+            ?INFO("conductor: unrecognized msg from client: ~p", [M]),
+            erlang:error(M)
+    end.

+ 89 - 0
apps/server/src/tavla_sup.erl

@@ -0,0 +1,89 @@
+-module(tavla_sup).
+-behaviour(supervisor).
+-export([start_link/0, stop/0]).
+-include_lib("server/include/conf.hrl").
+-include_lib("server/include/log.hrl").
+-export([init/1, start/0, start_game/3]).
+-define(SERVER, ?MODULE).
+
+-define(CROWD_STANDALONE_1PL_NUM, 15).
+
+start() -> supervisor:start({local, ?SERVER}, ?MODULE, []).
+start_link() -> supervisor:start_link({local, ?SERVER}, ?MODULE, []).
+stop() -> exit(?SERVER, shutdown).
+start_game(Mod,Par,GameId) -> 
+    ?INFO("TAVLA SUP STAR CHILD"),
+%%    Restart = transient,
+    Restart = temporary,
+    Shutdown = 200,
+    ChildSpec = {GameId, {Mod, start_link, Par}, Restart, Shutdown, worker, [Mod]},
+    supervisor:start_child(?MODULE,ChildSpec).
+
+init([]) ->
+    ?INFO("TAVLA SUP STARTED"),
+    RestartStrategy = one_for_one,
+    MaxRestarts = 1,
+    MaxSecondsBetweenRestarts = 600,
+    SupFlags = {RestartStrategy, MaxRestarts, MaxSecondsBetweenRestarts},
+    Specs = tavla_standalone_specs(?CROWD_STANDALONE_1PL_NUM, 1),
+    {ok, {SupFlags, Specs}}.
+
+tavla_standalone_specs(GamesNum, VirtUsersPerTable) ->
+    VirtualUsers = nsg_crowd_lib:virtual_users(),
+    if length(VirtualUsers) < VirtUsersPerTable ->
+           [];
+       true ->
+           F = fun(_) ->
+                       GameId = game_manager:gen_game_id(),
+                       GameName = "Tavla/Crowd game - " ++ erlang:integer_to_list(GameId),
+                       Users = nsg_crowd_lib:random_users(VirtUsersPerTable, VirtualUsers),
+%%%                    Users = [robot],
+                       TableParams = [
+                                      {table_name, ""},
+                                      {mult_factor, 1},
+                                      {slang_allowed, false},
+                                      {observers_allowed, false},
+                                      {tournament_type, standalone},
+                                      {round_timeout, infinity},
+                                      {set_timeout, infinity},
+                                      {speed, fast},
+                                      {game_mode, standard},
+                                      {rounds, 3},
+                                      {next_series_confirmation, no_exit},
+                                      {pause_mode, normal},
+                                      {social_actions_enabled, true},
+                                      {tables_num, 1}
+                                     ],
+                       CommonParams = [{speed, fast},
+                                       {rounds, 3},
+                                       {double_points, 1},
+                                       {game_mode,standard},
+                                       {speed, normal},
+                                       {slang, false},
+                                       {observers, false},
+                                       {owner,"maxim"}
+                                      ],
+                       Params = [{game, game_tavla},
+                                 {game_mode, standard},
+                                 {game_name, GameName},
+                                 {seats, 2},
+                                 {registrants, Users},
+                                 {initial_points, 0},
+                                 {quota_per_round, 1},
+                                 {kakush_for_winners, 1},
+                                 {kakush_for_loser, 1},
+                                 {win_game_points, 1},
+                                 {mul_factor, 1},
+                                 {table_module, game_tavla_ng_table},
+                                 {bot_module, game_tavla_bot},
+                                 {bots_replacement_mode, enabled},
+                                 {table_params, TableParams},
+                                 {common_params, CommonParams}
+                                ],
+                       {GameId,
+                        {nsg_trn_standalone, start_link, [GameId, Params]},
+                        _Restart = permanent, _Shutdown = 2000, worker, [nsg_trn_standalone]}
+               end,
+           lists:map(F, lists:seq(1, GamesNum))
+    end.
+

+ 214 - 0
apps/server/src/tests.erl

@@ -0,0 +1,214 @@
+-module(tests).
+
+-include_lib("eunit/include/eunit.hrl").
+-include_lib("server/include/conf.hrl").
+-include_lib("server/include/log.hrl").
+-include_lib("server/include/kamf.hrl").
+-include_lib("server/include/requests.hrl").
+-include_lib("server/include/classes.hrl").
+-include_lib("server/include/game_okey.hrl").
+
+-export([setup/0, cleanup/1, tests_on/0]).
+
+-define(BT, 3000000).
+
+-define(SIM_TIMEOUT, 300).
+
+%% ===================================================================
+%% ===================================================================
+%% Tests
+%% ===================================================================
+%% ===================================================================
+
+stateful_test_() ->
+    {setup,
+     fun setup/0,
+     fun cleanup/1,
+     ?_test(
+        begin
+            send_receive_object_t(),
+            policy_file_plain_t(),
+            policy_file_t(),
+            policy_file_complex_t(),
+            async_server_rpc_test_t(),
+            encoding_client_failure_t(),
+            ok
+        end)
+    }.
+
+session_attach_test_() ->
+    {setup,
+     fun setup/0,
+     fun cleanup/1,
+     ?_test(
+        begin
+            session_attach_fail_t(),
+            session_attach_ok_t(),
+            ok
+        end)
+    }.
+
+message_read_write_test() ->
+    A = #game_event{event = <<"game_ended">>, game = 1, args = [{good_shot, true}, {awarded_points, 42}, {reason, <<"out_of_toshes">>}, {results, #'OkeyGameResults'{}}]},
+    {A, A} = {A, ?AMF:object_to_record(?AMF:record_to_object(A))}.
+
+setup() ->
+    ok = app_util:start(kakaconfig),
+    kakaconfig:temp_db(),
+    meck:new(kakaconfig, [passthrough]),
+    meck:expect(kakaconfig, get, fun
+                                     ([debug, is_test], _) -> {ok, true};
+                                     (okey_robot_delay, _) -> {ok, 1};
+                                     (Key, X) -> meck:passthrough([Key, X])
+                                 end),
+
+    meck:new(okey_stats, [passthrough]),
+    meck:expect(okey_stats, add_game, fun(R) -> ?INFO("saving game: ~p", [R]), {1, 1} end),
+    meck:expect(okey_stats, get_skill, fun(_) -> {ok, 500} end),
+    meck:expect(okey_stats, get_game_points, fun(_, _) -> {ok, [{game_points, 100},
+                                                                {finished_with_okey, 5},
+                                                                {finished_with_8_tashes, 1}]} end),
+    ok = app_util:start(kaka_id_generator),
+    ok = app_util:start(kaka_utils),
+    ok = app_util:start(kaka_auth_server),
+    ok = app_util:start(kaka_game_session),
+    ok = app_util:start(kaka_game_manager),
+    ok = app_util:start(kaka_matchmaking),
+    ok = app_util:start(kaka_conn).
+
+cleanup(_State) ->
+    application:stop(kaka_conn),
+    application:stop(kaka_matchmaking),
+    application:stop(kaka_game_manager),
+    application:stop(kaka_game_session),
+    application:stop(kaka_auth_server),
+    application:stop(kaka_utils),
+    application:stop(kaka_id_generator),
+    meck:unload(kakaconfig),
+    meck:unload(okey_stats),
+    db:stop(),
+    ok.
+
+tests_on() ->
+    meck:new(kakaconfig, [passthrough]),
+    meck:expect(kakaconfig, get, fun
+                                     ([debug, is_test], _) -> {ok, true};
+                                     (Key, X) -> meck:passthrough([Key, X])
+                                 end).
+
+session_attach_fail_t() ->
+    Token = "some fake token",
+    S1 = tc:connect(),
+    {error, <<"invalid_token">>} = tc:call_rpc(S1, #session_attach{token = Token}).
+
+session_attach_ok_t() ->
+    Token = "some proper token",
+    UserId = "mr_customer",
+    auth_server:store_token(Token, UserId),
+    S1 = tc:connect(),
+    Res = tc:call_rpc(S1, #session_attach{token = Token}),
+    #'PlayerInfo'{id = Id, login = Login} = Res,
+    true = is_binary(Login),
+    true = is_binary(Id),
+    tc:call_rpc(S1, #logout{}).
+
+encoding_client_failure_t() ->
+    Socket = tc:connect(),
+    Answer = tc:call_rpc_raw(Socket, 'getobjecttypefromserver_zzz', []),
+    ?INFO("encoding_client_failure_t: ~p", [Answer]),
+    {error, _Reason} = Answer.
+
+
+
+async_server_rpc_test_t() ->
+    TestClient = spawn_link(fun() -> tc:async_test_client_loop() end),
+    timer:sleep(100),
+    [{_, Pid, _, _}] = nsm_conn_app:get_clients(),
+
+    Self = self(),
+    Ref = make_ref(),
+    spawn(fun() ->
+                  Msg = #slowping{},
+                  <<"slowpong">> = conn_kamf_worker:send_request(Pid, Msg),
+                  Self ! Ref
+          end),
+
+    Message = #fastping{},
+    <<"fastpong">> = conn_kamf_worker:send_request(Pid, Message),
+    receive
+        Ref -> ok
+    after 5000 -> erlang:error(timeout) end,
+
+    TestClient ! exit.
+
+message_embedding_test() ->
+    A = {object,<<"KamfResponse">>,
+         [{id,1001},
+          {result,{object,
+                   <<"PlayerInfo">>,
+                   [{login,"oldman"},
+                    {name,"Ernest"},
+                    {surname,"Hemingway"}]}}]},
+    Bin = ?AMF:encode(A),
+    {A, _} = ?AMF:decode(Bin).
+
+kamf_conversion_test() ->
+    AR = {object,
+          <<"KamfRequest">>,
+          [{id,0},
+           {method,
+            <<"session_attach">>},
+           {args,
+            [{<<"token">>,
+              "EBAs6dg2Xw6XuCdg8qiPmlBLgYJ6N4Ti0P+oGpWgYz4NW4nBBUzTe/wAuLYtPnjFpsjCExxSpV78fipmsPxcf+NGy+QKIM6rmVJhpnIlKf0bpFNuGaAPjZAWthhGO8nZ0V8UnA=="}]}]},
+    BR = {object,
+          <<"KamfRequest">>,
+          [{id,0},
+           {method,
+            <<"session_attach">>},
+           {args,
+            [{<<"token">>,
+              <<"EBAs6dg2Xw6XuCdg8qiPmlBLgYJ6N4Ti0P+oGpWgYz4NW4nBBUzTe/wAuLYtPnjFpsjCExxSpV78fipmsPxcf+NGy+QKIM6rmVJhpnIlKf0bpFNuGaAPjZAWthhGO8nZ0V8UnA==">>}]}]},
+    kamf:object_to_record(AR),
+    kamf:object_to_record(BR),
+    ok.
+
+send_receive_object_t() ->
+    Socket = tc:connect(),
+    L = #'some_named_object'{name1 = <<"value1">>, name2 = 2, name3 = 3.0},
+    R = tc:call_rpc(Socket, #'getobjecttypefromserver'{}),
+    {L, L} = {L, R},
+    <<"Hello, guys! Have a nice day :)">> = tc:call_rpc(Socket, #'getstringtypefromserver'{}),
+    42 = tc:call_rpc(Socket, #'getintegertypefromserver'{}),
+    [_, {some_number, 42}, _, _] = tc:call_rpc(Socket, #'getmixedtypesfromserver'{}),
+    tc:close(Socket).
+
+policy_file_t() ->
+    {ok, Socket} = gen_tcp:connect(localhost, ?LISTEN_PORT, [{active, false}, binary, {packet, 0}]),
+    ok = gen_tcp:send(Socket, <<"<policy-file-request/>", 0>>),
+    {ok, Data} = gen_tcp:recv(Socket, 0),
+    Data = conn_kamf_worker:policy_file_text(),
+    ok = gen_tcp:send(Socket, <<?KAMF_MAGIC:48>>),
+    Sock = tc:connect(Socket),
+    #'some_named_object'{name1 = <<"value1">>, name2 = 2, name3 = 3.0} =
+        tc:call_rpc(Sock, #'getobjecttypefromserver'{}),
+    ok = tc:close(Sock).
+
+policy_file_complex_t() ->
+    {ok, Socket} = gen_tcp:connect(localhost, ?LISTEN_PORT, [{active, false}, binary, {packet, 0}]),
+    ok = gen_tcp:send(Socket, <<"<policy-file-request/>", 0, ?KAMF_MAGIC:48>>),
+    {ok, Data} = gen_tcp:recv(Socket, 0),
+    Data = conn_kamf_worker:policy_file_text(),
+    Sock = tc:connect(Socket),
+    #'some_named_object'{name1 = <<"value1">>, name2 = 2, name3 = 3.0} =
+        tc:call_rpc(Sock, #'getobjecttypefromserver'{}),
+    ok = tc:close(Sock).
+
+
+policy_file_plain_t() ->
+    {ok, Socket} = gen_tcp:connect(localhost, ?LISTEN_PORT, [{active, false}, binary, {packet, 0}]),
+    ok = gen_tcp:send(Socket, <<"<policy-file-request/>", 0>>),
+    {ok, Data} = gen_tcp:recv(Socket, 0),
+    Data = conn_kamf_worker:policy_file_text(),
+    ok = gen_tcp:close(Socket).
+

+ 1 - 0
apps/server/src/timestamp

@@ -0,0 +1 @@
+Fri Mar 22 02:34:07 EET 2013