leonardo-bridge 0.4.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|