bitpoker 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +36 -0
- data/LICENSE +21 -0
- data/README.md +50 -0
- data/Rakefile +43 -0
- data/bitpoker.gemspec +16 -0
- data/bot/README.md +1 -0
- data/bot/dummy_bot.rb +50 -0
- data/lib/bitpoker.rb +61 -0
- data/lib/bitpoker/bot_interface.rb +99 -0
- data/lib/bitpoker/bot_proxy.rb +21 -0
- data/lib/bitpoker/bot_proxy_interface.rb +18 -0
- data/lib/bitpoker/croupier.rb +99 -0
- data/lib/bitpoker/duel.rb +71 -0
- data/lib/bitpoker/game_logic.rb +186 -0
- data/lib/bitpoker/round.rb +111 -0
- data/lib/bitpoker/rules.rb +16 -0
- data/test.rb +0 -0
- data/test/support/glass_bot.rb +46 -0
- data/test/support/sleepy_bot.rb +14 -0
- data/test/test_bitpoker.rb +21 -0
- data/test/test_bot_interface.rb +59 -0
- data/test/test_bot_proxy.rb +40 -0
- data/test/test_croupier.rb +147 -0
- data/test/test_duel.rb +58 -0
- data/test/test_game_logic.rb +272 -0
- data/test/test_round.rb +97 -0
- data/test/test_rules.rb +59 -0
- metadata +100 -0
@@ -0,0 +1,99 @@
|
|
1
|
+
module BitPoker
|
2
|
+
|
3
|
+
|
4
|
+
#
|
5
|
+
#
|
6
|
+
# @author Mckomo
|
7
|
+
class Croupier
|
8
|
+
|
9
|
+
attr_reader :rules
|
10
|
+
|
11
|
+
include GameLogic
|
12
|
+
|
13
|
+
def initialize( custom_rules = {} )
|
14
|
+
setup_with( custom_rules )
|
15
|
+
@prng = Random.new
|
16
|
+
end
|
17
|
+
|
18
|
+
def call( bot, action, args = [] )
|
19
|
+
|
20
|
+
raise ArgumentError, "Croupier plays only with BotProxy." unless bot.kind_of? BotProxyInterface
|
21
|
+
|
22
|
+
begin
|
23
|
+
# Get bot response within timeout
|
24
|
+
bot_response = timeout( @rules[:timeout] ) do
|
25
|
+
bot.trigger( action, args )
|
26
|
+
end
|
27
|
+
rescue Timeout::Error
|
28
|
+
raise BitPoker::BotError.new( bot, "Bot exceeded timeout" )
|
29
|
+
rescue NotImplementedError
|
30
|
+
raise BitPoker::BotError.new( bot, "Bot does not implement '#{action}' action" )
|
31
|
+
rescue => e
|
32
|
+
raise BitPoker::BotError.new( bot, "Bot failed during '#{action}' action execution." )
|
33
|
+
end
|
34
|
+
|
35
|
+
# Validate response if yield given
|
36
|
+
if block_given?
|
37
|
+
raise BitPoker::BotError.new( bot, "Bot response '#{bot_response}' after '#{action}' action is invalid" ) unless yield( bot_response )
|
38
|
+
end
|
39
|
+
|
40
|
+
bot_response
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
#
|
45
|
+
#
|
46
|
+
#
|
47
|
+
#
|
48
|
+
#
|
49
|
+
def parallel_call( bots, action, *args_list )
|
50
|
+
|
51
|
+
Parallel.map_with_index( bots, { :in_threads => 2 } ) do |b, i|
|
52
|
+
# Get args
|
53
|
+
args = args_list[i] || args_list[0]
|
54
|
+
# Call bot with or without yield
|
55
|
+
if block_given?
|
56
|
+
call( b, action, args ) do |result|
|
57
|
+
yield( result )
|
58
|
+
end
|
59
|
+
else
|
60
|
+
call( b, action, args )
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
#
|
67
|
+
#
|
68
|
+
#
|
69
|
+
def round_rules
|
70
|
+
{
|
71
|
+
"min_card" => @rules[:card_range].min,
|
72
|
+
"max_card" => @rules[:card_range].max,
|
73
|
+
"max_stake" => @rules[:max_stake],
|
74
|
+
"timeout" => @rules[:timeout]
|
75
|
+
}
|
76
|
+
end
|
77
|
+
|
78
|
+
def deal_cards
|
79
|
+
[ @prng.rand( Rules::CARD_RANGE ), @prng.rand( Rules::CARD_RANGE ) ]
|
80
|
+
end
|
81
|
+
|
82
|
+
def rules=( custom_rules )
|
83
|
+
setup( custom_rules )
|
84
|
+
end
|
85
|
+
|
86
|
+
def setup_with( rules )
|
87
|
+
@rules = {
|
88
|
+
rounds: rules[:rounds] || Rules::ROUNDS,
|
89
|
+
min_stake: rules[:min_stake] || Rules::MIN_STAKE,
|
90
|
+
max_stake: rules[:max_stake] || Rules::MAX_STAKE,
|
91
|
+
timeout: rules[:timeout] || Rules::TIMEOUT,
|
92
|
+
card_range: rules[:card_range] || Rules::CARD_RANGE,
|
93
|
+
}
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
|
99
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module BitPoker
|
2
|
+
|
3
|
+
# Basic element of the BitPoker game
|
4
|
+
# - poker duel between two bots
|
5
|
+
#
|
6
|
+
# @author Mckomo
|
7
|
+
class Duel
|
8
|
+
|
9
|
+
attr_reader :options, :total_score
|
10
|
+
|
11
|
+
# Constructor of a duel object
|
12
|
+
#
|
13
|
+
# @parma [Croupier] croupier Croupier that will perform duel
|
14
|
+
# @param [BotProxyInterface] bot_one First bot to participate the duel
|
15
|
+
# @param [BotProxyInterface] bot_two Second bot to participate the duel
|
16
|
+
# @return [Duel]
|
17
|
+
def initialize( croupier, bot_one, bot_two )
|
18
|
+
|
19
|
+
# Check if interfaces are implemented
|
20
|
+
raise ArgumentError, "Proxy one does not implement ProxyInterface." unless bot_one.kind_of?( BitPoker::BotProxyInterface )
|
21
|
+
raise ArgumentError, "Proxy two does not implement ProxyInterface." unless bot_two.kind_of?( BitPoker::BotProxyInterface )
|
22
|
+
|
23
|
+
# Set dependencies
|
24
|
+
@croupier = croupier
|
25
|
+
@bots = [bot_one, bot_two]
|
26
|
+
|
27
|
+
# Set properties
|
28
|
+
@round_counter = 0
|
29
|
+
@finished = false # Note: look on 'finished?' method
|
30
|
+
|
31
|
+
# Init score table with 0
|
32
|
+
@total_score = [0, 0]
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
# Play one round of the BitPoker
|
37
|
+
#
|
38
|
+
# @return [Duel]
|
39
|
+
def play_round
|
40
|
+
|
41
|
+
# Don't perform deal if a game is finished
|
42
|
+
return nil if finished?
|
43
|
+
|
44
|
+
@round_counter += 1
|
45
|
+
|
46
|
+
# Init new round
|
47
|
+
round = Round.new( @bots )
|
48
|
+
|
49
|
+
# Proceed round process until it is finished
|
50
|
+
until round.state == Round::STATE_FINISHED do
|
51
|
+
@croupier.perform_next_step( round )
|
52
|
+
end
|
53
|
+
|
54
|
+
# Update total score
|
55
|
+
@total_score[0] += round.score[0]
|
56
|
+
@total_score[1] += round.score[1]
|
57
|
+
|
58
|
+
self
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
# Return state of the duel
|
63
|
+
#
|
64
|
+
# @return [Mixed] True of false
|
65
|
+
def finished?
|
66
|
+
@round_counter >= @croupier.rules[:rounds]
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
@@ -0,0 +1,186 @@
|
|
1
|
+
module BitPoker
|
2
|
+
|
3
|
+
module GameLogic
|
4
|
+
|
5
|
+
def perform_next_step( round )
|
6
|
+
|
7
|
+
case round.state
|
8
|
+
when Round::STATE_RULES_INTRODUCTION
|
9
|
+
perform_rules_introduction( round )
|
10
|
+
when Round::STATE_CARD_DEAL
|
11
|
+
perform_card_deal( round )
|
12
|
+
when Round::STATE_FIRST_BETTING
|
13
|
+
perform_first_betting( round )
|
14
|
+
when Round::STATE_FIRST_CALL
|
15
|
+
perform_first_call( round )
|
16
|
+
when Round::STATE_FIRST_CALLED
|
17
|
+
perform_first_called( round )
|
18
|
+
when Round::STATE_SECOND_BETTING
|
19
|
+
perform_second_betting( round )
|
20
|
+
when Round::STATE_SECOND_CALL
|
21
|
+
perform_second_call( round )
|
22
|
+
when Round::STATE_SECOND_CALLED
|
23
|
+
perform_second_called( round )
|
24
|
+
when Round::STATE_FOLDED
|
25
|
+
perform_folded( round )
|
26
|
+
when Round::STATE_SHOWDOWN
|
27
|
+
perform_showdown( round )
|
28
|
+
when Round::STATE_POINTS_DISTRIBUTION
|
29
|
+
perform_points_distributions( round )
|
30
|
+
else
|
31
|
+
raise "Unsupported state"
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def perform_rules_introduction( round )
|
39
|
+
|
40
|
+
# Inform bots about rules
|
41
|
+
parallel_call( round.bots, :introduce, [round_rules] )
|
42
|
+
# Change round.state to first round of betting
|
43
|
+
round.state = Round::STATE_CARD_DEAL
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
def perform_card_deal( round )
|
48
|
+
|
49
|
+
# Set random cards
|
50
|
+
round.cards = deal_cards
|
51
|
+
# Give bots cards
|
52
|
+
parallel_call( round.bots, :get_card, round.cards[0], round.cards[1] )
|
53
|
+
# Change round.state to first round of betting
|
54
|
+
round.state = Round::STATE_FIRST_BETTING
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
def perform_first_betting( round )
|
59
|
+
|
60
|
+
min_stake = @rules[:min_stake] # Just shortcuts, no rocket science!
|
61
|
+
max_stake = @rules[:max_stake]
|
62
|
+
|
63
|
+
# Ask bots for bets
|
64
|
+
round.bets = parallel_call( round.bots, :bet_one, min_stake ) do |bet|
|
65
|
+
( min_stake .. max_stake ).include?( bet )
|
66
|
+
end
|
67
|
+
|
68
|
+
if round.bets_even?
|
69
|
+
# If bets are even go to second betting at once
|
70
|
+
round.state = Round::STATE_SECOND_BETTING
|
71
|
+
else
|
72
|
+
# Otherwise perform first call
|
73
|
+
round.state = Round::STATE_FIRST_CALL
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
def perform_first_call( round )
|
79
|
+
|
80
|
+
# Ask lower bidder if calls the stake
|
81
|
+
has_called = call( round.lower_bidder, :agree_one, round.stake )
|
82
|
+
|
83
|
+
if has_called
|
84
|
+
# Continue with second betting
|
85
|
+
round.state = Round::STATE_FIRST_CALLED
|
86
|
+
else
|
87
|
+
# Lower bidder has folded
|
88
|
+
round.state = Round::STATE_FOLDED
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
def perform_first_called( round )
|
94
|
+
|
95
|
+
# Lower bidder calls the stake, now bets should be leveled
|
96
|
+
round.bets[round.lower_bidder_index] = round.higher_bid
|
97
|
+
# Now second betting round can be preformed
|
98
|
+
round.state = Round::STATE_SECOND_BETTING
|
99
|
+
|
100
|
+
end
|
101
|
+
|
102
|
+
def perform_second_betting( round )
|
103
|
+
|
104
|
+
min_stake = round.stake
|
105
|
+
max_stake = @rules[:max_stake]
|
106
|
+
|
107
|
+
# If bots already bet the max stake, go to showdown at once
|
108
|
+
if round.stake == max_stake
|
109
|
+
round.state = Round::STATE_SHOWDOWN; return
|
110
|
+
end
|
111
|
+
|
112
|
+
# Ask bots for bets
|
113
|
+
round.bets = parallel_call( round.bots, :bet_two, min_stake ) do |bet|
|
114
|
+
( min_stake .. max_stake ).include?( bet )
|
115
|
+
end
|
116
|
+
|
117
|
+
if round.bets_even?
|
118
|
+
# If bets are even go to showdown right away
|
119
|
+
round.state = Round::STATE_SHOWDOWN
|
120
|
+
else
|
121
|
+
# otherwise second call is required
|
122
|
+
round.state = Round::STATE_SECOND_CALL
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
126
|
+
|
127
|
+
def perform_second_call( round )
|
128
|
+
|
129
|
+
# Ask lower bidder if calls the stake
|
130
|
+
has_called = call( round.lower_bidder, :agree_two, round.stake )
|
131
|
+
|
132
|
+
if has_called
|
133
|
+
# Bot accepted the challenge!
|
134
|
+
round.state = Round::STATE_SECOND_CALLED
|
135
|
+
else
|
136
|
+
# Bot has folded
|
137
|
+
round.state = Round::STATE_FOLDED
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|
141
|
+
|
142
|
+
def perform_second_called( round )
|
143
|
+
|
144
|
+
# Lower bidder calls the stake, now bets should be leveled
|
145
|
+
round.bets[round.lower_bidder_index] = round.higher_bid
|
146
|
+
# Time for showdown!
|
147
|
+
round.state = Round::STATE_SHOWDOWN
|
148
|
+
|
149
|
+
end
|
150
|
+
|
151
|
+
def perform_folded( round )
|
152
|
+
|
153
|
+
# Change the score
|
154
|
+
round.score[round.lower_bidder_index] -= round.lower_bid
|
155
|
+
round.score[round.higher_bidder_index] += round.lower_bid
|
156
|
+
|
157
|
+
# Round is over
|
158
|
+
round.state = Round::STATE_POINTS_DISTRIBUTION
|
159
|
+
|
160
|
+
end
|
161
|
+
|
162
|
+
def perform_showdown( round )
|
163
|
+
|
164
|
+
# Set score unless it is a draw
|
165
|
+
unless round.draw?
|
166
|
+
round.score[round.winner_index] += round.stake
|
167
|
+
round.score[round.loser_index] -= round.stake
|
168
|
+
end
|
169
|
+
|
170
|
+
# After showdown round it's time to distribute points
|
171
|
+
round.state = Round::STATE_POINTS_DISTRIBUTION
|
172
|
+
|
173
|
+
end
|
174
|
+
|
175
|
+
def perform_points_distributions( round )
|
176
|
+
|
177
|
+
# Send info to bots about their results
|
178
|
+
parallel_call( round.bots, :end_of_round, round.score[0], round.score[1] )
|
179
|
+
# Round if over
|
180
|
+
round.state = Round::STATE_FINISHED
|
181
|
+
|
182
|
+
end
|
183
|
+
|
184
|
+
end
|
185
|
+
|
186
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
module BitPoker
|
2
|
+
|
3
|
+
# Model of a BitPoker round
|
4
|
+
# @author Mckomo
|
5
|
+
class Round
|
6
|
+
|
7
|
+
STATE_RULES_INTRODUCTION = 0 # Bots are sent rules of duel
|
8
|
+
STATE_CARD_DEAL = 1 # Bots receive random cards
|
9
|
+
STATE_FIRST_BETTING = 2 # Bots set first bet
|
10
|
+
STATE_FIRST_CALL = 3 # Lower bidder is ask to call a stake after first betting round
|
11
|
+
STATE_FIRST_CALLED = 4 # Lower bidder called the stake
|
12
|
+
STATE_SECOND_BETTING = 5 # Bots set second bet
|
13
|
+
STATE_SECOND_CALL = 6 # Lower bidder is ask to call a stake after second betting round
|
14
|
+
STATE_SECOND_CALLED = 7 # Lower bidder called the stake
|
15
|
+
STATE_FOLDED = 8 # Lower bidder folded
|
16
|
+
STATE_SHOWDOWN = 9 # Result of the round is determined
|
17
|
+
STATE_POINTS_DISTRIBUTION = 10 # Bots receive their results after the round
|
18
|
+
STATE_FINISHED = -1 # Round is over
|
19
|
+
|
20
|
+
attr_accessor :bets, :cards, :score, :state
|
21
|
+
attr_reader :bots
|
22
|
+
|
23
|
+
# Contractor of a round object
|
24
|
+
#
|
25
|
+
# @param [Array] Array with two bots that participate in the round
|
26
|
+
# @return [Round]
|
27
|
+
def initialize( bots )
|
28
|
+
@bots = bots # Bind bots to the round
|
29
|
+
@score = [0, 0] # Init with clear score table
|
30
|
+
@state = STATE_RULES_INTRODUCTION # Start with card deal
|
31
|
+
end
|
32
|
+
|
33
|
+
# Value of a stake
|
34
|
+
#
|
35
|
+
# @return [Integer]
|
36
|
+
def stake
|
37
|
+
higher_bid
|
38
|
+
end
|
39
|
+
|
40
|
+
# Whether bets are even
|
41
|
+
#
|
42
|
+
# @return [Mixed] True of false
|
43
|
+
def bets_even?
|
44
|
+
@bets[0] == @bets[1]
|
45
|
+
end
|
46
|
+
|
47
|
+
# Return higher bid
|
48
|
+
#
|
49
|
+
# @return [Integer]
|
50
|
+
def higher_bid
|
51
|
+
@bets.max
|
52
|
+
end
|
53
|
+
|
54
|
+
# Return lower bid
|
55
|
+
#
|
56
|
+
# @return [Integer]
|
57
|
+
def lower_bid
|
58
|
+
@bets.min
|
59
|
+
end
|
60
|
+
|
61
|
+
# Return index of higher bidder
|
62
|
+
#
|
63
|
+
# @return [Integer] 0 or 1
|
64
|
+
def higher_bidder_index
|
65
|
+
@bets.index( higher_bid )
|
66
|
+
end
|
67
|
+
|
68
|
+
# Return index of lower bidder
|
69
|
+
#
|
70
|
+
# @return [Integer] 0 or 1
|
71
|
+
def lower_bidder_index
|
72
|
+
@bets.index( lower_bid )
|
73
|
+
end
|
74
|
+
|
75
|
+
# Return higher bidder
|
76
|
+
#
|
77
|
+
# @return [BotInterface]
|
78
|
+
def higher_bidder
|
79
|
+
@bots[higher_bidder_index]
|
80
|
+
end
|
81
|
+
|
82
|
+
# Return lower bidder
|
83
|
+
#
|
84
|
+
# @return [BotInterface]
|
85
|
+
def lower_bidder
|
86
|
+
@bots[lower_bidder_index]
|
87
|
+
end
|
88
|
+
|
89
|
+
# Whether it is a draw
|
90
|
+
#
|
91
|
+
# @return [Mixed] True of false
|
92
|
+
def draw?
|
93
|
+
@cards[0] == @cards[1]
|
94
|
+
end
|
95
|
+
|
96
|
+
# Return index of the round winner
|
97
|
+
#
|
98
|
+
# @return [Integer] 0 or 1
|
99
|
+
def winner_index
|
100
|
+
draw? ? nil : @cards.index( @cards.max )
|
101
|
+
end
|
102
|
+
|
103
|
+
# Return index of the round loser
|
104
|
+
#
|
105
|
+
# @return [Integer] 0 or 1
|
106
|
+
def loser_index
|
107
|
+
draw? ? nil : @cards.index( @cards.min )
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
end
|