ace_of_spades 0.0.1.pre
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +18 -0
- data/.rbenv-version +1 -0
- data/.rspec +2 -0
- data/Gemfile +3 -0
- data/Guardfile +10 -0
- data/LICENSE.txt +22 -0
- data/README.md +188 -0
- data/Rakefile +1 -0
- data/ace_of_spades.gemspec +20 -0
- data/lib/ace_of_spades.rb +8 -0
- data/lib/ace_of_spades/card.rb +88 -0
- data/lib/ace_of_spades/deck.rb +95 -0
- data/lib/ace_of_spades/suit.rb +45 -0
- data/lib/ace_of_spades/value.rb +58 -0
- data/spec/lib/card_spec.rb +294 -0
- data/spec/lib/deck_spec.rb +291 -0
- data/spec/lib/suit_spec.rb +196 -0
- data/spec/lib/value_spec.rb +234 -0
- data/spec/spec_helper.rb +14 -0
- metadata +117 -0
data/.gitignore
ADDED
data/.rbenv-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.9.3-p327
|
data/.rspec
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
guard 'rspec' do
|
5
|
+
watch(%r{^spec/lib/.+_spec\.rb$})
|
6
|
+
watch(%r{^lib/ace_of_spades/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
|
7
|
+
watch('lib/ace_of_spades.rb') { "spec" }
|
8
|
+
watch('spec/spec_helper.rb') { "spec" }
|
9
|
+
end
|
10
|
+
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Dom Kiva-Meyer
|
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,188 @@
|
|
1
|
+
# Ace Of Spades
|
2
|
+
|
3
|
+
A simple and flexible playing cards library.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'ace_of_spades'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install ace_of_spades
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
require "ace_of_spades"
|
22
|
+
include AceOfSpades
|
23
|
+
|
24
|
+
### Deck
|
25
|
+
|
26
|
+
Deck accepts an optional hash for initialization. Defaults are: `{aces_high: true, shuffle: true, jokers: false}`
|
27
|
+
|
28
|
+
deck = Deck.new(shuffle: false, jokers: true)
|
29
|
+
=> [Two of Clubs, Three of Clubs, Four of Clubs, Five of Clubs, . . . Jack of Spades, Queen of Spades, King of Spades, Ace of Spades, Joker, Joker]
|
30
|
+
|
31
|
+
Deck also accepts `false` to initialize without initializing cards.
|
32
|
+
|
33
|
+
deck = Deck.new(false)
|
34
|
+
=> []
|
35
|
+
|
36
|
+
Cards can be initialized later with `#initialize_cards`
|
37
|
+
|
38
|
+
deck.initialize_cards(shuffle: false, jokers: true)
|
39
|
+
=> [Two of Clubs, Three of Clubs, Four of Clubs, Five of Clubs, . . . Jack of Spades, Queen of Spades, King of Spades, Ace of Spades, Joker, Joker]
|
40
|
+
|
41
|
+
Deck is array-like and can be manipulated like an array. All Array and Enumerable methods are available.
|
42
|
+
|
43
|
+
deck.first
|
44
|
+
=> Two of Clubs
|
45
|
+
|
46
|
+
Ace ranking can be retrieved and set with `#aces_high?` and `#aces_high=`
|
47
|
+
|
48
|
+
deck.aces_high?
|
49
|
+
=> true
|
50
|
+
deck.aces_high = false
|
51
|
+
=> false
|
52
|
+
deck.aces_high?
|
53
|
+
=> false
|
54
|
+
|
55
|
+
Cards can be added with `#add_card`
|
56
|
+
|
57
|
+
deck.add_card(:ace, :spades)
|
58
|
+
deck.last
|
59
|
+
=> Ace of Spades
|
60
|
+
|
61
|
+
Any card-related methods on Deck accept valid arguments for Card.new (outlined below).
|
62
|
+
|
63
|
+
Specific cards can be found with `#find_cards`. It can accept arguments and/or a block. It will return all cards that match the arguments or for which the block evaluates to true.
|
64
|
+
|
65
|
+
deck.find_cards(:ace) { |card| card.joker? }
|
66
|
+
=>[Ace of Clubs, Ace of Diamonds, Ace of Hearts, Ace of Spades, Joker, Joker]
|
67
|
+
|
68
|
+
Similarly, `#find_card` will return the first card that the arguments match or for which the block evaluates to true.
|
69
|
+
|
70
|
+
deck.find_card(:spade) { |card| card.ace? }
|
71
|
+
=> Ace of Clubs
|
72
|
+
|
73
|
+
`Ace of Clubs` was returned because the block evaluated to true. If no matching cards are found, `#find_cards` will return `nil`.
|
74
|
+
|
75
|
+
`#remove_cards` works just like `#find_cards`, but will remove all matching cards from the deck.
|
76
|
+
|
77
|
+
deck.jokers?
|
78
|
+
=> true
|
79
|
+
deck.remove_cards(:joker)
|
80
|
+
=> [Joker, Joker]
|
81
|
+
deck.jokers?
|
82
|
+
=> false
|
83
|
+
|
84
|
+
`#deal` will remove the specified number of cards from the deck, store them separately, and return them. The default argument is 1.
|
85
|
+
|
86
|
+
deck.deal
|
87
|
+
=> Two of Clubs
|
88
|
+
deck.count
|
89
|
+
=> 51
|
90
|
+
deck.deal(9)
|
91
|
+
=> [Three of Clubs, Four of Clubs, Five of Clubs, Six of Clubs, Seven of Clubs, Eight of Clubs, Nine of Clubs, Ten of Clubs, Jack of Clubs]
|
92
|
+
deck.count
|
93
|
+
=> 42
|
94
|
+
|
95
|
+
`#shuffle`, `#shuffle!`, `#unshuffle`, `#unshuffle!`, do exactly what you'd think. The first two shuffle the deck and the last two unshuffle it (sorted from lowest to highest card). The bang versions are destructive.
|
96
|
+
|
97
|
+
`#valid?` returns true if the deck consists of 13 different cards from each suit and 0 or 2 jokers.
|
98
|
+
|
99
|
+
deck.valid?
|
100
|
+
=> true
|
101
|
+
deck.remove_cards(:ace, :spades)
|
102
|
+
deck.valid?
|
103
|
+
=> false
|
104
|
+
|
105
|
+
### Card
|
106
|
+
|
107
|
+
Card accepts a value and a suit for initialization. The value can be an integer (1-14), a face card name, or the first letter of a face card name. The latter two can be symbols or strings and are case insensitive. The suit can be the plural or singular name of a suit or the first letter of the name of the suit. Again, symbols or strings are accepted and are case insensitive. Example: `Card.new` with `(:ace, :spades)`, `("ACE", :spades)`, `(:ace, "Spade")`, `(1, :spades)`, `(14, :spades)`, `(:A, :S)`, `("a", :spade)`, etc., would all produce the same card.
|
108
|
+
|
109
|
+
card = Card.new(:ace, :spades)
|
110
|
+
=> Ace of Spades
|
111
|
+
|
112
|
+
A joker is a card with neither a value or suit and can be initialized by passing in `joker`.
|
113
|
+
|
114
|
+
joker = Card.new(:joker)
|
115
|
+
=> Joker
|
116
|
+
|
117
|
+
`.wrap` will return the same instance of Card that is passed in if a Card instance is passed in, a new Card instance if valid arguments are passed in, or nil if invalid arguments are passed.
|
118
|
+
|
119
|
+
Card.wrap(card)
|
120
|
+
=> Ace of Spades
|
121
|
+
Card.wrap('ace', 'spade')
|
122
|
+
=> Ace of Spades
|
123
|
+
Card.wrap('foo')
|
124
|
+
=> nil
|
125
|
+
|
126
|
+
Both suit and value have readers and writers defined. Setting a suit or value will first wrap the argument in the proper class.
|
127
|
+
|
128
|
+
card.value
|
129
|
+
=> Ace
|
130
|
+
card.value = 5
|
131
|
+
card.value
|
132
|
+
=> Five
|
133
|
+
|
134
|
+
Cards can be compared.
|
135
|
+
|
136
|
+
ace = Card.new(:ace, :spades)
|
137
|
+
king = Card.new(:king, :spades)
|
138
|
+
ace > king
|
139
|
+
=> true
|
140
|
+
|
141
|
+
Cards are compared by suit first and, in the event of a tie, by value. Aces respect the `aces_high` boolean value of the parent deck.
|
142
|
+
|
143
|
+
`#to_s` and `#to_sym` convert a card to a String and Symbol respectively.
|
144
|
+
|
145
|
+
ace.to_s
|
146
|
+
=> "Ace of Spades"
|
147
|
+
ace.to_sym
|
148
|
+
=> :"Ace of Spades"
|
149
|
+
|
150
|
+
Cards respond to valid predicate methods that are dynamically defined based on valid values and suits.
|
151
|
+
|
152
|
+
ace.joker? || ace.heart?
|
153
|
+
=> false
|
154
|
+
ace.ace? && ace.spade? && ace.spades? && ace.ace_of_spades? && ace.a? && ace.s?
|
155
|
+
=> true
|
156
|
+
ace.foo?
|
157
|
+
=> NoMethodError: undefined method 'foo?'
|
158
|
+
|
159
|
+
`#valid` checks whether a card is valid. A card is considered valid if it has a valid value and suit or if both the value and suit are `nil` (joker).
|
160
|
+
|
161
|
+
ace.valid?
|
162
|
+
=> true
|
163
|
+
ace.suit = :foo
|
164
|
+
ace.valid?
|
165
|
+
=> false
|
166
|
+
|
167
|
+
### Value & Suit
|
168
|
+
|
169
|
+
In general you shouldn't have to touch Value and Suit since Deck and Card handle this for you. However, their interfaces are very similar to Card, if you need to use them directly.
|
170
|
+
|
171
|
+
## Roadmap
|
172
|
+
|
173
|
+
* Abstract Deck functionality into a container class (Cardtainer?) from which Deck, Metadeck (deck of decks), Hand, and Game will inherit
|
174
|
+
* Implement Hand, Metadeck, and Game
|
175
|
+
* Implement customizable logic for joker comparison, Hand comparison, Suit ranking, and Deck validation
|
176
|
+
* Extract shared parsing functionality into a Parser module
|
177
|
+
* Extract shared functionality of Value and Suit into a module
|
178
|
+
* Separate unit and integration tests
|
179
|
+
* Add YARD documentation
|
180
|
+
|
181
|
+
|
182
|
+
## Contributing
|
183
|
+
|
184
|
+
1. Fork it
|
185
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
186
|
+
3. Commit your changes and tests (`git commit -am 'Add some feature'`)
|
187
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
188
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |gem|
|
4
|
+
gem.name = "ace_of_spades"
|
5
|
+
gem.version = "0.0.1.pre"
|
6
|
+
gem.authors = ["Dom Kiva-Meyer"]
|
7
|
+
gem.email = ["hello@domkm.com"]
|
8
|
+
gem.description = "A simple and flexible playing cards library."
|
9
|
+
gem.summary = "A simple and flexible playing cards library."
|
10
|
+
gem.homepage = "https://github.com/domkm/ace_of_spades"
|
11
|
+
|
12
|
+
gem.files = `git ls-files`.split($/)
|
13
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
14
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
|
17
|
+
gem.add_development_dependency "rspec", "~> 2.11.0"
|
18
|
+
gem.add_development_dependency "guard", "~> 1.5.3"
|
19
|
+
gem.add_development_dependency "guard-rspec", "~> 2.1.1"
|
20
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
class AceOfSpades::Card
|
2
|
+
include Comparable
|
3
|
+
attr_reader :value, :suit
|
4
|
+
attr_accessor :deck
|
5
|
+
|
6
|
+
def initialize(*args)
|
7
|
+
first, second = args[0] || false, args[1] || false
|
8
|
+
@value, @suit = Value.wrap(first), Suit.wrap(second)
|
9
|
+
@value, @suit = first, second if @value.nil? && @suit.nil?
|
10
|
+
@value = @suit = nil if args.count == 1 && /\Ajoker\z/i === first
|
11
|
+
@value.card = @suit.card = self unless joker?
|
12
|
+
validate!
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.wrap(*args)
|
16
|
+
return args.first if args.first.instance_of?(Card)
|
17
|
+
Card.new(*args)
|
18
|
+
rescue
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
|
22
|
+
def value=(v)
|
23
|
+
@value = Value.wrap(v)
|
24
|
+
end
|
25
|
+
|
26
|
+
def suit=(s)
|
27
|
+
@suit = Suit.wrap(s)
|
28
|
+
end
|
29
|
+
|
30
|
+
def <=>(card)
|
31
|
+
return nil if !card.instance_of?(Card) || card.joker?
|
32
|
+
suit_comparison = suit <=> card.suit
|
33
|
+
return suit_comparison unless suit_comparison == 0
|
34
|
+
value <=> card.value
|
35
|
+
end
|
36
|
+
|
37
|
+
def aces_high?
|
38
|
+
return true if deck.nil?
|
39
|
+
deck.aces_high?
|
40
|
+
end
|
41
|
+
|
42
|
+
def joker?
|
43
|
+
value.nil? && suit.nil?
|
44
|
+
end
|
45
|
+
|
46
|
+
def to_s
|
47
|
+
return 'Joker' if joker?
|
48
|
+
"#{value.to_s} of #{suit.to_s}"
|
49
|
+
end
|
50
|
+
|
51
|
+
def to_sym
|
52
|
+
to_s.to_sym
|
53
|
+
end
|
54
|
+
|
55
|
+
def valid?
|
56
|
+
return true if joker?
|
57
|
+
value.instance_of?(Value) && suit.instance_of?(Suit)
|
58
|
+
end
|
59
|
+
|
60
|
+
def validate!
|
61
|
+
valid? ? self : invalidate!
|
62
|
+
end
|
63
|
+
|
64
|
+
def method_missing(method, *args, &block)
|
65
|
+
return super unless match = %r(.+\?\z).match(method).to_s.chop.split('_')
|
66
|
+
v, s = match.first, match.last
|
67
|
+
valuable, suitable = Value.valuable?(v), Suit.suitable?(s)
|
68
|
+
if joker? && (suitable || valuable)
|
69
|
+
false
|
70
|
+
elsif valuable && suitable
|
71
|
+
value.send("#{v}?") && suit.send("#{s}?")
|
72
|
+
elsif match.count >= 2
|
73
|
+
super
|
74
|
+
elsif valuable
|
75
|
+
value.send("#{v}?")
|
76
|
+
elsif suitable
|
77
|
+
suit.send("#{s}?")
|
78
|
+
else
|
79
|
+
super
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def invalidate!
|
86
|
+
raise "Invalid Card: @value = #{value}, @suit = #{suit}"
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
class AceOfSpades::Deck < Array
|
2
|
+
attr_reader :dealt
|
3
|
+
attr_writer :aces_high
|
4
|
+
|
5
|
+
def initialize(opts = {})
|
6
|
+
super()
|
7
|
+
@dealt, @aces_high = [], true
|
8
|
+
return self unless opts
|
9
|
+
opts = {shuffle: true, jokers: false, aces_high: true, jokers: false}.merge(opts)
|
10
|
+
@aces_high = opts[:aces_high]
|
11
|
+
initialize_cards(opts)
|
12
|
+
end
|
13
|
+
|
14
|
+
def find_card(*args, &block)
|
15
|
+
find_cards(*args, &block).first
|
16
|
+
end
|
17
|
+
|
18
|
+
def find_cards(*args, &block)
|
19
|
+
args.map! { |arg| Value.parse(arg) || Suit.parse(arg) || (:joker if %r(\Ajoker\z)i === arg) }.compact!
|
20
|
+
cards.each_with_object([]) do |card, arr|
|
21
|
+
arr.push(card) if ( !args.empty? && args.all? { |arg| card.send("#{arg}?") } ) || ( block.call(card) if block_given? )
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def add_card(*args)
|
26
|
+
card = Card.new(*args)
|
27
|
+
card.deck = self
|
28
|
+
push(card)
|
29
|
+
end
|
30
|
+
|
31
|
+
def remove_cards(*args, &block)
|
32
|
+
to_be_removed = find_cards(*args, &block)
|
33
|
+
delete_if { |card| to_be_removed.include?(card) }
|
34
|
+
@dealt.delete_if { |card| to_be_removed.include?(card) }
|
35
|
+
to_be_removed
|
36
|
+
end
|
37
|
+
|
38
|
+
def aces_high?
|
39
|
+
!!@aces_high
|
40
|
+
end
|
41
|
+
|
42
|
+
def jokers?
|
43
|
+
cards.any? { |card| card.joker? }
|
44
|
+
end
|
45
|
+
|
46
|
+
def deal(num = 1)
|
47
|
+
dealt = shift(num)
|
48
|
+
@dealt.concat(dealt)
|
49
|
+
num > 1 ? dealt : dealt.first
|
50
|
+
end
|
51
|
+
|
52
|
+
def shuffle!
|
53
|
+
concat(@dealt)
|
54
|
+
@dealt.clear
|
55
|
+
super
|
56
|
+
end
|
57
|
+
|
58
|
+
def shuffle
|
59
|
+
dup.shuffle!
|
60
|
+
end
|
61
|
+
|
62
|
+
def unshuffle!
|
63
|
+
concat(@dealt)
|
64
|
+
@dealt.clear
|
65
|
+
jokers = remove_cards(:joker)
|
66
|
+
sort!.concat(jokers)
|
67
|
+
end
|
68
|
+
|
69
|
+
def unshuffle
|
70
|
+
dup.unshuffle!
|
71
|
+
end
|
72
|
+
|
73
|
+
def initialize_cards(opts = {})
|
74
|
+
VALUES[1..-1].product(SUITS) { |value, suit| add_card(value, suit) }
|
75
|
+
2.times { add_card(:joker) } if opts[:jokers]
|
76
|
+
opts[:shuffle] ? shuffle! : unshuffle!
|
77
|
+
end
|
78
|
+
|
79
|
+
def valid?
|
80
|
+
deck = unshuffle
|
81
|
+
return true if deck.remove_cards(:joker).count == 2 && deck.valid?
|
82
|
+
deck.reduce do |memo, card|
|
83
|
+
return false unless memo < card
|
84
|
+
card
|
85
|
+
end
|
86
|
+
return false unless deck.count == 52
|
87
|
+
true
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def cards
|
93
|
+
self + @dealt
|
94
|
+
end
|
95
|
+
end
|