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,66 @@
1
+ module Patience
2
+ ###
3
+ # Patience::Cursor is a high-level class, which deals with all events
4
+ # in the game. Cursor always knows current position of the mouse.
5
+ # cursor = Cursor.new
6
+ # always { @cursor.mouse_pos = mouse_pos }
7
+ # cursor.clicked_something? #=> false
8
+ # cursor.still_on_something? #=> false
9
+ #
10
+ class Cursor
11
+ extend Forwardable
12
+
13
+ attr_accessor :mouse_pos, :click, :drag, :drop
14
+
15
+ def click!
16
+ @click.scenario.call
17
+ end
18
+
19
+ # Calls calculated scenario for the drop event and then
20
+ # refreshes click, drag and drop by setting them to nil.
21
+ def drop!
22
+ @drop.scenario.call
23
+ @click, @drag, @drop = nil, nil, nil
24
+ end
25
+
26
+ # Checks whether cursor clicked something. If so, also checks, if the cursor
27
+ # is still in boundaries of that object at the time of mouse button release.
28
+ # Returns true, if this is true. Otherwise, returns false.
29
+ def clicked_something?
30
+ click and click.something? and still_on_something?
31
+ end
32
+
33
+ # Checks whether cursor is still over the clicked object by
34
+ # asking card or pile, if the mouse_pos is within the pale of
35
+ # them. Returns true, if this is true. Otherwise, returns false.
36
+ def still_on_something?
37
+ if carrying_card?
38
+ card.hit?(mouse_pos)
39
+ elsif pile
40
+ pile.hit?(mouse_pos)
41
+ else
42
+ false
43
+ end
44
+ end
45
+
46
+ # Returns true if cursor clicked some card and that card is avaliable
47
+ # for dragging (e.g. its face is up). Otherwise, returns false.
48
+ def movable?
49
+ carrying_card? and draggable?
50
+ end
51
+
52
+ # Returns true if cursor clicked something different from
53
+ # nothing. And by "something different" I mean any card.
54
+ def carrying_card?
55
+ click and card
56
+ end
57
+
58
+ ##
59
+ # Returns true if the clicked object is drawable. Otherwise,
60
+ # returns false. In fact, this method exists only for readability.
61
+ alias :drawable? :movable?
62
+
63
+ def_delegators :@click, :card, :pile, :offset, :cards
64
+ def_delegator :@drag, :draggable?
65
+ end
66
+ end
@@ -0,0 +1,21 @@
1
+ require_relative 'pile'
2
+
3
+ module Patience
4
+ ###
5
+ # Patience::Deck creates a play deck object, which
6
+ # contains 52 cards (just like a real world deck!).
7
+ # deck = Deck.new(cards) # Just imagine, that we've already created those!
8
+ # deck.cards.size #=> 52
9
+ #
10
+ class Deck < Pile
11
+
12
+ def initialize
13
+ @cards = []
14
+
15
+ Card::Rank.descendants.size.times do |r|
16
+ Card::Suit.descendants.size.times { |s| @cards << Card.new(r+1, s+1) }
17
+ end
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,99 @@
1
+ require_relative '../processable'
2
+
3
+ module Patience
4
+ class EventHandler
5
+ ###
6
+ # Click represents a state of every click in the game. Processable module
7
+ # endows Click with special abilities (the group of "detect" methods).
8
+ # Every object of Click has the scenario parameter, which is an instance of
9
+ # the Lambda. Thereby, an action, which should be performed on click, can be
10
+ # executed lately and lazily (on demand). Scenario is nothing but a bunch
11
+ # of certain actions, being performed on click.
12
+ # cursor.click = EventHandler::Click.new(mouse_pos, areas)
13
+ # cursor.click.card #=> Two of Hearts
14
+ # cursor.click.card.pos #=> (0, 0)
15
+ # cursor.click.scenario.call # Perform some action on the card.
16
+ # cursor.click.card.pos #=> (20, 20)
17
+ #
18
+ class Click
19
+ extend Forwardable
20
+
21
+ include Processable
22
+
23
+ attr_reader :cards, :offset, :scenario
24
+
25
+ def initialize(mouse_pos, areas)
26
+ @mouse_pos = mouse_pos
27
+ @areas = areas
28
+ @area = detect_area
29
+
30
+ # If area has been detected, calculate other parameters too.
31
+ if @area
32
+ @pile = detect_pile
33
+ @cards = collect_cards # A clicked card and tail cards.
34
+ @card = cards.keys.first if @cards # The very clicked card.
35
+ # Offset for dragged card.
36
+ @offset = pick_up(@card, mouse_pos) if @card and something?
37
+
38
+ @scenario = -> { stock }
39
+ end
40
+ end
41
+
42
+ protected
43
+
44
+ # Finds an area of a card, which was clicked.
45
+ def detect_area
46
+ detect_in(@areas, :area) { |area| area.hit?(@mouse_pos) }
47
+ end
48
+
49
+ # Finds a pile of a card, which was clicked.
50
+ def detect_pile
51
+ detect_in(@areas, :pile) { |pile| pile.hit?(@mouse_pos) }
52
+ end
53
+
54
+ # Finds a card, which was clicked and tries to find
55
+ # descending cards, in the pile (if there are any).
56
+ def collect_cards
57
+ card = detect_in(@areas, :card) { |card| card.hit?(@mouse_pos) }
58
+ return card unless card
59
+
60
+ n = @pile.size - @pile.cards.index(card)
61
+ tail_cards = @pile.cards.last(n)
62
+ Hash[*tail_cards.map { |card| [card, card.pos] }.flatten]
63
+ end
64
+
65
+ # Adds a card from Stock to Waste, if Stock was clicked.
66
+ def stock
67
+ if stock?
68
+ if pile.empty?
69
+ pile_background = 'patience/sprites/pile_background.png'
70
+ pile.background = Ray::Sprite.new path_of(pile_background)
71
+ refill_stock
72
+ else
73
+ displace_to_waste if @card
74
+ end
75
+
76
+ if pile.empty?
77
+ empty_stock = 'patience/sprites/empty_stock.png'
78
+ pile.background = Ray::Sprite.new path_of(empty_stock)
79
+ end
80
+ end
81
+ end
82
+
83
+ # Removes all cards from Stock and adds them to Waste.
84
+ def refill_stock
85
+ @areas[:waste].tap { |waste|
86
+ waste.cards.reverse_each do |card|
87
+ @areas[:stock].add_from(waste.piles.first, card)
88
+ end
89
+ }
90
+ end
91
+
92
+ # Adds clicked card from Stock to Waste.
93
+ def displace_to_waste
94
+ @areas[:waste].add_from(@pile, @card)
95
+ end
96
+
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,36 @@
1
+ require_relative '../processable'
2
+
3
+ module Patience
4
+ class EventHandler
5
+ ###
6
+ # Patience::EventHandler::Drag provides drag objects,
7
+ # that serves for moving cards in the window.
8
+ # areas = { :tableau => Tableau.new }
9
+ # cursor.drag = EventHandler::Drag.new(cursor)
10
+ # cursor.drag.move(mouse_pos) if cursor.movable?
11
+ #
12
+ class Drag
13
+ include Processable
14
+
15
+ def initialize(cursor)
16
+ @card = cursor.card
17
+ @tail_cards = cursor.cards
18
+ @offset = cursor.offset
19
+ end
20
+
21
+ # Changes position of the card's sprite, considering the offset.
22
+ def move(mouse_pos)
23
+ @tail_cards.keys.each_with_index do |card, i|
24
+ card.pos = mouse_pos + @offset + [0, i*20]
25
+ end
26
+ end
27
+
28
+ # Returns true if there is a card to drag and this card
29
+ # is turned to its face. Otherwise, returns false.
30
+ def draggable?
31
+ card.not.nil? and card.face_up?
32
+ end
33
+
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,147 @@
1
+ require_relative '../processable'
2
+
3
+ module Patience
4
+ class EventHandler
5
+ # Drop's objects handles dropping of the cards. It means, that these
6
+ # objects decide, what to do with dropped card. For example, they can return
7
+ # a dropped card to its initial position, cancelling the work of drag event.
8
+ # cursor.drop = EventHandler::Drop.new(click, areas)
9
+ # # Execute scenario for the drop event,
10
+ # # which calculates dynamically.
11
+ # cursor.drop.scenario.call
12
+ #
13
+ class Drop
14
+
15
+ include Processable
16
+
17
+ attr_reader :scenario
18
+
19
+ def initialize(click, areas)
20
+ @cards = click.cards
21
+ @areas = areas
22
+ @pile = click.pile
23
+ @card_to_drop = click.card
24
+
25
+ @card_beneath = find_card_beneath
26
+ @pile_beneath = find_pile_beneath
27
+ @area = find_area_beneath
28
+
29
+ @scenario = -> {
30
+ if tableau?
31
+ put_in_tableau
32
+ elsif foundation?
33
+ put_in_foundation
34
+ else
35
+ call_off
36
+ end
37
+ }
38
+ end
39
+
40
+ protected
41
+
42
+ # Finds an area beneath the dropped card.
43
+ def find_area_beneath
44
+ detect_in(@areas, :area) { |area| area.piles.include?(@pile_beneath) }
45
+ end
46
+
47
+ # Finds a pile beneath the dropped card.
48
+ def find_pile_beneath
49
+ # Find out, if dropped card is a king or an ace.
50
+ king_or_ace = @card_to_drop.rank.king? || @card_to_drop.rank.ace?
51
+ detect_in(@areas, :pile) do |pile|
52
+ # If dropped card is a king or an ace, don't
53
+ # check, whether a pile includes card beneath.
54
+ pile != @pile and pile.overlaps?(@card_to_drop) and
55
+ (king_or_ace ? pile.empty? : pile.cards.include?(@card_beneath))
56
+ end
57
+ end
58
+
59
+ # Finds a card beneath the dropped card.
60
+ def find_card_beneath
61
+ all_tail_cards = @areas.values.map do |area|
62
+ area.piles.map { |pile| pile.cards.last }
63
+ end.flatten.compact
64
+
65
+ # Iterate only over tail cards.
66
+ all_tail_cards.find do |card|
67
+ area = detect_in(@areas, :area) { |area| area.cards.include?(card)}
68
+ card.face_up? and card.overlaps?(@card_to_drop) and
69
+ (case area
70
+ when Tableau then tableau_conditions?(card)
71
+ when Foundation then foundation_conditions?(card)
72
+ end)
73
+ end
74
+ end
75
+
76
+ # Returns true, if card's rank is higher by 1 than the rank of
77
+ # the card which is dropped and the card's suit has different color.
78
+ def tableau_conditions?(card)
79
+ card.rank.higher_by_one_than?(@card_to_drop.rank) and
80
+ card.suit.different_color?(@card_to_drop.suit)
81
+ end
82
+
83
+ # Returns true, if card's rank is lower by 1 than the rank of the
84
+ # card, which is dropped and the card's suit must be the same color.
85
+ def foundation_conditions?(card)
86
+ @card_to_drop.rank.higher_by_one_than?(card.rank) and
87
+ card.suit.same_color?(@card_to_drop.suit)
88
+ end
89
+
90
+ # Removes card from its pile and adds to the pile beneath.
91
+ def add_to_pile_beneath(card)
92
+ @pile_beneath << @pile.remove(card)
93
+ end
94
+
95
+ # Sets the position of a dropped card to its initial location.
96
+ def call_off
97
+ @cards.each { |card, init_pos| card.pos = init_pos }
98
+ end
99
+
100
+ # Returns true, if dropped card meets
101
+ # requirements for dropping into Tableau.
102
+ def can_put_in_tableau?
103
+ (@pile_beneath.empty? and @card_to_drop.rank.king?) or
104
+ (@card_beneath and @card_to_drop.rank.not.ace? and
105
+ @pile_beneath.last_card?(@card_beneath) and
106
+ tableau_conditions?(@card_beneath))
107
+ end
108
+
109
+ # Returns true, if dropped card meets
110
+ # requirements for dropping into Foundation.
111
+ def can_put_in_foundation?
112
+ (@pile_beneath.empty? and @card_to_drop.rank.ace?) or
113
+ (@card_beneath and foundation_conditions?(@card_beneath))
114
+ end
115
+
116
+ # Adds dropped card to one of Tableau's piles, if it's
117
+ # possible. If not, calls off card to it's initital position.
118
+ def put_in_tableau
119
+ if can_put_in_tableau?
120
+ @cards.keys.each_with_index do |card, i|
121
+ add_to_pile_beneath(card)
122
+ if @pile_beneath.size == 1 # It was empty one line before.
123
+ card.pos = @pile_beneath.pos + [0, i*20]
124
+ else
125
+ card.pos = @pile_beneath.cards[-2].pos + [0, 20]
126
+ end
127
+ @pile.cards.last.face_up unless @pile.empty?
128
+ end
129
+ else
130
+ call_off
131
+ end
132
+ end
133
+
134
+ # Adds dropped card to one of Foundation's piles, if it's
135
+ # possible. If not, calls off card to it's initital position.
136
+ def put_in_foundation
137
+ if can_put_in_foundation?
138
+ add_to_pile_beneath(@card_to_drop)
139
+ @pile.cards.last.face_up unless @pile.empty?
140
+ else
141
+ call_off
142
+ end
143
+ end
144
+
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,30 @@
1
+ require_relative 'area'
2
+
3
+ module Patience
4
+ ###
5
+ # Patience::Area::Foundation is a class, which
6
+ # represents Foundation area of the game.
7
+ # foundation = Area::Foundation.new
8
+ # foundation.piles.size #=> 1
9
+ #
10
+ class Foundation < Area
11
+
12
+ def initialize
13
+ super([], 4)
14
+ self.pos = [361, 23]
15
+ end
16
+
17
+ protected
18
+
19
+ # Disposes Foundation in the window by specifying coordinates
20
+ # of every pile in this area, starting from the pos argument.
21
+ def pos=(pos)
22
+ x, y, step_x = pos[0], pos[1], 110
23
+ piles.each do |pile|
24
+ pile.pos = [x, y]
25
+ x += step_x # Margin between piles along the axis X.
26
+ end
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,10 @@
1
+ module Patience
2
+ class Game < Ray::Game
3
+ def initialize
4
+ super("Patience", :size => [800, 600])
5
+ Patience::GameScene.bind(self)
6
+
7
+ scenes << :game_scene
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,78 @@
1
+ module Patience
2
+ ###
3
+ # Patience::Pile is aimed to hold cards in the pile :surprise:.
4
+ # Every pile has its own background sprite and set of cards.
5
+ # cards = [Card.new(1, 1), Card.new(1, 2), Card.new(1, 3)]
6
+ # random_pile = Pile.new(cards)
7
+ # random_pile.background = [10, 10]
8
+ #
9
+ class Pile
10
+ extend Forwardable
11
+
12
+ attr_accessor :cards, :background
13
+
14
+ def initialize(cards=[])
15
+ @cards = cards
16
+ pile_background = 'patience/sprites/pile_background.png'
17
+ @background = Ray::Sprite.new path_of(pile_background)
18
+ end
19
+
20
+ # Changes background sprite of the pile considering position of the old one.
21
+ def background=(bg)
22
+ bg.pos = @background.pos
23
+ @background = bg
24
+ end
25
+
26
+ # Throws off 'num' quantity of cards and returns the array of them.
27
+ def shuffle_off!(num)
28
+ cards.slice!(0..num-1)
29
+ end
30
+
31
+ # Appends card to the pile considering position of that pile.
32
+ def <<(other_card)
33
+ other_card.pos = self.pos
34
+ cards << other_card
35
+ end
36
+
37
+ # Sets position of the pile. Applies to
38
+ # the cards in the pile and background both.
39
+ def pos=(pos)
40
+ background.pos = *pos
41
+ cards.each { |card| card.pos = *pos }
42
+ end
43
+
44
+ # Returns true if the given card is the last
45
+ # card in the pile Otherwise, returns false.
46
+ def last_card?(card)
47
+ card == cards.last
48
+ end
49
+
50
+ # Draws pile in the window.
51
+ def draw_on(win)
52
+ win.draw(background)
53
+ cards.each { |card| card.draw_on(win) }
54
+ end
55
+
56
+ # Returns true or card, if there was clicked background or card
57
+ # in the pile, respectively. Otherwise returns false or nil.
58
+ def hit?(mouse_pos)
59
+ background.to_rect.contain?(mouse_pos) or
60
+ cards.find { |card| card.hit?(mouse_pos) }
61
+ end
62
+
63
+ # If the pile is empty, returns the result of collision detection
64
+ # of the other_card and background of the pile. If the pile isn't
65
+ # empty, tries to find a card, which overlaps other_card.
66
+ def overlaps?(other_card)
67
+ if cards.empty?
68
+ background.to_rect.collide?(other_card)
69
+ else
70
+ cards.find { |card| card.face_up? and card.overlaps?(other_card) }
71
+ end
72
+ end
73
+
74
+ def_delegators :@cards, :size, :empty?, :delete
75
+ def_delegator :@cards, :delete, :remove
76
+ def_delegators :@background, :pos, :x, :y
77
+ end
78
+ end