├── .gitignore ├── Makefile ├── README.md ├── include ├── openpoker.hrl ├── openpoker_common.hrl ├── openpoker_game.hrl ├── openpoker_notify.hrl ├── openpoker_protocol.hrl ├── openpoker_schema.hrl └── openpoker_test.hrl ├── priv ├── generate_client_protocol.sh ├── generate_server_protocol.sh ├── protocol.rb ├── protocol_client.rb └── protocol_server.erl ├── rebar.config ├── rel ├── files │ ├── erl │ ├── install_upgrade.escript │ ├── nodetool │ ├── openpoker │ ├── openpoker.cmd │ ├── start_erl.cmd │ └── vm.args └── reltool.config ├── src ├── bits.erl ├── client.erl ├── counter.erl ├── handler │ ├── op_game_handler.erl │ └── op_protocol_handler.erl ├── mods │ ├── deal_cards.erl │ ├── op_mod_betting.erl │ ├── op_mod_blinds.erl │ ├── op_mod_suspend.erl │ ├── ranking.erl │ ├── restart.erl │ ├── rig.erl │ ├── showdown.erl │ ├── stop.erl │ └── wait_players.erl ├── op_common.erl ├── op_exch.erl ├── op_exch_event.erl ├── op_exch_event_logger.erl ├── op_exch_mod.erl ├── op_games_sup.erl ├── op_phantom.erl ├── op_players_sup.erl ├── op_sup.erl ├── op_webtekcos_event_logger.erl ├── openpoker.app.src ├── openpoker_app.erl ├── pickle.erl ├── player.erl ├── protocol.erl ├── schema.erl └── texas │ ├── deck.erl │ ├── game.erl │ ├── hand.erl │ ├── pot.erl │ └── seat.erl └── test ├── integration ├── mod_betting_allin_test.erl ├── mod_betting_headsup_test.erl ├── mod_betting_raise_test.erl ├── mod_betting_test.erl ├── mod_blinds_test.erl ├── mod_deal_test.erl ├── mod_game_test.erl └── mod_login_test.erl ├── sim.erl ├── sim_client.erl └── unit ├── bits_test.erl ├── hand_test.erl ├── player_test.erl ├── protocol_test.erl ├── texas_game_test.erl └── texas_seat_test.erl /.gitignore: -------------------------------------------------------------------------------- 1 | .eunit 2 | .DS_Store 3 | deps 4 | *.o 5 | *.beam 6 | *.plt 7 | ebin 8 | log 9 | Mnesia* 10 | *.dump 11 | 12 | rel/openpoker/ 13 | rel/log/ 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | REBAR = rebar 2 | DIALYZER = dialyzer 3 | 4 | DIALYZER_WARNINGS = -Wunmatched_returns -Werror_handling \ 5 | -Wrace_conditions -Wunderspecs 6 | 7 | .PHONY: all compile test clean get-deps build-plt dialyze 8 | 9 | all: compile 10 | 11 | compile: 12 | @$(REBAR) compile 13 | 14 | test: compile 15 | @$(REBAR) eunit skip_deps=true -v 16 | 17 | qc: compile 18 | @$(REBAR) qc skip_deps=true 19 | 20 | clean: 21 | @$(REBAR) clean 22 | 23 | get-deps: 24 | @$(REBAR) get-deps 25 | 26 | build-plt: 27 | @$(DIALYZER) --build_plt --output_plt .dialyzer_plt \ 28 | --apps kernel stdlib 29 | 30 | dialyze: compile 31 | @$(DIALYZER) --src src --plt .dialyzer_plt $(DIALYZER_WARNINGS) | \ 32 | fgrep -vf .dialyzer-ignore-warnings 33 | 34 | generate: 35 | ./priv/generate_server_protocol.sh 36 | ./priv/generate_client_protocol.sh 37 | 38 | console: 39 | rm -rf rel/openpoker/Mnesia* 40 | rebar compile generate 41 | ./rel/openpoker/bin/openpoker console 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenPoker Texas hold'em Game Server & Client 2 | #### This project is based on [wagerlabs/openpoker](https://github.com/wagerlabs/openpoker), but I modified almost all of the code: 3 | 4 | - Except distributed, I do not care about this. 5 | - Except object paramters from exch module. 6 | - Delete super big module g. 7 | - Delete db dirty operation and use mnesia transaction. 8 | - Reflection all texas module and game module, game context struct. 9 | - Less protocol and client communications. 10 | 11 | #### I added some feature: 12 | 13 | - HTML5 texas client, animate by CSS3, communicate by websocket API, NOT Flash!!! 14 | - Websocket communition layer based mochisocket. 15 | - Client simulator, can be based process message testing. 16 | - Manage Site based ChicagoBoss MVC Web Framework. 17 | - Script tools generate protocol code by need to change. 18 | 19 | #### Protocols 20 | 21 | OpenPoker support multi network connection, eg: WebSocket, Socket. 22 | 23 | Connection with client by some custom protocols, please check on [wiki](https://github.com/hpyhacking/openpoker/wiki/Protocols). 24 | 25 | # License 26 | 27 | OpenPoker is released under a Creative Commons NonCommercial Licence. 28 | -------------------------------------------------------------------------------- /include/openpoker.hrl: -------------------------------------------------------------------------------- 1 | -include("openpoker_common.hrl"). 2 | -include("openpoker_game.hrl"). 3 | -include("openpoker_protocol.hrl"). 4 | -include("openpoker_schema.hrl"). 5 | -------------------------------------------------------------------------------- /include/openpoker_common.hrl: -------------------------------------------------------------------------------- 1 | -define(UNDEF, undefined). 2 | -define(WAIT_TABLE, 10 * 1000). 3 | -define(CONNECT_TIMEOUT, 2000). 4 | -define(SLEEP(T), timer:sleep(T * 1000)). 5 | -define(SLEEP, timer:sleep(200)). 6 | 7 | %%% Error codes 8 | 9 | -define(LOG(L), error_logger:info_report([{debug, {?MODULE, ?LINE, self()}}] ++ L)). 10 | -define(ERROR(L), error_logger:error_report([{debug, {?MODULE, ?LINE, self()}}] ++ L)). 11 | 12 | -define(LOOKUP_GAME(Id), global:whereis_name({game, Id})). 13 | -define(PLAYER(Id), {global, {player, Id}}). 14 | -define(LOOKUP_PLAYER(Id), global:whereis_name({player, Id})). 15 | 16 | -define(HASH_PWD(Password), erlang:phash2(Password, 1 bsl 32)). 17 | -define(DEF_PWD, "def_pwd"). 18 | -define(DEF_HASH_PWD, ?HASH_PWD(?DEF_PWD)). 19 | 20 | -record(exch, { 21 | id, 22 | module, 23 | state, 24 | mods, 25 | stack, 26 | ctx, 27 | conf 28 | }). 29 | -------------------------------------------------------------------------------- /include/openpoker_game.hrl: -------------------------------------------------------------------------------- 1 | -define(GAME_UNDEF, undefined). 2 | -define(PLAYER_TIMEOUT, 25000). 3 | -define(PLAYER_OUT_TIMEOUT, 15000). 4 | -define(START_DELAY, 6000). 5 | 6 | %%% Game stage 7 | 8 | -define(GS_CANCEL, 0). 9 | -define(GS_PREFLOP, 1). 10 | -define(GS_FLOP, 2). 11 | -define(GS_TURN, 3). 12 | -define(GS_RIVER, 4). 13 | 14 | -record(limit, { 15 | small, % small blind 16 | big, % big blind 17 | min, % min buyin 18 | max % max buyin 19 | }). 20 | 21 | %%% Player state 22 | 23 | -define(PS_EMPTY, 0). 24 | -define(PS_PLAY, 1). %% player is not bet or other player is raised 25 | -define(PS_BET, 2). %% player was betted 26 | -define(PS_ALL_IN, 4). %% player inplay all in pot 27 | -define(PS_FOLD, 8). %% player fold 28 | -define(PS_OUT, 16). %% player out when inplay not enough 29 | -define(PS_LEAVE, 32). %% player is leave but game is not over 30 | -define(PS_WAIT, 64). %% player wait next game playing 31 | 32 | -define(PS_STANDING, %% player is survive game 33 | ?PS_PLAY bor 34 | ?PS_BET bor 35 | ?PS_ALL_IN). 36 | 37 | -define(PS_READY, %% player ready to play game again 38 | ?PS_STANDING bor 39 | ?PS_FOLD bor 40 | ?PS_WAIT). 41 | 42 | -define(PS_ANY, 43 | ?PS_STANDING bor 44 | ?PS_FOLD bor 45 | ?PS_LEAVE). 46 | 47 | %%% Face 48 | 49 | -define(CF_ACE, 13). 50 | -define(CF_KING, 12). 51 | -define(CF_QUEEN, 11). 52 | -define(CF_JACK, 10). 53 | -define(CF_TEN, 9). 54 | -define(CF_NINE, 8). 55 | -define(CF_EIGHT, 7). 56 | -define(CF_SEVEN, 6). 57 | -define(CF_SIX, 5). 58 | -define(CF_FIVE, 4). 59 | -define(CF_FOUR, 3). 60 | -define(CF_THREE, 2). 61 | -define(CF_TWO, 1). 62 | -define(CF_NONE, 0). 63 | 64 | %%% Suit 65 | 66 | -define(CS_SPADES, 4). 67 | -define(CS_HEARTS, 3). 68 | -define(CS_DIAMONDS, 2). 69 | -define(CS_CLUBS, 1). 70 | -define(CS_NONE, 0). 71 | 72 | %% 牌面的编码与解码 高8位存储牌面,低8位存储花色 73 | -define(POKER_ENCODE(Suit, Face), (Face bsl 8) bor Suit). 74 | -define(POKER_DECODE(Card), [Card band 16#ff, Card bsr 8]). 75 | 76 | %%% Hand combination 77 | 78 | -define(HC_HIGH_CARD, 0). 79 | -define(HC_PAIR, 1). 80 | -define(HC_TWO_PAIR, 2). 81 | -define(HC_THREE_KIND, 3). 82 | -define(HC_STRAIGHT, 4). 83 | -define(HC_FLUSH, 5). 84 | -define(HC_FULL_HOUSE, 6). 85 | -define(HC_FOUR_KIND, 7). 86 | -define(HC_STRAIGHT_FLUSH, 8). 87 | 88 | -define(DEF_MOD, [ 89 | {wait_players, []}, 90 | %% blind rules 91 | {op_mod_blinds, []}, 92 | %% deal 2 cards to each player 93 | {deal_cards, [2, private, 500]}, 94 | {ranking, []}, 95 | %% start after BB, 3 raises 96 | {op_mod_betting, [?GS_PREFLOP]}, 97 | {deal_cards, [3, shared, 500]}, 98 | {ranking, []}, 99 | %% flop 100 | {op_mod_betting, [?GS_FLOP]}, 101 | {deal_cards, [1, shared, 500]}, 102 | {ranking, []}, 103 | %% turn 104 | {op_mod_betting, [?GS_TURN]}, 105 | {deal_cards, [1, shared, 500]}, 106 | {ranking, []}, 107 | %% river 108 | {op_mod_betting, [?GS_RIVER]}, 109 | {showdown, []}, 110 | {restart, []} 111 | ]). 112 | 113 | -record(hand, { 114 | pid = ?GAME_UNDEF, 115 | seat_sn = ?GAME_UNDEF, 116 | cards = [], 117 | rank = ?GAME_UNDEF, 118 | high1 = ?GAME_UNDEF, 119 | high2 = ?GAME_UNDEF, 120 | suit = ?GAME_UNDEF, 121 | score = 0 122 | }). 123 | 124 | -record(player_hand, { 125 | rank = ?HC_HIGH_CARD, 126 | high1 = ?GAME_UNDEF, 127 | high2 = ?GAME_UNDEF, 128 | suit = ?GAME_UNDEF 129 | }). 130 | 131 | -record(seat, { 132 | sn = ?GAME_UNDEF, 133 | pid = ?GAME_UNDEF, %% player id 134 | identity = ?GAME_UNDEF, %% player identity 135 | process = ?GAME_UNDEF, %% player process 136 | hand = [], %% cards 137 | bet = 0, %% total bet 138 | inplay = 0, %% inplay balance 139 | state = ?PS_EMPTY, %% player state 140 | nick = <<"">>, 141 | photo = <<"">> 142 | }). 143 | 144 | -record(texas, { 145 | gid, %% runtime id 146 | seats, %% seat record list 147 | limit, %% limit record 148 | timeout = ?PLAYER_TIMEOUT, %% player action timeout 149 | start_delay = ?START_DELAY, %% before start delay 150 | xref, %% player to seat cross-reference 151 | pot, %% pot structure 152 | deck, %% card deck 153 | board = [], %% shared cards list 154 | observers = [], %% game observers [{pid, process}] -> proplists 155 | required = 2, %% players required to start a game 156 | joined = 0, %% joined players count 157 | max_joined = 0, %% can joined players limit max 158 | b = ?GAME_UNDEF, %% button 159 | sb = ?GAME_UNDEF, %% small blind 160 | bb = ?GAME_UNDEF, %% big blind 161 | sb_amt = 0, 162 | bb_amt = 0, 163 | headsup = false, 164 | max_betting = 0, %% current stage latest raised amount 165 | exp_seat = none, %% expecting seat 166 | exp_call = 0, %% expecting call amount 167 | exp_min = 0, %% expecting raise min amount 168 | exp_max = 0, %% expecting raise max amount 169 | stage = ?GS_CANCEL, 170 | winners = [], %% last winners 171 | timer = ?GAME_UNDEF, 172 | deal_timeout = 0 173 | }). 174 | -------------------------------------------------------------------------------- /include/openpoker_notify.hrl: -------------------------------------------------------------------------------- 1 | %% Game Notify Protocol 2 | -define(NOTIFY_ACOUNT, 100). 3 | -record(notify_acount, { balance, inplay }). 4 | 5 | -define(NOTIFY_GAME, 101). 6 | -record(notify_game, { game, name, limit, seats, require, joined }). 7 | 8 | -define(NOTIFY_GAME_DETAIL, 102). 9 | -record(notify_game_detail, { game, pot, stage, limit, seats, require, joined }). 10 | 11 | -define(NOTIFY_GAME_START, 103). 12 | -record(notify_game_start, { game }). 13 | 14 | -define(NOTIFY_GAME_END, 104). 15 | -record(notify_game_end, { game }). 16 | 17 | -define(NOTIFY_GAME_CANCEL, 105). 18 | -record(notify_game_cancel, { game }). 19 | 20 | %% Texas Notify Protocol 21 | -define(NOTIFY_STAGE, 106). 22 | -record(notify_stage, { game, stage }). 23 | 24 | -define(NOTIFY_STAGE_END, 107). 25 | -record(notify_stage_end, { game, stage }). 26 | 27 | -define(NOTIFY_JOIN, 108). 28 | -record(notify_join, { game, player, sn, buyin, nick, photo, '|', proc }). 29 | 30 | -define(NOTIFY_LEAVE, 109). 31 | -record(notify_leave, { game, sn, player, '|', proc }). 32 | 33 | -define(NOTIFY_BUTTON, 110). 34 | -record(notify_button, { game, b}). 35 | 36 | -define(NOTIFY_SB, 111). 37 | -record(notify_sb, { game, sb }). 38 | 39 | -define(NOTIFY_BB, 112). 40 | -record(notify_bb, { game, bb }). 41 | 42 | -define(NOTIFY_RAISE, 114). 43 | -record(notify_raise, { game, player, sn, raise, call }). 44 | 45 | -define(NOTIFY_SEAT, 115). 46 | -record(notify_seat, { game, sn, state, player, inplay, bet, nick, photo }). 47 | 48 | -define(NOTIFY_ACTOR, 116). 49 | -record(notify_actor, { game, player, sn, timeout }). 50 | 51 | -define(NOTIFY_BETTING, 117). 52 | -record(notify_betting, { game, player, sn, call, min, max }). 53 | 54 | -define(NOTIFY_DRAW, 118). 55 | -record(notify_draw, { game, player, sn, card }). 56 | 57 | -define(NOTIFY_PRIVATE, 119). 58 | -record(notify_private, { game, player, sn, card }). 59 | 60 | -define(NOTIFY_SHARED, 120). 61 | -record(notify_shared, { game, card }). 62 | 63 | -define(NOTIFY_HAND, 121). 64 | -record(notify_hand, { game, player, sn, rank, high1, high2, suit}). 65 | 66 | -define(NOTIFY_CARDS, 122). 67 | -record(notify_cards, { game, player, sn, cards }). 68 | 69 | -define(NOTIFY_WIN, 123). 70 | -record(notify_win, { game, player, sn, amount }). 71 | 72 | -define(NOTIFY_PLAYER, 124). 73 | -record(notify_player, { player, nick, photo }). 74 | 75 | -define(NOTIFY_FOLD, 125). 76 | -record(notify_fold, { game, sn }). 77 | 78 | -define(NOTIFY_OUT, 126). 79 | -record(notify_out, { game, player }). 80 | 81 | -define(NOTIFY_GAMES_LIST_END,127). 82 | -record(notify_games_list_end,{ size }). 83 | 84 | -define(NOTIFY_SEATS_LIST_END,128). 85 | -record(notify_seats_list_end,{ size }). 86 | 87 | -define(NOTIFY_WATCH, 129). 88 | -record(notify_watch, { game, player, '|', proc }). 89 | 90 | -define(NOTIFY_UNWATCH, 130). 91 | -record(notify_unwatch, { game, player, '|', proc }). 92 | 93 | -define(NOTIFY_SIGNIN, 131). 94 | -record(notify_signin, { player }). 95 | 96 | -define(NOTIFY_ERROR, 255). 97 | -record(notify_error, { error }). 98 | -------------------------------------------------------------------------------- /include/openpoker_protocol.hrl: -------------------------------------------------------------------------------- 1 | -define(ERR_UNAUTH, 1). 2 | -define(ERR_DATA, 2). 3 | -define(ERR_PROTOCOL, 3). 4 | -define(ERR_START_DISABLED, 4). 5 | -define(ERR_CONNECTION_TIMEOUT, 5). 6 | -define(ERR_AGENT_DISABLE, 6). 7 | -define(ERR_PLAYER_DISABLE, 7). 8 | -define(ERR_JOIN_LESS_BALANCE, 8). 9 | -define(ERR_PLAYER_BUSY, 9). 10 | 11 | %% Game Commands 12 | -define(CMD_LOGIN, 10). 13 | -record(cmd_login, { identity, password }). 14 | 15 | -define(CMD_LOGOUT, 11). 16 | -record(cmd_logout, {}). 17 | 18 | -define(CMD_QUERY_PLAYER, 12). 19 | -record(cmd_query_player, { player }). 20 | 21 | -define(CMD_QUERY_BALANCE, 13). 22 | -record(cmd_query_balance, {}). 23 | 24 | -define(CMD_QUERY_GAME, 14). 25 | -record(cmd_query_game, {}). 26 | 27 | %% Texas Commands 28 | -define(CMD_WATCH, 31). 29 | -record(cmd_watch, { game, '|', player, pid, identity, proc }). 30 | 31 | -define(CMD_UNWATCH, 32). 32 | -record(cmd_unwatch, { game, '|', player, pid, identity, proc }). 33 | 34 | -define(CMD_JOIN, 33). 35 | -record(cmd_join, { game, sn, buyin, '|', pid, identity, nick, photo, proc }). 36 | 37 | -define(CMD_LEAVE, 34). 38 | -record(cmd_leave, { game, '|', pid, sn}). 39 | 40 | -define(CMD_RAISE, 35). 41 | -record(cmd_raise, { game, amount, '|', pid, sn }). 42 | 43 | -define(CMD_FOLD, 36). 44 | -record(cmd_fold, { game, '|', pid }). 45 | 46 | -define(CMD_QUERY_SEATS, 37). 47 | -record(cmd_query_seats, { game }). 48 | 49 | -define(CMD_OUT, 38). 50 | -record(cmd_out, { game, sn, buyin, '|', pid, proc }). 51 | 52 | -include("openpoker_notify.hrl"). 53 | -------------------------------------------------------------------------------- /include/openpoker_schema.hrl: -------------------------------------------------------------------------------- 1 | -record(tab_player_info, { 2 | pid, 3 | identity, 4 | password, 5 | nick, 6 | photo, 7 | login_errors = 0, 8 | disabled = false, 9 | cash = 0, 10 | credit = 0 11 | }). 12 | 13 | -record(tab_inplay, { 14 | pid, 15 | inplay 16 | }). 17 | 18 | -record(tab_turnover_log, { 19 | id = now(), 20 | pid, %% pid 21 | game, %% {gid, sn} 22 | amt, %% amt 23 | cost, %% winner cost amt 24 | inplay, %% in out result inplay 25 | date = date(), %% {year, month, day} 26 | time = time() %% {hour, min, sec} 27 | }). 28 | 29 | -record(tab_buyin_log, { 30 | id = now(), 31 | pid, %% pid 32 | gid, %% gid 33 | amt, %% amt 34 | cash, %% cash result by change amt 35 | credit, %% credit 36 | date = date(), %% {year, month, day} 37 | time = time() %% {hour, min, sec} 38 | }). 39 | 40 | -record(tab_counter, { 41 | type, 42 | value 43 | }). 44 | 45 | -record(tab_player, { 46 | pid ::integer(), 47 | process = undefined ::pid() | undefined, 48 | socket = undefined ::pid() | undefined 49 | }). 50 | 51 | 52 | -record(tab_game_config, { 53 | id, 54 | module, 55 | mods, 56 | limit, 57 | seat_count, 58 | start_delay, 59 | required, 60 | timeout, 61 | max 62 | }). 63 | 64 | -record(tab_game_xref, { 65 | gid, 66 | process, 67 | module, 68 | limit, 69 | seat_count, 70 | timeout, 71 | required % min player count 72 | }). 73 | -------------------------------------------------------------------------------- /include/openpoker_test.hrl: -------------------------------------------------------------------------------- 1 | -include_lib("eunit/include/eunit.hrl"). 2 | 3 | -define(GAME, 1). 4 | -define(GAME_NAME(Game), {global, {game, Game}}). 5 | 6 | -define(JACK, jack). 7 | -define(JACK_IDENTITY, "jack"). 8 | -define(JACK_ID, 1). 9 | -define(TOMMY, tommy). 10 | -define(TOMMY_IDENTITY, "tommy"). 11 | -define(TOMMY_ID, 2). 12 | -define(FOO, foo). 13 | -define(FOO_IDENTITY, "foo"). 14 | -define(FOO_ID, 3). 15 | 16 | -define(PLAYERS, [{Key, #tab_player_info{pid = Id, identity = Identity, nick = "nick-" ++ Identity, photo = "default", password = ?DEF_HASH_PWD, cash = 0, credit = 1000, disabled = false}} || {Key, Id, Identity} <- [{?JACK, ?JACK_ID, ?JACK_IDENTITY}, {?TOMMY, ?TOMMY_ID, ?TOMMY_IDENTITY}, {?FOO, ?FOO_ID, ?FOO_IDENTITY}]]). 17 | -------------------------------------------------------------------------------- /priv/generate_client_protocol.sh: -------------------------------------------------------------------------------- 1 | sed -i '.bak' '/AUTO/,$'d ../game.kinseng/assets/javascripts/app/models/index.coffee 2 | ./priv/protocol_client.rb >> ../game.kinseng/assets/javascripts/app/models/index.coffee 3 | rm ../game.kinseng/assets/javascripts/app/models/index.coffee.bak 4 | -------------------------------------------------------------------------------- /priv/generate_server_protocol.sh: -------------------------------------------------------------------------------- 1 | sed -i '.bak' '/AUTO/,$'d ./src/protocol.erl 2 | ./priv/protocol.rb >> ./src/protocol.erl 3 | rm ./src/protocol.erl.bak 4 | -------------------------------------------------------------------------------- /priv/protocol.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | # 3 | DEFINE = Hash.new 4 | READ_OUT = "%% read binary to protocol record\n" 5 | WRITE_OUT = "%% write protocol record to binary\n" 6 | 7 | def parse_def file 8 | File.open(file).each_line do |line| 9 | next unless line =~ /record\((cmd_[a-z_]*|notify_[a-z_]*),\s*\{(.*)\}/ 10 | 11 | record = $1 12 | internal = false 13 | 14 | record_def = "" 15 | record_def << "record(#{record}, {" 16 | record_def << $2.split(',').map do |x| 17 | f = x.strip 18 | f = (f == "game" and record =~ /^notify/) ? "game_id" : f 19 | f = (f == "player" and record =~ /^notify/) ? "player_id" : f 20 | 21 | internal = true if (internal == false and f == "'|'") 22 | 23 | internal ? "internal()" : "#{f}()" 24 | end.join(", ") 25 | record_def << "})" 26 | DEFINE[record.upcase] = record_def 27 | end 28 | end 29 | 30 | def parse_wr file 31 | File.open(file).each_line do |line| 32 | next unless line =~ /^\-define\((CMD_[A-Z_]*|NOTIFY_[A-Z_]*)/ 33 | 34 | READ_OUT << "read(<>) ->\n" 35 | READ_OUT << " unpickle(#{DEFINE[$1]}, Bin);\n\n" 36 | 37 | WRITE_OUT << "write(R) when is_record(R, #{$1.downcase}) ->\n" 38 | WRITE_OUT << " [?#{$1} | pickle(#{DEFINE[$1]}, R)];\n\n" 39 | end 40 | end 41 | 42 | parse_def "include/openpoker_protocol.hrl" 43 | parse_def "include/openpoker_notify.hrl" 44 | 45 | parse_wr "include/openpoker_protocol.hrl" 46 | parse_wr "include/openpoker_notify.hrl" 47 | 48 | READ_OUT << "read(_ErrorBin) -> ok.\n" 49 | WRITE_OUT << "write(_ErrorRecord) -> ok.\n" 50 | 51 | puts "%% AUTO GENERATE - Don't edit manual\n" 52 | puts READ_OUT 53 | puts "\n" 54 | puts WRITE_OUT 55 | -------------------------------------------------------------------------------- /priv/protocol_client.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | # 3 | DEFINE = Hash.new 4 | DEF_OUT = "" 5 | EXPORT_OUT = "" 6 | 7 | SAVED = { 8 | :NotifyGame => 'game_id', 9 | :NotifySeat => 'sn' 10 | } 11 | 12 | INTEGER = 't_integer' 13 | STRING = 't_string' 14 | COIN = 't_coin' 15 | CARD = 't_card' 16 | CARDS = 't_cards' 17 | BYTE = 't_byte' 18 | LIMIT = 't_limit' 19 | 20 | TYPES = { 21 | :card => CARD, 22 | :cards => CARDS, 23 | :limit => LIMIT, 24 | 25 | :identity => STRING, 26 | :password => STRING, 27 | :name => STRING, 28 | :nick => STRING, 29 | :photo => STRING, 30 | 31 | :state => INTEGER, 32 | :game_id => INTEGER, 33 | :player_id => INTEGER, 34 | :size => INTEGER, 35 | :timeout => INTEGER, 36 | 37 | :min => COIN, 38 | :max => COIN, 39 | :pot => COIN, 40 | :bet => COIN, 41 | :call => COIN, 42 | :raise => COIN, 43 | :buyin => COIN, 44 | :inplay => COIN, 45 | :balance => COIN, 46 | :amount => COIN, 47 | 48 | :b => BYTE, 49 | :bb => BYTE, 50 | :sb => BYTE, 51 | :sn => BYTE, 52 | :stage => BYTE, 53 | :seats => BYTE, 54 | :joined => BYTE, :require => BYTE, :rank => BYTE, :suit => BYTE, :high1 => BYTE, 55 | :high2 => BYTE, 56 | :error => BYTE 57 | } 58 | 59 | def to_n str 60 | str.downcase.split('_').map do |x| 61 | x.gsub(/\b\w/) { $&.upcase } 62 | end.join() 63 | end 64 | 65 | def parse_def file 66 | File.open(file).each_line do |line| 67 | next unless line =~ /record\((cmd_[a-z_]*|notify_[a-z_]*),\s*\{(.*)\}/ 68 | 69 | record = $1 70 | internal = false 71 | 72 | record_def = "" 73 | record_def << $2.split(',').map do |x| 74 | f = x.strip 75 | f = (f == "game") ? "game_id" : f 76 | f = (f == "player") ? "player_id" : f 77 | 78 | internal = true if (internal == false and f == "'|'") 79 | internal ? nil : "#{f}: #{TYPES[f.to_sym]}" 80 | end.select do |x| not x.nil? end.join(", ") 81 | DEFINE[record.upcase] = record_def == "" ? nil : record_def << "\n" 82 | end 83 | end 84 | 85 | def parse_wr file 86 | File.open(file).each_line do |line| 87 | next unless line =~ /^\-define\((CMD_[A-Z_]*|NOTIFY_[A-Z_]*)\,\s*(\d+)\)\.$/ 88 | 89 | record = $1 90 | record_id = $2 91 | 92 | DEF_OUT << "class App.#{to_n record} extends App.Protocol\n" 93 | if DEFINE[record.upcase] 94 | DEF_OUT << " @reg '#{to_n record}', #{record_id}, #{DEFINE[record.upcase]}" 95 | if SAVED.has_key? to_n(record).to_sym 96 | DEF_OUT << " @received_save '#{SAVED[to_n(record).to_sym]}'\n\n" 97 | elsif 98 | DEF_OUT << "\n" 99 | end 100 | else 101 | DEF_OUT << " @reg '#{to_n record}', #{record_id}\n\n" 102 | end 103 | 104 | EXPORT_OUT << "App.Protocol.join App.#{to_n record}\n" 105 | end 106 | end 107 | 108 | parse_def "include/openpoker_protocol.hrl" 109 | parse_def "include/openpoker_notify.hrl" 110 | 111 | parse_wr "include/openpoker_protocol.hrl" 112 | parse_wr "include/openpoker_notify.hrl" 113 | 114 | puts "# AUTO GENERATE, DONT EDIT!!!\n" 115 | puts DEF_OUT 116 | puts EXPORT_OUT 117 | -------------------------------------------------------------------------------- /priv/protocol_server.erl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env escript 2 | 3 | -export([main/1]). 4 | 5 | main(_) -> 6 | code:add_patha("ebin"), 7 | code:add_patha("deps/mochiweb/ebin"), 8 | code:add_patha("deps/webtekcos/ebin"), 9 | 10 | Loop = fun () -> 11 | receive 12 | _ -> 13 | ok 14 | end 15 | end, 16 | 17 | % mock player id, beascuse protocol check player id to pair pid. 18 | MockPlayerPID = spawn(Loop), 19 | yes = global:register_name({player, 1}, MockPlayerPID), 20 | 21 | webtekcos:start_link("127.0.0.1", 8000), 22 | 23 | io:format("Open client.html test websocket server.~n"), 24 | io:format("It's listen on localhost:8000.~n"), 25 | io:format("Press Ctrl+C to shutdown server!!!~n"), 26 | 27 | receive 28 | _ -> ok 29 | end. 30 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {erl_first_files, ["op_exch", "op_exch_mod"]}. 2 | {sub_dirs, ["rel"]}. 3 | {deps, [ 4 | {webtekcos, ".*", {git, "git://github.com/hpyhacking/webtekcos.git", {tag, "HEAD"}}} 5 | ] 6 | }. 7 | -------------------------------------------------------------------------------- /rel/files/erl: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ## This script replaces the default "erl" in erts-VSN/bin. This is necessary 4 | ## as escript depends on erl and in turn, erl depends on having access to a 5 | ## bootscript (start.boot). Note that this script is ONLY invoked as a side-effect 6 | ## of running escript -- the embedded node bypasses erl and uses erlexec directly 7 | ## (as it should). 8 | ## 9 | ## Note that this script makes the assumption that there is a start_clean.boot 10 | ## file available in $ROOTDIR/release/VSN. 11 | 12 | # Determine the abspath of where this script is executing from. 13 | ERTS_BIN_DIR=$(cd ${0%/*} && pwd) 14 | 15 | # Now determine the root directory -- this script runs from erts-VSN/bin, 16 | # so we simply need to strip off two dirs from the end of the ERTS_BIN_DIR 17 | # path. 18 | ROOTDIR=${ERTS_BIN_DIR%/*/*} 19 | 20 | # Parse out release and erts info 21 | START_ERL=`cat $ROOTDIR/releases/start_erl.data` 22 | ERTS_VSN=${START_ERL% *} 23 | APP_VSN=${START_ERL#* } 24 | 25 | BINDIR=$ROOTDIR/erts-$ERTS_VSN/bin 26 | EMU=beam 27 | PROGNAME=`echo $0 | sed 's/.*\\///'` 28 | CMD="$BINDIR/erlexec" 29 | export EMU 30 | export ROOTDIR 31 | export BINDIR 32 | export PROGNAME 33 | 34 | exec $CMD -boot $ROOTDIR/releases/$APP_VSN/start_clean ${1+"$@"} 35 | -------------------------------------------------------------------------------- /rel/files/install_upgrade.escript: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env escript 2 | %%! -noshell -noinput 3 | %% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*- 4 | %% ex: ft=erlang ts=4 sw=4 et 5 | 6 | -define(TIMEOUT, 60000). 7 | -define(INFO(Fmt,Args), io:format(Fmt,Args)). 8 | 9 | main([NodeName, Cookie, ReleasePackage]) -> 10 | TargetNode = start_distribution(NodeName, Cookie), 11 | {ok, Vsn} = rpc:call(TargetNode, release_handler, unpack_release, 12 | [ReleasePackage], ?TIMEOUT), 13 | ?INFO("Unpacked Release ~p~n", [Vsn]), 14 | {ok, OtherVsn, Desc} = rpc:call(TargetNode, release_handler, 15 | check_install_release, [Vsn], ?TIMEOUT), 16 | {ok, OtherVsn, Desc} = rpc:call(TargetNode, release_handler, 17 | install_release, [Vsn], ?TIMEOUT), 18 | ?INFO("Installed Release ~p~n", [Vsn]), 19 | ok = rpc:call(TargetNode, release_handler, make_permanent, [Vsn], ?TIMEOUT), 20 | ?INFO("Made Release ~p Permanent~n", [Vsn]); 21 | main(_) -> 22 | init:stop(1). 23 | 24 | start_distribution(NodeName, Cookie) -> 25 | MyNode = make_script_node(NodeName), 26 | {ok, _Pid} = net_kernel:start([MyNode, shortnames]), 27 | erlang:set_cookie(node(), list_to_atom(Cookie)), 28 | TargetNode = make_target_node(NodeName), 29 | case {net_kernel:hidden_connect_node(TargetNode), 30 | net_adm:ping(TargetNode)} of 31 | {true, pong} -> 32 | ok; 33 | {_, pang} -> 34 | io:format("Node ~p not responding to pings.\n", [TargetNode]), 35 | init:stop(1) 36 | end, 37 | TargetNode. 38 | 39 | make_target_node(Node) -> 40 | [_, Host] = string:tokens(atom_to_list(node()), "@"), 41 | list_to_atom(lists:concat([Node, "@", Host])). 42 | 43 | make_script_node(Node) -> 44 | list_to_atom(lists:concat([Node, "_upgrader_", os:getpid()])). 45 | -------------------------------------------------------------------------------- /rel/files/nodetool: -------------------------------------------------------------------------------- 1 | %% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*- 2 | %% ex: ft=erlang ts=4 sw=4 et 3 | %% ------------------------------------------------------------------- 4 | %% 5 | %% nodetool: Helper Script for interacting with live nodes 6 | %% 7 | %% ------------------------------------------------------------------- 8 | 9 | main(Args) -> 10 | ok = start_epmd(), 11 | %% Extract the args 12 | {RestArgs, TargetNode} = process_args(Args, [], undefined), 13 | 14 | %% See if the node is currently running -- if it's not, we'll bail 15 | case {net_kernel:hidden_connect_node(TargetNode), net_adm:ping(TargetNode)} of 16 | {true, pong} -> 17 | ok; 18 | {_, pang} -> 19 | io:format("Node ~p not responding to pings.\n", [TargetNode]), 20 | halt(1) 21 | end, 22 | 23 | case RestArgs of 24 | ["ping"] -> 25 | %% If we got this far, the node already responsed to a ping, so just dump 26 | %% a "pong" 27 | io:format("pong\n"); 28 | ["stop"] -> 29 | io:format("~p\n", [rpc:call(TargetNode, init, stop, [], 60000)]); 30 | ["restart"] -> 31 | io:format("~p\n", [rpc:call(TargetNode, init, restart, [], 60000)]); 32 | ["reboot"] -> 33 | io:format("~p\n", [rpc:call(TargetNode, init, reboot, [], 60000)]); 34 | ["rpc", Module, Function | RpcArgs] -> 35 | case rpc:call(TargetNode, list_to_atom(Module), list_to_atom(Function), 36 | [RpcArgs], 60000) of 37 | ok -> 38 | ok; 39 | {badrpc, Reason} -> 40 | io:format("RPC to ~p failed: ~p\n", [TargetNode, Reason]), 41 | halt(1); 42 | _ -> 43 | halt(1) 44 | end; 45 | ["rpcterms", Module, Function, ArgsAsString] -> 46 | case rpc:call(TargetNode, list_to_atom(Module), list_to_atom(Function), 47 | consult(ArgsAsString), 60000) of 48 | {badrpc, Reason} -> 49 | io:format("RPC to ~p failed: ~p\n", [TargetNode, Reason]), 50 | halt(1); 51 | Other -> 52 | io:format("~p\n", [Other]) 53 | end; 54 | Other -> 55 | io:format("Other: ~p\n", [Other]), 56 | io:format("Usage: nodetool {ping|stop|restart|reboot}\n") 57 | end, 58 | net_kernel:stop(). 59 | 60 | process_args([], Acc, TargetNode) -> 61 | {lists:reverse(Acc), TargetNode}; 62 | process_args(["-setcookie", Cookie | Rest], Acc, TargetNode) -> 63 | erlang:set_cookie(node(), list_to_atom(Cookie)), 64 | process_args(Rest, Acc, TargetNode); 65 | process_args(["-name", TargetName | Rest], Acc, _) -> 66 | ThisNode = append_node_suffix(TargetName, "_maint_"), 67 | {ok, _} = net_kernel:start([ThisNode, longnames]), 68 | process_args(Rest, Acc, nodename(TargetName)); 69 | process_args(["-sname", TargetName | Rest], Acc, _) -> 70 | ThisNode = append_node_suffix(TargetName, "_maint_"), 71 | {ok, _} = net_kernel:start([ThisNode, shortnames]), 72 | process_args(Rest, Acc, nodename(TargetName)); 73 | process_args([Arg | Rest], Acc, Opts) -> 74 | process_args(Rest, [Arg | Acc], Opts). 75 | 76 | 77 | start_epmd() -> 78 | [] = os:cmd(epmd_path() ++ " -daemon"), 79 | ok. 80 | 81 | epmd_path() -> 82 | ErtsBinDir = filename:dirname(escript:script_name()), 83 | Name = "epmd", 84 | case os:find_executable(Name, ErtsBinDir) of 85 | false -> 86 | case os:find_executable(Name) of 87 | false -> 88 | io:format("Could not find epmd.~n"), 89 | halt(1); 90 | GlobalEpmd -> 91 | GlobalEpmd 92 | end; 93 | Epmd -> 94 | Epmd 95 | end. 96 | 97 | 98 | nodename(Name) -> 99 | case string:tokens(Name, "@") of 100 | [_Node, _Host] -> 101 | list_to_atom(Name); 102 | [Node] -> 103 | [_, Host] = string:tokens(atom_to_list(node()), "@"), 104 | list_to_atom(lists:concat([Node, "@", Host])) 105 | end. 106 | 107 | append_node_suffix(Name, Suffix) -> 108 | case string:tokens(Name, "@") of 109 | [Node, Host] -> 110 | list_to_atom(lists:concat([Node, Suffix, os:getpid(), "@", Host])); 111 | [Node] -> 112 | list_to_atom(lists:concat([Node, Suffix, os:getpid()])) 113 | end. 114 | 115 | 116 | %% 117 | %% Given a string or binary, parse it into a list of terms, ala file:consult/0 118 | %% 119 | consult(Str) when is_list(Str) -> 120 | consult([], Str, []); 121 | consult(Bin) when is_binary(Bin)-> 122 | consult([], binary_to_list(Bin), []). 123 | 124 | consult(Cont, Str, Acc) -> 125 | case erl_scan:tokens(Cont, Str, 0) of 126 | {done, Result, Remaining} -> 127 | case Result of 128 | {ok, Tokens, _} -> 129 | {ok, Term} = erl_parse:parse_term(Tokens), 130 | consult([], Remaining, [Term | Acc]); 131 | {eof, _Other} -> 132 | lists:reverse(Acc); 133 | {error, Info, _} -> 134 | {error, Info} 135 | end; 136 | {more, Cont1} -> 137 | consult(Cont1, eof, Acc) 138 | end. 139 | -------------------------------------------------------------------------------- /rel/files/openpoker: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # -*- tab-width:4;indent-tabs-mode:nil -*- 3 | # ex: ts=4 sw=4 et 4 | 5 | RUNNER_SCRIPT_DIR=$(cd ${0%/*} && pwd) 6 | 7 | RUNNER_BASE_DIR=${RUNNER_SCRIPT_DIR%/*} 8 | RUNNER_ETC_DIR=$RUNNER_BASE_DIR/etc 9 | RUNNER_LOG_DIR=$RUNNER_BASE_DIR/log 10 | # Note the trailing slash on $PIPE_DIR/ 11 | PIPE_DIR=/tmp/$RUNNER_BASE_DIR/ 12 | RUNNER_USER= 13 | 14 | # Make sure this script is running as the appropriate user 15 | if [ ! -z "$RUNNER_USER" ] && [ `whoami` != "$RUNNER_USER" ]; then 16 | exec sudo -u $RUNNER_USER -i $0 $@ 17 | fi 18 | 19 | # Make sure CWD is set to runner base dir 20 | cd $RUNNER_BASE_DIR 21 | 22 | # Make sure log directory exists 23 | mkdir -p $RUNNER_LOG_DIR 24 | # Identify the script name 25 | SCRIPT=`basename $0` 26 | 27 | # Parse out release and erts info 28 | START_ERL=`cat $RUNNER_BASE_DIR/releases/start_erl.data` 29 | ERTS_VSN=${START_ERL% *} 30 | APP_VSN=${START_ERL#* } 31 | 32 | # Use releases/VSN/vm.args if it exists otherwise use etc/vm.args 33 | if [ -e "$RUNNER_BASE_DIR/releases/$APP_VSN/vm.args" ]; then 34 | VMARGS_PATH="$RUNNER_BASE_DIR/releases/$APP_VSN/vm.args" 35 | else 36 | VMARGS_PATH="$RUNNER_ETC_DIR/vm.args" 37 | fi 38 | 39 | # Use releases/VSN/sys.config if it exists otherwise use etc/app.config 40 | if [ -e "$RUNNER_BASE_DIR/releases/$APP_VSN/sys.config" ]; then 41 | CONFIG_PATH="$RUNNER_BASE_DIR/releases/$APP_VSN/sys.config" 42 | else 43 | CONFIG_PATH="$RUNNER_ETC_DIR/app.config" 44 | fi 45 | 46 | # Extract the target node name from node.args 47 | NAME_ARG=`egrep '^-s?name' $VMARGS_PATH` 48 | if [ -z "$NAME_ARG" ]; then 49 | echo "vm.args needs to have either -name or -sname parameter." 50 | exit 1 51 | fi 52 | 53 | # Extract the name type and name from the NAME_ARG for REMSH 54 | REMSH_TYPE=`echo $NAME_ARG | awk '{print $1}'` 55 | REMSH_NAME=`echo $NAME_ARG | awk '{print $2}'` 56 | 57 | # Note the `date +%s`, used to allow multiple remsh to the same node transparently 58 | REMSH_NAME_ARG="$REMSH_TYPE remsh`date +%s`@`echo $REMSH_NAME | awk -F@ '{print $2}'`" 59 | REMSH_REMSH_ARG="-remsh $REMSH_NAME" 60 | 61 | # Extract the target cookie 62 | COOKIE_ARG=`grep '^-setcookie' $VMARGS_PATH` 63 | if [ -z "$COOKIE_ARG" ]; then 64 | echo "vm.args needs to have a -setcookie parameter." 65 | exit 1 66 | fi 67 | 68 | # Add ERTS bin dir to our path 69 | ERTS_PATH=$RUNNER_BASE_DIR/erts-$ERTS_VSN/bin 70 | 71 | # Setup command to control the node 72 | NODETOOL="$ERTS_PATH/escript $ERTS_PATH/nodetool $NAME_ARG $COOKIE_ARG" 73 | 74 | # Setup remote shell command to control node 75 | REMSH="$ERTS_PATH/erl $REMSH_NAME_ARG $REMSH_REMSH_ARG $COOKIE_ARG" 76 | 77 | # Check the first argument for instructions 78 | case "$1" in 79 | start) 80 | # Make sure there is not already a node running 81 | RES=`$NODETOOL ping` 82 | if [ "$RES" = "pong" ]; then 83 | echo "Node is already running!" 84 | exit 1 85 | fi 86 | shift # remove $1 87 | RUN_PARAM=$(printf "\'%s\' " "$@") 88 | HEART_COMMAND="$RUNNER_BASE_DIR/bin/$SCRIPT start $RUN_PARAM" 89 | export HEART_COMMAND 90 | mkdir -p $PIPE_DIR 91 | $ERTS_PATH/run_erl -daemon $PIPE_DIR $RUNNER_LOG_DIR "exec $RUNNER_BASE_DIR/bin/$SCRIPT console $RUN_PARAM" 2>&1 92 | ;; 93 | 94 | stop) 95 | # Wait for the node to completely stop... 96 | case `uname -s` in 97 | Linux|Darwin|FreeBSD|DragonFly|NetBSD|OpenBSD) 98 | # PID COMMAND 99 | PID=`ps ax -o pid= -o command=|\ 100 | grep "$RUNNER_BASE_DIR/.*/[b]eam"|awk '{print $1}'` 101 | ;; 102 | SunOS) 103 | # PID COMMAND 104 | PID=`ps -ef -o pid= -o args=|\ 105 | grep "$RUNNER_BASE_DIR/.*/[b]eam"|awk '{print $1}'` 106 | ;; 107 | CYGWIN*) 108 | # UID PID PPID TTY STIME COMMAND 109 | PID=`ps -efW|grep "$RUNNER_BASE_DIR/.*/[b]eam"|awk '{print $2}'` 110 | ;; 111 | esac 112 | $NODETOOL stop 113 | ES=$? 114 | if [ "$ES" -ne 0 ]; then 115 | exit $ES 116 | fi 117 | while `kill -0 $PID 2>/dev/null`; 118 | do 119 | sleep 1 120 | done 121 | ;; 122 | 123 | restart) 124 | ## Restart the VM without exiting the process 125 | $NODETOOL restart 126 | ES=$? 127 | if [ "$ES" -ne 0 ]; then 128 | exit $ES 129 | fi 130 | ;; 131 | 132 | reboot) 133 | ## Restart the VM completely (uses heart to restart it) 134 | $NODETOOL reboot 135 | ES=$? 136 | if [ "$ES" -ne 0 ]; then 137 | exit $ES 138 | fi 139 | ;; 140 | 141 | ping) 142 | ## See if the VM is alive 143 | $NODETOOL ping 144 | ES=$? 145 | if [ "$ES" -ne 0 ]; then 146 | exit $ES 147 | fi 148 | ;; 149 | 150 | attach) 151 | # Make sure a node IS running 152 | RES=`$NODETOOL ping` 153 | ES=$? 154 | if [ "$ES" -ne 0 ]; then 155 | echo "Node is not running!" 156 | exit $ES 157 | fi 158 | 159 | shift 160 | exec $ERTS_PATH/to_erl $PIPE_DIR 161 | ;; 162 | 163 | remote_console) 164 | # Make sure a node IS running 165 | RES=`$NODETOOL ping` 166 | ES=$? 167 | if [ "$ES" -ne 0 ]; then 168 | echo "Node is not running!" 169 | exit $ES 170 | fi 171 | 172 | shift 173 | exec $REMSH 174 | ;; 175 | 176 | upgrade) 177 | if [ -z "$2" ]; then 178 | echo "Missing upgrade package argument" 179 | echo "Usage: $SCRIPT upgrade {package base name}" 180 | echo "NOTE {package base name} MUST NOT include the .tar.gz suffix" 181 | exit 1 182 | fi 183 | 184 | # Make sure a node IS running 185 | RES=`$NODETOOL ping` 186 | ES=$? 187 | if [ "$ES" -ne 0 ]; then 188 | echo "Node is not running!" 189 | exit $ES 190 | fi 191 | 192 | node_name=`echo $NAME_ARG | awk '{print $2}'` 193 | erlang_cookie=`echo $COOKIE_ARG | awk '{print $2}'` 194 | 195 | $ERTS_PATH/escript $RUNNER_BASE_DIR/bin/install_upgrade.escript $node_name $erlang_cookie $2 196 | ;; 197 | 198 | console|console_clean) 199 | # .boot file typically just $SCRIPT (ie, the app name) 200 | # however, for debugging, sometimes start_clean.boot is useful: 201 | case "$1" in 202 | console) BOOTFILE=$SCRIPT ;; 203 | console_clean) BOOTFILE=start_clean ;; 204 | esac 205 | # Setup beam-required vars 206 | ROOTDIR=$RUNNER_BASE_DIR 207 | BINDIR=$ROOTDIR/erts-$ERTS_VSN/bin 208 | EMU=beam 209 | PROGNAME=`echo $0 | sed 's/.*\\///'` 210 | CMD="$BINDIR/erlexec -boot $RUNNER_BASE_DIR/releases/$APP_VSN/$BOOTFILE -mode embedded -config $CONFIG_PATH -args_file $VMARGS_PATH" 211 | export EMU 212 | export ROOTDIR 213 | export BINDIR 214 | export PROGNAME 215 | 216 | # Dump environment info for logging purposes 217 | echo "Exec: $CMD" -- ${1+"$@"} 218 | echo "Root: $ROOTDIR" 219 | 220 | # Log the startup 221 | logger -t "$SCRIPT[$$]" "Starting up" 222 | 223 | # Start the VM 224 | exec $CMD -- ${1+"$@"} 225 | ;; 226 | 227 | foreground) 228 | # start up the release in the foreground for use by runit 229 | # or other supervision services 230 | 231 | BOOTFILE=$SCRIPT 232 | FOREGROUNDOPTIONS="-noinput +Bd" 233 | 234 | # Setup beam-required vars 235 | ROOTDIR=$RUNNER_BASE_DIR 236 | BINDIR=$ROOTDIR/erts-$ERTS_VSN/bin 237 | EMU=beam 238 | PROGNAME=`echo $0 | sed 's/.*\///'` 239 | CMD="$BINDIR/erlexec $FOREGROUNDOPTIONS -boot $RUNNER_BASE_DIR/releases/$APP_VSN/$BOOTFILE -config $CONFIG_PATH -args_file $VMARGS_PATH" 240 | export EMU 241 | export ROOTDIR 242 | export BINDIR 243 | export PROGNAME 244 | 245 | # Dump environment info for logging purposes 246 | echo "Exec: $CMD" -- ${1+"$@"} 247 | echo "Root: $ROOTDIR" 248 | 249 | # Start the VM 250 | exec $CMD -- ${1+"$@"} 251 | ;; 252 | *) 253 | echo "Usage: $SCRIPT {start|foreground|stop|restart|reboot|ping|console|console_clean|attach|remote_console|upgrade}" 254 | exit 1 255 | ;; 256 | esac 257 | 258 | exit 0 259 | -------------------------------------------------------------------------------- /rel/files/openpoker.cmd: -------------------------------------------------------------------------------- 1 | @setlocal 2 | 3 | @set node_name=openpoker 4 | 5 | @rem Get the absolute path to the parent directory, 6 | @rem which is assumed to be the node root. 7 | @for /F "delims=" %%I in ("%~dp0..") do @set node_root=%%~fI 8 | 9 | @set releases_dir=%node_root%\releases 10 | 11 | @rem Parse ERTS version and release version from start_erl.data 12 | @for /F "tokens=1,2" %%I in (%releases_dir%\start_erl.data) do @( 13 | @call :set_trim erts_version %%I 14 | @call :set_trim release_version %%J 15 | ) 16 | 17 | @rem extract erlang cookie from vm.args 18 | @set vm_args=%releases_dir%\%release_version%\vm.args 19 | @for /f "usebackq tokens=1-2" %%I in (`findstr /b \-setcookie %vm_args%`) do @set erlang_cookie=%%J 20 | 21 | @set erts_bin=%node_root%\erts-%erts_version%\bin 22 | 23 | @set service_name=%node_name%_%release_version% 24 | 25 | @if "%1"=="usage" @goto usage 26 | @if "%1"=="install" @goto install 27 | @if "%1"=="uninstall" @goto uninstall 28 | @if "%1"=="start" @goto start 29 | @if "%1"=="stop" @goto stop 30 | @if "%1"=="restart" @call :stop && @goto start 31 | @if "%1"=="console" @goto console 32 | @if "%1"=="query" @goto query 33 | @if "%1"=="attach" @goto attach 34 | @if "%1"=="upgrade" @goto upgrade 35 | @echo Unknown command: "%1" 36 | 37 | :usage 38 | @echo Usage: %~n0 [install^|uninstall^|start^|stop^|restart^|console^|query^|attach^|upgrade] 39 | @goto :EOF 40 | 41 | :install 42 | @%erts_bin%\erlsrv.exe add %service_name% -c "Erlang node %node_name% in %node_root%" -sname %node_name% -w %node_root% -m %node_root%\bin\start_erl.cmd -args " ++ %node_name% ++ %node_root%" -stopaction "init:stop()." 43 | @goto :EOF 44 | 45 | :uninstall 46 | @%erts_bin%\erlsrv.exe remove %service_name% 47 | @%erts_bin%\epmd.exe -kill 48 | @goto :EOF 49 | 50 | :start 51 | @%erts_bin%\erlsrv.exe start %service_name% 52 | @goto :EOF 53 | 54 | :stop 55 | @%erts_bin%\erlsrv.exe stop %service_name% 56 | @goto :EOF 57 | 58 | :console 59 | @start %erts_bin%\werl.exe -boot %releases_dir%\%release_version%\%node_name% -config %releases_dir%\%release_version%\sys.config -args_file %vm_args% -sname %node_name% 60 | @goto :EOF 61 | 62 | :query 63 | @%erts_bin%\erlsrv.exe list %service_name% 64 | @exit /b %ERRORLEVEL% 65 | @goto :EOF 66 | 67 | :attach 68 | @for /f "usebackq" %%I in (`hostname`) do @set hostname=%%I 69 | start %erts_bin%\werl.exe -boot %releases_dir%\%release_version%\start_clean -remsh %node_name%@%hostname% -sname console -setcookie %erlang_cookie% 70 | @goto :EOF 71 | 72 | :upgrade 73 | @if "%2"=="" ( 74 | @echo Missing upgrade package argument 75 | @echo Usage: %~n0 upgrade {package base name} 76 | @echo NOTE {package base name} MUST NOT include the .tar.gz suffix 77 | @goto :EOF 78 | ) 79 | @%erts_bin%\escript.exe %node_root%\bin\install_upgrade.escript %node_name% %erlang_cookie% %2 80 | @goto :EOF 81 | 82 | :set_trim 83 | @set %1=%2 84 | @goto :EOF 85 | -------------------------------------------------------------------------------- /rel/files/start_erl.cmd: -------------------------------------------------------------------------------- 1 | @setlocal 2 | 3 | @rem Parse arguments. erlsrv.exe prepends erl arguments prior to first ++. 4 | @rem Other args are position dependent. 5 | @set args="%*" 6 | @for /F "delims=++ tokens=1,2,3" %%I in (%args%) do @( 7 | @set erl_args=%%I 8 | @call :set_trim node_name %%J 9 | @call :set_trim node_root %%K 10 | ) 11 | 12 | @set releases_dir=%node_root%\releases 13 | 14 | @rem parse ERTS version and release version from start_erl.dat 15 | @for /F "tokens=1,2" %%I in (%releases_dir%\start_erl.data) do @( 16 | @call :set_trim erts_version %%I 17 | @call :set_trim release_version %%J 18 | ) 19 | 20 | @set erl_exe=%node_root%\erts-%erts_version%\bin\erl.exe 21 | @set boot_file=%releases_dir%\%release_version%\%node_name% 22 | 23 | @if exist %releases_dir%\%release_version%\sys.config ( 24 | @set app_config=%releases_dir%\%release_version%\sys.config 25 | ) else ( 26 | @set app_config=%node_root%\etc\app.config 27 | ) 28 | 29 | @if exist %releases_dir%\%release_version%\vm.args ( 30 | @set vm_args=%releases_dir%\%release_version%\vm.args 31 | ) else ( 32 | @set vm_args=%node_root%\etc\vm.args 33 | ) 34 | 35 | @%erl_exe% %erl_args% -boot %boot_file% -config %app_config% -args_file %vm_args% 36 | 37 | :set_trim 38 | @set %1=%2 39 | @goto :EOF 40 | -------------------------------------------------------------------------------- /rel/files/vm.args: -------------------------------------------------------------------------------- 1 | ## Name of the node 2 | -name openpoker@127.0.0.1 3 | 4 | ## Cookie for distributed erlang 5 | -setcookie openpoker 6 | 7 | ## Heartbeat management; auto-restarts VM if it dies or becomes unresponsive 8 | ## (Disabled by default..use with caution!) 9 | ##-heart 10 | 11 | ## Enable kernel poll and a few async threads 12 | ##+K true 13 | ##+A 5 14 | 15 | ## Increase number of concurrent ports/sockets 16 | ##-env ERL_MAX_PORTS 4096 17 | 18 | ## Tweak GC to run more often 19 | ##-env ERL_FULLSWEEP_AFTER 10 20 | -------------------------------------------------------------------------------- /rel/reltool.config: -------------------------------------------------------------------------------- 1 | {sys, [ 2 | {lib_dirs, ["../deps/webtekcos/", "../deps/mochiweb/"]}, 3 | {erts, [{mod_cond, derived}, {app_file, strip}]}, 4 | {app_file, strip}, 5 | {rel, "openpoker", "1.0.0", 6 | [ 7 | kernel, 8 | stdlib, 9 | sasl, 10 | mnesia, 11 | webtekcos, 12 | openpoker 13 | ]}, 14 | {rel, "start_clean", "", 15 | [ 16 | kernel, 17 | stdlib 18 | ]}, 19 | {boot_rel, "openpoker"}, 20 | {profile, embedded}, 21 | {incl_cond, exclude}, 22 | {excl_archive_filters, [".*"]}, %% Do not archive built libs 23 | {excl_sys_filters, ["^bin/.*", "^erts.*/bin/(dialyzer|typer)", 24 | "^erts.*/(doc|info|include|lib|man|src)"]}, 25 | {excl_app_filters, ["\.gitignore"]}, 26 | {app, stdlib, [{incl_cond, include}]}, 27 | {app, kernel, [{incl_cond, include}]}, 28 | {app, sasl, [{incl_cond, include}]}, 29 | {app, crypto, [{incl_cond, include}]}, 30 | 31 | %% comment below line before release to production 32 | {app, gs, [{incl_cond, include}]}, 33 | {app, appmon, [{incl_cond, include}]}, 34 | 35 | {app, mnesia, [{incl_cond, include}]}, 36 | {app, mochiweb, [{incl_cond, include}, {lib_dir, "../deps/mochiweb/"}]}, 37 | {app, webtekcos, [{incl_cond, include}, {lib_dir, "../deps/webtekcos/"}]}, 38 | {app, openpoker, [{incl_cond, include}, {lib_dir, ".."}]} 39 | ]}. 40 | 41 | {target_dir, "openpoker"}. 42 | 43 | {overlay, [ 44 | {mkdir, "log/sasl"}, 45 | {copy, "files/erl", "\{\{erts_vsn\}\}/bin/erl"}, 46 | {copy, "files/nodetool", "\{\{erts_vsn\}\}/bin/nodetool"}, 47 | {copy, "files/openpoker", "bin/openpoker"}, 48 | {copy, "files/openpoker.cmd", "bin/openpoker.cmd"}, 49 | {copy, "files/start_erl.cmd", "bin/start_erl.cmd"}, 50 | {copy, "files/install_upgrade.escript", "bin/install_upgrade.escript"}, 51 | {copy, "files/sys.config", "releases/\{\{rel_vsn\}\}/sys.config"}, 52 | {copy, "files/vm.args", "releases/\{\{rel_vsn\}\}/vm.args"} 53 | ]}. 54 | -------------------------------------------------------------------------------- /src/bits.erl: -------------------------------------------------------------------------------- 1 | -module(bits). 2 | -export([bits0/1, bits1/1, log2/1, tzb0/1, tzb1/1, clear_extra_bits/2]). 3 | 4 | %%% Courtesy of Mark Scandariato 5 | 6 | %% number of bits set 7 | %% bits0 works on arbitrary integers. 8 | 9 | bits0(N) when N >= 0 -> 10 | bits0(N, 0). 11 | 12 | bits0(0, C) -> C; 13 | bits0(N, C) -> 14 | bits0((N band (N-1)), C+1). 15 | 16 | clear_extra_bits(N, _) when N =:= 0 -> 17 | N; 18 | 19 | clear_extra_bits(N, X) -> 20 | C = bits0(N), 21 | if 22 | X >= C -> 23 | N; 24 | true -> 25 | clear_extra_bits(N, X, C) 26 | end. 27 | 28 | clear_extra_bits(N, X, C) when N =:= 0; C =:= 0; X =:= C -> 29 | N; 30 | clear_extra_bits(N, X, C) -> 31 | clear_extra_bits(N band (N - 1), X, C - 1). 32 | 33 | bits1(0) -> 0; 34 | bits1(N) when N > 0, N < 16#100000000 -> 35 | bits1(N, [1, 16#55555555, 36 | 2, 16#33333333, 37 | 4, 16#0F0F0F0F, 38 | 8, 16#00FF00FF, 39 | 16, 16#0000FFFF]). 40 | 41 | bits1(N, []) -> N; 42 | bits1(N, [S, B|Magic]) -> 43 | bits1(((N bsr S) band B) + (N band B), Magic). 44 | 45 | 46 | %% log2, aka, position of the high bit 47 | 48 | log2(N) when N > 0, N < 16#100000000 -> 49 | log2(N, 0, [16, 16#FFFF0000, 8, 16#FF00, 4, 16#F0, 2, 16#C, 1, 16#2]). 50 | 51 | log2(_, C, []) -> C; 52 | log2(N, C, [S,B|Magic]) -> 53 | if (N band B) == 0 -> log2(N, C, Magic); 54 | true -> log2((N bsr S), (C bor S), Magic) 55 | end. 56 | 57 | %% trailing zero bits, aka position of the lowest set bit. 58 | 59 | tzb0(N) when N > 0, N < 16#100000000 -> 60 | tzb0(N, 32, [16, 16#0000FFFF, 61 | 8, 16#00FF00FF, 62 | 4, 16#0F0F0F0F, 63 | 2, 16#33333333, 64 | 1, 16#55555555]). 65 | 66 | tzb0(_, Z, []) -> Z-1; 67 | tzb0(N, Z, [S, B|Magic]) -> 68 | if (N band B) == 0 -> tzb0(N, Z, Magic); 69 | true -> tzb0((N bsl S), (Z - S), Magic) 70 | end. 71 | 72 | tzb1(N) when N > 0, N < 16#100000000 -> 73 | Mod = {32,0,1,26,2,23,27,0,3,16,24,30, 74 | 28,11,0,13,4,7,17,0,25,22,31,15, 75 | 29,10,12,6,0,21,14,9,5,20,8,19,18}, 76 | P = (-N band N) rem 37, 77 | element(P+1, Mod). 78 | -------------------------------------------------------------------------------- /src/client.erl: -------------------------------------------------------------------------------- 1 | -module(client). 2 | -export([loop/2, send/1, send/2]). 3 | 4 | -include("openpoker.hrl"). 5 | 6 | -record(pdata, { 7 | timer = ?UNDEF, 8 | server = global:whereis_name(server), 9 | player = ?UNDEF 10 | }). 11 | 12 | loop(connected, ?UNDEF) -> 13 | #pdata{timer = erlang:start_timer(?CONNECT_TIMEOUT, self(), ?MODULE)}; 14 | 15 | loop({connected, _Timeout}, ?UNDEF) -> 16 | #pdata{timer = erlang:start_timer(?CONNECT_TIMEOUT, self(), ?MODULE)}; 17 | 18 | loop(disconnected, _Data = #pdata{}) -> ok; 19 | 20 | loop({loopdata, player}, Data = #pdata{}) -> 21 | Data#pdata.player; 22 | 23 | loop({recv, Bin}, Data = #pdata{}) when is_binary(Bin) -> 24 | case catch protocol:read(Bin) of 25 | {'EXIT', {Reason, Stack}} -> 26 | ?LOG([{recv, Bin}, {error, {Reason, Stack}}]), 27 | err(?ERR_DATA); 28 | R -> 29 | loop({protocol, R}, Data) 30 | end; 31 | 32 | % cancel connection timer when remote client first send protocol must #login 33 | loop({protocol, R = #cmd_login{}}, Data = #pdata{timer = T}) when T /= ?UNDEF -> 34 | catch erlang:cancel_timer(T), 35 | loop({protocol, R}, Data#pdata{timer = ?UNDEF}); 36 | 37 | loop({protocol, #cmd_login{identity = Identity, password = Password}}, Data) -> 38 | case player:auth(binary_to_list(Identity), binary_to_list(Password)) of 39 | {ok, unauth} -> 40 | err(?ERR_UNAUTH); 41 | {ok, player_disable} -> 42 | err(?ERR_PLAYER_DISABLE); 43 | {ok, pass, Info} -> 44 | % create player process by client process, 45 | % receive {'EXIT'} when player process error 46 | case player:start(Info) of 47 | {ok, Player} when is_pid(Player) -> 48 | player:client(Player), 49 | player:info(Player), 50 | player:balance(Player), 51 | Data#pdata{player = Player} 52 | end 53 | end; 54 | 55 | loop({protocol, #cmd_logout{}}, #pdata{player = Player}) when is_pid(Player) -> 56 | {ok, logout} = player:logout(Player), 57 | close_connection(); 58 | 59 | loop({protocol, #cmd_logout{}}, _Data) -> 60 | err(?ERR_PROTOCOL); 61 | 62 | loop({protocol, #cmd_query_game{}}, Data = #pdata{player = Player}) when is_pid(Player) -> 63 | Infos = game:list(), 64 | lists:map(fun(Info) -> send(Info) end, Infos), 65 | Data; 66 | 67 | loop({protocol, R}, Data = #pdata{player = Player}) when is_pid(Player) -> 68 | player:cast(Player, R), 69 | Data; 70 | 71 | loop({msg, {timeout, _, ?MODULE}}, _Data) -> 72 | err(?ERR_CONNECTION_TIMEOUT). 73 | 74 | %%% 75 | %%% client 76 | %%% 77 | 78 | send(R) -> 79 | op_game_handler:send(R). 80 | 81 | send(PID, R) when is_pid(PID), is_tuple(R) -> 82 | op_game_handler:send(PID, R). 83 | 84 | %%% 85 | %%% private 86 | %%% 87 | 88 | err(Code) when is_integer(Code) -> 89 | send(#notify_error{error = Code}), 90 | close_connection(). 91 | 92 | close_connection() -> 93 | self() ! close. 94 | -------------------------------------------------------------------------------- /src/counter.erl: -------------------------------------------------------------------------------- 1 | -module(counter). 2 | 3 | -export([bump/1, bump/2, reset/1, reset/2]). 4 | 5 | -include("openpoker.hrl"). 6 | 7 | bump(Type) -> 8 | bump(Type, 1). 9 | 10 | bump(Type, Inc) -> 11 | mnesia:dirty_update_counter(tab_counter, Type, Inc). 12 | 13 | reset(Type) -> 14 | reset(Type, 0). 15 | 16 | reset(Type, Count) -> 17 | Counter = #tab_counter { 18 | type = Type, 19 | value = Count 20 | }, 21 | ok = mnesia:dirty_write(Counter). 22 | 23 | -------------------------------------------------------------------------------- /src/handler/op_game_handler.erl: -------------------------------------------------------------------------------- 1 | -module(op_game_handler). 2 | -behaviour(webtekcos). 3 | 4 | -export([connect/0, connect/1, disconnect/1, handle_message/2, handle_data/2]). 5 | 6 | -export([send/1, send/2]). 7 | 8 | -include("openpoker.hrl"). 9 | 10 | -record(pdata, { 11 | connection_timer = ?UNDEF, 12 | player = ?UNDEF, 13 | player_info = ?UNDEF 14 | }). 15 | 16 | connect() -> 17 | connect(?CONNECT_TIMEOUT). 18 | 19 | connect(ConnectTimeout) -> 20 | Timer = erlang:start_timer(ConnectTimeout, self(), ?MODULE), 21 | #pdata{connection_timer = Timer}. 22 | 23 | disconnect(#pdata{player = Player}) when is_pid(Player) -> 24 | player:phantom(Player), 25 | ok; 26 | 27 | disconnect(_) -> 28 | ok. 29 | 30 | handle_message({timeout, _, ?MODULE}, LoopData = #pdata{connection_timer =T}) when T =:= ?UNDEF -> 31 | LoopData; 32 | 33 | handle_message({timeout, _, ?MODULE}, _LoopData) -> 34 | send(#notify_error{error = ?ERR_CONNECTION_TIMEOUT}), 35 | webtekcos:close(). 36 | 37 | handle_data(Code, LoopData) when is_binary(Code) -> 38 | case catch protocol:read(base64:decode(Code)) of 39 | {'EXIT', {_Reason, _Stack}} -> 40 | error_logger:error_report({protocol, read, Code}), 41 | send(#notify_error{error = ?ERR_DATA}), 42 | webtekcos:close(), 43 | LoopData; 44 | R -> 45 | handle_protocol(R, LoopData) 46 | end; 47 | 48 | handle_data(Code, LoopData) when is_list(Code) -> 49 | handle_data(list_to_binary(Code), LoopData). 50 | 51 | %%%% 52 | %%%% handle internal protocol 53 | %%%% 54 | 55 | handle_protocol(R = #cmd_login{}, LoopData = #pdata{connection_timer =T}) when T /= ?UNDEF -> 56 | catch erlang:cancel_connection_timer(T), 57 | handle_protocol(R, LoopData#pdata{connection_timer = ?UNDEF}); 58 | 59 | handle_protocol(#cmd_login{identity = Identity, password = Password}, LoopData) -> 60 | case player:auth(binary_to_list(Identity), binary_to_list(Password)) of 61 | {ok, unauth} -> 62 | send(#notify_error{error = ?ERR_UNAUTH}), 63 | webtekcos:close(); 64 | {ok, player_disable} -> 65 | send(#notify_error{error = ?ERR_PLAYER_DISABLE}), 66 | webtekcos:close(); 67 | {ok, pass, Info} -> 68 | % create player process by client process, 69 | % receive {'EXIT'} when player process error 70 | case op_players_sup:start_child(Info) of 71 | {ok, Player} -> 72 | player:client(Player), 73 | player:info(Player), 74 | player:balance(Player), 75 | send(#notify_signin{player = Info#tab_player_info.pid}), 76 | LoopData#pdata{player = Player, player_info = Info}; 77 | {error, already_present} -> 78 | send(#notify_error{error = ?ERR_PLAYER_BUSY}), 79 | webtekcos:close(); 80 | {error, {already_started, _Player}} -> 81 | send(#notify_error{error = ?ERR_PLAYER_BUSY}), 82 | webtekcos:close() 83 | end 84 | end; 85 | 86 | handle_protocol(#cmd_logout{}, #pdata{player = Player, player_info = Info}) when is_pid(Player) -> 87 | op_players_sup:terminate_child(Info#tab_player_info.pid), 88 | webtekcos:close(); 89 | 90 | handle_protocol(#cmd_query_game{}, LoopData = #pdata{player = Player}) when is_pid(Player) -> 91 | GamesList = game:list(), 92 | lists:map(fun(Info) -> send(Info) end, GamesList), 93 | send(#notify_games_list_end{size = length(GamesList)}), 94 | LoopData; 95 | 96 | handle_protocol(R, LoopData = #pdata{player = Player}) when is_pid(Player) -> 97 | player:cast(Player, R), 98 | LoopData; 99 | 100 | handle_protocol(R, LoopData) -> 101 | error_logger:warning_report({undef_protocol, R, LoopData}), 102 | send(#notify_error{error = ?ERR_PROTOCOL}), 103 | webtekcos:close(). 104 | 105 | send(R) -> send(self(), R). 106 | 107 | send(PID, R) -> 108 | %io:format("~n===============================~n~p~n", [R]), 109 | case catch protocol:write(R) of 110 | {'EXIT', Raise} -> 111 | error_logger:error_report({protocol, write, R, Raise}); 112 | Data -> 113 | webtekcos:send_data(PID, base64:encode(list_to_binary(Data))) 114 | end. 115 | -------------------------------------------------------------------------------- /src/handler/op_protocol_handler.erl: -------------------------------------------------------------------------------- 1 | -module(op_protocol_handler). 2 | -export([connect/0, disconnect/1, handle_message/2, handle_data/2]). 3 | -export([loop/0]). 4 | 5 | -include("openpoker.hrl"). 6 | 7 | connect() -> 8 | console(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"), 9 | put(convert_id_to_process, true). 10 | 11 | disconnect(_) -> 12 | console("<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"). 13 | 14 | handle_message(Msg, _LoopData) -> console([op_protocol_msg, Msg]). 15 | 16 | handle_data(Data, _LoopData) when is_list(Data) -> 17 | Bin = base64:decode(list_to_binary(Data)), 18 | console([handle_data, Bin]), 19 | Result = protocol:read(Bin), 20 | console([handle_protocol, Result]), 21 | 22 | case Result of 23 | {'EXIT', {Reason, Stack}} -> 24 | ?LOG([{handle_data_error, {Reason, Stack}}]), 25 | send(#notify_error{error = ?ERR_DATA}), 26 | webtekcos:close(); 27 | R -> 28 | send(R) 29 | end. 30 | 31 | console(R) -> 32 | io:format("===> ~p~n", [R]). 33 | 34 | send(R) -> 35 | D = protocol:write(R), 36 | Bin = list_to_binary(D), 37 | Encode = base64:encode(Bin), 38 | webtekcos:send_data(Encode). 39 | 40 | loop() -> 41 | receive 42 | _ -> ok 43 | end. 44 | -------------------------------------------------------------------------------- /src/mods/deal_cards.erl: -------------------------------------------------------------------------------- 1 | -module(deal_cards). 2 | -behaviour(op_exch_mod). 3 | -export([start/2, dispatch/2]). 4 | 5 | -include("openpoker.hrl"). 6 | 7 | start([N, Type], Ctx) -> 8 | start([N, Type, 0], Ctx); 9 | start([0, private, _T], Ctx) -> {stop, Ctx}; 10 | start([N, private, T], Ctx = #texas{b = B, seats = S}) -> 11 | Seats = seat:lookup(?PS_STANDING, S, B), 12 | start([N-1, private], draw(Seats, Ctx#texas{deal_timeout = T})); 13 | 14 | start([0, shared, _T], Ctx) -> {stop, Ctx}; 15 | start([N, shared, T], Ctx) -> 16 | start([N-1, shared, T], draw_shared(Ctx#texas{deal_timeout = T})). 17 | 18 | dispatch(_R, _Ctx) -> 19 | ok. 20 | 21 | %%% 22 | %%% private 23 | %%% 24 | 25 | draw([], Ctx) -> Ctx; 26 | draw([H = #seat{hand = Hand, pid = PId, sn = SN, process = P, identity = Identity}|T], Ctx = #texas{gid = Id, deck = D, seats = S}) -> 27 | {Card, ND} = deck:draw(D), 28 | NS = H#seat{ hand = hand:add(Hand, Card) }, 29 | player:notify(P, #notify_private{game = Id, player = PId, sn = SN, card = Card}), 30 | game:broadcast(#notify_draw{game = Id, player = PId, sn = SN, card = 0}, Ctx, [Identity]), 31 | timer:sleep(Ctx#texas.deal_timeout), 32 | draw(T, Ctx#texas{ seats = seat:set(NS, S), deck = ND}). 33 | 34 | draw_shared(Ctx = #texas{gid = Id, deck = D, board = B}) -> 35 | {Card, ND} = deck:draw(D), 36 | game:broadcast(#notify_shared{ game = Id, card = Card }, Ctx), 37 | timer:sleep(Ctx#texas.deal_timeout), 38 | Ctx#texas{ board = [Card|B], deck = ND }. 39 | -------------------------------------------------------------------------------- /src/mods/op_mod_betting.erl: -------------------------------------------------------------------------------- 1 | -module(op_mod_betting). 2 | -behaviour(op_exch_mod). 3 | -export([start/2, dispatch/2, betting/2]). 4 | 5 | -include("openpoker.hrl"). 6 | 7 | -define(Z, 0). 8 | 9 | %%% 10 | %%% callback 11 | %%% 12 | 13 | start([?GS_PREFLOP], Ctx = #texas{bb = At}) -> 14 | game:broadcast(#notify_stage{game = Ctx#texas.gid, stage = ?GS_PREFLOP}, Ctx), 15 | ask(At, Ctx#texas{stage = ?GS_PREFLOP, max_betting = Ctx#texas.bb_amt}); 16 | 17 | start([Stage], Ctx = #texas{b = At}) -> 18 | game:broadcast(#notify_stage{game = Ctx#texas.gid, stage = Stage}, Ctx), 19 | ask(At, Ctx#texas{stage = Stage, max_betting = 0}). 20 | 21 | dispatch(_R, _Ctx) -> 22 | ok. 23 | 24 | betting({timeout, _, ?MODULE}, Ctx = #texas{exp_seat = Exp}) -> 25 | NotTimerCtx = cancel_timer(Ctx), 26 | betting(#cmd_fold{ pid = Exp#seat.pid }, NotTimerCtx); 27 | 28 | %%% 29 | %%% cmd_raise 30 | %%% 31 | 32 | % not expectation seat player 33 | betting(#cmd_raise{pid = ExpPId}, Ctx = #texas{exp_seat = Exp}) 34 | when ExpPId /= Exp#seat.pid -> 35 | {continue, Ctx}; 36 | 37 | %%% all-in - less call 38 | betting(#cmd_raise{ amount = ?Z }, Ctx = #texas{exp_seat = Exp, exp_call = ?Z, exp_min = ?Z, exp_max = ?Z}) -> 39 | NotTimerCtx = cancel_timer(Ctx), 40 | PooredCtx = game:bet({Exp, Exp#seat.inplay}, NotTimerCtx), 41 | next_turn(Exp, PooredCtx); 42 | 43 | %%% check or call 44 | betting(#cmd_raise{ amount = ?Z }, Ctx = #texas{exp_seat = Exp, exp_call = ExpCall}) -> 45 | NotTimerCtx = cancel_timer(Ctx), 46 | CheckedCtx = game:bet({Exp, ExpCall}, NotTimerCtx), 47 | next_turn(Exp, CheckedCtx); 48 | 49 | %%% fold other zero cmd_raise 50 | betting(#cmd_raise{ pid = Pid, amount = ?Z }, Ctx) -> 51 | NotTimerCtx = cancel_timer(Ctx), 52 | betting(#cmd_fold{ pid = Pid }, NotTimerCtx); 53 | 54 | %%% raise to all-in 55 | betting(R = #cmd_raise{ amount = Max }, Ctx = #texas{exp_min = ?Z, exp_max = Max}) -> 56 | raise(R, Ctx); 57 | 58 | %%% normal raise, perhaps to all-in 59 | betting(R = #cmd_raise{ amount = Raise }, Ctx = #texas{exp_min = Min, exp_max = Max}) 60 | when Min /= ?Z, Raise >= Min, Raise =< Max -> 61 | raise(R, Ctx); 62 | 63 | betting(#cmd_raise{pid = Pid}, Ctx) -> 64 | NotTimerCtx = cancel_timer(Ctx), 65 | betting(#cmd_fold{ pid = Pid }, NotTimerCtx); 66 | 67 | %%% 68 | %%% fold 69 | %%% 70 | 71 | betting(#cmd_fold{pid = PId}, Ctx = #texas{exp_seat = Exp}) 72 | when PId /= Exp#seat.pid -> 73 | {continue, Ctx}; 74 | 75 | betting(#cmd_fold{}, Ctx = #texas{seats = S, exp_seat = Exp}) -> 76 | NotTimerCtx = cancel_timer(Ctx), 77 | FoldCtx = NotTimerCtx#texas{seats = seat:set(Exp#seat.sn, ?PS_FOLD, S)}, 78 | game:broadcast(#notify_fold{game = Ctx#texas.gid, sn = Exp#seat.sn}, FoldCtx), 79 | next_turn(Exp, FoldCtx); 80 | 81 | %%% 82 | %%% leave 83 | %%% 84 | 85 | betting(R = #cmd_leave{pid = PId}, Ctx = #texas{seats = S, exp_seat = Exp}) 86 | when PId =:= Exp#seat.pid -> 87 | NotTimerCtx = cancel_timer(Ctx), 88 | FoldCtx = NotTimerCtx#texas{seats = seat:set(Exp#seat.sn, ?PS_FOLD, S)}, 89 | game:broadcast(#notify_fold{game = Ctx#texas.gid, sn = Exp#seat.sn}, FoldCtx), 90 | LeavedCtx = game:do_leave(R, FoldCtx), 91 | next_turn(Exp, LeavedCtx); 92 | 93 | % skip 94 | betting(_Msg, Ctx) -> 95 | {skip, Ctx}. 96 | 97 | %%% 98 | %%% private 99 | %%% 100 | 101 | raise(#cmd_raise{amount = R}, Ctx = #texas{exp_seat = Exp}) -> 102 | NotTimerCtx = cancel_timer(Ctx), 103 | BettedCtx = game:bet({Exp, Ctx#texas.exp_call, R}, NotTimerCtx), 104 | BettedSeats = seat:lookup(?PS_BET, BettedCtx#texas.seats), 105 | ResetedSeats = reset_seat(Exp#seat.sn, BettedSeats, BettedCtx#texas.seats), 106 | next_turn(Exp, BettedCtx#texas{ 107 | max_betting = BettedCtx#texas.max_betting + R, 108 | seats = ResetedSeats}). 109 | 110 | ask(At = #seat{}, Ctx = #texas{seats = S}) -> 111 | ask(seat:lookup(?PS_PLAY, S, At), Ctx); 112 | ask([], Ctx = #texas{}) -> 113 | {stop, Ctx}; 114 | ask([_H], Ctx = #texas{}) -> 115 | {stop, Ctx}; 116 | ask([H|_], Ctx = #texas{}) -> 117 | ask_for_bet(H, Ctx). 118 | 119 | 120 | get_min_raise(BB, _TurnMaxBet = ?Z) -> BB; 121 | get_min_raise(_BB, TurnMaxBet) -> TurnMaxBet. 122 | 123 | ask_for_bet(H = #seat{sn = SN, pid = PID, inplay = Inplay}, Ctx = #texas{gid = Id, max_betting = TurnMaxBet}) -> 124 | ExpCall = TurnMaxBet - H#seat.bet, 125 | MinRaise = get_min_raise(Ctx#texas.bb_amt, TurnMaxBet), 126 | 127 | [Call, Min, Max] = case Inplay of 128 | Inplay when Inplay < ExpCall -> 129 | [?Z, ?Z, ?Z]; %% ONLY All-in 130 | Inplay -> 131 | case Inplay - ExpCall of 132 | R when R < MinRaise -> 133 | [ExpCall, ?Z, R]; %% Call or All-in, Don't Raise 134 | R when R >= MinRaise -> 135 | [ExpCall, MinRaise, R] %% Call or Raise or All-in 136 | end 137 | end, 138 | 139 | game:broadcast(#notify_actor{ game = Id, player = PID, sn = SN, timeout = Ctx#texas.timeout }, Ctx), 140 | player:notify(H#seat.process, #notify_betting{ game = Id, player = PID, sn = SN, call = Call, min = Min, max = Max}), 141 | TimerCtx = start_timer(Ctx), 142 | ExpCtx = TimerCtx#texas{ exp_seat = H, exp_call = Call, exp_min = Min, exp_max = Max }, 143 | {next, betting, ExpCtx}. 144 | 145 | next_turn(At = #seat{}, Ctx = #texas{seats = S}) -> 146 | Active = seat:lookup(?PS_PLAY, S, At), 147 | Standing = seat:lookup(?PS_STANDING, S, At), 148 | next_turn(Standing, Active, Ctx). 149 | 150 | next_turn(Standing, _, Ctx) when length(Standing) < 2 -> 151 | game:broadcast(#notify_stage_end{game = Ctx#texas.gid, stage = Ctx#texas.stage}, Ctx), 152 | {goto, showdown, Ctx}; 153 | next_turn(_, [], Ctx = #texas{pot = Pot, seats = Seats}) -> 154 | NewPot = pot:new_stage(Pot), 155 | ResetSeats = reset_seat(seat:lookup(?PS_BET, Seats), Seats), 156 | game:broadcast(#notify_stage_end{game = Ctx#texas.gid, stage = Ctx#texas.stage}, Ctx), 157 | {stop, Ctx#texas{seats = ResetSeats, pot = NewPot}}; 158 | next_turn(_, [H|_], Ctx) -> 159 | ask_for_bet(H, Ctx). 160 | 161 | start_timer(Ctx) -> 162 | game:start_timer(Ctx, ?MODULE). 163 | 164 | cancel_timer(Ctx) -> 165 | game:cancel_timer(Ctx). 166 | 167 | reset_seat([], Seats) -> Seats; 168 | reset_seat([H|T], Seats) -> 169 | reset_seat(T, seat:set(H#seat{state = ?PS_PLAY, bet = ?Z}, Seats)). 170 | 171 | reset_seat(_SN, [], Seats) -> Seats; 172 | reset_seat(SN, [#seat{sn = SN}|T], Seats) -> 173 | reset_seat(SN, T, Seats); 174 | reset_seat(SN, [H = #seat{}|T], Seats) -> 175 | reset_seat(SN, T, seat:set(H#seat{state = ?PS_PLAY}, Seats)). 176 | -------------------------------------------------------------------------------- /src/mods/op_mod_blinds.erl: -------------------------------------------------------------------------------- 1 | -module(op_mod_blinds). 2 | -behaviour(op_exch_mod). 3 | -export([start/2, dispatch/2]). 4 | 5 | -include("openpoker.hrl"). 6 | -include_lib("eunit/include/eunit.hrl"). 7 | 8 | %%% 9 | %%% callback 10 | %%% 11 | 12 | start([], Ctx = #texas{gid = Id, limit = Limit}) -> 13 | {SmallAmt, BigAmt} = {Limit#limit.small, Limit#limit.big}, 14 | 15 | Button = advance_button(Ctx), 16 | game:broadcast(#notify_button{ game = Id, b = Button#seat.sn }, Ctx), 17 | 18 | {Small, Big, Headsup} = advance_blinds(Button, Ctx), 19 | game:broadcast(#notify_sb{ game = Id, sb = Small#seat.sn }, Ctx), 20 | game:broadcast(#notify_bb{ game = Id, bb = Big#seat.sn }, Ctx), 21 | 22 | BlindedCtx = blind([{Small, SmallAmt}, {Big, BigAmt}], Ctx), 23 | 24 | {stop, BlindedCtx#texas{ 25 | sb_amt = SmallAmt, bb_amt = BigAmt, 26 | b = Button, sb = Small, bb = Big, headsup = Headsup 27 | }}. 28 | 29 | dispatch(_R, _Ctx) -> 30 | ok. 31 | 32 | %% 33 | %% private 34 | %% 35 | 36 | advance_button(#texas{b = B, seats = Seats}) when B =:= ?UNDEF -> 37 | [H|_] = seat:lookup(?PS_PLAY, Seats), H; 38 | advance_button(#texas{b = B, seats = Seats}) -> 39 | [H|_] = seat:lookup(?PS_PLAY, Seats, B), H. 40 | 41 | advance_blinds(B, #texas{seats = Seats}) -> 42 | case seat:lookup(?PS_PLAY, Seats, B) of 43 | [Hb|[B]] -> % headsup game whit two player (button is small) 44 | {B, Hb, true}; 45 | [Hs|[Hb|_]] -> 46 | {Hs, Hb, false} 47 | end. 48 | 49 | blind([], Ctx) -> Ctx; 50 | blind([{Seat = #seat{sn = SN}, Amt}|T], Ctx = #texas{}) -> 51 | NewCtx = game:bet({Seat, Amt}, Ctx), 52 | RecoverSeats = seat:set(SN, ?PS_PLAY, NewCtx#texas.seats), 53 | blind(T, NewCtx#texas{max_betting = Amt, seats = RecoverSeats}). 54 | -------------------------------------------------------------------------------- /src/mods/op_mod_suspend.erl: -------------------------------------------------------------------------------- 1 | -module(op_mod_suspend). 2 | -behaviour(op_exch_mod). 3 | -export([start/2, dispatch/2, suspend/2]). 4 | 5 | -include("openpoker.hrl"). 6 | 7 | start([], Ctx) -> 8 | erlang:start_timer(10 * 1000, self(), ?MODULE), 9 | {next, suspend, Ctx}. 10 | 11 | dispatch(_R, _Ctx) -> 12 | ok. 13 | 14 | suspend({timeout, _, ?MODULE}, Ctx = #texas{}) -> 15 | {stop, Ctx}; 16 | 17 | suspend(go_go_go, Ctx) -> 18 | {stop, Ctx}; 19 | 20 | suspend(_, Ctx) -> 21 | {skip, Ctx}. 22 | 23 | -------------------------------------------------------------------------------- /src/mods/ranking.erl: -------------------------------------------------------------------------------- 1 | -module(ranking). 2 | -behaviour(op_exch_mod). 3 | -export([start/2, dispatch/2]). 4 | -export([rank/1, notify/1]). 5 | 6 | -include("openpoker.hrl"). 7 | 8 | %%% 9 | %%% callback 10 | %%% 11 | 12 | start([], Ctx) -> 13 | notify(Ctx), 14 | {stop, Ctx}. 15 | 16 | dispatch(_R, _Ctx) -> 17 | ok. 18 | 19 | %%% 20 | %%% client 21 | %%% 22 | 23 | rank(#texas{seats = S, board = Cards}) -> 24 | Seats = seat:lookup(?PS_STANDING, S), 25 | rank(Seats, Cards, []). 26 | 27 | notify(Ctx) -> 28 | notify(rank(Ctx), Ctx). 29 | 30 | %%% 31 | %%% private 32 | %%% 33 | 34 | rank([], _Cards, Acc) -> Acc; 35 | rank([H = #seat{hand = Hand}|T], Cards, Acc) -> 36 | RH = hand:rank(hand:merge(Hand, Cards)), 37 | rank(T, Cards, [H#seat{hand = RH}|Acc]). 38 | 39 | notify([], _Ctx) -> ok; 40 | notify([#seat{pid = PId, process = P, hand = Hand, sn = SN}|T], Ctx = #texas{gid = Id}) -> 41 | #player_hand{rank = Rank, high1 = H1, high2 = H2, suit = Suit} = hand:player_hand(Hand), 42 | player:notify(P, #notify_hand{ game = Id, player = PId, sn = SN, rank = Rank, high1 = H1, high2 = H2, suit = Suit}), 43 | notify(T, Ctx). 44 | -------------------------------------------------------------------------------- /src/mods/restart.erl: -------------------------------------------------------------------------------- 1 | -module(restart). 2 | -behaviour(op_exch_mod). 3 | -export([start/2, dispatch/2]). 4 | 5 | -include("openpoker.hrl"). 6 | 7 | start([], Ctx) -> 8 | ResetCtx= Ctx#texas{ 9 | exp_seat = ?UNDEF, 10 | exp_call = 0, 11 | exp_min = 0, 12 | exp_max = 0, 13 | board = [], 14 | pot = pot:new(), 15 | deck = deck:new(), 16 | seats = reset_seat(seat:get(Ctx#texas.seats), Ctx#texas.seats) 17 | }, 18 | 19 | {goto, top, ResetCtx}. 20 | 21 | dispatch(_R, _Ctx) -> 22 | ok. 23 | 24 | reset_seat([], Seats) -> Seats; 25 | reset_seat([H|T], Seats) -> 26 | reset_seat(T, seat:set(H#seat{hand = hand:new(), bet = 0}, Seats)). 27 | -------------------------------------------------------------------------------- /src/mods/rig.erl: -------------------------------------------------------------------------------- 1 | -module(rig). 2 | -export([start/2, dispatch/2]). 3 | 4 | -include("openpoker.hrl"). 5 | 6 | start([Cards], Ctx) when is_list(Cards) -> 7 | {stop, Ctx#texas{deck = deck:new(Cards)}}; 8 | start(_, Ctx) -> {stop, Ctx}. 9 | 10 | dispatch(_R, _Ctx) -> 11 | ok. 12 | -------------------------------------------------------------------------------- /src/mods/showdown.erl: -------------------------------------------------------------------------------- 1 | -module(showdown). 2 | -behaviour(op_exch_mod). 3 | -export([start/2, dispatch/2]). 4 | 5 | -include("openpoker.hrl"). 6 | 7 | -define(REWARD_TIMEOUT, 2000). 8 | 9 | %%% 10 | %%% callback 11 | %%% 12 | 13 | start([], Ctx = #texas{gid = Id, seats = S, pot = P}) -> 14 | RankedSeats = ranking:rank(Ctx), 15 | Winners = winners(RankedSeats, pot:pots(P)), 16 | 17 | ShowCardSeats = seat:lookup(?PS_STANDING, S), 18 | show_cards(ShowCardSeats, Ctx), 19 | 20 | broadcast_ranks(RankedSeats, Ctx), 21 | 22 | RewardedCtx = reward_winners(Winners, Ctx), 23 | RewardedSeats = RewardedCtx#texas.seats, 24 | KickedCtx = kick_poor_players(seat:lookup(?PS_READY, RewardedSeats), RewardedCtx), 25 | 26 | game:broadcast(#notify_game_end{ game = Id }, KickedCtx), 27 | 28 | {stop, KickedCtx}. 29 | 30 | dispatch(_R, _Ctx) -> 31 | ok. 32 | 33 | %%% 34 | %%% private 35 | %%% 36 | 37 | show_cards([], _Ctx) -> ok; 38 | show_cards([_H], _Ctx) -> ok; 39 | show_cards(L = [_H|_T], Ctx) -> do_show_cards(L, Ctx). 40 | 41 | do_show_cards([], _Ctx) -> ok; 42 | do_show_cards([#seat{pid = PId, identity = Identity, hand = Hand, sn = SN}|T], Ctx = #texas{gid = Id}) -> 43 | game:broadcast(#notify_cards{ game = Id, player = PId, sn = SN, cards = Hand#hand.cards}, Ctx, [Identity]), 44 | do_show_cards(T, Ctx). 45 | 46 | 47 | broadcast_ranks([], _Ctx) -> ok; 48 | broadcast_ranks([_H], _Ctx) -> ok; 49 | broadcast_ranks(L = [_H|_T], Ctx) -> do_broadcast_ranks(L, Ctx). 50 | 51 | do_broadcast_ranks([], _Ctx) -> ok; 52 | do_broadcast_ranks([#seat{pid = PId, hand = Hand, sn = SN}|T], Ctx = #texas{gid = Id}) -> 53 | #player_hand{rank = Rank, high1 = H1, high2 = H2, suit = Suit} = hand:player_hand(Hand), 54 | game:broadcast(#notify_hand{ player = PId, sn = SN, game = Id, rank = Rank, high1 = H1, high2 = H2, suit = Suit}, Ctx), 55 | do_broadcast_ranks(T, Ctx). 56 | 57 | reward_winners([], Ctx) -> Ctx; 58 | reward_winners([{H = #hand{}, Amt}|T], Ctx) -> 59 | NewCtx = game:reward(H, Amt, Ctx), 60 | timer:sleep(?REWARD_TIMEOUT), 61 | reward_winners(T, NewCtx). 62 | 63 | kick_poor_players([], Ctx) -> Ctx; 64 | kick_poor_players([#seat{pid = PId, sn = SN, inplay = Inplay}|T], Ctx = #texas{seats = S, limit = L}) 65 | when L#limit.big > Inplay -> 66 | game:broadcast(#notify_out{ game = Ctx#texas.gid, player = PId }, Ctx), 67 | kick_poor_players(T, Ctx#texas{seats = seat:set(SN, ?PS_OUT, S)}); 68 | kick_poor_players([_|T], Ctx = #texas{}) -> 69 | kick_poor_players(T, Ctx). 70 | 71 | %% fuck code is here, winners comput to depend on record field position 72 | %% e.g lists:keysort(5, M) is sort by hand record five point field rank 73 | %% TODO use lists:sort(Fun, List) rework winners function 74 | 75 | winners(RankedSeats, Pots) -> 76 | %% to cope fuck winners, set pid and process every seat hand 77 | Fun = fun(#seat{pid = PId, sn = SN, hand = Hand}) -> 78 | Hand#hand{seat_sn = SN, pid = PId} 79 | end, 80 | FuckedHands = lists:map(Fun, RankedSeats), 81 | gb_trees:to_list(winners(FuckedHands, Pots, gb_trees:empty())). 82 | 83 | winners(_Ranks, [], Winners) -> Winners; 84 | winners(Ranks, [{Total, Members}|Rest], Winners) -> 85 | M = lists:filter(fun(#hand{pid = PId}) -> gb_trees:is_defined(PId, Members) end, Ranks), 86 | %% sort by rank and leave top ranks only 87 | M1 = lists:reverse(lists:keysort(5, M)), 88 | TopRank = element(5, hd(M1)), 89 | M2 = lists:filter(fun(R) -> element(5, R) == TopRank end, M1), 90 | %% sort by high card and leave top high cards only 91 | M3 = lists:reverse(lists:keysort(6, M2)), 92 | TopHigh1 = element(6, hd(M3)), 93 | M4 = lists:filter(fun(R) -> element(6, R) == TopHigh1 end, M3), 94 | M5 = lists:reverse(lists:keysort(7, M4)), 95 | TopHigh2 = element(7, hd(M5)), 96 | M6 = lists:filter(fun(R) -> element(7, R) == TopHigh2 end, M5), 97 | %% sort by top score and leave top scores only 98 | M7 = lists:reverse(lists:keysort(8, M6)), 99 | TopScore = element(8, hd(M7)), 100 | M8 = lists:filter(fun(R) -> element(8, R) == TopScore end, M7), 101 | Win = Total div length(M8), 102 | Winners1 = update_winners(M8, Win, Winners), 103 | winners(Ranks, Rest, Winners1). 104 | 105 | update_winners([], _Amount, Tree) -> 106 | Tree; 107 | update_winners([Player|Rest], Amount, Tree) -> 108 | update_winners(Rest, Amount, update_counter(Player, Amount, Tree)). 109 | 110 | update_counter(Key, Amount, Tree) -> 111 | case gb_trees:lookup(Key, Tree) of 112 | {value, Old} -> 113 | Old = gb_trees:get(Key, Tree), 114 | gb_trees:update(Key, Old + Amount, Tree); 115 | none -> 116 | gb_trees:insert(Key, Amount, Tree) 117 | end. 118 | -------------------------------------------------------------------------------- /src/mods/stop.erl: -------------------------------------------------------------------------------- 1 | -module(stop). 2 | -behaviour(op_exch_mod). 3 | -export([start/2, dispatch/2, stop/2]). 4 | 5 | -include("openpoker.hrl"). 6 | 7 | start([], Ctx) -> 8 | erlang:start_timer(10 * 1000, self(), ?MODULE), 9 | {next, stop, Ctx}. 10 | 11 | dispatch(_R, _Ctx) -> 12 | ok. 13 | 14 | stop({timeout, _, ?MODULE}, Ctx = #texas{}) -> 15 | {stop, Ctx}; 16 | 17 | stop(_, Ctx) -> 18 | {skip, Ctx}. 19 | -------------------------------------------------------------------------------- /src/mods/wait_players.erl: -------------------------------------------------------------------------------- 1 | -module(wait_players). 2 | -behaviour(op_exch_mod). 3 | -export([start/2, dispatch/2, wait_for_players/2]). 4 | 5 | -include("openpoker.hrl"). 6 | 7 | start(_Params, Ctx = #texas{start_delay = 0}) -> 8 | wait_for_players({timeout, ?UNDEF, ?MODULE}, Ctx); 9 | start(_Params, Ctx = #texas{start_delay = StartDelay}) -> 10 | Timer = erlang:start_timer(StartDelay, self(), ?MODULE), 11 | NewCtx = Ctx#texas{timer = Timer}, 12 | {next, wait_for_players, NewCtx }. 13 | 14 | dispatch(#cmd_raise{}, _Ctx) -> 15 | skip; 16 | dispatch(#cmd_fold{}, _Ctx) -> 17 | skip; 18 | dispatch(_R, _Ctx) -> 19 | ok. 20 | 21 | wait_for_players({timeout, _, ?MODULE}, Ctx = #texas{seats = Seats, required = R, joined = J}) when J < R -> 22 | clear_out_players(seat:lookup(?PS_OUT, Seats), Ctx), 23 | game:broadcast(#notify_game_cancel{game = Ctx#texas.gid}, Ctx), 24 | {repeat, Ctx}; 25 | 26 | wait_for_players({timeout, _, ?MODULE}, Ctx = #texas{seats = Seats}) -> 27 | ReadySeats = seat:lookup(?PS_READY, Seats), 28 | case length(ReadySeats) >= Ctx#texas.required of 29 | true -> 30 | clear_out_players(seat:lookup(?PS_OUT, Seats), Ctx), 31 | game:broadcast(#notify_game_start{game = Ctx#texas.gid}, Ctx), 32 | ReadySeats = seat:lookup(?PS_READY, Seats), 33 | PlaySeats = seat:set(ReadySeats, ?PS_PLAY, Seats), 34 | {stop, Ctx#texas{seats = PlaySeats}}; 35 | _ -> 36 | clear_out_players(seat:lookup(?PS_OUT, Seats), Ctx), 37 | game:broadcast(#notify_game_cancel{game = Ctx#texas.gid}, Ctx), 38 | {repeat, Ctx} 39 | end; 40 | 41 | wait_for_players(_R, Ctx) -> 42 | {skip, Ctx}. 43 | 44 | clear_out_players([], _Ctx) -> ok; 45 | clear_out_players([#seat{pid = PId, sn = SN}|T], Ctx) -> 46 | game:leave(self(), #cmd_leave{game = Ctx#texas.gid, pid = PId, sn = SN}), 47 | clear_out_players(T, Ctx). 48 | -------------------------------------------------------------------------------- /src/op_common.erl: -------------------------------------------------------------------------------- 1 | -module(op_common). 2 | -compile([export_all]). 3 | 4 | get_env(App, Key, Def) -> 5 | case application:get_env(App, Key) of 6 | undefined -> Def; 7 | {ok, Value} -> Value 8 | end. 9 | 10 | get_env(Key, Def) -> 11 | get_env(openpoker, Key, Def). 12 | 13 | get_status(NameOrProc) -> 14 | {status, _Proc, _Mod, SItem} = sys:get_status(NameOrProc), 15 | [_PDict, _SysStaqte, _Parent, _Dbg, Misc] = SItem, 16 | [_H|[[{"State", State}]|[]]] = proplists:get_all_values(data, Misc), 17 | State. 18 | -------------------------------------------------------------------------------- /src/op_exch.erl: -------------------------------------------------------------------------------- 1 | -module(op_exch). 2 | -behaviour(gen_server). 3 | 4 | -export([behaviour_info/1]). 5 | -export([init/1, handle_call/3, handle_cast/2, 6 | handle_info/2, terminate/2, code_change/3]). 7 | -export([start_link/3, stop/1, stop/2, cast/2, call/2]). 8 | 9 | -include("openpoker.hrl"). 10 | 11 | behaviour_info(callbacks) -> [ 12 | {id, 0}, 13 | {init, 2}, 14 | {stop, 1}, 15 | {dispatch, 2}, 16 | {call, 2} 17 | ]. 18 | 19 | %%% 20 | %%% client 21 | %%% 22 | 23 | start_link(Module, Conf, Mods) -> 24 | Id = Module:id(), 25 | Pid = gen_server:start_link({global, {Module, Id}}, op_exch, [Module, Id, Conf, Mods], []), 26 | start_logger(Pid, Id). 27 | 28 | start_logger(R = {ok, Pid}, Id) -> 29 | LogGames = op_common:get_env(log_games, []), 30 | case lists:member(Id, LogGames) of 31 | true -> 32 | op_exch_event_logger:add_handler(Pid); 33 | false -> 34 | ok 35 | end, 36 | R. 37 | 38 | stop(Pid) when is_pid(Pid) -> 39 | gen_server:cast(Pid, stop). 40 | 41 | stop(Module, Id) when is_number(Id) -> 42 | gen_server:cast({global, {Module, Id}}, stop). 43 | 44 | call(Exch, Event) -> 45 | gen_server:call(Exch, Event). 46 | 47 | cast(Exch, Event) -> 48 | gen_server:cast(Exch, Event). 49 | 50 | %%% 51 | %%% callback 52 | %%% 53 | 54 | init([Module, Id, Conf, Mods]) -> 55 | process_flag(trap_exit, true), 56 | Ctx = Module:init(Id, Conf), 57 | 58 | Data = #exch{ 59 | id = Id, 60 | module = Module, 61 | mods = Mods, 62 | stack = Mods, 63 | ctx = Ctx, 64 | conf = Conf 65 | }, 66 | 67 | case init(?UNDEF, Data) of 68 | {stop, _, NewData} -> 69 | {stop, NewData}; 70 | {noreply, NewData} -> 71 | {ok, NewData} 72 | end. 73 | 74 | handle_cast(stop, Data) -> 75 | {stop, normal, Data}; 76 | 77 | handle_cast(Msg, Data = #exch{stack = Stack, ctx = Ctx, state = State}) -> 78 | {Mod, _} = hd(Stack), 79 | op_exch_event:cast([{mod, Mod}, {state, State}, {msg, Msg}]), 80 | 81 | %io:format("==========================================~n"), 82 | %io:format("msg: ~p~n", [Msg]), 83 | 84 | case advance(Mod:State(Msg, Ctx), Msg, Data) of 85 | R = {noreply, #exch{stack = NewStack, state = NewState}} -> 86 | {NewMod, _} = hd(NewStack), 87 | op_exch_event:cast([{next_mod, NewMod}, {next_state, NewState}, {msg, Msg}]), 88 | R; 89 | R = {stop, normal, _} -> 90 | R 91 | end. 92 | 93 | handle_call(Msg, _From, Data = #exch{module = Module, ctx = Context}) -> 94 | {ok, Result, NewContext} = Module:call(Msg, Context), 95 | {reply, Result, Data#exch{ctx = NewContext}}. 96 | 97 | terminate(normal, #exch{module = Module, ctx = Ctx}) -> 98 | Module:stop(Ctx); 99 | terminate(_, #exch{module = Module, ctx = Ctx}) -> 100 | Module:stop(Ctx). 101 | 102 | handle_info(Msg, Data) -> 103 | handle_cast(Msg, Data). 104 | 105 | code_change(_OldVsn, Data, _Extra) -> 106 | {ok, Data}. 107 | 108 | %%% 109 | %%% private 110 | %%% 111 | 112 | init(Msg, Data = #exch{ stack = [{Mod, Params}|_], ctx = Ctx }) -> 113 | op_exch_event:init(Mod, Msg), 114 | advance(Mod:start(Params, Ctx), Msg, Data#exch{ state = ?UNDEF }). 115 | 116 | %% continue current state 117 | advance({continue, Ctx}, _Msg, Data = #exch{}) -> 118 | {noreply, Data#exch{ ctx = Ctx }}; 119 | 120 | %% next new state 121 | advance({next, State, Ctx}, _Msg, Data) -> 122 | {noreply, Data#exch{ state = State, ctx = Ctx }}; 123 | 124 | %% skip mod process, post to Module:dispatch. 125 | advance({skip, Ctx}, Msg, Data = #exch{stack = Stack, module = Module}) -> 126 | {Mod, _} = hd(Stack), 127 | op_exch_event:advance([{mod, Mod}, {state, Data#exch.state}, {skip, Msg}]), 128 | case Mod:dispatch(Msg, Ctx) of 129 | ok -> 130 | {noreply, Data#exch{ ctx = Module:dispatch(Msg, Ctx) }}; 131 | skip -> 132 | {noreply, Data} 133 | end; 134 | 135 | %% 136 | advance({stop, Ctx}, _Msg, Data = #exch{ stack = [_] }) -> 137 | {stop, normal, Data#exch{ ctx = Ctx, stack = [] }}; 138 | 139 | %% stop current mod, init next mod 140 | advance({stop, Ctx}, Msg, Data = #exch{ stack = [_|T] }) -> 141 | init(Msg, Data#exch{ ctx = Ctx, stack = T }); 142 | 143 | %% repeat current mod, re init mod 144 | advance({repeat, Ctx}, Msg, Data = #exch{}) -> 145 | init(Msg, Data#exch{ ctx = Ctx }); 146 | 147 | %% goto first mod 148 | advance({goto, top, Ctx}, Msg, Data = #exch{mods = Mods}) -> 149 | init(Msg, Data#exch{ ctx = Ctx, stack = Mods}); 150 | 151 | %% goto specil mod 152 | advance({goto, Mod, Ctx}, Msg, Data = #exch{stack = Stack}) -> 153 | init(Msg, Data#exch{ ctx = Ctx, stack = trim_stack(Mod, Stack) }). 154 | 155 | trim_stack(_Mod, L = [{_LastMod, _}]) -> L; 156 | trim_stack(Mod, L = [{H, _}|_]) when Mod == H -> L; 157 | trim_stack(Mod, [_|T]) -> trim_stack(Mod, T). 158 | 159 | -------------------------------------------------------------------------------- /src/op_exch_event.erl: -------------------------------------------------------------------------------- 1 | -module(op_exch_event). 2 | -export([start_link/0, add_handler/2, delete_handler/2]). 3 | -export([dispatch/1, cast/1, phantom/1, advance/1, init/2]). 4 | 5 | start_link() -> 6 | gen_event:start_link({local, ?MODULE}). 7 | 8 | add_handler(Handler, Args) -> 9 | gen_event:add_handler(?MODULE, Handler, Args). 10 | 11 | delete_handler(Handler, Args) -> 12 | gen_event:delete_handler(?MODULE, Handler, Args). 13 | 14 | dispatch(Msg) when is_list(Msg) -> 15 | notify([{event, dispath}] ++ Msg). 16 | 17 | cast(Msg) when is_list(Msg) -> 18 | notify([{event, cast}] ++ Msg). 19 | 20 | phantom(Msg) when is_list(Msg) -> 21 | notify([{event, phantom}] ++ Msg). 22 | 23 | advance(Msg) when is_list(Msg) -> 24 | notify([{event, advance}] ++ Msg). 25 | 26 | init(Mod, Msg) when is_atom(Mod) -> 27 | notify([{event, init}, {Mod, Msg}]). 28 | 29 | notify(Msg) -> 30 | %io:format('~n~n================================~n~p~n', [Msg]), 31 | gen_event:notify(?MODULE, {self(), Msg}). 32 | -------------------------------------------------------------------------------- /src/op_exch_event_logger.erl: -------------------------------------------------------------------------------- 1 | -module(op_exch_event_logger). 2 | -behaviour(gen_event). 3 | -export([add_handler/1, delete_handler/0]). 4 | -export([init/1, handle_event/2, handle_call/2, 5 | handle_info/2, code_change/3, terminate/2]). 6 | 7 | -include("openpoker.hrl"). 8 | 9 | add_handler(GID) when is_integer(GID) -> 10 | op_exch_event:add_handler(?MODULE, [?LOOKUP_GAME(GID)]); 11 | 12 | add_handler(GamePID) when is_pid(GamePID) -> 13 | op_exch_event:add_handler(?MODULE, [GamePID]). 14 | 15 | delete_handler() -> 16 | op_exch_event:delete_handler(?MODULE, []). 17 | 18 | init([GamePID]) -> 19 | {ok, [GamePID]}. 20 | 21 | handle_event({log, Msg}, State) -> 22 | error_logger:info_report([Msg]), 23 | {ok, State}; 24 | 25 | handle_event({GamePID, Msg}, State = [GamePID]) -> 26 | handle_event({log, Msg}, State); 27 | 28 | handle_event({_, _Msg}, _State) -> 29 | remove_handler. 30 | 31 | handle_call(_Request, State) -> 32 | {ok, ok, State}. 33 | 34 | handle_info(_Info, State) -> 35 | {ok, State}. 36 | 37 | terminate(_Arg, _State) -> 38 | ok. 39 | 40 | code_change(_OldVsn, State, _Extra) -> 41 | {ok, State}. 42 | -------------------------------------------------------------------------------- /src/op_exch_mod.erl: -------------------------------------------------------------------------------- 1 | -module(op_exch_mod). 2 | -export([behaviour_info/1]). 3 | 4 | behaviour_info(callbacks) -> [ 5 | {start, 2}, 6 | {dispatch, 2} 7 | ]. 8 | -------------------------------------------------------------------------------- /src/op_games_sup.erl: -------------------------------------------------------------------------------- 1 | -module(op_games_sup). 2 | -behaviour(supervisor). 3 | 4 | %% API 5 | -export([start_link/0, start_child/1]). 6 | 7 | %% Supervisor callbacks 8 | -export([init/1]). 9 | 10 | -include("openpoker.hrl"). 11 | 12 | %% =================================================================== 13 | %% API functions 14 | %% =================================================================== 15 | 16 | start_link() -> 17 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 18 | 19 | %% =================================================================== 20 | %% Supervisor callbacks 21 | %% =================================================================== 22 | 23 | init([]) -> 24 | Fun = fun(R, Acc) -> gen(R, []) ++ Acc end, 25 | ok = mnesia:wait_for_tables([tab_game_config], ?WAIT_TABLE), 26 | {atomic, NewAcc} = mnesia:transaction(fun() -> 27 | mnesia:foldl(Fun, [], tab_game_config) 28 | end), 29 | {ok, {{one_for_one, 5, 10}, lists:reverse(NewAcc)}}. 30 | 31 | start_child(Conf = #tab_game_config{}) -> 32 | Fun = fun(Game) -> supervisor:start_child(?MODULE, Game) end, 33 | lists:map(Fun, gen(Conf, [])). 34 | 35 | gen(#tab_game_config{max = 0}, Acc) -> Acc; 36 | gen(Conf = #tab_game_config{module = Module, mods = Mods, max = N}, Acc) -> 37 | StartMod = {op_exch, start_link, [Module, Conf, Mods]}, 38 | NewAcc = [{make_ref(), StartMod, permanent, 2000, worker, []}] ++ Acc, 39 | gen(Conf#tab_game_config{max = N - 1}, NewAcc). 40 | -------------------------------------------------------------------------------- /src/op_phantom.erl: -------------------------------------------------------------------------------- 1 | -module(op_phantom). 2 | -behavior(gen_server). 3 | 4 | -export([start_link/3]). 5 | -export([init/1, terminate/2, handle_call/3, handle_cast/2, code_change/3, handle_info/2]). 6 | -export([send/2]). 7 | -include("openpoker.hrl"). 8 | 9 | %% 10 | %% Client API 11 | %% 12 | 13 | -record(pd, { pid, gid, sn }). 14 | 15 | start_link(PID, GID, SN) when is_integer(PID), is_pid(GID), is_integer(SN) -> 16 | gen_server:start_link(?MODULE, [PID, GID, SN], []). 17 | 18 | send(P, R) -> 19 | gen_server:cast(P, R). 20 | 21 | %% 22 | %% Callback Functions 23 | %% 24 | 25 | init([PID, GID, SN]) -> 26 | op_exch_event:phantom([{init, PID}]), 27 | {ok, #pd{pid = PID, gid = GID, sn = SN}}. 28 | 29 | terminate(_Reason, _LoopData) -> 30 | ok. 31 | 32 | handle_info(_Info, LoopData) -> 33 | {noreply, LoopData}. 34 | 35 | code_change(_OldVsn, LoopData, _Extra) -> 36 | {ok, LoopData}. 37 | 38 | handle_call(_Msg, _From, LoopData) -> 39 | {reply, ok, LoopData}. 40 | 41 | handle_cast(#notify_game_cancel{}, LoopData) -> 42 | handle_cast(#notify_game_end{}, LoopData); 43 | 44 | handle_cast(#notify_game_end{}, LoopData = #pd {pid = PID, gid = GID}) -> 45 | op_exch_event:phantom([{game_over, PID}]), 46 | player:leave(PID, GID), 47 | {noreply, LoopData}; 48 | 49 | handle_cast(#notify_leave{sn = SN}, LoopData = #pd{pid = PID, sn = SN}) -> 50 | op_exch_event:phantom([{player_leave, PID}]), 51 | %% TODO better process player, don't kill process 52 | op_players_sup:terminate_child_ex(PID), 53 | op_exch_event:phantom([{terminate_child, PID}]), 54 | {noreply, LoopData}; 55 | 56 | handle_cast(#notify_betting{}, LoopData = #pd{pid = PID, gid = GID}) -> 57 | error_logger:info_report([{phantom, notify_betting}]), 58 | player:fold(PID, GID), 59 | {noreply, LoopData}; 60 | 61 | handle_cast(_R, LoopData) -> 62 | {noreply, LoopData}. 63 | -------------------------------------------------------------------------------- /src/op_players_sup.erl: -------------------------------------------------------------------------------- 1 | -module(op_players_sup). 2 | -behaviour(supervisor). 3 | 4 | %% API 5 | -export([start_link/0, start_child/1, terminate_child_ex/1, terminate_child/1, start_anyone/0]). 6 | 7 | %% Supervisor callbacks 8 | -export([init/1]). 9 | 10 | -include("openpoker.hrl"). 11 | 12 | %% Helper macro for declaring children of supervisor 13 | -define(CHILD(I, Type), {I, {I, start_link, []}, permanent, 5000, Type, [I]}). 14 | -define(PID(Id), {player, Id}). 15 | 16 | %% =================================================================== 17 | %% API functions 18 | %% =================================================================== 19 | 20 | start_link() -> 21 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 22 | 23 | start_child(R) -> 24 | supervisor:start_child(?MODULE, {?PID(R#tab_player_info.pid), {player, start_link, [R]}, permanent, brutal_kill, worker, []}). 25 | 26 | terminate_child_ex(Id) -> 27 | spawn(?MODULE, terminate_child, [Id]). 28 | 29 | terminate_child(Id) -> 30 | ok = supervisor:terminate_child(?MODULE, ?PID(Id)), 31 | ok = supervisor:delete_child(?MODULE, ?PID(Id)). 32 | 33 | start_anyone() -> 34 | [R] = mnesia:dirty_read(tab_player_info, 1), 35 | start_child(R). 36 | 37 | %% =================================================================== 38 | %% Supervisor callbacks 39 | %% =================================================================== 40 | 41 | init([]) -> 42 | {ok, {{one_for_one, 5, 10}, []}}. 43 | -------------------------------------------------------------------------------- /src/op_sup.erl: -------------------------------------------------------------------------------- 1 | -module(op_sup). 2 | -behaviour(supervisor). 3 | 4 | %% API 5 | -export([start_link/0]). 6 | 7 | %% Supervisor callbacks 8 | -export([init/1]). 9 | 10 | %% Helper macro for declaring children of supervisor 11 | -define(CHILD(I, Type), {I, {I, start_link, []}, permanent, 5000, Type, [I]}). 12 | 13 | %% =================================================================== 14 | %% API functions 15 | %% =================================================================== 16 | 17 | start_link() -> 18 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 19 | 20 | %% =================================================================== 21 | %% Supervisor callbacks 22 | %% =================================================================== 23 | 24 | init([]) -> 25 | Event = {op_exch_event, {op_exch_event, start_link, []}, permanent, 2000, worker, [op_exch_event]}, 26 | GamesSup = {op_games_sup, {op_games_sup, start_link, []}, permanent, 10000, supervisor, [op_games_sup]}, 27 | PlayersSup = {op_players_sup, {op_players_sup, start_link, []}, permanent, 2000, supervisor, [op_players_sup]}, 28 | {ok, {{one_for_one, 5, 10}, [Event, GamesSup, PlayersSup]}}. 29 | -------------------------------------------------------------------------------- /src/op_webtekcos_event_logger.erl: -------------------------------------------------------------------------------- 1 | -module(op_webtekcos_event_logger). 2 | 3 | -behaviour(gen_event). 4 | 5 | -export([add_handler/0, delete_handler/0]). 6 | 7 | -export([init/1, handle_event/2, handle_call/2, 8 | handle_info/2, code_change/3, terminate/2]). 9 | 10 | add_handler() -> 11 | webtekcos_event:add_handler(?MODULE, []). 12 | 13 | delete_handler() -> 14 | webtekcos_event:delete_handler(?MODULE, []). 15 | 16 | init([]) -> 17 | {ok, []}. 18 | 19 | handle_event({rcv_data, Code}, State) -> 20 | Data = base64:decode(list_to_binary(Code)), 21 | case catch protocol:read(Data) of 22 | {'EXIT', {_Reason, _Stack}} -> 23 | io:format("GENESIS RECV ERROR_DATA [~p] ~n", [Data]); 24 | R -> 25 | io:format("GENESIS RECV ~p~n", [R]) 26 | end, 27 | {ok, State}; 28 | 29 | handle_event({send_data, Code}, State) -> 30 | Data = base64:decode(Code), 31 | case catch protocol:read(Data) of 32 | {'EXIT', {_Reason, _Stack}} -> 33 | io:format("GENESIS SEND ERROR_DATA [~p] ~n", [Data]); 34 | R -> 35 | io:format("GENESIS SEND ~p~n", [R]) 36 | end, 37 | {ok, State}; 38 | 39 | handle_event({start, _Args}, State) -> 40 | {ok, State}; 41 | 42 | handle_event(connect, State) -> 43 | io:format("GENESIS RECV CONNECTION~n"), 44 | {ok, State}; 45 | 46 | handle_event(disconnect, State) -> 47 | io:format("GENESIS DISCONNECTION~n"), 48 | {ok, State}. 49 | 50 | handle_call(_Request, State) -> 51 | {ok, ok, State}. 52 | 53 | handle_info(_Info, State) -> 54 | {ok, State}. 55 | 56 | terminate(_Arg, _State) -> 57 | ok. 58 | 59 | code_change(_OldVsn, State, _Extra) -> 60 | {ok, State}. 61 | -------------------------------------------------------------------------------- /src/openpoker.app.src: -------------------------------------------------------------------------------- 1 | {application, openpoker, 2 | [ 3 | {description, ""}, 4 | {vsn, "0.1.0"}, 5 | {registered, []}, 6 | {applications, [ 7 | kernel, 8 | stdlib, 9 | sasl 10 | ]}, 11 | {mod, { openpoker_app, []}}, 12 | {env, []} 13 | ]}. 14 | -------------------------------------------------------------------------------- /src/openpoker_app.erl: -------------------------------------------------------------------------------- 1 | -module(openpoker_app). 2 | 3 | -behaviour(application). 4 | 5 | %% Application callbacks 6 | -export([start/2, stop/1]). 7 | 8 | %% =================================================================== 9 | %% Application callbacks 10 | %% =================================================================== 11 | 12 | start(_StartType, _StartArgs) -> 13 | case mnesia:system_info(tables) of 14 | [schema] -> 15 | io:format("==============================================~n"), 16 | io:format(" REBUILD CORE ...~n"), 17 | schema:rebuild_schema(), 18 | schema:rebuild_core_and_data(), 19 | io:format(" REBUILD CORE SUCCESSFUL~n"); 20 | _ -> 21 | ok 22 | end, 23 | 24 | op_sup:start_link(). 25 | 26 | stop(_State) -> 27 | ok. 28 | -------------------------------------------------------------------------------- /src/pickle.erl: -------------------------------------------------------------------------------- 1 | -module(pickle). 2 | 3 | %%% 4 | %%% Serialization tools 5 | %%% 6 | 7 | -export([pickle/2, unpickle/2]). 8 | -export([byte/0, short/0, sshort/0, int/0, 9 | sint/0, long/0, slong/0, price/0]). 10 | -export([list/2, choice/2, optional/1, wrap/2, 11 | tuple/1, record/2, binary/1, wstring/0]). 12 | -export([string/0]). 13 | 14 | -include("openpoker.hrl"). 15 | 16 | %%% Pickle and unpickle. We accumulate into a list. 17 | 18 | pickle({Pickler, _}, Value) -> 19 | lists:reverse(Pickler([], Value)). 20 | 21 | unpickle({_, Pickler}, Bin) -> 22 | element(1, Pickler(Bin)). 23 | 24 | %%% Byte 25 | 26 | byte() -> 27 | {fun write_byte/2, fun read_byte/1}. 28 | 29 | write_byte(Acc, ?UNDEF) -> 30 | [<<0:8>>|Acc]; 31 | write_byte(Acc, Byte) -> 32 | [<>|Acc]. 33 | 34 | read_byte(Bin) -> 35 | <> = Bin, 36 | {Byte, Rest}. 37 | 38 | %%% Unsigned short 39 | 40 | short() -> 41 | {fun write_short/2, fun read_short/1}. 42 | 43 | write_short(Acc, Word) -> 44 | [<>|Acc]. 45 | 46 | read_short(Bin) -> 47 | <> = Bin, 48 | {Word, Rest}. 49 | 50 | %%% Signed short 51 | 52 | sshort() -> 53 | {fun write_sshort/2, fun read_sshort/1}. 54 | 55 | write_sshort(Acc, Word) -> 56 | [<>|Acc]. 57 | 58 | read_sshort(Bin) -> 59 | <> = Bin, 60 | {Word, Rest}. 61 | 62 | %%% Unsigned int 63 | 64 | int() -> 65 | {fun write_int/2, fun read_int/1}. 66 | 67 | write_int(Acc, undefined) -> 68 | [<<0:32>>|Acc]; 69 | 70 | write_int(Acc, Word) -> 71 | [<>|Acc]. 72 | 73 | read_int(Bin) -> 74 | <> = Bin, 75 | {Word, Rest}. 76 | 77 | %%% Signed int 78 | 79 | sint() -> 80 | {fun write_sint/2, fun read_sint/1}. 81 | 82 | write_sint(Acc, Word) -> 83 | [<>|Acc]. 84 | 85 | read_sint(Bin) -> 86 | <> = Bin, 87 | {Word, Rest}. 88 | 89 | %%% Unsigned long 90 | 91 | long() -> 92 | {fun write_long/2, fun read_long/1}. 93 | 94 | write_long(Acc, Word) -> 95 | [<>|Acc]. 96 | 97 | read_long(Bin) -> 98 | <> = Bin, 99 | {Word, Rest}. 100 | 101 | %%% Signed long 102 | 103 | slong() -> 104 | {fun write_slong/2, fun read_slong/1}. 105 | 106 | write_slong(Acc, Word) -> 107 | [<>|Acc]. 108 | 109 | read_slong(Bin) -> 110 | <> = Bin, 111 | {Word, Rest}. 112 | 113 | %%% Price with 5 decimal points 114 | 115 | price() -> 116 | {fun write_price/2, fun read_price/1}. 117 | 118 | write_price(Acc, Word) -> 119 | Price = trunc(Word * 10000), 120 | [<>|Acc]. 121 | 122 | read_price(Bin) -> 123 | <> = Bin, 124 | Price = Word / 10000, 125 | {Price, Rest}. 126 | 127 | %%% List. We supply a pickler for list length 128 | %%% as well as a pickler for list elements. 129 | 130 | list(Len, Elem) -> 131 | {fun(Acc, List) -> write_list(Len, Elem, Acc, List) end, 132 | fun(Bin) -> read_list(Len, Elem, Bin) end }. 133 | 134 | write_list({Len, _}, {Elem, _}, Acc, List) -> 135 | Acc1 = Len(Acc, length(List)), 136 | Fun = fun(A, Acc2) -> Elem(Acc2, A) end, 137 | lists:foldr(Fun, Acc1, List). 138 | 139 | read_list({_, Len}, {_, Elem}, Bin) -> 140 | {N, Bin1} = Len(Bin), 141 | read_list(N, [], Elem, Bin1). 142 | 143 | read_list(0, Acc, _, Bin) -> {Acc, Bin}; 144 | read_list(N, Acc, Elem, Bin) -> 145 | {E, Bin1} = Elem(Bin), 146 | read_list(N - 1, [E|Acc], Elem, Bin1). 147 | 148 | %%% Alternative selection. This could probably use some 149 | %%% deeper thinking. We take a pickler for the tag 150 | %%% as well as a tuple of two functions. The first one 151 | %%% returns the tag value and a pickler based on the supplied 152 | %%% value. The second one selects a pickler based on a tag value. 153 | 154 | choice(Tag, Choice) -> 155 | {fun(Acc, Value) -> write_choice(Tag, Choice, Acc, Value) end, 156 | fun(Bin) -> read_choice(Tag, Choice, Bin) end }. 157 | 158 | write_choice({Tag, _}, {Choice, _}, Acc, Value) 159 | when is_function(Tag), 160 | is_function(Choice) -> 161 | {T, {Pickler, _}} = Choice(Value), 162 | Acc1 = Tag(Acc, T), 163 | Pickler(Acc1, Value). 164 | 165 | read_choice({_, Tag}, {_, Choice}, Bin) 166 | when is_function(Tag), 167 | is_function(Choice) -> 168 | {T, Bin1} = Tag(Bin), 169 | {_, Pickler} = Choice(T), 170 | Pickler(Bin1). 171 | 172 | %%% Optional value. Use 'none' to indicate no value. 173 | 174 | optional(Pickler) -> 175 | {fun(Acc, Value) -> write_optional(Pickler, Acc, Value) end, 176 | fun(Bin) -> read_optional(Pickler, Bin) end}. 177 | 178 | write_optional(_, Acc, none) -> 179 | [<<0>>|Acc]; 180 | 181 | write_optional({Pickler, _}, Acc, Value) -> 182 | Pickler([<<1>>|Acc], Value). 183 | 184 | read_optional({_, Pickler}, Bin) -> 185 | <> = Bin, 186 | case Opt of 187 | 0 -> {none, Bin1}; 188 | _ -> Pickler(Bin1) 189 | end. 190 | 191 | %%% Wrapper. Take a pickler and a wrapper tuple of two functions 192 | %%% where the first one is used to convert the value before 193 | %%% pickling and the second one after unpickling. 194 | 195 | wrap(Wrap, Pickler) -> 196 | { 197 | fun(Acc, Value) -> 198 | write_wrap(Wrap, Pickler, Acc, Value) 199 | end, 200 | fun(Bin) -> 201 | read_wrap(Wrap, Pickler, Bin) 202 | end 203 | }. 204 | 205 | write_wrap({Wrap, _}, {Pickler, _}, Acc, Value) -> 206 | Pickler(Acc, Wrap(Value)). 207 | 208 | read_wrap({_, Wrap}, {_, Pickler}, Bin) -> 209 | {Value, Bin1} = Pickler(Bin), 210 | {Wrap(Value), Bin1}. 211 | 212 | %%% Erlang does not support enumerations but I want to have 213 | %%% {cow, sheep, horse} as well as [{cow, 10}, {sheep, 100}] 214 | %%% and be able to marshal these back and forth. Enumerated 215 | %%% values start from 1 for the tuple case. 216 | 217 | %%% Tuple. Uses a tuple of picklers of the same size. 218 | 219 | tuple(Picklers) 220 | when is_tuple(Picklers) -> 221 | wrap({fun tuple_to_list/1, 222 | fun list_to_tuple/1}, 223 | tuple_0(tuple_to_list(Picklers))). 224 | 225 | %%% Record. We rely on Erlang records being tuples 226 | %%% and just add the record tag as the first element 227 | %%% when unpickling the record. 228 | 229 | record(Tag, Picklers) 230 | when is_tuple(Picklers) -> 231 | wrap({fun(Record) -> record_to_list(Tag, Record) end, 232 | fun(List) -> list_to_record(Tag, List) end}, 233 | tuple_0(tuple_to_list(Picklers))). 234 | 235 | write_tuple_0([], Acc, _) -> 236 | Acc; 237 | 238 | write_tuple_0([{Pickler, _}|Rest], Acc, [Value|Tuple]) -> 239 | write_tuple_0(Rest, Pickler(Acc, Value), Tuple). 240 | 241 | read_tuple_0(Picklers, Bin) -> 242 | read_tuple_0(Picklers, Bin, []). 243 | 244 | read_tuple_0([], Bin, Acc) -> 245 | {lists:reverse(Acc), Bin}; 246 | 247 | read_tuple_0([{_, Pickler}|Rest], Bin, Acc) -> 248 | {Value, Bin1} = Pickler(Bin), 249 | read_tuple_0(Rest, Bin1, [Value|Acc]). 250 | 251 | %%% It's convenient to be able to convert the tuple 252 | %%% to a list first as there's no erlang:prepend_element/2. 253 | 254 | tuple_0(Picklers) 255 | when is_list(Picklers) -> 256 | {fun(Acc, Value) -> write_tuple_0(Picklers, Acc, Value) end, 257 | fun(Bin) -> read_tuple_0(Picklers, Bin) end}. 258 | 259 | record_to_list(Tag, Record) 260 | when is_atom(Tag) -> 261 | lists:nthtail(1, tuple_to_list(Record)). 262 | 263 | list_to_record(Tag, List) 264 | when is_atom(Tag), 265 | is_list(List) -> 266 | list_to_tuple([Tag|List]). 267 | 268 | %%% Binary 269 | 270 | binary(Size) -> 271 | {fun (Acc, Bin) -> write_binary(Size, Acc, Bin) end, 272 | fun (Bin) -> read_binary(Size, Bin) end}. 273 | 274 | write_binary({Size, _}, Acc, undefined) -> 275 | Size(Acc, 0); 276 | 277 | write_binary({Size, _}, Acc, Bin) -> 278 | Acc1 = Size(Acc, size(Bin)), 279 | [Bin|Acc1]. 280 | 281 | read_binary({_, Size}, Bin) -> 282 | {N, Bin1} = Size(Bin), 283 | <> = Bin1, 284 | {Value, Bin2}. 285 | 286 | %%% Wide string, little-endian style 287 | 288 | wstring() -> 289 | {fun write_wstring/2, 290 | fun read_wstring/1}. 291 | 292 | write_wstring(Acc, []) -> 293 | [<<0:16>>|Acc]; 294 | 295 | write_wstring(Acc, [H|T]) -> 296 | write_wstring([<>|Acc], T). 297 | 298 | read_wstring(Bin) -> 299 | read_wstring(Bin, []). 300 | 301 | read_wstring(<<0:16, Bin/binary>>, Acc) -> 302 | {lists:reverse(Acc), Bin}; 303 | 304 | read_wstring(<>, Acc) -> 305 | read_wstring(Bin, [X|Acc]). 306 | 307 | string() -> 308 | binary(byte()). 309 | 310 | %%% 311 | %%% Unit test 312 | %%% 313 | 314 | -ifdef(TEST). 315 | -include_lib("eunit/include/eunit.hrl"). 316 | 317 | prep_enum_tuple(Enum) 318 | when is_tuple(Enum) -> 319 | prep_enum_tuple(Enum, size(Enum), [], []). 320 | 321 | prep_enum_tuple(_, 0, Acc1, Acc2) -> 322 | {Acc1, Acc2}; 323 | 324 | prep_enum_tuple(Enum, N, Acc1, Acc2) -> 325 | prep_enum_tuple(Enum, N - 1, 326 | [{element(N, Enum), N}|Acc1], 327 | [{N, element(N, Enum)}|Acc2]). 328 | 329 | prep_enum_list(Enum) 330 | when is_list(Enum) -> 331 | % expect a list of {tag, #value} pairs 332 | Inv = fun({Key, Val}) -> {Val, Key} end, 333 | InvEnum = lists:map(Inv, Enum), 334 | {Enum, InvEnum}. 335 | 336 | wrap_enum(Enum) 337 | when is_tuple(Enum) -> 338 | wrap_enum_1(prep_enum_tuple(Enum)); 339 | 340 | wrap_enum(Enum) 341 | when is_list(Enum) -> 342 | wrap_enum_1(prep_enum_list(Enum)). 343 | 344 | wrap_enum_1({List1, List2}) -> 345 | F = fun(A, B) -> A < B end, 346 | %% gb_trees needs an ordered list 347 | Dict1 = lists:sort(F, List1), 348 | Dict2 = lists:sort(F, List2), 349 | Tree1 = gb_trees:from_orddict(Dict1), 350 | Tree2 = gb_trees:from_orddict(Dict2), 351 | {fun(Key) -> gb_trees:get(Key, Tree1) end, 352 | fun(Key) -> gb_trees:get(Key, Tree2) end}. 353 | 354 | enum(Enum, Pickler) -> 355 | wrap(wrap_enum(Enum), Pickler). 356 | 357 | -define(pickle(Value, Pickler), 358 | fun() -> 359 | ?assertEqual(Value, 360 | unpickle(Pickler, 361 | list_to_binary(pickle(Pickler, 362 | Value)))) 363 | end()). 364 | 365 | byte_test() -> 366 | X = 16#ff, 367 | ?pickle(X, byte()). 368 | 369 | short_test() -> 370 | X = 16#ffff, 371 | ?pickle(X, short()). 372 | 373 | sshort_test() -> 374 | X = -1, 375 | ?pickle(X, sshort()). 376 | 377 | int_test() -> 378 | X = 16#ffffffff, 379 | ?pickle(X, int()). 380 | 381 | sint_test() -> 382 | X = -1, 383 | ?pickle(X, sint()). 384 | 385 | long_test() -> 386 | X = 16#aabbccddeeff0011, 387 | ?pickle(X, long()). 388 | 389 | slong_test() -> 390 | X = -1, 391 | ?pickle(X, slong()). 392 | 393 | list_test() -> 394 | X = "Wazzup!", 395 | ?pickle(X, list(int(), byte())). 396 | 397 | %%% A choice of serializing either a list or a long. 398 | 399 | value2tag(Action) 400 | when is_list(Action) -> 401 | {0, list(byte(), byte())}; 402 | 403 | value2tag(_) -> 404 | {1, long()}. 405 | 406 | tag2value(0) -> 407 | list(byte(), byte()); 408 | 409 | tag2value(1) -> 410 | long(). 411 | 412 | selector() -> 413 | {fun value2tag/1, fun tag2value/1}. 414 | 415 | selector_test() -> 416 | X1 = "Just testing", 417 | X2 = 16#ffff, 418 | ?pickle(X1, choice(byte(), selector())), 419 | ?pickle(X2, choice(byte(), selector())). 420 | 421 | %%% Optional value 422 | 423 | optional_test() -> 424 | X1 = none, 425 | X2 = 55, 426 | ?pickle(X1, optional(byte())), 427 | ?pickle(X2, optional(byte())). 428 | 429 | %%% Tuple given as a tuple and a list of key/value pairs. 430 | 431 | tuple_enum_test() -> 432 | %% tuple enum 433 | Enum = {cow, sheep, horse}, 434 | {FROM, TO} = wrap_enum(Enum), 435 | ?assertEqual(1, FROM(cow)), 436 | ?assertEqual(2, FROM(sheep)), 437 | ?assertEqual(3, FROM(horse)), 438 | ?assertEqual(cow, TO(1)), 439 | ?assertEqual(sheep, TO(2)), 440 | ?assertEqual(horse, TO(3)). 441 | 442 | list_enum_test() -> 443 | %% list enum 444 | Enum = [{cow, 20}, {sheep, 30}, {horse, 40}], 445 | {FROM, TO} = wrap_enum(Enum), 446 | ?assertEqual(20, FROM(cow)), 447 | ?assertEqual(30, FROM(sheep)), 448 | ?assertEqual(40, FROM(horse)), 449 | ?assertEqual(cow, TO(20)), 450 | ?assertEqual(sheep, TO(30)), 451 | ?assertEqual(horse, TO(40)). 452 | 453 | enum_test() -> 454 | Enum1 = {cow, sheep, horse}, 455 | Enum2 = [{cow, 20}, {sheep, 30}, {horse, 40}], 456 | ?pickle(cow, enum(Enum1, byte())), 457 | ?pickle(sheep, enum(Enum2, byte())). 458 | 459 | tuple_test() -> 460 | Tuple = {"Joel", 16#ff00, none}, 461 | Spec = {list(byte(),byte()), short(), optional(byte())}, 462 | ?pickle(Tuple, tuple(Spec)). 463 | 464 | %%% Nested records. 465 | 466 | -record(foo, { a, b }). 467 | -record(bar, { c, d }). 468 | -record(baz, { e, f }). 469 | 470 | nested_record_test() -> 471 | R1 = #foo { 472 | a = 10, 473 | b = #bar { 474 | c = 20, 475 | d = #baz { 476 | e = 30, 477 | f = "Enough nesting!" 478 | } 479 | } 480 | }, 481 | Pickler = record(foo, { 482 | byte(), 483 | record(bar, { 484 | int(), 485 | record(baz, { 486 | sshort(), 487 | list(byte(), byte()) 488 | }) 489 | }) 490 | }), 491 | ?pickle(R1, Pickler). 492 | 493 | %%% Binary 494 | 495 | binary_test() -> 496 | X = <<1, 2, 3, 4>>, 497 | ?pickle(X, binary(int())). 498 | 499 | %%% Wide string 500 | 501 | wstring_test() -> 502 | X = "This is a wide string", 503 | ?pickle(X, wstring()). 504 | -endif. 505 | -------------------------------------------------------------------------------- /src/player.erl: -------------------------------------------------------------------------------- 1 | -module(player). 2 | -behaviour(gen_server). 3 | 4 | -export([init/1, handle_call/3, handle_cast/2, 5 | handle_info/2, terminate/2, code_change/3]). 6 | 7 | -export([start/1, start_link/1, notify/2, cast/2, auth/2, logout/1]). 8 | 9 | -export([client/1, info/1, balance/1]). 10 | 11 | -export([ctx/1, ctx/2]). 12 | 13 | -export([fold/2, leave/2, phantom/1]). 14 | 15 | -include("openpoker.hrl"). 16 | 17 | -record(pd, { %% process data 18 | pid, 19 | self, 20 | identity = ?UNDEF, 21 | client = ?UNDEF, 22 | playing = ?UNDEF, 23 | playing_sn = 0, 24 | watching = ?UNDEF, 25 | nick = ?UNDEF, 26 | photo = ?UNDEF, 27 | inplay = 0, 28 | record, 29 | phantom = ?UNDEF %% player phantom pid 30 | }). 31 | 32 | init([R = #tab_player_info{pid = PId, identity = Identity, nick = Nick, photo = Photo}]) -> 33 | process_flag(trap_exit, true), 34 | ok = create_runtime(PId, self()), 35 | {ok, #pd{pid = PId, self = self(), nick = list_to_binary(Nick), photo = list_to_binary(Photo), identity = Identity, record = R}}. 36 | 37 | handle_cast(R = #cmd_watch{game = G}, Data = #pd{}) 38 | when Data#pd.watching =:= ?UNDEF -> 39 | game:watch(G, R#cmd_watch{pid = Data#pd.pid, identity = Data#pd.identity}), 40 | {noreply, Data}; 41 | 42 | handle_cast(R = #cmd_unwatch{game = G}, Data = #pd{}) 43 | when Data#pd.watching /= ?UNDEF -> 44 | game:unwatch(G, R#cmd_unwatch{pid = Data#pd.pid, identity = Data#pd.identity}), 45 | {noreply, Data}; 46 | 47 | handle_cast(#cmd_join{game = G, buyin = B}, Data = #pd{watching = W, playing = P, record = R}) when is_pid(G), W =:= G, P =:= ?UNDEF, (R#tab_player_info.cash + R#tab_player_info.credit) < B -> 48 | notify(#notify_error{error = ?ERR_JOIN_LESS_BALANCE}), 49 | {noreply, Data}; 50 | 51 | handle_cast(R = #cmd_join{game = G}, Data = #pd{watching = W, playing = P}) when is_pid(G), W =:= G, P =:= ?UNDEF -> 52 | game:join(G, R#cmd_join{pid = Data#pd.pid, identity = Data#pd.identity, nick = Data#pd.nick, photo = Data#pd.photo}), 53 | {noreply, Data}; 54 | 55 | %% player leave game 56 | handle_cast(R = #cmd_leave{game = G}, Data = #pd{playing = P, playing_sn = SN}) when G =:= P, SN /= 0 -> 57 | game:leave(G, R#cmd_leave{pid = Data#pd.pid, sn = SN}), 58 | {noreply, Data}; 59 | 60 | handle_cast(#cmd_leave{}, Data) -> 61 | {noreply, Data}; 62 | 63 | handle_cast(R = #cmd_raise{game = G}, Data = #pd{playing = P}) when G =:= P -> 64 | game:raise(G, R#cmd_raise{sn = Data#pd.playing_sn, pid = Data#pd.pid}), 65 | {noreply, Data}; 66 | 67 | handle_cast(R = #cmd_fold{game = G}, Data = #pd{playing = P}) when G =:= P -> 68 | game:fold(G, R#cmd_fold{pid = Data#pd.pid}), 69 | {noreply, Data}; 70 | 71 | %% player info query 72 | handle_cast(#cmd_query_player{}, Data = #pd{}) -> 73 | R = #notify_player{ 74 | player = Data#pd.pid, 75 | nick = Data#pd.nick, 76 | photo = Data#pd.photo 77 | }, 78 | handle_cast({notify, R}, Data); 79 | 80 | handle_cast(#cmd_query_balance{}, Data = #pd{record = R, inplay = Inplay}) -> 81 | Balance = R#tab_player_info.cash + R#tab_player_info.credit, 82 | N = #notify_acount{ balance = Balance, inplay = Inplay }, 83 | handle_cast({notify, N}, Data); 84 | 85 | handle_cast(#cmd_query_seats{game = Game}, Data) -> 86 | game:query_seats(Game), 87 | {noreply, Data}; 88 | 89 | handle_cast({notify, R = #notify_watch{proc = G, player = P}}, Data = #pd{pid = PId}) 90 | when P =:= PId -> 91 | forward_to_client(R, Data), 92 | {noreply, Data#pd{ watching = G }}; 93 | 94 | handle_cast({notify, R = #notify_unwatch{player = P}}, Data = #pd{pid = PId}) 95 | when P =:= PId -> 96 | forward_to_client(R, Data), 97 | {noreply, Data#pd{ watching = ?UNDEF }}; 98 | 99 | handle_cast({notify, R = #notify_join{proc = G, player = P}}, Data = #pd{pid = PId}) when P =:= PId -> 100 | Info = reload_player_info(PId), 101 | forward_to_client(R, Data), 102 | {noreply, Data#pd{ playing = G, playing_sn = R#notify_join.sn, record = Info }}; 103 | 104 | handle_cast({notify, R = #notify_leave{player = P}}, Data = #pd{pid = PId}) when P =:= PId -> 105 | Info = reload_player_info(PId), 106 | forward_to_client(R, Data), 107 | {noreply, Data#pd{ playing = ?UNDEF, playing_sn = 0, record = Info }}; 108 | 109 | handle_cast({notify, R}, Data) -> 110 | forward_to_client(R, Data), 111 | {noreply, Data}; 112 | 113 | handle_cast(logout, Data = #pd{playing = ?UNDEF}) -> 114 | {noreply, Data}; 115 | 116 | handle_cast(phantom, Data = #pd{playing = ?UNDEF}) -> 117 | op_players_sup:terminate_child_ex(Data#pd.pid), 118 | {noreply, Data#pd{client = ?UNDEF, phantom = ?UNDEF}}; 119 | 120 | handle_cast(phantom, Data = #pd{playing = Game, playing_sn = SN}) -> 121 | {ok, Phantom} = op_phantom:start_link(Data#pd.pid, Game, SN), 122 | {noreply, Data#pd{client = ?UNDEF, phantom = Phantom}}; 123 | 124 | handle_cast(Msg, Data) -> 125 | error_logger:error_report([{player_unknown_cast, Msg}, {state, Data}]), 126 | {noreply, Data}. 127 | 128 | handle_call(ctx, _From, Data) -> 129 | {reply, Data, Data}; 130 | 131 | handle_call(info, _From, Data = #pd{record = R}) -> 132 | {reply, R, Data}; 133 | 134 | handle_call({client, Client}, _From, Data) when is_pid(Client) -> 135 | {reply, Client, Data#pd{ client = Client}}; 136 | 137 | handle_call(plist, _From, Data) -> 138 | {reply, [{nick, Data#pd.nick}, {photo, Data#pd.photo}, {pid, Data#pd.pid}, {proc, Data#pd.self}], Data}; 139 | 140 | handle_call(R, From, Data) -> 141 | error_logger:info_report([{module, ?MODULE}, {process, self()}, {unknown_call, R}, {from, From}]), 142 | {noreply, Data}. 143 | 144 | terminate(Reason, _Data) -> 145 | error_logger:info_report([{player, terminate}, {reason, Reason}]), 146 | ok. 147 | 148 | handle_info({'EXIT', _Pid, _Reason}, Data) -> 149 | %% child exit? 150 | {noreply, Data}; 151 | 152 | handle_info(_Info, Data) -> 153 | {noreply, Data}. 154 | 155 | code_change(_OldVsn, Data, _Extra) -> 156 | {ok, Data}. 157 | 158 | %%% 159 | %%% clinet function 160 | %%% 161 | 162 | ctx(PId) -> 163 | ctx(PId, ctx). 164 | 165 | ctx(PId, Type) -> 166 | gen_server:call(?LOOKUP_PLAYER(PId), Type). 167 | 168 | start_link(R = #tab_player_info{pid = PId}) -> 169 | gen_server:start_link(?PLAYER(PId), ?MODULE, [R], []). 170 | 171 | start(R = #tab_player_info{pid = PId}) -> 172 | gen_server:start(?PLAYER(PId), ?MODULE, [R], []). 173 | 174 | notify(R) -> 175 | notify(self(), R). 176 | 177 | notify(PId, R) when is_integer(PId) -> 178 | notify(?LOOKUP_PLAYER(PId), R); 179 | notify(Player, R) when is_pid(Player) -> 180 | gen_server:cast(Player, {notify, R}). 181 | 182 | cast(Player, R) when is_pid(Player) -> 183 | gen_server:cast(Player, R). 184 | 185 | auth(Identity, Password) when is_list(Identity), is_list(Password) -> 186 | ok = mnesia:wait_for_tables([tab_player_info], ?WAIT_TABLE), 187 | case mnesia:dirty_index_read(tab_player_info, Identity, identity) of 188 | [Info] -> 189 | auth(Info, Password); 190 | _ -> 191 | {ok, unauth} 192 | end; 193 | 194 | auth(Info = #tab_player_info{password = Pwd}, Password) when is_list(Password) -> 195 | %% password processed by phash, result is a integer 196 | case erlang:phash2(Password, 1 bsl 32) =:= Pwd of 197 | true -> auth(Info, player_disable); 198 | _ -> {ok, unauth} 199 | end; 200 | 201 | auth(Info = #tab_player_info{disabled = Disabled}, player_disable) -> 202 | case Disabled of 203 | false -> {ok, pass, Info}; 204 | _ -> {ok, player_disable} 205 | end. 206 | 207 | info(Player) when is_pid(Player) -> 208 | gen_server:cast(Player, #cmd_query_player{}). 209 | 210 | balance(Player) when is_pid(Player) -> 211 | gen_server:cast(Player, #cmd_query_balance{}). 212 | 213 | client(Player) when is_pid(Player) -> 214 | Client = self(), 215 | Client = gen_server:call(Player, {client, Client}). 216 | 217 | logout(Player) when is_pid(Player) -> 218 | gen_server:cast(Player, logout); 219 | 220 | logout(Player) when is_integer(Player) -> 221 | logout(?LOOKUP_PLAYER(Player)). 222 | 223 | fold(PID, GID) when is_integer(PID), is_pid(GID) -> 224 | gen_server:cast(?LOOKUP_PLAYER(PID), #cmd_fold{game = GID}). 225 | 226 | leave(PID, GID) when is_integer(PID), is_pid(GID) -> 227 | gen_server:cast(?LOOKUP_PLAYER(PID), #cmd_leave{game = GID}). 228 | 229 | phantom(PID) when is_pid(PID) -> 230 | gen_server:cast(PID, phantom). 231 | 232 | 233 | %%% 234 | %%% private 235 | %%% 236 | 237 | reload_player_info(PId) -> 238 | ReloadFun = fun() -> 239 | mnesia:read(tab_player_info, PId) 240 | end, 241 | {atomic, [R]} = mnesia:transaction(ReloadFun), 242 | R. 243 | 244 | create_runtime(PID, Process) when is_number(PID), is_pid(Process) -> 245 | mnesia:dirty_write(#tab_player{ pid = PID, process = Process }). 246 | 247 | forward_to_client(R, #pd{client = ?UNDEF, phantom = P}) when is_pid(P) -> 248 | op_phantom:send(P, R); 249 | forward_to_client(R, #pd{client = C, phantom = ?UNDEF}) when is_pid(C) -> 250 | op_game_handler:send(C, R). 251 | -------------------------------------------------------------------------------- /src/protocol.erl: -------------------------------------------------------------------------------- 1 | -module(protocol). 2 | -export([read/1, write/1]). 3 | -export([loop/2]). 4 | -export([id_to_player/1, id_to_game/1]). 5 | 6 | -import(pickle, [ 7 | pickle/2, unpickle/2, wrap/2, tuple/1, record/2, 8 | byte/0, short/0, int/0, list/2, binary/1, string/0]). 9 | 10 | -include("openpoker.hrl"). 11 | 12 | -define(int, int()). 13 | -define(byte, byte()). 14 | -define(string, string()). 15 | 16 | nick() -> ?string. 17 | name() -> ?string. 18 | photo() -> ?string. 19 | identity() -> ?string. 20 | password() -> ?string. 21 | 22 | state() -> ?int. 23 | game_id() -> ?int. 24 | player_id() -> ?int. 25 | size() -> ?int. 26 | 27 | min() -> ?int. 28 | max() -> ?int. 29 | pot() -> ?int. 30 | bet() -> ?int. 31 | call() -> ?int. 32 | raise() -> ?int. 33 | buyin() -> ?int. 34 | inplay() -> ?int. 35 | balance() -> ?int. 36 | amount() -> ?int. 37 | timeout() -> ?int. 38 | 39 | b() -> ?byte. 40 | bb() -> ?byte. 41 | sb() -> ?byte. 42 | sn() -> ?byte. 43 | stage() -> ?byte. 44 | seats() -> ?byte. 45 | joined() -> ?byte. 46 | require() -> ?byte. 47 | 48 | suit() -> ?byte. 49 | face() -> ?byte. 50 | rank() -> ?byte. 51 | error() -> ?byte. 52 | 53 | high1() -> face(). 54 | high2() -> face(). 55 | 56 | %% 扑克牌使用short类型,占16位,通过位运算得到 57 | %% 高8位代表扑克数值大小,低8位代表扑克的花色类型 58 | card() -> short(). 59 | cards() -> list(byte(), card()). 60 | 61 | limit() -> record(limit, {int(), int(), int(), int()}). 62 | 63 | game() -> game(get(pass_through)). 64 | player() -> player(get(pass_through)). 65 | 66 | loop(connected, ?UNDEF) -> 67 | {0, 0, 0}; 68 | 69 | loop(disconnected, {Sum, Good, Bad}) -> 70 | ?LOG([{sum, Sum}, {good, Good}, {bad, Bad}]), 71 | ok; 72 | 73 | loop({recv, Bin}, {Sum, Good, Bad}) when is_binary(Bin) -> 74 | case catch protocol:read(Bin) of 75 | {'EXIT', {Reason, Stack}} -> 76 | ?LOG([{recv, Bin}, {error, {Reason, Stack}}]), 77 | {Sum + 1, Good, Bad + 1}; 78 | R -> 79 | self() ! {send, list_to_binary(protocol:write(R))}, 80 | {Sum + 1, Good + 1, Bad} 81 | end. 82 | 83 | %%% 84 | %%% private 85 | %%% 86 | 87 | internal() -> 88 | {fun(Acc, _) -> Acc end, 89 | fun(Bin) -> {undefined, Bin} end}. 90 | 91 | game(true) -> game_id(); 92 | game(_) -> wrap({fun game_to_id/1, fun id_to_game/1}, int()). 93 | 94 | player(true) -> player_id(); 95 | player(_) -> wrap({fun player_to_id/1, fun id_to_player/1}, int()). 96 | 97 | game_to_id(G) when is_pid(G) -> erlang:process_display(self(), backtrace); 98 | game_to_id(GID) when is_integer(GID) -> GID. 99 | 100 | id_to_game(0) -> undefined; 101 | id_to_game(GID) -> 102 | case get(convert_id_to_process) of 103 | undefined -> global:whereis_name({game, GID}); 104 | _ -> GID 105 | end. 106 | 107 | player_to_id(undefined) -> 0; 108 | player_to_id(none) -> 0; 109 | player_to_id(PID) when is_integer(PID) -> PID. 110 | 111 | id_to_player(0) -> undefined; 112 | id_to_player(PID) -> 113 | case get(convert_id_to_process) of 114 | undefined -> global:whereis_name({player, PID}); 115 | _ -> PID 116 | end. 117 | 118 | %% AUTO GENERATE - Don't edit manual 119 | %% read binary to protocol record 120 | read(<>) -> 121 | unpickle(record(cmd_login, {identity(), password()}), Bin); 122 | 123 | read(<>) -> 124 | unpickle(record(cmd_logout, {}), Bin); 125 | 126 | read(<>) -> 127 | unpickle(record(cmd_query_player, {player()}), Bin); 128 | 129 | read(<>) -> 130 | unpickle(record(cmd_query_balance, {}), Bin); 131 | 132 | read(<>) -> 133 | unpickle(record(cmd_query_game, {}), Bin); 134 | 135 | read(<>) -> 136 | unpickle(record(cmd_watch, {game(), internal(), internal(), internal(), internal(), internal()}), Bin); 137 | 138 | read(<>) -> 139 | unpickle(record(cmd_unwatch, {game(), internal(), internal(), internal(), internal(), internal()}), Bin); 140 | 141 | read(<>) -> 142 | unpickle(record(cmd_join, {game(), sn(), buyin(), internal(), internal(), internal(), internal(), internal(), internal()}), Bin); 143 | 144 | read(<>) -> 145 | unpickle(record(cmd_leave, {game(), internal(), internal(), internal()}), Bin); 146 | 147 | read(<>) -> 148 | unpickle(record(cmd_raise, {game(), amount(), internal(), internal(), internal()}), Bin); 149 | 150 | read(<>) -> 151 | unpickle(record(cmd_fold, {game(), internal(), internal()}), Bin); 152 | 153 | read(<>) -> 154 | unpickle(record(cmd_query_seats, {game()}), Bin); 155 | 156 | read(<>) -> 157 | unpickle(record(cmd_out, {game(), sn(), buyin(), internal(), internal(), internal()}), Bin); 158 | 159 | read(<>) -> 160 | unpickle(record(notify_acount, {balance(), inplay()}), Bin); 161 | 162 | read(<>) -> 163 | unpickle(record(notify_game, {game_id(), name(), limit(), seats(), require(), joined()}), Bin); 164 | 165 | read(<>) -> 166 | unpickle(record(notify_game_detail, {game_id(), pot(), stage(), limit(), seats(), require(), joined()}), Bin); 167 | 168 | read(<>) -> 169 | unpickle(record(notify_game_start, {game_id()}), Bin); 170 | 171 | read(<>) -> 172 | unpickle(record(notify_game_end, {game_id()}), Bin); 173 | 174 | read(<>) -> 175 | unpickle(record(notify_game_cancel, {game_id()}), Bin); 176 | 177 | read(<>) -> 178 | unpickle(record(notify_stage, {game_id(), stage()}), Bin); 179 | 180 | read(<>) -> 181 | unpickle(record(notify_stage_end, {game_id(), stage()}), Bin); 182 | 183 | read(<>) -> 184 | unpickle(record(notify_join, {game_id(), player_id(), sn(), buyin(), nick(), photo(), internal(), internal()}), Bin); 185 | 186 | read(<>) -> 187 | unpickle(record(notify_leave, {game_id(), sn(), player_id(), internal(), internal()}), Bin); 188 | 189 | read(<>) -> 190 | unpickle(record(notify_button, {game_id(), b()}), Bin); 191 | 192 | read(<>) -> 193 | unpickle(record(notify_sb, {game_id(), sb()}), Bin); 194 | 195 | read(<>) -> 196 | unpickle(record(notify_bb, {game_id(), bb()}), Bin); 197 | 198 | read(<>) -> 199 | unpickle(record(notify_raise, {game_id(), player_id(), sn(), raise(), call()}), Bin); 200 | 201 | read(<>) -> 202 | unpickle(record(notify_seat, {game_id(), sn(), state(), player_id(), inplay(), bet(), nick(), photo()}), Bin); 203 | 204 | read(<>) -> 205 | unpickle(record(notify_actor, {game_id(), player_id(), sn(), timeout()}), Bin); 206 | 207 | read(<>) -> 208 | unpickle(record(notify_betting, {game_id(), player_id(), sn(), call(), min(), max()}), Bin); 209 | 210 | read(<>) -> 211 | unpickle(record(notify_draw, {game_id(), player_id(), sn(), card()}), Bin); 212 | 213 | read(<>) -> 214 | unpickle(record(notify_private, {game_id(), player_id(), sn(), card()}), Bin); 215 | 216 | read(<>) -> 217 | unpickle(record(notify_shared, {game_id(), card()}), Bin); 218 | 219 | read(<>) -> 220 | unpickle(record(notify_hand, {game_id(), player_id(), sn(), rank(), high1(), high2(), suit()}), Bin); 221 | 222 | read(<>) -> 223 | unpickle(record(notify_cards, {game_id(), player_id(), sn(), cards()}), Bin); 224 | 225 | read(<>) -> 226 | unpickle(record(notify_win, {game_id(), player_id(), sn(), amount()}), Bin); 227 | 228 | read(<>) -> 229 | unpickle(record(notify_player, {player_id(), nick(), photo()}), Bin); 230 | 231 | read(<>) -> 232 | unpickle(record(notify_fold, {game_id(), sn()}), Bin); 233 | 234 | read(<>) -> 235 | unpickle(record(notify_out, {game_id(), player_id()}), Bin); 236 | 237 | read(<>) -> 238 | unpickle(record(notify_games_list_end, {size()}), Bin); 239 | 240 | read(<>) -> 241 | unpickle(record(notify_seats_list_end, {size()}), Bin); 242 | 243 | read(<>) -> 244 | unpickle(record(notify_watch, {game_id(), player_id(), internal(), internal()}), Bin); 245 | 246 | read(<>) -> 247 | unpickle(record(notify_unwatch, {game_id(), player_id(), internal(), internal()}), Bin); 248 | 249 | read(<>) -> 250 | unpickle(record(notify_signin, {player_id()}), Bin); 251 | 252 | read(<>) -> 253 | unpickle(record(notify_error, {error()}), Bin); 254 | 255 | read(_ErrorBin) -> ok. 256 | 257 | %% write protocol record to binary 258 | write(R) when is_record(R, cmd_login) -> 259 | [?CMD_LOGIN | pickle(record(cmd_login, {identity(), password()}), R)]; 260 | 261 | write(R) when is_record(R, cmd_logout) -> 262 | [?CMD_LOGOUT | pickle(record(cmd_logout, {}), R)]; 263 | 264 | write(R) when is_record(R, cmd_query_player) -> 265 | [?CMD_QUERY_PLAYER | pickle(record(cmd_query_player, {player()}), R)]; 266 | 267 | write(R) when is_record(R, cmd_query_balance) -> 268 | [?CMD_QUERY_BALANCE | pickle(record(cmd_query_balance, {}), R)]; 269 | 270 | write(R) when is_record(R, cmd_query_game) -> 271 | [?CMD_QUERY_GAME | pickle(record(cmd_query_game, {}), R)]; 272 | 273 | write(R) when is_record(R, cmd_watch) -> 274 | [?CMD_WATCH | pickle(record(cmd_watch, {game(), internal(), internal(), internal(), internal(), internal()}), R)]; 275 | 276 | write(R) when is_record(R, cmd_unwatch) -> 277 | [?CMD_UNWATCH | pickle(record(cmd_unwatch, {game(), internal(), internal(), internal(), internal(), internal()}), R)]; 278 | 279 | write(R) when is_record(R, cmd_join) -> 280 | [?CMD_JOIN | pickle(record(cmd_join, {game(), sn(), buyin(), internal(), internal(), internal(), internal(), internal(), internal()}), R)]; 281 | 282 | write(R) when is_record(R, cmd_leave) -> 283 | [?CMD_LEAVE | pickle(record(cmd_leave, {game(), internal(), internal(), internal()}), R)]; 284 | 285 | write(R) when is_record(R, cmd_raise) -> 286 | [?CMD_RAISE | pickle(record(cmd_raise, {game(), amount(), internal(), internal(), internal()}), R)]; 287 | 288 | write(R) when is_record(R, cmd_fold) -> 289 | [?CMD_FOLD | pickle(record(cmd_fold, {game(), internal(), internal()}), R)]; 290 | 291 | write(R) when is_record(R, cmd_query_seats) -> 292 | [?CMD_QUERY_SEATS | pickle(record(cmd_query_seats, {game()}), R)]; 293 | 294 | write(R) when is_record(R, cmd_out) -> 295 | [?CMD_OUT | pickle(record(cmd_out, {game(), sn(), buyin(), internal(), internal(), internal()}), R)]; 296 | 297 | write(R) when is_record(R, notify_acount) -> 298 | [?NOTIFY_ACOUNT | pickle(record(notify_acount, {balance(), inplay()}), R)]; 299 | 300 | write(R) when is_record(R, notify_game) -> 301 | [?NOTIFY_GAME | pickle(record(notify_game, {game_id(), name(), limit(), seats(), require(), joined()}), R)]; 302 | 303 | write(R) when is_record(R, notify_game_detail) -> 304 | [?NOTIFY_GAME_DETAIL | pickle(record(notify_game_detail, {game_id(), pot(), stage(), limit(), seats(), require(), joined()}), R)]; 305 | 306 | write(R) when is_record(R, notify_game_start) -> 307 | [?NOTIFY_GAME_START | pickle(record(notify_game_start, {game_id()}), R)]; 308 | 309 | write(R) when is_record(R, notify_game_end) -> 310 | [?NOTIFY_GAME_END | pickle(record(notify_game_end, {game_id()}), R)]; 311 | 312 | write(R) when is_record(R, notify_game_cancel) -> 313 | [?NOTIFY_GAME_CANCEL | pickle(record(notify_game_cancel, {game_id()}), R)]; 314 | 315 | write(R) when is_record(R, notify_stage) -> 316 | [?NOTIFY_STAGE | pickle(record(notify_stage, {game_id(), stage()}), R)]; 317 | 318 | write(R) when is_record(R, notify_stage_end) -> 319 | [?NOTIFY_STAGE_END | pickle(record(notify_stage_end, {game_id(), stage()}), R)]; 320 | 321 | write(R) when is_record(R, notify_join) -> 322 | [?NOTIFY_JOIN | pickle(record(notify_join, {game_id(), player_id(), sn(), buyin(), nick(), photo(), internal(), internal()}), R)]; 323 | 324 | write(R) when is_record(R, notify_leave) -> 325 | [?NOTIFY_LEAVE | pickle(record(notify_leave, {game_id(), sn(), player_id(), internal(), internal()}), R)]; 326 | 327 | write(R) when is_record(R, notify_button) -> 328 | [?NOTIFY_BUTTON | pickle(record(notify_button, {game_id(), b()}), R)]; 329 | 330 | write(R) when is_record(R, notify_sb) -> 331 | [?NOTIFY_SB | pickle(record(notify_sb, {game_id(), sb()}), R)]; 332 | 333 | write(R) when is_record(R, notify_bb) -> 334 | [?NOTIFY_BB | pickle(record(notify_bb, {game_id(), bb()}), R)]; 335 | 336 | write(R) when is_record(R, notify_raise) -> 337 | [?NOTIFY_RAISE | pickle(record(notify_raise, {game_id(), player_id(), sn(), raise(), call()}), R)]; 338 | 339 | write(R) when is_record(R, notify_seat) -> 340 | [?NOTIFY_SEAT | pickle(record(notify_seat, {game_id(), sn(), state(), player_id(), inplay(), bet(), nick(), photo()}), R)]; 341 | 342 | write(R) when is_record(R, notify_actor) -> 343 | [?NOTIFY_ACTOR | pickle(record(notify_actor, {game_id(), player_id(), sn(), timeout()}), R)]; 344 | 345 | write(R) when is_record(R, notify_betting) -> 346 | [?NOTIFY_BETTING | pickle(record(notify_betting, {game_id(), player_id(), sn(), call(), min(), max()}), R)]; 347 | 348 | write(R) when is_record(R, notify_draw) -> 349 | [?NOTIFY_DRAW | pickle(record(notify_draw, {game_id(), player_id(), sn(), card()}), R)]; 350 | 351 | write(R) when is_record(R, notify_private) -> 352 | [?NOTIFY_PRIVATE | pickle(record(notify_private, {game_id(), player_id(), sn(), card()}), R)]; 353 | 354 | write(R) when is_record(R, notify_shared) -> 355 | [?NOTIFY_SHARED | pickle(record(notify_shared, {game_id(), card()}), R)]; 356 | 357 | write(R) when is_record(R, notify_hand) -> 358 | [?NOTIFY_HAND | pickle(record(notify_hand, {game_id(), player_id(), sn(), rank(), high1(), high2(), suit()}), R)]; 359 | 360 | write(R) when is_record(R, notify_cards) -> 361 | [?NOTIFY_CARDS | pickle(record(notify_cards, {game_id(), player_id(), sn(), cards()}), R)]; 362 | 363 | write(R) when is_record(R, notify_win) -> 364 | [?NOTIFY_WIN | pickle(record(notify_win, {game_id(), player_id(), sn(), amount()}), R)]; 365 | 366 | write(R) when is_record(R, notify_player) -> 367 | [?NOTIFY_PLAYER | pickle(record(notify_player, {player_id(), nick(), photo()}), R)]; 368 | 369 | write(R) when is_record(R, notify_fold) -> 370 | [?NOTIFY_FOLD | pickle(record(notify_fold, {game_id(), sn()}), R)]; 371 | 372 | write(R) when is_record(R, notify_out) -> 373 | [?NOTIFY_OUT | pickle(record(notify_out, {game_id(), player_id()}), R)]; 374 | 375 | write(R) when is_record(R, notify_games_list_end) -> 376 | [?NOTIFY_GAMES_LIST_END | pickle(record(notify_games_list_end, {size()}), R)]; 377 | 378 | write(R) when is_record(R, notify_seats_list_end) -> 379 | [?NOTIFY_SEATS_LIST_END | pickle(record(notify_seats_list_end, {size()}), R)]; 380 | 381 | write(R) when is_record(R, notify_watch) -> 382 | [?NOTIFY_WATCH | pickle(record(notify_watch, {game_id(), player_id(), internal(), internal()}), R)]; 383 | 384 | write(R) when is_record(R, notify_unwatch) -> 385 | [?NOTIFY_UNWATCH | pickle(record(notify_unwatch, {game_id(), player_id(), internal(), internal()}), R)]; 386 | 387 | write(R) when is_record(R, notify_signin) -> 388 | [?NOTIFY_SIGNIN | pickle(record(notify_signin, {player_id()}), R)]; 389 | 390 | write(R) when is_record(R, notify_error) -> 391 | [?NOTIFY_ERROR | pickle(record(notify_error, {error()}), R)]; 392 | 393 | write(_ErrorRecord) -> ok. 394 | -------------------------------------------------------------------------------- /src/schema.erl: -------------------------------------------------------------------------------- 1 | -module(schema). 2 | -export([rebuild_schema/0, rebuild_core/0, rebuild_core_and_data/0]). 3 | 4 | -include("openpoker.hrl"). 5 | 6 | -define(RAM, {ram_copies, Nodes}). 7 | -define(DISC, {disc_copies, Nodes}). 8 | -define(TABLE_DEF(Name, Type, Copies, Fields), {Name, [Copies, {type, Type}, {attributes, Fields}]}). 9 | 10 | rebuild_schema() -> 11 | stopped = mnesia:stop(), 12 | ok = mnesia:delete_schema(all_nodes()), 13 | timer:sleep(500), 14 | ok = mnesia:create_schema(all_nodes()), 15 | timer:sleep(500), 16 | ok = mnesia:start(). 17 | 18 | rebuild_core() -> 19 | rebuild_core(all_nodes()). 20 | 21 | rebuild_core_and_data() -> 22 | rebuild_core(), 23 | setup_games(), 24 | setup_players(). 25 | 26 | %% Private 27 | 28 | rebuild_core(Nodes) -> 29 | rebuild_core_table(Nodes), 30 | setup_counters(). 31 | 32 | rebuild_core_table(Nodes) -> 33 | RamTables = [ 34 | ?TABLE_DEF(tab_game_xref, set, ?RAM, record_info(fields, tab_game_xref)), 35 | ?TABLE_DEF(tab_player, set, ?RAM, record_info(fields, tab_player)) 36 | ], 37 | DiscTables = [ 38 | ?TABLE_DEF(tab_player_info, set, ?DISC, record_info(fields, tab_player_info)), 39 | ?TABLE_DEF(tab_inplay, set, ?DISC, record_info(fields, tab_inplay)), 40 | ?TABLE_DEF(tab_game_config, set, ?DISC, record_info(fields, tab_game_config)), 41 | ?TABLE_DEF(tab_counter, set, ?DISC, record_info(fields, tab_counter)), 42 | 43 | ?TABLE_DEF(tab_turnover_log, bag, ?DISC, record_info(fields, tab_turnover_log)), 44 | ?TABLE_DEF(tab_buyin_log, bag, ?DISC, record_info(fields, tab_buyin_log)) 45 | ], 46 | 47 | create_tables(RamTables), 48 | create_tables(DiscTables), 49 | 50 | create_indices(tab_player_info, [identity]), 51 | create_indices(tab_buyin_log, [pid, date]), 52 | create_indices(tab_turnover_log, [pid, date]). 53 | 54 | all_nodes() -> 55 | nodes() ++ [node()]. 56 | 57 | create_tables([]) -> ok; 58 | create_tables([{Name, TabDef}|T]) -> 59 | {atomic, ok} = mnesia:create_table(Name, TabDef), 60 | create_tables(T). 61 | 62 | create_indices(_, []) -> ok; 63 | create_indices(Name, Index) when is_atom(Index) -> 64 | create_indices(Name, [Index]); 65 | create_indices(Name, [Index|T]) -> 66 | {atomic, ok} = mnesia:add_table_index(Name, Index), 67 | create_indices(Name, T). 68 | 69 | setup_counters()-> 70 | counter:reset(game), 71 | counter:reset(player), 72 | counter:reset(inplay_xref), 73 | ok. 74 | 75 | setup_games() -> 76 | Limit = #limit{small = 10, big = 20, min = 500, max = 1000}, 77 | mnesia:dirty_write(#tab_game_config{id = 1, module = game, mods = ?DEF_MOD, limit = Limit, seat_count = 9, start_delay = ?START_DELAY, required = 2, timeout = 1000, max = 10}). 78 | %mnesia:dirty_write(#tab_game_config{id = 2, module = game, mods = ?DEF_MOD, limit = Limit#limit{small = 100, big = 200}, seat_count = 9, start_delay = ?START_DELAY, required = 2, timeout = 1000, max = 10}), 79 | %mnesia:dirty_write(#tab_game_config{id = 3, module = game, mods = ?DEF_MOD, limit = Limit#limit{small = 500, big = 1000}, seat_count = 5, start_delay = ?START_DELAY, required = 2, timeout = 1000, max = 10}). 80 | 81 | setup_players() -> 82 | mnesia:dirty_write(#tab_player_info{pid = 1, identity = "player", password = ?DEF_HASH_PWD, nick = "nick", photo = "default.png", cash = 10000}), 83 | mnesia:dirty_write(#tab_player_info{pid = 2, identity = "robot", password = ?DEF_HASH_PWD, nick = "robot", photo = "2012491443.jpg", cash = 10000}), 84 | mnesia:dirty_write(#tab_player_info{pid = 3, identity = "robot_foo", password = ?DEF_HASH_PWD, nick = "robot foo", photo = "2011962129360.jpg", cash = 10000}), 85 | mnesia:dirty_write(#tab_player_info{pid = 4, identity = "robot_doo", password = ?DEF_HASH_PWD, nick = "robot doo", photo = "2012491439.jpg", cash = 10000}), 86 | setup_test_players(200). 87 | 88 | setup_test_players(100) -> 89 | ok; 90 | setup_test_players(N) -> 91 | Nick = string:concat("TEST ", integer_to_list(N)), 92 | Identity = string:concat("tester_", integer_to_list(N)), 93 | mnesia:dirty_write(#tab_player_info{pid = N, identity = Identity, password = ?HASH_PWD("pwd"), nick = Nick, photo = "default.png", cash = 100000}), 94 | setup_test_players(N - 1). 95 | -------------------------------------------------------------------------------- /src/texas/deck.erl: -------------------------------------------------------------------------------- 1 | -module(deck). 2 | -export([new/0, new/1, draw/1, size/1]). 3 | 4 | -include("openpoker.hrl"). 5 | 6 | new() -> 7 | case op_common:get_env(deck, ?UNDEF) of 8 | ?UNDEF -> 9 | shuffle(make_deck()); 10 | Cards -> 11 | hand:make_cards(Cards) 12 | end. 13 | 14 | new(Cards) -> 15 | Cards. 16 | 17 | draw([]) -> 18 | none; 19 | 20 | draw([H|T]) -> 21 | {H, T}. 22 | 23 | size(Cards) -> 24 | length(Cards). 25 | 26 | make_deck() -> 27 | L1 = [ ?CF_TWO, 28 | ?CF_THREE, 29 | ?CF_FOUR, 30 | ?CF_FIVE, 31 | ?CF_SIX, 32 | ?CF_SEVEN, 33 | ?CF_EIGHT, 34 | ?CF_NINE, 35 | ?CF_TEN, 36 | ?CF_JACK, 37 | ?CF_QUEEN, 38 | ?CF_KING, 39 | ?CF_ACE ], 40 | L2 = [ ?CS_CLUBS, 41 | ?CS_DIAMONDS, 42 | ?CS_HEARTS, 43 | ?CS_SPADES ], 44 | [?POKER_ENCODE(Suit, Face) || Face <- L1, Suit <- L2]. 45 | 46 | shuffle(Cards) -> 47 | random:seed(now()), 48 | Temp = lists:map(fun(X) -> {random:uniform(), X} end, Cards), 49 | Temp1 = lists:keysort(1, Temp), 50 | lists:map(fun(X) -> element(2, X) end, Temp1). 51 | -------------------------------------------------------------------------------- /src/texas/game.erl: -------------------------------------------------------------------------------- 1 | -module(game). 2 | -behaviour(op_exch). 3 | 4 | -export([id/0, init/2, stop/1, dispatch/2, call/2]). 5 | -export([start/0, start/1, start_conf/2, config/0]). 6 | -export([watch/2, unwatch/2, join/2, leave/2, bet/2]). 7 | -export([reward/3, query_seats/1, list/0, raise/2, fold/2]). 8 | -export([do_leave/2, start_timer/2, cancel_timer/1]). 9 | -export([broadcast/2, broadcast/3, info/1]). 10 | 11 | -include("openpoker.hrl"). 12 | 13 | %%% 14 | %%% callback 15 | %%% 16 | 17 | id() -> 18 | counter:bump(game). 19 | 20 | init(GID, R = #tab_game_config{}) -> 21 | create_runtime(GID, R), 22 | #texas { 23 | gid = GID, 24 | seats = seat:new(R#tab_game_config.seat_count), 25 | max_joined = R#tab_game_config.seat_count, 26 | limit = R#tab_game_config.limit, 27 | timeout = ?PLAYER_TIMEOUT, 28 | start_delay = R#tab_game_config.start_delay, 29 | required = R#tab_game_config.required, 30 | xref = gb_trees:empty(), 31 | pot = pot:new(), 32 | deck = deck:new(), 33 | b = ?UNDEF, 34 | sb = ?UNDEF, 35 | bb = ?UNDEF 36 | }. 37 | 38 | stop(#texas{gid = GID, timer = Timer}) -> 39 | catch erlang:cancel_timer(Timer), 40 | clear_runtime(GID). 41 | 42 | call(_, Ctx) -> 43 | {ok, Ctx, Ctx}. 44 | 45 | dispatch(R = #cmd_watch{proc = Proc, identity = Identity}, Ctx = #texas{observers = Obs}) -> 46 | player:notify(Proc, gen_game_detail(Ctx)), 47 | 48 | WatchedCtx = case proplists:lookup(Identity, Obs) of 49 | {Identity, _Proc} -> 50 | NewObs = [{Identity, Proc}] ++ proplists:delete(Identity, Obs), 51 | Ctx#texas{observers = NewObs}; 52 | none -> 53 | Ctx#texas{observers = [{Identity, Proc}] ++ Obs} 54 | end, 55 | 56 | notify_player_seats(Proc, WatchedCtx), 57 | 58 | NotifyWatch = #notify_watch{ 59 | proc = self(), 60 | game = Ctx#texas.gid, 61 | player = R#cmd_watch.pid}, 62 | broadcast(NotifyWatch, WatchedCtx), 63 | 64 | WatchedCtx; 65 | 66 | dispatch(R = #cmd_unwatch{identity = Identity}, Ctx = #texas{observers = Obs}) -> 67 | case proplists:lookup(Identity, Obs) of 68 | {Identity, _Proc} -> 69 | NotifyUnwatch = #notify_unwatch{ 70 | proc = self(), 71 | game = Ctx#texas.gid, 72 | player = R#cmd_unwatch.pid}, 73 | broadcast(NotifyUnwatch, Ctx), 74 | Ctx#texas{observers = proplists:delete(Identity, Obs)}; 75 | none -> 76 | Ctx 77 | end; 78 | 79 | dispatch(#cmd_join{buyin = Buyin}, Ctx = #texas{limit = Limit, joined = Joined, max_joined = MaxJoin}) 80 | when Joined =:= MaxJoin; Buyin < Limit#limit.min; Buyin > Limit#limit.max -> 81 | Ctx; 82 | 83 | dispatch(R = #cmd_join{sn = SN}, Ctx = #texas{seats = Seats}) when SN /= 0 -> 84 | case seat:get(SN, Seats) of 85 | Seat = #seat{state = ?PS_EMPTY} -> 86 | do_join(R, Seat, Ctx); 87 | _ -> 88 | dispatch(R#cmd_join{sn = 0}, Ctx) 89 | end; 90 | 91 | dispatch(R = #cmd_join{sn = SN}, Ctx = #texas{seats = Seats}) when SN =:= 0 -> 92 | %% auto compute player seat number 93 | [H = #seat{}|_] = seat:lookup(?PS_EMPTY, Seats), 94 | do_join(R, H, Ctx); 95 | 96 | dispatch(R = #cmd_leave{}, Ctx = #texas{}) -> 97 | do_leave(R, Ctx); 98 | 99 | dispatch({query_seats, Player}, Ctx) when is_pid(Player)-> 100 | notify_player_seats(Player, Ctx). 101 | 102 | %%% 103 | %%% client 104 | %%% 105 | 106 | start() -> 107 | Fun = fun(R = #tab_game_config{max = Max}, _Acc) -> 108 | start_conf(R, Max) 109 | end, 110 | 111 | ok = mnesia:wait_for_tables([tab_game_config], ?WAIT_TABLE), 112 | {atomic, Result} = mnesia:transaction(fun() -> mnesia:foldl(Fun, nil, tab_game_config) end), 113 | Result. 114 | 115 | start(Mods) when is_list(Mods)-> 116 | Conf = #tab_game_config{id = 1, module = game, mods = Mods, limit = no_limit, seat_count = 9, start_delay = 3000, required = 2, timeout = 1000, max = 1}, 117 | start_conf(Conf, 1); 118 | 119 | start(Conf = #tab_game_config{max = Max}) -> 120 | start_conf(Conf, Max). 121 | 122 | start_conf(Conf, N) -> 123 | start_conf(Conf, N, []). 124 | 125 | start_conf(_Conf, 0, L) -> L; 126 | start_conf(Conf = #tab_game_config{module = Module, mods = Mods}, N, L) when is_list(Mods) -> 127 | {ok, Pid} = op_exch:start_link(Module, Conf, Mods), 128 | start_conf(Conf, N - 1, L ++ [{ok, Pid}]); 129 | start_conf(Conf = #tab_game_config{}, N, L) -> 130 | start_conf(Conf#tab_game_config{mods = default_mods()}, N, L). 131 | 132 | config() -> 133 | Fun = fun(R, Acc) -> [R] ++ Acc end, 134 | ok = mnesia:wait_for_tables([tab_game_config], ?WAIT_TABLE), 135 | {atomic, NewAcc} = mnesia:transaction(fun() -> mnesia:foldl(Fun, [], tab_game_config) end), 136 | lists:reverse(NewAcc). 137 | 138 | %% check 139 | 140 | bet({R = #seat{}, Amt}, Ctx = #texas{}) -> 141 | bet({R, Amt, 0}, Ctx); 142 | 143 | bet({R = #seat{sn = SN}, _Call = 0, _Raise = 0}, Ctx = #texas{seats = Seats}) -> 144 | broadcast(#notify_raise{game = Ctx#texas.gid, player = R#seat.pid, sn = SN, raise = 0, call = 0}, Ctx), 145 | NewSeats = seat:set(SN, ?PS_BET, Seats), 146 | Ctx#texas{seats = NewSeats}; 147 | 148 | %% call & raise 149 | bet({S = #seat{inplay = Inplay, bet = Bet, pid = PId, sn = SN}, Call, Raise}, Ctx = #texas{seats = Seats}) -> 150 | Amt = Call + Raise, 151 | {State, AllIn, CostAmt} = case Amt < Inplay of 152 | true -> {?PS_BET, false, Amt}; 153 | _ -> {?PS_ALL_IN, true, Inplay} 154 | end, 155 | 156 | Fun = fun() -> 157 | [R] = mnesia:read(tab_inplay, PId, write), 158 | ok = mnesia:write(R#tab_inplay{inplay = Inplay - CostAmt}), 159 | ok = mnesia:write(#tab_turnover_log{ 160 | pid = PId, game = Ctx#texas.gid, 161 | amt = 0 - CostAmt, cost = 0, inplay = Inplay - CostAmt}) 162 | end, 163 | 164 | case mnesia:transaction(Fun) of 165 | {atomic, ok} -> 166 | broadcast(#notify_raise{game = Ctx#texas.gid, player = PId, sn = SN, raise = Raise, call = Call}, Ctx), 167 | NewSeats = seat:set(S#seat{inplay = Inplay - CostAmt, state = State, bet = Bet + CostAmt}, Seats), 168 | NewPot = pot:add(Ctx#texas.pot, PId, Amt, AllIn), 169 | Ctx#texas{seats = NewSeats, pot = NewPot} 170 | end. 171 | 172 | reward(#hand{seat_sn = SN, pid = PId}, Amt, Ctx = #texas{seats = Seats}) when Amt > 0 -> 173 | WinAmt = Amt, 174 | Seat = seat:get(SN, Seats), 175 | PId = Seat#seat.pid, 176 | 177 | Fun = fun() -> 178 | [R] = mnesia:read(tab_inplay, PId, write), 179 | RewardInplay = R#tab_inplay.inplay + WinAmt, 180 | ok = mnesia:write(R#tab_inplay{inplay = R#tab_inplay.inplay + WinAmt}), 181 | ok = mnesia:write(#tab_turnover_log{ 182 | pid = Seat#seat.pid, game = Ctx#texas.gid, 183 | amt = WinAmt, cost = Amt - WinAmt, inplay = RewardInplay}), 184 | RewardInplay 185 | end, 186 | 187 | case mnesia:transaction(Fun) of 188 | {atomic, RewardInplay} -> 189 | broadcast(#notify_win{ game = Ctx#texas.gid, sn = SN, player = PId, amount = WinAmt}, Ctx), 190 | RewardedSeats = seat:set(Seat#seat{inplay = RewardInplay, bet = 0}, Seats), 191 | Ctx#texas{seats = RewardedSeats} 192 | end. 193 | 194 | broadcast(Msg, #texas{observers = Obs}, []) -> 195 | broadcast(Msg, Obs); 196 | broadcast(Msg, Ctx = #texas{observers = Obs}, _Exclude = [H|T]) -> 197 | broadcast(Msg, Ctx#texas{observers = proplists:delete(H, Obs)}, T). 198 | 199 | broadcast(Msg, #texas{observers = Obs}) -> 200 | broadcast(Msg, Obs); 201 | broadcast(_Msg, []) -> ok; 202 | broadcast(Msg, [{_, Process}|T]) -> 203 | player:notify(Process, Msg), 204 | broadcast(Msg, T). 205 | 206 | watch(Game, R = #cmd_watch{}) when is_pid(Game) -> 207 | gen_server:cast(Game, R#cmd_watch{proc = self()}). 208 | 209 | unwatch(Game, R = #cmd_unwatch{}) when is_pid(Game) -> 210 | gen_server:cast(Game, R#cmd_unwatch{proc = self()}). 211 | 212 | join(Game, R = #cmd_join{}) when is_pid(Game) -> 213 | gen_server:cast(Game, R#cmd_join{proc = self()}). 214 | 215 | leave(Game, R = #cmd_leave{}) when is_pid(Game) -> 216 | gen_server:cast(Game, R). 217 | 218 | raise(Game, R = #cmd_raise{}) when is_pid(Game) -> 219 | gen_server:cast(Game, R). 220 | 221 | fold(Game, R = #cmd_fold{}) when is_pid(Game) -> 222 | gen_server:cast(Game, R). 223 | 224 | query_seats(Game) when is_pid(Game) -> 225 | gen_server:cast(Game, {query_seats, self()}). 226 | 227 | list() -> 228 | Fun = fun(#tab_game_xref{process = Game}, Acc) -> 229 | Acc ++ [info(Game)] 230 | end, 231 | {atomic, Result} = mnesia:transaction(fun() -> mnesia:foldl(Fun, [], tab_game_xref) end), 232 | Result. 233 | 234 | info(Game) -> 235 | State = op_common:get_status(Game), 236 | get_notify_game(State#exch.ctx). 237 | 238 | start_timer(Ctx = #texas{timeout = Timeout}, Msg) -> 239 | Timer = erlang:start_timer(Timeout, self(), Msg), 240 | Ctx#texas{timer = Timer}. 241 | 242 | cancel_timer(Ctx = #texas{timer = T}) -> 243 | catch erlang:cancel_timer(T), 244 | Ctx#texas{timer = ?UNDEF}. 245 | 246 | do_leave(R = #cmd_leave{sn = SN, pid = PId}, Ctx = #texas{seats = Seats}) -> 247 | #seat{pid = PId} = seat:get(SN, Seats), 248 | 249 | DoLeaveFun = fun() -> 250 | [Info] = mnesia:read(tab_player_info, PId, write), 251 | [Inplay] = mnesia:read(tab_inplay, PId, write), 252 | 253 | true = Inplay#tab_inplay.inplay >= 0, 254 | 255 | NewCash = Info#tab_player_info.cash + Inplay#tab_inplay.inplay, 256 | 257 | ok = mnesia:write(#tab_buyin_log{ 258 | gid = Ctx#texas.gid, 259 | pid = R#cmd_leave.pid, 260 | amt = Inplay#tab_inplay.inplay, 261 | cash = NewCash, 262 | credit = Info#tab_player_info.credit}), 263 | ok = mnesia:write(Info#tab_player_info{cash = NewCash}), 264 | ok = mnesia:delete_object(Inplay) 265 | end, 266 | 267 | {atomic, ok} = mnesia:transaction(DoLeaveFun), 268 | 269 | LeaveMsg = #notify_leave{ 270 | sn = SN, 271 | player = PId, 272 | game = Ctx#texas.gid, 273 | proc = self()}, 274 | broadcast(LeaveMsg, Ctx), 275 | 276 | %% 目前还不清楚当玩家离开时设置PS_EMPTY会对后面的结算有什么影响。 277 | %% 但当收到cmd_leave请求时确实应该将其状态设置为EMPTY而不是什么LEAVE。 278 | %% 或者说还不太清楚LEAVE这个状态到底代表什么意思。 279 | 280 | LeavedSeats = seat:clear(SN, Seats), 281 | Ctx#texas{seats = LeavedSeats, joined = Ctx#texas.joined - 1}. 282 | 283 | %%% 284 | %%% private 285 | %%% 286 | 287 | get_notify_game(#texas{gid = GId, joined = Joined, required = Required, seats = Seats, limit = Limit}) -> 288 | #notify_game{ 289 | game = GId, 290 | name = <<"TEXAS_TABLE">>, 291 | limit = Limit, 292 | seats = seat:info(size, Seats), 293 | require = Required, 294 | joined = Joined 295 | }. 296 | 297 | 298 | create_runtime(GID, R) -> 299 | mnesia:dirty_write(#tab_game_xref { 300 | gid = GID, 301 | process = self(), 302 | module = R#tab_game_config.module, 303 | limit = R#tab_game_config.limit, 304 | seat_count = R#tab_game_config.seat_count, 305 | timeout = R#tab_game_config.timeout, 306 | required = R#tab_game_config.required 307 | }). 308 | 309 | clear_runtime(GID) -> 310 | ok = mnesia:dirty_delete(tab_game_xref, GID). 311 | 312 | notify_player_seats(Player, Ctx) -> 313 | Fun = fun(R) -> 314 | R1 = #notify_seat{ 315 | game = Ctx#texas.gid, 316 | sn = R#seat.sn, 317 | state = R#seat.state, 318 | player = R#seat.pid, 319 | inplay = R#seat.inplay, 320 | bet = R#seat.bet, 321 | nick = R#seat.nick, 322 | photo = R#seat.photo 323 | }, 324 | 325 | player:notify(Player, R1) 326 | end, 327 | SeatsList = seat:get(Ctx#texas.seats), 328 | lists:map(Fun, SeatsList), 329 | player:notify(Player, #notify_seats_list_end{size = length(SeatsList)}), 330 | Ctx. 331 | 332 | default_mods() -> 333 | ?DEF_MOD. 334 | 335 | do_join(R = #cmd_join{identity = Identity, proc = Process}, Seat = #seat{}, Ctx = #texas{observers = Obs, seats = Seats}) -> 336 | case proplists:lookup(Identity, Obs) of 337 | {Identity, Process} -> 338 | JoinedSeats = seat:set(Seat#seat{ 339 | identity = Identity, 340 | pid = R#cmd_join.pid, 341 | process = Process, 342 | hand = hand:new(), 343 | bet = 0, 344 | inplay = R#cmd_join.buyin, 345 | state = ?PS_WAIT, 346 | nick = R#cmd_join.nick, 347 | photo = R#cmd_join.photo 348 | }, Seats), 349 | 350 | 351 | JoinMsg = #notify_join{ 352 | game = Ctx#texas.gid, 353 | player = R#cmd_join.pid, 354 | sn = Seat#seat.sn, 355 | buyin = R#cmd_join.buyin, 356 | nick = R#cmd_join.nick, 357 | photo = R#cmd_join.photo, 358 | proc = self() 359 | }, 360 | 361 | Fun = fun() -> 362 | [] = mnesia:read(tab_inplay, R#cmd_join.pid), % check none inplay record 363 | [Info] = mnesia:read(tab_player_info, R#cmd_join.pid, write), 364 | Balance = Info#tab_player_info.cash + Info#tab_player_info.credit, 365 | 366 | case Balance < R#cmd_join.buyin of 367 | true -> 368 | exit(err_join_less_balance); 369 | _ -> 370 | ok 371 | end, 372 | 373 | ok = mnesia:write(#tab_buyin_log{ 374 | pid = R#cmd_join.pid, gid = Ctx#texas.gid, 375 | amt = 0 - R#cmd_join.buyin, cash = Info#tab_player_info.cash - R#cmd_join.buyin, 376 | credit = Info#tab_player_info.credit}), 377 | ok = mnesia:write(#tab_inplay{pid = R#cmd_join.pid, inplay = R#cmd_join.buyin}), 378 | ok = mnesia:write(Info#tab_player_info{cash = Info#tab_player_info.cash - R#cmd_join.buyin}) 379 | end, 380 | 381 | case mnesia:transaction(Fun) of 382 | {atomic, ok} -> 383 | broadcast(JoinMsg, Ctx), 384 | Ctx#texas{seats = JoinedSeats, joined = Ctx#texas.joined + 1}; 385 | {aborted, Err} -> 386 | ?LOG([{game, error}, {join, R}, {ctx, Ctx}, {error, Err}]), 387 | Ctx 388 | end; 389 | _ -> % not find watch in observers 390 | ?LOG([{game, error}, {join, R}, {ctx, Ctx}, {error, not_find_observer}]), 391 | Ctx 392 | end. 393 | 394 | gen_game_detail(Ctx = #texas{}) -> 395 | #notify_game_detail{ 396 | game = Ctx#texas.gid, 397 | pot = pot:total(Ctx#texas.pot), 398 | stage = Ctx#texas.stage, 399 | limit = Ctx#texas.limit, 400 | seats = seat:info(size, Ctx#texas.seats), 401 | require = Ctx#texas.required, 402 | joined = Ctx#texas.joined 403 | }. 404 | -------------------------------------------------------------------------------- /src/texas/hand.erl: -------------------------------------------------------------------------------- 1 | -module(hand). 2 | 3 | -export([new/0, new/1, add/2, size/1, rank/1, merge/2]). 4 | -export([make_card/1, make_cards/1, make_rep/1]). 5 | -export([player_hand/1]). 6 | 7 | -include("openpoker.hrl"). 8 | 9 | -define(MASK_A, 2#10000000000000). 10 | -define(MASK_MAX_STRAIGHT, 2#11111000000000). 11 | -define(MASK_MIN_STRAIGHT, 2#00000000011111). 12 | 13 | %%% 14 | %%% PUBLIC 15 | %%% 16 | 17 | new() -> 18 | new([]). 19 | 20 | new(Cards) when is_list(Cards) -> 21 | #hand{ cards = Cards }. 22 | 23 | add(Hand = #hand{}, NewCard) when is_list(NewCard) -> 24 | add(Hand, make_card(NewCard)); 25 | add(Hand = #hand{cards = Cards}, NewCard) when is_integer(NewCard) -> 26 | Hand#hand{ cards = [NewCard | Cards] }. 27 | 28 | size(Hand) -> 29 | length(Hand#hand.cards). 30 | 31 | rank(Hand) -> 32 | %% 按照花色生成对牌型的位组合 33 | %% 0b0001 向左位移相应Face的位数,通过对位移后的结果进行与运算 34 | %% 获取当前花色的手牌组合 35 | 36 | Rep = make_rep(Hand), 37 | L = [fun is_straight_flush/2, 38 | fun is_four_kind/2, 39 | fun is_full_house/2, 40 | fun is_flush/2, 41 | fun is_straight/2, 42 | fun is_three_kind/2, 43 | fun is_two_pair/2, 44 | fun is_pair/2], 45 | 46 | rank(Hand, L, Rep). 47 | 48 | merge(Hand = #hand{}, []) -> Hand; 49 | merge(Hand = #hand{}, [H|T]) -> 50 | merge(add(Hand, H), T). 51 | 52 | make_cards(S) when is_list(S) -> 53 | lists:map(fun make_card/1, string:tokens(S, " ")). 54 | 55 | make_card([Face, Suit]) -> 56 | FaceCode = case Face of 57 | $2 -> ?CF_TWO; 58 | $3 -> ?CF_THREE; 59 | $4 -> ?CF_FOUR; 60 | $5 -> ?CF_FIVE; 61 | $6 -> ?CF_SIX; 62 | $7 -> ?CF_SEVEN; 63 | $8 -> ?CF_EIGHT; 64 | $9 -> ?CF_NINE; 65 | $T -> ?CF_TEN; 66 | $J -> ?CF_JACK; 67 | $Q -> ?CF_QUEEN; 68 | $K -> ?CF_KING; 69 | $A -> ?CF_ACE 70 | end, 71 | SuitCode = case Suit of 72 | $C -> ?CS_CLUBS; 73 | $D -> ?CS_DIAMONDS; 74 | $H -> ?CS_HEARTS; 75 | $S -> ?CS_SPADES 76 | end, 77 | ?POKER_ENCODE(SuitCode, FaceCode). 78 | 79 | make_rep(Hand = #hand{}) -> 80 | make_rep(Hand#hand.cards); 81 | 82 | make_rep(Cards) when is_list(Cards) -> 83 | make_rep(Cards, {0, 0, 0, 0}). 84 | 85 | player_hand(#hand{ rank = Rank, high1 = High3, high2 = High2 }) 86 | when Rank == ?HC_FULL_HOUSE; 87 | Rank == ?HC_TWO_PAIR -> 88 | H1 = face_from_mask(High3), 89 | H2 = face_from_mask(High2), 90 | #player_hand{ rank = Rank, high1 = H1, high2 = H2 }; 91 | 92 | player_hand(#hand{ rank = Rank, high1 = High3, suit = Suit }) 93 | when Rank == ?HC_FLUSH; 94 | Rank == ?HC_STRAIGHT_FLUSH -> 95 | H1 = face_from_mask(High3), 96 | #player_hand{ rank = Rank, high1 = H1, suit = Suit }; 97 | 98 | player_hand(#hand{ rank = Rank, high1 = High }) -> 99 | #player_hand{ rank = Rank, high1 = face_from_mask(High) }. 100 | 101 | %%% 102 | %%% CHECK RANK 103 | %%% 104 | 105 | is_straight_flush(Hand, Rep) -> 106 | Mask = make_mask(Rep), 107 | case is_flush(Hand, Mask, Rep, ?CS_CLUBS) of 108 | none -> 109 | none; 110 | Hand1 -> 111 | High = Hand1#hand.high1, 112 | case is_straight(Hand, [High, High, High, High]) of 113 | none -> 114 | none; 115 | Hand2 -> 116 | Hand2#hand{ rank = ?HC_STRAIGHT_FLUSH, suit = Hand1#hand.suit } 117 | end 118 | end. 119 | 120 | is_flush(Hand, Rep) -> 121 | Mask = make_mask(Rep), 122 | is_flush(Hand, Mask, Rep, ?CS_CLUBS). 123 | 124 | is_flush(Hand, Mask, [H|T], Suit) -> 125 | Score = Mask band H, 126 | Count = bits:bits1(Score), 127 | if 128 | Count < 5 -> 129 | is_flush(Hand, Mask, T, Suit + 1); 130 | true -> 131 | High1 = bits:clear_extra_bits(Score, 5), 132 | Hand#hand{ rank = ?HC_FLUSH, high1 = High1, suit = Suit } 133 | end; 134 | 135 | is_flush(_, _, [], _) -> 136 | none. 137 | 138 | is_straight(Hand, Rep) -> 139 | Mask = make_mask(Rep), 140 | is_straight(Hand, Mask, Rep). 141 | 142 | is_straight(Hand, Mask, Rep) when is_list(Rep), Mask band ?MASK_A > 0 -> 143 | is_straight(Hand, Mask bor 1, ?MASK_MAX_STRAIGHT); 144 | is_straight(Hand, Mask, Rep) when is_list(Rep) -> 145 | is_straight(Hand, Mask, ?MASK_MAX_STRAIGHT); 146 | 147 | is_straight(_, _, Mask) when Mask < ?MASK_MIN_STRAIGHT -> none; 148 | is_straight(Hand, Value, Mask) when Value band Mask =:= Mask -> 149 | Hand#hand{ rank = ?HC_STRAIGHT, high1 = Mask }; 150 | is_straight(Hand, Value, Mask) -> 151 | is_straight(Hand, Value, Mask bsr 1). 152 | 153 | is_four_kind(Hand, [C, D, H, S]) when C band D band H band S > 0 -> 154 | V = C band D band H band S, 155 | Hand#hand{ 156 | rank = ?HC_FOUR_KIND, 157 | high1 = V, 158 | score = score([C, D, H, S], V, 1)}; 159 | is_four_kind(_, _) -> 160 | none. 161 | 162 | is_full_house(Hand, Rep) -> 163 | case is_three_kind(Hand, Rep) of 164 | none -> 165 | none; 166 | Hand1 -> 167 | High3 = Hand1#hand.high1, 168 | case is_pair(Hand1, clear_high_bit(Rep, High3)) of 169 | none -> 170 | none; 171 | Hand2 -> 172 | High2 = Hand2#hand.high1, 173 | Hand2#hand{ 174 | rank = ?HC_FULL_HOUSE, 175 | high1 = High3, 176 | high2 = High2, 177 | score = 0 178 | } 179 | end 180 | end. 181 | 182 | is_three_kind(Hand, [C, D, H, S]) -> 183 | LAND = [C band D band H, 184 | D band H band S, 185 | H band S band C, 186 | S band C band D], 187 | L = lists:sort(fun(A, B) -> A > B end, LAND), 188 | is_three_kind(Hand, L, [C, D, H, S]). 189 | 190 | is_three_kind(_, [], _) -> none; 191 | is_three_kind(Hand, [H|_], Rep) when H > 0 -> 192 | Hand#hand{ 193 | rank = ?HC_THREE_KIND, 194 | high1 = high_bit(H), 195 | score = score(Rep, H, 2)}; 196 | is_three_kind(Hand, [_|T], Rep) -> 197 | is_three_kind(Hand, T, Rep). 198 | 199 | is_two_pair(Hand, Rep) -> 200 | case is_pair(Hand, Rep) of 201 | none -> 202 | none; 203 | Hand1 = #hand{ rank = ?HC_PAIR, high1 = High1 } -> 204 | Rep1 = clear_high_bit(Rep, High1), 205 | case is_pair(Hand1, Rep1) of 206 | none -> 207 | none; 208 | Hand2 = #hand{ rank = ?HC_PAIR, high1 = High2 } -> 209 | Hand2#hand{ 210 | rank = ?HC_TWO_PAIR, 211 | high1 = High1, 212 | high2 = High2, 213 | score = score(Rep, High1 bor High2, 1) 214 | } 215 | end 216 | end. 217 | 218 | is_pair(Hand, [C, D, H, S]) -> 219 | LAND = [C band D, 220 | D band H, 221 | H band S, 222 | S band C, 223 | C band H, 224 | D band S], 225 | L = lists:sort(fun(A, B) -> A > B end, LAND), 226 | is_pair(Hand, L, [C, D, H, S]). 227 | 228 | is_pair(_, [], _) -> none; 229 | is_pair(Hand, [H|_], Rep) when H > 0 -> 230 | Hand#hand{ 231 | rank = ?HC_PAIR, 232 | high1 = high_bit(H), 233 | score = score(Rep, H, 3)}; 234 | is_pair(Hand, [_|T], Rep) -> 235 | is_pair(Hand, T, Rep). 236 | 237 | 238 | %%% 239 | %%% PRIVATE 240 | %%% 241 | 242 | rank(Hand, [Rank|T], Rep) -> 243 | case Rank(Hand, Rep) of 244 | none -> 245 | rank(Hand, T, Rep); 246 | Hand1 -> 247 | Hand1 248 | end; 249 | 250 | rank(Hand, [], Rep) -> 251 | Mask = make_mask(Rep), 252 | High = bits:clear_extra_bits(Mask, 5), 253 | Hand#hand{ rank = ?HC_HIGH_CARD, high1 = High, score = 0 }. 254 | 255 | make_rep([H|T], Rep) when is_integer(H) -> 256 | [Suit, Face] = ?POKER_DECODE(H), 257 | make_rep(T, setelement(Suit, Rep, element(Suit, Rep) bor (1 bsl (Face)))); 258 | 259 | make_rep([], Rep) -> 260 | tuple_to_list(Rep). 261 | 262 | make_mask([C, D, H, S]) -> 263 | C bor D bor H bor S. 264 | 265 | high_bit(Mask) -> 266 | 1 bsl bits:log2(Mask). 267 | 268 | clear_high_bit([C, D, H, S], High) -> 269 | [C band (bnot High), 270 | D band (bnot High), 271 | H band (bnot High), 272 | S band (bnot High)]. 273 | 274 | score(Rep, High, Bits) -> 275 | Mask = make_mask(Rep), 276 | Mask1 = Mask band (bnot High), 277 | bits:clear_extra_bits(Mask1, Bits). 278 | 279 | face_from_mask(0) -> 0; 280 | face_from_mask(X) when is_number(X) -> 281 | L = [?CF_ACE, ?CF_KING, ?CF_QUEEN, ?CF_JACK, 282 | ?CF_TEN, ?CF_NINE, ?CF_EIGHT, ?CF_SEVEN, 283 | ?CF_SIX, ?CF_FIVE, ?CF_FOUR, ?CF_THREE, ?CF_TWO], 284 | face_from_mask(X, [1 bsl Face || Face <- L]). 285 | 286 | face_from_mask(_, []) -> 0; 287 | face_from_mask(X, [H|_]) when (X band H) > 0 -> 288 | bits:log2(H); 289 | face_from_mask(X, [_|T]) -> 290 | face_from_mask(X, T). 291 | -------------------------------------------------------------------------------- /src/texas/seat.erl: -------------------------------------------------------------------------------- 1 | -module(seat). 2 | -export([new/1, set/2, set/3, get/1, get/2, lookup/2, lookup/3, info/2, clear/2]). 3 | 4 | -include("openpoker.hrl"). 5 | 6 | new(N) when N > 0 -> 7 | list_to_tuple(new(N, [])). 8 | 9 | lookup(Mask, Seats, _At = #seat{sn = SN}) 10 | when SN > 0, SN =< size(Seats) -> 11 | Size = size(Seats), 12 | lookup(Mask, Seats, Size, SN, Size, []). 13 | 14 | lookup(Mask, Seats) -> 15 | Size = size(Seats), 16 | lookup(Mask, Seats, Size, Size, Size, []). 17 | 18 | set(Seat = #seat{sn = SN}, Seats) -> 19 | setelement(SN, Seats, Seat). 20 | 21 | get(Seats) -> 22 | tuple_to_list(Seats). 23 | 24 | get(SN, Seats) when is_tuple(Seats), SN >= 1, SN =< size(Seats) -> element(SN, Seats); 25 | get(_SN, _Seats) -> ?UNDEF. 26 | 27 | set(#seat{sn = SN}, State, Seats) -> 28 | set(SN, State, Seats); 29 | 30 | set(SN, State, Seats) when is_integer(SN) -> 31 | S = element(SN, Seats), 32 | setelement(SN, Seats, S#seat{state = State}); 33 | 34 | set([], _State, Seats) -> 35 | Seats; 36 | 37 | set([#seat{sn = SN}|T], State, Seats) -> 38 | set(T, State, set(SN, State, Seats)). 39 | 40 | info(size, Seats) -> 41 | size(Seats). 42 | 43 | clear(SN, Seats) -> 44 | setelement(SN, Seats, #seat{sn = SN, state = ?PS_EMPTY}). 45 | 46 | %%% 47 | %%% private 48 | %%% 49 | 50 | new(0, Acc) -> Acc; 51 | new(N, Acc) -> new(N - 1, [#seat{sn = N, state = ?PS_EMPTY} | Acc]). 52 | 53 | lookup(_Mask, _Seats, 0, _At, _N, _Acc) -> []; 54 | lookup(_Mask, _Seats, _Size, _At, 0, Acc) -> lists:reverse(Acc); 55 | lookup(Mask, Seats, Size, At, N, Acc) -> 56 | SN = (At rem Size) + 1, 57 | R = element(SN, Seats), 58 | NewAcc = case check(R#seat.state, Mask) of 59 | true -> 60 | [R|Acc]; 61 | _ -> 62 | Acc 63 | end, 64 | lookup(Mask, Seats, Size, At + 1, N - 1, NewAcc). 65 | 66 | check(?PS_EMPTY, ?PS_EMPTY) -> true; 67 | check(_, ?PS_EMPTY) -> false; 68 | check(?PS_EMPTY, _) -> false; 69 | check(State, Mask) -> 70 | (State band Mask) =:= State. 71 | -------------------------------------------------------------------------------- /test/integration/mod_betting_allin_test.erl: -------------------------------------------------------------------------------- 1 | -module(mod_betting_allin_test). 2 | 3 | -include("openpoker.hrl"). 4 | -include("openpoker_test.hrl"). 5 | 6 | -define(PP, [{?JACK, ?JACK_ID}, {?TOMMY, ?TOMMY_ID}, {?FOO, ?FOO_ID}]). 7 | -define(B_SN, 1). 8 | -define(SB_SN, 2). 9 | -define(BB_SN, 3). 10 | 11 | normal_allin_test_() -> {setup, fun setup_normal/0, fun sim:clean/1, fun () -> 12 | sim:check_actor(?B_SN, {20, 0, 0}, ?PP), 13 | sim:check_raise(?B_SN, call, {20, 0}, ?PP), 14 | 15 | sim:check_actor(?SB_SN, {10, 20, 60}, ?PP), 16 | sim:check_raise(?SB_SN, call, {10, 0}, ?PP), 17 | 18 | sim:check_actor(?BB_SN, {0, 20, 40}, ?PP), 19 | sim:check_raise(?BB_SN, 20, {0, 20}, ?PP), 20 | 21 | sim:check_actor(?SB_SN, {20, 40, 40}, ?PP), 22 | sim:check_raise(?SB_SN, 40, {20, 40}, ?PP), 23 | 24 | sim:check_actor(?BB_SN, {0, 0, 0}, ?PP), 25 | sim:check_raise(?BB_SN, 0, {20, 0}, ?PP), 26 | 27 | sim:check_notify_stage_end(?GS_PREFLOP, ?PP), 28 | 29 | ?assertEqual(ok, ok) 30 | end}. 31 | 32 | setup_normal() -> 33 | setup([{op_mod_blinds, []}, {op_mod_betting, [?GS_PREFLOP]}, {op_mod_betting, [?GS_FLOP]}], 10, 20), 34 | sim:join_and_start_game(?PP, [20, 80, 60]), 35 | sim:check_blind(?PP, ?B_SN, ?SB_SN, ?BB_SN). 36 | 37 | setup(MixinMods, SB, BB) -> 38 | sim:setup(), 39 | sim:setup_game( 40 | #tab_game_config{ 41 | module = game, seat_count = 9, required = 2, 42 | limit = #limit{min = 0, max = 1000000, small = SB, big = BB}, 43 | mods = [{op_mod_suspend, []}, {wait_players, []}] ++ MixinMods ++ [{stop, []}], 44 | start_delay = 0, timeout = 1000, max = 1}). 45 | -------------------------------------------------------------------------------- /test/integration/mod_betting_headsup_test.erl: -------------------------------------------------------------------------------- 1 | -module(mod_betting_headsup_test). 2 | 3 | -include("openpoker.hrl"). 4 | -include("openpoker_test.hrl"). 5 | 6 | -define(TWO_PLAYERS, [{?JACK, ?JACK_ID}, {?TOMMY, ?TOMMY_ID}]). 7 | 8 | headsup_betting_one_call_one_raise_test_() -> {setup, fun setup_normal/0, fun sim:clean/1, fun () -> 9 | Players = ?TWO_PLAYERS, 10 | sim:join_and_start_game(Players), 11 | 12 | B = 1, SB = 1, BB = 2, 13 | sim:check_blind(Players, B, SB, BB), 14 | 15 | %% sb在首轮先下注,sb需要补齐bb的盲注。 16 | %% SB is Button, PREFLOP SB first betting CALL 10 17 | sim:check_notify_actor(SB, Players), 18 | ?assertMatch(#notify_betting{call = 10, min = 20, max = 80}, sim_client:head(?JACK)), 19 | sim_client:send(?JACK, #cmd_raise{game = ?GAME, amount = 0}), %% CALL 20 | sim:check_notify_raise(10, 0, Players), %% CALL 10 21 | 22 | %% bb在首轮后下注,此时由于所有玩家筹码持平,可以check也可以raise 23 | %% BB only last betting on PREFLOP 24 | sim:check_notify_actor(BB, Players), 25 | ?assertMatch(#notify_betting{call = 0, min = 20, max = 80}, sim_client:head(?TOMMY)), 26 | sim_client:send(?TOMMY, #cmd_raise{game = ?GAME, amount = 20}), %% RAISE 27 | sim:check_notify_raise(0, 20, Players), 28 | 29 | %% sb需要跟进bb的raise才能继续,当然也可继续加注。 30 | sim:check_notify_actor(SB, Players), 31 | ?assertMatch(#notify_betting{call = 20, min = 40, max = 60}, sim_client:head(?JACK)), 32 | sim_client:send(?JACK, #cmd_raise{game = ?GAME, amount = 0}), %% CALL 33 | sim:check_notify_raise(20, 0, Players), %% CALL 20 34 | 35 | %% 加注一次后所有玩家筹码持平,进入下一轮 36 | sim:check_notify_stage_end(?GS_PREFLOP, Players), 37 | sim:check_notify_stage(?GS_FLOP, Players), 38 | 39 | %% FLOP 由bb首先下注(headsup规则) 40 | sim:check_notify_actor(BB, Players), 41 | ?assertMatch(#notify_betting{call = 0, min = 20, max = 60}, sim_client:head(?TOMMY)), 42 | sim_client:send(?TOMMY, #cmd_raise{game = ?GAME, amount = 0}), %% CHECK 43 | sim:check_notify_raise(0, 0, Players), %% CHECK 44 | 45 | %% sb同样check 46 | sim:check_notify_actor(SB, Players), 47 | ?assertMatch(#notify_betting{call = 0, min = 20, max = 60}, sim_client:head(?JACK)), 48 | sim_client:send(?JACK, #cmd_raise{game = ?GAME, amount = 0}), %% CHECK 49 | sim:check_notify_raise(0, 0, Players), %% CHECK 50 | 51 | sim:check_notify_stage_end(?GS_FLOP, Players) 52 | 53 | end}. 54 | 55 | headsup_betting_one_call_one_check_test_() -> {setup, fun setup_normal/0, fun sim:clean/1, fun () -> 56 | Players = ?TWO_PLAYERS, 57 | sim:join_and_start_game(Players), 58 | 59 | B = 1, SB = 1, BB = 2, 60 | sim:check_blind(Players, B, SB, BB), 61 | 62 | %% SB is Button, PREFLOP SB first betting CALL 10 63 | sim:check_notify_actor(SB, Players), 64 | ?assertMatch(#notify_betting{call = 10, min = 20, max = 80}, sim_client:head(?JACK)), 65 | sim_client:send(?JACK, #cmd_raise{game = ?GAME, amount = 0}), %% CALL 66 | sim:check_notify_raise(10, 0, Players), %% CALL 10 67 | 68 | %% BB only last betting on PREFLOP 69 | sim:check_notify_actor(BB, Players), 70 | ?assertMatch(#notify_betting{call = 0, min = 20, max = 80}, sim_client:head(?TOMMY)), 71 | sim_client:send(?TOMMY, #cmd_raise{game = ?GAME, amount = 0}), %% CHECK 72 | sim:check_notify_raise(0, 0, Players), 73 | 74 | sim:check_notify_stage_end(?GS_PREFLOP, Players) 75 | end}. 76 | 77 | setup_normal() -> 78 | setup([{op_mod_blinds, []}, {op_mod_betting, [?GS_PREFLOP]}, {op_mod_betting, [?GS_FLOP]}, {stop, []}]). 79 | 80 | setup(MixinMods) -> 81 | sim:setup(), 82 | sim:setup_game( 83 | #tab_game_config{ 84 | module = game, seat_count = 9, required = 2, 85 | limit = #limit{min = 100, max = 400, small = 10, big = 20}, 86 | mods = [{op_mod_suspend, []}, {wait_players, []}] ++ MixinMods, 87 | start_delay = 0, timeout = 1000, max = 1}). 88 | -------------------------------------------------------------------------------- /test/integration/mod_betting_raise_test.erl: -------------------------------------------------------------------------------- 1 | -module(mod_betting_raise_test). 2 | 3 | -include("openpoker.hrl"). 4 | -include("openpoker_test.hrl"). 5 | 6 | -define(TWO_PLAYERS, [{?JACK, ?JACK_ID}, {?TOMMY, ?TOMMY_ID}]). 7 | -define(THREE_PLAYERS, ?TWO_PLAYERS ++ [{?FOO, ?FOO_ID}]). 8 | 9 | -define(BUYIN, 100). 10 | -define(SB, 10). 11 | -define(BB, ?SB * 2). 12 | 13 | normal_call_test_() -> {setup, fun setup_normal/0, fun sim:clean/1, fun () -> 14 | Players = ?THREE_PLAYERS, 15 | sim:join_and_start_game(Players), 16 | 17 | %% SB 10 BB 20 18 | B_SN = 1, SB_SN = 2, BB_SN = 3, 19 | sim:check_blind(Players, B_SN, SB_SN, BB_SN), 20 | 21 | %% 小盲10 大盲20 22 | %% 开局 小盲大盲之后该庄家说话,庄家需要跟大盲才可以继续 23 | %% 庄家选择跟注 跟注20 加注0 24 | sim:check_actor(B_SN, {20, 20, 80}, Players), 25 | sim:check_raise(B_SN, call, {20, 0}, Players), 26 | 27 | %% 庄家下注后轮到小盲说话 28 | %% 此时庄家已经跟注20,由于之前小盲注已经下注10 29 | %% 这次只需要补齐10即可跟注,如果加注最少需要加20 30 | sim:check_actor(SB_SN, {10, 20, 80}, Players), 31 | sim:check_raise(SB_SN, call, {10, 0}, Players), 32 | 33 | %% 小盲下注后轮到大盲说话 34 | %% 此时小盲已经补齐跟注20,由于之前大盲注已经下注20 35 | %% 这次不需要跟注,可以选择看牌或者加注,加注最小要等于大盲 36 | sim:check_actor(BB_SN, {0, 20, 80}, Players), 37 | sim:check_raise(BB_SN, call, {0, 0}, Players), 38 | 39 | %% 所有玩家均已下相同数额的筹码,讲进入下一轮 40 | sim:check_notify_stage_end(?GS_PREFLOP, Players), 41 | sim:check_notify_stage(?GS_FLOP, Players), 42 | 43 | ?assertEqual(ok, ok) 44 | end}. 45 | 46 | normal_raise_test_() -> {setup, fun setup_normal/0, fun sim:clean/1, fun () -> 47 | Players = ?THREE_PLAYERS, 48 | sim:join_and_start_game(Players), 49 | 50 | %% SB 10 BB 20 51 | B_SN = 1, SB_SN = 2, BB_SN = 3, 52 | sim:check_blind(Players, B_SN, SB_SN, BB_SN), 53 | 54 | %% b = s = d = 100 55 | %% d -> raise to 40 56 | %% s -> call 57 | %% b -> call 58 | 59 | sim:check_actor(B_SN, {20, 20, 80}, Players), 60 | sim:check_raise(B_SN, 20, {20, 20}, Players), 61 | 62 | sim:check_actor(SB_SN, {30, 40, 60}, Players), 63 | sim:check_raise(SB_SN, call, {30, 0}, Players), 64 | 65 | sim:check_actor(BB_SN, {20, 40, 60}, Players), 66 | sim:check_raise(BB_SN, call, {20, 0}, Players), 67 | 68 | sim:check_notify_stage_end(?GS_PREFLOP, Players), 69 | sim:check_notify_stage(?GS_FLOP, Players), 70 | 71 | %% b = s = d = 60 72 | %% s -> check 73 | %% b -> check 74 | %% d -> raise to 40 75 | %% s -> call 76 | %% b -> allin 60 77 | %% d -> allin 78 | %% s -> allin 79 | 80 | sim:check_actor(SB_SN, {0, 20, 60}, Players), 81 | sim:check_raise(SB_SN, check, {0, 0}, Players), 82 | 83 | sim:check_actor(BB_SN, {0, 20, 60}, Players), 84 | sim:check_raise(BB_SN, check, {0, 0}, Players), 85 | 86 | sim:check_actor(B_SN, {0, 20, 60}, Players), 87 | sim:check_raise(B_SN, 40, {0, 40}, Players), 88 | 89 | sim:check_actor(SB_SN, {40, 0, 20}, Players), 90 | sim:check_raise(SB_SN, call, {40, 0}, Players), 91 | 92 | sim:check_actor(BB_SN, {40, 0, 20}, Players), 93 | sim:check_raise(BB_SN, 20, {40, 20}, Players), 94 | 95 | sim:check_actor(B_SN, {20, 0, 0}, Players), 96 | sim:check_raise(B_SN, allin, {20, 0}, Players), 97 | 98 | sim:check_actor(SB_SN, {20, 0, 0}, Players), 99 | sim:check_raise(SB_SN, allin, {20, 0}, Players), 100 | 101 | sim:check_notify_stage_end(?GS_FLOP, Players), 102 | ?assertMatch(ok, ok) 103 | end}. 104 | 105 | setup_normal() -> 106 | setup([{op_mod_blinds, []}, {op_mod_betting, [?GS_PREFLOP]}, {op_mod_betting, [?GS_FLOP]}]). 107 | 108 | setup(MixinMods) -> 109 | sim:setup(), 110 | sim:setup_game( 111 | #tab_game_config{ 112 | module = game, seat_count = 9, required = 2, 113 | limit = #limit{min = 100, max = 400, small = ?SB, big = ?BB}, 114 | mods = [{op_mod_suspend, []}, {wait_players, []}] ++ MixinMods ++ [{stop, []}], 115 | start_delay = 0, timeout = 1000, max = 1}). 116 | -------------------------------------------------------------------------------- /test/integration/mod_betting_test.erl: -------------------------------------------------------------------------------- 1 | -module(mod_betting_test). 2 | 3 | -include("openpoker.hrl"). 4 | -include("openpoker_test.hrl"). 5 | 6 | -define(TWO_PLAYERS, [{?JACK, ?JACK_ID}, {?TOMMY, ?TOMMY_ID}]). 7 | -define(THREE_PLAYERS, ?TWO_PLAYERS ++ [{?FOO, ?FOO_ID}]). 8 | 9 | normal_betting_test_() -> {setup, fun setup_normal/0, fun sim:clean/1, fun () -> 10 | Players = ?THREE_PLAYERS, 11 | sim:join_and_start_game(Players), 12 | 13 | %% SB 10 BB 20 14 | B = 1, SB = 2, BB = 3, 15 | sim:check_blind(Players, B, SB, BB), 16 | 17 | %% B CALL 20 18 | sim:check_notify_actor(B, Players), 19 | ?assertMatch(#notify_betting{call = 20, min = 20, max = 80}, sim_client:head(?JACK)), 20 | sim_client:send(?JACK, #cmd_raise{game = ?GAME, amount = 0}), 21 | sim:check_notify_raise(20, 0, Players), 22 | 23 | %% SB CALL 10 24 | sim:check_notify_actor(SB, Players), 25 | ?assertMatch(#notify_betting{call = 10, min = 20, max = 80}, sim_client:head(?TOMMY)), 26 | sim_client:send(?TOMMY, #cmd_raise{game = ?GAME, amount = 0}), 27 | sim:check_notify_raise(10, 0, Players), 28 | 29 | %% BB RAISE 20 30 | sim:check_notify_actor(BB, Players), 31 | ?assertMatch(#notify_betting{call = 0, min = 20, max = 80}, sim_client:head(?FOO)), 32 | sim_client:send(?FOO, #cmd_raise{game = ?GAME, amount = 20}), 33 | sim:check_notify_raise(0, 20, Players), 34 | 35 | %% B CALL 20 36 | sim:check_notify_actor(B, Players), 37 | ?assertMatch(#notify_betting{call = 20, min = 40, max = 60}, sim_client:head(?JACK)), 38 | sim_client:send(?JACK, #cmd_raise{game = ?GAME, amount = 0}), 39 | sim:check_notify_raise(20, 0, Players), 40 | 41 | %% SB CALL 20 42 | sim:check_notify_actor(SB, Players), 43 | ?assertMatch(#notify_betting{call = 20, min = 40, max = 60}, sim_client:head(?TOMMY)), 44 | sim_client:send(?TOMMY, #cmd_raise{game = ?GAME, amount = 0}), 45 | sim:check_notify_raise(20, 0, Players), 46 | 47 | %% TURNOVER STAGE 48 | sim:check_notify_stage_end(?GS_PREFLOP, Players), 49 | sim:check_notify_stage(?GS_FLOP, Players), 50 | 51 | %% SB CHECK 52 | sim:check_notify_actor(SB, Players), 53 | ?assertMatch(#notify_betting{call = 0, min = 20, max = 60}, sim_client:head(?TOMMY)), 54 | sim_client:send(?TOMMY, #cmd_raise{game = ?GAME, amount = 0}), 55 | sim:check_notify_raise(0, 0, Players), 56 | 57 | %% BB RAISE 20 58 | sim:check_notify_actor(BB, Players), 59 | ?assertMatch(#notify_betting{call = 0, min = 20, max = 60}, sim_client:head(?FOO)), 60 | sim_client:send(?FOO, #cmd_raise{game = ?GAME, amount = 20}), 61 | sim:check_notify_raise(0, 20, Players), 62 | 63 | %% B CALL 20 64 | sim:check_notify_actor(B, Players), 65 | ?assertMatch(#notify_betting{call = 20, min = 20, max = 40}, sim_client:head(?JACK)), 66 | sim_client:send(?JACK, #cmd_raise{game = ?GAME, amount = 0}), 67 | sim:check_notify_raise(20, 0, Players), 68 | 69 | %% SB CALL 70 | sim:check_notify_actor(SB, Players), 71 | ?assertMatch(#notify_betting{call = 20, min = 20, max = 40}, sim_client:head(?TOMMY)), 72 | sim_client:send(?TOMMY, #cmd_raise{game = ?GAME, amount = 0}), 73 | sim:check_notify_raise(20, 0, Players), 74 | 75 | %% FLOP OVER 76 | sim:check_notify_stage_end(?GS_FLOP, Players), 77 | ?assertMatch(stop, sim:game_state()) 78 | end}. 79 | 80 | normal_betting_and_fold_test_() -> {setup, fun setup_normal/0, fun sim:clean/1, fun () -> 81 | B = 1, SB = 2, BB = 3, 82 | Players = set_sn([{?JACK, B}, {?TOMMY, SB}, {?FOO, BB}], ?THREE_PLAYERS), 83 | 84 | sim:join_and_start_game(Players), 85 | sim:check_blind(Players, B, SB, BB), 86 | 87 | sim:turnover_player_raise({?JACK, Players}, {20, 20, 80}, 0), 88 | sim:turnover_player_raise({?TOMMY, Players}, {10, 20, 80}, 0), 89 | sim:turnover_player_raise({?FOO, Players}, { 0, 20, 80}, 0), 90 | 91 | sim:check_notify_stage_end(?GS_PREFLOP, Players), 92 | sim:check_notify_stage(?GS_FLOP, Players), 93 | 94 | sim:turnover_player_raise({?TOMMY, Players}, { 0, 20, 80}, 20), 95 | sim:turnover_player_fold ({?FOO, Players}, {20, 20, 60}), 96 | sim:turnover_player_raise({?JACK, Players}, {20, 20, 60}, 0), 97 | 98 | sim:check_notify_stage_end(?GS_FLOP, Players), 99 | ?assertMatch(#texas{joined = 3}, sim:game_ctx()), 100 | ?assertMatch(stop, sim:game_state()) 101 | end}. 102 | 103 | normal_betting_and_leave_test_() -> {setup, fun setup_normal/0, fun sim:clean/1, fun () -> 104 | B = 1, SB = 2, BB = 3, 105 | Players = set_sn([{?JACK, B}, {?TOMMY, SB}, {?FOO, BB}], ?THREE_PLAYERS), 106 | 107 | sim:join_and_start_game(Players), 108 | sim:check_blind(Players, B, SB, BB), 109 | 110 | sim:turnover_player_raise({?JACK, Players}, {20, 20, 80}, 0), 111 | sim:turnover_player_raise({?TOMMY, Players}, {10, 20, 80}, 0), 112 | sim:turnover_player_raise({?FOO, Players}, { 0, 20, 80}, 0), 113 | 114 | sim:check_notify_stage_end(?GS_PREFLOP, Players), 115 | sim:check_notify_stage(?GS_FLOP, Players), 116 | 117 | sim:turnover_player_raise({?TOMMY, Players}, { 0, 20, 80}, 20), 118 | sim:turnover_player_leave({?FOO, Players}, {20, 20, 60}), 119 | sim:turnover_player_raise({?JACK, proplists:delete(?FOO, Players)}, {20, 20, 60}, 0), 120 | 121 | sim:check_notify_stage_end(?GS_FLOP, proplists:delete(?FOO, Players)), 122 | ?assertMatch(#texas{joined = 2}, sim:game_ctx()), 123 | ?assertMatch(stop, sim:game_state()) 124 | end}. 125 | 126 | headsup_betting_test_() -> {setup, fun setup_normal/0, fun sim:clean/1, fun () -> 127 | SB = 1, BB = 2, 128 | Players = set_sn([{?JACK, SB}, {?TOMMY, BB}], ?TWO_PLAYERS), 129 | 130 | sim:join_and_start_game(Players), 131 | sim:check_blind(Players, SB, SB, BB), 132 | 133 | sim:turnover_player_raise({?JACK, Players}, {10, 20, 80}, 0), 134 | sim:turnover_player_raise({?TOMMY, Players}, { 0, 20, 80}, 0), 135 | 136 | sim:check_notify_stage_end(?GS_PREFLOP, Players), 137 | sim:check_notify_stage(?GS_FLOP, Players), 138 | 139 | sim:turnover_player_raise({?TOMMY, Players}, {0, 20, 80}, 0), 140 | sim:turnover_player_raise({?JACK, Players}, {0, 20, 80}, 20), 141 | sim:turnover_player_raise({?TOMMY, Players}, {20, 20, 60}, 0), 142 | 143 | sim:check_notify_stage_end(?GS_FLOP, Players), 144 | ?assertMatch(stop, sim:game_state()) 145 | end}. 146 | 147 | headsup_betting_and_fold_test_() -> {setup, fun setup_normal/0, fun sim:clean/1, fun () -> 148 | SB = 1, BB = 2, 149 | Players = set_sn([{?JACK, SB}, {?TOMMY, BB}], ?TWO_PLAYERS), 150 | 151 | sim:join_and_start_game(Players), 152 | sim:check_blind(Players, SB, SB, BB), 153 | 154 | sim:turnover_player_raise({?JACK, Players}, {10, 20, 80}, 0), 155 | sim:turnover_player_fold({?TOMMY, Players}, { 0, 20, 80}), 156 | 157 | ?assertMatch(#texas{joined = 2}, sim:game_ctx()), 158 | ?assertMatch(stop, sim:game_state()) 159 | end}. 160 | 161 | headsup_betting_and_leave_test_() -> {setup, fun setup_normal/0, fun sim:clean/1, fun () -> 162 | SB = 1, BB = 2, 163 | Players = set_sn([{?JACK, SB}, {?TOMMY, BB}], ?TWO_PLAYERS), 164 | 165 | sim:join_and_start_game(Players), 166 | sim:check_blind(Players, SB, SB, BB), 167 | 168 | sim:turnover_player_raise({?JACK, Players}, {10, 20, 80}, 0), 169 | sim:turnover_player_leave({?TOMMY, Players}, { 0, 20, 80}), 170 | 171 | ?assertMatch(#texas{joined = 1}, sim:game_ctx()), 172 | ?assertMatch(stop, sim:game_state()) 173 | end}. 174 | 175 | setup_normal() -> 176 | setup([{op_mod_blinds, []}, {op_mod_betting, [?GS_PREFLOP]}, {op_mod_betting, [?GS_FLOP]}]). 177 | 178 | setup(MixinMods) -> 179 | sim:setup(), 180 | sim:setup_game( 181 | #tab_game_config{ 182 | module = game, seat_count = 9, required = 2, 183 | limit = #limit{min = 100, max = 400, small = 10, big = 20}, 184 | mods = [{op_mod_suspend, []}, {wait_players, []}] ++ MixinMods ++ [{stop, []}], 185 | start_delay = 0, timeout = 1000, max = 1}). 186 | 187 | %%% 188 | %%% private test until 189 | %%% 190 | 191 | set_sn([], Players) -> Players; 192 | set_sn([{Key, SN}|T], Players) -> 193 | lists:keyreplace(Key, 1, Players, {Key, SN}), 194 | set_sn(T, Players). 195 | -------------------------------------------------------------------------------- /test/integration/mod_blinds_test.erl: -------------------------------------------------------------------------------- 1 | -module(mod_blinds_test). 2 | 3 | -include("openpoker.hrl"). 4 | -include("openpoker_test.hrl"). 5 | 6 | -define(TWO_PLAYERS, [{?JACK, ?JACK_ID}, {?TOMMY, ?TOMMY_ID}]). 7 | -define(THREE_PLAYERS, ?TWO_PLAYERS ++ [{?FOO, ?FOO_ID}]). 8 | 9 | blind_headsup_game_test_() -> {setup, fun setup_blind/0, fun sim:clean/1, fun () -> 10 | sim:join_and_start_game(?TWO_PLAYERS), 11 | sim:check_blind_only_seat(?TWO_PLAYERS, 1, 1, 2), 12 | 13 | Ctx = sim:game_ctx(), 14 | Seats = Ctx#texas.seats, 15 | ?assertMatch(#texas{b = #seat{sn = 1}, sb = #seat{sn = 1}, bb = #seat{sn = 2}, headsup = true}, Ctx), 16 | ?assertMatch(#seat{sn = 1, bet = 5, inplay = 95}, seat:get(1, Seats)), 17 | ?assertMatch(#seat{sn = 2, bet = 10, inplay = 90}, seat:get(2, Seats)), 18 | ?assertMatch([#tab_inplay{inplay = 95}], mnesia:dirty_read(tab_inplay, ?JACK_ID)), 19 | ?assertMatch([#tab_inplay{inplay = 90}], mnesia:dirty_read(tab_inplay, ?TOMMY_ID)), 20 | ?assertMatch([#tab_player_info{cash = -100}], mnesia:dirty_read(tab_player_info, ?JACK_ID)), 21 | ?assertMatch([#tab_player_info{cash = -100}], mnesia:dirty_read(tab_player_info, ?TOMMY_ID)), 22 | 23 | sim_client:send(?JACK, #cmd_leave{game = ?GAME}), 24 | sim_client:send(?TOMMY, #cmd_leave{game = ?GAME}), 25 | 26 | ?assertMatch([#tab_player_info{cash = -5}], mnesia:dirty_read(tab_player_info, ?JACK_ID)), 27 | ?assertMatch([#tab_player_info{cash = -10}], mnesia:dirty_read(tab_player_info, ?TOMMY_ID)) 28 | end}. 29 | 30 | blind_game_test_() -> {setup, fun setup_blind/0, fun sim:clean/1, fun () -> 31 | sim:join_and_start_game(?THREE_PLAYERS), 32 | sim:check_blind_only_seat(?THREE_PLAYERS, 1, 2, 3), 33 | 34 | ?assertMatch(#texas{ 35 | b = #seat{sn = 1, pid = ?JACK_ID}, 36 | sb = #seat{sn = 2, pid = ?TOMMY_ID}, 37 | bb = #seat{sn = 3, pid = ?FOO_ID}, 38 | headsup = false}, sim:game_ctx()) 39 | end}. 40 | 41 | auto_compute_seat_sn_test_() -> {setup, fun setup_empty/0, fun sim:clean/1, fun () -> 42 | sim:join_and_start_game(?TWO_PLAYERS, ?UNDEF), 43 | ?assertMatch(#notify_join{player = ?JACK_ID, sn = 1}, sim_client:head(?JACK)), 44 | ?assertMatch(#notify_watch{player = ?TOMMY_ID}, sim_client:head(?JACK)), 45 | ?assertMatch(#notify_join{player = ?TOMMY_ID, sn = 2}, sim_client:head(?JACK)), 46 | ?assertMatch(#notify_join{player = ?TOMMY_ID, sn = 2}, sim_client:head(?TOMMY)) 47 | end}. 48 | 49 | %%% 50 | %%% setup 51 | %%% 52 | 53 | setup_blind() -> 54 | setup([{op_mod_blinds, []}]). 55 | 56 | setup_empty() -> 57 | setup([]). 58 | 59 | setup(MixinMods) -> 60 | sim:setup(), 61 | op_games_sup:start_child( 62 | #tab_game_config{ 63 | module = game, seat_count = 9, required = 2, 64 | limit = #limit{min = 100, max = 500, small = 5, big = 10}, 65 | mods = [{op_mod_suspend, []}, {wait_players, []}] ++ MixinMods ++ [{stop, []}], 66 | start_delay = 0, timeout = 1000, max = 1}). 67 | -------------------------------------------------------------------------------- /test/integration/mod_deal_test.erl: -------------------------------------------------------------------------------- 1 | -module(mod_deal_test). 2 | -include("openpoker.hrl"). 3 | -include("openpoker_test.hrl"). 4 | 5 | -define(TWO_PLAYERS, [{?JACK, ?JACK_ID}, {?TOMMY, ?TOMMY_ID}]). 6 | 7 | private_rank_test_() -> {setup, fun setup_with_private_rank/0, fun sim:clean/1, fun () -> 8 | Players = ?TWO_PLAYERS, 9 | sim:join_and_start_game(Players), 10 | sim:check_blind_only_raise(Players, 1, 1, 2), 11 | sim:check_deal(), 12 | 13 | ?assertMatch(#notify_hand{rank = ?HC_PAIR, high1 = ?CF_FOUR}, sim_client:head(?JACK)), 14 | ?assertMatch(#notify_hand{rank = ?HC_PAIR, high1 = ?CF_THREE}, sim_client:head(?TOMMY)) 15 | end}. 16 | 17 | deal_test_() -> {setup, fun setup_with_deal/0, fun sim:clean/1, fun () -> 18 | Players = ?TWO_PLAYERS, 19 | sim:join_and_start_game(Players), 20 | 21 | sim:check_blind_only_raise(Players, 1, 1, 2), 22 | sim:check_deal(), 23 | sim:check_shared(3, Players), 24 | 25 | Ctx = sim:game_ctx(), 26 | Jack = seat:get(1, Ctx#texas.seats), 27 | Tommy = seat:get(2, Ctx#texas.seats), 28 | ?assertEqual(52 - 4 - 3, deck:size(Ctx#texas.deck)), 29 | ?assertEqual(3, length(Ctx#texas.board)), 30 | ?assertEqual(2, hand:size(Jack#seat.hand)), 31 | ?assertEqual(2, hand:size(Tommy#seat.hand)), 32 | ?assertMatch(stop, sim:game_state()) 33 | end}. 34 | 35 | share_rank_test_() -> {setup, fun setup_with_share_rank/0, fun sim:clean/1, fun () -> 36 | Players = ?TWO_PLAYERS, 37 | sim:join_and_start_game(Players), 38 | sim:check_blind_only_raise(Players, 1, 1, 2), 39 | sim:check_deal(), 40 | sim:check_shared(1, Players), 41 | ?assertMatch(#notify_hand{rank = ?HC_PAIR, high1 = ?CF_FOUR}, sim_client:head(?JACK)), 42 | ?assertMatch(#notify_hand{rank = ?HC_THREE_KIND, high1 = ?CF_THREE}, sim_client:head(?TOMMY)) 43 | end}. 44 | 45 | share_rank2_test_() -> {setup, fun setup_with_share_rank2/0, fun sim:clean/1, fun () -> 46 | Players = ?TWO_PLAYERS, 47 | sim:join_and_start_game(Players), 48 | sim:check_blind_only_raise(Players, 1, 1, 2), 49 | sim:check_deal(), 50 | sim:check_shared(3, Players), 51 | ?assertMatch(#notify_hand{rank = ?HC_FOUR_KIND, high1 = ?CF_FOUR}, sim_client:head(?JACK)), 52 | ?assertMatch(#notify_hand{rank = ?HC_FULL_HOUSE, high1 = ?CF_THREE, high2 = ?CF_FOUR}, sim_client:head(?TOMMY)) 53 | end}. 54 | 55 | %%% 56 | %%% setup & cleanup 57 | %%% 58 | 59 | setup_with_share_rank2() -> 60 | MixinMods = [{op_mod_blinds, []}, {rig, [hand:make_cards("3H 4H 3D 4D 3C 4C 4S ")]}, {deal_cards, [2, private]}, {deal_cards, [3, shared]}, {ranking, []}], 61 | setup(MixinMods). 62 | 63 | setup_with_share_rank() -> 64 | MixinMods = [{op_mod_blinds, []}, {rig, [hand:make_cards("3H 4H 3D 4D 3C")]}, {deal_cards, [2, private]}, {deal_cards, [1, shared]}, {ranking, []}], 65 | setup(MixinMods). 66 | 67 | setup_with_deal() -> 68 | MixinMods = [{op_mod_blinds, []}, {deal_cards, [2, private]}, 69 | {deal_cards, [1, shared]}, {deal_cards, [1, shared]}, {deal_cards, [1, shared]}], 70 | setup(MixinMods). 71 | 72 | setup_with_private_rank() -> 73 | RigCards = hand:make_cards("3H 4H 3D 4D"), 74 | MixinMods = [{op_mod_blinds, []}, {rig, [RigCards]}, {deal_cards, [2, private]}, {ranking, []}], 75 | setup(MixinMods). 76 | 77 | setup(MixinMods) -> 78 | sim:setup(), 79 | sim:setup_game( 80 | #tab_game_config{ 81 | module = game, required = 2, seat_count = 9, 82 | limit = #limit{min = 100, max = 400, small = 10, big = 20}, 83 | mods = [{op_mod_suspend, []}, {wait_players, []}] ++ MixinMods ++ [{stop, []}], 84 | start_delay = 500, timeout = 1000, max = 1}). 85 | -------------------------------------------------------------------------------- /test/integration/mod_game_test.erl: -------------------------------------------------------------------------------- 1 | -module(mod_game_test). 2 | -include("openpoker.hrl"). 3 | -include("openpoker_test.hrl"). 4 | 5 | -define(TWO_PLAYERS, [{?JACK, ?JACK_ID}, {?TOMMY, ?TOMMY_ID}]). 6 | -define(THREE_PLAYERS, [{?JACK, ?JACK_ID}, {?TOMMY, ?TOMMY_ID}, {?FOO, ?FOO_ID}]). 7 | 8 | rank_test_() -> {setup, fun setup_with_rank/0, fun sim:clean/1, fun () -> 9 | Players = ?TWO_PLAYERS, 10 | sim:join_and_start_game(Players), 11 | sim:check_blind_only_raise(Players, 1, 1, 2), 12 | 13 | sim:check_deal(), 14 | sim:check_shared(1, Players), 15 | ?assertMatch(#notify_hand{rank = ?HC_PAIR, high1 = ?CF_FOUR}, sim_client:head(?JACK)), 16 | ?assertMatch(#notify_hand{rank = ?HC_THREE_KIND, high1 = ?CF_THREE}, sim_client:head(?TOMMY)) 17 | end}. 18 | 19 | two_players_fold_shutdown_test_() -> {setup, fun setup_with_shutdown/0, fun sim:clean/1, fun () -> 20 | Players = ?TWO_PLAYERS, 21 | sim:join_and_start_game(Players), 22 | 23 | sim:check_blind_only_raise(Players, 1, 1, 2), 24 | sim:check_deal(), 25 | sim:check_shared(3, Players), 26 | 27 | %% CHECK NOTIFY RANK 28 | ?assertMatch(#notify_hand{rank = ?HC_FOUR_KIND, high1 = ?CF_FOUR}, sim_client:head(?JACK)), 29 | ?assertMatch(#notify_hand{rank = ?HC_FULL_HOUSE, high1 = ?CF_THREE, high2 = ?CF_FOUR}, sim_client:head(?TOMMY)), 30 | 31 | 32 | %% betting 33 | sim:check_notify_stage(?GS_PREFLOP, Players), 34 | sim:turnover_player_raise({?JACK, Players}, {10, 20, 80}, 0), 35 | sim:turnover_player_fold ({?TOMMY, Players}, {0, 20, 80}), 36 | sim:check_notify_stage_end(?GS_PREFLOP, Players), 37 | 38 | %% Only one player, don't notify hand and rank protocol. 39 | 40 | sim:check_notify_win(?JACK_ID, 40, Players) 41 | end}. 42 | 43 | shutdown_test_() -> {setup, fun setup_with_shutdown/0, fun sim:clean/1, fun () -> 44 | Players = ?TWO_PLAYERS, 45 | sim:join_and_start_game(Players), 46 | 47 | sim:check_blind_only_raise(Players, 1, 1, 2), 48 | sim:check_deal(), 49 | sim:check_shared(3, Players), 50 | ?assertMatch(#notify_hand{rank = ?HC_FOUR_KIND, high1 = ?CF_FOUR}, sim_client:head(?JACK)), 51 | ?assertMatch(#notify_hand{rank = ?HC_FULL_HOUSE, high1 = ?CF_THREE, high2 = ?CF_FOUR}, sim_client:head(?TOMMY)), 52 | 53 | %% betting 54 | sim:check_notify_stage(?GS_PREFLOP, Players), 55 | sim:turnover_player_raise({?JACK, Players}, {10, 20, 80}, 0), 56 | sim:turnover_player_raise({?TOMMY, Players}, { 0, 20, 80}, 70), 57 | sim:turnover_player_raise({?JACK, Players}, {70, 0, 10}, 0), 58 | sim:check_notify_stage_end(?GS_PREFLOP, Players), 59 | 60 | ?assertMatch(#notify_cards{player = ?TOMMY_ID }, sim_client:head(?JACK)), 61 | ?assertMatch(#notify_cards{player = ?JACK_ID }, sim_client:head(?TOMMY)), 62 | 63 | ?assertMatch(#notify_hand{player = ?TOMMY_ID, rank = ?HC_FULL_HOUSE, high1 = ?CF_THREE, high2 = ?CF_FOUR}, sim_client:head(?TOMMY)), 64 | ?assertMatch(#notify_hand{player = ?JACK_ID, rank = ?HC_FOUR_KIND, high1 = ?CF_FOUR}, sim_client:head(?TOMMY)), 65 | ?assertMatch(#notify_hand{player = ?TOMMY_ID, rank = ?HC_FULL_HOUSE, high1 = ?CF_THREE, high2 = ?CF_FOUR}, sim_client:head(?JACK)), 66 | ?assertMatch(#notify_hand{player = ?JACK_ID, rank = ?HC_FOUR_KIND, high1 = ?CF_FOUR}, sim_client:head(?JACK)), 67 | 68 | sim:check_notify_win(?JACK_ID, 40 + 140, Players), 69 | sim:check_notify_out(?TOMMY_ID, Players), 70 | 71 | ?assertMatch([#tab_inplay{inplay = 190}], mnesia:dirty_read(tab_inplay, ?JACK_ID)), 72 | ?assertMatch([#tab_inplay{inplay = 10}], mnesia:dirty_read(tab_inplay, ?TOMMY_ID)), 73 | ?assertMatch([#tab_player_info{cash = -100}], mnesia:dirty_read(tab_player_info, ?JACK_ID)), 74 | ?assertMatch([#tab_player_info{cash = -100}], mnesia:dirty_read(tab_player_info, ?TOMMY_ID)), 75 | 76 | sim:check_notify_game_end(Players), 77 | timer:sleep(500), 78 | sim:check_notify_game_cancel(Players), 79 | sim:check_notify_leave(?TOMMY, Players) 80 | end}. 81 | 82 | shutdown2_test_() -> {setup, fun setup_with_shutdown2/0, fun sim:clean/1, fun () -> 83 | Players = ?TWO_PLAYERS, 84 | sim:join_and_start_game(Players), 85 | 86 | sim:check_blind_only_raise(Players, 1, 1, 2), 87 | 88 | C1 = hand:make_card("3H"), 89 | C2 = hand:make_card("5H"), 90 | C3 = hand:make_card("7S"), 91 | C4 = hand:make_card("6S"), 92 | 93 | ?assertMatch(#notify_private{card = C1}, sim_client:head(?TOMMY)), 94 | ?assertMatch(#notify_draw{}, sim_client:head(?TOMMY)), 95 | ?assertMatch(#notify_private{card = C3}, sim_client:head(?TOMMY)), 96 | ?assertMatch(#notify_draw{}, sim_client:head(?TOMMY)), 97 | 98 | ?assertMatch(#notify_draw{}, sim_client:head(?JACK)), 99 | ?assertMatch(#notify_private{card = C2}, sim_client:head(?JACK)), 100 | ?assertMatch(#notify_draw{}, sim_client:head(?JACK)), 101 | ?assertMatch(#notify_private{card = C4}, sim_client:head(?JACK)), 102 | 103 | sim:check_shared(3, Players), 104 | ?assertMatch(#notify_hand{rank = ?HC_HIGH_CARD}, sim_client:head(?JACK)), 105 | ?assertMatch(#notify_hand{rank = ?HC_HIGH_CARD}, sim_client:head(?TOMMY)), 106 | 107 | %% betting 108 | sim:check_notify_stage(?GS_PREFLOP, Players), 109 | sim:turnover_player_raise({?JACK, Players}, {10, 20, 80}, 0), 110 | sim:turnover_player_raise({?TOMMY, Players}, { 0, 20, 80}, 70), 111 | sim:turnover_player_raise({?JACK, Players}, {70, 0, 10}, 0), 112 | sim:check_notify_stage_end(?GS_PREFLOP, Players), 113 | 114 | sim:check_shared(1, Players), 115 | sim:check_shared(1, Players), 116 | 117 | ?assertMatch(#notify_cards{player = ?TOMMY_ID }, sim_client:head(?JACK)), 118 | ?assertMatch(#notify_cards{player = ?JACK_ID }, sim_client:head(?TOMMY)), 119 | 120 | ?assertMatch(#notify_hand{rank = ?HC_HIGH_CARD}, sim_client:head(?TOMMY)), 121 | ?assertMatch(#notify_hand{rank = ?HC_HIGH_CARD}, sim_client:head(?TOMMY)), 122 | ?assertMatch(#notify_hand{rank = ?HC_HIGH_CARD}, sim_client:head(?JACK)), 123 | ?assertMatch(#notify_hand{rank = ?HC_HIGH_CARD}, sim_client:head(?JACK)), 124 | 125 | sim:check_notify_win(?TOMMY_ID, 40 + 140, Players), 126 | sim:check_notify_out(?JACK_ID, Players) 127 | end}. 128 | 129 | setup_with_shutdown() -> 130 | Mods = [{op_mod_blinds, []}, {rig, [hand:make_cards("3H 4H 3D 4D 3C 4C 4S ")]}, 131 | {deal_cards, [2, private]}, {deal_cards, [3, shared]}, {ranking, []}, 132 | {op_mod_betting, [?GS_PREFLOP]}, {showdown, []}, {wait_players, []}], 133 | setup(Mods). 134 | 135 | setup_with_shutdown2() -> 136 | Mods = [{op_mod_blinds, []}, {rig, [hand:make_cards("3H 5H 7S 6S TD QD JD AS 2C")]}, 137 | {deal_cards, [2, private]}, {deal_cards, [3, shared]}, {ranking, []}, 138 | {op_mod_betting, [?GS_PREFLOP]}, {deal_cards, [1, shared]}, {deal_cards, [1, shared]}, {showdown, []}, {wait_players, []}], 139 | setup(Mods). 140 | 141 | setup_with_rank() -> 142 | setup([{op_mod_blinds, []}, {rig, [hand:make_cards("3H 4H 3D 4D 3C")]}, {deal_cards, [2, private]}, {deal_cards, [1, shared]}, {ranking, []}]). 143 | 144 | setup(MixinMods) -> 145 | sim:setup(), 146 | sim:setup_game( 147 | #tab_game_config{ 148 | module = game, required = 2, seat_count = 9, 149 | limit = #limit{min = 100, max = 400, small = 10, big = 20}, 150 | mods = [{op_mod_suspend, []}, {wait_players, []}] ++ MixinMods ++ [{stop, []}], 151 | start_delay = 500, timeout = 1000, max = 1}). 152 | -------------------------------------------------------------------------------- /test/integration/mod_login_test.erl: -------------------------------------------------------------------------------- 1 | -module(mod_login_test). 2 | -include("openpoker.hrl"). 3 | -include("openpoker_test.hrl"). 4 | 5 | login_timeout_test_() -> {setup, fun sim:setup/0, fun sim:clean/1, fun() -> 6 | sim_client:start(?MODULE, 100), 7 | ?assert(is_pid(whereis(?MODULE))), 8 | ?SLEEP, 9 | ?assertNot(is_pid(whereis(?MODULE))), 10 | ?assertMatch([#notify_error{error = ?ERR_CONNECTION_TIMEOUT}], sim_client:box()) 11 | end}. 12 | 13 | login_unauth_test_() -> {setup, fun sim:setup/0, fun sim:clean/1, fun() -> 14 | sim_client:start(?MODULE), 15 | ?assert(is_pid(whereis(?MODULE))), 16 | sim_client:send(?MODULE, #cmd_login{identity = <<"player">>, password = <<"def_pwd">>}), 17 | ?assertNot(is_pid(whereis(?MODULE))), 18 | ?assertMatch([#notify_error{error = ?ERR_UNAUTH}], sim_client:box()) 19 | end}. 20 | 21 | login_unauth_pwd_test_() -> {setup, fun sim:setup/0, fun sim:clean/1, fun() -> 22 | sim_client:start(?MODULE), 23 | ?assert(is_pid(whereis(?MODULE))), 24 | 25 | Jack = sim_client:player(?JACK, ?PLAYERS), 26 | mnesia:dirty_write(Jack#tab_player_info{password = <<"test">>}), 27 | 28 | sim_client:send(?MODULE, #cmd_login{identity = <<"jack">>, password = <<"def_pwd">>}), 29 | ?assertNot(is_pid(whereis(?MODULE))), 30 | ?assertMatch([#notify_error{error = ?ERR_UNAUTH}], sim_client:box()) 31 | end}. 32 | 33 | login_disable_test_() -> {setup, fun sim:setup/0, fun sim:clean/1, fun() -> 34 | sim_client:start(?MODULE), 35 | ?assert(is_pid(whereis(?MODULE))), 36 | 37 | Jack = sim_client:player(?JACK, ?PLAYERS), 38 | mnesia:dirty_write(Jack#tab_player_info{disabled = true}), 39 | sim_client:send(?MODULE, #cmd_login{identity = <<"jack">>, password = <<"def_pwd">>}), 40 | ?assertNot(is_pid(whereis(?MODULE))), 41 | ?assertMatch([#notify_error{error = ?ERR_PLAYER_DISABLE}], sim_client:box()) 42 | end}. 43 | -------------------------------------------------------------------------------- /test/sim.erl: -------------------------------------------------------------------------------- 1 | -module(sim). 2 | -compile([export_all]). 3 | 4 | -include("openpoker.hrl"). 5 | -include("openpoker_test.hrl"). 6 | 7 | set_buyin([], [], R) -> R; 8 | set_buyin([H|PT], [B|BT], R) -> 9 | set_buyin(PT, BT, R ++ [erlang:append_element(H, B)]). 10 | 11 | join_and_start_game(Players) -> 12 | join_and_start_game(Players, 1), 13 | sim:clean_start_game(Players). 14 | 15 | join_and_start_game(Players, Buyin) when is_list(Buyin) -> 16 | BP = sim:set_buyin(Players, Buyin, []), 17 | join_and_start_game(BP, 1), 18 | sim:clean_start_game(Players); 19 | join_and_start_game([], _SN) -> ok; 20 | join_and_start_game([{Key, Id}|T], SN) -> 21 | join_and_start_game([{Key, Id, 100}|T], SN); 22 | join_and_start_game([{Key, _Id, BuyIn}|T], SN) -> 23 | sim_client:send(Key, #cmd_watch{game = ?GAME}), 24 | GameDetail = sim_client:head(Key), 25 | ?assertMatch(#notify_game_detail{}, GameDetail), 26 | check_notify_seat(Key, GameDetail#notify_game_detail.seats), 27 | ?assertMatch(#notify_watch{}, sim_client:head(Key)), 28 | 29 | case SN of 30 | ?UNDEF -> 31 | sim_client:send(Key, #cmd_join{game = ?GAME, sn = SN, buyin = BuyIn}), 32 | join_and_start_game(T, ?UNDEF); 33 | SN -> 34 | sim_client:send(Key, #cmd_join{game = ?GAME, sn = 0, buyin = BuyIn}), 35 | join_and_start_game(T, SN + 1) 36 | end. 37 | 38 | clean_start_game(Players) -> 39 | clean_box(Players), 40 | go_go_go(), 41 | ?SLEEP, 42 | ?SLEEP, 43 | ?SLEEP, 44 | check_notify_start(Players). 45 | 46 | clean_box([]) -> ok; 47 | clean_box([{Key, _}|T]) -> 48 | sim_client:box(Key), 49 | clean_box(T). 50 | 51 | check_notify_seat(Key, 0) -> 52 | ?assertMatch(#notify_seats_list_end{}, sim_client:head(Key)); 53 | check_notify_seat(Key, N) -> 54 | ?assertMatch(#notify_seat{}, sim_client:head(Key)), 55 | check_notify_seat(Key, N - 1). 56 | 57 | check_notify_start([]) -> ok; 58 | check_notify_start([{Key, _Id}|T]) -> 59 | ?assertMatch(#notify_game_start{}, sim_client:head(Key)), 60 | check_notify_start(T). 61 | 62 | check_notify_join([], 0, 0) -> ok; 63 | check_notify_join([_|T], 0, S) -> 64 | check_notify_join(T, S - 1, S - 1); 65 | check_notify_join(Players = [{Key, _Id}|_], N, S) -> 66 | ?assertMatch(#notify_join{}, sim_client:head(Key)), 67 | check_notify_join(Players, N - 1, S). 68 | 69 | check_notify_leave(_Actor, []) -> ok; 70 | check_notify_leave(Actor, PL = [{Key, _SN}|T]) -> 71 | {Actor, SN} = proplists:lookup(Actor, PL), 72 | ?assertMatch(#notify_leave{game = ?GAME, sn = SN}, sim_client:head(Key)), 73 | check_notify_leave(Actor, T). 74 | 75 | check_notify_fold(_SN, []) -> ok; 76 | check_notify_fold(SN, [{Key, _Id}|T]) -> 77 | ?assertMatch(#notify_fold{game = ?GAME, sn = SN}, sim_client:head(Key)), 78 | check_notify_fold(SN, T). 79 | 80 | check_notify_raise(_Call, _Raise, []) -> ok; 81 | check_notify_raise(Call, Raise, [{Key, _Id}|T]) -> 82 | ?assertMatch(#notify_raise{call = Call, raise = Raise}, sim_client:head(Key)), 83 | check_notify_raise(Call, Raise, T). 84 | 85 | check_notify_actor(_SN, []) -> ok; 86 | check_notify_actor(SN, [{Key, _Id}|T]) -> 87 | ?assertMatch(#notify_actor{sn = SN}, sim_client:head(Key)), 88 | check_notify_actor(SN, T). 89 | 90 | check_notify_stage(_GS, []) -> ok; 91 | check_notify_stage(GS, [{Key, _}|T]) -> 92 | ?assertMatch(#notify_stage{stage = GS}, sim_client:head(Key)), 93 | check_notify_stage(GS, T). 94 | 95 | check_notify_stage_end(_GS, []) -> ok; 96 | check_notify_stage_end(GS, [{Key, _}|T]) -> 97 | ?assertMatch(#notify_stage_end{stage = GS}, sim_client:head(Key)), 98 | check_notify_stage_end(GS, T). 99 | 100 | check_blind([], _, _, _) -> ok; 101 | check_blind([{Key, _Id}|T], B, SB, BB) -> 102 | ?assertMatch(#notify_button{b = B}, sim_client:head(Key)), 103 | ?assertMatch(#notify_sb{sb = SB}, sim_client:head(Key)), 104 | ?assertMatch(#notify_bb{bb = BB}, sim_client:head(Key)), 105 | ?assertMatch(#notify_raise{call = 10}, sim_client:head(Key)), 106 | ?assertMatch(#notify_raise{call = 20}, sim_client:head(Key)), 107 | ?assertMatch(#notify_stage{stage = ?GS_PREFLOP}, sim_client:head(Key)), 108 | check_blind(T, B, SB, BB). 109 | 110 | check_blind_only_seat([], _, _, _) -> ok; 111 | check_blind_only_seat([{Key, _Id}|T], B, SB, BB) -> 112 | ?assertMatch(#notify_button{b = B}, sim_client:head(Key)), 113 | ?assertMatch(#notify_sb{sb = SB}, sim_client:head(Key)), 114 | ?assertMatch(#notify_bb{bb = BB}, sim_client:head(Key)), 115 | check_blind_only_seat(T, B, SB, BB). 116 | 117 | check_blind_only_raise([], _, _, _) -> ok; 118 | check_blind_only_raise([{Key, _Id}|T], B, SB, BB) -> 119 | ?assertMatch(#notify_button{b = B}, sim_client:head(Key)), 120 | ?assertMatch(#notify_sb{sb = SB}, sim_client:head(Key)), 121 | ?assertMatch(#notify_bb{bb = BB}, sim_client:head(Key)), 122 | ?assertMatch(#notify_raise{call = 10}, sim_client:head(Key)), 123 | ?assertMatch(#notify_raise{call = 20}, sim_client:head(Key)), 124 | check_blind_only_raise(T, B, SB, BB). 125 | 126 | check_deal() -> 127 | %% Tommy is left button player, first deal card. 128 | %% Button is last deal card player, in this is Jack. 129 | 130 | %% tommy first card 131 | ?assertMatch(#notify_draw{game = ?GAME, player = ?TOMMY_ID}, sim_client:head(?JACK)), 132 | ?assertMatch(#notify_private{game = ?GAME, player = ?TOMMY_ID}, sim_client:head(?TOMMY)), 133 | 134 | %% jack first card 135 | ?assertMatch(#notify_draw{game = ?GAME, player = ?JACK_ID}, sim_client:head(?TOMMY)), 136 | ?assertMatch(#notify_private{game = ?GAME, player = ?JACK_ID}, sim_client:head(?JACK)), 137 | 138 | %% tommy second card 139 | ?assertMatch(#notify_draw{game = ?GAME, player = ?TOMMY_ID}, sim_client:head(?JACK)), 140 | ?assertMatch(#notify_private{game = ?GAME, player = ?TOMMY_ID}, sim_client:head(?TOMMY)), 141 | 142 | %% jack second card 143 | ?assertMatch(#notify_draw{game = ?GAME, player = ?JACK_ID}, sim_client:head(?TOMMY)), 144 | ?assertMatch(#notify_private{game = ?GAME, player = ?JACK_ID}, sim_client:head(?JACK)). 145 | 146 | three_check_deal() -> 147 | %% Tommy is left button player, first deal card. 148 | %% Button is last deal card player, in this is Jack. 149 | 150 | %% tommy first card 151 | ?assertMatch(#notify_draw{game = ?GAME, player = ?TOMMY_ID}, sim_client:head(?JACK)), 152 | ?assertMatch(#notify_draw{game = ?GAME, player = ?TOMMY_ID}, sim_client:head(?FOO)), 153 | ?assertMatch(#notify_private{game = ?GAME, player = ?TOMMY_ID}, sim_client:head(?TOMMY)), 154 | 155 | %% foo first card 156 | ?assertMatch(#notify_draw{game = ?GAME, player = ?FOO_ID}, sim_client:head(?JACK)), 157 | ?assertMatch(#notify_draw{game = ?GAME, player = ?FOO_ID}, sim_client:head(?TOMMY)), 158 | ?assertMatch(#notify_private{game = ?GAME, player = ?FOO_ID}, sim_client:head(?FOO)), 159 | 160 | %% jack first card 161 | ?assertMatch(#notify_draw{game = ?GAME, player = ?JACK_ID}, sim_client:head(?TOMMY)), 162 | ?assertMatch(#notify_draw{game = ?GAME, player = ?JACK_ID}, sim_client:head(?FOO)), 163 | ?assertMatch(#notify_private{game = ?GAME, player = ?JACK_ID}, sim_client:head(?JACK)), 164 | 165 | %% tommy second card 166 | ?assertMatch(#notify_draw{game = ?GAME, player = ?TOMMY_ID}, sim_client:head(?JACK)), 167 | ?assertMatch(#notify_draw{game = ?GAME, player = ?TOMMY_ID}, sim_client:head(?FOO)), 168 | ?assertMatch(#notify_private{game = ?GAME, player = ?TOMMY_ID}, sim_client:head(?TOMMY)), 169 | 170 | %% foo second card 171 | ?assertMatch(#notify_draw{game = ?GAME, player = ?FOO_ID}, sim_client:head(?JACK)), 172 | ?assertMatch(#notify_draw{game = ?GAME, player = ?FOO_ID}, sim_client:head(?TOMMY)), 173 | ?assertMatch(#notify_private{game = ?GAME, player = ?FOO_ID}, sim_client:head(?FOO)), 174 | 175 | %% jack second card 176 | ?assertMatch(#notify_draw{game = ?GAME, player = ?JACK_ID}, sim_client:head(?TOMMY)), 177 | ?assertMatch(#notify_draw{game = ?GAME, player = ?JACK_ID}, sim_client:head(?FOO)), 178 | ?assertMatch(#notify_private{game = ?GAME, player = ?JACK_ID}, sim_client:head(?JACK)). 179 | check_shared(N, Players) -> 180 | check_shared(N, N, Players). 181 | 182 | check_shared(_, _, []) -> ok; 183 | check_shared(N, 0, [_|T]) -> 184 | check_shared(N, N, T); 185 | check_shared(N, S, L = [{Key, _Id}|_T]) -> 186 | ?assertMatch(#notify_shared{game = ?GAME}, sim_client:head(Key)), 187 | check_shared(N, S - 1, L). 188 | 189 | check_notify_game_end([]) -> ok; 190 | check_notify_game_end([{Key, _}|T]) -> 191 | ?assertMatch(#notify_game_end{game = ?GAME}, sim_client:head(Key)), 192 | check_notify_game_end(T). 193 | 194 | check_notify_game_cancel([]) -> ok; 195 | check_notify_game_cancel([{Key, _}|T]) -> 196 | ?assertMatch(#notify_game_cancel{game = ?GAME}, sim_client:head(Key)), 197 | check_notify_game_cancel(T). 198 | 199 | check_notify_win(_Player, _Amt, []) -> ok; 200 | check_notify_win(Player, Amt, [{Key, _Id}|T]) -> 201 | ?assertMatch(#notify_win{game = ?GAME, player = Player, amount = Amt}, sim_client:head(Key)), 202 | check_notify_win(Player, Amt, T). 203 | 204 | check_notify_out(_Player, []) -> ok; 205 | check_notify_out(Player, [{Key, _Id}|T]) -> 206 | ?assertMatch(#notify_out{game = ?GAME, player = Player}, sim_client:head(Key)), 207 | check_notify_out(Player, T). 208 | 209 | turnover_player_raise({Actor, Players}, {Call, Min, Max}, Raise) -> 210 | {Actor, SN} = proplists:lookup(Actor, Players), 211 | check_notify_actor(SN, Players), 212 | ?assertMatch(#notify_betting{call = Call, min = Min, max = Max}, sim_client:head(Actor)), 213 | sim_client:send(Actor, #cmd_raise{game = ?GAME, amount = Raise}), 214 | check_notify_raise(Call, Raise, Players). 215 | 216 | turnover_player_fold({Actor, Players}, {Call, Min, Max}) -> 217 | {Actor, SN} = proplists:lookup(Actor, Players), 218 | sim:check_notify_actor(SN, Players), 219 | ?assertMatch(#notify_betting{call = Call, min = Min, max = Max}, sim_client:head(Actor)), 220 | sim_client:send(Actor, #cmd_fold{game = ?GAME}), 221 | sim:check_notify_fold(SN, Players). 222 | 223 | turnover_player_leave({Actor, Players}, {Call, Min, Max}) -> 224 | {Actor, SN} = proplists:lookup(Actor, Players), 225 | sim:check_notify_actor(SN, Players), 226 | ?assertMatch(#notify_betting{call = Call, min = Min, max = Max}, sim_client:head(Actor)), 227 | sim_client:send(Actor, #cmd_leave{game = ?GAME}), 228 | sim:check_notify_fold(SN, Players), 229 | sim:check_notify_leave(Actor, Players). 230 | 231 | setup() -> 232 | error_logger:tty(false), 233 | application:start(sasl), 234 | setup_schema(), 235 | ?assertMatch(ok, application:start(openpoker)), 236 | setup_players(?PLAYERS), 237 | error_logger:tty(true). 238 | 239 | setup_schema() -> 240 | error_logger:tty(false), 241 | application:stop(mnesia), 242 | ?assertEqual(ok, mnesia:delete_schema([node()])), 243 | ?assertEqual(ok, mnesia:create_schema([node()])), 244 | ?assertEqual(ok, application:start(mnesia)), 245 | ?assertEqual(ok, schema:rebuild_core()), 246 | error_logger:tty(true). 247 | 248 | setup_players(L) when is_list(L) -> 249 | lists:map(fun ({Key, R}) -> 250 | case whereis(Key) of 251 | undefined -> ok; 252 | P -> exit(P, kill), ?SLEEP 253 | end, 254 | ?assertNot(is_pid(whereis(Key))), 255 | Identity = list_to_binary(R#tab_player_info.identity), 256 | mnesia:dirty_write(R), 257 | sim_client:start(Key), 258 | sim_client:send(Key, #cmd_login{identity = Identity, password = <>}), 259 | ?assertMatch(#notify_signin{}, sim_client:head(Key)), 260 | ?assertMatch(#notify_player{}, sim_client:head(Key)), 261 | ?assertMatch(#notify_acount{}, sim_client:head(Key)), 262 | {Key, R} 263 | end, L). 264 | 265 | clean(_) -> 266 | error_logger:tty(false), 267 | ?assert(ok =:= application:stop(openpoker)). 268 | 269 | go_go_go() -> 270 | gen_server:cast(?GAME_NAME(?GAME), go_go_go). 271 | 272 | get_status(Name) -> 273 | op_common:get_status(Name). 274 | 275 | game_state(GID) -> 276 | State = get_status(?GAME_NAME(GID)), 277 | State#exch.state. 278 | 279 | game_state() -> 280 | game_state(?GAME). 281 | 282 | game_ctx() -> 283 | State = get_status(?GAME_NAME(?GAME)), 284 | State#exch.ctx. 285 | 286 | player_state(PID) -> 287 | get_status(?PLAYER(PID)). 288 | 289 | setup_game(Conf) -> 290 | op_games_sup:start_child(Conf). 291 | 292 | check_actor(SN, {Call, Min, Max}, Players) -> 293 | {Key, _ID} = lists:nth(SN, Players), 294 | sim:check_notify_actor(SN, Players), 295 | ?assertMatch(#notify_betting{call = Call, min = Min, max = Max}, sim_client:head(Key)). 296 | 297 | check_raise(SN, allin, {Call, Raise}, Players) -> 298 | check_raise(SN, 0, {Call, Raise}, Players); 299 | check_raise(SN, check, {Call, Raise}, Players) -> 300 | check_raise(SN, 0, {Call, Raise}, Players); 301 | check_raise(SN, call, {Call, Raise}, Players) -> 302 | check_raise(SN, 0, {Call, Raise}, Players); 303 | check_raise(SN, Amount, {Call, Raise}, Players) -> 304 | {Key, _ID} = lists:nth(SN, Players), 305 | sim_client:send(Key, #cmd_raise{game = ?GAME, amount = Amount}), 306 | sim:check_notify_raise(Call, Raise, Players). 307 | -------------------------------------------------------------------------------- /test/sim_client.erl: -------------------------------------------------------------------------------- 1 | -module(sim_client). 2 | -export([start/1, start/2, stop/1, send/2, head/1, box/0, box/1]). 3 | -export([player/2, setup_players/1]). 4 | 5 | -export([loop/3]). 6 | 7 | %% 模拟网络链接到服务器 8 | %% 通过启动一个进程,模拟对网络链接的双向请求, 9 | %% 提供单元测试一种模拟客户端的机制。 10 | 11 | %% 由于使用webtekcos提供的wesocket链接,通过op_game模块 12 | %% 处理客户端与服务器间通信的各种消息。sim_client启动后同样模拟 13 | %% webtekcos的消息层,将各种消息同样发到op_game模块进行处理 14 | %% 以达到Mock的效果。同时sim_client为测试程序提供了一些格外的接口 15 | %% 以检查通信进程内部进程数据的正确性。 16 | 17 | %% sim_client采用erlang最基本的消息元语进行编写。 18 | 19 | -include("openpoker.hrl"). 20 | -include("openpoker_test.hrl"). 21 | 22 | -record(pdata, { box = [], host = ?UNDEF, timeout = 2000 }). 23 | 24 | %%% 25 | %%% client 26 | %%% 27 | 28 | start(Key, Timeout) -> 29 | ?assert(undefined =:= whereis(Key)), 30 | PD = #pdata{host = self(), timeout = Timeout}, 31 | PID = spawn(?MODULE, loop, [op_game_handler, ?UNDEF, PD]), 32 | ?assert(true =:= register(Key, PID)), 33 | PID. 34 | 35 | start(Key) when is_atom(Key) -> 36 | start(Key, 60 * 1000). 37 | 38 | stop(Id) when is_pid(Id) -> 39 | exit(Id, kill). 40 | 41 | send(Id, R) -> 42 | Id ! {sim, send, R}, 43 | ?SLEEP. 44 | 45 | head(Id) -> 46 | case whereis(Id) of 47 | ?UNDEF -> 48 | error_logger:error_report("sim_client:head/1 to undefined process"), 49 | ?UNDEF; 50 | Pid -> 51 | Pid ! {sim, head, self()}, 52 | receive 53 | R when is_tuple(R) -> R 54 | after 55 | 500 -> exit(request_timeout) 56 | end 57 | end. 58 | 59 | box() -> 60 | receive 61 | Box when is_list(Box) -> Box 62 | end. 63 | 64 | box(Id) -> 65 | Id ! {sim, box, self()}, 66 | box(). 67 | 68 | %% tools function 69 | 70 | player(Identity, Players) when is_atom(Identity) -> 71 | proplists:get_value(Identity, Players). 72 | 73 | setup_players(L) -> 74 | sim:setup_players(L). 75 | 76 | %%% 77 | %%% callback 78 | %%% 79 | 80 | loop(Mod, ?UNDEF, Data = #pdata{}) -> 81 | LoopData = Mod:connect(Data#pdata.timeout), 82 | loop(Mod, LoopData, Data); 83 | 84 | loop(Mod, LoopData, Data = #pdata{box = Box}) -> 85 | receive 86 | %% sim send protocol from client to server 87 | {sim, send, R} when is_tuple(R) -> 88 | Bin = base64:encode(list_to_binary(protocol:write(R))), 89 | NewLoopData = Mod:handle_data(Bin, LoopData), 90 | loop(Mod, NewLoopData, Data); 91 | %% sim get client side header message 92 | {sim, head, From} when is_pid(From) -> 93 | case Box of 94 | [H|T] -> 95 | From ! H, 96 | loop(Mod, LoopData, Data#pdata{box = T}); 97 | [] -> 98 | loop(Mod, LoopData, Data#pdata{box = []}) 99 | end; 100 | {sim, box, From} when is_pid(From) -> 101 | From ! Box, 102 | loop(Mod, LoopData, Data#pdata{box = []}); 103 | {sim, kill} -> 104 | exit(kill); 105 | close -> 106 | Data#pdata.host ! Box, 107 | exit(normal); 108 | {send, Bin} when is_binary(Bin) -> 109 | R = protocol:read(base64:decode(Bin)), 110 | loop(Mod, LoopData, Data#pdata{box = Box ++ [R]}); 111 | Message -> 112 | NewLoopData = Mod:handle_message(Message, LoopData), 113 | loop(Mod, NewLoopData, Data) 114 | end. 115 | -------------------------------------------------------------------------------- /test/unit/bits_test.erl: -------------------------------------------------------------------------------- 1 | -module(bits_test). 2 | -include("openpoker_test.hrl"). 3 | 4 | %% 计算有效位(位值为1)的数量 5 | bits0_test() -> 6 | L = [{1, 2#0001}, 7 | {2, 2#1001}, 8 | {1, 2#0100}, 9 | {0, 2#0000}, 10 | {3, 2#1101}], 11 | [?assertEqual(Out, bits:bits0(In)) || {Out, In} <- L]. 12 | 13 | %% 从高位保留有效位 14 | clear_extra_bits_test() -> 15 | L = [{2#00010110, 2, 2#00010100}, 16 | {2#10011101, 1, 2#10000000}, 17 | {2#01000101, 3, 2#01000101}, 18 | {2#00000101, 5, 2#00000101}, 19 | {2#11010101, 2, 2#11000000}], 20 | [?assertEqual(Out, bits:clear_extra_bits(In1, In2)) || {In1, In2, Out} <- L]. 21 | -------------------------------------------------------------------------------- /test/unit/player_test.erl: -------------------------------------------------------------------------------- 1 | -module(player_test). 2 | -include("openpoker.hrl"). 3 | -include("openpoker_test.hrl"). 4 | 5 | start_test_() -> {setup, fun sim:setup/0, fun sim:clean/1, fun () -> 6 | ?assert(is_tuple(sim:player_state(?JACK_ID))) 7 | end}. 8 | 9 | auth_test_() -> {setup, fun sim:setup/0, fun sim:clean/1, fun () -> 10 | ?assertMatch({ok, pass, #tab_player_info{identity = "jack"}}, player:auth("jack", ?DEF_PWD)), 11 | ?assertEqual({ok, unauth}, player:auth("unknown", ?DEF_PWD)), 12 | ?assertEqual({ok, unauth}, player:auth("jack", "bad_pwd")), 13 | 14 | Info = sim_client:player(jack, ?PLAYERS), 15 | 16 | ?assertEqual({ok, pass, Info}, player:auth(Info, ?DEF_PWD)), 17 | ?assertEqual({ok, unauth}, player:auth(Info, "bad_pwd")), 18 | ?assertEqual({ok, player_disable}, player:auth(Info#tab_player_info{disabled = true}, ?DEF_PWD)) 19 | end}. 20 | -------------------------------------------------------------------------------- /test/unit/protocol_test.erl: -------------------------------------------------------------------------------- 1 | -module(protocol_test). 2 | -import(protocol, [write/1, read/1]). 3 | -include("openpoker.hrl"). 4 | -include("openpoker_test.hrl"). 5 | 6 | -define(REG_GAME, {game, 1}). 7 | -define(REG_PLAYER, {player, 1}). 8 | 9 | id_to_game_test_() -> {setup, fun setup_game/0, fun cleanup/1, fun () -> 10 | ?assert(is_pid((turn(#cmd_watch{game = 1}))#cmd_watch.game)) 11 | end}. 12 | 13 | id_to_player_test_() -> {setup, fun setup_player/0, fun cleanup/1, fun () -> 14 | ?assert(is_pid((turn(#cmd_query_player{player = 1}))#cmd_query_player.player)) 15 | end}. 16 | 17 | notify_stage_test() -> 18 | R = turn(#notify_stage{game = 1, stage = 1}), 19 | ?assertMatch(#notify_stage{game = 1, stage = 1}, R). 20 | 21 | notify_win_test() -> 22 | R = turn(#notify_win{game = 1, player = 1, amount = 100}), 23 | ?assertMatch(#notify_win{game = 1, player = 1, amount = 100}, R). 24 | 25 | setup_game() -> 26 | Pid = spawn(fun loop_fun/0), 27 | ?assertEqual(yes, global:register_name(?REG_GAME, Pid)), 28 | Pid. 29 | 30 | setup_player() -> 31 | Pid = spawn(fun loop_fun/0), 32 | ?assertEqual(yes, global:register_name(?REG_PLAYER, Pid)), 33 | Pid. 34 | 35 | cleanup(Pid) -> 36 | exit(Pid,exit). 37 | 38 | turn(R) -> 39 | Data = protocol:write(R), 40 | protocol:read(list_to_binary(Data)). 41 | 42 | loop_fun() -> 43 | receive _ -> loop_fun() end. 44 | -------------------------------------------------------------------------------- /test/unit/texas_game_test.erl: -------------------------------------------------------------------------------- 1 | -module(texas_game_test). 2 | -include("openpoker.hrl"). 3 | -include("openpoker_test.hrl"). 4 | 5 | start_test_() -> {setup, fun setup/0, fun sim:clean/1, fun () -> 6 | ?assert(is_pid(?LOOKUP_GAME(1))), 7 | ?assert(is_pid(?LOOKUP_GAME(2))) 8 | end}. 9 | 10 | list_test_() -> {setup, fun setup/0, fun sim:clean/1, fun () -> 11 | lists:map(fun (R) -> ?assertMatch(#notify_game{}, R) end, game:list()) 12 | end}. 13 | 14 | setup() -> 15 | sim:setup(), 16 | sim:setup_game( 17 | #tab_game_config{ 18 | module = game, mods = [{wait_players, []}], limit = no_limit, 19 | seat_count = 9, start_delay = 3000, required = 2, timeout = 1000, max = 2}). 20 | -------------------------------------------------------------------------------- /test/unit/texas_seat_test.erl: -------------------------------------------------------------------------------- 1 | -module(texas_seat_test). 2 | -compile([export_all]). 3 | 4 | -include("openpoker.hrl"). 5 | -include("openpoker_test.hrl"). 6 | -include_lib("eunit/include/eunit.hrl"). 7 | 8 | new_test() -> 9 | R = seat:new(4), 10 | ?assertEqual(4, size(R)), 11 | First = element(1, R), 12 | Last = element(4, R), 13 | ?assertEqual(1, First#seat.sn), 14 | ?assertEqual(4, Last#seat.sn). 15 | 16 | set_test() -> 17 | R = seat:set(#seat{sn = 3}, ?PS_PLAY, seat:new(4)), 18 | [R1] = seat:lookup(?PS_PLAY, R), 19 | ?assertEqual(?PS_PLAY, R1#seat.state). 20 | 21 | get_test() -> 22 | All = seat:new(5), 23 | ?assertEqual(?UNDEF, seat:get(0, All)), 24 | ?assertEqual(?UNDEF, seat:get(6, All)), 25 | ?assertMatch(#seat{sn = 5, state = ?PS_EMPTY}, seat:get(5, All)). 26 | 27 | lookup_test() -> 28 | All = seat:new(5), 29 | R = seat:lookup(?PS_EMPTY, All), 30 | ?assertEqual(5, length(R)), 31 | First = lists:nth(1, R), 32 | Last = lists:nth(5, R), 33 | ?assertEqual(1, First#seat.sn), 34 | ?assertEqual(?PS_EMPTY, First#seat.state), 35 | ?assertEqual(5, Last#seat.sn), 36 | ?assertEqual(?PS_EMPTY, Last#seat.state). 37 | 38 | lookup_at_test() -> 39 | Seats = seat:set([#seat{sn = 2}, #seat{sn = 1}], ?PS_PLAY, seat:new(5)), 40 | ?assertMatch(#seat{state = ?PS_PLAY, sn = 1}, seat:get(1, Seats)), 41 | ?assertMatch(#seat{state = ?PS_PLAY, sn = 2}, seat:get(2, Seats)), 42 | ?assertMatch(#seat{state = ?PS_EMPTY, sn = 3}, seat:get(3, Seats)), 43 | ?assertMatch(#seat{state = ?PS_EMPTY, sn = 4}, seat:get(4, Seats)), 44 | ?assertMatch(#seat{state = ?PS_EMPTY, sn = 5}, seat:get(5, Seats)), 45 | LookupSeats = seat:lookup(?PS_PLAY, Seats, #seat{sn = 1}), 46 | ?assertMatch([#seat{state = ?PS_PLAY, sn = 2}, #seat{state = ?PS_PLAY, sn = 1}], LookupSeats). 47 | 48 | lookup_mask_test() -> 49 | S = #seat{sn = 3, state = ?PS_PLAY}, 50 | 51 | R = seat:lookup(?PS_EMPTY, seat:set(S, seat:new(5))), 52 | ?assertEqual(4, length(R)), 53 | ?assertEqual(4, (lists:nth(3, R))#seat.sn), 54 | 55 | R1 = seat:lookup(?PS_STANDING, seat:set(S, seat:new(5))), 56 | ?assertEqual(1, length(R1)), 57 | ?assertEqual(3, (lists:nth(1, R1))#seat.sn). 58 | 59 | set_list_test() -> 60 | Seats = seat:set([#seat{sn = 2}, #seat{sn = 4}], ?PS_PLAY, seat:new(4)), 61 | ?assertEqual(#seat{state = ?PS_PLAY, sn = 2}, seat:get(2, Seats)), 62 | ?assertEqual(#seat{state = ?PS_PLAY, sn = 4}, seat:get(4, Seats)). 63 | --------------------------------------------------------------------------------