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
@@ -0,0 +1,43 @@
1
+ module CoopAl
2
+ ##
3
+ # SessionDateGenerator
4
+ #
5
+ class SessionDateGenerator
6
+ def initialize(options)
7
+ @next_date = options[:end_date]
8
+ @session_frequency = options[:session_frequency]
9
+ @skip_frequency = options[:skip_frequency]
10
+ @sessions = []
11
+ @blackout_dates = options[:blackout_dates]
12
+ end
13
+
14
+ def add_session
15
+ @sessions.push(@next_date)
16
+ advance_next_date
17
+ end
18
+
19
+ def session(number)
20
+ @sessions[-number]
21
+ end
22
+
23
+ private
24
+
25
+ def advance_next_date
26
+ loop do
27
+ @next_date -= @session_frequency
28
+ break if ok?(@next_date)
29
+ end
30
+ end
31
+
32
+ def ok?(date)
33
+ return false if @blackout_dates.include?(date)
34
+ return true if @skip_frequency.nil?
35
+ return false if roll_dice("d#{@skip_frequency}") == 1
36
+ true
37
+ end
38
+
39
+ def blackout?(date)
40
+ @blackout_dates.include?(date)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,76 @@
1
+ module CoopAl
2
+ ##
3
+ # SessionEncounter
4
+ #
5
+ class SessionEncounter
6
+ attr_reader :xp, :treasure
7
+ def initialize(name, monsters, xp, treasure, items)
8
+ @name = name
9
+ @monster_counts = monster_counts(monsters)
10
+ @monsters = monsters.uniq.sort
11
+ @xp = xp
12
+ @treasure = treasure
13
+ @items = items
14
+ end
15
+
16
+ def counts?
17
+ @xp.nonzero?
18
+ end
19
+
20
+ def dump(s)
21
+ s.puts "Encounter: #{@name}"
22
+ s.puts " Monsters: #{monsters_s}"
23
+ s.puts " XP: #{@xp}"
24
+ s.puts " Treasure: #{@treasure}" unless @treasure.zero?
25
+ s.puts " Items: #{items_s}" unless @items.empty?
26
+ end
27
+
28
+ private
29
+
30
+ def monster_counts(monsters)
31
+ results = {}
32
+ monsters.each do |m|
33
+ if results.key?(m)
34
+ results[m] += 1
35
+ else
36
+ results[m] = 1
37
+ end
38
+ end
39
+ results
40
+ end
41
+
42
+ def monsters_s
43
+ @monsters.map { |m| pluralize(m, @monster_counts[m]) }.join(', ')
44
+ end
45
+
46
+ def pluralize(monster, count)
47
+ return monster if count == 1
48
+ "#{monster} (#{count})"
49
+ end
50
+
51
+ def items_s
52
+ @items.map(&:to_s).join(', ')
53
+ end
54
+ end
55
+
56
+ ##
57
+ # Downtime
58
+ #
59
+ class Downtime
60
+ def xp
61
+ 0
62
+ end
63
+
64
+ def treasure
65
+ Value.new
66
+ end
67
+
68
+ def counts?
69
+ true
70
+ end
71
+
72
+ def dump(s)
73
+ s.puts 'Downtime'
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,51 @@
1
+ module CoopAl
2
+ ##
3
+ # SessionLog
4
+ #
5
+ class SessionLog
6
+ def initialize(date_generator, options)
7
+ @date_generator = date_generator
8
+ @encounter_count = options.key?(:encounter_count) ? options[:encounter_count] : 10
9
+ @party_size = options.key?(:party_size) ? options[:party_size] : 6
10
+ @dm_name = options.key?(:dm_name) ? options[:dm_name] : 'Cameron'
11
+ @accumulated_xp = 0
12
+ @accumulated_treasure = Value.new
13
+ @sessions = []
14
+ end
15
+
16
+ def record_encounter(adventure_name, encounter_name, monsters, xp, loot)
17
+ ensure_active_session(adventure_name)
18
+ @accumulated_xp += xp / @party_size
19
+ @accumulated_treasure += loot.treasure_value / @party_size
20
+ active_session.add_encounter(SessionEncounter.new(encounter_name, monsters, xp / @party_size, loot.treasure_value / @party_size, loot.items.dup))
21
+ end
22
+
23
+ def record_downtime(adventure_name)
24
+ ensure_active_session(adventure_name)
25
+ active_session.add_encounter(Downtime.new)
26
+ end
27
+
28
+ def dump(s)
29
+ @sessions.each { |e| e.dump(s) }
30
+ s.puts "Session Count: #{@sessions.size}"
31
+ end
32
+
33
+ private
34
+
35
+ def ensure_active_session(adventure_name)
36
+ start_new_session(adventure_name) if @sessions.empty? || active_session.done?
37
+ end
38
+
39
+ def active_session
40
+ @sessions.last
41
+ end
42
+
43
+ def start_new_session(adventure_name)
44
+ @sessions.push(Session.new(session_number, @date_generator, adventure_name, @dm_name, @accumulated_xp, @accumulated_treasure.dup, @encounter_count))
45
+ end
46
+
47
+ def session_number
48
+ @sessions.size + 1
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,85 @@
1
+ module CoopAl
2
+ ##
3
+ # State
4
+ #
5
+ class State
6
+ attr_reader :xp, :loot, :items
7
+ attr_accessor :current_path
8
+
9
+ def initialize
10
+ @path_stack = [Path.root]
11
+ @xp = 0
12
+ @loot = Loot.empty
13
+ @visited_paths = []
14
+ end
15
+
16
+ def current_path
17
+ @path_stack.last
18
+ end
19
+
20
+ def path_depth
21
+ @path_stack.size
22
+ end
23
+
24
+ def in_downtime?
25
+ current_path.root?
26
+ end
27
+
28
+ def available_paths(library)
29
+ all_available_paths(library).select { |p| !@visited_paths.include?(p) }
30
+ end
31
+
32
+ def all_available_paths(library)
33
+ if path_depth == 1
34
+ if in_downtime?
35
+ library.all_entries
36
+ else
37
+ chapter = library.resolve(current_path)
38
+ chapter.links + downtime_if_available(chapter)
39
+ end
40
+ else
41
+ chapter = library.resolve(@path_stack[0])
42
+ library.all_entries + chapter.links + downtime_if_available(chapter)
43
+ end
44
+ end
45
+
46
+ def downtime_if_available(chapter)
47
+ return [Path.root] if chapter.links_to_downtime? && !in_downtime?
48
+ []
49
+ end
50
+
51
+ def apply_path(path)
52
+ if path_depth == 1
53
+ if path.root?
54
+ @path_stack.push(path)
55
+ else
56
+ @path_stack = [current_path + path]
57
+ @visited_paths << current_path
58
+ end
59
+ else
60
+ @path_stack = [path]
61
+ @visited_paths << path
62
+ end
63
+ end
64
+
65
+ def add_xp(xp)
66
+ @xp += xp
67
+ end
68
+
69
+ def add_loot(loot)
70
+ @loot += loot
71
+ end
72
+
73
+ def add_items(items)
74
+ @items.concat(items)
75
+ end
76
+
77
+ def treasure_value
78
+ @treasure.inject(Value.new) { |a, e| a + e.value }
79
+ end
80
+
81
+ def history_includes?(path)
82
+ @visited_paths.include?(path)
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,72 @@
1
+ module CoopAl
2
+ ##
3
+ # StateReporter
4
+ #
5
+ class StateReporter
6
+ def initialize(state, library, options = {})
7
+ @state = state
8
+ @library = library
9
+ @party_size = options.key?(:party_size) ? options[:party_size] : 6
10
+ @show_xp = options.key?(:show_xp) ? options[:show_xp] : true
11
+ @show_loot = options.key?(:show_loot) ? options[:show_loot] : true
12
+ @show_paths = options.key?(:show_paths) ? options[:show_paths] : true
13
+ end
14
+
15
+ def report(s)
16
+ report_xp(s) if @show_xp
17
+ report_loot(s) if @show_loot
18
+ report_paths(s) if @show_paths
19
+ end
20
+
21
+ private
22
+
23
+ def report_xp(s)
24
+ s.puts "Character Level: #{character_level} (#{character_xp} XP - #{@state.xp} XP total)"
25
+ end
26
+
27
+ def character_xp
28
+ @state.xp / @party_size
29
+ end
30
+
31
+ def character_level
32
+ Coop::XpRequirementTable.new.level_from_xp(character_xp)
33
+ end
34
+
35
+ def report_loot(s)
36
+ report_treasure(s)
37
+ report_items(s)
38
+ end
39
+
40
+ def report_treasure(s)
41
+ return if @state.loot.treasures.empty?
42
+ s.puts "Treasure: #{character_treasure} (#{@state.loot.treasure_value} total)"
43
+ end
44
+
45
+ def report_items(s)
46
+ items = @state.loot.items
47
+ return if items.empty?
48
+ s.puts 'Items:'
49
+ s.puts items.map { |item| " #{item.description_with_origin}" }
50
+ end
51
+
52
+ def character_treasure
53
+ @state.loot.treasure_value / @party_size
54
+ end
55
+
56
+ def report_paths(s)
57
+ s.puts 'Paths:'
58
+ if available_paths.empty?
59
+ s.puts ' None'
60
+ else
61
+ s.puts available_paths.map { |p| " #{p}" }.join("\n")
62
+ end
63
+ end
64
+
65
+ def available_paths
66
+ @state.available_paths(@library).map do |p|
67
+ return ['Downtime'] if p.root?
68
+ "#{@library.resolve(p).description} (#{p})"
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,29 @@
1
+ require 'singleton'
2
+
3
+ module CoopAl
4
+ ##
5
+ # Trace
6
+ #
7
+ class Trace
8
+ include Singleton
9
+
10
+ attr_writer :tracing, :stream
11
+
12
+ def initialize
13
+ @tracing = false
14
+ @stream = STDOUT
15
+ end
16
+
17
+ def info(s)
18
+ @stream.puts(s) if @tracing
19
+ end
20
+
21
+ def warn(s)
22
+ @stream.puts("[WARNING] #{s}") if @tracing
23
+ end
24
+
25
+ def error(s)
26
+ @stream.puts("[ERROR] #{s}") if @tracing
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,21 @@
1
+ module CoopAl
2
+ ##
3
+ # Treasure
4
+ #
5
+ class Treasure
6
+ attr_reader :value
7
+
8
+ def initialize(value, description = nil)
9
+ @value = value
10
+ @description = description
11
+ end
12
+
13
+ def to_s
14
+ if @description.nil?
15
+ "coins (#{@value})"
16
+ else
17
+ "#{@description} (#{@value})"
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,28 @@
1
+ table :individual_treasure_cr_0_4 do
2
+ f(1..30) { '5d6'.cp }
3
+ f(31..60) { '4d6'.sp }
4
+ f(61..70) { '3d6'.ep }
5
+ f(71..95) { '3d6'.gp }
6
+ f(96..100) { '1d6'.pp }
7
+ end
8
+
9
+ table :individual_treasure_cr_5_10 do
10
+ f(1..30) { '4d6*100'.cp + '1d6*10'.ep }
11
+ f(31..60) { '6d6*10'.sp + '2d6*10'.gp }
12
+ f(61..70) { '3d6*10'.ep + '2d6*10'.gp }
13
+ f(71..95) { '4d6*10'.gp }
14
+ f(96..100) { '2d6*10'.gp + '3d6'.pp }
15
+ end
16
+
17
+ table :individual_treasure_cr_11_16 do
18
+ f(1..20) { '4d6*100'.sp + '1d6*100'.gp }
19
+ f(21..35) { '1d6*100'.ep + '1d6*100'.gp }
20
+ f(36..75) { '2d6*100'.gp + '1d6*10'.pp }
21
+ f(76..100) { '2d6*100'.gp + '2d6*10'.pp }
22
+ end
23
+
24
+ table :individual_treasure_cr_17_ do
25
+ f(1..15) { '2d6*1000'.ep + '8d6*100'.gp }
26
+ f(16..55) { '1d6*1000'.gp + '1d6*100'.pp }
27
+ f(56..100) { '1d6*1000'.gp + '2d6*100'.pp }
28
+ end
@@ -0,0 +1,167 @@
1
+ module CoopAl
2
+ ##
3
+ # Value
4
+ #
5
+ class Value
6
+ attr_reader :platinum, :electrum, :gold, :silver, :copper
7
+
8
+ def initialize(amounts = {})
9
+ @platinum = amounts[:platinum] || 0
10
+ @electrum = amounts[:electrum] || 0
11
+ @gold = amounts[:gold] || 0
12
+ @silver = amounts[:silver] || 0
13
+ @copper = amounts[:copper] || 0
14
+ normalize
15
+ end
16
+
17
+ alias pp platinum
18
+ alias ep electrum
19
+ alias gp gold
20
+ alias sp silver
21
+ alias cp copper
22
+
23
+ def zero?
24
+ raw_value.zero?
25
+ end
26
+
27
+ def nonzero?
28
+ !zero?
29
+ end
30
+
31
+ def raw_value
32
+ @platinum * 10 + @gold + @silver / 10.0 + @copper / 100.0 + @electrum / 2.0
33
+ end
34
+
35
+ def normalize_from(value)
36
+ @platinum = 0
37
+ @electrum = 0
38
+ @gold = value.floor
39
+ value -= @gold
40
+ value *= 10
41
+ @silver = value.floor
42
+ value -= @silver
43
+ value *= 10
44
+ @copper = value.floor
45
+ end
46
+
47
+ def normalize
48
+ normalize_from(raw_value)
49
+ end
50
+
51
+ def +(other)
52
+ @platinum += other.pp
53
+ @electrum += other.ep
54
+ @gold += other.gp
55
+ @silver += other.sp
56
+ @copper += other.cp
57
+ normalize
58
+ self
59
+ end
60
+
61
+ def *(other)
62
+ normalize_from(raw_value * other)
63
+ self
64
+ end
65
+
66
+ def /(other)
67
+ normalize_from(raw_value / other)
68
+ self
69
+ end
70
+
71
+ def self.platinum(amount)
72
+ Value.new(platinum: amount)
73
+ end
74
+
75
+ def self.gold(amount)
76
+ Value.new(gold: amount)
77
+ end
78
+
79
+ def self.silver(amount)
80
+ Value.new(silver: amount)
81
+ end
82
+
83
+ def self.copper(amount)
84
+ Value.new(copper: amount)
85
+ end
86
+
87
+ def self.electrum(amount)
88
+ Value.new(electrum: amount)
89
+ end
90
+
91
+ def to_a
92
+ [@platinum, @electrum, @gold, @silver, @copper]
93
+ end
94
+
95
+ def to_s
96
+ [
97
+ [@platinum, 'pp'],
98
+ [@gold, 'gp'],
99
+ [@silver, 'sp'],
100
+ [@copper, 'cp'],
101
+ [@electrum, 'ep']
102
+ ].select { |v| v[0].nonzero? }.map { |v| "#{v[0]}#{v[1]}" }.join(', ')
103
+ end
104
+ end
105
+ end
106
+
107
+ ##
108
+ # Integer monkey-patch
109
+ #
110
+ class Integer
111
+ def platinum
112
+ Coop::Value.platinum(to_i)
113
+ end
114
+
115
+ def gold
116
+ Coop::Value.gold(to_i)
117
+ end
118
+
119
+ def silver
120
+ Coop::Value.silver(to_i)
121
+ end
122
+
123
+ def copper
124
+ Coop::Value.copper(to_i)
125
+ end
126
+
127
+ def electrum
128
+ Coop::Value.electrum(to_i)
129
+ end
130
+
131
+ alias gp gold
132
+ alias sp silver
133
+ alias cp copper
134
+ alias pp platinum
135
+ alias ep electrum
136
+ end
137
+
138
+ ##
139
+ # String monkey-patch
140
+ #
141
+ class String
142
+ def platinum
143
+ Coop::Value.platinum(roll_dice(self))
144
+ end
145
+
146
+ def gold
147
+ Coop::Value.gold(roll_dice(self))
148
+ end
149
+
150
+ def silver
151
+ Coop::Value.silver(roll_dice(self))
152
+ end
153
+
154
+ def copper
155
+ Coop::Value.copper(roll_dice(self))
156
+ end
157
+
158
+ def electrum
159
+ Coop::Value.electrum(roll_dice(self))
160
+ end
161
+
162
+ alias gp gold
163
+ alias sp silver
164
+ alias cp copper
165
+ alias pp platinum
166
+ alias ep electrum
167
+ end
@@ -0,0 +1,3 @@
1
+ module CoopAl
2
+ VERSION = '1.0.1'.freeze
3
+ end
data/lib/coop_al/xp.rb ADDED
@@ -0,0 +1,86 @@
1
+ module CoopAl
2
+ ##
3
+ # XpRewardTable
4
+ #
5
+ class XpRewardTable
6
+ def initialize
7
+ @xp_by_cr = {
8
+ cr0: 10,
9
+ cr1_8: 25,
10
+ cr1_4: 50,
11
+ cr1_2: 100,
12
+ cr1: 200,
13
+ cr2: 450,
14
+ cr3: 700,
15
+ cr4: 1_100,
16
+ cr5: 1_800,
17
+ cr6: 2_300,
18
+ cr7: 2_900,
19
+ cr8: 3_900,
20
+ cr9: 5_000,
21
+ cr10: 5_900,
22
+ cr11: 7_200,
23
+ cr12: 8_400,
24
+ cr13: 10_000,
25
+ cr14: 11_500,
26
+ cr15: 13_000,
27
+ cr16: 15_000,
28
+ cr17: 18_000,
29
+ cr18: 20_000,
30
+ cr19: 22_000,
31
+ cr20: 25_000,
32
+ cr21: 33_000,
33
+ cr22: 41_000,
34
+ cr23: 50_000,
35
+ cr24: 62_000,
36
+ cr25: 75_000,
37
+ cr26: 90_000,
38
+ cr27: 105_000,
39
+ cr28: 120_000,
40
+ cr29: 135_000,
41
+ cr30: 155_000
42
+ }
43
+ end
44
+
45
+ def [](cr)
46
+ @xp_by_cr[cr]
47
+ end
48
+ end
49
+
50
+ ##
51
+ # XpRequirementTable
52
+ #
53
+ class XpRequirementTable
54
+ def initialize
55
+ @xp_by_level = {
56
+ 1 => 0,
57
+ 2 => 300,
58
+ 3 => 900,
59
+ 4 => 2_700,
60
+ 5 => 6_500,
61
+ 6 => 14_000,
62
+ 7 => 23_000,
63
+ 8 => 34_000,
64
+ 9 => 48_000,
65
+ 10 => 64_000,
66
+ 11 => 85_000,
67
+ 12 => 100_000,
68
+ 13 => 120_000,
69
+ 14 => 140_000,
70
+ 15 => 165_000,
71
+ 16 => 195_000,
72
+ 17 => 225_000,
73
+ 18 => 265_000,
74
+ 19 => 305_000,
75
+ 20 => 355_000
76
+ }
77
+ end
78
+
79
+ def level_from_xp(xp)
80
+ 20.downto(1) do |i|
81
+ return i if xp >= @xp_by_level[i]
82
+ end
83
+ raise 'Invalid xp value (#{xp})'
84
+ end
85
+ end
86
+ end
data/lib/coop_al.rb ADDED
@@ -0,0 +1,34 @@
1
+ require 'tablescript'
2
+ require 'coop_al/treasure_tables'
3
+ require 'coop_al/trace'
4
+ require 'coop_al/version'
5
+ require 'coop_al/exception'
6
+ require 'coop_al/monster_definition'
7
+ require 'coop_al/loot_generator'
8
+ require 'coop_al/bestiary'
9
+ require 'coop_al/treasure'
10
+ require 'coop_al/value'
11
+ require 'coop_al/path'
12
+ require 'coop_al/state'
13
+ require 'coop_al/chapter'
14
+ require 'coop_al/monster'
15
+ require 'coop_al/item'
16
+ require 'coop_al/encounter'
17
+ require 'coop_al/random_encounter'
18
+ require 'coop_al/library_generator'
19
+ require 'coop_al/random_encounter_generator'
20
+ require 'coop_al/encounter_generator'
21
+ require 'coop_al/chapter_generator'
22
+ require 'coop_al/path_follower'
23
+ require 'coop_al/session_encounter'
24
+ require 'coop_al/session_date_generator'
25
+ require 'coop_al/session'
26
+ require 'coop_al/session_log'
27
+ require 'coop_al/xp'
28
+ require 'coop_al/adventure'
29
+ require 'coop_al/adventure_generator'
30
+ require 'coop_al/library'
31
+ require 'coop_al/loot'
32
+ require 'coop_al/state_reporter'
33
+ require 'coop_al/bestiary_populator'
34
+ require 'coop_al/bestiary_generator'