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
@@ -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'