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.
Files changed (50) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +18 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +46 -0
  6. data/Rakefile +1 -0
  7. data/bin/leo-console +7 -0
  8. data/bin/leo-play +143 -0
  9. data/bridge.gemspec +29 -0
  10. data/bridge.rc.rb +11 -0
  11. data/lib/bridge.rb +35 -0
  12. data/lib/bridge/auction.rb +182 -0
  13. data/lib/bridge/board.rb +84 -0
  14. data/lib/bridge/call.rb +98 -0
  15. data/lib/bridge/card.rb +54 -0
  16. data/lib/bridge/contract.rb +51 -0
  17. data/lib/bridge/db.rb +27 -0
  18. data/lib/bridge/deal.rb +33 -0
  19. data/lib/bridge/deck.rb +22 -0
  20. data/lib/bridge/game.rb +372 -0
  21. data/lib/bridge/hand.rb +25 -0
  22. data/lib/bridge/leonardo_result.rb +7 -0
  23. data/lib/bridge/player.rb +49 -0
  24. data/lib/bridge/result.rb +290 -0
  25. data/lib/bridge/trick.rb +28 -0
  26. data/lib/bridge/trick_play.rb +219 -0
  27. data/lib/bridge/version.rb +3 -0
  28. data/lib/enum.rb +32 -0
  29. data/lib/redis_model.rb +137 -0
  30. data/lib/uuid.rb +280 -0
  31. data/spec/auction_spec.rb +100 -0
  32. data/spec/board_spec.rb +19 -0
  33. data/spec/bridge_spec.rb +25 -0
  34. data/spec/call_spec.rb +44 -0
  35. data/spec/card_spec.rb +95 -0
  36. data/spec/db_spec.rb +19 -0
  37. data/spec/deck_spec.rb +14 -0
  38. data/spec/enum_spec.rb +14 -0
  39. data/spec/game_spec.rb +291 -0
  40. data/spec/hand_spec.rb +21 -0
  41. data/spec/player_spec.rb +22 -0
  42. data/spec/redis_model_spec.rb +50 -0
  43. data/spec/result_spec.rb +64 -0
  44. data/spec/spec_helper.rb +21 -0
  45. data/spec/support/auction_helper.rb +9 -0
  46. data/spec/support/test_enum.rb +5 -0
  47. data/spec/support/test_model.rb +3 -0
  48. data/spec/trick_play_spec.rb +90 -0
  49. data/spec/trick_spec.rb +15 -0
  50. metadata +240 -0
@@ -0,0 +1,84 @@
1
+ module Bridge
2
+ # Swiped from https://pybridge.svn.sourceforge.net/svnroot/pybridge/trunk/pybridge/pybridge/games/bridge/board.py
3
+ # An encapsulation of board information.
4
+ # @keyword deal: the cards in each hand.
5
+ # @type deal: Deal
6
+ # @keyword dealer: the position of the dealer.
7
+ # @type dealer: Direction
8
+ # @keyword event: the name of the event where the board was played.
9
+ # @type event: str
10
+ # @keyword num: the board number.
11
+ # @type num: int
12
+ # @keyword players: a mapping from positions to player names.
13
+ # @type players: dict
14
+ # @keyword site: the location (of the event) where the board was played.
15
+ # @type site: str
16
+ # @keyword time: the date/time when the board was generated.
17
+ # @type time: time.struct_time
18
+ # @keyword vuln: the board vulnerability.
19
+ # @type vuln: Vulnerable
20
+ class Board
21
+ attr_accessor :vulnerability, :players, :dealer, :deal, :number
22
+ attr_accessor :created_at
23
+
24
+ def initialize opts = {}
25
+ opts = {
26
+ :deal => Deal.new,
27
+ :number => 1,
28
+ :dealer => Direction.north,
29
+ :vulnerability => Vulnerability.none
30
+ }.merge(opts)
31
+
32
+ opts.map { |k,v| self.send(:"#{k}=",v) if self.respond_to?(k) }
33
+ self.created_at = Time.now
34
+ end
35
+
36
+ # Builds and returns a successor board to this board.
37
+ # The dealer and vulnerability of the successor board are determined from
38
+ # the board number, according to the rotation scheme for duplicate bridge.
39
+ # @param deal: if provided, the deal to be wrapped by next board.
40
+ # Otherwise, a randomly-generated deal is wrapped.
41
+ def self.first deal = nil
42
+ self.new(
43
+ :deal => deal || Deal.new,
44
+ :number => 1,
45
+ :dealer => Direction.north,
46
+ :vulnerability => Vulnerability.none
47
+ )
48
+ end
49
+
50
+ # Builds and returns a successor board to this board.
51
+ # The dealer and vulnerability of the successor board are determined from
52
+ # the board number, according to the rotation scheme for duplicate bridge.
53
+ # @param deal: if provided, the deal to be wrapped by next board.
54
+ # Otherwise, a randomly-generated deal is wrapped.
55
+ def next deal = nil
56
+ board = Board.new
57
+ board.deal = deal || Deal.new
58
+ board.number = self.number + 1
59
+ board.created_at = Time.now
60
+
61
+ # Dealer rotates clockwise.
62
+ board.dealer = Direction.next(self.dealer)
63
+
64
+ # Map from duplicate board index range 1..16 to vulnerability.
65
+ # See http://www.d21acbl.com/References/Laws/node5.html#law2
66
+ i = (board.number - 1) % 16
67
+ board.vulnerability = Vulnerability[(i%4 + i/4)%4]
68
+
69
+ return board
70
+ end
71
+
72
+ def to_json(opts = {})
73
+ h = {}
74
+ [:vulnerability, :players, :dealer, :deal, :number, :created_at].each do |a|
75
+ h[a] = send(a)
76
+ end
77
+ h.to_json
78
+ end
79
+
80
+ def copy
81
+ self.clone
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,98 @@
1
+ # ref: https://pybridge.svn.sourceforge.net/svnroot/pybridge/trunk/pybridge/pybridge/games/bridge/call.py
2
+ module Bridge
3
+ module Level
4
+ extend Enum
5
+ set_values :one, :two, :three, :four, :five, :six, :seven
6
+ end
7
+
8
+ module Suit
9
+ extend Enum
10
+ set_values :club, :diamond, :heart, :spade
11
+ end
12
+
13
+ module Rank
14
+ extend Enum
15
+ set_values :two, :three, :four, :five, :six, :seven, :eight, :nine, :ten, :jack, :queen, :king, :ace
16
+ end
17
+
18
+ module Strain
19
+ extend Enum
20
+ set_values :club, :diamond, :heart, :spade, :no_trump
21
+ end
22
+
23
+ class CallError < StandardError; end
24
+
25
+ # Abstract class, inherited by Bid, Pass, Double and Redouble.
26
+ class Call
27
+ def self.from_string string
28
+ string ||= ''
29
+ call = nil
30
+ case string.downcase
31
+ when 'p','pass'
32
+ call = Pass.new
33
+ when 'd', 'double'
34
+ call = Double.new
35
+ when 'r', 'redouble'
36
+ call = Redouble.new
37
+ when /^bi?d? [a-z]{3,5} [a-z\s\_]{4,8}$/i
38
+ bid = string.split
39
+ bid.shift # get rid of 'bid'
40
+ level = bid.shift
41
+ strain = bid.join('_')
42
+ call = Bid.new(level,strain)
43
+ end
44
+ raise CallError.new, "'#{string}' is not a call" if call.nil?
45
+ call
46
+ end
47
+
48
+ def self.all
49
+ calls = Strain.all.map { |s| Level.all.map { |l| Bid.new(l,s) } }.flatten
50
+ calls << Double.new
51
+ calls << Redouble.new
52
+ calls << Pass.new
53
+ calls
54
+ end
55
+
56
+ def to_s
57
+ self.class.to_s.downcase.gsub('bridge::','')
58
+ end
59
+ end
60
+
61
+ # A Bid represents a statement of a level and a strain.
62
+ # @param level: the level of the bid.
63
+ # @type level: L{Level}
64
+ # @param strain: the strain (denomination) of the bid.
65
+ # @type strain: L{Strain}
66
+ class Bid < Call
67
+ attr_accessor :level, :strain
68
+
69
+ def initialize(level, strain)
70
+ self.level = level.is_a?(Integer) ? level : Level.send(level.to_sym)
71
+ self.strain = strain.is_a?(Integer) ? strain : Strain.send(strain.to_sym)
72
+ end
73
+
74
+ include Comparable
75
+ def <=>(other)
76
+ if other.is_a?(Bid) # Compare two bids.
77
+ s_size = Strain.values.size
78
+ # puts "#{self.level*s_size + self.strain} <=> #{other.level*s_size + other.strain}"
79
+ (self.level*s_size + self.strain) <=> (other.level*s_size + other.strain)
80
+ else # Comparing non-bid calls returns true.
81
+ 1
82
+ end
83
+ end
84
+
85
+ def to_s
86
+ "#{Level.name(level)} #{Strain.name(strain)}"
87
+ end
88
+ end
89
+
90
+ # A Pass represents an abstention from the bidding.
91
+ class Pass < Call; end
92
+
93
+ # A Double over an opponent's current bid.
94
+ class Double < Call; end
95
+
96
+ # A Redouble over an opponent's double of partnership's current bid.
97
+ class Redouble < Call; end
98
+ end
@@ -0,0 +1,54 @@
1
+ module Bridge
2
+ class CardError < ArgumentError; end
3
+
4
+ class Card
5
+ include Comparable
6
+
7
+ RANKS = %w(2 3 4 5 6 7 8 9 10 J Q K A)
8
+ SUITS = %w(C D H S)
9
+
10
+ def initialize(rank, suit)
11
+ raise CardError.new "'#{rank}' is not a card rank" unless RANKS.include?(rank)
12
+ raise CardError.new "'#{suit}' is not a card suit" unless SUITS.include?(suit)
13
+ @rank = rank
14
+ @suit = suit
15
+ end
16
+ attr_reader :rank, :suit
17
+
18
+ def <=>(other)
19
+ # this ordering sorts first by rank, then by suit
20
+ (Card::SUITS.find_index(self.suit) <=> Card::SUITS.find_index(other.suit)).nonzero? or
21
+ (Card::RANKS.find_index(self.rank) <=> Card::RANKS.find_index(other.rank))
22
+ end
23
+
24
+ def to_s
25
+ @rank + @suit
26
+ end
27
+
28
+ def honour
29
+ case rank
30
+ when 'J'
31
+ 1
32
+ when 'Q'
33
+ 2
34
+ when 'K'
35
+ 3
36
+ when 'A'
37
+ 4
38
+ else
39
+ 0
40
+ end
41
+ end
42
+
43
+ def suit_i
44
+ SUITS.index(suit)
45
+ end
46
+
47
+ def self.from_string string
48
+ raise CardError.new, "'#{string}' is not a card" if string.size < 2
49
+ suit = string[string.size-1]
50
+ rank = string.chop
51
+ new(rank.upcase, suit.upcase)
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,51 @@
1
+ module Bridge
2
+ class InvalidAuctionError < StandardError; end
3
+
4
+ # Represents the result of an auction.
5
+ # Swiped from: https://pybridge.svn.sourceforge.net/svnroot/pybridge/trunk/pybridge/pybridge/games/bridge/auction.py
6
+ class Contract
7
+ attr_accessor :redouble_by, :double_by, :bid, :declarer
8
+
9
+ # @param auction: a completed, but not passed out, auction.
10
+ # @type auction: Auction
11
+ def initialize auction
12
+ raise InvalidAuctionError unless auction.complete? and !auction.passed_out?
13
+ # The contract is the last (and highest) bid.
14
+ self.bid = auction.current_bid
15
+
16
+ # The declarer is the first partner to bid the contract denomination.
17
+ caller = auction.who_called?(self.bid)
18
+ partnership = [caller, Direction[(caller + 2) % 4]]
19
+ # Determine which partner is declarer.
20
+ auction.calls.each do |call|
21
+ if call.is_a?(Bid) and call.strain == self.bid.strain
22
+ bidder = auction.who_called?(call)
23
+ if partnership.include?(bidder)
24
+ self.declarer = bidder
25
+ break
26
+ end
27
+ end
28
+ end
29
+
30
+ self.double_by, self.redouble_by = [nil, nil]
31
+
32
+ if auction.current_double
33
+ # The opponent who doubled the contract bid.
34
+ self.double_by = auction.who_called?(auction.current_double)
35
+ if auction.current_redouble
36
+ # The partner who redoubled an opponent's double.
37
+ self.redouble_by = auction.who_called?(auction.current_redouble)
38
+ end
39
+ end
40
+ end
41
+
42
+ def to_hash
43
+ {
44
+ :bid => bid,
45
+ :declarer => declarer,
46
+ :double_by => double_by,
47
+ :redouble_by => redouble_by
48
+ }
49
+ end
50
+ end
51
+ end
data/lib/bridge/db.rb ADDED
@@ -0,0 +1,27 @@
1
+ require 'redis'
2
+
3
+
4
+ module Bridge
5
+ class DB < Redis
6
+ # retrieves all values in a given namespace
7
+ # e.g. server:name, server:port and server:location all belong in the server namespace
8
+ def namespace ns
9
+ ns_keys = keys("#{ns.to_s}:*")
10
+ ns_keys = ns_keys.inject({}) { |h,k| h.update(k => type(k)) }
11
+ pipelined do
12
+ ns_keys.each do |key,type|
13
+ case type
14
+ when 'hash'
15
+ hgetall(key)
16
+ else
17
+ get(key)
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ def open_tables
24
+ lrange('opentables',0,llen('opentables')).map(&:to_i)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,33 @@
1
+ module Bridge
2
+ # deal, not game
3
+ # bidding create the contract
4
+ # bidding is also persistent
5
+ # bidding finishes after 3 passes
6
+
7
+ class Deal
8
+ attr_accessor :hands, :deck
9
+
10
+ def initialize
11
+ @hands = []
12
+ @deck = Deck.new
13
+ Direction.values.each do |dir|
14
+ @hands[Direction.send(dir)] = Hand.new
15
+ end
16
+ deal!
17
+ self
18
+ end
19
+
20
+ # deals one card per hand on a cycle until we run out of cards
21
+ def deal!
22
+ hands.cycle(deck.size/hands.size) { |hand| hand << deck.shift }
23
+ end
24
+
25
+ def method_missing(m, *args, &block)
26
+ if hands.respond_to?(m)
27
+ hands.send(m, *args, &block)
28
+ else
29
+ super
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,22 @@
1
+ module Bridge
2
+ class Deck
3
+ attr_accessor :cards
4
+
5
+ def initialize
6
+ @cards = Card::RANKS.product(Card::SUITS).map { |a| Card.new(a[0],a[1]) }
7
+ @cards.shuffle!
8
+ end
9
+
10
+ def inspect
11
+ cards.inspect
12
+ end
13
+
14
+ def method_missing(method, *args, &block)
15
+ begin
16
+ @cards.send(method, *args, &block)
17
+ rescue Exception => e
18
+ super
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,372 @@
1
+ #require File.join(File.dirname(__FILE__),'result')
2
+
3
+ module Bridge
4
+ # Raised by game in response to an unsatisfiable or erroneous request.
5
+ # ref: https://pybridge.svn.sourceforge.net/svnroot/pybridge/trunk/pybridge/pybridge/network/error.py
6
+ class GameError < StandardError
7
+ end
8
+
9
+ # A bridge game sequences the auction and trick play.
10
+ # The methods of this class comprise the interface of a state machine.
11
+ # Clients should only use the class methods to interact with the game state.
12
+ # Modifications to the state are typically made through BridgePlayer objects.
13
+ # Methods which change the game state (make_call, playCard) require a player
14
+ # argument as "authentication".
15
+ class Game < RedisModel
16
+ use_timestamps
17
+
18
+ attr_accessor :auction, :play, :players, :options, :number
19
+ attr_accessor :board, :board_queue, :results, :visible_hands
20
+ attr_accessor :trump_suit, :result, :contract, :state
21
+ attr_accessor :rubbers, :rubber_mode, :leonardo_mode
22
+
23
+ def initialize opts = {}
24
+ # Valid @positions (for Table).
25
+ @positions = Direction.values
26
+
27
+ # Mapping from Strain symbols (in auction) to Suit symbols (in play).
28
+ @trump_map = {
29
+ Strain.club => Suit.club,
30
+ Strain.diamond => Suit.diamond,
31
+ Strain.heart => Suit.heart,
32
+ Strain.spade => Suit.spade,
33
+ Strain.no_trump => nil
34
+ }
35
+
36
+ opts = {
37
+ :auction => nil,
38
+ :play => nil,
39
+ :players => {}, # One-to-one mapping from BridgePlayer to Direction
40
+ :options => {},
41
+ :board => nil,
42
+ :board_queue => [], # Boards for successive rounds.
43
+ :results => [], # Results of previous rounds.
44
+ :visible_hands => {}, # A subset of deal, containing revealed hands.
45
+ :state => :new,
46
+ :rubber_mode => false
47
+ }.merge(opts)
48
+
49
+ opts.map { |k,v| self.send(:"#{k}=",v) if self.respond_to?(k) }
50
+
51
+ if self.rubber_mode # Use rubber scoring?
52
+ self.rubbers = [] # Group results into Rubber objects.
53
+ end
54
+
55
+ self.contract = self.auction.nil? ? nil : self.auction.contract
56
+ trump_suit = self.play.nil? ? nil : self.play.trump_suit
57
+ result = self.in_progress? ? nil : self.results.last
58
+ end
59
+
60
+ # Implementation of ICardGame.
61
+ # ref: https://pybridge.svn.sourceforge.net/svnroot/pybridge/trunk/pybridge/pybridge/interfaces/game.py
62
+ def start! board = nil
63
+ raise GameError, "Game in progress" if self.in_progress?
64
+
65
+ if board # Use specified board.
66
+ self.board = board
67
+ elsif !self.board_queue.empty? # Use pre-specified board.
68
+ self.board = self.board_queue.pop
69
+ elsif self.board # Advance to next round.
70
+ self.board = self.board.next
71
+ else # Create an initial board.
72
+ self.board = Board.first
73
+ end
74
+
75
+ if self.rubber_mode
76
+ # Vulnerability determined by number of games won by each pair.
77
+ if self.rubbers.size == 0 or self.rubbers.last.winner
78
+ self.board.vulnerability = Vulnerability.none # First round, new rubber.
79
+ else
80
+ pairs = self.rubbers.last.games.map { |game, pair| pair }
81
+
82
+ if pairs.count([Direction.north, Direction.south]) > 0
83
+ if pairs.count([Direction.east, Direction.west]) > 0
84
+ self.board.vulnerability = Vulnerability.all
85
+ else
86
+ self.board.vulnerability = Vulnerability.north_south
87
+ end
88
+ else
89
+ if pairs.count([Direction.east, Direction.west]) > 0
90
+ self.board.vulnerability = Vulnerability.east_west
91
+ else
92
+ self.board.vulnerability = Vulnerability.none
93
+ end
94
+ end
95
+ end # if self.rubbers.size == 0 or self.rubbers[-1].winner
96
+ end # if self.rubber_mode
97
+ self.auction = Auction.new(self.board.dealer) # Start auction.
98
+ self.play = nil
99
+ self.visible_hands.clear
100
+
101
+ # Remove deal from board, so it does not appear to clients.
102
+ visible_board = self.board.copy
103
+ visible_board.deal = self.visible_hands
104
+
105
+ self.state = :auction
106
+ true
107
+ end
108
+
109
+
110
+ def in_progress?
111
+ if !self.play.nil?
112
+ !self.play.complete?
113
+ elsif !self.auction.nil?
114
+ !self.auction.passed_out?
115
+ else
116
+ false
117
+ end
118
+ end
119
+
120
+ def next_game_ready?
121
+ !self.in_progress? and self.players.size == 4
122
+ end
123
+
124
+ def get_state
125
+ state = {}
126
+
127
+ state[:options] = self.options
128
+ state[:results] = self.results
129
+ state[:state] = self.state
130
+ state[:contract] = self.contract
131
+ state[:calls] = Call.all
132
+ state[:available_calls] = []
133
+ begin
134
+ state[:turn] = self.get_turn
135
+ rescue Exception => e
136
+ state[:turn] = nil
137
+ end
138
+
139
+ if self.in_progress?
140
+ if state[:state] == :auction
141
+ state[:available_calls] = Call.all.select { |c| self.auction.valid_call?(c) }.compact
142
+ end
143
+ # Remove hidden hands from deal.
144
+ visible_board = self.board.copy
145
+ visible_board.deal = self.visible_hands
146
+ state[:board] = visible_board
147
+ end
148
+
149
+ state[:auction] = self.auction.to_a unless self.auction.nil?
150
+ state[:play] = self.play.to_a unless self.play.nil?
151
+
152
+ state
153
+ end
154
+
155
+ def add_player(position)
156
+ raise TypeError, "Expected valid Direction, got #{position}" unless Direction[position]
157
+ raise GameError, "Position #{position} is taken" if self.players.values.include?(position)
158
+
159
+ player = Player.new(self)
160
+ self.players[player] = position
161
+
162
+ return player
163
+ end
164
+
165
+ def remove_player(position)
166
+ raise TypeError, "Expected valid Direction, got #{position}" unless Direction[position]
167
+ raise GameError, "Position #{position} is vacant" unless self.players.values.include?(position)
168
+
169
+ self.players.reject! { |player,pos| pos == position }
170
+ end
171
+
172
+
173
+ # Bridge-specific methods.
174
+
175
+ # Make a call in the current auction.
176
+ # This method expects to receive either a player argument or a position.
177
+ # If both are given, the position argument is disregarded.
178
+ # @param call: a Call object.
179
+ # @type call: Bid or Pass or Double or Redouble
180
+ # @param player: if specified, a player object.
181
+ # @type player: BridgePlayer or nil
182
+ # @param position: if specified, the position of the player making call.
183
+ # @type position: Direction or nil
184
+ def make_call(call, player=nil, position=nil)
185
+ raise TypeError, "Expected Call, got #{call}" unless [Bid, Pass, Double, Redouble].include?(call.class)
186
+
187
+ if player
188
+ raise GameError, "Player unknown to this game" unless self.players.include?(player)
189
+ position = self.players[player]
190
+ end
191
+
192
+ raise TypeError, "Expected Direction, got #{position.class}" if position.nil? or Direction[position].nil?
193
+
194
+ # Validate call according to game state.
195
+ raise GameError, "No game in progress" if self.auction.nil?
196
+ raise GameError, "Auction complete" if self.auction.complete?
197
+ raise GameError, "Call made out of turn" if self.get_turn != position
198
+ raise GameError, "Call cannot be made" unless self.auction.valid_call?(call, position)
199
+
200
+ self.auction.make_call(call)
201
+
202
+ if self.auction.complete? and !self.auction.passed_out?
203
+ self.state = :playing
204
+ self.contract = self.auction.get_contract
205
+ trump_suit = @trump_map[self.contract[:bid].strain]
206
+ self.play = TrickPlay.new(self.contract[:declarer], trump_suit)
207
+ elsif self.auction.passed_out?
208
+ self.state = :finished
209
+ end
210
+
211
+ # If bidding is passed out, game is complete.
212
+ self._add_result(self.board, contract=nil) if not self.in_progress? and self.board.deal
213
+
214
+ if !self.in_progress? and self.board.deal
215
+ # Reveal all unrevealed hands.
216
+ Direction.each do |position|
217
+ hand = self.board.deal.hands[position]
218
+ self.reveal_hand(hand, position) if hand and !self.visible_hands.include?(position)
219
+ end
220
+ end
221
+ end
222
+
223
+ def signal_alert(alert, position)
224
+ pass # TODO
225
+ end
226
+
227
+ # Play a card in the current play session.
228
+ # This method expects to receive either a player argument or a position.
229
+ # If both are given, the position argument is disregarded.
230
+ # If position is specified, it must be that of the player of the card:
231
+ # declarer plays cards from dummy's hand when it is dummy's turn.
232
+ # @param card: a Card object.
233
+ # @type card: Card
234
+ # @param player: if specified, a player object.
235
+ # @type player: BridgePlayer or nil
236
+ # @param position: if specified, the position of the player of the card.
237
+ # @type position: Direction or nil
238
+ def play_card(card, player=nil, position=nil)
239
+ Bridge.assert_card(card)
240
+
241
+ if player
242
+ raise GameError, "Invalid player reference" unless self.players.include?(player)
243
+ position = self.players[player]
244
+ end
245
+
246
+ raise TypeError, "Expected Direction, got #{position}" unless Direction[position]
247
+ raise GameError, "No game in progress, or play complete" if self.play.nil? or self.play.complete?
248
+
249
+ playfrom = position
250
+
251
+ # Declarer controls dummy's turn.
252
+ if self.get_turn == self.play.dummy
253
+ if self.play.declarer == position
254
+ playfrom = self.play.dummy # Declarer can play from dummy.
255
+ elsif self.play.dummy == position
256
+ raise GameError, "Dummy cannot play hand"
257
+ end
258
+ end
259
+
260
+ raise GameError, "Card played out of turn" if self.get_turn != playfrom
261
+
262
+ hand = self.board.deal[playfrom] || []
263
+ # If complete deal known, validate card play.
264
+ if self.board.deal.size == Direction.size
265
+ unless self.play.valid_play?(card, position, hand)
266
+ raise GameError, "Card #{card} cannot be played from hand"
267
+ end
268
+ end
269
+
270
+ self.play.play_card(card)
271
+ hand.delete(card)
272
+
273
+ # Dummy's hand is revealed when the first card of first trick is played.
274
+ if self.play.get_trick(0).cards.compact.size == 1
275
+ dummyhand = self.board.deal[self.play.dummy]
276
+ # Reveal hand only if known.
277
+ self.reveal_hand(dummyhand, self.play.dummy) if dummyhand
278
+ end
279
+
280
+ # If play is complete, game is complete.
281
+ if !self.in_progress? and self.board.deal
282
+ self.state = :finished
283
+ tricks_made, _ = self.play.get_trick_count
284
+ self._add_result(self.board, self.contract, tricks_made)
285
+ end
286
+
287
+ if !self.in_progress? and self.board.deal
288
+ # Reveal all unrevealed hands.
289
+ Direction.each do |position|
290
+ hand = self.board.deal[position]
291
+ if hand and !self.visible_hands.include?(position)
292
+ self.reveal_hand(hand, position)
293
+ end
294
+ end
295
+ end
296
+
297
+ true
298
+ end
299
+
300
+ def _add_result(board, contract=nil, tricks_made=nil, opts = {})
301
+ if self.rubber_mode
302
+ result = RubberResult.new(board, contract, tricks_made, opts)
303
+ if self.rubbers.size > 0 and self.rubbers[-1].winner.nil?
304
+ rubber = self.rubbers[-1]
305
+ else # Instantiate new rubber.
306
+ rubber = Rubber()
307
+ self.rubbers << rubber
308
+ rubber << result
309
+ end
310
+ elsif self.leonardo_mode
311
+ result = LeonardoResult.new(board, contract, tricks_made, opts)
312
+ else
313
+ result = DuplicateResult.new(board, contract, tricks_made, opts)
314
+ end
315
+
316
+ self.results << result
317
+ end
318
+
319
+ # Reveal hand to all observers.
320
+ # @param hand: a hand of Card objects.
321
+ # @type hand: list
322
+ # @param position: the position of the hand.
323
+ # @type position: Direction
324
+ def reveal_hand(hand, position)
325
+ raise TypeError, "Expected Direction, got #{position}" unless Direction[position]
326
+
327
+ self.visible_hands[position] = hand
328
+ # Add hand to board only if it was previously unknown.
329
+ self.board.deal.hands[position] = hand unless self.board.deal.hands[position]
330
+ end
331
+
332
+ # If specified hand is visible, returns the list of cards in hand.
333
+ # @param position: the position of the requested hand.
334
+ # @type position: Direction
335
+ # @return: the hand of player at position.
336
+ def get_hand(position)
337
+ raise TypeError, "Expected Direction, got #{position}" unless Direction[position]
338
+
339
+ if self.board and self.board.deal.hands[position]
340
+ self.board.deal.hands[position]
341
+ else
342
+ raise GameError, "Hand unknown"
343
+ end
344
+ end
345
+
346
+ def get_turn
347
+ if self.in_progress?
348
+ if self.auction.complete? # In trick play.
349
+ self.play.whose_turn
350
+ else # Currently in the auction.
351
+ self.auction.whose_turn
352
+ end
353
+ else # Not in game.
354
+ raise GameError, "No game in progress"
355
+ end
356
+ end
357
+
358
+ def claim direction, tricks
359
+ if self.in_progress?
360
+ if self.auction.complete? # In trick play.
361
+ self.state = :finished
362
+ declarer_tricks, defender_tricks = self.play.get_trick_count
363
+ _add_result(self.board, self.contract, declarer_tricks, claim: [direction, tricks, defender_tricks])
364
+ else # Currently in the auction.
365
+ raise GameError, "Cannot claim during auction"
366
+ end
367
+ else # Not in game.
368
+ raise GameError, "No game in progress"
369
+ end
370
+ end
371
+ end
372
+ end