Class: Bridge::Game

Inherits:
Object
  • Object
show all
Defined in:
lib/bridge/game.rb

Overview

A bridge game sequences the auction and trick play. The methods of this class comprise the interface of a state machine. Clients should only use the class methods to interact with the game state. Modifications to the state are typically made through BridgePlayer objects. Methods which change the game state (make_call, playCard) require a player argument as “authentication”.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(opts = {}) ⇒ Game



19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/bridge/game.rb', line 19

def initialize opts = {}
  # Valid @positions (for Table).
  @positions = Direction.values

  # Mapping from Strain symbols (in auction) to Suit symbols (in play).
  @trump_map = {
    Strain.club => Suit.club, 
    Strain.diamond => Suit.diamond,
    Strain.heart => Suit.heart, 
    Strain.spade => Suit.spade,
    Strain.no_trump => nil
  }
  
  opts = {
    :auction => nil, 
    :play => nil,
    :players => {}, # One-to-one mapping from BridgePlayer to Direction
    :options => {},
    :board => nil, 
    :board_queue => [], # Boards for successive rounds.
    :results => [], # Results of previous rounds.
    :visible_hands => {}, # A subset of deal, containing revealed hands.
    :state => :new, 
    :rubber_mode => false
  }.merge(opts)
  
  opts.map { |k,v| self.send(:"#{k}=",v) if self.respond_to?(k) }
  
  if self.rubber_mode  # Use rubber scoring?
    self.rubbers = []  # Group results into Rubber objects.
  end
  
  self.contract = self.auction.nil? ? nil : self.auction.contract
  trump_suit = self.play.nil? ? nil : self.play.trump_suit
  result = self.in_progress? ? nil : self.results.last
end

Instance Attribute Details

#auctionObject

Returns the value of attribute auction.



14
15
16
# File 'lib/bridge/game.rb', line 14

def auction
  @auction
end

#boardObject

Returns the value of attribute board.



15
16
17
# File 'lib/bridge/game.rb', line 15

def board
  @board
end

#board_queueObject

Returns the value of attribute board_queue.



15
16
17
# File 'lib/bridge/game.rb', line 15

def board_queue
  @board_queue
end

#contractObject

Returns the value of attribute contract.



16
17
18
# File 'lib/bridge/game.rb', line 16

def contract
  @contract
end

#leonardo_modeObject

Returns the value of attribute leonardo_mode.



17
18
19
# File 'lib/bridge/game.rb', line 17

def leonardo_mode
  @leonardo_mode
end

#numberObject

Returns the value of attribute number.



14
15
16
# File 'lib/bridge/game.rb', line 14

def number
  @number
end

#optionsObject

Returns the value of attribute options.



14
15
16
# File 'lib/bridge/game.rb', line 14

def options
  @options
end

#playObject

Returns the value of attribute play.



14
15
16
# File 'lib/bridge/game.rb', line 14

def play
  @play
end

#playersObject

Returns the value of attribute players.



14
15
16
# File 'lib/bridge/game.rb', line 14

def players
  @players
end

#resultObject

Returns the value of attribute result.



16
17
18
# File 'lib/bridge/game.rb', line 16

def result
  @result
end

#resultsObject

Returns the value of attribute results.



15
16
17
# File 'lib/bridge/game.rb', line 15

def results
  @results
end

#rubber_modeObject

Returns the value of attribute rubber_mode.



17
18
19
# File 'lib/bridge/game.rb', line 17

def rubber_mode
  @rubber_mode
end

#rubbersObject

Returns the value of attribute rubbers.



17
18
19
# File 'lib/bridge/game.rb', line 17

def rubbers
  @rubbers
end

#stateObject

Returns the value of attribute state.



16
17
18
# File 'lib/bridge/game.rb', line 16

def state
  @state
end

#trump_suitObject

Returns the value of attribute trump_suit.



16
17
18
# File 'lib/bridge/game.rb', line 16

def trump_suit
  @trump_suit
end

#visible_handsObject

Returns the value of attribute visible_hands.



15
16
17
# File 'lib/bridge/game.rb', line 15

def visible_hands
  @visible_hands
end

Instance Method Details

#_add_result(board, contract = nil, tricks_made = nil, opts = {}) ⇒ Object



348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
# File 'lib/bridge/game.rb', line 348

def _add_result(board, contract=nil, tricks_made=nil, opts = {})
  if self.rubber_mode
    result = RubberResult.new(board, contract, tricks_made, opts)
    if self.rubbers.size > 0 and self.rubbers[-1].winner.nil?
      rubber = self.rubbers[-1]
    else   # Instantiate new rubber.
      rubber = Rubber()
      self.rubbers << rubber
      rubber << result
    end
  elsif self.leonardo_mode
    result = LeonardoResult.new(board, contract, tricks_made, opts)
  else
    result = DuplicateResult.new(board, contract, tricks_made, opts)
  end
  
  self.results << result
end

#add_player(position) ⇒ Object

Raises:

  • (TypeError)


166
167
168
169
170
171
172
173
174
# File 'lib/bridge/game.rb', line 166

def add_player(position)
  raise TypeError, "Expected valid Direction, got #{position}" unless Direction[position]
  raise GameError, "Position #{position} is taken" if self.players.values.include?(position)
  
  player = Player.new(self)
  self.players[player] = position

  return player
end

#claim(direction, tricks) ⇒ Object



406
407
408
409
410
411
412
413
414
415
416
417
418
# File 'lib/bridge/game.rb', line 406

def claim direction, tricks
  if self.in_progress?
    if self.auction.complete?  # In trick play.
      self.state = :finished
      declarer_tricks, defender_tricks = self.play.get_trick_count
      _add_result(self.board, self.contract, declarer_tricks, claim: [direction, tricks, defender_tricks])
    else  # Currently in the auction.
      raise GameError, "Cannot claim during auction"
    end
  else  # Not in game.
    raise GameError, "No game in progress"
  end
end

#get_hand(position) ⇒ Object

If specified hand is visible, returns the list of cards in hand. @return: the hand of player at position.

Raises:

  • (TypeError)


384
385
386
387
388
389
390
391
392
# File 'lib/bridge/game.rb', line 384

def get_hand(position)
  raise TypeError, "Expected Direction, got #{position}" unless Direction[position]

  if self.board and self.board.deal.hands[position]
    self.board.deal.hands[position]
  else
    raise GameError, "Hand unknown"
  end
end

#get_stateObject



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/bridge/game.rb', line 135

def get_state
  state = {}
  
  state[:options] = self.options
  state[:results] = self.results
  state[:state] = self.state
  state[:contract] = self.contract
  state[:calls] = Call.all
  state[:available_calls] = []
  begin
    state[:turn] = self.get_turn
  rescue Exception => e
    state[:turn] = nil
  end
  
  if self.in_progress?
    if state[:state] == :auction
      state[:available_calls] = Call.all.select { |c| self.auction.valid_call?(c) }.compact
    end
    # Remove hidden hands from deal.
    visible_board = self.board.copy
    visible_board.deal = self.visible_hands
    state[:board] = visible_board
  end
  
  state[:auction] = self.auction.to_a unless self.auction.nil?
  state[:play] = self.play.to_a unless self.play.nil?
  
  state
end

#get_turnObject



394
395
396
397
398
399
400
401
402
403
404
# File 'lib/bridge/game.rb', line 394

def get_turn
  if self.in_progress?
    if self.auction.complete?  # In trick play.
      self.play.whose_turn
    else  # Currently in the auction.
      self.auction.whose_turn
    end
  else  # Not in game.
    raise GameError, "No game in progress"
  end
end

#in_auction?Boolean



113
114
115
116
117
118
119
# File 'lib/bridge/game.rb', line 113

def in_auction?
  if !self.auction.nil?
    !self.auction.passed_out?
  else
    false
  end
end

#in_play?Boolean



105
106
107
108
109
110
111
# File 'lib/bridge/game.rb', line 105

def in_play?
  if !self.play.nil?
    !self.play.complete?
  else
    false
  end
end

#in_progress?Boolean



121
122
123
124
125
126
127
128
129
# File 'lib/bridge/game.rb', line 121

def in_progress?
  if !self.play.nil?
    !self.play.complete?
  elsif !self.auction.nil?
    !self.auction.passed_out?
  else
    false
  end
end

#make_call(call, player = nil, position = nil) ⇒ Object

Make a call in the current auction. This method expects to receive either a player argument or a position. If both are given, the position argument is disregarded.

Raises:

  • (TypeError)


232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
# File 'lib/bridge/game.rb', line 232

def make_call(call, player=nil, position=nil)
  raise TypeError, "Expected Call, got #{call}" unless [Bid, Pass, Double, Redouble].include?(call.class)
    
  if player
    raise GameError, "Player unknown to this game" unless self.players.include?(player)
    position = self.players[player]
  end
  
  raise TypeError, "Expected Direction, got #{position.class}" if position.nil? or Direction[position].nil?
  
  # Validate call according to game state.
  raise GameError, "No game in progress" if self.auction.nil?
  raise GameError, "Auction complete" if self.auction.complete?
  raise GameError, "Call made out of turn" if self.get_turn != position
  raise GameError, "Call cannot be made" unless self.auction.valid_call?(call, position)

  self.auction.make_call(call)
  
  if self.auction.complete? and !self.auction.passed_out?
    self.state = :playing
    self.contract = self.auction.get_contract
    trump_suit = @trump_map[self.contract[:bid].strain]
    self.play = TrickPlay.new(self.contract[:declarer], trump_suit)
  elsif self.auction.passed_out?
    self.state = :finished
  end
  
  # If bidding is passed out, game is complete.
  self._add_result(self.board, contract=nil) if not self.in_progress? and self.board.deal

  if !self.in_progress? and self.board.deal
    # Reveal all unrevealed hands.
    Direction.each do |position|
      hand = self.board.deal.hands[position]
      self.reveal_hand(hand, position) if hand and !self.visible_hands.include?(position)
    end
  end
end

#next_game_ready?Boolean



131
132
133
# File 'lib/bridge/game.rb', line 131

def next_game_ready?
  !self.in_progress? and self.players.size == 4
end

#play_card(card, player = nil, position = nil) ⇒ Object

Play a card in the current play session. This method expects to receive either a player argument or a position. If both are given, the position argument is disregarded. If position is specified, it must be that of the player of the card: declarer plays cards from dummy’s hand when it is dummy’s turn.

Raises:

  • (TypeError)


286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
# File 'lib/bridge/game.rb', line 286

def play_card(card, player=nil, position=nil)
  Bridge.assert_card(card)
    
  if player
    raise GameError, "Invalid player reference" unless self.players.include?(player)
    position = self.players[player]
  end
  
  raise TypeError, "Expected Direction, got #{position}" unless Direction[position]
  raise GameError, "No game in progress, or play complete" if self.play.nil? or self.play.complete?

  playfrom = position

  # Declarer controls dummy's turn.
  if self.get_turn == self.play.dummy
    if self.play.declarer == position
      playfrom = self.play.dummy  # Declarer can play from dummy.
    elsif self.play.dummy == position
      raise GameError, "Dummy cannot play hand"
    end
  end
  
  raise GameError, "Card played out of turn" if self.get_turn != playfrom
  
  hand = self.board.deal[playfrom] || []
  # If complete deal known, validate card play.
  if self.board.deal.size == Direction.size
    unless self.play.valid_play?(card, position, hand)
      raise GameError, "Card #{card} cannot be played from hand"
    end
  end
  
  self.play.play_card(card)
  hand.delete(card)
  
  # Dummy's hand is revealed when the first card of first trick is played.
  if self.play.get_trick(0).cards.compact.size == 1
    dummyhand = self.board.deal[self.play.dummy]
    # Reveal hand only if known.
    self.reveal_hand(dummyhand, self.play.dummy) if dummyhand
  end

  # If play is complete, game is complete.
  if !self.in_progress? and self.board.deal
    self.state = :finished
    tricks_made, _ = self.play.get_trick_count
    self._add_result(self.board, self.contract, tricks_made)
  end

  if !self.in_progress? and self.board.deal
    # Reveal all unrevealed hands.
    Direction.each do |position|
      hand = self.board.deal[position]
      if hand and !self.visible_hands.include?(position)
        self.reveal_hand(hand, position)
      end
    end
  end
  
  true
end

#remove_player(position) ⇒ Object

Raises:

  • (TypeError)


176
177
178
179
180
181
# File 'lib/bridge/game.rb', line 176

def remove_player(position)
  raise TypeError, "Expected valid Direction, got #{position}" unless Direction[position]
  raise GameError, "Position #{position} is vacant" unless self.players.values.include?(position)
  
  self.players.reject! { |player,pos| pos == position }
end

#reveal_hand(hand, position) ⇒ Object

Reveal hand to all observers.

Raises:

  • (TypeError)


372
373
374
375
376
377
378
# File 'lib/bridge/game.rb', line 372

def reveal_hand(hand, position)
  raise TypeError, "Expected Direction, got #{position}" unless Direction[position]
  
  self.visible_hands[position] = hand
  # Add hand to board only if it was previously unknown.
  self.board.deal.hands[position] = hand unless self.board.deal.hands[position]
end

#signal_alert(alert, position) ⇒ Object



271
272
273
# File 'lib/bridge/game.rb', line 271

def signal_alert(alert, position)
  pass  # TODO
end

#start!(board = nil) ⇒ Object

Raises:



58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/bridge/game.rb', line 58

def start! board = nil
  raise GameError, "Game in progress" if self.in_progress?

  if board  # Use specified board.
    self.board = board
  elsif !self.board_queue.empty?  # Use pre-specified board.
    self.board = self.board_queue.pop
  elsif self.board  # Advance to next round.
    self.board = self.board.next
  else  # Create an initial board.
    self.board = Board.first
  end
  
  if self.rubber_mode
    # Vulnerability determined by number of games won by each pair.
    if self.rubbers.size == 0 or self.rubbers.last.winner
      self.board.vulnerability = Vulnerability.none  # First round, new rubber.
    else
      pairs = self.rubbers.last.games.map { |game, pair| pair }
      
      if pairs.count([Direction.north, Direction.south]) > 0
        if pairs.count([Direction.east, Direction.west]) > 0
          self.board.vulnerability = Vulnerability.all
        else
          self.board.vulnerability = Vulnerability.north_south
        end
      else
        if pairs.count([Direction.east, Direction.west]) > 0
          self.board.vulnerability = Vulnerability.east_west
        else
          self.board.vulnerability = Vulnerability.none
        end
      end
    end # if self.rubbers.size == 0 or self.rubbers[-1].winner
  end # if self.rubber_mode
  self.auction = Auction.new(self.board.dealer)  # Start auction.
  self.play = nil
  self.visible_hands.clear

  # Remove deal from board, so it does not appear to clients.
  visible_board = self.board.copy
  visible_board.deal = self.visible_hands
  
  self.state = :auction
  true
end

#undo!Object

Send undo message to either auction or trick play this is only available while there is an auction or trick_play



185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/bridge/game.rb', line 185

def undo!
  case self.state
  when :auction
    if self.auction.complete?
      false # can't undo if auction is complete yo.
    else
      card = self.auction.calls.pop # remove the last undo
      if card
        true
      else
        false
      end
    end
  when :playing
    # remove the last card from everywhere
    card = self.play.history.pop
    if card
      trick = self.play.get_current_trick
      player = self.play.who_played?(card)
      # this was a completed trick, we need to remove it from the winner queue
      if trick.cards.compact.size == 4
        winner = self.play.who_played?(self.play.winning_card(trick))
        self.play.winners.pop if self.play.winners.last == winner
      end
      self.play.get_current_trick.cards.delete(card)
      self.play.played.each { |k,h| h.delete(card) }
      self.board.deal.hands[player] << card
      true
    else
      false          
    end
  else
    false
  end
end