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/board.rb
ADDED
@@ -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
|
data/lib/bridge/call.rb
ADDED
@@ -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
|
data/lib/bridge/card.rb
ADDED
@@ -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
|
data/lib/bridge/deal.rb
ADDED
@@ -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
|
data/lib/bridge/deck.rb
ADDED
@@ -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
|
data/lib/bridge/game.rb
ADDED
@@ -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
|