coop_al 1.0.1

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 (46) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +7 -0
  3. data/.rubocop.yml +9 -0
  4. data/Gemfile +3 -0
  5. data/README.md +89 -0
  6. data/Rakefile +10 -0
  7. data/coop.gemspec +29 -0
  8. data/examples/filler.rb +9 -0
  9. data/examples/test.rb +15 -0
  10. data/exe/coop +131 -0
  11. data/lib/coop_al/adventure.rb +52 -0
  12. data/lib/coop_al/adventure_generator.rb +35 -0
  13. data/lib/coop_al/bestiary.rb +51 -0
  14. data/lib/coop_al/bestiary_generator.rb +3 -0
  15. data/lib/coop_al/bestiary_populator.rb +14 -0
  16. data/lib/coop_al/chapter.rb +51 -0
  17. data/lib/coop_al/chapter_generator.rb +32 -0
  18. data/lib/coop_al/encounter.rb +148 -0
  19. data/lib/coop_al/encounter_generator.rb +67 -0
  20. data/lib/coop_al/exception.rb +7 -0
  21. data/lib/coop_al/item.rb +30 -0
  22. data/lib/coop_al/library.rb +54 -0
  23. data/lib/coop_al/library_generator.rb +4 -0
  24. data/lib/coop_al/loot.rb +49 -0
  25. data/lib/coop_al/loot_generator.rb +42 -0
  26. data/lib/coop_al/monster.rb +19 -0
  27. data/lib/coop_al/monster_definition.rb +18 -0
  28. data/lib/coop_al/path.rb +65 -0
  29. data/lib/coop_al/path_follower.rb +34 -0
  30. data/lib/coop_al/random_encounter.rb +30 -0
  31. data/lib/coop_al/random_encounter_generator.rb +37 -0
  32. data/lib/coop_al/session.rb +55 -0
  33. data/lib/coop_al/session_date_generator.rb +43 -0
  34. data/lib/coop_al/session_encounter.rb +76 -0
  35. data/lib/coop_al/session_log.rb +51 -0
  36. data/lib/coop_al/state.rb +85 -0
  37. data/lib/coop_al/state_reporter.rb +72 -0
  38. data/lib/coop_al/trace.rb +29 -0
  39. data/lib/coop_al/treasure.rb +21 -0
  40. data/lib/coop_al/treasure_tables.rb +28 -0
  41. data/lib/coop_al/value.rb +167 -0
  42. data/lib/coop_al/version.rb +3 -0
  43. data/lib/coop_al/xp.rb +86 -0
  44. data/lib/coop_al.rb +34 -0
  45. data/res/srd.rb +400 -0
  46. metadata +188 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 940d2406792da127d6db1a622ff9fe6d7dfae655
4
+ data.tar.gz: 742e038d58588e7b58634201f6ae371f83fa2b47
5
+ SHA512:
6
+ metadata.gz: 49ac99bf96894e7162ff25da3e4124511d4f2cce35d18131d610b674f8435327b9955bd13817326c2bd32885ac48ca44714df20960f61fd62669126648fbc4eb
7
+ data.tar.gz: fcb213c0563ecc6a93f03b9b4555650b4df458bf2a3ab8c51406f23a0ba20adc2095adb524798d06b3aa8363c56f9fa55dcd982da5b7042f392355b21cae40a9
data/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ *.swp
2
+ *.swo
3
+ /.bundle/
4
+ /Gemfile.lock
5
+ /coverage/
6
+ /pkg
7
+ /*.gem
data/.rubocop.yml ADDED
@@ -0,0 +1,9 @@
1
+ Metrics/LineLength:
2
+ Enabled: false
3
+ Metrics/ModuleLength:
4
+ Enabled: false
5
+ Metrics/BlockLength:
6
+ Enabled: true
7
+ Exclude:
8
+ - res/*.rb
9
+ - spec/**/*.rb
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # Coop - Adventurers League Character Log Generator
2
+
3
+ > "Reality is frequently inaccurate." -- Douglas Adams
4
+
5
+ Coop is a little tool that uses simple DSL-specified Dungeons and Dragons adventures to generate a log of pretend gameplay to support an Adventurers League character.
6
+
7
+ It includes SRD content, but nothing else. You'll have to supply your own copy of WoTC material.
8
+
9
+ ## Installation
10
+
11
+ $> gem install coop_al
12
+
13
+ ## Usage
14
+
15
+ $> coop --help
16
+ coop [options] resource [resource...]
17
+ --path <path1>[,<path2>,...] Specify path to follow
18
+ --path-file <filename> Specify paths in a file, one per line
19
+ --no-loot Do not report loot
20
+ --no-xp Do not report accumulated XP
21
+ --no-paths Do not report available paths
22
+ --party-size n Total XP and treasure is divided by n [6]
23
+ --encounter-count n Encounters per session [10]
24
+ --end-date YYYY-MM-DD Most recent session [today]
25
+ --session-frequency n Sessions every n days [7]
26
+ --blackout-dates YYYY-MM-DD[,YYYY-MM-DD,...]
27
+ --skip-frequency n Chance of skipping week is 1 in n [never]
28
+ --srd Use included SRD file for monsters
29
+ --trace Print encounter details
30
+ --help Print this help
31
+
32
+ ## Customization
33
+
34
+ Check out the examples provided.
35
+
36
+ ### Bestiary
37
+
38
+ Create your own bestiary by providing a resource file in the form:
39
+
40
+ bestiary do
41
+ add :scary_monster, :cr1_2
42
+ add :scarier_monster, :cr17
43
+ end
44
+
45
+ ### Adventures
46
+
47
+ Create your own adventures by providing a resource file in the form:
48
+
49
+ adventure :shortname, 'Adventure Name' do
50
+ entry :chapter_1
51
+ chapter :chapter_1, 'Chapter 1. Surrounding Area' do
52
+ 5.times do
53
+ random 'Random Encounter' do
54
+ f(1..2) { monster :scary_monster }
55
+ f(3..20) { monsters '3d6', :scarier_monster }
56
+ end
57
+ end
58
+ encounter 'Some old weak guy' do
59
+ npc :cr0
60
+ treasure 2000.gp + 400.pp
61
+ treasure 1000.gp, 'diamond'
62
+ item 'Vorpal sword'
63
+ end
64
+ encounter 'Bag of XP' do
65
+ xp 500
66
+ end
67
+ link_to :chapter_2
68
+ end
69
+ chapter :chapter_2, 'Chapter 2. The Caves' do
70
+ ...
71
+ link_to_downtime
72
+ end
73
+ end
74
+
75
+ ## Development
76
+
77
+ Coop is written in Ruby, and requires the [Tablescript gem](https://github.com/jamiehale/tablescript.rb).
78
+
79
+ ### Tests
80
+
81
+ Such as they are...
82
+
83
+ $> rake spec
84
+
85
+ Coverage provided by simplecov.
86
+
87
+ ### Rubocop
88
+
89
+ $> rake rubocop
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
7
+
8
+ task :rubocop do
9
+ sh 'bundle exec rubocop'
10
+ end
data/coop.gemspec ADDED
@@ -0,0 +1,29 @@
1
+ lib = File.expand_path('../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'coop_al/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'coop_al'
7
+ spec.version = CoopAl::VERSION
8
+ spec.authors = ['Jamie Hale']
9
+ spec.email = ['jamie@smallarmyofnerds.com']
10
+ spec.summary = 'Adventurers League Character Log Generator'
11
+ spec.description = 'Tool for automated character growth through predefined adventures'
12
+ spec.homepage = 'https://github.com/jamiehale/coop'
13
+ spec.license = 'GPL-3.0'
14
+
15
+ raise 'RubyGems 2.0 or newer is required to protect against public gem pushes.' unless spec.respond_to?(:metadata)
16
+ spec.metadata['allowed_push_host'] = 'https://rubygems.org'
17
+
18
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
19
+ spec.bindir = 'exe'
20
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
+ spec.require_paths = ['lib']
22
+ spec.add_dependency 'tablescript', '0.0.3'
23
+ spec.add_development_dependency 'bundler', '~> 1.14'
24
+ spec.add_development_dependency 'rake', '~> 12.0'
25
+ spec.add_development_dependency 'rspec', '~> 3.5'
26
+ spec.add_development_dependency 'cucumber', '~> 2.4'
27
+ spec.add_development_dependency 'simplecov', '~> 0.13'
28
+ spec.add_development_dependency 'rubocop', '~> 0.47'
29
+ end
@@ -0,0 +1,9 @@
1
+ adventure :filler, 'Filler' do
2
+ entry :chapter_1
3
+ chapter :chapter_1, 'Chapter 1. Filler' do
4
+ encounter 'Bag of XP' do
5
+ xp 4000 * 6
6
+ end
7
+ link_to_downtime
8
+ end
9
+ end
data/examples/test.rb ADDED
@@ -0,0 +1,15 @@
1
+ adventure :test, 'Simple Adventure' do
2
+ entry :chapter_1
3
+ chapter :chapter_1, 'Chapter 1' do
4
+ encounter '1. Entrance' do
5
+ monster :goblin
6
+ monsters 2, :orc
7
+ treasure '2d6*10'.gp
8
+ item 'Potion of healing'
9
+ end
10
+ link_to :chapter_2
11
+ link_to_downtime
12
+ end
13
+ chapter :chapter_2, 'Chapter 2' do
14
+ end
15
+ end
data/exe/coop ADDED
@@ -0,0 +1,131 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ require 'tablescript'
4
+ require 'coop_al'
5
+ require 'date'
6
+
7
+ def version
8
+ puts "Coop v#{CoopAl::VERSION} - \"Reality is frequently inaccurate.\""
9
+ exit
10
+ end
11
+
12
+ def display_message(message)
13
+ puts
14
+ puts message
15
+ end
16
+
17
+ def usage(message = nil)
18
+ puts 'coop [options] resource [resource...]'
19
+ puts ' --path <path1>[,<path2>,...] Specify path to follow'
20
+ puts ' --path-file <filename> Specify paths in a file, one per line'
21
+ puts ' --no-loot Do not report loot'
22
+ puts ' --no-xp Do not report accumulated XP'
23
+ puts ' --no-paths Do not report available paths'
24
+ puts ' --party-size n Total XP and treasure is divided by n [6]'
25
+ puts ' --encounter-count n Encounters per session [10]'
26
+ puts ' --end-date YYYY-MM-DD Most recent session [today]'
27
+ puts ' --session-frequency n Sessions every n days [7]'
28
+ puts ' --blackout-dates YYYY-MM-DD[,YYYY-MM-DD,...]'
29
+ puts ' --skip-frequency n Chance of skipping week is 1 in n [never]'
30
+ puts ' --srd Use included SRD file for monsters'
31
+ puts ' --trace Print encounter details'
32
+ puts ' --help Print this help'
33
+ display_message(message) unless message.nil?
34
+ exit
35
+ end
36
+
37
+ resource_files = []
38
+ paths = []
39
+ options = {
40
+ party_size: 6,
41
+ encounter_count: 10,
42
+ end_date: Date.today,
43
+ session_frequency: 7,
44
+ blackout_dates: []
45
+ }
46
+ until ARGV.empty?
47
+ parameter = ARGV.shift
48
+ case parameter
49
+ when '--help'
50
+ usage
51
+ when '--version'
52
+ version
53
+ when '--path'
54
+ usage unless paths.empty?
55
+ parameter = ARGV.shift
56
+ usage if parameter.nil?
57
+ paths = parameter.split(',').map { |p| CoopAl::Path.parse(p) }
58
+ when '--path-file'
59
+ usage unless paths.empty?
60
+ parameter = ARGV.shift
61
+ usage if parameter.nil?
62
+ File.open(parameter, 'r') do |f|
63
+ f.each_line do |line|
64
+ paths << CoopAl::Path.parse(line.strip)
65
+ end
66
+ end
67
+ when '--no-loot'
68
+ options[:show_loot] = false
69
+ when '--no-xp'
70
+ options[:show_xp] = false
71
+ when '--no-paths'
72
+ options[:show_paths] = false
73
+ when '--party-size'
74
+ parameter = ARGV.shift
75
+ usage if parameter.nil?
76
+ options[:party_size] = parameter.to_i
77
+ when '--encounter-count'
78
+ parameter = ARGV.shift
79
+ usage if parameter.nil?
80
+ options[:encounter_count] = parameter.to_i
81
+ when '--end-date'
82
+ parameter = ARGV.shift
83
+ usage if parameter.nil?
84
+ options[:end_date] = Date.parse(parameter)
85
+ when '--session-frequency'
86
+ parameter = ARGV.shift
87
+ usage if parameter.nil?
88
+ options[:session_frequency] = parameter.to_i
89
+ when '--blackout-dates'
90
+ parameter = ARGV.shift
91
+ usage if parameter.nil?
92
+ options[:blackout_dates] = parameter.split(',').map { |d| Date.parse(d) }
93
+ when '--skip-frequency'
94
+ parameter = ARGV.shift
95
+ usage if parameter.nil?
96
+ options[:skip_frequency] = parameter.to_i
97
+ usage('Invalid skip frequency') if options[:skip_frequency] < 1
98
+ when '--srd'
99
+ resource_files << File.expand_path('../res/srd.rb', File.dirname(__FILE__))
100
+ when '--trace'
101
+ CoopAl::Trace.instance.tracing = true
102
+ else
103
+ resource_files << parameter
104
+ end
105
+ end
106
+
107
+ usage('No resources specified') if resource_files.empty?
108
+ begin
109
+ resource_files.each do |filename|
110
+ require filename
111
+ end
112
+ rescue StandardError => e
113
+ puts e
114
+ exit
115
+ end
116
+
117
+ date_generator = CoopAl::SessionDateGenerator.new(options)
118
+ log = CoopAl::SessionLog.new(date_generator, options)
119
+
120
+ state = CoopAl::State.new
121
+ follower = CoopAl::PathFollower.new(CoopAl::Library.instance, state)
122
+ begin
123
+ follower.follow(paths, log)
124
+ rescue StandardError => e
125
+ puts e
126
+ exit
127
+ end
128
+
129
+ CoopAl::StateReporter.new(state, CoopAl::Library.instance, options).report(STDOUT)
130
+
131
+ log.dump(STDOUT)
@@ -0,0 +1,52 @@
1
+ module CoopAl
2
+ ##
3
+ # Adventure
4
+ #
5
+ class Adventure
6
+ attr_reader :name, :description, :chapters
7
+
8
+ def initialize(name, description)
9
+ @name = name
10
+ @description = description
11
+ @entries = []
12
+ @chapters = {}
13
+ end
14
+
15
+ def add_entry(path)
16
+ @entries << path
17
+ end
18
+
19
+ def add_chapter(chapter)
20
+ @chapters[chapter.name] = chapter
21
+ end
22
+
23
+ def all_chapter_names
24
+ @chapters.keys
25
+ end
26
+
27
+ def chapter_by_path(path)
28
+ @chapters[path.chapter]
29
+ end
30
+
31
+ def chapter_paths
32
+ @chapters.values.map(&:absolute_path)
33
+ end
34
+
35
+ def all_entries
36
+ @entries.map { |e| Path.absolute(@name, e) }
37
+ end
38
+
39
+ def chapter?(name)
40
+ @chapters.key?(name)
41
+ end
42
+
43
+ def chapter(name)
44
+ raise "Chapter (#{name}) not found" unless @chapters.key?(name)
45
+ @chapters[name]
46
+ end
47
+
48
+ def full_name
49
+ @description
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,35 @@
1
+ module CoopAl
2
+ ##
3
+ # AdventureGenerator
4
+ #
5
+ class AdventureGenerator
6
+ attr_reader :adventure
7
+
8
+ def initialize(name, description, bestiary)
9
+ initialize_adventure(name, description)
10
+ @bestiary = bestiary
11
+ end
12
+
13
+ def entry(name)
14
+ @adventure.add_entry(name)
15
+ end
16
+
17
+ def chapter(name, description, &blk)
18
+ generator = ChapterGenerator.new(name, description, @adventure, @bestiary)
19
+ generator.instance_eval(&blk)
20
+ @adventure.add_chapter(generator.chapter)
21
+ end
22
+
23
+ private
24
+
25
+ def initialize_adventure(name, description)
26
+ if Library.instance.adventure?(name)
27
+ @adventure = Library.instance.adventure(name)
28
+ else
29
+ raise "Description required for adventure (#{name})" if description.nil?
30
+ @adventure = Adventure.new(name, description)
31
+ Library.instance.add_adventure(@adventure)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,51 @@
1
+ require 'singleton'
2
+
3
+ module CoopAl
4
+ ##
5
+ # Bestiary
6
+ #
7
+ class Bestiary
8
+ include Singleton
9
+
10
+ def initialize
11
+ @monsters = {}
12
+ @xp_lookup = XpRewardTable.new
13
+ @loot_generator = LootGenerator.new
14
+ end
15
+
16
+ def empty?
17
+ @monsters.empty?
18
+ end
19
+
20
+ def add(monster)
21
+ @monsters[monster.id] = monster
22
+ end
23
+
24
+ def monster?(id)
25
+ @monsters.key?(id)
26
+ end
27
+
28
+ def monster(id)
29
+ @monsters[id]
30
+ end
31
+
32
+ def create(id, treasure, encounter)
33
+ raise "Invalid monster ID (#{id})" unless @monsters.key?(id)
34
+ monster = @monsters[id]
35
+ xp = calculate_xp(monster)
36
+ loot = generate_loot(monster, treasure)
37
+ Monster.new(id, xp, loot, encounter)
38
+ end
39
+
40
+ private
41
+
42
+ def calculate_xp(monster)
43
+ @xp_lookup[monster.cr]
44
+ end
45
+
46
+ def generate_loot(monster, treasure)
47
+ return @loot_generator.generate(monster.cr, monster.treasure) if treasure == :default
48
+ @loot_generator.generate(monster.cr, treasure)
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,3 @@
1
+ def bestiary(&blk)
2
+ CoopAl::BestiaryPopulator.new(&blk)
3
+ end
@@ -0,0 +1,14 @@
1
+ module CoopAl
2
+ ##
3
+ # BestiaryPopulator
4
+ #
5
+ class BestiaryPopulator
6
+ def initialize(&blk)
7
+ instance_eval(&blk)
8
+ end
9
+
10
+ def add(id, cr, treasure = :individual)
11
+ Bestiary.instance.add(MonsterDefinition.new(id, cr, treasure))
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,51 @@
1
+ module CoopAl
2
+ ##
3
+ # Chapter
4
+ #
5
+ class Chapter
6
+ attr_reader :name, :description, :encounters, :links
7
+
8
+ def initialize(name, description, adventure)
9
+ @name = name
10
+ @description = description
11
+ @adventure = adventure
12
+ @encounters = []
13
+ @links = []
14
+ @links_to_downtime = false
15
+ end
16
+
17
+ def adventure_name
18
+ @adventure.description
19
+ end
20
+
21
+ def add_encounter(encounter)
22
+ @encounters << encounter
23
+ end
24
+
25
+ def add_link(path)
26
+ @links << path
27
+ end
28
+
29
+ def link_to_downtime
30
+ @links_to_downtime = true
31
+ end
32
+
33
+ def links_to_downtime?
34
+ @links_to_downtime
35
+ end
36
+
37
+ def follow(state, log)
38
+ @encounters.each do |encounter|
39
+ encounter.run(state, log)
40
+ end
41
+ end
42
+
43
+ def absolute_path
44
+ Path.Absolute(@adventure.name, @name)
45
+ end
46
+
47
+ def full_name
48
+ @adventure.full_name + ' - ' + @description
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,32 @@
1
+ module CoopAl
2
+ ##
3
+ # ChapterGenerator
4
+ #
5
+ class ChapterGenerator
6
+ attr_reader :chapter
7
+
8
+ def initialize(name, description, adventure, bestiary)
9
+ @adventure = adventure
10
+ @chapter = Chapter.new(name, description, adventure)
11
+ @bestiary = bestiary
12
+ end
13
+
14
+ def encounter(name, &blk)
15
+ generator = EncounterGenerator.new(name, @chapter, @bestiary)
16
+ @chapter.add_encounter(generator.generate_encounter(&blk))
17
+ end
18
+
19
+ def random(name, &blk)
20
+ generator = RandomEncounterGenerator.new(name, @chapter, @bestiary)
21
+ @chapter.add_encounter(generator.generate_encounter(&blk))
22
+ end
23
+
24
+ def link_to(chapter)
25
+ @chapter.add_link(Path.absolute(@adventure.name, chapter))
26
+ end
27
+
28
+ def link_to_downtime
29
+ @chapter.link_to_downtime
30
+ end
31
+ end
32
+ end