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.
- data/CHANGELOG.md +6 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +17 -0
- data/LICENSE +19 -0
- data/README.md +111 -0
- data/Rakefile +11 -0
- data/bin/patience +5 -0
- data/lib/patience.rb +13 -0
- data/lib/patience/area.rb +62 -0
- data/lib/patience/card.rb +107 -0
- data/lib/patience/core_ext/class.rb +17 -0
- data/lib/patience/core_ext/core_ext.rb +1 -0
- data/lib/patience/core_ext/object.rb +37 -0
- data/lib/patience/core_ext/string.rb +33 -0
- data/lib/patience/cursor.rb +66 -0
- data/lib/patience/deck.rb +21 -0
- data/lib/patience/event_handlers/click.rb +99 -0
- data/lib/patience/event_handlers/drag.rb +36 -0
- data/lib/patience/event_handlers/drop.rb +147 -0
- data/lib/patience/foundation.rb +30 -0
- data/lib/patience/game.rb +10 -0
- data/lib/patience/pile.rb +78 -0
- data/lib/patience/processable.rb +87 -0
- data/lib/patience/rank.rb +56 -0
- data/lib/patience/scenes/game_scene.rb +66 -0
- data/lib/patience/sprites/card_deck.png +0 -0
- data/lib/patience/sprites/empty_stock.png +0 -0
- data/lib/patience/sprites/pile_background.png +0 -0
- data/lib/patience/stock.rb +20 -0
- data/lib/patience/suit.rb +73 -0
- data/lib/patience/tableau.rb +42 -0
- data/lib/patience/version.rb +3 -0
- data/lib/patience/waste.rb +18 -0
- data/test/patience/core_ext/test_class.rb +28 -0
- data/test/patience/core_ext/test_object.rb +15 -0
- data/test/patience/core_ext/test_string.rb +35 -0
- data/test/patience/event_handlers/test_click.rb +142 -0
- data/test/patience/event_handlers/test_drag.rb +45 -0
- data/test/patience/event_handlers/test_drop.rb +175 -0
- data/test/patience/helper.rb +8 -0
- data/test/patience/scenes/test_game_scene.rb +14 -0
- data/test/patience/test_area.rb +74 -0
- data/test/patience/test_card.rb +165 -0
- data/test/patience/test_cursor.rb +77 -0
- data/test/patience/test_deck.rb +53 -0
- data/test/patience/test_foundation.rb +38 -0
- data/test/patience/test_game.rb +29 -0
- data/test/patience/test_pile.rb +83 -0
- data/test/patience/test_processable.rb +159 -0
- data/test/patience/test_rank.rb +88 -0
- data/test/patience/test_stock.rb +43 -0
- data/test/patience/test_suit.rb +87 -0
- data/test/patience/test_tableau.rb +57 -0
- data/test/patience/test_version.rb +11 -0
- data/test/patience/test_waste.rb +35 -0
- 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,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
|