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
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ OWRjMmJjOGYwNTVlZmU3OGQ3ZDVkNTlhODg5NWVmOWZhMjllMGE3MA==
5
+ data.tar.gz: !binary |-
6
+ OGI1YTBkY2QwZjI0NjcwMDIyMzYxMDE1NzQzZDRkOTYzYTdmYTU5Ng==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ OGRmMDkzZjMzNGVjYjMxY2E4OTFlNTBjNzMwMzY1YmRhYjU0ZDQ3YTFmZTE2
10
+ ZDNjOWMzYzA0ZWY0NjY5MDM0NWZmODllZjM0NmU5NzEwZGI3MjhjMjBkYWQy
11
+ YmZhYjdmZWQ0NzdhZmZkNjlhMGM3MTE4MzZmM2FiOTk2YWQ2MmE=
12
+ data.tar.gz: !binary |-
13
+ OGY2OGRiOGIzYTJkMzg5MGZhNzcxODdjMWRhMGQwYmU5NTYyMWVkYTU3NjIz
14
+ MjhlNDU5NjRiZTk5ZjcyNzA1YzE5ODZiZjcwN2RjMTEzYmM1MDEwM2YwZmMz
15
+ NjgzNjM3MzcyNTU2OGVmY2Y3OGI1NjQzMjRlODE2MjFjZGE5MjI=
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .rvmrc
4
+ .bundle
5
+ .config
6
+ .yardoc
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in bridge.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Achilles Charmpilas
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # Bridge
2
+
3
+ A lean mean bridge playing machine.
4
+ Large portions of this gem have been ported from [pybridge](http://sourceforge.net/projects/pybridge/)
5
+
6
+ Bridge is the bridge playing engine sitting behind the Leonardo Bridge API.
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ #!ruby
13
+ gem 'bridge'
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install bridge
22
+
23
+ ## Usage
24
+
25
+ You can have a look at `bin/play` for an example of interaction with the Bridge::Game class.
26
+
27
+ Here's a quick run-through:
28
+
29
+ #!ruby
30
+ require 'rubygems'
31
+ require 'bridge'
32
+ include Bridge
33
+
34
+ game = Game.new # start game
35
+ players = [] # keep players somewhere handy
36
+ Direction.each { |d| players[d] << game.add_player(d) }
37
+ # you're ready to start playing
38
+
39
+
40
+ ## Contributing
41
+
42
+ 1. Fork it
43
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
44
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
45
+ 4. Push to the branch (`git push origin my-new-feature`)
46
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/bin/leo-console ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ libs = ['-rubygems']
3
+ libs << '-r irb/completion'
4
+ libs << '-r pp'
5
+ libs << '-r ./lib/bridge'
6
+ libs << '-r ./bridge.rc.rb'
7
+ exec "irb #{libs.join(' ')}"
data/bin/leo-play ADDED
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env ruby
2
+ require './lib/bridge'
3
+
4
+ [:INT, :TERM].each do |sig|
5
+ trap(sig) {
6
+ puts clear_line
7
+ puts 'Shutting down, bye!'.yellow
8
+ exit
9
+ }
10
+ end
11
+
12
+ def cursor
13
+ '> '
14
+ end
15
+
16
+ def put string, prompt = '> '
17
+ puts cursor + string
18
+ end
19
+
20
+ def put_contract c
21
+ if c
22
+ #puts c.inspect.red
23
+ print "#{Direction.name(c.declarer)} won auction with ".yellow
24
+ print "#{Level.name(c.bid.level)} #{Strain.name(c.bid.strain)}".yellow
25
+ print "double by #{Direction.name(c.double_by)}".yellow unless c.double_by.nil?
26
+ print "redouble by #{Direction.name(c.redouble_by)}".yellow unless c.redouble_by.nil?
27
+ puts ""
28
+ end
29
+ end
30
+
31
+ include Bridge
32
+ @history = []
33
+ @game = Game.new
34
+ @players = []
35
+ Direction.each do |position|
36
+ @players[position] = @game.add_player(position)
37
+ end
38
+ @game.start!(Board.first)
39
+
40
+ puts clear_screen
41
+ puts 'Started game on first board'.green
42
+ puts "\t#{Direction.name(@game.board.dealer)} deals".white
43
+ puts "\t#{Vulnerability.name(@game.board.vulnerability)} is vulnerable".white
44
+
45
+
46
+ print "#{Direction.name(@game.get_turn).to_s.upcase}> " if @game.in_progress?
47
+
48
+ def rush_auction
49
+ @players[@game.get_turn].make_call(Call.from_string('bid two club'))
50
+ @players[@game.get_turn].make_call(Pass.new)
51
+ @players[@game.get_turn].make_call(Pass.new)
52
+ @players[@game.get_turn].make_call(Pass.new)
53
+
54
+ put_contract @game.auction.contract
55
+ end
56
+
57
+ def rush_trick
58
+ @players[@game.get_turn].play_card(@players[@game.get_turn].get_hand.sample)
59
+
60
+ 3.times do
61
+ if @game.get_turn == @game.play.dummy
62
+ player = @players[@game.play.declarer]
63
+ hand = @players[@game.play.dummy].get_hand
64
+ else
65
+ player = @players[@game.get_turn]
66
+ hand = @players[@game.get_turn].get_hand
67
+ end
68
+
69
+ begin
70
+ player.play_card(hand.sample)
71
+ rescue Bridge::GameError => e
72
+ puts e.message.red
73
+ retry
74
+ end
75
+
76
+ end
77
+ end
78
+
79
+ while cmd = gets.strip
80
+ case cmd
81
+ when 'bye','exit','leave'
82
+ break
83
+ when 'rush trick' # play an automatic trick
84
+ rush_auction unless @game.auction.complete?
85
+ rush_trick
86
+ when 'rush auction' # play a canned auction
87
+ rush_auction
88
+ when 'history'
89
+ put 'Move history:'.yellow
90
+ put @history.join("\n")
91
+
92
+ when 'hand'
93
+ put @game.get_hand(@game.get_turn).inspect.white
94
+ print cursor
95
+
96
+ else
97
+ ary = cmd.split
98
+ if @game.in_progress?
99
+ position_i = @game.get_turn
100
+ position = Direction.name(position_i)
101
+ @history << [position, ary.join(' ')]
102
+
103
+ if @game.auction.complete?
104
+ card = nil
105
+
106
+ begin
107
+ if ary.size == 2 # overriding player
108
+ position = ary.first
109
+ position_i = Direction.send(position.to_sym)
110
+ card = Card.from_string(ary.last)
111
+ else
112
+ card = Card.from_string(ary.first)
113
+ end
114
+
115
+ @players[position_i].play_card(card)
116
+ print "#{position.white} plays #{card.to_s.magenta} ~> "
117
+ puts @game.play.get_current_trick.cards.compact.inspect.white
118
+ rescue Exception => e
119
+ put e.message.red
120
+ end
121
+ else
122
+ begin
123
+ @players[position_i].make_call(Call.from_string(ary.join(' ')))
124
+ put "#{position.white} calls #{ary.join(' ').green}"
125
+ rescue Exception => e
126
+ put e.message.red
127
+ end
128
+
129
+ if @game.auction.passed_out?
130
+ put 'Auction phase complete, @game passed out'.red
131
+ exit
132
+ elsif @game.auction.complete?
133
+ put 'Auction phase complete'.yellow
134
+ put_contract @game.auction.contract
135
+ end
136
+ end
137
+ end # if @game.in_progress?
138
+ end
139
+
140
+ print "#{Direction.name(@game.get_turn).to_s.upcase}> " if @game.in_progress?
141
+ end
142
+
143
+ puts 'Shutting down, bye!'.yellow
data/bridge.gemspec ADDED
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'bridge/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "leonardo-bridge"
8
+ spec.version = Bridge::VERSION
9
+ spec.authors = ["Achilles Charmpilas"]
10
+ spec.email = ["ac@humbuckercode.co.uk"]
11
+ spec.description = %q{A lean mean bridge playing machine}
12
+ spec.summary = %q{Encapsulates all the necessary logic that allows 4 players to play a bridge game. Also supports rubber scoring.}
13
+ spec.homepage = "http://bridge.leonardogames.net/"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+ spec.add_dependency "redis"
21
+ spec.add_dependency "nutrun-string"
22
+ spec.add_development_dependency "bundler", "~> 1.3"
23
+ spec.add_development_dependency "rspec"
24
+ spec.add_development_dependency "fuubar"
25
+ spec.add_development_dependency "geminabox"
26
+ spec.add_development_dependency "gem-release"
27
+ spec.add_development_dependency "rake"
28
+ spec.add_development_dependency "simplecov"
29
+ end
data/bridge.rc.rb ADDED
@@ -0,0 +1,11 @@
1
+ IRB.conf[:PROMPT][:CUSTOM] = {
2
+ :PROMPT_I => "Bridge >> ",
3
+ :PROMPT_S => "%l>> ",
4
+ :PROMPT_C => ">>",
5
+ :PROMPT_N => ">>",
6
+ :RETURN => "=> %s\n"
7
+ }
8
+ IRB.conf[:PROMPT_MODE] = :CUSTOM
9
+ IRB.conf[:AUTO_INDENT] = true
10
+ include Bridge
11
+ puts ">> I am Bridge Console".white.bg_black
data/lib/bridge.rb ADDED
@@ -0,0 +1,35 @@
1
+ require 'rubygems'
2
+ require 'ostruct'
3
+ require 'pathname'
4
+ require 'nutrun-string'
5
+
6
+ root = File.dirname(__FILE__)
7
+
8
+ %W{ uuid redis_model enum }.each { |r| require File.join(root,r) }
9
+
10
+ module Bridge
11
+ DEBUG = false
12
+
13
+ module Direction
14
+ extend Enum
15
+ set_values :north, :east, :south, :west
16
+ end
17
+
18
+ module Vulnerability
19
+ extend Enum
20
+ set_values :none, :north_south, :east_west, :all
21
+ end
22
+
23
+ def root
24
+ File.dirname(__FILE__)
25
+ end
26
+
27
+ def assert_card card
28
+ raise CardError, "Card #{card.inspect} is not valid" unless card.is_a?(Card)
29
+ end
30
+
31
+ module_function :assert_card
32
+ public :assert_card
33
+ end
34
+
35
+ Dir[File.join(root,'bridge','*.rb')].each { |r| require r }
@@ -0,0 +1,182 @@
1
+ module Bridge
2
+ class DuplicateCallError < StandardError; end
3
+ class InvalidCallError < StandardError; end
4
+ class InvalidCallClassError < StandardError; end
5
+
6
+ # The auction (bidding phase) of a game of bridge.
7
+ # Swiped from: https://pybridge.svn.sourceforge.net/svnroot/pybridge/trunk/pybridge/pybridge/games/bridge/auction.py
8
+ class Auction
9
+ attr_accessor :dealer, :calls, :contract
10
+
11
+ # @param dealer: who distributes the cards and makes the first call.
12
+ # @type dealer: Direction
13
+ def initialize dealer
14
+ self.dealer = dealer
15
+ self.contract = nil
16
+ self.calls = []
17
+ end
18
+
19
+ # Auction is complete if all players have called (ie. 4 or more calls)
20
+ # and the last 3 calls are Pass calls.
21
+ # @return: True if bidding is complete, False if not.
22
+ # @rtype: bool
23
+ def complete?
24
+ passes = self.calls.last(3).select { |c| c.is_a?(Pass) }.size
25
+ self.calls.size >= 4 and passes == 3
26
+ end
27
+
28
+ # Auction is passed out if each player has passed on their first turn.
29
+ # In this case, the bidding is complete, but no contract is established.
30
+ # @return: True if bidding is passed out, False if not.
31
+ # @rtype: bool
32
+ def passed_out?
33
+ passes = calls.select { |c| c.is_a?(Pass) }.size
34
+ self.calls.size == 4 and passes == 4
35
+ end
36
+
37
+ # When the bidding is complete, the contract is the last and highest
38
+ # bid, which may be doubled or redoubled.
39
+ # Hence, the contract represents the "final state" of the bidding.
40
+ # @return: a dict containing the keywords:
41
+ # @keyword bid: the last and highest bid.
42
+ # @keyword declarer: the partner who first bid the contract strain.
43
+ # @keyword doubleBy: the opponent who doubled the contract, or None.
44
+ # @keyword redoubleBy: the partner who redoubled an opponent's double
45
+ # on the contract, or None.
46
+ def get_contract
47
+ if self.complete? and not self.passed_out?
48
+ bid = self.get_current_call(Bid)
49
+ double = self.get_current_call(Double)
50
+ redouble = self.get_current_call(Redouble)
51
+ declarer_bid = nil
52
+ # Determine partnership.
53
+ caller = self.who_called?(bid)
54
+ partnership = [caller, Direction[(caller + 2) % 4]]
55
+ # Determine declarer.
56
+ self.calls.each do |call|
57
+ if call.is_a?(Bid) and call.strain == bid.strain and partnership.include?(self.who_called?(call))
58
+ declarer_bid = call
59
+ break
60
+ end
61
+ end
62
+
63
+ {
64
+ :bid => bid,
65
+ :declarer => declarer_bid.nil? ? nil : self.who_called?(declarer_bid),
66
+ :double_by => double.nil? ? nil : self.who_called?(double),
67
+ :redouble_by => redouble.nil? ? nil : self.who_called?(redouble)
68
+ }
69
+ else
70
+ nil # Bidding passed out or not complete, no contract.
71
+ end
72
+ end
73
+
74
+ # Appends call from position to the calls list.
75
+ # Please note that call validity should be checked with isValidCall()
76
+ # before calling this method!
77
+ # @param call: a candidate call.
78
+ def make_call call, player = nil
79
+ assert_call(call.class)
80
+ # Calls must be distinct.
81
+ raise InvalidCallError, "#{call.inspect} is invalid" unless self.valid_call?(call)
82
+
83
+ self.calls << call
84
+ if self.complete? and not self.passed_out?
85
+ self.contract = Contract.new(self)
86
+ end
87
+ true
88
+ end
89
+
90
+ # Check that call can be made, according to the rules of bidding.
91
+ # @param call: the candidate call.
92
+ # @param position: if specified, the position from which the call is made.
93
+ # @return: True if call is available, False if not.
94
+ def valid_call? call, position = nil
95
+ # The bidding must not be complete.
96
+ return false if complete?
97
+
98
+ # Position's turn to play.
99
+ return false if position and position != whose_turn
100
+
101
+ # A pass is always available.
102
+ return true if call.is_a?(Pass)
103
+
104
+ # A bid must be greater than the current bid.
105
+ if call.is_a?(Bid)
106
+ return (!current_bid or call > current_bid)
107
+ end
108
+
109
+ # Doubles and redoubles only when a bid has been made.
110
+ if current_bid
111
+ bidder = who_called?(current_bid)
112
+
113
+ # A double must be made on the current bid from opponents,
114
+ # which has not been already doubled by partnership.
115
+ if call.is_a?(Double)
116
+ opposition = [Direction[(whose_turn + 1) % 4], Direction[(whose_turn + 3) % 4]]
117
+ return (opposition.include?(bidder) and !current_double)
118
+
119
+ # A redouble must be made on the current bid from partnership,
120
+ # which has been doubled by an opponent.
121
+ elsif call.is_a?(Redouble)
122
+ partnership = [whose_turn, Direction[(whose_turn + 2) % 4]]
123
+ return (partnership.include?(bidder) and current_double and !current_redouble)
124
+ end
125
+ end
126
+
127
+ false # Otherwise unavailable.
128
+ end
129
+
130
+ # Returns the position from which the specified call was made.
131
+ # @param call: a call made in the auction.
132
+ # @return: the position of the player who made call, or None.
133
+ def who_called? call
134
+ raise ArgumentError, "#{call.inspect} is not a call" unless call.is_a?(Call)
135
+ return nil unless calls.include?(call) # Call not made by any player.
136
+ Direction[(self.dealer + calls.find_index(call)) % 4]
137
+ end
138
+
139
+ # Returns the position from which the next call should be made.
140
+ # @return: the next position to make a call, or None.
141
+ def whose_turn
142
+ return nil if complete?
143
+ return Direction[(self.dealer + calls.size) % 4]
144
+ end
145
+
146
+ # Returns most recent current call of specified class, or None.
147
+ # @param callclass: call class, in (Bid, Pass, Double, Redouble).
148
+ # @return: most recent call matching type, or None.
149
+ def get_current_call callclass
150
+ assert_call(callclass)
151
+
152
+ self.calls.reverse.each do |call|
153
+ if call.is_a?(callclass)
154
+ return call
155
+ elsif call.is_a?(Bid)
156
+ break # Bids cancel all preceding calls.
157
+ end
158
+ end
159
+ nil
160
+ end
161
+
162
+ def current_bid
163
+ get_current_call(Bid)
164
+ end
165
+
166
+ def current_double
167
+ get_current_call(Double)
168
+ end
169
+
170
+ def current_redouble
171
+ get_current_call(Redouble)
172
+ end
173
+
174
+ def assert_call callclass
175
+ raise InvalidCallClassError unless [Bid, Pass, Double, Redouble].include?(callclass)
176
+ end
177
+
178
+ def to_a
179
+ self.calls
180
+ end
181
+ end
182
+ end