patience 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (C) 2012 Kyrylo Silin
|
2
|
+
|
3
|
+
This software is provided 'as-is', without any express or implied
|
4
|
+
warranty. In no event will the authors be held liable for any damages
|
5
|
+
arising from the use of this software.
|
6
|
+
|
7
|
+
Permission is granted to anyone to use this software for any purpose,
|
8
|
+
including commercial applications, and to alter it and redistribute it
|
9
|
+
freely, subject to the following restrictions:
|
10
|
+
|
11
|
+
1. The origin of this software must not be misrepresented; you must not
|
12
|
+
claim that you wrote the original software. If you use this software
|
13
|
+
in a product, an acknowledgment in the product documentation would be
|
14
|
+
appreciated but is not required.
|
15
|
+
|
16
|
+
2. Altered source versions must be plainly marked as such, and must not be
|
17
|
+
misrepresented as being the original software.
|
18
|
+
|
19
|
+
3. This notice may not be removed or altered from any source distribution.
|
data/README.md
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
Patience
|
2
|
+
========
|
3
|
+
|
4
|
+
* [https://github.com/kyrylo/patience/](https://github.com/kyrylo/patience/ "Home page")
|
5
|
+
|
6
|
+
Description
|
7
|
+
-----------
|
8
|
+
|
9
|
+
![Patience, version 0.1.0](http://img-fotki.yandex.ru/get/5908/98991937.6/0_7259e_d9e01c58_orig "Patience, version 0.1.0")
|
10
|
+
|
11
|
+
_Patience_ is a card game (also known as Solitaire), which is written in Ruby.
|
12
|
+
The game is based on Ray, a Ruby library for games. Currenlty, _Patience_ runs
|
13
|
+
_only_ on GNU/Linux, although, I'm planning to support it on Windows.
|
14
|
+
|
15
|
+
How to play
|
16
|
+
-----------
|
17
|
+
|
18
|
+
The play area of _Patience_ (Solitaire) consits of four zones: Waste, Tableau,
|
19
|
+
Stock and Foundation. The goal of the game is to allocate all the cards from
|
20
|
+
Tableau to Foundation. You can drag cards from pile to pile with your mouse.
|
21
|
+
Remember, you can't drop cards anywhere: there are some rules, restricting that
|
22
|
+
mess. Brief rules:
|
23
|
+
|
24
|
+
![Patience, rules](http://img-fotki.yandex.ru/get/6101/98991937.7/0_726e0_789437d8_orig "Patience, rules")
|
25
|
+
|
26
|
+
### Stock
|
27
|
+
|
28
|
+
* In the very beginning of each game comprises of 24 cards
|
29
|
+
* A click on Stock reveals its card and moves it to Waste
|
30
|
+
* You can't drag cards from Stock
|
31
|
+
* You can't drop cards onto Stock
|
32
|
+
* If Stock is empty, you can refresh it with cards from Waste (if there are any)
|
33
|
+
by clicking the former again.
|
34
|
+
|
35
|
+
### Waste
|
36
|
+
|
37
|
+
* In the very beginning of each game has no cards
|
38
|
+
* You can drag cards from Waste and drop them onto Tableau
|
39
|
+
* You can't drop cards onto Waste
|
40
|
+
|
41
|
+
### Tableau
|
42
|
+
|
43
|
+
* In the very beginning of the game comprises of 28 cards
|
44
|
+
* Consists of 7 piles. Each pile has `number of pile` cards
|
45
|
+
* Accepts cards from Waste and other piles from Tableau
|
46
|
+
* You can drop _only_ a King onto the freed pile
|
47
|
+
* You can move whole (valid) piles onto a proper cards
|
48
|
+
* You can drop a card onto a card in the pile, which's lower by one rank and has
|
49
|
+
suit of different color.
|
50
|
+
|
51
|
+
Example:
|
52
|
+
|
53
|
+
![Correct drop](http://img-fotki.yandex.ru/get/6200/98991937.7/0_726e1_bc6edf54_orig "Correct drop")
|
54
|
+
![Wrong drop](http://img-fotki.yandex.ru/get/6200/98991937.7/0_726e3_640d9de0_orig "Wrong drop")
|
55
|
+
|
56
|
+
### Foundation
|
57
|
+
|
58
|
+
* In the very beginning of each game has no cards
|
59
|
+
* Consists of 4 piles
|
60
|
+
* If the pile of Foundation is empty, it can accept only an Ace.
|
61
|
+
* If the pile of Foundation isn't empty, it will accept only cards of the same
|
62
|
+
suit, that are higher by one rank.
|
63
|
+
|
64
|
+
### Winning conditions
|
65
|
+
|
66
|
+
* All the cards are in Foundation, other zones has no cards.
|
67
|
+
|
68
|
+
Requirements
|
69
|
+
------------
|
70
|
+
|
71
|
+
### Runs on
|
72
|
+
|
73
|
+
* GNU/Linux
|
74
|
+
|
75
|
+
### Dependencies
|
76
|
+
|
77
|
+
* [Ruby](http://ruby-lang.org/ "Ruby")
|
78
|
+
(Tested _only_ on 1.9.3 version)
|
79
|
+
|
80
|
+
* [Ray](https://github.com/Mon-Ouie/ray/ "Ray") (0.2.0)
|
81
|
+
|
82
|
+
Installation
|
83
|
+
------------
|
84
|
+
|
85
|
+
### Regular GNU/Linux way
|
86
|
+
|
87
|
+
gem install patience
|
88
|
+
|
89
|
+
To launch the game:
|
90
|
+
|
91
|
+
% patience
|
92
|
+
|
93
|
+
### Irregular way
|
94
|
+
|
95
|
+
git clone git://github.com/kyrylo/patience.git patience
|
96
|
+
|
97
|
+
To launch the game:
|
98
|
+
|
99
|
+
% cd patience
|
100
|
+
% ruby bin/patience
|
101
|
+
|
102
|
+
Credits
|
103
|
+
-------
|
104
|
+
|
105
|
+
* Thanks to [Bil Bas (Spooner)](https://github.com/Spooner "Bil Bas") for giving advices and answering my questions
|
106
|
+
* Thanks to [Mon Ouïe](https://github.com/Mon-Ouie "Mon Ouïe") for Ray
|
107
|
+
|
108
|
+
License
|
109
|
+
-------
|
110
|
+
|
111
|
+
The project uses Zlib License. See LICENSE file for more information.
|
data/Rakefile
ADDED
data/bin/patience
ADDED
data/lib/patience.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'ray'
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
def path_of(resource)
|
5
|
+
File.join(File.dirname(__FILE__), resource)
|
6
|
+
end
|
7
|
+
|
8
|
+
Dir.chdir(File.dirname(__FILE__)) do
|
9
|
+
Dir['patience/event_handlers/*.rb'].each { |eh| require_relative eh }
|
10
|
+
Dir['patience/core_ext/*.rb'].each { |ext| require_relative ext }
|
11
|
+
Dir['patience/scenes/*.rb'].each { |scene| require_relative scene }
|
12
|
+
Dir['patience/*.rb'].each { |file| require_relative file }
|
13
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Patience
|
2
|
+
###
|
3
|
+
# Patience::Area provides area objects, that consist of piles. The goal of
|
4
|
+
# this class is to assemble piles into a logical bundle with a few common
|
5
|
+
# methods, so they could be controlled via only one interface. Basically,
|
6
|
+
# this class is useless on its own. The purpose of Area is to be inherited
|
7
|
+
# by other classes, which are more verbose in their intentions. By default,
|
8
|
+
# Area object instantiates without any cards and only with one pile. Every
|
9
|
+
# new pile would be created empty, even though you feed some cards to the
|
10
|
+
# new object. All the cards you provide to the new object, wouldn't be
|
11
|
+
# placed anywhere. You have to allocate them manually.
|
12
|
+
# field = Area.new
|
13
|
+
# field.piles #=> [#<Patience::Pile>]
|
14
|
+
# filed.piles[0].pos #=> (0, 0)
|
15
|
+
# field.pos #=> (0, 0)
|
16
|
+
#
|
17
|
+
class Area
|
18
|
+
# Returns an array of piles in the area.
|
19
|
+
attr_reader :piles
|
20
|
+
|
21
|
+
def initialize(cards=[], piles_num=1)
|
22
|
+
@piles = []
|
23
|
+
@cards = Pile.new(cards)
|
24
|
+
piles_num.times { @piles << Pile.new }
|
25
|
+
end
|
26
|
+
|
27
|
+
# Shows position of the area. The position of the very
|
28
|
+
# first pile in the area, counts as its actual position.
|
29
|
+
def pos
|
30
|
+
piles.first.pos
|
31
|
+
end
|
32
|
+
|
33
|
+
# Sets the position of every pile in the area to the same value.
|
34
|
+
def pos=(pos)
|
35
|
+
piles.each { |pile| pile.pos = *pos }
|
36
|
+
end
|
37
|
+
|
38
|
+
# Collects all cards in every pile and returns the array of these
|
39
|
+
# cards. If there are no cards in the area, returns an empty array.
|
40
|
+
def cards
|
41
|
+
piles.inject([]) { |cards, pile| cards << pile.cards }.flatten
|
42
|
+
end
|
43
|
+
|
44
|
+
# Draws each pile of the area in the window.
|
45
|
+
def draw_on(win)
|
46
|
+
piles.each { |pile| pile.draw_on(win) }
|
47
|
+
end
|
48
|
+
|
49
|
+
# Returns pile in the area, which has been
|
50
|
+
# clicked. If there is no such, returns nil.
|
51
|
+
def hit?(mouse_pos)
|
52
|
+
piles.find { |pile| pile.hit?(mouse_pos) }
|
53
|
+
end
|
54
|
+
|
55
|
+
# Adds card from outer_pile to the very first
|
56
|
+
# pile of the area, flipping it en route.
|
57
|
+
def add_from(outer_pile, card)
|
58
|
+
card.flip! and piles.first << outer_pile.remove(card)
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
module Patience
|
2
|
+
###
|
3
|
+
# Patience::Card is a card creator class. Cards have ranks and suits. Every
|
4
|
+
# card has its own sprite. Sprite is a one big image, which contains every
|
5
|
+
# play card in the game. A sprite for a card is chosen by shift on the sprite.
|
6
|
+
# The shift is determined by integer values on X and Y axis repsectively.
|
7
|
+
# card = Card.new(13, 3)
|
8
|
+
# card.to_s #=> "Ace of Spades"
|
9
|
+
# card.face_down
|
10
|
+
# card.face_up? #=> false
|
11
|
+
#
|
12
|
+
class Card
|
13
|
+
extend Forwardable
|
14
|
+
|
15
|
+
attr_reader :rank, :suit, :sprite
|
16
|
+
|
17
|
+
# Creates new card object. Both arguments should be Fixnums
|
18
|
+
# in the valid ranges. Also, every card has its sprite, which
|
19
|
+
# is nothing but an instance of the Ray::Sprite class.
|
20
|
+
def initialize(rank, suit)
|
21
|
+
@rank = case rank
|
22
|
+
when 1 then Rank::Ace.new
|
23
|
+
when 2 then Rank::Two.new
|
24
|
+
when 3 then Rank::Three.new
|
25
|
+
when 4 then Rank::Four.new
|
26
|
+
when 5 then Rank::Five.new
|
27
|
+
when 6 then Rank::Six.new
|
28
|
+
when 7 then Rank::Seven.new
|
29
|
+
when 8 then Rank::Eight.new
|
30
|
+
when 9 then Rank::Nine.new
|
31
|
+
when 10 then Rank::Ten.new
|
32
|
+
when 11 then Rank::Jack.new
|
33
|
+
when 12 then Rank::Queen.new
|
34
|
+
when 13 then Rank::King.new
|
35
|
+
else
|
36
|
+
raise DefunctRank, "Nonexistent rank: #{rank}"
|
37
|
+
end
|
38
|
+
@suit = case suit
|
39
|
+
when 1 then Suit::Heart.new
|
40
|
+
when 2 then Suit::Diamond.new
|
41
|
+
when 3 then Suit::Spade.new
|
42
|
+
when 4 then Suit::Club.new
|
43
|
+
else
|
44
|
+
raise DefunctSuit, "Nonexistent suit: #{suit}"
|
45
|
+
end
|
46
|
+
@sprite = Ray::Sprite.new path_of('patience/sprites/card_deck.png')
|
47
|
+
# A sheet with 14 columns and 5 rows. First row and column
|
48
|
+
# corresponds to the card back (their coordinats are equal to [0, 0]).
|
49
|
+
@sprite.sheet_size = [14, 5]
|
50
|
+
@sprite.sheet_pos = [rank, suit]
|
51
|
+
end
|
52
|
+
|
53
|
+
# Prints human readable rank and suit of the card.
|
54
|
+
def to_s
|
55
|
+
"#{rank} of #{suit}"
|
56
|
+
end
|
57
|
+
|
58
|
+
# Turns the card to its face.
|
59
|
+
def face_up
|
60
|
+
sprite.sheet_pos = [rank.to_i, suit.to_i]
|
61
|
+
end
|
62
|
+
|
63
|
+
# The opposite of the Card#face_down? method. Returns true
|
64
|
+
# if the card is turned to its face. Otherwise, returns false.
|
65
|
+
def face_up?
|
66
|
+
self.not.face_down?
|
67
|
+
end
|
68
|
+
|
69
|
+
# Turns the card to its back (sets position of the sprite to zero values).
|
70
|
+
def face_down
|
71
|
+
sprite.sheet_pos = [0, 0]
|
72
|
+
end
|
73
|
+
|
74
|
+
# Returns true if the card is turned to its back (it means, that the
|
75
|
+
# sprite of the card is set position of zero). Otherwise, returns false.
|
76
|
+
def face_down?
|
77
|
+
sprite.sheet_pos == [0, 0]
|
78
|
+
end
|
79
|
+
|
80
|
+
# Either turns the card to its face if it's faced
|
81
|
+
# down or turns it to its back if it's faced up.
|
82
|
+
def flip!
|
83
|
+
(face_up if face_down?) or (face_down if face_up?)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Draws the sprite of card in the window.
|
87
|
+
def draw_on(win)
|
88
|
+
win.draw(sprite)
|
89
|
+
end
|
90
|
+
|
91
|
+
# Compares two cards with each other,
|
92
|
+
# considering their rank and suit equality.
|
93
|
+
def eql?(other_card)
|
94
|
+
(@rank == other_card.rank) and (@suit == other_card.suit)
|
95
|
+
end
|
96
|
+
|
97
|
+
def overlaps?(other_card)
|
98
|
+
sprite.collide?(other_card.sprite) and self.not.eql?(other_card)
|
99
|
+
end
|
100
|
+
|
101
|
+
def_delegators :@sprite, :pos, :pos=, :x, :y, :to_rect
|
102
|
+
def_delegator :"@sprite.to_rect", :contain?, :hit?
|
103
|
+
|
104
|
+
class DefunctRank < StandardError; end
|
105
|
+
class DefunctSuit < StandardError; end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require_relative 'core_ext'
|
2
|
+
|
3
|
+
class Class
|
4
|
+
rake_extension("descendants") do
|
5
|
+
|
6
|
+
# Returns the array of all descendants of a class. Although there is
|
7
|
+
# faster way to do this, I want to stick with this solutions, since
|
8
|
+
# it's much simpler to understand and performance isn't crucial for now.
|
9
|
+
#
|
10
|
+
# The example of faster implementation lives in:
|
11
|
+
# rails/activesupport/lib/active_support/descendants_tracker.rb
|
12
|
+
def descendants
|
13
|
+
ObjectSpace.each_object(::Class).select {|klass| klass < self }
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'rake'
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require_relative 'core_ext'
|
2
|
+
|
3
|
+
class Object
|
4
|
+
rake_extension("not") do
|
5
|
+
def not
|
6
|
+
Not.new(self)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
###
|
11
|
+
# The original idea belongs to Jay Fields:
|
12
|
+
# http://blog.jayfields.com/2007/08/ruby-adding-not-method-for-readability.html
|
13
|
+
#
|
14
|
+
# The Object::Not instances are proxies, that send all calls back to the
|
15
|
+
# original instance. The Object::Not class privatizes almost all of its
|
16
|
+
# methods so that most method calls will be handled by method_missing.
|
17
|
+
class Not
|
18
|
+
private *instance_methods.select { |m| m !~ /(^__|^\W|^binding$)/ }
|
19
|
+
|
20
|
+
rake_extension("initialize") do
|
21
|
+
def initialize(subject)
|
22
|
+
@subject = subject
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
rake_extension("method_missing") do
|
27
|
+
|
28
|
+
# Forwards on any method call to the subject and negates the result.
|
29
|
+
def method_missing(sym, *args, &block)
|
30
|
+
!@subject.send(sym, *args, &block)
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require_relative 'core_ext'
|
2
|
+
|
3
|
+
class String
|
4
|
+
rake_extension("demodulize") do
|
5
|
+
|
6
|
+
# Removes the module part from the
|
7
|
+
# constant expression in the string.
|
8
|
+
def demodulize
|
9
|
+
self.to_s.gsub(/^.*::/, '')
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
13
|
+
|
14
|
+
rake_extension("constantize") do
|
15
|
+
|
16
|
+
# Constantize tries to find a declared constant with the name specified
|
17
|
+
# in the string. It raises a NameError when the name is not in CamelCase
|
18
|
+
# or is not initialized.
|
19
|
+
# Example:
|
20
|
+
# "Module".constantize #=> Module
|
21
|
+
# "Class".constantize #=> Class
|
22
|
+
# "yadda".constantize #=> NameError: yadda is not a valid constant name!
|
23
|
+
#
|
24
|
+
def constantize
|
25
|
+
unless /\A(?:::)?([A-Z]\w*(?:::[A-Z]\w*)*)\z/ =~ self
|
26
|
+
raise NameError, "#{self.inspect} is not a valid constant name!"
|
27
|
+
end
|
28
|
+
|
29
|
+
Object.module_eval("::#{$1}", __FILE__, __LINE__)
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|