decktet 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ff11d273759fc3fdc43a12ffbedc2ab20e7343fdbb2de5afe46377689e495bb7
4
+ data.tar.gz: e97a708766a2b31fc965ebdf0fdc0e9e084ea7ca32da94275c6829b9d27a470d
5
+ SHA512:
6
+ metadata.gz: 7730b570fb0238086a2062d5c7c41c25aa0e584f966e74078742da37c3fa909fde474335479b3b912576d9a9f69478f5721b40b9f6f1a0f00cc752bdd782cedf
7
+ data.tar.gz: 5bfcf4e06c655fd202967e8c373449388e1a9567e4e22c8b300112e9e06c5df9acfd445603d7495ea1617469f0ca70ec9ef5f2a2d35daf0808967237e22a493f
data/.gitignore ADDED
@@ -0,0 +1,15 @@
1
+ /.bundle/
2
+ /.idea/
3
+ /.yardoc
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+
11
+ # not tracking Gemfile.lock in source control for gem
12
+ Gemfile.lock
13
+
14
+ # rspec failure tracking
15
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format progress
2
+ --color
3
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "https://rubygems.org"
2
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 Brody Fischer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,125 @@
1
+ # Decktet
2
+
3
+ This gem provides a basic interface to the Decktet playing card system. Visit [decktet.com](https://www.decktet.com/) for an overview of the Decktet. More detailed information, including many games that can be played with this unique deck, can be found on [The Decktet Wiki](http://decktet.wikidot.com/). This gem is meant as a core library for manipulating the elements of the Decktet itself. As such, it does not include any game engine or other such advanced functionality.
4
+
5
+ ## Getting Started
6
+
7
+ Include `decktet` in your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'decktet'
11
+ ```
12
+
13
+ Then run `bundle install`.
14
+
15
+ Alternatively, you can install it manually by running `gem install decktet`.
16
+
17
+ ## Usage
18
+
19
+ The decktet gem provides 3 classes: `Decktet::Card`, `Decktet::Deck`, and `Decktet::Pile`
20
+
21
+ A general use case includes creating a new `Decktet::Deck` and moving cards between the deck and various piles.
22
+
23
+ Here is an example of a simple game setup:
24
+
25
+ ```ruby
26
+ deck = Decktet::Deck.new_basic_deck
27
+ deck.shuffle
28
+
29
+ player1_hand = deck.draw 3
30
+ player2_hand = deck.draw 3
31
+
32
+ discard_pile = Decktet::Pile.new(deck.draw)
33
+ ```
34
+
35
+ ### Card
36
+
37
+ The `Decktet::Card` class is an immutable data container that provides some convenience methods for accessing card information.
38
+
39
+ All of the cards in the extended deck are defined by top-level constants in the `Decktet` namespace. Additional constants are defined for specific groups of cards based on card attributes, such as `Decktet::ACES`, `Decktet::MOONS`, and `Decktet::PERSONALITIES`.
40
+
41
+ ```ruby
42
+ card = Decktet::THE_SAVAGE
43
+ card.name # => "the SAVAGE"
44
+ card.excuse? # => false
45
+
46
+ card.rank # => 3
47
+ card.number? # => true
48
+ card.ace? # => false
49
+
50
+ card.suits # => [:leaves, :wyrms]
51
+ card.leaves? # => true
52
+ card.leaf? # => true
53
+ card.moons? # => false
54
+
55
+ card.types # => [:personality]
56
+ card.personality? # => true
57
+ card.location? # => false
58
+ ```
59
+
60
+ ### Pile
61
+
62
+ The `Decktet::Pile` class defines a collection of cards that can be manipulated.
63
+ #### Instantiation
64
+
65
+ A pile can be instantiated with any number of `Decktet::Card` objects or empty. Any combination of cards, card groups, and deck templates are valid arguments for instantiation.
66
+
67
+ ```ruby
68
+ p1 = Decktet::Pile.new
69
+ p2 = Decktet::Pile.new(Decktet::THE_ACE_OF_MOONS)
70
+ p3 = Decktet::Pile.new(Decktet::ACES, Decktet::CROWNS)
71
+ p4 = Decktet::Pile.new(Decktet::DeckTemplates::BASIC_DECK, Decktet::PAWNS, Decktet::THE_EXCUSE)
72
+ ```
73
+
74
+ #### Methods
75
+
76
+ The `cards` method returns a duplicated array of all cards currently in the pile. Any operations on the return value of this method that would alter it in some way will have no effect on the collection of cards managed internally by the pile object.
77
+
78
+ The `size` method returns the count of cards in the pile.
79
+
80
+ The `shuffle` method randomizes the order of cards in the pile. This method returns `self`.
81
+
82
+ The `cut` method will move some number of cards from the top of the pile to the bottom of the pile, preserving order. This method accepts an argument indicating the number of cards to move. If no argument is provided, the number of cards moved will be randomly selected. This method returns `self`.
83
+
84
+ The `draw` method removes some number of cards from the pile and returns them in an array. This method accepts an argument indicating the number of cards to draw. If no argument is provided, one card wil be drawn.
85
+
86
+ The `add` method places cards on the pile. This method accepts any number of `Decktet::Card` objects. This method returns `self`.
87
+
88
+
89
+ ```ruby
90
+
91
+ p = Decktet::Pile.new(card_1, card_2, card_3, card_4)
92
+
93
+ p.cards # => [card_1, card_2, card_3, card_4]
94
+
95
+ p.size # => 4
96
+
97
+ p.shuffle # => p
98
+ p.cards # => [card_4, card_1, card_3, card_2]
99
+
100
+ p.cut(1) # => p
101
+ p.cards # => [card_1, card_3, card_2, card_4]
102
+
103
+ p.draw(2) # => [card_1, card_3]
104
+ p.cards # => [card_2, card_4]
105
+
106
+ p.add(card_5) # => p
107
+ p.cards # => [card_5, card_1, card_3]
108
+ ```
109
+
110
+ ### Deck
111
+
112
+ The `Decktet::Deck` class is a subclass of `Decktet::Pile` that does not allow instantiation with an empty set.
113
+
114
+ Additionally, `Decktet::Deck` defines some helper methods for instantiating a new deck based on templates defined in `Decktet::DeckTemplates`.
115
+
116
+ ```ruby
117
+ Decktet::Deck.new_basic_deck
118
+
119
+ # The above is equivalent to:
120
+ Decktet::Deck.new(Decktet::DeckTemplates::BASIC_DECK)
121
+ ```
122
+
123
+ ## License
124
+
125
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ require "bundler/setup"
3
+ require "decktet"
4
+ require "irb"
5
+ IRB.start(__FILE__)
data/decktet.gemspec ADDED
@@ -0,0 +1,21 @@
1
+ require_relative 'lib/decktet/version'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = 'decktet'
5
+ spec.version = Decktet::VERSION
6
+ spec.authors = ['Brody Fischer']
7
+ spec.email = ['brodyf42@gmail.com']
8
+ spec.summary = 'This gem provides an interface to the versatile decktet playing card system.'
9
+ spec.homepage = 'https://github.com/brodyf42/decktet'
10
+ spec.license = 'MIT'
11
+
12
+ spec.required_ruby_version = '>= 3.0.0'
13
+
14
+ # Specify which files should be added to the gem when it is released.
15
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
16
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
17
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ end
19
+
20
+ spec.add_development_dependency 'rspec'
21
+ end
@@ -0,0 +1,111 @@
1
+ module Decktet
2
+ RANKS = ([:ace] + Array(2..9) + %i[pawn court crown]).freeze
3
+ SUITS = %i[moons suns waves leaves wyrms knots].freeze
4
+ TYPES = %i[location personality event].freeze
5
+
6
+ Card = Data.define(:name, :rank, :suits, :types) do
7
+ def initialize(name:, rank: nil, suits: nil, types: nil)
8
+ super(name: name.freeze, rank:, suits: Array(suits).freeze, types: Array(types).freeze)
9
+ end
10
+
11
+ def excuse? = rank.nil? && suits.empty? && types.empty?
12
+ def number? = Integer === rank
13
+
14
+ RANKS.select { |r| Symbol === r }.each { |rank| define_method(:"#{rank}?") { self.rank == rank } }
15
+ SUITS.each { |suit| define_method(:"#{suit}?") { suits.include?(suit) } }
16
+ TYPES.each { |type| define_method(:"#{type}?") { types.include?(type) } }
17
+
18
+ alias_method :moon?, :moons?
19
+ alias_method :sun?, :suns?
20
+ alias_method :wave?, :waves?
21
+ alias_method :leaf?, :leaves?
22
+ alias_method :wyrm?, :wyrms?
23
+ alias_method :knot?, :knots?
24
+ end
25
+
26
+ THE_EXCUSE = Card.new('the EXCUSE')
27
+
28
+ THE_ACE_OF_MOONS = Card.new('the ACE of MOONS', :ace, :moons)
29
+ THE_ACE_OF_SUNS = Card.new('the ACE of SUNS', :ace, :suns)
30
+ THE_ACE_OF_WAVES = Card.new('the ACE of WAVES', :ace, :waves)
31
+ THE_ACE_OF_LEAVES = Card.new('the ACE of LEAVES', :ace, :leaves)
32
+ THE_ACE_OF_WYRMS = Card.new('the ACE of WYRMS', :ace, :wyrms)
33
+ THE_ACE_OF_KNOTS = Card.new('the ACE of KNOTS', :ace, :knots)
34
+
35
+ THE_AUTHOR = Card.new('the AUTHOR', 2, %i[moons knots], :personality)
36
+ THE_DESERT = Card.new('the DESERT', 2, %i[suns wyrms], :location)
37
+ THE_ORIGIN = Card.new('the ORIGIN', 2, %i[waves leaves], %i[location event])
38
+ THE_JOURNEY = Card.new('the JOURNEY', 3, %i[moons waves], :event)
39
+ THE_PAINTER = Card.new('the PAINTER', 3, %i[suns knots], :personality)
40
+ THE_SAVAGE = Card.new('the SAVAGE', 3, %i[leaves wyrms], :personality)
41
+ THE_MOUNTAIN = Card.new('the MOUNTAIN', 4, %i[moons suns], :location)
42
+ THE_SAILOR = Card.new('the SAILOR', 4, %i[waves leaves], :personality)
43
+ THE_BATTLE = Card.new('the BATTLE', 4, %i[wyrms knots], :event)
44
+ THE_FOREST = Card.new('the FOREST', 5, %i[moons leaves], :location)
45
+ THE_DISCOVERY = Card.new('the DISCOVERY', 5, %i[suns waves], :event)
46
+ THE_SOLDIER = Card.new('the SOLDIER', 5, %i[wyrms knots], :personality)
47
+ THE_LUNATIC = Card.new('the LUNATIC', 6, %i[moons waves], :personality)
48
+ THE_PENITENT = Card.new('the PENITENT', 6, %i[suns wyrms], :personality)
49
+ THE_MARKET = Card.new('the MARKET', 6, %i[leaves knots], %i[location event])
50
+ THE_CHANCE_MEETING = Card.new('the CHANCE MEETING', 7, %i[moons leaves], :event)
51
+ THE_CASTLE = Card.new('the CASTLE', 7, %i[suns knots], :location)
52
+ THE_CAVE = Card.new('the CAVE', 7, %i[waves wyrms], :location)
53
+ THE_DIPLOMAT = Card.new('the DIPLOMAT', 8, %i[moons suns], :personality)
54
+ THE_MILL = Card.new('the MILL', 8, %i[waves leaves], :location)
55
+ THE_BETRAYAL = Card.new('the BETRAYAL', 8, %i[wyrms knots], :event)
56
+ THE_PACT = Card.new('the PACT', 9, %i[moons suns], :event)
57
+ THE_DARKNESS = Card.new('the DARKNESS', 9, %i[waves wyrms], :location)
58
+ THE_MERCHANT = Card.new('the MERCHANT', 9, %i[leaves knots], :personality)
59
+
60
+ THE_HARVEST = Card.new('the HARVEST', :pawn, %i[moons suns leaves], :event)
61
+ THE_WATCHMAN = Card.new('the WATCHMAN', :pawn, %i[moons wyrms knots], :personality)
62
+ THE_LIGHT_KEEPER = Card.new('the LIGHT KEEPER', :pawn, %i[suns waves knots], :personality)
63
+ THE_BORDERLAND = Card.new('the BORDERLAND', :pawn, %i[waves leaves wyrms], :location)
64
+
65
+ THE_CONSUL = Card.new('the CONSUL', :court, %i[moons waves knots], :personality)
66
+ THE_RITE = Card.new('the RITE', :court, %i[moons leaves wyrms], :event)
67
+ THE_ISLAND = Card.new('the ISLAND', :court, %i[suns waves wyrms], :location)
68
+ THE_WINDOW = Card.new('the WINDOW', :court, %i[suns leaves knots], :location)
69
+
70
+ THE_HUNTRESS = Card.new('the HUNTRESS', :crown, :moons, :personality)
71
+ THE_BARD = Card.new('the BARD', :crown, :suns, :personality)
72
+ THE_SEA = Card.new('the SEA', :crown, :waves, :location)
73
+ THE_END = Card.new('the END', :crown, :leaves, %i[location event])
74
+ THE_CALAMITY = Card.new('the CALAMITY', :crown, :wyrms, :event)
75
+ THE_WINDFALL = Card.new('the WINDFALL', :crown, :knots, :event)
76
+
77
+ Card.class_eval { private_class_method :new }
78
+
79
+ CARDS = [
80
+ THE_EXCUSE,
81
+ THE_ACE_OF_MOONS, THE_ACE_OF_SUNS, THE_ACE_OF_WAVES, THE_ACE_OF_LEAVES, THE_ACE_OF_WYRMS, THE_ACE_OF_KNOTS,
82
+ THE_AUTHOR, THE_DESERT, THE_ORIGIN, # rank 2
83
+ THE_JOURNEY, THE_PAINTER, THE_SAVAGE, # rank 3
84
+ THE_MOUNTAIN, THE_SAILOR, THE_BATTLE, # rank 4
85
+ THE_FOREST, THE_DISCOVERY, THE_SOLDIER, # rank 5
86
+ THE_LUNATIC, THE_PENITENT, THE_MARKET, # rank 6
87
+ THE_CHANCE_MEETING, THE_CASTLE, THE_CAVE, # rank 7
88
+ THE_DIPLOMAT, THE_MILL, THE_BETRAYAL, # rank 8
89
+ THE_PACT, THE_DARKNESS, THE_MERCHANT, # rank 9
90
+ THE_HARVEST, THE_WATCHMAN, THE_LIGHT_KEEPER, THE_BORDERLAND, # pawns
91
+ THE_CONSUL, THE_RITE, THE_ISLAND, THE_WINDOW, # courts
92
+ THE_HUNTRESS, THE_BARD, THE_SEA, THE_END, THE_CALAMITY, THE_WINDFALL, # crowns
93
+ ].freeze
94
+
95
+ ACES = CARDS.select(&:ace?).freeze
96
+ NUMBERS = CARDS.select(&:number?).freeze
97
+ PAWNS = CARDS.select(&:pawn?).freeze
98
+ COURTS = CARDS.select(&:court?).freeze
99
+ CROWNS = CARDS.select(&:crown?).freeze
100
+
101
+ MOONS = CARDS.select(&:moons?).freeze
102
+ SUNS = CARDS.select(&:suns?).freeze
103
+ WAVES = CARDS.select(&:waves?).freeze
104
+ LEAVES = CARDS.select(&:leaves?).freeze
105
+ WYRMS = CARDS.select(&:wyrms?).freeze
106
+ KNOTS = CARDS.select(&:knots?).freeze
107
+
108
+ LOCATIONS = CARDS.select(&:location?).freeze
109
+ PERSONALITIES = CARDS.select(&:personality?).freeze
110
+ EVENTS = CARDS.select(&:event?).freeze
111
+ end
@@ -0,0 +1,24 @@
1
+ module Decktet
2
+ module DeckTemplates
3
+ BASIC_DECK = [ACES, NUMBERS, CROWNS].flatten
4
+ BASIC_DECK_WITH_EXCUSE = [THE_EXCUSE] + BASIC_DECK
5
+ EXTENDED_DECK = BASIC_DECK_WITH_EXCUSE + [PAWNS, COURTS].flatten
6
+ DOUBLE_DECK = BASIC_DECK + BASIC_DECK
7
+ end
8
+
9
+ class Deck < Pile
10
+ def initialize(*cards)
11
+ @cards = cards.flatten
12
+
13
+ unless @cards.any? && @cards.all?{|card| Decktet::Card === card }
14
+ raise ArgumentError.new("A deck can only be initialized with Decktet::Card objects. Deck templates defined in Decktet::DeckTemplates and card groups or specific cards are accepted.")
15
+ end
16
+ end
17
+
18
+ class << self
19
+ DeckTemplates.constants.each do |template|
20
+ define_method("new_#{template}".downcase) { new DeckTemplates.const_get(template) }
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,49 @@
1
+ module Decktet
2
+ class Pile
3
+ def initialize(*cards)
4
+ @cards = cards.flatten.compact
5
+
6
+ unless cards.empty? || @cards.all? { |card| Decktet::Card === card }
7
+ raise ArgumentError.new('A pile must be initialized empty or with an array of cards or card groups')
8
+ end
9
+ end
10
+
11
+ def cards = @cards.dup
12
+
13
+ def size = @cards.size
14
+
15
+ def shuffle
16
+ @cards.shuffle!
17
+ self
18
+ end
19
+
20
+ def cut(depth=nil)
21
+ return self if @cards.empty?
22
+
23
+ unless depth.nil?
24
+ raise ArgumentError.new('cut depth cannot be negative') if depth < 0
25
+ raise ArgumentError.new('cut depth cannot be greater than the number of cards') if depth > @cards.count
26
+ end
27
+
28
+ depth ||= rand(1...@cards.count)
29
+ @cards.rotate!(depth)
30
+ self
31
+ end
32
+
33
+ def draw(count=1)
34
+ unless count.between?(0,@cards.count)
35
+ raise ArgumentError.new('draw amount cannot be negative or greater than the number of cards')
36
+ end
37
+ @cards.shift(count)
38
+ end
39
+
40
+ def add(*new_cards)
41
+ new_cards = new_cards.flatten
42
+ unless new_cards.all? { |card| Decktet::Card === card }
43
+ raise ArgumentError, "Only Decktet::Card objects can be added to a #{self.class.name}"
44
+ end
45
+ @cards.unshift(*new_cards)
46
+ self
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,3 @@
1
+ module Decktet
2
+ VERSION = "0.1.0"
3
+ end
data/lib/decktet.rb ADDED
@@ -0,0 +1,8 @@
1
+ require 'decktet/version'
2
+ require 'decktet/card'
3
+ require 'decktet/pile'
4
+ require 'decktet/deck'
5
+
6
+ module Decktet
7
+ Error = Class.new StandardError
8
+ end
metadata ADDED
@@ -0,0 +1,70 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: decktet
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Brody Fischer
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-06-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ description:
28
+ email:
29
+ - brodyf42@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - ".gitignore"
35
+ - ".rspec"
36
+ - Gemfile
37
+ - LICENSE.txt
38
+ - README.md
39
+ - Rakefile
40
+ - bin/console
41
+ - decktet.gemspec
42
+ - lib/decktet.rb
43
+ - lib/decktet/card.rb
44
+ - lib/decktet/deck.rb
45
+ - lib/decktet/pile.rb
46
+ - lib/decktet/version.rb
47
+ homepage: https://github.com/brodyf42/decktet
48
+ licenses:
49
+ - MIT
50
+ metadata: {}
51
+ post_install_message:
52
+ rdoc_options: []
53
+ require_paths:
54
+ - lib
55
+ required_ruby_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: 3.0.0
60
+ required_rubygems_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ requirements: []
66
+ rubygems_version: 3.5.22
67
+ signing_key:
68
+ specification_version: 4
69
+ summary: This gem provides an interface to the versatile decktet playing card system.
70
+ test_files: []