bitpoker 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|