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,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
|
Binary file
|
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,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
|