leonardo-bridge 0.4.3

Sign up to get free protection for your applications and to get access to all the features.
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