standalone.erl 59 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177
  1. %%% -------------------------------------------------------------------
  2. %%% Author : Sergei Polkovnikov <serge.polkovnikov@gmail.com>
  3. %%% Description : The "Stand alone table" logic
  4. %%%
  5. %%% Created : Nov 19, 2012
  6. %%% -------------------------------------------------------------------
  7. %%% Terms explanation:
  8. %%% GameId - uniq identifier of the tournament/game. Type: integer().
  9. %%% PlayerId - registration number of a player in the tournament/game. Type: integer()
  10. %%% UserId - cross system identifier of a physical user. Type: binary() (or string()?).
  11. %%% TableId - uniq identifier of a table in the tournament/game. Used by the
  12. %%% tournament/game logic. Type: integer().
  13. %%% TableGlobalId - uniq identifier of a table in the system. Can be used
  14. %%% to refer to a table directly - without pointing to a tournament/game.
  15. %%% Type: integer()
  16. -module(standalone).
  17. -behaviour(gen_fsm).
  18. %% --------------------------------------------------------------------
  19. %% Include files
  20. %% --------------------------------------------------------------------
  21. -include_lib("server/include/basic_types.hrl").
  22. -include_lib("db/include/table.hrl").
  23. -include_lib("db/include/scoring.hrl").
  24. -include_lib("db/include/transaction.hrl").
  25. -include_lib("eunit/include/eunit.hrl").
  26. %% --------------------------------------------------------------------
  27. %% External exports
  28. -export([start/2, start_link/2, reg/2]).
  29. %% gen_fsm callbacks
  30. -export([init/1, handle_event/3, handle_sync_event/4, handle_info/3, terminate/3, code_change/4]).
  31. -export([table_message/3, client_message/2, client_request/2, client_request/3]).
  32. -record(state,
  33. {%% Static values
  34. game_id :: pos_integer(),
  35. trn_id :: term(),
  36. game :: atom(),
  37. game_mode :: atom(),
  38. game_name :: string(),
  39. seats_per_table :: integer(),
  40. params :: proplists:proplist(),
  41. table_module :: atom(),
  42. bot_module :: atom(),
  43. quota_per_round :: integer(),
  44. kakush_for_winners :: integer(),
  45. kakush_for_loser :: integer(),
  46. win_game_points :: integer(),
  47. mul_factor :: integer(),
  48. registrants :: [robot | binary()],
  49. initial_points :: integer(),
  50. bots_replacement_mode :: enabled | disabled,
  51. common_params :: proplists:proplist(),
  52. %% Dinamic values
  53. players, %% The register of tournament players
  54. tables, %% The register of tournament tables
  55. seats, %% Stores relation between players and tables seats
  56. table_id_counter :: pos_integer(),
  57. player_id_counter :: pos_integer(),
  58. cur_table :: pos_integer(),
  59. tour :: pos_integer(),
  60. cr_tab_requests :: dict(), %% {TableId, PlayersIds}
  61. reg_requests :: dict(), %% {PlayerId, From}
  62. tab_requests :: dict(), %% {RequestId, RequestContext}
  63. timer :: undefined | reference(),
  64. timer_magic :: undefined | reference(),
  65. tables_wl :: list(), %% Tables waiting list
  66. tables_results :: list() %% [{TableId, TableResult}]
  67. }).
  68. -record(player,
  69. {
  70. id :: pos_integer(),
  71. user_id,
  72. user_info :: #'PlayerInfo'{},
  73. is_bot :: boolean()
  74. }).
  75. -record(table,
  76. {
  77. id :: pos_integer(),
  78. global_id :: pos_integer(),
  79. pid :: pid(),
  80. relay :: {atom(), pid()}, %% {RelayMod, RelayPid}
  81. mon_ref :: reference(),
  82. state :: initializing | ready | in_process | finished,
  83. context :: term(), %% Context term of a table. For failover proposes.
  84. timer :: reference()
  85. }).
  86. -record(seat,
  87. {
  88. table :: pos_integer(),
  89. seat_num :: integer(),
  90. player_id :: undefined | pos_integer(),
  91. is_bot :: undefined | boolean(),
  92. registered_by_table :: undefined | boolean(),
  93. connected :: undefined | boolean(),
  94. free :: boolean()
  95. }).
  96. -define(STATE_INIT, state_init).
  97. -define(STATE_WAITING_FOR_TABLES, state_waiting_for_tables).
  98. -define(STATE_EMPTY_SEATS_FILLING, state_empty_seats_filling).
  99. -define(STATE_WAITING_FOR_PLAYERS, state_waiting_for_players).
  100. -define(STATE_SET_PROCESSING, state_set_processing).
  101. -define(STATE_SET_FINISHED, state_set_finished).
  102. -define(STATE_SHOW_SET_RESULT, state_show_set_result).
  103. -define(STATE_FINISHED, state_finished).
  104. -define(TOURNAMENT_TYPE, standalone).
  105. -define(TABLE_STATE_INITIALIZING, initializing).
  106. -define(TABLE_STATE_READY, ready).
  107. -define(TABLE_STATE_IN_PROGRESS, in_progress).
  108. -define(TABLE_STATE_FINISHED, finished).
  109. -define(WAITING_PLAYERS_TIMEOUT, 1000) . %% Time between a table was created and start of first round
  110. -define(REST_TIMEOUT, 5000). %% Time between a round finish and start of a new one
  111. -define(SHOW_SET_RESULT_TIMEOUT, 15000). %% Time between a set finish and start of a new one
  112. -define(SHOW_TOURNAMENT_RESULT_TIMEOUT, 15000). %% Time between last tour result showing and the tournament finish
  113. %% ====================================================================
  114. %% External functions
  115. %% ====================================================================
  116. start(GameId, Params) ->
  117. gen_fsm:start(?MODULE, [GameId, Params, self()], []).
  118. start_link(GameId, Params) ->
  119. gen_fsm:start_link(?MODULE, [GameId, Params, self()], []).
  120. reg(Pid, User) ->
  121. client_request(Pid, {join, User}, 10000).
  122. table_message(Pid, TableId, Message) ->
  123. gen_fsm:send_all_state_event(Pid, {table_message, TableId, Message}).
  124. client_message(Pid, Message) ->
  125. gen_fsm:send_all_state_event(Pid, {client_message, Message}).
  126. client_request(Pid, Message) ->
  127. client_request(Pid, Message, 5000).
  128. client_request(Pid, Message, Timeout) ->
  129. gen_fsm:sync_send_all_state_event(Pid, {client_request, Message}, Timeout).
  130. %% ====================================================================
  131. %% Server functions
  132. %% ====================================================================
  133. init([GameId, Params, _Manager]) ->
  134. gas:info(?MODULE,"TRN_STANDALONE <~p> Init started.", [GameId]),
  135. Registrants = get_param(registrants, Params),
  136. SeatsPerTable = get_param(seats, Params),
  137. Game = get_param(game, Params),
  138. GameMode = get_param(game_mode, Params),
  139. GameName = get_param(game_name, Params),
  140. QuotaPerRound = get_param(quota_per_round, Params),
  141. KakushForWinners = get_param(kakush_for_winners, Params),
  142. KakushForLoser = get_param(kakush_for_loser, Params),
  143. WinGamePoints = get_param(win_game_points, Params),
  144. MulFactor = get_param(mul_factor, Params),
  145. TableParams = get_param(table_params, Params),
  146. TableModule = get_param(table_module, Params),
  147. BotModule = get_param(bot_module, Params),
  148. InitialPoints = get_param(initial_points, Params),
  149. BotsReplacementMode = get_param(bots_replacement_mode, Params),
  150. CommonParams = get_param(common_params, Params),
  151. gas:info(?MODULE,"TRN_STANDALONE <~p> All parameteres are read. Send the directive to start the game.", [GameId]),
  152. gen_fsm:send_all_state_event(self(), go),
  153. {ok, ?STATE_INIT, #state{game_id = GameId,
  154. game = Game,
  155. game_mode = GameMode,
  156. game_name = GameName,
  157. seats_per_table = SeatsPerTable,
  158. params = TableParams,
  159. table_module = TableModule,
  160. bot_module = BotModule,
  161. quota_per_round = QuotaPerRound,
  162. kakush_for_winners = KakushForWinners,
  163. kakush_for_loser = KakushForLoser,
  164. win_game_points = WinGamePoints,
  165. mul_factor = MulFactor,
  166. bots_replacement_mode = BotsReplacementMode,
  167. registrants = Registrants,
  168. initial_points = InitialPoints,
  169. table_id_counter = 1,
  170. common_params = CommonParams
  171. }}.
  172. %%===================================================================
  173. handle_event(go, ?STATE_INIT, #state{game_id = GameId, game = GameType,
  174. registrants = Registrants, bot_module = BotModule,
  175. common_params = CommonParams} = StateData) ->
  176. gas:info(?MODULE,"TRN_STANDALONE <~p> Received the directive to start the game.", [GameId]),
  177. DeclRec = create_decl_rec(GameType, CommonParams, GameId, Registrants),
  178. gproc:reg({p,l,self()}, DeclRec),
  179. {Players, PlayerIdCounter} = setup_players(Registrants, GameId, BotModule),
  180. NewStateData = StateData#state{players = Players,
  181. player_id_counter = PlayerIdCounter},
  182. init_tour(1, NewStateData);
  183. handle_event({client_message, Message}, StateName, #state{game_id = GameId} = StateData) ->
  184. gas:info(?MODULE,"TRN_STANDALONE <~p> Received the message from a client: ~p.", [GameId, Message]),
  185. handle_client_message(Message, StateName, StateData);
  186. handle_event({table_message, TableId, Message}, StateName, #state{game_id = GameId} = StateData) ->
  187. gas:info(?MODULE,"TRN_STANDALONE <~p> Received the message from table <~p>: ~p.", [GameId, TableId, Message]),
  188. handle_table_message(TableId, Message, StateName, StateData);
  189. handle_event(Message, StateName, #state{game_id = GameId} = StateData) ->
  190. gas:info(?MODULE,"TRN_STANDALONE <~p> Unhandled message(event) received in state <~p>: ~p.",
  191. [GameId, StateName, Message]),
  192. {next_state, StateName, StateData}.
  193. handle_sync_event({client_request, Request}, From, StateName, #state{game_id = GameId} = StateData) ->
  194. gas:info(?MODULE,"TRN_STANDALONE <~p> Received the request from a client: ~p.", [GameId, Request]),
  195. handle_client_request(Request, From, StateName, StateData);
  196. handle_sync_event(Request, From, StateName, #state{game_id = GameId} = StateData) ->
  197. gas:info(?MODULE,"TRN_STANDALONE <~p> Unhandled request(event) received in state <~p> from ~p: ~p.",
  198. [GameId, StateName, From, Request]),
  199. {reply, {error, unknown_request}, StateName, StateData}.
  200. %%===================================================================
  201. handle_info({'DOWN', MonRef, process, _Pid, _}, StateName,
  202. #state{game_id = GameId, tables = Tables} = StateData) ->
  203. case get_table_by_mon_ref(MonRef, Tables) of
  204. #table{id = TableId} ->
  205. gas:info(?MODULE,"TRN_STANDALONE <~p> Table <~p> is down. Stopping", [GameId, TableId]),
  206. %% TODO: More smart handling (failover) needed
  207. {stop, {one_of_tables_down, TableId}, StateData};
  208. not_found ->
  209. {next_state, StateName, StateData}
  210. end;
  211. handle_info({rest_timeout, TableId}, ?STATE_SET_PROCESSING = StateName,
  212. #state{game_id = GameId, game = GameType, game_mode = GameMode,
  213. quota_per_round = Amount, mul_factor = MulFactor, tables = Tables,
  214. players = Players, seats = Seats, cur_table = TableId, bot_module = BotModule,
  215. player_id_counter = PlayerIdCounter, tab_requests = Requests,
  216. table_module = TableMod, common_params = CommonParams} = StateData) ->
  217. gas:info(?MODULE,"TRN_STANDALONE <~p> Time to start new round for table <~p>.", [GameId, TableId]),
  218. Disconnected = find_disconnected_players(TableId, Seats),
  219. ConnectedRealPlayers = [PlayerId || #player{id = PlayerId, is_bot = false} <- players_to_list(Players),
  220. not lists:member(PlayerId, Disconnected)],
  221. case ConnectedRealPlayers of
  222. [] -> %% Finish game
  223. gas:info(?MODULE,"TRN_STANDALONE <~p> No real players left in table <~p>. "
  224. "Stopping the game.", [GameId, TableId]),
  225. finalize_tables_with_disconnect(TableMod, Tables),
  226. {stop, normal, StateData#state{tables = [], seats = []}};
  227. _ -> %% Replace disconnected players by bots
  228. gas:info(?MODULE,"TRN_STANDALONE <~p> Initiating new round at table <~p>.", [GameId, TableId]),
  229. {Replacements, NewPlayers, NewSeats, NewPlayerIdCounter} =
  230. replace_by_bots(Disconnected, GameId, BotModule, TableId, Players, Seats, PlayerIdCounter),
  231. #table{pid = TablePid} = Table = fetch_table(TableId, Tables),
  232. NewTables = store_table(Table#table{state = ?TABLE_STATE_IN_PROGRESS}, Tables),
  233. RealUsersIds = [UserId || #player{user_id = UserId, is_bot = false} <- players_to_list(NewPlayers)],
  234. deduct_quota(GameId, GameType, GameMode, Amount, MulFactor, RealUsersIds),
  235. NewRequests = table_req_replace_players(TableMod, TablePid, TableId, Replacements, Requests),
  236. send_to_table(TableMod, TablePid, start_round),
  237. Users = [if Bot -> robot; true -> UserId end || #player{user_id = UserId, is_bot = Bot} <- players_to_list(NewPlayers)],
  238. DeclRec = create_decl_rec(GameType, CommonParams, GameId, Users),
  239. gproc:set_value({p,l,self()}, DeclRec),
  240. {next_state, StateName, StateData#state{tables = NewTables, players = NewPlayers, seats = NewSeats,
  241. tab_requests = NewRequests, player_id_counter = NewPlayerIdCounter}}
  242. end;
  243. handle_info({rest_timeout, TableId}, ?STATE_SET_FINISHED,
  244. #state{game_id = GameId, game = GameType, game_mode = GameMode,
  245. game_name = GameName, tables_results = TablesResults, tables = Tables,
  246. players = Players, cur_table = TableId, table_module = TableMod,
  247. kakush_for_winners = KakushForWinners, kakush_for_loser = KakushForLoser,
  248. win_game_points = WinGamePoints, mul_factor = MulFactor} = StateData) ->
  249. gas:info(?MODULE,"TRN_STANDALONE <~p> Time to determinate set results (table: <~p>).", [GameId, TableId]),
  250. #table{pid = TablePid} = fetch_table(TableId, Tables),
  251. {_, TableScore} = lists:keyfind(TableId, 1, TablesResults),
  252. SeriesResult = series_result(TableScore),
  253. gas:info(?MODULE,"TRN_STANDALONE <~p> Set result: ~p", [GameId, SeriesResult]),
  254. send_to_table(TableMod, TablePid, {show_series_result, SeriesResult}),
  255. Points = calc_players_prize_points(SeriesResult, KakushForWinners, KakushForLoser, WinGamePoints, MulFactor, Players),
  256. UsersPrizePoints = prepare_users_prize_points(Points, Players),
  257. gas:info(?MODULE,"TRN_STANDALONE <~p> Prizes: ~p", [GameId, UsersPrizePoints]),
  258. add_points_to_accounts(UsersPrizePoints, GameId, GameType, GameMode, MulFactor),
  259. EndsNotePoints = prepare_ends_note_points(SeriesResult, Points, Players),
  260. send_ends_note(GameName, GameType, EndsNotePoints),
  261. {TRef, Magic} = start_timer(?SHOW_SET_RESULT_TIMEOUT),
  262. {next_state, ?STATE_SHOW_SET_RESULT, StateData#state{timer = TRef,
  263. timer_magic = Magic}};
  264. handle_info({timeout, Magic}, ?STATE_WAITING_FOR_PLAYERS,
  265. #state{timer_magic = Magic, game_id = GameId} = StateData) ->
  266. gas:info(?MODULE,"TRN_STANDALONE <~p> Time to start new set.", [GameId]),
  267. start_set(StateData);
  268. handle_info({timeout, Magic}, ?STATE_SHOW_SET_RESULT,
  269. #state{timer_magic = Magic, game_id = GameId} = StateData) ->
  270. gas:info(?MODULE,"TRN_STANDALONE <~p> Time to finalize the game.", [GameId]),
  271. finalize_tournament(StateData);
  272. handle_info({timeout, Magic}, ?STATE_FINISHED,
  273. #state{timer_magic = Magic, tables = Tables, game_id = GameId,
  274. table_module = TableMod} = StateData) ->
  275. gas:info(?MODULE,"TRN_STANDALONE <~p> Time to stopping the game.", [GameId]),
  276. finalize_tables_with_disconnect(TableMod, Tables),
  277. {stop, normal, StateData#state{tables = [], seats = []}};
  278. handle_info(Message, StateName, #state{game_id = GameId} = StateData) ->
  279. gas:info(?MODULE,"TRN_STANDALONE <~p> Unhandled message(info) received in state <~p>: ~p.",
  280. [GameId, StateName, Message]),
  281. {next_state, StateName, StateData}.
  282. %%===================================================================
  283. terminate(_Reason, _StateName, #state{game_id=GameId}=_StatData) ->
  284. gas:info(?MODULE,"TRN_STANDALONE <~p> Shutting down at state: <~p>. Reason: ~p",
  285. [GameId, _StateName, _Reason]),
  286. ok.
  287. %%===================================================================
  288. code_change(_OldVsn, StateName, StateData, _Extra) ->
  289. {ok, StateName, StateData}.
  290. %% --------------------------------------------------------------------
  291. %%% Internal functions
  292. %% --------------------------------------------------------------------
  293. handle_client_message(Message, StateName, #state{game_id = GameId} = StateData) ->
  294. gas:error(?MODULE,"TRN_STANDALONE <~p> Unhandled client message received in "
  295. "state <~p>: ~p.", [GameId, StateName, Message]),
  296. {next_state, StateName, StateData}.
  297. %%===================================================================
  298. handle_table_message(TableId, {player_connected, PlayerId},
  299. StateName,
  300. #state{seats = Seats} = StateData) ->
  301. case find_seats_by_player_id(PlayerId, Seats) of
  302. [#seat{seat_num = SeatNum}] ->
  303. NewSeats = update_seat_connect_status(TableId, SeatNum, true, Seats),
  304. {next_state, StateName, StateData#state{seats = NewSeats}};
  305. [] -> %% Ignoring the message
  306. {next_state, StateName, StateData}
  307. end;
  308. handle_table_message(TableId, {player_disconnected, PlayerId},
  309. StateName, #state{seats = Seats} = StateData) ->
  310. case find_seats_by_player_id(PlayerId, Seats) of
  311. [#seat{seat_num = SeatNum}] ->
  312. NewSeats = update_seat_connect_status(TableId, SeatNum, false, Seats),
  313. {next_state, StateName, StateData#state{seats = NewSeats}};
  314. [] -> %% Ignoring the message
  315. {next_state, StateName, StateData}
  316. end;
  317. handle_table_message(TableId, {table_created, Relay},
  318. ?STATE_WAITING_FOR_TABLES,
  319. #state{tables = Tables, seats = Seats, table_module = TableMod,
  320. cr_tab_requests = TCrRequests, reg_requests = RegRequests,
  321. seats_per_table = SeatsPerTable} = StateData) ->
  322. TabInitPlayers = dict:fetch(TableId, TCrRequests),
  323. NewTCrRequests = dict:erase(TableId, TCrRequests),
  324. %% Update status of players
  325. TabSeats = find_seats_by_table_id(TableId, Seats),
  326. F = fun(#seat{player_id = PlayerId} = S, Acc) ->
  327. case lists:member(PlayerId, TabInitPlayers) of
  328. true -> store_seat(S#seat{registered_by_table = true}, Acc);
  329. false -> Acc
  330. end
  331. end,
  332. NewSeats = lists:foldl(F, Seats, TabSeats),
  333. %% Process delayed join/registration requests
  334. TablePid = get_table_pid(TableId, Tables),
  335. F2 = fun(PlayerId, Acc) ->
  336. case dict:find(PlayerId, Acc) of
  337. {ok, From} ->
  338. gen_fsm:reply(From, {ok, {PlayerId, Relay, {TableMod, TablePid}}}),
  339. dict:erase(PlayerId, Acc);
  340. error -> Acc
  341. end
  342. end,
  343. NewRegRequests = lists:foldl(F2, RegRequests, TabInitPlayers),
  344. NewTables = update_created_table(TableId, Relay, Tables),
  345. case is_table_full_enought(TableId, NewSeats, SeatsPerTable) of
  346. true ->
  347. {TRef, Magic} = start_timer(?WAITING_PLAYERS_TIMEOUT),
  348. {next_state, ?STATE_WAITING_FOR_PLAYERS,
  349. StateData#state{tables = NewTables, seats = NewSeats, cr_tab_requests = NewTCrRequests,
  350. reg_requests = NewRegRequests, timer = TRef, timer_magic = Magic}};
  351. false ->
  352. {next_state, ?STATE_EMPTY_SEATS_FILLING,
  353. StateData#state{tables = NewTables, seats = NewSeats, cr_tab_requests = NewTCrRequests,
  354. reg_requests = NewRegRequests}}
  355. end;
  356. handle_table_message(TableId, {round_finished, TableContext, _RoundScore, _TotalScore},
  357. ?STATE_SET_PROCESSING,
  358. #state{game_id = GameId, tables = Tables, table_module = TableMod} = StateData) ->
  359. gas:info(?MODULE,"TRN_STANDALONE <~p> Round is finished (table: <~p>).", [GameId, TableId]),
  360. #table{pid = TablePid} = Table = fetch_table(TableId, Tables),
  361. TRef = erlang:send_after(?REST_TIMEOUT, self(), {rest_timeout, TableId}),
  362. NewTable = Table#table{context = TableContext, state = ?TABLE_STATE_FINISHED, timer = TRef},
  363. NewTables = store_table(NewTable, Tables),
  364. send_to_table(TableMod, TablePid, show_round_result),
  365. gas:info(?MODULE,"TRN_STANDALONE <~p> Waiting some time (~p secs) before start of next round.",
  366. [GameId, ?REST_TIMEOUT div 1000]),
  367. {next_state, ?STATE_SET_PROCESSING, StateData#state{tables = NewTables}};
  368. handle_table_message(TableId, {game_finished, TableContext, _RoundScore, TableScore},
  369. ?STATE_SET_PROCESSING,
  370. #state{game_id = GameId, tables = Tables, table_module = TableMod} = StateData) ->
  371. gas:info(?MODULE,"TRN_STANDALONE <~p> Last round of the set is finished (table: <~p>).", [GameId, TableId]),
  372. TablesResults = [{TableId, TableScore}],
  373. #table{pid = TablePid} = Table = fetch_table(TableId, Tables),
  374. TRef = erlang:send_after(?REST_TIMEOUT, self(), {rest_timeout, TableId}),
  375. NewTable = Table#table{context = TableContext, state = ?TABLE_STATE_FINISHED, timer = TRef},
  376. NewTables = store_table(NewTable, Tables),
  377. send_to_table(TableMod, TablePid, show_round_result),
  378. gas:info(?MODULE,"TRN_STANDALONE <~p> Waiting some time (~p secs) before the set results calculation.",
  379. [GameId, ?REST_TIMEOUT div 1000]),
  380. {next_state, ?STATE_SET_FINISHED, StateData#state{tables = NewTables,
  381. tables_results = TablesResults}};
  382. handle_table_message(TableId, {response, RequestId, Response},
  383. StateName,
  384. #state{game_id = GameId, tab_requests = TabRequests} = StateData) ->
  385. NewTabRequests = dict:erase(RequestId, TabRequests),
  386. case dict:find(RequestId, TabRequests) of
  387. {ok, ReqContext} ->
  388. gas:info(?MODULE,"TRN_STANDALONE <~p> The a response received from table <~p>. "
  389. "RequestId: ~p. Request context: ~p. Response: ~p",
  390. [GameId, TableId, RequestId, ReqContext, Response]),
  391. handle_table_response(TableId, ReqContext, Response, StateName,
  392. StateData#state{tab_requests = NewTabRequests});
  393. error ->
  394. gas:error(?MODULE,"TRN_STANDALONE <~p> Table <~p> sent a response for unknown request. "
  395. "RequestId: ~p. Response", []),
  396. {next_state, StateName, StateData#state{tab_requests = NewTabRequests}}
  397. end;
  398. handle_table_message(TableId, Message, StateName, #state{game_id = GameId} = StateData) ->
  399. gas:error(?MODULE,"TRN_STANDALONE <~p> Unhandled table message received from table <~p> in "
  400. "state <~p>: ~p.", [GameId, TableId, StateName, Message]),
  401. {next_state, StateName, StateData}.
  402. %%===================================================================
  403. %% handle_table_response(_TableId, {register_player, PlayerId, TableId, SeatNum}, ok = _Response,
  404. %% StateName,
  405. %% #state{reg_requests = RegRequests, seats = Seats,
  406. %% tables = Tables} = StateData) ->
  407. %% Seat = fetch_seat(TableId, SeatNum, Seats),
  408. %% NewSeats = store_seat(Seat#seat{registered_by_table = true}, Seats),
  409. %% %% Send response to a client for a delayed request
  410. %% NewRegRequests =
  411. %% case dict:find(PlayerId, RegRequests) of
  412. %% {ok, From} ->
  413. %% #table{relay = Relay, pid = TablePid} = fetch_table(TableId, Tables),
  414. %% gen_fsm:reply(From, {ok, {PlayerId, Relay, {?TAB_MOD, TablePid}}}),
  415. %% dict:erase(PlayerId, RegRequests);
  416. %% error -> RegRequests
  417. %% end,
  418. %% {next_state, StateName, StateData#state{seats = NewSeats,
  419. %% reg_requests = NewRegRequests}};
  420. handle_table_response(_TableId, {replace_player, PlayerId, TableId, SeatNum}, ok = _Response,
  421. StateName,
  422. #state{reg_requests = RegRequests, seats = Seats,
  423. tables = Tables, table_module = TableMod} = StateData) ->
  424. Seat = fetch_seat(TableId, SeatNum, Seats),
  425. NewSeats = store_seat(Seat#seat{registered_by_table = true}, Seats),
  426. %% Send response to a client for a delayed request
  427. NewRegRequests =
  428. case dict:find(PlayerId, RegRequests) of
  429. {ok, From} ->
  430. #table{relay = Relay, pid = TablePid} = fetch_table(TableId, Tables),
  431. gen_fsm:reply(From, {ok, {PlayerId, Relay, {TableMod, TablePid}}}),
  432. dict:erase(PlayerId, RegRequests);
  433. error -> RegRequests
  434. end,
  435. {next_state, StateName, StateData#state{seats = NewSeats,
  436. reg_requests = NewRegRequests}};
  437. handle_table_response(TableId, RequestContext, Response, StateName,
  438. #state{game_id = GameId} = StateData) ->
  439. gas:error(?MODULE,"TRN_STANDALONE <~p> Unhandled 'table response' received from table <~p> "
  440. "in state <~p>. Request context: ~p. Response: ~p.",
  441. [GameId, TableId, StateName, RequestContext, Response]),
  442. {next_state, StateName, StateData}.
  443. %%===================================================================
  444. handle_client_request({join, UserInfo}, From, StateName,
  445. #state{game_id = GameId, reg_requests = RegRequests,
  446. seats = Seats, players=Players, tables = Tables,
  447. table_module = TableMod, cur_table = TableId,
  448. bots_replacement_mode = BotsReplacementMode} = StateData) ->
  449. #'PlayerInfo'{id = UserId, robot = _IsBot} = UserInfo,
  450. gas:info(?MODULE,"TRN_STANDALONE <~p> The 'Join' request received from user: ~p.", [GameId, UserId]),
  451. if StateName == ?STATE_FINISHED ->
  452. gas:info(?MODULE,"TRN_STANDALONE <~p> The game is finished. "
  453. "Reject to join user ~p.", [GameId, UserId]),
  454. {reply, {error, finished}, StateName, StateData};
  455. true -> %% Game in progress. Find a seat for the user
  456. case get_player_by_user_id(UserId, Players) of
  457. {ok, #player{id = PlayerId}} -> %% The user is a registered member of the game (player)
  458. gas:info(?MODULE,"TRN_STANDALONE <~p> User ~p is a registered member of the game. "
  459. "Allow to join.", [GameId, UserId]),
  460. [#seat{table = TableId, registered_by_table = RegByTable}] = find_seats_by_player_id(PlayerId, Seats),
  461. case RegByTable of
  462. false -> %% The player is not registered by the table yet
  463. gas:info(?MODULE,"TRN_STANDALONE <~p> User ~p not yet regirested by the table. "
  464. "Add the request to the waiting pool.", [GameId, UserId]),
  465. NewRegRequests = dict:store(PlayerId, From, RegRequests),
  466. {next_state, StateName, StateData#state{reg_requests = NewRegRequests}};
  467. _ -> %% The player is registered by the table. Return the table requisites
  468. gas:info(?MODULE,"TRN_STANDALONE <~p> Return the join response for player ~p immediately.",
  469. [GameId, UserId]),
  470. #table{relay = Relay, pid = TPid} = fetch_table(TableId, Tables),
  471. {reply, {ok, {PlayerId, Relay, {TableMod, TPid}}}, StateName, StateData}
  472. end;
  473. error -> %% Not a member yet
  474. gas:info(?MODULE,"TRN_STANDALONE <~p> User ~p is not a member of the game.", [GameId, UserId]),
  475. case find_free_seats(TableId, Seats) of
  476. [] when BotsReplacementMode == disabled ->
  477. gas:info(?MODULE,"TRN_STANDALONE <~p> No free seats for user ~p. Robots replacement is disabled. "
  478. "Reject to join.", [GameId, UserId]),
  479. {reply, {error, not_allowed}, StateName, StateData};
  480. [] when BotsReplacementMode == enabled ->
  481. gas:info(?MODULE,"TRN_STANDALONE <~p> No free seats for user ~p. Robots replacement is enabled. "
  482. "Tring to find a robot for replace.", [GameId, UserId]),
  483. case find_registered_robot_seats(TableId, Seats) of
  484. [] ->
  485. gas:info(?MODULE,"TRN_STANDALONE <~p> No robots for replacement by user ~p. "
  486. "Reject to join.", [GameId, UserId]),
  487. {reply, {error, not_allowed}, StateName, StateData};
  488. [#seat{seat_num = SeatNum, player_id = OldPlayerId} | _] ->
  489. gas:info(?MODULE,"TRN_STANDALONE <~p> There is a robot for replacement by user ~p. "
  490. "Registering.", [GameId, UserId]),
  491. reg_player_with_replace(UserInfo, TableId, SeatNum, OldPlayerId, From, StateName, StateData)
  492. end;
  493. [#seat{seat_num = SeatNum} | _] ->
  494. gas:info(?MODULE,"TRN_STANDALONE <~p> There is a free seat for user ~p. "
  495. "Registering.", [GameId, UserId]),
  496. reg_new_player(UserInfo, TableId, SeatNum, From, StateName, StateData)
  497. end
  498. end
  499. end;
  500. handle_client_request(Request, From, StateName, #state{game_id = GameId} = StateData) ->
  501. gas:error(?MODULE,"TRN_STANDALONE <~p> Unhandled client request received from ~p in "
  502. "state <~p>: ~p.", [GameId, From, StateName, Request]),
  503. {reply, {error, unexpected_request}, StateName, StateData}.
  504. %%===================================================================
  505. init_tour(Tour, #state{game_id = GameId, seats_per_table = SeatsPerTable,
  506. params = TableParams, players = Players, table_module = TableMod,
  507. table_id_counter = TableIdCounter, tables = OldTables,
  508. initial_points = InitialPoints} = StateData) ->
  509. gas:info(?MODULE,"TRN_STANDALONE <~p> Initializing tour <~p>...", [GameId, Tour]),
  510. PlayersList = prepare_players_for_new_tour(InitialPoints, Players),
  511. {NewTables, Seats, NewTableIdCounter, CrRequests} =
  512. setup_tables(TableMod, PlayersList, SeatsPerTable, undefined, Tour,
  513. undefined, TableIdCounter, GameId, TableParams),
  514. if Tour > 1 -> finalize_tables_with_rejoin(TableMod, OldTables);
  515. true -> do_nothing
  516. end,
  517. gas:info(?MODULE,"TRN_STANDALONE <~p> Initializing of tour <~p> is finished. "
  518. "Waiting creating confirmations from the tours' tables...",
  519. [GameId, Tour]),
  520. {next_state, ?STATE_WAITING_FOR_TABLES, StateData#state{tables = NewTables,
  521. seats = Seats,
  522. table_id_counter = NewTableIdCounter,
  523. cur_table = TableIdCounter,
  524. tour = Tour,
  525. cr_tab_requests = CrRequests,
  526. reg_requests = dict:new(),
  527. tab_requests = dict:new(),
  528. tables_results = []
  529. }}.
  530. start_set(#state{game_id = GameId, game = Game, game_mode = GameMode, mul_factor = MulFactor,
  531. quota_per_round = Amount, tour = Tour, tables = Tables, players = Players,
  532. table_module = TableMod} = StateData) ->
  533. gas:info(?MODULE,"TRN_STANDALONE <~p> Starting tour <~p>...", [GameId, Tour]),
  534. UsersIds = [UserId || #player{user_id = UserId, is_bot = false} <- players_to_list(Players)],
  535. deduct_quota(GameId, Game, GameMode, Amount, MulFactor, UsersIds),
  536. TablesList = tables_to_list(Tables),
  537. [send_to_table(TableMod, Pid, start_round) || #table{pid = Pid} <- TablesList],
  538. F = fun(Table, Acc) ->
  539. store_table(Table#table{state = ?TABLE_STATE_IN_PROGRESS}, Acc)
  540. end,
  541. NewTables = lists:foldl(F, Tables, TablesList),
  542. WL = [T#table.id || T <- TablesList],
  543. gas:info(?MODULE,"TRN_STANDALONE <~p> Tour <~p> is started. Processing...",
  544. [GameId, Tour]),
  545. {next_state, ?STATE_SET_PROCESSING, StateData#state{tables = NewTables,
  546. tables_wl = WL}}.
  547. finalize_tournament(#state{game_id = GameId} = StateData) ->
  548. gas:info(?MODULE,"TRN_STANDALONE <~p> Finalizing the game...", [GameId]),
  549. %% TODO: Real finalization needed
  550. {TRef, Magic} = start_timer(?SHOW_TOURNAMENT_RESULT_TIMEOUT),
  551. gas:info(?MODULE,"TRN_STANDALONE <~p> The game is finalized. "
  552. "Waiting some time (~p secs) before continue...",
  553. [GameId, ?SHOW_TOURNAMENT_RESULT_TIMEOUT div 1000]),
  554. {next_state, ?STATE_FINISHED, StateData#state{timer = TRef, timer_magic = Magic}}.
  555. reg_player_with_replace(UserInfo, TableId, SeatNum, OldPlayerId, From, StateName,
  556. #state{game_id = GameId, players = Players, tables = Tables,
  557. game = GameType, seats = Seats, player_id_counter = PlayerId,
  558. tab_requests = TabRequests, reg_requests = RegRequests,
  559. table_module = TableModule, common_params = CommonParams} = StateData) ->
  560. #'PlayerInfo'{id = UserId, robot = IsBot} = UserInfo,
  561. NewPlayers = del_player(OldPlayerId, Players),
  562. NewPlayers2 = store_player(#player{id = PlayerId, user_id = UserId,
  563. user_info = UserInfo, is_bot = IsBot}, NewPlayers),
  564. gas:info(?MODULE,"TRN_STANDALONE <~p> User ~p registered as player <~p>.", [GameId, UserId, PlayerId]),
  565. NewSeats = set_seat(TableId, SeatNum, PlayerId, _Bot = false, _RegByTable = false,
  566. _Connected = false, _Free = false, Seats),
  567. gas:info(?MODULE,"TRN_STANDALONE <~p> User ~p assigned to seat <~p> of table <~p>.", [GameId, UserId, SeatNum, TableId]),
  568. NewRegRequests = dict:store(PlayerId, From, RegRequests),
  569. TablePid = get_table_pid(TableId, Tables),
  570. NewTabRequests = table_req_replace_player(TableModule, TablePid, PlayerId, UserInfo, TableId, SeatNum, TabRequests),
  571. Users = [if Bot -> robot; true -> UId end || #player{user_id = UId, is_bot = Bot}
  572. <- players_to_list(NewPlayers2)],
  573. DeclRec = create_decl_rec(GameType,CommonParams, GameId, Users),
  574. gproc:set_value({p,l,self()}, DeclRec),
  575. {next_state, StateName, StateData#state{players = NewPlayers2,
  576. seats = NewSeats,
  577. player_id_counter = PlayerId + 1,
  578. tab_requests = NewTabRequests,
  579. reg_requests = NewRegRequests}}.
  580. reg_new_player(UserInfo, TableId, SeatNum, From, StateName,
  581. #state{game_id = GameId, players = Players, tables = Tables,
  582. game = GameType, seats = Seats, player_id_counter = PlayerId,
  583. tab_requests = TabRequests, reg_requests = RegRequests,
  584. table_module = TableModule, common_params = CommonParams,
  585. seats_per_table = SeatsPerTable} = StateData) ->
  586. {SeatNum, NewPlayers, NewSeats} =
  587. register_new_player(UserInfo, TableId, Players, Seats, PlayerId),
  588. TablePid = get_table_pid(TableId, Tables),
  589. NewTabRequests = table_req_replace_player(TableModule, TablePid, PlayerId, UserInfo,
  590. TableId, SeatNum, TabRequests),
  591. NewRegRequests = dict:store(PlayerId, From, RegRequests),
  592. Users = [if Bot -> robot; true -> UId end || #player{user_id = UId, is_bot = Bot}
  593. <- players_to_list(NewPlayers)],
  594. DeclRec = create_decl_rec(GameType, CommonParams, GameId, Users),
  595. gproc:set_value({p,l,self()}, DeclRec),
  596. TableIsFull = is_table_full_enought(TableId, NewSeats, SeatsPerTable),
  597. NewStateData = StateData#state{reg_requests = NewRegRequests, tab_requests = NewTabRequests,
  598. players = NewPlayers, seats = NewSeats,
  599. player_id_counter = PlayerId + 1},
  600. if StateName == ?STATE_EMPTY_SEATS_FILLING andalso TableIsFull ->
  601. gas:info(?MODULE,"TRN_STANDALONE <~p> It's enough players registered to start the game. "
  602. "Initiating the procedure.", [GameId]),
  603. start_set(NewStateData);
  604. true ->
  605. gas:info(?MODULE,"TRN_STANDALONE <~p> Not enough players registered to start the game. "
  606. "Waiting for more registrations.", [GameId]),
  607. {next_state, StateName, NewStateData}
  608. end.
  609. %% series_result(TableResult) -> WithPlaceAndStatus
  610. %% Types: TableResult = [{PlayerId, Points}]
  611. %% WithPlaceAndStatus = [{PlayerId, Place, Points, Status}]
  612. %% Status = winner | looser
  613. series_result(TableResult) ->
  614. {_, PointsList} = lists:unzip(TableResult),
  615. Max = lists:max(PointsList),
  616. F = fun({Pl, Points}, {CurPlace, CurPos, LastPoints}) ->
  617. if Points == LastPoints ->
  618. {{Pl, CurPlace, Points, if Points == Max -> winner; true -> looser end},
  619. {CurPlace, CurPos + 1, Points}};
  620. true ->
  621. {{Pl, CurPos, Points, looser},
  622. {CurPos, CurPos + 1, Points}}
  623. end
  624. end,
  625. {WithPlaceAndStatus, _} = lists:mapfoldl(F, {1, 1, Max}, lists:reverse(lists:keysort(2, TableResult))),
  626. WithPlaceAndStatus.
  627. is_table_full_enought(TableId, Seats, EnoughtNumber) ->
  628. NonEmptySeats = find_non_free_seats(TableId, Seats),
  629. length(NonEmptySeats) >= EnoughtNumber.
  630. %% prepare_players_for_new_tour(InitialPoints, Players) -> [{PlayerId, UserInfo, Points}]
  631. prepare_players_for_new_tour(InitialPoints, Players) ->
  632. [{PlayerId, UserInfo, InitialPoints}
  633. || #player{id = PlayerId, user_info = UserInfo} <- players_to_list(Players)].
  634. %% setup_tables(TableMod, Players, SeatsPerTable, TTable, TableIdCounter, GameId, TableParams) ->
  635. %% {Tables, Seats, NewTableIdCounter, CrRequests}
  636. %% Types: Players = {PlayerId, UserInfo, Points}
  637. %% TTable = [{Tour, [{UserId, CommonPos, Score, Status}]}]
  638. setup_tables(TableMod, Players, SeatsPerTable, TTable, Tour, Tours, TableId, GameId, TableParams) ->
  639. EmptySeatsNum = SeatsPerTable - length(Players),
  640. Players2 = lists:duplicate(EmptySeatsNum, empty) ++ Players,
  641. SPlayers = shuffle(Players2),
  642. {TPlayers, _} = lists:mapfoldl(fun({PlayerId, UserInfo, Points}, SeatNum) ->
  643. {{PlayerId, UserInfo, SeatNum, Points}, SeatNum+1};
  644. (empty, SeatNum) ->
  645. {{_PlayerId = {empty, SeatNum+1}, empty_seat_userinfo(SeatNum+1), SeatNum, _Points=0}, SeatNum+1}
  646. end, 1, SPlayers),
  647. gas:info(?MODULE,"EmptySeatsNum:~p SPlayers: ~p TPlayers:~p", [EmptySeatsNum, SPlayers, TPlayers]),
  648. TableParams2 = [{players, TPlayers}, {ttable, TTable}, {tour, Tour},
  649. {tours, Tours}, {parent, {?MODULE, self()}} | TableParams],
  650. {ok, TabPid} = spawn_table(TableMod, GameId, TableId, TableParams2),
  651. MonRef = erlang:monitor(process, TabPid),
  652. Tables = reg_table(TableId, TabPid, MonRef, _GlTableId = 0, _Context = undefined, tables_init()),
  653. F2 = fun({PlId, #'PlayerInfo'{robot = Bot}, SNum, _Points}, Acc) ->
  654. case PlId of
  655. {empty,_} -> set_seat(TableId, SNum, _PlId = undefined, _Bot = undefined, _Reg = false, _Conn = false, _Free = true, Acc);
  656. _ -> set_seat(TableId, SNum, PlId, Bot, _Reg = false, _Conn = false, _Free = false, Acc)
  657. end
  658. end,
  659. Seats = lists:foldl(F2, seats_init(), TPlayers),
  660. PlayersIds = [PlayerId || {PlayerId, _, _} <- SPlayers],
  661. TCrRequests = dict:store(TableId, PlayersIds, dict:new()),
  662. {Tables, Seats, TableId + 1, TCrRequests}.
  663. empty_seat_userinfo(Num) ->
  664. #'PlayerInfo'{id = list_to_binary(["empty_", integer_to_list(Num)]),
  665. login = <<"">>,
  666. name = <<"empty">>,
  667. surname = <<" ">>,
  668. age = 0,
  669. skill = 0,
  670. score = 0,
  671. avatar_url = null,
  672. robot = true }.
  673. %% setup_players(Registrants, GameId, BotModule) -> {Players, PlayerIdCounter}
  674. setup_players(Registrants, GameId, BotModule) ->
  675. F = fun(robot, {Acc, PlayerId}) ->
  676. #'PlayerInfo'{id = UserId} = UserInfo = spawn_bot(GameId, BotModule),
  677. NewAcc = store_player(#player{id = PlayerId, user_id = UserId,
  678. user_info = UserInfo, is_bot = true}, Acc),
  679. {NewAcc, PlayerId + 1};
  680. (UserId, {Acc, PlayerId}) ->
  681. UserInfo = auth_server:get_user_info_by_user_id(UserId),
  682. NewAcc = store_player(#player{id = PlayerId, user_id = UserId,
  683. user_info = UserInfo, is_bot = false}, Acc),
  684. {NewAcc, PlayerId + 1}
  685. end,
  686. lists:foldl(F, {players_init(), 1}, Registrants).
  687. %% register_new_player(UserInfo, TableId, Players, Seats, PlayerId) -> {SeatNum, NewPlayers, NewSeats}
  688. register_new_player(UserInfo, TableId, Players, Seats, PlayerId) ->
  689. #'PlayerInfo'{id = UserId, robot = Bot} = UserInfo,
  690. [#seat{seat_num = SeatNum} |_] = find_free_seats(TableId, Seats),
  691. NewSeats = set_seat(TableId, SeatNum, PlayerId, Bot, _RegByTable = false,
  692. _Connected = false, _Free = false, Seats),
  693. NewPlayers = store_player(#player{id = PlayerId, user_id = UserId,
  694. user_info = UserInfo, is_bot = Bot}, Players),
  695. {SeatNum, NewPlayers, NewSeats}.
  696. %% replace_by_bots(Disconnected, GameId, BotModule, TableId, Players, Seats, PlayerIdCounter) ->
  697. %% {Replacements, NewPlayers, NewSeats, NewPlayerIdCounter}
  698. %% Types: Disconnected = [PlayerId]
  699. %% Replacements = [{PlayerId, UserInfo, SeatNum}]
  700. replace_by_bots(Disconnected, GameId, BotModule, TableId, Players, Seats, PlayerIdCounter) ->
  701. F = fun(PlayerId, {RAcc, PAcc, SAcc, Counter}) ->
  702. NewPAcc1 = del_player(PlayerId, PAcc),
  703. [#seat{seat_num = SeatNum}] = find_seats_by_player_id(PlayerId, TableId, SAcc),
  704. NewSAcc = set_seat(TableId, SeatNum, _PlayerId = Counter, _Bot = true,
  705. _RegByTable = false, _Connected = false, _Free = false, SAcc),
  706. #'PlayerInfo'{id = UserId} = UserInfo = spawn_bot(GameId, BotModule),
  707. NewPAcc = store_player(#player{id = Counter, user_id = UserId,
  708. user_info = UserInfo, is_bot = true}, NewPAcc1),
  709. NewRAcc = [{Counter, UserInfo, SeatNum} | RAcc],
  710. {NewRAcc, NewPAcc, NewSAcc, Counter + 1}
  711. end,
  712. lists:foldl(F, {[], Players, Seats, PlayerIdCounter}, Disconnected).
  713. %% finalize_tables_with_rejoin(TableMod, Tables) -> ok
  714. finalize_tables_with_rejoin(TableMod, Tables) ->
  715. F = fun(#table{mon_ref = MonRef, pid = TablePid}) ->
  716. erlang:demonitor(MonRef, [flush]),
  717. send_to_table(TableMod, TablePid, rejoin_players),
  718. send_to_table(TableMod, TablePid, stop)
  719. end,
  720. lists:foreach(F, tables_to_list(Tables)).
  721. %% finalize_tables_with_rejoin(TableMod, Tables) -> ok
  722. finalize_tables_with_disconnect(TableMod, Tables) ->
  723. F = fun(#table{mon_ref = MonRef, pid = TablePid}) ->
  724. erlang:demonitor(MonRef, [flush]),
  725. send_to_table(TableMod, TablePid, disconnect_players),
  726. send_to_table(TableMod, TablePid, stop)
  727. end,
  728. lists:foreach(F, tables_to_list(Tables)).
  729. %% table_req_replace_players(TableMod, TablePid, TableId, Replacements, TabRequests) -> NewRequests
  730. table_req_replace_players(TableMod, TablePid, TableId, Replacements, TabRequests) ->
  731. F = fun({NewPlayerId, UserInfo, SeatNum}, Acc) ->
  732. table_req_replace_player(TableMod, TablePid, NewPlayerId, UserInfo, TableId, SeatNum, Acc)
  733. end,
  734. lists:foldl(F, TabRequests, Replacements).
  735. %% table_req_replace_player(TableMod, TablePid, PlayerId, UserInfo, TableId, SeatNum, TabRequests) -> NewRequests
  736. table_req_replace_player(TableMod, TablePid, PlayerId, UserInfo, TableId, SeatNum, TabRequests) ->
  737. RequestId = make_ref(),
  738. NewRequests = dict:store(RequestId, {replace_player, PlayerId, TableId, SeatNum}, TabRequests),
  739. send_to_table(TableMod, TablePid, {replace_player, RequestId, UserInfo, PlayerId, SeatNum}),
  740. NewRequests.
  741. %% find_disconnected_players(TableId, Seats) -> PlayersIds
  742. find_disconnected_players(TableId, Seats) ->
  743. [PlayerId || #seat{player_id = PlayerId} <- find_disconnected_seats(TableId, Seats)].
  744. %% players_init() -> players()
  745. players_init() -> midict:new().
  746. %% store_player(#player{}, Players) -> NewPlayers
  747. store_player(#player{id =Id, user_id = UserId} = Player, Players) ->
  748. midict:store(Id, Player, [{user_id, UserId}], Players).
  749. get_player_by_user_id(UserId, Players) ->
  750. case midict:geti(UserId, user_id, Players) of
  751. [Player] -> {ok, Player};
  752. [] -> error
  753. end.
  754. %% players_to_list(Players) -> List
  755. players_to_list(Players) -> midict:all_values(Players).
  756. get_user_info(PlayerId, Players) ->
  757. #player{user_info = UserInfo} = midict:fetch(PlayerId, Players),
  758. UserInfo.
  759. fetch_player(PlayerId, Players) ->
  760. midict:fetch(PlayerId, Players).
  761. get_user_id(PlayerId, Players) ->
  762. #player{user_id = UserId} = midict:fetch(PlayerId, Players),
  763. UserId.
  764. %% del_player(PlayerId, Players) -> NewPlayers
  765. del_player(PlayerId, Players) ->
  766. midict:erase(PlayerId, Players).
  767. tables_init() -> midict:new().
  768. reg_table(TableId, Pid, MonRef, GlobalId, TableContext, Tables) ->
  769. Table = #table{id = TableId, pid = Pid, mon_ref = MonRef, global_id = GlobalId,
  770. state = initializing, context = TableContext},
  771. store_table(Table, Tables).
  772. update_created_table(TableId, Relay, Tables) ->
  773. Table = midict:fetch(TableId, Tables),
  774. NewTable = Table#table{relay = Relay, state = ?TABLE_STATE_READY},
  775. store_table(NewTable, Tables).
  776. store_table(#table{id = TableId, pid = Pid, mon_ref = MonRef, global_id = GlobalId} = Table, Tables) ->
  777. midict:store(TableId, Table, [{pid, Pid}, {global_id, GlobalId}, {mon_ref, MonRef}], Tables).
  778. fetch_table(TableId, Tables) -> midict:fetch(TableId, Tables).
  779. get_table_pid(TabId, Tables) ->
  780. #table{pid = TabPid} = midict:fetch(TabId, Tables),
  781. TabPid.
  782. del_table(TabId, Tables) -> midict:erase(TabId, Tables).
  783. get_table_by_mon_ref(MonRef, Tables) ->
  784. case midict:geti(MonRef, mon_ref, Tables) of
  785. [Table] -> Table;
  786. [] -> not_found
  787. end.
  788. tables_to_list(Tables) -> midict:all_values(Tables).
  789. seats_init() -> midict:new().
  790. find_seats_by_player_id(PlayerId, Seats) ->
  791. midict:geti(PlayerId, player_id, Seats).
  792. find_seats_by_player_id(PlayerId, TableId, Seats) ->
  793. midict:geti({PlayerId, TableId}, player_at_table, Seats).
  794. find_seats_by_table_id(TabId, Seats) ->
  795. midict:geti(TabId, table_id, Seats).
  796. find_disconnected_seats(TableId, Seats) ->
  797. midict:geti(false, {connected, TableId}, Seats).
  798. find_non_free_seats(TableId, Seats) ->
  799. midict:geti(false, {free_at_tab, TableId}, Seats).
  800. find_free_seats(TableId, Seats) ->
  801. midict:geti(true, {free_at_tab, TableId}, Seats).
  802. find_registered_robot_seats(TableId, Seats) ->
  803. [S || S = #seat{registered_by_table = true, is_bot = true} <- midict:geti(TableId, table_id, Seats)].
  804. fetch_seat(TableId, SeatNum, Seats) -> midict:fetch({TableId, SeatNum}, Seats).
  805. %% set_seat(TabId, SeatNum, PlayerId, IsBot, RegByTable, Connected, Free, Seats) -> NewSeats
  806. %% PlayerId = integer()
  807. %% IsBot = RegByTable = Connected = undefined | boolean()
  808. set_seat(TabId, SeatNum, PlayerId, IsBot, RegByTable, Connected, Free, Seats) ->
  809. Seat = #seat{table = TabId, seat_num = SeatNum, player_id = PlayerId, is_bot = IsBot,
  810. registered_by_table = RegByTable, connected = Connected, free = Free},
  811. store_seat(Seat, Seats).
  812. update_seat_connect_status(TableId, SeatNum, ConnStatus, Seats) ->
  813. Seat = midict:fetch({TableId, SeatNum}, Seats),
  814. NewSeat = Seat#seat{connected = ConnStatus},
  815. store_seat(NewSeat, Seats).
  816. store_seat(#seat{table = TabId, seat_num = SeatNum, player_id = PlayerId,
  817. is_bot = _IsBot, registered_by_table = _RegByTable,
  818. connected = Connected, free = Free} = Seat, Seats) ->
  819. Indices = if Free == true ->
  820. [{table_id, TabId}, {free, true}, {{free_at_tab, TabId}, true}];
  821. true ->
  822. [{table_id, TabId}, {free, false}, {{free_at_tab, TabId}, false},
  823. {player_at_table, {PlayerId, TabId}}, {player_id, PlayerId},
  824. {{connected, TabId}, Connected}]
  825. end,
  826. midict:store({TabId, SeatNum}, Seat, Indices, Seats).
  827. %% calc_players_prize_points(SeriesResult, KakushForWinner, KakushForLoser,
  828. %% WinGamePoints, MulFactor, Players) -> Points
  829. %% Types:
  830. %% SeriesResult = [{PlayerId, _Pos, _Points, Status}]
  831. %% Status = winner | looser
  832. %% Points = [{PlayerId, KakushPoints, GamePoints}]
  833. calc_players_prize_points(SeriesResult, KakushForWinners, KakushForLoser, WinGamePoints, MulFactor, Players) ->
  834. SeriesResult1 = [begin
  835. #'PlayerInfo'{id = UserId, robot = Robot} = get_user_info(PlayerId, Players),
  836. Paid = is_paid(user_id_to_string(UserId)),
  837. Winner = Status == winner,
  838. {PlayerId, Winner, Robot, Paid}
  839. end || {PlayerId, _Pos, _Points, Status} <- SeriesResult],
  840. Paids = [PlayerId || {PlayerId, _Winner, _Robot, _Paid = true} <- SeriesResult1],
  841. Winners = [PlayerId || {PlayerId, _Winner = true, _Robot, _Paid} <- SeriesResult1],
  842. TotalNum = length(SeriesResult),
  843. PaidsNum = length(Paids),
  844. WinnersNum = length(Winners),
  845. KakushPerWinner = round(((KakushForWinners * MulFactor) * PaidsNum div TotalNum) / WinnersNum),
  846. KakushPerLoser = (KakushForLoser * MulFactor) * PaidsNum div TotalNum,
  847. WinGamePoints1 = WinGamePoints * MulFactor,
  848. [begin
  849. {KakushPoints, GamePoints} = calc_points(KakushPerWinner, KakushPerLoser, WinGamePoints1, Paid, Robot, Winner),
  850. {PlayerId, KakushPoints, GamePoints}
  851. end || {PlayerId, Winner, Robot, Paid} <- SeriesResult1].
  852. calc_points(KakushPerWinner, KakushPerLoser, WinGamePoints, Paid, Robot, Winner) ->
  853. if Robot -> {0, 0};
  854. not Paid andalso Winner -> {0, WinGamePoints};
  855. not Paid -> {0, 0};
  856. Paid andalso Winner -> {KakushPerWinner, WinGamePoints};
  857. Paid -> {KakushPerLoser, 0}
  858. end.
  859. %% prepare_ends_note_points(SeriesResult, Points, Players) -> EndsNotePoints
  860. %% Types: EndsNotePoints = [{UserIdStr, Robot, Pos, KakushPoints, GamePoints}]
  861. prepare_ends_note_points(SeriesResult, Points, Players) ->
  862. [begin
  863. #player{user_id = UserId, is_bot = Robot} = fetch_player(PlayerId, Players),
  864. {_, KPoints, GPoints} = lists:keyfind(PlayerId, 1, Points),
  865. {user_id_to_string(UserId), Robot, Pos, KPoints, GPoints}
  866. end || {PlayerId, Pos, _Points, _Status} <- SeriesResult].
  867. %% prepare_users_prize_points(Points, Players) -> UsersPrizePoints
  868. %% Types:
  869. %% Points = [{PlayerId, KakushPoints, GamePoints}]
  870. %% UserPrizePoints = [{UserIdStr, KakushPoints, GamePoints}]
  871. prepare_users_prize_points(Points, Players) ->
  872. [{user_id_to_string(get_user_id(PlayerId, Players)), K, G} || {PlayerId, K, G} <- Points].
  873. is_paid(UserId) -> nsm_accounts:user_paid(UserId).
  874. deduct_quota(GameId, GameType, GameMode, Amount, MulFactor, UsersIds) ->
  875. RealAmount = Amount * MulFactor,
  876. [begin
  877. TI = #ti_game_event{game_name = GameType, game_mode = GameMode,
  878. id = GameId, double_points = MulFactor,
  879. type = start_round, tournament_type = ?TOURNAMENT_TYPE},
  880. kvs:add(#transaction{
  881. id=kvs:next_id(transaction,1),
  882. feed_id={quota,binary_to_list(UserId)},
  883. amount=-RealAmount,
  884. comment=TI})
  885. % nsm_accounts:transaction(binary_to_list(UserId), ?CURRENCY_QUOTA, -RealAmount, TI)
  886. end || UserId <- UsersIds],
  887. ok.
  888. %% add_points_to_accounts(Points, GameId, GameType, GameMode, MulFactor) -> ok
  889. %% Types: Points = [{UserId, KakushPoints, GamePoints}]
  890. add_points_to_accounts(Points, GameId, GameType, GameMode, MulFactor) ->
  891. TI = #ti_game_event{game_name = GameType, game_mode = GameMode,
  892. id = GameId, double_points = MulFactor,
  893. type = game_end, tournament_type = ?TOURNAMENT_TYPE},
  894. [begin
  895. if KakushPoints =/= 0 ->
  896. kvs:add(#transaction{
  897. id=kvs:next_id(transaction,1),
  898. feed_id={kakush,UserId},
  899. amount=KakushPoints,
  900. comment=TI});
  901. % ok = nsm_accounts:transaction(UserId, ?CURRENCY_KAKUSH, KakushPoints, TI);
  902. true -> do_nothing
  903. end,
  904. if GamePoints =/= 0 ->
  905. kvs:add(#transaction{
  906. id=kvs:next_id(transaction,1),
  907. feed_id={game_points,UserId},
  908. amount=GamePoints,
  909. comment=TI});
  910. % ok = nsm_accounts:transaction(UserId, ?CURRENCY_GAME_POINTS, GamePoints, TI);
  911. true -> do_nothing
  912. end
  913. end || {UserId, KakushPoints, GamePoints} <- Points],
  914. ok.
  915. send_ends_note(GameName, GameType, EndsNotePoints) ->
  916. wf:send(["system", "game_ends_note"], {{GameName, GameType}, EndsNotePoints}).
  917. shuffle(List) -> deck:to_list(deck:shuffle(deck:from_list(List))).
  918. create_decl_rec(GameType, CParams, GameId, Users) ->
  919. #game_table{id = GameId,
  920. name = proplists:get_value(table_name, CParams),
  921. % gameid,
  922. % trn_id,
  923. game_type = GameType,
  924. rounds = proplists:get_value(rounds, CParams),
  925. sets = proplists:get_value(sets, CParams),
  926. owner = proplists:get_value(owner, CParams),
  927. timestamp = now(),
  928. users = Users,
  929. users_options = proplists:get_value(users_options, CParams),
  930. game_mode = proplists:get_value(game_mode, CParams),
  931. % game_options,
  932. game_speed = proplists:get_value(speed, CParams),
  933. friends_only = proplists:get_value(friends_only, CParams),
  934. % invited_users = [],
  935. private = proplists:get_value(private, CParams),
  936. feel_lucky = false,
  937. % creator,
  938. age_limit = proplists:get_value(age, CParams),
  939. % groups_only = [],
  940. gender_limit = proplists:get_value(gender_limit, CParams),
  941. % location_limit = "",
  942. paid_only = proplists:get_value(paid_only, CParams),
  943. deny_robots = proplists:get_value(deny_robots, CParams),
  944. slang = proplists:get_value(slang, CParams),
  945. deny_observers = proplists:get_value(deny_observers, CParams),
  946. gosterge_finish = proplists:get_value(gosterge_finish, CParams),
  947. double_points = proplists:get_value(double_points, CParams),
  948. game_state = started,
  949. game_process = self(),
  950. game_module = ?MODULE,
  951. pointing_rules = proplists:get_value(pointing_rules, CParams),
  952. pointing_rules_ex = proplists:get_value(pointing_rules, CParams),
  953. % game_process_monitor =
  954. % tournament_type =
  955. robots_replacement_allowed = proplists:get_value(robots_replacement_allowed, CParams)
  956. }.
  957. user_id_to_string(UserId) -> binary_to_list(UserId).
  958. %% start_timer(Timeout) -> {TRef, Magic}
  959. start_timer(Timeout) ->
  960. Magic = make_ref(),
  961. TRef = erlang:send_after(Timeout, self(), {timeout, Magic}),
  962. {TRef, Magic}.
  963. %% spawn_bot(GameId, BotModule) -> UserInfo
  964. spawn_bot(GameId, BotModule) ->
  965. {NPid, UserInfo} = create_robot(BotModule, GameId),
  966. BotModule:join_game(NPid),
  967. UserInfo.
  968. create_robot(BM, GameId) ->
  969. UserInfo = auth_server:robot_credentials(),
  970. {ok, NPid} = BM:start(self(), UserInfo, GameId),
  971. {NPid, UserInfo}.
  972. spawn_table(TabMod, GameId, TableId, Params) -> TabMod:start(GameId, TableId, Params).
  973. send_to_table(TabMod, TabPid, Message) -> TabMod:parent_message(TabPid, Message).
  974. get_param(ParamId, Params) ->
  975. {_, Value} = lists:keyfind(ParamId, 1, Params),
  976. Value.
  977. get_option(OptionId, Params, DefValue) ->
  978. proplists:get_value(OptionId, Params, DefValue).
  979. series_result_test_() ->
  980. [
  981. ?_assertEqual(lists:sort([{2, 1, 100, winner},
  982. {4, 1, 100, winner},
  983. {1, 3, 50, looser},
  984. {3, 4, 20, looser}]),
  985. lists:sort(series_result([{1, 50},
  986. {2, 100},
  987. {3, 20},
  988. {4, 100}]))),
  989. ?_assertEqual(lists:sort([{2, 1, 100, winner},
  990. {4, 2, 50, looser},
  991. {1, 2, 50, looser},
  992. {3, 2, 50, looser}]),
  993. lists:sort(series_result([{1, 50},
  994. {2, 100},
  995. {3, 50},
  996. {4, 50}]))),
  997. ?_assertEqual(lists:sort([{2, 1, 100, winner},
  998. {4, 2, 70, looser},
  999. {1, 3, 50, looser},
  1000. {3, 3, 50, looser}]),
  1001. lists:sort(series_result([{1, 50},
  1002. {2, 100},
  1003. {3, 50},
  1004. {4, 70}])))
  1005. ].