leonardo-bridge 0.4.3
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 +15 -0
- data/.gitignore +18 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +46 -0
- data/Rakefile +1 -0
- data/bin/leo-console +7 -0
- data/bin/leo-play +143 -0
- data/bridge.gemspec +29 -0
- data/bridge.rc.rb +11 -0
- data/lib/bridge.rb +35 -0
- data/lib/bridge/auction.rb +182 -0
- data/lib/bridge/board.rb +84 -0
- data/lib/bridge/call.rb +98 -0
- data/lib/bridge/card.rb +54 -0
- data/lib/bridge/contract.rb +51 -0
- data/lib/bridge/db.rb +27 -0
- data/lib/bridge/deal.rb +33 -0
- data/lib/bridge/deck.rb +22 -0
- data/lib/bridge/game.rb +372 -0
- data/lib/bridge/hand.rb +25 -0
- data/lib/bridge/leonardo_result.rb +7 -0
- data/lib/bridge/player.rb +49 -0
- data/lib/bridge/result.rb +290 -0
- data/lib/bridge/trick.rb +28 -0
- data/lib/bridge/trick_play.rb +219 -0
- data/lib/bridge/version.rb +3 -0
- data/lib/enum.rb +32 -0
- data/lib/redis_model.rb +137 -0
- data/lib/uuid.rb +280 -0
- data/spec/auction_spec.rb +100 -0
- data/spec/board_spec.rb +19 -0
- data/spec/bridge_spec.rb +25 -0
- data/spec/call_spec.rb +44 -0
- data/spec/card_spec.rb +95 -0
- data/spec/db_spec.rb +19 -0
- data/spec/deck_spec.rb +14 -0
- data/spec/enum_spec.rb +14 -0
- data/spec/game_spec.rb +291 -0
- data/spec/hand_spec.rb +21 -0
- data/spec/player_spec.rb +22 -0
- data/spec/redis_model_spec.rb +50 -0
- data/spec/result_spec.rb +64 -0
- data/spec/spec_helper.rb +21 -0
- data/spec/support/auction_helper.rb +9 -0
- data/spec/support/test_enum.rb +5 -0
- data/spec/support/test_model.rb +3 -0
- data/spec/trick_play_spec.rb +90 -0
- data/spec/trick_spec.rb +15 -0
- metadata +240 -0
data/lib/bridge/hand.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
module Bridge
|
2
|
+
class Hand
|
3
|
+
attr_accessor :cards, :played, :current
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
self.cards = []
|
7
|
+
end
|
8
|
+
|
9
|
+
def method_missing(m, *args, &block)
|
10
|
+
if cards.respond_to?(m)
|
11
|
+
cards.send(m, *args, &block)
|
12
|
+
else
|
13
|
+
super
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_s
|
18
|
+
cards.to_s
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_json opts = {}
|
22
|
+
cards.to_json
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Bridge
|
2
|
+
# Actor representing a player's view of a BridgeGame object.
|
3
|
+
# ref: https://pybridge.svn.sourceforge.net/svnroot/pybridge/trunk/pybridge/pybridge/games/bridge/game.py
|
4
|
+
class Player < RedisModel
|
5
|
+
def initialize(game)
|
6
|
+
@game = game # Access to game is private to this object.
|
7
|
+
end
|
8
|
+
|
9
|
+
def get_hand
|
10
|
+
position = game.players[self]
|
11
|
+
return game.get_hand(position)
|
12
|
+
end
|
13
|
+
alias :hand :get_hand
|
14
|
+
|
15
|
+
def make_call(call)
|
16
|
+
begin
|
17
|
+
return game.make_call(call, player = self)
|
18
|
+
rescue Exception => e
|
19
|
+
if Bridge::DEBUG
|
20
|
+
puts e.backtrace.first(8).join("\n").red
|
21
|
+
puts "\n"
|
22
|
+
end
|
23
|
+
raise GameError, e.message
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def play_card(card)
|
28
|
+
begin
|
29
|
+
return self.game.play_card(card, self)
|
30
|
+
rescue Exception => e
|
31
|
+
if Bridge::DEBUG
|
32
|
+
puts e.backtrace.first(8).join("\n").red
|
33
|
+
puts "\n"
|
34
|
+
end
|
35
|
+
raise GameError, e.message
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def start_next_game
|
40
|
+
raise GameError, "Not ready to start game" unless game.next_game_ready?
|
41
|
+
game.start!
|
42
|
+
end
|
43
|
+
|
44
|
+
protected
|
45
|
+
def game
|
46
|
+
@game
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,290 @@
|
|
1
|
+
|
2
|
+
module Bridge
|
3
|
+
# Represents the result of a completed round of bridge.
|
4
|
+
# Swiped from: https://svn.code.sf.net/p/pybridge/code/trunk/pybridge/pybridge/games/bridge/result.py
|
5
|
+
class Result
|
6
|
+
VULN_MAP = {
|
7
|
+
Vulnerability.none => [],
|
8
|
+
Vulnerability.north_south => [Direction.north, Direction.south],
|
9
|
+
Vulnerability.east_west => [Direction.east, Direction.west],
|
10
|
+
Vulnerability.all => [
|
11
|
+
Direction.north, Direction.east,
|
12
|
+
Direction.west, Direction.south
|
13
|
+
]
|
14
|
+
}
|
15
|
+
|
16
|
+
def _get_score
|
17
|
+
raise NoMethodError # Expected to be implemented by subclasses.
|
18
|
+
end
|
19
|
+
|
20
|
+
# @type board: Board
|
21
|
+
# @type contract: Contract
|
22
|
+
# @type tricks_made: int or None
|
23
|
+
attr_accessor :board, :contract, :tricks_made, :is_vulnerable, :score
|
24
|
+
attr_accessor :is_doubled, :is_redoubled, :is_vulnerable, :is_major,
|
25
|
+
:contract_level, :tricks_made, :tricks_required, :trump_suit, :claimed, :claimed_by
|
26
|
+
|
27
|
+
def initialize(board, contract, tricks_made = nil, opts = {})
|
28
|
+
self.board = board
|
29
|
+
self.contract = contract
|
30
|
+
self.tricks_made = tricks_made
|
31
|
+
|
32
|
+
# a claim has been made. Let's modify the trick count accordingly
|
33
|
+
if opts[:claim].is_a?(Array)
|
34
|
+
self.claimed_by = opts[:claim][0]
|
35
|
+
self.claimed = opts[:claim][1]
|
36
|
+
defender_tricks = opts[:claim][2]
|
37
|
+
if [self.contract[:declarer],(self.contract[:declarer] + 2) % 4].include?(claimed_by)
|
38
|
+
self.tricks_made += claimed # if declarer claimed, add claim to tally
|
39
|
+
else # if defender claimed, add what remains to tally
|
40
|
+
self.tricks_made = 13 - (defender_tricks + claimed)
|
41
|
+
end
|
42
|
+
self.tricks_made
|
43
|
+
end
|
44
|
+
|
45
|
+
self.is_vulnerable = nil
|
46
|
+
if self.contract
|
47
|
+
vuln = self.board.vulnerability || Vulnerability.none
|
48
|
+
self.is_vulnerable = VULN_MAP[vuln].include?(self.contract[:declarer])
|
49
|
+
end
|
50
|
+
|
51
|
+
self.score = self._get_score
|
52
|
+
end
|
53
|
+
|
54
|
+
# Compute the component values which contribute to the score.
|
55
|
+
# Note that particular scoring schemes may ignore some of the components.
|
56
|
+
# Scoring values: http://en.wikipedia.org/wiki/Bridge_scoring
|
57
|
+
# @return: a dict of component values.
|
58
|
+
# @rtype: dict
|
59
|
+
def _get_score_components
|
60
|
+
components = {}
|
61
|
+
|
62
|
+
self.is_doubled = self.contract[:double_by] ? true : false
|
63
|
+
self.is_redoubled = self.contract[:redouble_by] ? true : false
|
64
|
+
self.contract_level = self.contract[:bid].level + 1
|
65
|
+
self.tricks_required = contract_level + 6
|
66
|
+
self.trump_suit = self.contract[:bid].strain
|
67
|
+
self.is_major = [Strain.spade, Strain.heart].include?(self.contract[:bid].strain)
|
68
|
+
|
69
|
+
if tricks_made >= tricks_required # Contract successful.
|
70
|
+
#### Contract tricks (bid and made) ####
|
71
|
+
if is_major || self.contract[:bid].strain == Strain.no_trump # Hearts, Spades and NT score 30 for each odd trick.
|
72
|
+
components['odd'] = contract_level * 30
|
73
|
+
if trump_suit == Strain.no_trump
|
74
|
+
components['odd'] += 10 # For NT, add a 10 point bonus.
|
75
|
+
end
|
76
|
+
else
|
77
|
+
components['odd'] = contract_level * 20
|
78
|
+
end
|
79
|
+
|
80
|
+
if is_redoubled
|
81
|
+
components['odd'] *= 4 # Double the doubled score.
|
82
|
+
elsif is_doubled
|
83
|
+
components['odd'] *= 2 # Double score.
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
#### over_tricks ####
|
88
|
+
over_tricks = tricks_made - tricks_required
|
89
|
+
|
90
|
+
if is_redoubled
|
91
|
+
# 400 for each overtrick if vulnerable, 200 if not.
|
92
|
+
if is_vulnerable
|
93
|
+
components['over'] = over_tricks * 400
|
94
|
+
else
|
95
|
+
components['over'] = over_tricks * 200
|
96
|
+
end
|
97
|
+
elsif is_doubled
|
98
|
+
# 200 for each overtrick if vulnerable, 100 if not.
|
99
|
+
if is_vulnerable
|
100
|
+
components['over'] = over_tricks * 200
|
101
|
+
else
|
102
|
+
components['over'] = over_tricks * 100
|
103
|
+
end
|
104
|
+
else # Undoubled contract.
|
105
|
+
if is_major || self.contract[:bid].strain == Strain.no_trump
|
106
|
+
# Hearts, Spades and NT score 30 for each overtrick.
|
107
|
+
components['over'] = over_tricks * 30
|
108
|
+
else
|
109
|
+
# Clubs and Diamonds score 20 for each overtrick.
|
110
|
+
components['over'] = over_tricks * 20
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
#### Premium bonuses ####
|
115
|
+
|
116
|
+
if tricks_required == 13
|
117
|
+
# 1500 for grand slam if vulnerable, 1000 if not.
|
118
|
+
if is_vulnerable
|
119
|
+
components['slambonus'] = 1500
|
120
|
+
else
|
121
|
+
components['slambonus'] = 1000
|
122
|
+
end
|
123
|
+
elsif tricks_required == 12
|
124
|
+
# 750 for small slam if vulnerable, 500 if not.
|
125
|
+
if is_vulnerable
|
126
|
+
components['slambonus'] = 750
|
127
|
+
else
|
128
|
+
components['slambonus'] = 500
|
129
|
+
end
|
130
|
+
elsif components['odd'] >= 100 # Game contract (non-slam).
|
131
|
+
# 500 for game if vulnerable, 300 if not.
|
132
|
+
if is_vulnerable
|
133
|
+
components['gamebonus'] = 500
|
134
|
+
else
|
135
|
+
components['gamebonus'] = 300
|
136
|
+
end
|
137
|
+
else # Non-game contract.
|
138
|
+
components['partscore'] = 50
|
139
|
+
end
|
140
|
+
|
141
|
+
#### Insult bonus ####
|
142
|
+
if is_redoubled
|
143
|
+
components['insultbonus'] = 100
|
144
|
+
elsif is_doubled
|
145
|
+
components['insultbonus'] = 50
|
146
|
+
end
|
147
|
+
else # Contract not successful.
|
148
|
+
under_tricks = tricks_required - tricks_made
|
149
|
+
if is_redoubled
|
150
|
+
if is_vulnerable
|
151
|
+
# -400 for first, then -600 each.
|
152
|
+
components['under'] = -400 + (under_tricks - 1) * -600
|
153
|
+
else
|
154
|
+
# -200 for first, -400 for second and third, then -600 each.
|
155
|
+
components['under'] = -200 + (under_tricks - 1) * -400
|
156
|
+
if under_tricks > 3
|
157
|
+
components['under'] += (under_tricks - 3) * -200
|
158
|
+
end
|
159
|
+
end
|
160
|
+
elsif is_doubled
|
161
|
+
if is_vulnerable
|
162
|
+
# -200 for first, then -300 each.
|
163
|
+
components['under'] = -200 + (under_tricks - 1) * -300
|
164
|
+
else
|
165
|
+
# -100 for first, -200 for second and third, then -300 each.
|
166
|
+
components['under'] = -100 + (under_tricks - 1) * -200
|
167
|
+
if under_tricks > 3
|
168
|
+
components['under'] += (under_tricks - 3) * -100
|
169
|
+
end
|
170
|
+
end
|
171
|
+
else
|
172
|
+
if is_vulnerable
|
173
|
+
# -100 each.
|
174
|
+
components['under'] = under_tricks * -100
|
175
|
+
else
|
176
|
+
# -50 each.
|
177
|
+
components['under'] = under_tricks * -50
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
components
|
182
|
+
end
|
183
|
+
|
184
|
+
def to_a
|
185
|
+
{
|
186
|
+
score: _get_score,
|
187
|
+
tricks_made: tricks_made,
|
188
|
+
tricks_required: tricks_required,
|
189
|
+
contract_level: contract_level,
|
190
|
+
trump_suit: trump_suit,
|
191
|
+
vulnerable: is_vulnerable,
|
192
|
+
major: is_major,
|
193
|
+
doubled: is_doubled,
|
194
|
+
redoubled: is_redoubled,
|
195
|
+
claimed: claimed,
|
196
|
+
claimed_by: claimed_by
|
197
|
+
}
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
|
202
|
+
#Represents the result of a completed round of duplicate bridge.
|
203
|
+
class DuplicateResult < Result
|
204
|
+
# Duplicate bridge scoring scheme.
|
205
|
+
# @return: score value: positive for declarer, negative for defenders.
|
206
|
+
def _get_score
|
207
|
+
score = 0
|
208
|
+
if self.contract and self.tricks_made
|
209
|
+
self._get_score_components.each do |key, value|
|
210
|
+
if ['odd', 'over', 'under', 'slambonus', 'gamebonus', 'partscore', 'insultbonus'].include?(key)
|
211
|
+
score += value
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
score
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
# Represents the result of a completed round of rubber bridge.
|
220
|
+
class RubberResult < Result
|
221
|
+
# Rubber bridge scoring scheme.
|
222
|
+
# @return: 2-tuple of numeric scores (above the line, below the line): positive for
|
223
|
+
# declarer, negative for defenders.
|
224
|
+
def _get_score
|
225
|
+
above, below = 0, 0
|
226
|
+
if self.contract and self.tricks_made
|
227
|
+
self._get_score_components.items.each do |key, value|
|
228
|
+
# Note: gamebonus/partscore are not assigned in rubber bridge.
|
229
|
+
if ['over', 'under', 'slambonus', 'insultbonus'].include?(key)
|
230
|
+
above += value
|
231
|
+
elsif key == 'odd'
|
232
|
+
below += value
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
return [above, below]
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
|
241
|
+
|
242
|
+
# A rubber set, in which pairs compete to make two consecutive games.
|
243
|
+
# A game is made by accumulation of 100+ points from below-the-line scores
|
244
|
+
# without interruption from an opponent's game.
|
245
|
+
class Rubber < Array
|
246
|
+
attr_accessor :games, :winner
|
247
|
+
|
248
|
+
# Returns a list of completed (ie. won) 'games' in this rubber, in the
|
249
|
+
# order of their completion.
|
250
|
+
|
251
|
+
# A game is represented as a list of consecutive results from this rubber,
|
252
|
+
# coupled with the identifier of the scoring pair.
|
253
|
+
def _get_games
|
254
|
+
games = []
|
255
|
+
|
256
|
+
thisgame = []
|
257
|
+
belowNS, belowEW = 0, 0 # Cumulative totals for results in this game.
|
258
|
+
|
259
|
+
self.each do |result|
|
260
|
+
thisgame << result
|
261
|
+
if [Direction.north, Direction.south].include?(result.contract.declarer)
|
262
|
+
belowNS += result.score[1]
|
263
|
+
if belowNS >= 100
|
264
|
+
games << [thisgame, [Direction.north, Direction.south]]
|
265
|
+
else
|
266
|
+
belowEW += result.score[1]
|
267
|
+
if belowEW >= 100
|
268
|
+
games << [thisgame, [Direction.east, Direction.west]]
|
269
|
+
end
|
270
|
+
end
|
271
|
+
# If either total for this game exceeds 100, proceed to next game.
|
272
|
+
if belowNS >= 100 or belowEW >= 100
|
273
|
+
thisgame = []
|
274
|
+
belowNS, belowEW = 0, 0 # Reset accumulators.
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
278
|
+
return games
|
279
|
+
end
|
280
|
+
|
281
|
+
# The rubber is won by the pair which have completed two games.
|
282
|
+
def _get_winner
|
283
|
+
pairs = self.games.map { |game, pair| pair }
|
284
|
+
[[Direction.north, Direction.south], [Direction.east, Direction.west]].each do |pair|
|
285
|
+
pair if pairs.count(pair) >= 2
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
data/lib/bridge/trick.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
module Bridge
|
2
|
+
class Trick
|
3
|
+
REQUIRED_CARDS = 4
|
4
|
+
attr_accessor :cards
|
5
|
+
attr_accessor :leader
|
6
|
+
def initialize(params = {})
|
7
|
+
params.map { |k,v| self.send(:"#{k}=",v) }
|
8
|
+
self.cards = [] if self.cards.nil?
|
9
|
+
end
|
10
|
+
|
11
|
+
def done?
|
12
|
+
self.cards.compact.size >= REQUIRED_CARDS
|
13
|
+
end
|
14
|
+
|
15
|
+
def leader_card
|
16
|
+
self.cards[self.leader]
|
17
|
+
end
|
18
|
+
|
19
|
+
def method_missing(method, *args, &block)
|
20
|
+
begin
|
21
|
+
self.cards = self.cards.to_s.split(' ').map { |c| Card.from_string(c) } unless self.cards.class == Array
|
22
|
+
self.cards.send(method, *args, &block)
|
23
|
+
rescue Exception => e
|
24
|
+
super
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,219 @@
|
|
1
|
+
module Bridge
|
2
|
+
# This class models the trick-taking phase of a game of bridge.
|
3
|
+
# This code is generalised, and could easily be adapted to support a
|
4
|
+
# variety of trick-taking card games.
|
5
|
+
# Swiped from: https://pybridge.svn.sourceforge.net/svnroot/pybridge/trunk/pybridge/pybridge/games/bridge/auction.py
|
6
|
+
class TrickPlay
|
7
|
+
attr_accessor :trumps, :declarer, :dummy, :lho, :rho, :played, :winners, :history
|
8
|
+
|
9
|
+
# TODO: tricks, leader, winner properties?
|
10
|
+
|
11
|
+
# @param declarer: the declarer from the auction.
|
12
|
+
# @type declarer: Direction
|
13
|
+
# @param trump_suit: the trump suit from the auction.
|
14
|
+
# @type trump_suit: Suit or None
|
15
|
+
def initialize(declarer, trump_suit)
|
16
|
+
raise TypeError, "Expected Direction, got #{declarer.inspect}" unless Direction[declarer]
|
17
|
+
raise TypeError, "Expected Suit, got #{trump_suit.inspect}" if !trump_suit.nil? and Suit[trump_suit].nil?
|
18
|
+
|
19
|
+
self.trumps = trump_suit
|
20
|
+
self.declarer = declarer
|
21
|
+
self.dummy = Direction[(declarer + 2) % 4]
|
22
|
+
self.lho = Direction[(declarer + 1) % 4]
|
23
|
+
self.rho = Direction[(declarer + 3) % 4]
|
24
|
+
# Each trick corresponds to a cross-section of lists.
|
25
|
+
self.played = {}
|
26
|
+
self.history = []
|
27
|
+
Direction.each do |position|
|
28
|
+
self.played[position] = []
|
29
|
+
end
|
30
|
+
self.winners = [] # Winning player of each trick.
|
31
|
+
end
|
32
|
+
|
33
|
+
# Playing is complete if there are 13 complete tricks.
|
34
|
+
# @return: True if playing is complete, False if not.
|
35
|
+
def complete?
|
36
|
+
self.winners.size == 13
|
37
|
+
end
|
38
|
+
|
39
|
+
# A trick is a set of cards, one from each player's hand.
|
40
|
+
# The leader plays the first card, the others play in clockwise order.
|
41
|
+
# @param: trick index, in range 0 to 12.
|
42
|
+
# @return: a trick object.
|
43
|
+
def get_trick(index)
|
44
|
+
raise ArgumentError unless 0 <= index and index < 13
|
45
|
+
if index == 0 # First trick.
|
46
|
+
leader = self.lho # Leader is declarer's left-hand opponent.
|
47
|
+
else # Leader is winner of previous trick.
|
48
|
+
leader = self.winners[index - 1]
|
49
|
+
end
|
50
|
+
|
51
|
+
cards = []
|
52
|
+
|
53
|
+
Direction.each do |position|
|
54
|
+
# If length of list exceeds index value, player's card in trick.
|
55
|
+
if self.played[position].size > index
|
56
|
+
cards[position] = self.played[position][index]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
Trick.new(:leader => leader, :cards => cards)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Returns the getTrick() tuple of the current trick.
|
64
|
+
# @return: a (leader, cards) trick tuple.
|
65
|
+
def get_current_trick
|
66
|
+
# Index of current trick is length of longest played list minus 1.
|
67
|
+
index = [0, (self.played.map { |dir,cards| cards.size }.max - 1)].max
|
68
|
+
self.get_trick(index)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Returns the number of tricks won by declarer/dummy and by defenders.
|
72
|
+
# @return: the declarer trick count, the defender trick count.
|
73
|
+
# @rtype: tuple
|
74
|
+
def get_trick_count
|
75
|
+
declarer_count, defender_count = 0, 0
|
76
|
+
|
77
|
+
(0..self.winners.size-1).each do |i|
|
78
|
+
trick = self.get_trick(i)
|
79
|
+
winner = self.who_played?(self.winning_card(trick))
|
80
|
+
if [self.declarer, self.dummy].include?(winner)
|
81
|
+
declarer_count += 1
|
82
|
+
else
|
83
|
+
defender_count += 1
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
[declarer_count, defender_count]
|
88
|
+
end
|
89
|
+
|
90
|
+
def get_tricks
|
91
|
+
(0..self.winners.size-1).map do |i|
|
92
|
+
self.get_trick(i)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Plays card to current trick.
|
97
|
+
# Card validity should be checked with isValidPlay() beforehand.
|
98
|
+
# @param card: the Card object to be played from player's hand.
|
99
|
+
# @param player: the player of card, or None.
|
100
|
+
# @param hand: the hand of player, or [].
|
101
|
+
def play_card(card, player=nil, hand=nil)
|
102
|
+
Bridge.assert_card(card)
|
103
|
+
|
104
|
+
player = player || self.whose_turn
|
105
|
+
hand = hand || [card] # Skip hand check.
|
106
|
+
|
107
|
+
raise ArgumentError, 'Not valid play' unless self.valid_play?(card, player, hand)
|
108
|
+
self.played[player] << card
|
109
|
+
self.history << card
|
110
|
+
|
111
|
+
# If trick is complete, determine winner.
|
112
|
+
trick = self.get_current_trick
|
113
|
+
if trick.cards.compact.size == 4
|
114
|
+
winner = self.who_played?(self.winning_card(trick))
|
115
|
+
self.winners << winner
|
116
|
+
end
|
117
|
+
return true
|
118
|
+
end
|
119
|
+
|
120
|
+
# Card is playable if and only if:
|
121
|
+
# - Play session is not complete.
|
122
|
+
# - Direction is on turn to play.
|
123
|
+
# - Card exists in hand.
|
124
|
+
# - Card has not been previously played.
|
125
|
+
# In addition, if the current trick has an established lead, then
|
126
|
+
# card must follow lead suit OR hand must be void in lead suit.
|
127
|
+
# Specification of player and hand are required for verification.
|
128
|
+
def valid_play?(card, player=nil, hand=[])
|
129
|
+
Bridge.assert_card(card)
|
130
|
+
|
131
|
+
if self.complete?
|
132
|
+
return false
|
133
|
+
elsif hand and !hand.include?(card)
|
134
|
+
return false # Playing a card not in hand.
|
135
|
+
elsif player and self.whose_turn != self.dummy and player != self.whose_turn
|
136
|
+
return false # Playing out of turn.
|
137
|
+
elsif self.who_played?(card)
|
138
|
+
return false # Card played previously.
|
139
|
+
end
|
140
|
+
trick = self.get_current_trick
|
141
|
+
# 0 if start of playing, 4 if complete trick.
|
142
|
+
if [0, 4].include?(trick.cards.compact.size)
|
143
|
+
return true # Card will be first in next trick.
|
144
|
+
else # Current trick has an established lead: check for revoke.
|
145
|
+
leadcard = trick.leader_card
|
146
|
+
# Cards in hand that match suit of leadcard.
|
147
|
+
followers = hand.select { |c| c.suit == leadcard.suit and !self.who_played?(c) }
|
148
|
+
# Hand void in lead suit or card follows lead suit.
|
149
|
+
return (followers.size == 0 or followers.include?(card))
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
# Returns the player who played the specified card.
|
154
|
+
# @param card: a Card.
|
155
|
+
# @return: the player who played card.
|
156
|
+
def who_played?(card)
|
157
|
+
Bridge.assert_card(card) unless card.nil?
|
158
|
+
self.played.each do |player,cards|
|
159
|
+
return player if cards.include?(card)
|
160
|
+
end
|
161
|
+
false
|
162
|
+
end
|
163
|
+
|
164
|
+
# If playing is not complete, returns the player who is next to play.
|
165
|
+
# @return: the player next to play.
|
166
|
+
def whose_turn
|
167
|
+
unless self.complete?
|
168
|
+
trick = self.get_current_trick
|
169
|
+
if trick.cards.compact.size == 4 # If trick is complete, trick winner's turn.
|
170
|
+
return self.who_played?(self.winning_card(trick))
|
171
|
+
else # Otherwise, turn is next (clockwise) player in trick.
|
172
|
+
return Direction[(trick.leader + trick.cards.compact.size) % 4]
|
173
|
+
end
|
174
|
+
end
|
175
|
+
return false
|
176
|
+
end
|
177
|
+
|
178
|
+
# Determine which card wins the specified trick:
|
179
|
+
# - In a trump contract, the highest ranked trump card wins.
|
180
|
+
# - Otherwise, the highest ranked card of the lead suit wins.
|
181
|
+
# @param: a complete (leader, cards) trick tuple.
|
182
|
+
# @return: the Card object which wins the trick.
|
183
|
+
def winning_card(trick)
|
184
|
+
if trick.cards.compact.size == 4 # Trick is complete.
|
185
|
+
if self.trumps # Suit contract.
|
186
|
+
trumpcards = trick.cards.compact.select { |c| c.suit_i == self.trumps }
|
187
|
+
if trumpcards.size > 0 # Highest ranked trump.
|
188
|
+
return trumpcards.max
|
189
|
+
else # we re in trump contract but play didn't have a trump.
|
190
|
+
followers = trick.cards.compact.select { |c| c.suit == trick.leader_card.suit }
|
191
|
+
return followers.max # Highest ranked card in lead suit.
|
192
|
+
end
|
193
|
+
else
|
194
|
+
# No Trump contract, or no trump cards played.
|
195
|
+
followers = trick.cards.compact.select { |c| c.suit == trick.leader_card.suit }
|
196
|
+
return followers.max # Highest ranked card in lead suit.
|
197
|
+
end
|
198
|
+
else
|
199
|
+
return false
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def to_a
|
204
|
+
trick_counts = self.get_trick_count
|
205
|
+
{
|
206
|
+
trumps: self.trumps,
|
207
|
+
declarer: self.declarer,
|
208
|
+
dummy: self.dummy,
|
209
|
+
lho: self.lho,
|
210
|
+
rho: self.rho,
|
211
|
+
played: self.played,
|
212
|
+
winners: self.winners,
|
213
|
+
declarer_trick_count: trick_counts.first,
|
214
|
+
defender_trick_count: trick_counts.last,
|
215
|
+
tricks: self.get_tricks
|
216
|
+
}
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|