nsg_trn_standalone.erl 59 KB

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