fiftytwo 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./has_cards"
4
+
5
+ module FiftyTwo
6
+ class Deck
7
+ include HasCards
8
+
9
+ class << self
10
+ def standard
11
+ build(FiftyTwo::Rank::ALL.product(FiftyTwo::Suit::ALL))
12
+ end
13
+
14
+ private
15
+
16
+ def build(items)
17
+ new.tap do |deck|
18
+ items.each do |rank, suit|
19
+ deck << FiftyTwo::Card.new(deck, rank, suit).freeze
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ def deal(hands, hand_size: 1)
26
+ hand_size.times.each do |card_idx|
27
+ hands.each { |h| h << draw }
28
+ end
29
+
30
+ hands
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./has_cards"
4
+
5
+ module FiftyTwo
6
+ class Hand
7
+ include HasCards
8
+ end
9
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FiftyTwo
4
+ module HasCards
5
+ extend Enumerable
6
+ attr_reader :cards
7
+ delegate :shuffle!, :sort!, :<<, :[], :each, :map, :count, :first, :last, to: :cards
8
+
9
+ CardUnavailableError = Class.new(StandardError)
10
+
11
+ def initialize(cards = [])
12
+ @cards = cards
13
+ end
14
+
15
+ def draw
16
+ cards.shift
17
+ end
18
+
19
+ def render
20
+ map(&:render).join(" ")
21
+ end
22
+
23
+ def locate(identifier)
24
+ cards.find { |c| c.identifier == identifier.upcase }
25
+ end
26
+
27
+ def transfer(card, destination = nil)
28
+ card = locate(card) unless card.is_a?(FiftyTwo::Card)
29
+ raise CardUnavailableError if card.nil?
30
+ raise CardUnavailableError unless cards.include?(card)
31
+ (destination || card.deck) << cards.delete(card)
32
+ end
33
+
34
+ FiftyTwo::Suit::ALL.each do |suit|
35
+ define_method(suit.name) { select(suit.name) }
36
+ end
37
+
38
+ FiftyTwo::Suit::Color::ALL.each do |color|
39
+ define_method(color.name.pluralize) { select(color.name) }
40
+ end
41
+
42
+ [*FiftyTwo::Rank::CATEGORIES, :jack, :king, :queen, :ace].each do |category|
43
+ define_method(category.to_s.pluralize) { select(category) }
44
+ end
45
+
46
+ private
47
+
48
+ def select(identifier)
49
+ self.class.new(cards.select { |c| c.send("#{identifier}?") })
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FiftyTwo
4
+ class Rank
5
+ include Comparable
6
+ CATEGORIES = %i[pip face]
7
+
8
+ attr_reader :value, :name, :category
9
+
10
+ def initialize(value, name = nil, category = :pip)
11
+ @value = value
12
+ @name = name || value.to_s
13
+ @category = category
14
+ end
15
+
16
+ ALL = [
17
+ TWO = Rank.new(2).freeze,
18
+ THREE = Rank.new(3).freeze,
19
+ FOUR = Rank.new(4).freeze,
20
+ FIVE = Rank.new(5).freeze,
21
+ SIX = Rank.new(6).freeze,
22
+ SEVEN = Rank.new(7).freeze,
23
+ EIGHT = Rank.new(8).freeze,
24
+ NINE = Rank.new(9).freeze,
25
+ TEN = Rank.new(10).freeze,
26
+ JACK = Rank.new(11, "jack", :face).freeze,
27
+ QUEEN = Rank.new(12, "queen", :face).freeze,
28
+ KING = Rank.new(13, "king", :face).freeze,
29
+ ACE = Rank.new(14, "ace").freeze
30
+ ]
31
+
32
+ CATEGORIES.each do |category|
33
+ define_method("#{category}?") { self.category == category }
34
+ end
35
+
36
+ ALL.select(&:face?).each do |rank|
37
+ define_method("#{rank.name}?") { self == rank }
38
+ end
39
+
40
+ def ace?
41
+ name == "ace"
42
+ end
43
+
44
+ def <=>(other)
45
+ value <=> other.value
46
+ end
47
+
48
+ def to_s
49
+ name.titleize
50
+ end
51
+
52
+ def identifier
53
+ name.to_i > 0 ? name : name[0].upcase
54
+ end
55
+ alias_method :code, :identifier
56
+ end
57
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./suit/color"
4
+
5
+ module FiftyTwo
6
+ class Suit
7
+ include Comparable
8
+
9
+ attr_reader :name, :color, :symbol
10
+ alias_method :code, :symbol
11
+
12
+ def initialize(name, color, symbol = nil)
13
+ @name = name
14
+ @color = color
15
+ @symbol = symbol
16
+ end
17
+
18
+ ALL = [
19
+ CLUBS = Suit.new("clubs", Color::BLACK, "♣").freeze,
20
+ DIAMONDS = Suit.new("diamonds", Color::RED, "♦").freeze,
21
+ HEARTS = Suit.new("hearts", Color::RED, "♥").freeze,
22
+ SPADES = Suit.new("spades", Color::BLACK, "♠").freeze
23
+ ]
24
+
25
+ Color::ALL.each do |color|
26
+ define_method("#{color.name}?") { self.color == color }
27
+ end
28
+
29
+ ALL.each do |suit|
30
+ define_method("#{suit.name.downcase}?") { self == suit }
31
+ end
32
+
33
+ def <=>(other)
34
+ name <=> other.name
35
+ end
36
+
37
+ def to_s
38
+ name.titleize
39
+ end
40
+
41
+ def identifier
42
+ name[0].upcase
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FiftyTwo
4
+ class Suit
5
+ class Color
6
+ attr_reader :name, :rgb
7
+
8
+ def initialize(name, rgb)
9
+ @name = name
10
+ @rgb = rgb
11
+ end
12
+
13
+ ALL = [
14
+ RED = new("red", "ff0000"),
15
+ BLACK = new("black", "000000")
16
+ ]
17
+
18
+ def ==(other)
19
+ name == other.name
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FiftyTwo
4
+ VERSION = "0.0.1"
5
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FiftyTwo
4
+ describe Card do
5
+ let(:deck) { instance_double(FiftyTwo::Deck) }
6
+ let(:rank) { instance_double(FiftyTwo::Rank, code: "RC", identifier: "RI", to_s: "92") }
7
+ let(:suit) { instance_double(FiftyTwo::Suit, code: "SC", identifier: "SI", to_s: "Mittens") }
8
+ let(:subject) { described_class.new(deck, rank, suit) }
9
+
10
+ it "exposes its basics" do
11
+ expect(subject.deck).to eq deck
12
+ expect(subject.rank).to eq rank
13
+ expect(subject.suit).to eq suit
14
+ end
15
+
16
+ it "has a variety of serializations" do
17
+ expect(subject.code).to eq "RCSC"
18
+ expect(subject.identifier).to eq "RISI"
19
+ expect(subject.to_s).to eq "92 of Mittens"
20
+ end
21
+
22
+ describe "#render" do
23
+ let(:color) { instance_double(FiftyTwo::Suit::Color, name: "blue") }
24
+ before(:each) { allow(suit).to receive(:color).and_return color }
25
+
26
+ it "builds a proper rendering from 2 characters" do
27
+ allow(subject).to receive(:code).and_return "XY"
28
+ expect(subject.render).to eq "\e[0;34;49m XY\e[0m"
29
+ end
30
+
31
+ it "builds a proper rendering from 3 characters" do
32
+ allow(subject).to receive(:code).and_return "XYZ"
33
+ expect(subject.render).to eq "\e[0;34;49mXYZ\e[0m"
34
+ end
35
+ end
36
+
37
+ context "questions" do
38
+ before(:each) do
39
+ allow(rank).to receive(:face?).and_return "grumpy"
40
+ allow(rank).to receive(:pip?).and_return "pip hooray"
41
+ allow(suit).to receive(:red?).and_return "roses"
42
+ allow(suit).to receive(:diamonds?).and_return "drip"
43
+ end
44
+
45
+ it "can answer questions only its rank knows" do
46
+ expect(subject.face?).to eq "grumpy"
47
+ expect(subject.pip?).to eq "pip hooray"
48
+ end
49
+
50
+ it "can answer questions only its suit knows" do
51
+ expect(subject.red?).to eq "roses"
52
+ expect(subject.diamonds?).to eq "drip"
53
+ end
54
+
55
+ it "will not answer silly questions" do
56
+ subject.hungry?
57
+ rescue NoMethodError
58
+ end
59
+ end
60
+
61
+ context "comparable" do
62
+ context "different rank, different suit" do
63
+ it "orders by rank first" do
64
+ card1 = described_class.new(deck, FiftyTwo::Rank::FOUR, FiftyTwo::Suit::CLUBS)
65
+ card2 = described_class.new(deck, FiftyTwo::Rank::TWO, FiftyTwo::Suit::DIAMONDS)
66
+ expect(card2).to be < card1
67
+ end
68
+ end
69
+
70
+ context "different rank, same suit" do
71
+ it "orders by rank first" do
72
+ card1 = described_class.new(deck, FiftyTwo::Rank::FOUR, FiftyTwo::Suit::SPADES)
73
+ card2 = described_class.new(deck, FiftyTwo::Rank::SEVEN, FiftyTwo::Suit::SPADES)
74
+ expect(card2).to be > card1
75
+ end
76
+ end
77
+
78
+ context "same rank, different suit" do
79
+ it "orders by suit" do
80
+ card1 = described_class.new(deck, FiftyTwo::Rank::KING, FiftyTwo::Suit::SPADES)
81
+ card2 = described_class.new(deck, FiftyTwo::Rank::KING, FiftyTwo::Suit::HEARTS)
82
+ expect(card2).to be < card1
83
+ end
84
+ end
85
+
86
+ context "same rank, same suit" do
87
+ it "calls it a tie" do
88
+ card1 = described_class.new(deck, FiftyTwo::Rank::TEN, FiftyTwo::Suit::DIAMONDS)
89
+ card2 = described_class.new(deck, FiftyTwo::Rank::TEN, FiftyTwo::Suit::DIAMONDS)
90
+ expect(card2).to eq card1
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,280 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FiftyTwo
4
+ describe Deck do
5
+ let(:subject) { described_class.standard }
6
+ let(:minideck) { described_class.new }
7
+ before(:each) do
8
+ minideck << FiftyTwo::Card.new(minideck, FiftyTwo::Rank::THREE, FiftyTwo::Suit::CLUBS)
9
+ minideck << FiftyTwo::Card.new(minideck, FiftyTwo::Rank::NINE, FiftyTwo::Suit::DIAMONDS)
10
+ end
11
+
12
+ it "is a deck, made up of cards" do
13
+ expect(subject).to be_a described_class
14
+ expect(subject.count).to eq 52
15
+ subject.each do |card|
16
+ expect(card).to be_a FiftyTwo::Card
17
+ expect(card.deck).to eq subject
18
+ end
19
+ end
20
+
21
+ it "has 13 distinct ranks" do
22
+ expect(subject.map(&:rank).uniq.count).to eq 13
23
+ end
24
+
25
+ it "has 4 distinct suits" do
26
+ expect(subject.map(&:suit).uniq.count).to eq 4
27
+ end
28
+
29
+ it "has 2 distinct colors" do
30
+ expect(subject.map(&:color).uniq.count).to eq 2
31
+ end
32
+
33
+ context "shuffling/sorting" do
34
+ def sorted?(deck)
35
+ is_sorted = true
36
+ deck[1..].each_with_index do |card, i|
37
+ is_sorted = false unless deck[i] < card
38
+ end
39
+ is_sorted
40
+ end
41
+
42
+ it "starts with cards ordered properly" do
43
+ expect(sorted?(subject)).to be true
44
+ end
45
+
46
+ it "can shuffle the cards" do
47
+ subject.shuffle!
48
+ expect(sorted?(subject)).to be false
49
+ end
50
+
51
+ it "can resort the cards" do
52
+ subject.shuffle!
53
+ subject.sort!
54
+ expect(sorted?(subject)).to be true
55
+ end
56
+ end
57
+
58
+ context "filtering" do
59
+ require "pry"
60
+ it "can find diamonds" do
61
+ cards = subject.diamonds
62
+ expect(cards.count).to eq 13
63
+ end
64
+
65
+ it "can find blacks" do
66
+ cards = subject.blacks
67
+ expect(cards.count).to eq 26
68
+ end
69
+
70
+ it "can find pips" do
71
+ cards = subject.pips
72
+ expect(cards.count).to eq 40
73
+ end
74
+
75
+ it "can find faces" do
76
+ cards = subject.faces
77
+ expect(cards.count).to eq 12
78
+ end
79
+
80
+ it "can find jacks" do
81
+ cards = subject.jacks
82
+ expect(cards.count).to eq 4
83
+ end
84
+
85
+ it "can find queens" do
86
+ cards = subject.queens
87
+ expect(cards.count).to eq 4
88
+ end
89
+
90
+ it "can find kings" do
91
+ cards = subject.kings
92
+ expect(cards.count).to eq 4
93
+ end
94
+
95
+ it "can find aces" do
96
+ cards = subject.aces
97
+ expect(cards.count).to eq 4
98
+ end
99
+
100
+ it "can find red faces" do
101
+ cards = subject.reds.faces
102
+ expect(cards.count).to eq 6
103
+ end
104
+
105
+ it "can find black jacks" do
106
+ cards = subject.jacks.blacks
107
+ expect(cards.count).to eq 2
108
+ end
109
+
110
+ it "can find pip spades" do
111
+ cards = subject.pips.spades
112
+ expect(cards.count).to eq 10
113
+ end
114
+ end
115
+
116
+ describe "#draw" do
117
+ let(:subject) { minideck }
118
+
119
+ it "takes cards in order from the deck" do
120
+ expect(subject.count).to eq 2
121
+
122
+ card = subject.draw
123
+ expect(subject.count).to eq 1
124
+ expect(card).to be_a FiftyTwo::Card
125
+ expect(card.identifier).to eq "3C"
126
+
127
+ card = subject.draw
128
+ expect(subject.count).to eq 0
129
+ expect(card).to be_a FiftyTwo::Card
130
+ expect(card.identifier).to eq "9D"
131
+
132
+ card = subject.draw
133
+ expect(subject.count).to eq 0
134
+ expect(card).to be nil
135
+ end
136
+ end
137
+
138
+ describe "#locate" do
139
+ it "can find the 3 of diamonds via 3D" do
140
+ card = subject.locate("3D")
141
+ expect(card.rank).to eq FiftyTwo::Rank::THREE
142
+ expect(card.suit).to eq FiftyTwo::Suit::DIAMONDS
143
+ end
144
+
145
+ it "can find the ace of spades via as" do
146
+ card = subject.locate("as")
147
+ expect(card.rank).to eq FiftyTwo::Rank::ACE
148
+ expect(card.suit).to eq FiftyTwo::Suit::SPADES
149
+ end
150
+
151
+ it "can find the 10 of clubs via 10C" do
152
+ card = subject.locate("10C")
153
+ expect(card.rank).to eq FiftyTwo::Rank::TEN
154
+ expect(card.suit).to eq FiftyTwo::Suit::CLUBS
155
+ end
156
+
157
+ it "returns nil on a nonsense code" do
158
+ card = subject.locate("MITTENS")
159
+ expect(card).to be nil
160
+ end
161
+
162
+ context "removed card" do
163
+ let(:subject) { minideck }
164
+
165
+ it "returns nil if the card is not currently in the deck" do
166
+ card = minideck.locate("10C")
167
+ expect(card).to be nil
168
+ end
169
+ end
170
+ end
171
+
172
+ describe "#transfer" do
173
+ let(:subject) { minideck }
174
+ let(:hand) { FiftyTwo::Hand.new }
175
+
176
+ it "has a deck with 2 cards, and a hand with none" do
177
+ expect(subject.count).to eq 2
178
+ expect(hand.count).to eq 0
179
+ end
180
+
181
+ it "can transfer a card by object" do
182
+ subject.transfer(subject[1], hand)
183
+ expect(subject.count).to eq 1
184
+ expect(hand.count).to eq 1
185
+ end
186
+
187
+ it "can transfer a card by identifier" do
188
+ subject.transfer("3C", hand)
189
+ expect(subject.count).to eq 1
190
+ expect(hand.count).to eq 1
191
+ end
192
+
193
+ context "missing card" do
194
+ let(:missing_card) { FiftyTwo::Card.new(subject, FiftyTwo::Rank::THREE, FiftyTwo::Suit::HEARTS) }
195
+
196
+ it "raises an error on a card not in the deck" do
197
+ expect { subject.transfer(missing_card, hand) }.to raise_error FiftyTwo::HasCards::CardUnavailableError
198
+ end
199
+
200
+ it "raises an error on an identifier not in the deck" do
201
+ expect { subject.transfer(missing_card.identifier, hand) }.to raise_error FiftyTwo::HasCards::CardUnavailableError
202
+ end
203
+ end
204
+
205
+ context "back to originating deck" do
206
+ let(:card) { subject[1] }
207
+ before(:each) { subject.transfer(card, hand) }
208
+
209
+ it "starts with a card in the hand and a card in the deck" do
210
+ expect(subject.count).to eq 1
211
+ expect(hand.count).to eq 1
212
+ end
213
+
214
+ it "can transfer a card from a hand back to the bottom of its originating deck" do
215
+ hand.transfer(card)
216
+ expect(subject.count).to eq 2
217
+ expect(hand.count).to eq 0
218
+ expect(subject.last).to eq card
219
+ end
220
+ end
221
+ end
222
+
223
+ describe "#render" do
224
+ let(:subject) { minideck }
225
+
226
+ before(:each) do
227
+ minideck.cards.each_with_index do |card, i|
228
+ allow(card).to receive(:render).and_return "CARD#{i}"
229
+ end
230
+ end
231
+
232
+ it "renders the set of cards in this deck" do
233
+ expect(subject.render).to eq "CARD0 CARD1"
234
+ end
235
+ end
236
+
237
+ describe "#deal" do
238
+ let(:hand1) { FiftyTwo::Hand.new }
239
+ let(:hand2) { FiftyTwo::Hand.new }
240
+ let(:hand3) { FiftyTwo::Hand.new }
241
+ let(:hands) { [hand1, hand2, hand3] }
242
+ before(:each) { subject.shuffle! }
243
+
244
+ it "starts out as expected" do
245
+ expect(subject.count).to eq 52
246
+ hands.each { |h| expect(h.count).to eq 0 }
247
+ end
248
+
249
+ it "can deal a single card to variety of hands" do
250
+ card1 = subject[0]
251
+ card2 = subject[1]
252
+ card3 = subject[2]
253
+
254
+ subject.deal(hands)
255
+ expect(subject.count).to eq 49
256
+ expect(hand1.cards).to match_array [card1]
257
+ expect(hand2.cards).to match_array [card2]
258
+ expect(hand3.cards).to match_array [card3]
259
+ end
260
+
261
+ it "can deal multiple cards to a variety of hands" do
262
+ card1 = subject[0]
263
+ card2 = subject[1]
264
+ card3 = subject[2]
265
+ card4 = subject[3]
266
+ card5 = subject[4]
267
+ card6 = subject[5]
268
+ card7 = subject[6]
269
+ card8 = subject[7]
270
+ card9 = subject[8]
271
+
272
+ subject.deal(hands, hand_size: 3)
273
+ expect(subject.count).to eq 43
274
+ expect(hand1.cards).to match_array [card1, card4, card7]
275
+ expect(hand2.cards).to match_array [card2, card5, card8]
276
+ expect(hand3.cards).to match_array [card3, card6, card9]
277
+ end
278
+ end
279
+ end
280
+ end