fiftytwo 0.0.1

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.
@@ -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