Class: Bridge::Game

Inherits:
RedisModel 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

Attributes inherited from RedisModel

#id

Instance Method Summary collapse

Methods inherited from RedisModel

#<=>, all, #destroy!, exists?, expire_in, #expires_in, load, #namespaced_id, namespaced_id, #persist, #persist!, #reload, #to_array, #to_hash, use_timestamps, #use_timestamps?

Constructor Details

#initialize(opts = {}) ⇒ Game

Returns a new instance of Game.



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
55
56
57
58
# File 'lib/bridge/game.rb', line 23

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.



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

def auction
  @auction
end

#boardObject

Returns the value of attribute board.



19
20
21
# File 'lib/bridge/game.rb', line 19

def board
  @board
end

#board_queueObject

Returns the value of attribute board_queue.



19
20
21
# File 'lib/bridge/game.rb', line 19

def board_queue
  @board_queue
end

#contractObject

Returns the value of attribute contract.



20
21
22
# File 'lib/bridge/game.rb', line 20

def contract
  @contract
end

#leonardo_modeObject

Returns the value of attribute leonardo_mode.



21
22
23
# File 'lib/bridge/game.rb', line 21

def leonardo_mode
  @leonardo_mode
end

#numberObject

Returns the value of attribute number.



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

def number
  @number
end

#optionsObject

Returns the value of attribute options.



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

def options
  @options
end

#playObject

Returns the value of attribute play.



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

def play
  @play
end

#playersObject

Returns the value of attribute players.



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

def players
  @players
end

#resultObject

Returns the value of attribute result.



20
21
22
# File 'lib/bridge/game.rb', line 20

def result
  @result
end

#resultsObject

Returns the value of attribute results.



19
20
21
# File 'lib/bridge/game.rb', line 19

def results
  @results
end

#rubber_modeObject

Returns the value of attribute rubber_mode.



21
22
23
# File 'lib/bridge/game.rb', line 21

def rubber_mode
  @rubber_mode
end

#rubbersObject

Returns the value of attribute rubbers.



21
22
23
# File 'lib/bridge/game.rb', line 21

def rubbers
  @rubbers
end

#stateObject

Returns the value of attribute state.



20
21
22
# File 'lib/bridge/game.rb', line 20

def state
  @state
end

#trump_suitObject

Returns the value of attribute trump_suit.



20
21
22
# File 'lib/bridge/game.rb', line 20

def trump_suit
  @trump_suit
end

#visible_handsObject

Returns the value of attribute visible_hands.



19
20
21
# File 'lib/bridge/game.rb', line 19

def visible_hands
  @visible_hands
end

Instance Method Details

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



300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
# File 'lib/bridge/game.rb', line 300

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)


155
156
157
158
159
160
161
162
163
# File 'lib/bridge/game.rb', line 155

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



358
359
360
361
362
363
364
365
366
367
368
369
370
# File 'lib/bridge/game.rb', line 358

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.

Parameters:

  • position:

    the position of the requested hand.

Raises:

  • (TypeError)


336
337
338
339
340
341
342
343
344
# File 'lib/bridge/game.rb', line 336

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



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/bridge/game.rb', line 124

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



346
347
348
349
350
351
352
353
354
355
356
# File 'lib/bridge/game.rb', line 346

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_progress?Boolean

Returns:

  • (Boolean)


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

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.

Parameters:

  • call:

    a Call object.

  • player:

    if specified, a player object.

  • position:

    if specified, the position of the player making call.

Raises:

  • (TypeError)


184
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
220
221
# File 'lib/bridge/game.rb', line 184

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

Returns:

  • (Boolean)


120
121
122
# File 'lib/bridge/game.rb', line 120

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.

Parameters:

  • card:

    a Card object.

  • player:

    if specified, a player object.

  • position:

    if specified, the position of the player of the card.

Raises:

  • (TypeError)


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
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
# File 'lib/bridge/game.rb', line 238

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)


165
166
167
168
169
170
# File 'lib/bridge/game.rb', line 165

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.

Parameters:

  • hand:

    a hand of Card objects.

  • position:

    the position of the hand.

Raises:

  • (TypeError)


324
325
326
327
328
329
330
# File 'lib/bridge/game.rb', line 324

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



223
224
225
# File 'lib/bridge/game.rb', line 223

def signal_alert(alert, position)
  pass  # TODO
end

#start!(board = nil) ⇒ Object

Raises:



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
104
105
106
107
# File 'lib/bridge/game.rb', line 62

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