coop_al 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
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