patience 0.1.0

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 (56) hide show
  1. data/CHANGELOG.md +6 -0
  2. data/Gemfile +5 -0
  3. data/Gemfile.lock +17 -0
  4. data/LICENSE +19 -0
  5. data/README.md +111 -0
  6. data/Rakefile +11 -0
  7. data/bin/patience +5 -0
  8. data/lib/patience.rb +13 -0
  9. data/lib/patience/area.rb +62 -0
  10. data/lib/patience/card.rb +107 -0
  11. data/lib/patience/core_ext/class.rb +17 -0
  12. data/lib/patience/core_ext/core_ext.rb +1 -0
  13. data/lib/patience/core_ext/object.rb +37 -0
  14. data/lib/patience/core_ext/string.rb +33 -0
  15. data/lib/patience/cursor.rb +66 -0
  16. data/lib/patience/deck.rb +21 -0
  17. data/lib/patience/event_handlers/click.rb +99 -0
  18. data/lib/patience/event_handlers/drag.rb +36 -0
  19. data/lib/patience/event_handlers/drop.rb +147 -0
  20. data/lib/patience/foundation.rb +30 -0
  21. data/lib/patience/game.rb +10 -0
  22. data/lib/patience/pile.rb +78 -0
  23. data/lib/patience/processable.rb +87 -0
  24. data/lib/patience/rank.rb +56 -0
  25. data/lib/patience/scenes/game_scene.rb +66 -0
  26. data/lib/patience/sprites/card_deck.png +0 -0
  27. data/lib/patience/sprites/empty_stock.png +0 -0
  28. data/lib/patience/sprites/pile_background.png +0 -0
  29. data/lib/patience/stock.rb +20 -0
  30. data/lib/patience/suit.rb +73 -0
  31. data/lib/patience/tableau.rb +42 -0
  32. data/lib/patience/version.rb +3 -0
  33. data/lib/patience/waste.rb +18 -0
  34. data/test/patience/core_ext/test_class.rb +28 -0
  35. data/test/patience/core_ext/test_object.rb +15 -0
  36. data/test/patience/core_ext/test_string.rb +35 -0
  37. data/test/patience/event_handlers/test_click.rb +142 -0
  38. data/test/patience/event_handlers/test_drag.rb +45 -0
  39. data/test/patience/event_handlers/test_drop.rb +175 -0
  40. data/test/patience/helper.rb +8 -0
  41. data/test/patience/scenes/test_game_scene.rb +14 -0
  42. data/test/patience/test_area.rb +74 -0
  43. data/test/patience/test_card.rb +165 -0
  44. data/test/patience/test_cursor.rb +77 -0
  45. data/test/patience/test_deck.rb +53 -0
  46. data/test/patience/test_foundation.rb +38 -0
  47. data/test/patience/test_game.rb +29 -0
  48. data/test/patience/test_pile.rb +83 -0
  49. data/test/patience/test_processable.rb +159 -0
  50. data/test/patience/test_rank.rb +88 -0
  51. data/test/patience/test_stock.rb +43 -0
  52. data/test/patience/test_suit.rb +87 -0
  53. data/test/patience/test_tableau.rb +57 -0
  54. data/test/patience/test_version.rb +11 -0
  55. data/test/patience/test_waste.rb +35 -0
  56. metadata +135 -0
@@ -0,0 +1,87 @@
1
+ module Patience
2
+ module Processable
3
+ attr_reader :area, :pile, :card
4
+
5
+ # Finds area or pile, or card (depends on the option) in
6
+ # the areas. If the option is wrong, raises ArgumentError.
7
+ def detect_in(areas, option, &block)
8
+ case option
9
+ when :area then find_area_in(areas, &block)
10
+ when :pile then find_pile_in(areas, &block)
11
+ when :card then find_card_in(areas, &block)
12
+ else
13
+ raise ArgumentError, "Unknown option: #{option}"
14
+ end
15
+ end
16
+
17
+ # Returns area, which is being clicked.
18
+ def find_area_in(areas)
19
+ areas.values.find { |area| yield(area) }
20
+ end
21
+
22
+ # Returns pile, which is being clicked.
23
+ def find_pile_in(areas)
24
+ find_area_in(areas) do |area|
25
+ pile = area.piles.find { |pile| yield(pile) }
26
+ return pile unless pile.nil?
27
+ end
28
+ nil
29
+ end
30
+
31
+ # Returns card, which is being clicked.
32
+ def find_card_in(areas)
33
+ find_pile_in(areas) do |pile|
34
+ card = pile.cards.reverse.find { |card| yield(card) }
35
+ return card unless card.nil?
36
+ end
37
+ nil
38
+ end
39
+
40
+ # Returns array, containing gathered hit elements.
41
+ def to_a
42
+ [area, pile, card]
43
+ end
44
+
45
+ # Returns hash, containing gathered hit elements.
46
+ def to_h
47
+ { :area => area, :pile => pile, :card => card }
48
+ end
49
+
50
+ # Returns true, if there hasn't been found something.
51
+ def nothing?
52
+ to_a.compact.size.zero?
53
+ end
54
+
55
+ # Returns true, if there's been found something. Opposite of #nothing?.
56
+ def something?
57
+ not nothing?
58
+ end
59
+
60
+ # Returns subtraction between card and mouse position at
61
+ # the moment of click, only if cursor clicked the card.
62
+ def pick_up(card, mouse_pos)
63
+ card.pos - mouse_pos
64
+ end
65
+
66
+ # Returns true, if the clicked area is Stock.
67
+ def stock?
68
+ area.instance_of? Stock
69
+ end
70
+
71
+ # Returns true, if the clicked area is Waste.
72
+ def waste?
73
+ area.instance_of? Waste
74
+ end
75
+
76
+ # Returns true, if the clicked area is Tableau.
77
+ def tableau?
78
+ area.instance_of? Tableau
79
+ end
80
+
81
+ # Returns true, if the clicked area is Foundation.
82
+ def foundation?
83
+ area.instance_of? Foundation
84
+ end
85
+
86
+ end
87
+ end
@@ -0,0 +1,56 @@
1
+ module Patience
2
+ class Card
3
+ ###
4
+ # Rank class provides underlying methods for every rank.
5
+ class Rank
6
+ ##
7
+ # This lambda defines the new class, designed to be a child of Rank.
8
+ create_rank_class = lambda { |num|
9
+ Class.new(Rank) do
10
+ include Comparable
11
+
12
+ define_method :initialize do
13
+ @num = num
14
+ end
15
+
16
+ # Returns integer representation of the rank. Basically, it's
17
+ # the position of the card in the ascending row of card ranks.
18
+ def to_i
19
+ @num
20
+ end
21
+
22
+ end
23
+ }
24
+
25
+ # Dynamically create rank classes.
26
+ %w[Ace Two Three Four Five Six Seven
27
+ Eight Nine Ten Jack Queen King].each_with_index do |class_name, i|
28
+ Rank.const_set(class_name, create_rank_class.call(i+1))
29
+ end
30
+
31
+ # Returns string representation of a rank. It asks class to
32
+ # give its full name, exscinding everything but its actual name.
33
+ def to_s
34
+ "#{self.class.name.demodulize}"
35
+ end
36
+
37
+ # Compares two ranks with each other. Based on integer values.
38
+ def <=>(other_rank)
39
+ @num <=> other_rank.to_i
40
+ end
41
+
42
+ def ace?
43
+ @num == 1
44
+ end
45
+
46
+ def king?
47
+ @num == 13
48
+ end
49
+
50
+ def higher_by_one_than?(other_rank)
51
+ @num - other_rank.to_i == 1
52
+ end
53
+
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,66 @@
1
+ module Patience
2
+ ###
3
+ # Patience::GameScene is a main scene of the game. All stuff happens here.
4
+ class GameScene < Ray::Scene
5
+
6
+ def setup
7
+ @bg_color = Ray::Color.new(31, 95, 25)
8
+ @cursor = Cursor.new
9
+ @deck = Deck.new
10
+ @deck.cards.shuffle!
11
+
12
+ @tableau = Tableau.new(@deck.shuffle_off! 28)
13
+ @stock = Stock.new(@deck.shuffle_off! 24)
14
+ @waste = Waste.new
15
+ @foundation = Foundation.new
16
+
17
+ @areas = { :tableau => @tableau,
18
+ :stock => @stock,
19
+ :waste => @waste,
20
+ :foundation => @foundation }
21
+ end
22
+
23
+ def register
24
+ add_hook :quit, method(:exit!)
25
+ add_hook :key_press, key(:q), method(:exit!)
26
+
27
+ # If mouse button pressed, create Click event. If a card has
28
+ # been clicked, create Drag event. Drag only face up cards.
29
+ on :mouse_press do
30
+ @cursor.click = EventHandler::Click.new(@cursor.mouse_pos, @areas)
31
+ if @cursor.carrying_card?
32
+ @cursor.drag = EventHandler::Drag.new(@cursor)
33
+ end
34
+ end
35
+
36
+ on :mouse_motion do
37
+ if @cursor.movable? and @cursor.click.not.stock?
38
+ @cursor.drag.move(@cursor.mouse_pos)
39
+ end
40
+ end
41
+
42
+ on :mouse_release do
43
+ @cursor.click! if @cursor.clicked_something?
44
+ if @cursor.carrying_card?
45
+ @cursor.drop = EventHandler::Drop.new(@cursor.click, @areas)
46
+ @cursor.drop! unless @cursor.click.stock?
47
+ end
48
+ end
49
+
50
+ always do
51
+ @cursor.mouse_pos = mouse_pos
52
+ end
53
+
54
+ end
55
+
56
+ def render(win)
57
+ win.clear @bg_color
58
+ @areas.values.to_a.each { |area| area.draw_on(win) }
59
+ # Draw the card, which is being dragged.
60
+ if @cursor.drawable?
61
+ @cursor.click.cards.keys.each { |card| card.draw_on(win) }
62
+ end
63
+ end
64
+
65
+ end
66
+ end
Binary file
@@ -0,0 +1,20 @@
1
+ require_relative 'area'
2
+
3
+ module Patience
4
+ ###
5
+ # Patience::Area::Stock creates Stock game object. By now, it's backed up only
6
+ # with methods from the Area class. Every card in Stock is turned to its back.
7
+ # stock = Area::Stock.new
8
+ # stock = Area::Stock.new([Card.new(1, 1)])
9
+ # stock.cards[0].face_down? #=> true
10
+ #
11
+ class Stock < Area
12
+
13
+ def initialize(cards)
14
+ super(cards, 1)
15
+ self.piles.first.cards += @cards.shuffle_off!(24).each(&:face_down)
16
+ self.pos = [31, 23]
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,73 @@
1
+ module Patience
2
+ class Card
3
+ ###
4
+ # Suit supplies only one method: #red?. The idea is that every
5
+ # actual suit has its own class, which is inherited from Suit.
6
+ # Each suit class defines its own #black? method dynamically.
7
+ class Suit
8
+ ##
9
+ # This lambda defines new class, designed to be a child
10
+ # of Suit and supplied with #black? method, contents of
11
+ # which depends on the given argument "true_or_false".
12
+ create_suit_class = lambda { |num, true_or_false|
13
+ Class.new(Suit) do
14
+ include Comparable
15
+
16
+ define_method :initialize do
17
+ @num = num
18
+ end
19
+
20
+ # Returns boolean value. For red suits the value
21
+ # is "false". For black suits the value is "true".
22
+ define_method :black? do
23
+ true_or_false
24
+ end
25
+
26
+ # Returns integer representation of a suit.
27
+ # Example:
28
+ def to_i
29
+ @num
30
+ end
31
+ end
32
+ }
33
+
34
+ # Create classes for every suit, giving the answer on the question about
35
+ # their blackness. If the answer is negative, obviously the suit is red.
36
+ Heart = create_suit_class.call(1, false)
37
+ Diamond = create_suit_class.call(2, false)
38
+ Spade = create_suit_class.call(3, true)
39
+ Club = create_suit_class.call(4, true)
40
+
41
+ # Checks whether card is "red" (being not black).
42
+ # The opposite of the Card#black?. Returns true
43
+ # if the card is red. Otherwise, returns false.
44
+ def red?
45
+ not black?
46
+ end
47
+
48
+ # Returns plural string representation of a suit. It asks class to
49
+ # give its full name, exscinding everything but its actual name.
50
+ def to_s
51
+ "#{self.class.name.demodulize}s"
52
+ end
53
+
54
+ # Compares two suits with each other for equality.
55
+ def <=>(other_suit)
56
+ @num <=> other_suit.to_i
57
+ end
58
+
59
+ # Returns true, if the color of suit is
60
+ # the same as the color of other suit.
61
+ def same_color?(other_suit)
62
+ (black? and other_suit.black?) or (red? and other_suit.red?)
63
+ end
64
+
65
+ # Returns true if the color of suit
66
+ # of differs from other suit's color.
67
+ def different_color?(other_suit)
68
+ (black? and other_suit.red?) or (red? and other_suit.black?)
69
+ end
70
+
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,42 @@
1
+ require_relative 'area'
2
+
3
+ module Patience
4
+ ###
5
+ # Patience::Area::Foundation is a class, which represents Tableau area of the
6
+ # game. Every ordinary Tableau should have 7 piles and 28 cards in them. Each
7
+ # pile has 1 + "serial number of the pile" cards. Every last card in every
8
+ # pile is turned to its face. The other cards are face down.
9
+ # cards = []
10
+ # cards = 28.times { cards << Card.new(1, 1) }
11
+ # tableau = Tableau.new(cards)
12
+ # tableau.piles.size #=> 7
13
+ # tableau.cards.size #=> 28
14
+ #
15
+ class Tableau < Area
16
+
17
+ def initialize(cards)
18
+ super(cards, 7)
19
+ @piles.each_with_index { |pile, i| pile.cards += @cards.shuffle_off!(i+1) }
20
+ self.pos = [31, 165]
21
+ end
22
+
23
+ protected
24
+
25
+ # Disposes Tableau in the window by specifying coordinates
26
+ # of every pile in this area, starting from the pos argument.
27
+ def pos=(pos)
28
+ x, y, step_x, step_y = pos[0], pos[1], 110, 26
29
+ piles.each { |pile|
30
+ pile.pos = [x, y]
31
+ x += step_x # Margin between piles along the axis X.
32
+ y2 = 0 # Y position of the first card.
33
+ pile.cards.each_with_index do |card, i|
34
+ card.sprite.y += y2
35
+ y2 += step_y # Margin between cards along the axis Y.
36
+ card.face_down unless pile.last_card?(card)
37
+ end
38
+ }
39
+ end
40
+
41
+ end
42
+ end
@@ -0,0 +1,3 @@
1
+ module Patience
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,18 @@
1
+ require_relative 'area'
2
+
3
+ module Patience
4
+ ###
5
+ # Patience::Area::Waste is a class, which represents Waste area of the
6
+ # game. By now, it's backed up only with methods from the Area class.
7
+ # waste = Area::Waste.new
8
+ # waste.piles.size #=> 1
9
+ #
10
+ class Waste < Area
11
+
12
+ def initialize
13
+ super([], 1)
14
+ self.pos = [141, 23]
15
+ end
16
+
17
+ end
18
+ end
@@ -0,0 +1,28 @@
1
+ require_relative '../helper'
2
+
3
+ module Patience
4
+ class TestClass < TestCase
5
+
6
+ class Parent
7
+ end
8
+
9
+ class Child1 < Parent
10
+ end
11
+
12
+ class Child2 < Parent
13
+ end
14
+
15
+ class Grandchild1 < Child1
16
+ end
17
+
18
+ class Grandchild2 < Child1
19
+ end
20
+
21
+ test 'Descendats' do
22
+ assert_equal [Grandchild2, Grandchild1, Child2, Child1], Parent.descendants
23
+ assert_equal [Grandchild2, Grandchild1], Child1.descendants
24
+ assert_equal [], Child2.descendants
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,15 @@
1
+ require_relative '../helper'
2
+
3
+ module Patience
4
+ class TestObject < TestCase
5
+
6
+ test 'The not object does negate true to false' do
7
+ refute nil.not.nil?, "The Not object doesn't negate true to false"
8
+ end
9
+
10
+ test 'The not object does negate false to true' do
11
+ assert Object.new.not.nil?, "The Not object doesn't negate false to true"
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,35 @@
1
+ require_relative '../helper'
2
+
3
+ module Please
4
+ module Forgive
5
+ class Me
6
+ end
7
+ end
8
+ end
9
+
10
+ module Patience
11
+ class TestString < TestCase
12
+
13
+ test 'Demodulize' do
14
+ assert_equal "Changes", "Going::Through::Changes".demodulize
15
+ end
16
+
17
+ test 'Constantize' do
18
+ assert_equal Please::Forgive::Me, "Please::Forgive::Me".constantize
19
+ assert_equal Please::Forgive::Me, "::Please::Forgive::Me".constantize
20
+ assert_equal Please::Forgive, "Please::Forgive".constantize
21
+ assert_equal Please::Forgive, "::Please::Forgive".constantize
22
+ assert_equal Please, "Please".constantize
23
+ assert_equal Please, "::Please".constantize
24
+ assert_raises(NameError) { "UnknownClass".constantize }
25
+ assert_raises(NameError) { "UnknownClass::NotForToffy".constantize }
26
+ assert_raises(NameError) { "UnknownClass::Please".constantize }
27
+ assert_raises(NameError) { "UnknownClass::Please::Forgive".constantize }
28
+ assert_raises(NameError) { "An invalid string".constantize }
29
+ assert_raises(NameError) { "InvalidClass\n".constantize }
30
+ assert_raises(NameError) { "Please::TestString".constantize }
31
+ assert_raises(NameError) { "Please::Forgive::TestString".constantize }
32
+ end
33
+
34
+ end
35
+ end